flowyml 1.1.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.
Files changed (92) hide show
  1. flowyml/__init__.py +3 -0
  2. flowyml/assets/base.py +10 -0
  3. flowyml/assets/metrics.py +6 -0
  4. flowyml/cli/main.py +108 -2
  5. flowyml/cli/run.py +9 -2
  6. flowyml/core/execution_status.py +52 -0
  7. flowyml/core/hooks.py +106 -0
  8. flowyml/core/observability.py +210 -0
  9. flowyml/core/orchestrator.py +274 -0
  10. flowyml/core/pipeline.py +193 -231
  11. flowyml/core/project.py +34 -2
  12. flowyml/core/remote_orchestrator.py +109 -0
  13. flowyml/core/resources.py +22 -5
  14. flowyml/core/retry_policy.py +80 -0
  15. flowyml/core/step.py +18 -1
  16. flowyml/core/submission_result.py +53 -0
  17. flowyml/core/versioning.py +2 -2
  18. flowyml/integrations/keras.py +95 -22
  19. flowyml/monitoring/alerts.py +2 -2
  20. flowyml/stacks/__init__.py +15 -0
  21. flowyml/stacks/aws.py +599 -0
  22. flowyml/stacks/azure.py +295 -0
  23. flowyml/stacks/components.py +24 -2
  24. flowyml/stacks/gcp.py +158 -11
  25. flowyml/stacks/local.py +5 -0
  26. flowyml/storage/artifacts.py +15 -5
  27. flowyml/storage/materializers/__init__.py +2 -0
  28. flowyml/storage/materializers/cloudpickle.py +74 -0
  29. flowyml/storage/metadata.py +166 -5
  30. flowyml/ui/backend/main.py +41 -1
  31. flowyml/ui/backend/routers/assets.py +356 -15
  32. flowyml/ui/backend/routers/client.py +46 -0
  33. flowyml/ui/backend/routers/execution.py +13 -2
  34. flowyml/ui/backend/routers/experiments.py +48 -12
  35. flowyml/ui/backend/routers/metrics.py +213 -0
  36. flowyml/ui/backend/routers/pipelines.py +63 -7
  37. flowyml/ui/backend/routers/projects.py +33 -7
  38. flowyml/ui/backend/routers/runs.py +150 -8
  39. flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
  40. flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
  41. flowyml/ui/frontend/dist/index.html +2 -2
  42. flowyml/ui/frontend/src/App.jsx +4 -1
  43. flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
  44. flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
  45. flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
  46. flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
  47. flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
  48. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
  49. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
  50. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
  51. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
  52. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
  53. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
  54. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
  55. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
  56. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
  57. flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
  58. flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
  59. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
  60. flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
  61. flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
  62. flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
  63. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
  64. flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
  65. flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
  66. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
  67. flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
  68. flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
  69. flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
  70. flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
  71. flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
  72. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
  73. flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
  74. flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
  75. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
  76. flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
  77. flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
  78. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
  79. flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
  80. flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
  81. flowyml/ui/frontend/src/router/index.jsx +4 -0
  82. flowyml/ui/frontend/src/utils/date.js +10 -0
  83. flowyml/ui/frontend/src/utils/downloads.js +11 -0
  84. flowyml/utils/config.py +6 -0
  85. flowyml/utils/stack_config.py +45 -3
  86. {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/METADATA +113 -12
  87. {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/RECORD +90 -53
  88. {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/licenses/LICENSE +1 -1
  89. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
  90. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
  91. {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/WHEEL +0 -0
  92. {flowyml-1.1.0.dist-info → flowyml-1.3.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
+ }