flowyml 1.1.0__py3-none-any.whl
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.
- flowyml/__init__.py +207 -0
- flowyml/assets/__init__.py +22 -0
- flowyml/assets/artifact.py +40 -0
- flowyml/assets/base.py +209 -0
- flowyml/assets/dataset.py +100 -0
- flowyml/assets/featureset.py +301 -0
- flowyml/assets/metrics.py +104 -0
- flowyml/assets/model.py +82 -0
- flowyml/assets/registry.py +157 -0
- flowyml/assets/report.py +315 -0
- flowyml/cli/__init__.py +5 -0
- flowyml/cli/experiment.py +232 -0
- flowyml/cli/init.py +256 -0
- flowyml/cli/main.py +327 -0
- flowyml/cli/run.py +75 -0
- flowyml/cli/stack_cli.py +532 -0
- flowyml/cli/ui.py +33 -0
- flowyml/core/__init__.py +68 -0
- flowyml/core/advanced_cache.py +274 -0
- flowyml/core/approval.py +64 -0
- flowyml/core/cache.py +203 -0
- flowyml/core/checkpoint.py +148 -0
- flowyml/core/conditional.py +373 -0
- flowyml/core/context.py +155 -0
- flowyml/core/error_handling.py +419 -0
- flowyml/core/executor.py +354 -0
- flowyml/core/graph.py +185 -0
- flowyml/core/parallel.py +452 -0
- flowyml/core/pipeline.py +764 -0
- flowyml/core/project.py +253 -0
- flowyml/core/resources.py +424 -0
- flowyml/core/scheduler.py +630 -0
- flowyml/core/scheduler_config.py +32 -0
- flowyml/core/step.py +201 -0
- flowyml/core/step_grouping.py +292 -0
- flowyml/core/templates.py +226 -0
- flowyml/core/versioning.py +217 -0
- flowyml/integrations/__init__.py +1 -0
- flowyml/integrations/keras.py +134 -0
- flowyml/monitoring/__init__.py +1 -0
- flowyml/monitoring/alerts.py +57 -0
- flowyml/monitoring/data.py +102 -0
- flowyml/monitoring/llm.py +160 -0
- flowyml/monitoring/monitor.py +57 -0
- flowyml/monitoring/notifications.py +246 -0
- flowyml/registry/__init__.py +5 -0
- flowyml/registry/model_registry.py +491 -0
- flowyml/registry/pipeline_registry.py +55 -0
- flowyml/stacks/__init__.py +27 -0
- flowyml/stacks/base.py +77 -0
- flowyml/stacks/bridge.py +288 -0
- flowyml/stacks/components.py +155 -0
- flowyml/stacks/gcp.py +499 -0
- flowyml/stacks/local.py +112 -0
- flowyml/stacks/migration.py +97 -0
- flowyml/stacks/plugin_config.py +78 -0
- flowyml/stacks/plugins.py +401 -0
- flowyml/stacks/registry.py +226 -0
- flowyml/storage/__init__.py +26 -0
- flowyml/storage/artifacts.py +246 -0
- flowyml/storage/materializers/__init__.py +20 -0
- flowyml/storage/materializers/base.py +133 -0
- flowyml/storage/materializers/keras.py +185 -0
- flowyml/storage/materializers/numpy.py +94 -0
- flowyml/storage/materializers/pandas.py +142 -0
- flowyml/storage/materializers/pytorch.py +135 -0
- flowyml/storage/materializers/sklearn.py +110 -0
- flowyml/storage/materializers/tensorflow.py +152 -0
- flowyml/storage/metadata.py +931 -0
- flowyml/tracking/__init__.py +1 -0
- flowyml/tracking/experiment.py +211 -0
- flowyml/tracking/leaderboard.py +191 -0
- flowyml/tracking/runs.py +145 -0
- flowyml/ui/__init__.py +15 -0
- flowyml/ui/backend/Dockerfile +31 -0
- flowyml/ui/backend/__init__.py +0 -0
- flowyml/ui/backend/auth.py +163 -0
- flowyml/ui/backend/main.py +187 -0
- flowyml/ui/backend/routers/__init__.py +0 -0
- flowyml/ui/backend/routers/assets.py +45 -0
- flowyml/ui/backend/routers/execution.py +179 -0
- flowyml/ui/backend/routers/experiments.py +49 -0
- flowyml/ui/backend/routers/leaderboard.py +118 -0
- flowyml/ui/backend/routers/notifications.py +72 -0
- flowyml/ui/backend/routers/pipelines.py +110 -0
- flowyml/ui/backend/routers/plugins.py +192 -0
- flowyml/ui/backend/routers/projects.py +85 -0
- flowyml/ui/backend/routers/runs.py +66 -0
- flowyml/ui/backend/routers/schedules.py +222 -0
- flowyml/ui/backend/routers/traces.py +84 -0
- flowyml/ui/frontend/Dockerfile +20 -0
- flowyml/ui/frontend/README.md +315 -0
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
- flowyml/ui/frontend/dist/index.html +16 -0
- flowyml/ui/frontend/index.html +15 -0
- flowyml/ui/frontend/nginx.conf +26 -0
- flowyml/ui/frontend/package-lock.json +3545 -0
- flowyml/ui/frontend/package.json +33 -0
- flowyml/ui/frontend/postcss.config.js +6 -0
- flowyml/ui/frontend/src/App.jsx +21 -0
- flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
- flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
- flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
- flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
- flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
- flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
- flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
- flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
- flowyml/ui/frontend/src/components/Layout.jsx +108 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
- flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
- flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
- flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
- flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
- flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
- flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
- flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
- flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
- flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
- flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
- flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
- flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
- flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
- flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
- flowyml/ui/frontend/src/index.css +11 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
- flowyml/ui/frontend/src/main.jsx +10 -0
- flowyml/ui/frontend/src/router/index.jsx +39 -0
- flowyml/ui/frontend/src/services/pluginService.js +90 -0
- flowyml/ui/frontend/src/utils/api.js +47 -0
- flowyml/ui/frontend/src/utils/cn.js +6 -0
- flowyml/ui/frontend/tailwind.config.js +31 -0
- flowyml/ui/frontend/vite.config.js +21 -0
- flowyml/ui/utils.py +77 -0
- flowyml/utils/__init__.py +67 -0
- flowyml/utils/config.py +308 -0
- flowyml/utils/debug.py +240 -0
- flowyml/utils/environment.py +346 -0
- flowyml/utils/git.py +319 -0
- flowyml/utils/logging.py +61 -0
- flowyml/utils/performance.py +314 -0
- flowyml/utils/stack_config.py +296 -0
- flowyml/utils/validation.py +270 -0
- flowyml-1.1.0.dist-info/METADATA +372 -0
- flowyml-1.1.0.dist-info/RECORD +159 -0
- flowyml-1.1.0.dist-info/WHEEL +4 -0
- flowyml-1.1.0.dist-info/entry_points.txt +3 -0
- flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { fetchApi } from '../../utils/api';
|
|
3
|
+
import { Link } from 'react-router-dom';
|
|
4
|
+
import { Play, Clock, Calendar, TrendingUp, Activity, ArrowRight, Search, CheckCircle, XCircle, Loader, FolderPlus } from 'lucide-react';
|
|
5
|
+
import { Card } from '../../components/ui/Card';
|
|
6
|
+
import { Badge } from '../../components/ui/Badge';
|
|
7
|
+
import { format } from 'date-fns';
|
|
8
|
+
import { motion } from 'framer-motion';
|
|
9
|
+
import { DataView } from '../../components/ui/DataView';
|
|
10
|
+
import { useProject } from '../../contexts/ProjectContext';
|
|
11
|
+
import { ExecutionStatus, StatusBadge } from '../../components/ui/ExecutionStatus';
|
|
12
|
+
import { EmptyState } from '../../components/ui/EmptyState';
|
|
13
|
+
|
|
14
|
+
export function Runs() {
|
|
15
|
+
const [runs, setRuns] = useState([]);
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
const [filter, setFilter] = useState('all'); // all, completed, failed, running
|
|
18
|
+
const [selectedRunIds, setSelectedRunIds] = useState([]);
|
|
19
|
+
const { selectedProject } = useProject();
|
|
20
|
+
|
|
21
|
+
const fetchRuns = async () => {
|
|
22
|
+
setLoading(true);
|
|
23
|
+
try {
|
|
24
|
+
const url = selectedProject
|
|
25
|
+
? `/api/runs?project=${encodeURIComponent(selectedProject)}`
|
|
26
|
+
: '/api/runs';
|
|
27
|
+
const res = await fetchApi(url);
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
setRuns(data.runs || []);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(err);
|
|
32
|
+
} finally {
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
fetchRuns();
|
|
39
|
+
}, [selectedProject]);
|
|
40
|
+
|
|
41
|
+
const filteredRuns = runs.filter(run => {
|
|
42
|
+
if (filter === 'all') return true;
|
|
43
|
+
return run.status === filter;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const stats = {
|
|
47
|
+
total: runs.length,
|
|
48
|
+
completed: runs.filter(r => r.status === 'completed').length,
|
|
49
|
+
failed: runs.filter(r => r.status === 'failed').length,
|
|
50
|
+
running: runs.filter(r => r.status === 'running').length,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const columns = [
|
|
54
|
+
{
|
|
55
|
+
header: (
|
|
56
|
+
<input
|
|
57
|
+
type="checkbox"
|
|
58
|
+
checked={selectedRunIds.length === filteredRuns.length && filteredRuns.length > 0}
|
|
59
|
+
onChange={(e) => {
|
|
60
|
+
if (e.target.checked) {
|
|
61
|
+
setSelectedRunIds(filteredRuns.map(r => r.run_id));
|
|
62
|
+
} else {
|
|
63
|
+
setSelectedRunIds([]);
|
|
64
|
+
}
|
|
65
|
+
}}
|
|
66
|
+
className="w-4 h-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500"
|
|
67
|
+
/>
|
|
68
|
+
),
|
|
69
|
+
key: 'select',
|
|
70
|
+
render: (run) => (
|
|
71
|
+
<input
|
|
72
|
+
type="checkbox"
|
|
73
|
+
checked={selectedRunIds.includes(run.run_id)}
|
|
74
|
+
onChange={(e) => {
|
|
75
|
+
if (e.target.checked) {
|
|
76
|
+
setSelectedRunIds([...selectedRunIds, run.run_id]);
|
|
77
|
+
} else {
|
|
78
|
+
setSelectedRunIds(selectedRunIds.filter(id => id !== run.run_id));
|
|
79
|
+
}
|
|
80
|
+
}}
|
|
81
|
+
onClick={(e) => e.stopPropagation()}
|
|
82
|
+
className="w-4 h-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500"
|
|
83
|
+
/>
|
|
84
|
+
)
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
header: 'Status',
|
|
88
|
+
key: 'status',
|
|
89
|
+
sortable: true,
|
|
90
|
+
render: (run) => <StatusBadge status={run.status} />
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
header: 'Pipeline',
|
|
94
|
+
key: 'pipeline_name',
|
|
95
|
+
sortable: true,
|
|
96
|
+
render: (run) => (
|
|
97
|
+
<span className="font-medium text-slate-900 dark:text-white">{run.pipeline_name}</span>
|
|
98
|
+
)
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
header: 'Project',
|
|
102
|
+
key: 'project',
|
|
103
|
+
sortable: true,
|
|
104
|
+
render: (run) => (
|
|
105
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
106
|
+
{run.project || '-'}
|
|
107
|
+
</span>
|
|
108
|
+
)
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
header: 'Run ID',
|
|
112
|
+
key: 'run_id',
|
|
113
|
+
render: (run) => (
|
|
114
|
+
<span className="font-mono text-xs bg-slate-100 dark:bg-slate-700 px-2 py-1 rounded text-slate-600 dark:text-slate-300">
|
|
115
|
+
{run.run_id.substring(0, 8)}...
|
|
116
|
+
</span>
|
|
117
|
+
)
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
header: 'Start Time',
|
|
121
|
+
key: 'start_time',
|
|
122
|
+
sortable: true,
|
|
123
|
+
render: (run) => (
|
|
124
|
+
<div className="flex items-center gap-2 text-slate-500">
|
|
125
|
+
<Calendar size={14} />
|
|
126
|
+
{run.start_time ? format(new Date(run.start_time), 'MMM d, HH:mm:ss') : '-'}
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
header: 'Duration',
|
|
132
|
+
key: 'duration',
|
|
133
|
+
sortable: true,
|
|
134
|
+
render: (run) => (
|
|
135
|
+
<div className="flex items-center gap-2 text-slate-500">
|
|
136
|
+
<Clock size={14} />
|
|
137
|
+
{run.duration ? `${run.duration.toFixed(2)}s` : '-'}
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
header: 'Actions',
|
|
143
|
+
key: 'actions',
|
|
144
|
+
render: (run) => (
|
|
145
|
+
<Link
|
|
146
|
+
to={`/runs/${run.run_id}`}
|
|
147
|
+
className="text-primary-600 hover:text-primary-700 font-medium text-sm flex items-center gap-1"
|
|
148
|
+
>
|
|
149
|
+
Details <ArrowRight size={14} />
|
|
150
|
+
</Link>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const renderGrid = (run) => {
|
|
156
|
+
const statusConfig = {
|
|
157
|
+
completed: {
|
|
158
|
+
icon: <CheckCircle size={20} />,
|
|
159
|
+
color: 'text-emerald-500',
|
|
160
|
+
bg: 'bg-emerald-50',
|
|
161
|
+
border: 'border-emerald-200',
|
|
162
|
+
badge: 'success'
|
|
163
|
+
},
|
|
164
|
+
failed: {
|
|
165
|
+
icon: <XCircle size={20} />,
|
|
166
|
+
color: 'text-rose-500',
|
|
167
|
+
bg: 'bg-rose-50',
|
|
168
|
+
border: 'border-rose-200',
|
|
169
|
+
badge: 'danger'
|
|
170
|
+
},
|
|
171
|
+
running: {
|
|
172
|
+
icon: <Loader size={20} className="animate-spin" />,
|
|
173
|
+
color: 'text-amber-500',
|
|
174
|
+
bg: 'bg-amber-50',
|
|
175
|
+
border: 'border-amber-200',
|
|
176
|
+
badge: 'warning'
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const config = statusConfig[run.status] || statusConfig.completed;
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<Link to={`/runs/${run.run_id}`}>
|
|
184
|
+
<Card className={`group hover:shadow-lg transition-all duration-200 border-l-4 ${config.border} hover:border-l-primary-400 h-full`}>
|
|
185
|
+
<div className="flex items-center justify-between mb-4">
|
|
186
|
+
<div className="flex items-center gap-3">
|
|
187
|
+
<div className={`p-2 rounded-lg ${config.bg} ${config.color}`}>
|
|
188
|
+
{config.icon}
|
|
189
|
+
</div>
|
|
190
|
+
<div>
|
|
191
|
+
<h3 className="font-bold text-slate-900 dark:text-white truncate max-w-[150px]" title={run.pipeline_name}>
|
|
192
|
+
{run.pipeline_name}
|
|
193
|
+
</h3>
|
|
194
|
+
<div className="text-xs text-slate-500 font-mono">
|
|
195
|
+
{run.run_id.substring(0, 8)}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
<Badge variant={config.badge} className="text-xs uppercase tracking-wide">
|
|
200
|
+
{run.status}
|
|
201
|
+
</Badge>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div className="space-y-2 text-sm text-slate-500 dark:text-slate-400">
|
|
205
|
+
<div className="flex items-center justify-between">
|
|
206
|
+
<span className="flex items-center gap-2"><Calendar size={14} /> Started</span>
|
|
207
|
+
<span>{run.start_time ? format(new Date(run.start_time), 'MMM d, HH:mm') : '-'}</span>
|
|
208
|
+
</div>
|
|
209
|
+
<div className="flex items-center justify-between">
|
|
210
|
+
<span className="flex items-center gap-2"><Clock size={14} /> Duration</span>
|
|
211
|
+
<span>{run.duration ? `${run.duration.toFixed(2)}s` : '-'}</span>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Steps Progress */}
|
|
216
|
+
{run.steps && (
|
|
217
|
+
<div className="mt-4 pt-4 border-t border-slate-100 dark:border-slate-700">
|
|
218
|
+
<div className="flex justify-between text-xs mb-1">
|
|
219
|
+
<span className="text-slate-500">Progress</span>
|
|
220
|
+
<span className="font-medium text-slate-900 dark:text-white">
|
|
221
|
+
{Object.values(run.steps).filter(s => s.success).length} / {Object.keys(run.steps).length}
|
|
222
|
+
</span>
|
|
223
|
+
</div>
|
|
224
|
+
<div className="w-full h-1.5 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
|
|
225
|
+
<div
|
|
226
|
+
className={`h-full ${config.color.replace('text-', 'bg-')} transition-all duration-300`}
|
|
227
|
+
style={{
|
|
228
|
+
width: `${(Object.values(run.steps).filter(s => s.success).length / Object.keys(run.steps).length) * 100}%`
|
|
229
|
+
}}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
</Card>
|
|
235
|
+
</Link>
|
|
236
|
+
);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
if (loading) {
|
|
240
|
+
return (
|
|
241
|
+
<div className="flex items-center justify-center h-96">
|
|
242
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div className="p-6 max-w-7xl mx-auto space-y-8">
|
|
249
|
+
{/* Stats Cards */}
|
|
250
|
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
251
|
+
<StatsCard
|
|
252
|
+
label="Total Runs"
|
|
253
|
+
value={stats.total}
|
|
254
|
+
icon={<Activity size={20} />}
|
|
255
|
+
color="slate"
|
|
256
|
+
active={filter === 'all'}
|
|
257
|
+
onClick={() => setFilter('all')}
|
|
258
|
+
/>
|
|
259
|
+
<StatsCard
|
|
260
|
+
label="Completed"
|
|
261
|
+
value={stats.completed}
|
|
262
|
+
icon={<CheckCircle size={20} />}
|
|
263
|
+
color="emerald"
|
|
264
|
+
active={filter === 'completed'}
|
|
265
|
+
onClick={() => setFilter('completed')}
|
|
266
|
+
/>
|
|
267
|
+
<StatsCard
|
|
268
|
+
label="Failed"
|
|
269
|
+
value={stats.failed}
|
|
270
|
+
icon={<XCircle size={20} />}
|
|
271
|
+
color="rose"
|
|
272
|
+
active={filter === 'failed'}
|
|
273
|
+
onClick={() => setFilter('failed')}
|
|
274
|
+
/>
|
|
275
|
+
<StatsCard
|
|
276
|
+
label="Running"
|
|
277
|
+
value={stats.running}
|
|
278
|
+
icon={<Loader size={20} />}
|
|
279
|
+
color="amber"
|
|
280
|
+
active={filter === 'running'}
|
|
281
|
+
onClick={() => setFilter('running')}
|
|
282
|
+
/>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<DataView
|
|
286
|
+
title="Pipeline Runs"
|
|
287
|
+
subtitle="Monitor and track all your pipeline executions"
|
|
288
|
+
items={filteredRuns}
|
|
289
|
+
loading={loading}
|
|
290
|
+
columns={columns}
|
|
291
|
+
renderGrid={renderGrid}
|
|
292
|
+
initialView="table" // Default to table for runs as it's usually more useful
|
|
293
|
+
actions={
|
|
294
|
+
<div className="flex items-center gap-2">
|
|
295
|
+
{/* Add to Project Action */}
|
|
296
|
+
<ProjectSelector
|
|
297
|
+
selectedRuns={selectedRunIds}
|
|
298
|
+
onComplete={() => {
|
|
299
|
+
// Call fetchRuns from parent scope
|
|
300
|
+
fetchRuns();
|
|
301
|
+
setSelectedRunIds([]);
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
</div>
|
|
305
|
+
}
|
|
306
|
+
emptyState={
|
|
307
|
+
<EmptyState
|
|
308
|
+
icon={Activity}
|
|
309
|
+
title="No runs found"
|
|
310
|
+
description={filter === 'all'
|
|
311
|
+
? 'Run a pipeline to see it here'
|
|
312
|
+
: `No ${filter} runs found. Try a different filter.`
|
|
313
|
+
}
|
|
314
|
+
/>
|
|
315
|
+
}
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function ProjectSelector({ selectedRuns, onComplete }) {
|
|
322
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
323
|
+
const [projects, setProjects] = useState([]);
|
|
324
|
+
const [updating, setUpdating] = useState(false);
|
|
325
|
+
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
if (isOpen) {
|
|
328
|
+
fetch('/api/projects/')
|
|
329
|
+
.then(res => res.json())
|
|
330
|
+
.then(data => setProjects(data))
|
|
331
|
+
.catch(err => console.error('Failed to load projects:', err));
|
|
332
|
+
}
|
|
333
|
+
}, [isOpen]);
|
|
334
|
+
|
|
335
|
+
const handleSelectProject = async (projectName) => {
|
|
336
|
+
setUpdating(true);
|
|
337
|
+
try {
|
|
338
|
+
// Update all selected runs
|
|
339
|
+
const updates = selectedRuns.map(runId =>
|
|
340
|
+
fetch(`/api/runs/${runId}/project`, {
|
|
341
|
+
method: 'PUT',
|
|
342
|
+
headers: { 'Content-Type': 'application/json' },
|
|
343
|
+
body: JSON.stringify({ project_name: projectName })
|
|
344
|
+
})
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
await Promise.all(updates);
|
|
348
|
+
|
|
349
|
+
// Show success notification
|
|
350
|
+
showNotification('success', `Added ${selectedRuns.length} run(s) to project ${projectName}`);
|
|
351
|
+
|
|
352
|
+
setIsOpen(false);
|
|
353
|
+
if (onComplete) onComplete();
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error('Failed to update projects:', error);
|
|
356
|
+
showNotification('error', 'Failed to update project attribution');
|
|
357
|
+
} finally {
|
|
358
|
+
setUpdating(false);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const showNotification = (type, message) => {
|
|
363
|
+
// Simple toast notification
|
|
364
|
+
const toast = document.createElement('div');
|
|
365
|
+
toast.className = `fixed top-4 right-4 px-4 py-3 rounded-lg shadow-lg z-50 ${type === 'success' ? 'bg-green-500' : 'bg-red-500'
|
|
366
|
+
} text-white animate-in slide-in-from-right`;
|
|
367
|
+
toast.textContent = message;
|
|
368
|
+
document.body.appendChild(toast);
|
|
369
|
+
setTimeout(() => {
|
|
370
|
+
toast.classList.add('animate-out', 'fade-out');
|
|
371
|
+
setTimeout(() => document.body.removeChild(toast), 300);
|
|
372
|
+
}, 3000);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<div className="relative">
|
|
377
|
+
<button
|
|
378
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
379
|
+
disabled={updating || selectedRuns.length === 0}
|
|
380
|
+
className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-slate-600 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
381
|
+
>
|
|
382
|
+
<FolderPlus size={16} />
|
|
383
|
+
{updating ? 'Updating...' : `Add to Project (${selectedRuns.length})`}
|
|
384
|
+
</button>
|
|
385
|
+
|
|
386
|
+
{isOpen && (
|
|
387
|
+
<>
|
|
388
|
+
<div
|
|
389
|
+
className="fixed inset-0 z-10"
|
|
390
|
+
onClick={() => setIsOpen(false)}
|
|
391
|
+
/>
|
|
392
|
+
<div className="absolute right-0 mt-2 w-56 bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 z-20 overflow-hidden animate-in fade-in zoom-in-95 duration-100">
|
|
393
|
+
<div className="p-2 border-b border-slate-100 dark:border-slate-700">
|
|
394
|
+
<span className="text-xs font-semibold text-slate-500 px-2">Select Project</span>
|
|
395
|
+
</div>
|
|
396
|
+
<div className="max-h-64 overflow-y-auto p-1">
|
|
397
|
+
{projects.length > 0 ? (
|
|
398
|
+
projects.map(p => (
|
|
399
|
+
<button
|
|
400
|
+
key={p.name}
|
|
401
|
+
onClick={() => handleSelectProject(p.name)}
|
|
402
|
+
disabled={updating}
|
|
403
|
+
className="w-full text-left px-3 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700 rounded-lg transition-colors disabled:opacity-50"
|
|
404
|
+
>
|
|
405
|
+
{p.name}
|
|
406
|
+
</button>
|
|
407
|
+
))
|
|
408
|
+
) : (
|
|
409
|
+
<div className="px-3 py-2 text-sm text-slate-400 italic">No projects found</div>
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
</>
|
|
414
|
+
)}
|
|
415
|
+
</div>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function StatsCard({ label, value, icon, color, active, onClick }) {
|
|
420
|
+
const colorClasses = {
|
|
421
|
+
slate: {
|
|
422
|
+
bg: "bg-slate-50",
|
|
423
|
+
text: "text-slate-600",
|
|
424
|
+
border: "border-slate-200",
|
|
425
|
+
activeBg: "bg-slate-100",
|
|
426
|
+
activeBorder: "border-slate-300"
|
|
427
|
+
},
|
|
428
|
+
emerald: {
|
|
429
|
+
bg: "bg-emerald-50",
|
|
430
|
+
text: "text-emerald-600",
|
|
431
|
+
border: "border-emerald-200",
|
|
432
|
+
activeBg: "bg-emerald-100",
|
|
433
|
+
activeBorder: "border-emerald-300"
|
|
434
|
+
},
|
|
435
|
+
rose: {
|
|
436
|
+
bg: "bg-rose-50",
|
|
437
|
+
text: "text-rose-600",
|
|
438
|
+
border: "border-rose-200",
|
|
439
|
+
activeBg: "bg-rose-100",
|
|
440
|
+
activeBorder: "border-rose-300"
|
|
441
|
+
},
|
|
442
|
+
amber: {
|
|
443
|
+
bg: "bg-amber-50",
|
|
444
|
+
text: "text-amber-600",
|
|
445
|
+
border: "border-amber-200",
|
|
446
|
+
activeBg: "bg-amber-100",
|
|
447
|
+
activeBorder: "border-amber-300"
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const colors = colorClasses[color];
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<Card
|
|
455
|
+
className={`cursor-pointer transition-all duration-200 hover:shadow-md border-2 ${active ? colors.activeBorder : 'border-transparent'
|
|
456
|
+
}`}
|
|
457
|
+
onClick={onClick}
|
|
458
|
+
>
|
|
459
|
+
<div className="flex items-center justify-between">
|
|
460
|
+
<div>
|
|
461
|
+
<p className="text-sm text-slate-500 font-medium mb-1">{label}</p>
|
|
462
|
+
<p className="text-3xl font-bold text-slate-900 dark:text-white">{value}</p>
|
|
463
|
+
</div>
|
|
464
|
+
<div className={`p-3 rounded-xl ${active ? colors.activeBg : colors.bg} ${colors.text}`}>
|
|
465
|
+
{icon}
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</Card>
|
|
469
|
+
);
|
|
470
|
+
}
|