flowyml 1.1.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. flowyml/__init__.py +3 -0
  2. flowyml/assets/base.py +10 -0
  3. flowyml/assets/metrics.py +6 -0
  4. flowyml/cli/main.py +108 -2
  5. flowyml/cli/run.py +9 -2
  6. flowyml/core/execution_status.py +52 -0
  7. flowyml/core/hooks.py +106 -0
  8. flowyml/core/observability.py +210 -0
  9. flowyml/core/orchestrator.py +274 -0
  10. flowyml/core/pipeline.py +193 -231
  11. flowyml/core/project.py +34 -2
  12. flowyml/core/remote_orchestrator.py +109 -0
  13. flowyml/core/resources.py +22 -5
  14. flowyml/core/retry_policy.py +80 -0
  15. flowyml/core/step.py +18 -1
  16. flowyml/core/submission_result.py +53 -0
  17. flowyml/core/versioning.py +2 -2
  18. flowyml/integrations/keras.py +95 -22
  19. flowyml/monitoring/alerts.py +2 -2
  20. flowyml/stacks/__init__.py +15 -0
  21. flowyml/stacks/aws.py +599 -0
  22. flowyml/stacks/azure.py +295 -0
  23. flowyml/stacks/components.py +24 -2
  24. flowyml/stacks/gcp.py +158 -11
  25. flowyml/stacks/local.py +5 -0
  26. flowyml/storage/artifacts.py +15 -5
  27. flowyml/storage/materializers/__init__.py +2 -0
  28. flowyml/storage/materializers/cloudpickle.py +74 -0
  29. flowyml/storage/metadata.py +166 -5
  30. flowyml/ui/backend/main.py +41 -1
  31. flowyml/ui/backend/routers/assets.py +356 -15
  32. flowyml/ui/backend/routers/client.py +46 -0
  33. flowyml/ui/backend/routers/execution.py +13 -2
  34. flowyml/ui/backend/routers/experiments.py +48 -12
  35. flowyml/ui/backend/routers/metrics.py +213 -0
  36. flowyml/ui/backend/routers/pipelines.py +63 -7
  37. flowyml/ui/backend/routers/projects.py +33 -7
  38. flowyml/ui/backend/routers/runs.py +150 -8
  39. flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
  40. flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
  41. flowyml/ui/frontend/dist/index.html +2 -2
  42. flowyml/ui/frontend/src/App.jsx +4 -1
  43. flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
  44. flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
  45. flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
  46. flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
  47. flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
  48. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
  49. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
  50. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
  51. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
  52. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
  53. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
  54. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
  55. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
  56. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
  57. flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
  58. flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
  59. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
  60. flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
  61. flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
  62. flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
  63. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
  64. flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
  65. flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
  66. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
  67. flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
  68. flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
  69. flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
  70. flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
  71. flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
  72. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
  73. flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
  74. flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
  75. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
  76. flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
  77. flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
  78. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
  79. flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
  80. flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
  81. flowyml/ui/frontend/src/router/index.jsx +4 -0
  82. flowyml/ui/frontend/src/utils/date.js +10 -0
  83. flowyml/ui/frontend/src/utils/downloads.js +11 -0
  84. flowyml/utils/config.py +6 -0
  85. flowyml/utils/stack_config.py +45 -3
  86. {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/METADATA +113 -12
  87. {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/RECORD +90 -53
  88. {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/licenses/LICENSE +1 -1
  89. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
  90. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
  91. {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/WHEEL +0 -0
  92. {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,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 = projectStats[project.name] || { runs: 0, pipelines: 0, artifacts: 0 };
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
- const stats = projectStats[project.name] || { runs: 0, pipelines: 0, artifacts: 0 };
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={`/runs?project=${encodeURIComponent(project.name)}`}
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">