From ae6a28eccd3f78af5ef4b92c7f005ac9a4774510 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 20 Feb 2025 18:28:10 +0000 Subject: [PATCH 1/4] Create ProvisionerTagsField --- site/src/components/Input/Input.tsx | 2 +- .../modules/provisioners/ProvisionerTag.tsx | 4 +- .../ProvisionerTagsField.stories.tsx | 108 ++++++++++++++ .../provisioners/ProvisionerTagsField.tsx | 141 ++++++++++++++++++ .../ProvisionerTagsPopover.stories.tsx | 54 ++++++- .../ProvisionerTagsPopover.test.tsx | 119 --------------- .../ProvisionerTagsPopover.tsx | 137 ++++------------- .../TemplateVersionEditor.tsx | 12 +- 8 files changed, 326 insertions(+), 251 deletions(-) create mode 100644 site/src/modules/provisioners/ProvisionerTagsField.stories.tsx create mode 100644 site/src/modules/provisioners/ProvisionerTagsField.tsx delete mode 100644 site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.test.tsx diff --git a/site/src/components/Input/Input.tsx b/site/src/components/Input/Input.tsx index b50d6415a8983..9f3896a1f4f6d 100644 --- a/site/src/components/Input/Input.tsx +++ b/site/src/components/Input/Input.tsx @@ -18,7 +18,7 @@ export const Input = forwardRef< file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-content-primary placeholder:text-content-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-content-link - disabled:cursor-not-allowed disabled:opacity-50 md:text-sm`, + disabled:cursor-not-allowed disabled:opacity-50 md:text-sm text-inherit`, className, )} ref={ref} diff --git a/site/src/modules/provisioners/ProvisionerTag.tsx b/site/src/modules/provisioners/ProvisionerTag.tsx index e174e4222bbfb..f120286b1e39e 100644 --- a/site/src/modules/provisioners/ProvisionerTag.tsx +++ b/site/src/modules/provisioners/ProvisionerTag.tsx @@ -45,7 +45,6 @@ export const ProvisionerTag: FC = ({ <> {kv} { @@ -53,6 +52,7 @@ export const ProvisionerTag: FC = ({ }} > + Delete {tagName} ) : ( @@ -62,7 +62,7 @@ export const ProvisionerTag: FC = ({ return {content}; } return ( - }> + } data-testid={`tag-${tagName}`}> {content} ); diff --git a/site/src/modules/provisioners/ProvisionerTagsField.stories.tsx b/site/src/modules/provisioners/ProvisionerTagsField.stories.tsx new file mode 100644 index 0000000000000..168fb72c2140e --- /dev/null +++ b/site/src/modules/provisioners/ProvisionerTagsField.stories.tsx @@ -0,0 +1,108 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { expect, userEvent, within } from "@storybook/test"; +import type { ProvisionerDaemon } from "api/typesGenerated"; +import { type FC, useState } from "react"; +import { ProvisionerTagsField } from "./ProvisionerTagsField"; + +const meta: Meta = { + title: "modules/provisioners/ProvisionerTagsField", + component: ProvisionerTagsField, + args: { + value: {}, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Empty: Story = { + args: { + value: {}, + }, +}; + +export const WithInitialValue: Story = { + args: { + value: { + cluster: "dogfood-2", + env: "gke", + scope: "organization", + }, + }, +}; + +type StatefulProvisionerTagsFieldProps = { + initialValue?: ProvisionerDaemon["tags"]; +}; + +const StatefulProvisionerTagsField: FC = ({ + initialValue = {}, +}) => { + const [value, setValue] = useState(initialValue); + return ; +}; + +export const OnOverwriteOwner: Story = { + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const canvas = within(canvasElement); + const keyInput = canvas.getByLabelText("Tag key"); + const valueInput = canvas.getByLabelText("Tag value"); + const addButton = canvas.getByRole("button", { name: "Add tag" }); + + await user.type(keyInput, "owner"); + await user.type(valueInput, "dogfood-2"); + await user.click(addButton); + + await canvas.findByText("Cannot override owner tag"); + }, +}; + +export const OnInvalidScope: Story = { + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const canvas = within(canvasElement); + const keyInput = canvas.getByLabelText("Tag key"); + const valueInput = canvas.getByLabelText("Tag value"); + const addButton = canvas.getByRole("button", { name: "Add tag" }); + + await user.type(keyInput, "scope"); + await user.type(valueInput, "invalid"); + await user.click(addButton); + + await canvas.findByText("Scope value must be 'organization' or 'user'"); + }, +}; + +export const OnAddTag: Story = { + render: () => , + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const canvas = within(canvasElement); + const keyInput = canvas.getByLabelText("Tag key"); + const valueInput = canvas.getByLabelText("Tag value"); + const addButton = canvas.getByRole("button", { name: "Add tag" }); + + await user.type(keyInput, "cluster"); + await user.type(valueInput, "dogfood-2"); + await user.click(addButton); + + const addedTag = await canvas.findByTestId("tag-cluster"); + await expect(addedTag).toHaveTextContent("cluster dogfood-2"); + }, +}; + +export const OnRemoveTag: Story = { + render: () => ( + + ), + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const canvas = within(canvasElement); + const removeButton = canvas.getByRole("button", { name: "Delete cluster" }); + + await user.click(removeButton); + + await expect(canvas.queryByTestId("tag-cluster")).toBeNull(); + }, +}; diff --git a/site/src/modules/provisioners/ProvisionerTagsField.tsx b/site/src/modules/provisioners/ProvisionerTagsField.tsx new file mode 100644 index 0000000000000..f77280a377dc8 --- /dev/null +++ b/site/src/modules/provisioners/ProvisionerTagsField.tsx @@ -0,0 +1,141 @@ +import type { ProvisionerDaemon } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; +import { Input } from "components/Input/Input"; +import { PlusIcon } from "lucide-react"; +import { ProvisionerTag } from "modules/provisioners/ProvisionerTag"; +import { type FC, useState } from "react"; +import * as Yup from "yup"; + +// Users can't delete these tags +const REQUIRED_TAGS = ["scope", "organization", "user"]; + +// Users can't override these tags +const IMMUTABLE_TAGS = ["owner"]; + +type ProvisionerTagsFieldProps = { + value: ProvisionerDaemon["tags"]; + onChange: (value: ProvisionerDaemon["tags"]) => void; +}; + +export const ProvisionerTagsField: FC = ({ + value: fieldValue, + onChange, +}) => { + return ( +
+
+ {Object.entries(fieldValue) + // Filter out since users cannot override it + .filter(([key]) => !IMMUTABLE_TAGS.includes(key)) + .map(([key, value]) => { + const onDelete = (key: string) => { + const { [key]: _, ...newFieldValue } = fieldValue; + onChange(newFieldValue); + }; + + return ( + + ); + })} +
+ + { + onChange({ ...fieldValue, [tag.key]: tag.value }); + }} + /> +
+ ); +}; + +const newTagSchema = Yup.object({ + key: Yup.string() + .required("Key is required") + .notOneOf(["owner"], "Cannot override owner tag"), + value: Yup.string() + .required("Value is required") + .when("key", ([key], schema) => { + if (key === "scope") { + return schema.oneOf( + ["organization", "scope"], + "Scope value must be 'organization' or 'user'", + ); + } + + return schema; + }), +}); + +type NewTagFormProps = { + onSubmit: (values: { key: string; value: string }) => void; +}; + +const NewTagForm: FC = ({ onSubmit }) => { + const [error, setError] = useState(); + + return ( +
{ + e.preventDefault(); + const form = e.currentTarget; + const key = form.key.value.trim(); + const value = form.value.value.trim(); + + try { + await newTagSchema.validate({ key, value }); + onSubmit({ key, value }); + form.reset(); + } catch (e) { + const isValidationError = e instanceof Yup.ValidationError; + + if (!isValidationError) { + throw e; + } + + if (e instanceof Yup.ValidationError) { + setError(e.errors[0]); + } + } + }} + > +
+ + + + + + + +
+ {error && ( + {error} + )} +
+ ); +}; diff --git a/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.stories.tsx b/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.stories.tsx index 5ee83a6938d54..4d9517f42d90c 100644 --- a/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.stories.tsx +++ b/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { userEvent, within } from "@storybook/test"; +import { expect, fn, userEvent, within } from "@storybook/test"; +import { useState } from "react"; import { chromatic } from "testHelpers/chromatic"; import { MockTemplateVersion } from "testHelpers/entities"; import { ProvisionerTagsPopover } from "./ProvisionerTagsPopover"; @@ -19,14 +20,53 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const Example: Story = { - play: async ({ canvasElement, step }) => { +export const Closed: Story = {}; + +export const Open: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole("button")); + }, +}; + +export const OnTagsChange: Story = { + parameters: { + chromatic: { disableSnapshot: true }, + }, + args: { + tags: {}, + }, + render: (args) => { + const [tags, setTags] = useState(args.tags); + return ; + }, + play: async ({ canvasElement }) => { + const user = userEvent.setup(); const canvas = within(canvasElement); - await step("Open popover", async () => { - await userEvent.click(canvas.getByRole("button")); + const expandButton = canvas.getByRole("button", { + name: "Expand provisioner tags", + }); + await userEvent.click(expandButton); + + const keyInput = await canvas.findByLabelText("Tag key"); + const valueInput = await canvas.findByLabelText("Tag value"); + const addButton = await canvas.findByRole("button", { + name: "Add tag", + hidden: true, }); + + await user.type(keyInput, "cluster"); + await user.type(valueInput, "dogfood-2"); + await user.click(addButton); + const addedTag = await canvas.findByTestId("tag-cluster"); + await expect(addedTag).toHaveTextContent("cluster dogfood-2"); + + const removeButton = canvas.getByRole("button", { + name: "Delete cluster", + hidden: true, + }); + await user.click(removeButton); + await expect(canvas.queryByTestId("tag-cluster")).toBeNull(); }, }; - -export { Example as ProvisionerTagsPopover }; diff --git a/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.test.tsx b/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.test.tsx deleted file mode 100644 index 71e372b32f800..0000000000000 --- a/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.test.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { fireEvent, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { MockTemplateVersion } from "testHelpers/entities"; -import { renderComponent } from "testHelpers/renderHelpers"; -import { ProvisionerTagsPopover } from "./ProvisionerTagsPopover"; - -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/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 fooTag = await screen.findByText(/foo/i); - expect(fooTag).toBeInTheDocument(); - const barValue = await screen.findByText(/bar/i); - expect(barValue).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/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/i); - expect(el2).not.toBeInTheDocument(); - }); - }); -}); diff --git a/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.tsx b/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.tsx index 49a6480ba217b..2305fbf33ce8d 100644 --- a/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.tsx +++ b/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.tsx @@ -1,68 +1,28 @@ -import AddIcon from "@mui/icons-material/Add"; import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined"; -import Button from "@mui/material/Button"; import Link from "@mui/material/Link"; -import TextField from "@mui/material/TextField"; import useTheme from "@mui/system/useTheme"; -import { FormFields, FormSection, VerticalForm } from "components/Form/Form"; +import type { ProvisionerDaemon } from "api/typesGenerated"; +import { FormSection } from "components/Form/Form"; import { TopbarButton } from "components/FullPageLayout/Topbar"; -import { Stack } from "components/Stack/Stack"; import { Popover, PopoverContent, PopoverTrigger, } from "components/deprecated/Popover/Popover"; -import { useFormik } from "formik"; -import { ProvisionerTag } from "modules/provisioners/ProvisionerTag"; -import { type FC, Fragment } from "react"; +import { ProvisionerTagsField } from "modules/provisioners/ProvisionerTagsField"; +import type { FC } from "react"; import { docs } from "utils/docs"; -import { getFormHelpers, onChangeTrimmed } from "utils/formUtils"; -import * as Yup from "yup"; - -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; - }), -}); export interface ProvisionerTagsPopoverProps { - tags: Record; - onSubmit: (values: typeof initialValues) => void; - onDelete: (key: string) => void; + tags: ProvisionerDaemon["tags"]; + onTagsChange: (values: ProvisionerDaemon["tags"]) => void; } export const ProvisionerTagsPopover: FC = ({ tags, - onSubmit, - onDelete, + onTagsChange, }) => { const theme = useTheme(); - const form = useFormik({ - initialValues, - validationSchema, - onSubmit: (values) => { - onSubmit(values); - form.resetForm(); - }, - }); - const getFieldHelpers = getFormHelpers(form); return ( @@ -72,6 +32,7 @@ export const ProvisionerTagsPopover: FC = ({ css={{ paddingLeft: 0, paddingRight: 0, minWidth: "28px !important" }} > + Expand provisioner tags = ({ borderBottom: `1px solid ${theme.palette.divider}`, }} > - - - - Tags are a way to control which provisioner daemons complete - which build jobs.  - - Learn more... - - - } - /> - - {Object.entries(tags) - // filter out owner since you cannot override it - .filter(([key]) => key !== "owner") - .map(([key, value]) => ( - - {key === "scope" ? ( - - ) : ( - - )} - - ))} - - - - - - - - - - - + + Tags are a way to control which provisioner daemons complete + which build jobs.  + + Learn more... + + + } + > + + diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx index eb5f96e654c44..00fcc5f29e6c8 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx @@ -272,17 +272,7 @@ export const TemplateVersionEditor: FC = ({ { - onUpdateProvisionerTags({ - ...provisionerTags, - [key]: value, - }); - }} - onDelete={(key) => { - const newTags = { ...provisionerTags }; - delete newTags[key]; - onUpdateProvisionerTags(newTags); - }} + onTagsChange={onUpdateProvisionerTags} /> From 73180f6a4f1f3a05e9224a5d562ea853ee9fbe52 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Thu, 20 Feb 2025 19:15:56 +0000 Subject: [PATCH 2/4] Add ProvisionerTagsField into CreateTemplateForm --- .../modules/provisioners/ProvisionerAlert.tsx | 4 +- .../provisioners/ProvisionerTagsField.tsx | 101 +++++++++++------- .../CreateTemplatePage/CreateTemplateForm.tsx | 28 +++++ .../DuplicateTemplateView.tsx | 1 + .../ImportStarterTemplateView.tsx | 2 +- .../CreateTemplatePage/UploadTemplateView.tsx | 2 +- site/src/pages/CreateTemplatePage/utils.ts | 6 +- .../ProvisionerTagsPopover.tsx | 3 + 8 files changed, 102 insertions(+), 45 deletions(-) diff --git a/site/src/modules/provisioners/ProvisionerAlert.tsx b/site/src/modules/provisioners/ProvisionerAlert.tsx index 86d69796cd4b9..95c4417ba68ce 100644 --- a/site/src/modules/provisioners/ProvisionerAlert.tsx +++ b/site/src/modules/provisioners/ProvisionerAlert.tsx @@ -52,13 +52,13 @@ export const ProvisionerAlert: FC = ({ {title}
{detail}
- +
{Object.entries(tags ?? {}) .filter(([key]) => key !== "owner") .map(([key, value]) => ( ))} - +
); diff --git a/site/src/modules/provisioners/ProvisionerTagsField.tsx b/site/src/modules/provisioners/ProvisionerTagsField.tsx index f77280a377dc8..26ef7f2ebefe9 100644 --- a/site/src/modules/provisioners/ProvisionerTagsField.tsx +++ b/site/src/modules/provisioners/ProvisionerTagsField.tsx @@ -1,9 +1,10 @@ +import TextField from "@mui/material/TextField"; import type { ProvisionerDaemon } from "api/typesGenerated"; import { Button } from "components/Button/Button"; import { Input } from "components/Input/Input"; import { PlusIcon } from "lucide-react"; import { ProvisionerTag } from "modules/provisioners/ProvisionerTag"; -import { type FC, useState } from "react"; +import { type FC, useRef, useState } from "react"; import * as Yup from "yup"; // Users can't delete these tags @@ -45,8 +46,8 @@ export const ProvisionerTagsField: FC = ({ })} - { + { onChange({ ...fieldValue, [tag.key]: tag.value }); }} /> @@ -72,63 +73,85 @@ const newTagSchema = Yup.object({ }), }); -type NewTagFormProps = { - onSubmit: (values: { key: string; value: string }) => void; +type Tag = { key: string; value: string }; + +type NewTagControlProps = { + onAdd: (tag: Tag) => void; }; -const NewTagForm: FC = ({ onSubmit }) => { +const NewTagControl: FC = ({ onAdd }) => { + const keyInputRef = useRef(null); const [error, setError] = useState(); + const [newTag, setNewTag] = useState({ + key: "", + value: "", + }); + + const addNewTag = async () => { + try { + await newTagSchema.validate(newTag); + onAdd(newTag); + setNewTag({ key: "", value: "" }); + keyInputRef.current?.focus(); + } catch (e) { + const isValidationError = e instanceof Yup.ValidationError; + + if (!isValidationError) { + throw e; + } - return ( -
{ - e.preventDefault(); - const form = e.currentTarget; - const key = form.key.value.trim(); - const value = form.value.value.trim(); - - try { - await newTagSchema.validate({ key, value }); - onSubmit({ key, value }); - form.reset(); - } catch (e) { - const isValidationError = e instanceof Yup.ValidationError; - - if (!isValidationError) { - throw e; - } + if (e instanceof Yup.ValidationError) { + setError(e.errors[0]); + } + } + }; - if (e instanceof Yup.ValidationError) { - setError(e.errors[0]); - } - } - }} - > + const addNewTagOnEnter = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + addNewTag(); + } + }; + + return ( +
- setNewTag({ ...newTag, key: e.target.value.trim() })} + onKeyDown={addNewTagOnEnter} /> - + setNewTag({ ...newTag, value: e.target.value.trim() }) + } + onKeyDown={addNewTagOnEnter} /> - @@ -136,6 +159,6 @@ const NewTagForm: FC = ({ onSubmit }) => { {error && ( {error} )} - +
); }; diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index 617b7052a2b73..c89f5eed8c34b 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -2,6 +2,7 @@ import Link from "@mui/material/Link"; import TextField from "@mui/material/TextField"; import { provisionerDaemons } from "api/queries/organizations"; import type { + CreateTemplateVersionRequest, Organization, ProvisionerJobLog, ProvisionerType, @@ -43,6 +44,7 @@ import { import * as Yup from "yup"; import { TemplateUpload, type TemplateUploadProps } from "./TemplateUpload"; import { VariableInput } from "./VariableInput"; +import { ProvisionerTagsField } from "modules/provisioners/ProvisionerTagsField"; const MAX_DESCRIPTION_CHAR_LIMIT = 128; @@ -63,6 +65,7 @@ export interface CreateTemplateFormData { allow_everyone_group_access: boolean; provisioner_type: ProvisionerType; organization: string; + tags: CreateTemplateVersionRequest["tags"]; } const validationSchema = Yup.object({ @@ -96,6 +99,7 @@ const defaultInitialValues: CreateTemplateFormData = { allow_everyone_group_access: true, provisioner_type: "terraform", organization: "default", + tags: {}, }; type GetInitialValuesParams = { @@ -326,6 +330,30 @@ export const CreateTemplateForm: FC = (props) => { + + Tags are a way to control which provisioner daemons complete which + build jobs.  + + Learn more... + + + } + > + + form.setFieldValue("tags", tags)} + /> + + + {/* Variables */} {variables && variables.length > 0 && ( = ({ templateVersionQuery.data!.job.file_id, formData.user_variable_values, formData.provisioner_type, + formData.tags, ), template: newTemplate(formData), }); diff --git a/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx b/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx index e1dcdbcf98cbe..dc611076e4d1b 100644 --- a/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx +++ b/site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx @@ -7,7 +7,6 @@ import { import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; import { useDashboard } from "modules/dashboard/useDashboard"; -import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import type { FC } from "react"; import { useQuery } from "react-query"; import { useNavigate, useSearchParams } from "react-router-dom"; @@ -79,6 +78,7 @@ export const ImportStarterTemplateView: FC = ({ version: firstVersionFromExample( templateExample!, formData.user_variable_values, + formData.tags, ), template: newTemplate(formData), }); diff --git a/site/src/pages/CreateTemplatePage/UploadTemplateView.tsx b/site/src/pages/CreateTemplatePage/UploadTemplateView.tsx index 8294bfc44ed16..fea9c0d934249 100644 --- a/site/src/pages/CreateTemplatePage/UploadTemplateView.tsx +++ b/site/src/pages/CreateTemplatePage/UploadTemplateView.tsx @@ -7,7 +7,6 @@ import { } from "api/queries/templates"; import { displayError } from "components/GlobalSnackbar/utils"; import { useDashboard } from "modules/dashboard/useDashboard"; -import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; import type { FC } from "react"; import { useMutation, useQuery } from "react-query"; import { useNavigate } from "react-router-dom"; @@ -73,6 +72,7 @@ export const UploadTemplateView: FC = ({ uploadedFile!.hash, formData.user_variable_values, formData.provisioner_type, + formData.tags, ), template: newTemplate(formData), }); diff --git a/site/src/pages/CreateTemplatePage/utils.ts b/site/src/pages/CreateTemplatePage/utils.ts index 48e45fbdaaf52..a10c52a70c16a 100644 --- a/site/src/pages/CreateTemplatePage/utils.ts +++ b/site/src/pages/CreateTemplatePage/utils.ts @@ -58,19 +58,21 @@ export const firstVersionFromFile = ( fileId: string, variables: VariableValue[] | undefined, provisionerType: ProvisionerType, + tags: CreateTemplateVersionRequest["tags"], ): CreateTemplateVersionRequest => { return { storage_method: "file" as const, provisioner: provisionerType, user_variable_values: variables, file_id: fileId, - tags: {}, + tags, }; }; export const firstVersionFromExample = ( example: TemplateExample, variables: VariableValue[] | undefined, + tags: CreateTemplateVersionRequest["tags"], ): CreateTemplateVersionRequest => { return { storage_method: "file" as const, @@ -78,6 +80,6 @@ export const firstVersionFromExample = ( provisioner: "terraform", user_variable_values: variables, example_id: example.id, - tags: {}, + tags, }; }; diff --git a/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.tsx b/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.tsx index 2305fbf33ce8d..2d76db8f9243d 100644 --- a/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.tsx +++ b/site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.tsx @@ -47,6 +47,9 @@ export const ProvisionerTagsPopover: FC = ({ }} > From fdb4d8364e3104d716eeb2f1ae191450d57bc6d8 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Feb 2025 16:51:35 +0000 Subject: [PATCH 3/4] Run fmt --- site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index c89f5eed8c34b..da5a4e548034a 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -25,6 +25,7 @@ import { Spinner } from "components/Spinner/Spinner"; import { useFormik } from "formik"; import camelCase from "lodash/camelCase"; import capitalize from "lodash/capitalize"; +import { ProvisionerTagsField } from "modules/provisioners/ProvisionerTagsField"; import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate"; import { type FC, useState } from "react"; import { useQuery } from "react-query"; @@ -44,7 +45,6 @@ import { import * as Yup from "yup"; import { TemplateUpload, type TemplateUploadProps } from "./TemplateUpload"; import { VariableInput } from "./VariableInput"; -import { ProvisionerTagsField } from "modules/provisioners/ProvisionerTagsField"; const MAX_DESCRIPTION_CHAR_LIMIT = 128; From 8a35d294ccd8dd6d3f629f4638398dc2aa5eb9fc Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 25 Feb 2025 16:59:52 +0000 Subject: [PATCH 4/4] Only display provisioner tags when having provisioners --- .../CreateTemplatePage/CreateTemplateForm.tsx | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index da5a4e548034a..f5417872b27cd 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -221,12 +221,11 @@ export const CreateTemplateForm: FC = (props) => { }); const getFieldHelpers = getFormHelpers(form, error); - const provisionerDaemonsQuery = useQuery( + const { data: provisioners } = useQuery( selectedOrg ? { ...provisionerDaemons(selectedOrg.id), enabled: showOrganizationPicker, - select: (provisioners) => provisioners.length < 1, } : { enabled: false }, ); @@ -237,7 +236,7 @@ export const CreateTemplateForm: FC = (props) => { // form submission**!! A user could easily see this warning, connect a // provisioner, and then not refresh the page. Even if they submit without // a provisioner, it'll just sit in the job queue until they connect one. - const showProvisionerWarning = provisionerDaemonsQuery.data; + const showProvisionerWarning = provisioners ? provisioners.length < 1 : false; return ( @@ -330,29 +329,31 @@ export const CreateTemplateForm: FC = (props) => { - - Tags are a way to control which provisioner daemons complete which - build jobs.  - - Learn more... - - - } - > - - form.setFieldValue("tags", tags)} - /> - - + {provisioners && provisioners.length > 0 && ( + + Tags are a way to control which provisioner daemons complete which + build jobs.  + + Learn more... + + + } + > + + form.setFieldValue("tags", tags)} + /> + + + )} {/* Variables */} {variables && variables.length > 0 && (