feat: show a warning when an organization has no provisioners (#14136)

This commit is contained in:
Kayla Washburn-Love
2024-08-05 10:44:39 -06:00
committed by GitHub
parent efbd6257e4
commit dfeafa8f5a
5 changed files with 113 additions and 11 deletions

View File

@ -67,6 +67,13 @@ There are two exceptions:
**Organization-scoped Provisioners** can pick up build jobs created by any user.
These provisioners always have the implicit tags `scope=organization owner=""`.
```shell
coder provisionerd start --org <organization_name>
```
If you omit the `--org` argument, the provisioner will be assigned to the
default organization.
```shell
coder provisionerd start
```

View File

@ -627,6 +627,18 @@ class ApiMethods {
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getProvisionerDaemonsByOrganization = async (
organization: string,
): Promise<TypesGen.ProvisionerDaemon[]> => {
const response = await this.axios.get<TypesGen.ProvisionerDaemon[]>(
`/api/v2/organizations/${organization}/provisionerdaemons`,
);
return response.data;
};
getTemplate = async (templateId: string): Promise<TypesGen.Template> => {
const response = await this.axios.get<TypesGen.Template>(
`/api/v2/templates/${templateId}`,

View File

@ -107,3 +107,16 @@ export const organizations = () => {
queryFn: () => API.getOrganizations(),
};
};
export const getProvisionerDaemonsKey = (organization: string) => [
"organization",
organization,
"provisionerDaemons",
];
export const provisionerDaemons = (organization: string) => {
return {
queryKey: getProvisionerDaemonsKey(organization),
queryFn: () => API.getProvisionerDaemonsByOrganization(organization),
};
};

View File

@ -1,6 +1,13 @@
import { action } from "@storybook/addon-actions";
import type { Meta, StoryObj } from "@storybook/react";
import { screen, userEvent } from "@storybook/test";
import {
getProvisionerDaemonsKey,
organizationsKey,
} from "api/queries/organizations";
import {
MockDefaultOrganization,
MockOrganization2,
MockTemplate,
MockTemplateExample,
MockTemplateVersionVariable1,
@ -54,6 +61,31 @@ export const StarterTemplateWithOrgPicker: Story = {
},
};
export const StarterTemplateWithProvisionerWarning: Story = {
parameters: {
queries: [
{
key: organizationsKey,
data: [MockDefaultOrganization, MockOrganization2],
},
{
key: getProvisionerDaemonsKey(MockOrganization2.id),
data: [],
},
],
},
args: {
...StarterTemplate.args,
showOrganizationPicker: true,
},
play: async () => {
const organizationPicker = screen.getByPlaceholderText("Organization name");
await userEvent.click(organizationPicker);
const org2 = await screen.findByText(MockOrganization2.display_name);
await userEvent.click(org2);
},
};
export const DuplicateTemplateWithVariables: Story = {
args: {
copiedTemplate: MockTemplate,

View File

@ -1,10 +1,13 @@
import Link from "@mui/material/Link";
import TextField from "@mui/material/TextField";
import { useFormik } from "formik";
import camelCase from "lodash/camelCase";
import capitalize from "lodash/capitalize";
import { useState, type FC } from "react";
import { useQuery } from "react-query";
import { useSearchParams } from "react-router-dom";
import * as Yup from "yup";
import { provisionerDaemons } from "api/queries/organizations";
import type {
Organization,
ProvisionerJobLog,
@ -14,6 +17,7 @@ import type {
TemplateVersionVariable,
VariableValue,
} from "api/typesGenerated";
import { Alert } from "components/Alert/Alert";
import {
HorizontalForm,
FormSection,
@ -23,6 +27,7 @@ import {
import { IconField } from "components/IconField/IconField";
import { OrganizationAutocomplete } from "components/OrganizationAutocomplete/OrganizationAutocomplete";
import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate";
import { docs } from "utils/docs";
import {
nameValidator,
getFormHelpers,
@ -210,6 +215,24 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
});
const getFieldHelpers = getFormHelpers<CreateTemplateFormData>(form, error);
const provisionerDaemonsQuery = useQuery(
selectedOrg
? {
...provisionerDaemons(selectedOrg.id),
enabled: showOrganizationPicker,
select: (provisioners) => provisioners.length < 1,
}
: { enabled: false },
);
// TODO: Ideally, we would have a backend endpoint that could notify the
// frontend that a provisioner has been connected, so that we could hide
// this warning. In the meantime, **do not use this variable to disable
// 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;
return (
<HorizontalForm onSubmit={form.handleSubmit}>
{/* General info */}
@ -232,17 +255,20 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
)}
{showOrganizationPicker && (
<OrganizationAutocomplete
{...getFieldHelpers("organization")}
required
label="Belongs to"
value={selectedOrg}
onChange={(newValue) => {
setSelectedOrg(newValue);
void form.setFieldValue("organization", newValue?.name || "");
}}
size="medium"
/>
<>
{showProvisionerWarning && <ProvisionerWarning />}
<OrganizationAutocomplete
{...getFieldHelpers("organization")}
required
label="Belongs to"
value={selectedOrg}
onChange={(newValue) => {
setSelectedOrg(newValue);
void form.setFieldValue("organization", newValue?.name || "");
}}
size="medium"
/>
</>
)}
{"copiedTemplate" in props && (
@ -369,3 +395,15 @@ const fillNameAndDisplayWithFilename = async (
form.setFieldValue("display_name", capitalize(name)),
]);
};
const ProvisionerWarning: FC = () => {
return (
<Alert severity="warning" css={{ marginBottom: 16 }}>
This organization does not have any provisioners. Before you create a
template, you&apos;ll need to configure a provisioner.{" "}
<Link href={docs("/admin/provisioners#organization-scoped-provisioners")}>
See our documentation.
</Link>
</Alert>
);
};