Skip to content

Commit 29d71bb

Browse files
feat(site): Add source code tab on template page, group buttons and add edit file option (#6681)
1 parent f97c225 commit 29d71bb

File tree

14 files changed

+481
-581
lines changed

14 files changed

+481
-581
lines changed

site/src/AppRouter.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,12 @@ const TemplateVariablesPage = lazy(
125125
const WorkspaceSettingsPage = lazy(
126126
() => import("./pages/WorkspaceSettingsPage/WorkspaceSettingsPage"),
127127
)
128-
129128
const CreateTokenPage = lazy(
130129
() => import("./pages/CreateTokenPage/CreateTokenPage"),
131130
)
131+
const TemplateFilesPage = lazy(
132+
() => import("./pages/TemplateFilesPage/TemplateFilesPage"),
133+
)
132134

133135
export const AppRouter: FC = () => {
134136
return (
@@ -162,6 +164,7 @@ export const AppRouter: FC = () => {
162164
path="permissions"
163165
element={<TemplatePermissionsPage />}
164166
/>
167+
<Route path="files" element={<TemplateFilesPage />} />
165168
</Route>
166169

167170
<Route path="workspace" element={<CreateWorkspacePage />} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { makeStyles } from "@material-ui/core/styles"
2+
import { DockerIcon } from "components/Icons/DockerIcon"
3+
import { MarkdownIcon } from "components/Icons/MarkdownIcon"
4+
import { TerraformIcon } from "components/Icons/TerraformIcon"
5+
import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter"
6+
import { UseTabResult } from "hooks/useTab"
7+
import { FC } from "react"
8+
import { combineClasses } from "util/combineClasses"
9+
import { TemplateVersionFiles } from "util/templateVersion"
10+
11+
const iconByExtension: Record<string, JSX.Element> = {
12+
tf: <TerraformIcon />,
13+
md: <MarkdownIcon />,
14+
mkd: <MarkdownIcon />,
15+
Dockerfile: <DockerIcon />,
16+
}
17+
18+
const getExtension = (filename: string) => {
19+
if (filename.includes(".")) {
20+
const [_, extension] = filename.split(".")
21+
return extension
22+
}
23+
24+
return filename
25+
}
26+
27+
const languageByExtension: Record<string, string> = {
28+
tf: "hcl",
29+
md: "markdown",
30+
mkd: "markdown",
31+
Dockerfile: "dockerfile",
32+
}
33+
34+
export const TemplateFiles: FC<{
35+
currentFiles: TemplateVersionFiles
36+
previousFiles?: TemplateVersionFiles
37+
tab: UseTabResult
38+
}> = ({ currentFiles, previousFiles, tab }) => {
39+
const styles = useStyles()
40+
const filenames = Object.keys(currentFiles)
41+
const selectedFilename = filenames[Number(tab.value)]
42+
const currentFile = currentFiles[selectedFilename]
43+
const previousFile = previousFiles && previousFiles[selectedFilename]
44+
45+
return (
46+
<div className={styles.files}>
47+
<div className={styles.tabs}>
48+
{filenames.map((filename, index) => {
49+
const tabValue = index.toString()
50+
const extension = getExtension(filename)
51+
const icon = iconByExtension[extension]
52+
const hasDiff =
53+
previousFiles &&
54+
previousFiles[filename] &&
55+
currentFiles[filename] !== previousFiles[filename]
56+
57+
return (
58+
<button
59+
className={combineClasses({
60+
[styles.tab]: true,
61+
[styles.tabActive]: tabValue === tab.value,
62+
})}
63+
onClick={() => {
64+
tab.set(tabValue)
65+
}}
66+
key={filename}
67+
>
68+
{icon}
69+
{filename}
70+
{hasDiff && <div className={styles.tabDiff} />}
71+
</button>
72+
)
73+
})}
74+
</div>
75+
76+
<SyntaxHighlighter
77+
value={currentFile}
78+
compareWith={previousFile}
79+
language={languageByExtension[getExtension(selectedFilename)]}
80+
/>
81+
</div>
82+
)
83+
}
84+
const useStyles = makeStyles((theme) => ({
85+
tabs: {
86+
display: "flex",
87+
alignItems: "baseline",
88+
borderBottom: `1px solid ${theme.palette.divider}`,
89+
gap: 1,
90+
},
91+
92+
tab: {
93+
background: "transparent",
94+
border: 0,
95+
padding: theme.spacing(0, 3),
96+
display: "flex",
97+
alignItems: "center",
98+
height: theme.spacing(6),
99+
opacity: 0.85,
100+
cursor: "pointer",
101+
gap: theme.spacing(0.5),
102+
position: "relative",
103+
104+
"& svg": {
105+
width: 22,
106+
maxHeight: 16,
107+
},
108+
109+
"&:hover": {
110+
backgroundColor: theme.palette.action.hover,
111+
},
112+
},
113+
114+
tabActive: {
115+
opacity: 1,
116+
background: theme.palette.action.hover,
117+
118+
"&:after": {
119+
content: '""',
120+
display: "block",
121+
height: 1,
122+
width: "100%",
123+
bottom: 0,
124+
left: 0,
125+
backgroundColor: theme.palette.secondary.dark,
126+
position: "absolute",
127+
},
128+
},
129+
130+
tabDiff: {
131+
height: 6,
132+
width: 6,
133+
backgroundColor: theme.palette.warning.light,
134+
borderRadius: "100%",
135+
marginLeft: theme.spacing(0.5),
136+
},
137+
138+
codeWrapper: {
139+
background: theme.palette.background.paperLight,
140+
},
141+
142+
files: {
143+
borderRadius: theme.shape.borderRadius,
144+
border: `1px solid ${theme.palette.divider}`,
145+
},
146+
147+
prism: {
148+
borderRadius: 0,
149+
},
150+
}))

site/src/components/TemplateLayout/TemplateLayout.tsx

+61-40
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,56 @@
11
import { makeStyles } from "@material-ui/core/styles"
2-
import { useMachine } from "@xstate/react"
32
import { useOrganizationId } from "hooks/useOrganizationId"
43
import { createContext, FC, Suspense, useContext } from "react"
54
import { NavLink, Outlet, useNavigate, useParams } from "react-router-dom"
65
import { combineClasses } from "util/combineClasses"
7-
import {
8-
TemplateContext,
9-
templateMachine,
10-
} from "xServices/template/templateXService"
116
import { Margins } from "components/Margins/Margins"
127
import { Stack } from "components/Stack/Stack"
13-
import { Permissions } from "xServices/auth/authXService"
148
import { Loader } from "components/Loader/Loader"
15-
import { usePermissions } from "hooks/usePermissions"
169
import { TemplatePageHeader } from "./TemplatePageHeader"
1710
import { AlertBanner } from "components/AlertBanner/AlertBanner"
11+
import {
12+
checkAuthorization,
13+
getTemplateByName,
14+
getTemplateVersion,
15+
} from "api/api"
16+
import { useQuery } from "@tanstack/react-query"
17+
import { useDashboard } from "components/Dashboard/DashboardProvider"
18+
19+
const templatePermissions = (templateId: string) => ({
20+
canUpdateTemplate: {
21+
object: {
22+
resource_type: "template",
23+
resource_id: templateId,
24+
},
25+
action: "update",
26+
},
27+
})
1828

19-
const useTemplateName = () => {
20-
const { template } = useParams()
29+
const fetchTemplate = async (orgId: string, templateName: string) => {
30+
const template = await getTemplateByName(orgId, templateName)
31+
const [activeVersion, permissions] = await Promise.all([
32+
getTemplateVersion(template.active_version_id),
33+
checkAuthorization({
34+
checks: templatePermissions(template.id),
35+
}),
36+
])
2137

22-
if (!template) {
23-
throw new Error("No template found in the URL")
38+
return {
39+
template,
40+
activeVersion,
41+
permissions,
2442
}
25-
26-
return template
2743
}
2844

29-
type TemplateLayoutContextValue = {
30-
context: TemplateContext
31-
permissions?: Permissions
45+
const useTemplateData = (orgId: string, templateName: string) => {
46+
return useQuery({
47+
queryKey: ["template", templateName],
48+
queryFn: () => fetchTemplate(orgId, templateName),
49+
})
3250
}
3351

52+
type TemplateLayoutContextValue = Awaited<ReturnType<typeof fetchTemplate>>
53+
3454
const TemplateLayoutContext = createContext<
3555
TemplateLayoutContextValue | undefined
3656
>(undefined)
@@ -50,38 +70,30 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
5070
}) => {
5171
const navigate = useNavigate()
5272
const styles = useStyles()
53-
const organizationId = useOrganizationId()
54-
const templateName = useTemplateName()
55-
const [templateState, _] = useMachine(templateMachine, {
56-
context: {
57-
templateName,
58-
organizationId,
59-
},
60-
})
61-
const {
62-
template,
63-
permissions: templatePermissions,
64-
getTemplateError,
65-
} = templateState.context
66-
const permissions = usePermissions()
73+
const orgId = useOrganizationId()
74+
const { template } = useParams() as { template: string }
75+
const templateData = useTemplateData(orgId, template)
76+
const dashboard = useDashboard()
6777

68-
if (getTemplateError) {
78+
if (templateData.error) {
6979
return (
7080
<div className={styles.error}>
71-
<AlertBanner severity="error" error={getTemplateError} />
81+
<AlertBanner severity="error" error={templateData.error} />
7282
</div>
7383
)
7484
}
7585

76-
if (!template || !templatePermissions) {
86+
if (templateData.isLoading || !templateData.data) {
7787
return <Loader />
7888
}
7989

8090
return (
8191
<>
8292
<TemplatePageHeader
83-
template={template}
84-
permissions={templatePermissions}
93+
template={templateData.data.template}
94+
activeVersion={templateData.data.activeVersion}
95+
permissions={templateData.data.permissions}
96+
canEditFiles={dashboard.experiments.includes("template_editor")}
8597
onDeleteTemplate={() => {
8698
navigate("/templates")
8799
}}
@@ -92,7 +104,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
92104
<Stack direction="row" spacing={0.25}>
93105
<NavLink
94106
end
95-
to={`/templates/${template.name}`}
107+
to={`/templates/${template}`}
96108
className={({ isActive }) =>
97109
combineClasses([
98110
styles.tabItem,
@@ -103,7 +115,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
103115
Summary
104116
</NavLink>
105117
<NavLink
106-
to={`/templates/${template.name}/permissions`}
118+
to={`/templates/${template}/permissions`}
107119
className={({ isActive }) =>
108120
combineClasses([
109121
styles.tabItem,
@@ -113,14 +125,23 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
113125
>
114126
Permissions
115127
</NavLink>
128+
<NavLink
129+
to={`/templates/${template}/files`}
130+
className={({ isActive }) =>
131+
combineClasses([
132+
styles.tabItem,
133+
isActive ? styles.tabItemActive : undefined,
134+
])
135+
}
136+
>
137+
Source Code
138+
</NavLink>
116139
</Stack>
117140
</Margins>
118141
</div>
119142

120143
<Margins>
121-
<TemplateLayoutContext.Provider
122-
value={{ permissions, context: templateState.context }}
123-
>
144+
<TemplateLayoutContext.Provider value={templateData.data}>
124145
<Suspense fallback={<Loader />}>{children}</Suspense>
125146
</TemplateLayoutContext.Provider>
126147
</Margins>

site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ComponentMeta, Story } from "@storybook/react"
2-
import { MockTemplate } from "testHelpers/entities"
2+
import { MockTemplate, MockTemplateVersion } from "testHelpers/entities"
33
import {
44
TemplatePageHeader,
55
TemplatePageHeaderProps,
@@ -12,6 +12,9 @@ export default {
1212
template: {
1313
defaultValue: MockTemplate,
1414
},
15+
activeVersion: {
16+
defaultValue: MockTemplateVersion,
17+
},
1518
permissions: {
1619
defaultValue: {
1720
canUpdateTemplate: true,

0 commit comments

Comments
 (0)