@motiadev/workbench 0.8.2-beta.139 → 0.8.2-beta.140-246724

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,5 +6,5 @@ import { memo } from 'react';
6
6
  import { EventIcon } from '../events/event-icon';
7
7
  import { TraceEvent } from '../events/trace-event';
8
8
  export const TraceItemDetail = memo(({ trace, onClose }) => {
9
- return (_jsxs(Sidebar, { onClose: onClose, title: "Trace Details", subtitle: `Viewing details from step ${trace.name}`, actions: [{ icon: _jsx(X, {}), onClick: onClose, label: 'Close' }], children: [_jsxs("div", { className: "px-2 w-[800px] overflow-auto", children: [_jsxs("div", { className: "flex items-center gap-4 text-sm text-muted-foreground mb-4", children: [trace.endTime && _jsxs("span", { children: ["Duration: ", formatDuration(trace.endTime - trace.startTime)] }), _jsx("div", { className: "bg-blue-500 font-bold text-xs px-[4px] py-[2px] rounded-sm text-blue-100", children: trace.entryPoint.type }), trace.correlationId && _jsxs(Badge, { variant: "outline", children: ["Correlated: ", trace.correlationId] })] }), _jsx("div", { className: "pl-6 border-l-1 border-gray-500/40 font-mono text-xs flex flex-col gap-3", children: trace.events.map((event, index) => (_jsxs("div", { className: "relative", children: [_jsx("div", { className: "absolute -left-[26px] top-[8px] w-1 h-1 rounded-full bg-emerald-500 outline outline-2 outline-emerald-500/50" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(EventIcon, { event: event }), _jsxs("span", { className: "text-sm font-mono text-muted-foreground", children: ["+", Math.floor(event.timestamp - trace.startTime), "ms"] }), _jsx(TraceEvent, { event: event })] })] }, index))) })] }), trace.error && (_jsxs("div", { className: "p-4 bg-red-800/10", children: [_jsx("div", { className: "text-sm text-red-800 dark:text-red-400 font-semibold", children: trace.error.message }), _jsx("div", { className: "text-sm text-red-800 dark:text-red-400 pl-4", children: trace.error.stack })] }))] }));
9
+ return (_jsxs(Sidebar, { onClose: onClose, title: "Trace Details", subtitle: `Viewing details from step ${trace.name}`, actions: [{ icon: _jsx(X, {}), onClick: onClose, label: 'Close' }], children: [_jsxs("div", { className: "px-2 w-[800px] overflow-auto", children: [_jsxs("div", { className: "flex items-center gap-4 text-sm text-muted-foreground mb-4", children: [trace.endTime && _jsxs("span", { children: ["Duration: ", formatDuration(trace.endTime - trace.startTime)] }), _jsx("div", { className: "bg-blue-500 font-bold text-xs px-[4px] py-[2px] rounded-sm text-blue-100", children: trace.entryPoint.type }), trace.correlationId && _jsxs(Badge, { variant: "outline", children: ["Correlated: ", trace.correlationId] })] }), _jsx("div", { className: "pl-6 border-l-1 border-gray-500/40 font-mono text-xs flex flex-col gap-3", children: trace.events.map((event, index) => (_jsxs("div", { className: "relative", children: [_jsx("div", { className: "absolute -left-[26px] top-[8px] w-1 h-1 rounded-full bg-emerald-500 outline outline-2 outline-emerald-500/50" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(EventIcon, { event: event }), _jsxs("span", { className: "text-sm font-mono text-muted-foreground", children: ["+", formatDuration(Math.floor(event.timestamp - trace.startTime))] }), _jsx(TraceEvent, { event: event })] })] }, index))) })] }), trace.error && (_jsxs("div", { className: "p-4 bg-red-800/10", children: [_jsx("div", { className: "text-sm text-red-800 dark:text-red-400 font-semibold", children: trace.error.message }), _jsx("div", { className: "text-sm text-red-800 dark:text-red-400 pl-4", children: trace.error.stack })] }))] }));
10
10
  });
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useGlobalStore } from '@/stores/use-global-store';
3
+ import { formatDuration } from '@/lib/utils';
3
4
  import { useStreamGroup, useStreamItem } from '@motiadev/stream-client-react';
4
5
  import { Button } from '@motiadev/ui';
5
6
  import { Minus, Plus } from 'lucide-react';
@@ -25,5 +26,5 @@ export const TraceTimeline = memo(({ groupId }) => {
25
26
  };
26
27
  if (!group)
27
28
  return null;
28
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex flex-col flex-1 overflow-x-auto h-full relative", children: _jsxs("div", { className: "flex flex-col items-center min-w-full sticky top-0", style: { width: `${zoom * 1000}px` }, children: [_jsxs("div", { className: "flex flex-1 w-full sticky top-0 bg-background z-10", children: [_jsxs("div", { className: "w-full min-h-[37px] h-[37px] min-w-[200px] max-w-[200px] flex items-center justify-center gap-2 sticky left-0 top-0 bg-card backdrop-blur-[4px] backdrop-filter", children: [_jsx(Button, { variant: "icon", size: "sm", className: "px-2", onClick: zoomMinus, children: _jsx(Minus, { className: "w-4 h-4 cursor-pointer" }) }), _jsxs("span", { className: "text-sm font-bold text-muted-foreground", children: [Math.floor(zoom * 100), "%"] }), _jsx(Button, { variant: "icon", size: "sm", className: "px-2", onClick: () => setZoom(zoom + 0.1), children: _jsx(Plus, { className: "w-4 h-4 cursor-pointer" }) })] }), _jsxs("div", { className: "flex justify-between font-mono p-2 w-full text-xs text-muted-foreground bg-card", children: [_jsx("span", { children: "0ms" }), _jsxs("span", { children: [Math.floor((endTime - group.startTime) * 0.25), "ms"] }), _jsxs("span", { children: [Math.floor((endTime - group.startTime) * 0.5), "ms"] }), _jsxs("span", { children: [Math.floor((endTime - group.startTime) * 0.75), "ms"] }), _jsxs("span", { children: [Math.floor(endTime - group.startTime), "ms"] }), _jsxs("div", { className: "absolute bottom-[-4px] w-full flex justify-between", children: [_jsx("span", { className: "w-[1px] h-full bg-blue-500" }), _jsx("span", { className: "w-[1px] h-full bg-blue-500" }), _jsx("span", { className: "w-[1px] h-full bg-blue-500" }), _jsx("span", { className: "w-[1px] h-full bg-blue-500" }), _jsx("span", { className: "w-[1px] h-full bg-blue-500" })] })] })] }), _jsx("div", { className: "flex flex-col w-full h-full", children: data?.map((trace) => (_jsx(TraceItem, { trace: trace, group: group, groupEndTime: endTime, onExpand: selectTraceId }, trace.id))) })] }) }), selectedTrace && _jsx(TraceItemDetail, { trace: selectedTrace, onClose: () => selectTraceId(undefined) })] }));
29
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex flex-col flex-1 overflow-x-auto h-full relative", children: _jsxs("div", { className: "flex flex-col items-center min-w-full sticky top-0", style: { width: `${zoom * 1000}px` }, children: [_jsxs("div", { className: "flex flex-1 w-full sticky top-0 bg-background z-10", children: [_jsxs("div", { className: "w-full min-h-[37px] h-[37px] min-w-[200px] max-w-[200px] flex items-center justify-center gap-2 sticky left-0 top-0 bg-card backdrop-blur-[4px] backdrop-filter", children: [_jsx(Button, { variant: "icon", size: "sm", className: "px-2", onClick: zoomMinus, children: _jsx(Minus, { className: "w-4 h-4 cursor-pointer" }) }), _jsxs("span", { className: "text-sm font-bold text-muted-foreground", children: [Math.floor(zoom * 100), "%"] }), _jsx(Button, { variant: "icon", size: "sm", className: "px-2", onClick: () => setZoom(zoom + 0.1), children: _jsx(Plus, { className: "w-4 h-4 cursor-pointer" }) })] }), _jsxs("div", { className: "flex justify-between font-mono p-2 w-full text-xs text-muted-foreground bg-card", children: [_jsx("span", { children: formatDuration(0) }), _jsx("span", { children: formatDuration(Math.floor((endTime - group.startTime) * 0.25)) }), _jsx("span", { children: formatDuration(Math.floor((endTime - group.startTime) * 0.5)) }), _jsx("span", { children: formatDuration(Math.floor((endTime - group.startTime) * 0.75)) }), _jsx("span", { children: formatDuration(Math.floor(endTime - group.startTime)) }), _jsxs("div", { className: "absolute bottom-[-4px] w-full flex justify-between", children: [_jsx("span", { className: "w-[1px] h-full bg-blue-500" }), _jsx("span", { className: "w-[1px] h-full bg-blue-500" }), _jsx("span", { className: "w-[1px] h-full bg-blue-500" }), _jsx("span", { className: "w-[1px] h-full bg-blue-500" }), _jsx("span", { className: "w-[1px] h-full bg-blue-500" })] })] })] }), _jsx("div", { className: "flex flex-col w-full h-full", children: data?.map((trace) => (_jsx(TraceItem, { trace: trace, group: group, groupEndTime: endTime, onExpand: selectTraceId }, trace.id))) })] }) }), selectedTrace && _jsx(TraceItemDetail, { trace: selectedTrace, onClose: () => selectTraceId(undefined) })] }));
29
30
  });
@@ -1,15 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { formatDuration } from '@/lib/utils';
2
3
  import { cn } from '@motiadev/ui';
3
4
  import { formatDistanceToNow } from 'date-fns';
4
5
  import { memo } from 'react';
5
6
  import { TraceStatusBadge } from './trace-status';
6
7
  export const TracesGroups = memo(({ groups, selectedGroupId, onGroupSelect }) => {
7
- const formatDuration = (duration) => {
8
- if (!duration)
9
- return 'N/A';
10
- if (duration < 1000)
11
- return `${duration}ms`;
12
- return `${(duration / 1000).toFixed(1)}s`;
13
- };
14
8
  return (_jsx("div", { className: "overflow-auto", children: groups.length > 0 && (_jsx("div", { children: [...groups].reverse().map((group) => (_jsx("div", { "data-testid": `trace-${group.id}`, className: cn('motia-trace-group cursor-pointer transition-colors', selectedGroupId === group.id ? 'bg-muted-foreground/10' : 'hover:bg-muted/70'), onClick: () => onGroupSelect(group), children: _jsxs("div", { className: "p-3 flex flex-col gap-1", children: [_jsxs("div", { className: "flex flex-row justify-between items-center gap-2", children: [_jsx("span", { className: "font-semibold text-lg", children: group.name }), _jsx(TraceStatusBadge, { status: group.status, duration: group.endTime ? formatDuration(group.endTime - group.startTime) : undefined })] }), _jsxs("div", { className: "text-xs text-muted-foreground space-y-1", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("div", { "data-testid": "trace-id", className: "text-xs text-muted-foreground font-mono tracking-[1px]", children: group.id }), _jsxs("span", { children: [group.metadata.totalSteps, " steps"] })] }), _jsxs("div", { className: "flex justify-between", children: [formatDistanceToNow(group.startTime), " ago"] }), group.metadata.activeSteps > 0 && (_jsxs("div", { className: "text-blue-600", children: [group.metadata.activeSteps, " active"] }))] })] }) }, group.id))) })) }));
15
9
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
1
+ import { formatDuration } from '../utils';
2
+ describe('formatDuration', () => {
3
+ describe('milliseconds', () => {
4
+ it('should format values under 1 second as milliseconds', () => {
5
+ expect(formatDuration(0)).toBe('0ms');
6
+ expect(formatDuration(1)).toBe('1ms');
7
+ expect(formatDuration(250)).toBe('250ms');
8
+ expect(formatDuration(500)).toBe('500ms');
9
+ expect(formatDuration(999)).toBe('999ms');
10
+ });
11
+ });
12
+ describe('seconds', () => {
13
+ it('should format values between 1 second and 1 minute as seconds with 1 decimal', () => {
14
+ expect(formatDuration(1000)).toBe('1.0s');
15
+ expect(formatDuration(1500)).toBe('1.5s');
16
+ expect(formatDuration(15000)).toBe('15.0s');
17
+ expect(formatDuration(45500)).toBe('45.5s');
18
+ expect(formatDuration(59999)).toBe('60.0s');
19
+ });
20
+ it('should round to 1 decimal place', () => {
21
+ expect(formatDuration(1234)).toBe('1.2s');
22
+ expect(formatDuration(1567)).toBe('1.6s');
23
+ expect(formatDuration(12345)).toBe('12.3s');
24
+ });
25
+ });
26
+ describe('minutes', () => {
27
+ it('should format values between 1 minute and 1 hour as minutes with 1 decimal', () => {
28
+ expect(formatDuration(60000)).toBe('1.0min');
29
+ expect(formatDuration(90000)).toBe('1.5min');
30
+ expect(formatDuration(300000)).toBe('5.0min');
31
+ expect(formatDuration(1800000)).toBe('30.0min');
32
+ expect(formatDuration(3599999)).toBe('60.0min');
33
+ });
34
+ it('should round to 1 decimal place', () => {
35
+ expect(formatDuration(123456)).toBe('2.1min');
36
+ expect(formatDuration(567890)).toBe('9.5min');
37
+ });
38
+ });
39
+ describe('hours', () => {
40
+ it('should format values 1 hour or more as hours with 1 decimal', () => {
41
+ expect(formatDuration(3600000)).toBe('1.0h');
42
+ expect(formatDuration(5400000)).toBe('1.5h');
43
+ expect(formatDuration(7200000)).toBe('2.0h');
44
+ expect(formatDuration(36000000)).toBe('10.0h');
45
+ });
46
+ it('should round to 1 decimal place', () => {
47
+ expect(formatDuration(3661000)).toBe('1.0h');
48
+ expect(formatDuration(5432100)).toBe('1.5h');
49
+ expect(formatDuration(9000000)).toBe('2.5h');
50
+ });
51
+ });
52
+ describe('edge cases', () => {
53
+ it('should return "N/A" for undefined', () => {
54
+ expect(formatDuration(undefined)).toBe('N/A');
55
+ });
56
+ it('should return "N/A" for null', () => {
57
+ expect(formatDuration(null)).toBe('N/A');
58
+ });
59
+ it('should return "N/A" for 0 when treated as falsy', () => {
60
+ expect(formatDuration(0)).toBe('0ms');
61
+ });
62
+ });
63
+ describe('boundary values', () => {
64
+ it('should correctly handle millisecond-second boundary (999ms vs 1.0s)', () => {
65
+ expect(formatDuration(999)).toBe('999ms');
66
+ expect(formatDuration(1000)).toBe('1.0s');
67
+ });
68
+ it('should correctly handle second-minute boundary (59.9s vs 1.0min)', () => {
69
+ expect(formatDuration(59999)).toBe('60.0s');
70
+ expect(formatDuration(60000)).toBe('1.0min');
71
+ });
72
+ it('should correctly handle minute-hour boundary (59.9min vs 1.0h)', () => {
73
+ expect(formatDuration(3599999)).toBe('60.0min');
74
+ expect(formatDuration(3600000)).toBe('1.0h');
75
+ });
76
+ });
77
+ describe('real-world scenarios', () => {
78
+ it('should format typical API response times', () => {
79
+ expect(formatDuration(50)).toBe('50ms');
80
+ expect(formatDuration(150)).toBe('150ms');
81
+ expect(formatDuration(2500)).toBe('2.5s');
82
+ });
83
+ it('should format typical workflow execution times', () => {
84
+ expect(formatDuration(5000)).toBe('5.0s');
85
+ expect(formatDuration(30000)).toBe('30.0s');
86
+ expect(formatDuration(120000)).toBe('2.0min');
87
+ });
88
+ it('should format long-running tasks', () => {
89
+ expect(formatDuration(600000)).toBe('10.0min');
90
+ expect(formatDuration(1800000)).toBe('30.0min');
91
+ expect(formatDuration(7200000)).toBe('2.0h');
92
+ });
93
+ });
94
+ });
@@ -1,9 +1,13 @@
1
1
  export const formatDuration = (duration) => {
2
- if (!duration)
2
+ if (duration === undefined || duration === null)
3
3
  return 'N/A';
4
4
  if (duration < 1000)
5
5
  return `${duration}ms`;
6
- return `${(duration / 1000).toFixed(1)}s`;
6
+ if (duration < 60000)
7
+ return `${(duration / 1000).toFixed(1)}s`;
8
+ if (duration < 3600000)
9
+ return `${(duration / 60000).toFixed(1)}min`;
10
+ return `${(duration / 3600000).toFixed(1)}h`;
7
11
  };
8
12
  export const formatTimestamp = (time) => {
9
13
  const date = new Date(Number(time));