flowyml 1.1.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 +207 -0
- flowyml/assets/__init__.py +22 -0
- flowyml/assets/artifact.py +40 -0
- flowyml/assets/base.py +209 -0
- flowyml/assets/dataset.py +100 -0
- flowyml/assets/featureset.py +301 -0
- flowyml/assets/metrics.py +104 -0
- flowyml/assets/model.py +82 -0
- flowyml/assets/registry.py +157 -0
- flowyml/assets/report.py +315 -0
- flowyml/cli/__init__.py +5 -0
- flowyml/cli/experiment.py +232 -0
- flowyml/cli/init.py +256 -0
- flowyml/cli/main.py +327 -0
- flowyml/cli/run.py +75 -0
- flowyml/cli/stack_cli.py +532 -0
- flowyml/cli/ui.py +33 -0
- flowyml/core/__init__.py +68 -0
- flowyml/core/advanced_cache.py +274 -0
- flowyml/core/approval.py +64 -0
- flowyml/core/cache.py +203 -0
- flowyml/core/checkpoint.py +148 -0
- flowyml/core/conditional.py +373 -0
- flowyml/core/context.py +155 -0
- flowyml/core/error_handling.py +419 -0
- flowyml/core/executor.py +354 -0
- flowyml/core/graph.py +185 -0
- flowyml/core/parallel.py +452 -0
- flowyml/core/pipeline.py +764 -0
- flowyml/core/project.py +253 -0
- flowyml/core/resources.py +424 -0
- flowyml/core/scheduler.py +630 -0
- flowyml/core/scheduler_config.py +32 -0
- flowyml/core/step.py +201 -0
- flowyml/core/step_grouping.py +292 -0
- flowyml/core/templates.py +226 -0
- flowyml/core/versioning.py +217 -0
- flowyml/integrations/__init__.py +1 -0
- flowyml/integrations/keras.py +134 -0
- flowyml/monitoring/__init__.py +1 -0
- flowyml/monitoring/alerts.py +57 -0
- flowyml/monitoring/data.py +102 -0
- flowyml/monitoring/llm.py +160 -0
- flowyml/monitoring/monitor.py +57 -0
- flowyml/monitoring/notifications.py +246 -0
- flowyml/registry/__init__.py +5 -0
- flowyml/registry/model_registry.py +491 -0
- flowyml/registry/pipeline_registry.py +55 -0
- flowyml/stacks/__init__.py +27 -0
- flowyml/stacks/base.py +77 -0
- flowyml/stacks/bridge.py +288 -0
- flowyml/stacks/components.py +155 -0
- flowyml/stacks/gcp.py +499 -0
- flowyml/stacks/local.py +112 -0
- flowyml/stacks/migration.py +97 -0
- flowyml/stacks/plugin_config.py +78 -0
- flowyml/stacks/plugins.py +401 -0
- flowyml/stacks/registry.py +226 -0
- flowyml/storage/__init__.py +26 -0
- flowyml/storage/artifacts.py +246 -0
- flowyml/storage/materializers/__init__.py +20 -0
- flowyml/storage/materializers/base.py +133 -0
- flowyml/storage/materializers/keras.py +185 -0
- flowyml/storage/materializers/numpy.py +94 -0
- flowyml/storage/materializers/pandas.py +142 -0
- flowyml/storage/materializers/pytorch.py +135 -0
- flowyml/storage/materializers/sklearn.py +110 -0
- flowyml/storage/materializers/tensorflow.py +152 -0
- flowyml/storage/metadata.py +931 -0
- flowyml/tracking/__init__.py +1 -0
- flowyml/tracking/experiment.py +211 -0
- flowyml/tracking/leaderboard.py +191 -0
- flowyml/tracking/runs.py +145 -0
- flowyml/ui/__init__.py +15 -0
- flowyml/ui/backend/Dockerfile +31 -0
- flowyml/ui/backend/__init__.py +0 -0
- flowyml/ui/backend/auth.py +163 -0
- flowyml/ui/backend/main.py +187 -0
- flowyml/ui/backend/routers/__init__.py +0 -0
- flowyml/ui/backend/routers/assets.py +45 -0
- flowyml/ui/backend/routers/execution.py +179 -0
- flowyml/ui/backend/routers/experiments.py +49 -0
- flowyml/ui/backend/routers/leaderboard.py +118 -0
- flowyml/ui/backend/routers/notifications.py +72 -0
- flowyml/ui/backend/routers/pipelines.py +110 -0
- flowyml/ui/backend/routers/plugins.py +192 -0
- flowyml/ui/backend/routers/projects.py +85 -0
- flowyml/ui/backend/routers/runs.py +66 -0
- flowyml/ui/backend/routers/schedules.py +222 -0
- flowyml/ui/backend/routers/traces.py +84 -0
- flowyml/ui/frontend/Dockerfile +20 -0
- flowyml/ui/frontend/README.md +315 -0
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
- flowyml/ui/frontend/dist/index.html +16 -0
- flowyml/ui/frontend/index.html +15 -0
- flowyml/ui/frontend/nginx.conf +26 -0
- flowyml/ui/frontend/package-lock.json +3545 -0
- flowyml/ui/frontend/package.json +33 -0
- flowyml/ui/frontend/postcss.config.js +6 -0
- flowyml/ui/frontend/src/App.jsx +21 -0
- flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
- flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
- flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
- flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
- flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
- flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
- flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
- flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
- flowyml/ui/frontend/src/components/Layout.jsx +108 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
- flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
- flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
- flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
- flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
- flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
- flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
- flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
- flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
- flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
- flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
- flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
- flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
- flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
- flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
- flowyml/ui/frontend/src/index.css +11 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
- flowyml/ui/frontend/src/main.jsx +10 -0
- flowyml/ui/frontend/src/router/index.jsx +39 -0
- flowyml/ui/frontend/src/services/pluginService.js +90 -0
- flowyml/ui/frontend/src/utils/api.js +47 -0
- flowyml/ui/frontend/src/utils/cn.js +6 -0
- flowyml/ui/frontend/tailwind.config.js +31 -0
- flowyml/ui/frontend/vite.config.js +21 -0
- flowyml/ui/utils.py +77 -0
- flowyml/utils/__init__.py +67 -0
- flowyml/utils/config.py +308 -0
- flowyml/utils/debug.py +240 -0
- flowyml/utils/environment.py +346 -0
- flowyml/utils/git.py +319 -0
- flowyml/utils/logging.py +61 -0
- flowyml/utils/performance.py +314 -0
- flowyml/utils/stack_config.py +296 -0
- flowyml/utils/validation.py +270 -0
- flowyml-1.1.0.dist-info/METADATA +372 -0
- flowyml-1.1.0.dist-info/RECORD +159 -0
- flowyml-1.1.0.dist-info/WHEEL +4 -0
- flowyml-1.1.0.dist-info/entry_points.txt +3 -0
- flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"clsx": "^2.1.1",
|
|
4
|
+
"dagre": "^0.8.5",
|
|
5
|
+
"date-fns": "^4.1.0",
|
|
6
|
+
"framer-motion": "^12.23.24",
|
|
7
|
+
"lucide-react": "^0.344.0",
|
|
8
|
+
"react": "^18.2.0",
|
|
9
|
+
"react-dom": "^18.2.0",
|
|
10
|
+
"react-router-dom": "^6.22.0",
|
|
11
|
+
"reactflow": "^11.11.4",
|
|
12
|
+
"recharts": "^2.12.0",
|
|
13
|
+
"tailwind-merge": "^2.6.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/react": "^18.2.64",
|
|
17
|
+
"@types/react-dom": "^18.2.21",
|
|
18
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
19
|
+
"autoprefixer": "^10.4.18",
|
|
20
|
+
"postcss": "^8.4.35",
|
|
21
|
+
"tailwindcss": "^3.4.1",
|
|
22
|
+
"vite": "^5.1.6"
|
|
23
|
+
},
|
|
24
|
+
"name": "flowy-ui",
|
|
25
|
+
"private": true,
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "vite build",
|
|
28
|
+
"dev": "vite",
|
|
29
|
+
"preview": "vite preview"
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
32
|
+
"version": "0.1.0"
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { RouterProvider } from 'react-router-dom';
|
|
5
|
+
import { ThemeProvider } from './contexts/ThemeContext';
|
|
6
|
+
import { ProjectProvider } from './contexts/ProjectContext';
|
|
7
|
+
import { router } from './router';
|
|
8
|
+
|
|
9
|
+
function App() {
|
|
10
|
+
return (
|
|
11
|
+
<ThemeProvider>
|
|
12
|
+
<ProjectProvider>
|
|
13
|
+
<RouterProvider router={router} />
|
|
14
|
+
</ProjectProvider>
|
|
15
|
+
</ThemeProvider>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export default App
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { fetchApi } from '../../utils/api';
|
|
3
|
+
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 { Card } from '../../components/ui/Card';
|
|
6
|
+
import { Badge } from '../../components/ui/Badge';
|
|
7
|
+
import { Button } from '../../components/ui/Button';
|
|
8
|
+
import { format } from 'date-fns';
|
|
9
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
10
|
+
import { DataView } from '../../components/ui/DataView';
|
|
11
|
+
import { useProject } from '../../contexts/ProjectContext';
|
|
12
|
+
import { EmptyState } from '../../components/ui/EmptyState';
|
|
13
|
+
import { KeyValue, KeyValueGrid } from '../../components/ui/KeyValue';
|
|
14
|
+
|
|
15
|
+
export function Assets() {
|
|
16
|
+
const [assets, setAssets] = useState([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
const [typeFilter, setTypeFilter] = useState('all');
|
|
19
|
+
const [selectedAsset, setSelectedAsset] = useState(null);
|
|
20
|
+
const { selectedProject } = useProject();
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const fetchAssets = async () => {
|
|
24
|
+
setLoading(true);
|
|
25
|
+
try {
|
|
26
|
+
const url = selectedProject
|
|
27
|
+
? `/api/assets?limit=50&project=${encodeURIComponent(selectedProject)}`
|
|
28
|
+
: '/api/assets?limit=50';
|
|
29
|
+
const res = await fetchApi(url);
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
setAssets(data.assets || []);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(err);
|
|
34
|
+
} finally {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
fetchAssets();
|
|
39
|
+
}, [selectedProject]);
|
|
40
|
+
|
|
41
|
+
// Get unique types
|
|
42
|
+
const types = ['all', ...new Set(assets.map(a => a.type))];
|
|
43
|
+
|
|
44
|
+
// Filter assets
|
|
45
|
+
const filteredAssets = assets.filter(asset => {
|
|
46
|
+
return typeFilter === 'all' || asset.type === typeFilter;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const columns = [
|
|
50
|
+
{
|
|
51
|
+
header: 'Type',
|
|
52
|
+
key: 'type',
|
|
53
|
+
sortable: true,
|
|
54
|
+
render: (asset) => {
|
|
55
|
+
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' }
|
|
60
|
+
};
|
|
61
|
+
const config = typeConfig[asset.type] || typeConfig.default;
|
|
62
|
+
return (
|
|
63
|
+
<div className={`flex items-center gap-2 px-2 py-1 rounded-md w-fit ${config.bg} ${config.color}`}>
|
|
64
|
+
{config.icon}
|
|
65
|
+
<span className="text-xs font-medium">{asset.type}</span>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
header: 'Name',
|
|
72
|
+
key: 'name',
|
|
73
|
+
sortable: true,
|
|
74
|
+
render: (asset) => (
|
|
75
|
+
<span className="font-medium text-slate-900 dark:text-white">{asset.name}</span>
|
|
76
|
+
)
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
header: 'Step',
|
|
80
|
+
key: 'step',
|
|
81
|
+
sortable: true,
|
|
82
|
+
render: (asset) => (
|
|
83
|
+
<span className="font-mono text-xs text-slate-500">{asset.step}</span>
|
|
84
|
+
)
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
header: 'Pipeline',
|
|
88
|
+
key: 'pipeline',
|
|
89
|
+
sortable: true,
|
|
90
|
+
render: (asset) => (
|
|
91
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
92
|
+
{asset.pipeline_name || '-'}
|
|
93
|
+
</span>
|
|
94
|
+
)
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
header: 'Project',
|
|
98
|
+
key: 'project',
|
|
99
|
+
sortable: true,
|
|
100
|
+
render: (asset) => (
|
|
101
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
102
|
+
{asset.project || '-'}
|
|
103
|
+
</span>
|
|
104
|
+
)
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
header: 'Run ID',
|
|
108
|
+
key: 'run_id',
|
|
109
|
+
render: (asset) => asset.run_id ? (
|
|
110
|
+
<Link
|
|
111
|
+
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"
|
|
113
|
+
>
|
|
114
|
+
{asset.run_id.substring(0, 8)}...
|
|
115
|
+
</Link>
|
|
116
|
+
) : (
|
|
117
|
+
<span className="font-mono text-xs text-slate-400">-</span>
|
|
118
|
+
)
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
header: 'Created',
|
|
122
|
+
key: 'created_at',
|
|
123
|
+
sortable: true,
|
|
124
|
+
render: (asset) => (
|
|
125
|
+
<div className="flex items-center gap-2 text-slate-500">
|
|
126
|
+
<Calendar size={14} />
|
|
127
|
+
{asset.created_at ? format(new Date(asset.created_at), 'MMM d, HH:mm') : '-'}
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
header: 'Actions',
|
|
133
|
+
key: 'actions',
|
|
134
|
+
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>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const renderGrid = (asset) => {
|
|
146
|
+
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' }
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const config = typeConfig[asset.type] || typeConfig.default;
|
|
154
|
+
|
|
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
|
+
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)}
|
|
166
|
+
>
|
|
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>
|
|
176
|
+
|
|
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>
|
|
181
|
+
|
|
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>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
|
|
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>
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
</Card>
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (loading) {
|
|
209
|
+
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>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
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
|
+
))}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
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
|
+
};
|
|
279
|
+
|
|
280
|
+
const config = typeConfig[asset.type] || typeConfig.default;
|
|
281
|
+
|
|
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}
|
|
303
|
+
</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>
|
|
307
|
+
</div>
|
|
308
|
+
</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
|
+
|
|
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
|
+
))}
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
|
|
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>
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
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
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
</motion.div>
|
|
383
|
+
</motion.div>
|
|
384
|
+
</AnimatePresence>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
|
|
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" />;
|
|
397
|
+
}
|