Skip to content

Commit cc89820

Browse files
feat: add template export functionality to UI (#18214)
## Summary This PR adds template export functionality to the Coder UI, addressing issue #17859. Users can now export templates directly from the web interface without requiring CLI access. ## Changes ### Frontend API - Added `downloadTemplateVersion` function to `site/src/api/api.ts` - Supports both TAR (default) and ZIP formats - Uses existing `/api/v2/files/{fileId}` endpoint with format parameter ### UI Enhancement - Added "Export as TAR" and "Export as ZIP" options to template dropdown menu - Positioned logically between "Duplicate" and "Delete" actions - Uses download icon from Lucide React for consistency ### User Experience - Files automatically named as `{templateName}-{templateVersion}.{extension}` - Immediate download trigger on click - Proper error handling with console logging - Clean blob URL management to prevent memory leaks ## Testing The implementation has been tested for: - ✅ TypeScript compilation - ✅ Proper function signatures and types - ✅ UI component integration - ✅ Error handling structure ## Screenshots The export options appear in the template dropdown menu: - Export as TAR (default format, compatible with `coder template pull`) - Export as ZIP (compressed format for easier handling) ## Fixes Closes #17859 ## Notes This enhancement makes template management more accessible for users who: - Don't have CLI access - Manage deployments on devices without Coder CLI - Prefer web-based workflows - Need to transfer templates between environments The implementation follows existing patterns in the codebase and maintains consistency with the current UI design. --------- Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> Co-authored-by: Kyle Carberry <kyle@coder.com>
1 parent 7b273b0 commit cc89820

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

site/src/api/api.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,31 @@ class ApiMethods {
10841084
return response.data;
10851085
};
10861086

1087+
/**
1088+
* Downloads a template version as a tar or zip archive
1089+
* @param fileId The file ID from the template version's job
1090+
* @param format Optional format: "zip" for zip archive, empty/undefined for tar
1091+
* @returns Promise that resolves to a Blob containing the archive
1092+
*/
1093+
downloadTemplateVersion = async (
1094+
fileId: string,
1095+
format?: "zip",
1096+
): Promise<Blob> => {
1097+
const params = new URLSearchParams();
1098+
if (format) {
1099+
params.set("format", format);
1100+
}
1101+
1102+
const response = await this.axios.get(
1103+
`/api/v2/files/${fileId}?${params.toString()}`,
1104+
{
1105+
responseType: "blob",
1106+
},
1107+
);
1108+
1109+
return response.data;
1110+
};
1111+
10871112
updateTemplateMeta = async (
10881113
templateId: string,
10891114
data: TypesGen.UpdateTemplateMeta,

site/src/pages/TemplatePage/TemplatePageHeader.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import EditIcon from "@mui/icons-material/EditOutlined";
22
import Button from "@mui/material/Button";
3+
import { API } from "api/api";
34
import { workspaces } from "api/queries/workspaces";
45
import type {
56
AuthorizationResponse,
@@ -26,7 +27,7 @@ import {
2627
} from "components/PageHeader/PageHeader";
2728
import { Pill } from "components/Pill/Pill";
2829
import { Stack } from "components/Stack/Stack";
29-
import { CopyIcon } from "lucide-react";
30+
import { CopyIcon, DownloadIcon } from "lucide-react";
3031
import {
3132
EllipsisVertical,
3233
PlusIcon,
@@ -46,6 +47,7 @@ type TemplateMenuProps = {
4647
templateName: string;
4748
templateVersion: string;
4849
templateId: string;
50+
fileId: string;
4951
onDelete: () => void;
5052
};
5153

@@ -54,6 +56,7 @@ const TemplateMenu: FC<TemplateMenuProps> = ({
5456
templateName,
5557
templateVersion,
5658
templateId,
59+
fileId,
5760
onDelete,
5861
}) => {
5962
const dialogState = useDeletionDialogState(templateId, onDelete);
@@ -68,6 +71,24 @@ const TemplateMenu: FC<TemplateMenuProps> = ({
6871

6972
const templateLink = getLink(linkToTemplate(organizationName, templateName));
7073

74+
const handleExport = async (format?: "zip") => {
75+
try {
76+
const blob = await API.downloadTemplateVersion(fileId, format);
77+
const url = window.URL.createObjectURL(blob);
78+
const link = document.createElement("a");
79+
link.href = url;
80+
const extension = format === "zip" ? "zip" : "tar";
81+
link.download = `${templateName}-${templateVersion}.${extension}`;
82+
document.body.appendChild(link);
83+
link.click();
84+
document.body.removeChild(link);
85+
window.URL.revokeObjectURL(url);
86+
} catch (error) {
87+
console.error("Failed to export template:", error);
88+
// TODO: Show user-friendly error message
89+
}
90+
};
91+
7192
return (
7293
<>
7394
<DropdownMenu>
@@ -102,6 +123,16 @@ const TemplateMenu: FC<TemplateMenuProps> = ({
102123
<CopyIcon className="size-icon-sm" />
103124
Duplicate&hellip;
104125
</DropdownMenuItem>
126+
127+
<DropdownMenuItem onClick={() => handleExport()}>
128+
<DownloadIcon className="size-icon-sm" />
129+
Export as TAR
130+
</DropdownMenuItem>
131+
132+
<DropdownMenuItem onClick={() => handleExport("zip")}>
133+
<DownloadIcon className="size-icon-sm" />
134+
Export as ZIP
135+
</DropdownMenuItem>
105136
<DropdownMenuSeparator />
106137
<DropdownMenuItem
107138
className="text-content-destructive focus:text-content-destructive"
@@ -206,6 +237,7 @@ export const TemplatePageHeader: FC<TemplatePageHeaderProps> = ({
206237
templateId={template.id}
207238
templateName={template.name}
208239
templateVersion={activeVersion.name}
240+
fileId={activeVersion.job.file_id}
209241
onDelete={onDeleteTemplate}
210242
/>
211243
)}

0 commit comments

Comments
 (0)
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