diff --git a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx index 5f617412a0c04..4cc6f52523edf 100644 --- a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx +++ b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPage.tsx @@ -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(); diff --git a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx index eb60361883b72..c631ed178b9a3 100644 --- a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx @@ -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 = ({ @@ -45,6 +47,7 @@ const LicensesSettingsPageView: FC = ({ removeLicense, refreshEntitlements, activeUsers, + managedAgentFeature, }) => { const theme = useTheme(); const { width, height } = useWindowSize(); @@ -151,6 +154,10 @@ const LicensesSettingsPageView: FC = ({ }))} /> )} + + {licenses && licenses.length > 0 && ( + + )} ); diff --git a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/ManagedAgentsConsumption.stories.tsx b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/ManagedAgentsConsumption.stories.tsx new file mode 100644 index 0000000000000..8b526914edd50 --- /dev/null +++ b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/ManagedAgentsConsumption.stories.tsx @@ -0,0 +1,196 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { ManagedAgentsConsumption } from "./ManagedAgentsConsumption"; + +const meta: Meta = { + 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; + +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", + }, + }, +}; diff --git a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/ManagedAgentsConsumption.tsx b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/ManagedAgentsConsumption.tsx new file mode 100644 index 0000000000000..e96d86b5a4c92 --- /dev/null +++ b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/ManagedAgentsConsumption.tsx @@ -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 = ({ + managedAgentFeature, +}) => { + // If no feature is provided or it's disabled, show disabled state + if (!managedAgentFeature?.enabled) { + return ( +
+ + + Managed AI Agents Disabled + + Managed AI agents are not included in your current license. + Contact sales to + upgrade your license and unlock this feature. + + + +
+ ); + } + + 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 ; + } + + if (!included || included < 0 || !limit || limit < 0) { + return ; + } + + if (!startDate || !endDate) { + return ; + } + + const start = dayjs(startDate); + const end = dayjs(endDate); + if (!start.isValid() || !end.isValid() || !start.isBefore(end)) { + return ; + } + + const usagePercentage = Math.min((usage / limit) * 100, 100); + const includedPercentage = Math.min((included / limit) * 100, 100); + const remainingPercentage = Math.max(100 - includedPercentage, 0); + + return ( +
+
+ +
+

Managed AI Agents Usage

+ + + + +
+ + +

+ + Coder Tasks + {" "} + and upcoming managed AI features are included in Coder Premium + licenses during beta. Usage limits and pricing subject to change. +

+
    +
  • +
    + Amount of started workspaces with an AI agent. +
  • +
  • +
    + Included allowance from your current license plan. +
  • +
  • +
    +
    +
    + Total limit after which further AI workspace builds will be + blocked. +
  • +
+
+
+
+ +
+
+ + {startDate ? dayjs(startDate).format("MMMM D, YYYY") : ""} + + {endDate ? dayjs(endDate).format("MMMM D, YYYY") : ""} +
+ +
+
+ +
+
+ +
+
+ Actual: + {usage.toLocaleString()} +
+ +
+ Included: + {included.toLocaleString()} +
+ +
+ Limit: + {limit.toLocaleString()} +
+
+ +
+
+
+ Actual: + {usage.toLocaleString()} +
+
+ Included: + {included.toLocaleString()} +
+
+ Limit: + {limit.toLocaleString()} +
+
+
+
+
+ ); +};