@microlight/core 0.2.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.
Files changed (49) hide show
  1. package/README.md +77 -0
  2. package/bin/microlight-core.js +70 -0
  3. package/dist/scripts/generate-folder-index.js +120 -0
  4. package/dist/scripts/generate-task-imports.js +64 -0
  5. package/dist/scripts/generate-task-index.js +61 -0
  6. package/dist/scripts/prepareFolders.js +119 -0
  7. package/dist/scripts/prepareServer.js +34 -0
  8. package/dist/scripts/prepareTasks.js +114 -0
  9. package/dist/server/app/api/tasks/[slug]/route.js +54 -0
  10. package/dist/server/app/layout.js +41 -0
  11. package/dist/server/app/library/[[...f_path]]/ViewFolder.js +113 -0
  12. package/dist/server/app/library/[[...f_path]]/page.js +42 -0
  13. package/dist/server/app/page.js +4 -0
  14. package/dist/server/app/tasks/[slug]/ViewTask.js +252 -0
  15. package/dist/server/app/tasks/[slug]/action.js +44 -0
  16. package/dist/server/app/tasks/[slug]/page.js +33 -0
  17. package/dist/server/app/tasks/[slug]/runs/[r_id]/ViewRun.js +230 -0
  18. package/dist/server/app/tasks/[slug]/runs/[r_id]/_components/DropdownActions/DropdownActions.js +46 -0
  19. package/dist/server/app/tasks/[slug]/runs/[r_id]/_components/DropdownActions/action.js +35 -0
  20. package/dist/server/app/tasks/[slug]/runs/[r_id]/page.js +43 -0
  21. package/dist/server/components/Icon.js +22 -0
  22. package/dist/server/components/Link.js +52 -0
  23. package/dist/server/components/MLInput.js +29 -0
  24. package/dist/server/components/Navbar/Navbar.js +38 -0
  25. package/dist/server/components/Navbar/NavbarContainer.js +26 -0
  26. package/dist/server/components/PageHeader.js +87 -0
  27. package/dist/server/components/StatusChip.js +11 -0
  28. package/dist/server/components/Test.js +5 -0
  29. package/dist/server/components/TopLoader.js +8 -0
  30. package/dist/server/database/microlight/index.js +52 -0
  31. package/dist/server/database/microlight/tables/Logs.model.js +34 -0
  32. package/dist/server/database/microlight/tables/Runs.model.js +61 -0
  33. package/dist/server/instrumentation.js +16 -0
  34. package/dist/server/lib/executeRun.js +80 -0
  35. package/dist/server/lib/generateDisplayFunctions.js +89 -0
  36. package/dist/server/lib/getAllTasks.js +32 -0
  37. package/dist/server/lib/getTaskDetails.js +17 -0
  38. package/dist/server/lib/loadSchedules.js +77 -0
  39. package/dist/server/tasks/1.intro/hello_world2.task.js +21 -0
  40. package/dist/server/tasks/1.intro/microlight.folder.js +5 -0
  41. package/dist/server/tasks/1.intro/ml.task.js +31 -0
  42. package/dist/server/tasks/1.intro/scheduled.task.js +18 -0
  43. package/dist/server/tasks/1.intro/takes_time.task.js +28 -0
  44. package/dist/server/tasks/1.intro/test/microlight.folder.js +5 -0
  45. package/dist/server/tasks/1.intro/test/takes_time2.task.js +28 -0
  46. package/dist/server/tasks/index.js +33 -0
  47. package/dist/server/tasks/microlight.folder.js +5 -0
  48. package/index.js +1 -0
  49. package/package.json +46 -0
@@ -0,0 +1,54 @@
1
+ import { executeTask } from "../../../tasks/[slug]/action";
2
+ export async function POST(request, {
3
+ params
4
+ }) {
5
+ try {
6
+ // Extract the slug from the route parameters
7
+ const {
8
+ slug
9
+ } = params;
10
+
11
+ // Extract query parameters
12
+ const url = new URL(request.url);
13
+ const date = url.searchParams.get('date');
14
+ const filename = url.searchParams.get('filename');
15
+
16
+ // Create FormData-like object with the query parameters
17
+ const formData = new FormData();
18
+ formData.append('date', date);
19
+ formData.append('filename', filename);
20
+
21
+ // Create task object using the dynamic slug
22
+ const task = {
23
+ slug: slug
24
+ // Add other required task properties here
25
+ };
26
+
27
+ // Execute the task using the same function
28
+ const result = await executeTask({
29
+ formData,
30
+ task
31
+ });
32
+ if (result.success) {
33
+ return Response.json({
34
+ success: true,
35
+ runId: result.run.id,
36
+ redirectUrl: `/tasks/${slug}/runs/${result.run.id}`
37
+ });
38
+ } else {
39
+ return Response.json({
40
+ success: false,
41
+ error: 'Task execution failed'
42
+ }, {
43
+ status: 400
44
+ });
45
+ }
46
+ } catch (error) {
47
+ return Response.json({
48
+ success: false,
49
+ error: error.message
50
+ }, {
51
+ status: 500
52
+ });
53
+ }
54
+ }
@@ -0,0 +1,41 @@
1
+ import { Inter } from "next/font/google";
2
+ const inter = Inter({
3
+ subsets: ["latin"]
4
+ });
5
+ import TopLoader from "../components/TopLoader";
6
+ // import ServiceWorkerRegistration from "@/components/ServiceWorkerRegistration";
7
+ import NavbarContainer from "../components/Navbar/NavbarContainer";
8
+ export const metadata = {
9
+ title: "Microlight",
10
+ 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
+ };
23
+ export default function RootLayout({
24
+ children
25
+ }) {
26
+ return <html lang="en">
27
+ <head>
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
+ }}>
34
+ {/* <ServiceWorkerRegistration /> */}
35
+ <TopLoader />
36
+ <NavbarContainer>
37
+ {children}
38
+ </NavbarContainer>
39
+ </body>
40
+ </html>;
41
+ }
@@ -0,0 +1,113 @@
1
+ 'use client';
2
+
3
+ import { Table, Box, Container, Typography } from '@mui/joy';
4
+ import React from 'react';
5
+ import Icon from "../../../components/Icon";
6
+
7
+ // import Link from '@/components/Link';
8
+
9
+ import { Link } from 'switchless';
10
+ import PageHeader from "../../../components/PageHeader";
11
+ function generateBreadcrumbs({
12
+ params
13
+ }) {
14
+ let breadcrumbs = [];
15
+ let b = {
16
+ text: "Library"
17
+ };
18
+ if (params.f_path?.length) {
19
+ let slug = '/library';
20
+ b.href = '/library';
21
+ breadcrumbs.push(b);
22
+ params.f_path.forEach(function (name, i) {
23
+ let b = {
24
+ text: name
25
+ };
26
+ if (i != params.f_path.length - 1) {
27
+ slug += '/' + name;
28
+ b.href = slug;
29
+ }
30
+ breadcrumbs.push(b);
31
+ });
32
+ } else {
33
+ breadcrumbs.push(b);
34
+ }
35
+ return breadcrumbs;
36
+ }
37
+ export default function ViewFolder({
38
+ params,
39
+ folder,
40
+ contents,
41
+ fileList
42
+ }) {
43
+ // params.f_path will be an array containing all segments after /library/
44
+ // e.g. for /library/level1/level2/level3
45
+ // params.f_path = ['level1', 'level2', 'level3']
46
+ const breadcrumbs = generateBreadcrumbs({
47
+ params
48
+ });
49
+ const dir = params.f_path ? '/' + params.f_path?.join('/') : '';
50
+ return <>
51
+ <Container>
52
+ <PageHeader breadcrumbs={breadcrumbs} header={{
53
+ part1: 'Folder:',
54
+ part2: folder.name
55
+ }} />
56
+ <Typography level='body-sm'>{folder.description}</Typography>
57
+ <Table sx={{
58
+ pt: 1,
59
+ "--Table-headerUnderlineThickness": "2px",
60
+ "--TableCell-height": "25px"
61
+ }}>
62
+ <thead>
63
+ <tr>
64
+ <th>Name</th>
65
+ <th>Description</th>
66
+ <th>Tasks</th>
67
+ </tr>
68
+ </thead>
69
+ <tbody>
70
+ {contents.map(content => {
71
+ return <React.Fragment key={`${content.type}__${content.slug}`}>
72
+ <tr>
73
+ <td>
74
+ {content.type == 'folder' && <>
75
+ <Box sx={{
76
+ display: 'flex',
77
+ alignItems: 'center',
78
+ gap: 1
79
+ }}>
80
+ {/* <Icon icon='folder' color='#444444'/> */}
81
+ <i class="fa-regular fa-folder fa-xl"></i>
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>;
103
+ })}
104
+
105
+
106
+ </tbody>
107
+ </Table>
108
+
109
+ {/* <pre>{JSON.stringify(folder,null,2)}</pre> */}
110
+ {/* <pre>{JSON.stringify(contents,null,2)}</pre> */}
111
+ </Container>
112
+ </>;
113
+ }
@@ -0,0 +1,42 @@
1
+ import ViewFolder from "./ViewFolder";
2
+ import fs from 'fs';
3
+ import { notFound } from 'next/navigation';
4
+ import folderMap from "../../../../folderMap";
5
+ const project_folder = process.cwd() + '/src/tasks/';
6
+ async function getFolderDetails({
7
+ params
8
+ }) {
9
+ const dir = params?.f_path?.join('/') || '';
10
+ console.log('\n\n\n\n======');
11
+ console.log("dir - ", dir);
12
+ let folderConfig = {};
13
+ folderConfig = folderMap[dir];
14
+ console.log(folderConfig);
15
+ // console.log('\n\n\n\n\n===========');
16
+ // console.log(dir)
17
+ // console.log(folderMap)
18
+ // console.log(folderConfig)
19
+ return folderConfig;
20
+ }
21
+ export default async function Page({
22
+ params
23
+ }) {
24
+ params = await params;
25
+ let folder = {
26
+ contents: []
27
+ };
28
+ let contents = [];
29
+ try {
30
+ folder = await getFolderDetails({
31
+ params
32
+ });
33
+ } catch (e) {
34
+ if (e.code === 'MODULE_NOT_FOUND') notFound();
35
+ }
36
+
37
+ // console.log('\n\n\n======');
38
+
39
+ // When accessing /library, params.f_path will be undefined
40
+ // When accessing /library/a/b, params.f_path will be ['a', 'b']
41
+ return <ViewFolder params={params} folder={folder} contents={folder.contents} />;
42
+ }
@@ -0,0 +1,4 @@
1
+ import { redirect } from "next/navigation";
2
+ export default function Home() {
3
+ redirect('/library');
4
+ }
@@ -0,0 +1,252 @@
1
+ 'use client';
2
+
3
+ import OpenInNew from '@mui/icons-material/OpenInNew';
4
+ import { Container, Typography, Box, Card, ButtonGroup, Button, Table, Chip, Link as MuiLink } from '@mui/joy';
5
+ import PageHeader from "../../../components/PageHeader";
6
+ import MLInput from "../../../components/MLInput";
7
+ import { useState } from 'react';
8
+ import { executeTask } from "./action";
9
+ import { redirect } from 'next/navigation';
10
+ import StatusChip from "../../../components/StatusChip";
11
+ import cronstrue from 'cronstrue';
12
+ import Link from "../../../components/Link";
13
+ function generateBreadcrumbs({
14
+ task
15
+ }) {
16
+ let breadcrumbs = [{
17
+ text: "Library",
18
+ href: "/library"
19
+ }];
20
+
21
+ // Add task path segments to breadcrumbs if available
22
+ if (task._folderPath) {
23
+ const f_path = task._folderPath.split('/');
24
+ let folderPath = '/library';
25
+ f_path.forEach((folder, index) => {
26
+ folderPath += '/' + folder;
27
+ breadcrumbs.push({
28
+ text: folder,
29
+ href: folderPath
30
+ });
31
+ });
32
+ breadcrumbs.push({
33
+ text: task.slug
34
+ });
35
+ }
36
+ return breadcrumbs;
37
+ }
38
+ export default function ViewTask({
39
+ params,
40
+ task,
41
+ runs,
42
+ searchParams
43
+ }) {
44
+ const breadcrumbs = generateBreadcrumbs({
45
+ task
46
+ });
47
+ const [loading, setLoading] = useState(false);
48
+ const RightButtons = function () {
49
+ return <>
50
+ {task?.links?.map(link => {
51
+ return <>
52
+ <MuiLink underline="none" variant="outlined" color="neutral" target='_blank' href={link.href} startDecorator={<i class="fa-solid fa-up-right-from-square"></i>} sx={{
53
+ mx: 0.5,
54
+ px: 1,
55
+ py: 0.5,
56
+ borderRadius: 'md'
57
+ }}>
58
+ {link.title}
59
+ </MuiLink>
60
+ </>;
61
+ })}
62
+ </>;
63
+ };
64
+ const handleSubmit = async event => {
65
+ event.preventDefault();
66
+ setLoading(true);
67
+ const form = event.currentTarget;
68
+ const formData = new FormData(form);
69
+ let result = await executeTask({
70
+ formData,
71
+ task
72
+ });
73
+ if (result.success)
74
+ // console.log('something went wrong');
75
+ console.log(`/tasks/${task.slug}/runs/${result.run.id}`);
76
+ redirect(`/tasks/${task.slug}/runs/${result.run.id}`);
77
+ };
78
+ return <Container>
79
+ <PageHeader breadcrumbs={breadcrumbs} header={{
80
+ part1: 'Task:',
81
+ part2: task.name
82
+ }} RightButtons={RightButtons} />
83
+ <Typography level="body-sm">{task.description}</Typography>
84
+
85
+
86
+ <Card sx={{
87
+ mt: 2,
88
+ backgroundColor: 'transparent',
89
+ maxWidth: 400
90
+ }}>
91
+ {/* {Object.keys(task.inputs)} */}
92
+ <form onSubmit={handleSubmit}>
93
+ {Object.keys(task.inputs).map(slug => <>
94
+ <MLInput key={slug} slug={slug} def={task.inputs[slug]} searchParams={searchParams} />
95
+ </>)}
96
+ <ButtonGroup spacing={1}>
97
+ {/* <Button type="submit" fullWidth color="primary" variant="solid" startDecorator={<FilterAltIcon />}>Apply filter</Button> */}
98
+ <Button loading={loading} disabled={loading} type="submit" color='primary' variant="solid">Execute task</Button>
99
+ {/* <Button loading={loading} disabled={loading} type="submit" color='primary' sx={{bgcolor:'#6435c9',borderRadius:3}} variant="solid">Execute task</Button> */}
100
+ {/* <Button variant="outlined" onClick={handleReset}>Reset</Button> */}
101
+ {/* <Button disabled={loading.apply} loading={loading.reset} fullWidth variant="outlined" color="primary" onClick={handleReset} >Reset</Button> */}
102
+ </ButtonGroup>
103
+ </form>
104
+ </Card>
105
+ <Typography level="title-lg" sx={{
106
+ mt: 3
107
+ }}>Schedules:</Typography>
108
+
109
+ <Table variant='outlined' aria-label="task runs table" size='md' sx={{
110
+ mt: 1,
111
+ maxWidth: 800,
112
+ '& th': {
113
+ height: {
114
+ sm: "22px",
115
+ md: "26px",
116
+ lg: "30px"
117
+ }
118
+ },
119
+ '& td': {
120
+ height: {
121
+ sm: "23px",
122
+ md: "27px",
123
+ lg: "31px"
124
+ }
125
+ }
126
+ }}>
127
+ <thead>
128
+ <tr>
129
+ {/* <th ></th> */}
130
+ <th style={{
131
+ width: 100
132
+ }}>Schedule</th>
133
+ <th style={{
134
+ width: 150
135
+ }}>Description</th>
136
+ <th style={{
137
+ width: 300
138
+ }}>Payload</th>
139
+ <th>Is Enabled?</th>
140
+ {/* <th>User</th> */}
141
+ </tr>
142
+ </thead>
143
+ <tbody>
144
+ {task?.schedules?.map((schedule, index) => <tr key={index}>
145
+ <td>{schedule.schedule}</td>
146
+ <td>
147
+ {cronstrue.toString(schedule.schedule)}
148
+ </td>
149
+ <td style={{
150
+ overflow: 'auto',
151
+ '&::-webkit-scrollbar': {
152
+ display: 'none'
153
+ },
154
+ height: 0,
155
+ scrollbarWidth: 'none',
156
+ // Firefox
157
+ msOverflowStyle: 'none' // IE and Edge
158
+ }}>
159
+ <pre style={{
160
+ margin: 0
161
+ }}>{JSON.stringify(schedule?.inputs, null, 2).slice(2, -2)}</pre>
162
+ </td>
163
+ <td>
164
+ {schedule.is_enabled ? 'enabled' : 'disabled'}
165
+ </td>
166
+
167
+ {/* <td>{run.user}</td> */}
168
+ </tr>)}
169
+ </tbody>
170
+ </Table>
171
+ <Typography level="title-lg" sx={{
172
+ mt: 3
173
+ }}>Recent runs:</Typography>
174
+ {/* <pre>{JSON.stringify(runs,null,2)}</pre> */}
175
+
176
+ <Table variant='outlined' aria-label="task runs table" size='md' sx={{
177
+ mt: 1,
178
+ maxWidth: 800,
179
+ '& th': {
180
+ height: {
181
+ sm: "22px",
182
+ md: "26px",
183
+ lg: "30px"
184
+ }
185
+ },
186
+ '& td': {
187
+ height: {
188
+ sm: "23px",
189
+ md: "27px",
190
+ lg: "31px"
191
+ }
192
+ }
193
+ }}>
194
+ <thead>
195
+ <tr>
196
+ {/* <th ></th> */}
197
+ <th style={{
198
+ width: 100
199
+ }}>Created At</th>
200
+ <th style={{
201
+ width: 150
202
+ }}>ID</th>
203
+ <th style={{
204
+ width: 300
205
+ }}>Payload</th>
206
+ <th>Status</th>
207
+ <th style={{
208
+ width: '58px'
209
+ }}>Duration</th>
210
+ <th>By</th>
211
+ {/* <th>User</th> */}
212
+ </tr>
213
+ </thead>
214
+ <tbody>
215
+ {runs.map(run => <tr key={run.id}>
216
+ <td>{new Date(run.updated_at).toLocaleString()}</td>
217
+ <td>
218
+ <Link href={`/tasks/${params.slug}/runs/${run.id}`} level="body-sm">
219
+ {task.slug} #{run.id}
220
+ </Link>
221
+ </td>
222
+ <td style={{
223
+ overflow: 'auto',
224
+ '&::-webkit-scrollbar': {
225
+ display: 'none'
226
+ },
227
+ height: 0,
228
+ scrollbarWidth: 'none',
229
+ // Firefox
230
+ msOverflowStyle: 'none' // IE and Edge
231
+ }}>
232
+ <pre style={{
233
+ margin: 0
234
+ }}>{JSON.stringify(run?.inputs, null, 1).slice(2, -2)}</pre>
235
+ </td>
236
+ <td>
237
+ <StatusChip status={run.status} />
238
+ </td>
239
+ <td style={{
240
+ textAlign: 'right'
241
+ }}>{run.duration / 1000 || 0}s</td>
242
+ <td>{run.triggered_by || 'user'}</td>
243
+ {/* <td>{run.user}</td> */}
244
+ </tr>)}
245
+ </tbody>
246
+ </Table>
247
+ {/* Add your task execution UI components here */}
248
+
249
+ {/* Uncomment for debugging */}
250
+ {/* <pre>{JSON.stringify(task, null, 2)}</pre> */}
251
+ </Container>;
252
+ }
@@ -0,0 +1,44 @@
1
+ "use server";
2
+
3
+ import async from 'async';
4
+ import microlightDB from "../../../database/microlight";
5
+ import executeRun from "../../../lib/executeRun";
6
+ import { redirect } from 'next/navigation';
7
+ import { revalidatePath } from 'next/cache';
8
+ export async function executeTask({
9
+ formData,
10
+ task
11
+ }) {
12
+ const workflow = {
13
+ createRun: async function () {
14
+ let run = await microlightDB.Runs.create({
15
+ task: task.slug,
16
+ logs: {},
17
+ inputs: Object.fromEntries(formData?.entries()),
18
+ triggered_by: 'user',
19
+ status: 'pending'
20
+ }, {
21
+ returning: true
22
+ });
23
+ return run.toJSON();
24
+ },
25
+ startRun: ['createRun', async function (results) {
26
+ process.nextTick(() => executeRun(results.createRun));
27
+ return;
28
+ }]
29
+ };
30
+ try {
31
+ const results = await async.auto(workflow);
32
+ console.log(results);
33
+ revalidatePath(`/tasks/${task.slug}`);
34
+ return {
35
+ success: true,
36
+ run: results.createRun
37
+ };
38
+ } catch (e) {
39
+ return {
40
+ success: false,
41
+ error: e
42
+ };
43
+ }
44
+ }
@@ -0,0 +1,33 @@
1
+ import getTaskDetails from "../../../lib/getTaskDetails";
2
+ import ViewTask from "./ViewTask";
3
+ import async from 'async';
4
+ import microlightDB from "../../../database/microlight";
5
+ import { orderBy } from "lodash";
6
+ export default async function Page({
7
+ params,
8
+ searchParams
9
+ }) {
10
+ params = await params;
11
+ searchParams = await searchParams;
12
+ const workflow = {
13
+ getTask: async function () {
14
+ const task = await getTaskDetails({
15
+ params
16
+ });
17
+ delete task.fn;
18
+ return task;
19
+ },
20
+ getRuns: async function () {
21
+ let runs = await microlightDB.Runs.findAll({
22
+ where: {
23
+ task: params.slug
24
+ },
25
+ order: [['updated_at', 'DESC']]
26
+ });
27
+ runs = runs.map(r => r.toJSON());
28
+ return runs;
29
+ }
30
+ };
31
+ let results = await async.auto(workflow);
32
+ return <ViewTask params={params} task={results.getTask} runs={results.getRuns} searchParams={searchParams} />;
33
+ }