fix(site): Fix template version editor rename (#6251)

This commit is contained in:
Bruno Quaresma
2023-03-06 13:53:24 +00:00
committed by GitHub
parent 1483b42259
commit a3201bd658
10 changed files with 205 additions and 56 deletions
@@ -87,7 +87,11 @@ const useStyles = makeStyles((theme) => ({
color: theme.palette.text.primary,
},
"& p": {
"& p:not(.MuiFormHelperText-root)": {
margin: 0,
},
"& > p": {
margin: theme.spacing(1, 0),
},
},
@@ -3,15 +3,18 @@ import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
import { Stack } from "components/Stack/Stack"
import { ChangeEvent, FC, useState } from "react"
import Typography from "@material-ui/core/Typography"
import { allowedExtensions, isAllowedFile } from "util/templateVersion"
import { FileTree, validatePath } from "util/filetree"
export const CreateFileDialog: FC<{
onClose: () => void
checkExists: (path: string) => boolean
onConfirm: (path: string) => void
open: boolean
}> = ({ checkExists, onClose, onConfirm, open }) => {
fileTree: FileTree
}> = ({ checkExists, onClose, onConfirm, open, fileTree }) => {
const [pathValue, setPathValue] = useState("")
const [error, setError] = useState("")
const [error, setError] = useState<string>()
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setPathValue(event.target.value)
}
@@ -24,7 +27,20 @@ export const CreateFileDialog: FC<{
setError("File already exists")
return
}
if (!isAllowedFile(pathValue)) {
const extensions = allowedExtensions.join(", ")
setError(
`This extension is not allowed. You only can create files with the following extensions: ${extensions}.`,
)
return
}
const pathError = validatePath(pathValue, fileTree)
if (pathError) {
setError(pathError)
return
}
onConfirm(pathValue)
setError(undefined)
setPathValue("")
}
@@ -33,6 +49,7 @@ export const CreateFileDialog: FC<{
open={open}
onClose={() => {
onClose()
setError(undefined)
setPathValue("")
}}
onConfirm={handleConfirm}
@@ -42,10 +59,10 @@ export const CreateFileDialog: FC<{
confirmText="Create"
title="Create File"
description={
<Stack spacing={1}>
<Stack>
<Typography>
Specify the path to a file to be created. This path can contain
slashes too!
slashes too.
</Typography>
<TextField
autoFocus
@@ -54,14 +71,18 @@ export const CreateFileDialog: FC<{
handleConfirm()
}
}}
error={Boolean(error)}
helperText={error}
name="file-path"
autoComplete="off"
id="file-path"
placeholder="main.tf"
placeholder="example.tf"
value={pathValue}
onChange={handleChange}
label="File Path"
InputLabelProps={{
shrink: true,
}}
/>
</Stack>
}
@@ -82,7 +103,12 @@ export const DeleteFileDialog: FC<{
open={open}
onConfirm={onConfirm}
title="Delete File"
description={`Are you sure you want to delete "${filename}"?`}
description={
<>
Are you sure you want to delete <strong>{filename}</strong>? It will
be deleted permanently.
</>
}
/>
)
}
@@ -93,9 +119,10 @@ export const RenameFileDialog: FC<{
checkExists: (path: string) => boolean
open: boolean
filename: string
}> = ({ checkExists, onClose, onConfirm, open, filename }) => {
fileTree: FileTree
}> = ({ checkExists, onClose, onConfirm, open, filename, fileTree }) => {
const [pathValue, setPathValue] = useState(filename)
const [error, setError] = useState("")
const [error, setError] = useState<string>()
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setPathValue(event.target.value)
}
@@ -108,7 +135,20 @@ export const RenameFileDialog: FC<{
setError("File already exists")
return
}
if (!isAllowedFile(pathValue)) {
const extensions = allowedExtensions.join(", ")
setError(
`This extension is not allowed. You only can rename files with the following extensions: ${extensions}.`,
)
return
}
const pathError = validatePath(pathValue, fileTree)
if (pathError) {
setError(pathError)
return
}
onConfirm(pathValue)
setError(undefined)
setPathValue("")
}
@@ -117,20 +157,21 @@ export const RenameFileDialog: FC<{
open={open}
onClose={() => {
onClose()
setError(undefined)
setPathValue("")
}}
onConfirm={handleConfirm}
hideCancel={false}
type="success"
cancelText="Cancel"
confirmText="Create"
confirmText="Rename"
title="Rename File"
description={
<Stack spacing={1}>
<Typography>
Rename {`"${filename}"`} to something else. This path can contain
slashes too!
</Typography>
<Stack>
<p>
Rename <strong>{filename}</strong> to something else. This path can
contain slashes too!
</p>
<TextField
autoFocus
onKeyDown={(event) => {
@@ -138,12 +179,12 @@ export const RenameFileDialog: FC<{
handleConfirm()
}
}}
error={Boolean(error)}
helperText={error}
name="file-path"
autoComplete="off"
id="file-path"
placeholder="main.tf"
defaultValue={filename}
placeholder={filename}
value={pathValue}
onChange={handleChange}
label="File Path"
@@ -67,7 +67,8 @@ export const FileTreeView: FC<{
onSelect(currentPath)
}}
onContextMenu={(event) => {
event.preventDefault()
event.preventDefault() // Avoid default browser behavior
event.stopPropagation() // Avoid trigger parent context menu
setContextMenu(
contextMenu
? undefined
@@ -137,7 +138,7 @@ export const FileTreeView: FC<{
setContextMenu(undefined)
}}
>
Rename...
Rename
</MenuItem>
<MenuItem
onClick={() => {
@@ -148,7 +149,7 @@ export const FileTreeView: FC<{
setContextMenu(undefined)
}}
>
Delete Permanently
Delete
</MenuItem>
</Menu>
</TreeView>
@@ -18,13 +18,15 @@ import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuild
import { FC, useCallback, useEffect, useRef, useState } from "react"
import { navHeight, dashboardContentBottomPadding } from "theme/constants"
import {
createFile,
existsFile,
FileTree,
getFileContent,
isFolder,
moveFile,
removeFile,
setFile,
traverse,
updateFile,
} from "util/filetree"
import {
CreateFileDialog,
@@ -216,13 +218,14 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
</Tooltip>
</div>
<CreateFileDialog
fileTree={fileTree}
open={createFileOpen}
onClose={() => {
setCreateFileOpen(false)
}}
checkExists={(path) => existsFile(path, fileTree)}
onConfirm={(path) => {
setFileTree((fileTree) => setFile(path, "", fileTree))
setFileTree((fileTree) => createFile(path, fileTree, ""))
setActivePath(path)
setCreateFileOpen(false)
setDirty(true)
@@ -245,6 +248,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
filename={deleteFileOpen || ""}
/>
<RenameFileDialog
fileTree={fileTree}
open={Boolean(renameFileOpen)}
onClose={() => {
setRenameFileOpen(undefined)
@@ -255,15 +259,9 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
if (!renameFileOpen) {
return
}
setFileTree((fileTree) => {
fileTree = setFile(
newPath,
getFileContent(renameFileOpen, fileTree) as string,
fileTree,
)
fileTree = removeFile(renameFileOpen, fileTree)
return fileTree
})
setFileTree((fileTree) =>
moveFile(renameFileOpen, newPath, fileTree),
)
setActivePath(newPath)
setRenameFileOpen(undefined)
setDirty(true)
@@ -294,7 +292,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
return
}
setFileTree((fileTree) =>
setFile(activePath, value, fileTree),
updateFile(activePath, value, fileTree),
)
setDirty(true)
}}
@@ -46,7 +46,7 @@ export const WorkspaceSchedule: FC<
return (
<div className={styles.schedule}>
<Stack spacing={2}>
<Stack>
<div>
<span className={styles.scheduleLabel}>{Language.timezoneLabel}</span>
<span className={styles.scheduleValue}>{timezone}</span>
@@ -39,12 +39,7 @@ export const WorkspaceChangeVersionForm: FC<{
return (
<form onSubmit={formik.handleSubmit}>
<Stack direction="column" spacing={3}>
<Stack
direction="row"
spacing={2}
className={styles.workspace}
alignItems="center"
>
<Stack direction="row" className={styles.workspace} alignItems="center">
<div className={styles.workspaceIcon}>
<img src={workspace.template_icon} alt="" />
</div>
+64 -7
View File
@@ -3,17 +3,22 @@ import {
FileTree,
getFileContent,
isFolder,
moveFile,
removeFile,
setFile,
createFile,
traverse,
} from "./filetree"
test("setFile() set file into the file tree", () => {
test("createFile() 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)
fileTree = createFile(
"images/python.Dockerfile",
fileTree,
"python dockerfile",
)
expect((fileTree.images as FileTree)["python.Dockerfile"]).toEqual(
"python dockerfile",
)
@@ -29,13 +34,65 @@ test("getFileContent() return the file content from the file tree", () => {
)
})
test("removeFile() removes a file from the file tree", () => {
test("removeFile() removes a file from a folder", () => {
let fileTree: FileTree = {
"main.tf": "terraform content",
images: { "java.Dockerfile": "java dockerfile" },
images: {
"java.Dockerfile": "java dockerfile",
"python.Dockerfile": "python Dockerfile",
},
}
fileTree = removeFile("images", fileTree)
expect(fileTree.images).toBeUndefined()
fileTree = removeFile("images/python.Dockerfile", fileTree)
const expectedFileTree = {
"main.tf": "terraform content",
images: {
"java.Dockerfile": "java dockerfile",
},
}
expect(expectedFileTree).toEqual(fileTree)
})
test("removeFile() removes a file from root", () => {
let fileTree: FileTree = {
"main.tf": "terraform content",
images: {
"java.Dockerfile": "java dockerfile",
"python.Dockerfile": "python Dockerfile",
},
}
fileTree = removeFile("main.tf", fileTree)
const expectedFileTree = {
images: {
"java.Dockerfile": "java dockerfile",
"python.Dockerfile": "python Dockerfile",
},
}
expect(expectedFileTree).toEqual(fileTree)
})
test("moveFile() moves a file from in file tree", () => {
let fileTree: FileTree = {
"main.tf": "terraform content",
images: {
"java.Dockerfile": "java dockerfile",
"python.Dockerfile": "python Dockerfile",
},
}
fileTree = moveFile(
"images/java.Dockerfile",
"other/java.Dockerfile",
fileTree,
)
const expectedFileTree = {
"main.tf": "terraform content",
images: {
"python.Dockerfile": "python Dockerfile",
},
other: {
"java.Dockerfile": "java dockerfile",
},
}
expect(fileTree).toEqual(expectedFileTree)
})
test("existsFile() returns if there is or not a file", () => {
+51 -4
View File
@@ -1,15 +1,46 @@
import set from "lodash/set"
import has from "lodash/has"
import omit from "lodash/omit"
import unset from "lodash/unset"
import get from "lodash/get"
export type FileTree = {
[key: string]: FileTree | string
}
export const setFile = (
export const createFile = (
path: string,
content: string,
fileTree: FileTree,
value: string,
): FileTree => {
if (existsFile(path, fileTree)) {
throw new Error(`File ${path} already exists`)
}
const pathError = validatePath(path, fileTree)
if (pathError) {
throw new Error(pathError)
}
return set(fileTree, path.split("/"), value)
}
export const validatePath = (
path: string,
fileTree: FileTree,
): string | undefined => {
const paths = path.split("/")
paths.pop() // The last item is the filename
for (let i = 0; i <= paths.length; i++) {
const path = paths.slice(0, i + 1)
const pathStr = path.join("/")
if (existsFile(pathStr, fileTree) && !isFolder(pathStr, fileTree)) {
return `Invalid path. The path ${path} is not a folder`
}
}
}
export const updateFile = (
path: string,
content: FileTree | string,
fileTree: FileTree,
): FileTree => {
return set(fileTree, path.split("/"), content)
@@ -20,7 +51,23 @@ export const existsFile = (path: string, fileTree: FileTree) => {
}
export const removeFile = (path: string, fileTree: FileTree) => {
return omit(fileTree, path.split("/"))
const updatedFileTree = { ...fileTree }
unset(updatedFileTree, path.split("/"))
return updatedFileTree
}
export const moveFile = (
currentPath: string,
newPath: string,
fileTree: FileTree,
) => {
const content = getFileContent(currentPath, fileTree)
if (typeof content !== "string") {
throw new Error("Move folders is not allowed")
}
fileTree = removeFile(currentPath, fileTree)
fileTree = createFile(newPath, fileTree, content)
return fileTree
}
export const getFileContent = (path: string, fileTree: FileTree) => {
+4 -4
View File
@@ -1,6 +1,6 @@
import * as API from "api/api"
import { TemplateVersion } from "api/typesGenerated"
import { FileTree, setFile } from "./filetree"
import { FileTree, createFile } from "./filetree"
import { TarReader } from "./tar"
/**
@@ -23,7 +23,7 @@ export const getTemplateVersionFiles = async (
return files
}
const allowedExtensions = ["tf", "md", "Dockerfile"]
export const allowedExtensions = ["tf", "md", "Dockerfile"]
export const isAllowedFile = (name: string) => {
return allowedExtensions.some((ext) => name.endsWith(ext))
@@ -35,10 +35,10 @@ export const createTemplateVersionFileTree = async (
let fileTree: FileTree = {}
for (const file of tarReader.fileInfo) {
if (isAllowedFile(file.name)) {
fileTree = setFile(
fileTree = createFile(
file.name,
tarReader.getTextFile(file.name) as string,
fileTree,
tarReader.getTextFile(file.name) as string,
)
}
}
@@ -252,11 +252,17 @@ export const templateVersionEditorMachine = createMachine(
}
// Add the editable files
traverse(fileTree, (content, _filename, fullPath) => {
// When a file is deleted. Don't add it to the tar.
if (content === undefined) {
return
}
if (typeof content === "string") {
tar.addFile(fullPath, content)
} else {
tar.addFolder(fullPath)
return
}
tar.addFolder(fullPath)
})
const blob = await tar.write()
return API.uploadTemplateFile(new File([blob], "template.tar"))