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 && ( pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy