Skip to content

Commit dfeafa8

Browse files
authored
feat: show a warning when an organization has no provisioners (#14136)
1 parent efbd625 commit dfeafa8

File tree

5 files changed

+113
-11
lines changed

5 files changed

+113
-11
lines changed

docs/admin/provisioners.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ There are two exceptions:
6767
**Organization-scoped Provisioners** can pick up build jobs created by any user.
6868
These provisioners always have the implicit tags `scope=organization owner=""`.
6969

70+
```shell
71+
coder provisionerd start --org <organization_name>
72+
```
73+
74+
If you omit the `--org` argument, the provisioner will be assigned to the
75+
default organization.
76+
7077
```shell
7178
coder provisionerd start
7279
```

site/src/api/api.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,18 @@ class ApiMethods {
627627
return response.data;
628628
};
629629

630+
/**
631+
* @param organization Can be the organization's ID or name
632+
*/
633+
getProvisionerDaemonsByOrganization = async (
634+
organization: string,
635+
): Promise<TypesGen.ProvisionerDaemon[]> => {
636+
const response = await this.axios.get<TypesGen.ProvisionerDaemon[]>(
637+
`/api/v2/organizations/${organization}/provisionerdaemons`,
638+
);
639+
return response.data;
640+
};
641+
630642
getTemplate = async (templateId: string): Promise<TypesGen.Template> => {
631643
const response = await this.axios.get<TypesGen.Template>(
632644
`/api/v2/templates/${templateId}`,

site/src/api/queries/organizations.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,16 @@ export const organizations = () => {
107107
queryFn: () => API.getOrganizations(),
108108
};
109109
};
110+
111+
export const getProvisionerDaemonsKey = (organization: string) => [
112+
"organization",
113+
organization,
114+
"provisionerDaemons",
115+
];
116+
117+
export const provisionerDaemons = (organization: string) => {
118+
return {
119+
queryKey: getProvisionerDaemonsKey(organization),
120+
queryFn: () => API.getProvisionerDaemonsByOrganization(organization),
121+
};
122+
};

site/src/pages/CreateTemplatePage/CreateTemplateForm.stories.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { action } from "@storybook/addon-actions";
22
import type { Meta, StoryObj } from "@storybook/react";
3+
import { screen, userEvent } from "@storybook/test";
34
import {
5+
getProvisionerDaemonsKey,
6+
organizationsKey,
7+
} from "api/queries/organizations";
8+
import {
9+
MockDefaultOrganization,
10+
MockOrganization2,
411
MockTemplate,
512
MockTemplateExample,
613
MockTemplateVersionVariable1,
@@ -54,6 +61,31 @@ export const StarterTemplateWithOrgPicker: Story = {
5461
},
5562
};
5663

64+
export const StarterTemplateWithProvisionerWarning: Story = {
65+
parameters: {
66+
queries: [
67+
{
68+
key: organizationsKey,
69+
data: [MockDefaultOrganization, MockOrganization2],
70+
},
71+
{
72+
key: getProvisionerDaemonsKey(MockOrganization2.id),
73+
data: [],
74+
},
75+
],
76+
},
77+
args: {
78+
...StarterTemplate.args,
79+
showOrganizationPicker: true,
80+
},
81+
play: async () => {
82+
const organizationPicker = screen.getByPlaceholderText("Organization name");
83+
await userEvent.click(organizationPicker);
84+
const org2 = await screen.findByText(MockOrganization2.display_name);
85+
await userEvent.click(org2);
86+
},
87+
};
88+
5789
export const DuplicateTemplateWithVariables: Story = {
5890
args: {
5991
copiedTemplate: MockTemplate,

site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import Link from "@mui/material/Link";
12
import TextField from "@mui/material/TextField";
23
import { useFormik } from "formik";
34
import camelCase from "lodash/camelCase";
45
import capitalize from "lodash/capitalize";
56
import { useState, type FC } from "react";
7+
import { useQuery } from "react-query";
68
import { useSearchParams } from "react-router-dom";
79
import * as Yup from "yup";
10+
import { provisionerDaemons } from "api/queries/organizations";
811
import type {
912
Organization,
1013
ProvisionerJobLog,
@@ -14,6 +17,7 @@ import type {
1417
TemplateVersionVariable,
1518
VariableValue,
1619
} from "api/typesGenerated";
20+
import { Alert } from "components/Alert/Alert";
1721
import {
1822
HorizontalForm,
1923
FormSection,
@@ -23,6 +27,7 @@ import {
2327
import { IconField } from "components/IconField/IconField";
2428
import { OrganizationAutocomplete } from "components/OrganizationAutocomplete/OrganizationAutocomplete";
2529
import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate";
30+
import { docs } from "utils/docs";
2631
import {
2732
nameValidator,
2833
getFormHelpers,
@@ -210,6 +215,24 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
210215
});
211216
const getFieldHelpers = getFormHelpers<CreateTemplateFormData>(form, error);
212217

218+
const provisionerDaemonsQuery = useQuery(
219+
selectedOrg
220+
? {
221+
...provisionerDaemons(selectedOrg.id),
222+
enabled: showOrganizationPicker,
223+
select: (provisioners) => provisioners.length < 1,
224+
}
225+
: { enabled: false },
226+
);
227+
228+
// TODO: Ideally, we would have a backend endpoint that could notify the
229+
// frontend that a provisioner has been connected, so that we could hide
230+
// this warning. In the meantime, **do not use this variable to disable
231+
// form submission**!! A user could easily see this warning, connect a
232+
// provisioner, and then not refresh the page. Even if they submit without
233+
// a provisioner, it'll just sit in the job queue until they connect one.
234+
const showProvisionerWarning = provisionerDaemonsQuery.data;
235+
213236
return (
214237
<HorizontalForm onSubmit={form.handleSubmit}>
215238
{/* General info */}
@@ -232,17 +255,20 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
232255
)}
233256

234257
{showOrganizationPicker && (
235-
<OrganizationAutocomplete
236-
{...getFieldHelpers("organization")}
237-
required
238-
label="Belongs to"
239-
value={selectedOrg}
240-
onChange={(newValue) => {
241-
setSelectedOrg(newValue);
242-
void form.setFieldValue("organization", newValue?.name || "");
243-
}}
244-
size="medium"
245-
/>
258+
<>
259+
{showProvisionerWarning && <ProvisionerWarning />}
260+
<OrganizationAutocomplete
261+
{...getFieldHelpers("organization")}
262+
required
263+
label="Belongs to"
264+
value={selectedOrg}
265+
onChange={(newValue) => {
266+
setSelectedOrg(newValue);
267+
void form.setFieldValue("organization", newValue?.name || "");
268+
}}
269+
size="medium"
270+
/>
271+
</>
246272
)}
247273

248274
{"copiedTemplate" in props && (
@@ -369,3 +395,15 @@ const fillNameAndDisplayWithFilename = async (
369395
form.setFieldValue("display_name", capitalize(name)),
370396
]);
371397
};
398+
399+
const ProvisionerWarning: FC = () => {
400+
return (
401+
<Alert severity="warning" css={{ marginBottom: 16 }}>
402+
This organization does not have any provisioners. Before you create a
403+
template, you&apos;ll need to configure a provisioner.{" "}
404+
<Link href={docs("/admin/provisioners#organization-scoped-provisioners")}>
405+
See our documentation.
406+
</Link>
407+
</Alert>
408+
);
409+
};

0 commit comments

Comments
 (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