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
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { fetchApi } from '../utils/api';
|
|
3
|
+
import {
|
|
4
|
+
PlayCircle,
|
|
5
|
+
Calendar,
|
|
6
|
+
Clock,
|
|
7
|
+
CheckCircle,
|
|
8
|
+
XCircle,
|
|
9
|
+
Activity,
|
|
10
|
+
ArrowRight,
|
|
11
|
+
Box,
|
|
12
|
+
FileText,
|
|
13
|
+
Layers,
|
|
14
|
+
X,
|
|
15
|
+
Terminal,
|
|
16
|
+
Cpu
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { Card } from './ui/Card';
|
|
19
|
+
import { Badge } from './ui/Badge';
|
|
20
|
+
import { Button } from './ui/Button';
|
|
21
|
+
import { format } from 'date-fns';
|
|
22
|
+
import { Link } from 'react-router-dom';
|
|
23
|
+
import { motion } from 'framer-motion';
|
|
24
|
+
import { StatusBadge } from './ui/ExecutionStatus';
|
|
25
|
+
import { PipelineGraph } from './PipelineGraph';
|
|
26
|
+
import { ProjectSelector } from './ProjectSelector';
|
|
27
|
+
|
|
28
|
+
export function RunDetailsPanel({ run, onClose }) {
|
|
29
|
+
const [details, setDetails] = useState(null);
|
|
30
|
+
const [loading, setLoading] = useState(false);
|
|
31
|
+
const [activeTab, setActiveTab] = useState('overview'); // overview, steps, artifacts
|
|
32
|
+
const [currentProject, setCurrentProject] = useState(run?.project);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (run) {
|
|
36
|
+
fetchRunDetails();
|
|
37
|
+
setCurrentProject(run.project);
|
|
38
|
+
}
|
|
39
|
+
}, [run]);
|
|
40
|
+
|
|
41
|
+
const fetchRunDetails = async () => {
|
|
42
|
+
setLoading(true);
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetchApi(`/api/runs/${run.run_id}`);
|
|
45
|
+
const data = await res.json();
|
|
46
|
+
setDetails(data);
|
|
47
|
+
if (data.project) setCurrentProject(data.project);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Failed to fetch run details:', error);
|
|
50
|
+
} finally {
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleProjectUpdate = async (newProject) => {
|
|
56
|
+
try {
|
|
57
|
+
await fetchApi(`/api/runs/${run.run_id}/project`, {
|
|
58
|
+
method: 'PUT',
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify({ project_name: newProject })
|
|
63
|
+
});
|
|
64
|
+
setCurrentProject(newProject);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Failed to update project:', error);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (!run) return null;
|
|
71
|
+
|
|
72
|
+
const runData = details || run;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="h-full flex flex-col bg-white dark:bg-slate-900">
|
|
76
|
+
{/* Header */}
|
|
77
|
+
<div className="p-6 border-b border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/50">
|
|
78
|
+
<div className="flex items-start justify-between mb-4">
|
|
79
|
+
<div className="flex items-center gap-4">
|
|
80
|
+
<div className="p-3 bg-blue-100 dark:bg-blue-900/30 rounded-xl text-blue-600 dark:text-blue-400">
|
|
81
|
+
<PlayCircle size={24} />
|
|
82
|
+
</div>
|
|
83
|
+
<div>
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">
|
|
86
|
+
{runData.name || `Run ${runData.run_id?.slice(0, 8)}`}
|
|
87
|
+
</h2>
|
|
88
|
+
<StatusBadge status={runData.status} />
|
|
89
|
+
</div>
|
|
90
|
+
<div className="flex items-center gap-2 mt-1 text-sm text-slate-500">
|
|
91
|
+
<span className="font-mono text-xs bg-slate-100 dark:bg-slate-800 px-1.5 py-0.5 rounded">
|
|
92
|
+
{runData.run_id}
|
|
93
|
+
</span>
|
|
94
|
+
<span>•</span>
|
|
95
|
+
<ProjectSelector
|
|
96
|
+
currentProject={currentProject}
|
|
97
|
+
onUpdate={handleProjectUpdate}
|
|
98
|
+
/>
|
|
99
|
+
{runData.pipeline_name && (
|
|
100
|
+
<>
|
|
101
|
+
<span>•</span>
|
|
102
|
+
<span className="flex items-center gap-1">
|
|
103
|
+
<Layers size={12} />
|
|
104
|
+
{runData.pipeline_name}
|
|
105
|
+
</span>
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
<div className="flex items-center gap-2">
|
|
112
|
+
<Link to={`/runs/${runData.run_id}`}>
|
|
113
|
+
<Button variant="outline" size="sm">
|
|
114
|
+
Full View <ArrowRight size={16} className="ml-1" />
|
|
115
|
+
</Button>
|
|
116
|
+
</Link>
|
|
117
|
+
<Button variant="ghost" size="sm" onClick={onClose}>
|
|
118
|
+
<X size={20} className="text-slate-400" />
|
|
119
|
+
</Button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Stats Grid */}
|
|
124
|
+
<div className="grid grid-cols-3 gap-4">
|
|
125
|
+
<StatCard
|
|
126
|
+
label="Duration"
|
|
127
|
+
value={runData.duration ? `${runData.duration.toFixed(2)}s` : '-'}
|
|
128
|
+
icon={Clock}
|
|
129
|
+
color="blue"
|
|
130
|
+
/>
|
|
131
|
+
<StatCard
|
|
132
|
+
label="Started"
|
|
133
|
+
value={runData.start_time ? format(new Date(runData.start_time), 'MMM d, HH:mm') : '-'}
|
|
134
|
+
icon={Calendar}
|
|
135
|
+
color="purple"
|
|
136
|
+
/>
|
|
137
|
+
<StatCard
|
|
138
|
+
label="Steps"
|
|
139
|
+
value={runData.steps ? Object.keys(runData.steps).length : 0}
|
|
140
|
+
icon={Activity}
|
|
141
|
+
color="emerald"
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Content */}
|
|
147
|
+
<div className="flex-1 overflow-hidden flex flex-col">
|
|
148
|
+
{/* Tabs */}
|
|
149
|
+
<div className="flex items-center gap-1 p-2 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
|
150
|
+
<TabButton
|
|
151
|
+
active={activeTab === 'overview'}
|
|
152
|
+
onClick={() => setActiveTab('overview')}
|
|
153
|
+
icon={Activity}
|
|
154
|
+
label="Overview"
|
|
155
|
+
/>
|
|
156
|
+
<TabButton
|
|
157
|
+
active={activeTab === 'steps'}
|
|
158
|
+
onClick={() => setActiveTab('steps')}
|
|
159
|
+
icon={Layers}
|
|
160
|
+
label="Steps"
|
|
161
|
+
/>
|
|
162
|
+
<TabButton
|
|
163
|
+
active={activeTab === 'artifacts'}
|
|
164
|
+
onClick={() => setActiveTab('artifacts')}
|
|
165
|
+
icon={Box}
|
|
166
|
+
label="Artifacts"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div className="flex-1 overflow-y-auto bg-slate-50 dark:bg-slate-900/50 p-4">
|
|
171
|
+
{loading ? (
|
|
172
|
+
<div className="flex justify-center py-12">
|
|
173
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
|
174
|
+
</div>
|
|
175
|
+
) : (
|
|
176
|
+
<>
|
|
177
|
+
{activeTab === 'overview' && (
|
|
178
|
+
<div className="space-y-4">
|
|
179
|
+
{/* DAG Visualization Preview */}
|
|
180
|
+
<Card className="p-0 overflow-hidden h-64 border-slate-200 dark:border-slate-700">
|
|
181
|
+
<div className="p-3 border-b border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 flex justify-between items-center">
|
|
182
|
+
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300">Pipeline Graph</h3>
|
|
183
|
+
</div>
|
|
184
|
+
<div className="h-full bg-slate-50/50">
|
|
185
|
+
{runData.dag ? (
|
|
186
|
+
<PipelineGraph
|
|
187
|
+
dag={runData.dag}
|
|
188
|
+
steps={runData.steps}
|
|
189
|
+
/>
|
|
190
|
+
) : (
|
|
191
|
+
<div className="h-full flex items-center justify-center text-slate-400 text-sm">
|
|
192
|
+
No graph data available
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</Card>
|
|
197
|
+
|
|
198
|
+
{/* Error Display if Failed */}
|
|
199
|
+
{runData.status === 'failed' && runData.error && (
|
|
200
|
+
<div className="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-800 rounded-xl p-4">
|
|
201
|
+
<h3 className="text-rose-700 dark:text-rose-400 font-semibold flex items-center gap-2 mb-2">
|
|
202
|
+
<XCircle size={18} />
|
|
203
|
+
Execution Failed
|
|
204
|
+
</h3>
|
|
205
|
+
<pre className="text-xs font-mono text-rose-600 dark:text-rose-300 whitespace-pre-wrap overflow-x-auto">
|
|
206
|
+
{runData.error}
|
|
207
|
+
</pre>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
|
|
213
|
+
{activeTab === 'steps' && (
|
|
214
|
+
<div className="space-y-3">
|
|
215
|
+
{runData.steps && Object.entries(runData.steps).map(([stepId, step]) => (
|
|
216
|
+
<div key={stepId} className="bg-white dark:bg-slate-800 p-3 rounded-xl border border-slate-200 dark:border-slate-700">
|
|
217
|
+
<div className="flex items-center justify-between mb-2">
|
|
218
|
+
<div className="flex items-center gap-2">
|
|
219
|
+
<StatusBadge status={step.success ? 'completed' : step.error ? 'failed' : 'running'} size="sm" />
|
|
220
|
+
<span className="font-medium text-slate-900 dark:text-white">{stepId}</span>
|
|
221
|
+
</div>
|
|
222
|
+
<span className="text-xs text-slate-500 font-mono">
|
|
223
|
+
{step.duration ? `${step.duration.toFixed(2)}s` : '-'}
|
|
224
|
+
</span>
|
|
225
|
+
</div>
|
|
226
|
+
{step.error && (
|
|
227
|
+
<div className="text-xs text-rose-600 bg-rose-50 dark:bg-rose-900/20 p-2 rounded mt-2 font-mono">
|
|
228
|
+
{step.error}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
))}
|
|
233
|
+
{(!runData.steps || Object.keys(runData.steps).length === 0) && (
|
|
234
|
+
<div className="text-center py-8 text-slate-500">No steps recorded</div>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{activeTab === 'artifacts' && (
|
|
240
|
+
<div className="space-y-3">
|
|
241
|
+
{/* This would need actual artifact data structure */}
|
|
242
|
+
<div className="text-center py-8 text-slate-500">
|
|
243
|
+
<Box size={32} className="mx-auto mb-2 opacity-50" />
|
|
244
|
+
<p>Artifacts view not fully implemented in preview</p>
|
|
245
|
+
<Link to={`/runs/${runData.run_id}`} className="text-primary-600 hover:underline text-sm mt-2 inline-block">
|
|
246
|
+
View in full details page
|
|
247
|
+
</Link>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
</>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function StatCard({ label, value, icon: Icon, color }) {
|
|
260
|
+
const colorClasses = {
|
|
261
|
+
blue: 'text-blue-600 bg-blue-50 dark:bg-blue-900/20',
|
|
262
|
+
emerald: 'text-emerald-600 bg-emerald-50 dark:bg-emerald-900/20',
|
|
263
|
+
purple: 'text-purple-600 bg-purple-50 dark:bg-purple-900/20',
|
|
264
|
+
rose: 'text-rose-600 bg-rose-50 dark:bg-rose-900/20',
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div className="bg-white dark:bg-slate-800 p-3 rounded-xl border border-slate-200 dark:border-slate-700">
|
|
269
|
+
<div className="flex items-center gap-2 mb-1">
|
|
270
|
+
<div className={`p-1 rounded-lg ${colorClasses[color]}`}>
|
|
271
|
+
<Icon size={14} />
|
|
272
|
+
</div>
|
|
273
|
+
<span className="text-xs text-slate-500 font-medium">{label}</span>
|
|
274
|
+
</div>
|
|
275
|
+
<div className="text-sm font-bold text-slate-900 dark:text-white pl-1 truncate" title={value}>
|
|
276
|
+
{value}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function TabButton({ active, onClick, icon: Icon, label }) {
|
|
283
|
+
return (
|
|
284
|
+
<button
|
|
285
|
+
onClick={onClick}
|
|
286
|
+
className={`
|
|
287
|
+
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
|
|
288
|
+
${active
|
|
289
|
+
? 'bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-white'
|
|
290
|
+
: 'text-slate-500 hover:bg-slate-50 dark:hover:bg-slate-800/50 hover:text-slate-700 dark:hover:text-slate-300'
|
|
291
|
+
}
|
|
292
|
+
`}
|
|
293
|
+
>
|
|
294
|
+
<Icon size={16} />
|
|
295
|
+
{label}
|
|
296
|
+
</button>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useLocation, Link } from 'react-router-dom';
|
|
3
|
-
import { Sun, Moon, ChevronRight, Home } from 'lucide-react';
|
|
3
|
+
import { Sun, Moon, ChevronRight, Home, Server, ExternalLink } from 'lucide-react';
|
|
4
4
|
import { useTheme } from '../../contexts/ThemeContext';
|
|
5
5
|
import { ProjectSelector } from '../ui/ProjectSelector';
|
|
6
|
+
import { useConfig } from '../../utils/api';
|
|
6
7
|
|
|
7
8
|
export function Header() {
|
|
8
9
|
const { theme, toggleTheme } = useTheme();
|
|
9
10
|
const location = useLocation();
|
|
11
|
+
const { config, loading } = useConfig();
|
|
10
12
|
|
|
11
13
|
// Generate breadcrumbs from path
|
|
12
14
|
const pathnames = location.pathname.split('/').filter((x) => x);
|
|
15
|
+
const isRemoteStack = !loading && config?.execution_mode === 'remote';
|
|
16
|
+
const remoteServices = isRemoteStack && Array.isArray(config?.remote_services)
|
|
17
|
+
? config.remote_services
|
|
18
|
+
: [];
|
|
13
19
|
|
|
14
20
|
return (
|
|
15
21
|
<header className="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-6 py-4 flex items-center justify-between shadow-sm z-10">
|
|
@@ -51,6 +57,47 @@ export function Header() {
|
|
|
51
57
|
</div>
|
|
52
58
|
|
|
53
59
|
<div className="flex items-center gap-4">
|
|
60
|
+
{isRemoteStack && (
|
|
61
|
+
<div className="flex flex-col gap-2 px-4 py-2 rounded-xl bg-primary-50 text-primary-800 border border-primary-100 dark:bg-primary-900/20 dark:text-primary-200 dark:border-primary-900/40">
|
|
62
|
+
<div className="flex items-center gap-2 text-xs uppercase tracking-wide font-semibold">
|
|
63
|
+
<Server size={14} /> Remote Stack
|
|
64
|
+
</div>
|
|
65
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
66
|
+
{config?.remote_ui_url && (
|
|
67
|
+
<a
|
|
68
|
+
href={config.remote_ui_url}
|
|
69
|
+
target="_blank"
|
|
70
|
+
rel="noopener noreferrer"
|
|
71
|
+
className="inline-flex items-center gap-1 text-xs font-medium bg-white/70 dark:bg-slate-800/40 px-2 py-1 rounded-lg hover:underline"
|
|
72
|
+
>
|
|
73
|
+
UI <ExternalLink size={12} />
|
|
74
|
+
</a>
|
|
75
|
+
)}
|
|
76
|
+
{config?.remote_server_url && (
|
|
77
|
+
<a
|
|
78
|
+
href={config.remote_server_url}
|
|
79
|
+
target="_blank"
|
|
80
|
+
rel="noopener noreferrer"
|
|
81
|
+
className="inline-flex items-center gap-1 text-xs font-medium bg-white/70 dark:bg-slate-800/40 px-2 py-1 rounded-lg hover:underline"
|
|
82
|
+
>
|
|
83
|
+
API <ExternalLink size={12} />
|
|
84
|
+
</a>
|
|
85
|
+
)}
|
|
86
|
+
{remoteServices.map((service, idx) => (
|
|
87
|
+
<a
|
|
88
|
+
key={`${service?.name || service?.label || 'service'}-${idx}`}
|
|
89
|
+
href={service?.url || service?.link}
|
|
90
|
+
target="_blank"
|
|
91
|
+
rel="noopener noreferrer"
|
|
92
|
+
className="inline-flex items-center gap-1 text-xs font-medium bg-white/70 dark:bg-slate-800/40 px-2 py-1 rounded-lg hover:underline"
|
|
93
|
+
>
|
|
94
|
+
{service?.label || service?.name || 'Service'} <ExternalLink size={12} />
|
|
95
|
+
</a>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
|
|
54
101
|
<ProjectSelector />
|
|
55
102
|
|
|
56
103
|
<div className="h-6 w-px bg-slate-200 dark:bg-slate-700 mx-2" />
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Server, ArrowRight, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
|
|
3
|
+
import { Button } from '../ui/Button';
|
|
4
|
+
import { Card } from '../ui/Card';
|
|
5
|
+
import { pluginService } from '../../services/pluginService';
|
|
6
|
+
|
|
7
|
+
export function ZenMLIntegration() {
|
|
8
|
+
const [stackName, setStackName] = useState('');
|
|
9
|
+
const [status, setStatus] = useState('idle'); // idle, importing, success, error
|
|
10
|
+
const [logs, setLogs] = useState([]);
|
|
11
|
+
|
|
12
|
+
const handleImport = async () => {
|
|
13
|
+
if (!stackName) return;
|
|
14
|
+
|
|
15
|
+
setStatus('importing');
|
|
16
|
+
setLogs(['Connecting to ZenML client...', 'Fetching stack details...']);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const result = await pluginService.importZenMLStack(stackName);
|
|
20
|
+
setLogs(prev => [...prev, `Found stack '${stackName}' with ${result.components.length} components.`]);
|
|
21
|
+
|
|
22
|
+
// Artificial delay for UX to show the progress
|
|
23
|
+
await new Promise(r => setTimeout(r, 800));
|
|
24
|
+
|
|
25
|
+
setLogs(prev => [...prev, 'Generating flowyml configuration...', 'Import successful!']);
|
|
26
|
+
setStatus('success');
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('Import failed:', error);
|
|
29
|
+
setLogs(prev => [...prev, `Error: ${error.message}`]);
|
|
30
|
+
setStatus('error');
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="space-y-6">
|
|
36
|
+
<div className="bg-slate-50 dark:bg-slate-800/50 p-4 rounded-xl border border-slate-200 dark:border-slate-700">
|
|
37
|
+
<div className="flex items-start gap-3">
|
|
38
|
+
<Server className="text-primary-500 mt-1" size={20} />
|
|
39
|
+
<div>
|
|
40
|
+
<h3 className="font-medium text-slate-900 dark:text-white">Import ZenML Stack</h3>
|
|
41
|
+
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
|
42
|
+
Migrate your existing ZenML infrastructure to flowyml. We'll automatically detect your components and generate the necessary configuration.
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="flex gap-4 items-end">
|
|
49
|
+
<div className="flex-1 space-y-2">
|
|
50
|
+
<label className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
51
|
+
ZenML Stack Name
|
|
52
|
+
</label>
|
|
53
|
+
<input
|
|
54
|
+
type="text"
|
|
55
|
+
placeholder="e.g., production-stack"
|
|
56
|
+
value={stackName}
|
|
57
|
+
onChange={(e) => setStackName(e.target.value)}
|
|
58
|
+
className="w-full px-3 py-2 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
<Button
|
|
62
|
+
onClick={handleImport}
|
|
63
|
+
disabled={status === 'importing' || !stackName}
|
|
64
|
+
className="flex items-center gap-2"
|
|
65
|
+
>
|
|
66
|
+
{status === 'importing' ? (
|
|
67
|
+
<>
|
|
68
|
+
<Loader2 size={16} className="animate-spin" />
|
|
69
|
+
Importing...
|
|
70
|
+
</>
|
|
71
|
+
) : (
|
|
72
|
+
<>
|
|
73
|
+
Start Import
|
|
74
|
+
<ArrowRight size={16} />
|
|
75
|
+
</>
|
|
76
|
+
)}
|
|
77
|
+
</Button>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{status !== 'idle' && (
|
|
81
|
+
<div className="bg-slate-900 rounded-xl p-4 font-mono text-sm overflow-hidden">
|
|
82
|
+
<div className="space-y-1">
|
|
83
|
+
{logs.map((log, i) => (
|
|
84
|
+
<div key={i} className="flex items-center gap-2 text-slate-300">
|
|
85
|
+
<span className="text-slate-600">➜</span>
|
|
86
|
+
{log}
|
|
87
|
+
</div>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
{status === 'success' && (
|
|
91
|
+
<div className="mt-4 pt-4 border-t border-slate-800 flex items-center gap-2 text-green-400">
|
|
92
|
+
<CheckCircle size={16} />
|
|
93
|
+
<span>Stack imported successfully! You can now use it in your pipelines.</span>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
{status === 'error' && (
|
|
97
|
+
<div className="mt-4 pt-4 border-t border-slate-800 flex items-center gap-2 text-red-400">
|
|
98
|
+
<AlertCircle size={16} />
|
|
99
|
+
<span>Failed to import stack. Please check the name and try again.</span>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -14,20 +14,42 @@ import {
|
|
|
14
14
|
Package,
|
|
15
15
|
ChevronLeft,
|
|
16
16
|
ChevronRight,
|
|
17
|
-
Menu
|
|
17
|
+
Menu,
|
|
18
|
+
Activity
|
|
18
19
|
} from 'lucide-react';
|
|
19
20
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
20
21
|
|
|
21
|
-
const
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
|
|
22
|
+
const NAV_GROUPS = [
|
|
23
|
+
{
|
|
24
|
+
title: 'Workspace',
|
|
25
|
+
items: [
|
|
26
|
+
{ icon: LayoutDashboard, label: 'Dashboard', path: '/' },
|
|
27
|
+
{ icon: FolderKanban, label: 'Projects', path: '/projects' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: 'Automation',
|
|
32
|
+
items: [
|
|
33
|
+
{ icon: PlayCircle, label: 'Pipelines', path: '/pipelines' },
|
|
34
|
+
{ icon: Calendar, label: 'Schedules', path: '/schedules' },
|
|
35
|
+
{ icon: PlayCircle, label: 'Runs', path: '/runs' },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
title: 'Insights',
|
|
40
|
+
items: [
|
|
41
|
+
{ icon: Trophy, label: 'Leaderboard', path: '/leaderboard' },
|
|
42
|
+
{ icon: FlaskConical, label: 'Experiments', path: '/experiments' },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
title: 'Data & Observability',
|
|
47
|
+
items: [
|
|
48
|
+
{ icon: Database, label: 'Assets', path: '/assets' },
|
|
49
|
+
{ icon: MessageSquare, label: 'Traces', path: '/traces' },
|
|
50
|
+
{ icon: Activity, label: 'Observability', path: '/observability' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
31
53
|
];
|
|
32
54
|
|
|
33
55
|
const SETTINGS_LINKS = [
|
|
@@ -65,22 +87,26 @@ export function Sidebar({ collapsed, setCollapsed }) {
|
|
|
65
87
|
</div>
|
|
66
88
|
|
|
67
89
|
{/* Navigation */}
|
|
68
|
-
<nav className="flex-1 p-4 space-y-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
<nav className="flex-1 p-4 space-y-4 overflow-y-auto overflow-x-hidden scrollbar-thin scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700">
|
|
91
|
+
{NAV_GROUPS.map((group) => (
|
|
92
|
+
<div key={group.title} className="space-y-1">
|
|
93
|
+
<div className={`px-4 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider transition-opacity duration-200 ${collapsed ? 'opacity-0 h-0' : 'opacity-100'}`}>
|
|
94
|
+
{group.title}
|
|
95
|
+
</div>
|
|
96
|
+
{group.items.map((link) => (
|
|
97
|
+
<NavItem
|
|
98
|
+
key={link.path}
|
|
99
|
+
to={link.path}
|
|
100
|
+
icon={link.icon}
|
|
101
|
+
label={link.label}
|
|
102
|
+
collapsed={collapsed}
|
|
103
|
+
isActive={location.pathname === link.path}
|
|
104
|
+
/>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
81
107
|
))}
|
|
82
108
|
|
|
83
|
-
<div className={`px-4 py-2 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider mt-
|
|
109
|
+
<div className={`px-4 py-2 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider mt-2 transition-opacity duration-200 ${collapsed ? 'opacity-0 h-0' : 'opacity-100'}`}>
|
|
84
110
|
Settings
|
|
85
111
|
</div>
|
|
86
112
|
{SETTINGS_LINKS.map((link) => (
|
|
@@ -100,7 +126,7 @@ export function Sidebar({ collapsed, setCollapsed }) {
|
|
|
100
126
|
<div className={`bg-slate-50 dark:bg-slate-900 rounded-lg p-4 border border-slate-100 dark:border-slate-700 transition-all duration-200 ${collapsed ? 'p-2 flex justify-center' : ''}`}>
|
|
101
127
|
{!collapsed ? (
|
|
102
128
|
<>
|
|
103
|
-
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 whitespace-nowrap">flowyml
|
|
129
|
+
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 whitespace-nowrap">flowyml v1.3.0</p>
|
|
104
130
|
<p className="text-xs text-slate-400 dark:text-slate-500 mt-1 whitespace-nowrap">Local Environment</p>
|
|
105
131
|
</>
|
|
106
132
|
) : (
|