mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat: Add diff and Dockerfile support for template version page (#5339)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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))
|
||||
}),
|
||||
|
||||
@@ -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)}`
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user