flowyml 1.2.0__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +34 -17
- flowyml/core/retry_policy.py +80 -0
- flowyml/core/scheduler.py +9 -9
- flowyml/core/scheduler_config.py +2 -3
- 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/bridge.py +9 -9
- flowyml/stacks/components.py +24 -2
- flowyml/stacks/gcp.py +158 -11
- flowyml/stacks/local.py +5 -0
- flowyml/stacks/plugins.py +2 -2
- flowyml/stacks/registry.py +21 -0
- flowyml/storage/artifacts.py +15 -5
- flowyml/storage/materializers/__init__.py +2 -0
- flowyml/storage/materializers/base.py +33 -0
- flowyml/storage/materializers/cloudpickle.py +74 -0
- flowyml/storage/metadata.py +3 -881
- flowyml/storage/remote.py +590 -0
- flowyml/storage/sql.py +911 -0
- flowyml/ui/backend/dependencies.py +28 -0
- flowyml/ui/backend/main.py +43 -80
- flowyml/ui/backend/routers/assets.py +483 -17
- flowyml/ui/backend/routers/client.py +46 -0
- flowyml/ui/backend/routers/execution.py +13 -2
- flowyml/ui/backend/routers/experiments.py +97 -14
- flowyml/ui/backend/routers/metrics.py +168 -0
- flowyml/ui/backend/routers/pipelines.py +77 -12
- flowyml/ui/backend/routers/projects.py +33 -7
- flowyml/ui/backend/routers/runs.py +221 -12
- flowyml/ui/backend/routers/schedules.py +5 -21
- flowyml/ui/backend/routers/stats.py +14 -0
- flowyml/ui/backend/routers/traces.py +37 -53
- 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.4.0.dist-info}/METADATA +44 -4
- flowyml-1.4.0.dist-info/RECORD +200 -0
- {flowyml-1.2.0.dist-info → flowyml-1.4.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/RECORD +0 -159
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/WHEEL +0 -0
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
X,
|
|
4
|
+
Download,
|
|
5
|
+
Box,
|
|
6
|
+
Database,
|
|
7
|
+
BarChart2,
|
|
8
|
+
FileText,
|
|
9
|
+
Calendar,
|
|
10
|
+
Tag,
|
|
11
|
+
FileBox,
|
|
12
|
+
Activity,
|
|
13
|
+
GitBranch,
|
|
14
|
+
Layers,
|
|
15
|
+
HardDrive,
|
|
16
|
+
ExternalLink,
|
|
17
|
+
Info
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
import { Card } from './ui/Card';
|
|
20
|
+
import { Badge } from './ui/Badge';
|
|
21
|
+
import { Button } from './ui/Button';
|
|
22
|
+
import { format } from 'date-fns';
|
|
23
|
+
import { downloadArtifactById } from '../utils/downloads';
|
|
24
|
+
import { Link } from 'react-router-dom';
|
|
25
|
+
import { motion } from 'framer-motion';
|
|
26
|
+
import { ProjectSelector } from './ProjectSelector';
|
|
27
|
+
import { fetchApi } from '../utils/api';
|
|
28
|
+
|
|
29
|
+
export function AssetDetailsPanel({ asset, onClose }) {
|
|
30
|
+
const [activeTab, setActiveTab] = useState('overview');
|
|
31
|
+
const [currentProject, setCurrentProject] = useState(asset?.project);
|
|
32
|
+
|
|
33
|
+
if (!asset) return null;
|
|
34
|
+
|
|
35
|
+
const handleProjectUpdate = async (newProject) => {
|
|
36
|
+
try {
|
|
37
|
+
await fetchApi(`/api/assets/${asset.artifact_id}/project`, {
|
|
38
|
+
method: 'PUT',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({ project_name: newProject })
|
|
43
|
+
});
|
|
44
|
+
setCurrentProject(newProject);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Failed to update project:', error);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const typeConfig = {
|
|
51
|
+
model: {
|
|
52
|
+
icon: Box,
|
|
53
|
+
color: 'from-purple-500 to-pink-500',
|
|
54
|
+
bgColor: 'bg-purple-50 dark:bg-purple-900/20',
|
|
55
|
+
textColor: 'text-purple-600 dark:text-purple-400',
|
|
56
|
+
borderColor: 'border-purple-200 dark:border-purple-800'
|
|
57
|
+
},
|
|
58
|
+
dataset: {
|
|
59
|
+
icon: Database,
|
|
60
|
+
color: 'from-blue-500 to-cyan-500',
|
|
61
|
+
bgColor: 'bg-blue-50 dark:bg-blue-900/20',
|
|
62
|
+
textColor: 'text-blue-600 dark:text-blue-400',
|
|
63
|
+
borderColor: 'border-blue-200 dark:border-blue-800'
|
|
64
|
+
},
|
|
65
|
+
metrics: {
|
|
66
|
+
icon: BarChart2,
|
|
67
|
+
color: 'from-emerald-500 to-teal-500',
|
|
68
|
+
bgColor: 'bg-emerald-50 dark:bg-emerald-900/20',
|
|
69
|
+
textColor: 'text-emerald-600 dark:text-emerald-400',
|
|
70
|
+
borderColor: 'border-emerald-200 dark:border-emerald-800'
|
|
71
|
+
},
|
|
72
|
+
default: {
|
|
73
|
+
icon: FileText,
|
|
74
|
+
color: 'from-slate-500 to-slate-600',
|
|
75
|
+
bgColor: 'bg-slate-50 dark:bg-slate-900/20',
|
|
76
|
+
textColor: 'text-slate-600 dark:text-slate-400',
|
|
77
|
+
borderColor: 'border-slate-200 dark:border-slate-800'
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const config = typeConfig[asset.type?.toLowerCase()] || typeConfig.default;
|
|
82
|
+
const Icon = config.icon;
|
|
83
|
+
|
|
84
|
+
const formatBytes = (bytes) => {
|
|
85
|
+
if (!bytes || bytes === 0) return 'N/A';
|
|
86
|
+
const k = 1024;
|
|
87
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
88
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
89
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<motion.div
|
|
94
|
+
initial={{ opacity: 0, x: 20 }}
|
|
95
|
+
animate={{ opacity: 1, x: 0 }}
|
|
96
|
+
exit={{ opacity: 0, x: 20 }}
|
|
97
|
+
transition={{ duration: 0.2 }}
|
|
98
|
+
className="h-full flex flex-col"
|
|
99
|
+
>
|
|
100
|
+
<Card className="flex-1 flex flex-col overflow-hidden">
|
|
101
|
+
{/* Header */}
|
|
102
|
+
<div className={`p-6 ${config.bgColor} border-b ${config.borderColor}`}>
|
|
103
|
+
<div className="flex items-start justify-between mb-4">
|
|
104
|
+
<div className={`p-3 rounded-xl bg-gradient-to-br ${config.color} text-white shadow-lg`}>
|
|
105
|
+
<Icon size={32} />
|
|
106
|
+
</div>
|
|
107
|
+
<button
|
|
108
|
+
onClick={onClose}
|
|
109
|
+
className="p-2 hover:bg-white/50 dark:hover:bg-slate-800/50 rounded-lg transition-colors"
|
|
110
|
+
>
|
|
111
|
+
<X size={20} className="text-slate-400" />
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">
|
|
116
|
+
{asset.name}
|
|
117
|
+
</h2>
|
|
118
|
+
|
|
119
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
120
|
+
<Badge className={`bg-gradient-to-r ${config.color} text-white border-0`}>
|
|
121
|
+
{asset.type}
|
|
122
|
+
</Badge>
|
|
123
|
+
<ProjectSelector
|
|
124
|
+
currentProject={currentProject}
|
|
125
|
+
onUpdate={handleProjectUpdate}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Tabs */}
|
|
131
|
+
<div className="flex items-center gap-1 p-2 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
|
|
132
|
+
<TabButton
|
|
133
|
+
active={activeTab === 'overview'}
|
|
134
|
+
onClick={() => setActiveTab('overview')}
|
|
135
|
+
icon={Info}
|
|
136
|
+
label="Overview"
|
|
137
|
+
/>
|
|
138
|
+
<TabButton
|
|
139
|
+
active={activeTab === 'properties'}
|
|
140
|
+
onClick={() => setActiveTab('properties')}
|
|
141
|
+
icon={Tag}
|
|
142
|
+
label="Properties"
|
|
143
|
+
/>
|
|
144
|
+
<TabButton
|
|
145
|
+
active={activeTab === 'metadata'}
|
|
146
|
+
onClick={() => setActiveTab('metadata')}
|
|
147
|
+
icon={FileBox}
|
|
148
|
+
label="Metadata"
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* Content */}
|
|
153
|
+
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
|
154
|
+
{activeTab === 'overview' && (
|
|
155
|
+
<>
|
|
156
|
+
{/* Key Information */}
|
|
157
|
+
<div>
|
|
158
|
+
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-3 flex items-center gap-2">
|
|
159
|
+
<FileBox size={16} />
|
|
160
|
+
Asset Information
|
|
161
|
+
</h3>
|
|
162
|
+
<div className="grid grid-cols-2 gap-4">
|
|
163
|
+
<InfoCard
|
|
164
|
+
icon={Calendar}
|
|
165
|
+
label="Created"
|
|
166
|
+
value={asset.created_at ? format(new Date(asset.created_at), 'MMM d, yyyy HH:mm') : 'N/A'}
|
|
167
|
+
/>
|
|
168
|
+
<InfoCard
|
|
169
|
+
icon={HardDrive}
|
|
170
|
+
label="Size"
|
|
171
|
+
value={formatBytes(asset.size || asset.storage_bytes)}
|
|
172
|
+
/>
|
|
173
|
+
<InfoCard
|
|
174
|
+
icon={Layers}
|
|
175
|
+
label="Step"
|
|
176
|
+
value={asset.step || 'N/A'}
|
|
177
|
+
/>
|
|
178
|
+
<InfoCard
|
|
179
|
+
icon={GitBranch}
|
|
180
|
+
label="Version"
|
|
181
|
+
value={asset.version || 'N/A'}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{/* Context Links */}
|
|
187
|
+
{(asset.run_id || asset.pipeline_name || currentProject) && (
|
|
188
|
+
<div>
|
|
189
|
+
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-3 flex items-center gap-2">
|
|
190
|
+
<GitBranch size={16} />
|
|
191
|
+
Context
|
|
192
|
+
</h3>
|
|
193
|
+
<div className="space-y-2">
|
|
194
|
+
{currentProject && (
|
|
195
|
+
<ContextLink
|
|
196
|
+
icon={Box}
|
|
197
|
+
label="Project"
|
|
198
|
+
value={currentProject}
|
|
199
|
+
to={`/assets?project=${encodeURIComponent(currentProject)}`}
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
{asset.pipeline_name && (
|
|
203
|
+
<ContextLink
|
|
204
|
+
icon={Activity}
|
|
205
|
+
label="Pipeline"
|
|
206
|
+
value={asset.pipeline_name}
|
|
207
|
+
to={`/pipelines?project=${encodeURIComponent(currentProject || '')}`}
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
{asset.run_id && (
|
|
211
|
+
<ContextLink
|
|
212
|
+
icon={Activity}
|
|
213
|
+
label="Run"
|
|
214
|
+
value={asset.run_id.slice(0, 12) + '...'}
|
|
215
|
+
to={`/runs/${asset.run_id}`}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{activeTab === 'properties' && (
|
|
225
|
+
<div>
|
|
226
|
+
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-3 flex items-center gap-2">
|
|
227
|
+
<Tag size={16} />
|
|
228
|
+
Properties ({asset.properties ? Object.keys(asset.properties).length : 0})
|
|
229
|
+
</h3>
|
|
230
|
+
{asset.properties && Object.keys(asset.properties).length > 0 ? (
|
|
231
|
+
<div className="bg-slate-50 dark:bg-slate-800/50 rounded-lg p-4 space-y-2">
|
|
232
|
+
{Object.entries(asset.properties).map(([key, value]) => (
|
|
233
|
+
<PropertyRow key={key} name={key} value={value} />
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
) : (
|
|
237
|
+
<div className="text-center py-8 text-slate-500 italic">
|
|
238
|
+
No properties available
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
|
|
244
|
+
{activeTab === 'metadata' && (
|
|
245
|
+
<div className="space-y-6">
|
|
246
|
+
<div>
|
|
247
|
+
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-2">
|
|
248
|
+
Artifact ID
|
|
249
|
+
</h3>
|
|
250
|
+
<div className="bg-slate-50 dark:bg-slate-800/50 rounded-lg p-3">
|
|
251
|
+
<code className="text-xs text-slate-600 dark:text-slate-400 font-mono break-all">
|
|
252
|
+
{asset.artifact_id}
|
|
253
|
+
</code>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{asset.uri && (
|
|
258
|
+
<div>
|
|
259
|
+
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-2">
|
|
260
|
+
Location
|
|
261
|
+
</h3>
|
|
262
|
+
<div className="bg-slate-50 dark:bg-slate-800/50 rounded-lg p-3">
|
|
263
|
+
<code className="text-xs text-slate-600 dark:text-slate-400 break-all">
|
|
264
|
+
{asset.uri}
|
|
265
|
+
</code>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{/* Footer Actions */}
|
|
274
|
+
<div className="p-4 border-t border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-800/50">
|
|
275
|
+
<div className="flex gap-2">
|
|
276
|
+
<Button
|
|
277
|
+
onClick={() => downloadArtifactById(asset.artifact_id)}
|
|
278
|
+
disabled={!asset.artifact_id}
|
|
279
|
+
className="flex-1"
|
|
280
|
+
>
|
|
281
|
+
<Download size={16} className="mr-2" />
|
|
282
|
+
Download Asset
|
|
283
|
+
</Button>
|
|
284
|
+
{asset.run_id && (
|
|
285
|
+
<Link to={`/runs/${asset.run_id}`}>
|
|
286
|
+
<Button variant="outline">
|
|
287
|
+
<ExternalLink size={16} className="mr-2" />
|
|
288
|
+
View in Run
|
|
289
|
+
</Button>
|
|
290
|
+
</Link>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</Card>
|
|
295
|
+
</motion.div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function InfoCard({ icon: Icon, label, value, mono = false }) {
|
|
300
|
+
return (
|
|
301
|
+
<div className="bg-white dark:bg-slate-800 rounded-lg p-3 border border-slate-200 dark:border-slate-700">
|
|
302
|
+
<div className="flex items-center gap-2 mb-1">
|
|
303
|
+
<Icon size={14} className="text-slate-400" />
|
|
304
|
+
<span className="text-xs text-slate-500 dark:text-slate-400">{label}</span>
|
|
305
|
+
</div>
|
|
306
|
+
<p className={`text-sm font-semibold text-slate-900 dark:text-white ${mono ? 'font-mono' : ''}`}>
|
|
307
|
+
{value}
|
|
308
|
+
</p>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function ContextLink({ icon: Icon, label, value, to }) {
|
|
314
|
+
return (
|
|
315
|
+
<Link
|
|
316
|
+
to={to}
|
|
317
|
+
className="flex items-center gap-3 p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-primary-300 dark:hover:border-primary-700 hover:shadow-md transition-all group"
|
|
318
|
+
>
|
|
319
|
+
<div className="p-2 bg-slate-100 dark:bg-slate-700 rounded-lg group-hover:bg-primary-100 dark:group-hover:bg-primary-900/30 transition-colors">
|
|
320
|
+
<Icon size={16} className="text-slate-600 dark:text-slate-400 group-hover:text-primary-600 dark:group-hover:text-primary-400" />
|
|
321
|
+
</div>
|
|
322
|
+
<div className="flex-1 min-w-0">
|
|
323
|
+
<p className="text-xs text-slate-500 dark:text-slate-400">{label}</p>
|
|
324
|
+
<p className="text-sm font-medium text-slate-900 dark:text-white truncate">{value}</p>
|
|
325
|
+
</div>
|
|
326
|
+
<ExternalLink size={14} className="text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
327
|
+
</Link>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function PropertyRow({ name, value }) {
|
|
332
|
+
const displayValue = () => {
|
|
333
|
+
if (typeof value === 'object') {
|
|
334
|
+
return JSON.stringify(value, null, 2);
|
|
335
|
+
}
|
|
336
|
+
if (typeof value === 'boolean') {
|
|
337
|
+
return value ? 'true' : 'false';
|
|
338
|
+
}
|
|
339
|
+
if (typeof value === 'number') {
|
|
340
|
+
return value.toLocaleString();
|
|
341
|
+
}
|
|
342
|
+
return String(value);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<div className="flex items-start justify-between gap-4 py-2 border-b border-slate-200 dark:border-slate-700 last:border-0">
|
|
347
|
+
<span className="text-sm font-medium text-slate-700 dark:text-slate-300 shrink-0">
|
|
348
|
+
{name}
|
|
349
|
+
</span>
|
|
350
|
+
<span className="text-sm text-slate-600 dark:text-slate-400 text-right font-mono break-all">
|
|
351
|
+
{displayValue()}
|
|
352
|
+
</span>
|
|
353
|
+
</div>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function TabButton({ active, onClick, icon: Icon, label }) {
|
|
358
|
+
return (
|
|
359
|
+
<button
|
|
360
|
+
onClick={onClick}
|
|
361
|
+
className={`
|
|
362
|
+
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
|
|
363
|
+
${active
|
|
364
|
+
? 'bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-white'
|
|
365
|
+
: 'text-slate-500 hover:bg-slate-50 dark:hover:bg-slate-800/50 hover:text-slate-700 dark:hover:text-slate-300'
|
|
366
|
+
}
|
|
367
|
+
`}
|
|
368
|
+
>
|
|
369
|
+
<Icon size={16} />
|
|
370
|
+
{label}
|
|
371
|
+
</button>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback } from 'react';
|
|
2
|
+
import ReactFlow, {
|
|
3
|
+
useNodesState,
|
|
4
|
+
useEdgesState,
|
|
5
|
+
Controls,
|
|
6
|
+
Background,
|
|
7
|
+
Panel,
|
|
8
|
+
MiniMap
|
|
9
|
+
} from 'reactflow';
|
|
10
|
+
import 'reactflow/dist/style.css';
|
|
11
|
+
import dagre from 'dagre';
|
|
12
|
+
import { Database, Box, BarChart2, FileText, Activity, Loader2, Info } from 'lucide-react';
|
|
13
|
+
import { fetchApi } from '../utils/api';
|
|
14
|
+
|
|
15
|
+
// Custom node component for artifacts
|
|
16
|
+
function ArtifactNode({ data }) {
|
|
17
|
+
const getIconAndStyle = (artifactType) => {
|
|
18
|
+
const configs = {
|
|
19
|
+
model: {
|
|
20
|
+
icon: Box,
|
|
21
|
+
gradient: 'from-purple-500 to-pink-500',
|
|
22
|
+
bg: 'bg-purple-50 dark:bg-purple-900/20',
|
|
23
|
+
border: 'border-purple-400',
|
|
24
|
+
text: 'text-purple-700 dark:text-purple-300'
|
|
25
|
+
},
|
|
26
|
+
dataset: {
|
|
27
|
+
icon: Database,
|
|
28
|
+
gradient: 'from-blue-500 to-cyan-500',
|
|
29
|
+
bg: 'bg-blue-50 dark:bg-blue-900/20',
|
|
30
|
+
border: 'border-blue-400',
|
|
31
|
+
text: 'text-blue-700 dark:text-blue-300'
|
|
32
|
+
},
|
|
33
|
+
metrics: {
|
|
34
|
+
icon: BarChart2,
|
|
35
|
+
gradient: 'from-emerald-500 to-teal-500',
|
|
36
|
+
bg: 'bg-emerald-50 dark:bg-emerald-900/20',
|
|
37
|
+
border: 'border-emerald-400',
|
|
38
|
+
text: 'text-emerald-700 dark:text-emerald-300'
|
|
39
|
+
},
|
|
40
|
+
default: {
|
|
41
|
+
icon: FileText,
|
|
42
|
+
gradient: 'from-slate-500 to-slate-600',
|
|
43
|
+
bg: 'bg-slate-50 dark:bg-slate-800',
|
|
44
|
+
border: 'border-slate-400',
|
|
45
|
+
text: 'text-slate-700 dark:text-slate-300'
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return configs[artifactType] || configs.default;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const config = getIconAndStyle(data.artifact_type);
|
|
52
|
+
const Icon = config.icon;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
className={`px-4 py-3 rounded-xl ${config.bg} border-2 ${config.border} shadow-lg hover:shadow-xl transition-all cursor-pointer min-w-[180px]`}
|
|
57
|
+
onClick={() => data.onClick?.(data)}
|
|
58
|
+
>
|
|
59
|
+
<div className="flex items-center gap-3">
|
|
60
|
+
<div className={`p-2 bg-gradient-to-br ${config.gradient} rounded-lg text-white`}>
|
|
61
|
+
<Icon size={18} />
|
|
62
|
+
</div>
|
|
63
|
+
<div className="flex-1 min-w-0">
|
|
64
|
+
<div className={`font-semibold text-sm ${config.text} truncate`} title={data.label}>
|
|
65
|
+
{data.label}
|
|
66
|
+
</div>
|
|
67
|
+
<div className="text-xs text-slate-500 dark:text-slate-400">
|
|
68
|
+
{data.artifact_type}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
{data.metadata?.properties && Object.keys(data.metadata.properties).length > 0 && (
|
|
73
|
+
<div className="mt-2 pt-2 border-t border-slate-200 dark:border-slate-700">
|
|
74
|
+
<div className="text-xs text-slate-500">
|
|
75
|
+
{Object.keys(data.metadata.properties).length} properties
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Custom node component for runs
|
|
84
|
+
function RunNode({ data }) {
|
|
85
|
+
const statusColors = {
|
|
86
|
+
completed: 'bg-emerald-100 dark:bg-emerald-900/20 border-emerald-400 text-emerald-700 dark:text-emerald-300',
|
|
87
|
+
failed: 'bg-rose-100 dark:bg-rose-900/20 border-rose-400 text-rose-700 dark:text-rose-300',
|
|
88
|
+
running: 'bg-blue-100 dark:bg-blue-900/20 border-blue-400 text-blue-700 dark:text-blue-300',
|
|
89
|
+
default: 'bg-slate-100 dark:bg-slate-800 border-slate-400 text-slate-700 dark:text-slate-300'
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const status = data.metadata?.status || 'unknown';
|
|
93
|
+
const colorClass = statusColors[status] || statusColors.default;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
className={`px-4 py-2 rounded-lg border-2 ${colorClass} shadow-md hover:shadow-lg transition-all min-w-[150px]`}
|
|
98
|
+
>
|
|
99
|
+
<div className="flex items-center gap-2">
|
|
100
|
+
<Activity size={16} />
|
|
101
|
+
<div>
|
|
102
|
+
<div className="font-semibold text-sm truncate" title={data.label}>
|
|
103
|
+
{data.label}
|
|
104
|
+
</div>
|
|
105
|
+
{data.metadata?.pipeline_name && (
|
|
106
|
+
<div className="text-xs opacity-75">
|
|
107
|
+
{data.metadata.pipeline_name}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const nodeTypes = {
|
|
117
|
+
artifact: ArtifactNode,
|
|
118
|
+
run: RunNode
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Auto-layout using dagre
|
|
122
|
+
const getLayoutedElements = (nodes, edges, direction = 'TB') => {
|
|
123
|
+
const dagreGraph = new dagre.graphlib.Graph();
|
|
124
|
+
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
|
125
|
+
|
|
126
|
+
const nodeWidth = 200;
|
|
127
|
+
const nodeHeight = 100;
|
|
128
|
+
|
|
129
|
+
dagreGraph.setGraph({ rankdir: direction, ranksep: 100, nodesep: 80 });
|
|
130
|
+
|
|
131
|
+
nodes.forEach((node) => {
|
|
132
|
+
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
edges.forEach((edge) => {
|
|
136
|
+
dagreGraph.setEdge(edge.source, edge.target);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
dagre.layout(dagreGraph);
|
|
140
|
+
|
|
141
|
+
const layoutedNodes = nodes.map((node) => {
|
|
142
|
+
const nodeWithPosition = dagreGraph.node(node.id);
|
|
143
|
+
return {
|
|
144
|
+
...node,
|
|
145
|
+
position: {
|
|
146
|
+
x: nodeWithPosition.x - nodeWidth / 2,
|
|
147
|
+
y: nodeWithPosition.y - nodeHeight / 2,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return { nodes: layoutedNodes, edges };
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export function AssetLineageGraph({ projectId, onNodeClick }) {
|
|
156
|
+
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
157
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
158
|
+
const [loading, setLoading] = useState(true);
|
|
159
|
+
const [error, setError] = useState(null);
|
|
160
|
+
|
|
161
|
+
const fetchLineage = useCallback(async () => {
|
|
162
|
+
setLoading(true);
|
|
163
|
+
setError(null);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const url = projectId
|
|
167
|
+
? `/api/assets/lineage?project=${encodeURIComponent(projectId)}&depth=3`
|
|
168
|
+
: '/api/assets/lineage?depth=2';
|
|
169
|
+
|
|
170
|
+
const res = await fetchApi(url);
|
|
171
|
+
const data = await res.json();
|
|
172
|
+
|
|
173
|
+
// Transform API data to ReactFlow format
|
|
174
|
+
const flowNodes = data.nodes.map(node => ({
|
|
175
|
+
id: node.id,
|
|
176
|
+
type: node.type,
|
|
177
|
+
data: {
|
|
178
|
+
label: node.label,
|
|
179
|
+
artifact_type: node.artifact_type,
|
|
180
|
+
metadata: node.metadata,
|
|
181
|
+
onClick: onNodeClick
|
|
182
|
+
},
|
|
183
|
+
position: { x: 0, y: 0 } // Will be set by layout
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
const flowEdges = data.edges.map(edge => ({
|
|
187
|
+
id: edge.id,
|
|
188
|
+
source: edge.source,
|
|
189
|
+
target: edge.target,
|
|
190
|
+
label: edge.label,
|
|
191
|
+
type: edge.type === 'produces' ? 'default' : 'step',
|
|
192
|
+
animated: edge.type === 'produces',
|
|
193
|
+
style: {
|
|
194
|
+
stroke: edge.type === 'produces' ? '#3b82f6' : '#94a3b8',
|
|
195
|
+
strokeWidth: 2
|
|
196
|
+
},
|
|
197
|
+
markerEnd: {
|
|
198
|
+
type: 'arrowclosed',
|
|
199
|
+
color: edge.type === 'produces' ? '#3b82f6' : '#94a3b8'
|
|
200
|
+
}
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
// Apply auto-layout
|
|
204
|
+
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
|
|
205
|
+
flowNodes,
|
|
206
|
+
flowEdges
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
setNodes(layoutedNodes);
|
|
210
|
+
setEdges(layoutedEdges);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.error('Failed to fetch lineage:', err);
|
|
213
|
+
setError(err.message);
|
|
214
|
+
} finally {
|
|
215
|
+
setLoading(false);
|
|
216
|
+
}
|
|
217
|
+
}, [projectId, onNodeClick, setNodes, setEdges]);
|
|
218
|
+
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
fetchLineage();
|
|
221
|
+
}, [fetchLineage]);
|
|
222
|
+
|
|
223
|
+
if (loading) {
|
|
224
|
+
return (
|
|
225
|
+
<div className="h-[600px] flex items-center justify-center bg-slate-50 dark:bg-slate-900 rounded-xl">
|
|
226
|
+
<div className="text-center">
|
|
227
|
+
<Loader2 className="animate-spin h-12 w-12 text-primary-600 mx-auto mb-4" />
|
|
228
|
+
<p className="text-slate-600 dark:text-slate-400">Loading artifact lineage...</p>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (error) {
|
|
235
|
+
return (
|
|
236
|
+
<div className="h-[600px] flex items-center justify-center bg-slate-50 dark:bg-slate-900 rounded-xl">
|
|
237
|
+
<div className="text-center">
|
|
238
|
+
<Info className="h-12 w-12 text-rose-600 mx-auto mb-4" />
|
|
239
|
+
<p className="text-rose-600 font-semibold mb-2">Failed to load lineage</p>
|
|
240
|
+
<p className="text-slate-600 dark:text-slate-400 text-sm">{error}</p>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (nodes.length === 0) {
|
|
247
|
+
return (
|
|
248
|
+
<div className="h-[600px] flex items-center justify-center bg-slate-50 dark:bg-slate-900 rounded-xl border-2 border-dashed border-slate-300 dark:border-slate-700">
|
|
249
|
+
<div className="text-center">
|
|
250
|
+
<Database className="h-12 w-12 text-slate-400 mx-auto mb-4" />
|
|
251
|
+
<p className="text-slate-600 dark:text-slate-400 font-semibold mb-2">No artifact lineage found</p>
|
|
252
|
+
<p className="text-slate-500 text-sm">Run some pipelines to generate artifact relationships</p>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<div className="h-[600px] border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden bg-white dark:bg-slate-900">
|
|
260
|
+
<ReactFlow
|
|
261
|
+
nodes={nodes}
|
|
262
|
+
edges={edges}
|
|
263
|
+
onNodesChange={onNodesChange}
|
|
264
|
+
onEdgesChange={onEdgesChange}
|
|
265
|
+
nodeTypes={nodeTypes}
|
|
266
|
+
fitView
|
|
267
|
+
attributionPosition="bottom-left"
|
|
268
|
+
>
|
|
269
|
+
<Background color="#94a3b8" gap={16} />
|
|
270
|
+
<Controls />
|
|
271
|
+
<Panel position="top-right" className="bg-white dark:bg-slate-800 p-3 rounded-lg shadow-lg border border-slate-200 dark:border-slate-700">
|
|
272
|
+
<div className="text-xs space-y-2">
|
|
273
|
+
<div className="font-semibold text-slate-900 dark:text-white mb-2">Legend</div>
|
|
274
|
+
<div className="flex items-center gap-2">
|
|
275
|
+
<div className="w-3 h-3 rounded bg-purple-500"></div>
|
|
276
|
+
<span className="text-slate-600 dark:text-slate-400">Models</span>
|
|
277
|
+
</div>
|
|
278
|
+
<div className="flex items-center gap-2">
|
|
279
|
+
<div className="w-3 h-3 rounded bg-blue-500"></div>
|
|
280
|
+
<span className="text-slate-600 dark:text-slate-400">Datasets</span>
|
|
281
|
+
</div>
|
|
282
|
+
<div className="flex items-center gap-2">
|
|
283
|
+
<div className="w-3 h-3 rounded bg-emerald-500"></div>
|
|
284
|
+
<span className="text-slate-600 dark:text-slate-400">Metrics</span>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</Panel>
|
|
288
|
+
</ReactFlow>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|