Skip to content

Commit dd9e1f3

Browse files
authored
feat: add template editor to the ui (#5963)
* Add initial editor * Fix editor file being reset onChange * Add updating the active build version * Update nav height * Add tabs * Fix title * Hide timestamps in build logs * Add create file dialog * Add validation for empty path * Hide resources tab * Fix label names * Add rename and delete * Improve UX * Add padding to the editor * Add dirty state * Hide build logs until a build is made * Add stories * Add experiment to enable the template editor * Fix linting errors * Fix duplicate fields * Fix theme type
1 parent 71a8937 commit dd9e1f3

27 files changed

+1751
-40
lines changed

coderd/apidoc/docs.go

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codersdk/deployment.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,10 @@ const (
421421
// interface for all RBAC operations. NOT READY FOR PRODUCTION USE.
422422
ExperimentAuthzQuerier Experiment = "authz_querier"
423423

424+
// ExperimentTemplateEditor is an internal experiment that enables the template editor
425+
// for all users.
426+
ExperimentTemplateEditor Experiment = "template_editor"
427+
424428
// Add new experiments here!
425429
// ExperimentExample Experiment = "example"
426430
)
@@ -430,7 +434,7 @@ var (
430434
// users to opt-in to via --experimental='*'.
431435
// Experiments that are not ready for consumption by all users should
432436
// not be included here and will be essentially hidden.
433-
ExperimentsAll = Experiments{}
437+
ExperimentsAll = Experiments{ExperimentTemplateEditor}
434438
)
435439

436440
// Experiments is a list of experiments that are enabled for the deployment.

docs/api/schemas.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2770,9 +2770,10 @@ CreateParameterRequest is a structure used to create a new parameter value for a
27702770

27712771
#### Enumerated Values
27722772

2773-
| Value |
2774-
| --------------- |
2775-
| `authz_querier` |
2773+
| Value |
2774+
| ----------------- |
2775+
| `authz_querier` |
2776+
| `template_editor` |
27762777

27772778
## codersdk.Feature
27782779

site/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"react-chartjs-2": "4.3.1",
6868
"react-color": "2.19.3",
6969
"react-dom": "18.2.0",
70+
"react-headless-tabs": "^6.0.3",
7071
"react-helmet-async": "1.3.0",
7172
"react-i18next": "12.1.1",
7273
"react-markdown": "8.0.3",
@@ -75,6 +76,7 @@
7576
"remark-gfm": "3.0.1",
7677
"rollup-plugin-visualizer": "5.9.0",
7778
"sourcemapped-stacktrace": "1.1.11",
79+
"tar-js": "^0.3.0",
7880
"ts-prune": "0.10.3",
7981
"tzdata": "1.0.30",
8082
"ua-parser-js": "1.0.33",
@@ -125,6 +127,7 @@
125127
"jest-esm-transformer": "^1.0.0",
126128
"jest-runner-eslint": "1.1.0",
127129
"jest-websocket-mock": "2.4.0",
130+
"monaco-editor": "^0.34.1",
128131
"msw": "0.47.0",
129132
"prettier": "2.8.1",
130133
"resize-observer": "1.0.4",

site/src/AppRouter.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ const GitAuthPage = lazy(() => import("./pages/GitAuthPage/GitAuthPage"))
108108
const TemplateVersionPage = lazy(
109109
() => import("./pages/TemplateVersionPage/TemplateVersionPage"),
110110
)
111+
const TemplateVersionEditorPage = lazy(
112+
() =>
113+
import(
114+
"./pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage"
115+
),
116+
)
111117
const StarterTemplatesPage = lazy(
112118
() => import("./pages/StarterTemplatesPage/StarterTemplatesPage"),
113119
)
@@ -155,7 +161,13 @@ export const AppRouter: FC = () => {
155161
<Route path="workspace" element={<CreateWorkspacePage />} />
156162
<Route path="settings" element={<TemplateSettingsPage />} />
157163
<Route path="versions">
158-
<Route path=":version" element={<TemplateVersionPage />} />
164+
<Route path=":version">
165+
<Route index element={<TemplateVersionPage />} />
166+
<Route
167+
path="edit"
168+
element={<TemplateVersionEditorPage />}
169+
/>
170+
</Route>
159171
</Route>
160172
</Route>
161173
</Route>

site/src/api/api.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,17 @@ export const createTemplate = async (
309309
return response.data
310310
}
311311

312+
export const updateActiveTemplateVersion = async (
313+
templateId: string,
314+
data: TypesGen.UpdateActiveTemplateVersion,
315+
): Promise<Types.Message> => {
316+
const response = await axios.patch<Types.Message>(
317+
`/api/v2/templates/${templateId}/versions`,
318+
data,
319+
)
320+
return response.data
321+
}
322+
312323
export const updateTemplateMeta = async (
313324
templateId: string,
314325
data: TypesGen.UpdateTemplateMeta,
@@ -433,6 +444,15 @@ export const cancelWorkspaceBuild = async (
433444
return response.data
434445
}
435446

447+
export const cancelTemplateVersionBuild = async (
448+
templateVersionId: TypesGen.TemplateVersion["id"],
449+
): Promise<Types.Message> => {
450+
const response = await axios.patch(
451+
`/api/v2/templateversions/${templateVersionId}/cancel`,
452+
)
453+
return response.data
454+
}
455+
436456
export const createUser = async (
437457
user: TypesGen.CreateUserRequest,
438458
): Promise<TypesGen.User> => {

site/src/api/typesGenerated.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,8 +1082,8 @@ export const Entitlements: Entitlement[] = [
10821082
]
10831083

10841084
// From codersdk/deployment.go
1085-
export type Experiment = "authz_querier"
1086-
export const Experiments: Experiment[] = ["authz_querier"]
1085+
export type Experiment = "authz_querier" | "template_editor"
1086+
export const Experiments: Experiment[] = ["authz_querier", "template_editor"]
10871087

10881088
// From codersdk/deployment.go
10891089
export type FeatureName =

site/src/components/Dashboard/DashboardLayout.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,5 @@ const useStyles = makeStyles((theme) => ({
7171
},
7272
siteContent: {
7373
flex: 1,
74-
paddingBottom: theme.spacing(10),
7574
},
7675
}))

site/src/components/Logs/Logs.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ interface Line {
1313

1414
export interface LogsProps {
1515
lines: Line[]
16+
hideTimestamps?: boolean
1617
className?: string
1718
}
1819

1920
export const Logs: FC<React.PropsWithChildren<LogsProps>> = ({
21+
hideTimestamps,
2022
lines,
2123
className = "",
2224
}) => {
@@ -27,10 +29,14 @@ export const Logs: FC<React.PropsWithChildren<LogsProps>> = ({
2729
<div className={styles.scrollWrapper}>
2830
{lines.map((line, idx) => (
2931
<div className={combineClasses([styles.line, line.level])} key={idx}>
30-
<span className={styles.time}>
31-
{dayjs(line.time).format(`HH:mm:ss.SSS`)}
32-
</span>
33-
<span className={styles.space}>&nbsp;&nbsp;&nbsp;&nbsp;</span>
32+
{!hideTimestamps && (
33+
<>
34+
<span className={styles.time}>
35+
{dayjs(line.time).format(`HH:mm:ss.SSS`)}
36+
</span>
37+
<span className={styles.space}>&nbsp;&nbsp;&nbsp;&nbsp;</span>
38+
</>
39+
)}
3440
<span>{line.output}</span>
3541
</div>
3642
))}

site/src/components/SyntaxHighlighter/coderTheme.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { Theme, useTheme } from "@material-ui/core/styles"
22
import { useMonaco } from "@monaco-editor/react"
33
import { useEffect, useState } from "react"
44
import { hslToHex } from "util/colors"
5+
import { editor } from "monaco-editor"
56

67
// Theme based on https://github.com/brijeshb42/monaco-themes/blob/master/themes/Dracula.json
78
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- The theme is not typed
8-
export const coderTheme = (theme: Theme): Record<string, any> => ({
9+
export const coderTheme = (theme: Theme): editor.IStandaloneThemeData => ({
910
base: "vs-dark",
1011
inherit: true,
1112
rules: [

site/src/components/TemplateLayout/TemplateLayout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { Avatar } from "components/Avatar/Avatar"
3131

3232
const Language = {
3333
settingsButton: "Settings",
34+
editButton: "Edit",
3435
createButton: "Create workspace",
3536
noDescription: "",
3637
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import TextField from "@material-ui/core/TextField"
2+
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
3+
import { Stack } from "components/Stack/Stack"
4+
import { ChangeEvent, FC, useState } from "react"
5+
import Typography from "@material-ui/core/Typography"
6+
7+
export const CreateFileDialog: FC<{
8+
onClose: () => void
9+
checkExists: (path: string) => boolean
10+
onConfirm: (path: string) => void
11+
open: boolean
12+
}> = ({ checkExists, onClose, onConfirm, open }) => {
13+
const [pathValue, setPathValue] = useState("")
14+
const [error, setError] = useState("")
15+
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
16+
setPathValue(event.target.value)
17+
}
18+
const handleConfirm = () => {
19+
if (pathValue === "") {
20+
setError("You must enter a path!")
21+
return
22+
}
23+
if (checkExists(pathValue)) {
24+
setError("File already exists")
25+
return
26+
}
27+
onConfirm(pathValue)
28+
setPathValue("")
29+
}
30+
31+
return (
32+
<ConfirmDialog
33+
open={open}
34+
onClose={() => {
35+
onClose()
36+
setPathValue("")
37+
}}
38+
onConfirm={handleConfirm}
39+
hideCancel={false}
40+
type="success"
41+
cancelText="Cancel"
42+
confirmText="Create"
43+
title="Create File"
44+
description={
45+
<Stack spacing={1}>
46+
<Typography>
47+
Specify the path to a file to be created. This path can contain
48+
slashes too!
49+
</Typography>
50+
<TextField
51+
autoFocus
52+
onKeyDown={(event) => {
53+
if (event.key === "Enter") {
54+
handleConfirm()
55+
}
56+
}}
57+
helperText={error}
58+
name="file-path"
59+
autoComplete="off"
60+
id="file-path"
61+
placeholder="main.tf"
62+
value={pathValue}
63+
onChange={handleChange}
64+
label="File Path"
65+
/>
66+
</Stack>
67+
}
68+
/>
69+
)
70+
}
71+
72+
export const DeleteFileDialog: FC<{
73+
onClose: () => void
74+
onConfirm: () => void
75+
open: boolean
76+
filename: string
77+
}> = ({ onClose, onConfirm, open, filename }) => {
78+
return (
79+
<ConfirmDialog
80+
type="delete"
81+
onClose={onClose}
82+
open={open}
83+
onConfirm={onConfirm}
84+
title="Delete File"
85+
description={`Are you sure you want to delete "${filename}"?`}
86+
/>
87+
)
88+
}
89+
90+
export const RenameFileDialog: FC<{
91+
onClose: () => void
92+
onConfirm: (filename: string) => void
93+
checkExists: (path: string) => boolean
94+
open: boolean
95+
filename: string
96+
}> = ({ checkExists, onClose, onConfirm, open, filename }) => {
97+
const [pathValue, setPathValue] = useState(filename)
98+
const [error, setError] = useState("")
99+
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
100+
setPathValue(event.target.value)
101+
}
102+
const handleConfirm = () => {
103+
if (pathValue === "") {
104+
setError("You must enter a path!")
105+
return
106+
}
107+
if (checkExists(pathValue)) {
108+
setError("File already exists")
109+
return
110+
}
111+
onConfirm(pathValue)
112+
setPathValue("")
113+
}
114+
115+
return (
116+
<ConfirmDialog
117+
open={open}
118+
onClose={() => {
119+
onClose()
120+
setPathValue("")
121+
}}
122+
onConfirm={handleConfirm}
123+
hideCancel={false}
124+
type="success"
125+
cancelText="Cancel"
126+
confirmText="Create"
127+
title="Rename File"
128+
description={
129+
<Stack spacing={1}>
130+
<Typography>
131+
Rename {`"${filename}"`} to something else. This path can contain
132+
slashes too!
133+
</Typography>
134+
<TextField
135+
autoFocus
136+
onKeyDown={(event) => {
137+
if (event.key === "Enter") {
138+
handleConfirm()
139+
}
140+
}}
141+
helperText={error}
142+
name="file-path"
143+
autoComplete="off"
144+
id="file-path"
145+
placeholder="main.tf"
146+
defaultValue={filename}
147+
value={pathValue}
148+
onChange={handleChange}
149+
label="File Path"
150+
/>
151+
</Stack>
152+
}
153+
/>
154+
)
155+
}

0 commit comments

Comments
 (0)