10000 feat: add Organization Provisioner Keys view by johnstcn · Pull Request #17889 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

feat: add Organization Provisioner Keys view #17889

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Few design changes
  • Loading branch information
BrunoQuaresma committed May 19, 2025
commit 2866a3b4e39cdbba9556f22596e4054fc4da4e17
23 changes: 20 additions & 3 deletions site/src/components/Badge/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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,
},
},
);
Expand All @@ -46,14 +57,20 @@ export interface 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";

return (
<Comp
{...props}
ref={ref}
className={cn(badgeVariants({ variant, size, border }), className)}
className={cn(
badgeVariants({ variant, size, border, hover }),
className,
)}
/>
);
},
Expand Down
2 changes: 1 addition & 1 deletion site/src/modules/provisioners/ProvisionerTags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const ProvisionerTags: FC<HTMLProps<HTMLDivElement>> = ({
return (
<div
{...props}
className={cn(["flex items-center gap-1 flex-wrap", className])}
className={cn(["flex items-center gap-1 flex-wrap py-0.5", className])}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ const OrganizationProvisionerKeysPage: FC = () => {
};
const { organization, organizationPermissions } = useOrganizationSettings();
const { entitlements } = useDashboard();
const { metadata } = useEmbeddedMetadata();
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
const provisionerKeyDaemonsQuery = useQuery({
...provisionerDaemonGroups(organizationName),
select: (data) =>
[...data].sort((a, b) => b.daemons.length - a.daemons.length),
});

if (!organization) {
Expand Down Expand Up @@ -53,7 +53,6 @@ const OrganizationProvisionerKeysPage: FC = () => {
{helmet}
<OrganizationProvisionerKeysPageView
showPaywall={!entitlements.features.multiple_organizations.enabled}
buildVersion={buildInfoQuery.data?.version}
provisionerKeyDaemons={provisionerKeyDaemonsQuery.data}
error={provisionerKeyDaemonsQuery.error}
onRetry={provisionerKeyDaemonsQuery.refetch}
8000 Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ type Story = StoryObj<typeof OrganizationProvisionerKeysPageView>;

export const Default: Story = {
args: {
buildVersion: MockBuildInfo.version,
error: undefined,
provisionerKeyDaemons: mockProvisionerKeyDaemons,
onRetry: () => {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,24 @@ 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;
buildVersion: string | undefined;
error: unknown;
onRetry: () => void;
}

export const OrganizationProvisionerKeysPageView: FC<
OrganizationProvisionerKeysPageViewProps
> = ({ showPaywall, provisionerKeyDaemons, buildVersion, error, onRetry }) => {
> = ({ showPaywall, provisionerKeyDaemons, error, onRetry }) => {
return (
<section>
<SettingsHeader>
Expand All @@ -57,11 +64,10 @@ export const OrganizationProvisionerKeysPageView: FC<
<Table className="mt-6">
<TableHeader>
<TableRow>
<TableHead>Created</TableHead>
<TableHead>Name</TableHead>
<TableHead>ID</TableHead>
<TableHead>Tags</TableHead>
<TableHead>Provisioners</TableHead>
<TableHead>Created</TableHead>
</TableRow>
</TableHeader>
<TableBody>
Expand All @@ -77,17 +83,12 @@ export const OrganizationProvisionerKeysPageView: FC<
</TableRow>
) : (
provisionerKeyDaemons
.filter((pkd) => {
return (
pkd.key.id !== ProvisionerKeyIDBuiltIn &&
pkd.key.id !== ProvisionerKeyIDUserAuth &&
pkd.key.id !== ProvisionerKeyIDPSK
);
})
.filter(
(pkd) => !HIDDEN_PROVISIONER_KEYS.includes(pkd.key.id),
)
.map((pkd) => (
<ProvisionerKeyRow
key={pkd.key.id}
buildVersion={buildVersion}
provisionerKey={pkd.key}
provisioners={pkd.daemons}
defaultIsOpen={false}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
import type { ProvisionerDaemon, ProvisionerKey } from "api/typesGenerated";
import { Badge } from "components/Badge/Badge";
import { Button } from "components/Button/Button";
import { CopyButton } from "components/CopyButton/CopyButton";
import {
Table,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "components/Table/Table";
import { TableCell, TableRow } from "components/Table/Table";
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
import { ProvisionerTag } from "modules/provisioners/ProvisionerTags";
import { LastConnectionHead } from "pages/OrganizationSettingsPage/OrganizationProvisionersPage/LastConnectionHead";
import { ProvisionerRow } from "pages/OrganizationSettingsPage/OrganizationProvisionersPage/ProvisionerRow";
import {
ProvisionerTag,
ProvisionerTags,
ProvisionerTruncateTags,
} from "modules/provisioners/ProvisionerTags";
import { type FC, useState } from "react";
import { cn } from "utils/cn";
import { relativeTime } from "utils/time";
import { Link as RouterLink } from "react-router-dom";

type ProvisionerKeyRowProps = {
readonly provisionerKey: ProvisionerKey;
readonly provisioners: readonly ProvisionerDaemon[];
readonly buildVersion: string | undefined;
defaultIsOpen: boolean;
};

export const ProvisionerKeyRow: FC<ProvisionerKeyRowProps> = ({
provisionerKey,
provisioners,
buildVersion,
defaultIsOpen = false,
}) => {
const [isOpen, setIsOpen] = useState(defaultIsOpen);
Expand All @@ -46,70 +41,96 @@ export const ProvisionerKeyRow: FC<ProvisionerKeyRowProps> = ({
>
{isOpen ? <ChevronDownIcon /> : <ChevronRightIcon />}
<span className="sr-only">({isOpen ? "Hide" : "Show more"})</span>
<span className="block first-letter:uppercase">
{relativeTime(new Date(provisionerKey.created_at))}
</span>
{provisionerKey.name}
</Button>
</TableCell>
<TableCell>{provisionerKey.name}</TableCell>
<TableCell>
<span className="font-mono text-content-primary">
{provisionerKey.id}
</span>
<CopyButton text={provisionerKey.id} label="Copy ID" />
{Object.entries(provisionerKey.tags).length > 0 ? (
<ProvisionerTruncateTags tags={provisionerKey.tags} />
) : (
<span className="text-content-disabled">No tags</span>
)}
</TableCell>
<TableCell>
{Object.entries(provisionerKey.tags).map(([k, v]) => (
<span key={k}>
<ProvisionerTag label={k} value={v} />
</span>
))}
{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>
<TableCell>{provisioners.length}</TableCell>
</TableRow>

{isOpen && (
<TableRow>
<TableCell
colSpan={999}
className="p-0 border-l-4 border-accent bg-muted/50"
style={{ paddingLeft: "1.5rem" }}
>
{provisioners.length === 0 ? (
<TableRow>
<TableCell colSpan={999} className="p-4 border-t-0">
<span className="text-muted-foreground">
No provisioners found for this key.
</span>
</TableCell>
</TableRow>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Key</TableHead>
<TableHead>Version</TableHead>
<TableHead>Status</TableHead>
<TableHead>Tags</TableHead>
<TableHead>
<LastConnectionHead />
</TableHead>
</TableRow>
</TableHeader>
{provisioners.map((p) => (
<ProvisionerRow
key={p.id}
buildVersion={buildVersion}
provisioner={p}
defaultIsOpen={false}
/>
))}
</Table>
)}
<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[];
};

export 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>
);
};
Loading
0