Skip to content

feat: manage provisioner tags in template editor #11600

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions site/src/components/Form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const VerticalForm: FC<HTMLProps<HTMLFormElement>> = ({
export const FormSection: FC<
PropsWithChildren & {
title: string | JSX.Element;
description: string | JSX.Element;
description: string | JSX.Element | undefined;
classes?: {
root?: string;
sectionInfo?: string;
Expand Down Expand Up @@ -125,7 +125,9 @@ export const FormSection: FC<
{alpha && <AlphaBadge />}
{deprecated && <DeprecatedBadge />}
</h2>
<div css={styles.formSectionInfoDescription}>{description}</div>
{description && (
<div css={styles.formSectionInfoDescription}>{description}</div>
)}
</div>

{children}
Expand Down
6 changes: 2 additions & 4 deletions site/src/pages/HealthPage/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: "8px 8px 8px 8px",
gap: 8,
cursor: "default",
}}
Expand All @@ -183,10 +183,8 @@ export const Pill = forwardRef<HTMLDivElement, PillProps>((props, ref) => {
});

type BooleanPillProps = Omit<
ComponentProps<typeof Pill>,
"children" | "icon" | "value"
ComponentProps<typeof Pill>, "icon" | "value"
> & {
children: string;
value: boolean;
};

Expand Down
47 changes: 34 additions & 13 deletions site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ 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 { additionalTags } from "utils/provisionertags";
import CloseIcon from '@mui/icons-material/Close';
import IconButton from "@mui/material/IconButton";

export const ProvisionerDaemonsPage = () => {
const healthStatus = useOutletContext<HealthcheckReport>();
Expand Down Expand Up @@ -58,15 +62,7 @@ export const ProvisionerDaemonsPage = () => {
const daemonScope = daemon.tags["scope"] || "organization";
const iconScope =
daemonScope === "organization" ? <Business /> : <Person />;
const extraTags = Object.keys(daemon.tags)
.filter((key) => key !== "scope" && key !== "owner")
.reduce(
(acc, key) => {
acc[key] = daemon.tags[key];
return acc;
},
{} as Record<string, string>,
);
const extraTags = additionalTags(daemon.tags)
const isWarning = warnings.length > 0;
return (
<div
Expand Down Expand Up @@ -130,7 +126,7 @@ export const ProvisionerDaemonsPage = () => {
</Pill>
</Tooltip>
{Object.keys(extraTags).map((k) =>
renderTag(k, extraTags[k]),
<ProvisionerTag key={k} k={k} v={extraTags[k]} />
)}
</div>
</header>
Expand Down Expand Up @@ -188,13 +184,38 @@ 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" 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;
130 changes: 130 additions & 0 deletions site/src/pages/TemplateVersionEditorPage/ProvisionerTagsPopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@

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';

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 ProviderTagsPopoverProps {
tags: Record <string, string>;
onSubmit: (values: typeof initialValues) => void;
onDelete: (key: string) => void;
}

export const ProviderTagsPopover: FC<ProviderTagsPopoverProps> = ({ tags, onSubmit, onDelete }) => {
const theme = useTheme();

const form = useFormik({
initialValues,
validationSchema,
onSubmit: (values) => {
onSubmit(values);
form.resetForm();
},
});
const getFieldHelpers = getFormHelpers(form);

return (
<Popover isDefaultOpen={false}>
<PopoverTrigger>
<TopbarButton
data-testid="build-parameters-button"
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 provisoner daemons process which build jobs. To learn more read the docs. "/>
{Object.keys(tags).length > 0 && (
<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"
disabled={!form.dirty || !form.isValid}
>
<AddIcon/>
</Button>
</Stack>
</FormFields>
</Stack>
</VerticalForm>
</div>
</PopoverContent>
</Popover>
);
};
55 changes: 43 additions & 12 deletions site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import {
TopbarIconButton,
} from "components/FullPageLayout/Topbar";
import { Sidebar } from "components/FullPageLayout/Sidebar";
import ButtonGroup from "@mui/material/ButtonGroup";
import { ProviderTagsPopover } from "./ProvisionerTagsPopover";

type Tab = "logs" | "resources" | undefined; // Undefined is to hide the tab

Expand All @@ -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 => {
Expand Down Expand Up @@ -114,6 +118,8 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
onSubmitMissingVariableValues,
onCancelSubmitMissingVariableValues,
defaultTab,
provisionerTags,
onUpdateProvisionerTags,
}) => {
const theme = useTheme();
const [selectedTab, setSelectedTab] = useState<Tab>(defaultTab);
Expand Down Expand Up @@ -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>
<ProviderTagsPopover
tags={provisionerTags}
onSubmit={({ key, value }) => {
onUpdateProvisionerTags({
...provisionerTags,
[key]: value,
});
}}
onDelete={(key) => {
const newTags = { ...provisionerTags };
delete newTags[key];
onUpdateProvisionerTags(newTags);
}}
/>
</ButtonGroup>

<TopbarButton
variant="contained"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ 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>
Expand All @@ -127,7 +135,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,
});
Expand Down Expand Up @@ -210,6 +218,10 @@ export const TemplateVersionEditorPage: FC = () => {
onCancelSubmitMissingVariableValues={() => {
setIsMissingVariablesDialogOpen(false);
}}
provisionerTags={provisionerTags}
onUpdateProvisionerTags={(tags) => {
setProvisionerTags(tags);
}}
/>
) : (
<FullScreenLoader />
Expand Down
11 changes: 11 additions & 0 deletions site/src/utils/provisionertags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const additionalTags = (records: Record<string, string>) => {
return Object.keys(records)
.filter((key) => key !== "scope" && key !== "owner")
.reduce(
(acc, key) => {
acc[key] = records[key];
return acc;
},
{} as Record<string, string>,
);
}