diff --git a/ui/package-lock.json b/ui/package-lock.json index 5726bb4a..38a158fe 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "hasInstallScript": true, "dependencies": { - "@cameleer/design-system": "^0.1.50", + "@cameleer/design-system": "^0.1.52", "@tanstack/react-query": "^5.90.21", "js-yaml": "^4.1.1", "lucide-react": "^1.7.0", @@ -281,9 +281,9 @@ } }, "node_modules/@cameleer/design-system": { - "version": "0.1.51", - "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.51/design-system-0.1.51.tgz", - "integrity": "sha512-ppZSiR6ZzzrUbtHTtnwpU4Zr2LPbcbJfAn0Ayh/OzDf9k6kFjn5myJWFlg+VJAZkFQoJA5y76GcKBdJ8nty4Tw==", + "version": "0.1.52", + "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.52/design-system-0.1.52.tgz", + "integrity": "sha512-yhxZodFoqGbucNdjRtnlmTt+SI3csv0+nOf8nvD6hmsOjj0WhaqMjdj+hqPpc6EZu3UVEWjfeX+9d/1B7cyy0A==", "dependencies": { "lucide-react": "^1.7.0", "react": "^19.0.0", diff --git a/ui/package.json b/ui/package.json index 0fbf4dfe..93d6ec44 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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')\"" }, "dependencies": { - "@cameleer/design-system": "^0.1.50", + "@cameleer/design-system": "^0.1.52", "@tanstack/react-query": "^5.90.21", "js-yaml": "^4.1.1", "lucide-react": "^1.7.0", diff --git a/ui/src/components/ExecutionDiagram/tabs/InfoTab.tsx b/ui/src/components/ExecutionDiagram/tabs/InfoTab.tsx index 853344e4..ea6a45d6 100644 --- a/ui/src/components/ExecutionDiagram/tabs/InfoTab.tsx +++ b/ui/src/components/ExecutionDiagram/tabs/InfoTab.tsx @@ -1,7 +1,7 @@ import { Badge } from '@cameleer/design-system'; import type { ProcessorNode, ExecutionDetail } from '../types'; import { attributeBadgeColor } from '../../../utils/attribute-color'; -import { formatDurationShort } from '../../../utils/format-utils'; +import { formatDurationShort, statusLabel } from '../../../utils/format-utils'; import styles from '../ExecutionDiagram.module.css'; interface InfoTabProps { @@ -13,11 +13,14 @@ function formatTime(iso: string | undefined): string { if (!iso) return '-'; try { const d = new Date(iso); + const y = d.getFullYear(); + const mo = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); const h = String(d.getHours()).padStart(2, '0'); const m = String(d.getMinutes()).padStart(2, '0'); const s = String(d.getSeconds()).padStart(2, '0'); const ms = String(d.getMilliseconds()).padStart(3, '0'); - return `${h}:${m}:${s}.${ms}`; + return `${y}-${mo}-${day} ${h}:${m}:${s}.${ms}`; } catch { return iso; } @@ -66,7 +69,7 @@ export function InfoTab({ processor, executionDetail }: InfoTabProps) {
Status
- {processor.status} + {statusLabel(processor.status)}
@@ -96,7 +99,7 @@ export function InfoTab({ processor, executionDetail }: InfoTabProps) {
Status
- {executionDetail.status} + {statusLabel(executionDetail.status)}
diff --git a/ui/src/components/ExecutionDiagram/tabs/LogTab.tsx b/ui/src/components/ExecutionDiagram/tabs/LogTab.tsx index 79b28189..4f6e7022 100644 --- a/ui/src/components/ExecutionDiagram/tabs/LogTab.tsx +++ b/ui/src/components/ExecutionDiagram/tabs/LogTab.tsx @@ -2,7 +2,8 @@ import { useState, useMemo } from 'react'; import { useNavigate } from 'react-router'; import { Input, Button, LogViewer } from '@cameleer/design-system'; import type { LogEntry } from '@cameleer/design-system'; -import { useApplicationLogs } from '../../../api/queries/logs'; +import { useLogs } from '../../../api/queries/logs'; +import type { LogEntryResponse } from '../../../api/queries/logs'; import { mapLogLevel } from '../../../utils/agent-utils'; import logStyles from './LogTab.module.css'; import diagramStyles from '../ExecutionDiagram.module.css'; @@ -13,26 +14,33 @@ interface LogTabProps { processorId: string | null; } +function matchesProcessor(e: LogEntryResponse, pid: string): boolean { + if (e.message?.includes(pid)) return true; + if (e.loggerName?.includes(pid)) return true; + if (e.mdc) { + for (const v of Object.values(e.mdc)) { + if (v === pid) return true; + } + } + return false; +} + export function LogTab({ applicationId, exchangeId, processorId }: LogTabProps) { const [filter, setFilter] = useState(''); const navigate = useNavigate(); - const { data: logs, isLoading } = useApplicationLogs( - applicationId, - undefined, + const { data: logPage, isLoading } = useLogs( { exchangeId, limit: 500 }, + { enabled: !!exchangeId }, ); const entries = useMemo(() => { - if (!logs) return []; - let items = [...logs]; + if (!logPage?.data) return []; + let items = [...logPage.data]; - // If a processor is selected, filter logs by logger name containing the processor ID + // If a processor is selected, filter logs to that processor if (processorId) { - items = items.filter((e) => - e.message?.includes(processorId) || - e.loggerName?.includes(processorId) - ); + items = items.filter((e) => matchesProcessor(e, processorId)); } // Text filter @@ -50,7 +58,7 @@ export function LogTab({ applicationId, exchangeId, processorId }: LogTabProps) level: mapLogLevel(e.level), message: e.message ?? '', })); - }, [logs, processorId, filter]); + }, [logPage, processorId, filter]); if (isLoading) { return
Loading logs...
; diff --git a/ui/src/components/LayoutShell.module.css b/ui/src/components/LayoutShell.module.css index 4f89366e..aeeee04d 100644 --- a/ui/src/components/LayoutShell.module.css +++ b/ui/src/components/LayoutShell.module.css @@ -62,15 +62,57 @@ font-size: 12px; font-weight: 500; color: var(--sidebar-muted); + opacity: 0.45; cursor: pointer; - transition: color 0.12s, background 0.12s; + transition: color 0.12s, background 0.12s, opacity 0.12s; } .addAppBtn:hover { + opacity: 1; color: var(--amber); background: rgba(255, 255, 255, 0.06); } +.sidebarFilters { + display: flex; + gap: 6px; + padding: 4px 12px 6px; +} + +.filterChip { + display: flex; + align-items: center; + gap: 4px; + background: none; + border: 1px solid transparent; + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + color: var(--sidebar-muted); + cursor: pointer; + white-space: nowrap; + transition: color 0.12s, border-color 0.12s, background 0.12s; + opacity: 0.6; +} + +.filterChip:hover { + opacity: 1; + color: var(--sidebar-text); + border-color: rgba(255, 255, 255, 0.12); +} + +.filterChipActive { + opacity: 1; + color: var(--amber); + border-color: var(--amber); + background: rgba(var(--amber-rgb, 245, 158, 11), 0.08); +} + +.filterChipIcon { + display: flex; + align-items: center; +} + .mainContent { flex: 1; display: flex; diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index 99c4f187..b8059c6c 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -21,7 +21,7 @@ import { } from '@cameleer/design-system'; import type { SearchResult, SidebarTreeNode, DropdownItem, ButtonGroupItem, ExchangeStatus } from '@cameleer/design-system'; import sidebarLogo from '@cameleer/design-system/assets/cameleer-logo.svg'; -import { Box, Settings, FileText, ChevronRight, Square, Pause, Star, X, User, Plus } from 'lucide-react'; +import { Box, Settings, FileText, ChevronRight, Square, Pause, Star, X, User, Plus, EyeOff } from 'lucide-react'; import { AboutMeDialog } from './AboutMeDialog'; import css from './LayoutShell.module.css'; import { useQueryClient } from '@tanstack/react-query'; @@ -270,9 +270,9 @@ function StarredList({ items, onNavigate, onRemove }: { items: StarredItem[]; on /* ------------------------------------------------------------------ */ const STATUS_ITEMS: ButtonGroupItem[] = [ - { value: 'completed', label: 'OK', color: 'var(--success)' }, - { value: 'warning', label: 'Warn', color: 'var(--warning)' }, - { value: 'failed', label: 'Error', color: 'var(--error)' }, + { value: 'completed', label: 'Completed', color: 'var(--success)' }, + { value: 'warning', label: 'Warning', color: 'var(--warning)' }, + { value: 'failed', label: 'Failed', color: 'var(--error)' }, { value: 'running', label: 'Running', color: 'var(--running)' }, ] @@ -345,6 +345,15 @@ function LayoutContent() { // --- Sidebar filter ----------------------------------------------- const [filterQuery, setFilterQuery] = useState(''); + const [hideEmptyRoutes, setHideEmptyRoutes] = useState(() => readCollapsed('sidebar:hideEmptyRoutes', false)); + const [hideOfflineApps, setHideOfflineApps] = useState(() => readCollapsed('sidebar:hideOfflineApps', false)); + + const toggleHideEmptyRoutes = useCallback(() => { + setHideEmptyRoutes((prev) => { writeCollapsed('sidebar:hideEmptyRoutes', !prev); return !prev; }); + }, []); + const toggleHideOfflineApps = useCallback(() => { + setHideOfflineApps((prev) => { writeCollapsed('sidebar:hideOfflineApps', !prev); return !prev; }); + }, []); const setSelectedEnv = useCallback((env: string | undefined) => { setSelectedEnvRaw(env); @@ -430,10 +439,27 @@ function LayoutContent() { })); }, [catalog]); + // --- Apply sidebar filters ----------------------------------------- + const filteredSidebarApps: SidebarApp[] = useMemo(() => { + let apps = sidebarApps; + if (hideOfflineApps) { + apps = apps.filter((a) => a.health !== 'dead' && a.health !== 'stale'); + } + if (hideEmptyRoutes) { + apps = apps + .map((a) => ({ + ...a, + routes: a.routes.filter((r) => r.exchangeCount > 0), + })) + .filter((a) => a.exchangeCount > 0 || a.routes.length > 0); + } + return apps; + }, [sidebarApps, hideOfflineApps, hideEmptyRoutes]); + // --- Tree nodes --------------------------------------------------- const appTreeNodes: SidebarTreeNode[] = useMemo( - () => buildAppTreeNodes(sidebarApps, makeStatusDot, makeChevron, makeStopIcon, makePauseIcon), - [sidebarApps], + () => buildAppTreeNodes(filteredSidebarApps, makeStatusDot, makeChevron, makeStopIcon, makePauseIcon), + [filteredSidebarApps], ); const adminTreeNodes: SidebarTreeNode[] = useMemo( @@ -692,9 +718,29 @@ function LayoutContent() { version={__APP_VERSION__} /> + {/* Sidebar filters */} + {!sidebarCollapsed &&
+ + +
} + {/* Applications section */}
- {canControl && ( + {canControl && !sidebarCollapsed && (
Sampling Rate - setSamplingRate(e.target.value)} className={styles.inputLg} /> + setSamplingRate(e.target.value)} className={styles.inputLg} placeholder="1.0" /> Compress Success
@@ -438,6 +459,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen !busy && setRouteControlEnabled(!routeControlEnabled)} disabled={busy} /> {routeControlEnabled ? 'Enabled' : 'Disabled'}
+ )} @@ -446,23 +468,42 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
Container Resources
+ Runtime Type + setCustomArgs(e.target.value)} + placeholder="-Xmx256m -Dfoo=bar" className={styles.inputLg} /> + + {runtimeType === 'native' ? 'Arguments passed to the native binary' : 'Additional JVM arguments appended to the start command'} + +
+ Memory Limit
- setMemoryLimit(e.target.value)} className={styles.inputLg} /> + setMemoryLimit(e.target.value)} className={styles.inputLg} placeholder="e.g. 512" /> MB
Memory Reserve
- setMemoryReserve(e.target.value)} placeholder="---" className={styles.inputLg} /> + setMemoryReserve(e.target.value)} placeholder="e.g. 256" className={styles.inputLg} /> MB
{!isProd && Available in production environments only}
CPU Request - setCpuRequest(e.target.value)} className={styles.inputLg} /> + setCpuRequest(e.target.value)} className={styles.inputLg} placeholder="e.g. 500 millicores" /> CPU Limit
@@ -485,10 +526,10 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
App Port - setAppPort(e.target.value)} className={styles.inputLg} /> + setAppPort(e.target.value)} className={styles.inputLg} placeholder="e.g. 8080" /> Replicas - setReplicas(e.target.value)} className={styles.inputSm} type="number" /> + setReplicas(e.target.value)} className={styles.inputSm} type="number" placeholder="1" /> Deploy Strategy setNewSensitiveKey(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + const v = newSensitiveKey.trim(); + if (v && !sensitiveKeys.some((k) => k.toLowerCase() === v.toLowerCase())) { + setSensitiveKeys([...sensitiveKeys, v]); + setNewSensitiveKey(''); + } + } + }} + placeholder="Add key or glob pattern (e.g. *password*)" + disabled={busy} + /> + +
+ +
+ + + The final masking configuration is: agent defaults + global keys + app-specific keys. + Supports exact header names and glob patterns. + +
+ + )} ); } diff --git a/ui/src/pages/Dashboard/Dashboard.tsx b/ui/src/pages/Dashboard/Dashboard.tsx index 38f9583c..65ec6ba7 100644 --- a/ui/src/pages/Dashboard/Dashboard.tsx +++ b/ui/src/pages/Dashboard/Dashboard.tsx @@ -54,12 +54,6 @@ function durationClass(ms: number, status: string): string { return styles.durBreach } -function shortAgentName(name: string): string { - const parts = name.split('-') - if (parts.length >= 3) return parts.slice(-2).join('-') - return name -} - // ─── Table columns ──────────────────────────────────────────────────────────── function buildColumns(hasAttributes: boolean): Column[] { @@ -77,22 +71,6 @@ function buildColumns(hasAttributes: boolean): Column[] { ), }, - { - key: 'routeId', - header: 'Route', - sortable: true, - render: (_: unknown, row: Row) => ( - {row.routeId} - ), - }, - { - key: 'applicationId', - header: 'Application', - sortable: true, - render: (_: unknown, row: Row) => ( - {row.applicationId ?? ''} - ), - }, ...(hasAttributes ? [{ key: 'attributes' as const, header: 'Attributes', @@ -115,20 +93,19 @@ function buildColumns(hasAttributes: boolean): Column[] { }, }] : []), { - key: 'executionId', - header: 'Exchange ID', + key: 'applicationId', + header: 'App', sortable: true, render: (_: unknown, row: Row) => ( - { - e.stopPropagation(); - navigator.clipboard.writeText(row.executionId); - }} - > - ...{row.executionId.slice(-8)} - + {row.applicationId ?? ''} + ), + }, + { + key: 'routeId', + header: 'Route', + sortable: true, + render: (_: unknown, row: Row) => ( + {row.routeId} ), }, { @@ -149,16 +126,6 @@ function buildColumns(hasAttributes: boolean): Column[] { ), }, - { - key: 'instanceId', - header: 'Agent', - render: (_: unknown, row: Row) => ( - - - {shortAgentName(row.instanceId)} - - ), - }, ] } diff --git a/ui/src/pages/Routes/RouteDetail.tsx b/ui/src/pages/Routes/RouteDetail.tsx index ee9a64ca..fd98a57e 100644 --- a/ui/src/pages/Routes/RouteDetail.tsx +++ b/ui/src/pages/Routes/RouteDetail.tsx @@ -40,6 +40,7 @@ import type { TapDefinition } from '../../api/queries/commands'; import type { ExecutionSummary } from '../../api/types'; import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog'; import { buildFlowSegments } from '../../utils/diagram-mapping'; +import { statusLabel } from '../../utils/format-utils'; import styles from './RouteDetail.module.css'; import tableStyles from '../../styles/table-section.module.css'; import rateStyles from '../../styles/rate-colors.module.css'; @@ -658,7 +659,7 @@ export default function RouteDetail() { const recentExchangeOptions = useMemo(() => exchangeRows.slice(0, 20).map(e => ({ value: e.executionId, - label: `${e.executionId.slice(0, 12)} — ${e.status}`, + label: `${e.executionId.slice(0, 12)} — ${statusLabel(e.status)}`, })), [exchangeRows], ); diff --git a/ui/src/utils/format-utils.ts b/ui/src/utils/format-utils.ts index 8e7a5a84..4d3de0a6 100644 --- a/ui/src/utils/format-utils.ts +++ b/ui/src/utils/format-utils.ts @@ -20,12 +20,8 @@ export function formatDurationShort(ms: number | undefined): string { } export function statusLabel(s: string): string { - switch (s) { - case 'COMPLETED': return 'OK'; - case 'FAILED': return 'ERR'; - case 'RUNNING': return 'RUN'; - default: return s; - } + if (!s) return s; + return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(); } export function timeAgo(iso?: string): string {