Neon / portal template support (#713)
TODOs: - [x] Do restart when checkout / restore if there is a DB - [x] List all branches (branch id, name, date) - [x] Allow checking out versions with no DB - [x] safeguard to never delete main branches - [x] create app hook for neon template - [x] weird UX with connector on configure panel - [x] tiny neon logo in connector - [x] deploy to vercel - [x] build forgot password page - [x] what about email setup - [x] lots of imgix errors - [x] edit file - db snapshot - [x] DYAD_DISABLE_DB_PUSH - [ ] update portal doc - [x] switch preview branch to be read-only endpoint - [x] disable supabase sys prompt if neon is enabled - [ ] https://payloadcms.com/docs/upload/storage-adapters - [x] need to use main branch... Phase 2? - [x] generate DB migrations
This commit is contained in:
@@ -23,6 +23,7 @@ import { showError, showSuccess } from "@/lib/toast";
|
||||
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { NeonConfigure } from "./NeonConfigure";
|
||||
|
||||
const EnvironmentVariablesTitle = () => (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -396,6 +397,12 @@ export const ConfigurePanel = () => {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Neon Database Configuration */}
|
||||
{/* Neon Connector */}
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<NeonConfigure />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
178
src/components/preview_panel/NeonConfigure.tsx
Normal file
178
src/components/preview_panel/NeonConfigure.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtomValue } from "jotai";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
import { Database, GitBranch } from "lucide-react";
|
||||
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||
import { useLoadApp } from "@/hooks/useLoadApp";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import type { GetNeonProjectResponse, NeonBranch } from "@/ipc/ipc_types";
|
||||
import { NeonDisconnectButton } from "@/components/NeonDisconnectButton";
|
||||
|
||||
const getBranchTypeColor = (type: NeonBranch["type"]) => {
|
||||
switch (type) {
|
||||
case "production":
|
||||
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300";
|
||||
case "development":
|
||||
return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300";
|
||||
case "snapshot":
|
||||
return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300";
|
||||
case "preview":
|
||||
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300";
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString();
|
||||
};
|
||||
|
||||
export const NeonConfigure = () => {
|
||||
const selectedAppId = useAtomValue(selectedAppIdAtom);
|
||||
const { app } = useLoadApp(selectedAppId);
|
||||
|
||||
// Query to get Neon project information
|
||||
const {
|
||||
data: neonProject,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery<GetNeonProjectResponse, Error>({
|
||||
queryKey: ["neon-project", selectedAppId],
|
||||
queryFn: async () => {
|
||||
if (!selectedAppId) throw new Error("No app selected");
|
||||
const ipcClient = IpcClient.getInstance();
|
||||
return await ipcClient.getNeonProject({ appId: selectedAppId });
|
||||
},
|
||||
enabled: !!selectedAppId && !!app?.neonProjectId,
|
||||
meta: { showErrorToast: true },
|
||||
});
|
||||
|
||||
// Don't show component if app doesn't have Neon project
|
||||
if (!app?.neonProjectId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database size={20} />
|
||||
Neon Database
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center py-8">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Loading Neon project information...
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database size={20} />
|
||||
Neon Database
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center py-8">
|
||||
<div className="text-sm text-red-500">
|
||||
Error loading Neon project: {error.message}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!neonProject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database size={20} />
|
||||
Neon Database
|
||||
</div>
|
||||
<NeonDisconnectButton />
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Project Information */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Project Information</div>
|
||||
<div className="bg-muted/50 p-3 rounded-md space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Project Name:</span>
|
||||
<span className="font-medium">{neonProject.projectName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Project ID:</span>
|
||||
<span className="font-mono text-xs">{neonProject.projectId}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Organization:</span>
|
||||
<span className="font-mono text-xs">{neonProject.orgId}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Branches */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium flex items-center gap-2">
|
||||
<GitBranch size={16} />
|
||||
Branches ({neonProject.branches.length})
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{neonProject.branches.map((branch) => (
|
||||
<div
|
||||
key={branch.branchId}
|
||||
className="flex items-center justify-between p-3 border rounded-md"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm truncate">
|
||||
{branch.branchName}
|
||||
</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={getBranchTypeColor(branch.type)}
|
||||
>
|
||||
{branch.type}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
ID: {branch.branchId}
|
||||
</div>
|
||||
{branch.parentBranchName && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Parent: {branch.parentBranchName.slice(0, 20)}...
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Updated: {formatDate(branch.lastUpdated)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -413,7 +413,20 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
|
||||
// Display loading state
|
||||
if (loading) {
|
||||
return <div className="p-4 dark:text-gray-300">Loading app preview...</div>;
|
||||
return (
|
||||
<div className="flex flex-col h-full relative">
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center space-y-4 bg-gray-50 dark:bg-gray-950">
|
||||
<div className="relative w-5 h-5 animate-spin">
|
||||
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-2 h-2 bg-primary rounded-full"></div>
|
||||
<div className="absolute bottom-0 left-0 w-2 h-2 bg-primary rounded-full opacity-80"></div>
|
||||
<div className="absolute bottom-0 right-0 w-2 h-2 bg-primary rounded-full opacity-60"></div>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Preparing app preview...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Display message if no app is selected
|
||||
@@ -565,7 +578,7 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center space-y-4 bg-gray-50 dark:bg-gray-950">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-gray-400 dark:text-gray-500" />
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Starting up your app...
|
||||
Starting your app server...
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||
import { useLoadApp } from "@/hooks/useLoadApp";
|
||||
import { GitHubConnector } from "@/components/GitHubConnector";
|
||||
import { VercelConnector } from "@/components/VercelConnector";
|
||||
import { PortalMigrate } from "@/components/PortalMigrate";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
@@ -78,6 +79,9 @@ export const PublishPanel = () => {
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Portal Section - Show only if app has neon project */}
|
||||
{app.neonProjectId && <PortalMigrate appId={selectedAppId} />}
|
||||
|
||||
{/* GitHub Section */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
|
||||
Reference in New Issue
Block a user