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:
Jake Howell
2026-01-29 11:22:11 +11:00
committed by GitHub
co-authored by Jaayden Halko
parent 1a94aa67a3
commit 62704eb858
5 changed files with 217 additions and 16 deletions
@@ -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,
},
},
};
@@ -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();
@@ -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>
</>
@@ -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">