@alepha/devtools 0.11.4 → 0.11.6
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/index.d.ts +408 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/{index.mjs → index.js} +63 -33
- package/dist/index.js.map +1 -0
- package/package.json +21 -19
- package/src/DevCollectorProvider.ts +107 -38
- package/src/index.ts +1 -1
- package/src/ui/AppRouter.tsx +33 -3
- package/src/ui/DevLogs.tsx +28 -26
- package/src/ui/main.server.ts +10 -0
- package/dist/index.d.mts +0 -370
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alepha/devtools",
|
|
3
3
|
"description": "Developer tools for monitoring and debugging Alepha applications.",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.0.0"
|
|
@@ -14,29 +14,31 @@
|
|
|
14
14
|
"src"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@alepha/
|
|
18
|
-
"@alepha/
|
|
19
|
-
"@alepha/
|
|
20
|
-
"@alepha/
|
|
21
|
-
"@alepha/
|
|
22
|
-
"@alepha/
|
|
23
|
-
"@alepha/
|
|
24
|
-
"@alepha/
|
|
25
|
-
"@alepha/
|
|
26
|
-
"@alepha/
|
|
27
|
-
"@alepha/
|
|
28
|
-
"@alepha/
|
|
17
|
+
"@alepha/batch": "0.11.6",
|
|
18
|
+
"@alepha/bucket": "0.11.6",
|
|
19
|
+
"@alepha/cache": "0.11.6",
|
|
20
|
+
"@alepha/core": "0.11.6",
|
|
21
|
+
"@alepha/logger": "0.11.6",
|
|
22
|
+
"@alepha/postgres": "0.11.6",
|
|
23
|
+
"@alepha/queue": "0.11.6",
|
|
24
|
+
"@alepha/react": "0.11.6",
|
|
25
|
+
"@alepha/react-i18n": "0.11.6",
|
|
26
|
+
"@alepha/scheduler": "0.11.6",
|
|
27
|
+
"@alepha/security": "0.11.6",
|
|
28
|
+
"@alepha/server": "0.11.6",
|
|
29
|
+
"@alepha/server-static": "0.11.6",
|
|
30
|
+
"@alepha/topic": "0.11.6"
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
31
|
-
"@alepha/cli": "0.11.
|
|
32
|
-
"@alepha/ui": "0.11.
|
|
33
|
-
"@alepha/vite": "0.11.
|
|
34
|
-
"@biomejs/biome": "^2.3.
|
|
33
|
+
"@alepha/cli": "0.11.6",
|
|
34
|
+
"@alepha/ui": "0.11.6",
|
|
35
|
+
"@alepha/vite": "0.11.6",
|
|
36
|
+
"@biomejs/biome": "^2.3.4",
|
|
35
37
|
"@tabler/icons-react": "^3.35.0",
|
|
36
38
|
"react": "^19.2.0",
|
|
37
|
-
"tsdown": "^0.16.
|
|
39
|
+
"tsdown": "^0.16.1",
|
|
38
40
|
"typescript": "^5.9.3",
|
|
39
|
-
"vitest": "^4.0.
|
|
41
|
+
"vitest": "^4.0.8"
|
|
40
42
|
},
|
|
41
43
|
"scripts": {
|
|
42
44
|
"test": "vitest run",
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { $batch } from "@alepha/batch";
|
|
3
4
|
import { $bucket } from "@alepha/bucket";
|
|
4
5
|
import { $cache } from "@alepha/cache";
|
|
5
|
-
import { $hook, $inject, Alepha, t } from "@alepha/core";
|
|
6
|
+
import { $hook, $inject, Alepha, pageQuerySchema, t } from "@alepha/core";
|
|
6
7
|
import { $logger, type LogEntry, logEntrySchema } from "@alepha/logger";
|
|
8
|
+
import {
|
|
9
|
+
$entity,
|
|
10
|
+
NodeSqliteProvider,
|
|
11
|
+
parseQueryString,
|
|
12
|
+
pg,
|
|
13
|
+
} from "@alepha/postgres";
|
|
14
|
+
import { Repository } from "@alepha/postgres/src/services/Repository.ts";
|
|
7
15
|
import { $queue } from "@alepha/queue";
|
|
8
|
-
import { $page } from "@alepha/react";
|
|
9
16
|
import { $scheduler } from "@alepha/scheduler";
|
|
10
17
|
import { $realm } from "@alepha/security";
|
|
11
18
|
import { $action, $route, ServerProvider } from "@alepha/server";
|
|
@@ -23,12 +30,45 @@ import type { DevRealmMetadata } from "./schemas/DevRealmMetadata.ts";
|
|
|
23
30
|
import type { DevSchedulerMetadata } from "./schemas/DevSchedulerMetadata.ts";
|
|
24
31
|
import type { DevTopicMetadata } from "./schemas/DevTopicMetadata.ts";
|
|
25
32
|
|
|
33
|
+
class DevToolsDatabaseProvider extends NodeSqliteProvider {
|
|
34
|
+
get name() {
|
|
35
|
+
return "devtools";
|
|
36
|
+
}
|
|
37
|
+
options = {
|
|
38
|
+
path: ":memory:",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const logs = $entity({
|
|
43
|
+
name: "logs",
|
|
44
|
+
schema: t.object({
|
|
45
|
+
id: pg.primaryKey(),
|
|
46
|
+
level: t.enum(["SILENT", "TRACE", "DEBUG", "INFO", "WARN", "ERROR"]),
|
|
47
|
+
message: t.text({
|
|
48
|
+
size: "rich",
|
|
49
|
+
}),
|
|
50
|
+
service: t.text(),
|
|
51
|
+
module: t.text(),
|
|
52
|
+
context: t.optional(t.text()),
|
|
53
|
+
app: t.optional(t.text()),
|
|
54
|
+
data: t.optional(t.json()),
|
|
55
|
+
timestamp: t.datetime(),
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
class LogRepository extends Repository<typeof logs.schema> {
|
|
60
|
+
constructor() {
|
|
61
|
+
super(logs, DevToolsDatabaseProvider);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
26
65
|
export class DevCollectorProvider {
|
|
27
66
|
protected readonly alepha = $inject(Alepha);
|
|
28
67
|
protected readonly serverProvider = $inject(ServerProvider);
|
|
68
|
+
protected readonly sqliteProvider = $inject(DevToolsDatabaseProvider);
|
|
29
69
|
protected readonly log = $logger();
|
|
30
|
-
|
|
31
|
-
|
|
70
|
+
|
|
71
|
+
logs = $inject(LogRepository);
|
|
32
72
|
|
|
33
73
|
protected readonly onStart = $hook({
|
|
34
74
|
on: "start",
|
|
@@ -39,21 +79,35 @@ export class DevCollectorProvider {
|
|
|
39
79
|
},
|
|
40
80
|
});
|
|
41
81
|
|
|
82
|
+
protected batchLogs = $batch({
|
|
83
|
+
maxSize: 50,
|
|
84
|
+
maxDuration: [10, "seconds"],
|
|
85
|
+
schema: logEntrySchema,
|
|
86
|
+
handler: async (entries: LogEntry[]) => {
|
|
87
|
+
await this.logs.createMany(entries);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
42
91
|
protected readonly onLog = $hook({
|
|
43
92
|
on: "log",
|
|
44
|
-
handler: (ev: { message?: string; entry: LogEntry }) => {
|
|
45
|
-
this.
|
|
93
|
+
handler: async (ev: { message?: string; entry: LogEntry }) => {
|
|
94
|
+
if (!this.alepha.isReady()) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
46
97
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
98
|
+
if (ev.entry.level === "TRACE" && ev.entry.module === "alepha.batch") {
|
|
99
|
+
// skip batch trace logs to avoid infinite loop
|
|
100
|
+
return;
|
|
50
101
|
}
|
|
102
|
+
|
|
103
|
+
await this.batchLogs.push(ev.entry);
|
|
51
104
|
},
|
|
52
105
|
});
|
|
53
106
|
|
|
54
107
|
protected readonly uiRoute = $serve({
|
|
55
108
|
path: "/devtools",
|
|
56
109
|
root: join(fileURLToPath(import.meta.url), "../../assets/devtools"),
|
|
110
|
+
historyApiFallback: true,
|
|
57
111
|
});
|
|
58
112
|
|
|
59
113
|
protected readonly metadataRoute = $route({
|
|
@@ -73,17 +127,30 @@ export class DevCollectorProvider {
|
|
|
73
127
|
path: "/devtools/api/logs",
|
|
74
128
|
silent: true,
|
|
75
129
|
schema: {
|
|
76
|
-
|
|
130
|
+
query: t.interface([pageQuerySchema], {
|
|
131
|
+
search: t.optional(t.string()),
|
|
132
|
+
}),
|
|
133
|
+
response: t.page(logEntrySchema),
|
|
77
134
|
},
|
|
78
|
-
handler: () => {
|
|
79
|
-
|
|
135
|
+
handler: ({ query }) => {
|
|
136
|
+
query.sort ??= "-timestamp";
|
|
137
|
+
if (query.search) {
|
|
138
|
+
console.log(parseQueryString(query.search));
|
|
139
|
+
}
|
|
140
|
+
return this.logs.paginate(
|
|
141
|
+
query,
|
|
142
|
+
query.search
|
|
143
|
+
? {
|
|
144
|
+
where: parseQueryString(query.search),
|
|
145
|
+
}
|
|
146
|
+
: {},
|
|
147
|
+
{
|
|
148
|
+
count: true,
|
|
149
|
+
},
|
|
150
|
+
);
|
|
80
151
|
},
|
|
81
152
|
});
|
|
82
153
|
|
|
83
|
-
public getLogs(): LogEntry[] {
|
|
84
|
-
return this.logs;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
154
|
// -------------------------------------------------------------------------------------------------------------------
|
|
88
155
|
|
|
89
156
|
public getActions(): DevActionMetadata[] {
|
|
@@ -191,28 +258,30 @@ export class DevCollectorProvider {
|
|
|
191
258
|
}
|
|
192
259
|
|
|
193
260
|
public getPages(): DevPageMetadata[] {
|
|
194
|
-
const pageDescriptors = this.alepha.descriptors($page);
|
|
195
|
-
|
|
196
|
-
return pageDescriptors.map((page) => ({
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}));
|
|
261
|
+
// const pageDescriptors = this.alepha.descriptors($page);
|
|
262
|
+
//
|
|
263
|
+
// return pageDescriptors.map((page) => ({
|
|
264
|
+
// name: page.name,
|
|
265
|
+
// description: page.options.description,
|
|
266
|
+
// path: page.options.path,
|
|
267
|
+
// params: page.options.schema?.params,
|
|
268
|
+
// query: page.options.schema?.query,
|
|
269
|
+
// hasComponent: !!page.options.component,
|
|
270
|
+
// hasLazy: !!page.options.lazy,
|
|
271
|
+
// hasResolve: !!page.options.resolve,
|
|
272
|
+
// hasChildren: !!page.options.children,
|
|
273
|
+
// hasParent: !!page.options.parent,
|
|
274
|
+
// hasErrorHandler: !!page.options.errorHandler,
|
|
275
|
+
// static:
|
|
276
|
+
// typeof page.options.static === "boolean"
|
|
277
|
+
// ? page.options.static
|
|
278
|
+
// : !!page.options.static,
|
|
279
|
+
// cache: page.options.cache,
|
|
280
|
+
// client: page.options.client,
|
|
281
|
+
// animation: page.options.animation,
|
|
282
|
+
// }));
|
|
283
|
+
|
|
284
|
+
return [];
|
|
216
285
|
}
|
|
217
286
|
|
|
218
287
|
public getProviders(): DevProviderMetadata[] {
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,6 @@ export const AlephaDevtools = $module({
|
|
|
33
33
|
services: [DevCollectorProvider],
|
|
34
34
|
register: (alepha) => {
|
|
35
35
|
alepha.with(DevCollectorProvider);
|
|
36
|
-
alepha.state.push("assets", "@alepha/devtools");
|
|
36
|
+
alepha.state.push("alepha.build.assets", "@alepha/devtools");
|
|
37
37
|
},
|
|
38
38
|
});
|
package/src/ui/AppRouter.tsx
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { $page } from "@alepha/react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AdminShell,
|
|
4
|
+
DarkModeButton,
|
|
5
|
+
OmnibarButton,
|
|
6
|
+
RootRouter,
|
|
7
|
+
Text,
|
|
8
|
+
ui,
|
|
9
|
+
} from "@alepha/ui";
|
|
10
|
+
import ToggleSidebarButton from "@alepha/ui/src/components/buttons/ToggleSidebarButton.tsx";
|
|
3
11
|
import { IconDashboard, IconLogs } from "@tabler/icons-react";
|
|
4
12
|
import DevLogs from "./DevLogs.tsx";
|
|
5
13
|
|
|
@@ -21,20 +29,42 @@ export class AppRouter extends RootRouter {
|
|
|
21
29
|
},
|
|
22
30
|
}}
|
|
23
31
|
sidebarProps={{
|
|
24
|
-
collapsed: true,
|
|
25
32
|
gap: "xs",
|
|
33
|
+
menu: [
|
|
34
|
+
{
|
|
35
|
+
element: <ToggleSidebarButton />,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: "Dashboard",
|
|
39
|
+
icon: <IconDashboard />,
|
|
40
|
+
href: "/",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "Logs",
|
|
44
|
+
icon: <IconLogs />,
|
|
45
|
+
href: "/logs",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
26
48
|
}}
|
|
27
49
|
appBarProps={{
|
|
28
50
|
items: [
|
|
29
51
|
{ position: "left", type: "burger" },
|
|
30
52
|
{
|
|
31
|
-
position: "
|
|
53
|
+
position: "left",
|
|
32
54
|
element: (
|
|
33
55
|
<Text fw="bold" size="lg">
|
|
34
56
|
Alepha DevTools
|
|
35
57
|
</Text>
|
|
36
58
|
),
|
|
37
59
|
},
|
|
60
|
+
{
|
|
61
|
+
position: "center",
|
|
62
|
+
element: <OmnibarButton />,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
position: "right",
|
|
66
|
+
element: <DarkModeButton />,
|
|
67
|
+
},
|
|
38
68
|
],
|
|
39
69
|
}}
|
|
40
70
|
/>
|
package/src/ui/DevLogs.tsx
CHANGED
|
@@ -1,35 +1,14 @@
|
|
|
1
|
-
import { t } from "@alepha/core";
|
|
1
|
+
import { type Page, t } from "@alepha/core";
|
|
2
2
|
import { type LogEntry, logEntrySchema } from "@alepha/logger";
|
|
3
|
-
import {
|
|
3
|
+
import { useInject } from "@alepha/react";
|
|
4
4
|
import { useI18n } from "@alepha/react-i18n";
|
|
5
5
|
import { HttpClient } from "@alepha/server";
|
|
6
6
|
import { DataTable, Flex, Text } from "@alepha/ui";
|
|
7
|
-
import { useState } from "react";
|
|
8
7
|
|
|
9
8
|
const DevLogs = () => {
|
|
10
9
|
const http = useInject(HttpClient);
|
|
11
|
-
const [logs, setLog] = useState<LogEntry[]>([]);
|
|
12
10
|
const { l } = useI18n();
|
|
13
11
|
|
|
14
|
-
useAction(
|
|
15
|
-
{
|
|
16
|
-
runOnInit: true,
|
|
17
|
-
runEvery: [10, "seconds"],
|
|
18
|
-
handler: async () => {
|
|
19
|
-
setLog(
|
|
20
|
-
await http
|
|
21
|
-
.fetch("/devtools/api/logs", {
|
|
22
|
-
schema: {
|
|
23
|
-
response: t.array(logEntrySchema),
|
|
24
|
-
},
|
|
25
|
-
})
|
|
26
|
-
.then(({ data }) => data),
|
|
27
|
-
);
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
[],
|
|
31
|
-
);
|
|
32
|
-
|
|
33
12
|
const renderLevel = (level: string) => {
|
|
34
13
|
switch (level.toLowerCase()) {
|
|
35
14
|
case "error":
|
|
@@ -68,12 +47,24 @@ const DevLogs = () => {
|
|
|
68
47
|
};
|
|
69
48
|
|
|
70
49
|
return (
|
|
71
|
-
<Flex>
|
|
72
|
-
<DataTable
|
|
50
|
+
<Flex flex={1}>
|
|
51
|
+
<DataTable<LogEntry>
|
|
52
|
+
submitOnInit
|
|
53
|
+
submitEvery={[10, "seconds"]}
|
|
54
|
+
defaultSize={20}
|
|
73
55
|
tableProps={{
|
|
74
56
|
horizontalSpacing: "xs",
|
|
75
57
|
verticalSpacing: 0,
|
|
76
58
|
}}
|
|
59
|
+
filters={t.object({
|
|
60
|
+
search: t.optional(
|
|
61
|
+
t.string({
|
|
62
|
+
$control: {
|
|
63
|
+
query: logEntrySchema,
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
})}
|
|
77
68
|
tableTrProps={(item) => {
|
|
78
69
|
if (item.level.toLowerCase() === "error") {
|
|
79
70
|
return {
|
|
@@ -87,7 +78,18 @@ const DevLogs = () => {
|
|
|
87
78
|
}
|
|
88
79
|
return {};
|
|
89
80
|
}}
|
|
90
|
-
items={
|
|
81
|
+
items={async (filters) => {
|
|
82
|
+
const response = await http.fetch(
|
|
83
|
+
`/devtools/api/logs?${new URLSearchParams(filters as any).toString()}`,
|
|
84
|
+
{
|
|
85
|
+
schema: {
|
|
86
|
+
response: t.page(logEntrySchema),
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return response.data as Page<LogEntry>;
|
|
92
|
+
}}
|
|
91
93
|
columns={{
|
|
92
94
|
timestamp: {
|
|
93
95
|
label: "Tme",
|