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,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
|
+
}
|