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,122 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { CheckCircle, XCircle, Clock, Loader, AlertCircle, PlayCircle } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
const statusConfig = {
|
|
5
|
+
completed: {
|
|
6
|
+
icon: CheckCircle,
|
|
7
|
+
label: 'Completed',
|
|
8
|
+
color: 'text-emerald-600 dark:text-emerald-400',
|
|
9
|
+
bg: 'bg-emerald-50 dark:bg-emerald-900/20',
|
|
10
|
+
border: 'border-emerald-200 dark:border-emerald-800'
|
|
11
|
+
},
|
|
12
|
+
success: {
|
|
13
|
+
icon: CheckCircle,
|
|
14
|
+
label: 'Success',
|
|
15
|
+
color: 'text-emerald-600 dark:text-emerald-400',
|
|
16
|
+
bg: 'bg-emerald-50 dark:bg-emerald-900/20',
|
|
17
|
+
border: 'border-emerald-200 dark:border-emerald-800'
|
|
18
|
+
},
|
|
19
|
+
failed: {
|
|
20
|
+
icon: XCircle,
|
|
21
|
+
label: 'Failed',
|
|
22
|
+
color: 'text-rose-600 dark:text-rose-400',
|
|
23
|
+
bg: 'bg-rose-50 dark:bg-rose-900/20',
|
|
24
|
+
border: 'border-rose-200 dark:border-rose-800'
|
|
25
|
+
},
|
|
26
|
+
running: {
|
|
27
|
+
icon: Loader,
|
|
28
|
+
label: 'Running',
|
|
29
|
+
color: 'text-blue-600 dark:text-blue-400',
|
|
30
|
+
bg: 'bg-blue-50 dark:bg-blue-900/20',
|
|
31
|
+
border: 'border-blue-200 dark:border-blue-800',
|
|
32
|
+
animate: true
|
|
33
|
+
},
|
|
34
|
+
pending: {
|
|
35
|
+
icon: Clock,
|
|
36
|
+
label: 'Pending',
|
|
37
|
+
color: 'text-amber-600 dark:text-amber-400',
|
|
38
|
+
bg: 'bg-amber-50 dark:bg-amber-900/20',
|
|
39
|
+
border: 'border-amber-200 dark:border-amber-800'
|
|
40
|
+
},
|
|
41
|
+
queued: {
|
|
42
|
+
icon: Clock,
|
|
43
|
+
label: 'Queued',
|
|
44
|
+
color: 'text-slate-600 dark:text-slate-400',
|
|
45
|
+
bg: 'bg-slate-50 dark:bg-slate-900/20',
|
|
46
|
+
border: 'border-slate-200 dark:border-slate-700'
|
|
47
|
+
},
|
|
48
|
+
initializing: {
|
|
49
|
+
icon: PlayCircle,
|
|
50
|
+
label: 'Initializing',
|
|
51
|
+
color: 'text-blue-600 dark:text-blue-400',
|
|
52
|
+
bg: 'bg-blue-50 dark:bg-blue-900/20',
|
|
53
|
+
border: 'border-blue-200 dark:border-blue-800'
|
|
54
|
+
},
|
|
55
|
+
cached: {
|
|
56
|
+
icon: CheckCircle,
|
|
57
|
+
label: 'Cached',
|
|
58
|
+
color: 'text-cyan-600 dark:text-cyan-400',
|
|
59
|
+
bg: 'bg-cyan-50 dark:bg-cyan-900/20',
|
|
60
|
+
border: 'border-cyan-200 dark:border-cyan-800'
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function ExecutionStatus({
|
|
65
|
+
status,
|
|
66
|
+
size = 'md',
|
|
67
|
+
showLabel = true,
|
|
68
|
+
className = '',
|
|
69
|
+
iconOnly = false
|
|
70
|
+
}) {
|
|
71
|
+
const config = statusConfig[status?.toLowerCase()] || statusConfig.pending;
|
|
72
|
+
const Icon = config.icon;
|
|
73
|
+
|
|
74
|
+
const sizeClasses = {
|
|
75
|
+
sm: { icon: 14, text: 'text-xs', padding: 'px-2 py-0.5' },
|
|
76
|
+
md: { icon: 16, text: 'text-sm', padding: 'px-2.5 py-1' },
|
|
77
|
+
lg: { icon: 20, text: 'text-base', padding: 'px-3 py-1.5' }
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const { icon: iconSize, text: textSize, padding } = sizeClasses[size];
|
|
81
|
+
|
|
82
|
+
if (iconOnly) {
|
|
83
|
+
return (
|
|
84
|
+
<Icon
|
|
85
|
+
size={iconSize}
|
|
86
|
+
className={`${config.color} ${config.animate ? 'animate-spin' : ''} ${className}`}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className={`inline-flex items-center gap-1.5 ${padding} rounded-full border ${config.bg} ${config.border} ${className}`}>
|
|
93
|
+
<Icon
|
|
94
|
+
size={iconSize}
|
|
95
|
+
className={`${config.color} ${config.animate ? 'animate-spin' : ''}`}
|
|
96
|
+
/>
|
|
97
|
+
{showLabel && (
|
|
98
|
+
<span className={`font-medium ${config.color} ${textSize}`}>
|
|
99
|
+
{config.label}
|
|
100
|
+
</span>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Badge variant for minimal display
|
|
107
|
+
export function StatusBadge({ status, className = '' }) {
|
|
108
|
+
return <ExecutionStatus status={status} size="sm" className={className} />;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Icon-only variant
|
|
112
|
+
export function StatusIcon({ status, size = 16, className = '' }) {
|
|
113
|
+
const config = statusConfig[status?.toLowerCase()] || statusConfig.pending;
|
|
114
|
+
const Icon = config.icon;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Icon
|
|
118
|
+
size={size}
|
|
119
|
+
className={`${config.color} ${config.animate ? 'animate-spin' : ''} ${className}`}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export function KeyValue({ label, value, icon, className = '', valueClassName = '' }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className={`flex items-start justify-between p-3 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-slate-200 dark:border-slate-700 ${className}`}>
|
|
6
|
+
<div className="flex items-center gap-2">
|
|
7
|
+
{icon && <span className="text-slate-400">{icon}</span>}
|
|
8
|
+
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">{label}</span>
|
|
9
|
+
</div>
|
|
10
|
+
<span className={`text-sm font-mono font-semibold text-slate-900 dark:text-white text-right ${valueClassName}`}>
|
|
11
|
+
{value}
|
|
12
|
+
</span>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function KeyValueGrid({ items, columns = 2, className = '' }) {
|
|
18
|
+
return (
|
|
19
|
+
<div className={`grid grid-cols-1 md:grid-cols-${columns} gap-3 ${className}`}>
|
|
20
|
+
{items.map((item, idx) => (
|
|
21
|
+
<KeyValue key={idx} {...item} />
|
|
22
|
+
))}
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChevronDown, FolderOpen, Globe } from 'lucide-react';
|
|
3
|
+
import { useProject } from '../../contexts/ProjectContext';
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
+
|
|
6
|
+
export function ProjectSelector() {
|
|
7
|
+
const { selectedProject, projects, loading, selectProject, clearProject } = useProject();
|
|
8
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
9
|
+
|
|
10
|
+
const currentProject = projects.find(p => p.name === selectedProject);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="relative">
|
|
14
|
+
<button
|
|
15
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
16
|
+
className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors min-w-[200px]"
|
|
17
|
+
>
|
|
18
|
+
{selectedProject ? (
|
|
19
|
+
<>
|
|
20
|
+
<FolderOpen size={18} className="text-primary-600 dark:text-primary-400" />
|
|
21
|
+
<div className="flex-1 text-left">
|
|
22
|
+
<div className="text-sm font-medium text-slate-900 dark:text-white">
|
|
23
|
+
{currentProject?.name || selectedProject}
|
|
24
|
+
</div>
|
|
25
|
+
{currentProject?.description && (
|
|
26
|
+
<div className="text-xs text-slate-500 dark:text-slate-400 truncate max-w-[150px]">
|
|
27
|
+
{currentProject.description}
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
</>
|
|
32
|
+
) : (
|
|
33
|
+
<>
|
|
34
|
+
<Globe size={18} className="text-slate-400" />
|
|
35
|
+
<span className="flex-1 text-left text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
36
|
+
All Projects
|
|
37
|
+
</span>
|
|
38
|
+
</>
|
|
39
|
+
)}
|
|
40
|
+
<ChevronDown
|
|
41
|
+
size={16}
|
|
42
|
+
className={`text-slate-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
|
43
|
+
/>
|
|
44
|
+
</button>
|
|
45
|
+
|
|
46
|
+
<AnimatePresence>
|
|
47
|
+
{isOpen && (
|
|
48
|
+
<>
|
|
49
|
+
{/* Backdrop */}
|
|
50
|
+
<div
|
|
51
|
+
className="fixed inset-0 z-10"
|
|
52
|
+
onClick={() => setIsOpen(false)}
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
{/* Dropdown */}
|
|
56
|
+
<motion.div
|
|
57
|
+
initial={{ opacity: 0, y: -10 }}
|
|
58
|
+
animate={{ opacity: 1, y: 0 }}
|
|
59
|
+
exit={{ opacity: 0, y: -10 }}
|
|
60
|
+
transition={{ duration: 0.15 }}
|
|
61
|
+
className="absolute top-full mt-2 left-0 w-full min-w-[280px] bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-xl z-20 max-h-[400px] overflow-y-auto"
|
|
62
|
+
>
|
|
63
|
+
{/* All Projects Option */}
|
|
64
|
+
<button
|
|
65
|
+
onClick={() => {
|
|
66
|
+
clearProject();
|
|
67
|
+
setIsOpen(false);
|
|
68
|
+
}}
|
|
69
|
+
className={`w-full flex items-center gap-3 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors ${!selectedProject ? 'bg-primary-50 dark:bg-primary-900/20' : ''
|
|
70
|
+
}`}
|
|
71
|
+
>
|
|
72
|
+
<Globe size={18} className="text-slate-400" />
|
|
73
|
+
<div className="flex-1 text-left">
|
|
74
|
+
<div className="text-sm font-medium text-slate-900 dark:text-white">
|
|
75
|
+
All Projects
|
|
76
|
+
</div>
|
|
77
|
+
<div className="text-xs text-slate-500 dark:text-slate-400">
|
|
78
|
+
View data from all projects
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
{!selectedProject && (
|
|
82
|
+
<div className="w-2 h-2 rounded-full bg-primary-600" />
|
|
83
|
+
)}
|
|
84
|
+
</button>
|
|
85
|
+
|
|
86
|
+
<div className="border-t border-slate-200 dark:border-slate-700" />
|
|
87
|
+
|
|
88
|
+
{/* Project List */}
|
|
89
|
+
{loading ? (
|
|
90
|
+
<div className="px-4 py-8 text-center text-slate-500 dark:text-slate-400 text-sm">
|
|
91
|
+
Loading projects...
|
|
92
|
+
</div>
|
|
93
|
+
) : projects.length === 0 ? (
|
|
94
|
+
<div className="px-4 py-8 text-center text-slate-500 dark:text-slate-400 text-sm">
|
|
95
|
+
No projects yet
|
|
96
|
+
</div>
|
|
97
|
+
) : (
|
|
98
|
+
projects.map(project => (
|
|
99
|
+
<button
|
|
100
|
+
key={project.name}
|
|
101
|
+
onClick={() => {
|
|
102
|
+
selectProject(project.name);
|
|
103
|
+
setIsOpen(false);
|
|
104
|
+
}}
|
|
105
|
+
className={`w-full flex items-center gap-3 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors ${selectedProject === project.name ? 'bg-primary-50 dark:bg-primary-900/20' : ''
|
|
106
|
+
}`}
|
|
107
|
+
>
|
|
108
|
+
<FolderOpen
|
|
109
|
+
size={18}
|
|
110
|
+
className={selectedProject === project.name ? 'text-primary-600 dark:text-primary-400' : 'text-slate-400'}
|
|
111
|
+
/>
|
|
112
|
+
<div className="flex-1 text-left">
|
|
113
|
+
<div className="text-sm font-medium text-slate-900 dark:text-white">
|
|
114
|
+
{project.name}
|
|
115
|
+
</div>
|
|
116
|
+
{project.description && (
|
|
117
|
+
<div className="text-xs text-slate-500 dark:text-slate-400">
|
|
118
|
+
{project.description}
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
{selectedProject === project.name && (
|
|
123
|
+
<div className="w-2 h-2 rounded-full bg-primary-600" />
|
|
124
|
+
)}
|
|
125
|
+
</button>
|
|
126
|
+
))
|
|
127
|
+
)}
|
|
128
|
+
</motion.div>
|
|
129
|
+
</>
|
|
130
|
+
)}
|
|
131
|
+
</AnimatePresence>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
const ProjectContext = createContext();
|
|
4
|
+
|
|
5
|
+
export function ProjectProvider({ children }) {
|
|
6
|
+
const [selectedProject, setSelectedProject] = useState(() => {
|
|
7
|
+
// Check localStorage first
|
|
8
|
+
const saved = localStorage.getItem('flowyml-selected-project');
|
|
9
|
+
return saved || null; // null means "All Projects"
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const [projects, setProjects] = useState([]);
|
|
13
|
+
const [loading, setLoading] = useState(true);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
// Fetch available projects
|
|
17
|
+
fetch('/api/projects/')
|
|
18
|
+
.then(res => res.json())
|
|
19
|
+
.then(data => {
|
|
20
|
+
setProjects(data || []);
|
|
21
|
+
setLoading(false);
|
|
22
|
+
})
|
|
23
|
+
.catch(err => {
|
|
24
|
+
console.error('Failed to fetch projects:', err);
|
|
25
|
+
setLoading(false);
|
|
26
|
+
});
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
// Save to localStorage
|
|
31
|
+
if (selectedProject) {
|
|
32
|
+
localStorage.setItem('flowyml-selected-project', selectedProject);
|
|
33
|
+
} else {
|
|
34
|
+
localStorage.removeItem('flowyml-selected-project');
|
|
35
|
+
}
|
|
36
|
+
}, [selectedProject]);
|
|
37
|
+
|
|
38
|
+
const selectProject = (projectName) => {
|
|
39
|
+
setSelectedProject(projectName);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const clearProject = () => {
|
|
43
|
+
setSelectedProject(null);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const refreshProjects = async () => {
|
|
47
|
+
setLoading(true);
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch('/api/projects/');
|
|
50
|
+
const data = await res.json();
|
|
51
|
+
setProjects(data || []);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error('Failed to refresh projects:', err);
|
|
54
|
+
} finally {
|
|
55
|
+
setLoading(false);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<ProjectContext.Provider value={{
|
|
61
|
+
selectedProject,
|
|
62
|
+
projects,
|
|
63
|
+
loading,
|
|
64
|
+
selectProject,
|
|
65
|
+
clearProject,
|
|
66
|
+
refreshProjects
|
|
67
|
+
}}>
|
|
68
|
+
{children}
|
|
69
|
+
</ProjectContext.Provider>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function useProject() {
|
|
74
|
+
const context = useContext(ProjectContext);
|
|
75
|
+
if (!context) {
|
|
76
|
+
throw new Error('useProject must be used within ProjectProvider');
|
|
77
|
+
}
|
|
78
|
+
return context;
|
|
79
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
const ThemeContext = createContext();
|
|
4
|
+
|
|
5
|
+
export function ThemeProvider({ children }) {
|
|
6
|
+
const [theme, setTheme] = useState(() => {
|
|
7
|
+
// Check localStorage first
|
|
8
|
+
const savedTheme = localStorage.getItem('flowyml-theme');
|
|
9
|
+
if (savedTheme) {
|
|
10
|
+
// Apply immediately before React renders
|
|
11
|
+
document.documentElement.classList.remove('light', 'dark');
|
|
12
|
+
document.documentElement.classList.add(savedTheme);
|
|
13
|
+
return savedTheme;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check system preference
|
|
17
|
+
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
18
|
+
document.documentElement.classList.remove('light');
|
|
19
|
+
document.documentElement.classList.add('dark');
|
|
20
|
+
return 'dark';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
document.documentElement.classList.remove('dark');
|
|
24
|
+
document.documentElement.classList.add('light');
|
|
25
|
+
return 'light';
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
// Apply theme to document
|
|
30
|
+
document.documentElement.classList.remove('light', 'dark');
|
|
31
|
+
document.documentElement.classList.add(theme);
|
|
32
|
+
|
|
33
|
+
// Save to localStorage
|
|
34
|
+
localStorage.setItem('flowyml-theme', theme);
|
|
35
|
+
}, [theme]);
|
|
36
|
+
|
|
37
|
+
const toggleTheme = () => {
|
|
38
|
+
setTheme(prev => prev === 'light' ? 'dark' : 'light');
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
|
43
|
+
{children}
|
|
44
|
+
</ThemeContext.Provider>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function useTheme() {
|
|
49
|
+
const context = useContext(ThemeContext);
|
|
50
|
+
if (!context) {
|
|
51
|
+
throw new Error('useTheme must be used within ThemeProvider');
|
|
52
|
+
}
|
|
53
|
+
return context;
|
|
54
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Outlet } from 'react-router-dom';
|
|
3
|
+
import { Sidebar } from '../components/sidebar/Sidebar';
|
|
4
|
+
import { Header } from '../components/header/Header';
|
|
5
|
+
|
|
6
|
+
export function MainLayout() {
|
|
7
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex h-screen bg-slate-50 dark:bg-slate-900 overflow-hidden">
|
|
11
|
+
<Sidebar collapsed={collapsed} setCollapsed={setCollapsed} />
|
|
12
|
+
|
|
13
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
14
|
+
<Header />
|
|
15
|
+
<main className="flex-1 overflow-y-auto p-6 scrollbar-thin scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700">
|
|
16
|
+
<div className="max-w-7xl mx-auto w-full">
|
|
17
|
+
<Outlet />
|
|
18
|
+
</div>
|
|
19
|
+
</main>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createBrowserRouter } from 'react-router-dom';
|
|
2
|
+
import { MainLayout } from '../layouts/MainLayout';
|
|
3
|
+
import { Dashboard } from '../app/dashboard/page';
|
|
4
|
+
import { Pipelines } from '../app/pipelines/page';
|
|
5
|
+
import { Runs } from '../app/runs/page';
|
|
6
|
+
import { RunDetails } from '../app/runs/[runId]/page';
|
|
7
|
+
import { Assets } from '../app/assets/page';
|
|
8
|
+
import { Experiments } from '../app/experiments/page';
|
|
9
|
+
import { ExperimentDetails } from '../app/experiments/[experimentId]/page';
|
|
10
|
+
import { Traces } from '../app/traces/page';
|
|
11
|
+
import { Projects } from '../app/projects/page';
|
|
12
|
+
import { Schedules } from '../app/schedules/page';
|
|
13
|
+
import { Leaderboard } from '../app/leaderboard/page';
|
|
14
|
+
import { Plugins } from '../app/plugins/page';
|
|
15
|
+
import { Settings } from '../app/settings/page';
|
|
16
|
+
import { TokenManagement } from '../app/tokens/page';
|
|
17
|
+
|
|
18
|
+
export const router = createBrowserRouter([
|
|
19
|
+
{
|
|
20
|
+
path: '/',
|
|
21
|
+
element: <MainLayout />,
|
|
22
|
+
children: [
|
|
23
|
+
{ index: true, element: <Dashboard /> },
|
|
24
|
+
{ path: 'pipelines', element: <Pipelines /> },
|
|
25
|
+
{ path: 'runs', element: <Runs /> },
|
|
26
|
+
{ path: 'runs/:runId', element: <RunDetails /> },
|
|
27
|
+
{ path: 'assets', element: <Assets /> },
|
|
28
|
+
{ path: 'experiments', element: <Experiments /> },
|
|
29
|
+
{ path: 'experiments/:experimentId', element: <ExperimentDetails /> },
|
|
30
|
+
{ path: 'traces', element: <Traces /> },
|
|
31
|
+
{ path: 'projects', element: <Projects /> },
|
|
32
|
+
{ path: 'schedules', element: <Schedules /> },
|
|
33
|
+
{ path: 'leaderboard', element: <Leaderboard /> },
|
|
34
|
+
{ path: 'plugins', element: <Plugins /> },
|
|
35
|
+
{ path: 'settings', element: <Settings /> },
|
|
36
|
+
{ path: 'tokens', element: <TokenManagement /> },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service for managing plugins and integrations.
|
|
3
|
+
* Communicates with the backend API for plugin operations.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const API_BASE_URL = '/api';
|
|
7
|
+
|
|
8
|
+
class PluginService {
|
|
9
|
+
async getAvailablePlugins() {
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetch(`${API_BASE_URL}/plugins/available`);
|
|
12
|
+
if (!response.ok) throw new Error('Failed to fetch available plugins');
|
|
13
|
+
return await response.json();
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error('Error fetching available plugins:', error);
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async getInstalledPlugins() {
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(`${API_BASE_URL}/plugins/installed`);
|
|
23
|
+
if (!response.ok) throw new Error('Failed to fetch installed plugins');
|
|
24
|
+
return await response.json();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error('Error fetching installed plugins:', error);
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async installPlugin(pluginId) {
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(`${API_BASE_URL}/plugins/install`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({ plugin_id: pluginId })
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const error = await response.json();
|
|
41
|
+
throw new Error(error.detail || 'Installation failed');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return await response.json();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Error installing plugin:', error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async uninstallPlugin(pluginId) {
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch(`${API_BASE_URL}/plugins/uninstall/${pluginId}`, {
|
|
54
|
+
method: 'POST'
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const error = await response.json();
|
|
59
|
+
throw new Error(error.detail || 'Uninstall failed');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return await response.json();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Error uninstalling plugin:', error);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async importZenMLStack(stackName) {
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(`${API_BASE_URL}/plugins/import-stack`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({ stack_name: stackName })
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const error = await response.json();
|
|
79
|
+
throw new Error(error.detail || 'Import failed');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return await response.json();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Error importing stack:', error);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const pluginService = new PluginService();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// Cache for config
|
|
4
|
+
let configCache = null;
|
|
5
|
+
|
|
6
|
+
export const getConfig = async () => {
|
|
7
|
+
if (configCache) return configCache;
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch('/api/config');
|
|
10
|
+
configCache = await res.json();
|
|
11
|
+
return configCache;
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error('Failed to fetch config:', err);
|
|
14
|
+
return { execution_mode: 'local' };
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getBaseUrl = async () => {
|
|
19
|
+
const config = await getConfig();
|
|
20
|
+
if (config.execution_mode === 'remote' && config.remote_server_url) {
|
|
21
|
+
return config.remote_server_url;
|
|
22
|
+
}
|
|
23
|
+
return '';
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const fetchApi = async (endpoint, options = {}) => {
|
|
27
|
+
const baseUrl = await getBaseUrl();
|
|
28
|
+
// Ensure endpoint starts with /
|
|
29
|
+
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
30
|
+
const url = `${baseUrl}${path}`;
|
|
31
|
+
|
|
32
|
+
return fetch(url, options);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const useConfig = () => {
|
|
36
|
+
const [config, setConfig] = useState(null);
|
|
37
|
+
const [loading, setLoading] = useState(true);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
getConfig().then(cfg => {
|
|
41
|
+
setConfig(cfg);
|
|
42
|
+
setLoading(false);
|
|
43
|
+
});
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
return { config, loading };
|
|
47
|
+
};
|