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,454 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { fetchApi } from '../../utils/api';
3
+ import { Link } from 'react-router-dom';
4
+ import { Layers, Play, Clock, CheckCircle, XCircle, TrendingUp, Calendar, Activity, ArrowRight, Zap, 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 { DataView } from '../../components/ui/DataView';
9
+ import { useProject } from '../../contexts/ProjectContext';
10
+
11
+ export function Pipelines() {
12
+ const [pipelines, setPipelines] = useState([]);
13
+ const [loading, setLoading] = useState(true);
14
+ const [selectedPipelines, setSelectedPipelines] = useState([]);
15
+ const { selectedProject } = useProject();
16
+
17
+ const fetchData = async () => {
18
+ setLoading(true);
19
+ try {
20
+ 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';
26
+
27
+ 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;
52
+
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
+ });
64
+
65
+ setPipelines(pipelinesWithStats);
66
+ } catch (err) {
67
+ console.error(err);
68
+ } finally {
69
+ setLoading(false);
70
+ }
71
+ };
72
+
73
+ useEffect(() => {
74
+ fetchData();
75
+ }, [selectedProject]);
76
+
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];
201
+
202
+ 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>
220
+ </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>
255
+ );
256
+ };
257
+
258
+ 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
284
+ </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
+ </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>
368
+ </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
+
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>
432
+ </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
+ ))
445
+ ) : (
446
+ <div className="px-3 py-2 text-sm text-slate-400 italic">No projects found</div>
447
+ )}
448
+ </div>
449
+ </div>
450
+ </>
451
+ )}
452
+ </div>
453
+ );
454
+ }
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+ import { Package } from 'lucide-react';
3
+ import { motion } from 'framer-motion';
4
+ import { PluginManager } from '../../components/plugins/PluginManager';
5
+
6
+ export function Plugins() {
7
+ const container = {
8
+ hidden: { opacity: 0 },
9
+ show: {
10
+ opacity: 1,
11
+ transition: {
12
+ staggerChildren: 0.1
13
+ }
14
+ }
15
+ };
16
+
17
+ const item = {
18
+ hidden: { opacity: 0, y: 20 },
19
+ show: { opacity: 1, y: 0 }
20
+ };
21
+
22
+ return (
23
+ <motion.div
24
+ initial="hidden"
25
+ animate="show"
26
+ variants={container}
27
+ className="space-y-6"
28
+ >
29
+ {/* Header */}
30
+ <motion.div variants={item}>
31
+ <div className="flex items-center gap-3 mb-2">
32
+ <div className="p-2 bg-gradient-to-br from-purple-600 to-purple-800 rounded-lg">
33
+ <Package className="text-white" size={24} />
34
+ </div>
35
+ <h2 className="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">Plugins & Integrations</h2>
36
+ </div>
37
+ <p className="text-slate-500 dark:text-slate-400 mt-2">
38
+ Extend flowyml with plugins from ZenML, Airflow, and other ecosystems. Browse, install, and manage integrations seamlessly.
39
+ </p>
40
+ </motion.div>
41
+
42
+ {/* Plugin Manager */}
43
+ <motion.div variants={item}>
44
+ <PluginManager />
45
+ </motion.div>
46
+ </motion.div>
47
+ );
48
+ }