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.
- 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/core/versioning.py +2 -2
- 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.1.0.dist-info → flowyml-1.3.0.dist-info}/METADATA +113 -12
- {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/RECORD +90 -53
- {flowyml-1.1.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.1.0.dist-info → flowyml-1.3.0.dist-info}/WHEEL +0 -0
- {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { fetchApi } from '../utils/api';
|
|
3
|
+
import {
|
|
4
|
+
ChevronRight,
|
|
5
|
+
ChevronDown,
|
|
6
|
+
Box,
|
|
7
|
+
Activity,
|
|
8
|
+
PlayCircle,
|
|
9
|
+
FileBox,
|
|
10
|
+
CheckCircle,
|
|
11
|
+
XCircle,
|
|
12
|
+
Clock,
|
|
13
|
+
Database,
|
|
14
|
+
Layers,
|
|
15
|
+
X,
|
|
16
|
+
Download,
|
|
17
|
+
Info,
|
|
18
|
+
BarChart2,
|
|
19
|
+
FileText,
|
|
20
|
+
Eye,
|
|
21
|
+
Folder,
|
|
22
|
+
GitBranch
|
|
23
|
+
} from 'lucide-react';
|
|
24
|
+
import { Link } from 'react-router-dom';
|
|
25
|
+
import { formatDate } from '../utils/date';
|
|
26
|
+
import { downloadArtifactById } from '../utils/downloads';
|
|
27
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
28
|
+
|
|
29
|
+
const StatusIcon = ({ status }) => {
|
|
30
|
+
switch (status?.toLowerCase()) {
|
|
31
|
+
case 'completed':
|
|
32
|
+
case 'success':
|
|
33
|
+
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
|
34
|
+
case 'failed':
|
|
35
|
+
return <XCircle className="w-4 h-4 text-red-500" />;
|
|
36
|
+
case 'running':
|
|
37
|
+
return <Activity className="w-4 h-4 text-blue-500 animate-spin" />;
|
|
38
|
+
default:
|
|
39
|
+
return <Clock className="w-4 h-4 text-slate-400" />;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const ArtifactIcon = ({ type }) => {
|
|
44
|
+
const iconProps = { className: "w-4 h-4" };
|
|
45
|
+
|
|
46
|
+
switch (type?.toLowerCase()) {
|
|
47
|
+
case 'model':
|
|
48
|
+
return <Box {...iconProps} className="w-4 h-4 text-purple-500" />;
|
|
49
|
+
case 'dataset':
|
|
50
|
+
case 'data':
|
|
51
|
+
return <Database {...iconProps} className="w-4 h-4 text-blue-500" />;
|
|
52
|
+
case 'metrics':
|
|
53
|
+
return <BarChart2 {...iconProps} className="w-4 h-4 text-emerald-500" />;
|
|
54
|
+
default:
|
|
55
|
+
return <FileBox {...iconProps} className="w-4 h-4 text-slate-400" />;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const TreeNode = ({
|
|
60
|
+
label,
|
|
61
|
+
icon: Icon,
|
|
62
|
+
children,
|
|
63
|
+
defaultExpanded = false,
|
|
64
|
+
actions,
|
|
65
|
+
status,
|
|
66
|
+
level = 0,
|
|
67
|
+
badge,
|
|
68
|
+
onClick
|
|
69
|
+
}) => {
|
|
70
|
+
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
|
71
|
+
const hasChildren = children && children.length > 0;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="select-none">
|
|
75
|
+
<motion.div
|
|
76
|
+
initial={{ opacity: 0, x: -10 }}
|
|
77
|
+
animate={{ opacity: 1, x: 0 }}
|
|
78
|
+
transition={{ duration: 0.2 }}
|
|
79
|
+
className={`
|
|
80
|
+
flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all
|
|
81
|
+
hover:bg-slate-100 dark:hover:bg-slate-800
|
|
82
|
+
${level === 0 ? 'bg-slate-50 dark:bg-slate-800/50 mb-1 font-semibold' : ''}
|
|
83
|
+
`}
|
|
84
|
+
style={{ paddingLeft: `${level * 1.5 + 0.5}rem` }}
|
|
85
|
+
onClick={() => {
|
|
86
|
+
if (hasChildren) {
|
|
87
|
+
setIsExpanded(!isExpanded);
|
|
88
|
+
}
|
|
89
|
+
onClick?.();
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<div className="flex items-center gap-1 text-slate-400">
|
|
93
|
+
{hasChildren ? (
|
|
94
|
+
<motion.div
|
|
95
|
+
animate={{ rotate: isExpanded ? 90 : 0 }}
|
|
96
|
+
transition={{ duration: 0.2 }}
|
|
97
|
+
>
|
|
98
|
+
<ChevronRight className="w-4 h-4" />
|
|
99
|
+
</motion.div>
|
|
100
|
+
) : (
|
|
101
|
+
<div className="w-4" />
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{Icon && <Icon className={`w-4 h-4 ${level === 0 ? 'text-blue-500' : 'text-slate-500 dark:text-slate-400'}`} />}
|
|
106
|
+
|
|
107
|
+
<div className="flex-1 flex items-center justify-between gap-3">
|
|
108
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
109
|
+
<span className={`text-sm truncate ${level === 0
|
|
110
|
+
? 'font-semibold text-slate-900 dark:text-white'
|
|
111
|
+
: 'text-slate-700 dark:text-slate-300'
|
|
112
|
+
}`}>
|
|
113
|
+
{label}
|
|
114
|
+
</span>
|
|
115
|
+
{badge}
|
|
116
|
+
</div>
|
|
117
|
+
<div className="flex items-center gap-3 shrink-0">
|
|
118
|
+
{status && <StatusIcon status={status} />}
|
|
119
|
+
{actions}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</motion.div>
|
|
123
|
+
|
|
124
|
+
<AnimatePresence>
|
|
125
|
+
{isExpanded && hasChildren && (
|
|
126
|
+
<motion.div
|
|
127
|
+
initial={{ opacity: 0, height: 0 }}
|
|
128
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
129
|
+
exit={{ opacity: 0, height: 0 }}
|
|
130
|
+
transition={{ duration: 0.2 }}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</motion.div>
|
|
134
|
+
)}
|
|
135
|
+
</AnimatePresence>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export function AssetTreeHierarchy({ projectId, onAssetSelect, compact = false }) {
|
|
141
|
+
const [data, setData] = useState({ projects: [], pipelines: [], runs: [], artifacts: [] });
|
|
142
|
+
const [loading, setLoading] = useState(true);
|
|
143
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
144
|
+
const [assetTypeFilter, setAssetTypeFilter] = useState('all'); // 'all', 'model', 'dataset'
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
const fetchData = async () => {
|
|
148
|
+
setLoading(true);
|
|
149
|
+
try {
|
|
150
|
+
// Determine what to fetch based on projectId
|
|
151
|
+
const endpoints = projectId
|
|
152
|
+
? [
|
|
153
|
+
fetchApi(`/api/pipelines?project=${projectId}`),
|
|
154
|
+
fetchApi(`/api/runs?project=${projectId}&limit=200`),
|
|
155
|
+
fetchApi(`/api/assets?project=${projectId}&limit=500`)
|
|
156
|
+
]
|
|
157
|
+
: [
|
|
158
|
+
fetchApi('/api/pipelines?limit=100'),
|
|
159
|
+
fetchApi('/api/runs?limit=200'),
|
|
160
|
+
fetchApi('/api/assets?limit=500')
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
const [pipelinesRes, runsRes, artifactsRes] = await Promise.all(endpoints);
|
|
164
|
+
|
|
165
|
+
const pipelinesData = await pipelinesRes.json();
|
|
166
|
+
const runsData = await runsRes.json();
|
|
167
|
+
const artifactsData = await artifactsRes.json();
|
|
168
|
+
|
|
169
|
+
// If no projectId, group by projects
|
|
170
|
+
let projects = [];
|
|
171
|
+
if (!projectId) {
|
|
172
|
+
const artifactProjects = new Set(
|
|
173
|
+
(artifactsData?.assets || []).map(a => a.project).filter(Boolean)
|
|
174
|
+
);
|
|
175
|
+
const runProjects = new Set(
|
|
176
|
+
(runsData?.runs || []).map(r => r.project).filter(Boolean)
|
|
177
|
+
);
|
|
178
|
+
projects = [...new Set([...artifactProjects, ...runProjects])].map(name => ({ name }));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setData({
|
|
182
|
+
projects,
|
|
183
|
+
pipelines: Array.isArray(pipelinesData?.pipelines) ? pipelinesData.pipelines : [],
|
|
184
|
+
runs: Array.isArray(runsData?.runs) ? runsData.runs : [],
|
|
185
|
+
artifacts: Array.isArray(artifactsData?.assets) ? artifactsData.assets : []
|
|
186
|
+
});
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error('Failed to fetch hierarchy data:', error);
|
|
189
|
+
} finally {
|
|
190
|
+
setLoading(false);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
fetchData();
|
|
195
|
+
}, [projectId]);
|
|
196
|
+
|
|
197
|
+
if (loading) {
|
|
198
|
+
return (
|
|
199
|
+
<div className={`w-full bg-slate-50 dark:bg-slate-800/50 animate-pulse flex items-center justify-center ${compact ? 'h-full min-h-[200px]' : 'h-[600px] rounded-xl'}`}>
|
|
200
|
+
<div className="text-center">
|
|
201
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto mb-3"></div>
|
|
202
|
+
<p className="text-xs text-slate-500 dark:text-slate-400">Loading...</p>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Filter functions
|
|
209
|
+
const getRunsForPipeline = (pipelineName, projectName = null) => {
|
|
210
|
+
return data.runs.filter(r =>
|
|
211
|
+
r.pipeline_name === pipelineName &&
|
|
212
|
+
(!projectName || r.project === projectName)
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const getArtifactsForRun = (runId) => {
|
|
217
|
+
let artifacts = data.artifacts.filter(a => a.run_id === runId);
|
|
218
|
+
|
|
219
|
+
// Apply asset type filter
|
|
220
|
+
if (assetTypeFilter !== 'all') {
|
|
221
|
+
artifacts = artifacts.filter(a => a.type?.toLowerCase() === assetTypeFilter);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return artifacts;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const getPipelinesForProject = (projectName) => {
|
|
228
|
+
const projectRuns = data.runs.filter(r => r.project === projectName);
|
|
229
|
+
const pipelineNames = [...new Set(projectRuns.map(r => r.pipeline_name))];
|
|
230
|
+
return data.pipelines.filter(p => pipelineNames.includes(p.name));
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Build hierarchy based on whether we have a single project or multiple
|
|
234
|
+
const renderContent = () => {
|
|
235
|
+
if (projectId || data.projects.length === 0) {
|
|
236
|
+
// Single project view or no projects - show pipelines directly
|
|
237
|
+
return data.pipelines.map((pipeline, idx) => {
|
|
238
|
+
const runs = getRunsForPipeline(pipeline.name, projectId);
|
|
239
|
+
return renderPipeline(pipeline, runs, 0, idx < 3); // Expand first 3 pipelines
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
// Multiple projects - group by project
|
|
243
|
+
return data.projects.map((project, projIdx) => {
|
|
244
|
+
const pipelines = getPipelinesForProject(project.name);
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<TreeNode
|
|
248
|
+
key={project.name}
|
|
249
|
+
label={project.name}
|
|
250
|
+
icon={Folder}
|
|
251
|
+
level={0}
|
|
252
|
+
defaultExpanded={projIdx === 0} // Expand first project
|
|
253
|
+
badge={
|
|
254
|
+
<span className="text-xs text-slate-400">
|
|
255
|
+
{pipelines.length} pipeline{pipelines.length !== 1 ? 's' : ''}
|
|
256
|
+
</span>
|
|
257
|
+
}
|
|
258
|
+
>
|
|
259
|
+
{pipelines.map((pipeline, idx) => {
|
|
260
|
+
const runs = getRunsForPipeline(pipeline.name, project.name);
|
|
261
|
+
return renderPipeline(pipeline, runs, 1, projIdx === 0 && idx < 2); // Expand first 2 pipelines of first project
|
|
262
|
+
})}
|
|
263
|
+
</TreeNode>
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const renderPipeline = (pipeline, runs, baseLevel, defaultExpanded = false) => {
|
|
270
|
+
const totalArtifacts = runs.reduce((sum, run) => {
|
|
271
|
+
return sum + getArtifactsForRun(run.run_id).length;
|
|
272
|
+
}, 0);
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<TreeNode
|
|
276
|
+
key={pipeline.name}
|
|
277
|
+
label={pipeline.name}
|
|
278
|
+
icon={Activity}
|
|
279
|
+
level={baseLevel}
|
|
280
|
+
defaultExpanded={defaultExpanded} // Use the parameter
|
|
281
|
+
badge={
|
|
282
|
+
<div className="flex gap-1">
|
|
283
|
+
{totalArtifacts > 0 && (
|
|
284
|
+
<span className="flex items-center gap-0.5 text-[10px] bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 px-1.5 py-0.5 rounded-full">
|
|
285
|
+
<FileBox className="w-3 h-3" /> {totalArtifacts}
|
|
286
|
+
</span>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
}
|
|
290
|
+
actions={
|
|
291
|
+
<Link
|
|
292
|
+
to={`/pipelines/${pipeline.name}`}
|
|
293
|
+
className="text-xs text-blue-500 hover:underline px-2 py-1 rounded hover:bg-blue-50 dark:hover:bg-blue-900/20"
|
|
294
|
+
onClick={(e) => e.stopPropagation()}
|
|
295
|
+
>
|
|
296
|
+
View
|
|
297
|
+
</Link>
|
|
298
|
+
}
|
|
299
|
+
>
|
|
300
|
+
{runs.length === 0 && (
|
|
301
|
+
<div className="pl-12 py-2 text-xs text-slate-400 italic">No runs yet</div>
|
|
302
|
+
)}
|
|
303
|
+
{runs.map(run => renderRun(run, baseLevel + 1))}
|
|
304
|
+
</TreeNode>
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const renderRun = (run, baseLevel) => {
|
|
309
|
+
const artifacts = getArtifactsForRun(run.run_id);
|
|
310
|
+
const modelCount = artifacts.filter(a => a.type?.toLowerCase() === 'model').length;
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<TreeNode
|
|
314
|
+
key={run.run_id}
|
|
315
|
+
label={run.name || `Run ${run.run_id.slice(0, 8)}`}
|
|
316
|
+
icon={PlayCircle}
|
|
317
|
+
level={baseLevel}
|
|
318
|
+
status={run.status}
|
|
319
|
+
defaultExpanded={artifacts.length > 0 && artifacts.length <= 5}
|
|
320
|
+
badge={
|
|
321
|
+
artifacts.length > 0 && (
|
|
322
|
+
<div className="flex gap-1">
|
|
323
|
+
{modelCount > 0 && (
|
|
324
|
+
<span className="flex items-center gap-0.5 text-[10px] bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400 px-1.5 py-0.5 rounded-full">
|
|
325
|
+
<Box className="w-3 h-3" /> {modelCount}
|
|
326
|
+
</span>
|
|
327
|
+
)}
|
|
328
|
+
<span className="flex items-center gap-0.5 text-[10px] bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400 px-1.5 py-0.5 rounded-full">
|
|
329
|
+
<FileBox className="w-3 h-3" /> {artifacts.length}
|
|
330
|
+
</span>
|
|
331
|
+
</div>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
actions={
|
|
335
|
+
<div className="flex items-center gap-2">
|
|
336
|
+
<span className="text-xs text-slate-400">{formatDate(run.created)}</span>
|
|
337
|
+
<Link
|
|
338
|
+
to={`/runs/${run.run_id}`}
|
|
339
|
+
className="text-xs text-blue-500 hover:underline"
|
|
340
|
+
onClick={(e) => e.stopPropagation()}
|
|
341
|
+
>
|
|
342
|
+
Details
|
|
343
|
+
</Link>
|
|
344
|
+
</div>
|
|
345
|
+
}
|
|
346
|
+
>
|
|
347
|
+
{artifacts.length === 0 && (
|
|
348
|
+
<div className="pl-16 py-1 text-xs text-slate-400 italic">No artifacts</div>
|
|
349
|
+
)}
|
|
350
|
+
{artifacts.map(artifact => renderArtifact(artifact, baseLevel + 1))}
|
|
351
|
+
</TreeNode>
|
|
352
|
+
);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const renderArtifact = (artifact, baseLevel) => {
|
|
356
|
+
const typeConfig = {
|
|
357
|
+
model: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300',
|
|
358
|
+
dataset: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
|
|
359
|
+
metrics: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300',
|
|
360
|
+
default: 'bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400'
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const bgClass = typeConfig[artifact.type?.toLowerCase()] || typeConfig.default;
|
|
364
|
+
|
|
365
|
+
return (
|
|
366
|
+
<TreeNode
|
|
367
|
+
key={artifact.artifact_id}
|
|
368
|
+
label={artifact.name}
|
|
369
|
+
icon={() => <ArtifactIcon type={artifact.type} />}
|
|
370
|
+
level={baseLevel}
|
|
371
|
+
onClick={() => onAssetSelect?.(artifact)}
|
|
372
|
+
badge={
|
|
373
|
+
<span className={`text-xs px-1.5 py-0.5 rounded ${bgClass}`}>
|
|
374
|
+
{artifact.type}
|
|
375
|
+
</span>
|
|
376
|
+
}
|
|
377
|
+
actions={
|
|
378
|
+
<div className="flex items-center gap-2">
|
|
379
|
+
{artifact.properties && Object.keys(artifact.properties).length > 0 && (
|
|
380
|
+
<span className="text-xs text-slate-400">
|
|
381
|
+
{Object.keys(artifact.properties).length} props
|
|
382
|
+
</span>
|
|
383
|
+
)}
|
|
384
|
+
<button
|
|
385
|
+
onClick={(e) => {
|
|
386
|
+
e.stopPropagation();
|
|
387
|
+
onAssetSelect?.(artifact);
|
|
388
|
+
}}
|
|
389
|
+
className="p-1 rounded hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
|
390
|
+
>
|
|
391
|
+
<Eye className="w-3 h-3 text-slate-400" />
|
|
392
|
+
</button>
|
|
393
|
+
<button
|
|
394
|
+
onClick={(e) => {
|
|
395
|
+
e.stopPropagation();
|
|
396
|
+
downloadArtifactById(artifact.artifact_id);
|
|
397
|
+
}}
|
|
398
|
+
className="p-1 rounded hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors disabled:opacity-40"
|
|
399
|
+
disabled={!artifact.artifact_id}
|
|
400
|
+
>
|
|
401
|
+
<Download className="w-3 h-3 text-slate-400" />
|
|
402
|
+
</button>
|
|
403
|
+
</div>
|
|
404
|
+
}
|
|
405
|
+
/>
|
|
406
|
+
);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<div className={compact ? "h-full flex flex-col" : "bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 overflow-hidden"}>
|
|
411
|
+
{!compact && (
|
|
412
|
+
<div className="p-4 border-b border-slate-200 dark:border-slate-800 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-800 dark:to-slate-800">
|
|
413
|
+
<div className="flex items-center justify-between mb-3">
|
|
414
|
+
<h3 className="font-semibold text-slate-900 dark:text-white flex items-center gap-2">
|
|
415
|
+
<GitBranch className="w-4 h-4 text-blue-500" />
|
|
416
|
+
Asset Hierarchy
|
|
417
|
+
</h3>
|
|
418
|
+
<span className="text-xs text-slate-500">
|
|
419
|
+
{data.artifacts.length} asset{data.artifacts.length !== 1 ? 's' : ''}
|
|
420
|
+
</span>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
)}
|
|
424
|
+
|
|
425
|
+
{/* Filters - Simplified for compact mode */}
|
|
426
|
+
<div className={`${compact ? 'p-2 border-b border-slate-100 dark:border-slate-700' : 'p-4 border-b border-slate-200 dark:border-slate-800 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-800 dark:to-slate-800'}`}>
|
|
427
|
+
<div className={`flex ${compact ? 'justify-between' : 'gap-2'}`}>
|
|
428
|
+
<button
|
|
429
|
+
onClick={() => setAssetTypeFilter('all')}
|
|
430
|
+
title="All Assets"
|
|
431
|
+
className={`flex items-center justify-center gap-1.5 ${compact ? 'p-1.5 rounded-md' : 'px-3 py-1.5 rounded-lg'} text-sm font-medium transition-all ${assetTypeFilter === 'all'
|
|
432
|
+
? 'bg-primary-500 text-white shadow-md'
|
|
433
|
+
: 'bg-white dark:bg-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-600'
|
|
434
|
+
}`}
|
|
435
|
+
>
|
|
436
|
+
<FileBox className="w-3.5 h-3.5" />
|
|
437
|
+
{!compact && <>All Assets <span className="text-xs opacity-75">({data.artifacts.length})</span></>}
|
|
438
|
+
</button>
|
|
439
|
+
<button
|
|
440
|
+
onClick={() => setAssetTypeFilter('model')}
|
|
441
|
+
title="Models"
|
|
442
|
+
className={`flex items-center justify-center gap-1.5 ${compact ? 'p-1.5 rounded-md' : 'px-3 py-1.5 rounded-lg'} text-sm font-medium transition-all ${assetTypeFilter === 'model'
|
|
443
|
+
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-md'
|
|
444
|
+
: 'bg-white dark:bg-slate-700 text-purple-600 dark:text-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20'
|
|
445
|
+
}`}
|
|
446
|
+
>
|
|
447
|
+
<Box className="w-3.5 h-3.5" />
|
|
448
|
+
{!compact && <>Models <span className="text-xs opacity-75">({data.artifacts.filter(a => a.type?.toLowerCase() === 'model').length})</span></>}
|
|
449
|
+
</button>
|
|
450
|
+
<button
|
|
451
|
+
onClick={() => setAssetTypeFilter('dataset')}
|
|
452
|
+
title="Datasets"
|
|
453
|
+
className={`flex items-center justify-center gap-1.5 ${compact ? 'p-1.5 rounded-md' : 'px-3 py-1.5 rounded-lg'} text-sm font-medium transition-all ${assetTypeFilter === 'dataset'
|
|
454
|
+
? 'bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-md'
|
|
455
|
+
: 'bg-white dark:bg-slate-700 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20'
|
|
456
|
+
}`}
|
|
457
|
+
>
|
|
458
|
+
<Database className="w-3.5 h-3.5" />
|
|
459
|
+
{!compact && <>Datasets <span className="text-xs opacity-75">({data.artifacts.filter(a => a.type?.toLowerCase() === 'dataset').length})</span></>}
|
|
460
|
+
</button>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
<div className={compact ? "flex-1 overflow-y-auto p-2" : "p-3 max-h-[700px] overflow-y-auto"}>
|
|
465
|
+
{data.pipelines.length === 0 && data.projects.length === 0 ? (
|
|
466
|
+
<div className="p-8 text-center">
|
|
467
|
+
<Database className="h-12 w-12 text-slate-300 dark:text-slate-600 mx-auto mb-3" />
|
|
468
|
+
<p className="text-slate-600 dark:text-slate-400 font-medium mb-1">No assets found</p>
|
|
469
|
+
<p className="text-slate-500 text-sm">Run a pipeline to generate artifacts</p>
|
|
470
|
+
</div>
|
|
471
|
+
) : (
|
|
472
|
+
renderContent()
|
|
473
|
+
)}
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
);
|
|
477
|
+
}
|