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.
- 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 +22 -5
- flowyml/core/retry_policy.py +80 -0
- 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/components.py +24 -2
- flowyml/stacks/gcp.py +158 -11
- flowyml/stacks/local.py +5 -0
- flowyml/storage/artifacts.py +15 -5
- flowyml/storage/materializers/__init__.py +2 -0
- flowyml/storage/materializers/cloudpickle.py +74 -0
- flowyml/storage/metadata.py +166 -5
- flowyml/ui/backend/main.py +41 -1
- flowyml/ui/backend/routers/assets.py +356 -15
- flowyml/ui/backend/routers/client.py +46 -0
- flowyml/ui/backend/routers/execution.py +13 -2
- flowyml/ui/backend/routers/experiments.py +48 -12
- flowyml/ui/backend/routers/metrics.py +213 -0
- flowyml/ui/backend/routers/pipelines.py +63 -7
- flowyml/ui/backend/routers/projects.py +33 -7
- flowyml/ui/backend/routers/runs.py +150 -8
- 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.3.0.dist-info}/METADATA +42 -4
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/RECORD +89 -52
- {flowyml-1.2.0.dist-info → flowyml-1.3.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 → flowyml-1.3.0.dist-info}/WHEEL +0 -0
- {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { fetchApi } from '../utils/api';
|
|
3
|
+
import {
|
|
4
|
+
Layers,
|
|
5
|
+
Calendar,
|
|
6
|
+
Activity,
|
|
7
|
+
Clock,
|
|
8
|
+
CheckCircle,
|
|
9
|
+
XCircle,
|
|
10
|
+
PlayCircle,
|
|
11
|
+
ArrowRight,
|
|
12
|
+
MoreHorizontal,
|
|
13
|
+
Download,
|
|
14
|
+
Trash2,
|
|
15
|
+
X,
|
|
16
|
+
TrendingUp,
|
|
17
|
+
Zap
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
import { Card } from './ui/Card';
|
|
20
|
+
import { Badge } from './ui/Badge';
|
|
21
|
+
import { Button } from './ui/Button';
|
|
22
|
+
import { format } from 'date-fns';
|
|
23
|
+
import { Link } from 'react-router-dom';
|
|
24
|
+
import { motion } from 'framer-motion';
|
|
25
|
+
import { StatusBadge } from './ui/ExecutionStatus';
|
|
26
|
+
import { ProjectSelector } from './ProjectSelector';
|
|
27
|
+
import { useToast } from '../contexts/ToastContext';
|
|
28
|
+
|
|
29
|
+
export function PipelineDetailsPanel({ pipeline, onClose, onProjectUpdate }) {
|
|
30
|
+
const [runs, setRuns] = useState([]);
|
|
31
|
+
const [loading, setLoading] = useState(false);
|
|
32
|
+
const [currentProject, setCurrentProject] = useState(pipeline?.project);
|
|
33
|
+
const toast = useToast();
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (pipeline) {
|
|
37
|
+
fetchRuns();
|
|
38
|
+
setCurrentProject(pipeline.project);
|
|
39
|
+
}
|
|
40
|
+
}, [pipeline]);
|
|
41
|
+
|
|
42
|
+
const fetchRuns = async () => {
|
|
43
|
+
setLoading(true);
|
|
44
|
+
try {
|
|
45
|
+
const url = `/api/runs?pipeline=${encodeURIComponent(pipeline.name)}&limit=50`;
|
|
46
|
+
const res = await fetchApi(url);
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
setRuns(data.runs || []);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Failed to fetch runs:', error);
|
|
51
|
+
toast.error(`Failed to load pipeline runs: ${error.message}`);
|
|
52
|
+
} finally {
|
|
53
|
+
setLoading(false);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleProjectUpdate = async (newProject) => {
|
|
58
|
+
try {
|
|
59
|
+
await fetchApi(`/api/pipelines/${pipeline.name}/project`, {
|
|
60
|
+
method: 'PUT',
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({ project_name: newProject })
|
|
65
|
+
});
|
|
66
|
+
setCurrentProject(newProject);
|
|
67
|
+
toast.success(`Pipeline assigned to project: ${newProject}`);
|
|
68
|
+
// Trigger parent component refresh to update the pipelines list
|
|
69
|
+
if (onProjectUpdate) {
|
|
70
|
+
onProjectUpdate();
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('Failed to update project:', error);
|
|
74
|
+
toast.error(`Failed to update project: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (!pipeline) return null;
|
|
79
|
+
|
|
80
|
+
const stats = {
|
|
81
|
+
total: runs.length,
|
|
82
|
+
success: runs.filter(r => r.status === 'completed').length,
|
|
83
|
+
failed: runs.filter(r => r.status === 'failed').length,
|
|
84
|
+
avgDuration: runs.length > 0
|
|
85
|
+
? runs.reduce((acc, r) => acc + (r.duration || 0), 0) / runs.length
|
|
86
|
+
: 0
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const successRate = stats.total > 0 ? (stats.success / stats.total) * 100 : 0;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="h-full flex flex-col bg-white dark:bg-slate-900">
|
|
93
|
+
{/* Header */}
|
|
94
|
+
<div className="p-6 border-b border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/50">
|
|
95
|
+
<div className="flex items-start justify-between mb-4">
|
|
96
|
+
<div className="flex items-center gap-4">
|
|
97
|
+
<div className="p-3 bg-gradient-to-br from-primary-500 to-purple-500 rounded-xl text-white shadow-lg">
|
|
98
|
+
<Layers size={24} />
|
|
99
|
+
</div>
|
|
100
|
+
<div>
|
|
101
|
+
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">
|
|
102
|
+
{pipeline.name}
|
|
103
|
+
</h2>
|
|
104
|
+
<div className="flex items-center gap-2 mt-1 text-sm text-slate-500">
|
|
105
|
+
<ProjectSelector
|
|
106
|
+
currentProject={currentProject}
|
|
107
|
+
onUpdate={handleProjectUpdate}
|
|
108
|
+
/>
|
|
109
|
+
{pipeline.version && (
|
|
110
|
+
<>
|
|
111
|
+
<span>•</span>
|
|
112
|
+
<Badge variant="secondary" className="text-xs">v{pipeline.version}</Badge>
|
|
113
|
+
</>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="flex items-center gap-2">
|
|
119
|
+
<Button variant="ghost" size="sm" onClick={onClose}>
|
|
120
|
+
<X size={20} className="text-slate-400" />
|
|
121
|
+
</Button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{/* Stats Grid */}
|
|
126
|
+
<div className="grid grid-cols-4 gap-4">
|
|
127
|
+
<StatCard
|
|
128
|
+
label="Total Runs"
|
|
129
|
+
value={stats.total}
|
|
130
|
+
icon={Activity}
|
|
131
|
+
color="blue"
|
|
132
|
+
/>
|
|
133
|
+
<StatCard
|
|
134
|
+
label="Success Rate"
|
|
135
|
+
value={`${Math.round(successRate)}%`}
|
|
136
|
+
icon={CheckCircle}
|
|
137
|
+
color={successRate >= 80 ? 'emerald' : successRate >= 50 ? 'amber' : 'rose'}
|
|
138
|
+
/>
|
|
139
|
+
<StatCard
|
|
140
|
+
label="Avg Duration"
|
|
141
|
+
value={`${stats.avgDuration.toFixed(1)}s`}
|
|
142
|
+
icon={Clock}
|
|
143
|
+
color="purple"
|
|
144
|
+
/>
|
|
145
|
+
<StatCard
|
|
146
|
+
label="Failed"
|
|
147
|
+
value={stats.failed}
|
|
148
|
+
icon={XCircle}
|
|
149
|
+
color="rose"
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{/* Content - Runs List */}
|
|
155
|
+
<div className="flex-1 overflow-hidden flex flex-col">
|
|
156
|
+
<div className="p-4 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between bg-white dark:bg-slate-900">
|
|
157
|
+
<h3 className="font-semibold text-slate-900 dark:text-white flex items-center gap-2">
|
|
158
|
+
<PlayCircle size={18} className="text-slate-400" />
|
|
159
|
+
Recent Executions
|
|
160
|
+
</h3>
|
|
161
|
+
<Link to={`/runs?pipeline=${encodeURIComponent(pipeline.name)}`}>
|
|
162
|
+
<Button variant="ghost" size="sm" className="text-primary-600">
|
|
163
|
+
View All Runs <ArrowRight size={16} className="ml-1" />
|
|
164
|
+
</Button>
|
|
165
|
+
</Link>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-slate-50 dark:bg-slate-900/50">
|
|
169
|
+
{loading ? (
|
|
170
|
+
<div className="flex justify-center py-8">
|
|
171
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
|
172
|
+
</div>
|
|
173
|
+
) : runs.length === 0 ? (
|
|
174
|
+
<div className="text-center py-12 text-slate-500">
|
|
175
|
+
No runs found for this pipeline
|
|
176
|
+
</div>
|
|
177
|
+
) : (
|
|
178
|
+
runs.map(run => (
|
|
179
|
+
<Link key={run.run_id} to={`/runs/${run.run_id}`}>
|
|
180
|
+
<motion.div
|
|
181
|
+
initial={{ opacity: 0, y: 10 }}
|
|
182
|
+
animate={{ opacity: 1, y: 0 }}
|
|
183
|
+
className="bg-white dark:bg-slate-800 p-4 rounded-xl border border-slate-200 dark:border-slate-700 hover:shadow-md hover:border-primary-300 dark:hover:border-primary-700 transition-all group"
|
|
184
|
+
>
|
|
185
|
+
<div className="flex items-center justify-between">
|
|
186
|
+
<div className="flex items-center gap-4">
|
|
187
|
+
<StatusBadge status={run.status} />
|
|
188
|
+
<div>
|
|
189
|
+
<div className="font-medium text-slate-900 dark:text-white flex items-center gap-2">
|
|
190
|
+
{run.name || `Run ${run.run_id.slice(0, 8)}`}
|
|
191
|
+
<span className="text-xs font-mono text-slate-400">#{run.run_id.slice(0, 6)}</span>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="flex items-center gap-3 text-xs text-slate-500 mt-1">
|
|
194
|
+
<span className="flex items-center gap-1">
|
|
195
|
+
<Calendar size={12} />
|
|
196
|
+
{format(new Date(run.created || run.start_time), 'MMM d, HH:mm')}
|
|
197
|
+
</span>
|
|
198
|
+
<span className="flex items-center gap-1">
|
|
199
|
+
<Clock size={12} />
|
|
200
|
+
{run.duration ? `${run.duration.toFixed(1)}s` : '-'}
|
|
201
|
+
</span>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
<ArrowRight size={16} className="text-slate-300 group-hover:text-primary-500 transition-colors" />
|
|
206
|
+
</div>
|
|
207
|
+
</motion.div>
|
|
208
|
+
</Link>
|
|
209
|
+
))
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function StatCard({ label, value, icon: Icon, color }) {
|
|
218
|
+
const colorClasses = {
|
|
219
|
+
blue: 'text-blue-600 bg-blue-50 dark:bg-blue-900/20',
|
|
220
|
+
emerald: 'text-emerald-600 bg-emerald-50 dark:bg-emerald-900/20',
|
|
221
|
+
purple: 'text-purple-600 bg-purple-50 dark:bg-purple-900/20',
|
|
222
|
+
rose: 'text-rose-600 bg-rose-50 dark:bg-rose-900/20',
|
|
223
|
+
amber: 'text-amber-600 bg-amber-50 dark:bg-amber-900/20',
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<div className="bg-white dark:bg-slate-800 p-3 rounded-xl border border-slate-200 dark:border-slate-700">
|
|
228
|
+
<div className="flex items-center gap-2 mb-1">
|
|
229
|
+
<div className={`p-1 rounded-lg ${colorClasses[color] || colorClasses.blue}`}>
|
|
230
|
+
<Icon size={14} />
|
|
231
|
+
</div>
|
|
232
|
+
<span className="text-xs text-slate-500 font-medium">{label}</span>
|
|
233
|
+
</div>
|
|
234
|
+
<div className="text-lg font-bold text-slate-900 dark:text-white pl-1">
|
|
235
|
+
{value}
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
@@ -276,16 +276,80 @@ function CustomStepNode({ data }) {
|
|
|
276
276
|
);
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
|
|
279
280
|
function CustomArtifactNode({ data }) {
|
|
281
|
+
// Determine icon and styling based on artifact type (inferred from label)
|
|
282
|
+
const getArtifactStyle = (label) => {
|
|
283
|
+
const lowerLabel = label.toLowerCase();
|
|
284
|
+
|
|
285
|
+
if (lowerLabel.includes('model') || lowerLabel.includes('weights')) {
|
|
286
|
+
return {
|
|
287
|
+
icon: Box,
|
|
288
|
+
bgColor: 'bg-purple-100 dark:bg-purple-900/30',
|
|
289
|
+
borderColor: 'border-purple-400 dark:border-purple-600',
|
|
290
|
+
iconColor: 'text-purple-600 dark:text-purple-400',
|
|
291
|
+
textColor: 'text-purple-900 dark:text-purple-100'
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (lowerLabel.includes('feature') || lowerLabel.includes('train_set') || lowerLabel.includes('test_set')) {
|
|
295
|
+
return {
|
|
296
|
+
icon: Layers,
|
|
297
|
+
bgColor: 'bg-emerald-100 dark:bg-emerald-900/30',
|
|
298
|
+
borderColor: 'border-emerald-400 dark:border-emerald-600',
|
|
299
|
+
iconColor: 'text-emerald-600 dark:text-emerald-400',
|
|
300
|
+
textColor: 'text-emerald-900 dark:text-emerald-100'
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (lowerLabel.includes('data') || lowerLabel.includes('batch') || lowerLabel.includes('set')) {
|
|
304
|
+
return {
|
|
305
|
+
icon: Database,
|
|
306
|
+
bgColor: 'bg-blue-100 dark:bg-blue-900/30',
|
|
307
|
+
borderColor: 'border-blue-400 dark:border-blue-600',
|
|
308
|
+
iconColor: 'text-blue-600 dark:text-blue-400',
|
|
309
|
+
textColor: 'text-blue-900 dark:text-blue-100'
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (lowerLabel.includes('metrics') || lowerLabel.includes('report') || lowerLabel.includes('status')) {
|
|
313
|
+
return {
|
|
314
|
+
icon: BarChart2,
|
|
315
|
+
bgColor: 'bg-orange-100 dark:bg-orange-900/30',
|
|
316
|
+
borderColor: 'border-orange-400 dark:border-orange-600',
|
|
317
|
+
iconColor: 'text-orange-600 dark:text-orange-400',
|
|
318
|
+
textColor: 'text-orange-900 dark:text-orange-100'
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (lowerLabel.includes('image') || lowerLabel.includes('docker')) {
|
|
322
|
+
return {
|
|
323
|
+
icon: Box,
|
|
324
|
+
bgColor: 'bg-cyan-100 dark:bg-cyan-900/30',
|
|
325
|
+
borderColor: 'border-cyan-400 dark:border-cyan-600',
|
|
326
|
+
iconColor: 'text-cyan-600 dark:text-cyan-400',
|
|
327
|
+
textColor: 'text-cyan-900 dark:text-cyan-100'
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Default style
|
|
332
|
+
return {
|
|
333
|
+
icon: FileText,
|
|
334
|
+
bgColor: 'bg-slate-100 dark:bg-slate-800',
|
|
335
|
+
borderColor: 'border-slate-300 dark:border-slate-600',
|
|
336
|
+
iconColor: 'text-slate-500 dark:text-slate-400',
|
|
337
|
+
textColor: 'text-slate-700 dark:text-slate-300'
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const style = getArtifactStyle(data.label);
|
|
342
|
+
const Icon = style.icon;
|
|
343
|
+
|
|
280
344
|
return (
|
|
281
345
|
<div
|
|
282
|
-
className=
|
|
346
|
+
className={`px-3 py-2 rounded-lg ${style.bgColor} border-2 ${style.borderColor} flex items-center justify-center gap-2 shadow-md hover:shadow-lg transition-all min-w-[140px]`}
|
|
283
347
|
style={{ height: artifactNodeHeight }}
|
|
284
348
|
>
|
|
285
349
|
<Handle type="target" position={Position.Top} className="!bg-slate-400 !w-2 !h-2" />
|
|
286
350
|
|
|
287
|
-
<
|
|
288
|
-
<span className=
|
|
351
|
+
<Icon size={14} className={style.iconColor} />
|
|
352
|
+
<span className={`text-xs font-semibold ${style.textColor} truncate max-w-[100px]`} title={data.label}>
|
|
289
353
|
{data.label}
|
|
290
354
|
</span>
|
|
291
355
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { fetchApi } from '../utils/api';
|
|
3
|
+
import { Folder, ChevronDown, Check } from 'lucide-react';
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
+
|
|
6
|
+
export function ProjectSelector({ currentProject, onUpdate, type = 'default' }) {
|
|
7
|
+
const [projects, setProjects] = useState([]);
|
|
8
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
9
|
+
const [loading, setLoading] = useState(false);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (isOpen && projects.length === 0) {
|
|
13
|
+
fetchProjects();
|
|
14
|
+
}
|
|
15
|
+
}, [isOpen]);
|
|
16
|
+
|
|
17
|
+
const fetchProjects = async () => {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetchApi('/api/projects/');
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
setProjects(data.projects || []);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Failed to fetch projects:', error);
|
|
25
|
+
} finally {
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleSelect = async (projectName) => {
|
|
31
|
+
if (projectName === currentProject) {
|
|
32
|
+
setIsOpen(false);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await onUpdate(projectName);
|
|
37
|
+
setIsOpen(false);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="relative">
|
|
42
|
+
<button
|
|
43
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
44
|
+
className={`
|
|
45
|
+
flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
|
|
46
|
+
${currentProject
|
|
47
|
+
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/30'
|
|
48
|
+
: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700'
|
|
49
|
+
}
|
|
50
|
+
`}
|
|
51
|
+
>
|
|
52
|
+
<Folder size={14} />
|
|
53
|
+
<span>{currentProject || 'Assign Project'}</span>
|
|
54
|
+
<ChevronDown size={12} className={`transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
55
|
+
</button>
|
|
56
|
+
|
|
57
|
+
<AnimatePresence>
|
|
58
|
+
{isOpen && (
|
|
59
|
+
<>
|
|
60
|
+
<div
|
|
61
|
+
className="fixed inset-0 z-40"
|
|
62
|
+
onClick={() => setIsOpen(false)}
|
|
63
|
+
/>
|
|
64
|
+
<motion.div
|
|
65
|
+
initial={{ opacity: 0, y: 5, scale: 0.95 }}
|
|
66
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
67
|
+
exit={{ opacity: 0, y: 5, scale: 0.95 }}
|
|
68
|
+
transition={{ duration: 0.1 }}
|
|
69
|
+
className="absolute top-full left-0 mt-2 w-56 bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 z-50 overflow-hidden"
|
|
70
|
+
>
|
|
71
|
+
<div className="p-2 border-b border-slate-100 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
|
|
72
|
+
<span className="text-xs font-semibold text-slate-500 uppercase tracking-wider px-2">
|
|
73
|
+
Select Project
|
|
74
|
+
</span>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="max-h-64 overflow-y-auto p-1">
|
|
78
|
+
{loading ? (
|
|
79
|
+
<div className="p-4 text-center text-slate-400 text-xs">Loading...</div>
|
|
80
|
+
) : (
|
|
81
|
+
<>
|
|
82
|
+
{projects.map(project => (
|
|
83
|
+
<button
|
|
84
|
+
key={project.name}
|
|
85
|
+
onClick={() => handleSelect(project.name)}
|
|
86
|
+
className={`
|
|
87
|
+
w-full flex items-center justify-between px-3 py-2 rounded-lg text-sm transition-colors
|
|
88
|
+
${currentProject === project.name
|
|
89
|
+
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
|
90
|
+
: 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700'
|
|
91
|
+
}
|
|
92
|
+
`}
|
|
93
|
+
>
|
|
94
|
+
<div className="flex items-center gap-2">
|
|
95
|
+
<Folder size={14} />
|
|
96
|
+
<span className="truncate max-w-[140px]">{project.name}</span>
|
|
97
|
+
</div>
|
|
98
|
+
{currentProject === project.name && <Check size={14} />}
|
|
99
|
+
</button>
|
|
100
|
+
))}
|
|
101
|
+
{projects.length === 0 && (
|
|
102
|
+
<div className="p-3 text-center text-slate-400 text-xs">
|
|
103
|
+
No projects found
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</motion.div>
|
|
110
|
+
</>
|
|
111
|
+
)}
|
|
112
|
+
</AnimatePresence>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|