Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
johnstcn merged 9 commits intomainfromcj/ui/provisionerkeys
May 19, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
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
2 changes: 1 addition & 1 deletionsite/src/api/queries/organizations.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -187,7 +187,7 @@ const getProvisionerDaemonGroupsKey = (organization: string) => [
"provisionerDaemons",
];

const provisionerDaemonGroups = (organization: string) => {
exportconst provisionerDaemonGroups = (organization: string) => {
return {
queryKey: getProvisionerDaemonGroupsKey(organization),
queryFn: () => API.getProvisionerDaemonGroupsByOrganization(organization),
Expand Down
23 changes: 20 additions & 3 deletionssite/src/components/Badge/Badge.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff 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
5 changes: 5 additions & 0 deletionssite/src/modules/management/OrganizationSidebarView.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -190,6 +190,11 @@ const OrganizationSettingsNavigation: FC<
>
Provisioners
</SettingsSidebarNavItem>
<SettingsSidebarNavItem
href={urlForSubpage(organization.name, "provisioner-keys")}
>
Provisioner Keys
</SettingsSidebarNavItem>
<SettingsSidebarNavItem
href={urlForSubpage(organization.name, "provisioner-jobs")}
>
Expand Down
2 changes: 1 addition & 1 deletionsite/src/modules/provisioners/ProvisionerTags.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -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;
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -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.",
}),
},
};
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -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."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Not just multi-org right? Keys themselves require license too.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

mafredri reacted with thumbs up emoji
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>
);
};
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp