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:
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user