Skip to content

Commit e54de1c

Browse files
committed
initial implementation
1 parent 62dc831 commit e54de1c

File tree

4 files changed

+282
-1
lines changed

4 files changed

+282
-1
lines changed

site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ const LicensesSettingsPage: FC = () => {
8585
isRemovingLicense={isRemovingLicense}
8686
removeLicense={(licenseId: number) => removeLicenseApi(licenseId)}
8787
activeUsers={userStatusCount?.active}
88+
entitlements={entitlementsQuery.data}
8889
refreshEntitlements={async () => {
8990
try {
9091
await refreshEntitlementsMutation.mutateAsync();

site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import MuiLink from "@mui/material/Link";
44
import Skeleton from "@mui/material/Skeleton";
55
import Tooltip from "@mui/material/Tooltip";
66
import type { GetLicensesResponse } from "api/api";
7-
import type { UserStatusChangeCount } from "api/typesGenerated";
7+
import type { Entitlements, UserStatusChangeCount } from "api/typesGenerated";
88
import { Button } from "components/Button/Button";
99
import {
1010
SettingsHeader,
@@ -20,6 +20,7 @@ import Confetti from "react-confetti";
2020
import { Link } from "react-router-dom";
2121
import { LicenseCard } from "./LicenseCard";
2222
import { LicenseSeatConsumptionChart } from "./LicenseSeatConsumptionChart";
23+
import { ManagedAgentsConsumption } from "./ManagedAgentsConsumption";
2324

2425
type Props = {
2526
showConfetti: boolean;
@@ -32,6 +33,7 @@ type Props = {
3233
removeLicense: (licenseId: number) => void;
3334
refreshEntitlements: () => void;
3435
activeUsers: UserStatusChangeCount[] | undefined;
36+
entitlements?: Entitlements;
3537
};
3638

3739
const LicensesSettingsPageView: FC<Props> = ({
@@ -45,9 +47,14 @@ const LicensesSettingsPageView: FC<Props> = ({
4547
removeLicense,
4648
refreshEntitlements,
4749
activeUsers,
50+
entitlements,
4851
}) => {
4952
const theme = useTheme();
5053
const { width, height } = useWindowSize();
54+
const managedAgentFeature = entitlements?.features?.managed_agent_limit;
55+
const managedAgentLimitStarts = entitlements?.features?.managed_agent_limit?.usage_period?.start;
56+
const managedAgentLimitExpires = entitlements?.features?.managed_agent_limit?.usage_period?.end;
57+
const managedAgentFeatureEnabled = entitlements?.features?.managed_agent_limit?.enabled;
5158

5259
return (
5360
<>
@@ -151,6 +158,17 @@ const LicensesSettingsPageView: FC<Props> = ({
151158
}))}
152159
/>
153160
)}
161+
162+
{licenses && licenses.length > 0 && managedAgentFeature && (
163+
<ManagedAgentsConsumption
164+
usage={managedAgentFeature.actual || 0}
165+
included={managedAgentFeature.soft_limit || 0}
166+
limit={managedAgentFeature.limit || 0}
167+
startDate={managedAgentLimitStarts || ""}
168+
endDate={managedAgentLimitExpires || ""}
169+
enabled={managedAgentFeatureEnabled}
170+
/>
171+
)}
154172
</div>
155173
</>
156174
);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { ManagedAgentsConsumption } from "./ManagedAgentsConsumption";
3+
4+
const meta: Meta<typeof ManagedAgentsConsumption> = {
5+
title:
6+
"pages/DeploymentSettingsPage/LicensesSettingsPage/ManagedAgentsConsumption",
7+
component: ManagedAgentsConsumption,
8+
args: {
9+
usage: 50000,
10+
included: 60000,
11+
limit: 120000,
12+
startDate: "February 27, 2025",
13+
endDate: "February 27, 2026",
14+
},
15+
};
16+
17+
export default meta;
18+
type Story = StoryObj<typeof ManagedAgentsConsumption>;
19+
20+
export const Default: Story = {};
21+
22+
export const NearLimit: Story = {
23+
args: {
24+
usage: 115000,
25+
included: 60000,
26+
limit: 120000,
27+
},
28+
};
29+
30+
export const OverIncluded: Story = {
31+
args: {
32+
usage: 80000,
33+
included: 60000,
34+
limit: 120000,
35+
},
36+
};
37+
38+
export const LowUsage: Story = {
39+
args: {
40+
usage: 25000,
41+
included: 60000,
42+
limit: 120000,
43+
},
44+
};
45+
46+
export const Disabled: Story = {
47+
args: {
48+
usage: NaN,
49+
included: NaN,
50+
limit: NaN,
51+
},
52+
};
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { Button } from "components/Button/Button";
2+
import {
3+
Collapsible,
4+
CollapsibleContent,
5+
CollapsibleTrigger,
6+
} from "components/Collapsible/Collapsible";
7+
import { Link } from "components/Link/Link";
8+
import { Stack } from "components/Stack/Stack";
9+
import { ChevronRightIcon } from "lucide-react";
10+
import type { FC } from "react";
11+
import { Link as RouterLink } from "react-router-dom";
12+
import { docs } from "utils/docs";
13+
import MuiLink from "@mui/material/Link";
14+
import { type Interpolation, type Theme } from "@emotion/react";
15+
import dayjs from "dayjs";
16+
17+
interface ManagedAgentsConsumptionProps {
18+
usage: number;
19+
included: number;
20+
limit: number;
21+
startDate: string;
22+
endDate: string;
23+
enabled?: boolean;
24+
}
25+
26+
export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
27+
usage,
28+
included,
29+
limit,
30+
startDate,
31+
endDate,
32+
enabled = true,
33+
}) => {
34+
// If feature is disabled, show disabled state
35+
if (!enabled) {
36+
return (
37+
<div css={styles.disabledRoot}>
38+
<Stack alignItems="center" spacing={1}>
39+
<Stack alignItems="center" spacing={0.5}>
40+
<span css={styles.disabledTitle}>
41+
Managed AI Agent Feature Disabled
42+
</span>
43+
<span css={styles.disabledDescription}>
44+
The managed AI agent feature is not included in your current license.
45+
Contact{" "}
46+
<MuiLink href="mailto:sales@coder.com">sales</MuiLink> to
47+
upgrade your license and unlock this feature.
48+
</span>
49+
</Stack>
50+
</Stack>
51+
</div>
52+
);
53+
}
54+
55+
// Calculate percentages for the progress bar
56+
const usagePercentage = Math.min((usage / limit) * 100, 100);
57+
const includedPercentage = Math.min((included / limit) * 100, 100);
58+
const remainingPercentage = Math.max(100 - includedPercentage, 0);
59+
60+
return (
61+
<section className="border border-solid rounded">
62+
<div className="p-4">
63+
<Collapsible>
64+
<header className="flex flex-col gap-2 items-start">
65+
<h3 className="text-md m-0 font-medium">
66+
Managed agents consumption
67+
</h3>
68+
69+
<CollapsibleTrigger asChild>
70+
<Button
71+
className={`
72+
h-auto p-0 border-0 bg-transparent font-medium text-content-secondary
73+
hover:bg-transparent hover:text-content-primary
74+
[&[data-state=open]_svg]:rotate-90
75+
`}
76+
>
77+
<ChevronRightIcon />
78+
How we calculate managed agent consumption
79+
</Button>
80+
</CollapsibleTrigger>
81+
</header>
82+
83+
<CollapsibleContent
84+
className={`
85+
pt-2 pl-7 pr-5 space-y-4 font-medium max-w-[720px]
86+
text-sm text-content-secondary
87+
[&_p]:m-0 [&_ul]:m-0 [&_ul]:p-0 [&_ul]:list-none
88+
`}
89+
>
90+
<p>
91+
Managed agents are counted based on active agent connections during the billing period.
92+
Each unique agent that connects to your deployment consumes one managed agent seat.
93+
</p>
94+
<ul>
95+
<li className="flex items-center gap-2">
96+
<div
97+
className="rounded-[2px] bg-highlight-green size-3 inline-block"
98+
aria-label="Legend for current usage in the chart"
99+
/>
100+
Current usage represents active managed agents during this period.
101+
</li>
102+
<li className="flex items-center gap-2">
103+
<div
104+
className="rounded-[2px] bg-content-disabled size-3 inline-block"
105+
aria-label="Legend for included allowance in the chart"
106+
/>
107+
Included allowance from your current license plan.
108+
</li>
109+
<li className="flex items-center gap-2">
110+
<div
111+
className="size-3 inline-flex items-center justify-center"
112+
aria-label="Legend for total limit in the chart"
113+
>
114+
<div className="w-full border-b-1 border-t-1 border-dashed border-content-disabled" />
115+
</div>
116+
Total limit including any additional purchased capacity.
117+
</li>
118+
</ul>
119+
<div>
120+
You might also check:
121+
<ul>
122+
<li>
123+
<Link asChild>
124+
<RouterLink to="/deployment/overview">
125+
Deployment overview
126+
</RouterLink>
127+
</Link>
128+
</li>
129+
<li>
130+
<Link
131+
href={docs("/admin/managed-agents")}
132+
target="_blank"
133+
rel="noreferrer"
134+
>
135+
More details on managed agents
136+
</Link>
137+
</li>
138+
</ul>
139+
</div>
140+
</CollapsibleContent>
141+
</Collapsible>
142+
</div>
143+
144+
<div className="p-6 border-0 border-t border-solid">
145+
{/* Date range */}
146+
<div className="flex justify-between text-sm text-content-secondary mb-4">
147+
<span>{startDate ? dayjs(startDate).format("MMMM D, YYYY") : ""}</span>
148+
<span>{endDate ? dayjs(endDate).format("MMMM D, YYYY") : ""}</span>
149+
</div>
150+
151+
{/* Progress bar container */}
152+
<div className="relative h-6 bg-surface-secondary rounded overflow-hidden">
153+
{/* Usage bar (green) */}
154+
<div
155+
className="absolute top-0 left-0 h-full bg-highlight-green transition-all duration-300"
156+
style={{ width: `${usagePercentage}%` }}
157+
/>
158+
159+
{/* Included allowance background (darker) */}
160+
<div
161+
className="absolute top-0 h-full bg-content-disabled opacity-30"
162+
style={{
163+
left: `${includedPercentage}%`,
164+
width: `${remainingPercentage}%`,
165+
}}
166+
/>
167+
</div>
168+
169+
{/* Labels */}
170+
<div className="flex justify-between mt-4 text-sm">
171+
<div className="flex flex-col items-start">
172+
<span className="text-content-secondary">Usage:</span>
173+
<span className="font-medium">{usage.toLocaleString()}</span>
174+
</div>
175+
<div className="flex flex-col items-center">
176+
<span className="text-content-secondary">Included:</span>
177+
<span className="font-medium">{included.toLocaleString()}</span>
178+
</div>
179+
<div className="flex flex-col items-end">
180+
<span className="text-content-secondary">Limit:</span>
181+
<span className="font-medium">{limit.toLocaleString()}</span>
182+
</div>
183+
</div>
184+
</div>
185+
</section>
186+
);
187+
};
188+
189+
const styles = {
190+
disabledTitle: {
191+
fontSize: 16,
192+
},
193+
194+
disabledRoot: (theme) => ({
195+
minHeight: 240,
196+
display: "flex",
197+
alignItems: "center",
198+
justifyContent: "center",
199+
borderRadius: 8,
200+
border: `1px solid ${theme.palette.divider}`,
201+
padding: 48,
202+
}),
203+
204+
disabledDescription: (theme) => ({
205+
color: theme.palette.text.secondary,
206+
textAlign: "center",
207+
maxWidth: 464,
208+
marginTop: 8,
209+
}),
210+
} satisfies Record<string, Interpolation<Theme>>;

0 commit comments

Comments
 (0)