Skip to content

feat: add template editor to the ui #5963

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 23 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
3 changes: 3 additions & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"react-chartjs-2": "4.3.1",
"react-color": "2.19.3",
"react-dom": "18.2.0",
"react-headless-tabs": "^6.0.3",
"react-helmet-async": "1.3.0",
"react-i18next": "12.1.1",
"react-markdown": "8.0.3",
Expand All @@ -75,6 +76,7 @@
"remark-gfm": "3.0.1",
"rollup-plugin-visualizer": "5.9.0",
"sourcemapped-stacktrace": "1.1.11",
"tar-js": "^0.3.0",
"ts-prune": "0.10.3",
"tzdata": "1.0.30",
"ua-parser-js": "1.0.33",
Expand Down Expand Up @@ -125,6 +127,7 @@
"jest-esm-transformer": "^1.0.0",
"jest-runner-eslint": "1.1.0",
"jest-websocket-mock": "2.4.0",
"monaco-editor": "^0.34.1",
"msw": "0.47.0",
"prettier": "2.8.1",
"resize-observer": "1.0.4",
Expand Down
8 changes: 7 additions & 1 deletion site/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ const GitAuthPage = lazy(() => import("./pages/GitAuthPage/GitAuthPage"))
const TemplateVersionPage = lazy(
() => import("./pages/TemplateVersionPage/TemplateVersionPage"),
)
const TemplateVersionEditorPage = lazy(
() => import("./pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage"),
)
const StarterTemplatesPage = lazy(
() => import("./pages/StarterTemplatesPage/StarterTemplatesPage"),
)
Expand Down Expand Up @@ -154,7 +157,10 @@ export const AppRouter: FC = () => {
<Route path="workspace" element={<CreateWorkspacePage />} />
<Route path="settings" element={<TemplateSettingsPage />} />
<Route path="versions">
<Route path=":version" element={<TemplateVersionPage />} />
<Route path=":version">
<Route index element={<TemplateVersionPage />} />
<Route path="edit" element={<TemplateVersionEditorPage />} />
</Route>
</Route>
</Route>
</Route>
Expand Down
20 changes: 20 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,17 @@ export const createTemplate = async (
return response.data
}

export const updateActiveTemplateVersion = async (
templateId: string,
data: TypesGen.UpdateActiveTemplateVersion,
): Promise<Types.Message> => {
const response = await axios.patch<Types.Message>(
`/api/v2/templates/${templateId}/versions`,
data,
)
return response.data
}

export const updateTemplateMeta = async (
templateId: string,
data: TypesGen.UpdateTemplateMeta,
Expand Down Expand Up @@ -420,6 +431,15 @@ export const cancelWorkspaceBuild = async (
return response.data
}

export const cancelTemplateVersionBuild = async (
templateVersionId: TypesGen.TemplateVersion["id"]
): Promise<Types.Message> => {
const response = await axios.patch(
`/api/v2/templateversions/${templateVersionId}/cancel`,
)
return response.data
}

export const createUser = async (
user: TypesGen.CreateUserRequest,
): Promise<TypesGen.User> => {
Expand Down
1 change: 0 additions & 1 deletion site/src/components/Dashboard/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,5 @@ const useStyles = makeStyles((theme) => ({
},
siteContent: {
flex: 1,
paddingBottom: theme.spacing(10),
},
}))
175 changes: 175 additions & 0 deletions site/src/components/TemplateVersionEditor/FileTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { makeStyles } from "@material-ui/core/styles"
import ChevronRightIcon from "@material-ui/icons/ChevronRight"
import ExpandMoreIcon from "@material-ui/icons/ExpandMore"
import TreeView from "@material-ui/lab/TreeView"
import TreeItem from "@material-ui/lab/TreeItem"

import { FC, useMemo } from "react"
import { TemplateVersionFiles } from "util/templateVersion"

export interface File {
path: string
content?: string
children: Record<string, File>
}

export const FileTree: FC<{
onSelect: (file: File) => void
files: TemplateVersionFiles
activeFile?: File
}> = ({ activeFile, files, 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 buildTreeItems = (name: string, file: File): JSX.Element => {
let icon: JSX.Element | null = null
if (file.path.endsWith(".tf")) {
icon = <FileTypeTerraform />
}
if (file.path.endsWith(".md")) {
icon = <FileTypeMarkdown />
}

return (
<TreeItem
nodeId={file.path}
key={file.path}
label={name}
className={`${styles.fileTreeItem} ${
file.path === activeFile?.path ? "active" : ""
}`}
onClick={() => {
if (file.content) {
onSelect(file)
}
}}
icon={icon}
>
{Object.entries(file.children || {}).map(([name, file]) => {
return buildTreeItems(name, file)
})}
</TreeItem>
)
}

return (
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
aria-label="Files"
className={styles.fileTree}
>
{Object.entries(fileTree).map(([name, file]) => {
return buildTreeItems(name, file)
})}
</TreeView>
)
}

const useStyles = makeStyles((theme) => ({
fileTree: {},
fileTreeItem: {
overflow: "hidden",
userSelect: "none",

"&:focus": {
"& > .MuiTreeItem-content": {
background: "rgba(255, 255, 255, 0.1)",
},
},
"& > .MuiTreeItem-content:hover": {
background: theme.palette.background.paperLight,
color: theme.palette.text.primary,
},

"& > .MuiTreeItem-content": {
padding: "1px 16px",
color: theme.palette.text.secondary,

"& svg": {
width: 16,
height: 16,
},

"& > .MuiTreeItem-label": {
marginLeft: 4,
fontSize: 14,
color: "inherit",
},
},

"&.active": {
background: theme.palette.background.paper,

"& > .MuiTreeItem-content": {
color: theme.palette.text.primary,
},
},
},
editor: {
flex: 1,
},
preview: {},
}))

const FileTypeTerraform = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#813cf3">
<title>file_type_terraform</title>
<polygon points="12.042 6.858 20.071 11.448 20.071 20.462 12.042 15.868 12.042 6.858 12.042 6.858" />
<polygon points="20.5 20.415 28.459 15.84 28.459 6.887 20.5 11.429 20.5 20.415 20.5 20.415" />
<polygon points="3.541 11.01 11.571 15.599 11.571 6.59 3.541 2 3.541 11.01 3.541 11.01" />
<polygon points="12.042 25.41 20.071 30 20.071 20.957 12.042 16.368 12.042 25.41 12.042 25.41" />
</svg>
)

const FileTypeMarkdown = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#755838">
<rect
x="2.5"
y="7.955"
width="27"
height="16.091"
style={{
fill: "none",
stroke: "#755838",
}}
/>
<polygon points="5.909 20.636 5.909 11.364 8.636 11.364 11.364 14.773 14.091 11.364 16.818 11.364 16.818 20.636 14.091 20.636 14.091 15.318 11.364 18.727 8.636 15.318 8.636 20.636 5.909 20.636" />
<polygon points="22.955 20.636 18.864 16.136 21.591 16.136 21.591 11.364 24.318 11.364 24.318 16.136 27.045 16.136 22.955 20.636" />
</svg>
)
104 changes: 104 additions & 0 deletions site/src/components/TemplateVersionEditor/MonacoEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useTheme } from "@material-ui/core/styles"
import Editor from "@monaco-editor/react"
import { FC, useLayoutEffect, useState } from "react"
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
import { hslToHex } from "util/colors"

export const MonacoEditor: FC<{
value?: string
path?: string
language?: string
onChange?: (value: string) => void
}> = ({ onChange, value, language, path }) => {
const theme = useTheme()
const [editor, setEditor] = useState<any>()
useLayoutEffect(() => {
if (!editor) {
return
}
console.log("Editor", editor)
;(window as any).editor = editor
const resizeListener = () => {
editor.layout({})
}
window.addEventListener("resize", resizeListener)
return () => {
window.removeEventListener("resize", resizeListener)
}
}, [editor])

return (
<Editor
value={value}
language={language || "hcl"}
theme="vs-dark"
options={{
automaticLayout: true,
fontFamily: MONOSPACE_FONT_FAMILY,
fontSize: 14,
wordWrap: "on",
}}
path={path}
onChange={(newValue) => {
if (onChange && newValue) {
onChange(newValue)
}
}}
onMount={(editor, monaco) => {
;(editor as any)._standaloneKeybindingService.addDynamicKeybinding(`-editor.action.insertLineAfter`, undefined, () => {
//
})

setEditor(editor)

document.fonts.ready
.then(() => {
// Ensures that all text is measured properly.
// If this isn't done, there can be weird selection issues.
monaco.editor.remeasureFonts()
})
.catch(() => {
// Not a biggie!
})

monaco.editor.defineTheme("min", {
base: "vs-dark",
inherit: true,
rules: [
{
token: "comment",
foreground: "6B737C",
},
{
token: "type",
foreground: "B392F0",
},
{
token: "string",
foreground: "9DB1C5",
},
{
token: "variable",
foreground: "BBBBBB",
},
{
token: "identifier",
foreground: "B392F0",
},
{
token: "delimiter.curly",
foreground: "EBB325",
},
],
colors: {
"editor.foreground": hslToHex(theme.palette.text.primary),
"editor.background": hslToHex(theme.palette.background.paper),
},
})
editor.updateOptions({
theme: "min",
})
}}
/>
)
}
Loading