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.
Files changed (159) hide show
  1. flowyml/__init__.py +207 -0
  2. flowyml/assets/__init__.py +22 -0
  3. flowyml/assets/artifact.py +40 -0
  4. flowyml/assets/base.py +209 -0
  5. flowyml/assets/dataset.py +100 -0
  6. flowyml/assets/featureset.py +301 -0
  7. flowyml/assets/metrics.py +104 -0
  8. flowyml/assets/model.py +82 -0
  9. flowyml/assets/registry.py +157 -0
  10. flowyml/assets/report.py +315 -0
  11. flowyml/cli/__init__.py +5 -0
  12. flowyml/cli/experiment.py +232 -0
  13. flowyml/cli/init.py +256 -0
  14. flowyml/cli/main.py +327 -0
  15. flowyml/cli/run.py +75 -0
  16. flowyml/cli/stack_cli.py +532 -0
  17. flowyml/cli/ui.py +33 -0
  18. flowyml/core/__init__.py +68 -0
  19. flowyml/core/advanced_cache.py +274 -0
  20. flowyml/core/approval.py +64 -0
  21. flowyml/core/cache.py +203 -0
  22. flowyml/core/checkpoint.py +148 -0
  23. flowyml/core/conditional.py +373 -0
  24. flowyml/core/context.py +155 -0
  25. flowyml/core/error_handling.py +419 -0
  26. flowyml/core/executor.py +354 -0
  27. flowyml/core/graph.py +185 -0
  28. flowyml/core/parallel.py +452 -0
  29. flowyml/core/pipeline.py +764 -0
  30. flowyml/core/project.py +253 -0
  31. flowyml/core/resources.py +424 -0
  32. flowyml/core/scheduler.py +630 -0
  33. flowyml/core/scheduler_config.py +32 -0
  34. flowyml/core/step.py +201 -0
  35. flowyml/core/step_grouping.py +292 -0
  36. flowyml/core/templates.py +226 -0
  37. flowyml/core/versioning.py +217 -0
  38. flowyml/integrations/__init__.py +1 -0
  39. flowyml/integrations/keras.py +134 -0
  40. flowyml/monitoring/__init__.py +1 -0
  41. flowyml/monitoring/alerts.py +57 -0
  42. flowyml/monitoring/data.py +102 -0
  43. flowyml/monitoring/llm.py +160 -0
  44. flowyml/monitoring/monitor.py +57 -0
  45. flowyml/monitoring/notifications.py +246 -0
  46. flowyml/registry/__init__.py +5 -0
  47. flowyml/registry/model_registry.py +491 -0
  48. flowyml/registry/pipeline_registry.py +55 -0
  49. flowyml/stacks/__init__.py +27 -0
  50. flowyml/stacks/base.py +77 -0
  51. flowyml/stacks/bridge.py +288 -0
  52. flowyml/stacks/components.py +155 -0
  53. flowyml/stacks/gcp.py +499 -0
  54. flowyml/stacks/local.py +112 -0
  55. flowyml/stacks/migration.py +97 -0
  56. flowyml/stacks/plugin_config.py +78 -0
  57. flowyml/stacks/plugins.py +401 -0
  58. flowyml/stacks/registry.py +226 -0
  59. flowyml/storage/__init__.py +26 -0
  60. flowyml/storage/artifacts.py +246 -0
  61. flowyml/storage/materializers/__init__.py +20 -0
  62. flowyml/storage/materializers/base.py +133 -0
  63. flowyml/storage/materializers/keras.py +185 -0
  64. flowyml/storage/materializers/numpy.py +94 -0
  65. flowyml/storage/materializers/pandas.py +142 -0
  66. flowyml/storage/materializers/pytorch.py +135 -0
  67. flowyml/storage/materializers/sklearn.py +110 -0
  68. flowyml/storage/materializers/tensorflow.py +152 -0
  69. flowyml/storage/metadata.py +931 -0
  70. flowyml/tracking/__init__.py +1 -0
  71. flowyml/tracking/experiment.py +211 -0
  72. flowyml/tracking/leaderboard.py +191 -0
  73. flowyml/tracking/runs.py +145 -0
  74. flowyml/ui/__init__.py +15 -0
  75. flowyml/ui/backend/Dockerfile +31 -0
  76. flowyml/ui/backend/__init__.py +0 -0
  77. flowyml/ui/backend/auth.py +163 -0
  78. flowyml/ui/backend/main.py +187 -0
  79. flowyml/ui/backend/routers/__init__.py +0 -0
  80. flowyml/ui/backend/routers/assets.py +45 -0
  81. flowyml/ui/backend/routers/execution.py +179 -0
  82. flowyml/ui/backend/routers/experiments.py +49 -0
  83. flowyml/ui/backend/routers/leaderboard.py +118 -0
  84. flowyml/ui/backend/routers/notifications.py +72 -0
  85. flowyml/ui/backend/routers/pipelines.py +110 -0
  86. flowyml/ui/backend/routers/plugins.py +192 -0
  87. flowyml/ui/backend/routers/projects.py +85 -0
  88. flowyml/ui/backend/routers/runs.py +66 -0
  89. flowyml/ui/backend/routers/schedules.py +222 -0
  90. flowyml/ui/backend/routers/traces.py +84 -0
  91. flowyml/ui/frontend/Dockerfile +20 -0
  92. flowyml/ui/frontend/README.md +315 -0
  93. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
  94. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
  95. flowyml/ui/frontend/dist/index.html +16 -0
  96. flowyml/ui/frontend/index.html +15 -0
  97. flowyml/ui/frontend/nginx.conf +26 -0
  98. flowyml/ui/frontend/package-lock.json +3545 -0
  99. flowyml/ui/frontend/package.json +33 -0
  100. flowyml/ui/frontend/postcss.config.js +6 -0
  101. flowyml/ui/frontend/src/App.jsx +21 -0
  102. flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
  103. flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
  104. flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
  105. flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
  106. flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
  107. flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
  108. flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
  109. flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
  110. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
  111. flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
  112. flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
  113. flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
  114. flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
  115. flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
  116. flowyml/ui/frontend/src/components/Layout.jsx +108 -0
  117. flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
  118. flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
  119. flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
  120. flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
  121. flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
  122. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
  123. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
  124. flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
  125. flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
  126. flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
  127. flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
  128. flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
  129. flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
  130. flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
  131. flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
  132. flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
  133. flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
  134. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
  135. flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
  136. flowyml/ui/frontend/src/index.css +11 -0
  137. flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
  138. flowyml/ui/frontend/src/main.jsx +10 -0
  139. flowyml/ui/frontend/src/router/index.jsx +39 -0
  140. flowyml/ui/frontend/src/services/pluginService.js +90 -0
  141. flowyml/ui/frontend/src/utils/api.js +47 -0
  142. flowyml/ui/frontend/src/utils/cn.js +6 -0
  143. flowyml/ui/frontend/tailwind.config.js +31 -0
  144. flowyml/ui/frontend/vite.config.js +21 -0
  145. flowyml/ui/utils.py +77 -0
  146. flowyml/utils/__init__.py +67 -0
  147. flowyml/utils/config.py +308 -0
  148. flowyml/utils/debug.py +240 -0
  149. flowyml/utils/environment.py +346 -0
  150. flowyml/utils/git.py +319 -0
  151. flowyml/utils/logging.py +61 -0
  152. flowyml/utils/performance.py +314 -0
  153. flowyml/utils/stack_config.py +296 -0
  154. flowyml/utils/validation.py +270 -0
  155. flowyml-1.1.0.dist-info/METADATA +372 -0
  156. flowyml-1.1.0.dist-info/RECORD +159 -0
  157. flowyml-1.1.0.dist-info/WHEEL +4 -0
  158. flowyml-1.1.0.dist-info/entry_points.txt +3 -0
  159. 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
+ }