flowyml 1.7.2__py3-none-any.whl → 1.8.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 (126) hide show
  1. flowyml/assets/base.py +15 -0
  2. flowyml/assets/metrics.py +5 -0
  3. flowyml/cli/main.py +709 -0
  4. flowyml/cli/stack_cli.py +138 -25
  5. flowyml/core/__init__.py +17 -0
  6. flowyml/core/executor.py +161 -26
  7. flowyml/core/image_builder.py +129 -0
  8. flowyml/core/log_streamer.py +227 -0
  9. flowyml/core/orchestrator.py +22 -2
  10. flowyml/core/pipeline.py +34 -10
  11. flowyml/core/routing.py +558 -0
  12. flowyml/core/step.py +9 -1
  13. flowyml/core/step_grouping.py +49 -35
  14. flowyml/core/types.py +407 -0
  15. flowyml/monitoring/alerts.py +10 -0
  16. flowyml/monitoring/notifications.py +104 -25
  17. flowyml/monitoring/slack_blocks.py +323 -0
  18. flowyml/plugins/__init__.py +251 -0
  19. flowyml/plugins/alerters/__init__.py +1 -0
  20. flowyml/plugins/alerters/slack.py +168 -0
  21. flowyml/plugins/base.py +752 -0
  22. flowyml/plugins/config.py +478 -0
  23. flowyml/plugins/deployers/__init__.py +22 -0
  24. flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
  25. flowyml/plugins/deployers/sagemaker.py +306 -0
  26. flowyml/plugins/deployers/vertex.py +290 -0
  27. flowyml/plugins/integration.py +369 -0
  28. flowyml/plugins/manager.py +510 -0
  29. flowyml/plugins/model_registries/__init__.py +22 -0
  30. flowyml/plugins/model_registries/mlflow.py +159 -0
  31. flowyml/plugins/model_registries/sagemaker.py +489 -0
  32. flowyml/plugins/model_registries/vertex.py +386 -0
  33. flowyml/plugins/orchestrators/__init__.py +13 -0
  34. flowyml/plugins/orchestrators/sagemaker.py +443 -0
  35. flowyml/plugins/orchestrators/vertex_ai.py +461 -0
  36. flowyml/plugins/registries/__init__.py +13 -0
  37. flowyml/plugins/registries/ecr.py +321 -0
  38. flowyml/plugins/registries/gcr.py +313 -0
  39. flowyml/plugins/registry.py +454 -0
  40. flowyml/plugins/stack.py +494 -0
  41. flowyml/plugins/stack_config.py +537 -0
  42. flowyml/plugins/stores/__init__.py +13 -0
  43. flowyml/plugins/stores/gcs.py +460 -0
  44. flowyml/plugins/stores/s3.py +453 -0
  45. flowyml/plugins/trackers/__init__.py +11 -0
  46. flowyml/plugins/trackers/mlflow.py +316 -0
  47. flowyml/plugins/validators/__init__.py +3 -0
  48. flowyml/plugins/validators/deepchecks.py +119 -0
  49. flowyml/registry/__init__.py +2 -1
  50. flowyml/registry/model_environment.py +109 -0
  51. flowyml/registry/model_registry.py +241 -96
  52. flowyml/serving/__init__.py +17 -0
  53. flowyml/serving/model_server.py +628 -0
  54. flowyml/stacks/__init__.py +60 -0
  55. flowyml/stacks/aws.py +93 -0
  56. flowyml/stacks/base.py +62 -0
  57. flowyml/stacks/components.py +12 -0
  58. flowyml/stacks/gcp.py +44 -9
  59. flowyml/stacks/plugins.py +115 -0
  60. flowyml/stacks/registry.py +2 -1
  61. flowyml/storage/sql.py +401 -12
  62. flowyml/tracking/experiment.py +8 -5
  63. flowyml/ui/backend/Dockerfile +87 -16
  64. flowyml/ui/backend/auth.py +12 -2
  65. flowyml/ui/backend/main.py +149 -5
  66. flowyml/ui/backend/routers/ai_context.py +226 -0
  67. flowyml/ui/backend/routers/assets.py +23 -4
  68. flowyml/ui/backend/routers/auth.py +96 -0
  69. flowyml/ui/backend/routers/deployments.py +660 -0
  70. flowyml/ui/backend/routers/model_explorer.py +597 -0
  71. flowyml/ui/backend/routers/plugins.py +103 -51
  72. flowyml/ui/backend/routers/projects.py +91 -8
  73. flowyml/ui/backend/routers/runs.py +20 -1
  74. flowyml/ui/backend/routers/schedules.py +22 -17
  75. flowyml/ui/backend/routers/templates.py +319 -0
  76. flowyml/ui/backend/routers/websocket.py +2 -2
  77. flowyml/ui/frontend/Dockerfile +55 -6
  78. flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
  79. flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
  80. flowyml/ui/frontend/dist/index.html +2 -2
  81. flowyml/ui/frontend/dist/logo.png +0 -0
  82. flowyml/ui/frontend/nginx.conf +65 -4
  83. flowyml/ui/frontend/package-lock.json +1404 -74
  84. flowyml/ui/frontend/package.json +3 -0
  85. flowyml/ui/frontend/public/logo.png +0 -0
  86. flowyml/ui/frontend/src/App.jsx +10 -7
  87. flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
  88. flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
  89. flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
  90. flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
  91. flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
  92. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
  93. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +36 -24
  94. flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
  95. flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
  96. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +29 -7
  97. flowyml/ui/frontend/src/components/Layout.jsx +6 -0
  98. flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
  99. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
  100. flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
  101. flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
  102. flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
  103. flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
  104. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
  105. flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
  106. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
  107. flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
  108. flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
  109. flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
  110. flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
  111. flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
  112. flowyml/ui/frontend/src/router/index.jsx +47 -20
  113. flowyml/ui/frontend/src/services/pluginService.js +3 -1
  114. flowyml/ui/server_manager.py +5 -5
  115. flowyml/ui/utils.py +157 -39
  116. flowyml/utils/config.py +37 -15
  117. flowyml/utils/model_introspection.py +123 -0
  118. flowyml/utils/observability.py +30 -0
  119. flowyml-1.8.0.dist-info/METADATA +174 -0
  120. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/RECORD +123 -65
  121. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
  122. flowyml/ui/frontend/dist/assets/index-B40RsQDq.css +0 -1
  123. flowyml/ui/frontend/dist/assets/index-CjI0zKCn.js +0 -685
  124. flowyml-1.7.2.dist-info/METADATA +0 -477
  125. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
  126. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,786 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useSearchParams } from 'react-router-dom';
3
+ import {
4
+ Rocket,
5
+ Plus,
6
+ Play,
7
+ Square,
8
+ Trash2,
9
+ RefreshCw,
10
+ Copy,
11
+ ExternalLink,
12
+ Terminal,
13
+ Shield,
14
+ Clock,
15
+ CheckCircle2,
16
+ AlertCircle,
17
+ Loader2,
18
+ Settings,
19
+ ChevronDown,
20
+ ChevronUp,
21
+ Eye,
22
+ EyeOff,
23
+ X,
24
+ Package,
25
+ Download
26
+ } from 'lucide-react';
27
+ import { Card } from '../../components/ui/Card';
28
+ import { Button } from '../../components/ui/Button';
29
+ import { Badge } from '../../components/ui/Badge';
30
+
31
+ export function DeploymentLab() {
32
+ const [searchParams, setSearchParams] = useSearchParams();
33
+ const [deployments, setDeployments] = useState([]);
34
+ const [availableModels, setAvailableModels] = useState([]);
35
+ const [loading, setLoading] = useState(true);
36
+ const [showCreateModal, setShowCreateModal] = useState(false);
37
+ const [selectedDeployment, setSelectedDeployment] = useState(null);
38
+ const [visibleTokens, setVisibleTokens] = useState(new Set());
39
+ const [modelSearch, setModelSearch] = useState('');
40
+ const [showModelDropdown, setShowModelDropdown] = useState(false);
41
+ const [deploymentLogs, setDeploymentLogs] = useState([]);
42
+ const [logsLoading, setLogsLoading] = useState(false);
43
+ const [deleteConfirmId, setDeleteConfirmId] = useState(null);
44
+
45
+ // Dependency installation state
46
+ const [showDependencies, setShowDependencies] = useState(false);
47
+ const [installedDeps, setInstalledDeps] = useState({});
48
+ const [installingDeps, setInstallingDeps] = useState(new Set());
49
+
50
+ // Form state
51
+ const [newDeployment, setNewDeployment] = useState({
52
+ name: '',
53
+ model_artifact_id: '',
54
+ model_version: null,
55
+ port: null,
56
+ config: {
57
+ rate_limit: 100,
58
+ timeout_seconds: 30,
59
+ max_batch_size: 1,
60
+ enable_cors: true,
61
+ ttl_seconds: null // Auto-destroy after N seconds (null = never)
62
+ }
63
+ });
64
+
65
+ useEffect(() => {
66
+ fetchDeployments();
67
+ fetchAvailableModels();
68
+
69
+ // Auto-refresh deployment status every 5 seconds for status updates and TTL countdown
70
+ const refreshInterval = setInterval(() => {
71
+ fetchDeployments();
72
+ }, 5000);
73
+
74
+ return () => clearInterval(refreshInterval);
75
+ }, []);
76
+
77
+ // Handle URL parameters for deploy action from Assets page
78
+ useEffect(() => {
79
+ const deployId = searchParams.get('deploy');
80
+ const modelName = searchParams.get('name');
81
+
82
+ if (deployId) {
83
+ // Pre-populate form with model info
84
+ setNewDeployment(prev => ({
85
+ ...prev,
86
+ model_artifact_id: deployId,
87
+ name: modelName ? `${modelName}-api` : ''
88
+ }));
89
+ setShowCreateModal(true);
90
+ // Clear the URL params
91
+ setSearchParams({});
92
+ }
93
+ }, [searchParams, setSearchParams]);
94
+
95
+ const fetchDeployments = async () => {
96
+ try {
97
+ const response = await fetch('/api/deployments/');
98
+ const data = await response.json();
99
+ setDeployments(Array.isArray(data) ? data : []);
100
+ } catch (error) {
101
+ console.error('Failed to fetch deployments:', error);
102
+ } finally {
103
+ setLoading(false);
104
+ }
105
+ };
106
+
107
+ const fetchAvailableModels = async () => {
108
+ try {
109
+ const response = await fetch('/api/deployments/available-models');
110
+ const data = await response.json();
111
+ setAvailableModels(Array.isArray(data) ? data : []);
112
+ } catch (error) {
113
+ console.error('Failed to fetch models:', error);
114
+ }
115
+ };
116
+
117
+ const fetchDependencyStatus = async () => {
118
+ try {
119
+ const response = await fetch('/api/deployments/dependencies/status');
120
+ const data = await response.json();
121
+ setInstalledDeps(data.installed || {});
122
+ } catch (error) {
123
+ console.error('Failed to fetch dependency status:', error);
124
+ }
125
+ };
126
+
127
+ const installDependency = async (framework) => {
128
+ setInstallingDeps(prev => new Set([...prev, framework]));
129
+ try {
130
+ const response = await fetch('/api/deployments/dependencies/install', {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify({ frameworks: [framework] })
134
+ });
135
+ if (response.ok) {
136
+ // Wait a bit for installation to start, then refresh status
137
+ setTimeout(() => {
138
+ fetchDependencyStatus();
139
+ setInstallingDeps(prev => {
140
+ const next = new Set(prev);
141
+ next.delete(framework);
142
+ return next;
143
+ });
144
+ }, 3000);
145
+ }
146
+ } catch (error) {
147
+ console.error('Failed to install dependency:', error);
148
+ setInstallingDeps(prev => {
149
+ const next = new Set(prev);
150
+ next.delete(framework);
151
+ return next;
152
+ });
153
+ }
154
+ };
155
+
156
+ const createDeployment = async (e) => {
157
+ e.preventDefault();
158
+ try {
159
+ const response = await fetch('/api/deployments/', {
160
+ method: 'POST',
161
+ headers: { 'Content-Type': 'application/json' },
162
+ body: JSON.stringify(newDeployment)
163
+ });
164
+ if (response.ok) {
165
+ setShowCreateModal(false);
166
+ setNewDeployment({
167
+ name: '',
168
+ model_artifact_id: '',
169
+ model_version: null,
170
+ port: null,
171
+ config: { rate_limit: 100, timeout_seconds: 30, max_batch_size: 1, enable_cors: true }
172
+ });
173
+ fetchDeployments();
174
+ }
175
+ } catch (error) {
176
+ console.error('Failed to create deployment:', error);
177
+ }
178
+ };
179
+
180
+ const toggleDeployment = async (id, action) => {
181
+ try {
182
+ await fetch(`/api/deployments/${id}/${action}`, { method: 'POST' });
183
+ fetchDeployments();
184
+ } catch (error) {
185
+ console.error(`Failed to ${action} deployment:`, error);
186
+ }
187
+ };
188
+
189
+ const deleteDeployment = async (id) => {
190
+ try {
191
+ await fetch(`/api/deployments/${id}`, { method: 'DELETE' });
192
+ setDeleteConfirmId(null);
193
+ fetchDeployments();
194
+ } catch (error) {
195
+ console.error('Failed to delete deployment:', error);
196
+ }
197
+ };
198
+
199
+ const copyToClipboard = (text) => {
200
+ navigator.clipboard.writeText(text);
201
+ };
202
+
203
+ const toggleTokenVisibility = (id) => {
204
+ setVisibleTokens(prev => {
205
+ const newSet = new Set(prev);
206
+ if (newSet.has(id)) newSet.delete(id);
207
+ else newSet.add(id);
208
+ return newSet;
209
+ });
210
+ };
211
+
212
+ const maskToken = (token) => {
213
+ if (!token) return '••••••••••••••••';
214
+ return `${token.substring(0, 8)}••••••••${token.substring(token.length - 4)}`;
215
+ };
216
+
217
+ const getStatusColor = (status) => {
218
+ switch (status) {
219
+ case 'running': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-400';
220
+ case 'pending':
221
+ case 'starting': return 'bg-amber-100 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400';
222
+ case 'stopped': return 'bg-slate-100 text-slate-700 dark:bg-slate-900/20 dark:text-slate-400';
223
+ case 'error': return 'bg-rose-100 text-rose-700 dark:bg-rose-900/20 dark:text-rose-400';
224
+ default: return 'bg-slate-100 text-slate-700';
225
+ }
226
+ };
227
+
228
+ const getStatusIcon = (status) => {
229
+ switch (status) {
230
+ case 'running': return <CheckCircle2 size={14} />;
231
+ case 'pending':
232
+ case 'starting': return <Loader2 size={14} className="animate-spin" />;
233
+ case 'stopped': return <Square size={14} />;
234
+ case 'error': return <AlertCircle size={14} />;
235
+ default: return <Clock size={14} />;
236
+ }
237
+ };
238
+
239
+ // Fetch deployment logs
240
+ const fetchDeploymentLogs = async (deployment) => {
241
+ setSelectedDeployment(deployment);
242
+ setLogsLoading(true);
243
+ try {
244
+ const response = await fetch(`/api/deployments/${deployment.id}/logs`);
245
+ if (response.ok) {
246
+ const data = await response.json();
247
+ setDeploymentLogs(data.logs || []);
248
+ }
249
+ } catch (error) {
250
+ console.error('Failed to fetch logs:', error);
251
+ setDeploymentLogs([{ timestamp: new Date().toISOString(), level: 'ERROR', message: 'Failed to fetch logs' }]);
252
+ } finally {
253
+ setLogsLoading(false);
254
+ }
255
+ };
256
+
257
+ // Calculate TTL countdown
258
+ const getTimeRemaining = (expiresAt) => {
259
+ if (!expiresAt) return null;
260
+ const diff = new Date(expiresAt) - new Date();
261
+ if (diff <= 0) return 'Expired';
262
+ const minutes = Math.floor(diff / 60000);
263
+ const seconds = Math.floor((diff % 60000) / 1000);
264
+ if (minutes >= 60) {
265
+ const hours = Math.floor(minutes / 60);
266
+ return `${hours}h ${minutes % 60}m`;
267
+ }
268
+ return `${minutes}m ${seconds}s`;
269
+ };
270
+
271
+
272
+ if (loading) {
273
+ return (
274
+ <div className="flex items-center justify-center min-h-screen">
275
+ <Loader2 className="w-12 h-12 animate-spin text-primary-600" />
276
+ </div>
277
+ );
278
+ }
279
+
280
+ return (
281
+ <div className="p-6 max-w-7xl mx-auto space-y-6">
282
+ {/* Header */}
283
+ <div className="flex items-center justify-between">
284
+ <div>
285
+ <h1 className="text-3xl font-bold text-slate-900 dark:text-white flex items-center gap-3">
286
+ <div className="p-3 bg-gradient-to-br from-orange-500 to-red-500 rounded-xl text-white">
287
+ <Rocket size={28} />
288
+ </div>
289
+ Deployment Lab
290
+ <Badge className="bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 text-xs font-medium">
291
+ Experimental
292
+ </Badge>
293
+ </h1>
294
+ <p className="text-slate-500 dark:text-slate-400 mt-2">
295
+ Deploy models as API endpoints (TFServing for Keras models coming soon)
296
+ </p>
297
+ </div>
298
+ <div className="flex gap-3">
299
+ <Button
300
+ onClick={() => {
301
+ setShowDependencies(!showDependencies);
302
+ if (!showDependencies) fetchDependencyStatus();
303
+ }}
304
+ variant="ghost"
305
+ className="flex items-center gap-2"
306
+ >
307
+ <Package size={16} />
308
+ Dependencies
309
+ </Button>
310
+ <Button
311
+ onClick={fetchDeployments}
312
+ variant="ghost"
313
+ className="flex items-center gap-2"
314
+ >
315
+ <RefreshCw size={16} />
316
+ Refresh
317
+ </Button>
318
+ <Button
319
+ onClick={() => setShowCreateModal(true)}
320
+ className="flex items-center gap-2 bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600"
321
+ >
322
+ <Plus size={16} />
323
+ New Deployment
324
+ </Button>
325
+ </div>
326
+ </div>
327
+
328
+ {/* ML Dependencies Panel */}
329
+ {showDependencies && (
330
+ <Card className="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-blue-200 dark:border-blue-800">
331
+ <div className="p-4">
332
+ <div className="flex items-center justify-between mb-4">
333
+ <div className="flex items-center gap-2">
334
+ <Package className="text-blue-500" size={20} />
335
+ <h3 className="font-semibold text-slate-900 dark:text-white">ML Framework Dependencies</h3>
336
+ </div>
337
+ <Button variant="ghost" size="sm" onClick={fetchDependencyStatus}>
338
+ <RefreshCw size={14} />
339
+ </Button>
340
+ </div>
341
+ <p className="text-sm text-slate-500 dark:text-slate-400 mb-4">
342
+ Install ML frameworks on the server to enable model predictions
343
+ </p>
344
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
345
+ {['keras', 'tensorflow', 'pytorch', 'sklearn', 'xgboost', 'onnx'].map(framework => (
346
+ <div key={framework} className="flex items-center justify-between p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
347
+ <span className="font-medium capitalize">{framework}</span>
348
+ {installedDeps[framework] ? (
349
+ <Badge className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
350
+ <CheckCircle2 size={12} className="mr-1" />
351
+ Installed
352
+ </Badge>
353
+ ) : installingDeps.has(framework) ? (
354
+ <Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">
355
+ <Loader2 size={12} className="mr-1 animate-spin" />
356
+ Installing...
357
+ </Badge>
358
+ ) : (
359
+ <Button
360
+ size="sm"
361
+ variant="ghost"
362
+ onClick={() => installDependency(framework)}
363
+ className="text-blue-600 hover:bg-blue-50"
364
+ >
365
+ <Download size={14} className="mr-1" />
366
+ Install
367
+ </Button>
368
+ )}
369
+ </div>
370
+ ))}
371
+ </div>
372
+ </div>
373
+ </Card>
374
+ )}
375
+
376
+ {/* Deployments Grid */}
377
+ {deployments.length === 0 ? (
378
+ <Card className="text-center py-16 bg-slate-50 dark:bg-slate-800/30">
379
+ <div className="mx-auto w-20 h-20 bg-gradient-to-br from-orange-100 to-red-100 dark:from-orange-900/20 dark:to-red-900/20 rounded-2xl flex items-center justify-center mb-6">
380
+ <Rocket className="text-orange-500" size={32} />
381
+ </div>
382
+ <h3 className="text-xl font-bold text-slate-900 dark:text-white mb-2">No Deployments Yet</h3>
383
+ <p className="text-slate-500 max-w-md mx-auto mb-6">
384
+ Deploy your first model as an API endpoint to start testing predictions
385
+ </p>
386
+ <Button
387
+ onClick={() => setShowCreateModal(true)}
388
+ className="bg-gradient-to-r from-orange-500 to-red-500"
389
+ >
390
+ <Plus size={16} className="mr-2" />
391
+ Create Your First Deployment
392
+ </Button>
393
+ </Card>
394
+ ) : (
395
+ <div className="grid gap-4 md:grid-cols-2">
396
+ {deployments.map((deployment) => (
397
+ <Card key={deployment.id} className="hover:shadow-lg transition-all duration-200">
398
+ <div className="flex items-start justify-between mb-4">
399
+ <div className="flex items-center gap-3">
400
+ <div className={`p-2 rounded-lg ${deployment.status === 'running' ? 'bg-emerald-100 dark:bg-emerald-900/20' : 'bg-slate-100 dark:bg-slate-800'}`}>
401
+ <Rocket className={deployment.status === 'running' ? 'text-emerald-600' : 'text-slate-500'} size={20} />
402
+ </div>
403
+ <div>
404
+ <h3 className="font-semibold text-slate-900 dark:text-white">{deployment.name}</h3>
405
+ <p className="text-xs text-slate-500">ID: {deployment.id.substring(0, 8)}...</p>
406
+ </div>
407
+ </div>
408
+ <Badge className={getStatusColor(deployment.status)}>
409
+ <span className="flex items-center gap-1">
410
+ {getStatusIcon(deployment.status)}
411
+ {deployment.status}
412
+ </span>
413
+ </Badge>
414
+ </div>
415
+
416
+ {/* Endpoint URL */}
417
+ <div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-3 mb-4">
418
+ <div className="flex items-center justify-between mb-1">
419
+ <span className="text-xs font-medium text-slate-500">Endpoint</span>
420
+ <button
421
+ onClick={() => copyToClipboard(deployment.endpoint_url + '/predict')}
422
+ className="p-1 hover:bg-slate-200 dark:hover:bg-slate-700 rounded"
423
+ >
424
+ <Copy size={12} className="text-slate-500" />
425
+ </button>
426
+ </div>
427
+ <code className="text-sm text-primary-600 dark:text-primary-400">
428
+ {deployment.endpoint_url}/predict
429
+ </code>
430
+ </div>
431
+
432
+ {/* API Token */}
433
+ <div className="bg-amber-50 dark:bg-amber-900/10 rounded-lg p-3 mb-4 border border-amber-200 dark:border-amber-800">
434
+ <div className="flex items-center justify-between mb-1">
435
+ <span className="text-xs font-medium text-amber-700 dark:text-amber-400 flex items-center gap-1">
436
+ <Shield size={12} />
437
+ API Token
438
+ </span>
439
+ <div className="flex gap-1">
440
+ <button
441
+ onClick={() => toggleTokenVisibility(deployment.id)}
442
+ className="p-1 hover:bg-amber-200 dark:hover:bg-amber-800 rounded"
443
+ >
444
+ {visibleTokens.has(deployment.id) ? <EyeOff size={12} /> : <Eye size={12} />}
445
+ </button>
446
+ <button
447
+ onClick={() => copyToClipboard(deployment.api_token)}
448
+ className="p-1 hover:bg-amber-200 dark:hover:bg-amber-800 rounded"
449
+ >
450
+ <Copy size={12} />
451
+ </button>
452
+ </div>
453
+ </div>
454
+ <code className="text-xs text-amber-800 dark:text-amber-300 font-mono">
455
+ {visibleTokens.has(deployment.id) ? deployment.api_token : maskToken(deployment.api_token)}
456
+ </code>
457
+ </div>
458
+
459
+ {/* TTL Countdown & Health Status */}
460
+ {(deployment.expires_at || deployment.status === 'running') && (
461
+ <div className="flex items-center gap-3 mb-4">
462
+ {deployment.expires_at && (
463
+ <div className="flex items-center gap-1.5 px-2 py-1 bg-purple-100 dark:bg-purple-900/20 rounded text-xs">
464
+ <Clock size={12} className="text-purple-600" />
465
+ <span className="font-medium text-purple-700 dark:text-purple-400">
466
+ TTL: {getTimeRemaining(deployment.expires_at)}
467
+ </span>
468
+ </div>
469
+ )}
470
+ {deployment.status === 'running' && (
471
+ <div className="flex items-center gap-1.5 px-2 py-1 bg-emerald-100 dark:bg-emerald-900/20 rounded text-xs">
472
+ <CheckCircle2 size={12} className="text-emerald-600" />
473
+ <span className="font-medium text-emerald-700 dark:text-emerald-400">Healthy</span>
474
+ </div>
475
+ )}
476
+ {deployment.status === 'error' && deployment.error_message && (
477
+ <div className="flex items-center gap-1.5 px-2 py-1 bg-rose-100 dark:bg-rose-900/20 rounded text-xs flex-1">
478
+ <AlertCircle size={12} className="text-rose-600" />
479
+ <span className="font-medium text-rose-700 dark:text-rose-400 truncate">{deployment.error_message}</span>
480
+ </div>
481
+ )}
482
+ </div>
483
+ )}
484
+
485
+ {/* Actions */}
486
+ <div className="flex gap-2">
487
+ {deployment.status === 'running' ? (
488
+ <Button
489
+ onClick={() => toggleDeployment(deployment.id, 'stop')}
490
+ variant="ghost"
491
+ className="flex-1 text-amber-600 hover:bg-amber-50"
492
+ >
493
+ <Square size={14} className="mr-1" />
494
+ Stop
495
+ </Button>
496
+ ) : deployment.status === 'stopped' ? (
497
+ <Button
498
+ onClick={() => toggleDeployment(deployment.id, 'start')}
499
+ variant="ghost"
500
+ className="flex-1 text-emerald-600 hover:bg-emerald-50"
501
+ >
502
+ <Play size={14} className="mr-1" />
503
+ Start
504
+ </Button>
505
+ ) : null}
506
+ <Button
507
+ onClick={() => fetchDeploymentLogs(deployment)}
508
+ variant="ghost"
509
+ className="flex-1"
510
+ >
511
+ <Terminal size={14} className="mr-1" />
512
+ Logs
513
+ </Button>
514
+ <Button
515
+ onClick={() => setDeleteConfirmId(deployment.id)}
516
+ variant="ghost"
517
+ className="text-rose-600 hover:bg-rose-50"
518
+ >
519
+ <Trash2 size={14} />
520
+ </Button>
521
+ </div>
522
+ </Card>
523
+ ))}
524
+ </div>
525
+ )}
526
+
527
+ {/* Create Deployment Modal */}
528
+ {showCreateModal && (
529
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
530
+ <Card className="max-w-lg w-full max-h-[90vh] overflow-y-auto">
531
+ <h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-6 flex items-center gap-2">
532
+ <Rocket className="text-orange-500" size={24} />
533
+ New Deployment
534
+ </h2>
535
+
536
+ <form onSubmit={createDeployment} className="space-y-4">
537
+ <div>
538
+ <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
539
+ Deployment Name
540
+ </label>
541
+ <input
542
+ type="text"
543
+ value={newDeployment.name}
544
+ onChange={(e) => setNewDeployment({ ...newDeployment, name: e.target.value })}
545
+ placeholder="e.g., My Model API"
546
+ className="w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-orange-500 bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
547
+ required
548
+ />
549
+ </div>
550
+
551
+ <div>
552
+ <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
553
+ Model {availableModels.length > 0 && <span className="text-slate-400">({availableModels.length} available)</span>}
554
+ </label>
555
+ {/* Searchable model selector */}
556
+ <div className="relative">
557
+ <input
558
+ type="text"
559
+ placeholder="Search models..."
560
+ value={modelSearch}
561
+ onChange={(e) => setModelSearch(e.target.value)}
562
+ onFocus={() => setShowModelDropdown(true)}
563
+ className="w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-orange-500 bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
564
+ />
565
+ {newDeployment.model_artifact_id && (
566
+ <div className="mt-1 px-2 py-1 bg-orange-100 dark:bg-orange-900/30 rounded text-sm text-orange-700 dark:text-orange-300 flex items-center justify-between">
567
+ <span>Selected: {availableModels.find(m => m.artifact_id === newDeployment.model_artifact_id)?.name || newDeployment.model_artifact_id}</span>
568
+ <button type="button" onClick={() => setNewDeployment({ ...newDeployment, model_artifact_id: '' })} className="text-orange-500 hover:text-orange-700">×</button>
569
+ </div>
570
+ )}
571
+ {showModelDropdown && (
572
+ <div className="absolute z-50 w-full mt-1 max-h-60 overflow-y-auto bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded-lg shadow-lg">
573
+ {availableModels
574
+ .filter(m =>
575
+ m.name?.toLowerCase().includes(modelSearch.toLowerCase()) ||
576
+ m.type?.toLowerCase().includes(modelSearch.toLowerCase()) ||
577
+ m.project?.toLowerCase().includes(modelSearch.toLowerCase())
578
+ )
579
+ .slice(0, 20)
580
+ .map((model) => (
581
+ <button
582
+ key={model.artifact_id}
583
+ type="button"
584
+ onClick={() => {
585
+ setNewDeployment({ ...newDeployment, model_artifact_id: model.artifact_id });
586
+ setModelSearch('');
587
+ setShowModelDropdown(false);
588
+ }}
589
+ className="w-full text-left px-4 py-2 hover:bg-slate-100 dark:hover:bg-slate-700 border-b border-slate-100 dark:border-slate-700 last:border-0"
590
+ >
591
+ <div className="font-medium text-slate-900 dark:text-white flex items-center gap-2">
592
+ {model.name}
593
+ {model.file_exists ? (
594
+ <span className="px-1 py-0.5 text-xs bg-emerald-100 text-emerald-700 rounded">Ready</span>
595
+ ) : model.has_file ? (
596
+ <span className="px-1 py-0.5 text-xs bg-amber-100 text-amber-700 rounded">Missing</span>
597
+ ) : (
598
+ <span className="px-1 py-0.5 text-xs bg-rose-100 text-rose-700 rounded">No File</span>
599
+ )}
600
+ </div>
601
+ <div className="text-xs text-slate-500 flex gap-2">
602
+ <span className="px-1 bg-slate-200 dark:bg-slate-600 rounded">{model.type}</span>
603
+ {model.project && <span>{model.project}</span>}
604
+ </div>
605
+ </button>
606
+ ))}
607
+ {availableModels.filter(m => m.name?.toLowerCase().includes(modelSearch.toLowerCase())).length === 0 && (
608
+ <div className="px-4 py-3 text-slate-500 text-center">No models found</div>
609
+ )}
610
+ </div>
611
+ )}
612
+ </div>
613
+ {availableModels.length === 0 && (
614
+ <p className="text-xs text-slate-500 mt-1">
615
+ No models available. Train a model first!
616
+ </p>
617
+ )}
618
+ </div>
619
+
620
+ <div>
621
+ <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
622
+ Port (optional)
623
+ </label>
624
+ <input
625
+ type="number"
626
+ value={newDeployment.port || ''}
627
+ onChange={(e) => setNewDeployment({ ...newDeployment, port: e.target.value ? parseInt(e.target.value) : null })}
628
+ placeholder="Auto-assigned if empty"
629
+ className="w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-orange-500 bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
630
+ />
631
+ </div>
632
+
633
+ <div>
634
+ <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
635
+ Auto-Destroy (TTL)
636
+ </label>
637
+ <select
638
+ value={newDeployment.config.ttl_seconds || ''}
639
+ onChange={(e) => setNewDeployment({
640
+ ...newDeployment,
641
+ config: {
642
+ ...newDeployment.config,
643
+ ttl_seconds: e.target.value ? parseInt(e.target.value) : null
644
+ }
645
+ })}
646
+ className="w-full px-4 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-orange-500 bg-white dark:bg-slate-800 text-slate-900 dark:text-white"
647
+ >
648
+ <option value="">Never (keep running)</option>
649
+ <option value="300">5 minutes</option>
650
+ <option value="900">15 minutes</option>
651
+ <option value="1800">30 minutes</option>
652
+ <option value="3600">1 hour</option>
653
+ <option value="7200">2 hours</option>
654
+ </select>
655
+ <p className="text-xs text-slate-500 mt-1">
656
+ Automatically stop deployment after selected time
657
+ </p>
658
+ </div>
659
+
660
+ <div className="pt-4 flex gap-3 justify-end">
661
+ <Button
662
+ type="button"
663
+ variant="ghost"
664
+ onClick={() => setShowCreateModal(false)}
665
+ >
666
+ Cancel
667
+ </Button>
668
+ <Button
669
+ type="submit"
670
+ className="bg-gradient-to-r from-orange-500 to-red-500"
671
+ >
672
+ <Rocket size={16} className="mr-2" />
673
+ Deploy
674
+ </Button>
675
+ </div>
676
+ </form>
677
+ </Card>
678
+ </div>
679
+ )}
680
+
681
+ {/* Logs Modal */}
682
+ {selectedDeployment && (
683
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
684
+ <Card className="max-w-2xl w-full max-h-[80vh] flex flex-col">
685
+ <div className="flex items-center justify-between mb-4">
686
+ <h2 className="text-xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
687
+ <Terminal className="text-orange-500" size={20} />
688
+ Logs: {selectedDeployment.name}
689
+ </h2>
690
+ <div className="flex items-center gap-2">
691
+ <Badge className={getStatusColor(selectedDeployment.status)}>
692
+ {selectedDeployment.status}
693
+ </Badge>
694
+ <button
695
+ onClick={() => { setSelectedDeployment(null); setDeploymentLogs([]); }}
696
+ className="p-1 hover:bg-slate-200 dark:hover:bg-slate-700 rounded"
697
+ >
698
+ <X size={20} />
699
+ </button>
700
+ </div>
701
+ </div>
702
+
703
+ <div className="flex-1 overflow-y-auto bg-slate-900 rounded-lg p-4 font-mono text-sm">
704
+ {logsLoading ? (
705
+ <div className="flex items-center justify-center py-8">
706
+ <Loader2 className="animate-spin text-slate-400" size={24} />
707
+ </div>
708
+ ) : deploymentLogs.length === 0 ? (
709
+ <div className="text-slate-500 text-center py-8">No logs available</div>
710
+ ) : (
711
+ deploymentLogs.map((log, index) => (
712
+ <div key={index} className="mb-1 flex gap-2">
713
+ <span className="text-slate-500 shrink-0">
714
+ {new Date(log.timestamp).toLocaleTimeString()}
715
+ </span>
716
+ <span className={`shrink-0 px-1 rounded text-xs font-bold ${log.level === 'ERROR' ? 'bg-rose-900 text-rose-400' :
717
+ log.level === 'WARN' ? 'bg-amber-900 text-amber-400' :
718
+ 'bg-emerald-900 text-emerald-400'
719
+ }`}>
720
+ {log.level}
721
+ </span>
722
+ <span className="text-slate-300">{log.message}</span>
723
+ </div>
724
+ ))
725
+ )}
726
+ </div>
727
+
728
+ <div className="mt-4 flex justify-between">
729
+ <Button
730
+ onClick={() => fetchDeploymentLogs(selectedDeployment)}
731
+ variant="ghost"
732
+ className="flex items-center gap-2"
733
+ >
734
+ <RefreshCw size={14} />
735
+ Refresh Logs
736
+ </Button>
737
+ <Button
738
+ onClick={() => { setSelectedDeployment(null); setDeploymentLogs([]); }}
739
+ variant="ghost"
740
+ >
741
+ Close
742
+ </Button>
743
+ </div>
744
+ </Card>
745
+ </div>
746
+ )}
747
+
748
+ {/* Delete Confirmation Modal */}
749
+ {deleteConfirmId && (
750
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
751
+ <Card className="max-w-sm w-full">
752
+ <h2 className="text-xl font-bold text-slate-900 dark:text-white mb-4 flex items-center gap-2">
753
+ <AlertCircle className="text-rose-500" size={24} />
754
+ Delete Deployment
755
+ </h2>
756
+ <p className="text-slate-600 dark:text-slate-400 mb-6">
757
+ Are you sure you want to delete this deployment? This action cannot be undone.
758
+ </p>
759
+ <div className="flex gap-3 justify-end">
760
+ <Button
761
+ variant="ghost"
762
+ onClick={() => setDeleteConfirmId(null)}
763
+ >
764
+ Cancel
765
+ </Button>
766
+ <Button
767
+ onClick={() => deleteDeployment(deleteConfirmId)}
768
+ className="bg-rose-500 hover:bg-rose-600 text-white"
769
+ >
770
+ <Trash2 size={14} className="mr-2" />
771
+ Delete
772
+ </Button>
773
+ </div>
774
+ </Card>
775
+ </div>
776
+ )}
777
+
778
+ {/* Footer Branding */}
779
+ <div className="text-center pt-8 pb-4 border-t border-slate-200 dark:border-slate-700">
780
+ <p className="text-xs text-slate-400 dark:text-slate-500">
781
+ Made with ❤️ by <span className="font-medium text-primary-500">UnicoLab</span>
782
+ </p>
783
+ </div>
784
+ </div>
785
+ );
786
+ }