feat: manage provisioner tags in template editor (#11600)

This commit is contained in:
Garrett Delfosse
2024-01-18 17:35:20 -05:00
committed by GitHub
parent 9ed3487f67
commit bf0a6fcc32
10 changed files with 495 additions and 43 deletions
+1
View File
@@ -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",
+71 -17
View File
@@ -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)
+2 -6
View File
@@ -170,7 +170,7 @@ export const Pill = forwardRef<HTMLDivElement, PillProps>((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<HTMLDivElement, PillProps>((props, ref) => {
);
});
type BooleanPillProps = Omit<
ComponentProps<typeof Pill>,
"children" | "icon" | "value"
> & {
children: string;
type BooleanPillProps = Omit<ComponentProps<typeof Pill>, "icon" | "value"> & {
value: boolean;
};
@@ -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<HealthcheckReport>();
@@ -129,9 +132,9 @@ export const ProvisionerDaemonsPage = () => {
</span>
</Pill>
</Tooltip>
{Object.keys(extraTags).map((k) =>
renderTag(k, extraTags[k]),
)}
{Object.keys(extraTags).map((k) => (
<ProvisionerTag key={k} k={k} v={extraTags[k]} />
))}
</div>
</header>
@@ -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<ProvisionerTagProps> = ({ k, v, onDelete }) => {
const { valid, value: boolValue } = parseBool(v);
const kv = `${k}: ${v}`;
const content = onDelete ? (
<>
{kv}
<IconButton
aria-label={"delete-" + k}
size="small"
color="secondary"
onClick={() => {
onDelete(k);
}}
>
<CloseIcon
fontSize="inherit"
css={{
width: 14,
height: 14,
}}
/>
</IconButton>
</>
) : (
kv
);
if (valid) {
return <BooleanPill value={boolValue}>{kv}</BooleanPill>;
return <BooleanPill value={boolValue}>{content}</BooleanPill>;
}
return <Pill icon={<Sell />}>{kv}</Pill>;
return <Pill icon={<Sell />}>{content}</Pill>;
};
export default ProvisionerDaemonsPage;
@@ -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<typeof ProvisionerTagsPopover> = {
title: "component/ProvisionerTagsPopover",
parameters: {
chromatic,
layout: "centered",
},
component: ProvisionerTagsPopover,
args: {
tags: MockTemplateVersion.job.tags,
},
render: function Render(args) {
const [{ tags }, updateArgs] = useArgs();
return (
<ProvisionerTagsPopover
{...args}
tags={tags}
onSubmit={({ key, value }) => {
updateArgs({ tags: { ...tags, [key]: value } });
}}
onDelete={(key) => {
const newTags = { ...tags };
delete newTags[key];
updateArgs({ tags: newTags });
}}
/>
);
},
};
export default meta;
type Story = StoryObj<typeof ProvisionerTagsPopover>;
export const Example: Story = {};
@@ -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(
<ProvisionerTagsPopover
tags={tags}
onSubmit={onSubmit}
onDelete={onDelete}
/>,
);
// 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(
<ProvisionerTagsPopover
tags={tags}
onSubmit={onSubmit}
onDelete={onDelete}
/>,
);
// 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(
<ProvisionerTagsPopover
tags={tags}
onSubmit={onSubmit}
onDelete={onDelete}
/>,
);
// 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(
<ProvisionerTagsPopover
tags={tags}
onSubmit={onSubmit}
onDelete={onDelete}
/>,
);
// Expect deleted tag to be gone
const el2 = screen.queryByText(/wowzers: whatatag/i);
expect(el2).not.toBeInTheDocument();
});
});
});
@@ -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<string, string>;
onSubmit: (values: typeof initialValues) => void;
onDelete: (key: string) => void;
}
export const ProvisionerTagsPopover: FC<ProvisionerTagsPopoverProps> = ({
tags,
onSubmit,
onDelete,
}) => {
const theme = useTheme();
const form = useFormik({
initialValues,
validationSchema,
onSubmit: (values) => {
onSubmit(values);
form.resetForm();
},
});
const getFieldHelpers = getFormHelpers(form);
return (
<Popover>
<PopoverTrigger>
<TopbarButton
color="neutral"
css={{ paddingLeft: 0, paddingRight: 0, minWidth: "28px !important" }}
>
<ExpandMoreOutlined css={{ fontSize: 14 }} />
</TopbarButton>
</PopoverTrigger>
<PopoverContent
horizontal="right"
css={{ ".MuiPaper-root": { width: 300 } }}
>
<div
css={{
color: theme.palette.text.secondary,
padding: 20,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<VerticalForm onSubmit={form.handleSubmit}>
<Stack>
<FormSection
title="Provisioner Tags"
description={
<>
Tags are a way to control which provisioner daemons complete
which build jobs.&nbsp;
<Link
href={docs("/admin/provisioners")}
target="_blank"
rel="noreferrer"
>
Learn more...
</Link>
</>
}
/>
<Stack direction="row" spacing={1} wrap="wrap">
{Object.keys(tags)
.filter((key) => {
// filter out owner since you cannot override it
return key !== "owner";
})
.map((k) => (
<>
{k === "scope" ? (
<ProvisionerTag key={k} k={k} v={tags[k]} />
) : (
<ProvisionerTag
key={k}
k={k}
v={tags[k]}
onDelete={onDelete}
/>
)}
</>
))}
</Stack>
<FormFields>
<Stack direction="row">
<TextField
{...getFieldHelpers("key")}
size="small"
onChange={onChangeTrimmed(form)}
label="Key"
/>
<TextField
{...getFieldHelpers("value")}
size="small"
onChange={onChangeTrimmed(form)}
label="Value"
/>
<Button
variant="contained"
color="secondary"
type="submit"
aria-label="add"
disabled={!form.dirty || !form.isValid}
>
<AddIcon />
</Button>
</Stack>
</FormFields>
</Stack>
</VerticalForm>
</div>
</PopoverContent>
</Popover>
);
};
@@ -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<string, string>;
onUpdateProvisionerTags: (tags: Record<string, string>) => void;
}
const findInitialFile = (fileTree: FileTree): string | undefined => {
@@ -114,6 +118,8 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
onSubmitMissingVariableValues,
onCancelSubmitMissingVariableValues,
defaultTab,
provisionerTags,
onUpdateProvisionerTags,
}) => {
const theme = useTheme();
const [selectedTab, setSelectedTab] = useState<Tab>(defaultTab);
@@ -236,20 +242,45 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
<TemplateVersionStatusBadge version={templateVersion} />
)}
<TopbarButton
startIcon={
<PlayArrowOutlined
css={{ color: theme.palette.success.light }}
/>
}
title="Build template (Ctrl + Enter)"
disabled={disablePreview}
onClick={() => {
triggerPreview();
<ButtonGroup
variant="outlined"
css={{
// Workaround to make the border transitions smoothly on button groups
"& > button:hover + button": {
borderLeft: "1px solid #FFF",
},
}}
disabled={disablePreview}
>
Build
</TopbarButton>
<TopbarButton
startIcon={
<PlayArrowOutlined
css={{ color: theme.palette.success.light }}
/>
}
title="Build template (Ctrl + Enter)"
disabled={disablePreview}
onClick={() => {
triggerPreview();
}}
>
Build
</TopbarButton>
<ProvisionerTagsPopover
tags={provisionerTags}
onSubmit={({ key, value }) => {
onUpdateProvisionerTags({
...provisionerTags,
[key]: value,
});
}}
onDelete={(key) => {
const newTags = { ...provisionerTags };
delete newTags[key];
onUpdateProvisionerTags(newTags);
}}
/>
</ButtonGroup>
<TopbarButton
variant="contained"
@@ -102,6 +102,16 @@ export const TemplateVersionEditorPage: FC = () => {
queryClient.setQueryData(templateVersionOptions.queryKey, newVersion);
};
// Provisioner Tags
const [provisionerTags, setProvisionerTags] = useState<
Record<string, string>
>({});
useEffect(() => {
if (templateVersionQuery.data?.job.tags) {
setProvisionerTags(templateVersionQuery.data.job.tags);
}
}, [templateVersionQuery.data?.job.tags]);
return (
<>
<Helmet>
@@ -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);
}}
/>
) : (
<FullScreenLoader />
+8 -1
View File
@@ -359,7 +359,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,
};