@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/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",
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/bucket": "0.11.4",
18
- "@alepha/cache": "0.11.4",
19
- "@alepha/core": "0.11.4",
20
- "@alepha/logger": "0.11.4",
21
- "@alepha/queue": "0.11.4",
22
- "@alepha/react": "0.11.4",
23
- "@alepha/react-i18n": "0.11.4",
24
- "@alepha/scheduler": "0.11.4",
25
- "@alepha/security": "0.11.4",
26
- "@alepha/server": "0.11.4",
27
- "@alepha/server-static": "0.11.4",
28
- "@alepha/topic": "0.11.4"
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.4",
32
- "@alepha/ui": "0.11.4",
33
- "@alepha/vite": "0.11.4",
34
- "@biomejs/biome": "^2.3.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.0",
39
+ "tsdown": "^0.16.1",
38
40
  "typescript": "^5.9.3",
39
- "vitest": "^4.0.6"
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
- protected readonly logs: LogEntry[] = [];
31
- protected readonly maxLogs = 10000;
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.logs.unshift(ev.entry);
93
+ handler: async (ev: { message?: string; entry: LogEntry }) => {
94
+ if (!this.alepha.isReady()) {
95
+ return;
96
+ }
46
97
 
47
- // keep only the last 10000 logs
48
- if (this.logs.length > this.maxLogs) {
49
- this.logs.pop();
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
- response: t.array(logEntrySchema),
130
+ query: t.interface([pageQuerySchema], {
131
+ search: t.optional(t.string()),
132
+ }),
133
+ response: t.page(logEntrySchema),
77
134
  },
78
- handler: () => {
79
- return this.getLogs();
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
- name: page.name,
198
- description: page.options.description,
199
- path: page.options.path,
200
- params: page.options.schema?.params,
201
- query: page.options.schema?.query,
202
- hasComponent: !!page.options.component,
203
- hasLazy: !!page.options.lazy,
204
- hasResolve: !!page.options.resolve,
205
- hasChildren: !!page.options.children,
206
- hasParent: !!page.options.parent,
207
- hasErrorHandler: !!page.options.errorHandler,
208
- static:
209
- typeof page.options.static === "boolean"
210
- ? page.options.static
211
- : !!page.options.static,
212
- cache: page.options.cache,
213
- client: page.options.client,
214
- animation: page.options.animation,
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
  });
@@ -1,5 +1,13 @@
1
1
  import { $page } from "@alepha/react";
2
- import { AdminShell, RootRouter, Text, ui } from "@alepha/ui";
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: "center",
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
  />
@@ -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 { useAction, useInject } from "@alepha/react";
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={logs}
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",
@@ -0,0 +1,10 @@
1
+ import { Alepha, run } from "@alepha/core";
2
+ import { AlephaDevtools } from "../index.ts";
3
+ import { AppRouter } from "./AppRouter.tsx";
4
+
5
+ const alepha = Alepha.create();
6
+
7
+ alepha.with(AppRouter);
8
+ alepha.with(AlephaDevtools);
9
+
10
+ run(alepha);