Export blueprint functionality

This commit is contained in:
Owen
2026-09-24 16:31:54 -04:00
parent ed28d34945
commit 6ddb1550e9
10 changed files with 1098 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
"use client";
import { useEffect, useState } from "react";
import Editor from "@monaco-editor/react";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle
} from "@app/components/Credenza";
import { Button } from "@app/components/ui/button";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { cn } from "@app/lib/cn";
import { Download, Loader2 } from "lucide-react";
import { useTranslations } from "next-intl";
type ExportBlueprintModalProps = {
open: boolean;
setOpen: (open: boolean) => void;
endpoint: string;
resourceName: string;
};
export default function ExportBlueprintModal({
open,
setOpen,
endpoint,
resourceName
}: ExportBlueprintModalProps) {
const t = useTranslations();
const { env } = useEnvContext();
const api = createApiClient({ env });
const [loading, setLoading] = useState(false);
const [contents, setContents] = useState<string | null>(null);
const [fileName, setFileName] = useState(resourceName);
useEffect(() => {
if (!open) {
return;
}
setContents(null);
setLoading(true);
api.get(endpoint)
.then((res) => {
setContents(res.data.data.contents);
setFileName(res.data.data.name || resourceName);
})
.catch((e) => {
toast({
variant: "destructive",
title: t("exportBlueprintError"),
description: formatAxiosError(
e,
t("exportBlueprintError")
)
});
setOpen(false);
})
.finally(() => setLoading(false));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, endpoint]);
function handleDownload() {
if (!contents) {
return;
}
const blob = new Blob([contents], { type: "application/x-yaml" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", `${fileName}.yaml`);
document.body.appendChild(link);
link.click();
link.parentNode?.removeChild(link);
window.URL.revokeObjectURL(url);
}
return (
<Credenza open={open} onOpenChange={setOpen}>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t("exportBlueprintTitle")}</CredenzaTitle>
<CredenzaDescription>
{t("exportBlueprintDescription", {
name: resourceName
})}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div
className={cn(
"resize-y h-64 min-h-64 overflow-y-auto overflow-x-clip max-w-full rounded-md border"
)}
>
{loading ? (
<div className="flex items-center justify-center w-full h-full">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : (
<Editor
className="w-full h-full max-w-full"
language="yaml"
theme="vs-dark"
value={contents ?? ""}
options={{
minimap: { enabled: false },
readOnly: true
}}
/>
)}
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button
onClick={handleDownload}
disabled={!contents || loading}
>
<Download className="mr-2 h-4 w-4" />
{t("download")}
</Button>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
}
+29
View File
@@ -2,6 +2,7 @@
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import CopyToClipboard from "@app/components/CopyToClipboard";
import ExportBlueprintModal from "@app/components/ExportBlueprintModal";
import { ResourceAccessCertIndicator } from "@app/components/ResourceAccessCertIndicator";
import {
ResourceSitesStatusCell,
@@ -171,6 +172,10 @@ export default function PrivateResourcesTable({
useState<PrivateResourceRow | null>();
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
const [exportResource, setExportResource] =
useState<PrivateResourceRow | null>(null);
const [isRefreshing, startRefreshTransition] = useTransition();
const { isPaidUser } = usePaidStatus();
@@ -566,6 +571,14 @@ export default function PrivateResourcesTable({
{t("viewSettings")}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setExportResource(resourceRow);
setIsExportModalOpen(true);
}}
>
{t("exportAsBlueprint")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSelectedInternalResource(
@@ -668,6 +681,22 @@ export default function PrivateResourcesTable({
/>
)}
{exportResource && (
<ExportBlueprintModal
open={isExportModalOpen}
setOpen={(val) => {
setIsExportModalOpen(val);
if (!val) {
setExportResource(null);
}
}}
endpoint={`/site-resource/${exportResource.id}/blueprint`}
resourceName={
exportResource.niceId || exportResource.name
}
/>
)}
<ControlledDataTable
columns={internalColumns}
rows={internalResources}
+28
View File
@@ -2,6 +2,7 @@
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import CopyToClipboard from "@app/components/CopyToClipboard";
import ExportBlueprintModal from "@app/components/ExportBlueprintModal";
import { ResourceAccessCertIndicator } from "@app/components/ResourceAccessCertIndicator";
import {
ResourceSitesStatusCell,
@@ -140,6 +141,11 @@ export default function PublicResourcesTable({
const [selectedResource, setSelectedResource] =
useState<ResourceRow | null>();
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
const [exportResource, setExportResource] = useState<ResourceRow | null>(
null
);
const [isRefreshing, startTransition] = useTransition();
const [isNavigatingToAddPage, startNavigation] = useTransition();
@@ -617,6 +623,14 @@ export default function PublicResourcesTable({
{t("viewSettings")}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setExportResource(resourceRow);
setIsExportModalOpen(true);
}}
>
{t("exportAsBlueprint")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSelectedResource(resourceRow);
@@ -720,6 +734,20 @@ export default function PublicResourcesTable({
/>
)}
{exportResource && (
<ExportBlueprintModal
open={isExportModalOpen}
setOpen={(val) => {
setIsExportModalOpen(val);
if (!val) {
setExportResource(null);
}
}}
endpoint={`/resource/${exportResource.id}/blueprint`}
resourceName={exportResource.nice || exportResource.name}
/>
)}
<ControlledDataTable
columns={proxyColumns}
rows={resources}