chore: add managed ai usage consumption to license view (#18934)

Customers with licenses not supporting managed AI agents will receive
the following information:
<img width="597" height="261" alt="image"
src="https://github.com/user-attachments/assets/b794a9f2-bca8-494e-a8d1-cc6e6bc43bfe"
/>

Customers with active licenes for managed AI agents will see:
<img width="604" height="293" alt="image"
src="https://github.com/user-attachments/assets/7ce8931c-05c6-4e64-a5a1-2e9364e99de2"
/>

Closes https://github.com/coder/internal/issues/813

---------

Co-authored-by: McKayla Washburn <mckayla@hey.com>
This commit is contained in:
Michael Suchacz
2025-07-28 20:27:49 +02:00
committed by GitHub
co-authored by McKayla Washburn
parent 0672bf5084
commit 36d2e01471
4 changed files with 409 additions and 1 deletions
@@ -85,6 +85,9 @@ const LicensesSettingsPage: FC = () => {
isRemovingLicense={isRemovingLicense}
removeLicense={(licenseId: number) => removeLicenseApi(licenseId)}
activeUsers={userStatusCount?.active}
managedAgentFeature={
entitlementsQuery.data?.features.managed_agent_limit
}
refreshEntitlements={async () => {
try {
await refreshEntitlementsMutation.mutateAsync();
@@ -4,7 +4,7 @@ import MuiLink from "@mui/material/Link";
import Skeleton from "@mui/material/Skeleton";
import Tooltip from "@mui/material/Tooltip";
import type { GetLicensesResponse } from "api/api";
import type { UserStatusChangeCount } from "api/typesGenerated";
import type { Feature, UserStatusChangeCount } from "api/typesGenerated";
import { Button } from "components/Button/Button";
import {
SettingsHeader,
@@ -20,6 +20,7 @@ import Confetti from "react-confetti";
import { Link } from "react-router-dom";
import { LicenseCard } from "./LicenseCard";
import { LicenseSeatConsumptionChart } from "./LicenseSeatConsumptionChart";
import { ManagedAgentsConsumption } from "./ManagedAgentsConsumption";
type Props = {
showConfetti: boolean;
@@ -32,6 +33,7 @@ type Props = {
removeLicense: (licenseId: number) => void;
refreshEntitlements: () => void;
activeUsers: UserStatusChangeCount[] | undefined;
managedAgentFeature?: Feature;
};
const LicensesSettingsPageView: FC<Props> = ({
@@ -45,6 +47,7 @@ const LicensesSettingsPageView: FC<Props> = ({
removeLicense,
refreshEntitlements,
activeUsers,
managedAgentFeature,
}) => {
const theme = useTheme();
const { width, height } = useWindowSize();
@@ -151,6 +154,10 @@ const LicensesSettingsPageView: FC<Props> = ({
}))}
/>
)}
{licenses && licenses.length > 0 && (
<ManagedAgentsConsumption managedAgentFeature={managedAgentFeature} />
)}
</div>
</>
);
@@ -0,0 +1,196 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ManagedAgentsConsumption } from "./ManagedAgentsConsumption";
const meta: Meta<typeof ManagedAgentsConsumption> = {
title:
"pages/DeploymentSettingsPage/LicensesSettingsPage/ManagedAgentsConsumption",
component: ManagedAgentsConsumption,
args: {
managedAgentFeature: {
enabled: true,
actual: 50000,
soft_limit: 60000,
limit: 120000,
usage_period: {
start: "February 27, 2025",
end: "February 27, 2026",
issued_at: "February 27, 2025",
},
entitlement: "entitled",
},
},
};
export default meta;
type Story = StoryObj<typeof ManagedAgentsConsumption>;
export const Default: Story = {};
export const NearLimit: Story = {
args: {
managedAgentFeature: {
enabled: true,
actual: 115000,
soft_limit: 60000,
limit: 120000,
usage_period: {
start: "February 27, 2025",
end: "February 27, 2026",
issued_at: "February 27, 2025",
},
entitlement: "entitled",
},
},
};
export const OverIncluded: Story = {
args: {
managedAgentFeature: {
enabled: true,
actual: 80000,
soft_limit: 60000,
limit: 120000,
usage_period: {
start: "February 27, 2025",
end: "February 27, 2026",
issued_at: "February 27, 2025",
},
entitlement: "entitled",
},
},
};
export const LowUsage: Story = {
args: {
managedAgentFeature: {
enabled: true,
actual: 25000,
soft_limit: 60000,
limit: 120000,
usage_period: {
start: "February 27, 2025",
end: "February 27, 2026",
issued_at: "February 27, 2025",
},
entitlement: "entitled",
},
},
};
export const IncludedAtLimit: Story = {
args: {
managedAgentFeature: {
enabled: true,
actual: 25000,
soft_limit: 30500,
limit: 30500,
usage_period: {
start: "February 27, 2025",
end: "February 27, 2026",
issued_at: "February 27, 2025",
},
entitlement: "entitled",
},
},
};
export const Disabled: Story = {
args: {
managedAgentFeature: {
enabled: false,
actual: undefined,
soft_limit: undefined,
limit: undefined,
usage_period: undefined,
entitlement: "not_entitled",
},
},
};
export const NoFeature: Story = {
args: {
managedAgentFeature: undefined,
},
};
// Error States for Validation
export const ErrorMissingData: Story = {
args: {
managedAgentFeature: {
enabled: true,
actual: undefined,
soft_limit: undefined,
limit: undefined,
usage_period: undefined,
entitlement: "entitled",
},
},
};
export const ErrorNegativeValues: Story = {
args: {
managedAgentFeature: {
enabled: true,
actual: -100,
soft_limit: 60000,
limit: 120000,
usage_period: {
start: "February 27, 2025",
end: "February 27, 2026",
issued_at: "February 27, 2025",
},
entitlement: "entitled",
},
},
};
export const ErrorSoftLimitExceedsLimit: Story = {
args: {
managedAgentFeature: {
enabled: true,
actual: 50000,
soft_limit: 150000,
limit: 120000,
usage_period: {
start: "February 27, 2025",
end: "February 27, 2026",
issued_at: "February 27, 2025",
},
entitlement: "entitled",
},
},
};
export const ErrorInvalidDates: Story = {
args: {
managedAgentFeature: {
enabled: true,
actual: 50000,
soft_limit: 60000,
limit: 120000,
usage_period: {
start: "invalid-date",
end: "February 27, 2026",
issued_at: "February 27, 2025",
},
entitlement: "entitled",
},
},
};
export const ErrorEndBeforeStart: Story = {
args: {
managedAgentFeature: {
enabled: true,
actual: 50000,
soft_limit: 60000,
limit: 120000,
usage_period: {
start: "February 27, 2026",
end: "February 27, 2025",
issued_at: "February 27, 2025",
},
entitlement: "entitled",
},
},
};
@@ -0,0 +1,202 @@
import MuiLink from "@mui/material/Link";
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 { Stack } from "components/Stack/Stack";
import dayjs from "dayjs";
import { ChevronRightIcon } from "lucide-react";
import type { FC } from "react";
import { docs } from "utils/docs";
interface ManagedAgentsConsumptionProps {
managedAgentFeature?: Feature;
}
export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
managedAgentFeature,
}) => {
// If no feature is provided or it's disabled, show disabled state
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>
<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
upgrade your license and unlock this feature.
</span>
</Stack>
</Stack>
</div>
);
}
const usage = managedAgentFeature.actual;
const included = managedAgentFeature.soft_limit;
const limit = managedAgentFeature.limit;
const startDate = managedAgentFeature.usage_period?.start;
const endDate = managedAgentFeature.usage_period?.end;
if (!usage || usage < 0) {
return <ErrorAlert error="Invalid usage data" />;
}
if (!included || included < 0 || !limit || limit < 0) {
return <ErrorAlert error="Invalid license usage limits" />;
}
if (!startDate || !endDate) {
return <ErrorAlert error="Missing license usage period" />;
}
const start = dayjs(startDate);
const end = dayjs(endDate);
if (!start.isValid() || !end.isValid() || !start.isBefore(end)) {
return <ErrorAlert error="Invalid license usage period" />;
}
const usagePercentage = Math.min((usage / limit) * 100, 100);
const includedPercentage = Math.min((included / limit) * 100, 100);
const remainingPercentage = Math.max(100 - includedPercentage, 0);
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">Managed AI Agents Usage</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>
<MuiLink
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.
</p>
<ul>
<li className="flex items-center gap-2">
<div
className="rounded-[2px] bg-highlight-green size-3 inline-block"
aria-label="Legend for current usage in the chart"
/>
Amount of started workspaces with an AI agent.
</li>
<li className="flex items-center gap-2">
<div
className="rounded-[2px] bg-content-disabled size-3 inline-block"
aria-label="Legend for included allowance in the chart"
/>
Included allowance from your current license plan.
</li>
<li className="flex items-center gap-2">
<div
className="size-3 inline-flex items-center justify-center"
aria-label="Legend for total limit in the chart"
>
<div className="w-full border-b-1 border-t-1 border-dashed border-content-disabled" />
</div>
Total limit after which further AI workspace builds will be
blocked.
</li>
</ul>
</CollapsibleContent>
</Collapsible>
</div>
<div className="p-6 border-0 border-t border-solid">
<div className="flex justify-between text-sm text-content-secondary mb-4">
<span>
{startDate ? dayjs(startDate).format("MMMM D, YYYY") : ""}
</span>
<span>{endDate ? dayjs(endDate).format("MMMM D, YYYY") : ""}</span>
</div>
<div className="relative h-6 bg-surface-secondary rounded overflow-hidden">
<div
className="absolute top-0 left-0 h-full bg-highlight-green transition-all duration-300"
style={{ width: `${usagePercentage}%` }}
/>
<div
className="absolute top-0 h-full bg-content-disabled opacity-30"
style={{
left: `${includedPercentage}%`,
width: `${remainingPercentage}%`,
}}
/>
</div>
<div className="relative hidden lg:flex justify-between mt-4 text-sm">
<div className="flex flex-col items-start">
<span className="text-content-secondary">Actual:</span>
<span className="font-medium">{usage.toLocaleString()}</span>
</div>
<div
className="absolute flex flex-col items-center transform -translate-x-1/2"
style={{
left: `${Math.max(Math.min(includedPercentage, 90), 10)}%`,
}}
>
<span className="text-content-secondary">Included:</span>
<span className="font-medium">{included.toLocaleString()}</span>
</div>
<div className="flex flex-col items-end">
<span className="text-content-secondary">Limit:</span>
<span className="font-medium">{limit.toLocaleString()}</span>
</div>
</div>
<div className="flex lg:hidden flex-col gap-3 mt-4 text-sm">
<div className="flex justify-between">
<div className="flex flex-col items-start">
<span className="text-content-secondary">Actual:</span>
<span className="font-medium">{usage.toLocaleString()}</span>
</div>
<div className="flex flex-col items-center">
<span className="text-content-secondary">Included:</span>
<span className="font-medium">{included.toLocaleString()}</span>
</div>
<div className="flex flex-col items-end">
<span className="text-content-secondary">Limit:</span>
<span className="font-medium">{limit.toLocaleString()}</span>
</div>
</div>
</div>
</div>
</section>
);
};