Content-Length: 18563 | pFad | http://github.com/coder/coder/pull/17889.diff
thub.com
diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts
index c7b42f5f0e79f..608b2fa2a1ac4 100644
--- a/site/src/api/queries/organizations.ts
+++ b/site/src/api/queries/organizations.ts
@@ -187,7 +187,7 @@ const getProvisionerDaemonGroupsKey = (organization: string) => [
"provisionerDaemons",
];
-const provisionerDaemonGroups = (organization: string) => {
+export const provisionerDaemonGroups = (organization: string) => {
return {
queryKey: getProvisionerDaemonGroupsKey(organization),
queryFn: () => API.getProvisionerDaemonGroupsByOrganization(organization),
diff --git a/site/src/components/Badge/Badge.tsx b/site/src/components/Badge/Badge.tsx
index e6b23b8a4dd94..b4d405055bb98 100644
--- a/site/src/components/Badge/Badge.tsx
+++ b/site/src/components/Badge/Badge.tsx
@@ -9,7 +9,6 @@ import { cn } from "utils/cn";
const badgeVariants = cva(
`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`,
{
variants: {
@@ -30,11 +29,23 @@ const badgeVariants = cva(
none: "border-transparent",
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: {
variant: "default",
size: "md",
border: "solid",
+ hover: false,
},
},
);
@@ -46,14 +57,20 @@ export interface BadgeProps
}
export const Badge = forwardRef(
- ({ className, variant, size, border, asChild = false, ...props }, ref) => {
+ (
+ { className, variant, size, border, hover, asChild = false, ...props },
+ ref,
+ ) => {
const Comp = asChild ? Slot : "div";
return (
);
},
diff --git a/site/src/modules/management/OrganizationSidebarView.tsx b/site/src/modules/management/OrganizationSidebarView.tsx
index a03dc62b65c0e..745268278da49 100644
--- a/site/src/modules/management/OrganizationSidebarView.tsx
+++ b/site/src/modules/management/OrganizationSidebarView.tsx
@@ -190,6 +190,11 @@ const OrganizationSettingsNavigation: FC<
>
Provisioners
+
+ Provisioner Keys
+
diff --git a/site/src/modules/provisioners/ProvisionerTags.tsx b/site/src/modules/provisioners/ProvisionerTags.tsx
index b31be42df234f..667d2cb56ef15 100644
--- a/site/src/modules/provisioners/ProvisionerTags.tsx
+++ b/site/src/modules/provisioners/ProvisionerTags.tsx
@@ -9,7 +9,7 @@ export const ProvisionerTags: FC> = ({
return (
);
};
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPage.tsx
new file mode 100644
index 0000000000000..77bcfe10cb229
--- /dev/null
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPage.tsx
@@ -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 ;
+ }
+
+ const helmet = (
+
+
+ {pageTitle(
+ "Provisioner Keys",
+ organization.display_name || organization.name,
+ )}
+
+
+ );
+
+ if (!organizationPermissions?.viewProvisioners) {
+ return (
+ <>
+ {helmet}
+
+ >
+ );
+ }
+
+ return (
+ <>
+ {helmet}
+
+ >
+ );
+};
+
+export default OrganizationProvisionerKeysPage;
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.stories.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.stories.tsx
new file mode 100644
index 0000000000000..f30ea66175e07
--- /dev/null
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.stories.tsx
@@ -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 = {
+ title: "pages/OrganizationProvisionerKeysPage",
+ component: OrganizationProvisionerKeysPageView,
+ args: {
+ error: undefined,
+ provisionerKeyDaemons: mockProvisionerKeyDaemons,
+ onRetry: () => {},
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+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.",
+ }),
+ },
+};
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx
new file mode 100644
index 0000000000000..5373636308f15
--- /dev/null
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx
@@ -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 (
+
+
+ Provisioner Keys
+
+ Manage provisioner keys used to authenticate provisioner instances.{" "}
+ View docs
+
+
+
+ {showPaywall ? (
+
+ ) : (
+
+
+
+ Name
+ Tags
+ Provisioners
+ Created
+
+
+
+ {provisionerKeyDaemons ? (
+ provisionerKeyDaemons.length === 0 ? (
+
+
+
+
+
+ ) : (
+ provisionerKeyDaemons
+ .filter(
+ (pkd) => !HIDDEN_PROVISIONER_KEYS.includes(pkd.key.id),
+ )
+ .map((pkd) => (
+
+ ))
+ )
+ ) : error ? (
+
+
+
+ Retry
+
+ }
+ />
+
+
+ ) : (
+
+
+
+
+
+ )}
+
+
+ )}
+
+ );
+};
diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/ProvisionerKeyRow.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/ProvisionerKeyRow.tsx
new file mode 100644
index 0000000000000..e1b337c85dacb
--- /dev/null
+++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/ProvisionerKeyRow.tsx
@@ -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 = ({
+ provisionerKey,
+ provisioners,
+ defaultIsOpen = false,
+}) => {
+ const [isOpen, setIsOpen] = useState(defaultIsOpen);
+
+ return (
+ <>
+
+
+
+
+
+ {Object.entries(provisionerKey.tags).length > 0 ? (
+
+ ) : (
+ No tags
+ )}
+
+
+ {provisioners.length > 0 ? (
+
+ ) : (
+ No provisioners
+ )}
+
+
+
+ {relativeTime(new Date(provisionerKey.created_at))}
+
+
+
+
+ {isOpen && (
+
+
+
+ - Creation time:
+ - {provisionerKey.created_at}
+
+ - Tags:
+ -
+
+ {Object.entries(provisionerKey.tags).length === 0 && (
+ No tags
+ )}
+ {Object.entries(provisionerKey.tags).map(([key, value]) => (
+
+ ))}
+
+
+
+ - Provisioners:
+ -
+
+ {provisioners.length === 0 && (
+
+ No provisioners
+
+ )}
+ {provisioners.map((provisioner) => (
+
+
+ {provisionerKey.name}
+
+
+ ))}
+
+
+
+
+
+ )}
+ >
+ );
+};
+
+type TruncateProvisionersProps = {
+ provisioners: readonly ProvisionerDaemon[];
+};
+
+const TruncateProvisioners: FC = ({
+ provisioners,
+}) => {
+ const firstProvisioner = provisioners[0];
+ const remainderCount = provisioners.length - 1;
+
+ return (
+
+ {firstProvisioner.name}
+ {remainderCount > 0 && +{remainderCount}}
+
+ );
+};
diff --git a/site/src/router.tsx b/site/src/router.tsx
index 534d4037d02b3..5784696a16f2d 100644
--- a/site/src/router.tsx
+++ b/site/src/router.tsx
@@ -313,6 +313,12 @@ const ChangePasswordPage = lazy(
const IdpOrgSyncPage = lazy(
() => import("./pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPage"),
);
+const ProvisionerKeysPage = lazy(
+ () =>
+ import(
+ "./pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPage"
+ ),
+);
const ProvisionerJobsPage = lazy(
() =>
import(
@@ -449,6 +455,10 @@ export const router = createBrowserRouter(
path="provisioner-jobs"
element={}
/>
+ }
+ />
} />
} />
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts
index 6351e74d3c54d..e09b196a82446 100644
--- a/site/src/testHelpers/entities.ts
+++ b/site/src/testHelpers/entities.ts
@@ -561,7 +561,7 @@ export const MockOrganizationMember2: TypesGen.OrganizationMemberWithUserData =
roles: [],
};
-const MockProvisionerKey: TypesGen.ProvisionerKey = {
+export const MockProvisionerKey: TypesGen.ProvisionerKey = {
id: "test-provisioner-key",
organization: MockOrganization.id,
created_at: "2022-05-17T17:39:01.382927298Z",
--- a PPN by Garber Painting Akron. With Image Size Reduction included!Fetched URL: http://github.com/coder/coder/pull/17889.diff
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy