@microlight/core 0.9.9 → 0.10.0
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.
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { Box, Typography, Card, CardContent, Grid, Table, Sheet, Button, Select, Option, Input, Chip, IconButton } from '@mui/joy';
|
|
5
|
+
import RefreshIcon from '@mui/icons-material/Refresh';
|
|
6
|
+
import SearchIcon from '@mui/icons-material/Search';
|
|
7
|
+
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
|
8
|
+
import StopIcon from '@mui/icons-material/Stop';
|
|
9
|
+
import ErrorIcon from '@mui/icons-material/Error';
|
|
10
|
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
11
|
+
import { getOverviewData, getTasksData, getRunsData, getLogsData } from "./action";
|
|
12
|
+
export default function MonitoringDashboard() {
|
|
13
|
+
const [overview, setOverview] = useState(null);
|
|
14
|
+
const [tasks, setTasks] = useState([]);
|
|
15
|
+
const [recentRuns, setRecentRuns] = useState([]);
|
|
16
|
+
const [logs, setLogs] = useState([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
const [autoRefresh, setAutoRefresh] = useState(false);
|
|
19
|
+
const [filters, setFilters] = useState({
|
|
20
|
+
status: 'all',
|
|
21
|
+
task: '',
|
|
22
|
+
dateRange: '24h'
|
|
23
|
+
});
|
|
24
|
+
const fetchOverview = async () => {
|
|
25
|
+
try {
|
|
26
|
+
const data = await getOverviewData();
|
|
27
|
+
setOverview(data);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('Failed to fetch overview:', error);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const fetchTasks = async () => {
|
|
33
|
+
try {
|
|
34
|
+
const data = await getTasksData();
|
|
35
|
+
setTasks(data);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Failed to fetch tasks:', error);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const DEFAULT_RUNS_LIMIT = 50;
|
|
41
|
+
const DEFAULT_LOGS_LIMIT = 100;
|
|
42
|
+
const fetchRecentRuns = async () => {
|
|
43
|
+
try {
|
|
44
|
+
const data = await getRunsData({
|
|
45
|
+
status: filters.status,
|
|
46
|
+
task: filters.task,
|
|
47
|
+
limit: DEFAULT_RUNS_LIMIT
|
|
48
|
+
});
|
|
49
|
+
setRecentRuns(data);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Failed to fetch recent runs:', error);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const fetchLogs = async () => {
|
|
55
|
+
try {
|
|
56
|
+
const data = await getLogsData({
|
|
57
|
+
limit: DEFAULT_LOGS_LIMIT
|
|
58
|
+
});
|
|
59
|
+
setLogs(data);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Failed to fetch logs:', error);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const refreshAll = async () => {
|
|
65
|
+
setLoading(true);
|
|
66
|
+
await Promise.all([fetchOverview(), fetchTasks(), fetchRecentRuns(), fetchLogs()]);
|
|
67
|
+
setLoading(false);
|
|
68
|
+
};
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
refreshAll();
|
|
71
|
+
}, [filters]);
|
|
72
|
+
const REFRESH_INTERVAL = 30000; // Refresh every 30 seconds
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
let interval;
|
|
76
|
+
if (autoRefresh) {
|
|
77
|
+
interval = setInterval(refreshAll, REFRESH_INTERVAL);
|
|
78
|
+
}
|
|
79
|
+
return () => {
|
|
80
|
+
if (interval) clearInterval(interval);
|
|
81
|
+
};
|
|
82
|
+
}, [autoRefresh]);
|
|
83
|
+
const getStatusColor = status => {
|
|
84
|
+
switch (status) {
|
|
85
|
+
case 'complete':
|
|
86
|
+
return 'success';
|
|
87
|
+
case 'failed':
|
|
88
|
+
return 'danger';
|
|
89
|
+
case 'running':
|
|
90
|
+
return 'primary';
|
|
91
|
+
case 'pending':
|
|
92
|
+
return 'neutral';
|
|
93
|
+
default:
|
|
94
|
+
return 'neutral';
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const getStatusIcon = status => {
|
|
98
|
+
switch (status) {
|
|
99
|
+
case 'complete':
|
|
100
|
+
return <CheckCircleIcon />;
|
|
101
|
+
case 'failed':
|
|
102
|
+
return <ErrorIcon />;
|
|
103
|
+
case 'running':
|
|
104
|
+
return <PlayArrowIcon />;
|
|
105
|
+
case 'pending':
|
|
106
|
+
return <StopIcon />;
|
|
107
|
+
default:
|
|
108
|
+
return <StopIcon />;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const formatDuration = duration => {
|
|
112
|
+
if (!duration) return 'N/A';
|
|
113
|
+
const seconds = Math.floor(duration / 1000);
|
|
114
|
+
const minutes = Math.floor(seconds / 60);
|
|
115
|
+
const hours = Math.floor(minutes / 60);
|
|
116
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
117
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
118
|
+
return `${seconds}s`;
|
|
119
|
+
};
|
|
120
|
+
const formatDate = dateString => {
|
|
121
|
+
if (!dateString) return 'N/A';
|
|
122
|
+
return new Date(dateString).toLocaleString();
|
|
123
|
+
};
|
|
124
|
+
if (loading && !overview) {
|
|
125
|
+
return <Box sx={{
|
|
126
|
+
p: 3
|
|
127
|
+
}}>
|
|
128
|
+
<Typography level="h2">Loading...</Typography>
|
|
129
|
+
</Box>;
|
|
130
|
+
}
|
|
131
|
+
return <Box sx={{
|
|
132
|
+
p: 3
|
|
133
|
+
}}>
|
|
134
|
+
{/* Header */}
|
|
135
|
+
<Box sx={{
|
|
136
|
+
mb: 3,
|
|
137
|
+
display: 'flex',
|
|
138
|
+
justifyContent: 'space-between',
|
|
139
|
+
alignItems: 'center'
|
|
140
|
+
}}>
|
|
141
|
+
<Typography level="h2">Task Monitoring Dashboard</Typography>
|
|
142
|
+
<Box sx={{
|
|
143
|
+
display: 'flex',
|
|
144
|
+
gap: 1,
|
|
145
|
+
alignItems: 'center'
|
|
146
|
+
}}>
|
|
147
|
+
<Button variant={autoRefresh ? 'solid' : 'outlined'} color={autoRefresh ? 'primary' : 'neutral'} onClick={() => setAutoRefresh(!autoRefresh)} size="sm">
|
|
148
|
+
{autoRefresh ? 'Auto-refresh ON' : 'Auto-refresh OFF'}
|
|
149
|
+
</Button>
|
|
150
|
+
<IconButton variant="outlined" onClick={refreshAll} loading={loading} size="sm">
|
|
151
|
+
<RefreshIcon />
|
|
152
|
+
</IconButton>
|
|
153
|
+
</Box>
|
|
154
|
+
</Box>
|
|
155
|
+
|
|
156
|
+
{/* Overview Cards */}
|
|
157
|
+
{overview && <Grid container spacing={2} sx={{
|
|
158
|
+
mb: 3
|
|
159
|
+
}}>
|
|
160
|
+
<Grid xs={12} sm={6} md={3}>
|
|
161
|
+
<Card>
|
|
162
|
+
<CardContent>
|
|
163
|
+
<Typography level="title-sm" color="neutral">Total Runs</Typography>
|
|
164
|
+
<Typography level="h2">{overview.totalRuns?.toLocaleString()}</Typography>
|
|
165
|
+
</CardContent>
|
|
166
|
+
</Card>
|
|
167
|
+
</Grid>
|
|
168
|
+
<Grid xs={12} sm={6} md={3}>
|
|
169
|
+
<Card>
|
|
170
|
+
<CardContent>
|
|
171
|
+
<Typography level="title-sm" color="success">Success Rate</Typography>
|
|
172
|
+
<Typography level="h2" color="success">
|
|
173
|
+
{overview.successRate ? `${overview.successRate.toFixed(1)}%` : 'N/A'}
|
|
174
|
+
</Typography>
|
|
175
|
+
</CardContent>
|
|
176
|
+
</Card>
|
|
177
|
+
</Grid>
|
|
178
|
+
<Grid xs={12} sm={6} md={3}>
|
|
179
|
+
<Card>
|
|
180
|
+
<CardContent>
|
|
181
|
+
<Typography level="title-sm" color="primary">Running Tasks</Typography>
|
|
182
|
+
<Typography level="h2" color="primary">{overview.runningTasks || 0}</Typography>
|
|
183
|
+
</CardContent>
|
|
184
|
+
</Card>
|
|
185
|
+
</Grid>
|
|
186
|
+
<Grid xs={12} sm={6} md={3}>
|
|
187
|
+
<Card>
|
|
188
|
+
<CardContent>
|
|
189
|
+
<Typography level="title-sm" color="danger">Failed (24h)</Typography>
|
|
190
|
+
<Typography level="h2" color="danger">{overview.recentFailures || 0}</Typography>
|
|
191
|
+
</CardContent>
|
|
192
|
+
</Card>
|
|
193
|
+
</Grid>
|
|
194
|
+
</Grid>}
|
|
195
|
+
|
|
196
|
+
{/* Filters */}
|
|
197
|
+
<Box sx={{
|
|
198
|
+
mb: 3,
|
|
199
|
+
display: 'flex',
|
|
200
|
+
gap: 2,
|
|
201
|
+
alignItems: 'center',
|
|
202
|
+
flexWrap: 'wrap'
|
|
203
|
+
}}>
|
|
204
|
+
<Select value={filters.status} onChange={(_, value) => setFilters(prev => ({
|
|
205
|
+
...prev,
|
|
206
|
+
status: value
|
|
207
|
+
}))} size="sm" sx={{
|
|
208
|
+
minWidth: 120
|
|
209
|
+
}}>
|
|
210
|
+
<Option value="all">All Status</Option>
|
|
211
|
+
<Option value="complete">Complete</Option>
|
|
212
|
+
<Option value="failed">Failed</Option>
|
|
213
|
+
<Option value="running">Running</Option>
|
|
214
|
+
<Option value="pending">Pending</Option>
|
|
215
|
+
</Select>
|
|
216
|
+
|
|
217
|
+
<Input placeholder="Filter by task name..." value={filters.task} onChange={e => setFilters(prev => ({
|
|
218
|
+
...prev,
|
|
219
|
+
task: e.target.value
|
|
220
|
+
}))} size="sm" sx={{
|
|
221
|
+
minWidth: 200
|
|
222
|
+
}} startDecorator={<SearchIcon />} />
|
|
223
|
+
|
|
224
|
+
<Select value={filters.dateRange} onChange={(_, value) => setFilters(prev => ({
|
|
225
|
+
...prev,
|
|
226
|
+
dateRange: value
|
|
227
|
+
}))} size="sm" sx={{
|
|
228
|
+
minWidth: 100
|
|
229
|
+
}}>
|
|
230
|
+
<Option value="1h">Last Hour</Option>
|
|
231
|
+
<Option value="24h">Last 24h</Option>
|
|
232
|
+
<Option value="7d">Last 7 days</Option>
|
|
233
|
+
<Option value="30d">Last 30 days</Option>
|
|
234
|
+
</Select>
|
|
235
|
+
</Box>
|
|
236
|
+
|
|
237
|
+
{/* Task Statistics Table */}
|
|
238
|
+
<Box sx={{
|
|
239
|
+
mb: 4
|
|
240
|
+
}}>
|
|
241
|
+
<Typography level="h3" sx={{
|
|
242
|
+
mb: 2
|
|
243
|
+
}}>Task Statistics</Typography>
|
|
244
|
+
<Sheet sx={{
|
|
245
|
+
borderRadius: 'sm',
|
|
246
|
+
overflow: 'auto'
|
|
247
|
+
}}>
|
|
248
|
+
<Table>
|
|
249
|
+
<thead>
|
|
250
|
+
<tr>
|
|
251
|
+
<th>Task Name</th>
|
|
252
|
+
<th>Total Runs</th>
|
|
253
|
+
<th>Success Rate</th>
|
|
254
|
+
<th>Last Run</th>
|
|
255
|
+
<th>Status</th>
|
|
256
|
+
<th>Avg Duration</th>
|
|
257
|
+
</tr>
|
|
258
|
+
</thead>
|
|
259
|
+
<tbody>
|
|
260
|
+
{tasks.map(task => <tr key={task.task}>
|
|
261
|
+
<td>
|
|
262
|
+
<Typography level="body-sm" fontWeight="md">
|
|
263
|
+
{task.task}
|
|
264
|
+
</Typography>
|
|
265
|
+
</td>
|
|
266
|
+
<td>{task.totalRuns}</td>
|
|
267
|
+
<td>
|
|
268
|
+
<Typography level="body-sm" color={task.successRate > 90 ? 'success' : task.successRate > 70 ? 'warning' : 'danger'}>
|
|
269
|
+
{task.successRate.toFixed(1)}%
|
|
270
|
+
</Typography>
|
|
271
|
+
</td>
|
|
272
|
+
<td>
|
|
273
|
+
<Typography level="body-sm">
|
|
274
|
+
{formatDate(task.lastRun)}
|
|
275
|
+
</Typography>
|
|
276
|
+
</td>
|
|
277
|
+
<td>
|
|
278
|
+
<Chip color={getStatusColor(task.lastStatus)} size="sm" startDecorator={getStatusIcon(task.lastStatus)}>
|
|
279
|
+
{task.lastStatus}
|
|
280
|
+
</Chip>
|
|
281
|
+
</td>
|
|
282
|
+
<td>
|
|
283
|
+
<Typography level="body-sm">
|
|
284
|
+
{formatDuration(task.avgDuration)}
|
|
285
|
+
</Typography>
|
|
286
|
+
</td>
|
|
287
|
+
</tr>)}
|
|
288
|
+
</tbody>
|
|
289
|
+
</Table>
|
|
290
|
+
</Sheet>
|
|
291
|
+
</Box>
|
|
292
|
+
|
|
293
|
+
{/* Recent Runs */}
|
|
294
|
+
<Box sx={{
|
|
295
|
+
mb: 4
|
|
296
|
+
}}>
|
|
297
|
+
<Typography level="h3" sx={{
|
|
298
|
+
mb: 2
|
|
299
|
+
}}>Recent Runs</Typography>
|
|
300
|
+
<Sheet sx={{
|
|
301
|
+
borderRadius: 'sm',
|
|
302
|
+
overflow: 'auto',
|
|
303
|
+
maxHeight: 400
|
|
304
|
+
}}>
|
|
305
|
+
<Table>
|
|
306
|
+
<thead>
|
|
307
|
+
<tr>
|
|
308
|
+
<th>Task</th>
|
|
309
|
+
<th>Status</th>
|
|
310
|
+
<th>Started</th>
|
|
311
|
+
<th>Duration</th>
|
|
312
|
+
<th>Triggered By</th>
|
|
313
|
+
</tr>
|
|
314
|
+
</thead>
|
|
315
|
+
<tbody>
|
|
316
|
+
{recentRuns.map(run => <tr key={run.id}>
|
|
317
|
+
<td>
|
|
318
|
+
<Typography level="body-sm" fontWeight="md">
|
|
319
|
+
{run.task}
|
|
320
|
+
</Typography>
|
|
321
|
+
</td>
|
|
322
|
+
<td>
|
|
323
|
+
<Chip color={getStatusColor(run.status)} size="sm" startDecorator={getStatusIcon(run.status)}>
|
|
324
|
+
{run.status}
|
|
325
|
+
</Chip>
|
|
326
|
+
</td>
|
|
327
|
+
<td>
|
|
328
|
+
<Typography level="body-sm">
|
|
329
|
+
{formatDate(run.started_at)}
|
|
330
|
+
</Typography>
|
|
331
|
+
</td>
|
|
332
|
+
<td>
|
|
333
|
+
<Typography level="body-sm">
|
|
334
|
+
{formatDuration(run.duration)}
|
|
335
|
+
</Typography>
|
|
336
|
+
</td>
|
|
337
|
+
<td>
|
|
338
|
+
<Chip size="sm" variant="outlined">
|
|
339
|
+
{run.triggered_by}
|
|
340
|
+
</Chip>
|
|
341
|
+
</td>
|
|
342
|
+
</tr>)}
|
|
343
|
+
</tbody>
|
|
344
|
+
</Table>
|
|
345
|
+
</Sheet>
|
|
346
|
+
</Box>
|
|
347
|
+
|
|
348
|
+
{/* Recent Logs */}
|
|
349
|
+
<Box>
|
|
350
|
+
<Typography level="h3" sx={{
|
|
351
|
+
mb: 2
|
|
352
|
+
}}>Recent Logs</Typography>
|
|
353
|
+
<Sheet sx={{
|
|
354
|
+
borderRadius: 'sm',
|
|
355
|
+
p: 2,
|
|
356
|
+
maxHeight: 300,
|
|
357
|
+
overflow: 'auto',
|
|
358
|
+
bgcolor: 'neutral.50'
|
|
359
|
+
}}>
|
|
360
|
+
{logs.map(log => <Box key={log.id} sx={{
|
|
361
|
+
mb: 1,
|
|
362
|
+
fontFamily: 'monospace',
|
|
363
|
+
fontSize: 'sm'
|
|
364
|
+
}}>
|
|
365
|
+
<Typography level="body-xs" color="neutral">
|
|
366
|
+
[{formatDate(log.created_at)}] [{log.type}]
|
|
367
|
+
</Typography>
|
|
368
|
+
<Typography level="body-sm" sx={{
|
|
369
|
+
whiteSpace: 'pre-wrap'
|
|
370
|
+
}}>
|
|
371
|
+
{log.content}
|
|
372
|
+
</Typography>
|
|
373
|
+
</Box>)}
|
|
374
|
+
</Sheet>
|
|
375
|
+
</Box>
|
|
376
|
+
</Box>;
|
|
377
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import microlightDB from "../../database/microlight/index.js";
|
|
4
|
+
export async function getOverviewData() {
|
|
5
|
+
try {
|
|
6
|
+
// Get overview statistics
|
|
7
|
+
const total_runs_result = await microlightDB.sequelize.query('SELECT COUNT(*) as count FROM runs', {
|
|
8
|
+
type: microlightDB.sequelize.QueryTypes.SELECT
|
|
9
|
+
});
|
|
10
|
+
const status_stats_result = await microlightDB.sequelize.query(`SELECT
|
|
11
|
+
status,
|
|
12
|
+
COUNT(*) as count
|
|
13
|
+
FROM runs
|
|
14
|
+
GROUP BY status`, {
|
|
15
|
+
type: microlightDB.sequelize.QueryTypes.SELECT
|
|
16
|
+
});
|
|
17
|
+
const recent_failures_result = await microlightDB.sequelize.query(`SELECT COUNT(*) as count
|
|
18
|
+
FROM runs
|
|
19
|
+
WHERE status = 'failed'
|
|
20
|
+
AND created_at >= datetime('now', '-24 hours')`, {
|
|
21
|
+
type: microlightDB.sequelize.QueryTypes.SELECT
|
|
22
|
+
});
|
|
23
|
+
const total_runs = total_runs_result[0]?.count || 0;
|
|
24
|
+
const successful_runs = status_stats_result.find(s => s.status === 'complete')?.count || 0;
|
|
25
|
+
const running_tasks = status_stats_result.find(s => s.status === 'running')?.count || 0;
|
|
26
|
+
const recent_failures = recent_failures_result[0]?.count || 0;
|
|
27
|
+
const success_rate = total_runs > 0 ? successful_runs / total_runs * 100 : 0;
|
|
28
|
+
return {
|
|
29
|
+
totalRuns: total_runs,
|
|
30
|
+
successRate: success_rate,
|
|
31
|
+
runningTasks: running_tasks,
|
|
32
|
+
recentFailures: recent_failures,
|
|
33
|
+
statusBreakdown: status_stats_result
|
|
34
|
+
};
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Error fetching overview:', error);
|
|
37
|
+
throw new Error('Failed to fetch overview data');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function getTasksData() {
|
|
41
|
+
try {
|
|
42
|
+
// Get task statistics
|
|
43
|
+
const task_stats_result = await microlightDB.sequelize.query(`SELECT
|
|
44
|
+
task,
|
|
45
|
+
COUNT(*) as totalRuns,
|
|
46
|
+
COUNT(CASE WHEN status = 'complete' THEN 1 END) as successfulRuns,
|
|
47
|
+
COUNT(CASE WHEN status = 'failed' THEN 1 END) as failedRuns,
|
|
48
|
+
MAX(started_at) as lastRun,
|
|
49
|
+
(
|
|
50
|
+
SELECT status
|
|
51
|
+
FROM runs r2
|
|
52
|
+
WHERE r2.task = runs.task
|
|
53
|
+
ORDER BY started_at DESC
|
|
54
|
+
LIMIT 1
|
|
55
|
+
) as lastStatus,
|
|
56
|
+
AVG(duration) as avgDuration
|
|
57
|
+
FROM runs
|
|
58
|
+
GROUP BY task
|
|
59
|
+
ORDER BY totalRuns DESC`, {
|
|
60
|
+
type: microlightDB.sequelize.QueryTypes.SELECT
|
|
61
|
+
});
|
|
62
|
+
return task_stats_result.map(task => ({
|
|
63
|
+
...task,
|
|
64
|
+
successRate: task.totalRuns > 0 ? task.successfulRuns / task.totalRuns * 100 : 0
|
|
65
|
+
}));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Error fetching task statistics:', error);
|
|
68
|
+
throw new Error('Failed to fetch task statistics');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function getRunsData(filters = {}) {
|
|
72
|
+
try {
|
|
73
|
+
const {
|
|
74
|
+
status,
|
|
75
|
+
task,
|
|
76
|
+
limit = 50
|
|
77
|
+
} = filters;
|
|
78
|
+
let where_clause = '';
|
|
79
|
+
const where_conditions = [];
|
|
80
|
+
if (status && status !== 'all') {
|
|
81
|
+
where_conditions.push(`status = '${status}'`);
|
|
82
|
+
}
|
|
83
|
+
if (task && task.trim() !== '') {
|
|
84
|
+
where_conditions.push(`task LIKE '%${task}%'`);
|
|
85
|
+
}
|
|
86
|
+
if (where_conditions.length > 0) {
|
|
87
|
+
where_clause = `WHERE ${where_conditions.join(' AND ')}`;
|
|
88
|
+
}
|
|
89
|
+
const runs_result = await microlightDB.sequelize.query(`SELECT
|
|
90
|
+
id,
|
|
91
|
+
task,
|
|
92
|
+
status,
|
|
93
|
+
started_at,
|
|
94
|
+
completed_at,
|
|
95
|
+
duration,
|
|
96
|
+
triggered_by,
|
|
97
|
+
inputs
|
|
98
|
+
FROM runs
|
|
99
|
+
${where_clause}
|
|
100
|
+
ORDER BY started_at DESC
|
|
101
|
+
LIMIT ${limit}`, {
|
|
102
|
+
type: microlightDB.sequelize.QueryTypes.SELECT
|
|
103
|
+
});
|
|
104
|
+
return runs_result;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Error fetching runs:', error);
|
|
107
|
+
throw new Error('Failed to fetch runs data');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function getLogsData(filters = {}) {
|
|
111
|
+
try {
|
|
112
|
+
const {
|
|
113
|
+
runId,
|
|
114
|
+
type,
|
|
115
|
+
limit = 100
|
|
116
|
+
} = filters;
|
|
117
|
+
let where_clause = '';
|
|
118
|
+
const where_conditions = [];
|
|
119
|
+
if (runId) {
|
|
120
|
+
where_conditions.push(`run = ${runId}`);
|
|
121
|
+
}
|
|
122
|
+
if (type && type !== 'all') {
|
|
123
|
+
where_conditions.push(`type = '${type}'`);
|
|
124
|
+
}
|
|
125
|
+
if (where_conditions.length > 0) {
|
|
126
|
+
where_clause = `WHERE ${where_conditions.join(' AND ')}`;
|
|
127
|
+
}
|
|
128
|
+
const logs_result = await microlightDB.sequelize.query(`SELECT
|
|
129
|
+
l.id,
|
|
130
|
+
l.created_at,
|
|
131
|
+
l.type,
|
|
132
|
+
l.content,
|
|
133
|
+
l.run,
|
|
134
|
+
r.task
|
|
135
|
+
FROM logs l
|
|
136
|
+
LEFT JOIN runs r ON l.run = r.id
|
|
137
|
+
${where_clause}
|
|
138
|
+
ORDER BY l.created_at DESC
|
|
139
|
+
LIMIT ${limit}`, {
|
|
140
|
+
type: microlightDB.sequelize.QueryTypes.SELECT
|
|
141
|
+
});
|
|
142
|
+
return logs_result;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Error fetching logs:', error);
|
|
145
|
+
throw new Error('Failed to fetch logs data');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
3
5
|
// import Logo from '../Logo';
|
|
4
|
-
import { Box, Sheet } from '@mui/joy';
|
|
6
|
+
import { Box, Sheet, Button } from '@mui/joy';
|
|
5
7
|
export default function Navbar({
|
|
6
8
|
user,
|
|
7
9
|
signOut
|
|
8
10
|
}) {
|
|
11
|
+
const pathname = usePathname();
|
|
9
12
|
return <Sheet component="nav" sx={{
|
|
10
13
|
px: 1,
|
|
11
14
|
// py: 0.5,
|
|
@@ -27,10 +30,38 @@ export default function Navbar({
|
|
|
27
30
|
<Box sx={{
|
|
28
31
|
display: 'flex',
|
|
29
32
|
alignItems: 'center',
|
|
30
|
-
gap:
|
|
33
|
+
gap: 2
|
|
31
34
|
}}>
|
|
32
35
|
{/* <Logo offering='Transactions' /> */}
|
|
33
|
-
|
|
36
|
+
<Link href="/" style={{
|
|
37
|
+
textDecoration: 'none',
|
|
38
|
+
color: 'inherit'
|
|
39
|
+
}}>
|
|
40
|
+
<Box sx={{
|
|
41
|
+
fontWeight: 'bold'
|
|
42
|
+
}}>Microlight</Box>
|
|
43
|
+
</Link>
|
|
44
|
+
|
|
45
|
+
<Box sx={{
|
|
46
|
+
display: 'flex',
|
|
47
|
+
gap: 1
|
|
48
|
+
}}>
|
|
49
|
+
<Link href="/library" style={{
|
|
50
|
+
textDecoration: 'none'
|
|
51
|
+
}}>
|
|
52
|
+
<Button variant={pathname?.startsWith('/library') ? 'solid' : 'plain'} size="sm" color="neutral">
|
|
53
|
+
Library
|
|
54
|
+
</Button>
|
|
55
|
+
</Link>
|
|
56
|
+
|
|
57
|
+
<Link href="/monitoring" style={{
|
|
58
|
+
textDecoration: 'none'
|
|
59
|
+
}}>
|
|
60
|
+
<Button variant={pathname?.startsWith('/monitoring') ? 'solid' : 'plain'} size="sm" color="neutral">
|
|
61
|
+
Monitoring
|
|
62
|
+
</Button>
|
|
63
|
+
</Link>
|
|
64
|
+
</Box>
|
|
34
65
|
</Box>
|
|
35
66
|
|
|
36
67
|
</Sheet>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microlight/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -51,7 +51,6 @@
|
|
|
51
51
|
"switchless": "0.19.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@babel/cli": "^7.28.3",
|
|
55
54
|
"@babel/preset-env": "^7.28.0",
|
|
56
55
|
"@babel/preset-react": "^7.27.1",
|
|
57
56
|
"babel-plugin-module-resolver": "^5.0.2",
|