mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat(site): add warnings and status indicator to provisioner groups (#14708)
This commit is contained in:
committed by
GitHub
parent
86f68b220e
commit
96e9a4f85c
@ -125,13 +125,13 @@ func (r *RootCmd) setOrganizationSettings(orgContext *OrganizationContext, setti
|
||||
|
||||
settingJSON, err := json.Marshal(output)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal organization setting %s: %w", inv.Args[0], err)
|
||||
return xerrors.Errorf("failed to marshal organization setting %s: %w", inv.Args[0], err)
|
||||
}
|
||||
|
||||
var dst bytes.Buffer
|
||||
err = json.Indent(&dst, settingJSON, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to indent organization setting as json %s: %w", inv.Args[0], err)
|
||||
return xerrors.Errorf("failed to indent organization setting as json %s: %w", inv.Args[0], err)
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(inv.Stdout, dst.String())
|
||||
@ -190,13 +190,13 @@ func (r *RootCmd) printOrganizationSetting(orgContext *OrganizationContext, sett
|
||||
|
||||
settingJSON, err := json.Marshal(output)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal organization setting %s: %w", inv.Args[0], err)
|
||||
return xerrors.Errorf("failed to marshal organization setting %s: %w", inv.Args[0], err)
|
||||
}
|
||||
|
||||
var dst bytes.Buffer
|
||||
err = json.Indent(&dst, settingJSON, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to indent organization setting as json %s: %w", inv.Args[0], err)
|
||||
return xerrors.Errorf("failed to indent organization setting as json %s: %w", inv.Args[0], err)
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(inv.Stdout, dst.String())
|
||||
|
@ -692,6 +692,18 @@ class ApiMethods {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param organization Can be the organization's ID or name
|
||||
*/
|
||||
getProvisionerDaemonGroupsByOrganization = async (
|
||||
organization: string,
|
||||
): Promise<TypesGen.ProvisionerKeyDaemons[]> => {
|
||||
const response = await this.axios.get<TypesGen.ProvisionerKeyDaemons[]>(
|
||||
`/api/v2/organizations/${organization}/provisionerkeys/daemons`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getTemplate = async (templateId: string): Promise<TypesGen.Template> => {
|
||||
const response = await this.axios.get<TypesGen.Template>(
|
||||
`/api/v2/templates/${templateId}`,
|
||||
|
@ -128,6 +128,19 @@ export const provisionerDaemons = (organization: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getProvisionerDaemonGroupsKey = (organization: string) => [
|
||||
"organization",
|
||||
organization,
|
||||
"provisionerDaemons",
|
||||
];
|
||||
|
||||
export const provisionerDaemonGroups = (organization: string) => {
|
||||
return {
|
||||
queryKey: getProvisionerDaemonGroupsKey(organization),
|
||||
queryFn: () => API.getProvisionerDaemonGroupsByOrganization(organization),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch permissions for a single organization.
|
||||
*
|
||||
|
@ -0,0 +1,63 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { StatusIndicator } from "./StatusIndicator";
|
||||
|
||||
const meta: Meta<typeof StatusIndicator> = {
|
||||
title: "components/StatusIndicator",
|
||||
component: StatusIndicator,
|
||||
args: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof StatusIndicator>;
|
||||
|
||||
export const Success: Story = {
|
||||
args: {
|
||||
color: "success",
|
||||
},
|
||||
};
|
||||
|
||||
export const SuccessOutline: Story = {
|
||||
args: {
|
||||
color: "success",
|
||||
variant: "outlined",
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
color: "warning",
|
||||
},
|
||||
};
|
||||
|
||||
export const WarningOutline: Story = {
|
||||
args: {
|
||||
color: "warning",
|
||||
variant: "outlined",
|
||||
},
|
||||
};
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
color: "danger",
|
||||
},
|
||||
};
|
||||
|
||||
export const DangerOutline: Story = {
|
||||
args: {
|
||||
color: "danger",
|
||||
variant: "outlined",
|
||||
},
|
||||
};
|
||||
|
||||
export const Inactive: Story = {
|
||||
args: {
|
||||
color: "inactive",
|
||||
},
|
||||
};
|
||||
|
||||
export const InactiveOutline: Story = {
|
||||
args: {
|
||||
color: "inactive",
|
||||
variant: "outlined",
|
||||
},
|
||||
};
|
@ -31,7 +31,7 @@ const Example: Story = {
|
||||
await step("click to open", async () => {
|
||||
await userEvent.click(canvas.getByRole("button"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/v2\.99\.99/i)).toBeInTheDocument(),
|
||||
expect(screen.getByText(/v2\.\d+\.\d+/i)).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
},
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "components/Popover/Popover";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { StatusIndicator } from "components/StatusIndicator/StatusIndicator";
|
||||
import { type FC, useState } from "react";
|
||||
import { createDayString } from "utils/createDayString";
|
||||
import { docs } from "utils/docs";
|
||||
@ -31,7 +32,7 @@ interface ProvisionerGroupProps {
|
||||
readonly buildInfo?: BuildInfoResponse;
|
||||
readonly keyName?: string;
|
||||
readonly type: ProvisionerGroupType;
|
||||
readonly provisioners: ProvisionerDaemon[];
|
||||
readonly provisioners: readonly ProvisionerDaemon[];
|
||||
}
|
||||
|
||||
export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
|
||||
@ -40,36 +41,65 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
|
||||
type,
|
||||
provisioners,
|
||||
}) => {
|
||||
const [provisioner] = provisioners;
|
||||
const theme = useTheme();
|
||||
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
const daemonScope = provisioner.tags.scope || "organization";
|
||||
const iconScope = daemonScope === "organization" ? <Business /> : <Person />;
|
||||
const firstProvisioner = provisioners[0];
|
||||
if (!firstProvisioner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const provisionerVersion = provisioner.version;
|
||||
const daemonScope = firstProvisioner.tags.scope || "organization";
|
||||
const allProvisionersAreSameVersion = provisioners.every(
|
||||
(provisioner) => provisioner.version === provisionerVersion,
|
||||
(it) => it.version === firstProvisioner.version,
|
||||
);
|
||||
const upToDate =
|
||||
allProvisionersAreSameVersion && buildInfo?.version === provisioner.version;
|
||||
const provisionerVersion = allProvisionersAreSameVersion
|
||||
? firstProvisioner.version
|
||||
: null;
|
||||
const provisionerCount =
|
||||
provisioners.length === 1
|
||||
? "1 provisioner"
|
||||
: `${provisioners.length} provisioners`;
|
||||
|
||||
const extraTags = Object.entries(provisioner.tags).filter(
|
||||
const extraTags = Object.entries(firstProvisioner.tags).filter(
|
||||
([key]) => key !== "scope" && key !== "owner",
|
||||
);
|
||||
|
||||
let warnings = 0;
|
||||
let provisionersWithWarnings = 0;
|
||||
const provisionersWithWarningInfo = provisioners.map((it) => {
|
||||
const outOfDate = Boolean(buildInfo) && it.version !== buildInfo?.version;
|
||||
const warningCount = outOfDate ? 1 : 0;
|
||||
warnings += warningCount;
|
||||
if (warnings > 0) {
|
||||
provisionersWithWarnings++;
|
||||
}
|
||||
|
||||
return { ...it, warningCount, outOfDate };
|
||||
});
|
||||
|
||||
const hasWarning = warnings > 0;
|
||||
const warningsCount =
|
||||
warnings === 0
|
||||
? "No warnings"
|
||||
: warnings === 1
|
||||
? "1 warning"
|
||||
: `${warnings} warnings`;
|
||||
const provisionersWithWarningsCount =
|
||||
provisionersWithWarnings === 1
|
||||
? "1 provisioner"
|
||||
: `${provisionersWithWarnings} provisioners`;
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 14,
|
||||
}}
|
||||
css={[
|
||||
{
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 14,
|
||||
},
|
||||
hasWarning && styles.warningBorder,
|
||||
]}
|
||||
>
|
||||
<header
|
||||
css={{
|
||||
@ -80,48 +110,39 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
|
||||
gap: 24,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 24,
|
||||
objectFit: "fill",
|
||||
}}
|
||||
>
|
||||
{type === "builtin" && (
|
||||
<div css={{ lineHeight: "160%" }}>
|
||||
<BuiltinProvisionerTitle />
|
||||
<span css={{ color: theme.palette.text.secondary }}>
|
||||
{provisionerCount} — Built-in
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{type === "psk" && (
|
||||
<div css={{ lineHeight: "160%" }}>
|
||||
<PskProvisionerTitle />
|
||||
<span css={{ color: theme.palette.text.secondary }}>
|
||||
{provisionerCount} —{" "}
|
||||
{allProvisionersAreSameVersion ? (
|
||||
<code>{provisionerVersion}</code>
|
||||
) : (
|
||||
<span>Multiple versions</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{type === "key" && (
|
||||
<div css={{ lineHeight: "160%" }}>
|
||||
<div css={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<StatusIndicator color={hasWarning ? "warning" : "success"} />
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{type === "builtin" && (
|
||||
<>
|
||||
<BuiltinProvisionerTitle />
|
||||
<span css={{ color: theme.palette.text.secondary }}>
|
||||
{provisionerCount} — Built-in
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "psk" && <PskProvisionerTitle />}
|
||||
{type === "key" && (
|
||||
<h4 css={styles.groupTitle}>Key group – {keyName}</h4>
|
||||
)}
|
||||
{type !== "builtin" && (
|
||||
<span css={{ color: theme.palette.text.secondary }}>
|
||||
{provisionerCount} —{" "}
|
||||
{allProvisionersAreSameVersion ? (
|
||||
{provisionerVersion ? (
|
||||
<code>{provisionerVersion}</code>
|
||||
) : (
|
||||
<span>Multiple versions</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
@ -133,7 +154,10 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
|
||||
}}
|
||||
>
|
||||
<Tooltip title="Scope">
|
||||
<Pill size="lg" icon={iconScope}>
|
||||
<Pill
|
||||
size="lg"
|
||||
icon={daemonScope === "organization" ? <Business /> : <Person />}
|
||||
>
|
||||
<span css={{ textTransform: "capitalize" }}>{daemonScope}</span>
|
||||
</Pill>
|
||||
</Tooltip>
|
||||
@ -153,16 +177,19 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{provisioners.map((provisioner) => (
|
||||
{provisionersWithWarningInfo.map((provisioner) => (
|
||||
<div
|
||||
key={provisioner.id}
|
||||
css={{
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 14,
|
||||
padding: "14px 18px",
|
||||
width: 375,
|
||||
}}
|
||||
css={[
|
||||
{
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 14,
|
||||
padding: "14px 18px",
|
||||
width: 375,
|
||||
},
|
||||
provisioner.warningCount > 0 && styles.warningBorder,
|
||||
]}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
@ -215,7 +242,10 @@ export const ProvisionerGroup: FC<ProvisionerGroupProps> = ({
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<span>No warnings from {provisionerCount}</span>
|
||||
<span>
|
||||
{warningsCount} from{" "}
|
||||
{hasWarning ? provisionersWithWarningsCount : provisionerCount}
|
||||
</span>
|
||||
<Button
|
||||
variant="text"
|
||||
css={{
|
||||
@ -379,6 +409,10 @@ const PskProvisionerTitle: FC = () => {
|
||||
};
|
||||
|
||||
const styles = {
|
||||
warningBorder: (theme) => ({
|
||||
borderColor: theme.roles.warning.fill.outline,
|
||||
}),
|
||||
|
||||
groupTitle: {
|
||||
fontWeight: 500,
|
||||
margin: 0,
|
||||
@ -389,7 +423,7 @@ const styles = {
|
||||
marginBottom: 0,
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: 14,
|
||||
lineHeight: "150%",
|
||||
lineHeight: 1.5,
|
||||
fontWeight: 600,
|
||||
}),
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { buildInfo } from "api/queries/buildInfo";
|
||||
import {
|
||||
organizationsPermissions,
|
||||
provisionerDaemons,
|
||||
provisionerDaemonGroups,
|
||||
} from "api/queries/organizations";
|
||||
import type { Organization, ProvisionerDaemon } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
@ -13,50 +13,7 @@ import type { FC } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useOrganizationSettings } from "./ManagementSettingsLayout";
|
||||
import {
|
||||
OrganizationProvisionersPageView,
|
||||
type ProvisionersByGroup,
|
||||
} from "./OrganizationProvisionersPageView";
|
||||
|
||||
const ProvisionerKeyIDBuiltIn = "00000000-0000-0000-0000-000000000001";
|
||||
const ProvisionerKeyIDUserAuth = "00000000-0000-0000-0000-000000000002";
|
||||
const ProvisionerKeyIDPSK = "00000000-0000-0000-0000-000000000003";
|
||||
|
||||
function groupProvisioners(
|
||||
provisioners: readonly ProvisionerDaemon[],
|
||||
): ProvisionersByGroup {
|
||||
const groups: ProvisionersByGroup = {
|
||||
builtin: [],
|
||||
psk: [],
|
||||
userAuth: [],
|
||||
keys: new Map(),
|
||||
};
|
||||
// NOTE: I'll fix this at the end of the PR chain
|
||||
const keyName = "TODO";
|
||||
|
||||
for (const it of provisioners) {
|
||||
if (it.key_id === ProvisionerKeyIDBuiltIn) {
|
||||
groups.builtin.push(it);
|
||||
continue;
|
||||
}
|
||||
if (it.key_id === ProvisionerKeyIDPSK) {
|
||||
groups.psk.push(it);
|
||||
continue;
|
||||
}
|
||||
if (it.key_id === ProvisionerKeyIDUserAuth) {
|
||||
groups.userAuth.push(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
const keyGroup = groups.keys.get(keyName) ?? [];
|
||||
if (!groups.keys.has(keyName)) {
|
||||
groups.keys.set(keyName, keyGroup);
|
||||
}
|
||||
keyGroup.push(it);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
import { OrganizationProvisionersPageView } from "./OrganizationProvisionersPageView";
|
||||
|
||||
const OrganizationProvisionersPage: FC = () => {
|
||||
const { organization: organizationName } = useParams() as {
|
||||
@ -73,7 +30,7 @@ const OrganizationProvisionersPage: FC = () => {
|
||||
const permissionsQuery = useQuery(
|
||||
organizationsPermissions(organizations?.map((o) => o.id)),
|
||||
);
|
||||
const provisionersQuery = useQuery(provisionerDaemons(organizationName));
|
||||
const provisionersQuery = useQuery(provisionerDaemonGroups(organizationName));
|
||||
|
||||
if (!organization) {
|
||||
return <EmptyState message="Organization not found" />;
|
||||
@ -105,7 +62,7 @@ const OrganizationProvisionersPage: FC = () => {
|
||||
return (
|
||||
<OrganizationProvisionersPageView
|
||||
buildInfo={buildInfoQuery.data}
|
||||
provisioners={groupProvisioners(provisioners)}
|
||||
provisioners={provisioners}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -115,6 +72,4 @@ export default OrganizationProvisionersPage;
|
||||
const getOrganizationByName = (
|
||||
organizations: readonly Organization[],
|
||||
name: string,
|
||||
) => {
|
||||
return organizations.find((org) => org.name === name);
|
||||
};
|
||||
) => organizations.find((org) => org.name === name);
|
||||
|
@ -3,6 +3,9 @@ import {
|
||||
MockBuildInfo,
|
||||
MockProvisioner,
|
||||
MockProvisioner2,
|
||||
MockProvisionerBuiltinKey,
|
||||
MockProvisionerKey,
|
||||
MockProvisionerPskKey,
|
||||
MockProvisionerWithTags,
|
||||
MockUserProvisioner,
|
||||
} from "testHelpers/entities";
|
||||
@ -21,42 +24,54 @@ type Story = StoryObj<typeof OrganizationProvisionersPageView>;
|
||||
|
||||
export const Provisioners: Story = {
|
||||
args: {
|
||||
provisioners: {
|
||||
builtin: [MockProvisioner, MockProvisioner2],
|
||||
psk: [MockProvisioner, MockUserProvisioner, MockProvisionerWithTags],
|
||||
userAuth: [],
|
||||
keys: new Map([
|
||||
[
|
||||
"ベン",
|
||||
[
|
||||
MockProvisioner,
|
||||
{
|
||||
...MockProvisioner2,
|
||||
version: "2.0.0",
|
||||
api_version: "1.0",
|
||||
warnings: [{ code: "EUNKNOWN", message: "私は時代遅れです" }],
|
||||
},
|
||||
],
|
||||
provisioners: [
|
||||
{
|
||||
key: MockProvisionerBuiltinKey,
|
||||
daemons: [MockProvisioner, MockProvisioner2],
|
||||
},
|
||||
{
|
||||
key: MockProvisionerPskKey,
|
||||
daemons: [
|
||||
MockProvisioner,
|
||||
MockUserProvisioner,
|
||||
MockProvisionerWithTags,
|
||||
],
|
||||
["ジャイデン", [MockProvisioner, MockProvisioner2]],
|
||||
[
|
||||
"ケイラ",
|
||||
[
|
||||
{
|
||||
...MockProvisioner,
|
||||
tags: {
|
||||
...MockProvisioner.tags,
|
||||
都市: "ユタ",
|
||||
きっぷ: "yes",
|
||||
ちいさい: "no",
|
||||
},
|
||||
warnings: [
|
||||
{ code: "EUNKNOWN", message: "私は日本語が話せません" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: { ...MockProvisionerKey, id: "ジャイデン", name: "ジャイデン" },
|
||||
daemons: [MockProvisioner, MockProvisioner2],
|
||||
},
|
||||
{
|
||||
key: { ...MockProvisionerKey, id: "ベン", name: "ベン" },
|
||||
daemons: [
|
||||
MockProvisioner,
|
||||
{
|
||||
...MockProvisioner2,
|
||||
version: "2.0.0",
|
||||
api_version: "1.0",
|
||||
},
|
||||
],
|
||||
]),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: { ...MockProvisionerKey, id: "ケイラ", name: "ケイラ" },
|
||||
daemons: [
|
||||
{
|
||||
...MockProvisioner,
|
||||
tags: {
|
||||
...MockProvisioner.tags,
|
||||
都市: "ユタ",
|
||||
きっぷ: "yes",
|
||||
ちいさい: "no",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
provisioners: [],
|
||||
},
|
||||
};
|
||||
|
@ -1,27 +1,30 @@
|
||||
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
|
||||
import Button from "@mui/material/Button";
|
||||
import type { BuildInfoResponse, ProvisionerDaemon } from "api/typesGenerated";
|
||||
import type {
|
||||
BuildInfoResponse,
|
||||
ProvisionerKey,
|
||||
ProvisionerKeyDaemons,
|
||||
} from "api/typesGenerated";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { ProvisionerGroup } from "modules/provisioners/ProvisionerGroup";
|
||||
import type { FC } from "react";
|
||||
import { docs } from "utils/docs";
|
||||
|
||||
export interface ProvisionersByGroup {
|
||||
builtin: ProvisionerDaemon[];
|
||||
psk: ProvisionerDaemon[];
|
||||
userAuth: ProvisionerDaemon[];
|
||||
keys: Map<string, ProvisionerDaemon[]>;
|
||||
}
|
||||
|
||||
interface OrganizationProvisionersPageViewProps {
|
||||
/** Info about the version of coderd */
|
||||
buildInfo?: BuildInfoResponse;
|
||||
provisioners: ProvisionersByGroup;
|
||||
|
||||
/** Groups of provisioners, along with their key information */
|
||||
provisioners: ProvisionerKeyDaemons[];
|
||||
}
|
||||
|
||||
export const OrganizationProvisionersPageView: FC<
|
||||
OrganizationProvisionersPageViewProps
|
||||
> = ({ buildInfo, provisioners }) => {
|
||||
const isEmpty = provisioners.every((group) => group.daemons.length === 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
@ -40,30 +43,65 @@ export const OrganizationProvisionersPageView: FC<
|
||||
<PageHeaderTitle>Provisioners</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
<Stack spacing={4.5}>
|
||||
{provisioners.builtin.length > 0 && (
|
||||
<ProvisionerGroup
|
||||
buildInfo={buildInfo}
|
||||
type="builtin"
|
||||
provisioners={provisioners.builtin}
|
||||
{isEmpty && (
|
||||
<EmptyState
|
||||
message="No provisioners"
|
||||
description="A provisioner is required before you can create templates and workspaces. You can connect your first provisioner by following our documentation."
|
||||
cta={
|
||||
<Button
|
||||
endIcon={<OpenInNewIcon />}
|
||||
target="_blank"
|
||||
href={docs("/admin/provisioners")}
|
||||
>
|
||||
Show me how to create a provisioner
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{provisioners.psk.length > 0 && (
|
||||
<ProvisionerGroup
|
||||
buildInfo={buildInfo}
|
||||
type="psk"
|
||||
provisioners={provisioners.psk}
|
||||
/>
|
||||
)}
|
||||
{[...provisioners.keys].map(([keyId, provisioners]) => (
|
||||
<ProvisionerGroup
|
||||
key={keyId}
|
||||
buildInfo={buildInfo}
|
||||
keyName={keyId}
|
||||
type="key"
|
||||
provisioners={provisioners}
|
||||
/>
|
||||
))}
|
||||
{provisioners.map((group) => {
|
||||
const type = getGroupType(group.key);
|
||||
|
||||
// We intentionally hide user-authenticated provisioners for now
|
||||
// because there are 1. some grouping issues on the backend and 2. we
|
||||
// should ideally group them by the user who authenticated them, and
|
||||
// not just lump them all together.
|
||||
if (type === "userAuth") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProvisionerGroup
|
||||
key={group.key.id}
|
||||
buildInfo={buildInfo}
|
||||
keyName={group.key.name}
|
||||
type={type}
|
||||
provisioners={group.daemons}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Ideally these would be generated and appear in typesGenerated.ts, but that is
|
||||
// not currently the case. In the meantime, these are taken from verbatim from
|
||||
// the corresponding codersdk declarations. The names remain unchanged to keep
|
||||
// usage of these special values "grep-able".
|
||||
// https://github.com/coder/coder/blob/7c77a3cc832fb35d9da4ca27df163c740f786137/codersdk/provisionerdaemons.go#L291-L295
|
||||
const ProvisionerKeyIDBuiltIn = "00000000-0000-0000-0000-000000000001";
|
||||
const ProvisionerKeyIDUserAuth = "00000000-0000-0000-0000-000000000002";
|
||||
const ProvisionerKeyIDPSK = "00000000-0000-0000-0000-000000000003";
|
||||
|
||||
function getGroupType(key: ProvisionerKey) {
|
||||
switch (key.id) {
|
||||
case ProvisionerKeyIDBuiltIn:
|
||||
return "builtin";
|
||||
case ProvisionerKeyIDUserAuth:
|
||||
return "userAuth";
|
||||
case ProvisionerKeyIDPSK:
|
||||
return "psk";
|
||||
default:
|
||||
return "key";
|
||||
}
|
||||
}
|
||||
|
@ -575,10 +575,28 @@ export const MockProvisionerKey: TypesGen.ProvisionerKey = {
|
||||
tags: { scope: "organization" },
|
||||
};
|
||||
|
||||
export const MockProvisionerBuiltinKey: TypesGen.ProvisionerKey = {
|
||||
...MockProvisionerKey,
|
||||
id: "00000000-0000-0000-0000-000000000001",
|
||||
name: "built-in",
|
||||
};
|
||||
|
||||
export const MockProvisionerUserAuthKey: TypesGen.ProvisionerKey = {
|
||||
...MockProvisionerKey,
|
||||
id: "00000000-0000-0000-0000-000000000002",
|
||||
name: "user-auth",
|
||||
};
|
||||
|
||||
export const MockProvisionerPskKey: TypesGen.ProvisionerKey = {
|
||||
...MockProvisionerKey,
|
||||
id: "00000000-0000-0000-0000-000000000003",
|
||||
name: "psk",
|
||||
};
|
||||
|
||||
export const MockProvisioner: TypesGen.ProvisionerDaemon = {
|
||||
created_at: "2022-05-17T17:39:01.382927298Z",
|
||||
id: "test-provisioner",
|
||||
key_id: "00000000-0000-0000-0000-000000000001",
|
||||
key_id: MockProvisionerBuiltinKey.id,
|
||||
organization_id: MockOrganization.id,
|
||||
name: "Test Provisioner",
|
||||
provisioners: ["echo"],
|
||||
@ -591,7 +609,7 @@ export const MockProvisioner: TypesGen.ProvisionerDaemon = {
|
||||
export const MockUserAuthProvisioner: TypesGen.ProvisionerDaemon = {
|
||||
...MockProvisioner,
|
||||
id: "test-user-auth-provisioner",
|
||||
key_id: "00000000-0000-0000-0000-000000000002",
|
||||
key_id: MockProvisionerUserAuthKey.id,
|
||||
name: `${MockUser.name}'s provisioner`,
|
||||
tags: { scope: "user" },
|
||||
};
|
||||
@ -599,7 +617,7 @@ export const MockUserAuthProvisioner: TypesGen.ProvisionerDaemon = {
|
||||
export const MockPskProvisioner: TypesGen.ProvisionerDaemon = {
|
||||
...MockProvisioner,
|
||||
id: "test-psk-provisioner",
|
||||
key_id: "00000000-0000-0000-0000-000000000003",
|
||||
key_id: MockProvisionerPskKey.id,
|
||||
name: "Test psk provisioner",
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user