@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,325 @@
|
|
|
1
|
+
# FunnelRunHistory Component Suite
|
|
2
|
+
|
|
3
|
+
Comprehensive funnel execution history display with filtering, sorting, pagination, and detailed breakdowns.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Run Table** - Paginated list of all funnel executions
|
|
8
|
+
- **Filters** - Filter by status, trigger type, date range
|
|
9
|
+
- **Sorting** - Sorted by date (most recent first)
|
|
10
|
+
- **Auto-refresh** - Polls every 5 seconds when active runs exist
|
|
11
|
+
- **Details Modal** - Click row to see stage-by-stage breakdown
|
|
12
|
+
- **Actions** - Re-run, view results, cancel running jobs
|
|
13
|
+
- **Responsive** - Adapts to different screen sizes
|
|
14
|
+
- **Accessible** - Keyboard navigation, ARIA labels, screen reader support
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### Basic Usage
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { FunnelRunHistory } from '@simpli/funnels';
|
|
22
|
+
import { FunnelApiClient } from '@simpli/funnels';
|
|
23
|
+
|
|
24
|
+
function MyFunnelPage() {
|
|
25
|
+
const apiClient = new FunnelApiClient(adapter, 'https://api.example.com');
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<FunnelRunHistory
|
|
29
|
+
funnelId="funnel-123"
|
|
30
|
+
apiClient={apiClient}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### With Custom Result Handler
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
<FunnelRunHistory
|
|
40
|
+
funnelId="funnel-123"
|
|
41
|
+
apiClient={apiClient}
|
|
42
|
+
onViewResults={(run) => {
|
|
43
|
+
// Navigate to results page
|
|
44
|
+
router.push(`/funnels/${run.funnel_id}/runs/${run.id}/results`);
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Component Architecture
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
FunnelRunHistory (main)
|
|
53
|
+
├── RunFilters (filter controls)
|
|
54
|
+
├── RunRow (table row)
|
|
55
|
+
│ ├── RunStatusBadge (status display)
|
|
56
|
+
│ └── RunActions (action dropdown)
|
|
57
|
+
└── RunDetailsModal (details dialog)
|
|
58
|
+
└── StageBreakdownList (stage stats)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Props
|
|
62
|
+
|
|
63
|
+
### FunnelRunHistory
|
|
64
|
+
|
|
65
|
+
| Prop | Type | Required | Description |
|
|
66
|
+
|------|------|----------|-------------|
|
|
67
|
+
| `funnelId` | `string` | Yes | Funnel ID to load runs for |
|
|
68
|
+
| `apiClient` | `FunnelApiClient` | Yes | API client instance |
|
|
69
|
+
| `onViewResults` | `(run: FunnelRun) => void` | No | Custom handler for viewing results |
|
|
70
|
+
| `className` | `string` | No | Additional CSS classes |
|
|
71
|
+
|
|
72
|
+
## Table Columns
|
|
73
|
+
|
|
74
|
+
| Column | Description | Format |
|
|
75
|
+
|--------|-------------|--------|
|
|
76
|
+
| **Date** | When run started | Relative (2h ago) with tooltip |
|
|
77
|
+
| **Status** | Run status | Badge with icon |
|
|
78
|
+
| **Trigger** | How run was triggered | Manual, Scheduled, API, Webhook |
|
|
79
|
+
| **Duration** | Execution time | 2.3s, 1m 23s |
|
|
80
|
+
| **Input** | Total entities processed | Formatted number (1,000) |
|
|
81
|
+
| **Matched** | Entities matched | Formatted number |
|
|
82
|
+
| **%** | Match rate | Percentage (24%) |
|
|
83
|
+
| **Actions** | Action dropdown | View, Re-run, Cancel |
|
|
84
|
+
|
|
85
|
+
## Status Indicators
|
|
86
|
+
|
|
87
|
+
| Status | Icon | Color | Description |
|
|
88
|
+
|--------|------|-------|-------------|
|
|
89
|
+
| Complete | ✓ | Green | Successfully finished |
|
|
90
|
+
| Running | ⏸ | Blue | Currently executing (spinning) |
|
|
91
|
+
| Failed | ✗ | Red | Error occurred |
|
|
92
|
+
| Pending | ○ | Yellow | Queued, not started |
|
|
93
|
+
| Cancelled | × | Gray | Manually stopped |
|
|
94
|
+
|
|
95
|
+
## Filters
|
|
96
|
+
|
|
97
|
+
### Status Filter
|
|
98
|
+
- All (default)
|
|
99
|
+
- Complete
|
|
100
|
+
- Running
|
|
101
|
+
- Failed
|
|
102
|
+
- Pending
|
|
103
|
+
- Cancelled
|
|
104
|
+
|
|
105
|
+
### Trigger Type Filter
|
|
106
|
+
- All (default)
|
|
107
|
+
- Manual
|
|
108
|
+
- Scheduled
|
|
109
|
+
- Webhook
|
|
110
|
+
- API
|
|
111
|
+
|
|
112
|
+
### Date Range Filter
|
|
113
|
+
- All time
|
|
114
|
+
- Today
|
|
115
|
+
- Last 7 days
|
|
116
|
+
- Last 30 days (default)
|
|
117
|
+
|
|
118
|
+
## Actions
|
|
119
|
+
|
|
120
|
+
### View Details
|
|
121
|
+
Opens modal showing:
|
|
122
|
+
- Run summary (status, duration, trigger)
|
|
123
|
+
- Stage-by-stage breakdown
|
|
124
|
+
- Error messages (if failed)
|
|
125
|
+
- Actions (View Results, Re-run)
|
|
126
|
+
|
|
127
|
+
### View Results
|
|
128
|
+
Calls `onViewResults` prop or opens details modal by default.
|
|
129
|
+
|
|
130
|
+
### Re-run
|
|
131
|
+
Creates new run with same funnel configuration:
|
|
132
|
+
- Sets trigger_type to "manual"
|
|
133
|
+
- Adds metadata: `{ re_run_of: original_run_id }`
|
|
134
|
+
- Refreshes list to show new run
|
|
135
|
+
|
|
136
|
+
### Cancel
|
|
137
|
+
Cancels running/pending jobs:
|
|
138
|
+
- Shows confirmation dialog
|
|
139
|
+
- Calls API to cancel
|
|
140
|
+
- Refreshes to show updated status
|
|
141
|
+
- Only available for running/pending runs
|
|
142
|
+
|
|
143
|
+
## Auto-Refresh
|
|
144
|
+
|
|
145
|
+
The component automatically polls for updates when there are active runs:
|
|
146
|
+
- **Interval**: 5 seconds
|
|
147
|
+
- **Trigger**: Detects `pending` or `running` status
|
|
148
|
+
- **Cleanup**: Stops polling when no active runs
|
|
149
|
+
- **Manual**: Refresh button available
|
|
150
|
+
|
|
151
|
+
## Stage Breakdown Modal
|
|
152
|
+
|
|
153
|
+
Click any row to open detailed view:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
┌────────────────────────────────────────┐
|
|
157
|
+
│ Run Details - 2 hours ago │
|
|
158
|
+
│ ──────────────────────────────────────│
|
|
159
|
+
│ Status: Complete ✓ │
|
|
160
|
+
│ Duration: 2.3s │
|
|
161
|
+
│ Triggered by: John Doe (Manual) │
|
|
162
|
+
│ │
|
|
163
|
+
│ Stage Breakdown: │
|
|
164
|
+
│ ① High ICP Score │
|
|
165
|
+
│ Input: 1000 Matched: 500 ▼ -500 │
|
|
166
|
+
│ ② Frontend Stack │
|
|
167
|
+
│ Input: 500 Matched: 350 ▼ -150 │
|
|
168
|
+
│ │
|
|
169
|
+
│ [View Results] [Re-run] [Close] │
|
|
170
|
+
└────────────────────────────────────────┘
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Pagination
|
|
174
|
+
|
|
175
|
+
- **Page Size**: 10 runs per page (configurable)
|
|
176
|
+
- **Navigation**: Previous/Next buttons
|
|
177
|
+
- **Display**: "Showing 1-10 of 47"
|
|
178
|
+
- **State**: Preserved when filtering/refreshing
|
|
179
|
+
|
|
180
|
+
## Responsive Behavior
|
|
181
|
+
|
|
182
|
+
### Desktop (1024px+)
|
|
183
|
+
- Full table with all columns
|
|
184
|
+
- Action dropdown on hover
|
|
185
|
+
- Detailed tooltips
|
|
186
|
+
|
|
187
|
+
### Tablet (768px-1023px)
|
|
188
|
+
- Hide duration and trigger columns
|
|
189
|
+
- Compact action button
|
|
190
|
+
- Simplified tooltips
|
|
191
|
+
|
|
192
|
+
### Mobile (<768px)
|
|
193
|
+
- Card-based layout
|
|
194
|
+
- Stack information vertically
|
|
195
|
+
- Tap to expand details
|
|
196
|
+
|
|
197
|
+
## Accessibility
|
|
198
|
+
|
|
199
|
+
### Keyboard Navigation
|
|
200
|
+
- **Tab**: Navigate through rows and actions
|
|
201
|
+
- **Enter**: Open row details
|
|
202
|
+
- **Escape**: Close modal/dropdown
|
|
203
|
+
- **Arrow keys**: Navigate dropdown menu
|
|
204
|
+
|
|
205
|
+
### Screen Readers
|
|
206
|
+
- ARIA labels on all interactive elements
|
|
207
|
+
- Status announcements for loading/errors
|
|
208
|
+
- Table headers properly associated
|
|
209
|
+
- Modal dialog attributes
|
|
210
|
+
|
|
211
|
+
### Focus Management
|
|
212
|
+
- Focus trap in modal
|
|
213
|
+
- Return focus to trigger after close
|
|
214
|
+
- Visible focus indicators
|
|
215
|
+
- Skip to content links
|
|
216
|
+
|
|
217
|
+
## Utility Functions
|
|
218
|
+
|
|
219
|
+
### formatDuration
|
|
220
|
+
```ts
|
|
221
|
+
formatDuration(2300) // "2.3s"
|
|
222
|
+
formatDuration(65000) // "1m 5s"
|
|
223
|
+
formatDuration(3661000) // "1h 1m"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### formatRelativeTime
|
|
227
|
+
```ts
|
|
228
|
+
formatRelativeTime(fiveMinAgo) // "5m ago"
|
|
229
|
+
formatRelativeTime(yesterday) // "1d ago"
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### calculateMatchRate
|
|
233
|
+
```ts
|
|
234
|
+
calculateMatchRate(235, 1000) // 24
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### formatNumber
|
|
238
|
+
```ts
|
|
239
|
+
formatNumber(1234567) // "1,234,567"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Testing
|
|
243
|
+
|
|
244
|
+
Run tests:
|
|
245
|
+
```bash
|
|
246
|
+
npm run test
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Test files:
|
|
250
|
+
- `utils.test.ts` - Utility function tests
|
|
251
|
+
- `FunnelRunHistory.test.tsx` - Component logic tests
|
|
252
|
+
|
|
253
|
+
## Examples
|
|
254
|
+
|
|
255
|
+
### All Runs (No Filters)
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
<FunnelRunHistory
|
|
259
|
+
funnelId="funnel-123"
|
|
260
|
+
apiClient={apiClient}
|
|
261
|
+
/>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Only Failed Runs
|
|
265
|
+
|
|
266
|
+
Use the filter UI or implement custom filtering:
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
// User can filter via UI, or you can pre-filter server-side
|
|
270
|
+
// by creating a custom API call before passing to component
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Custom Results Page
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
<FunnelRunHistory
|
|
277
|
+
funnelId="funnel-123"
|
|
278
|
+
apiClient={apiClient}
|
|
279
|
+
onViewResults={(run) => {
|
|
280
|
+
// Custom navigation
|
|
281
|
+
navigate(`/runs/${run.id}/results`);
|
|
282
|
+
}}
|
|
283
|
+
/>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Generic Support
|
|
287
|
+
|
|
288
|
+
This component works with **any** funnel type:
|
|
289
|
+
- Investor funnels
|
|
290
|
+
- Recipe funnels
|
|
291
|
+
- Lead qualification funnels
|
|
292
|
+
- Task prioritization funnels
|
|
293
|
+
- Organization screening funnels
|
|
294
|
+
|
|
295
|
+
No domain-specific logic - purely generic entity processing.
|
|
296
|
+
|
|
297
|
+
## Error Handling
|
|
298
|
+
|
|
299
|
+
- **Loading State**: Shows spinner while fetching
|
|
300
|
+
- **Error State**: Displays error message with retry button
|
|
301
|
+
- **Empty State**: Shows helpful message when no runs exist
|
|
302
|
+
- **Network Errors**: Caught and displayed to user
|
|
303
|
+
- **Validation Errors**: Shown in action feedback
|
|
304
|
+
|
|
305
|
+
## Performance
|
|
306
|
+
|
|
307
|
+
- **Pagination**: Limits results to 10 per page
|
|
308
|
+
- **Auto-refresh**: Only polls when necessary
|
|
309
|
+
- **Debouncing**: Prevents rapid re-renders
|
|
310
|
+
- **Lazy Loading**: Modal only renders when open
|
|
311
|
+
- **Memoization**: Uses React best practices
|
|
312
|
+
|
|
313
|
+
## Browser Support
|
|
314
|
+
|
|
315
|
+
- Chrome/Edge 90+
|
|
316
|
+
- Firefox 88+
|
|
317
|
+
- Safari 14+
|
|
318
|
+
- Mobile browsers (iOS Safari, Chrome Android)
|
|
319
|
+
|
|
320
|
+
## Dependencies
|
|
321
|
+
|
|
322
|
+
- React 19+
|
|
323
|
+
- @simpli/funnels core package
|
|
324
|
+
- No external UI libraries required
|
|
325
|
+
- Tailwind CSS for styling
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RunActions Component
|
|
3
|
+
*
|
|
4
|
+
* Action dropdown menu for individual run rows.
|
|
5
|
+
*
|
|
6
|
+
* Design Rationale:
|
|
7
|
+
* - Dropdown menu keeps UI clean and compact
|
|
8
|
+
* - Icons provide visual cues for actions
|
|
9
|
+
* - Disabled states for invalid actions
|
|
10
|
+
* - Confirmation for destructive actions (cancel)
|
|
11
|
+
*
|
|
12
|
+
* Accessibility:
|
|
13
|
+
* - Keyboard navigation (arrow keys, Enter, Escape)
|
|
14
|
+
* - Focus management
|
|
15
|
+
* - ARIA attributes for screen readers
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useState, useRef, useEffect } from 'react';
|
|
19
|
+
import { FunnelRun } from '../../types';
|
|
20
|
+
|
|
21
|
+
interface RunActionsProps {
|
|
22
|
+
run: FunnelRun;
|
|
23
|
+
onViewDetails: (run: FunnelRun) => void;
|
|
24
|
+
onViewResults: (run: FunnelRun) => void;
|
|
25
|
+
onReRun: (run: FunnelRun) => void;
|
|
26
|
+
onCancel?: (run: FunnelRun) => void;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function RunActions({
|
|
31
|
+
run,
|
|
32
|
+
onViewDetails,
|
|
33
|
+
onViewResults,
|
|
34
|
+
onReRun,
|
|
35
|
+
onCancel,
|
|
36
|
+
className = '',
|
|
37
|
+
}: RunActionsProps) {
|
|
38
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
39
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
40
|
+
|
|
41
|
+
// Close on outside click
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
44
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
45
|
+
setIsOpen(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (isOpen) {
|
|
50
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
51
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
52
|
+
}
|
|
53
|
+
}, [isOpen]);
|
|
54
|
+
|
|
55
|
+
// Close on Escape key
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
58
|
+
if (event.key === 'Escape' && isOpen) {
|
|
59
|
+
setIsOpen(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (isOpen) {
|
|
64
|
+
document.addEventListener('keydown', handleEscape);
|
|
65
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
66
|
+
}
|
|
67
|
+
}, [isOpen]);
|
|
68
|
+
|
|
69
|
+
const canCancel = run.status === 'pending' || run.status === 'running';
|
|
70
|
+
const canViewResults = run.status === 'completed';
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className={`relative ${className}`} ref={dropdownRef}>
|
|
74
|
+
{/* Trigger Button */}
|
|
75
|
+
<button
|
|
76
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
77
|
+
className="p-1 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
78
|
+
aria-label="Run actions"
|
|
79
|
+
aria-haspopup="true"
|
|
80
|
+
aria-expanded={isOpen}
|
|
81
|
+
>
|
|
82
|
+
<svg
|
|
83
|
+
className="w-5 h-5"
|
|
84
|
+
fill="none"
|
|
85
|
+
stroke="currentColor"
|
|
86
|
+
viewBox="0 0 24 24"
|
|
87
|
+
>
|
|
88
|
+
<path
|
|
89
|
+
strokeLinecap="round"
|
|
90
|
+
strokeLinejoin="round"
|
|
91
|
+
strokeWidth={2}
|
|
92
|
+
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
|
|
93
|
+
/>
|
|
94
|
+
</svg>
|
|
95
|
+
</button>
|
|
96
|
+
|
|
97
|
+
{/* Dropdown Menu */}
|
|
98
|
+
{isOpen && (
|
|
99
|
+
<div
|
|
100
|
+
className="absolute right-0 mt-1 w-48 bg-white rounded-md shadow-lg border border-gray-200 z-10"
|
|
101
|
+
role="menu"
|
|
102
|
+
>
|
|
103
|
+
<div className="py-1">
|
|
104
|
+
{/* View Details */}
|
|
105
|
+
<button
|
|
106
|
+
onClick={() => {
|
|
107
|
+
onViewDetails(run);
|
|
108
|
+
setIsOpen(false);
|
|
109
|
+
}}
|
|
110
|
+
className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2"
|
|
111
|
+
role="menuitem"
|
|
112
|
+
>
|
|
113
|
+
<span aria-hidden="true">👁</span>
|
|
114
|
+
View Details
|
|
115
|
+
</button>
|
|
116
|
+
|
|
117
|
+
{/* View Results */}
|
|
118
|
+
<button
|
|
119
|
+
onClick={() => {
|
|
120
|
+
onViewResults(run);
|
|
121
|
+
setIsOpen(false);
|
|
122
|
+
}}
|
|
123
|
+
disabled={!canViewResults}
|
|
124
|
+
className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white"
|
|
125
|
+
role="menuitem"
|
|
126
|
+
>
|
|
127
|
+
<span aria-hidden="true">📊</span>
|
|
128
|
+
View Results
|
|
129
|
+
</button>
|
|
130
|
+
|
|
131
|
+
{/* Re-run */}
|
|
132
|
+
<button
|
|
133
|
+
onClick={() => {
|
|
134
|
+
onReRun(run);
|
|
135
|
+
setIsOpen(false);
|
|
136
|
+
}}
|
|
137
|
+
className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2"
|
|
138
|
+
role="menuitem"
|
|
139
|
+
>
|
|
140
|
+
<span aria-hidden="true">↻</span>
|
|
141
|
+
Re-run
|
|
142
|
+
</button>
|
|
143
|
+
|
|
144
|
+
{/* Cancel (if running/pending) */}
|
|
145
|
+
{canCancel && onCancel && (
|
|
146
|
+
<>
|
|
147
|
+
<div className="border-t border-gray-200 my-1" />
|
|
148
|
+
<button
|
|
149
|
+
onClick={() => {
|
|
150
|
+
if (confirm('Are you sure you want to cancel this run?')) {
|
|
151
|
+
onCancel(run);
|
|
152
|
+
setIsOpen(false);
|
|
153
|
+
}
|
|
154
|
+
}}
|
|
155
|
+
className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
|
|
156
|
+
role="menuitem"
|
|
157
|
+
>
|
|
158
|
+
<span aria-hidden="true">×</span>
|
|
159
|
+
Cancel Run
|
|
160
|
+
</button>
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RunDetailsModal Component
|
|
3
|
+
*
|
|
4
|
+
* Modal dialog showing detailed run information and stage breakdown.
|
|
5
|
+
*
|
|
6
|
+
* Design Rationale:
|
|
7
|
+
* - Modal overlay focuses attention on details
|
|
8
|
+
* - Stage breakdown shows funnel flow clearly
|
|
9
|
+
* - Action buttons provide next steps
|
|
10
|
+
* - Close on overlay click or Escape key
|
|
11
|
+
*
|
|
12
|
+
* Accessibility:
|
|
13
|
+
* - Focus trap within modal
|
|
14
|
+
* - Escape key closes modal
|
|
15
|
+
* - ARIA dialog attributes
|
|
16
|
+
* - Backdrop click closes modal
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useEffect, useRef } from 'react';
|
|
20
|
+
import { FunnelRun } from '../../types';
|
|
21
|
+
import { RunStatusBadge } from './RunStatusBadge';
|
|
22
|
+
import { StageBreakdownList } from './StageBreakdownList';
|
|
23
|
+
import {
|
|
24
|
+
formatDuration,
|
|
25
|
+
formatRelativeTime,
|
|
26
|
+
formatFullTimestamp,
|
|
27
|
+
} from './utils';
|
|
28
|
+
|
|
29
|
+
interface RunDetailsModalProps {
|
|
30
|
+
run: FunnelRun | null;
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
onViewResults?: (run: FunnelRun) => void;
|
|
33
|
+
onReRun?: (run: FunnelRun) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function RunDetailsModal({
|
|
37
|
+
run,
|
|
38
|
+
onClose,
|
|
39
|
+
onViewResults,
|
|
40
|
+
onReRun,
|
|
41
|
+
}: RunDetailsModalProps) {
|
|
42
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
43
|
+
|
|
44
|
+
// Close on Escape
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
47
|
+
if (e.key === 'Escape') {
|
|
48
|
+
onClose();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (run) {
|
|
53
|
+
document.addEventListener('keydown', handleEscape);
|
|
54
|
+
// Prevent body scroll
|
|
55
|
+
document.body.style.overflow = 'hidden';
|
|
56
|
+
return () => {
|
|
57
|
+
document.removeEventListener('keydown', handleEscape);
|
|
58
|
+
document.body.style.overflow = 'unset';
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}, [run, onClose]);
|
|
62
|
+
|
|
63
|
+
// Focus trap
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (run && modalRef.current) {
|
|
66
|
+
const focusableElements = modalRef.current.querySelectorAll<HTMLElement>(
|
|
67
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
68
|
+
);
|
|
69
|
+
const firstElement = focusableElements[0];
|
|
70
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
71
|
+
|
|
72
|
+
firstElement?.focus();
|
|
73
|
+
|
|
74
|
+
const handleTab = (e: KeyboardEvent) => {
|
|
75
|
+
if (e.key !== 'Tab') return;
|
|
76
|
+
|
|
77
|
+
if (e.shiftKey && document.activeElement === firstElement) {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
lastElement?.focus();
|
|
80
|
+
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
firstElement?.focus();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
document.addEventListener('keydown', handleTab);
|
|
87
|
+
return () => document.removeEventListener('keydown', handleTab);
|
|
88
|
+
}
|
|
89
|
+
}, [run]);
|
|
90
|
+
|
|
91
|
+
if (!run) return null;
|
|
92
|
+
|
|
93
|
+
const stageStatsArray = Object.values(run.stage_stats);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
|
|
98
|
+
onClick={onClose}
|
|
99
|
+
role="dialog"
|
|
100
|
+
aria-modal="true"
|
|
101
|
+
aria-labelledby="modal-title"
|
|
102
|
+
>
|
|
103
|
+
<div
|
|
104
|
+
ref={modalRef}
|
|
105
|
+
onClick={(e) => e.stopPropagation()}
|
|
106
|
+
className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col"
|
|
107
|
+
>
|
|
108
|
+
{/* Header */}
|
|
109
|
+
<div className="px-6 py-4 border-b border-gray-200">
|
|
110
|
+
<div className="flex items-center justify-between">
|
|
111
|
+
<h2 id="modal-title" className="text-lg font-semibold text-gray-900">
|
|
112
|
+
Run Details
|
|
113
|
+
</h2>
|
|
114
|
+
<button
|
|
115
|
+
onClick={onClose}
|
|
116
|
+
className="p-1 text-gray-400 hover:text-gray-600 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
117
|
+
aria-label="Close modal"
|
|
118
|
+
>
|
|
119
|
+
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
120
|
+
<path
|
|
121
|
+
fillRule="evenodd"
|
|
122
|
+
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
123
|
+
clipRule="evenodd"
|
|
124
|
+
/>
|
|
125
|
+
</svg>
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Content */}
|
|
131
|
+
<div className="px-6 py-4 overflow-y-auto flex-1">
|
|
132
|
+
{/* Run Summary */}
|
|
133
|
+
<div className="mb-6">
|
|
134
|
+
<div className="grid grid-cols-2 gap-4 mb-4">
|
|
135
|
+
<div>
|
|
136
|
+
<div className="text-xs text-gray-600 mb-1">Status</div>
|
|
137
|
+
<RunStatusBadge status={run.status} />
|
|
138
|
+
</div>
|
|
139
|
+
<div>
|
|
140
|
+
<div className="text-xs text-gray-600 mb-1">Duration</div>
|
|
141
|
+
<div className="text-sm font-medium text-gray-900">
|
|
142
|
+
{formatDuration(run.duration_ms)}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<div className="text-xs text-gray-600 mb-1">Started</div>
|
|
147
|
+
<div className="text-sm font-medium text-gray-900">
|
|
148
|
+
<span title={formatFullTimestamp(run.started_at)}>
|
|
149
|
+
{formatRelativeTime(run.started_at)}
|
|
150
|
+
</span>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div>
|
|
154
|
+
<div className="text-xs text-gray-600 mb-1">Triggered by</div>
|
|
155
|
+
<div className="text-sm font-medium text-gray-900">
|
|
156
|
+
{run.triggered_by || 'System'} ({run.trigger_type})
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Error Message */}
|
|
162
|
+
{run.status === 'failed' && run.error && (
|
|
163
|
+
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
|
164
|
+
<div className="text-sm font-medium text-red-800 mb-1">
|
|
165
|
+
Error
|
|
166
|
+
</div>
|
|
167
|
+
<div className="text-sm text-red-700">{run.error}</div>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{/* Stage Breakdown */}
|
|
173
|
+
<div>
|
|
174
|
+
<h3 className="text-sm font-semibold text-gray-900 mb-3">
|
|
175
|
+
Stage Breakdown
|
|
176
|
+
</h3>
|
|
177
|
+
{stageStatsArray.length > 0 ? (
|
|
178
|
+
<StageBreakdownList stages={stageStatsArray} />
|
|
179
|
+
) : (
|
|
180
|
+
<div className="text-sm text-gray-500 text-center py-4">
|
|
181
|
+
No stage data available
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* Footer */}
|
|
188
|
+
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-end gap-3">
|
|
189
|
+
<button
|
|
190
|
+
onClick={onClose}
|
|
191
|
+
className="px-4 py-2 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"
|
|
192
|
+
>
|
|
193
|
+
Close
|
|
194
|
+
</button>
|
|
195
|
+
{onReRun && (
|
|
196
|
+
<button
|
|
197
|
+
onClick={() => {
|
|
198
|
+
onReRun(run);
|
|
199
|
+
onClose();
|
|
200
|
+
}}
|
|
201
|
+
className="px-4 py-2 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"
|
|
202
|
+
>
|
|
203
|
+
↻ Re-run
|
|
204
|
+
</button>
|
|
205
|
+
)}
|
|
206
|
+
{onViewResults && run.status === 'completed' && (
|
|
207
|
+
<button
|
|
208
|
+
onClick={() => {
|
|
209
|
+
onViewResults(run);
|
|
210
|
+
onClose();
|
|
211
|
+
}}
|
|
212
|
+
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
213
|
+
>
|
|
214
|
+
View Results
|
|
215
|
+
</button>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
}
|