diff --git a/site/package.json b/site/package.json index 98543875a1055..67709c129b031 100644 --- a/site/package.json +++ b/site/package.json @@ -106,6 +106,7 @@ "@storybook/addon-links": "7.5.2", "@storybook/addon-mdx-gfm": "7.5.2", "@storybook/addon-themes": "7.6.4", + "@storybook/preview-api": "7.6.9", "@storybook/react": "7.5.2", "@storybook/react-vite": "7.5.2", "@swc/core": "1.3.38", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index ff9b1c6a59983..b8641ff8f4f19 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -239,6 +239,9 @@ devDependencies: '@storybook/addon-themes': specifier: 7.6.4 version: 7.6.4 + '@storybook/preview-api': + specifier: 7.6.9 + version: 7.6.9 '@storybook/react': specifier: 7.5.2 version: 7.5.2(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2) @@ -406,7 +409,7 @@ devDependencies: version: 7.5.2 storybook-addon-react-router-v6: specifier: 2.0.0 - version: 2.0.0(@storybook/blocks@7.5.3)(@storybook/channels@7.5.3)(@storybook/components@7.5.3)(@storybook/core-events@7.5.3)(@storybook/manager-api@7.5.3)(@storybook/preview-api@7.5.3)(@storybook/theming@7.5.3)(react-dom@18.2.0)(react-router-dom@6.20.0)(react-router@6.20.0)(react@18.2.0) + version: 2.0.0(@storybook/blocks@7.5.3)(@storybook/channels@7.5.3)(@storybook/components@7.5.3)(@storybook/core-events@7.5.3)(@storybook/manager-api@7.5.3)(@storybook/preview-api@7.6.9)(@storybook/theming@7.5.3)(react-dom@18.2.0)(react-router-dom@6.20.0)(react-router@6.20.0)(react@18.2.0) storybook-react-context: specifier: 0.6.0 version: 0.6.0(react-dom@18.2.0) @@ -523,14 +526,14 @@ packages: resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 dev: true /@babel/helper-builder-binary-assignment-operator-visitor@7.22.15: resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 dev: true /@babel/helper-compilation-targets@7.22.15: @@ -627,7 +630,7 @@ packages: resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 dev: true /@babel/helper-module-imports@7.22.15: @@ -667,7 +670,7 @@ packages: resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 dev: true /@babel/helper-plugin-utils@7.22.5: @@ -708,7 +711,7 @@ packages: resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 dev: true /@babel/helper-split-export-declaration@7.22.6: @@ -740,7 +743,7 @@ packages: dependencies: '@babel/helper-function-name': 7.23.0 '@babel/template': 7.22.15 - '@babel/types': 7.23.0 + '@babel/types': 7.23.4 dev: true /@babel/helpers@7.23.2: @@ -4207,7 +4210,7 @@ packages: '@storybook/client-logger': 7.5.2 '@storybook/components': 7.5.2(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@storybook/core-events': 7.5.2 - '@storybook/csf': 0.1.1 + '@storybook/csf': 0.1.2 '@storybook/docs-tools': 7.5.2 '@storybook/global': 5.0.0 '@storybook/manager-api': 7.5.2(react-dom@18.2.0)(react@18.2.0) @@ -4365,6 +4368,17 @@ packages: tiny-invariant: 1.3.1 dev: true + /@storybook/channels@7.6.9: + resolution: {integrity: sha512-goGGZPT294CS1QDF65Fs+PCauvM/nTMseU913ZVSZbFTk4uvqIXOaOraqhQze8A/C8a0yls4qu2Wp00tCnyaTA==} + dependencies: + '@storybook/client-logger': 7.6.9 + '@storybook/core-events': 7.6.9 + '@storybook/global': 5.0.0 + qs: 6.11.2 + telejson: 7.2.0 + tiny-invariant: 1.3.1 + dev: true + /@storybook/cli@7.5.2: resolution: {integrity: sha512-8JPvA/K66zBmRFpRRwsD0JLqZUODRrGmNuAWx+Bj1K8wqbg68MYnOflbkSIxIVxrfhd39OrffV0h8CwKNL9gAg==} hasBin: true @@ -4436,13 +4450,19 @@ packages: '@storybook/global': 5.0.0 dev: true + /@storybook/client-logger@7.6.9: + resolution: {integrity: sha512-Xm6fa6AR3cjxabauMldBv/66OOp5IhDiUEpp4D/a7hXfvCWqwmjVJ6EPz9WzkMhcPbMJr8vWJBaS3glkFqsRng==} + dependencies: + '@storybook/global': 5.0.0 + dev: true + /@storybook/codemod@7.5.2: resolution: {integrity: sha512-PxZg0w4OlmFB4dBzB+sCgwmHNke0n1N8vNooxtcuusrLKlbUfmssYRnQn6yRSJw0WfkUYgI10CWxGaamaOFekA==} dependencies: '@babel/core': 7.23.2 '@babel/preset-env': 7.23.2(@babel/core@7.23.2) '@babel/types': 7.23.0 - '@storybook/csf': 0.1.1 + '@storybook/csf': 0.1.2 '@storybook/csf-tools': 7.5.2 '@storybook/node-logger': 7.5.2 '@storybook/types': 7.5.2 @@ -4590,6 +4610,12 @@ packages: ts-dedent: 2.2.0 dev: true + /@storybook/core-events@7.6.9: + resolution: {integrity: sha512-YCds7AA6sbnnZ2qq5l+AIxhQqYlXB8eVTkjj6phgczsLjkqKapYFxAFc3ppRnE0FcsL2iji17ikHzZ8+eHYznA==} + dependencies: + ts-dedent: 2.2.0 + dev: true + /@storybook/core-server@7.5.2: resolution: {integrity: sha512-4oXpy1L/NyHiz/OXNUFnSeMLA/+lTgQAlVx86pRbEBDj6snt1/NSx2+yZyFtZ/XTnJ22BPpM8IIrgm95ZlQKmA==} dependencies: @@ -4599,7 +4625,7 @@ packages: '@storybook/channels': 7.5.2 '@storybook/core-common': 7.5.2 '@storybook/core-events': 7.5.2 - '@storybook/csf': 0.1.1 + '@storybook/csf': 0.1.2 '@storybook/csf-tools': 7.5.2 '@storybook/docs-mdx': 0.1.0 '@storybook/global': 5.0.0 @@ -4657,7 +4683,7 @@ packages: '@babel/parser': 7.23.0 '@babel/traverse': 7.23.2 '@babel/types': 7.23.0 - '@storybook/csf': 0.1.1 + '@storybook/csf': 0.1.2 '@storybook/types': 7.5.2 fs-extra: 11.1.1 recast: 0.23.4 @@ -4834,6 +4860,25 @@ packages: util-deprecate: 1.0.2 dev: true + /@storybook/preview-api@7.6.9: + resolution: {integrity: sha512-qVRylkOc70Ivz/oRE3cXaQA9r60qXSCXhY8xFjnBvZFjoYr0ImGx+tt0818YzSkhTf6LsNbx9HxwW4+x7JD6dw==} + dependencies: + '@storybook/channels': 7.6.9 + '@storybook/client-logger': 7.6.9 + '@storybook/core-events': 7.6.9 + '@storybook/csf': 0.1.2 + '@storybook/global': 5.0.0 + '@storybook/types': 7.6.9 + '@types/qs': 6.9.10 + dequal: 2.0.3 + lodash: 4.17.21 + memoizerific: 1.11.3 + qs: 6.11.2 + synchronous-promise: 2.0.17 + ts-dedent: 2.2.0 + util-deprecate: 1.0.2 + dev: true + /@storybook/preview@7.5.2: resolution: {integrity: sha512-dA5VpHp0D9nh9/wOzWP8At1wtz/SiaMBbwaiEOFTFUGcPerrkroEWadIlSSB7vgQJ9yWiD4l3KDaS8ANzHWtPQ==} dev: true @@ -5041,6 +5086,15 @@ packages: file-system-cache: 2.3.0 dev: true + /@storybook/types@7.6.9: + resolution: {integrity: sha512-Qnx7exS6bO1MrqasHl12h8/HeBuxrwg2oMXROO7t0qmprV6+DGb6OxztsVIgbKR+m6uqFFM1q+f/Q5soI1qJ6g==} + dependencies: + '@storybook/channels': 7.6.9 + '@types/babel__core': 7.20.5 + '@types/express': 4.17.17 + file-system-cache: 2.3.0 + dev: true + /@swc/core-darwin-arm64@1.3.38: resolution: {integrity: sha512-4ZTJJ/cR0EsXW5UxFCifZoGfzQ07a8s4ayt1nLvLQ5QoB1GTAf9zsACpvWG8e7cmCR0L76R5xt8uJuyr+noIXA==} engines: {node: '>=10'} @@ -6474,7 +6528,7 @@ packages: dependencies: '@babel/core': 7.23.2 '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.3 + '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 babel-preset-jest: 29.5.0(@babel/core@7.23.2) chalk: 4.1.2 @@ -6502,9 +6556,9 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/template': 7.22.15 - '@babel/types': 7.23.0 - '@types/babel__core': 7.20.3 - '@types/babel__traverse': 7.20.3 + '@babel/types': 7.23.4 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.4 dev: true /babel-plugin-macros@3.1.0: @@ -13162,7 +13216,7 @@ packages: resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} dev: true - /storybook-addon-react-router-v6@2.0.0(@storybook/blocks@7.5.3)(@storybook/channels@7.5.3)(@storybook/components@7.5.3)(@storybook/core-events@7.5.3)(@storybook/manager-api@7.5.3)(@storybook/preview-api@7.5.3)(@storybook/theming@7.5.3)(react-dom@18.2.0)(react-router-dom@6.20.0)(react-router@6.20.0)(react@18.2.0): + /storybook-addon-react-router-v6@2.0.0(@storybook/blocks@7.5.3)(@storybook/channels@7.5.3)(@storybook/components@7.5.3)(@storybook/core-events@7.5.3)(@storybook/manager-api@7.5.3)(@storybook/preview-api@7.6.9)(@storybook/theming@7.5.3)(react-dom@18.2.0)(react-router-dom@6.20.0)(react-router@6.20.0)(react@18.2.0): resolution: {integrity: sha512-M+PR7rdacFDwUCQZRBJVnzyEOqHrDVrTqN8ufqo+TuXxk33QZvb3QeZuo0d2UTYctgA1GY74EX9RJCEXZpv6VQ==} peerDependencies: '@storybook/blocks': ^7.0.0 @@ -13187,7 +13241,7 @@ packages: '@storybook/components': 7.5.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@storybook/core-events': 7.5.3 '@storybook/manager-api': 7.5.3(react-dom@18.2.0)(react@18.2.0) - '@storybook/preview-api': 7.5.3 + '@storybook/preview-api': 7.6.9 '@storybook/theming': 7.5.3(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) diff --git a/site/src/pages/HealthPage/Content.tsx b/site/src/pages/HealthPage/Content.tsx index 33548e5011909..a304205c58fe6 100644 --- a/site/src/pages/HealthPage/Content.tsx +++ b/site/src/pages/HealthPage/Content.tsx @@ -170,7 +170,7 @@ export const Pill = forwardRef((props, ref) => { border: `1px solid ${theme.palette.divider}`, fontSize: 12, fontWeight: 500, - padding: "8px 16px 8px 8px", + padding: 8, gap: 8, cursor: "default", }} @@ -182,11 +182,7 @@ export const Pill = forwardRef((props, ref) => { ); }); -type BooleanPillProps = Omit< - ComponentProps, - "children" | "icon" | "value" -> & { - children: string; +type BooleanPillProps = Omit, "icon" | "value"> & { value: boolean; }; diff --git a/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx b/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx index 050450be9b360..b7e2b0f04d843 100644 --- a/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx +++ b/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx @@ -21,6 +21,9 @@ import Person from "@mui/icons-material/Person"; import SwapHoriz from "@mui/icons-material/SwapHoriz"; import Tooltip from "@mui/material/Tooltip"; import Sell from "@mui/icons-material/Sell"; +import { FC } from "react"; +import CloseIcon from "@mui/icons-material/Close"; +import IconButton from "@mui/material/IconButton"; export const ProvisionerDaemonsPage = () => { const healthStatus = useOutletContext(); @@ -129,9 +132,9 @@ export const ProvisionerDaemonsPage = () => { - {Object.keys(extraTags).map((k) => - renderTag(k, extraTags[k]), - )} + {Object.keys(extraTags).map((k) => ( + + ))} @@ -188,13 +191,42 @@ const parseBool = (s: string): { valid: boolean; value: boolean } => { } }; -const renderTag = (k: string, v: string) => { +interface ProvisionerTagProps { + k: string; + v: string; + onDelete?: (key: string) => void; +} + +export const ProvisionerTag: FC = ({ k, v, onDelete }) => { const { valid, value: boolValue } = parseBool(v); const kv = `${k}: ${v}`; + const content = onDelete ? ( + <> + {kv} + { + onDelete(k); + }} + > + + + + ) : ( + kv + ); if (valid) { - return {kv}; + return {content}; } - return }>{kv}; + return }>{content}; }; export default ProvisionerDaemonsPage; diff --git a/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.stories.tsx b/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.stories.tsx new file mode 100644 index 0000000000000..664fce53fe260 --- /dev/null +++ b/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { chromatic } from "testHelpers/chromatic"; +import { MockTemplateVersion } from "testHelpers/entities"; +import { ProvisionerTagsPopover } from "./ProvisionerTagsPopover"; +import { useArgs } from "@storybook/preview-api"; + +const meta: Meta = { + title: "component/ProvisionerTagsPopover", + parameters: { + chromatic, + layout: "centered", + }, + component: ProvisionerTagsPopover, + args: { + tags: MockTemplateVersion.job.tags, + }, + render: function Render(args) { + const [{ tags }, updateArgs] = useArgs(); + + return ( + { + updateArgs({ tags: { ...tags, [key]: value } }); + }} + onDelete={(key) => { + const newTags = { ...tags }; + delete newTags[key]; + updateArgs({ tags: newTags }); + }} + /> + ); + }, +}; + +export default meta; +type Story = StoryObj; + +export const Example: Story = {}; diff --git a/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.test.tsx b/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.test.tsx new file mode 100644 index 0000000000000..5db8a90f80bd2 --- /dev/null +++ b/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.test.tsx @@ -0,0 +1,117 @@ +import { renderComponent } from "testHelpers/renderHelpers"; +import { ProvisionerTagsPopover } from "./ProvisionerTagsPopover"; +import { fireEvent, screen } from "@testing-library/react"; +import { MockTemplateVersion } from "testHelpers/entities"; +import userEvent from "@testing-library/user-event"; + +let tags = MockTemplateVersion.job.tags; + +describe("ProvisionerTagsPopover", () => { + describe("click the button", () => { + it("can add a tag", async () => { + const onSubmit = jest.fn().mockImplementation(({ key, value }) => { + tags = { ...tags, [key]: value }; + }); + const onDelete = jest.fn().mockImplementation((key) => { + const newTags = { ...tags }; + delete newTags[key]; + tags = newTags; + }); + const { rerender } = renderComponent( + , + ); + + // Open Popover + const btn = await screen.findByRole("button"); + expect(btn).toBeEnabled(); + await userEvent.click(btn); + + // Check for existing tags + const el = await screen.findByText(/scope: organization/i); + expect(el).toBeInTheDocument(); + + // Add key and value + const el2 = await screen.findByLabelText("Key"); + expect(el2).toBeEnabled(); + fireEvent.change(el2, { target: { value: "foo" } }); + expect(el2).toHaveValue("foo"); + const el3 = await screen.findByLabelText("Value"); + expect(el3).toBeEnabled(); + fireEvent.change(el3, { target: { value: "bar" } }); + expect(el3).toHaveValue("bar"); + + // Submit + const btn2 = await screen.findByRole("button", { + name: /add/i, + hidden: true, + }); + expect(btn2).toBeEnabled(); + await userEvent.click(btn2); + expect(onSubmit).toHaveBeenCalledTimes(1); + + rerender( + , + ); + + // Check for new tag + const el4 = await screen.findByText(/foo: bar/i); + expect(el4).toBeInTheDocument(); + }); + it("can remove a tag", async () => { + const onSubmit = jest.fn().mockImplementation(({ key, value }) => { + tags = { ...tags, [key]: value }; + }); + const onDelete = jest.fn().mockImplementation((key) => { + delete tags[key]; + tags = { ...tags }; + }); + const { rerender } = renderComponent( + , + ); + + // Open Popover + const btn = await screen.findByRole("button"); + expect(btn).toBeEnabled(); + await userEvent.click(btn); + + // Check for existing tags + const el = await screen.findByText(/wowzers: whatatag/i); + expect(el).toBeInTheDocument(); + + // Find Delete button + const btn2 = await screen.findByRole("button", { + name: /delete-wowzers/i, + hidden: true, + }); + expect(btn2).toBeEnabled(); + + // Delete tag + await userEvent.click(btn2); + expect(onDelete).toHaveBeenCalledTimes(1); + + rerender( + , + ); + + // Expect deleted tag to be gone + const el2 = screen.queryByText(/wowzers: whatatag/i); + expect(el2).not.toBeInTheDocument(); + }); + }); +}); diff --git a/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.tsx b/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.tsx new file mode 100644 index 0000000000000..9d65021dc6b77 --- /dev/null +++ b/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.tsx @@ -0,0 +1,160 @@ +import { Stack } from "components/Stack/Stack"; +import { TopbarButton } from "components/FullPageLayout/Topbar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "components/Popover/Popover"; +import { ProvisionerTag } from "pages/HealthPage/ProvisionerDaemonsPage"; +import { type FC } from "react"; +import useTheme from "@mui/system/useTheme"; +import { useFormik } from "formik"; +import * as Yup from "yup"; +import { getFormHelpers, onChangeTrimmed } from "utils/formUtils"; +import { FormFields, FormSection, VerticalForm } from "components/Form/Form"; +import TextField from "@mui/material/TextField"; +import Button from "@mui/material/Button"; +import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined"; +import AddIcon from "@mui/icons-material/Add"; +import Link from "@mui/material/Link"; +import { docs } from "utils/docs"; + +const initialValues = { + key: "", + value: "", +}; + +const validationSchema = Yup.object({ + key: Yup.string() + .required("Required") + .notOneOf(["owner"], "Cannot override owner tag"), + value: Yup.string() + .required("Required") + .when("key", ([key], schema) => { + if (key === "scope") { + return schema.oneOf( + ["organization", "scope"], + "Scope value must be 'organization' or 'user'", + ); + } + + return schema; + }), +}); + +interface ProvisionerTagsPopoverProps { + tags: Record; + onSubmit: (values: typeof initialValues) => void; + onDelete: (key: string) => void; +} + +export const ProvisionerTagsPopover: FC = ({ + tags, + onSubmit, + onDelete, +}) => { + const theme = useTheme(); + + const form = useFormik({ + initialValues, + validationSchema, + onSubmit: (values) => { + onSubmit(values); + form.resetForm(); + }, + }); + const getFieldHelpers = getFormHelpers(form); + + return ( + + + + + + + +
+ + + + Tags are a way to control which provisioner daemons complete + which build jobs.  + + Learn more... + + + } + /> + + {Object.keys(tags) + .filter((key) => { + // filter out owner since you cannot override it + return key !== "owner"; + }) + .map((k) => ( + <> + {k === "scope" ? ( + + ) : ( + + )} + + ))} + + + + + + + + + + + +
+
+
+ ); +}; diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx index 11b93f1188f4e..db07c575db99e 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx @@ -53,6 +53,8 @@ import { TopbarIconButton, } from "components/FullPageLayout/Topbar"; import { Sidebar } from "components/FullPageLayout/Sidebar"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import { ProvisionerTagsPopover } from "./ProvisionerTagsPopover"; type Tab = "logs" | "resources" | undefined; // Undefined is to hide the tab @@ -78,6 +80,8 @@ export interface TemplateVersionEditorProps { onSubmitMissingVariableValues: (values: VariableValue[]) => void; onCancelSubmitMissingVariableValues: () => void; defaultTab?: Tab; + provisionerTags: Record; + onUpdateProvisionerTags: (tags: Record) => void; } const findInitialFile = (fileTree: FileTree): string | undefined => { @@ -114,6 +118,8 @@ export const TemplateVersionEditor: FC = ({ onSubmitMissingVariableValues, onCancelSubmitMissingVariableValues, defaultTab, + provisionerTags, + onUpdateProvisionerTags, }) => { const theme = useTheme(); const [selectedTab, setSelectedTab] = useState(defaultTab); @@ -236,20 +242,45 @@ export const TemplateVersionEditor: FC = ({ )} - - } - title="Build template (Ctrl + Enter)" - disabled={disablePreview} - onClick={() => { - triggerPreview(); + button:hover + button": { + borderLeft: "1px solid #FFF", + }, }} + disabled={disablePreview} > - Build - + + } + title="Build template (Ctrl + Enter)" + disabled={disablePreview} + onClick={() => { + triggerPreview(); + }} + > + Build + + { + onUpdateProvisionerTags({ + ...provisionerTags, + [key]: value, + }); + }} + onDelete={(key) => { + const newTags = { ...provisionerTags }; + delete newTags[key]; + onUpdateProvisionerTags(newTags); + }} + /> + { queryClient.setQueryData(templateVersionOptions.queryKey, newVersion); }; + // Provisioner Tags + const [provisionerTags, setProvisionerTags] = useState< + Record + >({}); + useEffect(() => { + if (templateVersionQuery.data?.job.tags) { + setProvisionerTags(templateVersionQuery.data.job.tags); + } + }, [templateVersionQuery.data?.job.tags]); + return ( <> @@ -127,7 +137,7 @@ export const TemplateVersionEditorPage: FC = () => { const newVersion = await createTemplateVersionMutation.mutateAsync({ provisioner: "terraform", storage_method: "file", - tags: templateVersionQuery.data.job.tags, + tags: provisionerTags, template_id: templateQuery.data.id, file_id: serverFile.hash, }); @@ -210,6 +220,10 @@ export const TemplateVersionEditorPage: FC = () => { onCancelSubmitMissingVariableValues={() => { setIsMissingVariablesDialogOpen(false); }} + provisionerTags={provisionerTags} + onUpdateProvisionerTags={(tags) => { + setProvisionerTags(tags); + }} /> ) : ( diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 1ccfec3395386..6d22ed072f567 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -355,7 +355,14 @@ export const MockProvisionerJob: TypesGen.ProvisionerJob = { status: "succeeded", file_id: MockOrganization.id, completed_at: "2022-05-17T17:39:01.382927298Z", - tags: {}, + tags: { + scope: "organization", + owner: "", + wowzers: "whatatag", + isCapable: "false", + department: "engineering", + dreaming: "true", + }, queue_position: 0, queue_size: 0, };