Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt standard monorepo conventions (apps/ for runnable apps, packages/ for reusable libraries). Update all config files, shared package imports, test fixtures, and documentation to reflect new paths. Key fixes: - Update workspace config to ["apps/*", "packages/*"] - Update tsconfig.json rootDir/include for apps/server/ - Add apps/web/** to vitest exclude list - Update drizzle.config.ts schema path - Fix ensure-schema.ts migration path detection (3 levels up in dev, 2 levels up in dist) - Fix tests/integration/cli-server.test.ts import paths - Update packages/shared imports to apps/server/ paths - Update all docs/ files with new paths
177 lines
5.0 KiB
TypeScript
177 lines
5.0 KiB
TypeScript
import { useState } from "react";
|
|
import {
|
|
Loader2,
|
|
ExternalLink,
|
|
Square,
|
|
RotateCcw,
|
|
CircleDot,
|
|
CircleX,
|
|
} from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { trpc } from "@/lib/trpc";
|
|
import { toast } from "sonner";
|
|
|
|
interface PreviewPanelProps {
|
|
initiativeId: string;
|
|
phaseId?: string;
|
|
projectId: string;
|
|
branch: string;
|
|
}
|
|
|
|
export function PreviewPanel({
|
|
initiativeId,
|
|
phaseId,
|
|
projectId,
|
|
branch,
|
|
}: PreviewPanelProps) {
|
|
const [activePreviewId, setActivePreviewId] = useState<string | null>(null);
|
|
|
|
// Check for existing previews for this initiative
|
|
const previewsQuery = trpc.listPreviews.useQuery(
|
|
{ initiativeId },
|
|
{ refetchInterval: activePreviewId ? 3000 : false },
|
|
);
|
|
|
|
const existingPreview = previewsQuery.data?.find(
|
|
(p) => p.phaseId === phaseId || (!phaseId && p.initiativeId === initiativeId),
|
|
);
|
|
|
|
const previewStatusQuery = trpc.getPreviewStatus.useQuery(
|
|
{ previewId: activePreviewId ?? existingPreview?.id ?? "" },
|
|
{
|
|
enabled: !!(activePreviewId ?? existingPreview?.id),
|
|
refetchInterval: 3000,
|
|
},
|
|
);
|
|
|
|
const preview = previewStatusQuery.data ?? existingPreview;
|
|
|
|
const startMutation = trpc.startPreview.useMutation({
|
|
onSuccess: (data) => {
|
|
setActivePreviewId(data.id);
|
|
toast.success(`Preview running at http://localhost:${data.port}`);
|
|
},
|
|
onError: (err) => {
|
|
toast.error(`Preview failed: ${err.message}`);
|
|
},
|
|
});
|
|
|
|
const stopMutation = trpc.stopPreview.useMutation({
|
|
onSuccess: () => {
|
|
setActivePreviewId(null);
|
|
toast.success("Preview stopped");
|
|
previewsQuery.refetch();
|
|
},
|
|
onError: (err) => {
|
|
toast.error(`Failed to stop preview: ${err.message}`);
|
|
},
|
|
});
|
|
|
|
const handleStart = () => {
|
|
startMutation.mutate({ initiativeId, phaseId, projectId, branch });
|
|
};
|
|
|
|
const handleStop = () => {
|
|
const id = activePreviewId ?? existingPreview?.id;
|
|
if (id) {
|
|
stopMutation.mutate({ previewId: id });
|
|
}
|
|
};
|
|
|
|
// Building state
|
|
if (startMutation.isPending) {
|
|
return (
|
|
<div className="flex items-center gap-3 rounded-lg border border-blue-200 dark:border-blue-900 bg-blue-50 dark:bg-blue-950/20 px-4 py-3">
|
|
<Loader2 className="h-4 w-4 animate-spin text-blue-600 dark:text-blue-400" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium text-blue-700 dark:text-blue-300">
|
|
Building preview...
|
|
</p>
|
|
<p className="text-xs text-blue-600/70 dark:text-blue-400/70">
|
|
Building containers and starting services
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Running state
|
|
if (preview && (preview.status === "running" || preview.status === "building")) {
|
|
const url = `http://localhost:${preview.port}`;
|
|
const isBuilding = preview.status === "building";
|
|
|
|
return (
|
|
<div
|
|
className={`flex items-center gap-3 rounded-lg border px-4 py-3 ${
|
|
isBuilding
|
|
? "border-blue-200 dark:border-blue-900 bg-blue-50 dark:bg-blue-950/20"
|
|
: "border-green-200 dark:border-green-900 bg-green-50 dark:bg-green-950/20"
|
|
}`}
|
|
>
|
|
{isBuilding ? (
|
|
<Loader2 className="h-4 w-4 animate-spin text-blue-600 dark:text-blue-400" />
|
|
) : (
|
|
<CircleDot className="h-4 w-4 text-green-600 dark:text-green-400" />
|
|
)}
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-foreground">
|
|
{isBuilding ? "Building..." : "Preview running"}
|
|
</p>
|
|
{!isBuilding && (
|
|
<a
|
|
href={url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
|
|
>
|
|
{url}
|
|
<ExternalLink className="h-3 w-3" />
|
|
</a>
|
|
)}
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={handleStop}
|
|
disabled={stopMutation.isPending}
|
|
className="shrink-0"
|
|
>
|
|
<Square className="h-3 w-3 mr-1" />
|
|
Stop
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Failed state
|
|
if (preview && preview.status === "failed") {
|
|
return (
|
|
<div className="flex items-center gap-3 rounded-lg border border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-950/20 px-4 py-3">
|
|
<CircleX className="h-4 w-4 text-red-600 dark:text-red-400" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium text-red-700 dark:text-red-300">
|
|
Preview failed
|
|
</p>
|
|
</div>
|
|
<Button size="sm" variant="outline" onClick={handleStart}>
|
|
<RotateCcw className="h-3 w-3 mr-1" />
|
|
Retry
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// No preview — show start button
|
|
return (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={handleStart}
|
|
disabled={startMutation.isPending}
|
|
>
|
|
<ExternalLink className="h-3.5 w-3.5 mr-1" />
|
|
Start Preview
|
|
</Button>
|
|
);
|
|
}
|