@startsimpli/funnels 0.1.4 → 0.1.5
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.
- package/package.json +9 -31
- package/src/api/README.md +507 -0
- package/src/api/adapter.ts +106 -0
- package/src/api/client.test.ts +640 -0
- package/src/api/client.ts +385 -0
- package/src/api/default-adapter.ts +243 -0
- package/src/api/index.ts +24 -0
- package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
- package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
- package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
- package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
- package/src/components/FilterRuleEditor/README.md +291 -0
- package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
- package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
- package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
- package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
- package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
- package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
- package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
- package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
- package/src/components/FilterRuleEditor/constants.ts +64 -0
- package/src/components/FilterRuleEditor/index.ts +14 -0
- package/src/components/FunnelCard/DESIGN.md +447 -0
- package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
- package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
- package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
- package/src/components/FunnelCard/FunnelCard.tsx +204 -0
- package/src/components/FunnelCard/FunnelStats.tsx +68 -0
- package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
- package/src/components/FunnelCard/INSTALLATION.md +304 -0
- package/src/components/FunnelCard/MatchBar.tsx +49 -0
- package/src/components/FunnelCard/README.md +294 -0
- package/src/components/FunnelCard/StageIndicator.tsx +62 -0
- package/src/components/FunnelCard/StatusBadge.tsx +52 -0
- package/src/components/FunnelCard/index.ts +14 -0
- package/src/components/FunnelPreview/EntityCard.tsx +72 -0
- package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
- package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
- package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
- package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
- package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
- package/src/components/FunnelPreview/README.md +337 -0
- package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
- package/src/components/FunnelPreview/example.tsx +286 -0
- package/src/components/FunnelPreview/index.ts +14 -0
- package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
- package/src/components/FunnelRunHistory/README.md +325 -0
- package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
- package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
- package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
- package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
- package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
- package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
- package/src/components/FunnelRunHistory/index.ts +51 -0
- package/src/components/FunnelRunHistory/types.ts +40 -0
- package/src/components/FunnelRunHistory/utils.test.ts +126 -0
- package/src/components/FunnelRunHistory/utils.ts +100 -0
- package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
- package/src/components/FunnelStageBuilder/README.md +341 -0
- package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
- package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
- package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
- package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
- package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
- package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
- package/src/components/FunnelStageBuilder/index.ts +21 -0
- package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
- package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
- package/src/components/FunnelVisualFlow/README.md +323 -0
- package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
- package/src/components/FunnelVisualFlow/example.tsx +227 -0
- package/src/components/FunnelVisualFlow/index.ts +10 -0
- package/src/components/index.ts +102 -0
- package/src/core/README.md +307 -0
- package/src/core/engine.test.ts +1087 -0
- package/src/core/engine.ts +329 -0
- package/src/core/evaluator.example.ts +353 -0
- package/src/core/evaluator.test.ts +639 -0
- package/src/core/evaluator.ts +261 -0
- package/src/core/field-resolver.example.ts +175 -0
- package/src/core/field-resolver.test.ts +541 -0
- package/src/core/field-resolver.ts +247 -0
- package/src/core/index.ts +34 -0
- package/src/core/operators.test.ts +539 -0
- package/src/core/operators.ts +241 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useDebouncedValue.ts +28 -0
- package/src/index.ts +155 -0
- package/src/store/README.md +342 -0
- package/src/store/create-funnel-store.test.ts +686 -0
- package/src/store/create-funnel-store.ts +538 -0
- package/src/store/index.ts +9 -0
- package/src/store/types.ts +294 -0
- package/src/stories/CrossDomain.stories.tsx +149 -0
- package/src/stories/Welcome.stories.tsx +81 -0
- package/src/stories/demo-data/index.ts +3 -0
- package/src/stories/demo-data/investors.ts +216 -0
- package/src/stories/demo-data/leads.ts +223 -0
- package/src/stories/demo-data/recipes.ts +217 -0
- package/src/test/setup.ts +5 -0
- package/src/types/index.ts +843 -0
- package/dist/client-3ESO2NHy.d.ts +0 -310
- package/dist/client-CZu03ACp.d.cts +0 -310
- package/dist/components/index.cjs +0 -3241
- package/dist/components/index.cjs.map +0 -1
- package/dist/components/index.css.map +0 -1
- package/dist/components/index.d.cts +0 -726
- package/dist/components/index.d.ts +0 -726
- package/dist/components/index.js +0 -3194
- package/dist/components/index.js.map +0 -1
- package/dist/core/index.cjs +0 -500
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -359
- package/dist/core/index.d.ts +0 -359
- package/dist/core/index.js +0 -486
- package/dist/core/index.js.map +0 -1
- package/dist/hooks/index.cjs +0 -20
- package/dist/hooks/index.cjs.map +0 -1
- package/dist/hooks/index.d.cts +0 -11
- package/dist/hooks/index.d.ts +0 -11
- package/dist/hooks/index.js +0 -18
- package/dist/hooks/index.js.map +0 -1
- package/dist/index-BGDEXbuz.d.cts +0 -434
- package/dist/index-BGDEXbuz.d.ts +0 -434
- package/dist/index.cjs +0 -4499
- package/dist/index.cjs.map +0 -1
- package/dist/index.css +0 -198
- package/dist/index.css.map +0 -1
- package/dist/index.d.cts +0 -99
- package/dist/index.d.ts +0 -99
- package/dist/index.js +0 -4421
- package/dist/index.js.map +0 -1
- package/dist/store/index.cjs +0 -389
- package/dist/store/index.cjs.map +0 -1
- package/dist/store/index.d.cts +0 -225
- package/dist/store/index.d.ts +0 -225
- package/dist/store/index.js +0 -386
- package/dist/store/index.js.map +0 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RunFilters Component
|
|
3
|
+
*
|
|
4
|
+
* Filter controls for funnel run history.
|
|
5
|
+
*
|
|
6
|
+
* Design Rationale:
|
|
7
|
+
* - Inline filter controls for immediate feedback
|
|
8
|
+
* - Dropdown selects for constrained options
|
|
9
|
+
* - Date range picker for flexible filtering
|
|
10
|
+
* - Clear visual separation from table
|
|
11
|
+
*
|
|
12
|
+
* Interaction Notes:
|
|
13
|
+
* - Filters apply immediately on change (no "Apply" button)
|
|
14
|
+
* - Default to "Last 30 days" for performance
|
|
15
|
+
* - Clear button resets all filters
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { FunnelRunStatus, TriggerType } from '../../types';
|
|
19
|
+
import { RunFilters as RunFiltersType } from './types';
|
|
20
|
+
|
|
21
|
+
interface RunFiltersProps {
|
|
22
|
+
filters: RunFiltersType;
|
|
23
|
+
onFiltersChange: (filters: RunFiltersType) => void;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function RunFilters({
|
|
28
|
+
filters,
|
|
29
|
+
onFiltersChange,
|
|
30
|
+
className = '',
|
|
31
|
+
}: RunFiltersProps) {
|
|
32
|
+
const updateFilter = <K extends keyof RunFiltersType>(
|
|
33
|
+
key: K,
|
|
34
|
+
value: RunFiltersType[K]
|
|
35
|
+
) => {
|
|
36
|
+
onFiltersChange({ ...filters, [key]: value });
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const clearFilters = () => {
|
|
40
|
+
onFiltersChange({
|
|
41
|
+
status: 'all',
|
|
42
|
+
trigger_type: 'all',
|
|
43
|
+
date_range: 'month',
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const hasActiveFilters =
|
|
48
|
+
filters.status !== 'all' ||
|
|
49
|
+
filters.trigger_type !== 'all' ||
|
|
50
|
+
filters.date_range !== 'month';
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
className={`flex items-center gap-3 p-3 bg-gray-50 border-b border-gray-200 ${className}`}
|
|
55
|
+
>
|
|
56
|
+
{/* Status Filter */}
|
|
57
|
+
<div className="flex items-center gap-2">
|
|
58
|
+
<label htmlFor="status-filter" className="text-sm font-medium text-gray-700">
|
|
59
|
+
Status:
|
|
60
|
+
</label>
|
|
61
|
+
<select
|
|
62
|
+
id="status-filter"
|
|
63
|
+
value={filters.status || 'all'}
|
|
64
|
+
onChange={(e) =>
|
|
65
|
+
updateFilter('status', e.target.value as FunnelRunStatus | 'all')
|
|
66
|
+
}
|
|
67
|
+
className="px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
68
|
+
>
|
|
69
|
+
<option value="all">All</option>
|
|
70
|
+
<option value="completed">Complete</option>
|
|
71
|
+
<option value="running">Running</option>
|
|
72
|
+
<option value="failed">Failed</option>
|
|
73
|
+
<option value="pending">Pending</option>
|
|
74
|
+
<option value="cancelled">Cancelled</option>
|
|
75
|
+
</select>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Trigger Type Filter */}
|
|
79
|
+
<div className="flex items-center gap-2">
|
|
80
|
+
<label htmlFor="trigger-filter" className="text-sm font-medium text-gray-700">
|
|
81
|
+
Trigger:
|
|
82
|
+
</label>
|
|
83
|
+
<select
|
|
84
|
+
id="trigger-filter"
|
|
85
|
+
value={filters.trigger_type || 'all'}
|
|
86
|
+
onChange={(e) =>
|
|
87
|
+
updateFilter('trigger_type', e.target.value as TriggerType | 'all')
|
|
88
|
+
}
|
|
89
|
+
className="px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
90
|
+
>
|
|
91
|
+
<option value="all">All</option>
|
|
92
|
+
<option value="manual">Manual</option>
|
|
93
|
+
<option value="scheduled">Scheduled</option>
|
|
94
|
+
<option value="webhook">Webhook</option>
|
|
95
|
+
<option value="api">API</option>
|
|
96
|
+
</select>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Date Range Filter */}
|
|
100
|
+
<div className="flex items-center gap-2">
|
|
101
|
+
<label htmlFor="date-filter" className="text-sm font-medium text-gray-700">
|
|
102
|
+
Date:
|
|
103
|
+
</label>
|
|
104
|
+
<select
|
|
105
|
+
id="date-filter"
|
|
106
|
+
value={filters.date_range || 'month'}
|
|
107
|
+
onChange={(e) => updateFilter('date_range', e.target.value as any)}
|
|
108
|
+
className="px-2 py-1 text-sm border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
109
|
+
>
|
|
110
|
+
<option value="all">All time</option>
|
|
111
|
+
<option value="today">Today</option>
|
|
112
|
+
<option value="week">Last 7 days</option>
|
|
113
|
+
<option value="month">Last 30 days</option>
|
|
114
|
+
</select>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Clear Button */}
|
|
118
|
+
{hasActiveFilters && (
|
|
119
|
+
<button
|
|
120
|
+
onClick={clearFilters}
|
|
121
|
+
className="ml-auto px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
122
|
+
>
|
|
123
|
+
Clear filters
|
|
124
|
+
</button>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RunRow Component
|
|
3
|
+
*
|
|
4
|
+
* Individual table row for a funnel run.
|
|
5
|
+
*
|
|
6
|
+
* Design Rationale:
|
|
7
|
+
* - Clickable row opens details modal
|
|
8
|
+
* - Hover state provides visual feedback
|
|
9
|
+
* - Action button visible on hover for cleaner UI
|
|
10
|
+
* - Tooltip on date shows full timestamp
|
|
11
|
+
*
|
|
12
|
+
* Interaction Notes:
|
|
13
|
+
* - Click anywhere on row to view details
|
|
14
|
+
* - Action dropdown prevents row click propagation
|
|
15
|
+
* - Keyboard accessible (Enter to open details)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { FunnelRun } from '../../types';
|
|
19
|
+
import { RunStatusBadge } from './RunStatusBadge';
|
|
20
|
+
import { RunActions } from './RunActions';
|
|
21
|
+
import {
|
|
22
|
+
formatDuration,
|
|
23
|
+
formatRelativeTime,
|
|
24
|
+
formatFullTimestamp,
|
|
25
|
+
calculateMatchRate,
|
|
26
|
+
formatNumber,
|
|
27
|
+
} from './utils';
|
|
28
|
+
|
|
29
|
+
interface RunRowProps {
|
|
30
|
+
run: FunnelRun;
|
|
31
|
+
onViewDetails: (run: FunnelRun) => void;
|
|
32
|
+
onViewResults: (run: FunnelRun) => void;
|
|
33
|
+
onReRun: (run: FunnelRun) => void;
|
|
34
|
+
onCancel?: (run: FunnelRun) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function RunRow({
|
|
38
|
+
run,
|
|
39
|
+
onViewDetails,
|
|
40
|
+
onViewResults,
|
|
41
|
+
onReRun,
|
|
42
|
+
onCancel,
|
|
43
|
+
}: RunRowProps) {
|
|
44
|
+
const matchRate =
|
|
45
|
+
run.status === 'completed'
|
|
46
|
+
? calculateMatchRate(run.total_matched, run.total_input)
|
|
47
|
+
: null;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<tr
|
|
51
|
+
onClick={() => onViewDetails(run)}
|
|
52
|
+
onKeyDown={(e) => {
|
|
53
|
+
if (e.key === 'Enter') {
|
|
54
|
+
onViewDetails(run);
|
|
55
|
+
}
|
|
56
|
+
}}
|
|
57
|
+
tabIndex={0}
|
|
58
|
+
className="border-b border-gray-200 hover:bg-gray-50 cursor-pointer transition-colors focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
|
|
59
|
+
>
|
|
60
|
+
{/* Date */}
|
|
61
|
+
<td className="px-4 py-3">
|
|
62
|
+
<span
|
|
63
|
+
className="text-sm text-gray-900"
|
|
64
|
+
title={formatFullTimestamp(run.started_at)}
|
|
65
|
+
>
|
|
66
|
+
{formatRelativeTime(run.started_at)}
|
|
67
|
+
</span>
|
|
68
|
+
</td>
|
|
69
|
+
|
|
70
|
+
{/* Status */}
|
|
71
|
+
<td className="px-4 py-3">
|
|
72
|
+
<RunStatusBadge status={run.status} />
|
|
73
|
+
</td>
|
|
74
|
+
|
|
75
|
+
{/* Trigger */}
|
|
76
|
+
<td className="px-4 py-3">
|
|
77
|
+
<span className="text-sm text-gray-700 capitalize">
|
|
78
|
+
{run.trigger_type}
|
|
79
|
+
</span>
|
|
80
|
+
</td>
|
|
81
|
+
|
|
82
|
+
{/* Duration */}
|
|
83
|
+
<td className="px-4 py-3">
|
|
84
|
+
<span className="text-sm text-gray-900">
|
|
85
|
+
{formatDuration(run.duration_ms)}
|
|
86
|
+
</span>
|
|
87
|
+
</td>
|
|
88
|
+
|
|
89
|
+
{/* Input */}
|
|
90
|
+
<td className="px-4 py-3 text-right">
|
|
91
|
+
<span className="text-sm font-medium text-gray-900">
|
|
92
|
+
{formatNumber(run.total_input)}
|
|
93
|
+
</span>
|
|
94
|
+
</td>
|
|
95
|
+
|
|
96
|
+
{/* Matched */}
|
|
97
|
+
<td className="px-4 py-3 text-right">
|
|
98
|
+
<span className="text-sm font-medium text-green-600">
|
|
99
|
+
{run.status === 'completed' ? formatNumber(run.total_matched) : '-'}
|
|
100
|
+
</span>
|
|
101
|
+
</td>
|
|
102
|
+
|
|
103
|
+
{/* Match Rate % */}
|
|
104
|
+
<td className="px-4 py-3 text-right">
|
|
105
|
+
<span className="text-sm font-medium text-gray-900">
|
|
106
|
+
{matchRate !== null ? `${matchRate}%` : '-'}
|
|
107
|
+
</span>
|
|
108
|
+
</td>
|
|
109
|
+
|
|
110
|
+
{/* Actions */}
|
|
111
|
+
<td className="px-4 py-3" onClick={(e) => e.stopPropagation()}>
|
|
112
|
+
<RunActions
|
|
113
|
+
run={run}
|
|
114
|
+
onViewDetails={onViewDetails}
|
|
115
|
+
onViewResults={onViewResults}
|
|
116
|
+
onReRun={onReRun}
|
|
117
|
+
onCancel={onCancel}
|
|
118
|
+
/>
|
|
119
|
+
</td>
|
|
120
|
+
</tr>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RunStatusBadge Component
|
|
3
|
+
*
|
|
4
|
+
* Displays funnel run status with icon and color coding.
|
|
5
|
+
*
|
|
6
|
+
* Design Rationale:
|
|
7
|
+
* - Icons provide instant visual recognition
|
|
8
|
+
* - Color coding reinforces status meaning
|
|
9
|
+
* - Spinning animation for active states
|
|
10
|
+
* - Accessible with both visual and text indicators
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { FunnelRunStatus } from '../../types';
|
|
14
|
+
|
|
15
|
+
interface RunStatusBadgeProps {
|
|
16
|
+
status: FunnelRunStatus;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface StatusConfig {
|
|
21
|
+
icon: string;
|
|
22
|
+
label: string;
|
|
23
|
+
color: string;
|
|
24
|
+
bgColor: string;
|
|
25
|
+
spinning?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const statusConfig: Record<FunnelRunStatus, StatusConfig> = {
|
|
29
|
+
completed: {
|
|
30
|
+
icon: '✓',
|
|
31
|
+
label: 'Complete',
|
|
32
|
+
color: 'text-green-800',
|
|
33
|
+
bgColor: 'bg-green-100',
|
|
34
|
+
},
|
|
35
|
+
running: {
|
|
36
|
+
icon: '⏸',
|
|
37
|
+
label: 'Running',
|
|
38
|
+
color: 'text-blue-800',
|
|
39
|
+
bgColor: 'bg-blue-100',
|
|
40
|
+
spinning: true,
|
|
41
|
+
},
|
|
42
|
+
failed: {
|
|
43
|
+
icon: '✗',
|
|
44
|
+
label: 'Failed',
|
|
45
|
+
color: 'text-red-800',
|
|
46
|
+
bgColor: 'bg-red-100',
|
|
47
|
+
},
|
|
48
|
+
pending: {
|
|
49
|
+
icon: '○',
|
|
50
|
+
label: 'Pending',
|
|
51
|
+
color: 'text-yellow-800',
|
|
52
|
+
bgColor: 'bg-yellow-100',
|
|
53
|
+
},
|
|
54
|
+
cancelled: {
|
|
55
|
+
icon: '×',
|
|
56
|
+
label: 'Cancelled',
|
|
57
|
+
color: 'text-gray-800',
|
|
58
|
+
bgColor: 'bg-gray-100',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function RunStatusBadge({ status, className = '' }: RunStatusBadgeProps) {
|
|
63
|
+
const config = statusConfig[status];
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<span
|
|
67
|
+
className={`inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium ${config.bgColor} ${config.color} ${className}`}
|
|
68
|
+
>
|
|
69
|
+
<span className={config.spinning ? 'animate-spin' : ''} aria-hidden="true">
|
|
70
|
+
{config.icon}
|
|
71
|
+
</span>
|
|
72
|
+
<span>{config.label}</span>
|
|
73
|
+
</span>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StageBreakdownList Component
|
|
3
|
+
*
|
|
4
|
+
* Displays stage-by-stage execution statistics.
|
|
5
|
+
*
|
|
6
|
+
* Design Rationale:
|
|
7
|
+
* - Sequential numbering shows funnel flow
|
|
8
|
+
* - Input → Matched with delta shows filtering effect
|
|
9
|
+
* - Color coding: green for matched, red for excluded
|
|
10
|
+
* - Arrow symbols reinforce flow direction
|
|
11
|
+
*
|
|
12
|
+
* Visual Hierarchy:
|
|
13
|
+
* - Stage name is prominent
|
|
14
|
+
* - Numbers are large and easy to scan
|
|
15
|
+
* - Delta shows impact at each stage
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { StageStats } from '../../types';
|
|
19
|
+
import { formatNumber } from './utils';
|
|
20
|
+
|
|
21
|
+
interface StageBreakdownListProps {
|
|
22
|
+
stages: StageStats[];
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function StageBreakdownList({
|
|
27
|
+
stages,
|
|
28
|
+
className = '',
|
|
29
|
+
}: StageBreakdownListProps) {
|
|
30
|
+
return (
|
|
31
|
+
<div className={`space-y-3 ${className}`}>
|
|
32
|
+
{stages.map((stage, index) => {
|
|
33
|
+
const delta = stage.matched_count - stage.input_count;
|
|
34
|
+
const matchRate =
|
|
35
|
+
stage.input_count > 0
|
|
36
|
+
? Math.round((stage.matched_count / stage.input_count) * 100)
|
|
37
|
+
: 0;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
key={stage.stage_id}
|
|
42
|
+
className="p-3 bg-gray-50 rounded-lg border border-gray-200"
|
|
43
|
+
>
|
|
44
|
+
{/* Stage Header */}
|
|
45
|
+
<div className="flex items-center gap-2 mb-2">
|
|
46
|
+
<span className="flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-blue-600 rounded-full">
|
|
47
|
+
{index + 1}
|
|
48
|
+
</span>
|
|
49
|
+
<h4 className="text-sm font-semibold text-gray-900">
|
|
50
|
+
{stage.stage_name}
|
|
51
|
+
</h4>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* Stage Stats */}
|
|
55
|
+
<div className="grid grid-cols-3 gap-2 text-center">
|
|
56
|
+
{/* Input */}
|
|
57
|
+
<div>
|
|
58
|
+
<div className="text-xs text-gray-600 mb-1">Input</div>
|
|
59
|
+
<div className="text-lg font-bold text-blue-600">
|
|
60
|
+
{formatNumber(stage.input_count)}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{/* Matched */}
|
|
65
|
+
<div>
|
|
66
|
+
<div className="text-xs text-gray-600 mb-1">Matched</div>
|
|
67
|
+
<div className="text-lg font-bold text-green-600">
|
|
68
|
+
{formatNumber(stage.matched_count)}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{/* Match Rate */}
|
|
73
|
+
<div>
|
|
74
|
+
<div className="text-xs text-gray-600 mb-1">Rate</div>
|
|
75
|
+
<div className="text-lg font-bold text-gray-700">{matchRate}%</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{/* Delta */}
|
|
80
|
+
{delta !== 0 && (
|
|
81
|
+
<div className="mt-2 pt-2 border-t border-gray-200">
|
|
82
|
+
<div className="flex items-center justify-center gap-1 text-sm">
|
|
83
|
+
<span
|
|
84
|
+
className={`font-medium ${
|
|
85
|
+
delta > 0 ? 'text-green-600' : 'text-red-600'
|
|
86
|
+
}`}
|
|
87
|
+
>
|
|
88
|
+
{delta > 0 ? '▲' : '▼'} {formatNumber(Math.abs(delta))}
|
|
89
|
+
</span>
|
|
90
|
+
<span className="text-gray-500">
|
|
91
|
+
{delta > 0 ? 'added' : 'excluded'}
|
|
92
|
+
</span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{/* Error Count */}
|
|
98
|
+
{stage.error_count && stage.error_count > 0 && (
|
|
99
|
+
<div className="mt-2 pt-2 border-t border-gray-200">
|
|
100
|
+
<div className="text-sm text-red-600 text-center">
|
|
101
|
+
⚠ {formatNumber(stage.error_count)} errors
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FunnelRunHistory - Component Suite
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive run history display with filtering, sorting, and detailed views.
|
|
5
|
+
*
|
|
6
|
+
* Primary Export:
|
|
7
|
+
* - FunnelRunHistory: Main component
|
|
8
|
+
*
|
|
9
|
+
* Sub-Components (for custom layouts):
|
|
10
|
+
* - RunStatusBadge: Status display
|
|
11
|
+
* - RunFilters: Filter controls
|
|
12
|
+
* - RunRow: Table row
|
|
13
|
+
* - RunActions: Action dropdown
|
|
14
|
+
* - RunDetailsModal: Details modal
|
|
15
|
+
* - StageBreakdownList: Stage statistics
|
|
16
|
+
*
|
|
17
|
+
* Utilities:
|
|
18
|
+
* - formatDuration: Format ms to human-readable
|
|
19
|
+
* - formatRelativeTime: Format date to relative time
|
|
20
|
+
* - calculateMatchRate: Calculate percentage
|
|
21
|
+
* - formatNumber: Format with commas
|
|
22
|
+
*
|
|
23
|
+
* Types:
|
|
24
|
+
* - RunFilters: Filter state type
|
|
25
|
+
* - RunSort: Sort configuration
|
|
26
|
+
* - Pagination: Pagination state
|
|
27
|
+
* - RunAction: Action types
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export { FunnelRunHistory } from './FunnelRunHistory';
|
|
31
|
+
export { RunStatusBadge } from './RunStatusBadge';
|
|
32
|
+
export { RunFilters } from './RunFilters';
|
|
33
|
+
export { RunRow } from './RunRow';
|
|
34
|
+
export { RunActions } from './RunActions';
|
|
35
|
+
export { RunDetailsModal } from './RunDetailsModal';
|
|
36
|
+
export { StageBreakdownList } from './StageBreakdownList';
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
formatDuration,
|
|
40
|
+
formatRelativeTime,
|
|
41
|
+
calculateMatchRate,
|
|
42
|
+
formatNumber,
|
|
43
|
+
formatFullTimestamp,
|
|
44
|
+
} from './utils';
|
|
45
|
+
|
|
46
|
+
export type {
|
|
47
|
+
RunFilters as RunFiltersType,
|
|
48
|
+
RunSort,
|
|
49
|
+
Pagination,
|
|
50
|
+
RunAction,
|
|
51
|
+
} from './types';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FunnelRunHistory Types
|
|
3
|
+
*
|
|
4
|
+
* Shared types for the run history component suite
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { FunnelRun, FunnelRunStatus, TriggerType } from '../../types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Filter state for run history
|
|
11
|
+
*/
|
|
12
|
+
export interface RunFilters {
|
|
13
|
+
status?: FunnelRunStatus | 'all';
|
|
14
|
+
trigger_type?: TriggerType | 'all';
|
|
15
|
+
date_range?: 'all' | 'today' | 'week' | 'month' | 'custom';
|
|
16
|
+
start_date?: Date;
|
|
17
|
+
end_date?: Date;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sort configuration for run history
|
|
22
|
+
*/
|
|
23
|
+
export interface RunSort {
|
|
24
|
+
field: 'started_at' | 'status' | 'match_rate' | 'duration_ms';
|
|
25
|
+
direction: 'asc' | 'desc';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Pagination state
|
|
30
|
+
*/
|
|
31
|
+
export interface Pagination {
|
|
32
|
+
page: number;
|
|
33
|
+
page_size: number;
|
|
34
|
+
total: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run action types
|
|
39
|
+
*/
|
|
40
|
+
export type RunAction = 'view_details' | 'view_results' | 're_run' | 'cancel';
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utils Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
formatDuration,
|
|
8
|
+
formatRelativeTime,
|
|
9
|
+
calculateMatchRate,
|
|
10
|
+
formatNumber,
|
|
11
|
+
formatFullTimestamp,
|
|
12
|
+
} from './utils';
|
|
13
|
+
|
|
14
|
+
describe('formatDuration', () => {
|
|
15
|
+
it('should format milliseconds', () => {
|
|
16
|
+
expect(formatDuration(500)).toBe('500ms');
|
|
17
|
+
expect(formatDuration(999)).toBe('999ms');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should format seconds', () => {
|
|
21
|
+
expect(formatDuration(1000)).toBe('1s');
|
|
22
|
+
expect(formatDuration(5000)).toBe('5s');
|
|
23
|
+
expect(formatDuration(59000)).toBe('59s');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should format minutes and seconds', () => {
|
|
27
|
+
expect(formatDuration(60000)).toBe('1m');
|
|
28
|
+
expect(formatDuration(65000)).toBe('1m 5s');
|
|
29
|
+
expect(formatDuration(125000)).toBe('2m 5s');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should format hours and minutes', () => {
|
|
33
|
+
expect(formatDuration(3600000)).toBe('1h');
|
|
34
|
+
expect(formatDuration(3660000)).toBe('1h 1m');
|
|
35
|
+
expect(formatDuration(7320000)).toBe('2h 2m');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should return "-" for undefined', () => {
|
|
39
|
+
expect(formatDuration(undefined)).toBe('-');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle zero duration', () => {
|
|
43
|
+
expect(formatDuration(0)).toBe('0ms');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('formatRelativeTime', () => {
|
|
48
|
+
it('should format recent times', () => {
|
|
49
|
+
const now = new Date();
|
|
50
|
+
|
|
51
|
+
// Just now
|
|
52
|
+
const justNow = new Date(now.getTime() - 30000); // 30 seconds ago
|
|
53
|
+
expect(formatRelativeTime(justNow)).toBe('Just now');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should format minutes ago', () => {
|
|
57
|
+
const now = new Date();
|
|
58
|
+
const fiveMinAgo = new Date(now.getTime() - 5 * 60 * 1000);
|
|
59
|
+
expect(formatRelativeTime(fiveMinAgo)).toBe('5m ago');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should format hours ago', () => {
|
|
63
|
+
const now = new Date();
|
|
64
|
+
const twoHoursAgo = new Date(now.getTime() - 2 * 60 * 60 * 1000);
|
|
65
|
+
expect(formatRelativeTime(twoHoursAgo)).toBe('2h ago');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should format days ago', () => {
|
|
69
|
+
const now = new Date();
|
|
70
|
+
const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000);
|
|
71
|
+
expect(formatRelativeTime(threeDaysAgo)).toBe('3d ago');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle string dates', () => {
|
|
75
|
+
const now = new Date();
|
|
76
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
77
|
+
expect(formatRelativeTime(yesterday.toISOString())).toBe('1d ago');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('calculateMatchRate', () => {
|
|
82
|
+
it('should calculate percentage', () => {
|
|
83
|
+
expect(calculateMatchRate(50, 100)).toBe(50);
|
|
84
|
+
expect(calculateMatchRate(1, 3)).toBe(33);
|
|
85
|
+
expect(calculateMatchRate(2, 3)).toBe(67);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle zero total', () => {
|
|
89
|
+
expect(calculateMatchRate(0, 0)).toBe(0);
|
|
90
|
+
expect(calculateMatchRate(5, 0)).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should round to nearest integer', () => {
|
|
94
|
+
expect(calculateMatchRate(1, 3)).toBe(33); // 33.33... rounds to 33
|
|
95
|
+
expect(calculateMatchRate(2, 3)).toBe(67); // 66.66... rounds to 67
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('formatNumber', () => {
|
|
100
|
+
it('should format with commas', () => {
|
|
101
|
+
expect(formatNumber(1000)).toBe('1,000');
|
|
102
|
+
expect(formatNumber(1234567)).toBe('1,234,567');
|
|
103
|
+
expect(formatNumber(999)).toBe('999');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should handle zero', () => {
|
|
107
|
+
expect(formatNumber(0)).toBe('0');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('formatFullTimestamp', () => {
|
|
112
|
+
it('should format date object', () => {
|
|
113
|
+
const date = new Date('2025-01-15T10:30:45Z');
|
|
114
|
+
const formatted = formatFullTimestamp(date);
|
|
115
|
+
// Format will vary by locale, just check it's a string
|
|
116
|
+
expect(typeof formatted).toBe('string');
|
|
117
|
+
expect(formatted.length).toBeGreaterThan(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should format ISO string', () => {
|
|
121
|
+
const isoString = '2025-01-15T10:30:45Z';
|
|
122
|
+
const formatted = formatFullTimestamp(isoString);
|
|
123
|
+
expect(typeof formatted).toBe('string');
|
|
124
|
+
expect(formatted.length).toBeGreaterThan(0);
|
|
125
|
+
});
|
|
126
|
+
});
|