diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index daea8c7afe..554f00f5ce 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1743,6 +1743,13 @@ func (q *fakeQuerier) GetPreviousTemplateVersion(_ context.Context, arg database if templateVersion.ID == currentTemplateVersion.ID { continue } + if templateVersion.OrganizationID != arg.OrganizationID { + continue + } + if templateVersion.TemplateID != currentTemplateVersion.TemplateID { + continue + } + if templateVersion.CreatedAt.Before(currentTemplateVersion.CreatedAt) { previousTemplateVersions = append(previousTemplateVersions, templateVersion) } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f622b1907b..96bc873acb 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3532,6 +3532,8 @@ WHERE FROM template_versions AS tv WHERE tv.organization_id = $1 AND tv.name = $2 AND tv.template_id = $3 ) + AND organization_id = $1 + AND template_id = $3 ORDER BY created_at DESC LIMIT 1 ` diff --git a/coderd/database/queries/templateversions.sql b/coderd/database/queries/templateversions.sql index 721a77f939..d49d86bf56 100644 --- a/coderd/database/queries/templateversions.sql +++ b/coderd/database/queries/templateversions.sql @@ -122,5 +122,7 @@ WHERE FROM template_versions AS tv WHERE tv.organization_id = $1 AND tv.name = $2 AND tv.template_id = $3 ) + AND organization_id = $1 + AND template_id = $3 ORDER BY created_at DESC LIMIT 1; diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index a17c39184c..d6e97d19ad 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -970,13 +970,24 @@ func TestPreviousTemplateVersion(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + + // Create two templates to be sure it is not returning a previous version + // from another template + templateAVersion1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.CreateTemplate(t, client, user.OrganizationID, templateAVersion1.ID) + coderdtest.AwaitTemplateVersionJob(t, client, templateAVersion1.ID) + // Create two versions for the template B to be sure if we try to get the + // previous version of the first version it will returns a 404 + templateBVersion1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + templateB := coderdtest.CreateTemplate(t, client, user.OrganizationID, templateBVersion1.ID) + coderdtest.AwaitTemplateVersionJob(t, client, templateBVersion1.ID) + templateBVersion2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, nil, templateB.ID) + coderdtest.AwaitTemplateVersionJob(t, client, templateBVersion2.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - _, err := client.PreviousTemplateVersion(ctx, user.OrganizationID, version.Name) + _, err := client.PreviousTemplateVersion(ctx, user.OrganizationID, templateBVersion1.Name) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) @@ -986,17 +997,25 @@ func TestPreviousTemplateVersion(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user := coderdtest.CreateFirstUser(t, client) - previousVersion := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - coderdtest.AwaitTemplateVersionJob(t, client, previousVersion.ID) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, previousVersion.ID) - latestVersion := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, nil, template.ID) - coderdtest.AwaitTemplateVersionJob(t, client, latestVersion.ID) + + // Create two templates to be sure it is not returning a previous version + // from another template + templateAVersion1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.CreateTemplate(t, client, user.OrganizationID, templateAVersion1.ID) + coderdtest.AwaitTemplateVersionJob(t, client, templateAVersion1.ID) + // Create two versions for the template B so we can try to get the previous + // version of version 2 + templateBVersion1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + templateB := coderdtest.CreateTemplate(t, client, user.OrganizationID, templateBVersion1.ID) + coderdtest.AwaitTemplateVersionJob(t, client, templateBVersion1.ID) + templateBVersion2 := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, nil, templateB.ID) + coderdtest.AwaitTemplateVersionJob(t, client, templateBVersion2.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - result, err := client.PreviousTemplateVersion(ctx, user.OrganizationID, latestVersion.Name) + result, err := client.PreviousTemplateVersion(ctx, user.OrganizationID, templateBVersion2.Name) require.NoError(t, err) - require.Equal(t, previousVersion.ID, result.ID) + require.Equal(t, templateBVersion1.ID, result.ID) }) } diff --git a/site/package.json b/site/package.json index 96bd943824..dc9457a0da 100644 --- a/site/package.json +++ b/site/package.json @@ -32,9 +32,10 @@ "@material-ui/core": "4.12.1", "@material-ui/icons": "4.5.1", "@material-ui/lab": "4.0.0-alpha.42", + "@monaco-editor/react": "4.4.6", "@testing-library/react-hooks": "8.0.1", - "@types/color-convert": "^2.0.0", - "@types/react-color": "^3.0.6", + "@types/color-convert": "2.0.0", + "@types/react-color": "3.0.6", "@vitejs/plugin-react": "2.1.0", "@xstate/inspect": "0.6.5", "@xstate/react": "3.0.1", @@ -42,7 +43,7 @@ "can-ndjson-stream": "1.0.2", "chart.js": "3.9.1", "chartjs-adapter-date-fns": "2.0.0", - "color-convert": "^2.0.1", + "color-convert": "2.0.1", "cron-parser": "4.7.0", "cronstrue": "2.14.0", "date-fns": "2.29.3", @@ -57,13 +58,12 @@ "just-debounce-it": "3.1.1", "react": "18.2.0", "react-chartjs-2": "4.3.1", - "react-color": "^2.19.3", + "react-color": "2.19.3", "react-dom": "18.2.0", "react-helmet-async": "1.3.0", "react-i18next": "12.0.0", "react-markdown": "8.0.3", "react-router-dom": "6.4.1", - "react-syntax-highlighter": "15.5.0", "remark-gfm": "3.0.1", "sourcemapped-stacktrace": "1.1.11", "tzdata": "1.0.30", diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 77881909cb..ad3eb9ed28 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -231,6 +231,34 @@ export const getTemplateVersionByName = async ( return response.data } +export type GetPreviousTemplateVersionByNameResponse = + | TypesGen.TemplateVersion + | undefined + +export const getPreviousTemplateVersionByName = async ( + organizationId: string, + versionName: string, +): Promise => { + try { + const response = await axios.get( + `/api/v2/organizations/${organizationId}/templateversions/${versionName}/previous`, + ) + return response.data + } catch (error) { + // When there is no previous version, like the first version of a template, + // the API returns 404 so in this case we can safely return undefined + if ( + axios.isAxiosError(error) && + error.response && + error.response.status === 404 + ) { + return undefined + } + + throw error + } +} + export const updateTemplateMeta = async ( templateId: string, data: TypesGen.UpdateTemplateMeta, diff --git a/site/src/components/Icons/DockerIcon.tsx b/site/src/components/Icons/DockerIcon.tsx new file mode 100644 index 0000000000..086a73c309 --- /dev/null +++ b/site/src/components/Icons/DockerIcon.tsx @@ -0,0 +1,22 @@ +import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon" + +export const DockerIcon = (props: SvgIconProps): JSX.Element => ( + + + + + + +) diff --git a/site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx b/site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx index 88d494b17c..5eefc1b60d 100644 --- a/site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx +++ b/site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx @@ -1,58 +1,47 @@ +import { FC } from "react" +import Editor, { DiffEditor } from "@monaco-editor/react" +import { useCoderTheme } from "./coderTheme" import { makeStyles } from "@material-ui/core/styles" -import { ComponentProps, FC } from "react" -import { Prism } from "react-syntax-highlighter" -import { colors } from "theme/colors" -import darcula from "react-syntax-highlighter/dist/cjs/styles/prism/darcula" -import { combineClasses } from "util/combineClasses" -export const SyntaxHighlighter: FC> = ({ - className, - ...props -}) => { +export const SyntaxHighlighter: FC<{ + value: string + language: string + compareWith?: string +}> = ({ value, compareWith, language }) => { const styles = useStyles() + const hasDiff = compareWith && value !== compareWith + const coderTheme = useCoderTheme() + const commonProps = { + language, + theme: coderTheme.name, + height: 560, + options: { + minimap: { + enabled: false, + }, + renderSideBySide: true, + readOnly: true, + }, + } + + if (coderTheme.isLoading) { + return null + } return ( - +
+ {hasDiff ? ( + + ) : ( + + )} +
) } const useStyles = makeStyles((theme) => ({ - prism: { - margin: 0, - background: theme.palette.background.paperLight, - borderRadius: theme.shape.borderRadius, - padding: theme.spacing(2, 3), - // Line breaks are broken when used with line numbers on react-syntax-highlighter - // https://github.com/react-syntax-highlighter/react-syntax-highlighter/pull/483 - overflowX: "auto", - - "& code": { - color: theme.palette.text.secondary, - }, - - "& .key, & .property, & .code-snippet, & .keyword": { - color: colors.turquoise[7], - }, - - "& .url": { - color: colors.blue[6], - }, - - "& .comment": { - color: theme.palette.text.disabled, - }, - - "& .title": { - color: theme.palette.text.primary, - fontWeight: 600, - }, + wrapper: { + padding: theme.spacing(1, 0), + background: theme.palette.background.paper, }, })) diff --git a/site/src/components/SyntaxHighlighter/coderTheme.ts b/site/src/components/SyntaxHighlighter/coderTheme.ts new file mode 100644 index 0000000000..d525ccdddf --- /dev/null +++ b/site/src/components/SyntaxHighlighter/coderTheme.ts @@ -0,0 +1,239 @@ +import { Theme, useTheme } from "@material-ui/core/styles" +import { useMonaco } from "@monaco-editor/react" +import { useEffect, useState } from "react" +import { hslToHex } from "util/colors" + +// Theme based on https://github.com/brijeshb42/monaco-themes/blob/master/themes/Dracula.json +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- The theme is not typed +export const coderTheme = (theme: Theme): Record => ({ + base: "vs-dark", + inherit: true, + rules: [ + { + background: "282a36", + token: "", + }, + { + foreground: "6272a4", + token: "comment", + }, + { + foreground: "f1fa8c", + token: "string", + }, + { + foreground: "bd93f9", + token: "constant.numeric", + }, + { + foreground: "bd93f9", + token: "constant.language", + }, + { + foreground: "bd93f9", + token: "constant.character", + }, + { + foreground: "bd93f9", + token: "constant.other", + }, + { + foreground: "ffb86c", + token: "variable.other.readwrite.instance", + }, + { + foreground: "ff79c6", + token: "constant.character.escaped", + }, + { + foreground: "ff79c6", + token: "constant.character.escape", + }, + { + foreground: "ff79c6", + token: "string source", + }, + { + foreground: "ff79c6", + token: "string source.ruby", + }, + { + foreground: "ff79c6", + token: "keyword", + }, + { + foreground: "ff79c6", + token: "storage", + }, + { + foreground: "8be9fd", + fontStyle: "italic", + token: "storage.type", + }, + { + foreground: "50fa7b", + fontStyle: "underline", + token: "entity.name.class", + }, + { + foreground: "50fa7b", + fontStyle: "italic underline", + token: "entity.other.inherited-class", + }, + { + foreground: "50fa7b", + token: "entity.name.function", + }, + { + foreground: "ffb86c", + fontStyle: "italic", + token: "variable.parameter", + }, + { + foreground: "ff79c6", + token: "entity.name.tag", + }, + { + foreground: "50fa7b", + token: "entity.other.attribute-name", + }, + { + foreground: "8be9fd", + token: "support.function", + }, + { + foreground: "6be5fd", + token: "support.constant", + }, + { + foreground: "66d9ef", + fontStyle: " italic", + token: "support.type", + }, + { + foreground: "66d9ef", + fontStyle: " italic", + token: "support.class", + }, + { + foreground: "f8f8f0", + background: "ff79c6", + token: "invalid", + }, + { + foreground: "f8f8f0", + background: "bd93f9", + token: "invalid.deprecated", + }, + { + foreground: "cfcfc2", + token: "meta.structure.dictionary.json string.quoted.double.json", + }, + { + foreground: "6272a4", + token: "meta.diff", + }, + { + foreground: "6272a4", + token: "meta.diff.header", + }, + { + foreground: "ff79c6", + token: "markup.deleted", + }, + { + foreground: "50fa7b", + token: "markup.inserted", + }, + { + foreground: "e6db74", + token: "markup.changed", + }, + { + foreground: "bd93f9", + token: "constant.numeric.line-number.find-in-files - match", + }, + { + foreground: "e6db74", + token: "entity.name.filename", + }, + { + foreground: "f83333", + token: "message.error", + }, + { + foreground: "eeeeee", + token: + "punctuation.definition.string.begin.json - meta.structure.dictionary.value.json", + }, + { + foreground: "eeeeee", + token: + "punctuation.definition.string.end.json - meta.structure.dictionary.value.json", + }, + { + foreground: "8be9fd", + token: "meta.structure.dictionary.json string.quoted.double.json", + }, + { + foreground: "f1fa8c", + token: "meta.structure.dictionary.value.json string.quoted.double.json", + }, + { + foreground: "50fa7b", + token: + "meta meta meta meta meta meta meta.structure.dictionary.value string", + }, + { + foreground: "ffb86c", + token: "meta meta meta meta meta meta.structure.dictionary.value string", + }, + { + foreground: "ff79c6", + token: "meta meta meta meta meta.structure.dictionary.value string", + }, + { + foreground: "bd93f9", + token: "meta meta meta meta.structure.dictionary.value string", + }, + { + foreground: "50fa7b", + token: "meta meta meta.structure.dictionary.value string", + }, + { + foreground: "ffb86c", + token: "meta meta.structure.dictionary.value string", + }, + ], + colors: { + "editor.foreground": hslToHex(theme.palette.text.primary), + "editor.background": hslToHex(theme.palette.background.paper), + "editor.selectionBackground": hslToHex(theme.palette.action.hover), + "editor.lineHighlightBackground": hslToHex( + theme.palette.background.paperLight, + ), + "editorCursor.foreground": "#f8f8f0", + "editorWhitespace.foreground": "#3B3A32", + "editorIndentGuide.activeBackground": "#9D550FB0", + "editor.selectionHighlightBorder": "#222218", + }, +}) + +export const useCoderTheme = (): { isLoading: boolean; name: string } => { + const [isLoading, setIsLoading] = useState(true) + const monaco = useMonaco() + const theme = useTheme() + const name = "coder" + + useEffect(() => { + if (monaco) { + monaco.editor.defineTheme(name, coderTheme(theme)) + setIsLoading(false) + } + }, [monaco, theme]) + + return { + isLoading, + name, + } +} diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPage.test.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPage.test.tsx index fe5a8dae7b..fc79077f3b 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPage.test.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPage.test.tsx @@ -6,7 +6,6 @@ import TemplateVersionPage from "./TemplateVersionPage" import * as templateVersionUtils from "util/templateVersion" import { screen } from "@testing-library/react" import * as CreateDayString from "util/createDayString" -import userEvent from "@testing-library/user-event" const TEMPLATE_NAME = "coder-ts" const VERSION_NAME = "12345" @@ -20,7 +19,7 @@ const TEMPLATE_VERSION_FILES = { const setup = async () => { jest .spyOn(templateVersionUtils, "getTemplateVersionFiles") - .mockResolvedValueOnce(TEMPLATE_VERSION_FILES) + .mockResolvedValue(TEMPLATE_VERSION_FILES) jest .spyOn(CreateDayString, "createDayString") @@ -40,11 +39,4 @@ describe("TemplateVersionPage", () => { expect(screen.queryByText(TERRAFORM_FILENAME)).toBeInTheDocument() expect(screen.queryByText(README_FILENAME)).toBeInTheDocument() }) - - it("shows the right content when click on the file name", async () => { - await userEvent.click(screen.getByText(README_FILENAME)) - expect( - screen.queryByText(TEMPLATE_VERSION_FILES[README_FILENAME]), - ).toBeInTheDocument() - }) }) diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.stories.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.stories.tsx index eaee1b5ebd..caefc6a572 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.stories.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.stories.tsx @@ -44,8 +44,8 @@ const defaultArgs = { context: { orgId: MockOrganization.id, versionName: MockTemplateVersion.name, - version: MockTemplateVersion, - files: { + currentVersion: MockTemplateVersion, + currentFiles: { "README.md": readmeContent, "main.tf": `{}`, }, @@ -60,8 +60,8 @@ Error.args = { ...defaultArgs, context: { ...defaultArgs.context, - version: undefined, - files: undefined, + currentVersion: undefined, + currentFiles: undefined, error: makeMockApiError({ message: "Error on loading the template version", }), diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx index 10ab59f19f..144f24d0e5 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx @@ -11,7 +11,6 @@ import { } from "components/PageHeader/PageHeader" import { Stack } from "components/Stack/Stack" import { Stats, StatsItem } from "components/Stats/Stats" -import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter" import { UseTabResult } from "hooks/useTab" import { FC } from "react" import { useTranslation } from "react-i18next" @@ -19,7 +18,83 @@ import { Link } from "react-router-dom" import { combineClasses } from "util/combineClasses" import { createDayString } from "util/createDayString" import { TemplateVersionMachineContext } from "xServices/templateVersion/templateVersionXService" +import { TemplateVersionFiles } from "util/templateVersion" +import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter" +import { DockerIcon } from "components/Icons/DockerIcon" +const iconByExtension: Record = { + tf: , + md: , + mkd: , + Dockerfile: , +} + +const getExtension = (filename: string) => { + if (filename.includes(".")) { + const [_, extension] = filename.split(".") + return extension + } + + return filename +} + +const languageByExtension: Record = { + tf: "hcl", + md: "markdown", + mkd: "markdown", + Dockerfile: "dockerfile", +} + +const Files: FC<{ + currentFiles: TemplateVersionFiles + previousFiles?: TemplateVersionFiles + tab: UseTabResult +}> = ({ currentFiles, previousFiles, tab }) => { + const styles = useStyles() + const filenames = Object.keys(currentFiles) + const selectedFilename = filenames[Number(tab.value)] + const currentFile = currentFiles[selectedFilename] + const previousFile = previousFiles && previousFiles[selectedFilename] + + return ( +
+
+ {filenames.map((filename, index) => { + const tabValue = index.toString() + const extension = getExtension(filename) + const icon = iconByExtension[extension] + const hasDiff = + previousFiles && + previousFiles[filename] && + currentFiles[filename] !== previousFiles[filename] + + return ( + + ) + })} +
+ + +
+ ) +} export interface TemplateVersionPageViewProps { /** * Used to display the version name before loading the version in the API @@ -36,8 +111,7 @@ export const TemplateVersionPageView: FC = ({ versionName, templateName, }) => { - const styles = useStyles() - const { files, error, version } = context + const { currentFiles, error, currentVersion, previousFiles } = context const { t } = useTranslation("templateVersionPage") return ( @@ -47,11 +121,11 @@ export const TemplateVersionPageView: FC = ({ {versionName} - {!files && !error && } + {!currentFiles && !error && } {Boolean(error) && } - {version && files && ( + {currentVersion && currentFiles && ( <> = ({ /> -
-
- {Object.keys(files).map((filename, index) => { - const tabValue = index.toString() - - return ( - - ) - })} -
- - - {Object.values(files)[Number(tab.value)]} - -
+ )}
@@ -125,6 +165,7 @@ const useStyles = makeStyles((theme) => ({ display: "flex", alignItems: "baseline", borderBottom: `1px solid ${theme.palette.divider}`, + gap: 1, }, tab: { @@ -134,7 +175,7 @@ const useStyles = makeStyles((theme) => ({ display: "flex", alignItems: "center", height: theme.spacing(6), - opacity: 0.75, + opacity: 0.85, cursor: "pointer", gap: theme.spacing(0.5), position: "relative", @@ -151,7 +192,7 @@ const useStyles = makeStyles((theme) => ({ tabActive: { opacity: 1, - fontWeight: 600, + background: theme.palette.action.hover, "&:after": { content: '""', @@ -165,6 +206,14 @@ const useStyles = makeStyles((theme) => ({ }, }, + tabDiff: { + height: 6, + width: 6, + backgroundColor: theme.palette.warning.light, + borderRadius: "100%", + marginLeft: theme.spacing(0.5), + }, + codeWrapper: { background: theme.palette.background.paperLight, }, diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 3c44d2ba94..06a577ec48 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -77,6 +77,12 @@ export const handlers = [ return res(ctx.status(200), ctx.json(M.MockTemplateVersion)) }, ), + rest.get( + "api/v2/organizations/:organizationId/templateversions/:templateVersionName/previous", + async (req, res, ctx) => { + return res(ctx.status(200), ctx.json(M.MockTemplateVersion2)) + }, + ), rest.delete("/api/v2/templates/:templateId", async (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockTemplate)) }), diff --git a/site/src/util/colors.ts b/site/src/util/colors.ts new file mode 100644 index 0000000000..054e2cb6e9 --- /dev/null +++ b/site/src/util/colors.ts @@ -0,0 +1,23 @@ +// Used to convert our theme colors to Hex since monaco theme only support hex colors +// From https://www.jameslmilner.com/posts/converting-rgb-hex-hsl-colors/ +export function hslToHex(hsl: string): string { + const [h, s, l] = hsl + .replace("hsl(", "") + .replace(")", "") + .replaceAll("%", "") + .split(",") + .map(Number) + + const hDecimal = l / 100 + const a = (s * Math.min(hDecimal, 1 - hDecimal)) / 100 + const f = (n: number) => { + const k = (n + h / 30) % 12 + const color = hDecimal - a * Math.max(Math.min(k - 3, 9 - k, 1), -1) + + // Convert to Hex and prefix with "0" if required + return Math.round(255 * color) + .toString(16) + .padStart(2, "0") + } + return `#${f(0)}${f(8)}${f(4)}` +} diff --git a/site/src/util/templateVersion.ts b/site/src/util/templateVersion.ts index 58fc185354..13fc45c8a5 100644 --- a/site/src/util/templateVersion.ts +++ b/site/src/util/templateVersion.ts @@ -10,6 +10,7 @@ export type TemplateVersionFiles = Record export const getTemplateVersionFiles = async ( version: TemplateVersion, allowedExtensions: string[], + allowedFiles: string[], ): Promise => { const files: TemplateVersionFiles = {} const tarFile = await getFile(version.job.file_id) @@ -20,7 +21,10 @@ export const getTemplateVersionFiles = async ( const filename = paths[paths.length - 1] const [_, extension] = filename.split(".") - if (allowedExtensions.includes(extension)) { + if ( + allowedExtensions.includes(extension) || + allowedFiles.includes(filename) + ) { blobs[filename] = file.blob } }) diff --git a/site/src/xServices/templateVersion/templateVersionXService.ts b/site/src/xServices/templateVersion/templateVersionXService.ts index 7b99fcd191..73cd0989bf 100644 --- a/site/src/xServices/templateVersion/templateVersionXService.ts +++ b/site/src/xServices/templateVersion/templateVersionXService.ts @@ -1,4 +1,8 @@ -import { getTemplateVersionByName } from "api/api" +import { + getPreviousTemplateVersionByName, + GetPreviousTemplateVersionByNameResponse, + getTemplateVersionByName, +} from "api/api" import { TemplateVersion } from "api/typesGenerated" import { getTemplateVersionFiles, @@ -9,9 +13,12 @@ import { assign, createMachine } from "xstate" export interface TemplateVersionMachineContext { orgId: string versionName: string - version?: TemplateVersion - files?: TemplateVersionFiles + currentVersion?: TemplateVersion + currentFiles?: TemplateVersionFiles error?: Error | unknown + // Get file diffs + previousVersion?: TemplateVersion + previousFiles?: TemplateVersionFiles } export const templateVersionMachine = createMachine( @@ -21,23 +28,29 @@ export const templateVersionMachine = createMachine( schema: { context: {} as TemplateVersionMachineContext, services: {} as { - loadVersion: { - data: TemplateVersion + loadVersions: { + data: { + currentVersion: GetPreviousTemplateVersionByNameResponse + previousVersion: GetPreviousTemplateVersionByNameResponse + } } loadFiles: { - data: TemplateVersionFiles + data: { + currentFiles: TemplateVersionFiles + previousFiles: TemplateVersionFiles + } } }, }, tsTypes: {} as import("./templateVersionXService.typegen").Typegen0, - initial: "loadingVersion", + initial: "loadingVersions", states: { - loadingVersion: { + loadingVersions: { invoke: { - src: "loadVersion", + src: "loadVersions", onDone: { target: "loadingFiles", - actions: ["assignVersion"], + actions: ["assignVersions"], }, onError: { target: "done.error", @@ -71,21 +84,58 @@ export const templateVersionMachine = createMachine( assignError: assign({ error: (_, { data }) => data, }), - assignVersion: assign({ - version: (_, { data }) => data, + assignVersions: assign({ + currentVersion: (_, { data }) => data.currentVersion, + previousVersion: (_, { data }) => data.previousVersion, }), assignFiles: assign({ - files: (_, { data }) => data, + currentFiles: (_, { data }) => data.currentFiles, + previousFiles: (_, { data }) => data.previousFiles, }), }, services: { - loadVersion: ({ orgId, versionName }) => - getTemplateVersionByName(orgId, versionName), - loadFiles: async ({ version }) => { - if (!version) { + loadVersions: async ({ orgId, versionName }) => { + const [currentVersion, previousVersion] = await Promise.all([ + getTemplateVersionByName(orgId, versionName), + getPreviousTemplateVersionByName(orgId, versionName), + ]) + + return { + currentVersion, + previousVersion, + } + }, + loadFiles: async ({ currentVersion, previousVersion }) => { + if (!currentVersion) { throw new Error("Version is not defined") } - return getTemplateVersionFiles(version, ["tf", "md"]) + const loadFilesPromises: ReturnType[] = + [] + const allowedExtensions = ["tf", "md"] + const allowedFiles = ["Dockerfile"] + loadFilesPromises.push( + getTemplateVersionFiles( + currentVersion, + allowedExtensions, + allowedFiles, + ), + ) + if (previousVersion) { + loadFilesPromises.push( + getTemplateVersionFiles( + previousVersion, + allowedExtensions, + allowedFiles, + ), + ) + } + const [currentFiles, previousFiles] = await Promise.all( + loadFilesPromises, + ) + return { + currentFiles, + previousFiles, + } }, }, }, diff --git a/site/vite.config.ts b/site/vite.config.ts index 907a40f083..deb9163060 100644 --- a/site/vite.config.ts +++ b/site/vite.config.ts @@ -9,6 +9,7 @@ export default defineConfig({ outDir: path.resolve(__dirname, "./out"), // We need to keep the /bin folder and GITKEEP files emptyOutDir: false, + sourcemap: process.env.NODE_ENV === "development", }, define: { "process.env": { diff --git a/site/yarn.lock b/site/yarn.lock index 7fd4038d93..f12e781f26 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -1700,6 +1700,21 @@ resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA== +"@monaco-editor/loader@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.3.2.tgz#04effbb87052d19cd7d3c9d81c0635490f9bb6d8" + integrity sha512-BTDbpHl3e47r3AAtpfVFTlAi7WXv4UQ/xZmz8atKl4q7epQV5e7+JbigFDViWF71VBi4IIBdcWP57Hj+OWuc9g== + dependencies: + state-local "^1.0.6" + +"@monaco-editor/react@4.4.6": + version "4.4.6" + resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.6.tgz#8ae500b0edf85276d860ed702e7056c316548218" + integrity sha512-Gr3uz3LYf33wlFE3eRnta4RxP5FSNxiIV9ENn2D2/rN8KgGAD8ecvcITRtsbbyuOuNkwbuHYxfeaz2Vr+CtyFA== + dependencies: + "@monaco-editor/loader" "^1.3.2" + prop-types "^15.7.2" + "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -2897,7 +2912,7 @@ "@types/connect" "*" "@types/node" "*" -"@types/color-convert@^2.0.0": +"@types/color-convert@2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.0.tgz#8f5ee6b9e863dcbee5703f5a517ffb13d3ea4e22" integrity sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ== @@ -3163,7 +3178,7 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-color@^3.0.6": +"@types/react-color@3.0.6": version "3.0.6" resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.6.tgz#602fed023802b2424e7cd6ff3594ccd3d5055f9a" integrity sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w== @@ -5224,6 +5239,13 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" +color-convert@2.0.1, color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -5231,13 +5253,6 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" @@ -11580,7 +11595,7 @@ react-chartjs-2@4.3.1: resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-4.3.1.tgz#9941e7397fb963f28bb557addb401e9ff96c6681" integrity sha512-5i3mjP6tU7QSn0jvb8I4hudTzHJqS8l00ORJnVwI2sYu0ihpj83Lv2YzfxunfxTZkscKvZu2F2w9LkwNBhj6xA== -react-color@^2.19.3: +react-color@2.19.3: version "2.19.3" resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d" integrity sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA== @@ -11737,7 +11752,7 @@ react-router@6.4.1: dependencies: "@remix-run/router" "1.0.1" -react-syntax-highlighter@15.5.0, react-syntax-highlighter@^15.4.5: +react-syntax-highlighter@^15.4.5: version "15.5.0" resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20" integrity sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg== @@ -12731,6 +12746,11 @@ stackframe@^1.3.4: resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== +state-local@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5" + integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w== + state-toggle@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe"