Skip to content

Commit ce76d9d

Browse files
feat: Add diff and Dockerfile support for template version page (#5339)
1 parent f68a656 commit ce76d9d

18 files changed

+606
-153
lines changed

coderd/database/databasefake/databasefake.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,6 +1743,13 @@ func (q *fakeQuerier) GetPreviousTemplateVersion(_ context.Context, arg database
17431743
if templateVersion.ID == currentTemplateVersion.ID {
17441744
continue
17451745
}
1746+
if templateVersion.OrganizationID != arg.OrganizationID {
1747+
continue
1748+
}
1749+
if templateVersion.TemplateID != currentTemplateVersion.TemplateID {
1750+
continue
1751+
}
1752+
17461753
if templateVersion.CreatedAt.Before(currentTemplateVersion.CreatedAt) {
17471754
previousTemplateVersions = append(previousTemplateVersions, templateVersion)
17481755
}

coderd/database/queries.sql.go

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

coderd/database/queries/templateversions.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,5 +122,7 @@ WHERE
122122
FROM template_versions AS tv
123123
WHERE tv.organization_id = $1 AND tv.name = $2 AND tv.template_id = $3
124124
)
125+
AND organization_id = $1
126+
AND template_id = $3
125127
ORDER BY created_at DESC
126128
LIMIT 1;

coderd/templateversions_test.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -970,13 +970,24 @@ func TestPreviousTemplateVersion(t *testing.T) {
970970
t.Parallel()
971971
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
972972
user := coderdtest.CreateFirstUser(t, client)
973-
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
974-
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
973+
974+
// Create two templates to be sure it is not returning a previous version
975+
// from another template
976+
templateAVersion1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
977+
coderdtest.CreateTemplate(t, client, user.OrganizationID, templateAVersion1.ID)
978+
coderdtest.AwaitTemplateVersionJob(t, client, templateAVersion1.ID)
979+
// Create two versions for the template B to be sure if we try to get the
980+
// previous version of the first version it will returns a 404
981+
templateBVersion1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
982+
templateB := coderdtest.CreateTemplate(t, client, user.OrganizationID, templateBVersion1.ID)
983+
coderdtest.AwaitTemplateVersionJob(t, client, templateBVersion1.ID)
984+
templateBVersion2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, nil, templateB.ID)
985+
coderdtest.AwaitTemplateVersionJob(t, client, templateBVersion2.ID)
975986

976987
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
977988
defer cancel()
978989

979-
_, err := client.PreviousTemplateVersion(ctx, user.OrganizationID, version.Name)
990+
_, err := client.PreviousTemplateVersion(ctx, user.OrganizationID, templateBVersion1.Name)
980991
var apiErr *codersdk.Error
981992
require.ErrorAs(t, err, &apiErr)
982993
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
@@ -986,17 +997,25 @@ func TestPreviousTemplateVersion(t *testing.T) {
986997
t.Parallel()
987998
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
988999
user := coderdtest.CreateFirstUser(t, client)
989-
previousVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
990-
coderdtest.AwaitTemplateVersionJob(t, client, previousVersion.ID)
991-
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, previousVersion.ID)
992-
latestVersion := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, nil, template.ID)
993-
coderdtest.AwaitTemplateVersionJob(t, client, latestVersion.ID)
1000+
1001+
// Create two templates to be sure it is not returning a previous version
1002+
// from another template
1003+
templateAVersion1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
1004+
coderdtest.CreateTemplate(t, client, user.OrganizationID, templateAVersion1.ID)
1005+
coderdtest.AwaitTemplateVersionJob(t, client, templateAVersion1.ID)
1006+
// Create two versions for the template B so we can try to get the previous
1007+
// version of version 2
1008+
templateBVersion1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
1009+
templateB := coderdtest.CreateTemplate(t, client, user.OrganizationID, templateBVersion1.ID)
1010+
coderdtest.AwaitTemplateVersionJob(t, client, templateBVersion1.ID)
1011+
templateBVersion2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, nil, templateB.ID)
1012+
coderdtest.AwaitTemplateVersionJob(t, client, templateBVersion2.ID)
9941013

9951014
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
9961015
defer cancel()
9971016

998-
result, err := client.PreviousTemplateVersion(ctx, user.OrganizationID, latestVersion.Name)
1017+
result, err := client.PreviousTemplateVersion(ctx, user.OrganizationID, templateBVersion2.Name)
9991018
require.NoError(t, err)
1000-
require.Equal(t, previousVersion.ID, result.ID)
1019+
require.Equal(t, templateBVersion1.ID, result.ID)
10011020
})
10021021
}

site/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,18 @@
3232
"@material-ui/core": "4.12.1",
3333
"@material-ui/icons": "4.5.1",
3434
"@material-ui/lab": "4.0.0-alpha.42",
35+
"@monaco-editor/react": "4.4.6",
3536
"@testing-library/react-hooks": "8.0.1",
36-
"@types/color-convert": "^2.0.0",
37-
"@types/react-color": "^3.0.6",
37+
"@types/color-convert": "2.0.0",
38+
"@types/react-color": "3.0.6",
3839
"@vitejs/plugin-react": "2.1.0",
3940
"@xstate/inspect": "0.6.5",
4041
"@xstate/react": "3.0.1",
4142
"axios": "0.26.1",
4243
"can-ndjson-stream": "1.0.2",
4344
"chart.js": "3.9.1",
4445
"chartjs-adapter-date-fns": "2.0.0",
45-
"color-convert": "^2.0.1",
46+
"color-convert": "2.0.1",
4647
"cron-parser": "4.7.0",
4748
"cronstrue": "2.14.0",
4849
"date-fns": "2.29.3",
@@ -57,13 +58,12 @@
5758
"just-debounce-it": "3.1.1",
5859
"react": "18.2.0",
5960
"react-chartjs-2": "4.3.1",
60-
"react-color": "^2.19.3",
61+
"react-color": "2.19.3",
6162
"react-dom": "18.2.0",
6263
"react-helmet-async": "1.3.0",
6364
"react-i18next": "12.0.0",
6465
"react-markdown": "8.0.3",
6566
"react-router-dom": "6.4.1",
66-
"react-syntax-highlighter": "15.5.0",
6767
"remark-gfm": "3.0.1",
6868
"sourcemapped-stacktrace": "1.1.11",
6969
"tzdata": "1.0.30",

site/src/api/api.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,34 @@ export const getTemplateVersionByName = async (
231231
return response.data
232232
}
233233

234+
export type GetPreviousTemplateVersionByNameResponse =
235+
| TypesGen.TemplateVersion
236+
| undefined
237+
238+
export const getPreviousTemplateVersionByName = async (
239+
organizationId: string,
240+
versionName: string,
241+
): Promise<GetPreviousTemplateVersionByNameResponse> => {
242+
try {
243+
const response = await axios.get<TypesGen.TemplateVersion>(
244+
`/api/v2/organizations/${organizationId}/templateversions/${versionName}/previous`,
245+
)
246+
return response.data
247+
} catch (error) {
248+
// When there is no previous version, like the first version of a template,
249+
// the API returns 404 so in this case we can safely return undefined
250+
if (
251+
axios.isAxiosError(error) &&
252+
error.response &&
253+
error.response.status === 404
254+
) {
255+
return undefined
256+
}
257+
258+
throw error
259+
}
260+
}
261+
234262
export const updateTemplateMeta = async (
235263
templateId: string,
236264
data: TypesGen.UpdateTemplateMeta,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
2+
3+
export const DockerIcon = (props: SvgIconProps): JSX.Element => (
4+
<SvgIcon {...props} viewBox="0 0 32 32">
5+
<path
6+
d="M16.54,12.663H19.4v2.924h1.446a6.272,6.272,0,0,0,1.988-.333,5.091,5.091,0,0,0,.966-.436,3.584,3.584,0,0,1-.67-1.849,3.907,3.907,0,0,1,.7-2.753l.3-.348.358.288a4.558,4.558,0,0,1,1.795,2.892,4.375,4.375,0,0,1,3.319.309l.393.226-.207.4a4.141,4.141,0,0,1-4.157,1.983c-2.48,6.168-7.871,9.088-14.409,9.088-3.378,0-6.476-1.263-8.241-4.259l-.029-.049L2.7,20.227a8.316,8.316,0,0,1-.659-4.208l.04-.433H4.526V12.663H7.387V9.8h5.721V6.942H16.54v5.721Z"
7+
style={{ fill: "#0096e6" }}
8+
/>
9+
<path
10+
d="M12.006,24.567a6.022,6.022,0,0,1-3.14-3.089,10.329,10.329,0,0,1-2.264.343q-.5.028-1.045.028-.632,0-1.331-.037a9.051,9.051,0,0,0,7,2.769Q11.618,24.581,12.006,24.567Z"
11+
style={{ fill: "#fff" }}
12+
/>
13+
<path
14+
d="M7.08,13.346h.2v2.067h-.2Zm-.376,0h.2v2.067H6.7V13.346Zm-.376,0h.2v2.067h-.2Zm-.376,0h.2v2.067h-.2Zm-.376,0h.2v2.067h-.2Zm-.368,0h.2v2.067h-.2V13.346ZM5,13.14H7.482v2.479H5Zm2.859-2.861h2.48v2.479H7.863Zm2.077.207h.2v2.066h-.2Zm-.376,0h.2v2.066h-.2Zm-.376,0h.2v2.066h-.2V10.486Zm-.376,0h.2v2.066h-.2Zm-.376,0h.2v2.066h-.2Zm-.368,0h.2v2.066h-.2Zm-.207,2.653h2.48v2.48H7.863V13.14Zm2.077.207h.2v2.067h-.2Zm-.376,0h.2v2.067h-.2Zm-.376,0h.2v2.067h-.2V13.346Zm-.376,0h.2v2.067h-.2Zm-.376,0h.2v2.067h-.2Zm-.368,0h.2v2.067h-.2Zm2.654-.207H13.2v2.48h-2.48V13.14Zm2.076.207H13v2.067h-.2Zm-.376,0h.2v2.067h-.2Zm-.376,0h.2v2.067h-.2Zm-.376,0h.2v2.067h-.2Zm-.376,0h.2v2.067h-.2Zm-.368,0h.2v2.067h-.2Zm-.206-3.067H13.2v2.479h-2.48V10.279Zm2.076.207H13v2.066h-.2Zm-.376,0h.2v2.066h-.2Zm-.376,0h.2v2.066h-.2Zm-.376,0h.2v2.066h-.2Zm-.376,0h.2v2.066h-.2Zm-.368,0h.2v2.066h-.2Zm2.654,2.653h2.479v2.48h-2.48V13.14Zm2.076.207h.2v2.067h-.2Zm-.376,0h.2v2.067h-.2Zm-.376,0h.2v2.067h-.2Zm-.376,0h.2v2.067h-.2Zm-.376,0h.2v2.067h-.2Zm-.368,0h.192v2.067h-.2V13.346Zm-.206-3.067h2.479v2.479h-2.48V10.279Zm2.076.207h.2v2.066h-.2Zm-.376,0h.2v2.066h-.2Zm-.376,0h.2v2.066h-.2Zm-.376,0h.2v2.066h-.2Zm-.376,0h.2v2.066h-.2Zm-.368,0h.192v2.066h-.2V10.486Zm-.206-3.067h2.479V9.9h-2.48V7.419Zm2.076.206h.2V9.691h-.2Zm-.376,0h.2V9.691h-.2Zm-.376,0h.2V9.691h-.2Zm-.376,0h.2V9.691h-.2Zm-.376,0h.2V9.691h-.2Zm-.368,0h.192V9.691h-.2V7.625Zm2.654,5.514h2.479v2.48h-2.48V13.14Zm2.076.207h.195v2.067h-.2V13.346Zm-.376,0h.206v2.067h-.206Zm-.376,0h.2v2.067h-.2Zm-.376,0h.2v2.067h-.2Zm-.376,0h.2v2.067h-.205V13.346Zm-.368,0h.2v2.067h-.194V13.346Z"
15+
style={{ fill: "#fff" }}
16+
/>
17+
<path
18+
d="M10.188,19.638a.684.684,0,1,1-.684.684A.684.684,0,0,1,10.188,19.638Zm0,.194a.489.489,0,0,1,.177.033.2.2,0,1,0,.275.269.49.49,0,1,1-.453-.3Z"
19+
style={{ fill: "#fff" }}
20+
/>
21+
</SvgIcon>
22+
)
Lines changed: 36 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,47 @@
1+
import { FC } from "react"
2+
import Editor, { DiffEditor } from "@monaco-editor/react"
3+
import { useCoderTheme } from "./coderTheme"
14
import { makeStyles } from "@material-ui/core/styles"
2-
import { ComponentProps, FC } from "react"
3-
import { Prism } from "react-syntax-highlighter"
4-
import { colors } from "theme/colors"
5-
import darcula from "react-syntax-highlighter/dist/cjs/styles/prism/darcula"
6-
import { combineClasses } from "util/combineClasses"
75

8-
export const SyntaxHighlighter: FC<ComponentProps<typeof Prism>> = ({
9-
className,
10-
...props
11-
}) => {
6+
export const SyntaxHighlighter: FC<{
7+
value: string
8+
language: string
9+
compareWith?: string
10+
}> = ({ value, compareWith, language }) => {
1211
const styles = useStyles()
12+
const hasDiff = compareWith && value !== compareWith
13+
const coderTheme = useCoderTheme()
14+
const commonProps = {
15+
language,
16+
theme: coderTheme.name,
17+
height: 560,
18+
options: {
19+
minimap: {
20+
enabled: false,
21+
},
22+
renderSideBySide: true,
23+
readOnly: true,
24+
},
25+
}
26+
27+
if (coderTheme.isLoading) {
28+
return null
29+
}
1330

1431
return (
15-
<Prism
16-
style={darcula}
17-
useInlineStyles={false}
18-
// Use inline styles does not work correctly
19-
// https://github.com/react-syntax-highlighter/react-syntax-highlighter/issues/329
20-
codeTagProps={{ style: {} }}
21-
className={combineClasses([styles.prism, className])}
22-
{...props}
23-
/>
32+
<div className={styles.wrapper}>
33+
{hasDiff ? (
34+
<DiffEditor original={compareWith} modified={value} {...commonProps} />
35+
) : (
36+
<Editor value={value} {...commonProps} />
37+
)}
38+
</div>
2439
)
2540
}
2641

2742
const useStyles = makeStyles((theme) => ({
28-
prism: {
29-
margin: 0,
30-
background: theme.palette.background.paperLight,
31-
borderRadius: theme.shape.borderRadius,
32-
padding: theme.spacing(2, 3),
33-
// Line breaks are broken when used with line numbers on react-syntax-highlighter
34-
// https://github.com/react-syntax-highlighter/react-syntax-highlighter/pull/483
35-
overflowX: "auto",
36-
37-
"& code": {
38-
color: theme.palette.text.secondary,
39-
},
40-
41-
"& .key, & .property, & .code-snippet, & .keyword": {
42-
color: colors.turquoise[7],
43-
},
44-
45-
"& .url": {
46-
color: colors.blue[6],
47-
},
48-
49-
"& .comment": {
50-
color: theme.palette.text.disabled,
51-
},
52-
53-
"& .title": {
54-
color: theme.palette.text.primary,
55-
fontWeight: 600,
56-
},
43+
wrapper: {
44+
padding: theme.spacing(1, 0),
45+
background: theme.palette.background.paper,
5746
},
5847
}))

0 commit comments

Comments
 (0)