flowyml 1.2.0__py3-none-any.whl → 1.4.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 +3 -0
- flowyml/assets/base.py +10 -0
- flowyml/assets/metrics.py +6 -0
- flowyml/cli/main.py +108 -2
- flowyml/cli/run.py +9 -2
- flowyml/core/execution_status.py +52 -0
- flowyml/core/hooks.py +106 -0
- flowyml/core/observability.py +210 -0
- flowyml/core/orchestrator.py +274 -0
- flowyml/core/pipeline.py +193 -231
- flowyml/core/project.py +34 -2
- flowyml/core/remote_orchestrator.py +109 -0
- flowyml/core/resources.py +34 -17
- flowyml/core/retry_policy.py +80 -0
- flowyml/core/scheduler.py +9 -9
- flowyml/core/scheduler_config.py +2 -3
- flowyml/core/step.py +18 -1
- flowyml/core/submission_result.py +53 -0
- flowyml/integrations/keras.py +95 -22
- flowyml/monitoring/alerts.py +2 -2
- flowyml/stacks/__init__.py +15 -0
- flowyml/stacks/aws.py +599 -0
- flowyml/stacks/azure.py +295 -0
- flowyml/stacks/bridge.py +9 -9
- flowyml/stacks/components.py +24 -2
- flowyml/stacks/gcp.py +158 -11
- flowyml/stacks/local.py +5 -0
- flowyml/stacks/plugins.py +2 -2
- flowyml/stacks/registry.py +21 -0
- flowyml/storage/artifacts.py +15 -5
- flowyml/storage/materializers/__init__.py +2 -0
- flowyml/storage/materializers/base.py +33 -0
- flowyml/storage/materializers/cloudpickle.py +74 -0
- flowyml/storage/metadata.py +3 -881
- flowyml/storage/remote.py +590 -0
- flowyml/storage/sql.py +911 -0
- flowyml/ui/backend/dependencies.py +28 -0
- flowyml/ui/backend/main.py +43 -80
- flowyml/ui/backend/routers/assets.py +483 -17
- flowyml/ui/backend/routers/client.py +46 -0
- flowyml/ui/backend/routers/execution.py +13 -2
- flowyml/ui/backend/routers/experiments.py +97 -14
- flowyml/ui/backend/routers/metrics.py +168 -0
- flowyml/ui/backend/routers/pipelines.py +77 -12
- flowyml/ui/backend/routers/projects.py +33 -7
- flowyml/ui/backend/routers/runs.py +221 -12
- flowyml/ui/backend/routers/schedules.py +5 -21
- flowyml/ui/backend/routers/stats.py +14 -0
- flowyml/ui/backend/routers/traces.py +37 -53
- flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
- flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/src/App.jsx +4 -1
- flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
- flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
- flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
- flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
- flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
- flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
- flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
- flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
- flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
- flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
- flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
- flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
- flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
- flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
- flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
- flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
- flowyml/ui/frontend/src/router/index.jsx +4 -0
- flowyml/ui/frontend/src/utils/date.js +10 -0
- flowyml/ui/frontend/src/utils/downloads.js +11 -0
- flowyml/utils/config.py +6 -0
- flowyml/utils/stack_config.py +45 -3
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/METADATA +44 -4
- flowyml-1.4.0.dist-info/RECORD +200 -0
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/licenses/LICENSE +1 -1
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
- flowyml-1.2.0.dist-info/RECORD +0 -159
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/WHEEL +0 -0
- {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,29 +1,39 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { fetchApi } from '../../utils/api';
|
|
3
|
-
import { Link } from 'react-router-dom';
|
|
4
|
-
import {
|
|
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 {
|
|
12
|
-
import {
|
|
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 [
|
|
18
|
-
const [
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
42
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
</
|
|
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="
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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="
|
|
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
|
-
</
|
|
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 {
|
|
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?.
|
|
189
|
-
|
|
190
|
-
key={perm}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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">
|