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,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
|
+
}
|