Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Move filetree
  • Loading branch information
BrunoQuaresma committed Feb 9, 2023
commit f0d2fc6458f1c4cec5a00932439ee25b40f15a3c
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,40 @@ 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
clientX: number
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()
const [contextMenu, setContextMenu] = useState<ContextMenu | undefined>()

const buildTreeItems = (
filename: string,
content?: TemplateVersionFileTree | string,
content?: FileTree | string,
parentPath?: string,
): JSX.Element => {
const currentPath = parentPath ? `${parentPath}/${filename}` : filename
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) => {
Expand All @@ -71,7 +69,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
disableUpdate,
template,
templateVersion,
initialFiles,
defaultFileTree,
onPreview,
onUpdate,
buildLogs,
Expand All @@ -82,7 +80,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
// 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<string>()
const [renameFileOpen, setRenameFileOpen] = useState<string>()
Expand Down Expand Up @@ -283,7 +281,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
}}
/>
</div>
<FileTree
<FileTreeView
fileTree={fileTree}
onDelete={(file) => setDeleteFileOpen(file)}
onSelect={(filePath) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const TemplateVersionEditorPage: FC = () => {
<TemplateVersionEditor
template={data.template}
templateVersion={editorState.context.version || data.currentVersion}
initialFiles={data.files}
defaultFileTree={data.fileTree}
onPreview={(files) => {
sendEvent({
type: "CREATE_VERSION",
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
}
}

Expand Down
52 changes: 52 additions & 0 deletions site/src/util/filetree.ts
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
81 changes: 10 additions & 71 deletions site/src/util/templateVersion.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<TemplateVersionFileTree> => {
let fileTree: TemplateVersionFileTree = {}
): 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) => {
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)
}
})
}