-
Notifications
You must be signed in to change notification settings - Fork 952
chore: add managed ai usage consumption to license view #18934
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e54de1c
74ff038
2e87ac2
bcb7faa
9d58c44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,9 +47,13 @@ const LicensesSettingsPageView: FC<Props> = ({ | |
removeLicense, | ||
refreshEntitlements, | ||
activeUsers, | ||
managedAgentFeature, | ||
}) => { | ||
const theme = useTheme(); | ||
const { width, height } = useWindowSize(); | ||
const managedAgentLimitStarts = managedAgentFeature?.usage_period?.start; | ||
const managedAgentLimitExpires = managedAgentFeature?.usage_period?.end; | ||
const managedAgentFeatureEnabled = managedAgentFeature?.enabled; | ||
|
||
return ( | ||
<> | ||
|
@@ -151,6 +157,17 @@ const LicensesSettingsPageView: FC<Props> = ({ | |
}))} | ||
/> | ||
)} | ||
|
||
{licenses && licenses.length > 0 && managedAgentFeature && ( | ||
<ManagedAgentsConsumption | ||
usage={managedAgentFeature.actual || 0} | ||
included={managedAgentFeature.soft_limit || 0} | ||
limit={managedAgentFeature.limit || 0} | ||
startDate={managedAgentLimitStarts || ""} | ||
endDate={managedAgentLimitExpires || ""} | ||
enabled={managedAgentFeatureEnabled} | ||
Comment on lines
+163
to
+168
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ...and then we don't need to do this defaulting here. we should do some validation inside of this component and return |
||
/> | ||
)} | ||
</div> | ||
</> | ||
); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
import { ManagedAgentsConsumption } from "./ManagedAgentsConsumption"; | ||
|
||
const meta: Meta<typeof ManagedAgentsConsumption> = { | ||
title: | ||
"pages/DeploymentSettingsPage/LicensesSettingsPage/ManagedAgentsConsumption", | ||
ibetitsmike marked this conversation as resolved.
Show resolved
Hide resolved
|
||
component: ManagedAgentsConsumption, | ||
args: { | ||
usage: 50000, | ||
included: 60000, | ||
limit: 120000, | ||
startDate: "February 27, 2025", | ||
endDate: "February 27, 2026", | ||
}, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof ManagedAgentsConsumption>; | ||
|
||
export const Default: Story = {}; | ||
|
||
export const NearLimit: Story = { | ||
args: { | ||
usage: 115000, | ||
included: 60000, | ||
limit: 120000, | ||
}, | ||
}; | ||
|
||
export const OverIncluded: Story = { | ||
args: { | ||
usage: 80000, | ||
included: 60000, | ||
limit: 120000, | ||
}, | ||
}; | ||
|
||
export const LowUsage: Story = { | ||
args: { | ||
usage: 25000, | ||
included: 60000, | ||
limit: 120000, | ||
}, | ||
}; | ||
|
||
export const IncludedAtLimit: Story = { | ||
args: { | ||
usage: 25000, | ||
included: 30500, | ||
limit: 30500, | ||
}, | ||
}; | ||
|
||
export const Disabled: Story = { | ||
args: { | ||
enabled: false, | ||
usage: Number.NaN, | ||
included: Number.NaN, | ||
limit: Number.NaN, | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,205 @@ | ||||||||||
import type { Interpolation, Theme } from "@emotion/react"; | ||||||||||
import MuiLink from "@mui/material/Link"; | ||||||||||
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"; | ||||||||||
|
||||||||||
interface ManagedAgentsConsumptionProps { | ||||||||||
usage: number; | ||||||||||
included: number; | ||||||||||
limit: number; | ||||||||||
startDate: string; | ||||||||||
endDate: string; | ||||||||||
enabled?: boolean; | ||||||||||
} | ||||||||||
|
||||||||||
export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({ | ||||||||||
usage, | ||||||||||
included, | ||||||||||
limit, | ||||||||||
startDate, | ||||||||||
endDate, | ||||||||||
enabled = true, | ||||||||||
}) => { | ||||||||||
if (!enabled) { | ||||||||||
return ( | ||||||||||
<div css={styles.disabledRoot}> | ||||||||||
<Stack alignItems="center" spacing={1}> | ||||||||||
<Stack alignItems="center" spacing={0.5}> | ||||||||||
<span css={styles.disabledTitle}> | ||||||||||
Managed AI Agents Disabled | ||||||||||
</span> | ||||||||||
<span css={styles.disabledDescription}> | ||||||||||
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 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 agents consumption | ||||||||||
ibetitsmike marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
</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 /> | ||||||||||
How we calculate managed agents consumption | ||||||||||
</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> | ||||||||||
Managed agents are counted based on the amount of successfully | ||||||||||
started workspaces with an AI agent. | ||||||||||
Comment on lines
+86
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested 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. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I feel like legend items don't usually have punctuation at the end |
||||||||||
</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. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
</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> | ||||||||||
); | ||||||||||
}; | ||||||||||
|
||||||||||
const styles = { | ||||||||||
disabledTitle: { | ||||||||||
fontSize: 16, | ||||||||||
}, | ||||||||||
|
||||||||||
disabledRoot: (theme) => ({ | ||||||||||
minHeight: 240, | ||||||||||
display: "flex", | ||||||||||
alignItems: "center", | ||||||||||
justifyContent: "center", | ||||||||||
borderRadius: 8, | ||||||||||
border: `1px solid ${theme.palette.divider}`, | ||||||||||
padding: 48, | ||||||||||
}), | ||||||||||
|
||||||||||
disabledDescription: (theme) => ({ | ||||||||||
color: theme.palette.text.secondary, | ||||||||||
textAlign: "center", | ||||||||||
maxWidth: 464, | ||||||||||
marginTop: 8, | ||||||||||
}), | ||||||||||
} satisfies Record<string, Interpolation<Theme>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only place these variables are used is to be passed in to
ManagedAgentsConsumption
. Why does that component take all of the values individually rather than just taking aFeature
?