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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy