feat: replace text labels with icons in runtime cards
Use Activity, Cpu, and HeartPulse icons instead of "tps", "cpu", and "ago" text in compact and expanded app cards. Bump design-system to v0.1.55 for sidebar footer alignment fix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
8
ui/package-lock.json
generated
8
ui/package-lock.json
generated
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cameleer/design-system": "^0.1.54",
|
"@cameleer/design-system": "^0.1.55",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.1",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
@@ -281,9 +281,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cameleer/design-system": {
|
"node_modules/@cameleer/design-system": {
|
||||||
"version": "0.1.54",
|
"version": "0.1.55",
|
||||||
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.54/design-system-0.1.54.tgz",
|
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.55/design-system-0.1.55.tgz",
|
||||||
"integrity": "sha512-IX05JmY/JcxTndfDWBHF7uizrRSqJgEM/J5uv5vQerM+Zq02yUzVNcV4QufVYBevGdnI4acUScnDlmSOOb85Qg==",
|
"integrity": "sha512-IdieQoRYnvxCWdj6PKJ6hnPmvTFlReWrimJcQ97URCv0bzkCaW9zJvXWISQ/r9b9kAKnz8xQ2HUNrJPBfYtw9g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"postinstall": "node -e \"const fs=require('fs');fs.mkdirSync('public',{recursive:true});fs.copyFileSync('node_modules/@cameleer/design-system/assets/cameleer-logo.svg','public/favicon.svg')\""
|
"postinstall": "node -e \"const fs=require('fs');fs.mkdirSync('public',{recursive:true});fs.copyFileSync('node_modules/@cameleer/design-system/assets/cameleer-logo.svg','public/favicon.svg')\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cameleer/design-system": "^0.1.54",
|
"@cameleer/design-system": "^0.1.55",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.1",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
|
|||||||
@@ -111,6 +111,12 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.groupMeta > span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.groupMeta strong {
|
.groupMeta strong {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
@@ -319,19 +325,31 @@
|
|||||||
.compactCardTps {
|
.compactCardTps {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compactCardCpu {
|
.compactCardCpu {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compactCardHeartbeat {
|
.compactCardHeartbeat {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compactCardHeartbeatWarn {
|
.compactCardHeartbeatWarn {
|
||||||
color: var(--card-accent);
|
color: var(--card-accent);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Wrapper for each compact grid cell — anchor for overlay */
|
/* Wrapper for each compact grid cell — anchor for overlay */
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { ExternalLink, RefreshCw, Pencil, LayoutGrid, List, ChevronRight, ChevronDown, ArrowUp, ArrowDown, Search } from 'lucide-react';
|
import { ExternalLink, RefreshCw, Pencil, LayoutGrid, List, ChevronRight, ChevronDown, ArrowUp, ArrowDown, Search, Cpu, HeartPulse, Activity } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
StatCard, StatusDot, Badge, MonoText,
|
StatCard, StatusDot, Badge, MonoText,
|
||||||
GroupCard, DataTable, EventFeed,
|
GroupCard, DataTable, EventFeed,
|
||||||
@@ -141,15 +141,15 @@ function CompactAppCard({ group, onExpand, onNavigate }: { group: AppGroup; onEx
|
|||||||
{group.liveCount}/{group.instances.length} live
|
{group.liveCount}/{group.instances.length} live
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.compactCardTps}>
|
<span className={styles.compactCardTps}>
|
||||||
{group.totalTps.toFixed(1)} tps
|
<Activity size={10} /> {group.totalTps.toFixed(1)}/s
|
||||||
</span>
|
</span>
|
||||||
{group.maxCpu >= 0 && (
|
{group.maxCpu >= 0 && (
|
||||||
<span className={styles.compactCardCpu}>
|
<span className={styles.compactCardCpu}>
|
||||||
{(group.maxCpu * 100).toFixed(0)}% cpu
|
<Cpu size={10} /> {(group.maxCpu * 100).toFixed(0)}%
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className={isHealthy ? styles.compactCardHeartbeat : styles.compactCardHeartbeatWarn}>
|
<span className={isHealthy ? styles.compactCardHeartbeat : styles.compactCardHeartbeatWarn}>
|
||||||
{heartbeat ? timeAgo(heartbeat) : '\u2014'}
|
<HeartPulse size={10} /> {heartbeat ? timeAgo(heartbeat, true) : '\u2014'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -763,20 +763,9 @@ export default function AgentHealth() {
|
|||||||
}
|
}
|
||||||
meta={
|
meta={
|
||||||
<div className={styles.groupMeta}>
|
<div className={styles.groupMeta}>
|
||||||
<span><strong>{group.totalTps.toFixed(1)}</strong> msg/s</span>
|
<span><Activity size={12} /> <strong>{group.totalTps.toFixed(1)}</strong>/s</span>
|
||||||
<span><strong>{group.totalActiveRoutes}</strong>/{group.totalRoutes} routes</span>
|
<span><strong>{group.totalActiveRoutes}</strong>/{group.totalRoutes} routes</span>
|
||||||
{group.maxCpu >= 0 && <span><strong>{(group.maxCpu * 100).toFixed(0)}%</strong> cpu</span>}
|
{group.maxCpu >= 0 && <span><Cpu size={12} /> <strong>{(group.maxCpu * 100).toFixed(0)}%</strong></span>}
|
||||||
<span>
|
|
||||||
<StatusDot
|
|
||||||
variant={
|
|
||||||
appHealth(group) === 'success'
|
|
||||||
? 'live'
|
|
||||||
: appHealth(group) === 'warning'
|
|
||||||
? 'stale'
|
|
||||||
: 'dead'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
@@ -860,20 +849,9 @@ export default function AgentHealth() {
|
|||||||
}
|
}
|
||||||
meta={
|
meta={
|
||||||
<div className={styles.groupMeta}>
|
<div className={styles.groupMeta}>
|
||||||
<span><strong>{group.totalTps.toFixed(1)}</strong> msg/s</span>
|
<span><Activity size={12} /> <strong>{group.totalTps.toFixed(1)}</strong>/s</span>
|
||||||
<span><strong>{group.totalActiveRoutes}</strong>/{group.totalRoutes} routes</span>
|
<span><strong>{group.totalActiveRoutes}</strong>/{group.totalRoutes} routes</span>
|
||||||
{group.maxCpu >= 0 && <span><strong>{(group.maxCpu * 100).toFixed(0)}%</strong> cpu</span>}
|
{group.maxCpu >= 0 && <span><Cpu size={12} /> <strong>{(group.maxCpu * 100).toFixed(0)}%</strong></span>}
|
||||||
<span>
|
|
||||||
<StatusDot
|
|
||||||
variant={
|
|
||||||
appHealth(group) === 'success'
|
|
||||||
? 'live'
|
|
||||||
: appHealth(group) === 'warning'
|
|
||||||
? 'stale'
|
|
||||||
: 'dead'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
|
|||||||
@@ -24,16 +24,17 @@ export function statusLabel(s: string): string {
|
|||||||
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
|
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function timeAgo(iso?: string): string {
|
export function timeAgo(iso?: string, short = false): string {
|
||||||
if (!iso) return '\u2014';
|
if (!iso) return '\u2014';
|
||||||
const diff = Date.now() - new Date(iso).getTime();
|
const diff = Date.now() - new Date(iso).getTime();
|
||||||
const secs = Math.floor(diff / 1000);
|
const secs = Math.floor(diff / 1000);
|
||||||
if (secs < 60) return `${secs}s ago`;
|
const suffix = short ? '' : ' ago';
|
||||||
|
if (secs < 60) return `${secs}s${suffix}`;
|
||||||
const mins = Math.floor(secs / 60);
|
const mins = Math.floor(secs / 60);
|
||||||
if (mins < 60) return `${mins}m ago`;
|
if (mins < 60) return `${mins}m${suffix}`;
|
||||||
const hours = Math.floor(mins / 60);
|
const hours = Math.floor(mins / 60);
|
||||||
if (hours < 24) return `${hours}h ago`;
|
if (hours < 24) return `${hours}h${suffix}`;
|
||||||
return `${Math.floor(hours / 24)}d ago`;
|
return `${Math.floor(hours / 24)}d${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMetric(value: number, unit: string, decimals = 1): string {
|
export function formatMetric(value: number, unit: string, decimals = 1): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user