@microlight/core 0.9.9 → 0.11.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.
- package/dist/server/app/layout.js +2 -18
- package/dist/server/app/library/[[...f_path]]/ViewFolder.js +37 -66
- package/dist/server/app/monitoring/MonitoringDashboard.js +329 -0
- package/dist/server/app/monitoring/action.js +147 -0
- package/dist/server/app/monitoring/page.js +8 -0
- package/dist/server/app/tasks/[slug]/ViewTask.js +81 -173
- package/dist/server/app/tasks/[slug]/runs/[r_id]/ViewRun.js +68 -164
- package/dist/server/app/tasks/[slug]/runs/[r_id]/_components/DropdownActions/DropdownActions.js +19 -25
- package/dist/server/components/Link.js +6 -15
- package/dist/server/components/MLInput.js +19 -22
- package/dist/server/components/Navbar/Navbar.js +25 -31
- package/dist/server/components/Navbar/NavbarContainer.js +7 -20
- package/dist/server/components/PageHeader.js +22 -54
- package/dist/server/components/StatusChip.js +5 -5
- package/dist/server/components/ui/alert.js +61 -0
- package/dist/server/components/ui/badge.js +37 -0
- package/dist/server/components/ui/breadcrumb.js +82 -0
- package/dist/server/components/ui/button.js +65 -0
- package/dist/server/components/ui/card.js +81 -0
- package/dist/server/components/ui/dropdown-menu.js +222 -0
- package/dist/server/components/ui/input.js +21 -0
- package/dist/server/components/ui/label.js +20 -0
- package/dist/server/components/ui/select.js +165 -0
- package/dist/server/components/ui/stack.js +104 -0
- package/dist/server/components/ui/table.js +77 -0
- package/dist/server/components/ui/tabs.js +59 -0
- package/dist/server/components/ui/typography.js +229 -0
- package/dist/server/utils/css/cn.js +11 -0
- package/package.json +15 -4
- package/dist/server/components/Icon.js +0 -22
|
@@ -2,35 +2,19 @@ import { Inter } from "next/font/google";
|
|
|
2
2
|
const inter = Inter({
|
|
3
3
|
subsets: ["latin"]
|
|
4
4
|
});
|
|
5
|
+
import "./globals.css";
|
|
5
6
|
import TopLoader from "../components/TopLoader";
|
|
6
7
|
// import ServiceWorkerRegistration from "@/components/ServiceWorkerRegistration";
|
|
7
8
|
import NavbarContainer from "../components/Navbar/NavbarContainer";
|
|
8
9
|
export const metadata = {
|
|
9
10
|
title: "Microlight",
|
|
10
11
|
description: "Simple single server task runner"
|
|
11
|
-
// icons: {
|
|
12
|
-
// icon: [
|
|
13
|
-
// { url: '/favicon.ico' },
|
|
14
|
-
// { url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
|
|
15
|
-
// { url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
|
|
16
|
-
// { url: '/favicon-48x48.png', sizes: '48x48', type: 'image/png' },
|
|
17
|
-
// ],
|
|
18
|
-
// apple: [
|
|
19
|
-
// { url: '/apple-touch-icon.png' },
|
|
20
|
-
// ],
|
|
21
|
-
// },
|
|
22
12
|
};
|
|
23
13
|
export default function RootLayout({
|
|
24
14
|
children
|
|
25
15
|
}) {
|
|
26
16
|
return <html lang="en">
|
|
27
|
-
<
|
|
28
|
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
|
29
|
-
</head>
|
|
30
|
-
<body className={inter.className} style={{
|
|
31
|
-
margin: 0,
|
|
32
|
-
padding: 0
|
|
33
|
-
}}>
|
|
17
|
+
<body className={`${inter.className} m-0 p-0`}>
|
|
34
18
|
{/* <ServiceWorkerRegistration /> */}
|
|
35
19
|
<TopLoader />
|
|
36
20
|
<NavbarContainer>
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Table, Box, Container, Typography } from '@mui/joy';
|
|
4
3
|
import React from 'react';
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
// import Link from '@/components/Link';
|
|
8
|
-
|
|
9
|
-
import { Link } from 'switchless';
|
|
4
|
+
import { Folder, Send } from 'lucide-react';
|
|
5
|
+
import Link from "../../../components/Link";
|
|
10
6
|
import PageHeader from "../../../components/PageHeader";
|
|
7
|
+
import { Typography } from "../../../components/ui/typography";
|
|
8
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../components/ui/table";
|
|
11
9
|
function generateBreadcrumbs({
|
|
12
10
|
params
|
|
13
11
|
}) {
|
|
@@ -47,67 +45,40 @@ export default function ViewFolder({
|
|
|
47
45
|
params
|
|
48
46
|
});
|
|
49
47
|
const dir = params.f_path ? '/' + params.f_path?.join('/') : '';
|
|
50
|
-
return
|
|
51
|
-
<Container>
|
|
48
|
+
return <div className="max-w-7xl mx-auto">
|
|
52
49
|
<PageHeader breadcrumbs={breadcrumbs} header={{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
part1: 'Folder:',
|
|
51
|
+
part2: folder.name
|
|
52
|
+
}} />
|
|
56
53
|
<Typography level='body-sm'>{folder.description}</Typography>
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<Link href={'/library' + '/' + content.slug}>{content.name}</Link>
|
|
83
|
-
</Box>
|
|
84
|
-
</>}
|
|
85
|
-
{content.type == 'task' && <>
|
|
86
|
-
<Box sx={{
|
|
87
|
-
display: 'flex',
|
|
88
|
-
alignItems: 'center',
|
|
89
|
-
gap: 1
|
|
90
|
-
}}>
|
|
91
|
-
{/* <Icon icon='send' color='#6435c9'/> */}
|
|
92
|
-
<i class="fa-solid fa-paper-plane fa-xl" style={{
|
|
93
|
-
color: '#6435c9'
|
|
94
|
-
}}></i>
|
|
95
|
-
<Link href={'/tasks/' + content.slug}>{content.name}</Link>
|
|
96
|
-
</Box>
|
|
97
|
-
</>}
|
|
98
|
-
</td>
|
|
99
|
-
<td>{content.description}</td>
|
|
100
|
-
<td></td>
|
|
101
|
-
</tr>
|
|
102
|
-
</React.Fragment>;
|
|
54
|
+
<div className="mt-2">
|
|
55
|
+
<Table>
|
|
56
|
+
<TableHeader>
|
|
57
|
+
<TableRow>
|
|
58
|
+
<TableHead>Name</TableHead>
|
|
59
|
+
<TableHead>Description</TableHead>
|
|
60
|
+
<TableHead>Tasks</TableHead>
|
|
61
|
+
</TableRow>
|
|
62
|
+
</TableHeader>
|
|
63
|
+
<TableBody>
|
|
64
|
+
{contents.map(content => {
|
|
65
|
+
return <TableRow key={`${content.type}__${content.slug}`}>
|
|
66
|
+
<TableCell>
|
|
67
|
+
{content.type == 'folder' && <div className="flex items-center gap-2">
|
|
68
|
+
<Folder className="h-5 w-5 text-muted-foreground" />
|
|
69
|
+
<Link href={'/library' + '/' + content.slug}>{content.name}</Link>
|
|
70
|
+
</div>}
|
|
71
|
+
{content.type == 'task' && <div className="flex items-center gap-2">
|
|
72
|
+
<Send className="h-5 w-5 text-[#6435c9]" />
|
|
73
|
+
<Link href={'/tasks/' + content.slug}>{content.name}</Link>
|
|
74
|
+
</div>}
|
|
75
|
+
</TableCell>
|
|
76
|
+
<TableCell>{content.description}</TableCell>
|
|
77
|
+
<TableCell></TableCell>
|
|
78
|
+
</TableRow>;
|
|
103
79
|
})}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
{/* <pre>{JSON.stringify(folder,null,2)}</pre> */}
|
|
110
|
-
{/* <pre>{JSON.stringify(contents,null,2)}</pre> */}
|
|
111
|
-
</Container>
|
|
112
|
-
</>;
|
|
80
|
+
</TableBody>
|
|
81
|
+
</Table>
|
|
82
|
+
</div>
|
|
83
|
+
</div>;
|
|
113
84
|
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { RefreshCw, Search, Play, Square, AlertCircle, CheckCircle } from 'lucide-react';
|
|
5
|
+
import { getOverviewData, getTasksData, getRunsData, getLogsData } from "./action";
|
|
6
|
+
import { Typography } from "../../components/ui/typography";
|
|
7
|
+
import { Card, CardContent } from "../../components/ui/card";
|
|
8
|
+
import { Button } from "../../components/ui/button";
|
|
9
|
+
import { Input } from "../../components/ui/input";
|
|
10
|
+
import { Badge } from "../../components/ui/badge";
|
|
11
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../components/ui/select";
|
|
12
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../components/ui/table";
|
|
13
|
+
export default function MonitoringDashboard() {
|
|
14
|
+
const [overview, setOverview] = useState(null);
|
|
15
|
+
const [tasks, setTasks] = useState([]);
|
|
16
|
+
const [recentRuns, setRecentRuns] = useState([]);
|
|
17
|
+
const [logs, setLogs] = useState([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [autoRefresh, setAutoRefresh] = useState(false);
|
|
20
|
+
const [filters, setFilters] = useState({
|
|
21
|
+
status: 'all',
|
|
22
|
+
task: '',
|
|
23
|
+
dateRange: '24h'
|
|
24
|
+
});
|
|
25
|
+
const fetchOverview = async () => {
|
|
26
|
+
try {
|
|
27
|
+
const data = await getOverviewData();
|
|
28
|
+
setOverview(data);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Failed to fetch overview:', error);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const fetchTasks = async () => {
|
|
34
|
+
try {
|
|
35
|
+
const data = await getTasksData();
|
|
36
|
+
setTasks(data);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Failed to fetch tasks:', error);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const DEFAULT_RUNS_LIMIT = 50;
|
|
42
|
+
const DEFAULT_LOGS_LIMIT = 100;
|
|
43
|
+
const fetchRecentRuns = async () => {
|
|
44
|
+
try {
|
|
45
|
+
const data = await getRunsData({
|
|
46
|
+
status: filters.status,
|
|
47
|
+
task: filters.task,
|
|
48
|
+
limit: DEFAULT_RUNS_LIMIT
|
|
49
|
+
});
|
|
50
|
+
setRecentRuns(data);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Failed to fetch recent runs:', error);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const fetchLogs = async () => {
|
|
56
|
+
try {
|
|
57
|
+
const data = await getLogsData({
|
|
58
|
+
limit: DEFAULT_LOGS_LIMIT
|
|
59
|
+
});
|
|
60
|
+
setLogs(data);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Failed to fetch logs:', error);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const refreshAll = async () => {
|
|
66
|
+
setLoading(true);
|
|
67
|
+
await Promise.all([fetchOverview(), fetchTasks(), fetchRecentRuns(), fetchLogs()]);
|
|
68
|
+
setLoading(false);
|
|
69
|
+
};
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
refreshAll();
|
|
72
|
+
}, [filters]);
|
|
73
|
+
const REFRESH_INTERVAL = 30000; // Refresh every 30 seconds
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
let interval;
|
|
77
|
+
if (autoRefresh) {
|
|
78
|
+
interval = setInterval(refreshAll, REFRESH_INTERVAL);
|
|
79
|
+
}
|
|
80
|
+
return () => {
|
|
81
|
+
if (interval) clearInterval(interval);
|
|
82
|
+
};
|
|
83
|
+
}, [autoRefresh]);
|
|
84
|
+
const getStatusVariant = status => {
|
|
85
|
+
switch (status) {
|
|
86
|
+
case 'complete':
|
|
87
|
+
return 'success';
|
|
88
|
+
case 'failed':
|
|
89
|
+
return 'destructive';
|
|
90
|
+
case 'running':
|
|
91
|
+
return 'default';
|
|
92
|
+
case 'pending':
|
|
93
|
+
return 'secondary';
|
|
94
|
+
default:
|
|
95
|
+
return 'secondary';
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const getStatusIcon = status => {
|
|
99
|
+
switch (status) {
|
|
100
|
+
case 'complete':
|
|
101
|
+
return <CheckCircle className="h-3 w-3" />;
|
|
102
|
+
case 'failed':
|
|
103
|
+
return <AlertCircle className="h-3 w-3" />;
|
|
104
|
+
case 'running':
|
|
105
|
+
return <Play className="h-3 w-3" />;
|
|
106
|
+
case 'pending':
|
|
107
|
+
return <Square className="h-3 w-3" />;
|
|
108
|
+
default:
|
|
109
|
+
return <Square className="h-3 w-3" />;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const formatDuration = duration => {
|
|
113
|
+
if (!duration) return 'N/A';
|
|
114
|
+
const seconds = Math.floor(duration / 1000);
|
|
115
|
+
const minutes = Math.floor(seconds / 60);
|
|
116
|
+
const hours = Math.floor(minutes / 60);
|
|
117
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
118
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
119
|
+
return `${seconds}s`;
|
|
120
|
+
};
|
|
121
|
+
const formatDate = dateString => {
|
|
122
|
+
if (!dateString) return 'N/A';
|
|
123
|
+
return new Date(dateString).toLocaleString();
|
|
124
|
+
};
|
|
125
|
+
if (loading && !overview) {
|
|
126
|
+
return <div className="p-6">
|
|
127
|
+
<Typography level="h2">Loading...</Typography>
|
|
128
|
+
</div>;
|
|
129
|
+
}
|
|
130
|
+
return <div className="p-6">
|
|
131
|
+
{/* Header */}
|
|
132
|
+
<div className="mb-6 flex justify-between items-center">
|
|
133
|
+
<Typography level="h2">Task Monitoring Dashboard</Typography>
|
|
134
|
+
<div className="flex gap-2 items-center">
|
|
135
|
+
<Button variant={autoRefresh ? 'default' : 'outline'} onClick={() => setAutoRefresh(!autoRefresh)} size="sm">
|
|
136
|
+
{autoRefresh ? 'Auto-refresh ON' : 'Auto-refresh OFF'}
|
|
137
|
+
</Button>
|
|
138
|
+
<Button variant="outline" onClick={refreshAll} loading={loading} size="icon">
|
|
139
|
+
<RefreshCw className="h-4 w-4" />
|
|
140
|
+
</Button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Overview Cards */}
|
|
145
|
+
{overview && <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
146
|
+
<Card>
|
|
147
|
+
<CardContent className="pt-4">
|
|
148
|
+
<Typography level="title-sm" color="muted">Total Runs</Typography>
|
|
149
|
+
<Typography level="h2">{overview.totalRuns?.toLocaleString()}</Typography>
|
|
150
|
+
</CardContent>
|
|
151
|
+
</Card>
|
|
152
|
+
<Card>
|
|
153
|
+
<CardContent className="pt-4">
|
|
154
|
+
<Typography level="title-sm" color="success">Success Rate</Typography>
|
|
155
|
+
<Typography level="h2" color="success">
|
|
156
|
+
{overview.successRate ? `${overview.successRate.toFixed(1)}%` : 'N/A'}
|
|
157
|
+
</Typography>
|
|
158
|
+
</CardContent>
|
|
159
|
+
</Card>
|
|
160
|
+
<Card>
|
|
161
|
+
<CardContent className="pt-4">
|
|
162
|
+
<Typography level="title-sm" color="primary">Running Tasks</Typography>
|
|
163
|
+
<Typography level="h2" color="primary">{overview.runningTasks || 0}</Typography>
|
|
164
|
+
</CardContent>
|
|
165
|
+
</Card>
|
|
166
|
+
<Card>
|
|
167
|
+
<CardContent className="pt-4">
|
|
168
|
+
<Typography level="title-sm" color="danger">Failed (24h)</Typography>
|
|
169
|
+
<Typography level="h2" color="danger">{overview.recentFailures || 0}</Typography>
|
|
170
|
+
</CardContent>
|
|
171
|
+
</Card>
|
|
172
|
+
</div>}
|
|
173
|
+
|
|
174
|
+
{/* Filters */}
|
|
175
|
+
<div className="mb-6 flex gap-4 items-center flex-wrap">
|
|
176
|
+
<Select value={filters.status} onValueChange={value => setFilters(prev => ({
|
|
177
|
+
...prev,
|
|
178
|
+
status: value
|
|
179
|
+
}))}>
|
|
180
|
+
<SelectTrigger size="sm" className="w-[140px]">
|
|
181
|
+
<SelectValue />
|
|
182
|
+
</SelectTrigger>
|
|
183
|
+
<SelectContent>
|
|
184
|
+
<SelectItem value="all">All Status</SelectItem>
|
|
185
|
+
<SelectItem value="complete">Complete</SelectItem>
|
|
186
|
+
<SelectItem value="failed">Failed</SelectItem>
|
|
187
|
+
<SelectItem value="running">Running</SelectItem>
|
|
188
|
+
<SelectItem value="pending">Pending</SelectItem>
|
|
189
|
+
</SelectContent>
|
|
190
|
+
</Select>
|
|
191
|
+
|
|
192
|
+
<div className="relative">
|
|
193
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
194
|
+
<Input placeholder="Filter by task name..." value={filters.task} onChange={e => setFilters(prev => ({
|
|
195
|
+
...prev,
|
|
196
|
+
task: e.target.value
|
|
197
|
+
}))} size="sm" className="pl-8 w-[200px]" />
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<Select value={filters.dateRange} onValueChange={value => setFilters(prev => ({
|
|
201
|
+
...prev,
|
|
202
|
+
dateRange: value
|
|
203
|
+
}))}>
|
|
204
|
+
<SelectTrigger size="sm" className="w-[120px]">
|
|
205
|
+
<SelectValue />
|
|
206
|
+
</SelectTrigger>
|
|
207
|
+
<SelectContent>
|
|
208
|
+
<SelectItem value="1h">Last Hour</SelectItem>
|
|
209
|
+
<SelectItem value="24h">Last 24h</SelectItem>
|
|
210
|
+
<SelectItem value="7d">Last 7 days</SelectItem>
|
|
211
|
+
<SelectItem value="30d">Last 30 days</SelectItem>
|
|
212
|
+
</SelectContent>
|
|
213
|
+
</Select>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
{/* Task Statistics Table */}
|
|
217
|
+
<div className="mb-8">
|
|
218
|
+
<Typography level="h3" className="mb-4">Task Statistics</Typography>
|
|
219
|
+
<div className="rounded-md border overflow-auto">
|
|
220
|
+
<Table>
|
|
221
|
+
<TableHeader>
|
|
222
|
+
<TableRow>
|
|
223
|
+
<TableHead>Task Name</TableHead>
|
|
224
|
+
<TableHead>Total Runs</TableHead>
|
|
225
|
+
<TableHead>Success Rate</TableHead>
|
|
226
|
+
<TableHead>Last Run</TableHead>
|
|
227
|
+
<TableHead>Status</TableHead>
|
|
228
|
+
<TableHead>Avg Duration</TableHead>
|
|
229
|
+
</TableRow>
|
|
230
|
+
</TableHeader>
|
|
231
|
+
<TableBody>
|
|
232
|
+
{tasks.map(task => <TableRow key={task.task}>
|
|
233
|
+
<TableCell>
|
|
234
|
+
<Typography level="body-sm" weight="medium">
|
|
235
|
+
{task.task}
|
|
236
|
+
</Typography>
|
|
237
|
+
</TableCell>
|
|
238
|
+
<TableCell>{task.totalRuns}</TableCell>
|
|
239
|
+
<TableCell>
|
|
240
|
+
<Typography level="body-sm" color={task.successRate > 90 ? 'success' : task.successRate > 70 ? 'warning' : 'danger'}>
|
|
241
|
+
{task.successRate.toFixed(1)}%
|
|
242
|
+
</Typography>
|
|
243
|
+
</TableCell>
|
|
244
|
+
<TableCell>
|
|
245
|
+
<Typography level="body-sm">
|
|
246
|
+
{formatDate(task.lastRun)}
|
|
247
|
+
</Typography>
|
|
248
|
+
</TableCell>
|
|
249
|
+
<TableCell>
|
|
250
|
+
<Badge variant={getStatusVariant(task.lastStatus)} className="gap-1">
|
|
251
|
+
{getStatusIcon(task.lastStatus)}
|
|
252
|
+
{task.lastStatus}
|
|
253
|
+
</Badge>
|
|
254
|
+
</TableCell>
|
|
255
|
+
<TableCell>
|
|
256
|
+
<Typography level="body-sm">
|
|
257
|
+
{formatDuration(task.avgDuration)}
|
|
258
|
+
</Typography>
|
|
259
|
+
</TableCell>
|
|
260
|
+
</TableRow>)}
|
|
261
|
+
</TableBody>
|
|
262
|
+
</Table>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{/* Recent Runs */}
|
|
267
|
+
<div className="mb-8">
|
|
268
|
+
<Typography level="h3" className="mb-4">Recent Runs</Typography>
|
|
269
|
+
<div className="rounded-md border overflow-auto max-h-[400px]">
|
|
270
|
+
<Table>
|
|
271
|
+
<TableHeader>
|
|
272
|
+
<TableRow>
|
|
273
|
+
<TableHead>Task</TableHead>
|
|
274
|
+
<TableHead>Status</TableHead>
|
|
275
|
+
<TableHead>Started</TableHead>
|
|
276
|
+
<TableHead>Duration</TableHead>
|
|
277
|
+
<TableHead>Triggered By</TableHead>
|
|
278
|
+
</TableRow>
|
|
279
|
+
</TableHeader>
|
|
280
|
+
<TableBody>
|
|
281
|
+
{recentRuns.map(run => <TableRow key={run.id}>
|
|
282
|
+
<TableCell>
|
|
283
|
+
<Typography level="body-sm" weight="medium">
|
|
284
|
+
{run.task}
|
|
285
|
+
</Typography>
|
|
286
|
+
</TableCell>
|
|
287
|
+
<TableCell>
|
|
288
|
+
<Badge variant={getStatusVariant(run.status)} className="gap-1">
|
|
289
|
+
{getStatusIcon(run.status)}
|
|
290
|
+
{run.status}
|
|
291
|
+
</Badge>
|
|
292
|
+
</TableCell>
|
|
293
|
+
<TableCell>
|
|
294
|
+
<Typography level="body-sm">
|
|
295
|
+
{formatDate(run.started_at)}
|
|
296
|
+
</Typography>
|
|
297
|
+
</TableCell>
|
|
298
|
+
<TableCell>
|
|
299
|
+
<Typography level="body-sm">
|
|
300
|
+
{formatDuration(run.duration)}
|
|
301
|
+
</Typography>
|
|
302
|
+
</TableCell>
|
|
303
|
+
<TableCell>
|
|
304
|
+
<Badge variant="outline">
|
|
305
|
+
{run.triggered_by}
|
|
306
|
+
</Badge>
|
|
307
|
+
</TableCell>
|
|
308
|
+
</TableRow>)}
|
|
309
|
+
</TableBody>
|
|
310
|
+
</Table>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{/* Recent Logs */}
|
|
315
|
+
<div>
|
|
316
|
+
<Typography level="h3" className="mb-4">Recent Logs</Typography>
|
|
317
|
+
<div className="rounded-md border p-4 max-h-[300px] overflow-auto bg-muted/30">
|
|
318
|
+
{logs.map(log => <div key={log.id} className="mb-2 font-mono text-sm">
|
|
319
|
+
<Typography level="body-xs" color="muted">
|
|
320
|
+
[{formatDate(log.created_at)}] [{log.type}]
|
|
321
|
+
</Typography>
|
|
322
|
+
<Typography level="body-sm" className="whitespace-pre-wrap">
|
|
323
|
+
{log.content}
|
|
324
|
+
</Typography>
|
|
325
|
+
</div>)}
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>;
|
|
329
|
+
}
|
|
@@ -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
|
+
}
|