feat(ui): add license usage visualization with progress bars
Split license limits into metered "Resource Usage" (with color-coded progress bars) and static "Plan Limits" cards. Updated UsageIndicator with 8px bars, green/amber/red thresholds, and tabular-nums formatting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Spinner,
|
||||
Alert,
|
||||
useToast,
|
||||
} from '@cameleer/design-system';
|
||||
import { Copy, Eye, EyeOff } from 'lucide-react';
|
||||
import { useTenantLicense } from '../../api/tenant-hooks';
|
||||
import { UsageIndicator } from '../../components/UsageIndicator';
|
||||
import { tierColor } from '../../utils/tier';
|
||||
import styles from '../../styles/platform.module.css';
|
||||
|
||||
@@ -28,6 +29,13 @@ const LIMIT_LABELS: Record<string, string> = {
|
||||
max_jar_retention_count: 'JAR Retention Count',
|
||||
};
|
||||
|
||||
const METERED_LIMITS: Record<string, string> = {
|
||||
max_agents: 'agents',
|
||||
max_environments: 'environments',
|
||||
max_apps: 'apps',
|
||||
max_users: 'users',
|
||||
};
|
||||
|
||||
function daysColor(days: number): 'success' | 'warning' | 'error' | 'auto' {
|
||||
if (days <= 0) return 'error';
|
||||
if (days <= 30) return 'warning';
|
||||
@@ -57,6 +65,22 @@ export function TenantLicensePage() {
|
||||
);
|
||||
}
|
||||
|
||||
const usage = data.usage ?? {};
|
||||
const meteredEntries = Object.entries(METERED_LIMITS)
|
||||
.filter(([limitKey]) => data.limits[limitKey] != null)
|
||||
.map(([limitKey, usageKey]) => ({
|
||||
label: LIMIT_LABELS[limitKey] ?? limitKey,
|
||||
used: usage[usageKey] ?? 0,
|
||||
limit: data.limits[limitKey],
|
||||
}));
|
||||
|
||||
const configEntries = Object.entries(data.limits)
|
||||
.filter(([key]) => !(key in METERED_LIMITS))
|
||||
.map(([key, value]) => ({
|
||||
label: LIMIT_LABELS[key] ?? key,
|
||||
value,
|
||||
}));
|
||||
|
||||
async function handleCopy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(data!.token);
|
||||
@@ -68,14 +92,12 @@ export function TenantLicensePage() {
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<h1 style={{ margin: 0, fontSize: '1.25rem', fontWeight: 600 }}>License</h1>
|
||||
<Badge label={data.tier} color={tierColor(data.tier)} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 16 }}>
|
||||
{/* Validity card */}
|
||||
<Card title="Validity">
|
||||
<div className={styles.dividerList}>
|
||||
<div className={styles.kvRow}>
|
||||
@@ -108,25 +130,37 @@ export function TenantLicensePage() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Limits card */}
|
||||
<Card title="Limits">
|
||||
<div className={styles.dividerList}>
|
||||
{Object.entries(data.limits).map(([key, value]) => (
|
||||
<div key={key} className={styles.kvRow}>
|
||||
<span className={styles.kvLabel}>{LIMIT_LABELS[key] ?? key}</span>
|
||||
<span className={styles.kvValue} style={{ fontVariantNumeric: 'tabular-nums' }}>
|
||||
{value.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{Object.keys(data.limits).length === 0 && (
|
||||
<p className={styles.description}>No limits configured.</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
{meteredEntries.length > 0 && (
|
||||
<Card title="Resource Usage">
|
||||
<div className={styles.dividerList}>
|
||||
{meteredEntries.map((entry) => (
|
||||
<UsageIndicator
|
||||
key={entry.label}
|
||||
label={entry.label}
|
||||
used={entry.used}
|
||||
limit={entry.limit}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{configEntries.length > 0 && (
|
||||
<Card title="Plan Limits">
|
||||
<div className={styles.dividerList}>
|
||||
{configEntries.map((entry) => (
|
||||
<div key={entry.label} className={styles.kvRow}>
|
||||
<span className={styles.kvLabel}>{entry.label}</span>
|
||||
<span className={styles.kvValue} style={{ fontVariantNumeric: 'tabular-nums' }}>
|
||||
{entry.value.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* License token card — full width */}
|
||||
<Card title="License Token">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<p className={styles.description}>
|
||||
@@ -134,7 +168,7 @@ export function TenantLicensePage() {
|
||||
</p>
|
||||
<div className={styles.tokenBlock}>
|
||||
<code className={styles.tokenCode}>
|
||||
{showToken ? data.token : '••••••••••••••••••••••••••••••••'}
|
||||
{showToken ? data.token : '\u2022'.repeat(32)}
|
||||
</code>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
|
||||
Reference in New Issue
Block a user