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.
Files changed (104) 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 +34 -17
  14. flowyml/core/retry_policy.py +80 -0
  15. flowyml/core/scheduler.py +9 -9
  16. flowyml/core/scheduler_config.py +2 -3
  17. flowyml/core/step.py +18 -1
  18. flowyml/core/submission_result.py +53 -0
  19. flowyml/integrations/keras.py +95 -22
  20. flowyml/monitoring/alerts.py +2 -2
  21. flowyml/stacks/__init__.py +15 -0
  22. flowyml/stacks/aws.py +599 -0
  23. flowyml/stacks/azure.py +295 -0
  24. flowyml/stacks/bridge.py +9 -9
  25. flowyml/stacks/components.py +24 -2
  26. flowyml/stacks/gcp.py +158 -11
  27. flowyml/stacks/local.py +5 -0
  28. flowyml/stacks/plugins.py +2 -2
  29. flowyml/stacks/registry.py +21 -0
  30. flowyml/storage/artifacts.py +15 -5
  31. flowyml/storage/materializers/__init__.py +2 -0
  32. flowyml/storage/materializers/base.py +33 -0
  33. flowyml/storage/materializers/cloudpickle.py +74 -0
  34. flowyml/storage/metadata.py +3 -881
  35. flowyml/storage/remote.py +590 -0
  36. flowyml/storage/sql.py +911 -0
  37. flowyml/ui/backend/dependencies.py +28 -0
  38. flowyml/ui/backend/main.py +43 -80
  39. flowyml/ui/backend/routers/assets.py +483 -17
  40. flowyml/ui/backend/routers/client.py +46 -0
  41. flowyml/ui/backend/routers/execution.py +13 -2
  42. flowyml/ui/backend/routers/experiments.py +97 -14
  43. flowyml/ui/backend/routers/metrics.py +168 -0
  44. flowyml/ui/backend/routers/pipelines.py +77 -12
  45. flowyml/ui/backend/routers/projects.py +33 -7
  46. flowyml/ui/backend/routers/runs.py +221 -12
  47. flowyml/ui/backend/routers/schedules.py +5 -21
  48. flowyml/ui/backend/routers/stats.py +14 -0
  49. flowyml/ui/backend/routers/traces.py +37 -53
  50. flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
  51. flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
  52. flowyml/ui/frontend/dist/index.html +2 -2
  53. flowyml/ui/frontend/src/App.jsx +4 -1
  54. flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
  55. flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
  56. flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
  57. flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
  58. flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
  59. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
  60. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
  61. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
  62. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
  63. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
  64. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
  65. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
  66. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
  67. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
  68. flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
  69. flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
  70. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
  71. flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
  72. flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
  73. flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
  74. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
  75. flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
  76. flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
  77. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
  78. flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
  79. flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
  80. flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
  81. flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
  82. flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
  83. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
  84. flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
  85. flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
  86. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
  87. flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
  88. flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
  89. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
  90. flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
  91. flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
  92. flowyml/ui/frontend/src/router/index.jsx +4 -0
  93. flowyml/ui/frontend/src/utils/date.js +10 -0
  94. flowyml/ui/frontend/src/utils/downloads.js +11 -0
  95. flowyml/utils/config.py +6 -0
  96. flowyml/utils/stack_config.py +45 -3
  97. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/METADATA +44 -4
  98. flowyml-1.4.0.dist-info/RECORD +200 -0
  99. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/licenses/LICENSE +1 -1
  100. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
  101. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
  102. flowyml-1.2.0.dist-info/RECORD +0 -159
  103. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/WHEEL +0 -0
  104. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/entry_points.txt +0 -0
@@ -5,8 +5,8 @@
5
5
  <meta charset="UTF-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>FlowyML</title>
8
- <script type="module" crossorigin src="/assets/index-DFNQnrUj.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-pWI271rZ.css">
8
+ <script type="module" crossorigin src="/assets/index-Dlz_ygOL.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-DcYwrn2j.css">
10
10
  </head>
11
11
 
12
12
  <body>
@@ -4,13 +4,16 @@ import React from 'react';
4
4
  import { RouterProvider } from 'react-router-dom';
5
5
  import { ThemeProvider } from './contexts/ThemeContext';
6
6
  import { ProjectProvider } from './contexts/ProjectContext';
7
+ import { ToastProvider } from './contexts/ToastContext';
7
8
  import { router } from './router';
8
9
 
9
10
  function App() {
10
11
  return (
11
12
  <ThemeProvider>
12
13
  <ProjectProvider>
13
- <RouterProvider router={router} />
14
+ <ToastProvider>
15
+ <RouterProvider router={router} />
16
+ </ToastProvider>
14
17
  </ProjectProvider>
15
18
  </ThemeProvider>
16
19
  );
@@ -1,7 +1,8 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
  import { fetchApi } from '../../utils/api';
3
+ import { downloadArtifactById } from '../../utils/downloads';
3
4
  import { Link } from 'react-router-dom';
4
- import { Database, Box, BarChart2, FileText, Search, Filter, Calendar, Package, Download, Eye, X, ArrowRight } from 'lucide-react';
5
+ import { Database, Box, BarChart2, FileText, Search, Filter, Calendar, Package, Download, Eye, X, ArrowRight, Network, Activity, HardDrive, List, Grid } from 'lucide-react';
5
6
  import { Card } from '../../components/ui/Card';
6
7
  import { Badge } from '../../components/ui/Badge';
7
8
  import { Button } from '../../components/ui/Button';
@@ -11,26 +12,37 @@ import { DataView } from '../../components/ui/DataView';
11
12
  import { useProject } from '../../contexts/ProjectContext';
12
13
  import { EmptyState } from '../../components/ui/EmptyState';
13
14
  import { KeyValue, KeyValueGrid } from '../../components/ui/KeyValue';
15
+ import { AssetStatsDashboard } from '../../components/AssetStatsDashboard';
16
+ import { AssetTreeHierarchy } from '../../components/AssetTreeHierarchy';
17
+ import { AssetDetailsPanel } from '../../components/AssetDetailsPanel';
18
+ import { ProjectSelector } from '../../components/ProjectSelector';
14
19
 
15
20
  export function Assets() {
16
21
  const [assets, setAssets] = useState([]);
17
22
  const [loading, setLoading] = useState(true);
23
+ const [error, setError] = useState(null);
18
24
  const [typeFilter, setTypeFilter] = useState('all');
19
25
  const [selectedAsset, setSelectedAsset] = useState(null);
26
+ const [viewMode, setViewMode] = useState('table'); // Default to table for better density
27
+ const [stats, setStats] = useState(null);
20
28
  const { selectedProject } = useProject();
21
29
 
22
30
  useEffect(() => {
23
31
  const fetchAssets = async () => {
24
32
  setLoading(true);
33
+ setError(null);
25
34
  try {
26
35
  const url = selectedProject
27
- ? `/api/assets?limit=50&project=${encodeURIComponent(selectedProject)}`
28
- : '/api/assets?limit=50';
36
+ ? `/api/assets/?limit=50&project=${encodeURIComponent(selectedProject)}`
37
+ : '/api/assets/?limit=50';
29
38
  const res = await fetchApi(url);
39
+ if (!res.ok) throw new Error(`Failed to fetch assets: ${res.statusText}`);
40
+
30
41
  const data = await res.json();
31
42
  setAssets(data.assets || []);
32
43
  } catch (err) {
33
44
  console.error(err);
45
+ setError(err.message);
34
46
  } finally {
35
47
  setLoading(false);
36
48
  }
@@ -38,6 +50,26 @@ export function Assets() {
38
50
  fetchAssets();
39
51
  }, [selectedProject]);
40
52
 
53
+ // Fetch stats for compact display
54
+ useEffect(() => {
55
+ const fetchStats = async () => {
56
+ try {
57
+ const url = selectedProject
58
+ ? `/api/assets/stats?project=${encodeURIComponent(selectedProject)}`
59
+ : '/api/assets/stats';
60
+ const res = await fetchApi(url);
61
+ if (res.ok) {
62
+ const data = await res.json();
63
+ setStats(data);
64
+ }
65
+ } catch (err) {
66
+ console.error('Failed to fetch stats:', err);
67
+ }
68
+ };
69
+
70
+ fetchStats();
71
+ }, [selectedProject]);
72
+
41
73
  // Get unique types
42
74
  const types = ['all', ...new Set(assets.map(a => a.type))];
43
75
 
@@ -53,10 +85,10 @@ export function Assets() {
53
85
  sortable: true,
54
86
  render: (asset) => {
55
87
  const typeConfig = {
56
- Dataset: { icon: <Database size={16} />, color: 'text-blue-600', bg: 'bg-blue-50' },
57
- Model: { icon: <Box size={16} />, color: 'text-purple-600', bg: 'bg-purple-50' },
58
- Metrics: { icon: <BarChart2 size={16} />, color: 'text-emerald-600', bg: 'bg-emerald-50' },
59
- default: { icon: <FileText size={16} />, color: 'text-slate-600', bg: 'bg-slate-50' }
88
+ Dataset: { icon: <Database size={14} />, color: 'text-blue-600', bg: 'bg-blue-50 dark:bg-blue-900/20' },
89
+ Model: { icon: <Box size={14} />, color: 'text-purple-600', bg: 'bg-purple-50 dark:bg-purple-900/20' },
90
+ Metrics: { icon: <BarChart2 size={14} />, color: 'text-emerald-600', bg: 'bg-emerald-50 dark:bg-emerald-900/20' },
91
+ default: { icon: <FileText size={14} />, color: 'text-slate-600', bg: 'bg-slate-50 dark:bg-slate-800' }
60
92
  };
61
93
  const config = typeConfig[asset.type] || typeConfig.default;
62
94
  return (
@@ -109,9 +141,10 @@ export function Assets() {
109
141
  render: (asset) => asset.run_id ? (
110
142
  <Link
111
143
  to={`/runs/${asset.run_id}`}
112
- className="font-mono text-xs bg-slate-100 dark:bg-slate-700 px-2 py-1 rounded text-primary-600 dark:text-primary-400 hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors"
144
+ className="font-mono text-xs text-primary-600 hover:underline"
145
+ onClick={(e) => e.stopPropagation()}
113
146
  >
114
- {asset.run_id.substring(0, 8)}...
147
+ {asset.run_id.substring(0, 8)}
115
148
  </Link>
116
149
  ) : (
117
150
  <span className="font-mono text-xs text-slate-400">-</span>
@@ -122,276 +155,273 @@ export function Assets() {
122
155
  key: 'created_at',
123
156
  sortable: true,
124
157
  render: (asset) => (
125
- <div className="flex items-center gap-2 text-slate-500">
126
- <Calendar size={14} />
158
+ <span className="text-sm text-slate-500">
127
159
  {asset.created_at ? format(new Date(asset.created_at), 'MMM d, HH:mm') : '-'}
128
- </div>
160
+ </span>
129
161
  )
130
162
  },
131
163
  {
132
- header: 'Actions',
164
+ header: '',
133
165
  key: 'actions',
134
166
  render: (asset) => (
135
- <button
136
- onClick={() => setSelectedAsset(asset)}
137
- className="text-primary-600 hover:text-primary-700 font-medium text-sm flex items-center gap-1"
138
- >
139
- View <Eye size={14} />
140
- </button>
167
+ <div className="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
168
+ <button
169
+ onClick={() => setSelectedAsset(asset)}
170
+ className="p-1 hover:bg-slate-100 dark:hover:bg-slate-800 rounded text-slate-500 hover:text-primary-600"
171
+ title="View Details"
172
+ >
173
+ <Eye size={16} />
174
+ </button>
175
+ <button
176
+ onClick={(e) => {
177
+ e.stopPropagation();
178
+ downloadArtifactById(asset.artifact_id);
179
+ }}
180
+ className="p-1 hover:bg-slate-100 dark:hover:bg-slate-800 rounded text-slate-500 hover:text-primary-600"
181
+ disabled={!asset.artifact_id}
182
+ title="Download"
183
+ >
184
+ <Download size={16} />
185
+ </button>
186
+ </div>
141
187
  )
142
188
  }
143
189
  ];
144
190
 
145
191
  const renderGrid = (asset) => {
146
192
  const typeConfig = {
147
- Dataset: { icon: <Database size={20} />, color: 'blue' },
148
- Model: { icon: <Box size={20} />, color: 'purple' },
149
- Metrics: { icon: <BarChart2 size={20} />, color: 'emerald' },
150
- default: { icon: <FileText size={20} />, color: 'slate' }
193
+ Dataset: { icon: <Database size={18} />, color: 'text-blue-600', bg: 'bg-blue-50 dark:bg-blue-900/20' },
194
+ Model: { icon: <Box size={18} />, color: 'text-purple-600', bg: 'bg-purple-50 dark:bg-purple-900/20' },
195
+ Metrics: { icon: <BarChart2 size={18} />, color: 'text-emerald-600', bg: 'bg-emerald-50 dark:bg-emerald-900/20' },
196
+ default: { icon: <FileText size={18} />, color: 'text-slate-600', bg: 'bg-slate-50 dark:bg-slate-800' }
151
197
  };
152
198
 
153
199
  const config = typeConfig[asset.type] || typeConfig.default;
154
200
 
155
- const colorClasses = {
156
- blue: 'from-blue-500 to-cyan-500',
157
- purple: 'from-purple-500 to-pink-500',
158
- emerald: 'from-emerald-500 to-teal-500',
159
- slate: 'from-slate-500 to-slate-600'
160
- };
161
-
162
201
  return (
163
- <Card
164
- className="group cursor-pointer hover:shadow-xl hover:border-primary-300 transition-all duration-200 overflow-hidden h-full"
165
- onClick={() => setSelectedAsset(asset)}
202
+ <motion.div
203
+ initial={{ opacity: 0, y: 10 }}
204
+ animate={{ opacity: 1, y: 0 }}
205
+ whileHover={{ y: -2 }}
206
+ transition={{ duration: 0.2 }}
166
207
  >
167
- {/* Icon Header */}
168
- <div className="flex items-start justify-between mb-3">
169
- <div className={`p-3 rounded-xl bg-gradient-to-br ${colorClasses[config.color]} text-white shadow-lg group-hover:scale-110 transition-transform`}>
170
- {config.icon}
171
- </div>
172
- <button className="p-2 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors opacity-0 group-hover:opacity-100">
173
- <Eye size={16} className="text-slate-400" />
174
- </button>
175
- </div>
208
+ <Card
209
+ className="group cursor-pointer hover:shadow-md hover:border-primary-200 dark:hover:border-primary-800 transition-all duration-200 h-full border border-slate-200 dark:border-slate-800"
210
+ onClick={() => setSelectedAsset(asset)}
211
+ >
212
+ <div className="p-4">
213
+ <div className="flex items-start justify-between mb-3">
214
+ <div className={`p-2 rounded-lg ${config.bg} ${config.color}`}>
215
+ {config.icon}
216
+ </div>
217
+ <Badge variant="outline" className="text-[10px] px-1.5 py-0 h-5 border-slate-200 dark:border-slate-700 text-slate-500">
218
+ {asset.type}
219
+ </Badge>
220
+ </div>
176
221
 
177
- {/* Name */}
178
- <h4 className="font-bold text-slate-900 dark:text-white mb-1 truncate group-hover:text-primary-600 transition-colors">
179
- {asset.name}
180
- </h4>
222
+ <h4 className="font-medium text-slate-900 dark:text-white mb-1 truncate group-hover:text-primary-600 transition-colors">
223
+ {asset.name}
224
+ </h4>
181
225
 
182
- {/* Metadata */}
183
- <div className="space-y-2 text-xs text-slate-500 dark:text-slate-400">
184
- <div className="flex items-center justify-between">
185
- <span>Step:</span>
186
- <span className="font-mono text-slate-700 dark:text-slate-300 truncate ml-2">{asset.step}</span>
187
- </div>
188
- {asset.created_at && (
189
- <div className="flex items-center justify-between">
190
- <span>Created:</span>
191
- <span className="text-slate-700 dark:text-slate-300">{format(new Date(asset.created_at), 'MMM d, HH:mm')}</span>
226
+ <div className="flex items-center gap-2 mb-4 text-xs text-slate-500">
227
+ <span className="truncate max-w-[120px]">{asset.step}</span>
228
+ {asset.project && (
229
+ <>
230
+ <span>•</span>
231
+ <span className="truncate max-w-[80px]">{asset.project}</span>
232
+ </>
233
+ )}
192
234
  </div>
193
- )}
194
- </div>
195
235
 
196
- {/* Properties Count */}
197
- {asset.properties && Object.keys(asset.properties).length > 0 && (
198
- <div className="mt-3 pt-3 border-t border-slate-100 dark:border-slate-700">
199
- <span className="text-xs text-slate-500">
200
- {Object.keys(asset.properties).length} properties
201
- </span>
236
+ <div className="pt-3 border-t border-slate-100 dark:border-slate-800 flex items-center justify-between text-xs text-slate-400">
237
+ <span>
238
+ {asset.created_at ? format(new Date(asset.created_at), 'MMM d') : '-'}
239
+ </span>
240
+ {asset.run_id && (
241
+ <span className="font-mono bg-slate-50 dark:bg-slate-800 px-1.5 py-0.5 rounded">
242
+ {asset.run_id.slice(0, 6)}
243
+ </span>
244
+ )}
245
+ </div>
202
246
  </div>
203
- )}
204
- </Card>
247
+ </Card>
248
+ </motion.div>
205
249
  );
206
250
  };
207
251
 
208
252
  if (loading) {
209
253
  return (
210
- <div className="flex items-center justify-center h-96">
211
- <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
254
+ <div className="flex items-center justify-center h-screen bg-slate-50 dark:bg-slate-900">
255
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
212
256
  </div>
213
257
  );
214
258
  }
215
259
 
216
- return (
217
- <div className="p-6 max-w-7xl mx-auto space-y-6">
218
- {/* Type Filter */}
219
- <div className="flex items-center gap-2 overflow-x-auto pb-2">
220
- <Filter size={16} className="text-slate-400 shrink-0" />
221
- <div className="flex gap-2">
222
- {types.map(type => (
223
- <button
224
- key={type}
225
- onClick={() => setTypeFilter(type)}
226
- className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-all whitespace-nowrap ${typeFilter === type
227
- ? 'bg-primary-500 text-white shadow-md'
228
- : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700'
229
- }`}
230
- >
231
- {type === 'all' ? 'All Types' : type}
232
- {type !== 'all' && (
233
- <span className="ml-1.5 text-xs opacity-75">
234
- ({assets.filter(a => a.type === type).length})
235
- </span>
236
- )}
237
- </button>
238
- ))}
260
+ if (error) {
261
+ return (
262
+ <div className="flex items-center justify-center h-screen bg-slate-50 dark:bg-slate-900">
263
+ <div className="text-center p-8 bg-red-50 dark:bg-red-900/20 rounded-2xl border border-red-100 dark:border-red-800 max-w-md">
264
+ <XCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
265
+ <h3 className="text-lg font-bold text-red-700 dark:text-red-300 mb-2">Failed to load assets</h3>
266
+ <p className="text-red-600 dark:text-red-400 mb-6">{error}</p>
267
+ <button
268
+ onClick={() => window.location.reload()}
269
+ className="px-4 py-2 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-sm hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors text-slate-700 dark:text-slate-300 font-medium"
270
+ >
271
+ Retry Connection
272
+ </button>
239
273
  </div>
240
274
  </div>
275
+ );
276
+ }
241
277
 
242
- <DataView
243
- title="Artifacts"
244
- subtitle="Browse and manage pipeline artifacts and outputs"
245
- items={filteredAssets}
246
- loading={loading}
247
- columns={columns}
248
- renderGrid={renderGrid}
249
- emptyState={
250
- <EmptyState
251
- icon={Package}
252
- title="No artifacts found"
253
- description={typeFilter !== 'all'
254
- ? 'Try adjusting your filters'
255
- : 'Run a pipeline to generate artifacts'
256
- }
257
- />
258
- }
259
- />
260
-
261
- {/* Asset Detail Modal */}
262
- <AssetDetailModal
263
- asset={selectedAsset}
264
- onClose={() => setSelectedAsset(null)}
265
- />
266
- </div>
267
- );
268
- }
269
-
270
- function AssetDetailModal({ asset, onClose }) {
271
- if (!asset) return null;
272
-
273
- const typeConfig = {
274
- Dataset: { icon: <Database size={24} />, color: 'text-blue-600', bg: 'bg-blue-50' },
275
- Model: { icon: <Box size={24} />, color: 'text-purple-600', bg: 'bg-purple-50' },
276
- Metrics: { icon: <BarChart2 size={24} />, color: 'text-emerald-600', bg: 'bg-emerald-50' },
277
- default: { icon: <FileText size={24} />, color: 'text-slate-600', bg: 'bg-slate-50' }
278
- };
278
+ return (
279
+ <div className="h-screen flex flex-col overflow-hidden bg-slate-50 dark:bg-slate-900">
280
+ {/* Header */}
281
+ <div className="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-6 py-4">
282
+ <div className="flex items-center justify-between max-w-[1800px] mx-auto">
283
+ <div>
284
+ <h1 className="text-xl font-bold text-slate-900 dark:text-white">Assets</h1>
285
+ <p className="text-sm text-slate-500 dark:text-slate-400 mt-0.5">
286
+ Manage and track your pipeline artifacts
287
+ </p>
288
+ </div>
279
289
 
280
- const config = typeConfig[asset.type] || typeConfig.default;
290
+ {!selectedAsset && (
291
+ <div className="flex items-center gap-2 bg-slate-100 dark:bg-slate-700 p-1 rounded-lg">
292
+ <button
293
+ onClick={() => setViewMode('grid')}
294
+ className={`p-1.5 rounded transition-all ${viewMode === 'grid'
295
+ ? 'bg-white dark:bg-slate-600 text-slate-900 dark:text-white shadow-sm'
296
+ : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
297
+ }`}
298
+ title="Grid View"
299
+ >
300
+ <Grid size={16} />
301
+ </button>
302
+ <button
303
+ onClick={() => setViewMode('table')}
304
+ className={`p-1.5 rounded transition-all ${viewMode === 'table'
305
+ ? 'bg-white dark:bg-slate-600 text-slate-900 dark:text-white shadow-sm'
306
+ : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
307
+ }`}
308
+ title="Table View"
309
+ >
310
+ <List size={16} />
311
+ </button>
312
+ </div>
313
+ )}
314
+ </div>
315
+ </div>
281
316
 
282
- return (
283
- <AnimatePresence>
284
- <motion.div
285
- initial={{ opacity: 0 }}
286
- animate={{ opacity: 1 }}
287
- exit={{ opacity: 0 }}
288
- className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
289
- onClick={onClose}
290
- >
291
- <motion.div
292
- initial={{ scale: 0.9, opacity: 0 }}
293
- animate={{ scale: 1, opacity: 1 }}
294
- exit={{ scale: 0.9, opacity: 0 }}
295
- onClick={(e) => e.stopPropagation()}
296
- className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-4xl w-full max-h-[85vh] overflow-hidden border border-slate-200 dark:border-slate-700"
297
- >
298
- {/* Header */}
299
- <div className={`flex items-center justify-between p-6 border-b border-slate-100 dark:border-slate-700 ${config.bg} dark:bg-slate-800`}>
300
- <div className="flex items-center gap-4">
301
- <div className={`p-3 bg-white dark:bg-slate-700 rounded-xl shadow-sm ${config.color}`}>
302
- {config.icon}
317
+ {/* Main Content */}
318
+ <div className="flex-1 overflow-hidden">
319
+ <div className="h-full max-w-[1800px] mx-auto px-6 py-6">
320
+ <div className="h-full flex gap-6">
321
+ {/* Sidebar */}
322
+ <div className="w-[380px] shrink-0 flex flex-col gap-4 overflow-y-auto pb-6">
323
+ {/* Stats */}
324
+ <div className="grid grid-cols-2 gap-3">
325
+ <StatCardCompact
326
+ icon={Package}
327
+ label="Total"
328
+ value={stats?.total_assets || 0}
329
+ />
330
+ <StatCardCompact
331
+ icon={HardDrive}
332
+ label="Size"
333
+ value={formatBytes(stats?.total_storage_bytes || 0)}
334
+ />
303
335
  </div>
304
- <div>
305
- <h3 className="text-2xl font-bold text-slate-900 dark:text-white">{asset.name}</h3>
306
- <p className="text-sm text-slate-500 mt-1">{asset.type} {asset.step}</p>
336
+
337
+ {/* Tree */}
338
+ <div className="flex-1 min-h-0 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden flex flex-col">
339
+ <div className="p-3 border-b border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50">
340
+ <h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Explorer</h3>
341
+ </div>
342
+ <div className="flex-1 overflow-y-auto p-2">
343
+ <AssetTreeHierarchy
344
+ projectId={selectedProject}
345
+ onAssetSelect={(asset) => setSelectedAsset(asset)}
346
+ compact={true}
347
+ />
348
+ </div>
307
349
  </div>
308
350
  </div>
309
- <button
310
- onClick={onClose}
311
- className="p-2 hover:bg-white dark:hover:bg-slate-700 rounded-lg transition-colors"
312
- >
313
- <X size={20} className="text-slate-400" />
314
- </button>
315
- </div>
316
351
 
317
- {/* Content */}
318
- <div className="p-6 overflow-y-auto max-h-[calc(85vh-200px)]">
319
- <div className="space-y-6">
320
- {/* Properties */}
321
- {asset.properties && Object.keys(asset.properties).length > 0 && (
322
- <div>
323
- <h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Properties</h4>
324
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
325
- {Object.entries(asset.properties).map(([key, value]) => (
326
- <div key={key} className="p-4 bg-slate-50 dark:bg-slate-700/50 rounded-lg border border-slate-100 dark:border-slate-700">
327
- <span className="text-sm text-slate-500 dark:text-slate-400 block mb-2 font-medium">{key}</span>
328
- <span className="text-base font-mono font-semibold text-slate-900 dark:text-white break-all">
329
- {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)}
330
- </span>
331
- </div>
332
- ))}
352
+ {/* Content Area */}
353
+ <div className="flex-1 min-w-0 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden flex flex-col shadow-sm">
354
+ {selectedAsset ? (
355
+ <AssetDetailsPanel
356
+ asset={selectedAsset}
357
+ onClose={() => setSelectedAsset(null)}
358
+ />
359
+ ) : (
360
+ <div className="h-full flex flex-col">
361
+ {/* Filters */}
362
+ <div className="p-4 border-b border-slate-100 dark:border-slate-700 flex items-center gap-4 overflow-x-auto">
363
+ <Filter size={16} className="text-slate-400 shrink-0" />
364
+ <div className="flex gap-2">
365
+ {types.map(type => (
366
+ <button
367
+ key={type}
368
+ onClick={() => setTypeFilter(type)}
369
+ className={`px-3 py-1.5 rounded-full text-xs font-medium transition-all whitespace-nowrap border ${typeFilter === type
370
+ ? 'bg-slate-900 text-white border-slate-900 dark:bg-white dark:text-slate-900 dark:border-white'
371
+ : 'bg-white text-slate-600 border-slate-200 hover:border-slate-300 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-700 dark:hover:border-slate-600'
372
+ }`}
373
+ >
374
+ {type === 'all' ? 'All' : type}
375
+ </button>
376
+ ))}
377
+ </div>
333
378
  </div>
334
- </div>
335
- )}
336
379
 
337
- {/* Value Preview */}
338
- {asset.value && (
339
- <div>
340
- <h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Value Preview</h4>
341
- <pre className="p-4 bg-slate-900 text-slate-100 rounded-lg text-sm font-mono overflow-x-auto leading-relaxed">
342
- {asset.value}
343
- </pre>
380
+ {/* Data View */}
381
+ <div className="flex-1 min-h-0 overflow-hidden">
382
+ <DataView
383
+ items={filteredAssets}
384
+ loading={loading}
385
+ columns={columns}
386
+ renderGrid={renderGrid}
387
+ initialView={viewMode}
388
+ emptyState={
389
+ <EmptyState
390
+ icon={Package}
391
+ title="No artifacts found"
392
+ description="Try adjusting your filters or run a pipeline to generate artifacts."
393
+ />
394
+ }
395
+ />
396
+ </div>
344
397
  </div>
345
398
  )}
346
-
347
- {/* Metadata */}
348
- <div>
349
- <h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Metadata</h4>
350
- <KeyValueGrid items={[
351
- { label: "Artifact ID", value: asset.artifact_id, valueClassName: "font-mono text-xs" },
352
- { label: "Type", value: asset.type },
353
- { label: "Step", value: asset.step },
354
- { label: "Run ID", value: asset.run_id, valueClassName: "font-mono text-xs" },
355
- ...(asset.created_at ? [{
356
- label: "Created At",
357
- value: format(new Date(asset.created_at), 'MMM d, yyyy HH:mm:ss')
358
- }] : []),
359
- ...(asset.path ? [{
360
- label: "Path",
361
- value: asset.path,
362
- valueClassName: "font-mono text-xs"
363
- }] : [])
364
- ]} columns={2} />
365
- </div>
366
- </div>
367
- </div>
368
-
369
- {/* Footer */}
370
- <div className="p-4 border-t border-slate-100 dark:border-slate-700 bg-slate-50 dark:bg-slate-800 flex justify-between items-center">
371
- <span className="text-sm text-slate-500">
372
- {asset.created_at && `Created ${format(new Date(asset.created_at), 'MMM d, yyyy')}`}
373
- </span>
374
- <div className="flex gap-2">
375
- <Button variant="ghost" onClick={onClose}>Close</Button>
376
- <Button variant="primary" className="flex items-center gap-2">
377
- <Download size={16} />
378
- Download
379
- </Button>
380
399
  </div>
381
400
  </div>
382
- </motion.div>
383
- </motion.div>
384
- </AnimatePresence>
401
+ </div>
402
+ </div>
403
+ </div>
385
404
  );
386
405
  }
387
406
 
407
+ function StatCardCompact({ icon: Icon, label, value }) {
408
+ return (
409
+ <div className="bg-white dark:bg-slate-800 rounded-xl p-3 border border-slate-200 dark:border-slate-700 shadow-sm">
410
+ <div className="flex items-center gap-2 mb-1">
411
+ <Icon size={14} className="text-slate-400" />
412
+ <span className="text-xs text-slate-500 dark:text-slate-400">{label}</span>
413
+ </div>
414
+ <p className="text-lg font-semibold text-slate-900 dark:text-white">
415
+ {typeof value === 'number' ? value.toLocaleString() : value}
416
+ </p>
417
+ </div>
418
+ );
419
+ }
388
420
 
389
-
390
- function getTypeIcon(type) {
391
- const icons = {
392
- Dataset: <Database size={18} className="text-blue-600" />,
393
- Model: <Box size={18} className="text-purple-600" />,
394
- Metrics: <BarChart2 size={18} className="text-emerald-600" />
395
- };
396
- return icons[type] || <FileText size={18} className="text-slate-600" />;
421
+ function formatBytes(bytes) {
422
+ if (!bytes || bytes === 0) return '0 B';
423
+ const k = 1024;
424
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
425
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
426
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
397
427
  }