feat: Add diff and Dockerfile support for template version page (#5339)

This commit is contained in:
Bruno Quaresma
2022-12-08 10:24:15 -05:00
committed by GitHub
parent f68a65697d
commit ce76d9d5a3
18 changed files with 606 additions and 153 deletions
@@ -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)
}
+2
View File
@@ -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
`
@@ -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;
+29 -10
View File
@@ -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)
})
}
+5 -5
View File
@@ -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",
+28
View File
@@ -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<GetPreviousTemplateVersionByNameResponse> => {
try {
const response = await axios.get<TypesGen.TemplateVersion>(
`/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,
+22
View File
@@ -0,0 +1,22 @@
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
export const DockerIcon = (props: SvgIconProps): JSX.Element => (
<SvgIcon {...props} viewBox="0 0 32 32">
<path
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"
style={{ fill: "#0096e6" }}
/>
<path
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"
style={{ fill: "#fff" }}
/>
<path
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"
style={{ fill: "#fff" }}
/>
<path
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"
style={{ fill: "#fff" }}
/>
</SvgIcon>
)
@@ -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<ComponentProps<typeof Prism>> = ({
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 (
<Prism
style={darcula}
useInlineStyles={false}
// Use inline styles does not work correctly
// https://github.com/react-syntax-highlighter/react-syntax-highlighter/issues/329
codeTagProps={{ style: {} }}
className={combineClasses([styles.prism, className])}
{...props}
/>
<div className={styles.wrapper}>
{hasDiff ? (
<DiffEditor original={compareWith} modified={value} {...commonProps} />
) : (
<Editor value={value} {...commonProps} />
)}
</div>
)
}
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,
},
}))
@@ -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<string, any> => ({
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<Theme>()
const name = "coder"
useEffect(() => {
if (monaco) {
monaco.editor.defineTheme(name, coderTheme(theme))
setIsLoading(false)
}
}, [monaco, theme])
return {
isLoading,
name,
}
}
@@ -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()
})
})
@@ -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",
}),
@@ -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<string, JSX.Element> = {
tf: <TerraformIcon />,
md: <MarkdownIcon />,
mkd: <MarkdownIcon />,
Dockerfile: <DockerIcon />,
}
const getExtension = (filename: string) => {
if (filename.includes(".")) {
const [_, extension] = filename.split(".")
return extension
}
return filename
}
const languageByExtension: Record<string, string> = {
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 (
<div className={styles.files}>
<div className={styles.tabs}>
{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 (
<button
className={combineClasses({
[styles.tab]: true,
[styles.tabActive]: tabValue === tab.value,
})}
onClick={() => {
tab.set(tabValue)
}}
key={filename}
>
{icon}
{filename}
{hasDiff && <div className={styles.tabDiff} />}
</button>
)
})}
</div>
<SyntaxHighlighter
value={currentFile}
compareWith={previousFile}
language={languageByExtension[getExtension(selectedFilename)]}
/>
</div>
)
}
export interface TemplateVersionPageViewProps {
/**
* Used to display the version name before loading the version in the API
@@ -36,8 +111,7 @@ export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
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<TemplateVersionPageViewProps> = ({
<PageHeaderTitle>{versionName}</PageHeaderTitle>
</PageHeader>
{!files && !error && <Loader />}
{!currentFiles && !error && <Loader />}
<Stack spacing={4}>
{Boolean(error) && <AlertBanner severity="error" error={error} />}
{version && files && (
{currentVersion && currentFiles && (
<>
<Stats>
<StatsItem
@@ -62,53 +136,19 @@ export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
/>
<StatsItem
label={t("stats.createdBy")}
value={version.created_by.username}
value={currentVersion.created_by.username}
/>
<StatsItem
label={t("stats.created")}
value={createDayString(version.created_at)}
value={createDayString(currentVersion.created_at)}
/>
</Stats>
<div className={styles.files}>
<div className={styles.tabs}>
{Object.keys(files).map((filename, index) => {
const tabValue = index.toString()
return (
<button
className={combineClasses({
[styles.tab]: true,
[styles.tabActive]: tabValue === tab.value,
})}
onClick={() => {
tab.set(tabValue)
}}
key={filename}
>
{filename.endsWith("tf") ? (
<TerraformIcon />
) : (
<MarkdownIcon />
)}
{filename}
</button>
)
})}
</div>
<SyntaxHighlighter
showLineNumbers
className={styles.prism}
language={
Object.keys(files)[Number(tab.value)].endsWith("tf")
? "hcl"
: "markdown"
}
>
{Object.values(files)[Number(tab.value)]}
</SyntaxHighlighter>
</div>
<Files
tab={tab}
currentFiles={currentFiles}
previousFiles={previousFiles}
/>
</>
)}
</Stack>
@@ -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,
},
+6
View File
@@ -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))
}),
+23
View File
@@ -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)}`
}
+5 -1
View File
@@ -10,6 +10,7 @@ export type TemplateVersionFiles = Record<string, string>
export const getTemplateVersionFiles = async (
version: TemplateVersion,
allowedExtensions: string[],
allowedFiles: string[],
): Promise<TemplateVersionFiles> => {
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
}
})
@@ -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<typeof getTemplateVersionFiles>[] =
[]
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,
}
},
},
},
+1
View File
@@ -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": {
+31 -11
View File
@@ -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"