flowyml 1.3.0__py3-none-any.whl → 1.5.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 (52) hide show
  1. flowyml/core/execution_status.py +1 -0
  2. flowyml/core/executor.py +175 -3
  3. flowyml/core/observability.py +7 -7
  4. flowyml/core/resources.py +12 -12
  5. flowyml/core/retry_policy.py +2 -2
  6. flowyml/core/scheduler.py +9 -9
  7. flowyml/core/scheduler_config.py +2 -3
  8. flowyml/core/submission_result.py +4 -4
  9. flowyml/stacks/bridge.py +9 -9
  10. flowyml/stacks/plugins.py +2 -2
  11. flowyml/stacks/registry.py +21 -0
  12. flowyml/storage/materializers/base.py +33 -0
  13. flowyml/storage/metadata.py +3 -1042
  14. flowyml/storage/remote.py +590 -0
  15. flowyml/storage/sql.py +951 -0
  16. flowyml/ui/backend/dependencies.py +28 -0
  17. flowyml/ui/backend/main.py +4 -79
  18. flowyml/ui/backend/routers/assets.py +170 -9
  19. flowyml/ui/backend/routers/client.py +6 -6
  20. flowyml/ui/backend/routers/execution.py +2 -2
  21. flowyml/ui/backend/routers/experiments.py +53 -6
  22. flowyml/ui/backend/routers/metrics.py +23 -68
  23. flowyml/ui/backend/routers/pipelines.py +19 -10
  24. flowyml/ui/backend/routers/runs.py +287 -9
  25. flowyml/ui/backend/routers/schedules.py +5 -21
  26. flowyml/ui/backend/routers/stats.py +14 -0
  27. flowyml/ui/backend/routers/traces.py +37 -53
  28. flowyml/ui/backend/routers/websocket.py +121 -0
  29. flowyml/ui/frontend/dist/assets/index-CBUXOWze.css +1 -0
  30. flowyml/ui/frontend/dist/assets/index-DF8dJaFL.js +629 -0
  31. flowyml/ui/frontend/dist/index.html +2 -2
  32. flowyml/ui/frontend/package-lock.json +289 -0
  33. flowyml/ui/frontend/package.json +1 -0
  34. flowyml/ui/frontend/src/app/compare/page.jsx +213 -0
  35. flowyml/ui/frontend/src/app/experiments/compare/page.jsx +289 -0
  36. flowyml/ui/frontend/src/app/experiments/page.jsx +61 -1
  37. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +418 -203
  38. flowyml/ui/frontend/src/app/runs/page.jsx +64 -3
  39. flowyml/ui/frontend/src/app/settings/page.jsx +1 -1
  40. flowyml/ui/frontend/src/app/tokens/page.jsx +8 -6
  41. flowyml/ui/frontend/src/components/ArtifactViewer.jsx +159 -0
  42. flowyml/ui/frontend/src/components/NavigationTree.jsx +26 -9
  43. flowyml/ui/frontend/src/components/PipelineGraph.jsx +26 -24
  44. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +42 -14
  45. flowyml/ui/frontend/src/router/index.jsx +4 -0
  46. {flowyml-1.3.0.dist-info → flowyml-1.5.0.dist-info}/METADATA +3 -1
  47. {flowyml-1.3.0.dist-info → flowyml-1.5.0.dist-info}/RECORD +50 -42
  48. flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +0 -1
  49. flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +0 -592
  50. {flowyml-1.3.0.dist-info → flowyml-1.5.0.dist-info}/WHEEL +0 -0
  51. {flowyml-1.3.0.dist-info → flowyml-1.5.0.dist-info}/entry_points.txt +0 -0
  52. {flowyml-1.3.0.dist-info → flowyml-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
  import { fetchApi } from '../../utils/api';
3
- import { Link, useSearchParams } from 'react-router-dom';
4
- import { PlayCircle, Clock, CheckCircle, XCircle, Activity, ArrowRight, Calendar, Filter, RefreshCw, Layout } from 'lucide-react';
3
+ import { Link, useSearchParams, useNavigate } from 'react-router-dom';
4
+ import { PlayCircle, Clock, CheckCircle, XCircle, Activity, ArrowRight, Calendar, Filter, RefreshCw, Layout, GitCompare, CheckSquare, X } from 'lucide-react';
5
5
  import { Card } from '../../components/ui/Card';
6
6
  import { Badge } from '../../components/ui/Badge';
7
7
  import { Button } from '../../components/ui/Button';
@@ -17,8 +17,14 @@ export function Runs() {
17
17
  const [loading, setLoading] = useState(true);
18
18
  const [selectedRun, setSelectedRun] = useState(null);
19
19
  const [searchParams] = useSearchParams();
20
+ const navigate = useNavigate();
20
21
  const { selectedProject } = useProject();
21
22
 
23
+ // Selection & Comparison State
24
+ const [selectionMode, setSelectionMode] = useState('single');
25
+ const [selectedRunIds, setSelectedRunIds] = useState([]);
26
+
27
+
22
28
  // Filter states
23
29
  const [statusFilter, setStatusFilter] = useState('all');
24
30
  const pipelineFilter = searchParams.get('pipeline');
@@ -49,7 +55,33 @@ export function Runs() {
49
55
  }, [selectedProject, pipelineFilter]);
50
56
 
51
57
  const handleRunSelect = (run) => {
52
- setSelectedRun(run);
58
+ if (selectionMode === 'single') {
59
+ setSelectedRun(run);
60
+ }
61
+ };
62
+
63
+ const toggleSelectionMode = () => {
64
+ if (selectionMode === 'single') {
65
+ setSelectionMode('multi');
66
+ setSelectedRunIds([]);
67
+ } else {
68
+ setSelectionMode('single');
69
+ setSelectedRunIds([]);
70
+ }
71
+ };
72
+
73
+ const handleMultiSelect = (runId, checked) => {
74
+ if (checked) {
75
+ setSelectedRunIds(prev => [...prev, runId]);
76
+ } else {
77
+ setSelectedRunIds(prev => prev.filter(id => id !== runId));
78
+ }
79
+ };
80
+
81
+ const handleCompare = () => {
82
+ if (selectedRunIds.length >= 2) {
83
+ navigate(`/compare?runs=${selectedRunIds.join(',')}`);
84
+ }
53
85
  };
54
86
 
55
87
  return (
@@ -67,6 +99,32 @@ export function Runs() {
67
99
  </p>
68
100
  </div>
69
101
  <div className="flex items-center gap-3">
102
+ {selectionMode === 'multi' ? (
103
+ <>
104
+ <span className="text-sm text-slate-500 mr-2">
105
+ {selectedRunIds.length} selected
106
+ </span>
107
+ <Button
108
+ size="sm"
109
+ variant="primary"
110
+ disabled={selectedRunIds.length < 2}
111
+ onClick={handleCompare}
112
+ >
113
+ <GitCompare size={16} className="mr-2" />
114
+ Compare
115
+ </Button>
116
+ <Button variant="ghost" size="sm" onClick={toggleSelectionMode}>
117
+ <X size={16} className="mr-2" />
118
+ Cancel
119
+ </Button>
120
+ </>
121
+ ) : (
122
+ <Button variant="outline" size="sm" onClick={toggleSelectionMode}>
123
+ <CheckSquare size={16} className="mr-2" />
124
+ Select to Compare
125
+ </Button>
126
+ )}
127
+
70
128
  <Button variant="outline" size="sm" onClick={fetchRuns} disabled={loading}>
71
129
  <RefreshCw size={16} className={`mr-2 ${loading ? 'animate-spin' : ''}`} />
72
130
  Refresh
@@ -95,6 +153,9 @@ export function Runs() {
95
153
  projectId={selectedProject}
96
154
  onSelect={handleRunSelect}
97
155
  selectedId={selectedRun?.run_id}
156
+ selectionMode={selectionMode}
157
+ selectedIds={selectedRunIds}
158
+ onMultiSelect={handleMultiSelect}
98
159
  />
99
160
  </div>
100
161
  </div>
@@ -146,7 +146,7 @@ export function Settings() {
146
146
  </Button>
147
147
  </Card>
148
148
  ) : (
149
- tokens.map((token) => (
149
+ Array.isArray(tokens) && tokens.map((token) => (
150
150
  <Card key={token.id} className="hover:shadow-lg transition-all duration-200">
151
151
  <div className="flex items-center justify-between gap-4">
152
152
  <div className="flex-1 min-w-0">
@@ -42,6 +42,7 @@ export function TokenManagement() {
42
42
  return;
43
43
  }
44
44
  const data = await res.json();
45
+ console.log("Tokens fetched:", data);
45
46
  setTokens(data.tokens || []);
46
47
  } catch (err) {
47
48
  console.error('Failed to fetch tokens:', err);
@@ -127,7 +128,7 @@ export function TokenManagement() {
127
128
  )}
128
129
 
129
130
  {/* Tokens List */}
130
- {tokens.length > 0 && (
131
+ {Array.isArray(tokens) && tokens.length > 0 && (
131
132
  <Card>
132
133
  <CardHeader>
133
134
  <div className="flex items-center justify-between">
@@ -210,7 +211,7 @@ function TokenItem({ token, onRevoke }) {
210
211
  const handleRevoke = async () => {
211
212
  try {
212
213
  // This would need to be implemented in the backend
213
- await fetch(`/ api / execution / tokens / ${token.name} `, {
214
+ await fetchApi(`/api/execution/tokens/${token.name}`, {
214
215
  method: 'DELETE'
215
216
  });
216
217
  onRevoke();
@@ -232,7 +233,7 @@ function TokenItem({ token, onRevoke }) {
232
233
  )}
233
234
  </div>
234
235
  <div className="flex flex-wrap gap-2 mb-3">
235
- {token.permissions?.length ? (
236
+ {Array.isArray(token.permissions) && token.permissions.length > 0 ? (
236
237
  token.permissions.map(perm => (
237
238
  <PermissionChip key={perm} perm={perm} />
238
239
  ))
@@ -347,9 +348,10 @@ function CreateTokenModal({ onClose, onCreate }) {
347
348
  try {
348
349
  const res = await fetchApi('/api/projects/');
349
350
  const data = await res.json();
350
- setProjects(data || []);
351
+ setProjects(data.projects || []);
351
352
  } catch (err) {
352
353
  console.error('Failed to fetch projects:', err);
354
+ setProjects([]);
353
355
  } finally {
354
356
  setLoadingProjects(false);
355
357
  }
@@ -364,7 +366,7 @@ function CreateTokenModal({ onClose, onCreate }) {
364
366
  setLoading(true);
365
367
  setError(null);
366
368
  try {
367
- const res = await fetch('/api/execution/tokens', {
369
+ const res = await fetchApi('/api/execution/tokens', {
368
370
  method: 'POST',
369
371
  headers: { 'Content-Type': 'application/json' },
370
372
  body: JSON.stringify({
@@ -448,7 +450,7 @@ function CreateTokenModal({ onClose, onCreate }) {
448
450
  className="w-full px-3 py-2 border border-slate-200 dark:border-slate-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 bg-white dark:bg-slate-900 text-slate-900 dark:text-white"
449
451
  >
450
452
  <option value="">All Projects</option>
451
- {projects.map(proj => (
453
+ {Array.isArray(projects) && projects.map(proj => (
452
454
  <option key={proj.name} value={proj.name}>
453
455
  {proj.name}
454
456
  </option>
@@ -0,0 +1,159 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { fetchApi } from '../utils/api';
3
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
4
+ import json from 'react-syntax-highlighter/dist/esm/languages/hljs/json';
5
+ import python from 'react-syntax-highlighter/dist/esm/languages/hljs/python';
6
+ import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
7
+ import {
8
+ Image,
9
+ FileText,
10
+ Code,
11
+ Table as TableIcon,
12
+ AlertCircle,
13
+ Download
14
+ } from 'lucide-react';
15
+ import { Button } from './ui/Button'; // Assuming you have a Button component, or use native button if not
16
+
17
+ SyntaxHighlighter.registerLanguage('json', json);
18
+ SyntaxHighlighter.registerLanguage('python', python);
19
+
20
+ export function ArtifactViewer({ artifact }) {
21
+ const [content, setContent] = useState(null);
22
+ const [contentType, setContentType] = useState(null);
23
+ const [loading, setLoading] = useState(false);
24
+ const [error, setError] = useState(null);
25
+
26
+ useEffect(() => {
27
+ if (!artifact?.artifact_id) return;
28
+ fetchContent();
29
+ }, [artifact]);
30
+
31
+ const fetchContent = async () => {
32
+ setLoading(true);
33
+ setError(null);
34
+ try {
35
+ // First try to infer type from artifact metadata
36
+ let type = 'text';
37
+ if (artifact.path) {
38
+ const ext = artifact.path.split('.').pop().toLowerCase();
39
+ if (['png', 'jpg', 'jpeg', 'svg', 'gif'].includes(ext)) type = 'image';
40
+ else if (['json'].includes(ext)) type = 'json';
41
+ else if (['csv', 'tsv'].includes(ext)) type = 'table';
42
+ }
43
+
44
+ // If it's a value-based artifact (no file path), use the value directly if possible
45
+ if (!artifact.path && artifact.value) {
46
+ try {
47
+ const parsed = JSON.parse(artifact.value);
48
+ setContent(parsed);
49
+ setContentType('json');
50
+ } catch {
51
+ setContent(artifact.value);
52
+ setContentType('text');
53
+ }
54
+ setLoading(false);
55
+ return;
56
+ }
57
+
58
+ // Fetch actual content from backend
59
+ const res = await fetchApi(`/api/assets/${artifact.artifact_id}/content`);
60
+ if (!res.ok) {
61
+ // If content fetch fails, fallback to 'value' preview if available
62
+ if (artifact.value) {
63
+ setContent(artifact.value);
64
+ setContentType('text');
65
+ } else {
66
+ throw new Error('Failed to load artifact content');
67
+ }
68
+ } else {
69
+ const blob = await res.blob();
70
+ const mime = blob.type;
71
+
72
+ if (mime.startsWith('image/') || type === 'image') {
73
+ const url = URL.createObjectURL(blob);
74
+ setContent(url);
75
+ setContentType('image');
76
+ } else if (mime.includes('json') || type === 'json') {
77
+ const text = await blob.text();
78
+ setContent(JSON.parse(text));
79
+ setContentType('json');
80
+ } else {
81
+ const text = await blob.text();
82
+ setContent(text);
83
+ setContentType('text');
84
+ }
85
+ }
86
+
87
+ } catch (err) {
88
+ console.error('Failed to load artifact:', err);
89
+ setError('Could not load full content. Showing metadata only.');
90
+ } finally {
91
+ setLoading(false);
92
+ }
93
+ };
94
+
95
+ if (loading) {
96
+ return (
97
+ <div className="flex flex-col items-center justify-center p-12 text-slate-400">
98
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-500 mb-2"></div>
99
+ <p className="text-sm">Loading content...</p>
100
+ </div>
101
+ );
102
+ }
103
+
104
+ if (error) {
105
+ return (
106
+ <div className="flex flex-col items-center justify-center p-8 text-rose-500 bg-rose-50 rounded-lg border border-rose-100">
107
+ <AlertCircle size={24} className="mb-2" />
108
+ <p className="text-sm font-medium">{error}</p>
109
+ </div>
110
+ );
111
+ }
112
+
113
+ if (contentType === 'image') {
114
+ return (
115
+ <div className="flex flex-col items-center bg-slate-50 dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-700">
116
+ <div className="relative rounded-lg overflow-hidden shadow-sm bg-white dark:bg-black">
117
+ <img src={content} alt={artifact.name} className="max-w-full max-h-[60vh] object-contain" />
118
+ </div>
119
+ <p className="text-xs text-slate-500 dark:text-slate-400 mt-2 flex items-center gap-1">
120
+ <Image size={12} />
121
+ Image Preview
122
+ </p>
123
+ </div>
124
+ );
125
+ }
126
+
127
+ if (contentType === 'json') {
128
+ return (
129
+ <div className="rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700 shadow-sm">
130
+ <div className="bg-slate-50 dark:bg-slate-800 px-3 py-2 border-b border-slate-200 dark:border-slate-700 flex items-center gap-2">
131
+ <Code size={14} className="text-slate-500 dark:text-slate-400" />
132
+ <span className="text-xs font-semibold text-slate-600 dark:text-slate-300">JSON Viewer</span>
133
+ </div>
134
+ <div className="max-h-[50vh] overflow-y-auto bg-white">
135
+ <SyntaxHighlighter
136
+ language="json"
137
+ style={docco}
138
+ customStyle={{ margin: 0, padding: '1rem', fontSize: '0.85rem' }}
139
+ >
140
+ {JSON.stringify(content, null, 2)}
141
+ </SyntaxHighlighter>
142
+ </div>
143
+ </div>
144
+ );
145
+ }
146
+
147
+ // Default Text Viewer
148
+ return (
149
+ <div className="rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700 shadow-sm">
150
+ <div className="bg-slate-50 dark:bg-slate-800 px-3 py-2 border-b border-slate-200 dark:border-slate-700 flex items-center gap-2">
151
+ <FileText size={14} className="text-slate-500 dark:text-slate-400" />
152
+ <span className="text-xs font-semibold text-slate-600 dark:text-slate-300">Text Content</span>
153
+ </div>
154
+ <pre className="p-4 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-200 text-xs font-mono overflow-x-auto whitespace-pre-wrap max-h-[50vh] overflow-y-auto">
155
+ {typeof content === 'string' ? content : JSON.stringify(content, null, 2)}
156
+ </pre>
157
+ </div>
158
+ );
159
+ }
@@ -53,7 +53,10 @@ const TreeNode = ({
53
53
  badge,
54
54
  onClick,
55
55
  isActive = false,
56
- subLabel
56
+ subLabel,
57
+ checkable = false,
58
+ checked = false,
59
+ onCheck
57
60
  }) => {
58
61
  const [isExpanded, setIsExpanded] = useState(defaultExpanded);
59
62
  const hasChildren = children && children.length > 0;
@@ -78,6 +81,18 @@ const TreeNode = ({
78
81
  onClick?.();
79
82
  }}
80
83
  >
84
+ {/* Checkbox for selection mode */}
85
+ {checkable && (
86
+ <div className="shrink-0 mr-1" onClick={(e) => e.stopPropagation()}>
87
+ <input
88
+ type="checkbox"
89
+ checked={checked}
90
+ onChange={(e) => onCheck?.(e.target.checked)}
91
+ className="w-4 h-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500 cursor-pointer accent-blue-600"
92
+ />
93
+ </div>
94
+ )}
95
+
81
96
  <div className="flex items-center gap-1 text-slate-400 shrink-0">
82
97
  {hasChildren ? (
83
98
  <motion.div
@@ -135,7 +150,10 @@ export function NavigationTree({
135
150
  onSelect,
136
151
  selectedId,
137
152
  mode = 'experiments', // experiments, pipelines, runs
138
- className = ''
153
+ className = '',
154
+ selectionMode = 'single', // 'single' | 'multi'
155
+ selectedIds = [], // Array of IDs for multi-select
156
+ onMultiSelect // Callback for multi-select (id, checked)
139
157
  }) {
140
158
  const [data, setData] = useState({ projects: [], items: [] });
141
159
  const [loading, setLoading] = useState(true);
@@ -238,13 +256,6 @@ export function NavigationTree({
238
256
  );
239
257
 
240
258
  const renderExperiments = () => {
241
- const getRunsForExperiment = (expName) => {
242
- // This would ideally come from the API or be passed in,
243
- // but for now we might not have runs loaded here.
244
- // We'll just show the experiment node.
245
- return [];
246
- };
247
-
248
259
  const renderExperimentNode = (exp, level) => (
249
260
  <TreeNode
250
261
  key={exp.experiment_id}
@@ -253,6 +264,9 @@ export function NavigationTree({
253
264
  level={level}
254
265
  isActive={selectedId === exp.experiment_id}
255
266
  onClick={() => onSelect?.(exp)}
267
+ checkable={selectionMode === 'multi'}
268
+ checked={selectedIds?.includes(exp.experiment_id)}
269
+ onCheck={(checked) => onMultiSelect?.(exp.experiment_id, checked)}
256
270
  badge={
257
271
  <span className="text-[10px] bg-slate-100 dark:bg-slate-800 text-slate-500 px-1.5 py-0.5 rounded-full">
258
272
  {exp.run_count || 0}
@@ -338,6 +352,9 @@ export function NavigationTree({
338
352
  status={run.status}
339
353
  isActive={selectedId === run.run_id}
340
354
  onClick={() => onSelect?.(run)}
355
+ checkable={selectionMode === 'multi'}
356
+ checked={selectedIds?.includes(run.run_id)}
357
+ onCheck={(checked) => onMultiSelect?.(run.run_id, checked)}
341
358
  />
342
359
  );
343
360
 
@@ -57,7 +57,7 @@ const getLayoutedElements = (nodes, edges, direction = 'TB') => {
57
57
  return { nodes, edges };
58
58
  };
59
59
 
60
- export function PipelineGraph({ dag, steps, selectedStep, onStepSelect }) {
60
+ export function PipelineGraph({ dag, steps, selectedStep, onStepSelect, onArtifactSelect }) {
61
61
  // Transform DAG data to ReactFlow format with Artifact Nodes
62
62
  const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
63
63
  if (!dag || !dag.nodes) return { nodes: [], edges: [] };
@@ -159,8 +159,10 @@ export function PipelineGraph({ dag, steps, selectedStep, onStepSelect }) {
159
159
  const onNodeClick = useCallback((event, node) => {
160
160
  if (node.type === 'step' && onStepSelect) {
161
161
  onStepSelect(node.id);
162
+ } else if (node.type === 'artifact' && onArtifactSelect) {
163
+ onArtifactSelect(node.data.label);
162
164
  }
163
- }, [onStepSelect]);
165
+ }, [onStepSelect, onArtifactSelect]);
164
166
 
165
167
  const nodeTypes = useMemo(() => ({
166
168
  step: CustomStepNode,
@@ -168,7 +170,7 @@ export function PipelineGraph({ dag, steps, selectedStep, onStepSelect }) {
168
170
  }), []);
169
171
 
170
172
  return (
171
- <div className="w-full h-full bg-slate-50/50 rounded-xl border border-slate-200 overflow-hidden">
173
+ <div className="w-full h-full bg-slate-50/50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800 overflow-hidden">
172
174
  <ReactFlow
173
175
  nodes={nodes}
174
176
  edges={edges}
@@ -199,35 +201,35 @@ function CustomStepNode({ data }) {
199
201
  const statusConfig = {
200
202
  success: {
201
203
  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'
204
+ color: 'text-emerald-600 dark:text-emerald-400',
205
+ bg: 'bg-white dark:bg-slate-800',
206
+ border: 'border-emerald-500 dark:border-emerald-500',
207
+ ring: 'ring-emerald-200 dark:ring-emerald-900',
208
+ shadow: 'shadow-emerald-100 dark:shadow-none'
207
209
  },
208
210
  failed: {
209
211
  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'
212
+ color: 'text-rose-600 dark:text-rose-400',
213
+ bg: 'bg-white dark:bg-slate-800',
214
+ border: 'border-rose-500 dark:border-rose-500',
215
+ ring: 'ring-rose-200 dark:ring-rose-900',
216
+ shadow: 'shadow-rose-100 dark:shadow-none'
215
217
  },
216
218
  running: {
217
219
  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'
220
+ color: 'text-amber-600 dark:text-amber-400',
221
+ bg: 'bg-white dark:bg-slate-800',
222
+ border: 'border-amber-500 dark:border-amber-500',
223
+ ring: 'ring-amber-200 dark:ring-amber-900',
224
+ shadow: 'shadow-amber-100 dark:shadow-none'
223
225
  },
224
226
  pending: {
225
227
  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'
228
+ color: 'text-slate-400 dark:text-slate-500',
229
+ bg: 'bg-slate-50 dark:bg-slate-800/50',
230
+ border: 'border-slate-300 dark:border-slate-700',
231
+ ring: 'ring-slate-200 dark:ring-slate-800',
232
+ shadow: 'shadow-slate-100 dark:shadow-none'
231
233
  }
232
234
  };
233
235
 
@@ -343,7 +345,7 @@ function CustomArtifactNode({ data }) {
343
345
 
344
346
  return (
345
347
  <div
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]`}
348
+ 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] cursor-pointer`}
347
349
  style={{ height: artifactNodeHeight }}
348
350
  >
349
351
  <Handle type="target" position={Position.Top} className="!bg-slate-400 !w-2 !h-2" />
@@ -27,6 +27,7 @@ import { ProjectSelector } from './ProjectSelector';
27
27
 
28
28
  export function RunDetailsPanel({ run, onClose }) {
29
29
  const [details, setDetails] = useState(null);
30
+ const [artifacts, setArtifacts] = useState([]);
30
31
  const [loading, setLoading] = useState(false);
31
32
  const [activeTab, setActiveTab] = useState('overview'); // overview, steps, artifacts
32
33
  const [currentProject, setCurrentProject] = useState(run?.project);
@@ -41,9 +42,17 @@ export function RunDetailsPanel({ run, onClose }) {
41
42
  const fetchRunDetails = async () => {
42
43
  setLoading(true);
43
44
  try {
44
- const res = await fetchApi(`/api/runs/${run.run_id}`);
45
- const data = await res.json();
45
+ const [runRes, assetsRes] = await Promise.all([
46
+ fetchApi(`/api/runs/${run.run_id}`),
47
+ fetchApi(`/api/assets?run_id=${run.run_id}`)
48
+ ]);
49
+
50
+ const data = await runRes.json();
51
+ const assetsData = await assetsRes.json();
52
+
46
53
  setDetails(data);
54
+ setArtifacts(assetsData.assets || []);
55
+
47
56
  if (data.project) setCurrentProject(data.project);
48
57
  } catch (error) {
49
58
  console.error('Failed to fetch run details:', error);
@@ -177,11 +186,11 @@ export function RunDetailsPanel({ run, onClose }) {
177
186
  {activeTab === 'overview' && (
178
187
  <div className="space-y-4">
179
188
  {/* DAG Visualization Preview */}
180
- <Card className="p-0 overflow-hidden h-64 border-slate-200 dark:border-slate-700">
181
- <div className="p-3 border-b border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 flex justify-between items-center">
189
+ <div className="rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden h-[400px] flex flex-col bg-white dark:bg-slate-800">
190
+ <div className="p-3 border-b border-slate-100 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 flex justify-between items-center shrink-0">
182
191
  <h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300">Pipeline Graph</h3>
183
192
  </div>
184
- <div className="h-full bg-slate-50/50">
193
+ <div className="flex-1 min-h-0 bg-slate-50/50 dark:bg-slate-900/50">
185
194
  {runData.dag ? (
186
195
  <PipelineGraph
187
196
  dag={runData.dag}
@@ -193,7 +202,7 @@ export function RunDetailsPanel({ run, onClose }) {
193
202
  </div>
194
203
  )}
195
204
  </div>
196
- </Card>
205
+ </div>
197
206
 
198
207
  {/* Error Display if Failed */}
199
208
  {runData.status === 'failed' && runData.error && (
@@ -238,14 +247,33 @@ export function RunDetailsPanel({ run, onClose }) {
238
247
 
239
248
  {activeTab === 'artifacts' && (
240
249
  <div className="space-y-3">
241
- {/* This would need actual artifact data structure */}
242
- <div className="text-center py-8 text-slate-500">
243
- <Box size={32} className="mx-auto mb-2 opacity-50" />
244
- <p>Artifacts view not fully implemented in preview</p>
245
- <Link to={`/runs/${runData.run_id}`} className="text-primary-600 hover:underline text-sm mt-2 inline-block">
246
- View in full details page
247
- </Link>
248
- </div>
250
+ {artifacts.length > 0 ? (
251
+ artifacts.map(art => (
252
+ <div
253
+ key={art.artifact_id}
254
+ className="flex items-center gap-3 p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-primary-300 dark:hover:border-primary-700 transition-colors"
255
+ >
256
+ <div className="p-2 bg-slate-50 dark:bg-slate-700 rounded-md text-slate-500 dark:text-slate-400">
257
+ <FileText size={18} />
258
+ </div>
259
+ <div className="min-w-0 flex-1">
260
+ <p className="text-sm font-semibold text-slate-900 dark:text-white truncate">
261
+ {art.name}
262
+ </p>
263
+ <p className="text-xs text-slate-500 truncate">{art.type}</p>
264
+ </div>
265
+ <Link to={`/runs/${run.run_id}`}>
266
+ <Button variant="ghost" size="sm">
267
+ <ArrowRight size={14} />
268
+ </Button>
269
+ </Link>
270
+ </div>
271
+ ))
272
+ ) : (
273
+ <div className="text-center py-8 text-slate-500">
274
+ No artifacts found for this run.
275
+ </div>
276
+ )}
249
277
  </div>
250
278
  )}
251
279
  </>
@@ -16,6 +16,8 @@ import { Leaderboard } from '../app/leaderboard/page';
16
16
  import { Plugins } from '../app/plugins/page';
17
17
  import { Settings } from '../app/settings/page';
18
18
  import { TokenManagement } from '../app/tokens/page';
19
+ import { RunComparisonPage } from '../app/compare/page';
20
+ import { ExperimentComparisonPage } from '../app/experiments/compare/page';
19
21
 
20
22
  export const router = createBrowserRouter([
21
23
  {
@@ -25,9 +27,11 @@ export const router = createBrowserRouter([
25
27
  { index: true, element: <Dashboard /> },
26
28
  { path: 'pipelines', element: <Pipelines /> },
27
29
  { path: 'runs', element: <Runs /> },
30
+ { path: 'compare', element: <RunComparisonPage /> },
28
31
  { path: 'runs/:runId', element: <RunDetails /> },
29
32
  { path: 'assets', element: <Assets /> },
30
33
  { path: 'experiments', element: <Experiments /> },
34
+ { path: 'experiments/compare', element: <ExperimentComparisonPage /> },
31
35
  { path: 'experiments/:experimentId', element: <ExperimentDetails /> },
32
36
  { path: 'traces', element: <Traces /> },
33
37
  { path: 'projects', element: <Projects /> },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowyml
3
- Version: 1.3.0
3
+ Version: 1.5.0
4
4
  Summary: Next-Generation ML Pipeline Framework
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -36,11 +36,13 @@ Requires-Dist: httpx (>=0.24,<0.28)
36
36
  Requires-Dist: loguru (>=0.7.3,<0.8.0)
37
37
  Requires-Dist: numpy (>=1.20.0)
38
38
  Requires-Dist: pandas (>=1.3.0)
39
+ Requires-Dist: psycopg2-binary (>=2.9.0)
39
40
  Requires-Dist: pydantic (>=2.0.0)
40
41
  Requires-Dist: python-multipart (>=0.0.6) ; extra == "ui" or extra == "all"
41
42
  Requires-Dist: pytz (>=2024.1,<2025.0)
42
43
  Requires-Dist: pyyaml (>=6.0)
43
44
  Requires-Dist: scikit-learn (>=1.0.0) ; extra == "sklearn" or extra == "all"
45
+ Requires-Dist: sqlalchemy (>=2.0.0)
44
46
  Requires-Dist: tensorflow (>=2.12.0) ; extra == "tensorflow" or extra == "all"
45
47
  Requires-Dist: toml (>=0.10.2)
46
48
  Requires-Dist: torch (>=2.0.0) ; extra == "pytorch" or extra == "all"