From f6d3627abc3fec1750988b3d9767ce26e8e62d67 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:04:55 +0200 Subject: [PATCH] feat: add license page with tier features and limits Co-Authored-By: Claude Sonnet 4.6 --- ui/src/pages/LicensePage.tsx | 185 ++++++++++++++++++++++++++++++++++- 1 file changed, 183 insertions(+), 2 deletions(-) diff --git a/ui/src/pages/LicensePage.tsx b/ui/src/pages/LicensePage.tsx index 47f25bd..8f1cbd2 100644 --- a/ui/src/pages/LicensePage.tsx +++ b/ui/src/pages/LicensePage.tsx @@ -1,3 +1,184 @@ -export function LicensePage() { - return
License
; +import React, { useState } from 'react'; +import { + Badge, + Card, + EmptyState, + Spinner, +} from '@cameleer/design-system'; +import { useAuthStore } from '../auth/auth-store'; +import { useLicense } from '../api/hooks'; + +const FEATURE_LABELS: Record = { + topology: 'Topology', + lineage: 'Lineage', + correlation: 'Correlation', + debugger: 'Debugger', + replay: 'Replay', +}; + +const LIMIT_LABELS: Record = { + maxAgents: 'Max Agents', + retentionDays: 'Retention Days', + maxEnvironments: 'Max Environments', +}; + +function tierColor(tier: string): 'primary' | 'success' | 'warning' | 'error' { + switch (tier?.toUpperCase()) { + case 'BUSINESS': return 'success'; + case 'HIGH': return 'primary'; + case 'MID': return 'warning'; + case 'LOW': return 'error'; + default: return 'primary'; + } +} + +function daysRemaining(expiresAt: string): number { + const now = Date.now(); + const exp = new Date(expiresAt).getTime(); + return Math.max(0, Math.ceil((exp - now) / (1000 * 60 * 60 * 24))); +} + +export function LicensePage() { + const tenantId = useAuthStore((s) => s.tenantId); + const { data: license, isLoading, isError } = useLicense(tenantId ?? ''); + const [tokenExpanded, setTokenExpanded] = useState(false); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!tenantId) { + return ( + + ); + } + + if (isError || !license) { + return ( + + ); + } + + const expDate = new Date(license.expiresAt).toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + const days = daysRemaining(license.expiresAt); + const isExpiringSoon = days <= 30; + const isExpired = days === 0; + + return ( +
+ {/* Header */} +
+

License

+ +
+ + {/* Expiry info */} + +
+
+ Issued + + {new Date(license.issuedAt).toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + })} + +
+
+ Expires + {expDate} +
+
+ Days remaining + +
+
+
+ + {/* Feature matrix */} + +
+ {Object.entries(FEATURE_LABELS).map(([key, label]) => { + const enabled = license.features[key] ?? false; + return ( +
+ {label} + +
+ ); + })} +
+
+ + {/* Limits */} + +
+ {Object.entries(LIMIT_LABELS).map(([key, label]) => { + const value = license.limits[key]; + return ( +
+ {label} + + {value !== undefined ? value : '—'} + +
+ ); + })} +
+
+ + {/* License token */} + +
+

+ Use this token when registering Cameleer agents with your tenant. +

+ + {tokenExpanded && ( +
+ + {license.token} + +
+ )} +
+
+
+ ); }