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,246 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { fetchApi } from '../../utils/api';
|
|
3
|
+
import { Activity, Zap, MessageSquare, Clock, DollarSign } from 'lucide-react';
|
|
4
|
+
import { useProject } from '../../contexts/ProjectContext';
|
|
5
|
+
|
|
6
|
+
export function Traces() {
|
|
7
|
+
const [traces, setTraces] = useState([]);
|
|
8
|
+
const [selectedTrace, setSelectedTrace] = useState(null);
|
|
9
|
+
const [loading, setLoading] = useState(true);
|
|
10
|
+
const [filterType, setFilterType] = useState('all');
|
|
11
|
+
const { selectedProject } = useProject();
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
fetchTraces();
|
|
15
|
+
}, [filterType, selectedProject]);
|
|
16
|
+
|
|
17
|
+
const fetchTraces = async () => {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
try {
|
|
20
|
+
const params = new URLSearchParams();
|
|
21
|
+
if (filterType !== 'all') {
|
|
22
|
+
params.append('event_type', filterType);
|
|
23
|
+
}
|
|
24
|
+
if (selectedProject) {
|
|
25
|
+
params.append('project', selectedProject);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const response = await fetchApi(`/api/traces?${params}`);
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
setTraces(data);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Failed to fetch traces:', error);
|
|
33
|
+
} finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const fetchTraceDetails = async (traceId) => {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetchApi(`/api/traces/${traceId}`);
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
setSelectedTrace(data);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Failed to fetch trace details:', error);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const formatDuration = (duration) => {
|
|
49
|
+
if (!duration) return 'N/A';
|
|
50
|
+
return `${(duration * 1000).toFixed(0)}ms`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const getStatusColor = (status) => {
|
|
54
|
+
switch (status) {
|
|
55
|
+
case 'success': return 'text-green-400';
|
|
56
|
+
case 'error': return 'text-red-400';
|
|
57
|
+
case 'running': return 'text-yellow-400';
|
|
58
|
+
default: return 'text-gray-400';
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const getEventTypeIcon = (type) => {
|
|
63
|
+
switch (type) {
|
|
64
|
+
case 'llm': return <MessageSquare className="w-4 h-4" />;
|
|
65
|
+
case 'tool': return <Zap className="w-4 h-4" />;
|
|
66
|
+
default: return <Activity className="w-4 h-4" />;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const TraceTree = ({ events, level = 0 }) => {
|
|
71
|
+
if (!events) return null;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className={`pl-${level * 4}`}>
|
|
75
|
+
{events.map((event, idx) => (
|
|
76
|
+
<div key={idx} className="mb-2">
|
|
77
|
+
<div className="flex items-center gap-2 p-2 bg-gray-800/50 rounded border border-gray-700/50 hover:border-blue-500/50 transition-colors">
|
|
78
|
+
<div className="flex items-center gap-2 flex-1">
|
|
79
|
+
{getEventTypeIcon(event.event_type)}
|
|
80
|
+
<span className="font-medium">{event.name}</span>
|
|
81
|
+
<span className={`text-sm ${getStatusColor(event.status)}`}>
|
|
82
|
+
{event.status}
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="flex items-center gap-4 text-sm text-gray-400">
|
|
87
|
+
{event.duration && (
|
|
88
|
+
<div className="flex items-center gap-1">
|
|
89
|
+
<Clock className="w-3 h-3" />
|
|
90
|
+
{formatDuration(event.duration)}
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{event.total_tokens > 0 && (
|
|
95
|
+
<div className="flex items-center gap-1">
|
|
96
|
+
<Activity className="w-3 h-3" />
|
|
97
|
+
{event.total_tokens} tokens
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{event.cost > 0 && (
|
|
102
|
+
<div className="flex items-center gap-1">
|
|
103
|
+
<DollarSign className="w-3 h-3" />
|
|
104
|
+
${event.cost.toFixed(4)}
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{event.children && event.children.length > 0 && (
|
|
111
|
+
<div className="ml-6 mt-2 border-l-2 border-gray-700/50 pl-2">
|
|
112
|
+
<TraceTree events={event.children} level={level + 1} />
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className="p-6">
|
|
123
|
+
<div className="flex justify-between items-center mb-6">
|
|
124
|
+
<h1 className="text-2xl font-bold">🔍 LLM Traces</h1>
|
|
125
|
+
|
|
126
|
+
<div className="flex gap-2">
|
|
127
|
+
<select
|
|
128
|
+
value={filterType}
|
|
129
|
+
onChange={(e) => setFilterType(e.target.value)}
|
|
130
|
+
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
|
|
131
|
+
>
|
|
132
|
+
<option value="all">All Types</option>
|
|
133
|
+
<option value="llm">LLM Calls</option>
|
|
134
|
+
<option value="tool">Tool Calls</option>
|
|
135
|
+
<option value="chain">Chains</option>
|
|
136
|
+
<option value="agent">Agents</option>
|
|
137
|
+
</select>
|
|
138
|
+
|
|
139
|
+
<button
|
|
140
|
+
onClick={fetchTraces}
|
|
141
|
+
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
|
|
142
|
+
>
|
|
143
|
+
Refresh
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{loading ? (
|
|
149
|
+
<div className="text-center py-12">
|
|
150
|
+
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
|
151
|
+
<p className="mt-4 text-gray-400">Loading traces...</p>
|
|
152
|
+
</div>
|
|
153
|
+
) : traces.length === 0 ? (
|
|
154
|
+
<div className="text-center py-12 bg-gray-800/30 rounded-lg border-2 border-dashed border-gray-700">
|
|
155
|
+
<Activity className="w-12 h-12 mx-auto text-gray-600 mb-4" />
|
|
156
|
+
<p className="text-gray-400">No traces found</p>
|
|
157
|
+
<p className="text-sm text-gray-500 mt-2">
|
|
158
|
+
Use @trace_llm decorator to track LLM calls
|
|
159
|
+
</p>
|
|
160
|
+
</div>
|
|
161
|
+
) : (
|
|
162
|
+
<div className="grid gap-4">
|
|
163
|
+
{traces.map((trace) => {
|
|
164
|
+
// Group by trace_id
|
|
165
|
+
const traceId = trace.trace_id;
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div
|
|
169
|
+
key={trace.event_id}
|
|
170
|
+
className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4 hover:border-blue-500/50 transition-all cursor-pointer"
|
|
171
|
+
onClick={() => fetchTraceDetails(traceId)}
|
|
172
|
+
>
|
|
173
|
+
<div className="flex items-start justify-between mb-3">
|
|
174
|
+
<div className="flex items-center gap-3">
|
|
175
|
+
{getEventTypeIcon(trace.event_type)}
|
|
176
|
+
<div>
|
|
177
|
+
<h3 className="font-semibold text-lg">{trace.name}</h3>
|
|
178
|
+
<p className="text-sm text-gray-400">
|
|
179
|
+
Trace ID: {traceId.slice(0, 8)}...
|
|
180
|
+
</p>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<span className={`px-3 py-1 rounded-full text-sm ${getStatusColor(trace.status)}`}>
|
|
185
|
+
{trace.status}
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div className="grid grid-cols-4 gap-4 text-sm">
|
|
190
|
+
<div>
|
|
191
|
+
<span className="text-gray-500">Duration:</span>
|
|
192
|
+
<span className="ml-2 text-gray-300">{formatDuration(trace.duration)}</span>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{trace.model && (
|
|
196
|
+
<div>
|
|
197
|
+
<span className="text-gray-500">Model:</span>
|
|
198
|
+
<span className="ml-2 text-gray-300">{trace.model}</span>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{trace.total_tokens > 0 && (
|
|
203
|
+
<div>
|
|
204
|
+
<span className="text-gray-500">Tokens:</span>
|
|
205
|
+
<span className="ml-2 text-gray-300">
|
|
206
|
+
{trace.total_tokens} ({trace.prompt_tokens}/{trace.completion_tokens})
|
|
207
|
+
</span>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
|
|
211
|
+
{trace.cost > 0 && (
|
|
212
|
+
<div>
|
|
213
|
+
<span className="text-gray-500">Cost:</span>
|
|
214
|
+
<span className="ml-2 text-gray-300">${trace.cost.toFixed(4)}</span>
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
})}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{/* Trace Details Modal */}
|
|
225
|
+
{selectedTrace && (
|
|
226
|
+
<div className="fixed inset-0 bg-black/80 flex items-center justify-center p-6 z-50">
|
|
227
|
+
<div className="bg-gray-900 rounded-lg max-w-4xl w-full max-h-[80vh] overflow-auto border border-gray-700">
|
|
228
|
+
<div className="sticky top-0 bg-gray-900 border-b border-gray-700 p-4 flex justify-between items-center">
|
|
229
|
+
<h2 className="text-xl font-bold">Trace Details</h2>
|
|
230
|
+
<button
|
|
231
|
+
onClick={() => setSelectedTrace(null)}
|
|
232
|
+
className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg"
|
|
233
|
+
>
|
|
234
|
+
Close
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div className="p-6">
|
|
239
|
+
<TraceTree events={selectedTrace} />
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
3
|
+
import { LayoutDashboard, PlayCircle, FolderKanban, FlaskConical, Database, Settings, Trophy, Calendar, MessageSquare, Moon, Sun, Key, Package } from 'lucide-react';
|
|
4
|
+
import { useTheme } from '../contexts/ThemeContext';
|
|
5
|
+
import { ProjectSelector } from './ui/ProjectSelector';
|
|
6
|
+
|
|
7
|
+
function NavLink({ to, icon, label }) {
|
|
8
|
+
const location = useLocation();
|
|
9
|
+
const isActive = location.pathname === to;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Link
|
|
13
|
+
to={to}
|
|
14
|
+
className={`flex items-center gap-3 px-4 py-2.5 rounded-lg transition-all duration-200 group ${isActive
|
|
15
|
+
? 'bg-primary-50 dark:bg-primary-900/20 text-primary-700 dark:text-primary-400 font-medium shadow-sm'
|
|
16
|
+
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700 hover:text-slate-900 dark:hover:text-white'
|
|
17
|
+
}`}
|
|
18
|
+
>
|
|
19
|
+
<span className={`transition-colors ${isActive ? 'text-primary-600 dark:text-primary-400' : 'text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300'
|
|
20
|
+
}`}>
|
|
21
|
+
{icon}
|
|
22
|
+
</span>
|
|
23
|
+
<span className="text-sm">{label}</span>
|
|
24
|
+
</Link>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Layout({ children }) {
|
|
29
|
+
const { theme, toggleTheme } = useTheme();
|
|
30
|
+
|
|
31
|
+
const navLinks = [
|
|
32
|
+
{ icon: LayoutDashboard, label: 'Dashboard', path: '/' },
|
|
33
|
+
{ icon: FolderKanban, label: 'Projects', path: '/projects' },
|
|
34
|
+
{ icon: PlayCircle, label: 'Pipelines', path: '/pipelines' },
|
|
35
|
+
{ icon: Calendar, label: 'Schedules', path: '/schedules' },
|
|
36
|
+
{ icon: PlayCircle, label: 'Runs', path: '/runs' },
|
|
37
|
+
{ icon: Trophy, label: 'Leaderboard', path: '/leaderboard' },
|
|
38
|
+
{ icon: Database, label: 'Assets', path: '/assets' },
|
|
39
|
+
{ icon: FlaskConical, label: 'Experiments', path: '/experiments' },
|
|
40
|
+
{ icon: MessageSquare, label: 'Traces', path: '/traces' },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="flex h-screen bg-slate-50 dark:bg-slate-900">
|
|
45
|
+
{/* Sidebar */}
|
|
46
|
+
<aside className="w-64 bg-white dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700 flex flex-col shadow-sm z-10">
|
|
47
|
+
<div className="p-6 border-b border-slate-100 dark:border-slate-700 flex items-center gap-3">
|
|
48
|
+
<div className="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center shadow-lg shadow-primary-500/30">
|
|
49
|
+
<PlayCircle className="text-white w-5 h-5" />
|
|
50
|
+
</div>
|
|
51
|
+
<h1 className="text-xl font-bold text-slate-900 dark:text-white tracking-tight">flowyml</h1>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
|
|
55
|
+
<div className="px-4 py-2 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider">
|
|
56
|
+
Platform
|
|
57
|
+
</div>
|
|
58
|
+
{navLinks.map((link) => (
|
|
59
|
+
<NavLink
|
|
60
|
+
key={link.path}
|
|
61
|
+
to={link.path}
|
|
62
|
+
icon={<link.icon size={18} />}
|
|
63
|
+
label={link.label}
|
|
64
|
+
/>
|
|
65
|
+
))}
|
|
66
|
+
<div className="px-4 py-2 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider mt-4">
|
|
67
|
+
Settings
|
|
68
|
+
</div>
|
|
69
|
+
<NavLink to="/plugins" icon={<Package size={18} />} label="Plugins" />
|
|
70
|
+
<NavLink to="/tokens" icon={<Key size={18} />} label="API Tokens" />
|
|
71
|
+
<NavLink to="/settings" icon={<Settings size={18} />} label="Settings" />
|
|
72
|
+
</nav>
|
|
73
|
+
|
|
74
|
+
<div className="p-4 border-t border-slate-100 dark:border-slate-700">
|
|
75
|
+
<div className="bg-slate-50 dark:bg-slate-900 rounded-lg p-4 border border-slate-100 dark:border-slate-700">
|
|
76
|
+
<p className="text-xs font-medium text-slate-500 dark:text-slate-400">flowyml v0.1.0</p>
|
|
77
|
+
<p className="text-xs text-slate-400 dark:text-slate-500 mt-1">Local Environment</p>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</aside>
|
|
81
|
+
|
|
82
|
+
{/* Main Content */}
|
|
83
|
+
<main className="flex-1 flex flex-col overflow-hidden">
|
|
84
|
+
{/* Header with Project Selector */}
|
|
85
|
+
<header className="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-6 py-4 flex items-center justify-between shadow-sm">
|
|
86
|
+
<ProjectSelector />
|
|
87
|
+
|
|
88
|
+
<button
|
|
89
|
+
onClick={toggleTheme}
|
|
90
|
+
className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
|
|
91
|
+
aria-label="Toggle theme"
|
|
92
|
+
>
|
|
93
|
+
{theme === 'dark' ? (
|
|
94
|
+
<Sun size={20} className="text-slate-400" />
|
|
95
|
+
) : (
|
|
96
|
+
<Moon size={20} className="text-slate-400" />
|
|
97
|
+
)}
|
|
98
|
+
</button>
|
|
99
|
+
</header>
|
|
100
|
+
|
|
101
|
+
{/* Page Content */}
|
|
102
|
+
<div className="flex-1 overflow-y-auto p-6">
|
|
103
|
+
{children}
|
|
104
|
+
</div>
|
|
105
|
+
</main>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useEffect } from 'react';
|
|
2
|
+
import ReactFlow, {
|
|
3
|
+
Background,
|
|
4
|
+
Controls,
|
|
5
|
+
MiniMap,
|
|
6
|
+
useNodesState,
|
|
7
|
+
useEdgesState,
|
|
8
|
+
MarkerType,
|
|
9
|
+
Handle,
|
|
10
|
+
Position
|
|
11
|
+
} from 'reactflow';
|
|
12
|
+
import 'reactflow/dist/style.css';
|
|
13
|
+
import { CheckCircle, XCircle, Clock, Loader, Database, Box, BarChart2, FileText, Layers } from 'lucide-react';
|
|
14
|
+
import dagre from 'dagre';
|
|
15
|
+
|
|
16
|
+
const stepNodeWidth = 240;
|
|
17
|
+
const stepNodeHeight = 100;
|
|
18
|
+
const artifactNodeWidth = 180;
|
|
19
|
+
const artifactNodeHeight = 50;
|
|
20
|
+
|
|
21
|
+
const getLayoutedElements = (nodes, edges, direction = 'TB') => {
|
|
22
|
+
const dagreGraph = new dagre.graphlib.Graph();
|
|
23
|
+
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
|
24
|
+
|
|
25
|
+
const isHorizontal = direction === 'LR';
|
|
26
|
+
dagreGraph.setGraph({ rankdir: direction, nodesep: 80, ranksep: 100 });
|
|
27
|
+
|
|
28
|
+
nodes.forEach((node) => {
|
|
29
|
+
const width = node.type === 'artifact' ? artifactNodeWidth : stepNodeWidth;
|
|
30
|
+
const height = node.type === 'artifact' ? artifactNodeHeight : stepNodeHeight;
|
|
31
|
+
dagreGraph.setNode(node.id, { width, height });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
edges.forEach((edge) => {
|
|
35
|
+
dagreGraph.setEdge(edge.source, edge.target);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
dagre.layout(dagreGraph);
|
|
39
|
+
|
|
40
|
+
nodes.forEach((node) => {
|
|
41
|
+
const nodeWithPosition = dagreGraph.node(node.id);
|
|
42
|
+
node.targetPosition = isHorizontal ? 'left' : 'top';
|
|
43
|
+
node.sourcePosition = isHorizontal ? 'right' : 'bottom';
|
|
44
|
+
|
|
45
|
+
// Shift to center the node
|
|
46
|
+
const width = node.type === 'artifact' ? artifactNodeWidth : stepNodeWidth;
|
|
47
|
+
const height = node.type === 'artifact' ? artifactNodeHeight : stepNodeHeight;
|
|
48
|
+
|
|
49
|
+
node.position = {
|
|
50
|
+
x: nodeWithPosition.x - width / 2,
|
|
51
|
+
y: nodeWithPosition.y - height / 2,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return node;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { nodes, edges };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function PipelineGraph({ dag, steps, selectedStep, onStepSelect }) {
|
|
61
|
+
// Transform DAG data to ReactFlow format with Artifact Nodes
|
|
62
|
+
const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
|
|
63
|
+
if (!dag || !dag.nodes) return { nodes: [], edges: [] };
|
|
64
|
+
|
|
65
|
+
const nodes = [];
|
|
66
|
+
const edges = [];
|
|
67
|
+
const artifactIds = new Set();
|
|
68
|
+
const createdArtifacts = new Map(); // Map name -> id
|
|
69
|
+
|
|
70
|
+
// Helper to get or create artifact node
|
|
71
|
+
const getArtifactId = (name) => {
|
|
72
|
+
if (createdArtifacts.has(name)) return createdArtifacts.get(name);
|
|
73
|
+
const id = `artifact-${name}`;
|
|
74
|
+
if (!artifactIds.has(id)) {
|
|
75
|
+
nodes.push({
|
|
76
|
+
id: id,
|
|
77
|
+
type: 'artifact',
|
|
78
|
+
data: { label: name }
|
|
79
|
+
});
|
|
80
|
+
artifactIds.add(id);
|
|
81
|
+
createdArtifacts.set(name, id);
|
|
82
|
+
}
|
|
83
|
+
return id;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 1. Create Step Nodes and Connections
|
|
87
|
+
dag.nodes.forEach(node => {
|
|
88
|
+
const stepData = steps?.[node.id] || {};
|
|
89
|
+
const status = stepData.success ? 'success' : stepData.error ? 'failed' : stepData.running ? 'running' : 'pending';
|
|
90
|
+
|
|
91
|
+
nodes.push({
|
|
92
|
+
id: node.id,
|
|
93
|
+
type: 'step',
|
|
94
|
+
data: {
|
|
95
|
+
label: node.name,
|
|
96
|
+
status,
|
|
97
|
+
duration: stepData.duration,
|
|
98
|
+
cached: stepData.cached,
|
|
99
|
+
selected: selectedStep === node.id
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Input Edges: Artifact -> Step
|
|
104
|
+
node.inputs?.forEach(inputName => {
|
|
105
|
+
const artifactId = getArtifactId(inputName);
|
|
106
|
+
edges.push({
|
|
107
|
+
id: `e-${artifactId}-${node.id}`,
|
|
108
|
+
source: artifactId,
|
|
109
|
+
target: node.id,
|
|
110
|
+
type: 'smoothstep',
|
|
111
|
+
animated: true,
|
|
112
|
+
style: { stroke: '#94a3b8', strokeWidth: 2 },
|
|
113
|
+
markerEnd: { type: MarkerType.ArrowClosed, color: '#94a3b8' }
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Output Edges: Step -> Artifact
|
|
118
|
+
node.outputs?.forEach(outputName => {
|
|
119
|
+
const artifactId = getArtifactId(outputName);
|
|
120
|
+
edges.push({
|
|
121
|
+
id: `e-${node.id}-${artifactId}`,
|
|
122
|
+
source: node.id,
|
|
123
|
+
target: artifactId,
|
|
124
|
+
type: 'smoothstep',
|
|
125
|
+
animated: true,
|
|
126
|
+
style: { stroke: '#94a3b8', strokeWidth: 2 },
|
|
127
|
+
markerEnd: { type: MarkerType.ArrowClosed, color: '#94a3b8' }
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return { nodes, edges };
|
|
133
|
+
}, [dag, steps, selectedStep]);
|
|
134
|
+
|
|
135
|
+
const { nodes: layoutedNodes, edges: layoutedEdges } = useMemo(() => {
|
|
136
|
+
return getLayoutedElements(initialNodes, initialEdges, 'TB');
|
|
137
|
+
}, [initialNodes, initialEdges]);
|
|
138
|
+
|
|
139
|
+
const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);
|
|
140
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges);
|
|
141
|
+
|
|
142
|
+
// Update nodes when selection changes or layout changes
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
setNodes(layoutedNodes.map(node => {
|
|
145
|
+
if (node.type === 'step') {
|
|
146
|
+
return {
|
|
147
|
+
...node,
|
|
148
|
+
data: {
|
|
149
|
+
...node.data,
|
|
150
|
+
selected: selectedStep === node.id
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return node;
|
|
155
|
+
}));
|
|
156
|
+
setEdges(layoutedEdges);
|
|
157
|
+
}, [layoutedNodes, layoutedEdges, selectedStep, setNodes, setEdges]);
|
|
158
|
+
|
|
159
|
+
const onNodeClick = useCallback((event, node) => {
|
|
160
|
+
if (node.type === 'step' && onStepSelect) {
|
|
161
|
+
onStepSelect(node.id);
|
|
162
|
+
}
|
|
163
|
+
}, [onStepSelect]);
|
|
164
|
+
|
|
165
|
+
const nodeTypes = useMemo(() => ({
|
|
166
|
+
step: CustomStepNode,
|
|
167
|
+
artifact: CustomArtifactNode
|
|
168
|
+
}), []);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div className="w-full h-full bg-slate-50/50 rounded-xl border border-slate-200 overflow-hidden">
|
|
172
|
+
<ReactFlow
|
|
173
|
+
nodes={nodes}
|
|
174
|
+
edges={edges}
|
|
175
|
+
onNodesChange={onNodesChange}
|
|
176
|
+
onEdgesChange={onEdgesChange}
|
|
177
|
+
onNodeClick={onNodeClick}
|
|
178
|
+
nodeTypes={nodeTypes}
|
|
179
|
+
fitView
|
|
180
|
+
attributionPosition="bottom-left"
|
|
181
|
+
minZoom={0.2}
|
|
182
|
+
maxZoom={1.5}
|
|
183
|
+
defaultViewport={{ x: 0, y: 0, zoom: 0.8 }}
|
|
184
|
+
fitViewOptions={{ padding: 0.2 }}
|
|
185
|
+
>
|
|
186
|
+
<Background color="#e2e8f0" gap={20} size={1} />
|
|
187
|
+
<Controls className="bg-white border border-slate-200 shadow-sm rounded-lg" />
|
|
188
|
+
<MiniMap
|
|
189
|
+
nodeColor={n => n.type === 'step' ? '#3b82f6' : '#cbd5e1'}
|
|
190
|
+
maskColor="rgba(241, 245, 249, 0.7)"
|
|
191
|
+
className="bg-white border border-slate-200 shadow-sm rounded-lg"
|
|
192
|
+
/>
|
|
193
|
+
</ReactFlow>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function CustomStepNode({ data }) {
|
|
199
|
+
const statusConfig = {
|
|
200
|
+
success: {
|
|
201
|
+
icon: <CheckCircle size={18} />,
|
|
202
|
+
color: 'text-emerald-600',
|
|
203
|
+
bg: 'bg-white',
|
|
204
|
+
border: 'border-emerald-500',
|
|
205
|
+
ring: 'ring-emerald-200',
|
|
206
|
+
shadow: 'shadow-emerald-100'
|
|
207
|
+
},
|
|
208
|
+
failed: {
|
|
209
|
+
icon: <XCircle size={18} />,
|
|
210
|
+
color: 'text-rose-600',
|
|
211
|
+
bg: 'bg-white',
|
|
212
|
+
border: 'border-rose-500',
|
|
213
|
+
ring: 'ring-rose-200',
|
|
214
|
+
shadow: 'shadow-rose-100'
|
|
215
|
+
},
|
|
216
|
+
running: {
|
|
217
|
+
icon: <Loader size={18} className="animate-spin" />,
|
|
218
|
+
color: 'text-amber-600',
|
|
219
|
+
bg: 'bg-white',
|
|
220
|
+
border: 'border-amber-500',
|
|
221
|
+
ring: 'ring-amber-200',
|
|
222
|
+
shadow: 'shadow-amber-100'
|
|
223
|
+
},
|
|
224
|
+
pending: {
|
|
225
|
+
icon: <Clock size={18} />,
|
|
226
|
+
color: 'text-slate-400',
|
|
227
|
+
bg: 'bg-slate-50',
|
|
228
|
+
border: 'border-slate-300',
|
|
229
|
+
ring: 'ring-slate-200',
|
|
230
|
+
shadow: 'shadow-slate-100'
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const config = statusConfig[data.status] || statusConfig.pending;
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<div
|
|
238
|
+
className={`
|
|
239
|
+
relative px-4 py-3 rounded-lg border-2 transition-all duration-200
|
|
240
|
+
${config.bg} ${config.border}
|
|
241
|
+
${data.selected ? `ring-4 ${config.ring} shadow-lg` : `hover:shadow-md ${config.shadow}`}
|
|
242
|
+
`}
|
|
243
|
+
style={{ width: stepNodeWidth, height: stepNodeHeight }}
|
|
244
|
+
>
|
|
245
|
+
<Handle type="target" position={Position.Top} className="!bg-slate-400 !w-2 !h-2" />
|
|
246
|
+
|
|
247
|
+
<div className="flex flex-col h-full justify-between">
|
|
248
|
+
<div className="flex items-start gap-3">
|
|
249
|
+
<div className={`p-1.5 rounded-md bg-slate-50 border border-slate-100 ${config.color}`}>
|
|
250
|
+
{config.icon}
|
|
251
|
+
</div>
|
|
252
|
+
<div className="min-w-0 flex-1">
|
|
253
|
+
<h3 className="font-bold text-slate-900 text-sm truncate" title={data.label}>
|
|
254
|
+
{data.label}
|
|
255
|
+
</h3>
|
|
256
|
+
<p className="text-xs text-slate-500 capitalize">{data.status}</p>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{data.duration !== undefined && (
|
|
261
|
+
<div className="flex items-center justify-between pt-2 border-t border-slate-100 mt-1">
|
|
262
|
+
<span className="text-xs text-slate-400 font-mono">
|
|
263
|
+
{data.duration.toFixed(2)}s
|
|
264
|
+
</span>
|
|
265
|
+
{data.cached && (
|
|
266
|
+
<span className="text-[10px] font-bold text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded uppercase tracking-wider">
|
|
267
|
+
Cached
|
|
268
|
+
</span>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<Handle type="source" position={Position.Bottom} className="!bg-slate-400 !w-2 !h-2" />
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function CustomArtifactNode({ data }) {
|
|
280
|
+
return (
|
|
281
|
+
<div
|
|
282
|
+
className="px-3 py-2 rounded-full bg-slate-100 border border-slate-300 flex items-center justify-center gap-2 shadow-sm min-w-[120px]"
|
|
283
|
+
style={{ height: artifactNodeHeight }}
|
|
284
|
+
>
|
|
285
|
+
<Handle type="target" position={Position.Top} className="!bg-slate-400 !w-2 !h-2" />
|
|
286
|
+
|
|
287
|
+
<Database size={12} className="text-slate-500" />
|
|
288
|
+
<span className="text-xs font-medium text-slate-700 truncate max-w-[140px]" title={data.label}>
|
|
289
|
+
{data.label}
|
|
290
|
+
</span>
|
|
291
|
+
|
|
292
|
+
<Handle type="source" position={Position.Bottom} className="!bg-slate-400 !w-2 !h-2" />
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|