flowyml 1.2.0__py3-none-any.whl → 1.3.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 +3 -0
- flowyml/assets/base.py +10 -0
- flowyml/assets/metrics.py +6 -0
- flowyml/cli/main.py +108 -2
- flowyml/cli/run.py +9 -2
- flowyml/core/execution_status.py +52 -0
- flowyml/core/hooks.py +106 -0
- flowyml/core/observability.py +210 -0
- flowyml/core/orchestrator.py +274 -0
- flowyml/core/pipeline.py +193 -231
- flowyml/core/project.py +34 -2
- flowyml/core/remote_orchestrator.py +109 -0
- flowyml/core/resources.py +22 -5
- flowyml/core/retry_policy.py +80 -0
- flowyml/core/step.py +18 -1
- flowyml/core/submission_result.py +53 -0
- flowyml/integrations/keras.py +95 -22
- flowyml/monitoring/alerts.py +2 -2
- flowyml/stacks/__init__.py +15 -0
- flowyml/stacks/aws.py +599 -0
- flowyml/stacks/azure.py +295 -0
- flowyml/stacks/components.py +24 -2
- flowyml/stacks/gcp.py +158 -11
- flowyml/stacks/local.py +5 -0
- flowyml/storage/artifacts.py +15 -5
- flowyml/storage/materializers/__init__.py +2 -0
- flowyml/storage/materializers/cloudpickle.py +74 -0
- flowyml/storage/metadata.py +166 -5
- flowyml/ui/backend/main.py +41 -1
- flowyml/ui/backend/routers/assets.py +356 -15
- flowyml/ui/backend/routers/client.py +46 -0
- flowyml/ui/backend/routers/execution.py +13 -2
- flowyml/ui/backend/routers/experiments.py +48 -12
- flowyml/ui/backend/routers/metrics.py +213 -0
- flowyml/ui/backend/routers/pipelines.py +63 -7
- flowyml/ui/backend/routers/projects.py +33 -7
- flowyml/ui/backend/routers/runs.py +150 -8
- flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
- flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/src/App.jsx +4 -1
- flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
- flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
- flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
- flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
- flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
- flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
- flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
- flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
- flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
- flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
- flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
- flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
- flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
- flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
- flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
- flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
- flowyml/ui/frontend/src/router/index.jsx +4 -0
- flowyml/ui/frontend/src/utils/date.js +10 -0
- flowyml/ui/frontend/src/utils/downloads.js +11 -0
- flowyml/utils/config.py +6 -0
- flowyml/utils/stack_config.py +45 -3
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/METADATA +42 -4
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/RECORD +89 -52
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/licenses/LICENSE +1 -1
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/WHEEL +0 -0
- {flowyml-1.2.0.dist-info → flowyml-1.3.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 [
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
<
|
|
204
|
-
<div className="
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
</
|
|
208
|
-
<
|
|
209
|
-
{
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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="
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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="
|
|
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
|
}
|