From 564d40e42062c75cbb65bce09703b0acf878b2f8 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 1 Feb 2023 16:07:38 +0000 Subject: [PATCH 01/21] Add initial editor --- site/package.json | 2 + site/src/AppRouter.tsx | 8 +- site/src/api/api.ts | 9 + .../components/Dashboard/DashboardLayout.tsx | 1 - .../TemplateVersionEditor/FileTree.tsx | 175 ++++++++++++++ .../TemplateVersionEditor/MonacoEditor.tsx | 96 ++++++++ .../TemplateVersionEditor.stories.tsx | 225 ++++++++++++++++++ .../TemplateVersionEditor.tsx | 208 ++++++++++++++++ .../TemplateVersionEditorPage.tsx | 60 +++++ .../TemplateVersionPage.tsx | 2 +- .../templateVersionXService.ts | 73 +++++- .../templateVersionEditorXService.ts | 205 ++++++++++++++++ site/yarn.lock | 10 + 13 files changed, 1060 insertions(+), 14 deletions(-) create mode 100644 site/src/components/TemplateVersionEditor/FileTree.tsx create mode 100644 site/src/components/TemplateVersionEditor/MonacoEditor.tsx create mode 100644 site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx create mode 100644 site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx create mode 100644 site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx create mode 100644 site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts diff --git a/site/package.json b/site/package.json index 19421e62f3c6c..6f937b56f9b65 100644 --- a/site/package.json +++ b/site/package.json @@ -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", @@ -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", diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 715a4b080bb96..a4d25f63a6585 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -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"), ) @@ -154,7 +157,10 @@ export const AppRouter: FC = () => { } /> } /> - } /> + + } /> + } /> + diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 2aee82ed96c85..5d9ad1eb47c01 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -420,6 +420,15 @@ export const cancelWorkspaceBuild = async ( return response.data } +export const cancelTemplateVersionBuild = async ( + templateVersionId: TypesGen.TemplateVersion["id"] +): Promise => { + const response = await axios.patch( + `/api/v2/templateversions/${templateVersionId}/cancel`, + ) + return response.data +} + export const createUser = async ( user: TypesGen.CreateUserRequest, ): Promise => { diff --git a/site/src/components/Dashboard/DashboardLayout.tsx b/site/src/components/Dashboard/DashboardLayout.tsx index 920c3cfe30c31..372297f931e47 100644 --- a/site/src/components/Dashboard/DashboardLayout.tsx +++ b/site/src/components/Dashboard/DashboardLayout.tsx @@ -71,6 +71,5 @@ const useStyles = makeStyles((theme) => ({ }, siteContent: { flex: 1, - paddingBottom: theme.spacing(10), }, })) diff --git a/site/src/components/TemplateVersionEditor/FileTree.tsx b/site/src/components/TemplateVersionEditor/FileTree.tsx new file mode 100644 index 0000000000000..0d60fc22baacf --- /dev/null +++ b/site/src/components/TemplateVersionEditor/FileTree.tsx @@ -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 +} + +export const FileTree: FC<{ + onSelect: (file: File) => void + files: TemplateVersionFiles + activeFile?: File +}> = ({ activeFile, files, 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 buildTreeItems = (name: string, file: File): JSX.Element => { + let icon: JSX.Element | null = null + if (file.path.endsWith(".tf")) { + icon = + } + if (file.path.endsWith(".md")) { + icon = + } + + return ( + { + if (file.content) { + onSelect(file) + } + }} + icon={icon} + > + {Object.entries(file.children || {}).map(([name, file]) => { + return buildTreeItems(name, file) + })} + + ) + } + + return ( + } + defaultExpandIcon={} + aria-label="Files" + className={styles.fileTree} + > + {Object.entries(fileTree).map(([name, file]) => { + return buildTreeItems(name, file) + })} + + ) +} + +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 = () => ( + + file_type_terraform + + + + + +) + +const FileTypeMarkdown = () => ( + + + + + +) diff --git a/site/src/components/TemplateVersionEditor/MonacoEditor.tsx b/site/src/components/TemplateVersionEditor/MonacoEditor.tsx new file mode 100644 index 0000000000000..52fec4d7df747 --- /dev/null +++ b/site/src/components/TemplateVersionEditor/MonacoEditor.tsx @@ -0,0 +1,96 @@ +import { useTheme } from "@material-ui/core/styles" +import Editor from "@monaco-editor/react" +import { FC, useEffect, useState } from "react" +import { MONOSPACE_FONT_FAMILY } from "theme/constants" +import { hslToHex } from "util/colors" + +export const MonacoEditor: FC<{ + value?: string + language?: string + onChange?: (value: string) => void +}> = ({ onChange, value, language }) => { + const theme = useTheme() + const [editor, setEditor] = useState() + useEffect(() => { + if (!editor) { + return + } + const resizeListener = () => { + editor.layout({}) + } + window.addEventListener("resize", resizeListener) + return () => { + window.removeEventListener("resize", resizeListener) + } + }, [editor]) + + return ( + { + if (onChange && newValue) { + onChange(newValue) + } + }} + onMount={(editor, monaco) => { + 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", + }) + }} + /> + ) +} diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx new file mode 100644 index 0000000000000..a2bd98f527e4b --- /dev/null +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx @@ -0,0 +1,225 @@ +import { Story } from "@storybook/react" +import { TemplateVersionEditor, TemplateVersionEditorProps } from "./TemplateVersionEditor" + +export default { + title: "components/TemplateEditor", + component: TemplateVersionEditor, + parameters: { + layout: "fullscreen", + }, +} + +const Template: Story = (args: any) => ( + +) + +export const Example = Template.bind({}) +Example.args = { + initialFiles: { + "README.md": "# Example\n\nThis is an example template.", + "main.tf": `terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.6.10" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 2.22.0" + } + } +} + +# User parameters + +variable "region" { + type = string + description = "Which region to deploy to." + default = "us-pittsburgh" + validation { + condition = contains(["us-pittsburgh", "eu-helsinki", "ap-sydney"], var.region) + error_message = "Region must be one of us-pittsburg, eu-helsinki, or ap-sydney." + } +} + +variable "dotfiles_uri" { + type = string + description = <<-EOF + Dotfiles repo URI (optional) + + see https://dotfiles.github.io + EOF + default = "" +} + +locals { + // These are Tailscale IP addresses. Ask Dean or Kyle for help. + docker_host = { + "" = "tcp://100.94.74.63:2375" + "us-pittsburgh" = "tcp://100.94.74.63:2375" + "eu-helsinki" = "tcp://100.117.102.81:2375" + "ap-sydney" = "tcp://100.127.2.1:2375" + } +} + +provider "docker" { + host = lookup(local.docker_host, var.region) +} + +provider "coder" {} + +data "coder_workspace" "me" {} + +resource "coder_agent" "dev" { + arch = "amd64" + os = "linux" + + login_before_ready = false + startup_script_timeout = 60 + startup_script = <<-EOT + set -ex + # install and start code-server + curl -fsSL https://code-server.dev/install.sh | sh -s -- --version 4.8.3 + code-server --auth none --port 13337 & + sudo service docker start + DOTFILES_URI=\${var.dotfiles_uri} + rm -f ~/.personalize.log + if [ -n "$DOTFILES_URI" ]; then + coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee -a ~/.personalize.log + fi + if [ -x ~/personalize ]; then + ~/personalize 2>&1 | tee -a ~/.personalize.log + elif [ -f ~/personalize ]; then + echo "~/personalize is not executable, skipping..." | tee -a ~/.personalize.log + fi + EOT +} + +resource "coder_app" "code-server" { + agent_id = coder_agent.dev.id + slug = "code-server" + display_name = "code-server" + url = "http://localhost:13337/" + icon = "/icon/code.svg" + subdomain = false + share = "owner" + + healthcheck { + url = "http://localhost:13337/healthz" + interval = 3 + threshold = 10 + } +} + +resource "docker_volume" "home_volume" { + name = "coder-\${data.coder_workspace.me.id}-home" + # Protect the volume from being deleted due to changes in attributes. + lifecycle { + ignore_changes = all + } + # Add labels in Docker to keep track of orphan resources. + labels { + label = "coder.owner" + value = data.coder_workspace.me.owner + } + labels { + label = "coder.owner_id" + value = data.coder_workspace.me.owner_id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + # This field becomes outdated if the workspace is renamed but can + # be useful for debugging or cleaning out dangling volumes. + labels { + label = "coder.workspace_name_at_creation" + value = data.coder_workspace.me.name + } +} + +resource "coder_metadata" "home_info" { + resource_id = docker_volume.home_volume.id + item { + key = "🤫🤫🤫

" + value = "❤️❤️❤️" + sensitive = true + } +} + +locals { + container_name = "coder-\${data.coder_workspace.me.owner}-\${lower(data.coder_workspace.me.name)}" + registry_name = "codercom/oss-dogfood" +} +data "docker_registry_image" "dogfood" { + name = "\${local.registry_name}:main" +} + +resource "docker_image" "dogfood" { + name = "\${local.registry_name}@\${data.docker_registry_image.dogfood.sha256_digest}" + pull_triggers = [ + data.docker_registry_image.dogfood.sha256_digest, + sha1(join("", [for f in fileset(path.module, "files/*") : filesha1(f)])), + filesha1("Dockerfile"), + ] + keep_locally = true +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = docker_image.dogfood.name + name = local.container_name + # Hostname makes the shell more user friendly: coder@my-workspace:~$ + hostname = data.coder_workspace.me.name + # Use the docker gateway if the access URL is 127.0.0.1 + entrypoint = ["sh", "-c", replace(coder_agent.dev.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")] + # CPU limits are unnecessary since Docker will load balance automatically + memory = 32768 + runtime = "sysbox-runc" + env = [ + "CODER_AGENT_TOKEN=\${coder_agent.dev.token}", + "DATOCMS_API_TOKEN=\${var.datocms_api_token}", + ] + host { + host = "host.docker.internal" + ip = "host-gateway" + } + volumes { + container_path = "/home/coder/" + volume_name = docker_volume.home_volume.name + read_only = false + } + # Add labels in Docker to keep track of orphan resources. + labels { + label = "coder.owner" + value = data.coder_workspace.me.owner + } + labels { + label = "coder.owner_id" + value = data.coder_workspace.me.owner_id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + labels { + label = "coder.workspace_name" + value = data.coder_workspace.me.name + } +} + +resource "coder_metadata" "container_info" { + count = data.coder_workspace.me.start_count + resource_id = docker_container.workspace[0].id + item { + key = "memory" + value = docker_container.workspace[0].memory + } + item { + key = "runtime" + value = docker_container.workspace[0].runtime + } +} +`, + }, +} diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx new file mode 100644 index 0000000000000..93e10da4cbb78 --- /dev/null +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -0,0 +1,208 @@ +import Button from "@material-ui/core/Button" +import { makeStyles } from "@material-ui/core/styles" +import { ProvisionerJobLog, Template, TemplateVersion, WorkspaceResource } from "api/typesGenerated" +import { Avatar } from "components/Avatar/Avatar" +import { AvatarData } from "components/AvatarData/AvatarData" +import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable" +import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs" +import { FC, useEffect, useState } from "react" +import { navHeight } from "theme/constants" +import { TemplateVersionFiles } from "util/templateVersion" +import { FileTree } from "./FileTree" +import { MonacoEditor } from "./MonacoEditor" + +interface File { + path: string + content?: string + children: Record +} + +export interface TemplateVersionEditorProps { + template: Template + templateVersion: TemplateVersion + initialFiles: TemplateVersionFiles + + buildLogs?: ProvisionerJobLog[] + resources?: WorkspaceResource[] + + onBuild: (files: TemplateVersionFiles) => void +} + +const topbarHeight = 90 + +export const TemplateVersionEditor: FC = ({ + template, + templateVersion, + initialFiles, + onBuild, + buildLogs, + resources, +}) => { + const styles = useStyles() + const [files, setFiles] = useState(initialFiles) + const [activeFile, setActiveFile] = useState(() => { + const fileKeys = Object.keys(initialFiles) + for (let i = 0; i < fileKeys.length; i++) { + if (fileKeys[i].endsWith(".tf")) { + return { + path: fileKeys[i], + content: initialFiles[fileKeys[i]], + children: {}, + } + } + } + }) + useEffect(() => { + const saveListener = (event: KeyboardEvent) => { + if ( + (navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey) && + event.key === "s" + ) { + // Prevent opening the save dialog! + event.preventDefault() + } + } + document.addEventListener("keydown", saveListener) + return () => { + document.removeEventListener("keydown", saveListener) + } + }, []) + const hasIcon = template.icon && template.icon !== "" + + return ( +
+
+
+ Edit Template + + ) + } + /> +
+ +
+ + + + +
+
+ +
+
+
File Explorer
+ setActiveFile(file)} + activeFile={activeFile} + /> +
+ +
+
+ { + setFiles({ + ...files, + [activeFile?.path || ""]: value, + }) + }} /> +
+ +
+
+ {buildLogs && } + {templateVersion.job.error && ( +
+ {templateVersion.job.error} +
+ )} +
+
+ {resources && } +
+
+
+ +
+
+ ) +} + +const useStyles = makeStyles((theme) => ({ + root: { + height: `calc(100vh - ${navHeight}px)`, + background: theme.palette.background.default, + flex: 1, + display: "flex", + flexDirection: "column", + }, + topbar: { + padding: theme.spacing(2), + borderBottom: `1px solid ${theme.palette.divider}`, + display: "flex", + justifyContent: "space-between", + height: topbarHeight, + }, + sidebarAndEditor: { + display: "flex", + flex: 1, + }, + sidebar: { + minWidth: 256, + }, + sidebarTitle: { + fontSize: 12, + textTransform: "uppercase", + padding: "8px 16px", + color: theme.palette.text.hint, + }, + editorPane: { + display: "flex", + flexDirection: "row", + flex: 1, + height: `calc(100vh - ${navHeight + topbarHeight}px)`, + overflow: "hidden", + }, + editor: { + flex: 1, + }, + panel: { + flex: 1, + display: "flex", + flexDirection: "column", + }, + buildLogs: { + height: "50%", + display: "flex", + flexDirection: "column-reverse", + overflowY: "auto", + }, + buildLogError: { + whiteSpace: "pre-wrap", + }, + resources: { + height: "50%", + overflowY: "auto", + }, +})) diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx new file mode 100644 index 0000000000000..ed7346d0c6127 --- /dev/null +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -0,0 +1,60 @@ +import { useMachine } from "@xstate/react" +import { TemplateEditor } from "components/TemplateVersionEditor/TemplateEditor" +import { TemplateVersionEditor } from "components/TemplateVersionEditor/TemplateVersionEditor" +import { useOrganizationId } from "hooks/useOrganizationId" +import { FC } from "react" +import { Helmet } from "react-helmet-async" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" +import { pageTitle } from "util/page" +import { templateVersionMachine } from "xServices/templateVersion/templateVersionXService" +import { templateVersionEditorMachine } from "xServices/templateVersionEditor/templateVersionEditorXService" + +type Params = { + version: string + template: string +} + +export const TemplateVersionEditorPage: FC = () => { + const { version: versionName, template: templateName } = useParams() as Params + const orgId = useOrganizationId() + const [versionState] = useMachine(templateVersionMachine, { + context: { templateName, versionName, orgId }, + }) + const [editorState, sendEvent] = useMachine(templateVersionEditorMachine, { + context: { orgId } + }) + const { t } = useTranslation("templateVersionPage") + + return ( + <> + + + {pageTitle(`${t("title")} ${versionName} · ${templateName}`)} + + + + {versionState.context.template && versionState.context.currentFiles && versionState.context.currentVersion && ( + { + if (!versionState.context.template) { + throw new Error("no template") + } + sendEvent({ + type: "CREATE_BUILD", + files: files, + templateId: versionState.context.template.id, + }) + }} + resources={editorState.context.resources} + buildLogs={editorState.context.buildLogs} + /> + )} + + ) +} + +export default TemplateVersionEditorPage diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx index 0f2b40d37cf93..ed836d58fd79b 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx @@ -18,7 +18,7 @@ export const TemplateVersionPage: FC = () => { const { version: versionName, template: templateName } = useParams() as Params const orgId = useOrganizationId() const [state] = useMachine(templateVersionMachine, { - context: { versionName, orgId }, + context: { templateName, versionName, orgId }, }) const tab = useTab("file", "0") const { t } = useTranslation("templateVersionPage") diff --git a/site/src/xServices/templateVersion/templateVersionXService.ts b/site/src/xServices/templateVersion/templateVersionXService.ts index 73cd0989bfc09..8655ef9f36c88 100644 --- a/site/src/xServices/templateVersion/templateVersionXService.ts +++ b/site/src/xServices/templateVersion/templateVersionXService.ts @@ -1,9 +1,10 @@ import { getPreviousTemplateVersionByName, GetPreviousTemplateVersionByNameResponse, + getTemplateByName, getTemplateVersionByName, } from "api/api" -import { TemplateVersion } from "api/typesGenerated" +import { Template, TemplateVersion } from "api/typesGenerated" import { getTemplateVersionFiles, TemplateVersionFiles, @@ -13,6 +14,8 @@ import { assign, createMachine } from "xstate" export interface TemplateVersionMachineContext { orgId: string versionName: string + templateName: string + template?: Template currentVersion?: TemplateVersion currentFiles?: TemplateVersionFiles error?: Error | unknown @@ -34,6 +37,11 @@ export const templateVersionMachine = createMachine( previousVersion: GetPreviousTemplateVersionByNameResponse } } + loadTemplate: { + data: { + template: Template + }, + }, loadFiles: { data: { currentFiles: TemplateVersionFiles @@ -43,20 +51,53 @@ export const templateVersionMachine = createMachine( }, }, tsTypes: {} as import("./templateVersionXService.typegen").Typegen0, - initial: "loadingVersions", + initial: "initialInfo", states: { - loadingVersions: { - invoke: { - src: "loadVersions", - onDone: { - target: "loadingFiles", - actions: ["assignVersions"], + initialInfo: { + type: "parallel", + states: { + versions: { + initial: "loadingVersions", + states: { + loadingVersions: { + invoke: { + src: "loadVersions", + onDone: [ + { + actions: "assignVersions", + target: "success", + } + ], + }, + }, + success: { + type: "final", + }, + }, }, - onError: { - target: "done.error", - actions: ["assignError"], + template: { + initial: "loadingTemplate", + states: { + loadingTemplate: { + invoke: { + src: "loadTemplate", + onDone: [ + { + actions: "assignTemplate", + target: "success", + } + ], + }, + }, + success: { + type: "final", + }, + }, }, }, + onDone: { + target: "loadingFiles", + }, }, loadingFiles: { invoke: { @@ -84,6 +125,9 @@ export const templateVersionMachine = createMachine( assignError: assign({ error: (_, { data }) => data, }), + assignTemplate: assign({ + template: (_, { data }) => data.template, + }), assignVersions: assign({ currentVersion: (_, { data }) => data.currentVersion, previousVersion: (_, { data }) => data.previousVersion, @@ -105,6 +149,13 @@ export const templateVersionMachine = createMachine( previousVersion, } }, + loadTemplate: async ({ orgId, templateName }) => { + const template = await getTemplateByName(orgId, templateName) + + return { + template, + } + }, loadFiles: async ({ currentVersion, previousVersion }) => { if (!currentVersion) { throw new Error("Version is not defined") diff --git a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts new file mode 100644 index 0000000000000..451c9e79e177e --- /dev/null +++ b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts @@ -0,0 +1,205 @@ +import { + ProvisionerJobLog, + TemplateVersion, + UploadResponse, + WorkspaceResource, +} from "api/typesGenerated" +import { assign, createMachine } from "xstate" +import * as API from "api/api" +import { TemplateVersionFiles } from "util/templateVersion" +import * as Tar from "tar-js" + +export interface CreateVersionData { + file: File +} + +export interface TemplateVersionEditorMachineContext { + orgId: string + + templateId?: string + files?: TemplateVersionFiles + uploadResponse?: UploadResponse + version?: TemplateVersion + resources?: WorkspaceResource[] + buildLogs?: ProvisionerJobLog[] +} + +export const templateVersionEditorMachine = createMachine( + { + id: "templateVersionEditor", + schema: { + context: {} as TemplateVersionEditorMachineContext, + events: {} as + | { type: "CREATE_BUILD"; files: TemplateVersionFiles, templateId: string } + | { type: "CANCEL_BUILD" } + | { type: "ADD_BUILD_LOG"; log: ProvisionerJobLog }, + services: {} as { + createBuild: { + data: TemplateVersion + } + cancelBuild: { + data: TemplateVersion + } + }, + }, + initial: "idle", + states: { + idle: { + on: { + CREATE_BUILD: { + actions: ["assignCreateBuild"], + target: "uploadTar", + }, + }, + }, + uploadTar: { + invoke: { + id: "uploadTar", + src: "uploadTar", + onDone: { + target: "creatingBuild", + actions: ["assignUploadResponse"], + }, + }, + }, + creatingBuild: { + invoke: { + id: "createBuild", + src: "createBuild", + onDone: { + actions: ["assignBuild"], + target: "watchingBuildLogs", + }, + }, + }, + watchingBuildLogs: { + invoke: { + id: "watchBuildLogs", + src: "watchBuildLogs", + onDone: { + target: "fetchingVersion", + }, + }, + on: { + ADD_BUILD_LOG: { + actions: ["addBuildLog"], + }, + CANCEL_BUILD: { + actions: ["cancelBuild"], + target: "idle", + }, + }, + }, + fetchingVersion: { + invoke: { + id: "fetchVersion", + src: "fetchVersion", + onDone: { + actions: ["assignBuild"], + target: "fetchResources", + }, + }, + }, + fetchResources: { + invoke: { + id: "getResources", + src: "getResources", + onDone: { + actions: ["assignResources"], + target: "idle", + }, + }, + }, + }, + }, + { + actions: { + assignCreateBuild: assign({ + files: (_, event) => event.files, + templateId: (_, event) => event.templateId, + buildLogs: [], + resources: [], + }), + assignResources: assign({ + resources: (_, event) => event.data, + }), + assignUploadResponse: assign({ + uploadResponse: (_, event) => event.data, + }), + assignBuild: assign({ + version: (_, event) => event.data, + }), + addBuildLog: assign({ + buildLogs: (context, event) => { + const previousLogs = context.buildLogs ?? [] + return [...previousLogs, event.log] + }, + }), + }, + services: { + uploadTar: (ctx) => { + if (!ctx.files) { + 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) + }) + return API.uploadTemplateFile(new File([out], "template.tar")) + }, + createBuild: (ctx) => { + if (!ctx.uploadResponse) { + throw new Error("no upload response") + } + return API.createTemplateVersion(ctx.orgId, { + provisioner: "terraform", + storage_method: "file", + tags: {}, + template_id: ctx.templateId, + file_id: ctx.uploadResponse.hash, + }) + }, + fetchVersion: (ctx) => { + if (!ctx.version) { + throw new Error("template version must be set") + } + return API.getTemplateVersion(ctx.version.id) + }, + watchBuildLogs: (ctx) => async (callback) => { + return new Promise((resolve, reject) => { + if (!ctx.version) { + return reject("version must be set") + } + const proto = location.protocol === "https:" ? "wss:" : "ws:" + const socket = new WebSocket( + `${proto}//${location.host}/api/v2/templateversions/${ctx.version?.id}/logs?follow=true`, + ) + socket.binaryType = "blob" + socket.addEventListener("message", (event) => { + callback({ type: "ADD_BUILD_LOG", log: JSON.parse(event.data) }) + }) + socket.addEventListener("error", () => { + reject(new Error("socket errored")) + }) + socket.addEventListener("close", () => { + // When the socket closes, logs have finished streaming! + resolve() + }) + }) + }, + getResources: (ctx) => { + if (!ctx.version) { + throw new Error("template version must be set") + } + return API.getTemplateVersionResources(ctx.version.id) + }, + cancelBuild: (ctx) => { + if (!ctx.version) { + throw new Error("template version must be set") + } + return API.cancelTemplateVersionBuild(ctx.version.id) + }, + }, + }, +) diff --git a/site/yarn.lock b/site/yarn.lock index 225353cb8750a..e2175c50c09b5 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -11706,6 +11706,11 @@ react-fast-compare@^3.2.0: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== +react-headless-tabs@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/react-headless-tabs/-/react-headless-tabs-6.0.3.tgz#c10c899befdaa4f3fb3a16422810254ecc8e668d" + integrity sha512-VBkN/P1Mpc/4WPcbl0Is+LvVzlqjWxX3G6kOxGQZX63zKaVpU0WP5NBS43JqqgZTG7nJ3JMYTHVXe79uTmRg7w== + react-helmet-async@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.3.0.tgz#7bd5bf8c5c69ea9f02f6083f14ce33ef545c222e" @@ -13094,6 +13099,11 @@ tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tar-js@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/tar-js/-/tar-js-0.3.0.tgz#6949aabfb0ba18bb1562ae51a439fd0f30183a17" + integrity sha512-9uqP2hJUZNKRkwPDe5nXxXdzo6w+BFBPq9x/tyi5/U/DneuSesO/HMb0y5TeWpfcv49YDJTs7SrrZeeu8ZHWDA== + tar@^6.0.2, tar@^6.1.11: version "6.1.12" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.12.tgz#3b742fb05669b55671fb769ab67a7791ea1a62e6" From 6986531547c97956e951d77981e4292162be624a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 1 Feb 2023 16:46:52 +0000 Subject: [PATCH 02/21] Fix editor file being reset onChange --- .../TemplateVersionEditor/MonacoEditor.tsx | 13 +++++++++---- .../TemplateVersionEditor/TemplateVersionEditor.tsx | 12 +++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/MonacoEditor.tsx b/site/src/components/TemplateVersionEditor/MonacoEditor.tsx index 52fec4d7df747..014c659b31bb6 100644 --- a/site/src/components/TemplateVersionEditor/MonacoEditor.tsx +++ b/site/src/components/TemplateVersionEditor/MonacoEditor.tsx @@ -1,17 +1,18 @@ import { useTheme } from "@material-ui/core/styles" import Editor from "@monaco-editor/react" -import { FC, useEffect, useState } from "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 }) => { +}> = ({ onChange, value, language, path }) => { const theme = useTheme() const [editor, setEditor] = useState() - useEffect(() => { + useLayoutEffect(() => { if (!editor) { return } @@ -26,7 +27,7 @@ export const MonacoEditor: FC<{ return ( { + console.log("onChange", newValue?.substring(0, 30), path) if (onChange && newValue) { onChange(newValue) } }} onMount={(editor, monaco) => { + console.log("editor mounted", editor) + setEditor(editor) document.fonts.ready diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 93e10da4cbb78..39a6f4e860b23 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -5,7 +5,7 @@ import { Avatar } from "components/Avatar/Avatar" import { AvatarData } from "components/AvatarData/AvatarData" import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable" import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs" -import { FC, useEffect, useState } from "react" +import { FC, useEffect, useMemo, useState } from "react" import { navHeight } from "theme/constants" import { TemplateVersionFiles } from "util/templateVersion" import { FileTree } from "./FileTree" @@ -83,6 +83,9 @@ export const TemplateVersionEditor: FC = ({ ) } /> +
+ Used By: {template.active_user_count} developers +
@@ -121,10 +124,13 @@ export const TemplateVersionEditor: FC = ({
- { + { + if (!activeFile) { + return + } setFiles({ ...files, - [activeFile?.path || ""]: value, + [activeFile.path]: value, }) }} />
From b149c08aaa70a7b2e9a3f32717d33f52e91cfa78 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 1 Feb 2023 17:33:48 +0000 Subject: [PATCH 03/21] Add updating the active build version --- site/package.json | 1 + site/src/api/api.ts | 11 ++ .../TemplateVersionEditor/MonacoEditor.tsx | 9 +- .../TemplateVersionEditor.tsx | 151 ++++++++++++------ .../TemplateVersionEditorPage.tsx | 8 +- .../templateVersionEditorXService.ts | 31 +++- site/yarn.lock | 5 + 7 files changed, 163 insertions(+), 53 deletions(-) diff --git a/site/package.json b/site/package.json index 6f937b56f9b65..e4ff3aabfadf4 100644 --- a/site/package.json +++ b/site/package.json @@ -127,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", diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 5d9ad1eb47c01..947ee0a18b40d 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -299,6 +299,17 @@ export const createTemplate = async ( return response.data } +export const updateActiveTemplateVersion = async ( + templateId: string, + data: TypesGen.UpdateActiveTemplateVersion, +): Promise => { + const response = await axios.patch( + `/api/v2/templates/${templateId}/versions`, + data, + ) + return response.data +} + export const updateTemplateMeta = async ( templateId: string, data: TypesGen.UpdateTemplateMeta, diff --git a/site/src/components/TemplateVersionEditor/MonacoEditor.tsx b/site/src/components/TemplateVersionEditor/MonacoEditor.tsx index 014c659b31bb6..c96cfeb9f0ea1 100644 --- a/site/src/components/TemplateVersionEditor/MonacoEditor.tsx +++ b/site/src/components/TemplateVersionEditor/MonacoEditor.tsx @@ -16,6 +16,8 @@ export const MonacoEditor: FC<{ if (!editor) { return } + console.log("Editor", editor) + ;(window as any).editor = editor const resizeListener = () => { editor.layout({}) } @@ -33,18 +35,19 @@ export const MonacoEditor: FC<{ options={{ automaticLayout: true, fontFamily: MONOSPACE_FONT_FAMILY, - fontSize: 16, + fontSize: 14, wordWrap: "on", }} path={path} onChange={(newValue) => { - console.log("onChange", newValue?.substring(0, 30), path) if (onChange && newValue) { onChange(newValue) } }} onMount={(editor, monaco) => { - console.log("editor mounted", editor) + ;(editor as any)._standaloneKeybindingService.addDynamicKeybinding(`-editor.action.insertLineAfter`, undefined, () => { + // + }) setEditor(editor) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 39a6f4e860b23..5abe489da8b70 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -1,11 +1,16 @@ import Button from "@material-ui/core/Button" import { makeStyles } from "@material-ui/core/styles" -import { ProvisionerJobLog, Template, TemplateVersion, WorkspaceResource } from "api/typesGenerated" +import { + ProvisionerJobLog, + Template, + TemplateVersion, + WorkspaceResource, +} from "api/typesGenerated" import { Avatar } from "components/Avatar/Avatar" import { AvatarData } from "components/AvatarData/AvatarData" import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable" import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs" -import { FC, useEffect, useMemo, useState } from "react" +import { FC, useEffect, useState } from "react" import { navHeight } from "theme/constants" import { TemplateVersionFiles } from "util/templateVersion" import { FileTree } from "./FileTree" @@ -25,7 +30,8 @@ export interface TemplateVersionEditorProps { buildLogs?: ProvisionerJobLog[] resources?: WorkspaceResource[] - onBuild: (files: TemplateVersionFiles) => void + onPreview: (files: TemplateVersionFiles) => void + onUpdate: () => void } const topbarHeight = 90 @@ -34,7 +40,8 @@ export const TemplateVersionEditor: FC = ({ template, templateVersion, initialFiles, - onBuild, + onPreview, + onUpdate, buildLogs, resources, }) => { @@ -53,20 +60,26 @@ export const TemplateVersionEditor: FC = ({ } }) useEffect(() => { - const saveListener = (event: KeyboardEvent) => { - if ( - (navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey) && - event.key === "s" - ) { - // Prevent opening the save dialog! - event.preventDefault() + const keyListener = (event: KeyboardEvent) => { + if (!(navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey)) { + return + } + switch (event.key) { + case "s": + // Prevent opening the save dialog! + event.preventDefault() + break + case "Enter": + event.preventDefault() + onPreview(files) + break } } - document.addEventListener("keydown", saveListener) + document.addEventListener("keydown", keyListener) return () => { - document.removeEventListener("keydown", saveListener) + document.removeEventListener("keydown", keyListener) } - }, []) + }, [files, onPreview]) const hasIcon = template.icon && template.icon !== "" return ( @@ -83,9 +96,7 @@ export const TemplateVersionEditor: FC = ({ ) } /> -
- Used By: {template.active_user_count} developers -
+
Used By: {template.active_user_count} developers
@@ -94,20 +105,21 @@ export const TemplateVersionEditor: FC = ({ variant="outlined" color="primary" onClick={() => { - onBuild(files) + onPreview(files) }} > - Preview + Preview (Ctrl + Enter)
@@ -123,33 +135,52 @@ export const TemplateVersionEditor: FC = ({
-
- { - if (!activeFile) { - return - } - setFiles({ - ...files, - [activeFile.path]: value, - }) - }} /> -
+
+ { + if (!activeFile) { + return + } + setFiles({ + ...files, + [activeFile.path]: value, + }) + }} + /> +
-
-
- {buildLogs && } - {templateVersion.job.error && ( -
- {templateVersion.job.error} +
+
+
Build Logs
+ +
+ {buildLogs && } + {templateVersion.job.error && ( +
+ {templateVersion.job.error} +
+ )} +
+
+ +
+ +
+
Resources
+
+ {resources && ( + r.workspace_transition === "start", + )} + /> + )} +
- )} -
-
- {resources && }
-
-
) @@ -170,6 +201,10 @@ const useStyles = makeStyles((theme) => ({ justifyContent: "space-between", height: topbarHeight, }, + panelDivider: { + height: 1, + background: theme.palette.divider, + }, sidebarAndEditor: { display: "flex", flex: 1, @@ -193,22 +228,42 @@ const useStyles = makeStyles((theme) => ({ editor: { flex: 1, }, + panelWrapper: { + flex: 1, + display: "flex", + flexDirection: "column", + }, panel: { + height: "50%", flex: 1, display: "flex", flexDirection: "column", + borderLeft: `1px solid ${theme.palette.divider}`, + }, + tabBar: { + padding: "8px 16px", + position: "sticky", + top: 0, + background: theme.palette.background.default, + borderBottom: `1px solid ${theme.palette.divider}`, + color: theme.palette.text.hint, + textTransform: "uppercase", + fontSize: 12, + + "&.top": { + borderTop: `1px solid ${theme.palette.divider}`, + }, }, buildLogs: { - height: "50%", display: "flex", flexDirection: "column-reverse", + padding: 16, overflowY: "auto", }, buildLogError: { whiteSpace: "pre-wrap", }, resources: { - height: "50%", - overflowY: "auto", + padding: 16, }, })) diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index ed7346d0c6127..2e739e58877ad 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -39,7 +39,7 @@ export const TemplateVersionEditorPage: FC = () => { template={versionState.context.template} templateVersion={editorState.context.version || versionState.context.currentVersion} initialFiles={versionState.context.currentFiles} - onBuild={(files) => { + onPreview={(files) => { if (!versionState.context.template) { throw new Error("no template") } @@ -49,6 +49,12 @@ export const TemplateVersionEditorPage: FC = () => { templateId: versionState.context.template.id, }) }} + onUpdate={() => { + sendEvent({ + type: "UPDATE_ACTIVE", + }) + console.log("We made a new version active") + }} resources={editorState.context.resources} buildLogs={editorState.context.buildLogs} /> diff --git a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts index 451c9e79e177e..3deca07614b5f 100644 --- a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts +++ b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts @@ -26,13 +26,15 @@ export interface TemplateVersionEditorMachineContext { export const templateVersionEditorMachine = createMachine( { + predictableActionArguments: true, id: "templateVersionEditor", schema: { context: {} as TemplateVersionEditorMachineContext, events: {} as | { type: "CREATE_BUILD"; files: TemplateVersionFiles, templateId: string } | { type: "CANCEL_BUILD" } - | { type: "ADD_BUILD_LOG"; log: ProvisionerJobLog }, + | { type: "ADD_BUILD_LOG"; log: ProvisionerJobLog } + | { type: "UPDATE_ACTIVE" }, services: {} as { createBuild: { data: TemplateVersion @@ -50,6 +52,18 @@ export const templateVersionEditorMachine = createMachine( actions: ["assignCreateBuild"], target: "uploadTar", }, + UPDATE_ACTIVE: { + target: "updatingActiveVersion", + }, + }, + }, + updatingActiveVersion: { + invoke: { + id: "updateActiveVersion", + src: "updateActiveVersion", + onDone: { + target: "idle", + }, }, }, uploadTar: { @@ -88,6 +102,10 @@ export const templateVersionEditorMachine = createMachine( actions: ["cancelBuild"], target: "idle", }, + CREATE_BUILD: { + actions: ["cancelBuild", "assignCreateBuild"], + target: "uploadTar", + }, }, }, fetchingVersion: { @@ -200,6 +218,17 @@ export const templateVersionEditorMachine = createMachine( } return API.cancelTemplateVersionBuild(ctx.version.id) }, + updateActiveVersion: (ctx) => { + if (!ctx.templateId) { + throw new Error("template must be set") + } + if (!ctx.version) { + throw new Error("template version must be set") + } + return API.updateActiveTemplateVersion(ctx.templateId, { + id: ctx.version.id, + }) + }, }, }, ) diff --git a/site/yarn.lock b/site/yarn.lock index e2175c50c09b5..c38258b61b21c 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -10408,6 +10408,11 @@ mock-socket@^9.1.0: resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-9.1.5.tgz#2c4e44922ad556843b6dfe09d14ed8041fa2cdeb" integrity sha512-3DeNIcsQixWHHKk6NdoBhWI4t1VMj5/HzfnI1rE/pLl5qKx7+gd4DNA07ehTaZ6MoUU053si6Hd+YtiM/tQZfg== +monaco-editor@^0.34.1: + version "0.34.1" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.34.1.tgz#1b75c4ad6bc4c1f9da656d740d98e0b850a22f87" + integrity sha512-FKc80TyiMaruhJKKPz5SpJPIjL+dflGvz4CpuThaPMc94AyN7SeC9HQ8hrvaxX7EyHdJcUY5i4D0gNyJj1vSZQ== + moo-color@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.3.tgz#d56435f8359c8284d83ac58016df7427febece74" From a78837bdf8c4c23fa98c9e47c4ae0210a67f4c90 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 1 Feb 2023 17:39:19 +0000 Subject: [PATCH 04/21] Update nav height --- .../TemplateVersionEditor/TemplateVersionEditor.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 5abe489da8b70..f5173481d665b 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -34,7 +34,7 @@ export interface TemplateVersionEditorProps { onUpdate: () => void } -const topbarHeight = 90 +const topbarHeight = navHeight export const TemplateVersionEditor: FC = ({ template, @@ -85,7 +85,7 @@ export const TemplateVersionEditor: FC = ({ return (
-
+
Edit Template = ({
Used By: {template.active_user_count} developers
-
+
- + +
@@ -152,41 +179,53 @@ export const TemplateVersionEditor: FC = ({
-
-
Build Logs
+ { + setSelectedTab(value) + }} + > + + + -
- {buildLogs && } - {templateVersion.job.error && ( -
- {templateVersion.job.error} -
- )} -
+
+ {buildLogs && } + {templateVersion.job.error && ( +
+ {templateVersion.job.error} +
+ )}
-
- -
-
Resources
-
- {resources && ( - r.workspace_transition === "start", - )} - /> - )} -
+
+ {resources && ( + r.workspace_transition === "start", + )} + /> + )}
+ + {templateVersionSucceeded && ( + <> +
+ + )}
) } -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles< + Theme, + { + templateVersionSucceeded: boolean + } +>((theme) => ({ root: { height: `calc(100vh - ${navHeight}px)`, background: theme.palette.background.default, @@ -207,10 +246,6 @@ const useStyles = makeStyles((theme) => ({ alignItems: "center", gap: 16, }, - panelDivider: { - height: 1, - background: theme.palette.divider, - }, sidebarAndEditor: { display: "flex", flex: 1, @@ -238,13 +273,13 @@ const useStyles = makeStyles((theme) => ({ flex: 1, display: "flex", flexDirection: "column", + borderLeft: `1px solid ${theme.palette.divider}`, + overflowY: "auto", }, panel: { - height: "50%", - flex: 1, - display: "flex", - flexDirection: "column", - borderLeft: `1px solid ${theme.palette.divider}`, + "&.hidden": { + display: "none", + }, }, tabBar: { padding: "8px 16px", diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index 2e739e58877ad..f2061b9dd29d3 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -22,7 +22,7 @@ export const TemplateVersionEditorPage: FC = () => { context: { templateName, versionName, orgId }, }) const [editorState, sendEvent] = useMachine(templateVersionEditorMachine, { - context: { orgId } + context: { orgId }, }) const { t } = useTranslation("templateVersionPage") @@ -34,31 +34,39 @@ export const TemplateVersionEditorPage: FC = () => { - {versionState.context.template && versionState.context.currentFiles && versionState.context.currentVersion && ( - { - if (!versionState.context.template) { - throw new Error("no template") + {versionState.context.template && + versionState.context.currentFiles && + versionState.context.currentVersion && ( + { - sendEvent({ - type: "UPDATE_ACTIVE", - }) - console.log("We made a new version active") - }} - resources={editorState.context.resources} - buildLogs={editorState.context.buildLogs} - /> - )} + initialFiles={versionState.context.currentFiles} + onPreview={(files) => { + if (!versionState.context.template) { + throw new Error("no template") + } + sendEvent({ + type: "CREATE_BUILD", + files: files, + templateId: versionState.context.template.id, + }) + }} + onUpdate={() => { + sendEvent({ + type: "UPDATE_ACTIVE", + }) + }} + 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/xServices/templateVersionEditor/templateVersionEditorXService.ts b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts index 3deca07614b5f..3f81a91e978f1 100644 --- a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts +++ b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts @@ -58,6 +58,7 @@ export const templateVersionEditorMachine = createMachine( }, }, updatingActiveVersion: { + tags: "loading", invoke: { id: "updateActiveVersion", src: "updateActiveVersion", @@ -67,6 +68,7 @@ export const templateVersionEditorMachine = createMachine( }, }, uploadTar: { + tags: "loading", invoke: { id: "uploadTar", src: "uploadTar", @@ -77,6 +79,7 @@ export const templateVersionEditorMachine = createMachine( }, }, creatingBuild: { + tags: "loading", invoke: { id: "createBuild", src: "createBuild", @@ -87,6 +90,7 @@ export const templateVersionEditorMachine = createMachine( }, }, watchingBuildLogs: { + tags: "loading", invoke: { id: "watchBuildLogs", src: "watchBuildLogs", @@ -109,6 +113,7 @@ export const templateVersionEditorMachine = createMachine( }, }, fetchingVersion: { + tags: "loading", invoke: { id: "fetchVersion", src: "fetchVersion", @@ -119,6 +124,7 @@ export const templateVersionEditorMachine = createMachine( }, }, fetchResources: { + tags: "loading", invoke: { id: "getResources", src: "getResources", From 191e2f09c2187e2508eb543adc1a9a9544be65df Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 1 Feb 2023 19:10:38 +0000 Subject: [PATCH 06/21] Fix title --- .../TemplateVersionEditor/MonacoEditor.tsx | 13 +++++++++---- .../TemplateVersionEditor/TemplateVersionEditor.tsx | 5 +---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/MonacoEditor.tsx b/site/src/components/TemplateVersionEditor/MonacoEditor.tsx index c96cfeb9f0ea1..2e84ea5c85b9f 100644 --- a/site/src/components/TemplateVersionEditor/MonacoEditor.tsx +++ b/site/src/components/TemplateVersionEditor/MonacoEditor.tsx @@ -3,6 +3,7 @@ import Editor from "@monaco-editor/react" import { FC, useLayoutEffect, useState } from "react" import { MONOSPACE_FONT_FAMILY } from "theme/constants" import { hslToHex } from "util/colors" +import type { editor } from "monaco-editor" export const MonacoEditor: FC<{ value?: string @@ -11,15 +12,16 @@ export const MonacoEditor: FC<{ onChange?: (value: string) => void }> = ({ onChange, value, language, path }) => { const theme = useTheme() - const [editor, setEditor] = useState() + const [editor, setEditor] = useState() useLayoutEffect(() => { if (!editor) { return } - console.log("Editor", editor) - ;(window as any).editor = editor const resizeListener = () => { - editor.layout({}) + editor.layout({ + height: 0, + width: 0, + }) } window.addEventListener("resize", resizeListener) return () => { @@ -45,6 +47,9 @@ export const MonacoEditor: FC<{ } }} onMount={(editor, monaco) => { + // This jank allows for Ctrl + Enter to work outside the editor. + // We use this keybind to trigger a build. + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Private type in Monaco! ;(editor as any)._standaloneKeybindingService.addDynamicKeybinding(`-editor.action.insertLineAfter`, undefined, () => { // }) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 95c1c47ec99ab..fe72bccf677c7 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -36,8 +36,6 @@ export interface TemplateVersionEditorProps { disablePreview: boolean disableUpdate: boolean - loading: boolean - onPreview: (files: TemplateVersionFiles) => void onUpdate: () => void } @@ -105,7 +103,6 @@ export const TemplateVersionEditor: FC = ({
- Edit Template = ({
-
File Explorer
+
Template Editor
setActiveFile(file)} From 78e35d64adb230c1c6c43af3815a03b88d0f06bc Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 1 Feb 2023 20:35:56 +0000 Subject: [PATCH 07/21] Hide timestamps in build logs --- site/src/AppRouter.tsx | 10 ++++- site/src/api/api.ts | 2 +- site/src/components/Logs/Logs.tsx | 14 +++++-- .../TemplateVersionEditor/FileTree.tsx | 11 ++++++ .../TemplateVersionEditor/MonacoEditor.tsx | 37 +++++++++++++++---- .../TemplateVersionEditor.stories.tsx | 5 ++- .../TemplateVersionEditor.tsx | 20 ++++++++-- .../WorkspaceBuildLogs/WorkspaceBuildLogs.tsx | 8 +++- .../TemplateVersionEditorPage.tsx | 7 +--- .../templateVersionXService.ts | 8 ++-- .../templateVersionEditorXService.ts | 6 ++- 11 files changed, 96 insertions(+), 32 deletions(-) diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index a4d25f63a6585..f83c8c134eb7d 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -108,7 +108,10 @@ const TemplateVersionPage = lazy( () => import("./pages/TemplateVersionPage/TemplateVersionPage"), ) const TemplateVersionEditorPage = lazy( - () => import("./pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage"), + () => + import( + "./pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage" + ), ) const StarterTemplatesPage = lazy( () => import("./pages/StarterTemplatesPage/StarterTemplatesPage"), @@ -159,7 +162,10 @@ export const AppRouter: FC = () => { } /> - } /> + } + /> diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 947ee0a18b40d..307e6cb652813 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -432,7 +432,7 @@ export const cancelWorkspaceBuild = async ( } export const cancelTemplateVersionBuild = async ( - templateVersionId: TypesGen.TemplateVersion["id"] + templateVersionId: TypesGen.TemplateVersion["id"], ): Promise => { const response = await axios.patch( `/api/v2/templateversions/${templateVersionId}/cancel`, diff --git a/site/src/components/Logs/Logs.tsx b/site/src/components/Logs/Logs.tsx index 6b70dd552c849..b1a101ecb5890 100644 --- a/site/src/components/Logs/Logs.tsx +++ b/site/src/components/Logs/Logs.tsx @@ -13,10 +13,12 @@ interface Line { export interface LogsProps { lines: Line[] + hideTimestamps?: boolean className?: string } export const Logs: FC> = ({ + hideTimestamps, lines, className = "", }) => { @@ -27,10 +29,14 @@ export const Logs: FC> = ({
{lines.map((line, idx) => (
- - {dayjs(line.time).format(`HH:mm:ss.SSS`)} - -      + {!hideTimestamps && ( + <> + + {dayjs(line.time).format(`HH:mm:ss.SSS`)} + +      + + )} {line.output}
))} diff --git a/site/src/components/TemplateVersionEditor/FileTree.tsx b/site/src/components/TemplateVersionEditor/FileTree.tsx index 0d60fc22baacf..0318762d6b3af 100644 --- a/site/src/components/TemplateVersionEditor/FileTree.tsx +++ b/site/src/components/TemplateVersionEditor/FileTree.tsx @@ -64,6 +64,9 @@ export const FileTree: FC<{ if (file.path.endsWith(".md")) { icon = } + if (file.path.endsWith("Dockerfile")) { + icon = + } return ( ( ) + +const FileTypeDockerfile = () => ( + + + + + +) diff --git a/site/src/components/TemplateVersionEditor/MonacoEditor.tsx b/site/src/components/TemplateVersionEditor/MonacoEditor.tsx index 2e84ea5c85b9f..15b7b4b6b50f6 100644 --- a/site/src/components/TemplateVersionEditor/MonacoEditor.tsx +++ b/site/src/components/TemplateVersionEditor/MonacoEditor.tsx @@ -1,6 +1,6 @@ import { useTheme } from "@material-ui/core/styles" import Editor from "@monaco-editor/react" -import { FC, useLayoutEffect, useState } from "react" +import { FC, useLayoutEffect, useMemo, useState } from "react" import { MONOSPACE_FONT_FAMILY } from "theme/constants" import { hslToHex } from "util/colors" import type { editor } from "monaco-editor" @@ -8,9 +8,8 @@ import type { editor } from "monaco-editor" export const MonacoEditor: FC<{ value?: string path?: string - language?: string onChange?: (value: string) => void -}> = ({ onChange, value, language, path }) => { +}> = ({ onChange, value, path }) => { const theme = useTheme() const [editor, setEditor] = useState() useLayoutEffect(() => { @@ -29,15 +28,33 @@ export const MonacoEditor: FC<{ } }, [editor]) + const language = useMemo(() => { + if (path?.endsWith(".tf")) { + return "hcl" + } + if (path?.endsWith(".md")) { + return "markdown" + } + if (path?.endsWith(".json")) { + return "json" + } + if (path?.endsWith(".yaml")) { + return "yaml" + } + if (path?.endsWith("Dockerfile")) { + return "dockerfile" + } + }, [path]) + return ( { - // - }) + ;(editor as any)._standaloneKeybindingService.addDynamicKeybinding( + `-editor.action.insertLineAfter`, + undefined, + () => { + // + }, + ) setEditor(editor) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx index a2bd98f527e4b..ef11cf71b23bd 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx @@ -1,5 +1,8 @@ import { Story } from "@storybook/react" -import { TemplateVersionEditor, TemplateVersionEditorProps } from "./TemplateVersionEditor" +import { + TemplateVersionEditor, + TemplateVersionEditorProps, +} from "./TemplateVersionEditor" export default { title: "components/TemplateEditor", diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index fe72bccf677c7..b4a4a996b78d2 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -181,13 +181,20 @@ export const TemplateVersionEditor: FC = ({ onChange={(_, value) => { setSelectedTab(value) }} + className={styles.tabs} > -
- {buildLogs && } +
+ {buildLogs && ( + + )} {templateVersion.job.error && (
{templateVersion.job.error} @@ -195,7 +202,11 @@ export const TemplateVersionEditor: FC = ({ )}
-
+
{resources && ( { export interface WorkspaceBuildLogsProps { logs: ProvisionerJobLog[] + hideTimestamps?: boolean } -export const WorkspaceBuildLogs: FC = ({ logs }) => { +export const WorkspaceBuildLogs: FC = ({ + hideTimestamps, + logs, +}) => { const groupedLogsByStage = groupLogsByStage(logs) const stages = Object.keys(groupedLogsByStage) const styles = useStyles() @@ -69,7 +73,7 @@ export const WorkspaceBuildLogs: FC = ({ logs }) => {
)}
- {!isEmpty && } + {!isEmpty && } ) })} diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index f2061b9dd29d3..e6426c052fbf6 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -1,10 +1,8 @@ import { useMachine } from "@xstate/react" -import { TemplateEditor } from "components/TemplateVersionEditor/TemplateEditor" import { TemplateVersionEditor } from "components/TemplateVersionEditor/TemplateVersionEditor" import { useOrganizationId } from "hooks/useOrganizationId" import { FC } from "react" import { Helmet } from "react-helmet-async" -import { useTranslation } from "react-i18next" import { useParams } from "react-router-dom" import { pageTitle } from "util/page" import { templateVersionMachine } from "xServices/templateVersion/templateVersionXService" @@ -24,14 +22,11 @@ export const TemplateVersionEditorPage: FC = () => { const [editorState, sendEvent] = useMachine(templateVersionEditorMachine, { context: { orgId }, }) - const { t } = useTranslation("templateVersionPage") return ( <> - - {pageTitle(`${t("title")} ${versionName} · ${templateName}`)} - + {pageTitle(`${templateName} · Template Editor`)} {versionState.context.template && diff --git a/site/src/xServices/templateVersion/templateVersionXService.ts b/site/src/xServices/templateVersion/templateVersionXService.ts index 8655ef9f36c88..338ec48fc9b25 100644 --- a/site/src/xServices/templateVersion/templateVersionXService.ts +++ b/site/src/xServices/templateVersion/templateVersionXService.ts @@ -40,8 +40,8 @@ export const templateVersionMachine = createMachine( loadTemplate: { data: { template: Template - }, - }, + } + } loadFiles: { data: { currentFiles: TemplateVersionFiles @@ -66,7 +66,7 @@ export const templateVersionMachine = createMachine( { actions: "assignVersions", target: "success", - } + }, ], }, }, @@ -85,7 +85,7 @@ export const templateVersionMachine = createMachine( { actions: "assignTemplate", target: "success", - } + }, ], }, }, diff --git a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts index 3f81a91e978f1..7fcd569bef3d9 100644 --- a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts +++ b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts @@ -31,7 +31,11 @@ export const templateVersionEditorMachine = createMachine( schema: { context: {} as TemplateVersionEditorMachineContext, events: {} as - | { type: "CREATE_BUILD"; files: TemplateVersionFiles, templateId: string } + | { + type: "CREATE_BUILD" + files: TemplateVersionFiles + templateId: string + } | { type: "CANCEL_BUILD" } | { type: "ADD_BUILD_LOG"; log: ProvisionerJobLog } | { type: "UPDATE_ACTIVE" }, From 15e0a06c61f595750c6a08a06cedc65dbe2a36bb Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 1 Feb 2023 23:57:07 +0000 Subject: [PATCH 08/21] Add create file dialog --- .../TemplateVersionEditor/FileDialog.tsx | 66 +++++++++++++++++++ .../TemplateVersionEditor.tsx | 64 ++++++++++++++++-- 2 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 site/src/components/TemplateVersionEditor/FileDialog.tsx diff --git a/site/src/components/TemplateVersionEditor/FileDialog.tsx b/site/src/components/TemplateVersionEditor/FileDialog.tsx new file mode 100644 index 0000000000000..5fc03efa18f4e --- /dev/null +++ b/site/src/components/TemplateVersionEditor/FileDialog.tsx @@ -0,0 +1,66 @@ +import TextField from "@material-ui/core/TextField" +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" + +export const CreateFileDialog: FC<{ + onClose: () => void + checkExists: (path: string) => boolean + onConfirm: (path: string) => void + open: boolean +}> = ({ checkExists, onClose, onConfirm, open }) => { + const [pathValue, setPathValue] = useState("") + const [error, setError] = useState("") + const handleChange = (event: ChangeEvent) => { + setPathValue(event.target.value) + } + const handleConfirm = () => { + if (checkExists(pathValue)) { + setError("File already exists") + return + } + onConfirm(pathValue) + setPathValue("") + } + + return ( + { + onClose() + setPathValue("") + }} + onConfirm={handleConfirm} + hideCancel={false} + type="success" + cancelText="Cancel" + confirmText="Create" + title="Create File" + description={ + + + Specify the path to a file to be created. This path can contain slashes too! + + { + if (event.key === "Enter") { + handleConfirm() + } + }} + helperText={error} + name="file-path" + autoComplete="off" + id="file-path" + placeholder="main.tf" + value={pathValue} + onChange={handleChange} + label="File Path" + /> + + + } + /> + ) +} diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index b4a4a996b78d2..575d5d31c021a 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -1,6 +1,10 @@ -import Tooltip from "@material-ui/core/Tooltip" import Button from "@material-ui/core/Button" +import IconButton from "@material-ui/core/IconButton" import { makeStyles, Theme } from "@material-ui/core/styles" +import Tab from "@material-ui/core/Tab" +import Tabs from "@material-ui/core/Tabs" +import Tooltip from "@material-ui/core/Tooltip" +import CreateIcon from "@material-ui/icons/AddBox" import { ProvisionerJobLog, Template, @@ -14,10 +18,9 @@ import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuild import { FC, useCallback, useEffect, useState } from "react" import { navHeight } from "theme/constants" import { TemplateVersionFiles } from "util/templateVersion" +import { CreateFileDialog } from "./FileDialog" import { FileTree } from "./FileTree" import { MonacoEditor } from "./MonacoEditor" -import Tab from "@material-ui/core/Tab" -import Tabs from "@material-ui/core/Tabs" interface File { path: string @@ -55,6 +58,7 @@ export const TemplateVersionEditor: FC = ({ }) => { const [selectedTab, setSelectedTab] = useState(0) const [files, setFiles] = useState(initialFiles) + const [createFileOpen, setCreateFileOpen] = useState(false) const [activeFile, setActiveFile] = useState(() => { const fileKeys = Object.keys(initialFiles) for (let i = 0; i < fileKeys.length; i++) { @@ -150,7 +154,41 @@ export const TemplateVersionEditor: FC = ({
-
Template Editor
+
+ Template Editor + + { + setCreateFileOpen(true) + event.currentTarget.blur() + }} + > + + + + { + setCreateFileOpen(false) + }} + checkExists={(path) => Boolean(files[path])} + onConfirm={(path) => { + setFiles({ + ...files, + [path]: "", + }) + setActiveFile({ + path, + content: "", + children: {}, + }) + setCreateFileOpen(false) + }} + /> +
setActiveFile(file)} @@ -183,8 +221,15 @@ export const TemplateVersionEditor: FC = ({ }} className={styles.tabs} > - - + Build Log {templateVersion.job.status}
} + /> +
Date: Thu, 2 Feb 2023 00:28:03 +0000 Subject: [PATCH 09/21] Add validation for empty path --- site/src/components/TemplateVersionEditor/FileDialog.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/site/src/components/TemplateVersionEditor/FileDialog.tsx b/site/src/components/TemplateVersionEditor/FileDialog.tsx index 5fc03efa18f4e..254ae221ae895 100644 --- a/site/src/components/TemplateVersionEditor/FileDialog.tsx +++ b/site/src/components/TemplateVersionEditor/FileDialog.tsx @@ -16,6 +16,10 @@ export const CreateFileDialog: FC<{ setPathValue(event.target.value) } const handleConfirm = () => { + if (pathValue === "") { + setError("You must enter a path!") + return + } if (checkExists(pathValue)) { setError("File already exists") return From 03abb15ac2ea36b4822ec129a8fdce3becd6f511 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 2 Feb 2023 18:54:58 +0000 Subject: [PATCH 10/21] Hide resources tab --- .../TemplateVersionEditor/TemplateVersionEditor.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 575d5d31c021a..a445de0d90d97 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -225,11 +225,9 @@ export const TemplateVersionEditor: FC = ({ style={{ minWidth: 120 }} label={
Build Log {templateVersion.job.status}
} /> - + {!disableUpdate && ( + + )}
Date: Thu, 2 Feb 2023 19:19:47 +0000 Subject: [PATCH 11/21] Fix label names --- .../TemplateVersionEditor/TemplateVersionEditor.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index a445de0d90d97..b817e2b2c1e17 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -129,7 +129,7 @@ export const TemplateVersionEditor: FC = ({ triggerPreview() }} > - Preview (Ctrl + Enter) + Build (Ctrl + Enter) = ({ onUpdate() }} > - Update Template + Update
@@ -226,7 +226,7 @@ export const TemplateVersionEditor: FC = ({ label={
Build Log {templateVersion.job.status}
} /> {!disableUpdate && ( - + )} From 3f152cdc36dec70b8992f5119909364011130a95 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 5 Feb 2023 18:11:39 +0000 Subject: [PATCH 12/21] Add rename and delete --- .../TemplateVersionEditor/FileDialog.tsx | 119 +++++++++++++++--- .../TemplateVersionEditor/FileTree.tsx | 73 ++++++++++- .../TemplateVersionEditor.tsx | 77 +++++++++--- 3 files changed, 235 insertions(+), 34 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/FileDialog.tsx b/site/src/components/TemplateVersionEditor/FileDialog.tsx index 254ae221ae895..3d7fe0f18cb44 100644 --- a/site/src/components/TemplateVersionEditor/FileDialog.tsx +++ b/site/src/components/TemplateVersionEditor/FileDialog.tsx @@ -44,26 +44,111 @@ export const CreateFileDialog: FC<{ description={ - Specify the path to a file to be created. This path can contain slashes too! + Specify the path to a file to be created. This path can contain + slashes too! - { - if (event.key === "Enter") { - handleConfirm() - } - }} - helperText={error} - name="file-path" - autoComplete="off" - id="file-path" - placeholder="main.tf" - value={pathValue} - onChange={handleChange} - label="File Path" - /> + { + if (event.key === "Enter") { + handleConfirm() + } + }} + helperText={error} + name="file-path" + autoComplete="off" + id="file-path" + placeholder="main.tf" + value={pathValue} + onChange={handleChange} + label="File Path" + /> + } + /> + ) +} + +export const DeleteFileDialog: FC<{ + onClose: () => void + onConfirm: () => void + open: boolean + filename: string +}> = ({ onClose, onConfirm, open, filename }) => { + return ( + + ) +} + +export const RenameFileDialog: FC<{ + onClose: () => void + onConfirm: (filename: string) => void + checkExists: (path: string) => boolean + open: boolean + filename: string +}> = ({ checkExists, onClose, onConfirm, open, filename }) => { + const [pathValue, setPathValue] = useState(filename) + const [error, setError] = useState("") + const handleChange = (event: ChangeEvent) => { + setPathValue(event.target.value) + } + const handleConfirm = () => { + if (pathValue === "") { + setError("You must enter a path!") + return + } + if (checkExists(pathValue)) { + setError("File already exists") + return + } + onConfirm(pathValue) + setPathValue("") + } + return ( + { + onClose() + setPathValue("") + }} + onConfirm={handleConfirm} + hideCancel={false} + type="success" + cancelText="Cancel" + confirmText="Create" + title="Rename File" + description={ + + + Rename {`"${filename}"`} to something else. This path can contain + slashes too! + + { + if (event.key === "Enter") { + handleConfirm() + } + }} + helperText={error} + name="file-path" + autoComplete="off" + id="file-path" + placeholder="main.tf" + defaultValue={filename} + value={pathValue} + onChange={handleChange} + label="File Path" + /> + } /> ) diff --git a/site/src/components/TemplateVersionEditor/FileTree.tsx b/site/src/components/TemplateVersionEditor/FileTree.tsx index 0318762d6b3af..8431739211f21 100644 --- a/site/src/components/TemplateVersionEditor/FileTree.tsx +++ b/site/src/components/TemplateVersionEditor/FileTree.tsx @@ -3,8 +3,9 @@ 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 Menu from "@material-ui/core/Menu" +import MenuItem from "@material-ui/core/MenuItem" +import { FC, useMemo, useState } from "react" import { TemplateVersionFiles } from "util/templateVersion" export interface File { @@ -15,9 +16,11 @@ export interface File { export const FileTree: FC<{ onSelect: (file: File) => void + onDelete: (file: File) => void + onRename: (file: File) => void files: TemplateVersionFiles activeFile?: File -}> = ({ activeFile, files, onSelect }) => { +}> = ({ activeFile, files, onDelete, onRename, onSelect }) => { const styles = useStyles() const fileTree = useMemo>(() => { const paths = Object.keys(files) @@ -55,6 +58,14 @@ export const FileTree: FC<{ }) return roots }, [files]) + const [contextMenu, setContextMenu] = useState< + | { + file: File + clientX: number + clientY: number + } + | undefined + >() const buildTreeItems = (name: string, file: File): JSX.Element => { let icon: JSX.Element | null = null @@ -81,6 +92,21 @@ export const FileTree: FC<{ onSelect(file) } }} + onContextMenu={(event) => { + event.preventDefault() + if (!file.content) { + return + } + setContextMenu( + contextMenu + ? undefined + : { + file: file, + clientY: event.clientY, + clientX: event.clientX, + }, + ) + }} icon={icon} > {Object.entries(file.children || {}).map(([name, file]) => { @@ -100,6 +126,47 @@ export const FileTree: FC<{ {Object.entries(fileTree).map(([name, file]) => { return buildTreeItems(name, file) })} + + setContextMenu(undefined)} + open={Boolean(contextMenu)} + anchorReference="anchorPosition" + anchorPosition={contextMenu ? { + top: contextMenu.clientY, + left: contextMenu.clientX, + } : undefined} + anchorOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + > + { + if (!contextMenu) { + return + } + onRename(contextMenu.file) + setContextMenu(undefined) + }} + > + Rename... + + { + if (!contextMenu) { + return + } + onDelete(contextMenu.file) + setContextMenu(undefined) + }} + > + Delete Permanently + + ) } diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index b817e2b2c1e17..da5043589d317 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -18,7 +18,11 @@ import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuild import { FC, useCallback, useEffect, useState } from "react" import { navHeight } from "theme/constants" import { TemplateVersionFiles } from "util/templateVersion" -import { CreateFileDialog } from "./FileDialog" +import { + CreateFileDialog, + DeleteFileDialog, + RenameFileDialog, +} from "./FileDialog" import { FileTree } from "./FileTree" import { MonacoEditor } from "./MonacoEditor" @@ -59,6 +63,8 @@ export const TemplateVersionEditor: FC = ({ const [selectedTab, setSelectedTab] = useState(0) const [files, setFiles] = 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++) { @@ -188,29 +194,72 @@ export const TemplateVersionEditor: FC = ({ setCreateFileOpen(false) }} /> + { + if (!deleteFileOpen) { + throw new Error("delete file must be set") + } + const deleted = { ...files } + delete deleted[deleteFileOpen.path] + setFiles(deleted) + setDeleteFileOpen(undefined) + if (activeFile?.path === deleteFileOpen.path) { + setActiveFile(undefined) + } + }} + open={Boolean(deleteFileOpen)} + onClose={() => setDeleteFileOpen(undefined)} + filename={deleteFileOpen?.path || ""} + /> + { + setRenameFileOpen(undefined) + }} + filename={renameFileOpen?.path || ""} + checkExists={(path) => Boolean(files[path])} + onConfirm={(newPath) => { + if (!renameFileOpen) { + return + } + const renamed = { ...files } + renamed[newPath] = renamed[renameFileOpen.path] + delete renamed[renameFileOpen.path] + setFiles(renamed) + renameFileOpen.path = newPath + setActiveFile(renameFileOpen) + setRenameFileOpen(undefined) + }} + />
setDeleteFileOpen(file)} onSelect={(file) => setActiveFile(file)} + onRename={(file) => setRenameFileOpen(file)} activeFile={activeFile} />
- { - if (!activeFile) { - return - } - setFiles({ - ...files, - [activeFile.path]: value, - }) - }} - /> + {activeFile ? ( + { + if (!activeFile) { + return + } + setFiles({ + ...files, + [activeFile.path]: value, + }) + }} + /> + ) : ( +
No file opened
+ )}
From a315d7874fc642e991d2dc3cb4566d69f85cf668 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 5 Feb 2023 19:13:39 +0000 Subject: [PATCH 13/21] Improve UX --- .../TemplateLayout/TemplateLayout.tsx | 1 + .../TemplateVersionEditor.tsx | 109 +++++++++++++++--- .../TemplateVersionStatusBadge.tsx | 67 +++++++++++ .../WorkspaceBuildLogs/WorkspaceBuildLogs.tsx | 15 ++- .../TemplateVersionPageView.tsx | 19 ++- .../templateVersionEditorXService.ts | 16 +++ 6 files changed, 201 insertions(+), 26 deletions(-) create mode 100644 site/src/components/TemplateVersionEditor/TemplateVersionStatusBadge.tsx diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index eb8644e242ac3..fa02caff789ac 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -31,6 +31,7 @@ import { Avatar } from "components/Avatar/Avatar" const Language = { settingsButton: "Settings", + editButton: "Edit", createButton: "Create workspace", noDescription: "", } diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index da5043589d317..fdcd697e42de5 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -5,6 +5,8 @@ import Tab from "@material-ui/core/Tab" import Tabs from "@material-ui/core/Tabs" import Tooltip from "@material-ui/core/Tooltip" import CreateIcon from "@material-ui/icons/AddBox" +import BuildIcon from "@material-ui/icons/BuildOutlined" +import PreviewIcon from "@material-ui/icons/Visibility" import { ProvisionerJobLog, Template, @@ -15,7 +17,7 @@ import { Avatar } from "components/Avatar/Avatar" import { AvatarData } from "components/AvatarData/AvatarData" import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable" import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs" -import { FC, useCallback, useEffect, useState } from "react" +import { FC, useCallback, useEffect, useRef, useState } from "react" import { navHeight } from "theme/constants" import { TemplateVersionFiles } from "util/templateVersion" import { @@ -25,6 +27,10 @@ import { } from "./FileDialog" import { FileTree } from "./FileTree" import { MonacoEditor } from "./MonacoEditor" +import { + getStatus, + TemplateVersionStatusBadge, +} from "./TemplateVersionStatusBadge" interface File { path: string @@ -103,6 +109,23 @@ export const TemplateVersionEditor: FC = ({ document.removeEventListener("keydown", keyListener) } }, [files, triggerPreview]) + + // Automatically switch to the template preview tab when the build succeeds. + const previousVersion = useRef() + useEffect(() => { + if (!previousVersion.current) { + previousVersion.current = templateVersion + return + } + if ( + previousVersion.current.job.status === "running" && + templateVersion.job.status === "succeeded" + ) { + setSelectedTab(1) + } + previousVersion.current = templateVersion + }, [templateVersion]) + const hasIcon = template.icon && template.icon !== "" const templateVersionSucceeded = templateVersion.job.status === "succeeded" const styles = useStyles({ @@ -126,6 +149,11 @@ export const TemplateVersionEditor: FC = ({
+
+ Build Status: + +
+
- { - setSelectedTab(value) - }} - className={styles.tabs} - > - Build Log {templateVersion.job.status}
} - /> +
+ + {!disableUpdate && ( - + )} - +
= ({ }`} > {buildLogs && ( - + )} {templateVersion.job.error && (
@@ -360,9 +406,9 @@ const useStyles = makeStyles< color: theme.palette.text.hint, }, editorPane: { - display: "flex", - flexDirection: "row", - flex: 1, + display: "grid", + width: "100%", + gridTemplateColumns: "0.6fr 0.4fr", height: `calc(100vh - ${navHeight + topbarHeight}px)`, overflow: "hidden", }, @@ -383,6 +429,8 @@ const useStyles = makeStyles< }, tabs: { borderBottom: `1px solid ${theme.palette.divider}`, + display: "flex", + boxShadow: "#000000 0 6px 6px -6px inset", "& .MuiTab-root": { padding: 0, @@ -391,6 +439,30 @@ const useStyles = makeStyles< letterSpacing: "unset", }, }, + tab: { + cursor: "pointer", + padding: "12px 24px", + fontSize: 14, + background: "transparent", + fontFamily: "inherit", + border: 0, + color: theme.palette.text.hint, + transition: "150ms ease all", + display: "flex", + gap: 8, + alignItems: "center", + justifyContent: "center", + + "& svg": { + maxWidth: 16, + maxHeight: 16, + }, + + "&.active": { + color: "white", + background: theme.palette.background.paperLight, + }, + }, tabBar: { padding: "8px 16px", position: "sticky", @@ -408,7 +480,6 @@ const useStyles = makeStyles< buildLogs: { display: "flex", flexDirection: "column-reverse", - padding: 16, overflowY: "auto", }, buildLogError: { diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionStatusBadge.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionStatusBadge.tsx new file mode 100644 index 0000000000000..c52e6ca957908 --- /dev/null +++ b/site/src/components/TemplateVersionEditor/TemplateVersionStatusBadge.tsx @@ -0,0 +1,67 @@ +import { TemplateVersion } from "api/typesGenerated" +import { FC, ReactNode } from "react" +import { PaletteIndex } from "theme/palettes" + +import CircularProgress from "@material-ui/core/CircularProgress" +import ErrorIcon from "@material-ui/icons/ErrorOutline" +import CheckIcon from "@material-ui/icons/CheckOutlined" +import { Pill } from "components/Pill/Pill" + +export const TemplateVersionStatusBadge: FC<{ + version: TemplateVersion +}> = ({ version }) => { + const { text, icon, type } = getStatus(version) + return +} + +const LoadingIcon: FC = () => { + return +} + +export const getStatus = ( + version: TemplateVersion, +): { + type?: PaletteIndex + text: string + icon: ReactNode +} => { + console.log("Getting status", version.job.status) + switch (version.job.status) { + case "running": + return { + type: "info", + text: "Running", + icon: , + } + case "pending": + return { + text: "Pending", + icon: , + type: "info", + } + case "canceling": + return { + type: "warning", + text: "Canceling", + icon: , + } + case "canceled": + return { + type: "warning", + text: "Canceled", + icon: , + } + case "failed": + return { + type: "error", + text: "Failed", + icon: , + } + case "succeeded": + return { + type: "success", + text: "Success", + icon: , + } + } +} diff --git a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx index 3438c6bdd0aa2..03736ed51d960 100644 --- a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx +++ b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx @@ -1,4 +1,4 @@ -import { makeStyles } from "@material-ui/core/styles" +import { makeStyles, Theme } from "@material-ui/core/styles" import dayjs from "dayjs" import { FC, Fragment } from "react" import { ProvisionerJobLog } from "../../api/typesGenerated" @@ -40,15 +40,20 @@ const getStageDurationInSeconds = (logs: ProvisionerJobLog[]) => { export interface WorkspaceBuildLogsProps { logs: ProvisionerJobLog[] hideTimestamps?: boolean + + // If true, render different styles that fit the template editor pane + // a bit better. + templateEditorPane?: boolean } export const WorkspaceBuildLogs: FC = ({ hideTimestamps, logs, + templateEditorPane, }) => { const groupedLogsByStage = groupLogsByStage(logs) const stages = Object.keys(groupedLogsByStage) - const styles = useStyles() + const styles = useStyles({ templateEditorPane }) return (
@@ -81,10 +86,12 @@ export const WorkspaceBuildLogs: FC = ({ ) } -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles((theme) => ({ logs: { border: `1px solid ${theme.palette.divider}`, - borderRadius: theme.shape.borderRadius, + borderRadius: (props) => props.templateEditorPane ? "0px" : theme.shape.borderRadius, fontFamily: MONOSPACE_FONT_FAMILY, }, diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx index 144f24d0e557f..1ad6e18691319 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx @@ -4,6 +4,7 @@ import { MarkdownIcon } from "components/Icons/MarkdownIcon" import { TerraformIcon } from "components/Icons/TerraformIcon" import { Loader } from "components/Loader/Loader" import { Margins } from "components/Margins/Margins" +import EditIcon from "@material-ui/icons/Edit" import { PageHeader, PageHeaderCaption, @@ -13,14 +14,16 @@ import { Stack } from "components/Stack/Stack" import { Stats, StatsItem } from "components/Stats/Stats" import { UseTabResult } from "hooks/useTab" import { FC } from "react" +import { Link as RouterLink } from "react-router-dom" import { useTranslation } from "react-i18next" -import { Link } from "react-router-dom" import { combineClasses } from "util/combineClasses" import { createDayString } from "util/createDayString" import { TemplateVersionMachineContext } from "xServices/templateVersion/templateVersionXService" import { TemplateVersionFiles } from "util/templateVersion" import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter" import { DockerIcon } from "components/Icons/DockerIcon" +import Button from "@material-ui/core/Button" +import Link from "@material-ui/core/Link" const iconByExtension: Record = { tf: , @@ -116,7 +119,17 @@ export const TemplateVersionPageView: FC = ({ return ( - + + + + } + > {t("header.caption")} {versionName} @@ -131,7 +144,7 @@ export const TemplateVersionPageView: FC = ({ {templateName} + {templateName} } /> { + if (!context.version || context.buildLogs?.length !== 0) { + return context.version + } + return { + ...context.version, + job: { + ...context.version.job, + status: "running", + }, + } + } }), }, services: { From aad213bf9c0d0a2f4dc2e1c2cec929c9d388037a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 5 Feb 2023 19:23:05 +0000 Subject: [PATCH 14/21] Add padding to the editor --- site/src/components/TemplateVersionEditor/MonacoEditor.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/site/src/components/TemplateVersionEditor/MonacoEditor.tsx b/site/src/components/TemplateVersionEditor/MonacoEditor.tsx index 15b7b4b6b50f6..405a71885c24c 100644 --- a/site/src/components/TemplateVersionEditor/MonacoEditor.tsx +++ b/site/src/components/TemplateVersionEditor/MonacoEditor.tsx @@ -56,6 +56,10 @@ export const MonacoEditor: FC<{ fontFamily: MONOSPACE_FONT_FAMILY, fontSize: 16, wordWrap: "on", + padding: { + top: 16, + bottom: 16, + }, }} path={path} onChange={(newValue) => { From 97e549673e20606f8d830d737956d4104d5b3769 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sun, 5 Feb 2023 21:21:27 +0000 Subject: [PATCH 15/21] Add dirty state --- .../TemplateVersionEditor.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index fdcd697e42de5..8fec2b497bb68 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -122,10 +122,12 @@ export const TemplateVersionEditor: FC = ({ templateVersion.job.status === "succeeded" ) { setSelectedTab(1) + setDirty(false) } previousVersion.current = templateVersion }, [templateVersion]) + const [dirty, setDirty] = useState(false) const hasIcon = template.icon && template.icon !== "" const templateVersionSucceeded = templateVersion.job.status === "succeeded" const styles = useStyles({ @@ -168,20 +170,23 @@ export const TemplateVersionEditor: FC = ({ + +
@@ -220,6 +225,7 @@ export const TemplateVersionEditor: FC = ({ children: {}, }) setCreateFileOpen(false) + setDirty(true) }} /> = ({ if (activeFile?.path === deleteFileOpen.path) { setActiveFile(undefined) } + setDirty(true) }} open={Boolean(deleteFileOpen)} onClose={() => setDeleteFileOpen(undefined)} @@ -257,6 +264,7 @@ export const TemplateVersionEditor: FC = ({ renameFileOpen.path = newPath setActiveFile(renameFileOpen) setRenameFileOpen(undefined) + setDirty(true) }} />
@@ -283,6 +291,7 @@ export const TemplateVersionEditor: FC = ({ ...files, [activeFile.path]: value, }) + setDirty(true) }} /> ) : ( @@ -441,7 +450,7 @@ const useStyles = makeStyles< }, tab: { cursor: "pointer", - padding: "12px 24px", + padding: "8px 12px", fontSize: 14, background: "transparent", fontFamily: "inherit", From 65c875711a54767691af6c737566f0551cd17491 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 6 Feb 2023 19:21:52 +0000 Subject: [PATCH 16/21] Hide build logs until a build is made --- .../TemplateVersionEditor/FileTree.tsx | 29 ++++--- .../TemplateVersionEditor.tsx | 84 ++++++++++++------- .../WorkspaceBuildLogs/WorkspaceBuildLogs.tsx | 12 ++- .../TemplateVersionEditorPage.tsx | 8 +- .../TemplateVersionPageView.tsx | 4 +- .../templateVersionEditorXService.ts | 16 ++-- 6 files changed, 92 insertions(+), 61 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/FileTree.tsx b/site/src/components/TemplateVersionEditor/FileTree.tsx index 8431739211f21..c3033c6564721 100644 --- a/site/src/components/TemplateVersionEditor/FileTree.tsx +++ b/site/src/components/TemplateVersionEditor/FileTree.tsx @@ -7,6 +7,7 @@ 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 { DockerIcon } from "components/Icons/DockerIcon" export interface File { path: string @@ -131,17 +132,21 @@ export const FileTree: FC<{ onClose={() => setContextMenu(undefined)} open={Boolean(contextMenu)} anchorReference="anchorPosition" - anchorPosition={contextMenu ? { - top: contextMenu.clientY, - left: contextMenu.clientX, - } : undefined} + anchorPosition={ + contextMenu + ? { + top: contextMenu.clientY, + left: contextMenu.clientX, + } + : undefined + } anchorOrigin={{ - vertical: 'top', - horizontal: 'left', + vertical: "top", + horizontal: "left", }} transformOrigin={{ - vertical: 'top', - horizontal: 'left', + vertical: "top", + horizontal: "left", }} > ( ) -const FileTypeDockerfile = () => ( - - - - - -) +const FileTypeDockerfile = () => diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 8fec2b497bb68..6fc18fb829732 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -1,8 +1,6 @@ import Button from "@material-ui/core/Button" import IconButton from "@material-ui/core/IconButton" import { makeStyles, Theme } from "@material-ui/core/styles" -import Tab from "@material-ui/core/Tab" -import Tabs from "@material-ui/core/Tabs" import Tooltip from "@material-ui/core/Tooltip" import CreateIcon from "@material-ui/icons/AddBox" import BuildIcon from "@material-ui/icons/BuildOutlined" @@ -130,8 +128,13 @@ export const TemplateVersionEditor: FC = ({ const [dirty, setDirty] = useState(false) const hasIcon = template.icon && template.icon !== "" const templateVersionSucceeded = templateVersion.job.status === "succeeded" + const showBuildLogs = Boolean(buildLogs) + useEffect(() => { + window.dispatchEvent(new Event("resize")) + }, [showBuildLogs]) const styles = useStyles({ templateVersionSucceeded, + showBuildLogs, }) return ( @@ -151,7 +154,7 @@ export const TemplateVersionEditor: FC = ({
-
+
Build Status:
@@ -170,22 +173,25 @@ export const TemplateVersionEditor: FC = ({ - +
@@ -195,19 +201,20 @@ export const TemplateVersionEditor: FC = ({
Template Editor - - { - setCreateFileOpen(true) - event.currentTarget.blur() - }} - > - - - +
+ + { + setCreateFileOpen(true) + event.currentTarget.blur() + }} + > + + + +
{ @@ -379,6 +386,7 @@ const useStyles = makeStyles< Theme, { templateVersionSucceeded: boolean + showBuildLogs: boolean } >((theme) => ({ root: { @@ -401,6 +409,11 @@ const useStyles = makeStyles< alignItems: "center", gap: 16, }, + buildStatus: { + display: "flex", + alignItems: "center", + gap: 8, + }, sidebarAndEditor: { display: "flex", flex: 1, @@ -413,11 +426,20 @@ const useStyles = makeStyles< textTransform: "uppercase", padding: "8px 16px", color: theme.palette.text.hint, + display: "flex", + alignItems: "center", + }, + sidebarActions: { + marginLeft: "auto", + "& svg": { + fill: theme.palette.text.hint, + }, }, editorPane: { display: "grid", width: "100%", - gridTemplateColumns: "0.6fr 0.4fr", + gridTemplateColumns: (props) => + props.showBuildLogs ? "0.6fr 0.4fr" : "1fr 0fr", height: `calc(100vh - ${navHeight + topbarHeight}px)`, overflow: "hidden", }, @@ -495,6 +517,6 @@ const useStyles = makeStyles< whiteSpace: "pre-wrap", }, resources: { - padding: 16, + // padding: 16, }, })) diff --git a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx index 03736ed51d960..e376712b3ef3a 100644 --- a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx +++ b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx @@ -86,12 +86,16 @@ export const WorkspaceBuildLogs: FC = ({ ) } -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles< + Theme, + { + templateEditorPane: boolean + } +>((theme) => ({ logs: { border: `1px solid ${theme.palette.divider}`, - borderRadius: (props) => props.templateEditorPane ? "0px" : theme.shape.borderRadius, + borderRadius: (props) => + props.templateEditorPane ? "0px" : theme.shape.borderRadius, fontFamily: MONOSPACE_FONT_FAMILY, }, diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx index e6426c052fbf6..f4e6596d25921 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage.tsx @@ -42,15 +42,19 @@ export const TemplateVersionEditorPage: FC = () => { if (!versionState.context.template) { throw new Error("no template") } + // Send a cancel just in case a version is already being created! sendEvent({ - type: "CREATE_BUILD", + type: "CANCEL_VERSION", + }) + sendEvent({ + type: "CREATE_VERSION", files: files, templateId: versionState.context.template.id, }) }} onUpdate={() => { sendEvent({ - type: "UPDATE_ACTIVE", + type: "UPDATE_ACTIVE_VERSION", }) }} disablePreview={editorState.hasTag("loading")} diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx index 1ad6e18691319..b112d04ef6fab 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx @@ -144,7 +144,9 @@ export const TemplateVersionPageView: FC = ({ {templateName} + + {templateName} + } /> Date: Tue, 7 Feb 2023 00:08:11 +0000 Subject: [PATCH 17/21] Add stories --- .../TemplateVersionEditor.stories.tsx | 232 +++--------------- .../TemplateVersionEditor.tsx | 6 +- site/src/testHelpers/entities.ts | 33 +++ 3 files changed, 68 insertions(+), 203 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx index ef11cf71b23bd..032012867f3dc 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx @@ -1,11 +1,20 @@ import { Story } from "@storybook/react" +import { + MockTemplate, + MockTemplateVersion, + MockTemplateVersionFiles, + MockWorkspaceBuildLogs, + MockWorkspaceResource, + MockWorkspaceResource2, + MockWorkspaceResource3, +} from "testHelpers/entities" import { TemplateVersionEditor, TemplateVersionEditorProps, } from "./TemplateVersionEditor" export default { - title: "components/TemplateEditor", + title: "components/TemplateVersionEditor", component: TemplateVersionEditor, parameters: { layout: "fullscreen", @@ -18,211 +27,30 @@ const Template: Story = (args: any) => ( export const Example = Template.bind({}) Example.args = { - initialFiles: { - "README.md": "# Example\n\nThis is an example template.", - "main.tf": `terraform { - required_providers { - coder = { - source = "coder/coder" - version = "0.6.10" - } - docker = { - source = "kreuzwerker/docker" - version = "~> 2.22.0" - } - } -} - -# User parameters - -variable "region" { - type = string - description = "Which region to deploy to." - default = "us-pittsburgh" - validation { - condition = contains(["us-pittsburgh", "eu-helsinki", "ap-sydney"], var.region) - error_message = "Region must be one of us-pittsburg, eu-helsinki, or ap-sydney." - } -} - -variable "dotfiles_uri" { - type = string - description = <<-EOF - Dotfiles repo URI (optional) - - see https://dotfiles.github.io - EOF - default = "" -} - -locals { - // These are Tailscale IP addresses. Ask Dean or Kyle for help. - docker_host = { - "" = "tcp://100.94.74.63:2375" - "us-pittsburgh" = "tcp://100.94.74.63:2375" - "eu-helsinki" = "tcp://100.117.102.81:2375" - "ap-sydney" = "tcp://100.127.2.1:2375" - } -} - -provider "docker" { - host = lookup(local.docker_host, var.region) -} - -provider "coder" {} - -data "coder_workspace" "me" {} - -resource "coder_agent" "dev" { - arch = "amd64" - os = "linux" - - login_before_ready = false - startup_script_timeout = 60 - startup_script = <<-EOT - set -ex - # install and start code-server - curl -fsSL https://code-server.dev/install.sh | sh -s -- --version 4.8.3 - code-server --auth none --port 13337 & - sudo service docker start - DOTFILES_URI=\${var.dotfiles_uri} - rm -f ~/.personalize.log - if [ -n "$DOTFILES_URI" ]; then - coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee -a ~/.personalize.log - fi - if [ -x ~/personalize ]; then - ~/personalize 2>&1 | tee -a ~/.personalize.log - elif [ -f ~/personalize ]; then - echo "~/personalize is not executable, skipping..." | tee -a ~/.personalize.log - fi - EOT -} - -resource "coder_app" "code-server" { - agent_id = coder_agent.dev.id - slug = "code-server" - display_name = "code-server" - url = "http://localhost:13337/" - icon = "/icon/code.svg" - subdomain = false - share = "owner" - - healthcheck { - url = "http://localhost:13337/healthz" - interval = 3 - threshold = 10 - } -} - -resource "docker_volume" "home_volume" { - name = "coder-\${data.coder_workspace.me.id}-home" - # Protect the volume from being deleted due to changes in attributes. - lifecycle { - ignore_changes = all - } - # Add labels in Docker to keep track of orphan resources. - labels { - label = "coder.owner" - value = data.coder_workspace.me.owner - } - labels { - label = "coder.owner_id" - value = data.coder_workspace.me.owner_id - } - labels { - label = "coder.workspace_id" - value = data.coder_workspace.me.id - } - # This field becomes outdated if the workspace is renamed but can - # be useful for debugging or cleaning out dangling volumes. - labels { - label = "coder.workspace_name_at_creation" - value = data.coder_workspace.me.name - } + template: MockTemplate, + templateVersion: MockTemplateVersion, + initialFiles: MockTemplateVersionFiles, } -resource "coder_metadata" "home_info" { - resource_id = docker_volume.home_volume.id - item { - key = "🤫🤫🤫

" - value = "❤️❤️❤️" - sensitive = true - } -} +export const Logs = Template.bind({}) -locals { - container_name = "coder-\${data.coder_workspace.me.owner}-\${lower(data.coder_workspace.me.name)}" - registry_name = "codercom/oss-dogfood" -} -data "docker_registry_image" "dogfood" { - name = "\${local.registry_name}:main" +Logs.args = { + template: MockTemplate, + templateVersion: MockTemplateVersion, + initialFiles: MockTemplateVersionFiles, + buildLogs: MockWorkspaceBuildLogs, } -resource "docker_image" "dogfood" { - name = "\${local.registry_name}@\${data.docker_registry_image.dogfood.sha256_digest}" - pull_triggers = [ - data.docker_registry_image.dogfood.sha256_digest, - sha1(join("", [for f in fileset(path.module, "files/*") : filesha1(f)])), - filesha1("Dockerfile"), - ] - keep_locally = true -} +export const Resources = Template.bind({}) -resource "docker_container" "workspace" { - count = data.coder_workspace.me.start_count - image = docker_image.dogfood.name - name = local.container_name - # Hostname makes the shell more user friendly: coder@my-workspace:~$ - hostname = data.coder_workspace.me.name - # Use the docker gateway if the access URL is 127.0.0.1 - entrypoint = ["sh", "-c", replace(coder_agent.dev.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")] - # CPU limits are unnecessary since Docker will load balance automatically - memory = 32768 - runtime = "sysbox-runc" - env = [ - "CODER_AGENT_TOKEN=\${coder_agent.dev.token}", - "DATOCMS_API_TOKEN=\${var.datocms_api_token}", - ] - host { - host = "host.docker.internal" - ip = "host-gateway" - } - volumes { - container_path = "/home/coder/" - volume_name = docker_volume.home_volume.name - read_only = false - } - # Add labels in Docker to keep track of orphan resources. - labels { - label = "coder.owner" - value = data.coder_workspace.me.owner - } - labels { - label = "coder.owner_id" - value = data.coder_workspace.me.owner_id - } - labels { - label = "coder.workspace_id" - value = data.coder_workspace.me.id - } - labels { - label = "coder.workspace_name" - value = data.coder_workspace.me.name - } -} - -resource "coder_metadata" "container_info" { - count = data.coder_workspace.me.start_count - resource_id = docker_container.workspace[0].id - item { - key = "memory" - value = docker_container.workspace[0].memory - } - item { - key = "runtime" - value = docker_container.workspace[0].runtime - } -} -`, - }, +Resources.args = { + template: MockTemplate, + templateVersion: MockTemplateVersion, + initialFiles: MockTemplateVersionFiles, + buildLogs: MockWorkspaceBuildLogs, + resources: [ + MockWorkspaceResource, + MockWorkspaceResource2, + MockWorkspaceResource3, + ], } diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index 6fc18fb829732..f6e6566b9aa32 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -64,7 +64,11 @@ export const TemplateVersionEditor: FC = ({ buildLogs, resources, }) => { - const [selectedTab, setSelectedTab] = useState(0) + const [selectedTab, setSelectedTab] = useState(() => { + // If resources are provided, show them by default! + // This is for Storybook! + return resources ? 1 : 0 + }) const [files, setFiles] = useState(initialFiles) const [createFileOpen, setCreateFileOpen] = useState(false) const [deleteFileOpen, setDeleteFileOpen] = useState() diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 89b684584b3bc..f53083a6cceaa 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -5,6 +5,7 @@ import * as Types from "../api/types" import * as TypesGen from "../api/typesGenerated" import range from "lodash/range" import { Permissions } from "xServices/auth/authXService" +import { TemplateVersionFiles } from "util/templateVersion" export const MockTemplateDAUResponse: TypesGen.TemplateDAUsResponse = { entries: [ @@ -270,6 +271,38 @@ export const MockTemplate: TypesGen.Template = { allow_user_cancel_workspace_jobs: true, } +export const MockTemplateVersionFiles: TemplateVersionFiles = { + "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 + } + } +} +} +`, +} + export const MockWorkspaceApp: TypesGen.WorkspaceApp = { id: "test-app", slug: "test-app", From 0d03b927707dd2c63d74fc138ab5bda9d664a324 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Feb 2023 00:14:29 +0000 Subject: [PATCH 18/21] Add experiment to enable the template editor --- coderd/apidoc/docs.go | 6 ++-- coderd/apidoc/swagger.json | 4 +-- codersdk/deployment.go | 6 +++- docs/api/schemas.md | 7 ++-- site/src/api/typesGenerated.ts | 4 +-- .../TemplateVersionEditor.tsx | 4 +++ .../TemplateVersionPage.tsx | 3 ++ .../TemplateVersionPageView.stories.tsx | 1 + .../TemplateVersionPageView.tsx | 32 +++++++++++-------- 9 files changed, 43 insertions(+), 24 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 6bb45c4fb3c23..0c838fde83354 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6367,10 +6367,12 @@ const docTemplate = `{ "codersdk.Experiment": { "type": "string", "enum": [ - "authz_querier" + "authz_querier", + "template_editor" ], "x-enum-varnames": [ - "ExperimentAuthzQuerier" + "ExperimentAuthzQuerier", + "ExperimentTemplateEditor" ] }, "codersdk.Feature": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 91c1d15ff255a..6b9a254b32ed0 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5693,8 +5693,8 @@ }, "codersdk.Experiment": { "type": "string", - "enum": ["authz_querier"], - "x-enum-varnames": ["ExperimentAuthzQuerier"] + "enum": ["authz_querier", "template_editor"], + "x-enum-varnames": ["ExperimentAuthzQuerier", "ExperimentTemplateEditor"] }, "codersdk.Feature": { "type": "object", diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 1cbbe0cf9b8fb..c0d09b8b7fe24 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -418,6 +418,10 @@ const ( // interface for all RBAC operations. NOT READY FOR PRODUCTION USE. ExperimentAuthzQuerier Experiment = "authz_querier" + // ExperimentTemplateEditor is an internal experiment that enables the template editor + // for all users. + ExperimentTemplateEditor Experiment = "template_editor" + // Add new experiments here! // ExperimentExample Experiment = "example" ) @@ -427,7 +431,7 @@ var ( // users to opt-in to via --experimental='*'. // Experiments that are not ready for consumption by all users should // not be included here and will be essentially hidden. - ExperimentsAll = Experiments{} + ExperimentsAll = Experiments{ExperimentTemplateEditor} ) // Experiments is a list of experiments that are enabled for the deployment. diff --git a/docs/api/schemas.md b/docs/api/schemas.md index da334604839a2..7bd1f5b554af6 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2732,9 +2732,10 @@ CreateParameterRequest is a structure used to create a new parameter value for a #### Enumerated Values -| Value | -| --------------- | -| `authz_querier` | +| Value | +| ----------------- | +| `authz_querier` | +| `template_editor` | ## codersdk.Feature diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 6474f676e47e3..e4a9eb195a3be 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1070,8 +1070,8 @@ export const Entitlements: Entitlement[] = [ ] // From codersdk/deployment.go -export type Experiment = "authz_querier" -export const Experiments: Experiment[] = ["authz_querier"] +export type Experiment = "authz_querier" | "template_editor" +export const Experiments: Experiment[] = ["authz_querier", "template_editor"] // From codersdk/deployment.go export type FeatureName = diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx index f6e6566b9aa32..ed328d67e28f5 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx @@ -76,6 +76,7 @@ export const TemplateVersionEditor: FC = ({ 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], @@ -85,11 +86,14 @@ export const TemplateVersionEditor: FC = ({ } } }) + const triggerPreview = useCallback(() => { onPreview(files) // Switch to the build log! setSelectedTab(0) }, [files, onPreview]) + + // Stop ctrl+s from saving files and make ctrl+enter trigger a preview. useEffect(() => { const keyListener = (event: KeyboardEvent) => { if (!(navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey)) { diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx index ed836d58fd79b..b83ff5cbc10c1 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPage.tsx @@ -1,4 +1,5 @@ import { useMachine } from "@xstate/react" +import { useDashboard } from "components/Dashboard/DashboardProvider" import { useOrganizationId } from "hooks/useOrganizationId" import { useTab } from "hooks/useTab" import { FC } from "react" @@ -22,6 +23,7 @@ export const TemplateVersionPage: FC = () => { }) const tab = useTab("file", "0") const { t } = useTranslation("templateVersionPage") + const dashboard = useDashboard() return ( <> @@ -36,6 +38,7 @@ export const TemplateVersionPage: FC = () => { versionName={versionName} templateName={templateName} tab={tab} + canEdit={dashboard.experiments.includes("template_editor")} /> ) diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.stories.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.stories.tsx index caefc6a57261a..f7cf56cc99d30 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.stories.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.stories.tsx @@ -41,6 +41,7 @@ const defaultArgs = { tab, templateName: MockTemplate.name, versionName: MockTemplateVersion.name, + canEdit: true, context: { orgId: MockOrganization.id, versionName: MockTemplateVersion.name, diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx index b112d04ef6fab..6b45d0da56bf3 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx @@ -1,10 +1,13 @@ +import Button from "@material-ui/core/Button" +import Link from "@material-ui/core/Link" import { makeStyles } from "@material-ui/core/styles" +import EditIcon from "@material-ui/icons/Edit" import { AlertBanner } from "components/AlertBanner/AlertBanner" +import { DockerIcon } from "components/Icons/DockerIcon" import { MarkdownIcon } from "components/Icons/MarkdownIcon" import { TerraformIcon } from "components/Icons/TerraformIcon" import { Loader } from "components/Loader/Loader" import { Margins } from "components/Margins/Margins" -import EditIcon from "@material-ui/icons/Edit" import { PageHeader, PageHeaderCaption, @@ -12,18 +15,15 @@ import { } from "components/PageHeader/PageHeader" import { Stack } from "components/Stack/Stack" import { Stats, StatsItem } from "components/Stats/Stats" +import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter" import { UseTabResult } from "hooks/useTab" import { FC } from "react" -import { Link as RouterLink } from "react-router-dom" import { useTranslation } from "react-i18next" +import { Link as RouterLink } from "react-router-dom" import { combineClasses } from "util/combineClasses" import { createDayString } from "util/createDayString" -import { TemplateVersionMachineContext } from "xServices/templateVersion/templateVersionXService" import { TemplateVersionFiles } from "util/templateVersion" -import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter" -import { DockerIcon } from "components/Icons/DockerIcon" -import Button from "@material-ui/core/Button" -import Link from "@material-ui/core/Link" +import { TemplateVersionMachineContext } from "xServices/templateVersion/templateVersionXService" const iconByExtension: Record = { tf: , @@ -104,6 +104,7 @@ export interface TemplateVersionPageViewProps { */ versionName: string templateName: string + canEdit: boolean tab: UseTabResult context: TemplateVersionMachineContext } @@ -113,6 +114,7 @@ export const TemplateVersionPageView: FC = ({ tab, versionName, templateName, + canEdit, }) => { const { currentFiles, error, currentVersion, previousFiles } = context const { t } = useTranslation("templateVersionPage") @@ -121,13 +123,15 @@ export const TemplateVersionPageView: FC = ({ - - + canEdit ? ( + + + + ) : undefined } > {t("header.caption")} From 4cbd69b020c1cc720236fe7bd38e9c2e39a69159 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Feb 2023 00:19:40 +0000 Subject: [PATCH 19/21] Fix linting errors --- .../TemplateVersionEditor/TemplateVersionEditor.stories.tsx | 2 +- .../TemplateVersionEditor/TemplateVersionStatusBadge.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx index 032012867f3dc..4d45540fc446e 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx @@ -21,7 +21,7 @@ export default { }, } -const Template: Story = (args: any) => ( +const Template: Story = (args: TemplateVersionEditorProps) => ( ) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionStatusBadge.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionStatusBadge.tsx index c52e6ca957908..5dc4c5872cb21 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionStatusBadge.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionStatusBadge.tsx @@ -25,7 +25,6 @@ export const getStatus = ( text: string icon: ReactNode } => { - console.log("Getting status", version.job.status) switch (version.job.status) { case "running": return { From b87ec5d2bb3124b90a60d057167a3b62db737785 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Feb 2023 00:20:40 +0000 Subject: [PATCH 20/21] Fix duplicate fields --- .../TemplateVersionEditor/TemplateVersionEditor.stories.tsx | 6 +++--- .../components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx | 2 +- .../xServices/templateVersion/templateVersionXService.ts | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx index 4d45540fc446e..78b5863bd74ce 100644 --- a/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx +++ b/site/src/components/TemplateVersionEditor/TemplateVersionEditor.stories.tsx @@ -21,9 +21,9 @@ export default { }, } -const Template: Story = (args: TemplateVersionEditorProps) => ( - -) +const Template: Story = ( + args: TemplateVersionEditorProps, +) => export const Example = Template.bind({}) Example.args = { diff --git a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx index e376712b3ef3a..7ba82f2b7075f 100644 --- a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx +++ b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx @@ -53,7 +53,7 @@ export const WorkspaceBuildLogs: FC = ({ }) => { const groupedLogsByStage = groupLogsByStage(logs) const stages = Object.keys(groupedLogsByStage) - const styles = useStyles({ templateEditorPane }) + const styles = useStyles({ templateEditorPane: Boolean(templateEditorPane) }) return (
diff --git a/site/src/xServices/templateVersion/templateVersionXService.ts b/site/src/xServices/templateVersion/templateVersionXService.ts index b99dbb08c8916..bd7d371894723 100644 --- a/site/src/xServices/templateVersion/templateVersionXService.ts +++ b/site/src/xServices/templateVersion/templateVersionXService.ts @@ -15,7 +15,6 @@ export interface TemplateVersionMachineContext { orgId: string templateName: string versionName: string - templateName: string template?: Template currentVersion?: TemplateVersion currentFiles?: TemplateVersionFiles From 8440b44acfd6dfa4a652e3171918471c3dfe5bf3 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 7 Feb 2023 00:59:51 +0000 Subject: [PATCH 21/21] Fix theme type --- site/src/components/SyntaxHighlighter/coderTheme.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/src/components/SyntaxHighlighter/coderTheme.ts b/site/src/components/SyntaxHighlighter/coderTheme.ts index d525ccdddf033..cbeb0a6393297 100644 --- a/site/src/components/SyntaxHighlighter/coderTheme.ts +++ b/site/src/components/SyntaxHighlighter/coderTheme.ts @@ -2,10 +2,11 @@ import { Theme, useTheme } from "@material-ui/core/styles" import { useMonaco } from "@monaco-editor/react" import { useEffect, useState } from "react" import { hslToHex } from "util/colors" +import { editor } from "monaco-editor" // Theme based on https://github.com/brijeshb42/monaco-themes/blob/master/themes/Dracula.json // eslint-disable-next-line @typescript-eslint/no-explicit-any -- The theme is not typed -export const coderTheme = (theme: Theme): Record => ({ +export const coderTheme = (theme: Theme): editor.IStandaloneThemeData => ({ base: "vs-dark", inherit: true, rules: [