feat(ui): add license minting form, verify tool, and update all pages
Vendor UI: - TenantDetailPage: full minting form with tier presets, 13 configurable limits, expiry/grace period, label. Mint & Push or Mint & Copy actions. License bundle display with all three env vars for standalone deployment. - LicenseVerifyPage: paste token to decode + validate signature, shows envelope details and state badge. Public key viewer with copy button. - Layout: added "License Tools" nav item under Vendor section. - vendor-hooks: useMintLicense, useLicensePresets, useVerifyLicense, usePublicKey Tenant UI: - TenantLicensePage: replaced features card with full 13-key limits display, added grace period and label fields - TenantDashboardPage: fixed limit keys (agents→max_agents, environments→max_environments) Common: - Updated types (dropped features, added label/gracePeriodDays/bundle types) - Updated tier colors for STARTER/TEAM/BUSINESS/ENTERPRISE - Updated CreateTenantPage tier dropdown Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -53,8 +53,8 @@ export function TenantDashboardPage() {
|
||||
const serverDown = !data.serverHealthy;
|
||||
const daysRemaining = data.licenseDaysRemaining ?? 0;
|
||||
|
||||
const agentLimit = data.limits?.['agents'] ?? -1;
|
||||
const envLimit = data.limits?.['environments'] ?? -1;
|
||||
const agentLimit = data.limits?.['max_agents'] ?? -1;
|
||||
const envLimit = data.limits?.['max_environments'] ?? -1;
|
||||
const agentUsed = data.agentCount ?? 0;
|
||||
const envUsed = data.environmentCount ?? 0;
|
||||
|
||||
|
||||
@@ -9,10 +9,25 @@ import {
|
||||
} 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';
|
||||
|
||||
const LIMIT_LABELS: Record<string, string> = {
|
||||
max_environments: 'Environments',
|
||||
max_apps: 'Apps',
|
||||
max_agents: 'Agents',
|
||||
max_users: 'Users',
|
||||
max_outbound_connections: 'Outbound Connections',
|
||||
max_alert_rules: 'Alert Rules',
|
||||
max_total_cpu_millis: 'CPU (millicores)',
|
||||
max_total_memory_mb: 'Memory (MB)',
|
||||
max_total_replicas: 'Replicas',
|
||||
max_execution_retention_days: 'Execution Retention (days)',
|
||||
max_log_retention_days: 'Log Retention (days)',
|
||||
max_metric_retention_days: 'Metric Retention (days)',
|
||||
max_jar_retention_count: 'JAR Retention Count',
|
||||
};
|
||||
|
||||
function daysColor(days: number): 'success' | 'warning' | 'error' | 'auto' {
|
||||
if (days <= 0) return 'error';
|
||||
if (days <= 30) return 'warning';
|
||||
@@ -51,10 +66,6 @@ export function TenantLicensePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const agentLimit = data.limits?.['agents'] ?? -1;
|
||||
const envLimit = data.limits?.['environments'] ?? -1;
|
||||
const retentionDays = data.limits?.['retentionDays'] ?? -1;
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
{/* Header */}
|
||||
@@ -82,37 +93,34 @@ export function TenantLicensePage() {
|
||||
color={daysColor(data.daysRemaining)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Features card */}
|
||||
<Card title="Features">
|
||||
<div className={styles.dividerList}>
|
||||
{Object.entries(data.features).map(([key, enabled]) => (
|
||||
<div key={key} className={styles.kvRow}>
|
||||
<span className={styles.kvLabel}>{key}</span>
|
||||
<Badge
|
||||
label={enabled ? 'Enabled' : 'Disabled'}
|
||||
color={enabled ? 'success' : 'auto'}
|
||||
/>
|
||||
{data.gracePeriodDays > 0 && (
|
||||
<div className={styles.kvRow}>
|
||||
<span className={styles.kvLabel}>Grace period</span>
|
||||
<span className={styles.kvValue}>{data.gracePeriodDays}d</span>
|
||||
</div>
|
||||
)}
|
||||
{data.label && (
|
||||
<div className={styles.kvRow}>
|
||||
<span className={styles.kvLabel}>Label</span>
|
||||
<span className={styles.kvValue}>{data.label}</span>
|
||||
</div>
|
||||
))}
|
||||
{Object.keys(data.features).length === 0 && (
|
||||
<p className={styles.description}>No feature flags configured.</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Limits & Usage card */}
|
||||
<Card title="Limits & Usage">
|
||||
{/* Limits card */}
|
||||
<Card title="Limits">
|
||||
<div className={styles.dividerList}>
|
||||
<UsageIndicator used={0} limit={agentLimit} label="Agents" />
|
||||
<UsageIndicator used={0} limit={envLimit} label="Environments" />
|
||||
{retentionDays >= 0 && (
|
||||
<div className={styles.kvRow}>
|
||||
<span className={styles.kvLabel}>Data retention</span>
|
||||
<span className={styles.kvValue}>{retentionDays}d</span>
|
||||
{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>
|
||||
|
||||
Reference in New Issue
Block a user