refactor: extract duplicated utility functions into shared modules
Consolidate 20+ duplicate function definitions across UI components into three shared util files (format-utils, agent-utils, config-draft-utils). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { Badge } from '@cameleer/design-system';
|
import { Badge } from '@cameleer/design-system';
|
||||||
import type { ProcessorNode, ExecutionDetail } from '../types';
|
import type { ProcessorNode, ExecutionDetail } from '../types';
|
||||||
import { attributeBadgeColor } from '../../../utils/attribute-color';
|
import { attributeBadgeColor } from '../../../utils/attribute-color';
|
||||||
|
import { formatDurationShort } from '../../../utils/format-utils';
|
||||||
import styles from '../ExecutionDiagram.module.css';
|
import styles from '../ExecutionDiagram.module.css';
|
||||||
|
|
||||||
interface InfoTabProps {
|
interface InfoTabProps {
|
||||||
@@ -22,12 +23,6 @@ function formatTime(iso: string | undefined): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDuration(ms: number | undefined): string {
|
|
||||||
if (ms === undefined || ms === null) return '-';
|
|
||||||
if (ms < 1000) return `${ms}ms`;
|
|
||||||
return `${(ms / 1000).toFixed(1)}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusClass(status: string): string {
|
function statusClass(status: string): string {
|
||||||
const s = status?.toUpperCase();
|
const s = status?.toUpperCase();
|
||||||
if (s === 'COMPLETED') return styles.statusCompleted;
|
if (s === 'COMPLETED') return styles.statusCompleted;
|
||||||
@@ -77,7 +72,7 @@ export function InfoTab({ processor, executionDetail }: InfoTabProps) {
|
|||||||
|
|
||||||
<Field label="Start Time" value={formatTime(processor.startTime)} mono />
|
<Field label="Start Time" value={formatTime(processor.startTime)} mono />
|
||||||
<Field label="End Time" value={formatTime(processor.endTime)} mono />
|
<Field label="End Time" value={formatTime(processor.endTime)} mono />
|
||||||
<Field label="Duration" value={formatDuration(processor.durationMs)} mono />
|
<Field label="Duration" value={formatDurationShort(processor.durationMs)} mono />
|
||||||
|
|
||||||
<Field label="Endpoint URI" value={processor.processorType} />
|
<Field label="Endpoint URI" value={processor.processorType} />
|
||||||
<Field label="Resolved URI" value={processor.resolvedEndpointUri ?? '-'} mono />
|
<Field label="Resolved URI" value={processor.resolvedEndpointUri ?? '-'} mono />
|
||||||
@@ -107,7 +102,7 @@ export function InfoTab({ processor, executionDetail }: InfoTabProps) {
|
|||||||
|
|
||||||
<Field label="Start Time" value={formatTime(executionDetail.startTime)} mono />
|
<Field label="Start Time" value={formatTime(executionDetail.startTime)} mono />
|
||||||
<Field label="End Time" value={formatTime(executionDetail.endTime)} mono />
|
<Field label="End Time" value={formatTime(executionDetail.endTime)} mono />
|
||||||
<Field label="Duration" value={formatDuration(executionDetail.durationMs)} mono />
|
<Field label="Duration" value={formatDurationShort(executionDetail.durationMs)} mono />
|
||||||
</div>
|
</div>
|
||||||
<Attributes attrs={executionDetail.attributes} />
|
<Attributes attrs={executionDetail.attributes} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import type { ReactNode } from 'react';
|
|||||||
import { ContentTabs } from './ContentTabs';
|
import { ContentTabs } from './ContentTabs';
|
||||||
import { EnvironmentSelector } from './EnvironmentSelector';
|
import { EnvironmentSelector } from './EnvironmentSelector';
|
||||||
import { useScope } from '../hooks/useScope';
|
import { useScope } from '../hooks/useScope';
|
||||||
|
import { formatDuration } from '../utils/format-utils';
|
||||||
import {
|
import {
|
||||||
buildAppTreeNodes,
|
buildAppTreeNodes,
|
||||||
buildAdminTreeNodes,
|
buildAdminTreeNodes,
|
||||||
@@ -162,12 +163,6 @@ function healthToSearchColor(health: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`;
|
|
||||||
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
|
|
||||||
return `${ms}ms`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusToColor(status: string): string {
|
function statusToColor(status: string): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'COMPLETED': return 'success';
|
case 'COMPLETED': return 'success';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams'
|
|||||||
import type { NodeConfig, LatencyHeatmapEntry } from './types';
|
import type { NodeConfig, LatencyHeatmapEntry } from './types';
|
||||||
import type { NodeExecutionState } from '../ExecutionDiagram/types';
|
import type { NodeExecutionState } from '../ExecutionDiagram/types';
|
||||||
import { colorForType, iconForType, type IconElement } from './node-colors';
|
import { colorForType, iconForType, type IconElement } from './node-colors';
|
||||||
|
import { formatDurationShort } from '../../utils/format-utils';
|
||||||
|
|
||||||
const TOP_BAR_HEIGHT = 6;
|
const TOP_BAR_HEIGHT = 6;
|
||||||
const TEXT_LEFT = 32;
|
const TEXT_LEFT = 32;
|
||||||
@@ -37,11 +38,6 @@ function heatmapBorderColor(pct: number): string {
|
|||||||
return `hsl(${Math.round(hue)}, 60%, 50%)`;
|
return `hsl(${Math.round(hue)}, 60%, 50%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
if (ms < 1000) return `${ms}ms`;
|
|
||||||
return `${(ms / 1000).toFixed(1)}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DiagramNode({
|
export function DiagramNode({
|
||||||
node, isHovered, isSelected, config,
|
node, isHovered, isSelected, config,
|
||||||
executionState, overlayActive, heatmapEntry,
|
executionState, overlayActive, heatmapEntry,
|
||||||
@@ -273,7 +269,7 @@ export function DiagramNode({
|
|||||||
fontSize={9}
|
fontSize={9}
|
||||||
fontWeight={500}
|
fontWeight={500}
|
||||||
>
|
>
|
||||||
{formatDuration(executionState.durationMs)}
|
{formatDurationShort(executionState.durationMs)}
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -287,7 +283,7 @@ export function DiagramNode({
|
|||||||
fontSize={9}
|
fontSize={9}
|
||||||
fontWeight={600}
|
fontWeight={600}
|
||||||
>
|
>
|
||||||
{formatDuration(heatmapEntry.avgDurationMs)}
|
{formatDurationShort(heatmapEntry.avgDurationMs)}
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/quer
|
|||||||
import type { ApplicationConfig, TapDefinition, ConfigUpdateResponse } from '../../api/queries/commands';
|
import type { ApplicationConfig, TapDefinition, ConfigUpdateResponse } from '../../api/queries/commands';
|
||||||
import { useCatalog } from '../../api/queries/catalog';
|
import { useCatalog } from '../../api/queries/catalog';
|
||||||
import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog';
|
import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog';
|
||||||
|
import { applyTracedProcessorUpdate, applyRouteRecordingUpdate } from '../../utils/config-draft-utils';
|
||||||
import styles from './AppConfigDetailPage.module.css';
|
import styles from './AppConfigDetailPage.module.css';
|
||||||
|
|
||||||
type BadgeColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto';
|
type BadgeColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto';
|
||||||
@@ -130,18 +131,11 @@ export default function AppConfigDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateTracedProcessor(processorId: string, mode: string) {
|
function updateTracedProcessor(processorId: string, mode: string) {
|
||||||
setTracedDraft((prev) => {
|
setTracedDraft((prev) => applyTracedProcessorUpdate(prev, processorId, mode));
|
||||||
if (mode === 'REMOVE') {
|
|
||||||
const next = { ...prev };
|
|
||||||
delete next[processorId];
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
return { ...prev, [processorId]: mode };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRouteRecording(routeId: string, recording: boolean) {
|
function updateRouteRecording(routeId: string, recording: boolean) {
|
||||||
setRouteRecordingDraft((prev) => ({ ...prev, [routeId]: recording }));
|
setRouteRecordingDraft((prev) => applyRouteRecordingUpdate(prev, routeId, recording));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSave() {
|
function handleSave() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useMemo, useCallback } from 'react';
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { ExternalLink, RefreshCw, Pencil, UserPlus, UserMinus, Play, Square, Clock, Skull, HeartPulse, Route, Send, Activity } from 'lucide-react';
|
import { ExternalLink, RefreshCw, Pencil } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
StatCard, StatusDot, Badge, MonoText,
|
StatCard, StatusDot, Badge, MonoText,
|
||||||
GroupCard, DataTable, EventFeed,
|
GroupCard, DataTable, EventFeed,
|
||||||
@@ -13,31 +13,11 @@ import { useApplicationLogs } from '../../api/queries/logs';
|
|||||||
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
|
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
|
||||||
import type { ConfigUpdateResponse } from '../../api/queries/commands';
|
import type { ConfigUpdateResponse } from '../../api/queries/commands';
|
||||||
import type { AgentInstance } from '../../api/types';
|
import type { AgentInstance } from '../../api/types';
|
||||||
|
import { timeAgo } from '../../utils/format-utils';
|
||||||
|
import { formatUptime, mapLogLevel, eventSeverity, eventIcon } from '../../utils/agent-utils';
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function timeAgo(iso?: string): string {
|
|
||||||
if (!iso) return '\u2014';
|
|
||||||
const diff = Date.now() - new Date(iso).getTime();
|
|
||||||
const secs = Math.floor(diff / 1000);
|
|
||||||
if (secs < 60) return `${secs}s ago`;
|
|
||||||
const mins = Math.floor(secs / 60);
|
|
||||||
if (mins < 60) return `${mins}m ago`;
|
|
||||||
const hours = Math.floor(mins / 60);
|
|
||||||
if (hours < 24) return `${hours}h ago`;
|
|
||||||
return `${Math.floor(hours / 24)}d ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatUptime(seconds?: number): string {
|
|
||||||
if (!seconds) return '\u2014';
|
|
||||||
const days = Math.floor(seconds / 86400);
|
|
||||||
const hours = Math.floor((seconds % 86400) / 3600);
|
|
||||||
const mins = Math.floor((seconds % 3600) / 60);
|
|
||||||
if (days > 0) return `${days}d ${hours}h`;
|
|
||||||
if (hours > 0) return `${hours}h ${mins}m`;
|
|
||||||
return `${mins}m`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatErrorRate(rate?: number): string {
|
function formatErrorRate(rate?: number): string {
|
||||||
if (rate == null) return '\u2014';
|
if (rate == null) return '\u2014';
|
||||||
return `${(rate * 100).toFixed(1)}%`;
|
return `${(rate * 100).toFixed(1)}%`;
|
||||||
@@ -104,16 +84,6 @@ const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [
|
|||||||
{ value: 'trace', label: 'Trace', color: 'var(--text-muted)' },
|
{ value: 'trace', label: 'Trace', color: 'var(--text-muted)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function mapLogLevel(level: string): LogEntry['level'] {
|
|
||||||
switch (level?.toUpperCase()) {
|
|
||||||
case 'ERROR': return 'error';
|
|
||||||
case 'WARN': case 'WARNING': return 'warn';
|
|
||||||
case 'DEBUG': return 'debug';
|
|
||||||
case 'TRACE': return 'trace';
|
|
||||||
default: return 'info';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── AgentHealth page ─────────────────────────────────────────────────────────
|
// ── AgentHealth page ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function AgentHealth() {
|
export default function AgentHealth() {
|
||||||
@@ -195,35 +165,6 @@ export default function AgentHealth() {
|
|||||||
|
|
||||||
// Map events to FeedEvent
|
// Map events to FeedEvent
|
||||||
const feedEvents: FeedEvent[] = useMemo(() => {
|
const feedEvents: FeedEvent[] = useMemo(() => {
|
||||||
const eventIcon = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'REGISTERED': return <UserPlus size={14} />;
|
|
||||||
case 'DEREGISTERED': return <UserMinus size={14} />;
|
|
||||||
case 'AGENT_STARTED': return <Play size={14} />;
|
|
||||||
case 'AGENT_STOPPED': return <Square size={14} />;
|
|
||||||
case 'WENT_STALE': return <Clock size={14} />;
|
|
||||||
case 'WENT_DEAD': return <Skull size={14} />;
|
|
||||||
case 'RECOVERED': return <HeartPulse size={14} />;
|
|
||||||
case 'ROUTE_STATE_CHANGED': return <Route size={14} />;
|
|
||||||
case 'COMMAND_DELIVERED':
|
|
||||||
case 'COMMAND_ACKNOWLEDGED': return <Send size={14} />;
|
|
||||||
default: return <Activity size={14} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventSeverity = (type: string): FeedEvent['severity'] => {
|
|
||||||
switch (type) {
|
|
||||||
case 'WENT_DEAD':
|
|
||||||
case 'AGENT_STOPPED':
|
|
||||||
case 'DEREGISTERED': return 'error';
|
|
||||||
case 'WENT_STALE': return 'warning';
|
|
||||||
case 'RECOVERED':
|
|
||||||
case 'REGISTERED':
|
|
||||||
case 'AGENT_STARTED': return 'success';
|
|
||||||
default: return 'running';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapped = (events ?? []).map((e: { id: number; instanceId: string; eventType: string; detail: string; timestamp: string }) => ({
|
const mapped = (events ?? []).map((e: { id: number; instanceId: string; eventType: string; detail: string; timestamp: string }) => ({
|
||||||
id: String(e.id),
|
id: String(e.id),
|
||||||
severity: eventSeverity(e.eventType),
|
severity: eventSeverity(e.eventType),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useParams, Link } from 'react-router';
|
import { useParams, Link } from 'react-router';
|
||||||
import { RefreshCw, ChevronRight, UserPlus, UserMinus, Play, Square, Clock, Skull, HeartPulse, Route, Send, Activity } from 'lucide-react';
|
import { RefreshCw, ChevronRight } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
StatCard, StatusDot, Badge, LineChart, AreaChart, BarChart,
|
StatCard, StatusDot, Badge, LineChart, AreaChart, BarChart,
|
||||||
EventFeed, Spinner, EmptyState, SectionHeader, MonoText,
|
EventFeed, Spinner, EmptyState, SectionHeader, MonoText,
|
||||||
@@ -12,6 +12,7 @@ import { useAgents, useAgentEvents } from '../../api/queries/agents';
|
|||||||
import { useApplicationLogs } from '../../api/queries/logs';
|
import { useApplicationLogs } from '../../api/queries/logs';
|
||||||
import { useStatsTimeseries } from '../../api/queries/executions';
|
import { useStatsTimeseries } from '../../api/queries/executions';
|
||||||
import { useAgentMetrics } from '../../api/queries/agent-metrics';
|
import { useAgentMetrics } from '../../api/queries/agent-metrics';
|
||||||
|
import { formatUptime, mapLogLevel, eventSeverity, eventIcon } from '../../utils/agent-utils';
|
||||||
|
|
||||||
const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [
|
const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [
|
||||||
{ value: 'error', label: 'Error', color: 'var(--error)' },
|
{ value: 'error', label: 'Error', color: 'var(--error)' },
|
||||||
@@ -21,16 +22,6 @@ const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [
|
|||||||
{ value: 'trace', label: 'Trace', color: 'var(--text-muted)' },
|
{ value: 'trace', label: 'Trace', color: 'var(--text-muted)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function mapLogLevel(level: string): LogEntry['level'] {
|
|
||||||
switch (level?.toUpperCase()) {
|
|
||||||
case 'ERROR': return 'error';
|
|
||||||
case 'WARN': case 'WARNING': return 'warn';
|
|
||||||
case 'DEBUG': return 'debug';
|
|
||||||
case 'TRACE': return 'trace';
|
|
||||||
default: return 'info';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AgentInstance() {
|
export default function AgentInstance() {
|
||||||
const { appId, instanceId } = useParams();
|
const { appId, instanceId } = useParams();
|
||||||
const { timeRange } = useGlobalFilters();
|
const { timeRange } = useGlobalFilters();
|
||||||
@@ -82,35 +73,6 @@ export default function AgentInstance() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const feedEvents = useMemo<FeedEvent[]>(() => {
|
const feedEvents = useMemo<FeedEvent[]>(() => {
|
||||||
const eventIcon = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'REGISTERED': return <UserPlus size={14} />;
|
|
||||||
case 'DEREGISTERED': return <UserMinus size={14} />;
|
|
||||||
case 'AGENT_STARTED': return <Play size={14} />;
|
|
||||||
case 'AGENT_STOPPED': return <Square size={14} />;
|
|
||||||
case 'WENT_STALE': return <Clock size={14} />;
|
|
||||||
case 'WENT_DEAD': return <Skull size={14} />;
|
|
||||||
case 'RECOVERED': return <HeartPulse size={14} />;
|
|
||||||
case 'ROUTE_STATE_CHANGED': return <Route size={14} />;
|
|
||||||
case 'COMMAND_DELIVERED':
|
|
||||||
case 'COMMAND_ACKNOWLEDGED': return <Send size={14} />;
|
|
||||||
default: return <Activity size={14} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventSeverity = (type: string): FeedEvent['severity'] => {
|
|
||||||
switch (type) {
|
|
||||||
case 'WENT_DEAD':
|
|
||||||
case 'AGENT_STOPPED':
|
|
||||||
case 'DEREGISTERED': return 'error';
|
|
||||||
case 'WENT_STALE': return 'warning';
|
|
||||||
case 'RECOVERED':
|
|
||||||
case 'REGISTERED':
|
|
||||||
case 'AGENT_STARTED': return 'success';
|
|
||||||
default: return 'running';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapped = (events || [])
|
const mapped = (events || [])
|
||||||
.filter((e: any) => !instanceId || e.instanceId === instanceId)
|
.filter((e: any) => !instanceId || e.instanceId === instanceId)
|
||||||
.map((e: any) => ({
|
.map((e: any) => ({
|
||||||
@@ -495,13 +457,3 @@ export default function AgentInstance() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatUptime(seconds?: number): string {
|
|
||||||
if (!seconds) return '\u2014';
|
|
||||||
const days = Math.floor(seconds / 86400);
|
|
||||||
const hours = Math.floor((seconds % 86400) / 3600);
|
|
||||||
const mins = Math.floor((seconds % 3600) / 60);
|
|
||||||
if (days > 0) return `${days}d ${hours}h`;
|
|
||||||
if (hours > 0) return `${hours}h ${mins}m`;
|
|
||||||
return `${mins}m`;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import type { ApplicationConfig, TapDefinition } from '../../api/queries/command
|
|||||||
import { useCatalog } from '../../api/queries/catalog';
|
import { useCatalog } from '../../api/queries/catalog';
|
||||||
import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog';
|
import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog';
|
||||||
import { DeploymentProgress } from '../../components/DeploymentProgress';
|
import { DeploymentProgress } from '../../components/DeploymentProgress';
|
||||||
|
import { timeAgo } from '../../utils/format-utils';
|
||||||
|
import { applyTracedProcessorUpdate, applyRouteRecordingUpdate } from '../../utils/config-draft-utils';
|
||||||
import styles from './AppsTab.module.css';
|
import styles from './AppsTab.module.css';
|
||||||
|
|
||||||
function formatBytes(bytes: number): string {
|
function formatBytes(bytes: number): string {
|
||||||
@@ -44,16 +46,6 @@ function formatBytes(bytes: number): string {
|
|||||||
return `${bytes} B`;
|
return `${bytes} B`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeAgo(date: string): string {
|
|
||||||
const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000);
|
|
||||||
if (seconds < 60) return `${seconds}s ago`;
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
if (minutes < 60) return `${minutes}m ago`;
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
if (hours < 24) return `${hours}h ago`;
|
|
||||||
return `${Math.floor(hours / 24)}d ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, 'success' | 'warning' | 'error' | 'auto' | 'running'> = {
|
const STATUS_COLORS: Record<string, 'success' | 'warning' | 'error' | 'auto' | 'running'> = {
|
||||||
RUNNING: 'running', STARTING: 'warning', FAILED: 'error', STOPPED: 'auto',
|
RUNNING: 'running', STARTING: 'warning', FAILED: 'error', STOPPED: 'auto',
|
||||||
DEGRADED: 'warning', STOPPING: 'auto',
|
DEGRADED: 'warning', STOPPING: 'auto',
|
||||||
@@ -801,14 +793,11 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateTracedProcessor(processorId: string, mode: string) {
|
function updateTracedProcessor(processorId: string, mode: string) {
|
||||||
setTracedDraft((prev) => {
|
setTracedDraft((prev) => applyTracedProcessorUpdate(prev, processorId, mode));
|
||||||
if (mode === 'REMOVE') { const next = { ...prev }; delete next[processorId]; return next; }
|
|
||||||
return { ...prev, [processorId]: mode };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRouteRecording(routeId: string, recording: boolean) {
|
function updateRouteRecording(routeId: string, recording: boolean) {
|
||||||
setRouteRecordingDraft((prev) => ({ ...prev, [routeId]: recording }));
|
setRouteRecordingDraft((prev) => applyRouteRecordingUpdate(prev, routeId, recording));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '../../api/queries/executions'
|
} from '../../api/queries/executions'
|
||||||
import type { ExecutionSummary } from '../../api/types'
|
import type { ExecutionSummary } from '../../api/types'
|
||||||
import { attributeBadgeColor } from '../../utils/attribute-color'
|
import { attributeBadgeColor } from '../../utils/attribute-color'
|
||||||
|
import { formatDuration, statusLabel } from '../../utils/format-utils'
|
||||||
import styles from './Dashboard.module.css'
|
import styles from './Dashboard.module.css'
|
||||||
|
|
||||||
// Row type extends ExecutionSummary with an `id` field for DataTable
|
// Row type extends ExecutionSummary with an `id` field for DataTable
|
||||||
@@ -23,12 +24,6 @@ interface Row extends ExecutionSummary {
|
|||||||
|
|
||||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`
|
|
||||||
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`
|
|
||||||
return `${ms}ms`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTimestamp(iso: string): string {
|
function formatTimestamp(iso: string): string {
|
||||||
const date = new Date(iso)
|
const date = new Date(iso)
|
||||||
const y = date.getFullYear()
|
const y = date.getFullYear()
|
||||||
@@ -49,15 +44,6 @@ function statusToVariant(status: string): 'success' | 'error' | 'running' | 'war
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusLabel(status: string): string {
|
|
||||||
switch (status) {
|
|
||||||
case 'COMPLETED': return 'OK'
|
|
||||||
case 'FAILED': return 'ERR'
|
|
||||||
case 'RUNNING': return 'RUN'
|
|
||||||
default: return 'WARN'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function durationClass(ms: number, status: string): string {
|
function durationClass(ms: number, status: string): string {
|
||||||
if (status === 'FAILED') return styles.durBreach
|
if (status === 'FAILED') return styles.durBreach
|
||||||
if (ms < 100) return styles.durFast
|
if (ms < 100) return styles.durFast
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useCatalog } from '../../api/queries/catalog';
|
|||||||
import { useCanControl } from '../../auth/auth-store';
|
import { useCanControl } from '../../auth/auth-store';
|
||||||
import type { ExecutionDetail } from '../../components/ExecutionDiagram/types';
|
import type { ExecutionDetail } from '../../components/ExecutionDiagram/types';
|
||||||
import { attributeBadgeColor } from '../../utils/attribute-color';
|
import { attributeBadgeColor } from '../../utils/attribute-color';
|
||||||
|
import { formatDuration, statusLabel } from '../../utils/format-utils';
|
||||||
import { RouteControlBar } from './RouteControlBar';
|
import { RouteControlBar } from './RouteControlBar';
|
||||||
import styles from './ExchangeHeader.module.css';
|
import styles from './ExchangeHeader.module.css';
|
||||||
|
|
||||||
@@ -28,21 +29,6 @@ function statusVariant(s: string): StatusVariant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusLabel(s: string): string {
|
|
||||||
switch (s) {
|
|
||||||
case 'COMPLETED': return 'OK';
|
|
||||||
case 'FAILED': return 'ERR';
|
|
||||||
case 'RUNNING': return 'RUN';
|
|
||||||
default: return s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
|
||||||
if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`;
|
|
||||||
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
|
|
||||||
return `${ms}ms`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ExchangeHeader({ detail, onCorrelatedSelect, onClearSelection }: ExchangeHeaderProps) {
|
export function ExchangeHeader({ detail, onCorrelatedSelect, onClearSelection }: ExchangeHeaderProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { timeRange } = useGlobalFilters();
|
const { timeRange } = useGlobalFilters();
|
||||||
|
|||||||
53
ui/src/utils/agent-utils.ts
Normal file
53
ui/src/utils/agent-utils.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { FeedEvent } from '@cameleer/design-system';
|
||||||
|
import type { LogEntry } from '@cameleer/design-system';
|
||||||
|
import { UserPlus, UserMinus, Play, Square, Clock, Skull, HeartPulse, Route, Send, Activity } from 'lucide-react';
|
||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
export function formatUptime(seconds?: number): string {
|
||||||
|
if (!seconds) return '\u2014';
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const mins = Math.floor((seconds % 3600) / 60);
|
||||||
|
if (days > 0) return `${days}d ${hours}h`;
|
||||||
|
if (hours > 0) return `${hours}h ${mins}m`;
|
||||||
|
return `${mins}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapLogLevel(level: string): LogEntry['level'] {
|
||||||
|
switch (level?.toUpperCase()) {
|
||||||
|
case 'ERROR': return 'error';
|
||||||
|
case 'WARN': case 'WARNING': return 'warn';
|
||||||
|
case 'DEBUG': return 'debug';
|
||||||
|
case 'TRACE': return 'trace';
|
||||||
|
default: return 'info';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function eventSeverity(type: string): FeedEvent['severity'] {
|
||||||
|
switch (type) {
|
||||||
|
case 'WENT_DEAD':
|
||||||
|
case 'AGENT_STOPPED':
|
||||||
|
case 'DEREGISTERED': return 'error';
|
||||||
|
case 'WENT_STALE': return 'warning';
|
||||||
|
case 'RECOVERED':
|
||||||
|
case 'REGISTERED':
|
||||||
|
case 'AGENT_STARTED': return 'success';
|
||||||
|
default: return 'running';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function eventIcon(type: string) {
|
||||||
|
switch (type) {
|
||||||
|
case 'REGISTERED': return createElement(UserPlus, { size: 14 });
|
||||||
|
case 'DEREGISTERED': return createElement(UserMinus, { size: 14 });
|
||||||
|
case 'AGENT_STARTED': return createElement(Play, { size: 14 });
|
||||||
|
case 'AGENT_STOPPED': return createElement(Square, { size: 14 });
|
||||||
|
case 'WENT_STALE': return createElement(Clock, { size: 14 });
|
||||||
|
case 'WENT_DEAD': return createElement(Skull, { size: 14 });
|
||||||
|
case 'RECOVERED': return createElement(HeartPulse, { size: 14 });
|
||||||
|
case 'ROUTE_STATE_CHANGED': return createElement(Route, { size: 14 });
|
||||||
|
case 'COMMAND_DELIVERED':
|
||||||
|
case 'COMMAND_ACKNOWLEDGED': return createElement(Send, { size: 14 });
|
||||||
|
default: return createElement(Activity, { size: 14 });
|
||||||
|
}
|
||||||
|
}
|
||||||
20
ui/src/utils/config-draft-utils.ts
Normal file
20
ui/src/utils/config-draft-utils.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export function applyTracedProcessorUpdate(
|
||||||
|
prev: Record<string, string>,
|
||||||
|
processorId: string,
|
||||||
|
mode: string,
|
||||||
|
): Record<string, string> {
|
||||||
|
if (mode === 'REMOVE') {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[processorId];
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
return { ...prev, [processorId]: mode };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyRouteRecordingUpdate(
|
||||||
|
prev: Record<string, boolean>,
|
||||||
|
routeId: string,
|
||||||
|
recording: boolean,
|
||||||
|
): Record<string, boolean> {
|
||||||
|
return { ...prev, [routeId]: recording };
|
||||||
|
}
|
||||||
32
ui/src/utils/format-utils.ts
Normal file
32
ui/src/utils/format-utils.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export function formatDuration(ms: number): string {
|
||||||
|
if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`;
|
||||||
|
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
|
||||||
|
return `${ms}ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDurationShort(ms: number | undefined): string {
|
||||||
|
if (ms == null) return '-';
|
||||||
|
if (ms < 1000) return `${ms}ms`;
|
||||||
|
return `${(ms / 1000).toFixed(1)}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function statusLabel(s: string): string {
|
||||||
|
switch (s) {
|
||||||
|
case 'COMPLETED': return 'OK';
|
||||||
|
case 'FAILED': return 'ERR';
|
||||||
|
case 'RUNNING': return 'RUN';
|
||||||
|
default: return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timeAgo(iso?: string): string {
|
||||||
|
if (!iso) return '\u2014';
|
||||||
|
const diff = Date.now() - new Date(iso).getTime();
|
||||||
|
const secs = Math.floor(diff / 1000);
|
||||||
|
if (secs < 60) return `${secs}s ago`;
|
||||||
|
const mins = Math.floor(secs / 60);
|
||||||
|
if (mins < 60) return `${mins}m ago`;
|
||||||
|
const hours = Math.floor(mins / 60);
|
||||||
|
if (hours < 24) return `${hours}h ago`;
|
||||||
|
return `${Math.floor(hours / 24)}d ago`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user