@alepha/devtools 0.11.6 → 0.11.7

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,254 @@
1
+ import { type Page, t } from "@alepha/core";
2
+ import { dayjs } from "@alepha/datetime";
3
+ import { type LogEntry, logEntrySchema } from "@alepha/logger";
4
+ import { useInject } from "@alepha/react";
5
+ import { useI18n } from "@alepha/react-i18n";
6
+ import { HttpClient } from "@alepha/server";
7
+ import { ActionButton, DataTable, DialogService, Flex, Text } from "@alepha/ui";
8
+ import { logs } from "../../entities/logs.ts";
9
+
10
+ const DevLogViewer = () => {
11
+ const http = useInject(HttpClient);
12
+ const { l } = useI18n();
13
+
14
+ const renderLevel = (level: string) => {
15
+ switch (level.toLowerCase()) {
16
+ case "error":
17
+ return (
18
+ <Text ff={"monospace"} c="red">
19
+ {level.slice(0, 5)}
20
+ </Text>
21
+ );
22
+ case "warn":
23
+ return (
24
+ <Text ff={"monospace"} c="orange">
25
+ {level.slice(0, 5)}
26
+ </Text>
27
+ );
28
+ case "info":
29
+ return (
30
+ <Text ff={"monospace"} c="green">
31
+ {level.slice(0, 5)}
32
+ </Text>
33
+ );
34
+ case "debug":
35
+ return (
36
+ <Text ff={"monospace"} c="grape">
37
+ {level.slice(0, 5)}
38
+ </Text>
39
+ );
40
+ case "trace":
41
+ return (
42
+ <Text ff={"monospace"} c="dimmed">
43
+ {level.slice(0, 5)}
44
+ </Text>
45
+ );
46
+ default:
47
+ return <Text>{level}</Text>;
48
+ }
49
+ };
50
+
51
+ const filters = t.object({
52
+ search: t.optional(
53
+ t.string({
54
+ $control: {
55
+ query: t.omit(logs.schema, ["id"]),
56
+ },
57
+ }),
58
+ ),
59
+ level: t.optional(
60
+ t.array(
61
+ t.enum(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"], {
62
+ $control: {},
63
+ }),
64
+ ),
65
+ ),
66
+ });
67
+
68
+ return (
69
+ <Flex flex={1}>
70
+ <DataTable<LogEntry, typeof filters>
71
+ submitOnInit
72
+ infinityScroll
73
+ submitEvery={[10, "seconds"]}
74
+ defaultSize={20}
75
+ typeFormProps={{
76
+ skipSubmitButton: true,
77
+ columns: 2,
78
+ }}
79
+ tableProps={{
80
+ horizontalSpacing: "xs",
81
+ verticalSpacing: 0,
82
+ }}
83
+ onFilterChange={(key, value, form) => {
84
+ if (key === "search" || key === "level") {
85
+ return form.submit();
86
+ }
87
+ }}
88
+ filters={filters}
89
+ tableTrProps={(item) => {
90
+ if (item.level.toLowerCase() === "error") {
91
+ return {
92
+ bg: "rgba(255,0,0,0.1)",
93
+ };
94
+ }
95
+ if (item.level.toLowerCase() === "warn") {
96
+ return {
97
+ bg: "rgba(255,153,0,0.1)",
98
+ };
99
+ }
100
+ return {};
101
+ }}
102
+ items={async (filters, ctx) => {
103
+ if (filters.search) {
104
+ filters.search = filters.search.replace(
105
+ "now()",
106
+ new Date().toISOString(),
107
+ );
108
+ }
109
+
110
+ if (filters.page && filters.page > 0) {
111
+ const next = `timestamp < ${ctx.items[0].timestamp}`;
112
+ if (filters.search) {
113
+ filters.search += `& ${next}`;
114
+ } else {
115
+ filters.search = next;
116
+ }
117
+ }
118
+
119
+ if (filters.level) {
120
+ const levelFilter = filters.level
121
+ .map((it) => `level = ${it}`)
122
+ .join(" | ");
123
+
124
+ if (filters.search) {
125
+ filters.search += `& (${levelFilter})`;
126
+ } else {
127
+ filters.search = levelFilter;
128
+ }
129
+ }
130
+
131
+ const queryParams = new URLSearchParams(
132
+ filters as Record<string, any>,
133
+ ).toString();
134
+
135
+ const response = await http.fetch(
136
+ `/devtools/api/logs?${queryParams}`,
137
+ {
138
+ schema: {
139
+ response: t.page(logEntrySchema),
140
+ },
141
+ },
142
+ );
143
+
144
+ return response.data as Page<LogEntry>;
145
+ }}
146
+ columns={{
147
+ timestamp: {
148
+ label: "timestamp",
149
+ value: (item, { form }) => (
150
+ <ActionButton
151
+ h={20}
152
+ c={"dimmed"}
153
+ size={"xs"}
154
+ tooltip={l(item.timestamp, {
155
+ date: "DD MMM YYYY HH:mm:ss.SSS",
156
+ })}
157
+ onClick={() => {
158
+ const before = dayjs(item.timestamp).subtract(1, "minute");
159
+ const after = dayjs(item.timestamp).add(1, "minute");
160
+ form.input.search.set(
161
+ `timestamp >= ${before.toISOString()} & timestamp <= ${after.toISOString()}`,
162
+ );
163
+ }}
164
+ >
165
+ {l(item.timestamp, {
166
+ date: "HH:mm:ss.SSS",
167
+ })}
168
+ </ActionButton>
169
+ ),
170
+ },
171
+ level: {
172
+ label: "level",
173
+ value: (item) => renderLevel(item.level),
174
+ },
175
+ app: {
176
+ label: "app",
177
+ value: (item) => item.app,
178
+ },
179
+ context: {
180
+ fit: true,
181
+ label: "context",
182
+ value: (item, { form }) =>
183
+ item.context && (
184
+ <ActionButton
185
+ h={20}
186
+ size={"xs"}
187
+ onClick={() => {
188
+ form.input.search.set(`context = ${item.context}`);
189
+ }}
190
+ >
191
+ <Text ff={"monospace"} size={"xs"} c="dimmed">
192
+ {item.context.replaceAll("-", "").slice(0, 10)}
193
+ </Text>
194
+ </ActionButton>
195
+ ),
196
+ },
197
+ service: {
198
+ fit: true,
199
+ label: "service",
200
+ value: (item) => (
201
+ <Flex align={"center"} justify={"end"}>
202
+ {item.module && (
203
+ <Text c="dimmed" size={"xs"}>
204
+ {item.module}.
205
+ </Text>
206
+ )}
207
+ <Text size={"sm"}>{item.service}</Text>
208
+ </Flex>
209
+ ),
210
+ },
211
+ message: {
212
+ label: "message",
213
+ value: (item) => {
214
+ if (item.data?.ms && item.data.status) {
215
+ return `${item.message} - ${item.data.status} [${item.data.ms}ms]`;
216
+ }
217
+ if (item.data?.path && item.data.method) {
218
+ return `${item.message} - ${item.data.method} ${item.data.path}`;
219
+ }
220
+ return item.message;
221
+ },
222
+ },
223
+ data: {
224
+ label: "more",
225
+ value: (item, { alepha }) => {
226
+ if (!item.data) {
227
+ return;
228
+ }
229
+
230
+ if (Object.keys(item.data).length === 0) {
231
+ return;
232
+ }
233
+
234
+ return (
235
+ <ActionButton
236
+ opacity={0.5}
237
+ h={20}
238
+ px={4}
239
+ size={"xs"}
240
+ fw={"bold"}
241
+ onClick={() => alepha.inject(DialogService).json(item.data)}
242
+ >
243
+ {"{ ... }"}
244
+ </ActionButton>
245
+ );
246
+ },
247
+ },
248
+ }}
249
+ />
250
+ </Flex>
251
+ );
252
+ };
253
+
254
+ export default DevLogViewer;
package/src/ui/styles.css CHANGED
@@ -1,5 +1,5 @@
1
1
  @import "@alepha/ui";
2
- @import "wotfardregular/stylesheet.css";
2
+ @import "./resources/wotfardregular/stylesheet.css";
3
3
 
4
4
  :root {
5
5
  --mantine-font-family: "wotfardregular", Arial, sans-serif;
@@ -1,145 +0,0 @@
1
- import { type Page, t } from "@alepha/core";
2
- import { type LogEntry, logEntrySchema } from "@alepha/logger";
3
- import { useInject } from "@alepha/react";
4
- import { useI18n } from "@alepha/react-i18n";
5
- import { HttpClient } from "@alepha/server";
6
- import { DataTable, Flex, Text } from "@alepha/ui";
7
-
8
- const DevLogs = () => {
9
- const http = useInject(HttpClient);
10
- const { l } = useI18n();
11
-
12
- const renderLevel = (level: string) => {
13
- switch (level.toLowerCase()) {
14
- case "error":
15
- return (
16
- <Text ff={"monospace"} c="red">
17
- {level.slice(0, 5)}
18
- </Text>
19
- );
20
- case "warn":
21
- return (
22
- <Text ff={"monospace"} c="orange">
23
- {level.slice(0, 5)}
24
- </Text>
25
- );
26
- case "info":
27
- return (
28
- <Text ff={"monospace"} c="green">
29
- {level.slice(0, 5)}
30
- </Text>
31
- );
32
- case "debug":
33
- return (
34
- <Text ff={"monospace"} c="grape">
35
- {level.slice(0, 5)}
36
- </Text>
37
- );
38
- case "trace":
39
- return (
40
- <Text ff={"monospace"} c="dimmed">
41
- {level.slice(0, 5)}
42
- </Text>
43
- );
44
- default:
45
- return <Text>{level}</Text>;
46
- }
47
- };
48
-
49
- return (
50
- <Flex flex={1}>
51
- <DataTable<LogEntry>
52
- submitOnInit
53
- submitEvery={[10, "seconds"]}
54
- defaultSize={20}
55
- tableProps={{
56
- horizontalSpacing: "xs",
57
- verticalSpacing: 0,
58
- }}
59
- filters={t.object({
60
- search: t.optional(
61
- t.string({
62
- $control: {
63
- query: logEntrySchema,
64
- },
65
- }),
66
- ),
67
- })}
68
- tableTrProps={(item) => {
69
- if (item.level.toLowerCase() === "error") {
70
- return {
71
- bg: "rgba(255,0,0,0.1)",
72
- };
73
- }
74
- if (item.level.toLowerCase() === "warn") {
75
- return {
76
- bg: "rgba(255,153,0,0.1)",
77
- };
78
- }
79
- return {};
80
- }}
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
- }}
93
- columns={{
94
- timestamp: {
95
- label: "Tme",
96
- value: (item) => (
97
- <Text c={"dimmed"} size={"xs"}>
98
- {l(item.timestamp, {
99
- date: "HH:mm:ss.SSS",
100
- })}
101
- </Text>
102
- ),
103
- },
104
- level: {
105
- label: "Lvl",
106
- value: (item) => renderLevel(item.level),
107
- },
108
- context: {
109
- label: "Ctx",
110
- value: (item) =>
111
- item.context && (
112
- <Text ff={"monospace"} size={"xs"} c="dimmed">
113
- {item.context.slice(0, 8)}
114
- </Text>
115
- ),
116
- },
117
- service: {
118
- label: "Srv",
119
- value: (item) => (
120
- <Flex align={"center"} justify={"end"}>
121
- {item.module && (
122
- <Text c="dimmed" size={"xs"}>
123
- {item.module}.
124
- </Text>
125
- )}
126
- <Text size={"sm"}>{item.service}</Text>
127
- </Flex>
128
- ),
129
- },
130
- message: {
131
- label: "Msg",
132
- value: (item) => item.message.slice(0, 64),
133
- },
134
- data: {
135
- label: "Dat",
136
- value: (item) =>
137
- item.data ? JSON.stringify(item.data, null, 2).slice(0, 32) : "",
138
- },
139
- }}
140
- />
141
- </Flex>
142
- );
143
- };
144
-
145
- export default DevLogs;
@@ -1,10 +0,0 @@
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);