flowyml 1.2.0__py3-none-any.whl → 1.3.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 (91) hide show
  1. flowyml/__init__.py +3 -0
  2. flowyml/assets/base.py +10 -0
  3. flowyml/assets/metrics.py +6 -0
  4. flowyml/cli/main.py +108 -2
  5. flowyml/cli/run.py +9 -2
  6. flowyml/core/execution_status.py +52 -0
  7. flowyml/core/hooks.py +106 -0
  8. flowyml/core/observability.py +210 -0
  9. flowyml/core/orchestrator.py +274 -0
  10. flowyml/core/pipeline.py +193 -231
  11. flowyml/core/project.py +34 -2
  12. flowyml/core/remote_orchestrator.py +109 -0
  13. flowyml/core/resources.py +22 -5
  14. flowyml/core/retry_policy.py +80 -0
  15. flowyml/core/step.py +18 -1
  16. flowyml/core/submission_result.py +53 -0
  17. flowyml/integrations/keras.py +95 -22
  18. flowyml/monitoring/alerts.py +2 -2
  19. flowyml/stacks/__init__.py +15 -0
  20. flowyml/stacks/aws.py +599 -0
  21. flowyml/stacks/azure.py +295 -0
  22. flowyml/stacks/components.py +24 -2
  23. flowyml/stacks/gcp.py +158 -11
  24. flowyml/stacks/local.py +5 -0
  25. flowyml/storage/artifacts.py +15 -5
  26. flowyml/storage/materializers/__init__.py +2 -0
  27. flowyml/storage/materializers/cloudpickle.py +74 -0
  28. flowyml/storage/metadata.py +166 -5
  29. flowyml/ui/backend/main.py +41 -1
  30. flowyml/ui/backend/routers/assets.py +356 -15
  31. flowyml/ui/backend/routers/client.py +46 -0
  32. flowyml/ui/backend/routers/execution.py +13 -2
  33. flowyml/ui/backend/routers/experiments.py +48 -12
  34. flowyml/ui/backend/routers/metrics.py +213 -0
  35. flowyml/ui/backend/routers/pipelines.py +63 -7
  36. flowyml/ui/backend/routers/projects.py +33 -7
  37. flowyml/ui/backend/routers/runs.py +150 -8
  38. flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
  39. flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
  40. flowyml/ui/frontend/dist/index.html +2 -2
  41. flowyml/ui/frontend/src/App.jsx +4 -1
  42. flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
  43. flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
  44. flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
  45. flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
  46. flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
  47. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
  48. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
  49. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
  50. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
  51. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
  52. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
  53. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
  54. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
  55. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
  56. flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
  57. flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
  58. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
  59. flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
  60. flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
  61. flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
  62. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
  63. flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
  64. flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
  65. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
  66. flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
  67. flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
  68. flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
  69. flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
  70. flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
  71. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
  72. flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
  73. flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
  74. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
  75. flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
  76. flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
  77. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
  78. flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
  79. flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
  80. flowyml/ui/frontend/src/router/index.jsx +4 -0
  81. flowyml/ui/frontend/src/utils/date.js +10 -0
  82. flowyml/ui/frontend/src/utils/downloads.js +11 -0
  83. flowyml/utils/config.py +6 -0
  84. flowyml/utils/stack_config.py +45 -3
  85. {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/METADATA +42 -4
  86. {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/RECORD +89 -52
  87. {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/licenses/LICENSE +1 -1
  88. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
  89. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
  90. {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/WHEEL +0 -0
  91. {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,29 +1,39 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
  import { fetchApi } from '../../utils/api';
3
- import { Link } from 'react-router-dom';
4
- import { Play, Clock, Calendar, TrendingUp, Activity, ArrowRight, Search, CheckCircle, XCircle, Loader, FolderPlus } from 'lucide-react';
3
+ import { Link, useSearchParams } from 'react-router-dom';
4
+ import { PlayCircle, Clock, CheckCircle, XCircle, Activity, ArrowRight, Calendar, Filter, RefreshCw, Layout } from 'lucide-react';
5
5
  import { Card } from '../../components/ui/Card';
6
6
  import { Badge } from '../../components/ui/Badge';
7
+ import { Button } from '../../components/ui/Button';
7
8
  import { format } from 'date-fns';
8
- import { motion } from 'framer-motion';
9
9
  import { DataView } from '../../components/ui/DataView';
10
+ import { StatusBadge } from '../../components/ui/ExecutionStatus';
10
11
  import { useProject } from '../../contexts/ProjectContext';
11
- import { ExecutionStatus, StatusBadge } from '../../components/ui/ExecutionStatus';
12
- import { EmptyState } from '../../components/ui/EmptyState';
12
+ import { NavigationTree } from '../../components/NavigationTree';
13
+ import { RunDetailsPanel } from '../../components/RunDetailsPanel';
13
14
 
14
15
  export function Runs() {
15
16
  const [runs, setRuns] = useState([]);
16
17
  const [loading, setLoading] = useState(true);
17
- const [filter, setFilter] = useState('all'); // all, completed, failed, running
18
- const [selectedRunIds, setSelectedRunIds] = useState([]);
18
+ const [selectedRun, setSelectedRun] = useState(null);
19
+ const [searchParams] = useSearchParams();
19
20
  const { selectedProject } = useProject();
20
21
 
22
+ // Filter states
23
+ const [statusFilter, setStatusFilter] = useState('all');
24
+ const pipelineFilter = searchParams.get('pipeline');
25
+
21
26
  const fetchRuns = async () => {
22
27
  setLoading(true);
23
28
  try {
24
- const url = selectedProject
25
- ? `/api/runs?project=${encodeURIComponent(selectedProject)}`
26
- : '/api/runs';
29
+ let url = '/api/runs/?limit=100';
30
+ if (selectedProject) {
31
+ url += `&project=${encodeURIComponent(selectedProject)}`;
32
+ }
33
+ if (pipelineFilter) {
34
+ url += `&pipeline=${encodeURIComponent(pipelineFilter)}`;
35
+ }
36
+
27
37
  const res = await fetchApi(url);
28
38
  const data = await res.json();
29
39
  setRuns(data.runs || []);
@@ -36,435 +46,83 @@ export function Runs() {
36
46
 
37
47
  useEffect(() => {
38
48
  fetchRuns();
39
- }, [selectedProject]);
49
+ }, [selectedProject, pipelineFilter]);
40
50
 
41
- const filteredRuns = runs.filter(run => {
42
- if (filter === 'all') return true;
43
- return run.status === filter;
44
- });
45
-
46
- const stats = {
47
- total: runs.length,
48
- completed: runs.filter(r => r.status === 'completed').length,
49
- failed: runs.filter(r => r.status === 'failed').length,
50
- running: runs.filter(r => r.status === 'running').length,
51
+ const handleRunSelect = (run) => {
52
+ setSelectedRun(run);
51
53
  };
52
54
 
53
- const columns = [
54
- {
55
- header: (
56
- <input
57
- type="checkbox"
58
- checked={selectedRunIds.length === filteredRuns.length && filteredRuns.length > 0}
59
- onChange={(e) => {
60
- if (e.target.checked) {
61
- setSelectedRunIds(filteredRuns.map(r => r.run_id));
62
- } else {
63
- setSelectedRunIds([]);
64
- }
65
- }}
66
- className="w-4 h-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500"
67
- />
68
- ),
69
- key: 'select',
70
- render: (run) => (
71
- <input
72
- type="checkbox"
73
- checked={selectedRunIds.includes(run.run_id)}
74
- onChange={(e) => {
75
- if (e.target.checked) {
76
- setSelectedRunIds([...selectedRunIds, run.run_id]);
77
- } else {
78
- setSelectedRunIds(selectedRunIds.filter(id => id !== run.run_id));
79
- }
80
- }}
81
- onClick={(e) => e.stopPropagation()}
82
- className="w-4 h-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500"
83
- />
84
- )
85
- },
86
- {
87
- header: 'Status',
88
- key: 'status',
89
- sortable: true,
90
- render: (run) => <StatusBadge status={run.status} />
91
- },
92
- {
93
- header: 'Pipeline',
94
- key: 'pipeline_name',
95
- sortable: true,
96
- render: (run) => (
97
- <span className="font-medium text-slate-900 dark:text-white">{run.pipeline_name}</span>
98
- )
99
- },
100
- {
101
- header: 'Project',
102
- key: 'project',
103
- sortable: true,
104
- render: (run) => (
105
- <span className="text-sm text-slate-600 dark:text-slate-400">
106
- {run.project || '-'}
107
- </span>
108
- )
109
- },
110
- {
111
- header: 'Run ID',
112
- key: 'run_id',
113
- render: (run) => (
114
- <span className="font-mono text-xs bg-slate-100 dark:bg-slate-700 px-2 py-1 rounded text-slate-600 dark:text-slate-300">
115
- {run.run_id.substring(0, 8)}...
116
- </span>
117
- )
118
- },
119
- {
120
- header: 'Start Time',
121
- key: 'start_time',
122
- sortable: true,
123
- render: (run) => (
124
- <div className="flex items-center gap-2 text-slate-500">
125
- <Calendar size={14} />
126
- {run.start_time ? format(new Date(run.start_time), 'MMM d, HH:mm:ss') : '-'}
127
- </div>
128
- )
129
- },
130
- {
131
- header: 'Duration',
132
- key: 'duration',
133
- sortable: true,
134
- render: (run) => (
135
- <div className="flex items-center gap-2 text-slate-500">
136
- <Clock size={14} />
137
- {run.duration ? `${run.duration.toFixed(2)}s` : '-'}
138
- </div>
139
- )
140
- },
141
- {
142
- header: 'Actions',
143
- key: 'actions',
144
- render: (run) => (
145
- <Link
146
- to={`/runs/${run.run_id}`}
147
- className="text-primary-600 hover:text-primary-700 font-medium text-sm flex items-center gap-1"
148
- >
149
- Details <ArrowRight size={14} />
150
- </Link>
151
- )
152
- }
153
- ];
154
-
155
- const renderGrid = (run) => {
156
- const statusConfig = {
157
- completed: {
158
- icon: <CheckCircle size={20} />,
159
- color: 'text-emerald-500',
160
- bg: 'bg-emerald-50',
161
- border: 'border-emerald-200',
162
- badge: 'success'
163
- },
164
- failed: {
165
- icon: <XCircle size={20} />,
166
- color: 'text-rose-500',
167
- bg: 'bg-rose-50',
168
- border: 'border-rose-200',
169
- badge: 'danger'
170
- },
171
- running: {
172
- icon: <Loader size={20} className="animate-spin" />,
173
- color: 'text-amber-500',
174
- bg: 'bg-amber-50',
175
- border: 'border-amber-200',
176
- badge: 'warning'
177
- }
178
- };
179
-
180
- const config = statusConfig[run.status] || statusConfig.completed;
181
-
182
- return (
183
- <Link to={`/runs/${run.run_id}`}>
184
- <Card className={`group hover:shadow-lg transition-all duration-200 border-l-4 ${config.border} hover:border-l-primary-400 h-full`}>
185
- <div className="flex items-center justify-between mb-4">
186
- <div className="flex items-center gap-3">
187
- <div className={`p-2 rounded-lg ${config.bg} ${config.color}`}>
188
- {config.icon}
189
- </div>
190
- <div>
191
- <h3 className="font-bold text-slate-900 dark:text-white truncate max-w-[150px]" title={run.pipeline_name}>
192
- {run.pipeline_name}
193
- </h3>
194
- <div className="text-xs text-slate-500 font-mono">
195
- {run.run_id.substring(0, 8)}
196
- </div>
197
- </div>
198
- </div>
199
- <Badge variant={config.badge} className="text-xs uppercase tracking-wide">
200
- {run.status}
201
- </Badge>
55
+ return (
56
+ <div className="h-screen flex flex-col overflow-hidden bg-slate-50 dark:bg-slate-900">
57
+ {/* Header */}
58
+ <div className="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-6 py-4 shrink-0">
59
+ <div className="flex items-center justify-between max-w-[1800px] mx-auto">
60
+ <div>
61
+ <h1 className="text-xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
62
+ <PlayCircle className="text-blue-500" />
63
+ Pipeline Runs
64
+ </h1>
65
+ <p className="text-sm text-slate-600 dark:text-slate-400">
66
+ Monitor and track all your pipeline executions
67
+ </p>
202
68
  </div>
203
-
204
- <div className="space-y-2 text-sm text-slate-500 dark:text-slate-400">
205
- <div className="flex items-center justify-between">
206
- <span className="flex items-center gap-2"><Calendar size={14} /> Started</span>
207
- <span>{run.start_time ? format(new Date(run.start_time), 'MMM d, HH:mm') : '-'}</span>
208
- </div>
209
- <div className="flex items-center justify-between">
210
- <span className="flex items-center gap-2"><Clock size={14} /> Duration</span>
211
- <span>{run.duration ? `${run.duration.toFixed(2)}s` : '-'}</span>
212
- </div>
69
+ <div className="flex items-center gap-3">
70
+ <Button variant="outline" size="sm" onClick={fetchRuns} disabled={loading}>
71
+ <RefreshCw size={16} className={`mr-2 ${loading ? 'animate-spin' : ''}`} />
72
+ Refresh
73
+ </Button>
213
74
  </div>
75
+ </div>
76
+ </div>
214
77
 
215
- {/* Steps Progress */}
216
- {run.steps && (
217
- <div className="mt-4 pt-4 border-t border-slate-100 dark:border-slate-700">
218
- <div className="flex justify-between text-xs mb-1">
219
- <span className="text-slate-500">Progress</span>
220
- <span className="font-medium text-slate-900 dark:text-white">
221
- {Object.values(run.steps).filter(s => s.success).length} / {Object.keys(run.steps).length}
222
- </span>
78
+ {/* Main Content */}
79
+ <div className="flex-1 overflow-hidden">
80
+ <div className="h-full max-w-[1800px] mx-auto px-6 py-6">
81
+ <div className="h-full flex gap-6">
82
+ {/* Left Sidebar - Navigation */}
83
+ <div className="w-[320px] shrink-0 flex flex-col bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden shadow-sm">
84
+ <div className="p-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 flex justify-between items-center">
85
+ <h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Explorer</h3>
86
+ {pipelineFilter && (
87
+ <Badge variant="secondary" className="text-[10px]">
88
+ Filtered: {pipelineFilter}
89
+ </Badge>
90
+ )}
223
91
  </div>
224
- <div className="w-full h-1.5 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden">
225
- <div
226
- className={`h-full ${config.color.replace('text-', 'bg-')} transition-all duration-300`}
227
- style={{
228
- width: `${(Object.values(run.steps).filter(s => s.success).length / Object.keys(run.steps).length) * 100}%`
229
- }}
92
+ <div className="flex-1 min-h-0">
93
+ <NavigationTree
94
+ mode="runs"
95
+ projectId={selectedProject}
96
+ onSelect={handleRunSelect}
97
+ selectedId={selectedRun?.run_id}
230
98
  />
231
99
  </div>
232
100
  </div>
233
- )}
234
- </Card>
235
- </Link>
236
- );
237
- };
238
-
239
- if (loading) {
240
- return (
241
- <div className="flex items-center justify-center h-96">
242
- <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
243
- </div>
244
- );
245
- }
246
-
247
- return (
248
- <div className="p-6 max-w-7xl mx-auto space-y-8">
249
- {/* Stats Cards */}
250
- <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
251
- <StatsCard
252
- label="Total Runs"
253
- value={stats.total}
254
- icon={<Activity size={20} />}
255
- color="slate"
256
- active={filter === 'all'}
257
- onClick={() => setFilter('all')}
258
- />
259
- <StatsCard
260
- label="Completed"
261
- value={stats.completed}
262
- icon={<CheckCircle size={20} />}
263
- color="emerald"
264
- active={filter === 'completed'}
265
- onClick={() => setFilter('completed')}
266
- />
267
- <StatsCard
268
- label="Failed"
269
- value={stats.failed}
270
- icon={<XCircle size={20} />}
271
- color="rose"
272
- active={filter === 'failed'}
273
- onClick={() => setFilter('failed')}
274
- />
275
- <StatsCard
276
- label="Running"
277
- value={stats.running}
278
- icon={<Loader size={20} />}
279
- color="amber"
280
- active={filter === 'running'}
281
- onClick={() => setFilter('running')}
282
- />
283
- </div>
284
101
 
285
- <DataView
286
- title="Pipeline Runs"
287
- subtitle="Monitor and track all your pipeline executions"
288
- items={filteredRuns}
289
- loading={loading}
290
- columns={columns}
291
- renderGrid={renderGrid}
292
- initialView="table" // Default to table for runs as it's usually more useful
293
- actions={
294
- <div className="flex items-center gap-2">
295
- {/* Add to Project Action */}
296
- <ProjectSelector
297
- selectedRuns={selectedRunIds}
298
- onComplete={() => {
299
- // Call fetchRuns from parent scope
300
- fetchRuns();
301
- setSelectedRunIds([]);
302
- }}
303
- />
304
- </div>
305
- }
306
- emptyState={
307
- <EmptyState
308
- icon={Activity}
309
- title="No runs found"
310
- description={filter === 'all'
311
- ? 'Run a pipeline to see it here'
312
- : `No ${filter} runs found. Try a different filter.`
313
- }
314
- />
315
- }
316
- />
317
- </div>
318
- );
319
- }
320
-
321
- function ProjectSelector({ selectedRuns, onComplete }) {
322
- const [isOpen, setIsOpen] = useState(false);
323
- const [projects, setProjects] = useState([]);
324
- const [updating, setUpdating] = useState(false);
325
-
326
- useEffect(() => {
327
- if (isOpen) {
328
- fetch('/api/projects/')
329
- .then(res => res.json())
330
- .then(data => setProjects(data))
331
- .catch(err => console.error('Failed to load projects:', err));
332
- }
333
- }, [isOpen]);
334
-
335
- const handleSelectProject = async (projectName) => {
336
- setUpdating(true);
337
- try {
338
- // Update all selected runs
339
- const updates = selectedRuns.map(runId =>
340
- fetch(`/api/runs/${runId}/project`, {
341
- method: 'PUT',
342
- headers: { 'Content-Type': 'application/json' },
343
- body: JSON.stringify({ project_name: projectName })
344
- })
345
- );
346
-
347
- await Promise.all(updates);
348
-
349
- // Show success notification
350
- showNotification('success', `Added ${selectedRuns.length} run(s) to project ${projectName}`);
351
-
352
- setIsOpen(false);
353
- if (onComplete) onComplete();
354
- } catch (error) {
355
- console.error('Failed to update projects:', error);
356
- showNotification('error', 'Failed to update project attribution');
357
- } finally {
358
- setUpdating(false);
359
- }
360
- };
361
-
362
- const showNotification = (type, message) => {
363
- // Simple toast notification
364
- const toast = document.createElement('div');
365
- toast.className = `fixed top-4 right-4 px-4 py-3 rounded-lg shadow-lg z-50 ${type === 'success' ? 'bg-green-500' : 'bg-red-500'
366
- } text-white animate-in slide-in-from-right`;
367
- toast.textContent = message;
368
- document.body.appendChild(toast);
369
- setTimeout(() => {
370
- toast.classList.add('animate-out', 'fade-out');
371
- setTimeout(() => document.body.removeChild(toast), 300);
372
- }, 3000);
373
- };
374
-
375
- return (
376
- <div className="relative">
377
- <button
378
- onClick={() => setIsOpen(!isOpen)}
379
- disabled={updating || selectedRuns.length === 0}
380
- className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-slate-600 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
381
- >
382
- <FolderPlus size={16} />
383
- {updating ? 'Updating...' : `Add to Project (${selectedRuns.length})`}
384
- </button>
385
-
386
- {isOpen && (
387
- <>
388
- <div
389
- className="fixed inset-0 z-10"
390
- onClick={() => setIsOpen(false)}
391
- />
392
- <div className="absolute right-0 mt-2 w-56 bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 z-20 overflow-hidden animate-in fade-in zoom-in-95 duration-100">
393
- <div className="p-2 border-b border-slate-100 dark:border-slate-700">
394
- <span className="text-xs font-semibold text-slate-500 px-2">Select Project</span>
395
- </div>
396
- <div className="max-h-64 overflow-y-auto p-1">
397
- {projects.length > 0 ? (
398
- projects.map(p => (
399
- <button
400
- key={p.name}
401
- onClick={() => handleSelectProject(p.name)}
402
- disabled={updating}
403
- className="w-full text-left px-3 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700 rounded-lg transition-colors disabled:opacity-50"
404
- >
405
- {p.name}
406
- </button>
407
- ))
102
+ {/* Right Content - Details Panel or Empty State */}
103
+ <div className="flex-1 min-w-0 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden shadow-sm">
104
+ {selectedRun ? (
105
+ <RunDetailsPanel
106
+ run={selectedRun}
107
+ onClose={() => setSelectedRun(null)}
108
+ />
408
109
  ) : (
409
- <div className="px-3 py-2 text-sm text-slate-400 italic">No projects found</div>
110
+ <div className="h-full flex flex-col items-center justify-center text-center p-8 bg-slate-50/50 dark:bg-slate-900/50">
111
+ <div className="w-20 h-20 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center mb-6 animate-pulse">
112
+ <PlayCircle size={40} className="text-blue-500" />
113
+ </div>
114
+ <h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">
115
+ Select a Run
116
+ </h2>
117
+ <p className="text-slate-500 max-w-md">
118
+ Choose a run from the sidebar to view execution details, logs, and artifacts.
119
+ </p>
120
+ </div>
410
121
  )}
411
122
  </div>
412
123
  </div>
413
- </>
414
- )}
415
- </div>
416
- );
417
- }
418
-
419
- function StatsCard({ label, value, icon, color, active, onClick }) {
420
- const colorClasses = {
421
- slate: {
422
- bg: "bg-slate-50",
423
- text: "text-slate-600",
424
- border: "border-slate-200",
425
- activeBg: "bg-slate-100",
426
- activeBorder: "border-slate-300"
427
- },
428
- emerald: {
429
- bg: "bg-emerald-50",
430
- text: "text-emerald-600",
431
- border: "border-emerald-200",
432
- activeBg: "bg-emerald-100",
433
- activeBorder: "border-emerald-300"
434
- },
435
- rose: {
436
- bg: "bg-rose-50",
437
- text: "text-rose-600",
438
- border: "border-rose-200",
439
- activeBg: "bg-rose-100",
440
- activeBorder: "border-rose-300"
441
- },
442
- amber: {
443
- bg: "bg-amber-50",
444
- text: "text-amber-600",
445
- border: "border-amber-200",
446
- activeBg: "bg-amber-100",
447
- activeBorder: "border-amber-300"
448
- }
449
- };
450
-
451
- const colors = colorClasses[color];
452
-
453
- return (
454
- <Card
455
- className={`cursor-pointer transition-all duration-200 hover:shadow-md border-2 ${active ? colors.activeBorder : 'border-transparent'
456
- }`}
457
- onClick={onClick}
458
- >
459
- <div className="flex items-center justify-between">
460
- <div>
461
- <p className="text-sm text-slate-500 font-medium mb-1">{label}</p>
462
- <p className="text-3xl font-bold text-slate-900 dark:text-white">{value}</p>
463
- </div>
464
- <div className={`p-3 rounded-xl ${active ? colors.activeBg : colors.bg} ${colors.text}`}>
465
- {icon}
466
124
  </div>
467
125
  </div>
468
- </Card>
126
+ </div>
469
127
  );
470
128
  }
@@ -77,6 +77,7 @@ export function Settings() {
77
77
  };
78
78
 
79
79
  const maskToken = (token) => {
80
+ if (!token || typeof token !== 'string') return '••••••••••••••••••••••••••••••••••••••••••••••••';
80
81
  return `${token.substring(0, 8)}${'•'.repeat(32)}${token.substring(token.length - 8)}`;
81
82
  };
82
83
 
@@ -1,6 +1,19 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { fetchApi } from '../../utils/api';
3
- import { Key, Plus, Trash2, Copy, Check, Shield, Calendar, AlertCircle } from 'lucide-react';
3
+ import {
4
+ Key,
5
+ Plus,
6
+ Trash2,
7
+ Copy,
8
+ Check,
9
+ Shield,
10
+ Calendar,
11
+ AlertCircle,
12
+ Eye,
13
+ PenTool,
14
+ Zap,
15
+ ShieldCheck
16
+ } from 'lucide-react';
4
17
  import { Card, CardHeader, CardTitle, CardContent } from '../../components/ui/Card';
5
18
  import { Button } from '../../components/ui/Button';
6
19
  import { Badge } from '../../components/ui/Badge';
@@ -150,6 +163,47 @@ export function TokenManagement() {
150
163
  );
151
164
  }
152
165
 
166
+ const PERMISSION_STYLES = {
167
+ read: {
168
+ label: 'Read',
169
+ icon: Eye,
170
+ className: 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-200 dark:border-blue-800'
171
+ },
172
+ write: {
173
+ label: 'Write',
174
+ icon: PenTool,
175
+ className: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-200 dark:border-emerald-800'
176
+ },
177
+ execute: {
178
+ label: 'Execute',
179
+ icon: Zap,
180
+ className: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-200 dark:border-purple-800'
181
+ },
182
+ admin: {
183
+ label: 'Admin',
184
+ icon: ShieldCheck,
185
+ className: 'bg-rose-50 text-rose-700 border-rose-200 dark:bg-rose-900/30 dark:text-rose-200 dark:border-rose-800'
186
+ }
187
+ };
188
+
189
+ function PermissionChip({ perm }) {
190
+ const config = PERMISSION_STYLES[perm] || {
191
+ label: perm,
192
+ icon: Shield,
193
+ className: 'bg-slate-100 text-slate-700 border-slate-200 dark:bg-slate-800/60 dark:text-slate-100 dark:border-slate-700'
194
+ };
195
+ const Icon = config.icon;
196
+
197
+ return (
198
+ <span
199
+ className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold border ${config.className}`}
200
+ >
201
+ <Icon size={12} />
202
+ {config.label}
203
+ </span>
204
+ );
205
+ }
206
+
153
207
  function TokenItem({ token, onRevoke }) {
154
208
  const [showRevoke, setShowRevoke] = useState(false);
155
209
 
@@ -165,13 +219,6 @@ function TokenItem({ token, onRevoke }) {
165
219
  }
166
220
  };
167
221
 
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
222
  return (
176
223
  <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
224
  <div className="flex items-start justify-between">
@@ -185,14 +232,13 @@ function TokenItem({ token, onRevoke }) {
185
232
  )}
186
233
  </div>
187
234
  <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
- ))}
235
+ {token.permissions?.length ? (
236
+ token.permissions.map(perm => (
237
+ <PermissionChip key={perm} perm={perm} />
238
+ ))
239
+ ) : (
240
+ <span className="text-xs text-slate-400">No permissions assigned</span>
241
+ )}
196
242
  </div>
197
243
  <div className="flex items-center gap-4 text-xs text-slate-500 dark:text-slate-400">
198
244
  <span className="flex items-center gap-1">