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.
Files changed (159) hide show
  1. flowyml/__init__.py +207 -0
  2. flowyml/assets/__init__.py +22 -0
  3. flowyml/assets/artifact.py +40 -0
  4. flowyml/assets/base.py +209 -0
  5. flowyml/assets/dataset.py +100 -0
  6. flowyml/assets/featureset.py +301 -0
  7. flowyml/assets/metrics.py +104 -0
  8. flowyml/assets/model.py +82 -0
  9. flowyml/assets/registry.py +157 -0
  10. flowyml/assets/report.py +315 -0
  11. flowyml/cli/__init__.py +5 -0
  12. flowyml/cli/experiment.py +232 -0
  13. flowyml/cli/init.py +256 -0
  14. flowyml/cli/main.py +327 -0
  15. flowyml/cli/run.py +75 -0
  16. flowyml/cli/stack_cli.py +532 -0
  17. flowyml/cli/ui.py +33 -0
  18. flowyml/core/__init__.py +68 -0
  19. flowyml/core/advanced_cache.py +274 -0
  20. flowyml/core/approval.py +64 -0
  21. flowyml/core/cache.py +203 -0
  22. flowyml/core/checkpoint.py +148 -0
  23. flowyml/core/conditional.py +373 -0
  24. flowyml/core/context.py +155 -0
  25. flowyml/core/error_handling.py +419 -0
  26. flowyml/core/executor.py +354 -0
  27. flowyml/core/graph.py +185 -0
  28. flowyml/core/parallel.py +452 -0
  29. flowyml/core/pipeline.py +764 -0
  30. flowyml/core/project.py +253 -0
  31. flowyml/core/resources.py +424 -0
  32. flowyml/core/scheduler.py +630 -0
  33. flowyml/core/scheduler_config.py +32 -0
  34. flowyml/core/step.py +201 -0
  35. flowyml/core/step_grouping.py +292 -0
  36. flowyml/core/templates.py +226 -0
  37. flowyml/core/versioning.py +217 -0
  38. flowyml/integrations/__init__.py +1 -0
  39. flowyml/integrations/keras.py +134 -0
  40. flowyml/monitoring/__init__.py +1 -0
  41. flowyml/monitoring/alerts.py +57 -0
  42. flowyml/monitoring/data.py +102 -0
  43. flowyml/monitoring/llm.py +160 -0
  44. flowyml/monitoring/monitor.py +57 -0
  45. flowyml/monitoring/notifications.py +246 -0
  46. flowyml/registry/__init__.py +5 -0
  47. flowyml/registry/model_registry.py +491 -0
  48. flowyml/registry/pipeline_registry.py +55 -0
  49. flowyml/stacks/__init__.py +27 -0
  50. flowyml/stacks/base.py +77 -0
  51. flowyml/stacks/bridge.py +288 -0
  52. flowyml/stacks/components.py +155 -0
  53. flowyml/stacks/gcp.py +499 -0
  54. flowyml/stacks/local.py +112 -0
  55. flowyml/stacks/migration.py +97 -0
  56. flowyml/stacks/plugin_config.py +78 -0
  57. flowyml/stacks/plugins.py +401 -0
  58. flowyml/stacks/registry.py +226 -0
  59. flowyml/storage/__init__.py +26 -0
  60. flowyml/storage/artifacts.py +246 -0
  61. flowyml/storage/materializers/__init__.py +20 -0
  62. flowyml/storage/materializers/base.py +133 -0
  63. flowyml/storage/materializers/keras.py +185 -0
  64. flowyml/storage/materializers/numpy.py +94 -0
  65. flowyml/storage/materializers/pandas.py +142 -0
  66. flowyml/storage/materializers/pytorch.py +135 -0
  67. flowyml/storage/materializers/sklearn.py +110 -0
  68. flowyml/storage/materializers/tensorflow.py +152 -0
  69. flowyml/storage/metadata.py +931 -0
  70. flowyml/tracking/__init__.py +1 -0
  71. flowyml/tracking/experiment.py +211 -0
  72. flowyml/tracking/leaderboard.py +191 -0
  73. flowyml/tracking/runs.py +145 -0
  74. flowyml/ui/__init__.py +15 -0
  75. flowyml/ui/backend/Dockerfile +31 -0
  76. flowyml/ui/backend/__init__.py +0 -0
  77. flowyml/ui/backend/auth.py +163 -0
  78. flowyml/ui/backend/main.py +187 -0
  79. flowyml/ui/backend/routers/__init__.py +0 -0
  80. flowyml/ui/backend/routers/assets.py +45 -0
  81. flowyml/ui/backend/routers/execution.py +179 -0
  82. flowyml/ui/backend/routers/experiments.py +49 -0
  83. flowyml/ui/backend/routers/leaderboard.py +118 -0
  84. flowyml/ui/backend/routers/notifications.py +72 -0
  85. flowyml/ui/backend/routers/pipelines.py +110 -0
  86. flowyml/ui/backend/routers/plugins.py +192 -0
  87. flowyml/ui/backend/routers/projects.py +85 -0
  88. flowyml/ui/backend/routers/runs.py +66 -0
  89. flowyml/ui/backend/routers/schedules.py +222 -0
  90. flowyml/ui/backend/routers/traces.py +84 -0
  91. flowyml/ui/frontend/Dockerfile +20 -0
  92. flowyml/ui/frontend/README.md +315 -0
  93. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
  94. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
  95. flowyml/ui/frontend/dist/index.html +16 -0
  96. flowyml/ui/frontend/index.html +15 -0
  97. flowyml/ui/frontend/nginx.conf +26 -0
  98. flowyml/ui/frontend/package-lock.json +3545 -0
  99. flowyml/ui/frontend/package.json +33 -0
  100. flowyml/ui/frontend/postcss.config.js +6 -0
  101. flowyml/ui/frontend/src/App.jsx +21 -0
  102. flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
  103. flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
  104. flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
  105. flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
  106. flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
  107. flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
  108. flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
  109. flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
  110. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
  111. flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
  112. flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
  113. flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
  114. flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
  115. flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
  116. flowyml/ui/frontend/src/components/Layout.jsx +108 -0
  117. flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
  118. flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
  119. flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
  120. flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
  121. flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
  122. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
  123. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
  124. flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
  125. flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
  126. flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
  127. flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
  128. flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
  129. flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
  130. flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
  131. flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
  132. flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
  133. flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
  134. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
  135. flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
  136. flowyml/ui/frontend/src/index.css +11 -0
  137. flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
  138. flowyml/ui/frontend/src/main.jsx +10 -0
  139. flowyml/ui/frontend/src/router/index.jsx +39 -0
  140. flowyml/ui/frontend/src/services/pluginService.js +90 -0
  141. flowyml/ui/frontend/src/utils/api.js +47 -0
  142. flowyml/ui/frontend/src/utils/cn.js +6 -0
  143. flowyml/ui/frontend/tailwind.config.js +31 -0
  144. flowyml/ui/frontend/vite.config.js +21 -0
  145. flowyml/ui/utils.py +77 -0
  146. flowyml/utils/__init__.py +67 -0
  147. flowyml/utils/config.py +308 -0
  148. flowyml/utils/debug.py +240 -0
  149. flowyml/utils/environment.py +346 -0
  150. flowyml/utils/git.py +319 -0
  151. flowyml/utils/logging.py +61 -0
  152. flowyml/utils/performance.py +314 -0
  153. flowyml/utils/stack_config.py +296 -0
  154. flowyml/utils/validation.py +270 -0
  155. flowyml-1.1.0.dist-info/METADATA +372 -0
  156. flowyml-1.1.0.dist-info/RECORD +159 -0
  157. flowyml-1.1.0.dist-info/WHEEL +4 -0
  158. flowyml-1.1.0.dist-info/entry_points.txt +3 -0
  159. 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
+ }