flowyml 1.1.0__py3-none-any.whl → 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowyml/__init__.py +3 -0
- flowyml/assets/base.py +10 -0
- flowyml/assets/metrics.py +6 -0
- flowyml/cli/main.py +108 -2
- flowyml/cli/run.py +9 -2
- flowyml/core/execution_status.py +52 -0
- flowyml/core/hooks.py +106 -0
- flowyml/core/observability.py +210 -0
- flowyml/core/orchestrator.py +274 -0
- flowyml/core/pipeline.py +193 -231
- flowyml/core/project.py +34 -2
- flowyml/core/remote_orchestrator.py +109 -0
- flowyml/core/resources.py +22 -5
- flowyml/core/retry_policy.py +80 -0
- flowyml/core/step.py +18 -1
- flowyml/core/submission_result.py +53 -0
- flowyml/core/versioning.py +2 -2
- flowyml/integrations/keras.py +95 -22
- flowyml/monitoring/alerts.py +2 -2
- flowyml/stacks/__init__.py +15 -0
- flowyml/stacks/aws.py +599 -0
- flowyml/stacks/azure.py +295 -0
- flowyml/stacks/components.py +24 -2
- flowyml/stacks/gcp.py +158 -11
- flowyml/stacks/local.py +5 -0
- flowyml/storage/artifacts.py +15 -5
- flowyml/storage/materializers/__init__.py +2 -0
- flowyml/storage/materializers/cloudpickle.py +74 -0
- flowyml/storage/metadata.py +166 -5
- flowyml/ui/backend/main.py +41 -1
- flowyml/ui/backend/routers/assets.py +356 -15
- flowyml/ui/backend/routers/client.py +46 -0
- flowyml/ui/backend/routers/execution.py +13 -2
- flowyml/ui/backend/routers/experiments.py +48 -12
- flowyml/ui/backend/routers/metrics.py +213 -0
- flowyml/ui/backend/routers/pipelines.py +63 -7
- flowyml/ui/backend/routers/projects.py +33 -7
- flowyml/ui/backend/routers/runs.py +150 -8
- flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
- flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/src/App.jsx +4 -1
- flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
- flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
- flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
- flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
- flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
- flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
- flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
- flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
- flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
- flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
- flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
- flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
- flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
- flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
- flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
- flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
- flowyml/ui/frontend/src/router/index.jsx +4 -0
- flowyml/ui/frontend/src/utils/date.js +10 -0
- flowyml/ui/frontend/src/utils/downloads.js +11 -0
- flowyml/utils/config.py +6 -0
- flowyml/utils/stack_config.py +45 -3
- {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/METADATA +113 -12
- {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/RECORD +90 -53
- {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/licenses/LICENSE +1 -1
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
- {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/WHEEL +0 -0
- {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { fetchApi } from '../../../../utils/api';
|
|
3
|
+
import { DataView } from '../../../../components/ui/DataView';
|
|
4
|
+
import { Link } from 'react-router-dom';
|
|
5
|
+
import { Clock, CheckCircle, XCircle, AlertCircle, Loader2 } from 'lucide-react';
|
|
6
|
+
import { formatDate } from '../../../../utils/date';
|
|
7
|
+
|
|
8
|
+
const StatusIcon = ({ status }) => {
|
|
9
|
+
switch (status?.toLowerCase()) {
|
|
10
|
+
case 'completed':
|
|
11
|
+
case 'success':
|
|
12
|
+
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
|
13
|
+
case 'failed':
|
|
14
|
+
return <XCircle className="w-4 h-4 text-red-500" />;
|
|
15
|
+
case 'running':
|
|
16
|
+
return <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />;
|
|
17
|
+
default:
|
|
18
|
+
return <AlertCircle className="w-4 h-4 text-slate-400" />;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function ProjectRunsList({ projectId }) {
|
|
23
|
+
const [runs, setRuns] = useState([]);
|
|
24
|
+
const [loading, setLoading] = useState(true);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const fetchRuns = async () => {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetchApi(`/api/runs?project=${projectId}`);
|
|
30
|
+
const data = await response.json();
|
|
31
|
+
// API returns {runs: [...]}
|
|
32
|
+
setRuns(Array.isArray(data?.runs) ? data.runs : []);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Failed to fetch runs:', error);
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (projectId) {
|
|
41
|
+
fetchRuns();
|
|
42
|
+
}
|
|
43
|
+
}, [projectId]);
|
|
44
|
+
|
|
45
|
+
const columns = [
|
|
46
|
+
{
|
|
47
|
+
header: 'Run Name',
|
|
48
|
+
key: 'name',
|
|
49
|
+
render: (run) => (
|
|
50
|
+
<Link to={`/runs/${run.run_id}`} className="flex items-center gap-3 group">
|
|
51
|
+
<div className="p-2 bg-slate-100 dark:bg-slate-700 rounded-lg group-hover:bg-slate-200 dark:group-hover:bg-slate-600 transition-colors">
|
|
52
|
+
<Clock className="w-4 h-4 text-slate-500" />
|
|
53
|
+
</div>
|
|
54
|
+
<div>
|
|
55
|
+
<div className="font-medium text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
|
56
|
+
{run.name || run.run_id.substring(0, 8)}
|
|
57
|
+
</div>
|
|
58
|
+
<div className="text-xs text-slate-500">{run.pipeline_name}</div>
|
|
59
|
+
</div>
|
|
60
|
+
</Link>
|
|
61
|
+
)
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
header: 'Status',
|
|
65
|
+
key: 'status',
|
|
66
|
+
render: (run) => (
|
|
67
|
+
<div className="flex items-center gap-2">
|
|
68
|
+
<StatusIcon status={run.status} />
|
|
69
|
+
<span className="capitalize text-sm">{run.status}</span>
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
header: 'Created',
|
|
75
|
+
key: 'created',
|
|
76
|
+
render: (run) => (
|
|
77
|
+
<span className="text-slate-500 text-sm">
|
|
78
|
+
{formatDate(run.created, 'MMM d, yyyy HH:mm')}
|
|
79
|
+
</span>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<DataView
|
|
86
|
+
items={runs}
|
|
87
|
+
loading={loading}
|
|
88
|
+
columns={columns}
|
|
89
|
+
initialView="table"
|
|
90
|
+
renderGrid={(run) => (
|
|
91
|
+
<Link to={`/runs/${run.run_id}`} className="block">
|
|
92
|
+
<div className="group p-5 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 hover:border-blue-500/50 hover:shadow-md transition-all duration-300">
|
|
93
|
+
<div className="flex items-start justify-between mb-3">
|
|
94
|
+
<div className="flex items-center gap-2">
|
|
95
|
+
<StatusIcon status={run.status} />
|
|
96
|
+
<span className={`text-sm font-medium capitalize ${run.status === 'completed' ? 'text-green-600 dark:text-green-400' :
|
|
97
|
+
run.status === 'failed' ? 'text-red-600 dark:text-red-400' :
|
|
98
|
+
'text-slate-600 dark:text-slate-400'
|
|
99
|
+
}`}>
|
|
100
|
+
{run.status}
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
<span className="text-xs font-mono text-slate-400 bg-slate-50 dark:bg-slate-900 px-2 py-1 rounded">
|
|
104
|
+
{run.run_id.substring(0, 8)}
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<h3 className="font-semibold text-slate-900 dark:text-white mb-1 truncate group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
|
109
|
+
{run.name || `Run ${run.run_id.substring(0, 8)}`}
|
|
110
|
+
</h3>
|
|
111
|
+
<p className="text-sm text-slate-500 dark:text-slate-400 mb-4 flex items-center gap-1">
|
|
112
|
+
<span className="opacity-75">Pipeline:</span>
|
|
113
|
+
<span className="font-medium text-slate-700 dark:text-slate-300">{run.pipeline_name}</span>
|
|
114
|
+
</p>
|
|
115
|
+
|
|
116
|
+
<div className="flex items-center justify-between text-xs text-slate-500 dark:text-slate-400 pt-3 border-t border-slate-100 dark:border-slate-700/50">
|
|
117
|
+
<div className="flex items-center gap-1.5">
|
|
118
|
+
<Clock className="w-3.5 h-3.5" />
|
|
119
|
+
<span>{formatDate(run.created, 'MMM d, HH:mm')}</span>
|
|
120
|
+
</div>
|
|
121
|
+
{run.duration && (
|
|
122
|
+
<span>{run.duration.toFixed(2)}s</span>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</Link>
|
|
127
|
+
)}
|
|
128
|
+
emptyState={
|
|
129
|
+
<div className="text-center py-8">
|
|
130
|
+
<Clock className="w-10 h-10 mx-auto text-slate-300 mb-2" />
|
|
131
|
+
<p className="text-slate-500">No runs found for this project</p>
|
|
132
|
+
</div>
|
|
133
|
+
}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Activity, Clock, Database, Box, LayoutDashboard, TrendingUp } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export function ProjectTabs({ activeTab, onTabChange }) {
|
|
5
|
+
return (
|
|
6
|
+
<div className="border-b border-slate-200 dark:border-slate-700 mb-6">
|
|
7
|
+
<div className="flex gap-6 overflow-x-auto">
|
|
8
|
+
<button
|
|
9
|
+
onClick={() => onTabChange('overview')}
|
|
10
|
+
className={`
|
|
11
|
+
flex items-center gap-2 py-4 px-1 border-b-2 transition-colors whitespace-nowrap
|
|
12
|
+
${activeTab === 'overview'
|
|
13
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400 font-medium'
|
|
14
|
+
: 'border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'}
|
|
15
|
+
`}
|
|
16
|
+
>
|
|
17
|
+
<LayoutDashboard className="w-4 h-4" />
|
|
18
|
+
Overview
|
|
19
|
+
</button>
|
|
20
|
+
<button
|
|
21
|
+
onClick={() => onTabChange('pipelines')}
|
|
22
|
+
className={`
|
|
23
|
+
flex items-center gap-2 py-4 px-1 border-b-2 transition-colors whitespace-nowrap
|
|
24
|
+
${activeTab === 'pipelines'
|
|
25
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400 font-medium'
|
|
26
|
+
: 'border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'}
|
|
27
|
+
`}
|
|
28
|
+
>
|
|
29
|
+
<Activity className="w-4 h-4" />
|
|
30
|
+
Pipelines
|
|
31
|
+
</button>
|
|
32
|
+
<button
|
|
33
|
+
onClick={() => onTabChange('runs')}
|
|
34
|
+
className={`
|
|
35
|
+
flex items-center gap-2 py-4 px-1 border-b-2 transition-colors whitespace-nowrap
|
|
36
|
+
${activeTab === 'runs'
|
|
37
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400 font-medium'
|
|
38
|
+
: 'border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'}
|
|
39
|
+
`}
|
|
40
|
+
>
|
|
41
|
+
<Clock className="w-4 h-4" />
|
|
42
|
+
Runs
|
|
43
|
+
</button>
|
|
44
|
+
<button
|
|
45
|
+
onClick={() => onTabChange('experiments')}
|
|
46
|
+
className={`
|
|
47
|
+
flex items-center gap-2 py-4 px-1 border-b-2 transition-colors whitespace-nowrap
|
|
48
|
+
${activeTab === 'experiments'
|
|
49
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400 font-medium'
|
|
50
|
+
: 'border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'}
|
|
51
|
+
`}
|
|
52
|
+
>
|
|
53
|
+
<Activity className="w-4 h-4" />
|
|
54
|
+
Experiments
|
|
55
|
+
</button>
|
|
56
|
+
<button
|
|
57
|
+
onClick={() => onTabChange('models')}
|
|
58
|
+
className={`
|
|
59
|
+
flex items-center gap-2 py-4 px-1 border-b-2 transition-colors whitespace-nowrap
|
|
60
|
+
${activeTab === 'models'
|
|
61
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400 font-medium'
|
|
62
|
+
: 'border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'}
|
|
63
|
+
`}
|
|
64
|
+
>
|
|
65
|
+
<Box className="w-4 h-4" />
|
|
66
|
+
Models
|
|
67
|
+
</button>
|
|
68
|
+
<button
|
|
69
|
+
onClick={() => onTabChange('artifacts')}
|
|
70
|
+
className={`
|
|
71
|
+
flex items-center gap-2 py-4 px-1 border-b-2 transition-colors whitespace-nowrap
|
|
72
|
+
${activeTab === 'artifacts'
|
|
73
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400 font-medium'
|
|
74
|
+
: 'border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'}
|
|
75
|
+
`}
|
|
76
|
+
>
|
|
77
|
+
<Database className="w-4 h-4" />
|
|
78
|
+
Artifacts
|
|
79
|
+
</button>
|
|
80
|
+
<button
|
|
81
|
+
onClick={() => onTabChange('metrics')}
|
|
82
|
+
className={`
|
|
83
|
+
flex items-center gap-2 py-4 px-1 border-b-2 transition-colors whitespace-nowrap
|
|
84
|
+
${activeTab === 'metrics'
|
|
85
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400 font-medium'
|
|
86
|
+
: 'border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'}
|
|
87
|
+
`}
|
|
88
|
+
>
|
|
89
|
+
<TrendingUp className="w-4 h-4" />
|
|
90
|
+
Metrics
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { useParams } from 'react-router-dom';
|
|
3
|
+
import { fetchApi } from '../../../utils/api';
|
|
4
|
+
import {
|
|
5
|
+
LayoutDashboard,
|
|
6
|
+
GitBranch,
|
|
7
|
+
PlayCircle,
|
|
8
|
+
FlaskConical,
|
|
9
|
+
BarChart2,
|
|
10
|
+
Settings,
|
|
11
|
+
Package,
|
|
12
|
+
HardDrive,
|
|
13
|
+
Activity,
|
|
14
|
+
Box,
|
|
15
|
+
Database,
|
|
16
|
+
Clock
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { AssetTreeHierarchy } from '../../../components/AssetTreeHierarchy';
|
|
19
|
+
import { AssetDetailsPanel } from '../../../components/AssetDetailsPanel';
|
|
20
|
+
import { ProjectHeader } from './_components/ProjectHeader';
|
|
21
|
+
import { ProjectMetricsPanel } from './_components/ProjectMetricsPanel';
|
|
22
|
+
import { ProjectExperimentsList } from './_components/ProjectExperimentsList';
|
|
23
|
+
import { ProjectRunsList } from './_components/ProjectRunsList';
|
|
24
|
+
import { ProjectPipelinesList } from './_components/ProjectPipelinesList';
|
|
25
|
+
import { ErrorBoundary } from '../../../components/ui/ErrorBoundary';
|
|
26
|
+
import { Card } from '../../../components/ui/Card';
|
|
27
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
28
|
+
|
|
29
|
+
export function ProjectDetails() {
|
|
30
|
+
const { projectId } = useParams();
|
|
31
|
+
const [project, setProject] = useState(null);
|
|
32
|
+
const [stats, setStats] = useState(null);
|
|
33
|
+
const [loading, setLoading] = useState(true);
|
|
34
|
+
const [activeView, setActiveView] = useState('overview');
|
|
35
|
+
const [selectedAsset, setSelectedAsset] = useState(null);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const fetchProjectDetails = async () => {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetchApi(`/api/projects/${projectId}`);
|
|
41
|
+
const projectData = await response.json();
|
|
42
|
+
|
|
43
|
+
// Ensure pipelines is an array
|
|
44
|
+
if (projectData.pipelines && !Array.isArray(projectData.pipelines)) {
|
|
45
|
+
projectData.pipelines = [];
|
|
46
|
+
}
|
|
47
|
+
setProject(projectData);
|
|
48
|
+
|
|
49
|
+
// Fetch stats
|
|
50
|
+
const [runsRes, artifactsRes, experimentsRes] = await Promise.all([
|
|
51
|
+
fetchApi(`/api/runs?project=${projectId}&limit=1000`),
|
|
52
|
+
fetchApi(`/api/assets?project=${projectId}&limit=1000`),
|
|
53
|
+
fetchApi(`/api/experiments?project=${projectId}`)
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const runsData = await runsRes.json();
|
|
57
|
+
const artifactsData = await artifactsRes.json();
|
|
58
|
+
const experimentsData = await experimentsRes.json();
|
|
59
|
+
|
|
60
|
+
const runs = Array.isArray(runsData?.runs) ? runsData.runs : [];
|
|
61
|
+
const artifacts = Array.isArray(artifactsData?.assets) ? artifactsData.assets : [];
|
|
62
|
+
const experiments = Array.isArray(experimentsData?.experiments) ? experimentsData.experiments : [];
|
|
63
|
+
|
|
64
|
+
const pipelineNames = new Set(runs.map(r => r.pipeline_name).filter(Boolean));
|
|
65
|
+
const models = artifacts.filter(a => a.type === 'Model');
|
|
66
|
+
|
|
67
|
+
setStats({
|
|
68
|
+
runs: runs.length,
|
|
69
|
+
pipelines: pipelineNames.size,
|
|
70
|
+
artifacts: artifacts.length,
|
|
71
|
+
models: models.length,
|
|
72
|
+
experiments: experiments.length,
|
|
73
|
+
total_storage_bytes: artifacts.reduce((acc, curr) => acc + (curr.size_bytes || 0), 0)
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Failed to fetch project details:', error);
|
|
78
|
+
} finally {
|
|
79
|
+
setLoading(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (projectId) {
|
|
84
|
+
fetchProjectDetails();
|
|
85
|
+
}
|
|
86
|
+
}, [projectId]);
|
|
87
|
+
|
|
88
|
+
const handleAssetSelect = (asset) => {
|
|
89
|
+
setSelectedAsset(asset);
|
|
90
|
+
// We don't change activeView, just show the asset details panel overlay or replacement
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const renderContent = () => {
|
|
94
|
+
if (selectedAsset) {
|
|
95
|
+
return (
|
|
96
|
+
<AssetDetailsPanel
|
|
97
|
+
asset={selectedAsset}
|
|
98
|
+
onClose={() => setSelectedAsset(null)}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
switch (activeView) {
|
|
104
|
+
case 'overview':
|
|
105
|
+
return (
|
|
106
|
+
<div className="space-y-6">
|
|
107
|
+
{/* Quick Stats Row */}
|
|
108
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
109
|
+
<StatCard
|
|
110
|
+
icon={Activity}
|
|
111
|
+
label="Total Runs"
|
|
112
|
+
value={stats?.runs || 0}
|
|
113
|
+
color="text-blue-500"
|
|
114
|
+
bg="bg-blue-50 dark:bg-blue-900/20"
|
|
115
|
+
/>
|
|
116
|
+
<StatCard
|
|
117
|
+
icon={Box}
|
|
118
|
+
label="Models"
|
|
119
|
+
value={stats?.models || 0}
|
|
120
|
+
color="text-purple-500"
|
|
121
|
+
bg="bg-purple-50 dark:bg-purple-900/20"
|
|
122
|
+
/>
|
|
123
|
+
<StatCard
|
|
124
|
+
icon={FlaskConical}
|
|
125
|
+
label="Experiments"
|
|
126
|
+
value={stats?.experiments || 0}
|
|
127
|
+
color="text-pink-500"
|
|
128
|
+
bg="bg-pink-50 dark:bg-pink-900/20"
|
|
129
|
+
/>
|
|
130
|
+
<StatCard
|
|
131
|
+
icon={HardDrive}
|
|
132
|
+
label="Storage"
|
|
133
|
+
value={formatBytes(stats?.total_storage_bytes || 0)}
|
|
134
|
+
color="text-slate-500"
|
|
135
|
+
bg="bg-slate-50 dark:bg-slate-800"
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
140
|
+
<Card className="p-0 overflow-hidden border-slate-200 dark:border-slate-800">
|
|
141
|
+
<div className="p-4 border-b border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/50 flex items-center justify-between">
|
|
142
|
+
<h3 className="font-semibold text-slate-900 dark:text-white flex items-center gap-2">
|
|
143
|
+
<GitBranch size={16} className="text-slate-400" />
|
|
144
|
+
Recent Pipelines
|
|
145
|
+
</h3>
|
|
146
|
+
<button
|
|
147
|
+
onClick={() => setActiveView('pipelines')}
|
|
148
|
+
className="text-xs text-primary-600 hover:text-primary-700 font-medium"
|
|
149
|
+
>
|
|
150
|
+
View All
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
<div className="p-4">
|
|
154
|
+
<ProjectPipelinesList projectId={projectId} limit={5} compact />
|
|
155
|
+
</div>
|
|
156
|
+
</Card>
|
|
157
|
+
|
|
158
|
+
<Card className="p-0 overflow-hidden border-slate-200 dark:border-slate-800">
|
|
159
|
+
<div className="p-4 border-b border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/50 flex items-center justify-between">
|
|
160
|
+
<h3 className="font-semibold text-slate-900 dark:text-white flex items-center gap-2">
|
|
161
|
+
<PlayCircle size={16} className="text-slate-400" />
|
|
162
|
+
Recent Runs
|
|
163
|
+
</h3>
|
|
164
|
+
<button
|
|
165
|
+
onClick={() => setActiveView('runs')}
|
|
166
|
+
className="text-xs text-primary-600 hover:text-primary-700 font-medium"
|
|
167
|
+
>
|
|
168
|
+
View All
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
<div className="p-4">
|
|
172
|
+
<ProjectRunsList projectId={projectId} limit={5} compact />
|
|
173
|
+
</div>
|
|
174
|
+
</Card>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<Card className="p-0 overflow-hidden border-slate-200 dark:border-slate-800">
|
|
178
|
+
<div className="p-4 border-b border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/50">
|
|
179
|
+
<h3 className="font-semibold text-slate-900 dark:text-white flex items-center gap-2">
|
|
180
|
+
<BarChart2 size={16} className="text-slate-400" />
|
|
181
|
+
Production Metrics
|
|
182
|
+
</h3>
|
|
183
|
+
</div>
|
|
184
|
+
<div className="p-4">
|
|
185
|
+
<ProjectMetricsPanel projectId={projectId} />
|
|
186
|
+
</div>
|
|
187
|
+
</Card>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
case 'pipelines':
|
|
191
|
+
return <ProjectPipelinesList projectId={projectId} />;
|
|
192
|
+
case 'runs':
|
|
193
|
+
return <ProjectRunsList projectId={projectId} />;
|
|
194
|
+
case 'experiments':
|
|
195
|
+
return <ProjectExperimentsList projectId={projectId} />;
|
|
196
|
+
case 'metrics':
|
|
197
|
+
return <ProjectMetricsPanel projectId={projectId} />;
|
|
198
|
+
default:
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div className="h-screen flex flex-col overflow-hidden bg-slate-50 dark:bg-slate-900">
|
|
205
|
+
{/* Header */}
|
|
206
|
+
<div className="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-6 py-4 shrink-0">
|
|
207
|
+
<ProjectHeader project={project} stats={stats} loading={loading} />
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{/* Main Layout */}
|
|
211
|
+
<div className="flex-1 overflow-hidden">
|
|
212
|
+
<div className="h-full max-w-[1800px] mx-auto px-6 py-6">
|
|
213
|
+
<div className="h-full flex gap-6">
|
|
214
|
+
{/* Left Sidebar */}
|
|
215
|
+
<div className="w-[380px] shrink-0 flex flex-col gap-4 overflow-y-auto pb-6">
|
|
216
|
+
{/* Navigation Menu */}
|
|
217
|
+
<nav className="space-y-1">
|
|
218
|
+
<NavButton
|
|
219
|
+
active={activeView === 'overview' && !selectedAsset}
|
|
220
|
+
onClick={() => { setActiveView('overview'); setSelectedAsset(null); }}
|
|
221
|
+
icon={LayoutDashboard}
|
|
222
|
+
label="Overview"
|
|
223
|
+
/>
|
|
224
|
+
<NavButton
|
|
225
|
+
active={activeView === 'pipelines' && !selectedAsset}
|
|
226
|
+
onClick={() => { setActiveView('pipelines'); setSelectedAsset(null); }}
|
|
227
|
+
icon={GitBranch}
|
|
228
|
+
label="Pipelines"
|
|
229
|
+
/>
|
|
230
|
+
<NavButton
|
|
231
|
+
active={activeView === 'runs' && !selectedAsset}
|
|
232
|
+
onClick={() => { setActiveView('runs'); setSelectedAsset(null); }}
|
|
233
|
+
icon={PlayCircle}
|
|
234
|
+
label="Runs"
|
|
235
|
+
/>
|
|
236
|
+
<NavButton
|
|
237
|
+
active={activeView === 'experiments' && !selectedAsset}
|
|
238
|
+
onClick={() => { setActiveView('experiments'); setSelectedAsset(null); }}
|
|
239
|
+
icon={FlaskConical}
|
|
240
|
+
label="Experiments"
|
|
241
|
+
/>
|
|
242
|
+
<NavButton
|
|
243
|
+
active={activeView === 'metrics' && !selectedAsset}
|
|
244
|
+
onClick={() => { setActiveView('metrics'); setSelectedAsset(null); }}
|
|
245
|
+
icon={BarChart2}
|
|
246
|
+
label="Metrics"
|
|
247
|
+
/>
|
|
248
|
+
</nav>
|
|
249
|
+
|
|
250
|
+
{/* Hierarchy Tree */}
|
|
251
|
+
<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 shadow-sm">
|
|
252
|
+
<div className="p-3 border-b border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50">
|
|
253
|
+
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Project Assets</h3>
|
|
254
|
+
</div>
|
|
255
|
+
<div className="flex-1 overflow-y-auto p-2">
|
|
256
|
+
<AssetTreeHierarchy
|
|
257
|
+
projectId={projectId}
|
|
258
|
+
onAssetSelect={handleAssetSelect}
|
|
259
|
+
compact={true}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Right Content Area */}
|
|
266
|
+
<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">
|
|
267
|
+
<div className="flex-1 overflow-y-auto p-6">
|
|
268
|
+
<ErrorBoundary>
|
|
269
|
+
<AnimatePresence mode="wait">
|
|
270
|
+
<motion.div
|
|
271
|
+
key={selectedAsset ? 'asset' : activeView}
|
|
272
|
+
initial={{ opacity: 0, y: 10 }}
|
|
273
|
+
animate={{ opacity: 1, y: 0 }}
|
|
274
|
+
exit={{ opacity: 0, y: -10 }}
|
|
275
|
+
transition={{ duration: 0.2 }}
|
|
276
|
+
className="h-full"
|
|
277
|
+
>
|
|
278
|
+
{renderContent()}
|
|
279
|
+
</motion.div>
|
|
280
|
+
</AnimatePresence>
|
|
281
|
+
</ErrorBoundary>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function NavButton({ active, onClick, icon: Icon, label }) {
|
|
292
|
+
return (
|
|
293
|
+
<button
|
|
294
|
+
onClick={onClick}
|
|
295
|
+
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${active
|
|
296
|
+
? 'bg-primary-50 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400 shadow-sm'
|
|
297
|
+
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-900 dark:hover:text-white'
|
|
298
|
+
}`}
|
|
299
|
+
>
|
|
300
|
+
<Icon size={18} className={active ? 'text-primary-600 dark:text-primary-400' : 'text-slate-400'} />
|
|
301
|
+
{label}
|
|
302
|
+
</button>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function StatCard({ icon: Icon, label, value, color, bg }) {
|
|
307
|
+
return (
|
|
308
|
+
<div className="bg-white dark:bg-slate-800 rounded-xl p-4 border border-slate-200 dark:border-slate-700 shadow-sm flex items-center gap-4">
|
|
309
|
+
<div className={`p-3 rounded-lg ${bg}`}>
|
|
310
|
+
<Icon size={20} className={color} />
|
|
311
|
+
</div>
|
|
312
|
+
<div>
|
|
313
|
+
<p className="text-sm text-slate-500 dark:text-slate-400">{label}</p>
|
|
314
|
+
<p className="text-xl font-bold text-slate-900 dark:text-white">{typeof value === 'number' ? value.toLocaleString() : value}</p>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function formatBytes(bytes) {
|
|
321
|
+
if (!bytes || bytes === 0) return '0 B';
|
|
322
|
+
const k = 1024;
|
|
323
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
324
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
325
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
326
|
+
}
|
|
@@ -135,7 +135,11 @@ export function Projects() {
|
|
|
135
135
|
header: 'Stats',
|
|
136
136
|
key: 'stats',
|
|
137
137
|
render: (project) => {
|
|
138
|
-
const stats =
|
|
138
|
+
const stats = {
|
|
139
|
+
runs: project.runs || 0,
|
|
140
|
+
pipelines: project.pipelines || 0,
|
|
141
|
+
artifacts: project.artifacts || 0
|
|
142
|
+
};
|
|
139
143
|
return (
|
|
140
144
|
<div className="flex gap-4 text-sm text-slate-500">
|
|
141
145
|
<span className="flex items-center gap-1"><Activity size={14} /> {stats.pipelines || 0}</span>
|
|
@@ -160,11 +164,16 @@ export function Projects() {
|
|
|
160
164
|
];
|
|
161
165
|
|
|
162
166
|
const renderGrid = (project) => {
|
|
163
|
-
|
|
167
|
+
// Stats are already attached to the project object
|
|
168
|
+
const stats = {
|
|
169
|
+
runs: project.runs || 0,
|
|
170
|
+
pipelines: project.pipelines || 0,
|
|
171
|
+
artifacts: project.artifacts || 0
|
|
172
|
+
};
|
|
164
173
|
|
|
165
174
|
return (
|
|
166
175
|
<Link
|
|
167
|
-
to={`/
|
|
176
|
+
to={`/projects/${encodeURIComponent(project.name)}`}
|
|
168
177
|
onClick={() => setSelectedProject(project.name)}
|
|
169
178
|
className="block"
|
|
170
179
|
>
|
|
@@ -226,6 +235,7 @@ export function Projects() {
|
|
|
226
235
|
items={projects}
|
|
227
236
|
loading={loading}
|
|
228
237
|
columns={columns}
|
|
238
|
+
initialView="grid"
|
|
229
239
|
renderGrid={renderGrid}
|
|
230
240
|
actions={
|
|
231
241
|
<Button onClick={() => setShowCreateModal(true)} className="flex items-center gap-2">
|