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,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useLocation, Link } from 'react-router-dom';
|
|
3
|
+
import { Sun, Moon, ChevronRight, Home } from 'lucide-react';
|
|
4
|
+
import { useTheme } from '../../contexts/ThemeContext';
|
|
5
|
+
import { ProjectSelector } from '../ui/ProjectSelector';
|
|
6
|
+
|
|
7
|
+
export function Header() {
|
|
8
|
+
const { theme, toggleTheme } = useTheme();
|
|
9
|
+
const location = useLocation();
|
|
10
|
+
|
|
11
|
+
// Generate breadcrumbs from path
|
|
12
|
+
const pathnames = location.pathname.split('/').filter((x) => x);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<header className="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-6 py-4 flex items-center justify-between shadow-sm z-10">
|
|
16
|
+
<div className="flex items-center gap-4 flex-1">
|
|
17
|
+
{/* Breadcrumbs */}
|
|
18
|
+
<nav className="flex items-center text-sm text-slate-500 dark:text-slate-400">
|
|
19
|
+
<Link to="/" className="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
|
|
20
|
+
<Home size={16} />
|
|
21
|
+
</Link>
|
|
22
|
+
{pathnames.length > 0 && (
|
|
23
|
+
<ChevronRight size={14} className="mx-2 text-slate-300 dark:text-slate-600" />
|
|
24
|
+
)}
|
|
25
|
+
{pathnames.map((name, index) => {
|
|
26
|
+
const routeTo = `/${pathnames.slice(0, index + 1).join('/')}`;
|
|
27
|
+
const isLast = index === pathnames.length - 1;
|
|
28
|
+
const formattedName = name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, ' ');
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<React.Fragment key={name}>
|
|
32
|
+
{isLast ? (
|
|
33
|
+
<span className="font-medium text-slate-900 dark:text-white">
|
|
34
|
+
{formattedName}
|
|
35
|
+
</span>
|
|
36
|
+
) : (
|
|
37
|
+
<Link
|
|
38
|
+
to={routeTo}
|
|
39
|
+
className="hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
|
|
40
|
+
>
|
|
41
|
+
{formattedName}
|
|
42
|
+
</Link>
|
|
43
|
+
)}
|
|
44
|
+
{!isLast && (
|
|
45
|
+
<ChevronRight size={14} className="mx-2 text-slate-300 dark:text-slate-600" />
|
|
46
|
+
)}
|
|
47
|
+
</React.Fragment>
|
|
48
|
+
);
|
|
49
|
+
})}
|
|
50
|
+
</nav>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className="flex items-center gap-4">
|
|
54
|
+
<ProjectSelector />
|
|
55
|
+
|
|
56
|
+
<div className="h-6 w-px bg-slate-200 dark:bg-slate-700 mx-2" />
|
|
57
|
+
|
|
58
|
+
<button
|
|
59
|
+
onClick={toggleTheme}
|
|
60
|
+
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors text-slate-500 hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
|
|
61
|
+
aria-label="Toggle theme"
|
|
62
|
+
>
|
|
63
|
+
{theme === 'dark' ? (
|
|
64
|
+
<Sun size={20} />
|
|
65
|
+
) : (
|
|
66
|
+
<Moon size={20} />
|
|
67
|
+
)}
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
</header>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Plus, X, Package, Link as LinkIcon } from 'lucide-react';
|
|
3
|
+
import { Button } from '../ui/Button';
|
|
4
|
+
|
|
5
|
+
export function AddPluginDialog({ isOpen, onClose, onAdd }) {
|
|
6
|
+
const [method, setMethod] = useState('package'); // 'package' or 'url'
|
|
7
|
+
const [packageName, setPackageName] = useState('');
|
|
8
|
+
const [url, setUrl] = useState('');
|
|
9
|
+
|
|
10
|
+
const handleAdd = () => {
|
|
11
|
+
if (method === 'package' && packageName) {
|
|
12
|
+
onAdd({ type: 'package', value: packageName });
|
|
13
|
+
setPackageName('');
|
|
14
|
+
onClose();
|
|
15
|
+
} else if (method === 'url' && url) {
|
|
16
|
+
onAdd({ type: 'url', value: url });
|
|
17
|
+
setUrl('');
|
|
18
|
+
onClose();
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (!isOpen) return null;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={onClose}>
|
|
26
|
+
<div
|
|
27
|
+
className="bg-white dark:bg-slate-800 rounded-xl p-6 max-w-md w-full mx-4 shadow-2xl"
|
|
28
|
+
onClick={(e) => e.stopPropagation()}
|
|
29
|
+
>
|
|
30
|
+
<div className="flex items-center justify-between mb-4">
|
|
31
|
+
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">Add Plugin</h3>
|
|
32
|
+
<button
|
|
33
|
+
onClick={onClose}
|
|
34
|
+
className="p-1 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
|
|
35
|
+
>
|
|
36
|
+
<X size={20} className="text-slate-500" />
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{/* Method Selector */}
|
|
41
|
+
<div className="flex gap-2 mb-4 bg-slate-100 dark:bg-slate-900 p-1 rounded-lg">
|
|
42
|
+
<button
|
|
43
|
+
onClick={() => setMethod('package')}
|
|
44
|
+
className={`flex-1 px-4 py-2 rounded-md text-sm font-medium transition-all ${method === 'package'
|
|
45
|
+
? 'bg-white dark:bg-slate-700 text-slate-900 dark:text-white shadow-sm'
|
|
46
|
+
: 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'
|
|
47
|
+
}`}
|
|
48
|
+
>
|
|
49
|
+
<div className="flex items-center justify-center gap-2">
|
|
50
|
+
<Package size={16} />
|
|
51
|
+
Package Name
|
|
52
|
+
</div>
|
|
53
|
+
</button>
|
|
54
|
+
<button
|
|
55
|
+
onClick={() => setMethod('url')}
|
|
56
|
+
className={`flex-1 px-4 py-2 rounded-md text-sm font-medium transition-all ${method === 'url'
|
|
57
|
+
? 'bg-white dark:bg-slate-700 text-slate-900 dark:text-white shadow-sm'
|
|
58
|
+
: 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'
|
|
59
|
+
}`}
|
|
60
|
+
>
|
|
61
|
+
<div className="flex items-center justify-center gap-2">
|
|
62
|
+
<LinkIcon size={16} />
|
|
63
|
+
URL/Git
|
|
64
|
+
</div>
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Input Field */}
|
|
69
|
+
<div className="mb-4">
|
|
70
|
+
{method === 'package' ? (
|
|
71
|
+
<div>
|
|
72
|
+
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
|
73
|
+
Package Name
|
|
74
|
+
</label>
|
|
75
|
+
<input
|
|
76
|
+
type="text"
|
|
77
|
+
placeholder="e.g., zenml-kubernetes"
|
|
78
|
+
value={packageName}
|
|
79
|
+
onChange={(e) => setPackageName(e.target.value)}
|
|
80
|
+
className="w-full px-3 py-2 bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
81
|
+
/>
|
|
82
|
+
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
|
83
|
+
Install from PyPI by package name
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
) : (
|
|
87
|
+
<div>
|
|
88
|
+
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
|
89
|
+
URL
|
|
90
|
+
</label>
|
|
91
|
+
<input
|
|
92
|
+
type="text"
|
|
93
|
+
placeholder="e.g., git+https://github.com/..."
|
|
94
|
+
value={url}
|
|
95
|
+
onChange={(e) => setUrl(e.target.value)}
|
|
96
|
+
className="w-full px-3 py-2 bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
97
|
+
/>
|
|
98
|
+
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
|
99
|
+
Install from Git repository or URL
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Actions */}
|
|
106
|
+
<div className="flex gap-2">
|
|
107
|
+
<Button variant="outline" onClick={onClose} className="flex-1">
|
|
108
|
+
Cancel
|
|
109
|
+
</Button>
|
|
110
|
+
<Button
|
|
111
|
+
onClick={handleAdd}
|
|
112
|
+
disabled={method === 'package' ? !packageName : !url}
|
|
113
|
+
className="flex-1"
|
|
114
|
+
>
|
|
115
|
+
Install
|
|
116
|
+
</Button>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Trash2, Settings, RefreshCw, Loader2 } from 'lucide-react';
|
|
3
|
+
import { Button } from '../ui/Button';
|
|
4
|
+
import { Badge } from '../ui/Badge';
|
|
5
|
+
import { pluginService } from '../../services/pluginService';
|
|
6
|
+
|
|
7
|
+
export function InstalledPlugins() {
|
|
8
|
+
const [plugins, setPlugins] = useState([]);
|
|
9
|
+
const [loading, setLoading] = useState(true);
|
|
10
|
+
const [uninstalling, setUninstalling] = useState(null);
|
|
11
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
loadPlugins();
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
const loadPlugins = async () => {
|
|
18
|
+
try {
|
|
19
|
+
setLoading(true);
|
|
20
|
+
const data = await pluginService.getInstalledPlugins();
|
|
21
|
+
setPlugins(data);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Failed to load installed plugins:', error);
|
|
24
|
+
} finally {
|
|
25
|
+
setLoading(false);
|
|
26
|
+
setRefreshing(false);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleRefresh = async () => {
|
|
31
|
+
setRefreshing(true);
|
|
32
|
+
await loadPlugins();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleUninstall = async (id) => {
|
|
36
|
+
if (!confirm('Are you sure you want to uninstall this plugin?')) return;
|
|
37
|
+
|
|
38
|
+
setUninstalling(id);
|
|
39
|
+
try {
|
|
40
|
+
await pluginService.uninstallPlugin(id);
|
|
41
|
+
setPlugins(prev => prev.filter(p => p.id !== id));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Uninstall failed:', error);
|
|
44
|
+
alert(`Uninstall failed: ${error.message}`);
|
|
45
|
+
} finally {
|
|
46
|
+
setUninstalling(null);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (loading) {
|
|
51
|
+
return (
|
|
52
|
+
<div className="flex justify-center items-center py-12">
|
|
53
|
+
<Loader2 className="animate-spin text-primary-500" size={32} />
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (plugins.length === 0) {
|
|
59
|
+
return (
|
|
60
|
+
<div className="text-center py-12 text-slate-500 dark:text-slate-400">
|
|
61
|
+
<p>No plugins installed yet.</p>
|
|
62
|
+
<p className="text-sm mt-2">Install plugins from the Browser tab</p>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="space-y-4">
|
|
69
|
+
{/* Refresh Button */}
|
|
70
|
+
<div className="flex justify-end">
|
|
71
|
+
<Button
|
|
72
|
+
variant="outline"
|
|
73
|
+
size="sm"
|
|
74
|
+
onClick={handleRefresh}
|
|
75
|
+
disabled={refreshing}
|
|
76
|
+
className="flex items-center gap-2"
|
|
77
|
+
>
|
|
78
|
+
<RefreshCw size={14} className={refreshing ? 'animate-spin' : ''} />
|
|
79
|
+
Refresh
|
|
80
|
+
</Button>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{plugins.map((plugin) => (
|
|
84
|
+
<div
|
|
85
|
+
key={plugin.id}
|
|
86
|
+
className="flex items-center justify-between p-4 bg-white dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-xl"
|
|
87
|
+
>
|
|
88
|
+
<div className="flex items-start gap-4">
|
|
89
|
+
<div className="p-2 bg-primary-50 dark:bg-primary-900/20 rounded-lg">
|
|
90
|
+
<Settings className="text-primary-500" size={24} />
|
|
91
|
+
</div>
|
|
92
|
+
<div>
|
|
93
|
+
<div className="flex items-center gap-2">
|
|
94
|
+
<h3 className="font-semibold text-slate-900 dark:text-white">{plugin.name}</h3>
|
|
95
|
+
<Badge variant="outline" className="text-xs">v{plugin.version}</Badge>
|
|
96
|
+
<Badge variant="success" className="text-xs">Active</Badge>
|
|
97
|
+
</div>
|
|
98
|
+
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
|
99
|
+
{plugin.description}
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<Button
|
|
106
|
+
variant="ghost"
|
|
107
|
+
size="sm"
|
|
108
|
+
className="text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
|
|
109
|
+
title="Uninstall"
|
|
110
|
+
onClick={() => handleUninstall(plugin.id)}
|
|
111
|
+
disabled={uninstalling === plugin.id}
|
|
112
|
+
>
|
|
113
|
+
{uninstalling === plugin.id ? (
|
|
114
|
+
<Loader2 size={16} className="animate-spin" />
|
|
115
|
+
) : (
|
|
116
|
+
<Trash2 size={16} />
|
|
117
|
+
)}
|
|
118
|
+
</Button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Search, Download, Star, CheckCircle, Loader2, Plus } from 'lucide-react';
|
|
3
|
+
import { Button } from '../ui/Button';
|
|
4
|
+
import { Badge } from '../ui/Badge';
|
|
5
|
+
import { pluginService } from '../../services/pluginService';
|
|
6
|
+
import { AddPluginDialog } from './AddPluginDialog';
|
|
7
|
+
|
|
8
|
+
export function PluginBrowser() {
|
|
9
|
+
const [search, setSearch] = useState('');
|
|
10
|
+
const [plugins, setPlugins] = useState([]);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
const [installing, setInstalling] = useState(null);
|
|
13
|
+
const [showAddDialog, setShowAddDialog] = useState(false);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
loadPlugins();
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
const loadPlugins = async () => {
|
|
20
|
+
try {
|
|
21
|
+
setLoading(true);
|
|
22
|
+
const data = await pluginService.getAvailablePlugins();
|
|
23
|
+
setPlugins(data);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Failed to load plugins:', error);
|
|
26
|
+
} finally {
|
|
27
|
+
setLoading(false);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const filteredPlugins = plugins.filter(p =>
|
|
32
|
+
p.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
33
|
+
p.description.toLowerCase().includes(search.toLowerCase())
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const handleInstall = async (id) => {
|
|
37
|
+
setInstalling(id);
|
|
38
|
+
try {
|
|
39
|
+
await pluginService.installPlugin(id);
|
|
40
|
+
// Update local state to show installed
|
|
41
|
+
setPlugins(prev => prev.map(p =>
|
|
42
|
+
p.id === id ? { ...p, installed: true } : p
|
|
43
|
+
));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Install failed:', error);
|
|
46
|
+
alert(`Installation failed: ${error.message}`);
|
|
47
|
+
} finally {
|
|
48
|
+
setInstalling(null);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleManualAdd = async ({ type, value }) => {
|
|
53
|
+
setInstalling('manual');
|
|
54
|
+
try {
|
|
55
|
+
await pluginService.installPlugin(value);
|
|
56
|
+
// Reload the list to show the new plugin
|
|
57
|
+
await loadPlugins();
|
|
58
|
+
alert(`Successfully installed ${value}`);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Install failed:', error);
|
|
61
|
+
alert(`Installation failed: ${error.message}`);
|
|
62
|
+
} finally {
|
|
63
|
+
setInstalling(null);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (loading) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex justify-center items-center py-12">
|
|
70
|
+
<Loader2 className="animate-spin text-primary-500" size={32} />
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<>
|
|
77
|
+
<div className="space-y-6">
|
|
78
|
+
<div className="flex gap-4">
|
|
79
|
+
<div className="relative flex-1">
|
|
80
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
|
|
81
|
+
<input
|
|
82
|
+
type="text"
|
|
83
|
+
placeholder="Search plugins..."
|
|
84
|
+
value={search}
|
|
85
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
86
|
+
className="w-full pl-10 pr-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
<Button
|
|
90
|
+
onClick={() => setShowAddDialog(true)}
|
|
91
|
+
className="flex items-center gap-2"
|
|
92
|
+
>
|
|
93
|
+
<Plus size={18} />
|
|
94
|
+
Add Plugin
|
|
95
|
+
</Button>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
99
|
+
{filteredPlugins.map((plugin) => (
|
|
100
|
+
<div
|
|
101
|
+
key={plugin.id}
|
|
102
|
+
className="p-4 border border-slate-200 dark:border-slate-700 rounded-xl hover:border-primary-500/50 dark:hover:border-primary-500/50 transition-colors bg-white dark:bg-slate-800/50"
|
|
103
|
+
>
|
|
104
|
+
<div className="flex justify-between items-start mb-2">
|
|
105
|
+
<div>
|
|
106
|
+
<h3 className="font-semibold text-slate-900 dark:text-white">{plugin.name}</h3>
|
|
107
|
+
<div className="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400 mt-1">
|
|
108
|
+
<span>v{plugin.version}</span>
|
|
109
|
+
<span>•</span>
|
|
110
|
+
<span>by {plugin.author}</span>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
{plugin.installed ? (
|
|
114
|
+
<Badge variant="success" className="flex items-center gap-1">
|
|
115
|
+
<CheckCircle size={12} /> Installed
|
|
116
|
+
</Badge>
|
|
117
|
+
) : (
|
|
118
|
+
<Button
|
|
119
|
+
size="sm"
|
|
120
|
+
variant="outline"
|
|
121
|
+
onClick={() => handleInstall(plugin.id)}
|
|
122
|
+
disabled={installing === plugin.id || installing === 'manual'}
|
|
123
|
+
>
|
|
124
|
+
{installing === plugin.id ? (
|
|
125
|
+
<>
|
|
126
|
+
<Loader2 size={12} className="animate-spin mr-1" />
|
|
127
|
+
Installing...
|
|
128
|
+
</>
|
|
129
|
+
) : 'Install'}
|
|
130
|
+
</Button>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<p className="text-sm text-slate-600 dark:text-slate-300 mb-4 line-clamp-2">
|
|
135
|
+
{plugin.description}
|
|
136
|
+
</p>
|
|
137
|
+
|
|
138
|
+
<div className="flex items-center justify-between mt-auto">
|
|
139
|
+
<div className="flex gap-2">
|
|
140
|
+
{plugin.tags.map(tag => (
|
|
141
|
+
<span key={tag} className="px-2 py-0.5 bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 text-xs rounded-full">
|
|
142
|
+
{tag}
|
|
143
|
+
</span>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
<div className="flex items-center gap-3 text-xs text-slate-400">
|
|
147
|
+
<div className="flex items-center gap-1">
|
|
148
|
+
<Download size={12} /> {plugin.downloads}
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex items-center gap-1">
|
|
151
|
+
<Star size={12} /> {plugin.stars}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<AddPluginDialog
|
|
161
|
+
isOpen={showAddDialog}
|
|
162
|
+
onClose={() => setShowAddDialog(false)}
|
|
163
|
+
onAdd={handleManualAdd}
|
|
164
|
+
/>
|
|
165
|
+
</>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Package, Download, RefreshCw, Server } from 'lucide-react';
|
|
3
|
+
import { Card, CardHeader, CardTitle, CardContent } from '../ui/Card';
|
|
4
|
+
import { PluginBrowser } from './PluginBrowser';
|
|
5
|
+
import { InstalledPlugins } from './InstalledPlugins';
|
|
6
|
+
import { ZenMLIntegration } from './ZenMLIntegration';
|
|
7
|
+
|
|
8
|
+
export function PluginManager() {
|
|
9
|
+
const [activeTab, setActiveTab] = useState('browser');
|
|
10
|
+
|
|
11
|
+
const tabs = [
|
|
12
|
+
{ id: 'browser', label: 'Plugin Browser', icon: Package },
|
|
13
|
+
{ id: 'installed', label: 'Installed', icon: Download },
|
|
14
|
+
{ id: 'zenml', label: 'ZenML Integration', icon: Server },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Card className="overflow-hidden">
|
|
19
|
+
<CardHeader className="border-b border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 pb-0">
|
|
20
|
+
<div className="flex items-center justify-between mb-4">
|
|
21
|
+
<div className="flex items-center gap-2">
|
|
22
|
+
<Package size={20} className="text-primary-500" />
|
|
23
|
+
<CardTitle>Plugins & Integrations</CardTitle>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div className="flex gap-6">
|
|
28
|
+
{tabs.map((tab) => {
|
|
29
|
+
const Icon = tab.icon;
|
|
30
|
+
const isActive = activeTab === tab.id;
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
key={tab.id}
|
|
34
|
+
onClick={() => setActiveTab(tab.id)}
|
|
35
|
+
className={`
|
|
36
|
+
flex items-center gap-2 pb-3 text-sm font-medium transition-all relative
|
|
37
|
+
${isActive
|
|
38
|
+
? 'text-primary-600 dark:text-primary-400'
|
|
39
|
+
: 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200'
|
|
40
|
+
}
|
|
41
|
+
`}
|
|
42
|
+
>
|
|
43
|
+
<Icon size={16} />
|
|
44
|
+
{tab.label}
|
|
45
|
+
{isActive && (
|
|
46
|
+
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary-500 rounded-t-full" />
|
|
47
|
+
)}
|
|
48
|
+
</button>
|
|
49
|
+
);
|
|
50
|
+
})}
|
|
51
|
+
</div>
|
|
52
|
+
</CardHeader>
|
|
53
|
+
<CardContent className="p-6">
|
|
54
|
+
{activeTab === 'browser' && <PluginBrowser />}
|
|
55
|
+
{activeTab === 'installed' && <InstalledPlugins />}
|
|
56
|
+
{activeTab === 'zenml' && <ZenMLIntegration />}
|
|
57
|
+
</CardContent>
|
|
58
|
+
</Card>
|
|
59
|
+
);
|
|
60
|
+
}
|