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.
Files changed (159) hide show
  1. flowyml/__init__.py +207 -0
  2. flowyml/assets/__init__.py +22 -0
  3. flowyml/assets/artifact.py +40 -0
  4. flowyml/assets/base.py +209 -0
  5. flowyml/assets/dataset.py +100 -0
  6. flowyml/assets/featureset.py +301 -0
  7. flowyml/assets/metrics.py +104 -0
  8. flowyml/assets/model.py +82 -0
  9. flowyml/assets/registry.py +157 -0
  10. flowyml/assets/report.py +315 -0
  11. flowyml/cli/__init__.py +5 -0
  12. flowyml/cli/experiment.py +232 -0
  13. flowyml/cli/init.py +256 -0
  14. flowyml/cli/main.py +327 -0
  15. flowyml/cli/run.py +75 -0
  16. flowyml/cli/stack_cli.py +532 -0
  17. flowyml/cli/ui.py +33 -0
  18. flowyml/core/__init__.py +68 -0
  19. flowyml/core/advanced_cache.py +274 -0
  20. flowyml/core/approval.py +64 -0
  21. flowyml/core/cache.py +203 -0
  22. flowyml/core/checkpoint.py +148 -0
  23. flowyml/core/conditional.py +373 -0
  24. flowyml/core/context.py +155 -0
  25. flowyml/core/error_handling.py +419 -0
  26. flowyml/core/executor.py +354 -0
  27. flowyml/core/graph.py +185 -0
  28. flowyml/core/parallel.py +452 -0
  29. flowyml/core/pipeline.py +764 -0
  30. flowyml/core/project.py +253 -0
  31. flowyml/core/resources.py +424 -0
  32. flowyml/core/scheduler.py +630 -0
  33. flowyml/core/scheduler_config.py +32 -0
  34. flowyml/core/step.py +201 -0
  35. flowyml/core/step_grouping.py +292 -0
  36. flowyml/core/templates.py +226 -0
  37. flowyml/core/versioning.py +217 -0
  38. flowyml/integrations/__init__.py +1 -0
  39. flowyml/integrations/keras.py +134 -0
  40. flowyml/monitoring/__init__.py +1 -0
  41. flowyml/monitoring/alerts.py +57 -0
  42. flowyml/monitoring/data.py +102 -0
  43. flowyml/monitoring/llm.py +160 -0
  44. flowyml/monitoring/monitor.py +57 -0
  45. flowyml/monitoring/notifications.py +246 -0
  46. flowyml/registry/__init__.py +5 -0
  47. flowyml/registry/model_registry.py +491 -0
  48. flowyml/registry/pipeline_registry.py +55 -0
  49. flowyml/stacks/__init__.py +27 -0
  50. flowyml/stacks/base.py +77 -0
  51. flowyml/stacks/bridge.py +288 -0
  52. flowyml/stacks/components.py +155 -0
  53. flowyml/stacks/gcp.py +499 -0
  54. flowyml/stacks/local.py +112 -0
  55. flowyml/stacks/migration.py +97 -0
  56. flowyml/stacks/plugin_config.py +78 -0
  57. flowyml/stacks/plugins.py +401 -0
  58. flowyml/stacks/registry.py +226 -0
  59. flowyml/storage/__init__.py +26 -0
  60. flowyml/storage/artifacts.py +246 -0
  61. flowyml/storage/materializers/__init__.py +20 -0
  62. flowyml/storage/materializers/base.py +133 -0
  63. flowyml/storage/materializers/keras.py +185 -0
  64. flowyml/storage/materializers/numpy.py +94 -0
  65. flowyml/storage/materializers/pandas.py +142 -0
  66. flowyml/storage/materializers/pytorch.py +135 -0
  67. flowyml/storage/materializers/sklearn.py +110 -0
  68. flowyml/storage/materializers/tensorflow.py +152 -0
  69. flowyml/storage/metadata.py +931 -0
  70. flowyml/tracking/__init__.py +1 -0
  71. flowyml/tracking/experiment.py +211 -0
  72. flowyml/tracking/leaderboard.py +191 -0
  73. flowyml/tracking/runs.py +145 -0
  74. flowyml/ui/__init__.py +15 -0
  75. flowyml/ui/backend/Dockerfile +31 -0
  76. flowyml/ui/backend/__init__.py +0 -0
  77. flowyml/ui/backend/auth.py +163 -0
  78. flowyml/ui/backend/main.py +187 -0
  79. flowyml/ui/backend/routers/__init__.py +0 -0
  80. flowyml/ui/backend/routers/assets.py +45 -0
  81. flowyml/ui/backend/routers/execution.py +179 -0
  82. flowyml/ui/backend/routers/experiments.py +49 -0
  83. flowyml/ui/backend/routers/leaderboard.py +118 -0
  84. flowyml/ui/backend/routers/notifications.py +72 -0
  85. flowyml/ui/backend/routers/pipelines.py +110 -0
  86. flowyml/ui/backend/routers/plugins.py +192 -0
  87. flowyml/ui/backend/routers/projects.py +85 -0
  88. flowyml/ui/backend/routers/runs.py +66 -0
  89. flowyml/ui/backend/routers/schedules.py +222 -0
  90. flowyml/ui/backend/routers/traces.py +84 -0
  91. flowyml/ui/frontend/Dockerfile +20 -0
  92. flowyml/ui/frontend/README.md +315 -0
  93. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
  94. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
  95. flowyml/ui/frontend/dist/index.html +16 -0
  96. flowyml/ui/frontend/index.html +15 -0
  97. flowyml/ui/frontend/nginx.conf +26 -0
  98. flowyml/ui/frontend/package-lock.json +3545 -0
  99. flowyml/ui/frontend/package.json +33 -0
  100. flowyml/ui/frontend/postcss.config.js +6 -0
  101. flowyml/ui/frontend/src/App.jsx +21 -0
  102. flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
  103. flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
  104. flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
  105. flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
  106. flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
  107. flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
  108. flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
  109. flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
  110. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
  111. flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
  112. flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
  113. flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
  114. flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
  115. flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
  116. flowyml/ui/frontend/src/components/Layout.jsx +108 -0
  117. flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
  118. flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
  119. flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
  120. flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
  121. flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
  122. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
  123. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
  124. flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
  125. flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
  126. flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
  127. flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
  128. flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
  129. flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
  130. flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
  131. flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
  132. flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
  133. flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
  134. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
  135. flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
  136. flowyml/ui/frontend/src/index.css +11 -0
  137. flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
  138. flowyml/ui/frontend/src/main.jsx +10 -0
  139. flowyml/ui/frontend/src/router/index.jsx +39 -0
  140. flowyml/ui/frontend/src/services/pluginService.js +90 -0
  141. flowyml/ui/frontend/src/utils/api.js +47 -0
  142. flowyml/ui/frontend/src/utils/cn.js +6 -0
  143. flowyml/ui/frontend/tailwind.config.js +31 -0
  144. flowyml/ui/frontend/vite.config.js +21 -0
  145. flowyml/ui/utils.py +77 -0
  146. flowyml/utils/__init__.py +67 -0
  147. flowyml/utils/config.py +308 -0
  148. flowyml/utils/debug.py +240 -0
  149. flowyml/utils/environment.py +346 -0
  150. flowyml/utils/git.py +319 -0
  151. flowyml/utils/logging.py +61 -0
  152. flowyml/utils/performance.py +314 -0
  153. flowyml/utils/stack_config.py +296 -0
  154. flowyml/utils/validation.py +270 -0
  155. flowyml-1.1.0.dist-info/METADATA +372 -0
  156. flowyml-1.1.0.dist-info/RECORD +159 -0
  157. flowyml-1.1.0.dist-info/WHEEL +4 -0
  158. flowyml-1.1.0.dist-info/entry_points.txt +3 -0
  159. 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
+ }