@startsimpli/funnels 0.4.6 → 0.4.7
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 +1 -1
- package/src/api/client.test.ts +76 -76
- package/src/api/client.ts +16 -16
- package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +24 -24
- package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +39 -39
- package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +1 -1
- package/src/components/FilterRuleEditor/RuleRow.tsx +8 -8
- package/src/components/FunnelCard/FunnelCard.stories.tsx +118 -118
- package/src/components/FunnelCard/FunnelCard.test.ts +48 -48
- package/src/components/FunnelCard/FunnelCard.test.tsx +40 -40
- package/src/components/FunnelCard/FunnelCard.tsx +3 -3
- package/src/components/FunnelPreview/FunnelPreview.test.tsx +63 -63
- package/src/components/FunnelPreview/FunnelPreview.tsx +11 -11
- package/src/components/FunnelPreview/StageBreakdown.tsx +5 -5
- package/src/components/FunnelPreview/example.tsx +43 -43
- package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +70 -70
- package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +63 -63
- package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +12 -12
- package/src/components/FunnelRunHistory/RunDetailsModal.tsx +5 -5
- package/src/components/FunnelRunHistory/RunFilters.tsx +8 -8
- package/src/components/FunnelRunHistory/RunRow.tsx +7 -7
- package/src/components/FunnelRunHistory/StageBreakdownList.tsx +9 -9
- package/src/components/FunnelRunHistory/types.ts +6 -6
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +41 -41
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +6 -6
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +5 -5
- package/src/components/FunnelStageBuilder/StageActions.test.tsx +7 -7
- package/src/components/FunnelStageBuilder/StageActions.tsx +4 -4
- package/src/components/FunnelStageBuilder/StageCard.tsx +3 -3
- package/src/components/FunnelStageBuilder/StageForm.tsx +14 -14
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +42 -42
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +31 -31
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +4 -4
- package/src/components/FunnelVisualFlow/StageNode.tsx +8 -8
- package/src/components/FunnelVisualFlow/example.tsx +7 -7
- package/src/core/engine.test.ts +271 -271
- package/src/core/engine.ts +60 -60
- package/src/core/evaluator.example.ts +31 -31
- package/src/core/evaluator.test.ts +92 -92
- package/src/core/evaluator.ts +23 -23
- package/src/core/field-resolver.example.ts +1 -1
- package/src/core/field-resolver.test.ts +4 -4
- package/src/store/create-funnel-store.test.ts +32 -32
- package/src/store/create-funnel-store.ts +3 -3
- package/src/store/types.ts +2 -2
- package/src/types/index.ts +64 -64
|
@@ -26,17 +26,17 @@ const createMockApiClient = (runs: FunnelRun[]): FunnelApiClient => {
|
|
|
26
26
|
// Sample runs
|
|
27
27
|
const completedRun: FunnelRun = {
|
|
28
28
|
id: 'run-1',
|
|
29
|
-
|
|
29
|
+
funnelId: 'funnel-1',
|
|
30
30
|
status: 'completed',
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
triggerType: 'manual',
|
|
32
|
+
startedAt: '2024-02-10T14:30:00Z',
|
|
33
|
+
completedAt: '2024-02-10T14:35:00Z',
|
|
34
|
+
durationMs: 300000,
|
|
35
|
+
totalInput: 1000,
|
|
36
|
+
totalMatched: 235,
|
|
37
|
+
totalExcluded: 765,
|
|
38
|
+
totalTagged: 235,
|
|
39
|
+
stageStats: {
|
|
40
40
|
'stage-1': { input: 1000, matched: 450, excluded: 550 },
|
|
41
41
|
'stage-2': { input: 450, matched: 300, excluded: 150 },
|
|
42
42
|
'stage-3': { input: 300, matched: 235, excluded: 65 },
|
|
@@ -45,46 +45,46 @@ const completedRun: FunnelRun = {
|
|
|
45
45
|
|
|
46
46
|
const runningRun: FunnelRun = {
|
|
47
47
|
id: 'run-2',
|
|
48
|
-
|
|
48
|
+
funnelId: 'funnel-1',
|
|
49
49
|
status: 'running',
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
triggerType: 'scheduled',
|
|
51
|
+
startedAt: new Date().toISOString(),
|
|
52
|
+
totalInput: 1000,
|
|
53
|
+
totalMatched: 0,
|
|
54
|
+
totalExcluded: 0,
|
|
55
|
+
totalTagged: 0,
|
|
56
|
+
stageStats: {},
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
const failedRun: FunnelRun = {
|
|
60
60
|
id: 'run-3',
|
|
61
|
-
|
|
61
|
+
funnelId: 'funnel-1',
|
|
62
62
|
status: 'failed',
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
triggerType: 'manual',
|
|
64
|
+
startedAt: '2024-02-10T12:00:00Z',
|
|
65
|
+
completedAt: '2024-02-10T12:01:30Z',
|
|
66
|
+
durationMs: 90000,
|
|
67
|
+
totalInput: 1000,
|
|
68
|
+
totalMatched: 0,
|
|
69
|
+
totalExcluded: 0,
|
|
70
|
+
totalTagged: 0,
|
|
71
71
|
error: 'Database connection timeout',
|
|
72
|
-
|
|
72
|
+
stageStats: {},
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
const cancelledRun: FunnelRun = {
|
|
76
76
|
id: 'run-4',
|
|
77
|
-
|
|
77
|
+
funnelId: 'funnel-1',
|
|
78
78
|
status: 'cancelled',
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
triggerType: 'api',
|
|
80
|
+
startedAt: '2024-02-10T11:00:00Z',
|
|
81
|
+
completedAt: '2024-02-10T11:02:00Z',
|
|
82
|
+
durationMs: 120000,
|
|
83
|
+
totalInput: 500,
|
|
84
|
+
totalMatched: 50,
|
|
85
|
+
totalExcluded: 450,
|
|
86
|
+
totalTagged: 50,
|
|
87
|
+
stageStats: {},
|
|
88
88
|
};
|
|
89
89
|
|
|
90
90
|
const meta: Meta<typeof FunnelRunHistory> = {
|
|
@@ -113,18 +113,18 @@ export const CompletedRuns: Story = {
|
|
|
113
113
|
{
|
|
114
114
|
...completedRun,
|
|
115
115
|
id: 'run-5',
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
startedAt: '2024-02-09T14:30:00Z',
|
|
117
|
+
completedAt: '2024-02-09T14:32:00Z',
|
|
118
|
+
durationMs: 120000,
|
|
119
|
+
totalMatched: 180,
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
...completedRun,
|
|
123
123
|
id: 'run-6',
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
startedAt: '2024-02-08T10:00:00Z',
|
|
125
|
+
completedAt: '2024-02-08T10:05:00Z',
|
|
126
|
+
durationMs: 300000,
|
|
127
|
+
totalMatched: 300,
|
|
128
128
|
},
|
|
129
129
|
]),
|
|
130
130
|
},
|
|
@@ -142,8 +142,8 @@ export const MixedStatuses: Story = {
|
|
|
142
142
|
{
|
|
143
143
|
...completedRun,
|
|
144
144
|
id: 'run-7',
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
startedAt: '2024-02-09T14:30:00Z',
|
|
146
|
+
completedAt: '2024-02-09T14:32:00Z',
|
|
147
147
|
},
|
|
148
148
|
]),
|
|
149
149
|
},
|
|
@@ -159,15 +159,15 @@ export const FailedRuns: Story = {
|
|
|
159
159
|
...failedRun,
|
|
160
160
|
id: 'run-8',
|
|
161
161
|
error: 'Timeout connecting to database',
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
startedAt: '2024-02-09T14:30:00Z',
|
|
163
|
+
completedAt: '2024-02-09T14:31:00Z',
|
|
164
164
|
},
|
|
165
165
|
{
|
|
166
166
|
...failedRun,
|
|
167
167
|
id: 'run-9',
|
|
168
168
|
error: 'Invalid field reference: firm.invalid_field',
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
startedAt: '2024-02-08T10:00:00Z',
|
|
170
|
+
completedAt: '2024-02-08T10:00:30Z',
|
|
171
171
|
},
|
|
172
172
|
]),
|
|
173
173
|
},
|
|
@@ -182,7 +182,7 @@ export const RunningRuns: Story = {
|
|
|
182
182
|
{
|
|
183
183
|
...runningRun,
|
|
184
184
|
id: 'run-10',
|
|
185
|
-
|
|
185
|
+
startedAt: new Date(Date.now() - 60000).toISOString(), // 1 minute ago
|
|
186
186
|
},
|
|
187
187
|
]),
|
|
188
188
|
},
|
|
@@ -204,10 +204,10 @@ export const LargeHistory: Story = {
|
|
|
204
204
|
Array.from({ length: 50 }, (_, i) => ({
|
|
205
205
|
...completedRun,
|
|
206
206
|
id: `run-${i}`,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
startedAt: new Date(Date.now() - i * 86400000).toISOString(), // i days ago
|
|
208
|
+
totalMatched: 200 + Math.floor(Math.random() * 100),
|
|
209
|
+
durationMs: 120000 + Math.floor(Math.random() * 180000),
|
|
210
|
+
triggerType: i % 3 === 0 ? 'scheduled' : i % 3 === 1 ? 'manual' : 'api',
|
|
211
211
|
}))
|
|
212
212
|
),
|
|
213
213
|
},
|
|
@@ -218,10 +218,10 @@ export const DifferentTriggers: Story = {
|
|
|
218
218
|
args: {
|
|
219
219
|
funnelId: 'funnel-1',
|
|
220
220
|
apiClient: createMockApiClient([
|
|
221
|
-
{ ...completedRun, id: 'run-11',
|
|
222
|
-
{ ...completedRun, id: 'run-12',
|
|
223
|
-
{ ...completedRun, id: 'run-13',
|
|
224
|
-
{ ...completedRun, id: 'run-14',
|
|
221
|
+
{ ...completedRun, id: 'run-11', triggerType: 'manual' },
|
|
222
|
+
{ ...completedRun, id: 'run-12', triggerType: 'scheduled' },
|
|
223
|
+
{ ...completedRun, id: 'run-13', triggerType: 'api' },
|
|
224
|
+
{ ...completedRun, id: 'run-14', triggerType: 'webhook' },
|
|
225
225
|
]),
|
|
226
226
|
},
|
|
227
227
|
};
|
|
@@ -233,9 +233,9 @@ export const HighMatchRate: Story = {
|
|
|
233
233
|
apiClient: createMockApiClient([
|
|
234
234
|
{
|
|
235
235
|
...completedRun,
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
totalInput: 1000,
|
|
237
|
+
totalMatched: 950,
|
|
238
|
+
totalExcluded: 50,
|
|
239
239
|
},
|
|
240
240
|
]),
|
|
241
241
|
},
|
|
@@ -248,9 +248,9 @@ export const LowMatchRate: Story = {
|
|
|
248
248
|
apiClient: createMockApiClient([
|
|
249
249
|
{
|
|
250
250
|
...completedRun,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
251
|
+
totalInput: 10000,
|
|
252
|
+
totalMatched: 12,
|
|
253
|
+
totalExcluded: 9988,
|
|
254
254
|
},
|
|
255
255
|
]),
|
|
256
256
|
},
|
|
@@ -263,9 +263,9 @@ export const ZeroMatches: Story = {
|
|
|
263
263
|
apiClient: createMockApiClient([
|
|
264
264
|
{
|
|
265
265
|
...completedRun,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
totalInput: 1000,
|
|
267
|
+
totalMatched: 0,
|
|
268
|
+
totalExcluded: 1000,
|
|
269
269
|
},
|
|
270
270
|
]),
|
|
271
271
|
},
|
|
@@ -32,43 +32,43 @@ const createMockRun = (
|
|
|
32
32
|
): FunnelRun => {
|
|
33
33
|
const stageStats: Record<string, StageStats> = {
|
|
34
34
|
'stage-1': {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
stageId: 'stage-1',
|
|
36
|
+
stageName: 'High ICP Score',
|
|
37
|
+
inputCount: 1000,
|
|
38
|
+
matchedCount: 500,
|
|
39
|
+
notMatchedCount: 500,
|
|
40
|
+
excludedCount: 0,
|
|
41
|
+
taggedCount: 0,
|
|
42
|
+
continuedCount: 500,
|
|
43
|
+
durationMs: 1000,
|
|
44
44
|
},
|
|
45
45
|
'stage-2': {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
stageId: 'stage-2',
|
|
47
|
+
stageName: 'Frontend Stack',
|
|
48
|
+
inputCount: 500,
|
|
49
|
+
matchedCount: 350,
|
|
50
|
+
notMatchedCount: 150,
|
|
51
|
+
excludedCount: 0,
|
|
52
|
+
taggedCount: 0,
|
|
53
|
+
continuedCount: 350,
|
|
54
|
+
durationMs: 800,
|
|
55
55
|
},
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
return {
|
|
59
59
|
id: 'run-123',
|
|
60
|
-
|
|
60
|
+
funnelId: 'funnel-456',
|
|
61
61
|
status: 'completed',
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
triggerType: 'manual',
|
|
63
|
+
triggeredBy: 'user-789',
|
|
64
|
+
startedAt: new Date().toISOString(),
|
|
65
|
+
completedAt: new Date().toISOString(),
|
|
66
|
+
durationMs: 2300,
|
|
67
|
+
totalInput: 1000,
|
|
68
|
+
totalMatched: 350,
|
|
69
|
+
totalExcluded: 150,
|
|
70
|
+
totalTagged: 0,
|
|
71
|
+
stageStats: stageStats,
|
|
72
72
|
...overrides,
|
|
73
73
|
};
|
|
74
74
|
};
|
|
@@ -97,15 +97,15 @@ describe('FunnelRunHistory Component Logic', () => {
|
|
|
97
97
|
await getFunnelRuns('funnel-456', {
|
|
98
98
|
status: 'completed',
|
|
99
99
|
page: 1,
|
|
100
|
-
|
|
101
|
-
ordering: '-
|
|
100
|
+
pageSize: 10,
|
|
101
|
+
ordering: '-startedAt',
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
expect(getFunnelRuns).toHaveBeenCalledWith('funnel-456', {
|
|
105
105
|
status: 'completed',
|
|
106
106
|
page: 1,
|
|
107
|
-
|
|
108
|
-
ordering: '-
|
|
107
|
+
pageSize: 10,
|
|
108
|
+
ordering: '-startedAt',
|
|
109
109
|
});
|
|
110
110
|
});
|
|
111
111
|
|
|
@@ -113,17 +113,17 @@ describe('FunnelRunHistory Component Logic', () => {
|
|
|
113
113
|
const getFunnelRuns = mockApiClient.getFunnelRuns as any;
|
|
114
114
|
|
|
115
115
|
await getFunnelRuns('funnel-456', {
|
|
116
|
-
|
|
116
|
+
triggerType: 'scheduled',
|
|
117
117
|
page: 1,
|
|
118
|
-
|
|
119
|
-
ordering: '-
|
|
118
|
+
pageSize: 10,
|
|
119
|
+
ordering: '-startedAt',
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
expect(getFunnelRuns).toHaveBeenCalledWith('funnel-456', {
|
|
123
|
-
|
|
123
|
+
triggerType: 'scheduled',
|
|
124
124
|
page: 1,
|
|
125
|
-
|
|
126
|
-
ordering: '-
|
|
125
|
+
pageSize: 10,
|
|
126
|
+
ordering: '-startedAt',
|
|
127
127
|
});
|
|
128
128
|
});
|
|
129
129
|
});
|
|
@@ -134,12 +134,12 @@ describe('FunnelRunHistory Component Logic', () => {
|
|
|
134
134
|
const runFunnel = mockApiClient.runFunnel as any;
|
|
135
135
|
|
|
136
136
|
await runFunnel('funnel-456', {
|
|
137
|
-
|
|
137
|
+
triggerType: 'manual',
|
|
138
138
|
metadata: { re_run_of: 'run-123' },
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
expect(runFunnel).toHaveBeenCalledWith('funnel-456', {
|
|
142
|
-
|
|
142
|
+
triggerType: 'manual',
|
|
143
143
|
metadata: { re_run_of: 'run-123' },
|
|
144
144
|
});
|
|
145
145
|
});
|
|
@@ -210,19 +210,19 @@ describe('Run Status Scenarios', () => {
|
|
|
210
210
|
it('should handle completed run', () => {
|
|
211
211
|
const run = createMockRun({
|
|
212
212
|
status: 'completed',
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
totalInput: 1000,
|
|
214
|
+
totalMatched: 235,
|
|
215
215
|
});
|
|
216
216
|
|
|
217
217
|
expect(run.status).toBe('completed');
|
|
218
|
-
expect(run.
|
|
218
|
+
expect(run.totalMatched).toBe(235);
|
|
219
219
|
});
|
|
220
220
|
|
|
221
221
|
it('should handle failed run', () => {
|
|
222
222
|
const run = createMockRun({
|
|
223
223
|
status: 'failed',
|
|
224
224
|
error: 'Database connection timeout',
|
|
225
|
-
|
|
225
|
+
totalMatched: 0,
|
|
226
226
|
});
|
|
227
227
|
|
|
228
228
|
expect(run.status).toBe('failed');
|
|
@@ -232,19 +232,19 @@ describe('Run Status Scenarios', () => {
|
|
|
232
232
|
it('should handle running run', () => {
|
|
233
233
|
const run = createMockRun({
|
|
234
234
|
status: 'running',
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
completedAt: undefined,
|
|
236
|
+
durationMs: undefined,
|
|
237
237
|
});
|
|
238
238
|
|
|
239
239
|
expect(run.status).toBe('running');
|
|
240
|
-
expect(run.
|
|
240
|
+
expect(run.completedAt).toBeUndefined();
|
|
241
241
|
});
|
|
242
242
|
|
|
243
243
|
it('should handle pending run', () => {
|
|
244
244
|
const run = createMockRun({
|
|
245
245
|
status: 'pending',
|
|
246
|
-
|
|
247
|
-
|
|
246
|
+
startedAt: new Date().toISOString(),
|
|
247
|
+
completedAt: undefined,
|
|
248
248
|
});
|
|
249
249
|
|
|
250
250
|
expect(run.status).toBe('pending');
|
|
@@ -273,12 +273,12 @@ describe('Filter Scenarios', () => {
|
|
|
273
273
|
|
|
274
274
|
it('should filter by manual trigger', () => {
|
|
275
275
|
const runs = [
|
|
276
|
-
createMockRun({ id: '1',
|
|
277
|
-
createMockRun({ id: '2',
|
|
278
|
-
createMockRun({ id: '3',
|
|
276
|
+
createMockRun({ id: '1', triggerType: 'manual' }),
|
|
277
|
+
createMockRun({ id: '2', triggerType: 'scheduled' }),
|
|
278
|
+
createMockRun({ id: '3', triggerType: 'manual' }),
|
|
279
279
|
];
|
|
280
280
|
|
|
281
|
-
const filtered = runs.filter((r) => r.
|
|
281
|
+
const filtered = runs.filter((r) => r.triggerType === 'manual');
|
|
282
282
|
expect(filtered).toHaveLength(2);
|
|
283
283
|
});
|
|
284
284
|
});
|
|
@@ -291,21 +291,21 @@ describe('Edge Cases', () => {
|
|
|
291
291
|
|
|
292
292
|
it('should handle run with no stage stats', () => {
|
|
293
293
|
const run = createMockRun({
|
|
294
|
-
|
|
294
|
+
stageStats: {},
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
-
const stageStatsArray = Object.values(run.
|
|
297
|
+
const stageStatsArray = Object.values(run.stageStats);
|
|
298
298
|
expect(stageStatsArray).toHaveLength(0);
|
|
299
299
|
});
|
|
300
300
|
|
|
301
301
|
it('should handle run with zero match rate', () => {
|
|
302
302
|
const run = createMockRun({
|
|
303
|
-
|
|
304
|
-
|
|
303
|
+
totalInput: 1000,
|
|
304
|
+
totalMatched: 0,
|
|
305
305
|
});
|
|
306
306
|
|
|
307
|
-
const matchRate = run.
|
|
308
|
-
? Math.round((run.
|
|
307
|
+
const matchRate = run.totalInput > 0
|
|
308
|
+
? Math.round((run.totalMatched / run.totalInput) * 100)
|
|
309
309
|
: 0;
|
|
310
310
|
|
|
311
311
|
expect(matchRate).toBe(0);
|
|
@@ -313,11 +313,11 @@ describe('Edge Cases', () => {
|
|
|
313
313
|
|
|
314
314
|
it('should handle run with 100% match rate', () => {
|
|
315
315
|
const run = createMockRun({
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
totalInput: 1000,
|
|
317
|
+
totalMatched: 1000,
|
|
318
318
|
});
|
|
319
319
|
|
|
320
|
-
const matchRate = Math.round((run.
|
|
320
|
+
const matchRate = Math.round((run.totalMatched / run.totalInput) * 100);
|
|
321
321
|
expect(matchRate).toBe(100);
|
|
322
322
|
});
|
|
323
323
|
});
|
|
@@ -61,12 +61,12 @@ export function FunnelRunHistory({
|
|
|
61
61
|
const [selectedRun, setSelectedRun] = useState<FunnelRun | null>(null);
|
|
62
62
|
const [filters, setFilters] = useState<RunFiltersType>({
|
|
63
63
|
status: 'all',
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
triggerType: 'all',
|
|
65
|
+
dateRange: 'month',
|
|
66
66
|
});
|
|
67
67
|
const [pagination, setPagination] = useState<Pagination>({
|
|
68
68
|
page: 1,
|
|
69
|
-
|
|
69
|
+
pageSize: 10,
|
|
70
70
|
total: 0,
|
|
71
71
|
});
|
|
72
72
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
@@ -77,16 +77,16 @@ export function FunnelRunHistory({
|
|
|
77
77
|
setError(null);
|
|
78
78
|
const params: any = {
|
|
79
79
|
page: pagination.page,
|
|
80
|
-
|
|
81
|
-
ordering: '-
|
|
80
|
+
pageSize: pagination.pageSize,
|
|
81
|
+
ordering: '-startedAt', // Most recent first
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
// Add filters
|
|
85
85
|
if (filters.status && filters.status !== 'all') {
|
|
86
86
|
params.status = filters.status;
|
|
87
87
|
}
|
|
88
|
-
if (filters.
|
|
89
|
-
params.
|
|
88
|
+
if (filters.triggerType && filters.triggerType !== 'all') {
|
|
89
|
+
params.triggerType = filters.triggerType;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
const response = await apiClient.getFunnelRuns(funnelId, params);
|
|
@@ -103,7 +103,7 @@ export function FunnelRunHistory({
|
|
|
103
103
|
setIsLoading(false);
|
|
104
104
|
setIsRefreshing(false);
|
|
105
105
|
}
|
|
106
|
-
}, [funnelId, apiClient, pagination.page, pagination.
|
|
106
|
+
}, [funnelId, apiClient, pagination.page, pagination.pageSize, filters]);
|
|
107
107
|
|
|
108
108
|
// Load runs on mount and when dependencies change
|
|
109
109
|
useEffect(() => {
|
|
@@ -136,7 +136,7 @@ export function FunnelRunHistory({
|
|
|
136
136
|
const handleReRun = async (run: FunnelRun) => {
|
|
137
137
|
try {
|
|
138
138
|
await apiClient.runFunnel(funnelId, {
|
|
139
|
-
|
|
139
|
+
triggerType: 'manual',
|
|
140
140
|
metadata: { re_run_of: run.id },
|
|
141
141
|
});
|
|
142
142
|
// Refresh to show new run
|
|
@@ -168,7 +168,7 @@ export function FunnelRunHistory({
|
|
|
168
168
|
};
|
|
169
169
|
|
|
170
170
|
// Pagination
|
|
171
|
-
const totalPages = Math.ceil(pagination.total / pagination.
|
|
171
|
+
const totalPages = Math.ceil(pagination.total / pagination.pageSize);
|
|
172
172
|
const canGoBack = pagination.page > 1;
|
|
173
173
|
const canGoForward = pagination.page < totalPages;
|
|
174
174
|
|
|
@@ -184,9 +184,9 @@ export function FunnelRunHistory({
|
|
|
184
184
|
}
|
|
185
185
|
};
|
|
186
186
|
|
|
187
|
-
const startIndex = (pagination.page - 1) * pagination.
|
|
187
|
+
const startIndex = (pagination.page - 1) * pagination.pageSize + 1;
|
|
188
188
|
const endIndex = Math.min(
|
|
189
|
-
pagination.page * pagination.
|
|
189
|
+
pagination.page * pagination.pageSize,
|
|
190
190
|
pagination.total
|
|
191
191
|
);
|
|
192
192
|
|
|
@@ -90,7 +90,7 @@ export function RunDetailsModal({
|
|
|
90
90
|
|
|
91
91
|
if (!run) return null;
|
|
92
92
|
|
|
93
|
-
const stageStatsArray = Object.values(run.
|
|
93
|
+
const stageStatsArray = Object.values(run.stageStats);
|
|
94
94
|
|
|
95
95
|
return (
|
|
96
96
|
<div
|
|
@@ -139,21 +139,21 @@ export function RunDetailsModal({
|
|
|
139
139
|
<div>
|
|
140
140
|
<div className="text-xs text-gray-600 mb-1">Duration</div>
|
|
141
141
|
<div className="text-sm font-medium text-gray-900">
|
|
142
|
-
{formatDuration(run.
|
|
142
|
+
{formatDuration(run.durationMs)}
|
|
143
143
|
</div>
|
|
144
144
|
</div>
|
|
145
145
|
<div>
|
|
146
146
|
<div className="text-xs text-gray-600 mb-1">Started</div>
|
|
147
147
|
<div className="text-sm font-medium text-gray-900">
|
|
148
|
-
<span title={formatFullTimestamp(run.
|
|
149
|
-
{formatRelativeTime(run.
|
|
148
|
+
<span title={formatFullTimestamp(run.startedAt)}>
|
|
149
|
+
{formatRelativeTime(run.startedAt)}
|
|
150
150
|
</span>
|
|
151
151
|
</div>
|
|
152
152
|
</div>
|
|
153
153
|
<div>
|
|
154
154
|
<div className="text-xs text-gray-600 mb-1">Triggered by</div>
|
|
155
155
|
<div className="text-sm font-medium text-gray-900">
|
|
156
|
-
{run.
|
|
156
|
+
{run.triggeredBy || 'System'} ({run.triggerType})
|
|
157
157
|
</div>
|
|
158
158
|
</div>
|
|
159
159
|
</div>
|
|
@@ -39,15 +39,15 @@ export function RunFilters({
|
|
|
39
39
|
const clearFilters = () => {
|
|
40
40
|
onFiltersChange({
|
|
41
41
|
status: 'all',
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
triggerType: 'all',
|
|
43
|
+
dateRange: 'month',
|
|
44
44
|
});
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
const hasActiveFilters =
|
|
48
48
|
filters.status !== 'all' ||
|
|
49
|
-
filters.
|
|
50
|
-
filters.
|
|
49
|
+
filters.triggerType !== 'all' ||
|
|
50
|
+
filters.dateRange !== 'month';
|
|
51
51
|
|
|
52
52
|
return (
|
|
53
53
|
<div
|
|
@@ -82,9 +82,9 @@ export function RunFilters({
|
|
|
82
82
|
</label>
|
|
83
83
|
<select
|
|
84
84
|
id="trigger-filter"
|
|
85
|
-
value={filters.
|
|
85
|
+
value={filters.triggerType || 'all'}
|
|
86
86
|
onChange={(e) =>
|
|
87
|
-
updateFilter('
|
|
87
|
+
updateFilter('triggerType', e.target.value as TriggerType | 'all')
|
|
88
88
|
}
|
|
89
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
90
|
>
|
|
@@ -103,8 +103,8 @@ export function RunFilters({
|
|
|
103
103
|
</label>
|
|
104
104
|
<select
|
|
105
105
|
id="date-filter"
|
|
106
|
-
value={filters.
|
|
107
|
-
onChange={(e) => updateFilter('
|
|
106
|
+
value={filters.dateRange || 'month'}
|
|
107
|
+
onChange={(e) => updateFilter('dateRange', e.target.value as any)}
|
|
108
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
109
|
>
|
|
110
110
|
<option value="all">All time</option>
|