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,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
+ }