flowyml 1.2.0__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowyml/__init__.py +3 -0
- flowyml/assets/base.py +10 -0
- flowyml/assets/metrics.py +6 -0
- flowyml/cli/main.py +108 -2
- flowyml/cli/run.py +9 -2
- flowyml/core/execution_status.py +52 -0
- flowyml/core/hooks.py +106 -0
- flowyml/core/observability.py +210 -0
- flowyml/core/orchestrator.py +274 -0
- flowyml/core/pipeline.py +193 -231
- flowyml/core/project.py +34 -2
- flowyml/core/remote_orchestrator.py +109 -0
- flowyml/core/resources.py +34 -17
- flowyml/core/retry_policy.py +80 -0
- flowyml/core/scheduler.py +9 -9
- flowyml/core/scheduler_config.py +2 -3
- flowyml/core/step.py +18 -1
- flowyml/core/submission_result.py +53 -0
- flowyml/integrations/keras.py +95 -22
- flowyml/monitoring/alerts.py +2 -2
- flowyml/stacks/__init__.py +15 -0
- flowyml/stacks/aws.py +599 -0
- flowyml/stacks/azure.py +295 -0
- flowyml/stacks/bridge.py +9 -9
- flowyml/stacks/components.py +24 -2
- flowyml/stacks/gcp.py +158 -11
- flowyml/stacks/local.py +5 -0
- flowyml/stacks/plugins.py +2 -2
- flowyml/stacks/registry.py +21 -0
- flowyml/storage/artifacts.py +15 -5
- flowyml/storage/materializers/__init__.py +2 -0
- flowyml/storage/materializers/base.py +33 -0
- flowyml/storage/materializers/cloudpickle.py +74 -0
- flowyml/storage/metadata.py +3 -881
- flowyml/storage/remote.py +590 -0
- flowyml/storage/sql.py +911 -0
- flowyml/ui/backend/dependencies.py +28 -0
- flowyml/ui/backend/main.py +43 -80
- flowyml/ui/backend/routers/assets.py +483 -17
- flowyml/ui/backend/routers/client.py +46 -0
- flowyml/ui/backend/routers/execution.py +13 -2
- flowyml/ui/backend/routers/experiments.py +97 -14
- flowyml/ui/backend/routers/metrics.py +168 -0
- flowyml/ui/backend/routers/pipelines.py +77 -12
- flowyml/ui/backend/routers/projects.py +33 -7
- flowyml/ui/backend/routers/runs.py +221 -12
- flowyml/ui/backend/routers/schedules.py +5 -21
- flowyml/ui/backend/routers/stats.py +14 -0
- flowyml/ui/backend/routers/traces.py +37 -53
- flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
- flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/src/App.jsx +4 -1
- flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
- flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
- flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
- flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
- flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
- flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
- flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
- flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
- flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
- flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
- flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
- flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
- flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
- flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
- flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
- flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
- flowyml/ui/frontend/src/router/index.jsx +4 -0
- flowyml/ui/frontend/src/utils/date.js +10 -0
- flowyml/ui/frontend/src/utils/downloads.js +11 -0
- flowyml/utils/config.py +6 -0
- flowyml/utils/stack_config.py +45 -3
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/METADATA +44 -4
- flowyml-1.4.0.dist-info/RECORD +200 -0
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/licenses/LICENSE +1 -1
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
- flowyml-1.2.0.dist-info/RECORD +0 -159
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/WHEEL +0 -0
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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>
|
flowyml/ui/frontend/src/App.jsx
CHANGED
|
@@ -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
|
-
<
|
|
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
|
|
28
|
-
: '/api/assets
|
|
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={
|
|
57
|
-
Model: { icon: <Box size={
|
|
58
|
-
Metrics: { icon: <BarChart2 size={
|
|
59
|
-
default: { icon: <FileText size={
|
|
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
|
|
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
|
-
<
|
|
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
|
-
</
|
|
160
|
+
</span>
|
|
129
161
|
)
|
|
130
162
|
},
|
|
131
163
|
{
|
|
132
|
-
header: '
|
|
164
|
+
header: '',
|
|
133
165
|
key: 'actions',
|
|
134
166
|
render: (asset) => (
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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={
|
|
148
|
-
Model: { icon: <Box size={
|
|
149
|
-
Metrics: { icon: <BarChart2 size={
|
|
150
|
-
default: { icon: <FileText size={
|
|
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
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
{
|
|
201
|
-
|
|
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
|
-
</
|
|
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-
|
|
211
|
-
<div className="animate-spin rounded-full h-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
</
|
|
383
|
-
</
|
|
384
|
-
</
|
|
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
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
}
|