mirror of
https://github.com/coder/coder.git
synced 2026-09-21 12:44:32 +08:00
feat: remove agent workspaces limit (#21998)
In relation to [`internal#1281`](https://github.com/coder/internal/issues/1281) Managed agent workspace build limits are now advisory only. Breaching the limit no longer blocks workspace creation — it only surfaces a warning. - Removed hard-limit enforcement in `checkAIBuildUsage` so AI task builds are always permitted regardless of managed agent count. - Updated the license warning to remove "Further managed agent builds will be blocked." verbiage. - Updated tests to assert builds succeed beyond the limit instead of failing. - Removed the "Limit" display from the `ManagedAgentsConsumption` progress bar — the bar is now relative to the included allowance (soft limit) only, and turns orange when usage exceeds it. Bonus: - De-MUI'd `LicenseBannerView` — replaced Emotion CSS and MUI `Link` with Tailwind classes. - Added `highlight-orange` color token to the Tailwind theme.
This commit is contained in:
@@ -12,8 +12,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
LicenseExpiryClaim = "license_expires"
|
||||
LicenseTelemetryRequiredErrorText = "License requires telemetry but telemetry is disabled"
|
||||
LicenseExpiryClaim = "license_expires"
|
||||
LicenseTelemetryRequiredErrorText = "License requires telemetry but telemetry is disabled"
|
||||
LicenseManagedAgentLimitExceededWarningText = "You have built more workspaces with managed agents than your license allows."
|
||||
)
|
||||
|
||||
type AddLicenseRequest struct {
|
||||
|
||||
@@ -983,7 +983,13 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
||||
|
||||
var _ wsbuilder.UsageChecker = &API{}
|
||||
|
||||
func (api *API) CheckBuildUsage(ctx context.Context, store database.Store, templateVersion *database.TemplateVersion, task *database.Task, transition database.WorkspaceTransition) (wsbuilder.UsageCheckResponse, error) {
|
||||
func (api *API) CheckBuildUsage(
|
||||
_ context.Context,
|
||||
_ database.Store,
|
||||
templateVersion *database.TemplateVersion,
|
||||
_ *database.Task,
|
||||
_ database.WorkspaceTransition,
|
||||
) (wsbuilder.UsageCheckResponse, error) {
|
||||
// If the template version has an external agent, we need to check that the
|
||||
// license is entitled to this feature.
|
||||
if templateVersion.HasExternalAgent.Valid && templateVersion.HasExternalAgent.Bool {
|
||||
@@ -996,61 +1002,6 @@ func (api *API) CheckBuildUsage(ctx context.Context, store database.Store, templ
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := api.checkAIBuildUsage(ctx, store, task, transition)
|
||||
if err != nil {
|
||||
return wsbuilder.UsageCheckResponse{}, err
|
||||
}
|
||||
if !resp.Permitted {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
return wsbuilder.UsageCheckResponse{Permitted: true}, nil
|
||||
}
|
||||
|
||||
// checkAIBuildUsage validates AI-related usage constraints. It is a no-op
|
||||
// unless the transition is "start" and the template version has an AI task.
|
||||
func (api *API) checkAIBuildUsage(ctx context.Context, store database.Store, task *database.Task, transition database.WorkspaceTransition) (wsbuilder.UsageCheckResponse, error) {
|
||||
// Only check AI usage rules for start transitions.
|
||||
if transition != database.WorkspaceTransitionStart {
|
||||
return wsbuilder.UsageCheckResponse{Permitted: true}, nil
|
||||
}
|
||||
|
||||
// If the template version doesn't have an AI task, we don't need to check usage.
|
||||
if task == nil {
|
||||
return wsbuilder.UsageCheckResponse{Permitted: true}, nil
|
||||
}
|
||||
|
||||
// When licensed, ensure we haven't breached the managed agent limit.
|
||||
// Unlicensed deployments are allowed to use unlimited managed agents.
|
||||
if api.Entitlements.HasLicense() {
|
||||
managedAgentLimit, ok := api.Entitlements.Feature(codersdk.FeatureManagedAgentLimit)
|
||||
if !ok || !managedAgentLimit.Enabled || managedAgentLimit.Limit == nil || managedAgentLimit.UsagePeriod == nil {
|
||||
return wsbuilder.UsageCheckResponse{
|
||||
Permitted: false,
|
||||
Message: "Your license is not entitled to managed agents. Please contact sales to continue using managed agents.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// This check is intentionally not committed to the database. It's fine
|
||||
// if it's not 100% accurate or allows for minor breaches due to build
|
||||
// races.
|
||||
// nolint:gocritic // Requires permission to read all usage events.
|
||||
managedAgentCount, err := store.GetTotalUsageDCManagedAgentsV1(agpldbauthz.AsSystemRestricted(ctx), database.GetTotalUsageDCManagedAgentsV1Params{
|
||||
StartDate: managedAgentLimit.UsagePeriod.Start,
|
||||
EndDate: managedAgentLimit.UsagePeriod.End,
|
||||
})
|
||||
if err != nil {
|
||||
return wsbuilder.UsageCheckResponse{}, xerrors.Errorf("get managed agent count: %w", err)
|
||||
}
|
||||
|
||||
if managedAgentCount >= *managedAgentLimit.Limit {
|
||||
return wsbuilder.UsageCheckResponse{
|
||||
Permitted: false,
|
||||
Message: "You have breached the managed agent limit in your license. Please contact sales to continue using managed agents.",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return wsbuilder.UsageCheckResponse{Permitted: true}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -765,15 +765,20 @@ func TestManagedAgentLimit(t *testing.T) {
|
||||
require.NoError(t, err, "fetching AI workspace must succeed")
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, cli, workspace.LatestBuild.ID)
|
||||
|
||||
// Create a second AI task, which should fail due to breaching the limit.
|
||||
_, err = cli.CreateTask(ctx, owner.UserID.String(), codersdk.CreateTaskRequest{
|
||||
// Create a second AI task, which should succeed even though the limit is
|
||||
// breached. Managed agent limits are advisory only and should never block
|
||||
// workspace creation.
|
||||
task2, err := cli.CreateTask(ctx, owner.UserID.String(), codersdk.CreateTaskRequest{
|
||||
Name: namesgenerator.UniqueNameWith("-"),
|
||||
TemplateVersionID: aiTemplate.ActiveVersionID,
|
||||
TemplateVersionPresetID: uuid.Nil,
|
||||
Input: "hi",
|
||||
DisplayName: namesgenerator.UniqueName(),
|
||||
})
|
||||
require.ErrorContains(t, err, "You have breached the managed agent limit in your license")
|
||||
require.NoError(t, err, "creating task beyond managed agent limit must succeed")
|
||||
workspace2, err := cli.Workspace(ctx, task2.WorkspaceID.UUID)
|
||||
require.NoError(t, err, "fetching AI workspace must succeed")
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, cli, workspace2.LatestBuild.ID)
|
||||
|
||||
// Create a third workspace using the same template, which should succeed.
|
||||
workspace = coderdtest.CreateWorkspace(t, cli, aiTemplate.ID)
|
||||
@@ -784,12 +789,12 @@ func TestManagedAgentLimit(t *testing.T) {
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, cli, workspace.LatestBuild.ID)
|
||||
}
|
||||
|
||||
func TestCheckBuildUsage_SkipsAIForNonStartTransitions(t *testing.T) {
|
||||
func TestCheckBuildUsage_NeverBlocksOnManagedAgentLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
// Prepare entitlements with a managed agent limit to enforce.
|
||||
// Prepare entitlements with a managed agent limit.
|
||||
entSet := entitlements.New()
|
||||
entSet.Modify(func(e *codersdk.Entitlements) {
|
||||
e.HasLicense = true
|
||||
@@ -825,27 +830,24 @@ func TestCheckBuildUsage_SkipsAIForNonStartTransitions(t *testing.T) {
|
||||
TemplateVersionID: tv.ID,
|
||||
}
|
||||
|
||||
// Mock DB: expect exactly one count call for the "start" transition.
|
||||
// Mock DB: no calls expected since managed agent limits are
|
||||
// advisory only and no longer query the database at build time.
|
||||
mDB := dbmock.NewMockStore(ctrl)
|
||||
mDB.EXPECT().
|
||||
GetTotalUsageDCManagedAgentsV1(gomock.Any(), gomock.Any()).
|
||||
Times(1).
|
||||
Return(int64(1), nil) // equal to limit -> should breach
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Start transition: should be not permitted due to limit breach.
|
||||
// Start transition: should be permitted even though the limit is
|
||||
// breached. Managed agent limits are advisory only.
|
||||
startResp, err := eapi.CheckBuildUsage(ctx, mDB, tv, task, database.WorkspaceTransitionStart)
|
||||
require.NoError(t, err)
|
||||
require.False(t, startResp.Permitted)
|
||||
require.Contains(t, startResp.Message, "breached the managed agent limit")
|
||||
require.True(t, startResp.Permitted)
|
||||
|
||||
// Stop transition: should be permitted and must not trigger additional DB calls.
|
||||
// Stop transition: should also be permitted.
|
||||
stopResp, err := eapi.CheckBuildUsage(ctx, mDB, tv, task, database.WorkspaceTransitionStop)
|
||||
require.NoError(t, err)
|
||||
require.True(t, stopResp.Permitted)
|
||||
|
||||
// Delete transition: should be permitted and must not trigger additional DB calls.
|
||||
// Delete transition: should also be permitted.
|
||||
deleteResp, err := eapi.CheckBuildUsage(ctx, mDB, tv, task, database.WorkspaceTransitionDelete)
|
||||
require.NoError(t, err)
|
||||
require.True(t, deleteResp.Permitted)
|
||||
|
||||
@@ -578,7 +578,7 @@ func LicensesEntitlements(
|
||||
}
|
||||
if managedAgentCount >= *agentLimit.Limit {
|
||||
entitlements.Warnings = append(entitlements.Warnings,
|
||||
"You have built more workspaces with managed agents than your license allows. Further managed agent builds will be blocked.")
|
||||
codersdk.LicenseManagedAgentLimitExceededWarningText)
|
||||
} else if managedAgentCount >= softWarningThreshold {
|
||||
entitlements.Warnings = append(entitlements.Warnings,
|
||||
"You are approaching the managed agent limit in your license. Please refer to the Deployment Licenses page for more information.")
|
||||
|
||||
@@ -1265,7 +1265,7 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
},
|
||||
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
|
||||
assert.Len(t, entitlements.Warnings, 1)
|
||||
assert.Equal(t, "You have built more workspaces with managed agents than your license allows. Further managed agent builds will be blocked.", entitlements.Warnings[0])
|
||||
assert.Equal(t, "You have built more workspaces with managed agents than your license allows.", entitlements.Warnings[0])
|
||||
assertNoErrors(t, entitlements)
|
||||
|
||||
feature := entitlements.Features[codersdk.FeatureManagedAgentLimit]
|
||||
|
||||
Generated
+4
@@ -2587,6 +2587,10 @@ export interface License {
|
||||
// From codersdk/licenses.go
|
||||
export const LicenseExpiryClaim = "license_expires";
|
||||
|
||||
// From codersdk/licenses.go
|
||||
export const LicenseManagedAgentLimitExceededWarningText =
|
||||
"You have built more workspaces with managed agents than your license allows.";
|
||||
|
||||
// From codersdk/licenses.go
|
||||
export const LicenseTelemetryRequiredErrorText =
|
||||
"License requires telemetry but telemetry is disabled";
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
--radius: 0.5rem;
|
||||
--highlight-purple: 271, 61%, 35%;
|
||||
--highlight-green: 143 64% 24%;
|
||||
--highlight-orange: 30 100% 54%;
|
||||
--highlight-grey: 240 5% 65%;
|
||||
--highlight-sky: 195, 61%, 22%;
|
||||
--highlight-red: 0 74% 42%;
|
||||
@@ -91,6 +92,7 @@
|
||||
--overlay-default: 240 10% 4% / 80%;
|
||||
--highlight-purple: 269, 100%, 74%;
|
||||
--highlight-green: 141 79% 85%;
|
||||
--highlight-orange: 31 100% 70%;
|
||||
--highlight-grey: 240 4% 46%;
|
||||
--highlight-sky: 188, 75%, 80%;
|
||||
--highlight-red: 0 91% 71%;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { chromatic } from "testHelpers/chromatic";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { LicenseManagedAgentLimitExceededWarningText } from "api/typesGenerated";
|
||||
import { LicenseBannerView } from "./LicenseBannerView";
|
||||
|
||||
const meta: Meta<typeof LicenseBannerView> = {
|
||||
@@ -36,3 +37,20 @@ export const OneError: Story = {
|
||||
warnings: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const ManagedAgentLimitExceeded: Story = {
|
||||
args: {
|
||||
errors: [],
|
||||
warnings: [LicenseManagedAgentLimitExceededWarningText],
|
||||
},
|
||||
};
|
||||
|
||||
export const ManagedAgentLimitExceededWithOtherWarnings: Story = {
|
||||
args: {
|
||||
errors: [],
|
||||
warnings: [
|
||||
LicenseManagedAgentLimitExceededWarningText,
|
||||
"You have exceeded the number of seats in your license.",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,32 +1,13 @@
|
||||
import {
|
||||
type CSSObject,
|
||||
css,
|
||||
type Interpolation,
|
||||
type Theme,
|
||||
useTheme,
|
||||
} from "@emotion/react";
|
||||
import Link from "@mui/material/Link";
|
||||
import { LicenseTelemetryRequiredErrorText } from "api/typesGenerated";
|
||||
LicenseManagedAgentLimitExceededWarningText,
|
||||
LicenseTelemetryRequiredErrorText,
|
||||
} from "api/typesGenerated";
|
||||
import { Expander } from "components/Expander/Expander";
|
||||
import { Link } from "components/Link/Link";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import { type FC, useState } from "react";
|
||||
|
||||
const Language = {
|
||||
licenseIssue: "License Issue",
|
||||
licenseIssues: (num: number): string => `${num} License Issues`,
|
||||
upgrade: "Contact sales@coder.com.",
|
||||
exception: "Contact sales@coder.com if you need an exception.",
|
||||
exceeded: "It looks like you've exceeded some limits of your license.",
|
||||
lessDetails: "Less",
|
||||
moreDetails: "More",
|
||||
};
|
||||
|
||||
const styles = {
|
||||
leftContent: {
|
||||
marginRight: 8,
|
||||
marginLeft: 8,
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
import { useState } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
import { docs } from "utils/docs";
|
||||
|
||||
const formatMessage = (message: string) => {
|
||||
// If the message ends with an alphanumeric character, add a period.
|
||||
@@ -36,73 +17,92 @@ const formatMessage = (message: string) => {
|
||||
return message;
|
||||
};
|
||||
|
||||
const messageLinkProps = (
|
||||
message: string,
|
||||
): Pick<React.ComponentProps<typeof Link>, "href" | "children" | "target"> => {
|
||||
if (message === LicenseManagedAgentLimitExceededWarningText) {
|
||||
return {
|
||||
href: docs("/ai-coder/ai-governance"),
|
||||
children: "View AI Governance",
|
||||
target: "_blank",
|
||||
};
|
||||
}
|
||||
if (message === LicenseTelemetryRequiredErrorText) {
|
||||
return {
|
||||
href: "mailto:sales@coder.com",
|
||||
children: "Contact sales@coder.com if you need an exception.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
href: "mailto:sales@coder.com",
|
||||
children: "Contact sales@coder.com.",
|
||||
};
|
||||
};
|
||||
|
||||
interface LicenseBannerViewProps {
|
||||
errors: readonly string[];
|
||||
warnings: readonly string[];
|
||||
}
|
||||
|
||||
export const LicenseBannerView: FC<LicenseBannerViewProps> = ({
|
||||
export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({
|
||||
errors,
|
||||
warnings,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const isError = errors.length > 0;
|
||||
const messages = [...errors, ...warnings];
|
||||
const type = isError ? "error" : "warning";
|
||||
|
||||
const containerStyles = css`
|
||||
${theme.typography.body2 as CSSObject}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background-color: ${theme.roles[type].background};
|
||||
`;
|
||||
|
||||
const textColor = theme.roles[type].text;
|
||||
|
||||
if (messages.length === 1) {
|
||||
const [message] = messages;
|
||||
|
||||
return (
|
||||
<div css={containerStyles}>
|
||||
<Pill type={type}>{Language.licenseIssue}</Pill>
|
||||
<div css={styles.leftContent}>
|
||||
<span>{formatMessage(messages[0])}</span>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center p-3 text-sm",
|
||||
isError ? "bg-surface-red" : "bg-surface-orange",
|
||||
)}
|
||||
>
|
||||
<Pill type={type}>License Issue</Pill>
|
||||
<div className="mx-2">
|
||||
<span>{formatMessage(message)}</span>
|
||||
|
||||
<Link
|
||||
color={textColor}
|
||||
fontWeight="medium"
|
||||
href="mailto:sales@coder.com"
|
||||
>
|
||||
{messages[0] === LicenseTelemetryRequiredErrorText
|
||||
? Language.exception
|
||||
: Language.upgrade}
|
||||
</Link>
|
||||
className={cn(
|
||||
"font-medium",
|
||||
isError ? "!text-content-destructive" : "!text-content-warning",
|
||||
)}
|
||||
{...messageLinkProps(message)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div css={containerStyles}>
|
||||
<Pill type={type}>{Language.licenseIssues(messages.length)}</Pill>
|
||||
<div css={styles.leftContent}>
|
||||
<div>
|
||||
{Language.exceeded}
|
||||
|
||||
<Link
|
||||
color={textColor}
|
||||
fontWeight="medium"
|
||||
href="mailto:sales@coder.com"
|
||||
>
|
||||
{Language.upgrade}
|
||||
</Link>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center p-3 text-sm",
|
||||
isError ? "bg-surface-red" : "bg-surface-orange",
|
||||
)}
|
||||
>
|
||||
<Pill type={type}>{`${messages.length} License Issues`}</Pill>
|
||||
<div className="mx-2">
|
||||
<div>It looks like you've exceeded some limits of your license.</div>
|
||||
<Expander expanded={showDetails} setExpanded={setShowDetails}>
|
||||
<ul css={{ padding: 8, margin: 0 }}>
|
||||
<ul className="p-2 m-0">
|
||||
{messages.map((message) => (
|
||||
<li css={{ margin: 4 }} key={message}>
|
||||
{formatMessage(message)}
|
||||
<li className="m-1" key={message}>
|
||||
{formatMessage(message)}
|
||||
<Link
|
||||
className={cn(
|
||||
"font-medium text-xs px-0",
|
||||
isError
|
||||
? "!text-content-destructive"
|
||||
: "!text-content-warning",
|
||||
)}
|
||||
{...messageLinkProps(message)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
+15
-46
@@ -10,6 +10,7 @@ import { Link } from "components/Link/Link";
|
||||
import dayjs from "dayjs";
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
import { docs } from "utils/docs";
|
||||
|
||||
interface ManagedAgentsConsumptionProps {
|
||||
@@ -39,7 +40,6 @@ export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
|
||||
|
||||
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;
|
||||
|
||||
@@ -47,12 +47,7 @@ export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
|
||||
return <ErrorAlert error="Invalid usage data" />;
|
||||
}
|
||||
|
||||
if (
|
||||
included === undefined ||
|
||||
included < 0 ||
|
||||
limit === undefined ||
|
||||
limit < 0
|
||||
) {
|
||||
if (included === undefined || included < 0) {
|
||||
return <ErrorAlert error="Invalid license usage limits" />;
|
||||
}
|
||||
|
||||
@@ -66,9 +61,7 @@ export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
|
||||
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);
|
||||
const usagePercentage = Math.min((usage / included) * 100, 100);
|
||||
|
||||
return (
|
||||
<section className="border border-solid rounded">
|
||||
@@ -132,20 +125,13 @@ export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
|
||||
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">
|
||||
<span className="sr-only">Legend for included allowance</span>
|
||||
</div>
|
||||
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">
|
||||
<div className="rounded-[2px] bg-highlight-orange size-3 inline-block">
|
||||
<span className="sr-only">
|
||||
Legend for total limit in the chart
|
||||
Legend for usage exceeding included allowance
|
||||
</span>
|
||||
<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.
|
||||
Usage has exceeded included allowance from your current license
|
||||
plan.
|
||||
</li>
|
||||
</ul>
|
||||
</CollapsibleContent>
|
||||
@@ -162,17 +148,14 @@ export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
|
||||
|
||||
<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"
|
||||
className={cn(
|
||||
"absolute top-0 left-0 h-full transition-all duration-300",
|
||||
usagePercentage < 100
|
||||
? "bg-highlight-green"
|
||||
: "bg-highlight-orange",
|
||||
)}
|
||||
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">
|
||||
@@ -181,20 +164,10 @@ export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
|
||||
<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)}%`,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-end">
|
||||
<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">
|
||||
@@ -203,14 +176,10 @@ export const ManagedAgentsConsumption: FC<ManagedAgentsConsumptionProps> = ({
|
||||
<span className="text-content-secondary">Actual:</span>
|
||||
<span className="font-medium">{usage.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex flex-col items-end">
|
||||
<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>
|
||||
|
||||
@@ -74,6 +74,7 @@ module.exports = {
|
||||
highlight: {
|
||||
purple: "hsl(var(--highlight-purple))",
|
||||
green: "hsl(var(--highlight-green))",
|
||||
orange: "hsl(var(--highlight-orange))",
|
||||
grey: "hsl(var(--highlight-grey))",
|
||||
sky: "hsl(var(--highlight-sky))",
|
||||
red: "hsl(var(--highlight-red))",
|
||||
|
||||
Reference in New Issue
Block a user