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,456 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { fetchApi } from '../../utils/api';
3
+ import { Key, Plus, Trash2, Copy, Check, Shield, Calendar, AlertCircle } from 'lucide-react';
4
+ import { Card, CardHeader, CardTitle, CardContent } from '../../components/ui/Card';
5
+ import { Button } from '../../components/ui/Button';
6
+ import { Badge } from '../../components/ui/Badge';
7
+ import { format } from 'date-fns';
8
+ import { motion, AnimatePresence } from 'framer-motion';
9
+
10
+ export function TokenManagement() {
11
+ const [tokens, setTokens] = useState([]);
12
+ const [loading, setLoading] = useState(true);
13
+ const [showCreateModal, setShowCreateModal] = useState(false);
14
+ const [newToken, setNewToken] = useState(null);
15
+ const [error, setError] = useState(null);
16
+
17
+ useEffect(() => {
18
+ fetchTokens();
19
+ }, []);
20
+
21
+ const fetchTokens = async () => {
22
+ try {
23
+ // First check if we have any tokens (for initial token creation)
24
+ const res = await fetchApi('/api/execution/tokens');
25
+ if (res.status === 401) {
26
+ // No tokens exist yet
27
+ setTokens([]);
28
+ setLoading(false);
29
+ return;
30
+ }
31
+ const data = await res.json();
32
+ setTokens(data.tokens || []);
33
+ } catch (err) {
34
+ console.error('Failed to fetch tokens:', err);
35
+ setError('Failed to load tokens');
36
+ } finally {
37
+ setLoading(false);
38
+ }
39
+ };
40
+
41
+ const createInitialToken = async () => {
42
+ try {
43
+ const res = await fetchApi('/api/execution/tokens/init', {
44
+ method: 'POST'
45
+ });
46
+ const data = await res.json();
47
+ if (res.ok) {
48
+ setNewToken(data.token);
49
+ await fetchTokens();
50
+ } else {
51
+ setError(data.detail);
52
+ }
53
+ } catch (err) {
54
+ setError('Failed to create initial token');
55
+ }
56
+ };
57
+
58
+ if (loading) {
59
+ return (
60
+ <div className="flex items-center justify-center h-96">
61
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
62
+ </div>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <div className="space-y-6 max-w-6xl mx-auto">
68
+ {/* Header */}
69
+ <div>
70
+ <div className="flex items-center gap-3 mb-2">
71
+ <div className="p-2 bg-gradient-to-br from-amber-600 to-orange-800 rounded-lg">
72
+ <Key className="text-white" size={24} />
73
+ </div>
74
+ <h2 className="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">API Tokens</h2>
75
+ </div>
76
+ <p className="text-slate-500 dark:text-slate-400 mt-2">
77
+ Manage API tokens for authenticating CLI and SDK requests
78
+ </p>
79
+ </div>
80
+
81
+ {/* Error Message */}
82
+ {error && (
83
+ <div className="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-800 rounded-lg p-4 flex items-start gap-3">
84
+ <AlertCircle className="text-rose-600 shrink-0" size={20} />
85
+ <div>
86
+ <h4 className="font-medium text-rose-900 dark:text-rose-200">Error</h4>
87
+ <p className="text-sm text-rose-700 dark:text-rose-300 mt-1">{error}</p>
88
+ </div>
89
+ </div>
90
+ )}
91
+
92
+ {/* New Token Display */}
93
+ {newToken && <NewTokenDisplay token={newToken} onClose={() => setNewToken(null)} />}
94
+
95
+ {/* No Tokens State */}
96
+ {tokens.length === 0 && !newToken && (
97
+ <Card>
98
+ <CardContent className="py-16 text-center">
99
+ <div className="mx-auto w-20 h-20 bg-amber-100 dark:bg-amber-900/30 rounded-2xl flex items-center justify-center mb-6">
100
+ <Key className="text-amber-600" size={32} />
101
+ </div>
102
+ <h3 className="text-xl font-bold text-slate-900 dark:text-white mb-2">
103
+ No API Tokens Yet
104
+ </h3>
105
+ <p className="text-slate-500 dark:text-slate-400 max-w-md mx-auto mb-6">
106
+ Create your first admin token to start using the flowyml API
107
+ </p>
108
+ <Button onClick={createInitialToken} className="flex items-center gap-2 mx-auto">
109
+ <Plus size={18} />
110
+ Create Initial Token
111
+ </Button>
112
+ </CardContent>
113
+ </Card>
114
+ )}
115
+
116
+ {/* Tokens List */}
117
+ {tokens.length > 0 && (
118
+ <Card>
119
+ <CardHeader>
120
+ <div className="flex items-center justify-between">
121
+ <CardTitle>Active Tokens</CardTitle>
122
+ <Button onClick={() => setShowCreateModal(true)} variant="primary" className="flex items-center gap-2">
123
+ <Plus size={18} />
124
+ Create Token
125
+ </Button>
126
+ </div>
127
+ </CardHeader>
128
+ <CardContent>
129
+ <div className="space-y-3">
130
+ {tokens.map((token, idx) => (
131
+ <TokenItem key={idx} token={token} onRevoke={fetchTokens} />
132
+ ))}
133
+ </div>
134
+ </CardContent>
135
+ </Card>
136
+ )}
137
+
138
+ {/* Create Token Modal */}
139
+ {showCreateModal && (
140
+ <CreateTokenModal
141
+ onClose={() => setShowCreateModal(false)}
142
+ onCreate={(token) => {
143
+ setNewToken(token);
144
+ setShowCreateModal(false);
145
+ fetchTokens();
146
+ }}
147
+ />
148
+ )}
149
+ </div>
150
+ );
151
+ }
152
+
153
+ function TokenItem({ token, onRevoke }) {
154
+ const [showRevoke, setShowRevoke] = useState(false);
155
+
156
+ const handleRevoke = async () => {
157
+ try {
158
+ // This would need to be implemented in the backend
159
+ await fetch(`/ api / execution / tokens / ${token.name} `, {
160
+ method: 'DELETE'
161
+ });
162
+ onRevoke();
163
+ } catch (err) {
164
+ console.error('Failed to revoke token:', err);
165
+ }
166
+ };
167
+
168
+ const permissionColors = {
169
+ read: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
170
+ write: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300',
171
+ execute: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300',
172
+ admin: 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-300'
173
+ };
174
+
175
+ return (
176
+ <div className="p-4 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-primary-300 dark:hover:border-primary-700 transition-colors">
177
+ <div className="flex items-start justify-between">
178
+ <div className="flex-1">
179
+ <div className="flex items-center gap-3 mb-2">
180
+ <h4 className="font-semibold text-slate-900 dark:text-white">{token.name}</h4>
181
+ {token.project && (
182
+ <Badge variant="secondary" className="text-xs">
183
+ {token.project}
184
+ </Badge>
185
+ )}
186
+ </div>
187
+ <div className="flex flex-wrap gap-2 mb-3">
188
+ {token.permissions?.map(perm => (
189
+ <span
190
+ key={perm}
191
+ className={`px - 2 py - 0.5 rounded text - xs font - medium ${permissionColors[perm] || 'bg-slate-100 text-slate-700'} `}
192
+ >
193
+ {perm}
194
+ </span>
195
+ ))}
196
+ </div>
197
+ <div className="flex items-center gap-4 text-xs text-slate-500 dark:text-slate-400">
198
+ <span className="flex items-center gap-1">
199
+ <Calendar size={12} />
200
+ Created: {format(new Date(token.created_at), 'MMM d, yyyy')}
201
+ </span>
202
+ {token.last_used && (
203
+ <span>Last used: {format(new Date(token.last_used), 'MMM d, yyyy')}</span>
204
+ )}
205
+ </div>
206
+ </div>
207
+ <button
208
+ onClick={() => setShowRevoke(true)}
209
+ className="p-2 hover:bg-rose-100 dark:hover:bg-rose-900/30 text-slate-400 hover:text-rose-600 rounded-lg transition-colors"
210
+ >
211
+ <Trash2 size={16} />
212
+ </button>
213
+ </div>
214
+
215
+ {/* Revoke Confirmation */}
216
+ {showRevoke && (
217
+ <div className="mt-3 pt-3 border-t border-slate-200 dark:border-slate-700">
218
+ <p className="text-sm text-slate-600 dark:text-slate-400 mb-3">
219
+ Are you sure you want to revoke this token? This action cannot be undone.
220
+ </p>
221
+ <div className="flex gap-2">
222
+ <Button variant="danger" size="sm" onClick={handleRevoke}>
223
+ Yes, Revoke
224
+ </Button>
225
+ <Button variant="ghost" size="sm" onClick={() => setShowRevoke(false)}>
226
+ Cancel
227
+ </Button>
228
+ </div>
229
+ </div>
230
+ )}
231
+ </div>
232
+ );
233
+ }
234
+
235
+ function NewTokenDisplay({ token, onClose }) {
236
+ const [copied, setCopied] = useState(false);
237
+
238
+ const copyToken = () => {
239
+ navigator.clipboard.writeText(token);
240
+ setCopied(true);
241
+ setTimeout(() => setCopied(false), 2000);
242
+ };
243
+
244
+ return (
245
+ <motion.div
246
+ initial={{ opacity: 0, y: -20 }}
247
+ animate={{ opacity: 1, y: 0 }}
248
+ className="bg-gradient-to-br from-emerald-50 to-teal-50 dark:from-emerald-900/20 dark:to-teal-900/20 border-2 border-emerald-300 dark:border-emerald-700 rounded-lg p-6"
249
+ >
250
+ <div className="flex items-start justify-between mb-4">
251
+ <div className="flex items-center gap-3">
252
+ <div className="p-2 bg-emerald-500 rounded-lg">
253
+ <Shield className="text-white" size={20} />
254
+ </div>
255
+ <div>
256
+ <h3 className="font-bold text-emerald-900 dark:text-emerald-200">Token Created Successfully!</h3>
257
+ <p className="text-sm text-emerald-700 dark:text-emerald-300 mt-1">
258
+ Save this token now - it won't be shown again
259
+ </p>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ <div className="bg-white dark:bg-slate-800 rounded-lg p-4 border border-emerald-200 dark:border-emerald-800">
264
+ <div className="flex items-center gap-2 mb-2">
265
+ <code className="flex-1 font-mono text-sm text-slate-900 dark:text-white break-all">
266
+ {token}
267
+ </code>
268
+ <button
269
+ onClick={copyToken}
270
+ className="p-2 hover:bg-slate-100 dark:hover:bg-slate-700 rounded transition-colors shrink-0"
271
+ >
272
+ {copied ? (
273
+ <Check className="text-emerald-600" size={18} />
274
+ ) : (
275
+ <Copy className="text-slate-400" size={18} />
276
+ )}
277
+ </button>
278
+ </div>
279
+ </div>
280
+ <Button variant="ghost" onClick={onClose} className="mt-4 w-full">
281
+ I've saved my token
282
+ </Button>
283
+ </motion.div>
284
+ );
285
+ }
286
+
287
+ function CreateTokenModal({ onClose, onCreate }) {
288
+ const [name, setName] = useState('');
289
+ const [project, setProject] = useState('');
290
+ const [permissions, setPermissions] = useState(['read', 'write', 'execute']);
291
+ const [loading, setLoading] = useState(false);
292
+ const [error, setError] = useState(null);
293
+ const [projects, setProjects] = useState([]);
294
+ const [loadingProjects, setLoadingProjects] = useState(true);
295
+
296
+ useEffect(() => {
297
+ fetchProjects();
298
+ }, []);
299
+
300
+ const fetchProjects = async () => {
301
+ try {
302
+ const res = await fetchApi('/api/projects/');
303
+ const data = await res.json();
304
+ setProjects(data || []);
305
+ } catch (err) {
306
+ console.error('Failed to fetch projects:', err);
307
+ } finally {
308
+ setLoadingProjects(false);
309
+ }
310
+ };
311
+
312
+ const handleCreate = async () => {
313
+ if (!name) {
314
+ setError('Token name is required');
315
+ return;
316
+ }
317
+
318
+ setLoading(true);
319
+ setError(null);
320
+ try {
321
+ const res = await fetch('/api/execution/tokens', {
322
+ method: 'POST',
323
+ headers: { 'Content-Type': 'application/json' },
324
+ body: JSON.stringify({
325
+ name,
326
+ project: project || null,
327
+ permissions
328
+ })
329
+ });
330
+ const data = await res.json();
331
+ if (res.ok) {
332
+ onCreate(data.token);
333
+ } else {
334
+ setError(data.detail);
335
+ }
336
+ } catch (err) {
337
+ setError('Failed to create token');
338
+ } finally {
339
+ setLoading(false);
340
+ }
341
+ };
342
+
343
+ const togglePermission = (perm) => {
344
+ setPermissions(prev =>
345
+ prev.includes(perm)
346
+ ? prev.filter(p => p !== perm)
347
+ : [...prev, perm]
348
+ );
349
+ };
350
+
351
+ return (
352
+ <AnimatePresence>
353
+ <motion.div
354
+ initial={{ opacity: 0 }}
355
+ animate={{ opacity: 1 }}
356
+ exit={{ opacity: 0 }}
357
+ className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
358
+ onClick={onClose}
359
+ >
360
+ <motion.div
361
+ initial={{ scale: 0.9, opacity: 0 }}
362
+ animate={{ scale: 1, opacity: 1 }}
363
+ exit={{ scale: 0.9, opacity: 0 }}
364
+ onClick={(e) => e.stopPropagation()}
365
+ className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-lg w-full border border-slate-200 dark:border-slate-700"
366
+ >
367
+ <div className="p-6 border-b border-slate-100 dark:border-slate-700">
368
+ <h3 className="text-2xl font-bold text-slate-900 dark:text-white">Create New Token</h3>
369
+ <p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
370
+ Generate a new API token for authentication
371
+ </p>
372
+ </div>
373
+
374
+ <div className="p-6 space-y-4">
375
+ {error && (
376
+ <div className="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-800 rounded-lg p-3 text-sm text-rose-700 dark:text-rose-300">
377
+ {error}
378
+ </div>
379
+ )}
380
+
381
+ <div>
382
+ <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
383
+ Token Name *
384
+ </label>
385
+ <input
386
+ type="text"
387
+ value={name}
388
+ onChange={(e) => setName(e.target.value)}
389
+ placeholder="e.g., Production CLI Token"
390
+ className="w-full px-3 py-2 border border-slate-200 dark:border-slate-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 bg-white dark:bg-slate-900 text-slate-900 dark:text-white"
391
+ />
392
+ </div>
393
+
394
+ <div>
395
+ <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
396
+ Project Scope
397
+ </label>
398
+ <select
399
+ value={project}
400
+ onChange={(e) => setProject(e.target.value)}
401
+ disabled={loadingProjects}
402
+ className="w-full px-3 py-2 border border-slate-200 dark:border-slate-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 bg-white dark:bg-slate-900 text-slate-900 dark:text-white"
403
+ >
404
+ <option value="">All Projects</option>
405
+ {projects.map(proj => (
406
+ <option key={proj.name} value={proj.name}>
407
+ {proj.name}
408
+ </option>
409
+ ))}
410
+ </select>
411
+ <p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
412
+ Restrict this token to a specific project or allow access to all
413
+ </p>
414
+ </div>
415
+
416
+ <div>
417
+ <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
418
+ Permissions
419
+ </label>
420
+ <div className="space-y-2">
421
+ {['read', 'write', 'execute', 'admin'].map(perm => (
422
+ <label key={perm} className="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-900/50 rounded-lg cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700/50">
423
+ <input
424
+ type="checkbox"
425
+ checked={permissions.includes(perm)}
426
+ onChange={() => togglePermission(perm)}
427
+ className="w-4 h-4 text-primary-600 border-slate-300 rounded focus:ring-primary-500"
428
+ />
429
+ <div className="flex-1">
430
+ <span className="font-medium text-slate-900 dark:text-white capitalize">{perm}</span>
431
+ <p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
432
+ {perm === 'read' && 'View pipelines, runs, and artifacts'}
433
+ {perm === 'write' && 'Modify configurations and metadata'}
434
+ {perm === 'execute' && 'Run pipelines and workflows'}
435
+ {perm === 'admin' && 'Full access including token management'}
436
+ </p>
437
+ </div>
438
+ </label>
439
+ ))}
440
+ </div>
441
+ </div>
442
+ </div>
443
+
444
+ <div className="p-6 border-t border-slate-100 dark:border-slate-700 flex gap-3">
445
+ <Button variant="ghost" onClick={onClose} className="flex-1">
446
+ Cancel
447
+ </Button>
448
+ <Button onClick={handleCreate} disabled={loading} className="flex-1">
449
+ {loading ? 'Creating...' : 'Create Token'}
450
+ </Button>
451
+ </div>
452
+ </motion.div>
453
+ </motion.div>
454
+ </AnimatePresence>
455
+ );
456
+ }