Skip to content

Commit 0928011

Browse files
committed
Show source code in the template
1 parent 77592c7 commit 0928011

File tree

4 files changed

+241
-1
lines changed

4 files changed

+241
-1
lines changed

site/src/AppRouter.tsx

Lines changed: 4 additions & 1 deletion
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 />} />
Lines changed: 150 additions & 0 deletions
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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,17 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
130130
>
131131
Permissions
132132
</NavLink>
133+
<NavLink
134+
to={`/templates/${template}/files`}
135+
className={({ isActive }) =>
136+
combineClasses([
137+
styles.tabItem,
138+
isActive ? styles.tabItemActive : undefined,
139+
])
140+
}
141+
>
142+
Source Code
143+
</NavLink>
133144
</Stack>
134145
</Margins>
135146
</div>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useQuery } from "@tanstack/react-query"
2+
import { getPreviousTemplateVersionByName } from "api/api"
3+
import { TemplateVersion } from "api/typesGenerated"
4+
import { Loader } from "components/Loader/Loader"
5+
import { TemplateFiles } from "components/TemplateFiles/TemplateFiles"
6+
import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout"
7+
import { useOrganizationId } from "hooks/useOrganizationId"
8+
import { useTab } from "hooks/useTab"
9+
import { FC } from "react"
10+
import { Helmet } from "react-helmet-async"
11+
import { pageTitle } from "util/page"
12+
import { getTemplateVersionFiles } from "util/templateVersion"
13+
14+
const fetchTemplateFiles = async (
15+
organizationId: string,
16+
templateName: string,
17+
activeVersion: TemplateVersion,
18+
) => {
19+
const previousVersion = await getPreviousTemplateVersionByName(
20+
organizationId,
21+
templateName,
22+
activeVersion.name,
23+
)
24+
const loadFilesPromises: ReturnType<typeof getTemplateVersionFiles>[] = []
25+
loadFilesPromises.push(getTemplateVersionFiles(activeVersion))
26+
if (previousVersion) {
27+
loadFilesPromises.push(getTemplateVersionFiles(previousVersion))
28+
}
29+
const [currentFiles, previousFiles] = await Promise.all(loadFilesPromises)
30+
return {
31+
currentFiles,
32+
previousFiles,
33+
}
34+
}
35+
36+
const useTemplateFiles = (
37+
organizationId: string,
38+
templateName: string,
39+
activeVersion: TemplateVersion,
40+
) =>
41+
useQuery({
42+
queryKey: ["templateFiles", templateName],
43+
queryFn: () =>
44+
fetchTemplateFiles(organizationId, templateName, activeVersion),
45+
})
46+
47+
const TemplateFilesPage: FC = () => {
48+
const { template, activeVersion } = useTemplateLayoutContext()
49+
const orgId = useOrganizationId()
50+
const tab = useTab("file", "0")
51+
const { data: templateFiles } = useTemplateFiles(
52+
orgId,
53+
template.name,
54+
activeVersion,
55+
)
56+
57+
return (
58+
<>
59+
<Helmet>
60+
<title>{pageTitle(`${template?.name} · Source Code`)}</title>
61+
</Helmet>
62+
63+
{templateFiles ? (
64+
<TemplateFiles
65+
currentFiles={templateFiles.currentFiles}
66+
previousFiles={templateFiles.previousFiles}
67+
tab={tab}
68+
/>
69+
) : (
70+
<Loader />
71+
)}
72+
</>
73+
)
74+
}
75+
76+
export default TemplateFilesPage

0 commit comments

Comments
 (0)