refactor: extract duplicated utility functions into shared modules
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 41s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped

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:
hsiegeln
2026-04-09 08:28:31 +02:00
parent 2df5e0d7ba
commit 04c90bde06
12 changed files with 126 additions and 187 deletions

View File

@@ -1,6 +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 styles from '../ExecutionDiagram.module.css';
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 {
const s = status?.toUpperCase();
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="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="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="End Time" value={formatTime(executionDetail.endTime)} mono />
<Field label="Duration" value={formatDuration(executionDetail.durationMs)} mono />
<Field label="Duration" value={formatDurationShort(executionDetail.durationMs)} mono />
</div>
<Attributes attrs={executionDetail.attributes} />
</div>

View File

@@ -34,6 +34,7 @@ import type { ReactNode } from 'react';
import { ContentTabs } from './ContentTabs';
import { EnvironmentSelector } from './EnvironmentSelector';
import { useScope } from '../hooks/useScope';
import { formatDuration } from '../utils/format-utils';
import {
buildAppTreeNodes,
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 {
switch (status) {
case 'COMPLETED': return 'success';

View File

@@ -3,6 +3,7 @@ import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams'
import type { NodeConfig, LatencyHeatmapEntry } from './types';
import type { NodeExecutionState } from '../ExecutionDiagram/types';
import { colorForType, iconForType, type IconElement } from './node-colors';
import { formatDurationShort } from '../../utils/format-utils';
const TOP_BAR_HEIGHT = 6;
const TEXT_LEFT = 32;
@@ -37,11 +38,6 @@ function heatmapBorderColor(pct: number): string {
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({
node, isHovered, isSelected, config,
executionState, overlayActive, heatmapEntry,
@@ -273,7 +269,7 @@ export function DiagramNode({
fontSize={9}
fontWeight={500}
>
{formatDuration(executionState.durationMs)}
{formatDurationShort(executionState.durationMs)}
</text>
)}
@@ -287,7 +283,7 @@ export function DiagramNode({
fontSize={9}
fontWeight={600}
>
{formatDuration(heatmapEntry.avgDurationMs)}
{formatDurationShort(heatmapEntry.avgDurationMs)}
</text>
)}

View File

@@ -9,6 +9,7 @@ import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/quer
import type { ApplicationConfig, TapDefinition, ConfigUpdateResponse } from '../../api/queries/commands';
import { useCatalog } 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';
type BadgeColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto';
@@ -130,18 +131,11 @@ export default function AppConfigDetailPage() {
}
function updateTracedProcessor(processorId: string, mode: string) {
setTracedDraft((prev) => {
if (mode === 'REMOVE') {
const next = { ...prev };
delete next[processorId];
return next;
}
return { ...prev, [processorId]: mode };
});
setTracedDraft((prev) => applyTracedProcessorUpdate(prev, processorId, mode));
}
function updateRouteRecording(routeId: string, recording: boolean) {
setRouteRecordingDraft((prev) => ({ ...prev, [routeId]: recording }));
setRouteRecordingDraft((prev) => applyRouteRecordingUpdate(prev, routeId, recording));
}
function handleSave() {

View File

@@ -1,6 +1,6 @@
import { useState, useMemo, useCallback } from 'react';
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 {
StatCard, StatusDot, Badge, MonoText,
GroupCard, DataTable, EventFeed,
@@ -13,31 +13,11 @@ import { useApplicationLogs } from '../../api/queries/logs';
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
import type { ConfigUpdateResponse } from '../../api/queries/commands';
import type { AgentInstance } from '../../api/types';
import { timeAgo } from '../../utils/format-utils';
import { formatUptime, mapLogLevel, eventSeverity, eventIcon } from '../../utils/agent-utils';
// ── 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 {
if (rate == null) return '\u2014';
return `${(rate * 100).toFixed(1)}%`;
@@ -104,16 +84,6 @@ const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [
{ 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 ─────────────────────────────────────────────────────────
export default function AgentHealth() {
@@ -195,35 +165,6 @@ export default function AgentHealth() {
// Map events to FeedEvent
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 }) => ({
id: String(e.id),
severity: eventSeverity(e.eventType),

View File

@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react';
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 {
StatCard, StatusDot, Badge, LineChart, AreaChart, BarChart,
EventFeed, Spinner, EmptyState, SectionHeader, MonoText,
@@ -12,6 +12,7 @@ import { useAgents, useAgentEvents } from '../../api/queries/agents';
import { useApplicationLogs } from '../../api/queries/logs';
import { useStatsTimeseries } from '../../api/queries/executions';
import { useAgentMetrics } from '../../api/queries/agent-metrics';
import { formatUptime, mapLogLevel, eventSeverity, eventIcon } from '../../utils/agent-utils';
const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [
{ value: 'error', label: 'Error', color: 'var(--error)' },
@@ -21,16 +22,6 @@ const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [
{ 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() {
const { appId, instanceId } = useParams();
const { timeRange } = useGlobalFilters();
@@ -82,35 +73,6 @@ export default function AgentInstance() {
);
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 || [])
.filter((e: any) => !instanceId || e.instanceId === instanceId)
.map((e: any) => ({
@@ -495,13 +457,3 @@ export default function AgentInstance() {
</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`;
}

View File

@@ -36,6 +36,8 @@ import type { ApplicationConfig, TapDefinition } from '../../api/queries/command
import { useCatalog } from '../../api/queries/catalog';
import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog';
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';
function formatBytes(bytes: number): string {
@@ -44,16 +46,6 @@ function formatBytes(bytes: number): string {
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'> = {
RUNNING: 'running', STARTING: 'warning', FAILED: 'error', STOPPED: 'auto',
DEGRADED: 'warning', STOPPING: 'auto',
@@ -801,14 +793,11 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
}
function updateTracedProcessor(processorId: string, mode: string) {
setTracedDraft((prev) => {
if (mode === 'REMOVE') { const next = { ...prev }; delete next[processorId]; return next; }
return { ...prev, [processorId]: mode };
});
setTracedDraft((prev) => applyTracedProcessorUpdate(prev, processorId, mode));
}
function updateRouteRecording(routeId: string, recording: boolean) {
setRouteRecordingDraft((prev) => ({ ...prev, [routeId]: recording }));
setRouteRecordingDraft((prev) => applyRouteRecordingUpdate(prev, routeId, recording));
}
async function handleSave() {

View File

@@ -14,6 +14,7 @@ import {
} from '../../api/queries/executions'
import type { ExecutionSummary } from '../../api/types'
import { attributeBadgeColor } from '../../utils/attribute-color'
import { formatDuration, statusLabel } from '../../utils/format-utils'
import styles from './Dashboard.module.css'
// Row type extends ExecutionSummary with an `id` field for DataTable
@@ -23,12 +24,6 @@ interface Row extends ExecutionSummary {
// ─── 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 {
const date = new Date(iso)
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 {
if (status === 'FAILED') return styles.durBreach
if (ms < 100) return styles.durFast

View File

@@ -8,6 +8,7 @@ import { useCatalog } from '../../api/queries/catalog';
import { useCanControl } from '../../auth/auth-store';
import type { ExecutionDetail } from '../../components/ExecutionDiagram/types';
import { attributeBadgeColor } from '../../utils/attribute-color';
import { formatDuration, statusLabel } from '../../utils/format-utils';
import { RouteControlBar } from './RouteControlBar';
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) {
const navigate = useNavigate();
const { timeRange } = useGlobalFilters();

View 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 });
}
}

View 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 };
}

View 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`;
}