From 637161a2df2e8f23487838a159890afbca91e05a Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:34:50 +0000 Subject: [PATCH 1/2] feat: add template export functionality to UI - Add downloadTemplateVersion function to frontend API - Add Export as TAR and Export as ZIP options to template dropdown menu - Implement file download with proper naming convention - Support both tar and zip formats as requested in issue #17859 Fixes #17859 --- site/src/api/api.ts | 25 ++++++++++++++ .../pages/TemplatePage/TemplatePageHeader.tsx | 34 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) 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..5002f244c7d01 100644 --- a/site/src/pages/TemplatePage/TemplatePageHeader.tsx +++ b/site/src/pages/TemplatePage/TemplatePageHeader.tsx @@ -26,7 +26,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, @@ -34,6 +34,7 @@ import { TrashIcon, } from "lucide-react"; import { linkToTemplate, useLinks } from "modules/navigation"; +import { API } from "api/api"; import type { WorkspacePermissions } from "modules/permissions/workspaces"; import type { FC } from "react"; import { useQuery } from "react-query"; @@ -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} /> )} From b82313026590ca0877b57bd060ec3b581ace6e43 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:49:45 +0000 Subject: [PATCH 2/2] fix: correct import ordering in TemplatePageHeader.tsx Fix import ordering to comply with biome linting rules by moving API import to the correct position in the import order. --- site/src/pages/TemplatePage/TemplatePageHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/TemplatePage/TemplatePageHeader.tsx b/site/src/pages/TemplatePage/TemplatePageHeader.tsx index 5002f244c7d01..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, @@ -34,7 +35,6 @@ import { TrashIcon, } from "lucide-react"; import { linkToTemplate, useLinks } from "modules/navigation"; -import { API } from "api/api"; import type { WorkspacePermissions } from "modules/permissions/workspaces"; import type { FC } from "react"; import { useQuery } from "react-query";