mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: implement ai governance consumption frontend (#21595)
Closes [#1246](https://github.com/coder/internal/issues/1246) This PR adds a new component to display AI Governance user entitlements in the Licenses Settings page. The implementation includes: - New `AIGovernanceUsersConsumptionChart` component that shows the number of entitled users for AI Governance features - Storybook stories for various states (default, disabled, error states) - Integration with the existing license settings page - Collapsible "Learn more" section with links to relevant documentation - Updated the ManagedAgentsConsumption component with clearer terminology ("Agent Workspace Builds" instead of "Managed AI Agents") The chart displays the number of users entitled to use AI features like AI Bridge, Boundary, and Tasks, with a note that additional analytics are coming soon. ### Preview <img width="3516" height="2390" alt="CleanShot 2026-01-27 at 22 44 25@2x" src="https://github.com/user-attachments/assets/cb97a215-f054-45cb-a3e7-3055c249ef04" /> <img width="3516" height="2390" alt="CleanShot 2026-01-27 at 22 45 04@2x" src="https://github.com/user-attachments/assets/d2534189-cffb-4ad2-b2e2-67eb045572e8" /> --------- Co-authored-by: Jaayden Halko <jaayden.halko@gmail.com>
This commit is contained in:
co-authored by
Jaayden Halko
parent
1a94aa67a3
commit
62704eb858
+55
@@ -0,0 +1,55 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { AIGovernanceUsersConsumption } from "./AIGovernanceUsersConsumptionChart";
|
||||
|
||||
const meta: Meta<typeof AIGovernanceUsersConsumption> = {
|
||||
title:
|
||||
"pages/DeploymentSettingsPage/LicensesSettingsPage/AIGovernanceUsersConsumptionChart",
|
||||
component: AIGovernanceUsersConsumption,
|
||||
args: {
|
||||
aiGovernanceUserFeature: {
|
||||
enabled: true,
|
||||
entitlement: "entitled",
|
||||
limit: 1000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AIGovernanceUsersConsumption>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
aiGovernanceUserFeature: {
|
||||
enabled: false,
|
||||
entitlement: "not_entitled",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoFeature: Story = {
|
||||
args: {
|
||||
aiGovernanceUserFeature: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const ErrorMissingData: Story = {
|
||||
args: {
|
||||
aiGovernanceUserFeature: {
|
||||
enabled: true,
|
||||
entitlement: "entitled",
|
||||
limit: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ErrorNegativeValues: Story = {
|
||||
args: {
|
||||
aiGovernanceUserFeature: {
|
||||
enabled: true,
|
||||
entitlement: "entitled",
|
||||
limit: -1000,
|
||||
},
|
||||
},
|
||||
};
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import type { Feature } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "components/Collapsible/Collapsible";
|
||||
import { Link } from "components/Link/Link";
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { docs } from "utils/docs";
|
||||
|
||||
interface AIGovernanceUsersConsumptionProps {
|
||||
aiGovernanceUserFeature?: Feature;
|
||||
}
|
||||
|
||||
export const AIGovernanceUsersConsumption: FC<
|
||||
AIGovernanceUsersConsumptionProps
|
||||
> = ({ aiGovernanceUserFeature }) => {
|
||||
// If no feature is provided or it's disabled, show disabled state
|
||||
if (!aiGovernanceUserFeature?.enabled) {
|
||||
return (
|
||||
<div className="min-h-60 flex items-center justify-center rounded-lg border border-solid p-12">
|
||||
<div className="flex flex-col gap-4 items-center justify-center">
|
||||
<div className="flex flex-col gap-2 items-center justify-center">
|
||||
<span className="text-base">Users with AI Governance Add-On</span>
|
||||
<span className="text-content-secondary text-center max-w-[464px] mt-2">
|
||||
AI Governance is not included in your current license. Contact{" "}
|
||||
<Link href="mailto:sales@coder.com">sales</Link> to upgrade your
|
||||
license and unlock this addon.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const limit = aiGovernanceUserFeature.limit;
|
||||
|
||||
if (limit === undefined || limit < 0) {
|
||||
return <ErrorAlert error="Invalid license usage limits" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="border border-solid rounded">
|
||||
<div className="p-4">
|
||||
<Collapsible>
|
||||
<header className="flex flex-col gap-2 items-start">
|
||||
<h3 className="text-md m-0 font-medium">
|
||||
Users with AI Governance Add-On
|
||||
</h3>
|
||||
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
className={`
|
||||
h-auto p-0 border-0 bg-transparent font-medium text-content-secondary
|
||||
hover:bg-transparent hover:text-content-primary
|
||||
[&[data-state=open]_svg]:rotate-90
|
||||
`}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
Learn more
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</header>
|
||||
|
||||
<CollapsibleContent
|
||||
className={`
|
||||
pt-2 pl-7 pr-5 space-y-4 font-medium max-w-[720px]
|
||||
text-sm text-content-secondary
|
||||
[&_p]:m-0 [&_ul]:m-0 [&_ul]:p-0 [&_ul]:list-none
|
||||
`}
|
||||
>
|
||||
<p>
|
||||
Users using AI features like{" "}
|
||||
<Link
|
||||
href={docs("/ai-coder/ai-bridge")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
AI Bridge
|
||||
</Link>
|
||||
,{" "}
|
||||
<Link
|
||||
href={docs("/ai-coder/boundary/agent-boundary")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Boundary
|
||||
</Link>
|
||||
, or{" "}
|
||||
<Link
|
||||
href={docs("/ai-coder/tasks")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Tasks
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-12 border-0 border-t border-solid">
|
||||
<div className="flex flex-col gap-4 text-center justify-center items-center">
|
||||
<div className="flex flex-col gap-2 text-center justify-center items-center">
|
||||
<div className="text-3xl font-bold">{limit.toLocaleString()}</div>
|
||||
<div className="text-sm text-content-secondary">Users Entitled</div>
|
||||
</div>
|
||||
<div className="text-sm text-content-secondary">
|
||||
Additional analytics and measurements coming soon
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -86,6 +86,9 @@ const LicensesSettingsPage: FC = () => {
|
||||
managedAgentFeature={
|
||||
entitlementsQuery.data?.features.managed_agent_limit
|
||||
}
|
||||
aiGovernanceUserFeature={
|
||||
entitlementsQuery.data?.features.ai_governance_user_limit
|
||||
}
|
||||
refreshEntitlements={async () => {
|
||||
try {
|
||||
await refreshEntitlementsMutation.mutateAsync();
|
||||
|
||||
+11
-1
@@ -21,6 +21,7 @@ import { PlusIcon, RotateCwIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import Confetti from "react-confetti";
|
||||
import { Link } from "react-router";
|
||||
import { AIGovernanceUsersConsumption } from "./AIGovernanceUsersConsumptionChart";
|
||||
import { LicenseCard } from "./LicenseCard";
|
||||
import { LicenseSeatConsumptionChart } from "./LicenseSeatConsumptionChart";
|
||||
import { ManagedAgentsConsumption } from "./ManagedAgentsConsumption";
|
||||
@@ -37,6 +38,7 @@ type Props = {
|
||||
refreshEntitlements: () => void;
|
||||
activeUsers: UserStatusChangeCount[] | undefined;
|
||||
managedAgentFeature?: Feature;
|
||||
aiGovernanceUserFeature?: Feature;
|
||||
};
|
||||
|
||||
const LicensesSettingsPageView: FC<Props> = ({
|
||||
@@ -51,6 +53,7 @@ const LicensesSettingsPageView: FC<Props> = ({
|
||||
refreshEntitlements,
|
||||
activeUsers,
|
||||
managedAgentFeature,
|
||||
aiGovernanceUserFeature,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { width, height } = useWindowSize();
|
||||
@@ -164,7 +167,14 @@ const LicensesSettingsPageView: FC<Props> = ({
|
||||
)}
|
||||
|
||||
{licenses && licenses.length > 0 && (
|
||||
<ManagedAgentsConsumption managedAgentFeature={managedAgentFeature} />
|
||||
<>
|
||||
<ManagedAgentsConsumption
|
||||
managedAgentFeature={managedAgentFeature}
|
||||
/>
|
||||
<AIGovernanceUsersConsumption
|
||||
aiGovernanceUserFeature={aiGovernanceUserFeature}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
+29
-15
@@ -1,4 +1,3 @@
|
||||
import MuiLink from "@mui/material/Link";
|
||||
import type { Feature } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Button } from "components/Button/Button";
|
||||
@@ -7,7 +6,7 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "components/Collapsible/Collapsible";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { Link } from "components/Link/Link";
|
||||
import dayjs from "dayjs";
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
@@ -24,16 +23,16 @@ export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
|
||||
if (!managedAgentFeature?.enabled) {
|
||||
return (
|
||||
<div className="min-h-60 flex items-center justify-center rounded-lg border border-solid p-12">
|
||||
<Stack alignItems="center" spacing={1}>
|
||||
<Stack alignItems="center" spacing={0.5}>
|
||||
<span className="text-base">Managed AI Agents Disabled</span>
|
||||
<div className="flex flex-col gap-4 items-center justify-center">
|
||||
<div className="flex flex-col gap-2 items-center justify-center">
|
||||
<span className="text-base">Agent Workspace Builds Disabled</span>
|
||||
<span className="text-content-secondary text-center max-w-[464px] mt-2">
|
||||
Managed AI agents are not included in your current license.
|
||||
Contact <MuiLink href="mailto:sales@coder.com">sales</MuiLink> to
|
||||
Agent Workspace Builds are not included in your current license.
|
||||
Contact <Link href="mailto:sales@coder.com">sales</Link> to
|
||||
upgrade your license and unlock this feature.
|
||||
</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -76,7 +75,7 @@ export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
|
||||
<div className="p-4">
|
||||
<Collapsible>
|
||||
<header className="flex flex-col gap-2 items-start">
|
||||
<h3 className="text-md m-0 font-medium">Managed AI Agents Usage</h3>
|
||||
<h3 className="text-md m-0 font-medium">Agent Workspace Builds</h3>
|
||||
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
@@ -100,15 +99,30 @@ export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
|
||||
`}
|
||||
>
|
||||
<p>
|
||||
<MuiLink
|
||||
Agent Workspace Builds are measured when you start an ephemeral
|
||||
workspace, purely for running an agentic workload. These are not
|
||||
to be confused with workspaces used for day-to-day development,
|
||||
even if AI tooling is involved.
|
||||
</p>
|
||||
<p>
|
||||
Today,{" "}
|
||||
<Link
|
||||
href={docs("/ai-coder/tasks")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Coder Tasks
|
||||
</MuiLink>{" "}
|
||||
and upcoming managed AI features are included in Coder Premium
|
||||
licenses during beta. Usage limits and pricing subject to change.
|
||||
Coder Tasks (via UI, CLI, or API)
|
||||
</Link>{" "}
|
||||
is the only way to create agentic workspaces, but additional
|
||||
protocols and APIs may be supported as standards emerge. Learn
|
||||
more in{" "}
|
||||
<Link
|
||||
href={docs("/ai-coder/ai-governance")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
the Coder documentation
|
||||
</Link>
|
||||
</p>
|
||||
<ul>
|
||||
<li className="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user