diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 81931c003c99d..5463ad7a44dd6 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1084,6 +1084,31 @@ class ApiMethods { return response.data; }; + /** + * Downloads a template version as a tar or zip archive + * @param fileId The file ID from the template version's job + * @param format Optional format: "zip" for zip archive, empty/undefined for tar + * @returns Promise that resolves to a Blob containing the archive + */ + downloadTemplateVersion = async ( + fileId: string, + format?: "zip", + ): Promise => { + const params = new URLSearchParams(); + if (format) { + params.set("format", format); + } + + const response = await this.axios.get( + `/api/v2/files/${fileId}?${params.toString()}`, + { + responseType: "blob", + }, + ); + + return response.data; + }; + updateTemplateMeta = async ( templateId: string, data: TypesGen.UpdateTemplateMeta, diff --git a/site/src/pages/TemplatePage/TemplatePageHeader.tsx b/site/src/pages/TemplatePage/TemplatePageHeader.tsx index 54c3c04de8bdf..a7ebbf0ad00b1 100644 --- a/site/src/pages/TemplatePage/TemplatePageHeader.tsx +++ b/site/src/pages/TemplatePage/TemplatePageHeader.tsx @@ -1,5 +1,6 @@ import EditIcon from "@mui/icons-material/EditOutlined"; import Button from "@mui/material/Button"; +import { API } from "api/api"; import { workspaces } from "api/queries/workspaces"; import type { AuthorizationResponse, @@ -26,7 +27,7 @@ import { } from "components/PageHeader/PageHeader"; import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; -import { CopyIcon } from "lucide-react"; +import { CopyIcon, DownloadIcon } from "lucide-react"; import { EllipsisVertical, PlusIcon, @@ -46,6 +47,7 @@ type TemplateMenuProps = { templateName: string; templateVersion: string; templateId: string; + fileId: string; onDelete: () => void; }; @@ -54,6 +56,7 @@ const TemplateMenu: FC = ({ templateName, templateVersion, templateId, + fileId, onDelete, }) => { const dialogState = useDeletionDialogState(templateId, onDelete); @@ -68,6 +71,24 @@ const TemplateMenu: FC = ({ const templateLink = getLink(linkToTemplate(organizationName, templateName)); + const handleExport = async (format?: "zip") => { + try { + const blob = await API.downloadTemplateVersion(fileId, format); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + const extension = format === "zip" ? "zip" : "tar"; + link.download = `${templateName}-${templateVersion}.${extension}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error("Failed to export template:", error); + // TODO: Show user-friendly error message + } + }; + return ( <> @@ -102,6 +123,16 @@ const TemplateMenu: FC = ({ Duplicate… + + handleExport()}> + + Export as TAR + + + handleExport("zip")}> + + Export as ZIP + = ({ templateId={template.id} templateName={template.name} templateVersion={activeVersion.name} + fileId={activeVersion.job.file_id} onDelete={onDeleteTemplate} /> )} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy