Skip to content

Commit 564d40e

Browse files
committed
Add initial editor
1 parent fa5b612 commit 564d40e

File tree

13 files changed

+1060
-14
lines changed

13 files changed

+1060
-14
lines changed

site/package.json

Lines changed: 2 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",

site/src/AppRouter.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ const GitAuthPage = lazy(() => import("./pages/GitAuthPage/GitAuthPage"))
107107
const TemplateVersionPage = lazy(
108108
() => import("./pages/TemplateVersionPage/TemplateVersionPage"),
109109
)
110+
const TemplateVersionEditorPage = lazy(
111+
() => import("./pages/TemplateVersionPage/TemplateVersionEditorPage/TemplateVersionEditorPage"),
112+
)
110113
const StarterTemplatesPage = lazy(
111114
() => import("./pages/StarterTemplatesPage/StarterTemplatesPage"),
112115
)
@@ -154,7 +157,10 @@ export const AppRouter: FC = () => {
154157
<Route path="workspace" element={<CreateWorkspacePage />} />
155158
<Route path="settings" element={<TemplateSettingsPage />} />
156159
<Route path="versions">
157-
<Route path=":version" element={<TemplateVersionPage />} />
160+
<Route path=":version">
161+
<Route index element={<TemplateVersionPage />} />
162+
<Route path="edit" element={<TemplateVersionEditorPage />} />
163+
</Route>
158164
</Route>
159165
</Route>
160166
</Route>

site/src/api/api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,15 @@ export const cancelWorkspaceBuild = async (
420420
return response.data
421421
}
422422

423+
export const cancelTemplateVersionBuild = async (
424+
templateVersionId: TypesGen.TemplateVersion["id"]
425+
): Promise<Types.Message> => {
426+
const response = await axios.patch(
427+
`/api/v2/templateversions/${templateVersionId}/cancel`,
428+
)
429+
return response.data
430+
}
431+
423432
export const createUser = async (
424433
user: TypesGen.CreateUserRequest,
425434
): Promise<TypesGen.User> => {

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
}))
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { makeStyles } from "@material-ui/core/styles"
2+
import ChevronRightIcon from "@material-ui/icons/ChevronRight"
3+
import ExpandMoreIcon from "@material-ui/icons/ExpandMore"
4+
import TreeView from "@material-ui/lab/TreeView"
5+
import TreeItem from "@material-ui/lab/TreeItem"
6+
7+
import { FC, useMemo } from "react"
8+
import { TemplateVersionFiles } from "util/templateVersion"
9+
10+
export interface File {
11+
path: string
12+
content?: string
13+
children: Record<string, File>
14+
}
15+
16+
export const FileTree: FC<{
17+
onSelect: (file: File) => void
18+
files: TemplateVersionFiles
19+
activeFile?: File
20+
}> = ({ activeFile, files, onSelect }) => {
21+
const styles = useStyles()
22+
const fileTree = useMemo<Record<string, File>>(() => {
23+
const paths = Object.keys(files)
24+
const roots: Record<string, File> = {}
25+
paths.forEach((path) => {
26+
const pathParts = path.split("/")
27+
const firstPart = pathParts.shift()
28+
if (!firstPart) {
29+
// Not possible!
30+
return
31+
}
32+
let activeFile = roots[firstPart]
33+
if (!activeFile) {
34+
activeFile = {
35+
path: firstPart,
36+
children: {},
37+
}
38+
roots[firstPart] = activeFile
39+
}
40+
while (pathParts.length > 0) {
41+
const pathPart = pathParts.shift()
42+
if (!pathPart) {
43+
continue
44+
}
45+
if (!activeFile.children[pathPart]) {
46+
activeFile.children[pathPart] = {
47+
path: activeFile.path + "/" + pathPart,
48+
children: {},
49+
}
50+
}
51+
activeFile = activeFile.children[pathPart]
52+
}
53+
activeFile.content = files[path]
54+
activeFile.path = path
55+
})
56+
return roots
57+
}, [files])
58+
59+
const buildTreeItems = (name: string, file: File): JSX.Element => {
60+
let icon: JSX.Element | null = null
61+
if (file.path.endsWith(".tf")) {
62+
icon = <FileTypeTerraform />
63+
}
64+
if (file.path.endsWith(".md")) {
65+
icon = <FileTypeMarkdown />
66+
}
67+
68+
return (
69+
<TreeItem
70+
nodeId={file.path}
71+
key={file.path}
72+
label={name}
73+
className={`${styles.fileTreeItem} ${
74+
file.path === activeFile?.path ? "active" : ""
75+
}`}
76+
onClick={() => {
77+
if (file.content) {
78+
onSelect(file)
79+
}
80+
}}
81+
icon={icon}
82+
>
83+
{Object.entries(file.children || {}).map(([name, file]) => {
84+
return buildTreeItems(name, file)
85+
})}
86+
</TreeItem>
87+
)
88+
}
89+
90+
return (
91+
<TreeView
92+
defaultCollapseIcon={<ExpandMoreIcon />}
93+
defaultExpandIcon={<ChevronRightIcon />}
94+
aria-label="Files"
95+
className={styles.fileTree}
96+
>
97+
{Object.entries(fileTree).map(([name, file]) => {
98+
return buildTreeItems(name, file)
99+
})}
100+
</TreeView>
101+
)
102+
}
103+
104+
const useStyles = makeStyles((theme) => ({
105+
fileTree: {},
106+
fileTreeItem: {
107+
overflow: "hidden",
108+
userSelect: "none",
109+
110+
"&:focus": {
111+
"& > .MuiTreeItem-content": {
112+
background: "rgba(255, 255, 255, 0.1)",
113+
},
114+
},
115+
"& > .MuiTreeItem-content:hover": {
116+
background: theme.palette.background.paperLight,
117+
color: theme.palette.text.primary,
118+
},
119+
120+
"& > .MuiTreeItem-content": {
121+
padding: "1px 16px",
122+
color: theme.palette.text.secondary,
123+
124+
"& svg": {
125+
width: 16,
126+
height: 16,
127+
},
128+
129+
"& > .MuiTreeItem-label": {
130+
marginLeft: 4,
131+
fontSize: 14,
132+
color: "inherit",
133+
},
134+
},
135+
136+
"&.active": {
137+
background: theme.palette.background.paper,
138+
139+
"& > .MuiTreeItem-content": {
140+
color: theme.palette.text.primary,
141+
},
142+
},
143+
},
144+
editor: {
145+
flex: 1,
146+
},
147+
preview: {},
148+
}))
149+
150+
const FileTypeTerraform = () => (
151+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#813cf3">
152+
<title>file_type_terraform</title>
153+
<polygon points="12.042 6.858 20.071 11.448 20.071 20.462 12.042 15.868 12.042 6.858 12.042 6.858" />
154+
<polygon points="20.5 20.415 28.459 15.84 28.459 6.887 20.5 11.429 20.5 20.415 20.5 20.415" />
155+
<polygon points="3.541 11.01 11.571 15.599 11.571 6.59 3.541 2 3.541 11.01 3.541 11.01" />
156+
<polygon points="12.042 25.41 20.071 30 20.071 20.957 12.042 16.368 12.042 25.41 12.042 25.41" />
157+
</svg>
158+
)
159+
160+
const FileTypeMarkdown = () => (
161+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#755838">
162+
<rect
163+
x="2.5"
164+
y="7.955"
165+
width="27"
166+
height="16.091"
167+
style={{
168+
fill: "none",
169+
stroke: "#755838",
170+
}}
171+
/>
172+
<polygon points="5.909 20.636 5.909 11.364 8.636 11.364 11.364 14.773 14.091 11.364 16.818 11.364 16.818 20.636 14.091 20.636 14.091 15.318 11.364 18.727 8.636 15.318 8.636 20.636 5.909 20.636" />
173+
<polygon points="22.955 20.636 18.864 16.136 21.591 16.136 21.591 11.364 24.318 11.364 24.318 16.136 27.045 16.136 22.955 20.636" />
174+
</svg>
175+
)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { useTheme } from "@material-ui/core/styles"
2+
import Editor from "@monaco-editor/react"
3+
import { FC, useEffect, useState } from "react"
4+
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
5+
import { hslToHex } from "util/colors"
6+
7+
export const MonacoEditor: FC<{
8+
value?: string
9+
language?: string
10+
onChange?: (value: string) => void
11+
}> = ({ onChange, value, language }) => {
12+
const theme = useTheme()
13+
const [editor, setEditor] = useState<any>()
14+
useEffect(() => {
15+
if (!editor) {
16+
return
17+
}
18+
const resizeListener = () => {
19+
editor.layout({})
20+
}
21+
window.addEventListener("resize", resizeListener)
22+
return () => {
23+
window.removeEventListener("resize", resizeListener)
24+
}
25+
}, [editor])
26+
27+
return (
28+
<Editor
29+
value={value || ""}
30+
language={language || "hcl"}
31+
theme="vs-dark"
32+
options={{
33+
automaticLayout: true,
34+
fontFamily: MONOSPACE_FONT_FAMILY,
35+
fontSize: 16,
36+
wordWrap: "on",
37+
}}
38+
onChange={(newValue) => {
39+
if (onChange && newValue) {
40+
onChange(newValue)
41+
}
42+
}}
43+
onMount={(editor, monaco) => {
44+
setEditor(editor)
45+
46+
document.fonts.ready
47+
.then(() => {
48+
// Ensures that all text is measured properly.
49+
// If this isn't done, there can be weird selection issues.
50+
monaco.editor.remeasureFonts()
51+
})
52+
.catch(() => {
53+
// Not a biggie!
54+
})
55+
56+
monaco.editor.defineTheme("min", {
57+
base: "vs-dark",
58+
inherit: true,
59+
rules: [
60+
{
61+
token: "comment",
62+
foreground: "6B737C",
63+
},
64+
{
65+
token: "type",
66+
foreground: "B392F0",
67+
},
68+
{
69+
token: "string",
70+
foreground: "9DB1C5",
71+
},
72+
{
73+
token: "variable",
74+
foreground: "BBBBBB",
75+
},
76+
{
77+
token: "identifier",
78+
foreground: "B392F0",
79+
},
80+
{
81+
token: "delimiter.curly",
82+
foreground: "EBB325",
83+
},
84+
],
85+
colors: {
86+
"editor.foreground": hslToHex(theme.palette.text.primary),
87+
"editor.background": hslToHex(theme.palette.background.paper),
88+
},
89+
})
90+
editor.updateOptions({
91+
theme: "min",
92+
})
93+
}}
94+
/>
95+
)
96+
}

0 commit comments

Comments
 (0)