flowyml 1.2.0__py3-none-any.whl → 1.4.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 (104) hide show
  1. flowyml/__init__.py +3 -0
  2. flowyml/assets/base.py +10 -0
  3. flowyml/assets/metrics.py +6 -0
  4. flowyml/cli/main.py +108 -2
  5. flowyml/cli/run.py +9 -2
  6. flowyml/core/execution_status.py +52 -0
  7. flowyml/core/hooks.py +106 -0
  8. flowyml/core/observability.py +210 -0
  9. flowyml/core/orchestrator.py +274 -0
  10. flowyml/core/pipeline.py +193 -231
  11. flowyml/core/project.py +34 -2
  12. flowyml/core/remote_orchestrator.py +109 -0
  13. flowyml/core/resources.py +34 -17
  14. flowyml/core/retry_policy.py +80 -0
  15. flowyml/core/scheduler.py +9 -9
  16. flowyml/core/scheduler_config.py +2 -3
  17. flowyml/core/step.py +18 -1
  18. flowyml/core/submission_result.py +53 -0
  19. flowyml/integrations/keras.py +95 -22
  20. flowyml/monitoring/alerts.py +2 -2
  21. flowyml/stacks/__init__.py +15 -0
  22. flowyml/stacks/aws.py +599 -0
  23. flowyml/stacks/azure.py +295 -0
  24. flowyml/stacks/bridge.py +9 -9
  25. flowyml/stacks/components.py +24 -2
  26. flowyml/stacks/gcp.py +158 -11
  27. flowyml/stacks/local.py +5 -0
  28. flowyml/stacks/plugins.py +2 -2
  29. flowyml/stacks/registry.py +21 -0
  30. flowyml/storage/artifacts.py +15 -5
  31. flowyml/storage/materializers/__init__.py +2 -0
  32. flowyml/storage/materializers/base.py +33 -0
  33. flowyml/storage/materializers/cloudpickle.py +74 -0
  34. flowyml/storage/metadata.py +3 -881
  35. flowyml/storage/remote.py +590 -0
  36. flowyml/storage/sql.py +911 -0
  37. flowyml/ui/backend/dependencies.py +28 -0
  38. flowyml/ui/backend/main.py +43 -80
  39. flowyml/ui/backend/routers/assets.py +483 -17
  40. flowyml/ui/backend/routers/client.py +46 -0
  41. flowyml/ui/backend/routers/execution.py +13 -2
  42. flowyml/ui/backend/routers/experiments.py +97 -14
  43. flowyml/ui/backend/routers/metrics.py +168 -0
  44. flowyml/ui/backend/routers/pipelines.py +77 -12
  45. flowyml/ui/backend/routers/projects.py +33 -7
  46. flowyml/ui/backend/routers/runs.py +221 -12
  47. flowyml/ui/backend/routers/schedules.py +5 -21
  48. flowyml/ui/backend/routers/stats.py +14 -0
  49. flowyml/ui/backend/routers/traces.py +37 -53
  50. flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
  51. flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
  52. flowyml/ui/frontend/dist/index.html +2 -2
  53. flowyml/ui/frontend/src/App.jsx +4 -1
  54. flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
  55. flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
  56. flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
  57. flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
  58. flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
  59. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
  60. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
  61. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
  62. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
  63. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
  64. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
  65. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
  66. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
  67. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
  68. flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
  69. flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
  70. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
  71. flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
  72. flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
  73. flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
  74. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
  75. flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
  76. flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
  77. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
  78. flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
  79. flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
  80. flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
  81. flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
  82. flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
  83. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
  84. flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
  85. flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
  86. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
  87. flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
  88. flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
  89. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
  90. flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
  91. flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
  92. flowyml/ui/frontend/src/router/index.jsx +4 -0
  93. flowyml/ui/frontend/src/utils/date.js +10 -0
  94. flowyml/ui/frontend/src/utils/downloads.js +11 -0
  95. flowyml/utils/config.py +6 -0
  96. flowyml/utils/stack_config.py +45 -3
  97. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/METADATA +44 -4
  98. flowyml-1.4.0.dist-info/RECORD +200 -0
  99. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/licenses/LICENSE +1 -1
  100. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
  101. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
  102. flowyml-1.2.0.dist-info/RECORD +0 -159
  103. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/WHEEL +0 -0
  104. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/entry_points.txt +0 -0
@@ -7,64 +7,33 @@ import { Badge } from '../../components/ui/Badge';
7
7
  import { format } from 'date-fns';
8
8
  import { DataView } from '../../components/ui/DataView';
9
9
  import { useProject } from '../../contexts/ProjectContext';
10
+ import { NavigationTree } from '../../components/NavigationTree';
11
+ import { PipelineDetailsPanel } from '../../components/PipelineDetailsPanel';
10
12
 
11
13
  export function Pipelines() {
12
14
  const [pipelines, setPipelines] = useState([]);
13
15
  const [loading, setLoading] = useState(true);
14
- const [selectedPipelines, setSelectedPipelines] = useState([]);
16
+ const [error, setError] = useState(null);
17
+ const [selectedPipeline, setSelectedPipeline] = useState(null);
15
18
  const { selectedProject } = useProject();
16
19
 
17
20
  const fetchData = async () => {
18
21
  setLoading(true);
22
+ setError(null);
19
23
  try {
20
24
  const pipelinesUrl = selectedProject
21
- ? `/api/pipelines?project=${encodeURIComponent(selectedProject)}`
22
- : '/api/pipelines';
23
- const runsUrl = selectedProject
24
- ? `/api/runs?project=${encodeURIComponent(selectedProject)}`
25
- : '/api/runs';
25
+ ? `/api/pipelines/?project=${encodeURIComponent(selectedProject)}`
26
+ : '/api/pipelines/';
26
27
 
27
28
  const pipelinesRes = await fetchApi(pipelinesUrl);
28
- const pipelinesData = await pipelinesRes.json();
29
-
30
- // Fetch runs to calculate stats per pipeline
31
- const runsRes = await fetchApi(runsUrl);
32
- const runsData = await runsRes.json();
33
-
34
- // Calculate stats for each pipeline
35
- const pipelinesWithStats = pipelinesData.pipelines.map(pipeline => {
36
- const pipelineRuns = runsData.runs.filter(r => r.pipeline_name === pipeline);
37
- const completedRuns = pipelineRuns.filter(r => r.status === 'completed');
38
- const failedRuns = pipelineRuns.filter(r => r.status === 'failed');
39
- const avgDuration = pipelineRuns.length > 0
40
- ? pipelineRuns.reduce((sum, r) => sum + (r.duration || 0), 0) / pipelineRuns.length
41
- : 0;
42
-
43
- const lastRun = pipelineRuns.length > 0
44
- ? pipelineRuns.sort((a, b) => new Date(b.start_time) - new Date(a.start_time))[0]
45
- : null;
46
-
47
- // Get most common project from runs
48
- const projects = pipelineRuns.map(r => r.project).filter(Boolean);
49
- const projectCounts = {};
50
- projects.forEach(p => projectCounts[p] = (projectCounts[p] || 0) + 1);
51
- const mostCommonProject = Object.keys(projectCounts).sort((a, b) => projectCounts[b] - projectCounts[a])[0] || null;
29
+ if (!pipelinesRes.ok) throw new Error(`Failed to fetch pipelines: ${pipelinesRes.statusText}`);
52
30
 
53
- return {
54
- name: pipeline,
55
- totalRuns: pipelineRuns.length,
56
- completedRuns: completedRuns.length,
57
- failedRuns: failedRuns.length,
58
- successRate: pipelineRuns.length > 0 ? (completedRuns.length / pipelineRuns.length) * 100 : 0,
59
- avgDuration,
60
- lastRun,
61
- project: mostCommonProject
62
- };
63
- });
31
+ const pipelinesData = await pipelinesRes.json();
64
32
 
65
- setPipelines(pipelinesWithStats);
33
+ setPipelines(pipelinesData.pipelines || []);
66
34
  } catch (err) {
67
35
  console.error(err);
36
+ setError(err.message);
68
37
  } finally {
69
38
  setLoading(false);
70
39
  }
@@ -74,381 +43,89 @@ export function Pipelines() {
74
43
  fetchData();
75
44
  }, [selectedProject]);
76
45
 
77
- const columns = [
78
- {
79
- header: (
80
- <input
81
- type="checkbox"
82
- checked={selectedPipelines.length === pipelines.length && pipelines.length > 0}
83
- onChange={(e) => {
84
- if (e.target.checked) {
85
- setSelectedPipelines(pipelines.map(p => p.name));
86
- } else {
87
- setSelectedPipelines([]);
88
- }
89
- }}
90
- className="w-4 h-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500"
91
- />
92
- ),
93
- key: 'select',
94
- render: (item) => (
95
- <input
96
- type="checkbox"
97
- checked={selectedPipelines.includes(item.name)}
98
- onChange={(e) => {
99
- if (e.target.checked) {
100
- setSelectedPipelines([...selectedPipelines, item.name]);
101
- } else {
102
- setSelectedPipelines(selectedPipelines.filter(n => n !== item.name));
103
- }
104
- }}
105
- onClick={(e) => e.stopPropagation()}
106
- className="w-4 h-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500"
107
- />
108
- )
109
- },
110
- {
111
- header: 'Pipeline',
112
- key: 'name',
113
- sortable: true,
114
- render: (item) => (
115
- <div className="flex items-center gap-3">
116
- <div className="p-2 bg-gradient-to-br from-primary-500 to-purple-500 rounded-lg text-white">
117
- <Layers size={16} />
118
- </div>
119
- <span className="font-medium text-slate-900 dark:text-white">{item.name}</span>
120
- </div>
121
- )
122
- },
123
- {
124
- header: 'Project',
125
- key: 'project',
126
- render: (item) => (
127
- <span className="text-sm text-slate-600 dark:text-slate-400">
128
- {item.project || '-'}
129
- </span>
130
- )
131
- },
132
- {
133
- header: 'Success Rate',
134
- key: 'successRate',
135
- sortable: true,
136
- render: (item) => {
137
- const rate = item.successRate;
138
- const color = rate >= 80 ? 'text-emerald-600 bg-emerald-50' : rate >= 50 ? 'text-amber-600 bg-amber-50' : 'text-rose-600 bg-rose-50';
139
- return (
140
- <span className={`px-2 py-1 rounded text-xs font-semibold ${color}`}>
141
- {rate.toFixed(0)}%
142
- </span>
143
- );
144
- }
145
- },
146
- {
147
- header: 'Total Runs',
148
- key: 'totalRuns',
149
- sortable: true,
150
- render: (item) => (
151
- <div className="flex items-center gap-2 text-slate-600 dark:text-slate-400">
152
- <Activity size={14} />
153
- {item.totalRuns}
154
- </div>
155
- )
156
- },
157
- {
158
- header: 'Avg Duration',
159
- key: 'avgDuration',
160
- sortable: true,
161
- render: (item) => (
162
- <div className="flex items-center gap-2 text-slate-600 dark:text-slate-400">
163
- <Clock size={14} />
164
- {item.avgDuration > 0 ? `${item.avgDuration.toFixed(1)}s` : '-'}
165
- </div>
166
- )
167
- },
168
- {
169
- header: 'Last Run',
170
- key: 'lastRun',
171
- render: (item) => item.lastRun ? (
172
- <div className="text-sm text-slate-500">
173
- {format(new Date(item.lastRun.start_time), 'MMM d, HH:mm')}
174
- </div>
175
- ) : '-'
176
- },
177
- {
178
- header: 'Actions',
179
- key: 'actions',
180
- render: (item) => (
181
- <Link
182
- to={`/runs?pipeline=${encodeURIComponent(item.name)}`}
183
- className="text-primary-600 hover:text-primary-700 font-medium text-sm flex items-center gap-1"
184
- >
185
- View Runs <ArrowRight size={14} />
186
- </Link>
187
- )
188
- }
189
- ];
190
-
191
- const renderGrid = (item) => {
192
- const successRate = item.successRate || 0;
193
- const statusColor = successRate >= 80 ? 'emerald' : successRate >= 50 ? 'amber' : 'rose';
194
-
195
- const colorClasses = {
196
- emerald: { bg: 'bg-emerald-50', text: 'text-emerald-600' },
197
- amber: { bg: 'bg-amber-50', text: 'text-amber-600' },
198
- rose: { bg: 'bg-rose-50', text: 'text-rose-600' }
199
- };
200
- const colors = colorClasses[statusColor];
46
+ const handlePipelineSelect = (pipeline) => {
47
+ setSelectedPipeline(pipeline);
48
+ };
201
49
 
50
+ if (error) {
202
51
  return (
203
- <Card className="group cursor-pointer hover:shadow-xl hover:border-primary-300 transition-all duration-200 overflow-hidden h-full">
204
- <div className="flex items-start justify-between mb-4">
205
- <div className="p-3 bg-gradient-to-br from-primary-500 to-purple-500 rounded-xl text-white group-hover:scale-110 transition-transform shadow-lg">
206
- <Layers size={24} />
207
- </div>
208
- <div className="flex flex-col items-end gap-1">
209
- {item.totalRuns > 0 && (
210
- <Badge variant="secondary" className="text-xs bg-slate-100 text-slate-600">
211
- {item.totalRuns} runs
212
- </Badge>
213
- )}
214
- {successRate > 0 && (
215
- <div className={`text-xs font-semibold px-2 py-0.5 rounded ${colors.bg} ${colors.text}`}>
216
- {successRate.toFixed(0)}% success
217
- </div>
218
- )}
219
- </div>
52
+ <div className="flex items-center justify-center h-screen bg-slate-50 dark:bg-slate-900">
53
+ <div className="text-center p-8 bg-red-50 dark:bg-red-900/20 rounded-2xl border border-red-100 dark:border-red-800 max-w-md">
54
+ <XCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
55
+ <h3 className="text-lg font-bold text-red-700 dark:text-red-300 mb-2">Failed to load pipelines</h3>
56
+ <p className="text-red-600 dark:text-red-400 mb-6">{error}</p>
57
+ <button
58
+ onClick={() => window.location.reload()}
59
+ className="px-4 py-2 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-sm hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors text-slate-700 dark:text-slate-300 font-medium"
60
+ >
61
+ Retry Connection
62
+ </button>
220
63
  </div>
221
-
222
- <h3 className="text-lg font-bold text-slate-900 dark:text-white mb-3 group-hover:text-primary-600 transition-colors">
223
- {item.name}
224
- </h3>
225
-
226
- <div className="grid grid-cols-2 gap-3 mb-4">
227
- <StatItem icon={<Activity size={14} />} label="Total" value={item.totalRuns} color="blue" />
228
- <StatItem icon={<CheckCircle size={14} />} label="Success" value={item.completedRuns} color="emerald" />
229
- <StatItem icon={<Clock size={14} />} label="Avg Time" value={item.avgDuration > 0 ? `${item.avgDuration.toFixed(1)}s` : '-'} color="purple" />
230
- <StatItem icon={<XCircle size={14} />} label="Failed" value={item.failedRuns} color="rose" />
231
- </div>
232
-
233
- {item.lastRun && (
234
- <div className="pt-4 border-t border-slate-100 dark:border-slate-700">
235
- <div className="flex items-center justify-between text-xs">
236
- <span className="text-slate-500 flex items-center gap-1">
237
- <Calendar size={12} />
238
- Last run
239
- </span>
240
- <span className="text-slate-700 dark:text-slate-300 font-medium">
241
- {format(new Date(item.lastRun.start_time), 'MMM d, HH:mm')}
242
- </span>
243
- </div>
244
- </div>
245
- )}
246
-
247
- <Link
248
- to={`/runs?pipeline=${encodeURIComponent(item.name)}`}
249
- className="mt-4 flex items-center justify-center gap-2 py-2 px-4 bg-slate-50 dark:bg-slate-700 hover:bg-primary-50 dark:hover:bg-primary-900/20 text-slate-700 dark:text-slate-200 hover:text-primary-600 rounded-lg transition-all group-hover:bg-primary-50"
250
- >
251
- <span className="text-sm font-semibold">View Runs</span>
252
- <ArrowRight size={16} className="group-hover:translate-x-1 transition-transform" />
253
- </Link>
254
- </Card>
64
+ </div>
255
65
  );
256
- };
66
+ }
257
67
 
258
68
  return (
259
- <div className="p-6 max-w-7xl mx-auto">
260
- <DataView
261
- title="Pipelines"
262
- subtitle="View and manage your ML pipelines"
263
- items={pipelines}
264
- loading={loading}
265
- columns={columns}
266
- renderGrid={renderGrid}
267
- actions={
268
- <PipelineProjectSelector
269
- selectedPipelines={selectedPipelines}
270
- onComplete={() => {
271
- fetchData();
272
- setSelectedPipelines([]);
273
- }}
274
- />
275
- }
276
- emptyState={
277
- <div className="text-center py-16 bg-slate-50 dark:bg-slate-800/30 rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-700">
278
- <div className="mx-auto w-20 h-20 bg-slate-100 dark:bg-slate-700 rounded-2xl flex items-center justify-center mb-6">
279
- <Layers className="text-slate-400" size={32} />
280
- </div>
281
- <h3 className="text-xl font-bold text-slate-900 dark:text-white mb-2">No pipelines found</h3>
282
- <p className="text-slate-500 max-w-md mx-auto">
283
- Create your first pipeline by defining steps and running them with flowyml
69
+ <div className="h-screen flex flex-col overflow-hidden bg-slate-50 dark:bg-slate-900">
70
+ {/* Header */}
71
+ <div className="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-6 py-4 shrink-0">
72
+ <div className="flex items-center justify-between max-w-[1800px] mx-auto">
73
+ <div>
74
+ <h1 className="text-xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
75
+ <Layers className="text-primary-500" />
76
+ Pipelines
77
+ </h1>
78
+ <p className="text-sm text-slate-600 dark:text-slate-400">
79
+ View and manage your ML pipelines
284
80
  </p>
285
- </div >
286
- }
287
- />
288
- </div >
289
- );
290
- }
291
-
292
-
293
-
294
- function ProjectSelector({ onSelect }) {
295
- const [isOpen, setIsOpen] = useState(false);
296
- const [projects, setProjects] = useState([]);
297
-
298
- useEffect(() => {
299
- if (isOpen) {
300
- fetch('/api/projects/')
301
- .then(res => res.json())
302
- .then(data => setProjects(data))
303
- .catch(err => console.error('Failed to load projects:', err));
304
- }
305
- }, [isOpen]);
306
-
307
- return (
308
- <div className="relative">
309
- <button
310
- onClick={() => setIsOpen(!isOpen)}
311
- 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"
312
- >
313
- <FolderPlus size={16} />
314
- Add to Project
315
- </button>
316
-
317
- {isOpen && (
318
- <>
319
- <div
320
- className="fixed inset-0 z-10"
321
- onClick={() => setIsOpen(false)}
322
- />
323
- <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">
324
- <div className="p-2 border-b border-slate-100 dark:border-slate-700">
325
- <span className="text-xs font-semibold text-slate-500 px-2">Select Project</span>
326
- </div>
327
- <div className="max-h-64 overflow-y-auto p-1">
328
- {projects.length > 0 ? (
329
- projects.map(p => (
330
- <button
331
- key={p.name}
332
- onClick={() => {
333
- onSelect(p.name);
334
- setIsOpen(false);
335
- }}
336
- 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"
337
- >
338
- {p.name}
339
- </button>
340
- ))
341
- ) : (
342
- <div className="px-3 py-2 text-sm text-slate-400 italic">No projects found</div>
343
- )}
344
- </div>
345
81
  </div>
346
- </>
347
- )}
348
- </div>
349
- );
350
- }
351
-
352
- function StatItem({ icon, label, value, color }) {
353
- const colorClasses = {
354
- blue: 'bg-blue-50 text-blue-600',
355
- emerald: 'bg-emerald-50 text-emerald-600',
356
- purple: 'bg-purple-50 text-purple-600',
357
- rose: 'bg-rose-50 text-rose-600'
358
- };
359
-
360
- return (
361
- <div className="flex items-center gap-2">
362
- <div className={`p-1.5 rounded ${colorClasses[color]}`}>
363
- {icon}
364
- </div>
365
- <div>
366
- <p className="text-xs text-slate-500">{label}</p>
367
- <p className="text-sm font-bold text-slate-900 dark:text-white">{value}</p>
82
+ </div>
368
83
  </div>
369
- </div>
370
- );
371
- }
372
-
373
- function PipelineProjectSelector({ selectedPipelines, onComplete }) {
374
- const [isOpen, setIsOpen] = useState(false);
375
- const [projects, setProjects] = useState([]);
376
- const [updating, setUpdating] = useState(false);
377
84
 
378
- useEffect(() => {
379
- if (isOpen) {
380
- fetch('/api/projects/')
381
- .then(res => res.json())
382
- .then(data => setProjects(data))
383
- .catch(err => console.error('Failed to load projects:', err));
384
- }
385
- }, [isOpen]);
386
-
387
- const handleSelectProject = async (projectName) => {
388
- setUpdating(true);
389
- try {
390
- const updates = selectedPipelines.map(pipelineName =>
391
- fetch(`/api/pipelines/${pipelineName}/project`, {
392
- method: 'PUT',
393
- headers: { 'Content-Type': 'application/json' },
394
- body: JSON.stringify({ project_name: projectName })
395
- })
396
- );
397
-
398
- await Promise.all(updates);
399
-
400
- const toast = document.createElement('div');
401
- toast.className = 'fixed top-4 right-4 px-4 py-3 rounded-lg shadow-lg z-50 bg-green-500 text-white';
402
- toast.textContent = `Added ${selectedPipelines.length} pipeline(s) to project ${projectName}`;
403
- document.body.appendChild(toast);
404
- setTimeout(() => document.body.removeChild(toast), 3000);
405
-
406
- setIsOpen(false);
407
- if (onComplete) onComplete();
408
- } catch (error) {
409
- console.error('Failed to update projects:', error);
410
- } finally {
411
- setUpdating(false);
412
- }
413
- };
414
-
415
- return (
416
- <div className="relative">
417
- <button
418
- onClick={() => setIsOpen(!isOpen)}
419
- disabled={updating || selectedPipelines.length === 0}
420
- 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"
421
- >
422
- <FolderPlus size={16} />
423
- {updating ? 'Updating...' : `Add to Project (${selectedPipelines.length})`}
424
- </button>
425
-
426
- {isOpen && (
427
- <>
428
- <div className="fixed inset-0 z-10" onClick={() => setIsOpen(false)} />
429
- <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">
430
- <div className="p-2 border-b border-slate-100 dark:border-slate-700">
431
- <span className="text-xs font-semibold text-slate-500 px-2">Select Project</span>
85
+ {/* Main Content */}
86
+ <div className="flex-1 overflow-hidden">
87
+ <div className="h-full max-w-[1800px] mx-auto px-6 py-6">
88
+ <div className="h-full flex gap-6">
89
+ {/* Left Sidebar - Navigation */}
90
+ <div className="w-[320px] shrink-0 flex flex-col bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden shadow-sm">
91
+ <div className="p-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50">
92
+ <h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Explorer</h3>
93
+ </div>
94
+ <div className="flex-1 min-h-0">
95
+ <NavigationTree
96
+ mode="pipelines"
97
+ projectId={selectedProject}
98
+ onSelect={handlePipelineSelect}
99
+ selectedId={selectedPipeline?.name}
100
+ />
101
+ </div>
432
102
  </div>
433
- <div className="max-h-64 overflow-y-auto p-1">
434
- {projects.length > 0 ? (
435
- projects.map(p => (
436
- <button
437
- key={p.name}
438
- onClick={() => handleSelectProject(p.name)}
439
- disabled={updating}
440
- 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"
441
- >
442
- {p.name}
443
- </button>
444
- ))
103
+
104
+ {/* Right Content - Details Panel or Empty State */}
105
+ <div className="flex-1 min-w-0 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden shadow-sm">
106
+ {selectedPipeline ? (
107
+ <PipelineDetailsPanel
108
+ pipeline={selectedPipeline}
109
+ onClose={() => setSelectedPipeline(null)}
110
+ onProjectUpdate={fetchData}
111
+ />
445
112
  ) : (
446
- <div className="px-3 py-2 text-sm text-slate-400 italic">No projects found</div>
113
+ <div className="h-full flex flex-col items-center justify-center text-center p-8 bg-slate-50/50 dark:bg-slate-900/50">
114
+ <div className="w-20 h-20 bg-primary-100 dark:bg-primary-900/30 rounded-full flex items-center justify-center mb-6 animate-pulse">
115
+ <Layers size={40} className="text-primary-500" />
116
+ </div>
117
+ <h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">
118
+ Select a Pipeline
119
+ </h2>
120
+ <p className="text-slate-500 max-w-md">
121
+ Choose a pipeline from the sidebar to view execution history and statistics.
122
+ </p>
123
+ </div>
447
124
  )}
448
125
  </div>
449
126
  </div>
450
- </>
451
- )}
127
+ </div>
128
+ </div>
452
129
  </div>
453
130
  );
454
131
  }