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:
Will Chen
2025-08-04 16:36:09 -07:00
committed by GitHub
parent 0f1a5c5c77
commit b0f08eaf15
50 changed files with 3525 additions and 205 deletions

View File

@@ -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>
);
};

View 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>
);
};

View File

@@ -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>
) : (

View File

@@ -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">