mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: add Organization Provisioner Keys view (#17889)
Fixes https://github.com/coder/coder/issues/17698 **Demo:** https://github.com/user-attachments/assets/ba92693f-29b7-43ee-8d69-3d77214f3230 --------- Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
This commit is contained in:
@ -187,7 +187,7 @@ const getProvisionerDaemonGroupsKey = (organization: string) => [
|
|||||||
"provisionerDaemons",
|
"provisionerDaemons",
|
||||||
];
|
];
|
||||||
|
|
||||||
const provisionerDaemonGroups = (organization: string) => {
|
export const provisionerDaemonGroups = (organization: string) => {
|
||||||
return {
|
return {
|
||||||
queryKey: getProvisionerDaemonGroupsKey(organization),
|
queryKey: getProvisionerDaemonGroupsKey(organization),
|
||||||
queryFn: () => API.getProvisionerDaemonGroupsByOrganization(organization),
|
queryFn: () => API.getProvisionerDaemonGroupsByOrganization(organization),
|
||||||
|
@ -9,7 +9,6 @@ import { cn } from "utils/cn";
|
|||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
`inline-flex items-center rounded-md border px-2 py-1 transition-colors
|
`inline-flex items-center rounded-md border px-2 py-1 transition-colors
|
||||||
focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2
|
|
||||||
[&_svg]:pointer-events-none [&_svg]:pr-0.5 [&_svg]:py-0.5 [&_svg]:mr-0.5`,
|
[&_svg]:pointer-events-none [&_svg]:pr-0.5 [&_svg]:py-0.5 [&_svg]:mr-0.5`,
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
@ -30,11 +29,23 @@ const badgeVariants = cva(
|
|||||||
none: "border-transparent",
|
none: "border-transparent",
|
||||||
solid: "border border-solid",
|
solid: "border border-solid",
|
||||||
},
|
},
|
||||||
|
hover: {
|
||||||
|
false: null,
|
||||||
|
true: "no-underline focus:outline-none focus-visible:ring-2 focus-visible:ring-content-link",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
hover: true,
|
||||||
|
variant: "default",
|
||||||
|
class: "hover:bg-surface-tertiary",
|
||||||
|
},
|
||||||
|
],
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "md",
|
size: "md",
|
||||||
border: "solid",
|
border: "solid",
|
||||||
|
hover: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -46,14 +57,20 @@ export interface BadgeProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
|
export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
|
||||||
({ className, variant, size, border, asChild = false, ...props }, ref) => {
|
(
|
||||||
|
{ className, variant, size, border, hover, asChild = false, ...props },
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
const Comp = asChild ? Slot : "div";
|
const Comp = asChild ? Slot : "div";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(badgeVariants({ variant, size, border }), className)}
|
className={cn(
|
||||||
|
badgeVariants({ variant, size, border, hover }),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -190,6 +190,11 @@ const OrganizationSettingsNavigation: FC<
|
|||||||
>
|
>
|
||||||
Provisioners
|
Provisioners
|
||||||
</SettingsSidebarNavItem>
|
</SettingsSidebarNavItem>
|
||||||
|
<SettingsSidebarNavItem
|
||||||
|
href={urlForSubpage(organization.name, "provisioner-keys")}
|
||||||
|
>
|
||||||
|
Provisioner Keys
|
||||||
|
</SettingsSidebarNavItem>
|
||||||
<SettingsSidebarNavItem
|
<SettingsSidebarNavItem
|
||||||
href={urlForSubpage(organization.name, "provisioner-jobs")}
|
href={urlForSubpage(organization.name, "provisioner-jobs")}
|
||||||
>
|
>
|
||||||
|
@ -9,7 +9,7 @@ export const ProvisionerTags: FC<HTMLProps<HTMLDivElement>> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
className={cn(["flex items-center gap-1 flex-wrap", className])}
|
className={cn(["flex items-center gap-1 flex-wrap py-0.5", className])}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
import { provisionerDaemonGroups } from "api/queries/organizations";
|
||||||
|
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||||
|
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||||
|
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout";
|
||||||
|
import { RequirePermission } from "modules/permissions/RequirePermission";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { Helmet } from "react-helmet-async";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { pageTitle } from "utils/page";
|
||||||
|
import { OrganizationProvisionerKeysPageView } from "./OrganizationProvisionerKeysPageView";
|
||||||
|
|
||||||
|
const OrganizationProvisionerKeysPage: FC = () => {
|
||||||
|
const { organization: organizationName } = useParams() as {
|
||||||
|
organization: string;
|
||||||
|
};
|
||||||
|
const { organization, organizationPermissions } = useOrganizationSettings();
|
||||||
|
const { entitlements } = useDashboard();
|
||||||
|
const provisionerKeyDaemonsQuery = useQuery({
|
||||||
|
...provisionerDaemonGroups(organizationName),
|
||||||
|
select: (data) =>
|
||||||
|
[...data].sort((a, b) => b.daemons.length - a.daemons.length),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!organization) {
|
||||||
|
return <EmptyState message="Organization not found" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const helmet = (
|
||||||
|
<Helmet>
|
||||||
|
<title>
|
||||||
|
{pageTitle(
|
||||||
|
"Provisioner Keys",
|
||||||
|
organization.display_name || organization.name,
|
||||||
|
)}
|
||||||
|
</title>
|
||||||
|
</Helmet>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!organizationPermissions?.viewProvisioners) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{helmet}
|
||||||
|
<RequirePermission isFeatureVisible={false} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{helmet}
|
||||||
|
<OrganizationProvisionerKeysPageView
|
||||||
|
showPaywall={!entitlements.features.multiple_organizations.enabled}
|
||||||
|
provisionerKeyDaemons={provisionerKeyDaemonsQuery.data}
|
||||||
|
error={provisionerKeyDaemonsQuery.error}
|
||||||
|
onRetry={provisionerKeyDaemonsQuery.refetch}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrganizationProvisionerKeysPage;
|
@ -0,0 +1,112 @@
|
|||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import {
|
||||||
|
type ProvisionerKeyDaemons,
|
||||||
|
ProvisionerKeyIDBuiltIn,
|
||||||
|
ProvisionerKeyIDPSK,
|
||||||
|
ProvisionerKeyIDUserAuth,
|
||||||
|
} from "api/typesGenerated";
|
||||||
|
import {
|
||||||
|
MockProvisioner,
|
||||||
|
MockProvisionerKey,
|
||||||
|
mockApiError,
|
||||||
|
} from "testHelpers/entities";
|
||||||
|
import { OrganizationProvisionerKeysPageView } from "./OrganizationProvisionerKeysPageView";
|
||||||
|
|
||||||
|
const mockProvisionerKeyDaemons: ProvisionerKeyDaemons[] = [
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
...MockProvisionerKey,
|
||||||
|
},
|
||||||
|
daemons: [
|
||||||
|
{
|
||||||
|
...MockProvisioner,
|
||||||
|
name: "Test Provisioner 1",
|
||||||
|
id: "daemon-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...MockProvisioner,
|
||||||
|
name: "Test Provisioner 2",
|
||||||
|
id: "daemon-2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
...MockProvisionerKey,
|
||||||
|
name: "no-daemons",
|
||||||
|
},
|
||||||
|
daemons: [],
|
||||||
|
},
|
||||||
|
// Built-in provisioners, user-auth, and PSK keys are not shown here.
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
...MockProvisionerKey,
|
||||||
|
id: ProvisionerKeyIDBuiltIn,
|
||||||
|
name: "built-in",
|
||||||
|
},
|
||||||
|
daemons: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
...MockProvisionerKey,
|
||||||
|
id: ProvisionerKeyIDUserAuth,
|
||||||
|
name: "user-auth",
|
||||||
|
},
|
||||||
|
daemons: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
...MockProvisionerKey,
|
||||||
|
id: ProvisionerKeyIDPSK,
|
||||||
|
name: "PSK",
|
||||||
|
},
|
||||||
|
daemons: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const meta: Meta<typeof OrganizationProvisionerKeysPageView> = {
|
||||||
|
title: "pages/OrganizationProvisionerKeysPage",
|
||||||
|
component: OrganizationProvisionerKeysPageView,
|
||||||
|
args: {
|
||||||
|
error: undefined,
|
||||||
|
provisionerKeyDaemons: mockProvisionerKeyDaemons,
|
||||||
|
onRetry: () => {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof OrganizationProvisionerKeysPageView>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
error: undefined,
|
||||||
|
provisionerKeyDaemons: mockProvisionerKeyDaemons,
|
||||||
|
onRetry: () => {},
|
||||||
|
showPaywall: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Paywalled: Story = {
|
||||||
|
...Default,
|
||||||
|
args: {
|
||||||
|
showPaywall: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Empty: Story = {
|
||||||
|
...Default,
|
||||||
|
args: {
|
||||||
|
provisionerKeyDaemons: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithError: Story = {
|
||||||
|
...Default,
|
||||||
|
args: {
|
||||||
|
provisionerKeyDaemons: undefined,
|
||||||
|
error: mockApiError({
|
||||||
|
message: "Error loading provisioner keys",
|
||||||
|
detail: "Something went wrong. This is an unhelpful error message.",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,123 @@
|
|||||||
|
import {
|
||||||
|
type ProvisionerKeyDaemons,
|
||||||
|
ProvisionerKeyIDBuiltIn,
|
||||||
|
ProvisionerKeyIDPSK,
|
||||||
|
ProvisionerKeyIDUserAuth,
|
||||||
|
} from "api/typesGenerated";
|
||||||
|
import { Button } from "components/Button/Button";
|
||||||
|
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||||
|
import { Link } from "components/Link/Link";
|
||||||
|
import { Loader } from "components/Loader/Loader";
|
||||||
|
import { Paywall } from "components/Paywall/Paywall";
|
||||||
|
import {
|
||||||
|
SettingsHeader,
|
||||||
|
SettingsHeaderDescription,
|
||||||
|
SettingsHeaderTitle,
|
||||||
|
} from "components/SettingsHeader/SettingsHeader";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "components/Table/Table";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { docs } from "utils/docs";
|
||||||
|
import { ProvisionerKeyRow } from "./ProvisionerKeyRow";
|
||||||
|
|
||||||
|
// If the user using provisioner keys for external provisioners you're unlikely to
|
||||||
|
// want to keep the built-in provisioners.
|
||||||
|
const HIDDEN_PROVISIONER_KEYS = [
|
||||||
|
ProvisionerKeyIDBuiltIn,
|
||||||
|
ProvisionerKeyIDUserAuth,
|
||||||
|
ProvisionerKeyIDPSK,
|
||||||
|
];
|
||||||
|
|
||||||
|
interface OrganizationProvisionerKeysPageViewProps {
|
||||||
|
showPaywall: boolean | undefined;
|
||||||
|
provisionerKeyDaemons: ProvisionerKeyDaemons[] | undefined;
|
||||||
|
error: unknown;
|
||||||
|
onRetry: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrganizationProvisionerKeysPageView: FC<
|
||||||
|
OrganizationProvisionerKeysPageViewProps
|
||||||
|
> = ({ showPaywall, provisionerKeyDaemons, error, onRetry }) => {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<SettingsHeader>
|
||||||
|
<SettingsHeaderTitle>Provisioner Keys</SettingsHeaderTitle>
|
||||||
|
<SettingsHeaderDescription>
|
||||||
|
Manage provisioner keys used to authenticate provisioner instances.{" "}
|
||||||
|
<Link href={docs("/admin/provisioners")}>View docs</Link>
|
||||||
|
</SettingsHeaderDescription>
|
||||||
|
</SettingsHeader>
|
||||||
|
|
||||||
|
{showPaywall ? (
|
||||||
|
<Paywall
|
||||||
|
message="Provisioners"
|
||||||
|
description="Provisioners run your Terraform to create templates and workspaces. You need a Premium license to use this feature for multiple organizations."
|
||||||
|
documentationLink={docs("/")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Table className="mt-6">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Tags</TableHead>
|
||||||
|
<TableHead>Provisioners</TableHead>
|
||||||
|
<TableHead>Created</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{provisionerKeyDaemons ? (
|
||||||
|
provisionerKeyDaemons.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5}>
|
||||||
|
<EmptyState
|
||||||
|
message="No provisioner keys"
|
||||||
|
description="Create your first provisioner key to authenticate external provisioner daemons."
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
provisionerKeyDaemons
|
||||||
|
.filter(
|
||||||
|
(pkd) => !HIDDEN_PROVISIONER_KEYS.includes(pkd.key.id),
|
||||||
|
)
|
||||||
|
.map((pkd) => (
|
||||||
|
<ProvisionerKeyRow
|
||||||
|
key={pkd.key.id}
|
||||||
|
provisionerKey={pkd.key}
|
||||||
|
provisioners={pkd.daemons}
|
||||||
|
defaultIsOpen={false}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)
|
||||||
|
) : error ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5}>
|
||||||
|
<EmptyState
|
||||||
|
message="Error loading provisioner keys"
|
||||||
|
cta={
|
||||||
|
<Button onClick={onRetry} size="sm">
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={999}>
|
||||||
|
<Loader />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,136 @@
|
|||||||
|
import type { ProvisionerDaemon, ProvisionerKey } from "api/typesGenerated";
|
||||||
|
import { Badge } from "components/Badge/Badge";
|
||||||
|
import { Button } from "components/Button/Button";
|
||||||
|
import { TableCell, TableRow } from "components/Table/Table";
|
||||||
|
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
ProvisionerTag,
|
||||||
|
ProvisionerTags,
|
||||||
|
ProvisionerTruncateTags,
|
||||||
|
} from "modules/provisioners/ProvisionerTags";
|
||||||
|
import { type FC, useState } from "react";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
import { cn } from "utils/cn";
|
||||||
|
import { relativeTime } from "utils/time";
|
||||||
|
|
||||||
|
type ProvisionerKeyRowProps = {
|
||||||
|
readonly provisionerKey: ProvisionerKey;
|
||||||
|
readonly provisioners: readonly ProvisionerDaemon[];
|
||||||
|
defaultIsOpen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProvisionerKeyRow: FC<ProvisionerKeyRowProps> = ({
|
||||||
|
provisionerKey,
|
||||||
|
provisioners,
|
||||||
|
defaultIsOpen = false,
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(defaultIsOpen);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableRow key={provisionerKey.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
className={cn([
|
||||||
|
isOpen && "text-content-primary",
|
||||||
|
"p-0 h-auto min-w-0 align-middle",
|
||||||
|
])}
|
||||||
|
onClick={() => setIsOpen((v) => !v)}
|
||||||
|
>
|
||||||
|
{isOpen ? <ChevronDownIcon /> : <ChevronRightIcon />}
|
||||||
|
<span className="sr-only">({isOpen ? "Hide" : "Show more"})</span>
|
||||||
|
{provisionerKey.name}
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{Object.entries(provisionerKey.tags).length > 0 ? (
|
||||||
|
<ProvisionerTruncateTags tags={provisionerKey.tags} />
|
||||||
|
) : (
|
||||||
|
<span className="text-content-disabled">No tags</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{provisioners.length > 0 ? (
|
||||||
|
<TruncateProvisioners provisioners={provisioners} />
|
||||||
|
) : (
|
||||||
|
<span className="text-content-disabled">No provisioners</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="block first-letter:uppercase">
|
||||||
|
{relativeTime(new Date(provisionerKey.created_at))}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={999} className="p-4 border-t-0">
|
||||||
|
<dl
|
||||||
|
className={cn([
|
||||||
|
"text-xs text-content-secondary",
|
||||||
|
"m-0 grid grid-cols-[auto_1fr] gap-x-4 items-center",
|
||||||
|
"[&_dd]:text-content-primary [&_dd]:font-mono [&_dd]:leading-[22px] [&_dt]:font-medium",
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
<dt>Creation time:</dt>
|
||||||
|
<dd data-chromatic="ignore">{provisionerKey.created_at}</dd>
|
||||||
|
|
||||||
|
<dt>Tags:</dt>
|
||||||
|
<dd>
|
||||||
|
<ProvisionerTags>
|
||||||
|
{Object.entries(provisionerKey.tags).length === 0 && (
|
||||||
|
<span className="text-content-disabled">No tags</span>
|
||||||
|
)}
|
||||||
|
{Object.entries(provisionerKey.tags).map(([key, value]) => (
|
||||||
|
<ProvisionerTag key={key} label={key} value={value} />
|
||||||
|
))}
|
||||||
|
</ProvisionerTags>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Provisioners:</dt>
|
||||||
|
<dd>
|
||||||
|
<ProvisionerTags>
|
||||||
|
{provisioners.length === 0 && (
|
||||||
|
<span className="text-content-disabled">
|
||||||
|
No provisioners
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{provisioners.map((provisioner) => (
|
||||||
|
<Badge hover key={provisioner.id} size="sm" asChild>
|
||||||
|
<RouterLink
|
||||||
|
to={`../provisioners?${new URLSearchParams({ ids: provisioner.id })}`}
|
||||||
|
>
|
||||||
|
{provisionerKey.name}
|
||||||
|
</RouterLink>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</ProvisionerTags>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type TruncateProvisionersProps = {
|
||||||
|
provisioners: readonly ProvisionerDaemon[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const TruncateProvisioners: FC<TruncateProvisionersProps> = ({
|
||||||
|
provisioners,
|
||||||
|
}) => {
|
||||||
|
const firstProvisioner = provisioners[0];
|
||||||
|
const remainderCount = provisioners.length - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProvisionerTags>
|
||||||
|
<Badge size="sm">{firstProvisioner.name}</Badge>
|
||||||
|
{remainderCount > 0 && <Badge size="sm">+{remainderCount}</Badge>}
|
||||||
|
</ProvisionerTags>
|
||||||
|
);
|
||||||
|
};
|
@ -313,6 +313,12 @@ const ChangePasswordPage = lazy(
|
|||||||
const IdpOrgSyncPage = lazy(
|
const IdpOrgSyncPage = lazy(
|
||||||
() => import("./pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage"),
|
() => import("./pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage"),
|
||||||
);
|
);
|
||||||
|
const ProvisionerKeysPage = lazy(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
"./pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPage"
|
||||||
|
),
|
||||||
|
);
|
||||||
const ProvisionerJobsPage = lazy(
|
const ProvisionerJobsPage = lazy(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
@ -449,6 +455,10 @@ export const router = createBrowserRouter(
|
|||||||
path="provisioner-jobs"
|
path="provisioner-jobs"
|
||||||
element={<ProvisionerJobsPage />}
|
element={<ProvisionerJobsPage />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="provisioner-keys"
|
||||||
|
element={<ProvisionerKeysPage />}
|
||||||
|
/>
|
||||||
<Route path="idp-sync" element={<OrganizationIdPSyncPage />} />
|
<Route path="idp-sync" element={<OrganizationIdPSyncPage />} />
|
||||||
<Route path="settings" element={<OrganizationSettingsPage />} />
|
<Route path="settings" element={<OrganizationSettingsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -561,7 +561,7 @@ export const MockOrganizationMember2: TypesGen.OrganizationMemberWithUserData =
|
|||||||
roles: [],
|
roles: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const MockProvisionerKey: TypesGen.ProvisionerKey = {
|
export const MockProvisionerKey: TypesGen.ProvisionerKey = {
|
||||||
id: "test-provisioner-key",
|
id: "test-provisioner-key",
|
||||||
organization: MockOrganization.id,
|
organization: MockOrganization.id,
|
||||||
created_at: "2022-05-17T17:39:01.382927298Z",
|
created_at: "2022-05-17T17:39:01.382927298Z",
|
||||||
|
Reference in New Issue
Block a user