From e11f8d30d69e2eb6c82186a21741ebd1bd589d8f Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 9 Feb 2023 12:06:02 +0000 Subject: [PATCH 1/8] Sort files --- .../TemplateVersionEditor/FileTree.tsx | 111 +++++++++++------- .../TemplateVersionEditor.tsx | 100 +++++++++------- .../TemplateVersionEditorPage.tsx | 69 +++++------ .../TemplateVersionEditorPage/data.ts | 39 ++++++ site/src/util/templateVersion.ts | 69 ++++++++++- .../templateVersionEditorXService.ts | 30 +++-- 6 files changed, 284 insertions(+), 134 deletions(-) create mode 100644 site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/data.ts diff --git a/site/src/components/TemplateVersionEditor/FileTree.tsx b/site/src/components/TemplateVersionEditor/FileTree.tsx index c3033c6564721..2be4d49a98cde 100644 --- a/site/src/components/TemplateVersionEditor/FileTree.tsx +++ b/site/src/components/TemplateVersionEditor/FileTree.tsx @@ -6,7 +6,7 @@ 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 { TemplateVersionFileTree } from "util/templateVersion" import { DockerIcon } from "components/Icons/DockerIcon" export interface File { @@ -15,50 +15,44 @@ export interface File { children: Record } +const mapFileTreeToFiles = ( + fileTree: TemplateVersionFileTree, + parent?: string, +): Record => { + const files: Record = {} + + Object.keys(fileTree).forEach((filename) => { + const currentPath = parent ? `${parent}/${filename}` : filename + const content = fileTree[filename] + if (typeof content === "string") { + files[currentPath] = { + path: currentPath, + content, + children: {}, + } + } else { + files[currentPath] = { + path: currentPath, + children: mapFileTreeToFiles(content, currentPath), + } + } + }) + + return files +} + export const FileTree: FC<{ onSelect: (file: File) => void onDelete: (file: File) => void onRename: (file: File) => void - files: TemplateVersionFiles + files: TemplateVersionFileTree activeFile?: File }> = ({ activeFile, files, onDelete, onRename, onSelect }) => { const styles = useStyles() - const fileTree = useMemo>(() => { - const paths = Object.keys(files) - const roots: Record = {} - 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 fileTree = useMemo>( + () => mapFileTreeToFiles(files), + [files], + ) const [contextMenu, setContextMenu] = useState< | { file: File @@ -89,7 +83,8 @@ export const FileTree: FC<{ file.path === activeFile?.path ? "active" : "" }`} onClick={() => { - if (file.content) { + // Content can be an empty string + if (file.content !== undefined) { onSelect(file) } }} @@ -110,9 +105,22 @@ export const FileTree: FC<{ }} icon={icon} > - {Object.entries(file.children || {}).map(([name, file]) => { - return buildTreeItems(name, file) - })} + {Object.keys(file.children) + .sort((a, b) => { + const child = file.children[a] + const childB = file.children[b] + if (child.content === undefined) { + return -1 + } + if (childB.content === undefined) { + return 1 + } + return a.localeCompare(b) + }) + .map((path) => { + const child = file.children[path] + return buildTreeItems(path, child) + })} ) } @@ -124,9 +132,22 @@ export const FileTree: FC<{ aria-label="Files" className={styles.fileTree} > - {Object.entries(fileTree).map(([name, file]) => { - return buildTreeItems(name, file) - })} + {Object.keys(fileTree) + .sort((a, b) => { + const child = fileTree[a] + const childB = fileTree[b] + if (child.content === undefined) { + return -1 + } + if (childB.content === undefined) { + return 1 + } + return a.localeCompare(b) + }) + .map((path) => { + const child = fileTree[path] + return buildTreeItems(path, child) + })} setContextMenu(undefined)} diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 071c450630980..5889f479d73fa 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -17,7 +17,13 @@ import { TemplateResourcesTable } from "components/TemplateResourcesTable/Templa import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs" import { FC, useCallback, useEffect, useRef, useState } from "react" import { navHeight } from "theme/constants" -import { TemplateVersionFiles } from "util/templateVersion" +import { + existsFile, + getFileContent, + removeFile, + setFile, + TemplateVersionFileTree, +} from "util/templateVersion" import { CreateFileDialog, DeleteFileDialog, @@ -39,20 +45,41 @@ interface File { export interface TemplateVersionEditorProps { template: Template templateVersion: TemplateVersion - initialFiles: TemplateVersionFiles - + initialFiles: TemplateVersionFileTree buildLogs?: ProvisionerJobLog[] resources?: WorkspaceResource[] - disablePreview: boolean disableUpdate: boolean - - onPreview: (files: TemplateVersionFiles) => void + onPreview: (files: TemplateVersionFileTree) => void onUpdate: () => void } const topbarHeight = navHeight +const findInitialFile = ( + fileTree: TemplateVersionFileTree, + parent?: string, +): File | undefined => { + for (const key of Object.keys(fileTree)) { + const currentPath = parent ? `${parent}/${key}` : key + + if (key.endsWith(".tf")) { + return { + path: currentPath, + content: fileTree[key] as string, + children: {}, + } + } + + if (typeof fileTree[key] !== "string") { + return findInitialFile( + fileTree[key] as TemplateVersionFileTree, + currentPath, + ) + } + } +} + export const TemplateVersionEditor: FC = ({ disablePreview, disableUpdate, @@ -69,29 +96,19 @@ export const TemplateVersionEditor: FC = ({ // This is for Storybook! return resources ? 1 : 0 }) - const [files, setFiles] = useState(initialFiles) + const [fileTree, setFileTree] = useState(initialFiles) const [createFileOpen, setCreateFileOpen] = useState(false) const [deleteFileOpen, setDeleteFileOpen] = useState() const [renameFileOpen, setRenameFileOpen] = useState() - const [activeFile, setActiveFile] = useState(() => { - 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 [activeFile, setActiveFile] = useState(() => + 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 +131,7 @@ export const TemplateVersionEditor: FC = ({ return () => { document.removeEventListener("keydown", keyListener) } - }, [files, triggerPreview]) + }, [triggerPreview]) // Automatically switch to the template preview tab when the build succeeds. const previousVersion = useRef() @@ -228,12 +245,9 @@ export const TemplateVersionEditor: FC = ({ onClose={() => { setCreateFileOpen(false) }} - checkExists={(path) => Boolean(files[path])} + checkExists={(path) => existsFile(path, fileTree)} onConfirm={(path) => { - setFiles({ - ...files, - [path]: "", - }) + setFileTree((fileTree) => setFile(path, "", fileTree)) setActiveFile({ path, content: "", @@ -248,9 +262,9 @@ export const TemplateVersionEditor: FC = ({ if (!deleteFileOpen) { throw new Error("delete file must be set") } - const deleted = { ...files } - delete deleted[deleteFileOpen.path] - setFiles(deleted) + setFileTree((fileTree) => + removeFile(deleteFileOpen.path, fileTree), + ) setDeleteFileOpen(undefined) if (activeFile?.path === deleteFileOpen.path) { setActiveFile(undefined) @@ -267,15 +281,20 @@ export const TemplateVersionEditor: FC = ({ setRenameFileOpen(undefined) }} filename={renameFileOpen?.path || ""} - checkExists={(path) => Boolean(files[path])} + checkExists={(path) => existsFile(path, fileTree)} onConfirm={(newPath) => { if (!renameFileOpen) { return } - const renamed = { ...files } - renamed[newPath] = renamed[renameFileOpen.path] - delete renamed[renameFileOpen.path] - setFiles(renamed) + setFileTree((fileTree) => { + fileTree = setFile( + newPath, + getFileContent(renameFileOpen.path, fileTree), + fileTree, + ) + fileTree = removeFile(renameFileOpen.path, fileTree) + return fileTree + }) renameFileOpen.path = newPath setActiveFile(renameFileOpen) setRenameFileOpen(undefined) @@ -284,7 +303,7 @@ export const TemplateVersionEditor: FC = ({ /> setDeleteFileOpen(file)} onSelect={(file) => setActiveFile(file)} onRename={(file) => setRenameFileOpen(file)} @@ -302,10 +321,9 @@ export const TemplateVersionEditor: FC = ({ if (!activeFile) { return } - setFiles({ - ...files, - [activeFile.path]: value, - }) + setFileTree((fileTree) => + setFile(activeFile.path, value, fileTree), + ) setDirty(true) }} /> diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index c57970398c164..209b56f221895 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -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 = () => { {pageTitle(`${templateName} ยท Template Editor`)} - {versionState.context.template && - versionState.context.currentFiles && - versionState.context.currentVersion && ( - { - 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 && ( + { + sendEvent({ + type: "CREATE_VERSION", + files, + 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} + /> + )} ) } diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/data.ts b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/data.ts new file mode 100644 index 0000000000000..b10f8e6df199d --- /dev/null +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/data.ts @@ -0,0 +1,39 @@ +import { useQuery } from "@tanstack/react-query" +import { getTemplateByName, getTemplateVersionByName } from "api/api" +import { getTemplateVersionFileTree } 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 allowedExtensions = ["tf", "md", "Dockerfile"] + const allowedFiles = ["Dockerfile"] + const files = await getTemplateVersionFileTree( + currentVersion, + allowedExtensions, + allowedFiles, + ) + + return { + template, + currentVersion, + files, + } +} + +export const useTemplateVersionData = ( + orgId: string, + templateName: string, + versionName: string, +) => { + return useQuery({ + queryKey: ["templateVersion", templateName, versionName], + queryFn: () => getTemplateVersionData(orgId, templateName, versionName), + }) +} diff --git a/site/src/util/templateVersion.ts b/site/src/util/templateVersion.ts index 13fc45c8a54a9..8bcfb76479113 100644 --- a/site/src/util/templateVersion.ts +++ b/site/src/util/templateVersion.ts @@ -1,6 +1,10 @@ -import { getFile } from "api/api" +import * as API from "api/api" import { TemplateVersion } from "api/typesGenerated" import untar from "js-untar" +import set from "lodash/set" +import has from "lodash/has" +import omit from "lodash/omit" +import get from "lodash/get" /** * Content by filename @@ -13,7 +17,7 @@ export const getTemplateVersionFiles = async ( allowedFiles: string[], ): Promise => { const files: TemplateVersionFiles = {} - const tarFile = await getFile(version.job.file_id) + const tarFile = await API.getFile(version.job.file_id) const blobs: Record = {} await untar(tarFile).then(undefined, undefined, async (file) => { @@ -37,3 +41,64 @@ export const getTemplateVersionFiles = async ( return files } + +export type TemplateVersionFileTree = { + [key: string]: TemplateVersionFileTree | string +} + +export const getTemplateVersionFileTree = async ( + version: TemplateVersion, + allowedExtensions: string[], + allowedFiles: string[], +): Promise => { + let fileTree: TemplateVersionFileTree = {} + const tarFile = await API.getFile(version.job.file_id) + const blobs: Record = {} + + await untar(tarFile).then(undefined, undefined, async (file) => { + const fullPath = file.name + const paths = fullPath.split("/") + const filename = paths[paths.length - 1] + const [_, extension] = filename.split(".") + + if ( + allowedExtensions.includes(extension) || + allowedFiles.includes(filename) + ) { + blobs[fullPath] = file.blob + } + }) + + await Promise.all( + Object.entries(blobs).map(async ([fullPath, blob]) => { + const paths = fullPath.split("/") + const content = await blob.text() + fileTree = set(fileTree, paths, content) + }), + ) + + return fileTree +} + +export const setFile = ( + path: string, + content: string, + fileTree: TemplateVersionFileTree, +): TemplateVersionFileTree => { + return set(fileTree, path.split("/"), content) +} + +export const existsFile = (path: string, fileTree: TemplateVersionFileTree) => { + return has(fileTree, path.split("/")) +} + +export const removeFile = (path: string, fileTree: TemplateVersionFileTree) => { + return omit(fileTree, path.split("/")) +} + +export const getFileContent = ( + path: string, + fileTree: TemplateVersionFileTree, +) => { + return get(fileTree, path.split("/")) +} diff --git a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts index 63078591cd064..40532ab3437fa 100644 --- a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts +++ b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts @@ -7,7 +7,7 @@ import { } from "api/typesGenerated" import { assign, createMachine } from "xstate" import * as API from "api/api" -import { TemplateVersionFiles } from "util/templateVersion" +import { TemplateVersionFileTree } from "util/templateVersion" import Tar from "tar-js" export interface CreateVersionData { @@ -16,9 +16,8 @@ export interface CreateVersionData { export interface TemplateVersionEditorMachineContext { orgId: string - templateId?: string - files?: TemplateVersionFiles + files?: TemplateVersionFileTree uploadResponse?: UploadResponse version?: TemplateVersion resources?: WorkspaceResource[] @@ -34,7 +33,7 @@ export const templateVersionEditorMachine = createMachine( events: {} as | { type: "CREATE_VERSION" - files: TemplateVersionFiles + files: TemplateVersionFileTree 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"], @@ -211,9 +209,23 @@ export const templateVersionEditorMachine = createMachine( } const tar = new Tar() let out: Uint8Array = new Uint8Array() - Object.entries(ctx.files).forEach(([path, content]) => { - out = tar.append(path, content) - }) + + const appendToTar = ( + fileTree: TemplateVersionFileTree, + parent?: string, + ) => { + Object.keys(fileTree).forEach((filename) => { + const currentPath = parent ? `${parent}/${filename}` : filename + const content = fileTree[filename] + + if (typeof content === "string") { + out = tar.append(currentPath, content) + } else { + appendToTar(content, currentPath) + } + }) + } + appendToTar(ctx.files) return API.uploadTemplateFile(new File([out], "template.tar")) }, createBuild: (ctx) => { From 818868d13658b8d44dafd4d7c7f7de2a0ce4d8d4 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 9 Feb 2023 12:35:26 +0000 Subject: [PATCH 2/8] Polish a bit --- .../TemplateVersionEditor/FileTree.tsx | 49 ++++++++++--------- .../TemplateVersionEditor.tsx | 23 ++++----- site/src/util/templateVersion.ts | 19 +++++++ 3 files changed, 55 insertions(+), 36 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/FileTree.tsx b/site/src/components/TemplateVersionEditor/FileTree.tsx index 2be4d49a98cde..8f95de0b88b98 100644 --- a/site/src/components/TemplateVersionEditor/FileTree.tsx +++ b/site/src/components/TemplateVersionEditor/FileTree.tsx @@ -41,6 +41,31 @@ const mapFileTreeToFiles = ( return files } +const sortFileChildren = (file: File) => (a: string, b: string) => { + const child = file.children[a] + const childB = file.children[b] + if (child.content === undefined) { + return -1 + } + if (childB.content === undefined) { + return 1 + } + return a.localeCompare(b) +} + +const sortRootFiles = + (rootFiles: Record) => (a: string, b: string) => { + const child = rootFiles[a] + const childB = rootFiles[b] + if (child.content === undefined) { + return -1 + } + if (childB.content === undefined) { + return 1 + } + return a.localeCompare(b) + } + export const FileTree: FC<{ onSelect: (file: File) => void onDelete: (file: File) => void @@ -106,17 +131,7 @@ export const FileTree: FC<{ icon={icon} > {Object.keys(file.children) - .sort((a, b) => { - const child = file.children[a] - const childB = file.children[b] - if (child.content === undefined) { - return -1 - } - if (childB.content === undefined) { - return 1 - } - return a.localeCompare(b) - }) + .sort(sortFileChildren(file)) .map((path) => { const child = file.children[path] return buildTreeItems(path, child) @@ -133,17 +148,7 @@ export const FileTree: FC<{ className={styles.fileTree} > {Object.keys(fileTree) - .sort((a, b) => { - const child = fileTree[a] - const childB = fileTree[b] - if (child.content === undefined) { - return -1 - } - if (childB.content === undefined) { - return 1 - } - return a.localeCompare(b) - }) + .sort(sortRootFiles(fileTree)) .map((path) => { const child = fileTree[path] return buildTreeItems(path, child) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 5889f479d73fa..e033f00a4d6b0 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -23,6 +23,7 @@ import { removeFile, setFile, TemplateVersionFileTree, + traverse, } from "util/templateVersion" import { CreateFileDialog, @@ -58,26 +59,20 @@ const topbarHeight = navHeight const findInitialFile = ( fileTree: TemplateVersionFileTree, - parent?: string, ): File | undefined => { - for (const key of Object.keys(fileTree)) { - const currentPath = parent ? `${parent}/${key}` : key + let initialFile: File | undefined - if (key.endsWith(".tf")) { - return { - path: currentPath, - content: fileTree[key] as string, + traverse(fileTree, (content, filename, path) => { + if (filename.endsWith(".tf")) { + initialFile = { + path, + content: content as string, children: {}, } } + }) - if (typeof fileTree[key] !== "string") { - return findInitialFile( - fileTree[key] as TemplateVersionFileTree, - currentPath, - ) - } - } + return initialFile } export const TemplateVersionEditor: FC = ({ diff --git a/site/src/util/templateVersion.ts b/site/src/util/templateVersion.ts index 8bcfb76479113..4c49d63a140d9 100644 --- a/site/src/util/templateVersion.ts +++ b/site/src/util/templateVersion.ts @@ -102,3 +102,22 @@ export const getFileContent = ( ) => { return get(fileTree, path.split("/")) } + +export const traverse = ( + fileTree: TemplateVersionFileTree, + callback: ( + content: TemplateVersionFileTree | 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) + } + }) +} From f388d83224b6e22b6808adb93cc66de703650bd5 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 9 Feb 2023 20:11:44 +0000 Subject: [PATCH 3/8] Fix open folder --- .../TemplateVersionEditor/FileTree.tsx | 150 ++++++------------ .../TemplateVersionEditor.tsx | 70 ++++---- site/src/util/templateVersion.ts | 7 +- 3 files changed, 88 insertions(+), 139 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/FileTree.tsx b/site/src/components/TemplateVersionEditor/FileTree.tsx index 8f95de0b88b98..7d26cbd104d59 100644 --- a/site/src/components/TemplateVersionEditor/FileTree.tsx +++ b/site/src/components/TemplateVersionEditor/FileTree.tsx @@ -5,124 +5,74 @@ 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 { FC, useState } from "react" import { TemplateVersionFileTree } from "util/templateVersion" import { DockerIcon } from "components/Icons/DockerIcon" -export interface File { - path: string - content?: string - children: Record -} - -const mapFileTreeToFiles = ( - fileTree: TemplateVersionFileTree, - parent?: string, -): Record => { - const files: Record = {} - - Object.keys(fileTree).forEach((filename) => { - const currentPath = parent ? `${parent}/${filename}` : filename - const content = fileTree[filename] - if (typeof content === "string") { - files[currentPath] = { - path: currentPath, - content, - children: {}, - } - } else { - files[currentPath] = { - path: currentPath, - children: mapFileTreeToFiles(content, currentPath), - } - } - }) - - return files -} - -const sortFileChildren = (file: File) => (a: string, b: string) => { - const child = file.children[a] - const childB = file.children[b] - if (child.content === undefined) { - return -1 - } - if (childB.content === undefined) { - return 1 - } - return a.localeCompare(b) -} - -const sortRootFiles = - (rootFiles: Record) => (a: string, b: string) => { - const child = rootFiles[a] - const childB = rootFiles[b] - if (child.content === undefined) { +const sortFileTree = + (fileTree: TemplateVersionFileTree) => (a: string, b: string) => { + const contentA = fileTree[a] + const contentB = fileTree[b] + if (typeof contentA === "object") { return -1 } - if (childB.content === undefined) { + if (typeof contentB === "object") { return 1 } return a.localeCompare(b) } +type ContextMenu = { + path: string + clientX: number + clientY: number +} + export const FileTree: FC<{ - onSelect: (file: File) => void - onDelete: (file: File) => void - onRename: (file: File) => void - files: TemplateVersionFileTree - activeFile?: File -}> = ({ activeFile, files, onDelete, onRename, onSelect }) => { + onSelect: (path: string) => void + onDelete: (path: string) => void + onRename: (path: string) => void + fileTree: TemplateVersionFileTree + activePath?: string +}> = ({ fileTree, activePath, onDelete, onRename, onSelect }) => { const styles = useStyles() - const fileTree = useMemo>( - () => mapFileTreeToFiles(files), - [files], - ) - const [contextMenu, setContextMenu] = useState< - | { - file: File - clientX: number - clientY: number - } - | undefined - >() - - const buildTreeItems = (name: string, file: File): JSX.Element => { + const [contextMenu, setContextMenu] = useState() + + const buildTreeItems = ( + filename: string, + content?: TemplateVersionFileTree | 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 = } - if (file.path.endsWith(".md")) { + if (filename.endsWith(".md")) { icon = } - if (file.path.endsWith("Dockerfile")) { + if (filename.endsWith("Dockerfile")) { icon = } return ( { - // Content can be an empty string - if (file.content !== undefined) { - 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, }, @@ -130,12 +80,16 @@ export const FileTree: FC<{ }} icon={icon} > - {Object.keys(file.children) - .sort(sortFileChildren(file)) - .map((path) => { - const child = file.children[path] - return buildTreeItems(path, child) - })} + {typeof content === "object" ? ( + Object.keys(content) + .sort(sortFileTree(content)) + .map((filename) => { + const child = content[filename] + return buildTreeItems(filename, child, currentPath) + }) + ) : ( + <> + )} ) } @@ -148,10 +102,10 @@ export const FileTree: FC<{ className={styles.fileTree} > {Object.keys(fileTree) - .sort(sortRootFiles(fileTree)) - .map((path) => { - const child = fileTree[path] - return buildTreeItems(path, child) + .sort(sortFileTree(fileTree)) + .map((filename) => { + const child = fileTree[filename] + return buildTreeItems(filename, child) })} @@ -191,7 +145,7 @@ export const FileTree: FC<{ if (!contextMenu) { return } - onDelete(contextMenu.file) + onDelete(contextMenu.path) setContextMenu(undefined) }} > diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index e033f00a4d6b0..a353930443447 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -20,6 +20,7 @@ import { navHeight } from "theme/constants" import { existsFile, getFileContent, + isFolder, removeFile, setFile, TemplateVersionFileTree, @@ -37,12 +38,6 @@ import { TemplateVersionStatusBadge, } from "./TemplateVersionStatusBadge" -interface File { - path: string - content?: string - children: Record -} - export interface TemplateVersionEditorProps { template: Template templateVersion: TemplateVersion @@ -59,16 +54,12 @@ const topbarHeight = navHeight const findInitialFile = ( fileTree: TemplateVersionFileTree, -): File | undefined => { - let initialFile: File | undefined +): string | undefined => { + let initialFile: string | undefined traverse(fileTree, (content, filename, path) => { if (filename.endsWith(".tf")) { - initialFile = { - path, - content: content as string, - children: {}, - } + initialFile = path } }) @@ -93,9 +84,9 @@ export const TemplateVersionEditor: FC = ({ }) const [fileTree, setFileTree] = useState(initialFiles) const [createFileOpen, setCreateFileOpen] = useState(false) - const [deleteFileOpen, setDeleteFileOpen] = useState() - const [renameFileOpen, setRenameFileOpen] = useState() - const [activeFile, setActiveFile] = useState(() => + const [deleteFileOpen, setDeleteFileOpen] = useState() + const [renameFileOpen, setRenameFileOpen] = useState() + const [activePath, setActivePath] = useState(() => findInitialFile(fileTree), ) @@ -149,6 +140,8 @@ export const TemplateVersionEditor: FC = ({ 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]) @@ -243,11 +236,7 @@ export const TemplateVersionEditor: FC = ({ checkExists={(path) => existsFile(path, fileTree)} onConfirm={(path) => { setFileTree((fileTree) => setFile(path, "", fileTree)) - setActiveFile({ - path, - content: "", - children: {}, - }) + setActivePath(path) setCreateFileOpen(false) setDirty(true) }} @@ -257,25 +246,23 @@ export const TemplateVersionEditor: FC = ({ if (!deleteFileOpen) { throw new Error("delete file must be set") } - setFileTree((fileTree) => - removeFile(deleteFileOpen.path, fileTree), - ) + 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 || ""} /> { setRenameFileOpen(undefined) }} - filename={renameFileOpen?.path || ""} + filename={renameFileOpen || ""} checkExists={(path) => existsFile(path, fileTree)} onConfirm={(newPath) => { if (!renameFileOpen) { @@ -284,40 +271,43 @@ export const TemplateVersionEditor: FC = ({ setFileTree((fileTree) => { fileTree = setFile( newPath, - getFileContent(renameFileOpen.path, fileTree), + getFileContent(renameFileOpen, fileTree) as string, fileTree, ) - fileTree = removeFile(renameFileOpen.path, fileTree) + fileTree = removeFile(renameFileOpen, fileTree) return fileTree }) - renameFileOpen.path = newPath - setActiveFile(renameFileOpen) + setActivePath(newPath) setRenameFileOpen(undefined) setDirty(true) }} /> setDeleteFileOpen(file)} - onSelect={(file) => setActiveFile(file)} + onSelect={(filePath) => { + if (!isFolder(filePath, fileTree)) { + setActivePath(filePath) + } + }} onRename={(file) => setRenameFileOpen(file)} - activeFile={activeFile} + activePath={activePath} />
- {activeFile ? ( + {activePath ? ( { - if (!activeFile) { + if (!activePath) { return } setFileTree((fileTree) => - setFile(activeFile.path, value, fileTree), + setFile(activePath, value, fileTree), ) setDirty(true) }} diff --git a/site/src/util/templateVersion.ts b/site/src/util/templateVersion.ts index 4c49d63a140d9..52882456b3998 100644 --- a/site/src/util/templateVersion.ts +++ b/site/src/util/templateVersion.ts @@ -100,7 +100,12 @@ export const getFileContent = ( path: string, fileTree: TemplateVersionFileTree, ) => { - return get(fileTree, path.split("/")) + return get(fileTree, path.split("/")) as string | TemplateVersionFileTree +} + +export const isFolder = (path: string, fileTree: TemplateVersionFileTree) => { + const content = getFileContent(path, fileTree) + return typeof content === "object" } export const traverse = ( From f0d2fc6458f1c4cec5a00932439ee25b40f15a3c Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 9 Feb 2023 20:39:40 +0000 Subject: [PATCH 4/8] Move filetree --- .../{FileTree.tsx => FileTreeView.tsx} | 29 ++++--- .../TemplateVersionEditor.tsx | 20 +++-- .../TemplateVersionEditorPage.tsx | 2 +- .../TemplateVersionEditorPage/data.ts | 13 +-- site/src/util/filetree.ts | 52 ++++++++++++ site/src/util/templateVersion.ts | 81 +++---------------- 6 files changed, 89 insertions(+), 108 deletions(-) rename site/src/components/TemplateVersionEditor/{FileTree.tsx => FileTreeView.tsx} (92%) create mode 100644 site/src/util/filetree.ts diff --git a/site/src/components/TemplateVersionEditor/FileTree.tsx b/site/src/components/TemplateVersionEditor/FileTreeView.tsx similarity index 92% rename from site/src/components/TemplateVersionEditor/FileTree.tsx rename to site/src/components/TemplateVersionEditor/FileTreeView.tsx index 7d26cbd104d59..ba5b2236795c8 100644 --- a/site/src/components/TemplateVersionEditor/FileTree.tsx +++ b/site/src/components/TemplateVersionEditor/FileTreeView.tsx @@ -6,21 +6,20 @@ import TreeItem from "@material-ui/lab/TreeItem" import Menu from "@material-ui/core/Menu" import MenuItem from "@material-ui/core/MenuItem" import { FC, useState } from "react" -import { TemplateVersionFileTree } from "util/templateVersion" +import { FileTree } from "util/filetree" import { DockerIcon } from "components/Icons/DockerIcon" -const sortFileTree = - (fileTree: TemplateVersionFileTree) => (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) +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) +} type ContextMenu = { path: string @@ -28,11 +27,11 @@ type ContextMenu = { clientY: number } -export const FileTree: FC<{ +export const FileTreeView: FC<{ onSelect: (path: string) => void onDelete: (path: string) => void onRename: (path: string) => void - fileTree: TemplateVersionFileTree + fileTree: FileTree activePath?: string }> = ({ fileTree, activePath, onDelete, onRename, onSelect }) => { const styles = useStyles() @@ -40,7 +39,7 @@ export const FileTree: FC<{ const buildTreeItems = ( filename: string, - content?: TemplateVersionFileTree | string, + content?: FileTree | string, parentPath?: string, ): JSX.Element => { const currentPath = parentPath ? `${parentPath}/${filename}` : filename diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index a353930443447..987bea6e8fd81 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -19,19 +19,19 @@ import { FC, useCallback, useEffect, useRef, useState } from "react" import { navHeight } from "theme/constants" import { existsFile, + FileTree, getFileContent, isFolder, removeFile, setFile, - TemplateVersionFileTree, traverse, -} from "util/templateVersion" +} from "util/filetree" import { CreateFileDialog, DeleteFileDialog, RenameFileDialog, } from "./FileDialog" -import { FileTree } from "./FileTree" +import { FileTreeView } from "./FileTreeView" import { MonacoEditor } from "./MonacoEditor" import { getStatus, @@ -41,20 +41,18 @@ import { export interface TemplateVersionEditorProps { template: Template templateVersion: TemplateVersion - initialFiles: TemplateVersionFileTree + defaultFileTree: FileTree buildLogs?: ProvisionerJobLog[] resources?: WorkspaceResource[] disablePreview: boolean disableUpdate: boolean - onPreview: (files: TemplateVersionFileTree) => void + onPreview: (files: FileTree) => void onUpdate: () => void } const topbarHeight = navHeight -const findInitialFile = ( - fileTree: TemplateVersionFileTree, -): string | undefined => { +const findInitialFile = (fileTree: FileTree): string | undefined => { let initialFile: string | undefined traverse(fileTree, (content, filename, path) => { @@ -71,7 +69,7 @@ export const TemplateVersionEditor: FC = ({ disableUpdate, template, templateVersion, - initialFiles, + defaultFileTree, onPreview, onUpdate, buildLogs, @@ -82,7 +80,7 @@ export const TemplateVersionEditor: FC = ({ // This is for Storybook! return resources ? 1 : 0 }) - const [fileTree, setFileTree] = useState(initialFiles) + const [fileTree, setFileTree] = useState(defaultFileTree) const [createFileOpen, setCreateFileOpen] = useState(false) const [deleteFileOpen, setDeleteFileOpen] = useState() const [renameFileOpen, setRenameFileOpen] = useState() @@ -283,7 +281,7 @@ export const TemplateVersionEditor: FC = ({ }} />
- setDeleteFileOpen(file)} onSelect={(filePath) => { diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index 209b56f221895..bccc2869bb541 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -35,7 +35,7 @@ export const TemplateVersionEditorPage: FC = () => { { sendEvent({ type: "CREATE_VERSION", diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/data.ts b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/data.ts index b10f8e6df199d..6270e746d0b2c 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/data.ts +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/data.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query" import { getTemplateByName, getTemplateVersionByName } from "api/api" -import { getTemplateVersionFileTree } from "util/templateVersion" +import { createTemplateVersionFileTree } from "util/templateVersion" const getTemplateVersionData = async ( orgId: string, @@ -11,19 +11,12 @@ const getTemplateVersionData = async ( getTemplateByName(orgId, templateName), getTemplateVersionByName(orgId, templateName, versionName), ]) - - const allowedExtensions = ["tf", "md", "Dockerfile"] - const allowedFiles = ["Dockerfile"] - const files = await getTemplateVersionFileTree( - currentVersion, - allowedExtensions, - allowedFiles, - ) + const fileTree = await createTemplateVersionFileTree(currentVersion) return { template, currentVersion, - files, + fileTree, } } diff --git a/site/src/util/filetree.ts b/site/src/util/filetree.ts new file mode 100644 index 0000000000000..f14018ef17271 --- /dev/null +++ b/site/src/util/filetree.ts @@ -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) + } + }) +} diff --git a/site/src/util/templateVersion.ts b/site/src/util/templateVersion.ts index 52882456b3998..fbce12a9c7f6a 100644 --- a/site/src/util/templateVersion.ts +++ b/site/src/util/templateVersion.ts @@ -1,10 +1,7 @@ import * as API from "api/api" import { TemplateVersion } from "api/typesGenerated" import untar from "js-untar" -import set from "lodash/set" -import has from "lodash/has" -import omit from "lodash/omit" -import get from "lodash/get" +import { FileTree, setFile } from "./filetree" /** * Content by filename @@ -42,87 +39,29 @@ export const getTemplateVersionFiles = async ( return files } -export type TemplateVersionFileTree = { - [key: string]: TemplateVersionFileTree | string -} +const allowedExtensions = ["tf", "md", "Dockerfile"] -export const getTemplateVersionFileTree = async ( +export const createTemplateVersionFileTree = async ( version: TemplateVersion, - allowedExtensions: string[], - allowedFiles: string[], -): Promise => { - let fileTree: TemplateVersionFileTree = {} +): Promise => { + let fileTree: FileTree = {} const tarFile = await API.getFile(version.job.file_id) const blobs: Record = {} await untar(tarFile).then(undefined, undefined, async (file) => { - const fullPath = file.name - const paths = fullPath.split("/") - const filename = paths[paths.length - 1] - const [_, extension] = filename.split(".") - - if ( - allowedExtensions.includes(extension) || - allowedFiles.includes(filename) - ) { - blobs[fullPath] = file.blob + 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 paths = fullPath.split("/") const content = await blob.text() - fileTree = set(fileTree, paths, content) + fileTree = setFile(fullPath, content, fileTree) }), ) return fileTree } - -export const setFile = ( - path: string, - content: string, - fileTree: TemplateVersionFileTree, -): TemplateVersionFileTree => { - return set(fileTree, path.split("/"), content) -} - -export const existsFile = (path: string, fileTree: TemplateVersionFileTree) => { - return has(fileTree, path.split("/")) -} - -export const removeFile = (path: string, fileTree: TemplateVersionFileTree) => { - return omit(fileTree, path.split("/")) -} - -export const getFileContent = ( - path: string, - fileTree: TemplateVersionFileTree, -) => { - return get(fileTree, path.split("/")) as string | TemplateVersionFileTree -} - -export const isFolder = (path: string, fileTree: TemplateVersionFileTree) => { - const content = getFileContent(path, fileTree) - return typeof content === "object" -} - -export const traverse = ( - fileTree: TemplateVersionFileTree, - callback: ( - content: TemplateVersionFileTree | 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) - } - }) -} From b070a0eb4ab7cf3d6b073643a5c15ff46e2b1317 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 9 Feb 2023 21:09:34 +0000 Subject: [PATCH 5/8] Use traverse for append tar --- .../TemplateVersionEditorPage.tsx | 4 +-- .../templateVersionEditorXService.ts | 32 ++++++------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index bccc2869bb541..5b45f1767bd65 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -36,10 +36,10 @@ export const TemplateVersionEditorPage: FC = () => { template={data.template} templateVersion={editorState.context.version || data.currentVersion} defaultFileTree={data.fileTree} - onPreview={(files) => { + onPreview={(fileTree) => { sendEvent({ type: "CREATE_VERSION", - files, + fileTree, templateId: data.template.id, }) }} diff --git a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts index 40532ab3437fa..15bc033f0b594 100644 --- a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts +++ b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts @@ -7,8 +7,8 @@ import { } from "api/typesGenerated" import { assign, createMachine } from "xstate" import * as API from "api/api" -import { TemplateVersionFileTree } from "util/templateVersion" import Tar from "tar-js" +import { FileTree, traverse } from "util/filetree" export interface CreateVersionData { file: File @@ -17,7 +17,7 @@ export interface CreateVersionData { export interface TemplateVersionEditorMachineContext { orgId: string templateId?: string - files?: TemplateVersionFileTree + fileTree?: FileTree uploadResponse?: UploadResponse version?: TemplateVersion resources?: WorkspaceResource[] @@ -33,7 +33,7 @@ export const templateVersionEditorMachine = createMachine( events: {} as | { type: "CREATE_VERSION" - files: TemplateVersionFileTree + fileTree: FileTree templateId: string } | { type: "CANCEL_VERSION" } @@ -165,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) => [], @@ -204,28 +204,16 @@ 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() - - const appendToTar = ( - fileTree: TemplateVersionFileTree, - parent?: string, - ) => { - Object.keys(fileTree).forEach((filename) => { - const currentPath = parent ? `${parent}/${filename}` : filename - const content = fileTree[filename] - - if (typeof content === "string") { - out = tar.append(currentPath, content) - } else { - appendToTar(content, currentPath) - } - }) - } - appendToTar(ctx.files) + traverse(ctx.fileTree, (content, _filename, fullPath) => { + if (typeof content === "string") { + out = tar.append(fullPath, content) + } + }) return API.uploadTemplateFile(new File([out], "template.tar")) }, createBuild: (ctx) => { From 77b55f5ecd5d506fcb9b1a6bc8b12a039ed59dcf Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 10 Feb 2023 15:33:26 +0000 Subject: [PATCH 6/8] Minor e2e test change --- .github/workflows/ci.yaml | 3 --- site/package.json | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3a5057c13f3d5..d37e8b2c1e929 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/site/package.json b/site/package.json index 2cc3c480f0b34..e63df76dff585 100644 --- a/site/package.json +++ b/site/package.json @@ -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", From 55548d342af817aad592ae5ed89632545008c997 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 10 Feb 2023 15:39:41 +0000 Subject: [PATCH 7/8] Fix stories --- .../TemplateVersionEditor.stories.tsx | 8 ++-- site/src/testHelpers/entities.ts | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx index 78b5863bd74ce..24a40ad4d4fae 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx @@ -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, diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 8f57f49691432..cce61e6c75876 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -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", From 370335ccc505fcf75807578aee0deb0860b9323d Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Fri, 10 Feb 2023 15:55:52 +0000 Subject: [PATCH 8/8] Add tests for filetree --- site/src/util/filetree.test.ts | 70 ++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 site/src/util/filetree.test.ts diff --git a/site/src/util/filetree.test.ts b/site/src/util/filetree.test.ts new file mode 100644 index 0000000000000..2f85af20e34b9 --- /dev/null +++ b/site/src/util/filetree.test.ts @@ -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) +})