fix(site): Show folders in the template version editor (#6145)

This commit is contained in:
Bruno Quaresma
2023-02-10 13:22:20 -03:00
committed by GitHub
parent 32fbd10a1f
commit 77afdf71dc
12 changed files with 399 additions and 209 deletions
-3
View File
@@ -583,9 +583,6 @@ jobs:
- run: yarn playwright:install
working-directory: site
- run: yarn playwright:install-deps
working-directory: site
- run: yarn playwright:test
env:
DEBUG: pw:api
+1 -2
View File
@@ -15,8 +15,7 @@
"format:write:only": "prettier --write",
"lint": "jest --selectProjects lint",
"lint:fix": "FIX=true yarn lint",
"playwright:install": "playwright install",
"playwright:install-deps": "playwright install-deps",
"playwright:install": "playwright install --with-deps chromium",
"playwright:test": "playwright test --config=e2e/playwright.config.ts",
"storybook": "start-storybook -p 6006",
"storybook:build": "build-storybook",
@@ -5,104 +5,73 @@ import TreeView from "@material-ui/lab/TreeView"
import TreeItem from "@material-ui/lab/TreeItem"
import Menu from "@material-ui/core/Menu"
import MenuItem from "@material-ui/core/MenuItem"
import { FC, useMemo, useState } from "react"
import { TemplateVersionFiles } from "util/templateVersion"
import { FC, useState } from "react"
import { FileTree } from "util/filetree"
import { DockerIcon } from "components/Icons/DockerIcon"
export interface File {
path: string
content?: string
children: Record<string, File>
const sortFileTree = (fileTree: FileTree) => (a: string, b: string) => {
const contentA = fileTree[a]
const contentB = fileTree[b]
if (typeof contentA === "object") {
return -1
}
if (typeof contentB === "object") {
return 1
}
return a.localeCompare(b)
}
export const FileTree: FC<{
onSelect: (file: File) => void
onDelete: (file: File) => void
onRename: (file: File) => void
files: TemplateVersionFiles
activeFile?: File
}> = ({ activeFile, files, onDelete, onRename, onSelect }) => {
const styles = useStyles()
const fileTree = useMemo<Record<string, File>>(() => {
const paths = Object.keys(files)
const roots: Record<string, File> = {}
paths.forEach((path) => {
const pathParts = path.split("/")
const firstPart = pathParts.shift()
if (!firstPart) {
// Not possible!
return
}
let activeFile = roots[firstPart]
if (!activeFile) {
activeFile = {
path: firstPart,
children: {},
}
roots[firstPart] = activeFile
}
while (pathParts.length > 0) {
const pathPart = pathParts.shift()
if (!pathPart) {
continue
}
if (!activeFile.children[pathPart]) {
activeFile.children[pathPart] = {
path: activeFile.path + "/" + pathPart,
children: {},
}
}
activeFile = activeFile.children[pathPart]
}
activeFile.content = files[path]
activeFile.path = path
})
return roots
}, [files])
const [contextMenu, setContextMenu] = useState<
| {
file: File
clientX: number
clientY: number
}
| undefined
>()
type ContextMenu = {
path: string
clientX: number
clientY: number
}
const buildTreeItems = (name: string, file: File): JSX.Element => {
export const FileTreeView: FC<{
onSelect: (path: string) => void
onDelete: (path: string) => void
onRename: (path: string) => void
fileTree: FileTree
activePath?: string
}> = ({ fileTree, activePath, onDelete, onRename, onSelect }) => {
const styles = useStyles()
const [contextMenu, setContextMenu] = useState<ContextMenu | undefined>()
const buildTreeItems = (
filename: string,
content?: FileTree | string,
parentPath?: string,
): JSX.Element => {
const currentPath = parentPath ? `${parentPath}/${filename}` : filename
let icon: JSX.Element | null = null
if (file.path.endsWith(".tf")) {
if (filename.endsWith(".tf")) {
icon = <FileTypeTerraform />
}
if (file.path.endsWith(".md")) {
if (filename.endsWith(".md")) {
icon = <FileTypeMarkdown />
}
if (file.path.endsWith("Dockerfile")) {
if (filename.endsWith("Dockerfile")) {
icon = <FileTypeDockerfile />
}
return (
<TreeItem
nodeId={file.path}
key={file.path}
label={name}
nodeId={currentPath}
key={currentPath}
label={filename}
className={`${styles.fileTreeItem} ${
file.path === activeFile?.path ? "active" : ""
currentPath === activePath ? "active" : ""
}`}
onClick={() => {
if (file.content) {
onSelect(file)
}
onSelect(currentPath)
}}
onContextMenu={(event) => {
event.preventDefault()
if (!file.content) {
return
}
setContextMenu(
contextMenu
? undefined
: {
file: file,
path: currentPath,
clientY: event.clientY,
clientX: event.clientX,
},
@@ -110,9 +79,16 @@ export const FileTree: FC<{
}}
icon={icon}
>
{Object.entries(file.children || {}).map(([name, file]) => {
return buildTreeItems(name, file)
})}
{typeof content === "object" ? (
Object.keys(content)
.sort(sortFileTree(content))
.map((filename) => {
const child = content[filename]
return buildTreeItems(filename, child, currentPath)
})
) : (
<></>
)}
</TreeItem>
)
}
@@ -124,9 +100,12 @@ export const FileTree: FC<{
aria-label="Files"
className={styles.fileTree}
>
{Object.entries(fileTree).map(([name, file]) => {
return buildTreeItems(name, file)
})}
{Object.keys(fileTree)
.sort(sortFileTree(fileTree))
.map((filename) => {
const child = fileTree[filename]
return buildTreeItems(filename, child)
})}
<Menu
onClose={() => setContextMenu(undefined)}
@@ -154,7 +133,7 @@ export const FileTree: FC<{
if (!contextMenu) {
return
}
onRename(contextMenu.file)
onRename(contextMenu.path)
setContextMenu(undefined)
}}
>
@@ -165,7 +144,7 @@ export const FileTree: FC<{
if (!contextMenu) {
return
}
onDelete(contextMenu.file)
onDelete(contextMenu.path)
setContextMenu(undefined)
}}
>
@@ -2,7 +2,7 @@ import { Story } from "@storybook/react"
import {
MockTemplate,
MockTemplateVersion,
MockTemplateVersionFiles,
MockTemplateVersionFileTree,
MockWorkspaceBuildLogs,
MockWorkspaceResource,
MockWorkspaceResource2,
@@ -29,7 +29,7 @@ export const Example = Template.bind({})
Example.args = {
template: MockTemplate,
templateVersion: MockTemplateVersion,
initialFiles: MockTemplateVersionFiles,
defaultFileTree: MockTemplateVersionFileTree,
}
export const Logs = Template.bind({})
@@ -37,7 +37,7 @@ export const Logs = Template.bind({})
Logs.args = {
template: MockTemplate,
templateVersion: MockTemplateVersion,
initialFiles: MockTemplateVersionFiles,
defaultFileTree: MockTemplateVersionFileTree,
buildLogs: MockWorkspaceBuildLogs,
}
@@ -46,7 +46,7 @@ export const Resources = Template.bind({})
Resources.args = {
template: MockTemplate,
templateVersion: MockTemplateVersion,
initialFiles: MockTemplateVersionFiles,
defaultFileTree: MockTemplateVersionFileTree,
buildLogs: MockWorkspaceBuildLogs,
resources: [
MockWorkspaceResource,
@@ -16,49 +16,60 @@ import { AvatarData } from "components/AvatarData/AvatarData"
import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable"
import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"
import { FC, useCallback, useEffect, useRef, useState } from "react"
import { dashboardContentBottomPadding, navHeight } from "theme/constants"
import { TemplateVersionFiles } from "util/templateVersion"
import { navHeight, dashboardContentBottomPadding } from "theme/constants"
import {
existsFile,
FileTree,
getFileContent,
isFolder,
removeFile,
setFile,
traverse,
} from "util/filetree"
import {
CreateFileDialog,
DeleteFileDialog,
RenameFileDialog,
} from "./FileDialog"
import { FileTree } from "./FileTree"
import { FileTreeView } from "./FileTreeView"
import { MonacoEditor } from "./MonacoEditor"
import {
getStatus,
TemplateVersionStatusBadge,
} from "./TemplateVersionStatusBadge"
interface File {
path: string
content?: string
children: Record<string, File>
}
export interface TemplateVersionEditorProps {
template: Template
templateVersion: TemplateVersion
initialFiles: TemplateVersionFiles
defaultFileTree: FileTree
buildLogs?: ProvisionerJobLog[]
resources?: WorkspaceResource[]
disablePreview: boolean
disableUpdate: boolean
onPreview: (files: TemplateVersionFiles) => void
onPreview: (files: FileTree) => void
onUpdate: () => void
}
const topbarHeight = navHeight
const findInitialFile = (fileTree: FileTree): string | undefined => {
let initialFile: string | undefined
traverse(fileTree, (content, filename, path) => {
if (filename.endsWith(".tf")) {
initialFile = path
}
})
return initialFile
}
export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
disablePreview,
disableUpdate,
template,
templateVersion,
initialFiles,
defaultFileTree,
onPreview,
onUpdate,
buildLogs,
@@ -69,29 +80,19 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
// This is for Storybook!
return resources ? 1 : 0
})
const [files, setFiles] = useState(initialFiles)
const [fileTree, setFileTree] = useState(defaultFileTree)
const [createFileOpen, setCreateFileOpen] = useState(false)
const [deleteFileOpen, setDeleteFileOpen] = useState<File>()
const [renameFileOpen, setRenameFileOpen] = useState<File>()
const [activeFile, setActiveFile] = useState<File | undefined>(() => {
const fileKeys = Object.keys(initialFiles)
for (let i = 0; i < fileKeys.length; i++) {
// Open a Terraform file by default!
if (fileKeys[i].endsWith(".tf")) {
return {
path: fileKeys[i],
content: initialFiles[fileKeys[i]],
children: {},
}
}
}
})
const [deleteFileOpen, setDeleteFileOpen] = useState<string>()
const [renameFileOpen, setRenameFileOpen] = useState<string>()
const [activePath, setActivePath] = useState<string | undefined>(() =>
findInitialFile(fileTree),
)
const triggerPreview = useCallback(() => {
onPreview(files)
onPreview(fileTree)
// Switch to the build log!
setSelectedTab(0)
}, [files, onPreview])
}, [fileTree, onPreview])
// Stop ctrl+s from saving files and make ctrl+enter trigger a preview.
useEffect(() => {
@@ -114,7 +115,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
return () => {
document.removeEventListener("keydown", keyListener)
}
}, [files, triggerPreview])
}, [triggerPreview])
// Automatically switch to the template preview tab when the build succeeds.
const previousVersion = useRef<TemplateVersion>()
@@ -137,6 +138,8 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
const hasIcon = template.icon && template.icon !== ""
const templateVersionSucceeded = templateVersion.job.status === "succeeded"
const showBuildLogs = Boolean(buildLogs)
const editorValue = getFileContent(activePath ?? "", fileTree) as string
useEffect(() => {
window.dispatchEvent(new Event("resize"))
}, [showBuildLogs])
@@ -228,17 +231,10 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
onClose={() => {
setCreateFileOpen(false)
}}
checkExists={(path) => Boolean(files[path])}
checkExists={(path) => existsFile(path, fileTree)}
onConfirm={(path) => {
setFiles({
...files,
[path]: "",
})
setActiveFile({
path,
content: "",
children: {},
})
setFileTree((fileTree) => setFile(path, "", fileTree))
setActivePath(path)
setCreateFileOpen(false)
setDirty(true)
}}
@@ -248,64 +244,69 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
if (!deleteFileOpen) {
throw new Error("delete file must be set")
}
const deleted = { ...files }
delete deleted[deleteFileOpen.path]
setFiles(deleted)
setFileTree((fileTree) => removeFile(deleteFileOpen, fileTree))
setDeleteFileOpen(undefined)
if (activeFile?.path === deleteFileOpen.path) {
setActiveFile(undefined)
if (activePath === deleteFileOpen) {
setActivePath(undefined)
}
setDirty(true)
}}
open={Boolean(deleteFileOpen)}
onClose={() => setDeleteFileOpen(undefined)}
filename={deleteFileOpen?.path || ""}
filename={deleteFileOpen || ""}
/>
<RenameFileDialog
open={Boolean(renameFileOpen)}
onClose={() => {
setRenameFileOpen(undefined)
}}
filename={renameFileOpen?.path || ""}
checkExists={(path) => Boolean(files[path])}
filename={renameFileOpen || ""}
checkExists={(path) => existsFile(path, fileTree)}
onConfirm={(newPath) => {
if (!renameFileOpen) {
return
}
const renamed = { ...files }
renamed[newPath] = renamed[renameFileOpen.path]
delete renamed[renameFileOpen.path]
setFiles(renamed)
renameFileOpen.path = newPath
setActiveFile(renameFileOpen)
setFileTree((fileTree) => {
fileTree = setFile(
newPath,
getFileContent(renameFileOpen, fileTree) as string,
fileTree,
)
fileTree = removeFile(renameFileOpen, fileTree)
return fileTree
})
setActivePath(newPath)
setRenameFileOpen(undefined)
setDirty(true)
}}
/>
</div>
<FileTree
files={files}
<FileTreeView
fileTree={fileTree}
onDelete={(file) => setDeleteFileOpen(file)}
onSelect={(file) => setActiveFile(file)}
onSelect={(filePath) => {
if (!isFolder(filePath, fileTree)) {
setActivePath(filePath)
}
}}
onRename={(file) => setRenameFileOpen(file)}
activeFile={activeFile}
activePath={activePath}
/>
</div>
<div className={styles.editorPane}>
<div className={styles.editor} data-chromatic="ignore">
{activeFile ? (
{activePath ? (
<MonacoEditor
value={activeFile?.content}
path={activeFile?.path}
value={editorValue}
path={activePath}
onChange={(value) => {
if (!activeFile) {
if (!activePath) {
return
}
setFiles({
...files,
[activeFile.path]: value,
})
setFileTree((fileTree) =>
setFile(activePath, value, fileTree),
)
setDirty(true)
}}
/>
@@ -5,8 +5,8 @@ import { FC } from "react"
import { Helmet } from "react-helmet-async"
import { useParams } from "react-router-dom"
import { pageTitle } from "util/page"
import { templateVersionMachine } from "xServices/templateVersion/templateVersionXService"
import { templateVersionEditorMachine } from "xServices/templateVersionEditor/templateVersionEditorXService"
import { useTemplateVersionData } from "./data"
type Params = {
version: string
@@ -16,9 +16,11 @@ type Params = {
export const TemplateVersionEditorPage: FC = () => {
const { version: versionName, template: templateName } = useParams() as Params
const orgId = useOrganizationId()
const [versionState] = useMachine(templateVersionMachine, {
context: { templateName, versionName, orgId },
})
const { isSuccess, data } = useTemplateVersionData(
orgId,
templateName,
versionName,
)
const [editorState, sendEvent] = useMachine(templateVersionEditorMachine, {
context: { orgId },
})
@@ -29,39 +31,32 @@ export const TemplateVersionEditorPage: FC = () => {
<title>{pageTitle(`${templateName} · Template Editor`)}</title>
</Helmet>
{versionState.context.template &&
versionState.context.currentFiles &&
versionState.context.currentVersion && (
<TemplateVersionEditor
template={versionState.context.template}
templateVersion={
editorState.context.version || versionState.context.currentVersion
}
initialFiles={versionState.context.currentFiles}
onPreview={(files) => {
if (!versionState.context.template) {
throw new Error("no template")
}
sendEvent({
type: "CREATE_VERSION",
files: files,
templateId: versionState.context.template.id,
})
}}
onUpdate={() => {
sendEvent({
type: "UPDATE_ACTIVE_VERSION",
})
}}
disablePreview={editorState.hasTag("loading")}
disableUpdate={
editorState.hasTag("loading") ||
editorState.context.version?.job.status !== "succeeded"
}
resources={editorState.context.resources}
buildLogs={editorState.context.buildLogs}
/>
)}
{isSuccess && (
<TemplateVersionEditor
template={data.template}
templateVersion={editorState.context.version || data.currentVersion}
defaultFileTree={data.fileTree}
onPreview={(fileTree) => {
sendEvent({
type: "CREATE_VERSION",
fileTree,
templateId: data.template.id,
})
}}
onUpdate={() => {
sendEvent({
type: "UPDATE_ACTIVE_VERSION",
})
}}
disablePreview={editorState.hasTag("loading")}
disableUpdate={
editorState.hasTag("loading") ||
editorState.context.version?.job.status !== "succeeded"
}
resources={editorState.context.resources}
buildLogs={editorState.context.buildLogs}
/>
)}
</>
)
}
@@ -0,0 +1,32 @@
import { useQuery } from "@tanstack/react-query"
import { getTemplateByName, getTemplateVersionByName } from "api/api"
import { createTemplateVersionFileTree } from "util/templateVersion"
const getTemplateVersionData = async (
orgId: string,
templateName: string,
versionName: string,
) => {
const [template, currentVersion] = await Promise.all([
getTemplateByName(orgId, templateName),
getTemplateVersionByName(orgId, templateName, versionName),
])
const fileTree = await createTemplateVersionFileTree(currentVersion)
return {
template,
currentVersion,
fileTree,
}
}
export const useTemplateVersionData = (
orgId: string,
templateName: string,
versionName: string,
) => {
return useQuery({
queryKey: ["templateVersion", templateName, versionName],
queryFn: () => getTemplateVersionData(orgId, templateName, versionName),
})
}
+37
View File
@@ -6,6 +6,7 @@ import * as TypesGen from "../api/typesGenerated"
import range from "lodash/range"
import { Permissions } from "xServices/auth/authXService"
import { TemplateVersionFiles } from "util/templateVersion"
import { FileTree } from "util/filetree"
export const MockTemplateDAUResponse: TypesGen.TemplateDAUsResponse = {
entries: [
@@ -303,6 +304,42 @@ spec {
`,
}
export const MockTemplateVersionFileTree: FileTree = {
"README.md": "# Example\n\nThis is an example template.",
"main.tf": `// Provides info about the workspace.
data "coder_workspace" "me" {}
// Provides the startup script used to download
// the agent and communicate with Coder.
resource "coder_agent" "dev" {
os = "linux"
arch = "amd64"
}
resource "kubernetes_pod" "main" {
// Ensures that the Pod dies when the workspace shuts down!
count = data.coder_workspace.me.start_count
metadata {
name = "dev-\${data.coder_workspace.me.id}"
}
spec {
container {
image = "ubuntu"
command = ["sh", "-c", coder_agent.main.init_script]
env {
name = "CODER_AGENT_TOKEN"
value = coder_agent.main.token
}
}
}
}
`,
images: {
"java.Dockerfile": "FROM eclipse-temurin:17-jdk-jammy",
"python.Dockerfile": "FROM python:3.8-slim-buster",
},
}
export const MockWorkspaceApp: TypesGen.WorkspaceApp = {
id: "test-app",
slug: "test-app",
+70
View File
@@ -0,0 +1,70 @@
import {
existsFile,
FileTree,
getFileContent,
isFolder,
removeFile,
setFile,
traverse,
} from "./filetree"
test("setFile() set file into the file tree", () => {
let fileTree: FileTree = {
"main.tf": "terraform",
images: { "java.Dockerfile": "java dockerfile" },
}
fileTree = setFile("images/python.Dockerfile", "python dockerfile", fileTree)
expect((fileTree.images as FileTree)["python.Dockerfile"]).toEqual(
"python dockerfile",
)
})
test("getFileContent() return the file content from the file tree", () => {
const fileTree: FileTree = {
"main.tf": "terraform content",
images: { "java.Dockerfile": "java dockerfile" },
}
expect(getFileContent("images/java.Dockerfile", fileTree)).toEqual(
"java dockerfile",
)
})
test("removeFile() removes a file from the file tree", () => {
let fileTree: FileTree = {
"main.tf": "terraform content",
images: { "java.Dockerfile": "java dockerfile" },
}
fileTree = removeFile("images", fileTree)
expect(fileTree.images).toBeUndefined()
})
test("existsFile() returns if there is or not a file", () => {
const fileTree: FileTree = {
"main.tf": "terraform content",
images: { "java.Dockerfile": "java dockerfile" },
}
expect(existsFile("images/java.Dockerfile", fileTree)).toBeTruthy()
expect(existsFile("no-existent-path", fileTree)).toBeFalsy()
})
test("isFolder() returns when a path is a folder or not", () => {
const fileTree: FileTree = {
"main.tf": "terraform content",
images: { "java.Dockerfile": "java dockerfile" },
}
expect(isFolder("images", fileTree)).toBeTruthy()
expect(isFolder("images/java.Dockerfile", fileTree)).toBeFalsy()
})
test("traverse() go trough all the file tree files", () => {
const fileTree: FileTree = {
"main.tf": "terraform content",
images: { "java.Dockerfile": "java dockerfile" },
}
const filePaths: string[] = []
traverse(fileTree, (_content, _filename, fullPath) => {
filePaths.push(fullPath)
})
const expectedFilePaths = ["main.tf", "images", "images/java.Dockerfile"]
expect(filePaths).toEqual(expectedFilePaths)
})
+52
View File
@@ -0,0 +1,52 @@
import set from "lodash/set"
import has from "lodash/has"
import omit from "lodash/omit"
import get from "lodash/get"
export type FileTree = {
[key: string]: FileTree | string
}
export const setFile = (
path: string,
content: string,
fileTree: FileTree,
): FileTree => {
return set(fileTree, path.split("/"), content)
}
export const existsFile = (path: string, fileTree: FileTree) => {
return has(fileTree, path.split("/"))
}
export const removeFile = (path: string, fileTree: FileTree) => {
return omit(fileTree, path.split("/"))
}
export const getFileContent = (path: string, fileTree: FileTree) => {
return get(fileTree, path.split("/")) as string | FileTree
}
export const isFolder = (path: string, fileTree: FileTree) => {
const content = getFileContent(path, fileTree)
return typeof content === "object"
}
export const traverse = (
fileTree: FileTree,
callback: (
content: FileTree | string,
filename: string,
fullPath: string,
) => void,
parent?: string,
) => {
Object.keys(fileTree).forEach((filename) => {
const fullPath = parent ? `${parent}/${filename}` : filename
const content = fileTree[filename]
callback(content, filename, fullPath)
if (typeof content === "object") {
traverse(content, callback, fullPath)
}
})
}
+30 -2
View File
@@ -1,6 +1,7 @@
import { getFile } from "api/api"
import * as API from "api/api"
import { TemplateVersion } from "api/typesGenerated"
import untar from "js-untar"
import { FileTree, setFile } from "./filetree"
/**
* Content by filename
@@ -13,7 +14,7 @@ export const getTemplateVersionFiles = async (
allowedFiles: string[],
): Promise<TemplateVersionFiles> => {
const files: TemplateVersionFiles = {}
const tarFile = await getFile(version.job.file_id)
const tarFile = await API.getFile(version.job.file_id)
const blobs: Record<string, Blob> = {}
await untar(tarFile).then(undefined, undefined, async (file) => {
@@ -37,3 +38,30 @@ export const getTemplateVersionFiles = async (
return files
}
const allowedExtensions = ["tf", "md", "Dockerfile"]
export const createTemplateVersionFileTree = async (
version: TemplateVersion,
): Promise<FileTree> => {
let fileTree: FileTree = {}
const tarFile = await API.getFile(version.job.file_id)
const blobs: Record<string, Blob> = {}
await untar(tarFile).then(undefined, undefined, async (file) => {
if (allowedExtensions.some((ext) => file.name.endsWith(ext))) {
blobs[file.name] = file.blob
}
})
// We don't want to get the blob text during untar to not block the main thread.
// Also, by doing it here, we can make all the loading in parallel.
await Promise.all(
Object.entries(blobs).map(async ([fullPath, blob]) => {
const content = await blob.text()
fileTree = setFile(fullPath, content, fileTree)
}),
)
return fileTree
}
@@ -7,8 +7,8 @@ import {
} from "api/typesGenerated"
import { assign, createMachine } from "xstate"
import * as API from "api/api"
import { TemplateVersionFiles } from "util/templateVersion"
import Tar from "tar-js"
import { FileTree, traverse } from "util/filetree"
export interface CreateVersionData {
file: File
@@ -16,9 +16,8 @@ export interface CreateVersionData {
export interface TemplateVersionEditorMachineContext {
orgId: string
templateId?: string
files?: TemplateVersionFiles
fileTree?: FileTree
uploadResponse?: UploadResponse
version?: TemplateVersion
resources?: WorkspaceResource[]
@@ -34,7 +33,7 @@ export const templateVersionEditorMachine = createMachine(
events: {} as
| {
type: "CREATE_VERSION"
files: TemplateVersionFiles
fileTree: FileTree
templateId: string
}
| { type: "CANCEL_VERSION" }
@@ -131,8 +130,7 @@ export const templateVersionEditorMachine = createMachine(
actions: "addBuildLog",
},
CANCEL_VERSION: {
actions: "cancelBuild",
target: "idle",
target: "cancelingBuild",
},
CREATE_VERSION: {
actions: ["assignCreateBuild"],
@@ -167,7 +165,7 @@ export const templateVersionEditorMachine = createMachine(
{
actions: {
assignCreateBuild: assign({
files: (_, event) => event.files,
fileTree: (_, event) => event.fileTree,
templateId: (_, event) => event.templateId,
buildLogs: (_, _1) => [],
resources: (_, _1) => [],
@@ -206,13 +204,15 @@ export const templateVersionEditorMachine = createMachine(
},
services: {
uploadTar: (ctx) => {
if (!ctx.files) {
if (!ctx.fileTree) {
throw new Error("files must be set")
}
const tar = new Tar()
let out: Uint8Array = new Uint8Array()
Object.entries(ctx.files).forEach(([path, content]) => {
out = tar.append(path, content)
traverse(ctx.fileTree, (content, _filename, fullPath) => {
if (typeof content === "string") {
out = tar.append(fullPath, content)
}
})
return API.uploadTemplateFile(new File([out], "template.tar"))
},