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,585 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Calendar, Play, Pause, Trash2, Plus, Clock, CheckCircle, XCircle, Activity, Globe, History, AlertCircle } from 'lucide-react';
|
|
3
|
+
import { format } from 'date-fns';
|
|
4
|
+
import { DataView } from '../../components/ui/DataView';
|
|
5
|
+
import { Card } from '../../components/ui/Card';
|
|
6
|
+
import { Badge } from '../../components/ui/Badge';
|
|
7
|
+
import { Button } from '../../components/ui/Button';
|
|
8
|
+
import { useProject } from '../../contexts/ProjectContext';
|
|
9
|
+
import { EmptyState } from '../../components/ui/EmptyState';
|
|
10
|
+
|
|
11
|
+
export function Schedules() {
|
|
12
|
+
const { selectedProject } = useProject();
|
|
13
|
+
const [schedules, setSchedules] = useState([]);
|
|
14
|
+
const [pipelines, setPipelines] = useState({ registered: [], templates: [], metadata: [] });
|
|
15
|
+
const [health, setHealth] = useState(null);
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
18
|
+
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
|
19
|
+
const [selectedSchedule, setSelectedSchedule] = useState(null);
|
|
20
|
+
const [history, setHistory] = useState([]);
|
|
21
|
+
const [historyLoading, setHistoryLoading] = useState(false);
|
|
22
|
+
|
|
23
|
+
// Form state
|
|
24
|
+
const [formData, setFormData] = useState({
|
|
25
|
+
name: '',
|
|
26
|
+
pipeline_name: '',
|
|
27
|
+
schedule_type: 'daily',
|
|
28
|
+
hour: 0,
|
|
29
|
+
minute: 0,
|
|
30
|
+
interval_seconds: 3600,
|
|
31
|
+
cron_expression: '* * * * *',
|
|
32
|
+
timezone: 'UTC',
|
|
33
|
+
project_name: selectedProject || null
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
fetchData();
|
|
38
|
+
const interval = setInterval(fetchData, 10000); // Refresh every 10s
|
|
39
|
+
return () => clearInterval(interval);
|
|
40
|
+
}, [selectedProject]);
|
|
41
|
+
|
|
42
|
+
const fetchData = async () => {
|
|
43
|
+
try {
|
|
44
|
+
const projectParam = selectedProject ? `?project=${selectedProject}` : '';
|
|
45
|
+
const [schedulesRes, pipelinesRes, healthRes] = await Promise.all([
|
|
46
|
+
fetch(`/api/schedules/${projectParam}`),
|
|
47
|
+
fetch(`/api/schedules/registered-pipelines${projectParam}`),
|
|
48
|
+
fetch('/api/schedules/health')
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const schedulesData = await schedulesRes.json();
|
|
52
|
+
const pipelinesData = await pipelinesRes.json();
|
|
53
|
+
|
|
54
|
+
let healthData = null;
|
|
55
|
+
if (healthRes.ok) {
|
|
56
|
+
healthData = await healthRes.json();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setSchedules(schedulesData);
|
|
60
|
+
setPipelines(pipelinesData);
|
|
61
|
+
setHealth(healthData);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Failed to fetch data:', error);
|
|
64
|
+
} finally {
|
|
65
|
+
setLoading(false);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const fetchHistory = async (scheduleName) => {
|
|
70
|
+
setHistoryLoading(true);
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(`/api/schedules/${scheduleName}/history`);
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
setHistory(data);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Failed to fetch history:', error);
|
|
77
|
+
} finally {
|
|
78
|
+
setHistoryLoading(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const openHistory = (schedule) => {
|
|
83
|
+
setSelectedSchedule(schedule);
|
|
84
|
+
setShowHistoryModal(true);
|
|
85
|
+
fetchHistory(schedule.pipeline_name);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const createSchedule = async (e) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetch('/api/schedules', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
...formData,
|
|
96
|
+
project_name: selectedProject || null
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (response.ok) {
|
|
101
|
+
setShowCreateModal(false);
|
|
102
|
+
setFormData({
|
|
103
|
+
name: '',
|
|
104
|
+
pipeline_name: '',
|
|
105
|
+
schedule_type: 'daily',
|
|
106
|
+
hour: 0,
|
|
107
|
+
minute: 0,
|
|
108
|
+
interval_seconds: 3600,
|
|
109
|
+
cron_expression: '* * * * *',
|
|
110
|
+
timezone: 'UTC'
|
|
111
|
+
});
|
|
112
|
+
fetchData();
|
|
113
|
+
} else {
|
|
114
|
+
const error = await response.json();
|
|
115
|
+
alert(`Failed to create schedule: ${error.detail}`);
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('Failed to create schedule:', error);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const toggleSchedule = async (name, enabled) => {
|
|
123
|
+
try {
|
|
124
|
+
const action = enabled ? 'disable' : 'enable';
|
|
125
|
+
await fetch(`/api/schedules/${name}/${action}`, { method: 'POST' });
|
|
126
|
+
fetchData();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Failed to toggle schedule:', error);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const deleteSchedule = async (name) => {
|
|
133
|
+
if (!confirm(`Delete schedule "${name}"?`)) return;
|
|
134
|
+
try {
|
|
135
|
+
await fetch(`/api/schedules/${name}`, { method: 'DELETE' });
|
|
136
|
+
fetchData();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('Failed to delete schedule:', error);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const columns = [
|
|
143
|
+
{
|
|
144
|
+
header: 'Pipeline',
|
|
145
|
+
key: 'pipeline_name',
|
|
146
|
+
sortable: true,
|
|
147
|
+
render: (schedule) => (
|
|
148
|
+
<div className="flex items-center gap-3">
|
|
149
|
+
<div className={`p-2 rounded-lg ${schedule.enabled ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-100 text-slate-400'}`}>
|
|
150
|
+
<Clock size={16} />
|
|
151
|
+
</div>
|
|
152
|
+
<div>
|
|
153
|
+
<div className="font-medium text-slate-900 dark:text-white">{schedule.pipeline_name}</div>
|
|
154
|
+
<div className="text-xs text-slate-500 flex items-center gap-1">
|
|
155
|
+
<Globe size={10} /> {schedule.timezone}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
header: 'Type',
|
|
163
|
+
key: 'schedule_type',
|
|
164
|
+
sortable: true,
|
|
165
|
+
render: (schedule) => (
|
|
166
|
+
<div className="flex flex-col">
|
|
167
|
+
<Badge variant="secondary" className="capitalize w-fit mb-1">
|
|
168
|
+
{schedule.schedule_type}
|
|
169
|
+
</Badge>
|
|
170
|
+
<span className="text-xs font-mono text-slate-500">
|
|
171
|
+
{schedule.schedule_value}
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
)
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
header: 'Next Run',
|
|
178
|
+
key: 'next_run',
|
|
179
|
+
sortable: true,
|
|
180
|
+
render: (schedule) => (
|
|
181
|
+
<div className="flex items-center gap-2 text-slate-500">
|
|
182
|
+
<Calendar size={14} />
|
|
183
|
+
{schedule.next_run ? format(new Date(schedule.next_run), 'MMM d, HH:mm:ss') : 'N/A'}
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
header: 'Status',
|
|
189
|
+
key: 'enabled',
|
|
190
|
+
sortable: true,
|
|
191
|
+
render: (schedule) => (
|
|
192
|
+
<div className={`flex items-center gap-2 text-sm ${schedule.enabled ? 'text-emerald-600' : 'text-slate-400'}`}>
|
|
193
|
+
{schedule.enabled ? <CheckCircle size={14} /> : <XCircle size={14} />}
|
|
194
|
+
<span className="font-medium">{schedule.enabled ? 'Active' : 'Paused'}</span>
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
header: 'Actions',
|
|
200
|
+
key: 'actions',
|
|
201
|
+
render: (schedule) => (
|
|
202
|
+
<div className="flex items-center gap-2">
|
|
203
|
+
<button
|
|
204
|
+
onClick={() => openHistory(schedule)}
|
|
205
|
+
className="p-1.5 rounded-lg bg-blue-50 text-blue-600 hover:bg-blue-100 transition-colors"
|
|
206
|
+
title="History"
|
|
207
|
+
>
|
|
208
|
+
<History size={16} />
|
|
209
|
+
</button>
|
|
210
|
+
<button
|
|
211
|
+
onClick={() => toggleSchedule(schedule.pipeline_name, schedule.enabled)}
|
|
212
|
+
className={`p-1.5 rounded-lg transition-colors ${schedule.enabled
|
|
213
|
+
? 'bg-amber-50 text-amber-600 hover:bg-amber-100'
|
|
214
|
+
: 'bg-emerald-50 text-emerald-600 hover:bg-emerald-100'
|
|
215
|
+
}`}
|
|
216
|
+
title={schedule.enabled ? 'Pause' : 'Resume'}
|
|
217
|
+
>
|
|
218
|
+
{schedule.enabled ? <Pause size={16} /> : <Play size={16} />}
|
|
219
|
+
</button>
|
|
220
|
+
<button
|
|
221
|
+
onClick={() => deleteSchedule(schedule.pipeline_name)}
|
|
222
|
+
className="p-1.5 rounded-lg bg-rose-50 text-rose-600 hover:bg-rose-100 transition-colors"
|
|
223
|
+
title="Delete"
|
|
224
|
+
>
|
|
225
|
+
<Trash2 size={16} />
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
const renderGrid = (schedule) => (
|
|
233
|
+
<Card className="group hover:shadow-lg transition-all duration-200 border-l-4 border-l-transparent hover:border-l-primary-500 h-full">
|
|
234
|
+
<div className="flex items-start justify-between mb-4">
|
|
235
|
+
<div className="flex items-center gap-3">
|
|
236
|
+
<div className={`p-3 rounded-xl ${schedule.enabled ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-100 text-slate-400'}`}>
|
|
237
|
+
<Clock size={24} />
|
|
238
|
+
</div>
|
|
239
|
+
<div>
|
|
240
|
+
<h3 className="font-bold text-slate-900 dark:text-white truncate max-w-[150px]" title={schedule.pipeline_name}>
|
|
241
|
+
{schedule.pipeline_name}
|
|
242
|
+
</h3>
|
|
243
|
+
<div className={`text-xs font-medium flex items-center gap-1 ${schedule.enabled ? 'text-emerald-600' : 'text-slate-400'}`}>
|
|
244
|
+
{schedule.enabled ? <CheckCircle size={12} /> : <XCircle size={12} />}
|
|
245
|
+
{schedule.enabled ? 'Active' : 'Paused'}
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
<Badge variant="secondary" className="capitalize">
|
|
250
|
+
{schedule.schedule_type}
|
|
251
|
+
</Badge>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<div className="space-y-3 mb-4">
|
|
255
|
+
<div className="flex items-center justify-between text-sm">
|
|
256
|
+
<span className="text-slate-500 flex items-center gap-2"><Calendar size={14} /> Next Run</span>
|
|
257
|
+
<span className="font-mono text-slate-700 dark:text-slate-300">
|
|
258
|
+
{schedule.next_run ? format(new Date(schedule.next_run), 'MMM d, HH:mm') : 'N/A'}
|
|
259
|
+
</span>
|
|
260
|
+
</div>
|
|
261
|
+
<div className="flex items-center justify-between text-sm">
|
|
262
|
+
<span className="text-slate-500 flex items-center gap-2"><Globe size={14} /> Timezone</span>
|
|
263
|
+
<span className="text-slate-700 dark:text-slate-300">
|
|
264
|
+
{schedule.timezone}
|
|
265
|
+
</span>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<div className="flex items-center gap-2 pt-4 border-t border-slate-100 dark:border-slate-700">
|
|
270
|
+
<Button
|
|
271
|
+
variant="ghost"
|
|
272
|
+
className="text-blue-600 hover:bg-blue-50 hover:text-blue-700"
|
|
273
|
+
onClick={() => openHistory(schedule)}
|
|
274
|
+
title="History"
|
|
275
|
+
>
|
|
276
|
+
<History size={16} />
|
|
277
|
+
</Button>
|
|
278
|
+
<Button
|
|
279
|
+
variant="outline"
|
|
280
|
+
className={`flex-1 flex items-center justify-center gap-2 ${!schedule.enabled ? 'text-emerald-600 border-emerald-200 hover:bg-emerald-50' : 'text-amber-600 border-amber-200 hover:bg-amber-50'}`}
|
|
281
|
+
onClick={() => toggleSchedule(schedule.pipeline_name, schedule.enabled)}
|
|
282
|
+
>
|
|
283
|
+
{schedule.enabled ? <><Pause size={14} /> Pause</> : <><Play size={14} /> Resume</>}
|
|
284
|
+
</Button>
|
|
285
|
+
<Button
|
|
286
|
+
variant="ghost"
|
|
287
|
+
className="text-rose-600 hover:bg-rose-50 hover:text-rose-700"
|
|
288
|
+
onClick={() => deleteSchedule(schedule.pipeline_name)}
|
|
289
|
+
>
|
|
290
|
+
<Trash2 size={16} />
|
|
291
|
+
</Button>
|
|
292
|
+
</div>
|
|
293
|
+
</Card>
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div className="p-6 max-w-7xl mx-auto">
|
|
298
|
+
{health && health.metrics && (
|
|
299
|
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
|
300
|
+
<Card className="p-4 flex items-center gap-4">
|
|
301
|
+
<div className={`p-3 rounded-full ${health.status === 'running' ? 'bg-emerald-100 text-emerald-600' : 'bg-rose-100 text-rose-600'}`}>
|
|
302
|
+
<Activity size={24} />
|
|
303
|
+
</div>
|
|
304
|
+
<div>
|
|
305
|
+
<div className="text-sm text-slate-500">Scheduler Status</div>
|
|
306
|
+
<div className="text-lg font-bold capitalize">{health.status}</div>
|
|
307
|
+
</div>
|
|
308
|
+
</Card>
|
|
309
|
+
<Card className="p-4">
|
|
310
|
+
<div className="text-sm text-slate-500 mb-1">Total Runs</div>
|
|
311
|
+
<div className="text-2xl font-bold">{health.metrics.total_runs}</div>
|
|
312
|
+
</Card>
|
|
313
|
+
<Card className="p-4">
|
|
314
|
+
<div className="text-sm text-slate-500 mb-1">Success Rate</div>
|
|
315
|
+
<div className="text-2xl font-bold text-emerald-600">
|
|
316
|
+
{(health.metrics.success_rate * 100).toFixed(1)}%
|
|
317
|
+
</div>
|
|
318
|
+
</Card>
|
|
319
|
+
<Card className="p-4">
|
|
320
|
+
<div className="text-sm text-slate-500 mb-1">Active Schedules</div>
|
|
321
|
+
<div className="text-2xl font-bold">{health.enabled_schedules} / {health.num_schedules}</div>
|
|
322
|
+
</Card>
|
|
323
|
+
</div>
|
|
324
|
+
)}
|
|
325
|
+
|
|
326
|
+
<DataView
|
|
327
|
+
title="Schedules"
|
|
328
|
+
subtitle="Manage automated pipeline executions"
|
|
329
|
+
items={schedules}
|
|
330
|
+
loading={loading}
|
|
331
|
+
columns={columns}
|
|
332
|
+
renderGrid={renderGrid}
|
|
333
|
+
actions={
|
|
334
|
+
<Button onClick={() => setShowCreateModal(true)} className="flex items-center gap-2">
|
|
335
|
+
<Plus size={18} />
|
|
336
|
+
New Schedule
|
|
337
|
+
</Button>
|
|
338
|
+
}
|
|
339
|
+
emptyState={
|
|
340
|
+
<EmptyState
|
|
341
|
+
icon={Calendar}
|
|
342
|
+
title="No active schedules"
|
|
343
|
+
description="Automate your pipelines by creating a schedule."
|
|
344
|
+
action={
|
|
345
|
+
<Button onClick={() => setShowCreateModal(true)}>
|
|
346
|
+
Create your first schedule
|
|
347
|
+
</Button>
|
|
348
|
+
}
|
|
349
|
+
/>
|
|
350
|
+
}
|
|
351
|
+
/>
|
|
352
|
+
|
|
353
|
+
{showCreateModal && (
|
|
354
|
+
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
|
355
|
+
<div className="bg-white dark:bg-slate-800 p-6 rounded-2xl w-full max-w-md border border-slate-200 dark:border-slate-700 shadow-2xl animate-in fade-in zoom-in duration-200 max-h-[90vh] overflow-y-auto">
|
|
356
|
+
<h2 className="text-xl font-bold mb-4 text-slate-900 dark:text-white">Create Schedule</h2>
|
|
357
|
+
<form onSubmit={createSchedule}>
|
|
358
|
+
<div className="mb-4">
|
|
359
|
+
<label className="block text-sm font-medium mb-1 text-slate-700 dark:text-slate-300">Schedule Name</label>
|
|
360
|
+
<input
|
|
361
|
+
type="text"
|
|
362
|
+
value={formData.name}
|
|
363
|
+
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
364
|
+
className="w-full px-3 py-2 bg-slate-50 dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-700 focus:ring-2 focus:ring-primary-500 outline-none transition-all"
|
|
365
|
+
required
|
|
366
|
+
placeholder="e.g., daily_etl_job"
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<div className="mb-4">
|
|
371
|
+
<label className="block text-sm font-medium mb-1 text-slate-700 dark:text-slate-300">Pipeline</label>
|
|
372
|
+
<select
|
|
373
|
+
value={formData.pipeline_name}
|
|
374
|
+
onChange={(e) => setFormData({ ...formData, pipeline_name: e.target.value })}
|
|
375
|
+
className="w-full px-3 py-2 bg-slate-50 dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-700 focus:ring-2 focus:ring-primary-500 outline-none transition-all"
|
|
376
|
+
required
|
|
377
|
+
>
|
|
378
|
+
<option value="">Select a pipeline...</option>
|
|
379
|
+
{pipelines.registered.length > 0 && (
|
|
380
|
+
<optgroup label={`Registered Pipelines (${pipelines.registered.length})`}>
|
|
381
|
+
{pipelines.registered.map(p => (
|
|
382
|
+
<option key={p} value={p}>{p}</option>
|
|
383
|
+
))}
|
|
384
|
+
</optgroup>
|
|
385
|
+
)}
|
|
386
|
+
{pipelines.templates.length > 0 && (
|
|
387
|
+
<optgroup label={`Templates (${pipelines.templates.length})`}>
|
|
388
|
+
{pipelines.templates.map(p => (
|
|
389
|
+
<option key={p} value={p}>{p}</option>
|
|
390
|
+
))}
|
|
391
|
+
</optgroup>
|
|
392
|
+
)}
|
|
393
|
+
{pipelines.metadata.length > 0 && (
|
|
394
|
+
<optgroup label={`Historical Pipelines (${pipelines.metadata.length})`}>
|
|
395
|
+
{pipelines.metadata.map(p => (
|
|
396
|
+
<option key={`meta-${p}`} value={p}>{p}</option>
|
|
397
|
+
))}
|
|
398
|
+
</optgroup>
|
|
399
|
+
)}
|
|
400
|
+
</select>
|
|
401
|
+
{pipelines.registered.length === 0 && pipelines.templates.length === 0 && pipelines.metadata.length === 0 && (
|
|
402
|
+
<p className="text-xs text-amber-600 mt-1">
|
|
403
|
+
No pipelines available. Run a pipeline first or register one using @register_pipeline.
|
|
404
|
+
</p>
|
|
405
|
+
)}
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<div className="grid grid-cols-2 gap-4 mb-4">
|
|
409
|
+
<div>
|
|
410
|
+
<label className="block text-sm font-medium mb-1 text-slate-700 dark:text-slate-300">Type</label>
|
|
411
|
+
<select
|
|
412
|
+
value={formData.schedule_type}
|
|
413
|
+
onChange={(e) => setFormData({ ...formData, schedule_type: e.target.value })}
|
|
414
|
+
className="w-full px-3 py-2 bg-slate-50 dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-700 focus:ring-2 focus:ring-primary-500 outline-none transition-all"
|
|
415
|
+
>
|
|
416
|
+
<option value="daily">Daily</option>
|
|
417
|
+
<option value="hourly">Hourly</option>
|
|
418
|
+
<option value="interval">Interval</option>
|
|
419
|
+
<option value="cron">Cron</option>
|
|
420
|
+
</select>
|
|
421
|
+
</div>
|
|
422
|
+
<div>
|
|
423
|
+
<label className="block text-sm font-medium mb-1 text-slate-700 dark:text-slate-300">Timezone</label>
|
|
424
|
+
<select
|
|
425
|
+
value={formData.timezone}
|
|
426
|
+
onChange={(e) => setFormData({ ...formData, timezone: e.target.value })}
|
|
427
|
+
className="w-full px-3 py-2 bg-slate-50 dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-700 focus:ring-2 focus:ring-primary-500 outline-none transition-all"
|
|
428
|
+
>
|
|
429
|
+
<option value="UTC">UTC</option>
|
|
430
|
+
<option value="America/New_York">New York (EST/EDT)</option>
|
|
431
|
+
<option value="America/Los_Angeles">Los Angeles (PST/PDT)</option>
|
|
432
|
+
<option value="Europe/London">London (GMT/BST)</option>
|
|
433
|
+
<option value="Europe/Paris">Paris (CET/CEST)</option>
|
|
434
|
+
<option value="Asia/Tokyo">Tokyo (JST)</option>
|
|
435
|
+
<option value="Asia/Shanghai">Shanghai (CST)</option>
|
|
436
|
+
</select>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
{formData.schedule_type === 'daily' && (
|
|
441
|
+
<div className="grid grid-cols-2 gap-4 mb-4">
|
|
442
|
+
<div>
|
|
443
|
+
<label className="block text-sm font-medium mb-1 text-slate-700 dark:text-slate-300">Hour (0-23)</label>
|
|
444
|
+
<input
|
|
445
|
+
type="number"
|
|
446
|
+
min="0"
|
|
447
|
+
max="23"
|
|
448
|
+
value={formData.hour}
|
|
449
|
+
onChange={(e) => setFormData({ ...formData, hour: parseInt(e.target.value) })}
|
|
450
|
+
className="w-full px-3 py-2 bg-slate-50 dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-700 focus:ring-2 focus:ring-primary-500 outline-none transition-all"
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
<div>
|
|
454
|
+
<label className="block text-sm font-medium mb-1 text-slate-700 dark:text-slate-300">Minute (0-59)</label>
|
|
455
|
+
<input
|
|
456
|
+
type="number"
|
|
457
|
+
min="0"
|
|
458
|
+
max="59"
|
|
459
|
+
value={formData.minute}
|
|
460
|
+
onChange={(e) => setFormData({ ...formData, minute: parseInt(e.target.value) })}
|
|
461
|
+
className="w-full px-3 py-2 bg-slate-50 dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-700 focus:ring-2 focus:ring-primary-500 outline-none transition-all"
|
|
462
|
+
/>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
|
|
467
|
+
{formData.schedule_type === 'hourly' && (
|
|
468
|
+
<div className="mb-4">
|
|
469
|
+
<label className="block text-sm font-medium mb-1 text-slate-700 dark:text-slate-300">Minute (0-59)</label>
|
|
470
|
+
<input
|
|
471
|
+
type="number"
|
|
472
|
+
min="0"
|
|
473
|
+
max="59"
|
|
474
|
+
value={formData.minute}
|
|
475
|
+
onChange={(e) => setFormData({ ...formData, minute: parseInt(e.target.value) })}
|
|
476
|
+
className="w-full px-3 py-2 bg-slate-50 dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-700 focus:ring-2 focus:ring-primary-500 outline-none transition-all"
|
|
477
|
+
/>
|
|
478
|
+
</div>
|
|
479
|
+
)}
|
|
480
|
+
|
|
481
|
+
{formData.schedule_type === 'interval' && (
|
|
482
|
+
<div className="mb-4">
|
|
483
|
+
<label className="block text-sm font-medium mb-1 text-slate-700 dark:text-slate-300">Interval (seconds)</label>
|
|
484
|
+
<input
|
|
485
|
+
type="number"
|
|
486
|
+
min="1"
|
|
487
|
+
value={formData.interval_seconds}
|
|
488
|
+
onChange={(e) => setFormData({ ...formData, interval_seconds: parseInt(e.target.value) })}
|
|
489
|
+
className="w-full px-3 py-2 bg-slate-50 dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-700 focus:ring-2 focus:ring-primary-500 outline-none transition-all"
|
|
490
|
+
/>
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
|
|
494
|
+
{formData.schedule_type === 'cron' && (
|
|
495
|
+
<div className="mb-4">
|
|
496
|
+
<label className="block text-sm font-medium mb-1 text-slate-700 dark:text-slate-300">Cron Expression</label>
|
|
497
|
+
<input
|
|
498
|
+
type="text"
|
|
499
|
+
value={formData.cron_expression}
|
|
500
|
+
onChange={(e) => setFormData({ ...formData, cron_expression: e.target.value })}
|
|
501
|
+
className="w-full px-3 py-2 bg-slate-50 dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-700 focus:ring-2 focus:ring-primary-500 outline-none transition-all font-mono"
|
|
502
|
+
placeholder="* * * * *"
|
|
503
|
+
/>
|
|
504
|
+
<p className="text-xs text-slate-500 mt-1">
|
|
505
|
+
Format: minute hour day month day-of-week
|
|
506
|
+
</p>
|
|
507
|
+
</div>
|
|
508
|
+
)}
|
|
509
|
+
|
|
510
|
+
<div className="flex justify-end gap-3 mt-6">
|
|
511
|
+
<Button
|
|
512
|
+
variant="ghost"
|
|
513
|
+
type="button"
|
|
514
|
+
onClick={() => setShowCreateModal(false)}
|
|
515
|
+
>
|
|
516
|
+
Cancel
|
|
517
|
+
</Button>
|
|
518
|
+
<Button
|
|
519
|
+
type="submit"
|
|
520
|
+
variant="primary"
|
|
521
|
+
>
|
|
522
|
+
Create Schedule
|
|
523
|
+
</Button>
|
|
524
|
+
</div>
|
|
525
|
+
</form>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
)}
|
|
529
|
+
|
|
530
|
+
{showHistoryModal && selectedSchedule && (
|
|
531
|
+
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
|
532
|
+
<div className="bg-white dark:bg-slate-800 p-6 rounded-2xl w-full max-w-2xl border border-slate-200 dark:border-slate-700 shadow-2xl animate-in fade-in zoom-in duration-200 max-h-[90vh] overflow-y-auto">
|
|
533
|
+
<div className="flex items-center justify-between mb-6">
|
|
534
|
+
<div>
|
|
535
|
+
<h2 className="text-xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
|
|
536
|
+
<History size={20} />
|
|
537
|
+
Execution History
|
|
538
|
+
</h2>
|
|
539
|
+
<p className="text-slate-500 text-sm">{selectedSchedule.pipeline_name}</p>
|
|
540
|
+
</div>
|
|
541
|
+
<Button variant="ghost" onClick={() => setShowHistoryModal(false)}>
|
|
542
|
+
<XCircle size={20} />
|
|
543
|
+
</Button>
|
|
544
|
+
</div>
|
|
545
|
+
|
|
546
|
+
{historyLoading ? (
|
|
547
|
+
<div className="flex justify-center py-8">
|
|
548
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-500"></div>
|
|
549
|
+
</div>
|
|
550
|
+
) : history.length > 0 ? (
|
|
551
|
+
<div className="space-y-4">
|
|
552
|
+
{history.map((run, i) => (
|
|
553
|
+
<div key={i} className="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-900 rounded-xl border border-slate-100 dark:border-slate-700">
|
|
554
|
+
<div className="flex items-center gap-4">
|
|
555
|
+
<div className={`p-2 rounded-full ${run.success ? 'bg-emerald-100 text-emerald-600' : 'bg-rose-100 text-rose-600'}`}>
|
|
556
|
+
{run.success ? <CheckCircle size={16} /> : <AlertCircle size={16} />}
|
|
557
|
+
</div>
|
|
558
|
+
<div>
|
|
559
|
+
<div className="font-medium text-slate-900 dark:text-white">
|
|
560
|
+
{format(new Date(run.started_at), 'MMM d, yyyy HH:mm:ss')}
|
|
561
|
+
</div>
|
|
562
|
+
<div className="text-xs text-slate-500">
|
|
563
|
+
Duration: {run.duration_seconds ? `${run.duration_seconds.toFixed(2)}s` : 'N/A'}
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
{!run.success && (
|
|
568
|
+
<div className="text-sm text-rose-600 max-w-xs truncate" title={run.error}>
|
|
569
|
+
{run.error}
|
|
570
|
+
</div>
|
|
571
|
+
)}
|
|
572
|
+
</div>
|
|
573
|
+
))}
|
|
574
|
+
</div>
|
|
575
|
+
) : (
|
|
576
|
+
<div className="text-center py-8 text-slate-500">
|
|
577
|
+
No execution history found.
|
|
578
|
+
</div>
|
|
579
|
+
)}
|
|
580
|
+
</div>
|
|
581
|
+
</div>
|
|
582
|
+
)}
|
|
583
|
+
</div>
|
|
584
|
+
);
|
|
585
|
+
}
|