@oneuptime/common 8.0.5480 → 8.0.5488

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 (28) hide show
  1. package/UI/Components/LogsViewer/LogsViewer.tsx +331 -367
  2. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +343 -0
  3. package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +74 -0
  4. package/UI/Components/LogsViewer/components/LogsPagination.tsx +109 -0
  5. package/UI/Components/LogsViewer/components/LogsTable.tsx +270 -0
  6. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +51 -0
  7. package/UI/Components/LogsViewer/components/SeverityBadge.tsx +28 -0
  8. package/UI/Components/LogsViewer/components/severityTheme.ts +69 -0
  9. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +211 -201
  10. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  11. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +151 -0
  12. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -0
  13. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +40 -0
  14. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -0
  15. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +49 -0
  16. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -0
  17. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +130 -0
  18. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -0
  19. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +20 -0
  20. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -0
  21. package/build/dist/UI/Components/LogsViewer/components/SeverityBadge.js +13 -0
  22. package/build/dist/UI/Components/LogsViewer/components/SeverityBadge.js.map +1 -0
  23. package/build/dist/UI/Components/LogsViewer/components/severityTheme.js +54 -0
  24. package/build/dist/UI/Components/LogsViewer/components/severityTheme.js.map +1 -0
  25. package/package.json +1 -1
  26. package/UI/Components/LogsViewer/LogItem.tsx +0 -503
  27. package/build/dist/UI/Components/LogsViewer/LogItem.js +0 -221
  28. package/build/dist/UI/Components/LogsViewer/LogItem.js.map +0 -1
@@ -0,0 +1,343 @@
1
+ import React, { FunctionComponent, ReactElement, useMemo } from "react";
2
+ import Log from "../../../../Models/AnalyticsModels/Log";
3
+ import TelemetryService from "../../../../Models/DatabaseModels/TelemetryService";
4
+ import Dictionary from "../../../../Types/Dictionary";
5
+ import Route from "../../../../Types/API/Route";
6
+ import URL from "../../../../Types/API/URL";
7
+ import CopyTextButton from "../../CopyTextButton/CopyTextButton";
8
+ import Icon from "../../Icon/Icon";
9
+ import IconProp from "../../../../Types/Icon/IconProp";
10
+ import Link from "../../Link/Link";
11
+ import OneUptimeDate from "../../../../Types/Date";
12
+ import JSONFunctions from "../../../../Types/JSONFunctions";
13
+ import SeverityBadge from "./SeverityBadge";
14
+
15
+ export interface LogDetailsPanelProps {
16
+ log: Log;
17
+ serviceMap: Dictionary<TelemetryService>;
18
+ onClose?: () => void;
19
+ getTraceRoute?:
20
+ | ((traceId: string, log: Log) => Route | URL | undefined)
21
+ | undefined;
22
+ getSpanRoute?:
23
+ | ((spanId: string, log: Log) => Route | URL | undefined)
24
+ | undefined;
25
+ variant?: "floating" | "embedded";
26
+ }
27
+
28
+ interface PreparedBody {
29
+ isJson: boolean;
30
+ pretty: string;
31
+ compact: string;
32
+ raw: string;
33
+ }
34
+
35
+ const prepareBody: (body: string | undefined) => PreparedBody = (
36
+ body: string | undefined,
37
+ ): PreparedBody => {
38
+ if (!body) {
39
+ return {
40
+ isJson: false,
41
+ pretty: "",
42
+ compact: "",
43
+ raw: "",
44
+ };
45
+ }
46
+
47
+ try {
48
+ const parsed: any = JSON.parse(body);
49
+ const pretty: string = JSON.stringify(parsed, null, 2);
50
+ const compact: string = JSON.stringify(parsed);
51
+ return {
52
+ isJson: true,
53
+ pretty,
54
+ compact,
55
+ raw: body,
56
+ };
57
+ } catch {
58
+ return {
59
+ isJson: false,
60
+ pretty: body,
61
+ compact: body,
62
+ raw: body,
63
+ };
64
+ }
65
+ };
66
+
67
+ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
68
+ props: LogDetailsPanelProps,
69
+ ): ReactElement => {
70
+ const variant: "floating" | "embedded" = props.variant || "floating";
71
+ const serviceId: string = props.log.serviceId?.toString() || "";
72
+ const service: TelemetryService | undefined = props.serviceMap[serviceId];
73
+ const serviceName: string = service?.name || serviceId || "Unknown service";
74
+ const serviceColor: string =
75
+ (service?.serviceColor && service?.serviceColor.toString()) || "#64748b";
76
+
77
+ const bodyDetails: PreparedBody = useMemo(() => {
78
+ return prepareBody(props.log.body?.toString());
79
+ }, [props.log.body]);
80
+
81
+ const prettyAttributes: string | null = useMemo(() => {
82
+ if (!props.log.attributes) {
83
+ return null;
84
+ }
85
+
86
+ try {
87
+ const normalized: Record<string, unknown> = JSONFunctions.unflattenObject(
88
+ props.log.attributes || {},
89
+ );
90
+ return JSON.stringify(normalized, null, 2);
91
+ } catch {
92
+ return null;
93
+ }
94
+ }, [props.log.attributes]);
95
+
96
+ const traceId: string = props.log.traceId?.toString() || "";
97
+ const spanId: string = props.log.spanId?.toString() || "";
98
+
99
+ const traceRoute: Route | URL | undefined = useMemo(() => {
100
+ if (!traceId || !props.getTraceRoute) {
101
+ return undefined;
102
+ }
103
+
104
+ return props.getTraceRoute(traceId, props.log);
105
+ }, [traceId, props]);
106
+
107
+ const spanRoute: Route | URL | undefined = useMemo(() => {
108
+ if (!spanId) {
109
+ return undefined;
110
+ }
111
+
112
+ if (props.getSpanRoute) {
113
+ return props.getSpanRoute(spanId, props.log);
114
+ }
115
+
116
+ if (props.getTraceRoute && traceId) {
117
+ const baseRoute: Route | URL | undefined = props.getTraceRoute(
118
+ traceId,
119
+ props.log,
120
+ );
121
+
122
+ if (!baseRoute) {
123
+ return undefined;
124
+ }
125
+
126
+ if (baseRoute instanceof Route) {
127
+ const nextRoute: Route = new Route(baseRoute.toString());
128
+ nextRoute.addQueryParams({ spanId });
129
+ return nextRoute;
130
+ }
131
+
132
+ const nextUrl: URL = URL.fromURL(baseRoute);
133
+ nextUrl.addQueryParam("spanId", spanId);
134
+ return nextUrl;
135
+ }
136
+
137
+ return undefined;
138
+ }, [spanId, props, traceId]);
139
+
140
+ const containerClassName: string =
141
+ variant === "embedded"
142
+ ? "rounded-xl border border-slate-900 bg-slate-950 p-5 shadow-inner shadow-slate-950/40"
143
+ : "rounded-xl border border-slate-800 bg-slate-950/90 p-5 shadow-sm ring-1 ring-inset ring-transparent";
144
+
145
+ const headerBorderClass: string =
146
+ variant === "embedded" ? "border-slate-900" : "border-slate-800";
147
+
148
+ const surfaceCardClass: string =
149
+ variant === "embedded"
150
+ ? "border-slate-900 bg-slate-950/70"
151
+ : "border-slate-800 bg-slate-950/80";
152
+
153
+ const smallBadgeClass: string =
154
+ "inline-flex items-center gap-1 rounded-full border border-slate-800 bg-slate-900 px-2 py-1 text-[11px] font-mono uppercase tracking-wide text-slate-300";
155
+
156
+ return (
157
+ <div className={containerClassName}>
158
+ <div
159
+ className={`flex flex-col gap-4 border-b ${headerBorderClass} pb-4 lg:flex-row lg:items-start lg:justify-between`}
160
+ >
161
+ <div className="flex flex-1 items-start gap-3">
162
+ <span
163
+ className="mt-1 h-3 w-3 flex-none rounded-full border border-slate-700"
164
+ style={{ backgroundColor: serviceColor }}
165
+ aria-hidden="true"
166
+ />
167
+ <div className="space-y-3">
168
+ <div className="flex flex-wrap items-center gap-3">
169
+ <h3 className="text-lg font-semibold text-slate-50">
170
+ {serviceName}
171
+ </h3>
172
+ <SeverityBadge severity={props.log.severityText} />
173
+ </div>
174
+ <div className="flex flex-wrap items-center gap-2">
175
+ {props.log.time && (
176
+ <span className={smallBadgeClass}>
177
+ <Icon icon={IconProp.Clock} className="h-3 w-3" />
178
+ {OneUptimeDate.getDateAsUserFriendlyFormattedString(
179
+ props.log.time,
180
+ )}
181
+ </span>
182
+ )}
183
+ {traceId && (
184
+ <span className={smallBadgeClass}>
185
+ <Icon icon={IconProp.Logs} className="h-3 w-3" />
186
+ Trace
187
+ </span>
188
+ )}
189
+ {spanId && (
190
+ <span className={smallBadgeClass}>
191
+ <Icon icon={IconProp.Terminal} className="h-3 w-3" />
192
+ Span
193
+ </span>
194
+ )}
195
+ </div>
196
+ </div>
197
+ </div>
198
+
199
+ {props.onClose && (
200
+ <button
201
+ type="button"
202
+ onClick={props.onClose}
203
+ className="flex h-9 w-9 items-center justify-center rounded-full border border-slate-800 bg-slate-900 text-slate-400 transition-colors hover:border-slate-700 hover:text-slate-100"
204
+ title="Close details"
205
+ >
206
+ <Icon icon={IconProp.Close} className="h-4 w-4" />
207
+ </button>
208
+ )}
209
+ </div>
210
+
211
+ <div className="mt-4 space-y-5 text-sm text-slate-200">
212
+ <section className="space-y-3">
213
+ <header className="flex items-center justify-between text-[11px] uppercase tracking-wide text-slate-500">
214
+ <span>Log Body</span>
215
+ <CopyTextButton
216
+ textToBeCopied={bodyDetails.raw}
217
+ size="xs"
218
+ variant="ghost"
219
+ iconOnly={false}
220
+ title="Copy log body"
221
+ />
222
+ </header>
223
+
224
+ <div className={`rounded-lg border ${surfaceCardClass} p-4`}>
225
+ {bodyDetails.isJson ? (
226
+ <pre className="max-h-80 overflow-auto whitespace-pre-wrap break-words font-mono text-[13px] leading-6 text-slate-100">
227
+ {bodyDetails.pretty}
228
+ </pre>
229
+ ) : (
230
+ <p className="whitespace-pre-wrap break-words font-mono text-[13px] leading-6 text-slate-100">
231
+ {bodyDetails.pretty || "—"}
232
+ </p>
233
+ )}
234
+ </div>
235
+ </section>
236
+
237
+ {(traceId || spanId) && (
238
+ <section className="grid gap-4 md:grid-cols-2">
239
+ {traceId && (
240
+ <div className={`rounded-lg border ${surfaceCardClass} p-4`}>
241
+ <div className="mb-2 flex items-center justify-between text-[11px] uppercase tracking-wide text-slate-500">
242
+ <span>Trace ID</span>
243
+ <CopyTextButton
244
+ textToBeCopied={traceId}
245
+ size="xs"
246
+ variant="ghost"
247
+ iconOnly={true}
248
+ title="Copy trace id"
249
+ />
250
+ </div>
251
+ <div className="flex items-center justify-between gap-2">
252
+ {traceRoute ? (
253
+ <Link
254
+ to={traceRoute}
255
+ className="max-w-full truncate font-mono text-xs text-indigo-200 hover:text-indigo-100"
256
+ title={`View trace ${traceId}`}
257
+ >
258
+ {traceId}
259
+ </Link>
260
+ ) : (
261
+ <span
262
+ className="max-w-full truncate font-mono text-xs text-slate-200"
263
+ title={traceId}
264
+ >
265
+ {traceId}
266
+ </span>
267
+ )}
268
+ {traceRoute && (
269
+ <Icon
270
+ icon={IconProp.ExternalLink}
271
+ className="h-4 w-4 flex-none text-indigo-300"
272
+ />
273
+ )}
274
+ </div>
275
+ </div>
276
+ )}
277
+
278
+ {spanId && (
279
+ <div className={`rounded-lg border ${surfaceCardClass} p-4`}>
280
+ <div className="mb-2 flex items-center justify-between text-[11px] uppercase tracking-wide text-slate-500">
281
+ <span>Span ID</span>
282
+ <CopyTextButton
283
+ textToBeCopied={spanId}
284
+ size="xs"
285
+ variant="ghost"
286
+ iconOnly={true}
287
+ title="Copy span id"
288
+ />
289
+ </div>
290
+ <div className="flex items-center justify-between gap-2">
291
+ {spanRoute ? (
292
+ <Link
293
+ to={spanRoute}
294
+ className="max-w-full truncate font-mono text-xs text-indigo-200 hover:text-indigo-100"
295
+ title={`View span ${spanId}`}
296
+ >
297
+ {spanId}
298
+ </Link>
299
+ ) : (
300
+ <span
301
+ className="max-w-full truncate font-mono text-xs text-slate-200"
302
+ title={spanId}
303
+ >
304
+ {spanId}
305
+ </span>
306
+ )}
307
+ {spanRoute && (
308
+ <Icon
309
+ icon={IconProp.ExternalLink}
310
+ className="h-4 w-4 flex-none text-indigo-300"
311
+ />
312
+ )}
313
+ </div>
314
+ </div>
315
+ )}
316
+ </section>
317
+ )}
318
+
319
+ {prettyAttributes && (
320
+ <section className="space-y-3">
321
+ <header className="flex items-center justify-between text-[11px] uppercase tracking-wide text-slate-500">
322
+ <span>Attributes</span>
323
+ <CopyTextButton
324
+ textToBeCopied={prettyAttributes}
325
+ size="xs"
326
+ variant="ghost"
327
+ iconOnly={false}
328
+ title="Copy attributes"
329
+ />
330
+ </header>
331
+ <div className={`rounded-lg border ${surfaceCardClass} p-4`}>
332
+ <pre className="max-h-72 overflow-auto whitespace-pre-wrap break-words font-mono text-[13px] leading-6 text-slate-100">
333
+ {prettyAttributes}
334
+ </pre>
335
+ </div>
336
+ </section>
337
+ )}
338
+ </div>
339
+ </div>
340
+ );
341
+ };
342
+
343
+ export default LogDetailsPanel;
@@ -0,0 +1,74 @@
1
+ import React, { FunctionComponent, ReactElement, ReactNode } from "react";
2
+ import Card from "../../Card/Card";
3
+ import FiltersForm from "../../Filters/FiltersForm";
4
+ import FieldType from "../../Types/FieldType";
5
+ import DropdownUtil from "../../../Utils/Dropdown";
6
+ import LogSeverity from "../../../../Types/Log/LogSeverity";
7
+ import Query from "../../../../Types/BaseDatabase/Query";
8
+ import Log from "../../../../Models/AnalyticsModels/Log";
9
+
10
+ export interface LogsFilterCardProps {
11
+ filterData: Query<Log>;
12
+ onFilterChanged: (filterData: Query<Log>) => void;
13
+ onAdvancedFiltersToggle: (show: boolean) => void;
14
+ isFilterLoading: boolean;
15
+ filterError?: string | undefined;
16
+ onFilterRefreshClick?: (() => void) | undefined;
17
+ logAttributes: Array<string>;
18
+ toolbar: ReactNode;
19
+ }
20
+
21
+ const LogsFilterCard: FunctionComponent<LogsFilterCardProps> = (
22
+ props: LogsFilterCardProps,
23
+ ): ReactElement => {
24
+ return (
25
+ <Card>
26
+ <div className="-mt-8">
27
+ <FiltersForm<Log>
28
+ id="logs-filter"
29
+ showFilter={true}
30
+ filterData={props.filterData}
31
+ onFilterChanged={props.onFilterChanged}
32
+ onAdvancedFiltersToggle={props.onAdvancedFiltersToggle}
33
+ isFilterLoading={props.isFilterLoading}
34
+ filterError={props.filterError}
35
+ onFilterRefreshClick={props.onFilterRefreshClick}
36
+ filters={[
37
+ {
38
+ key: "body",
39
+ type: FieldType.Text,
40
+ title: "Search Log",
41
+ },
42
+ {
43
+ key: "severityText",
44
+ filterDropdownOptions:
45
+ DropdownUtil.getDropdownOptionsFromEnum(LogSeverity),
46
+ type: FieldType.Dropdown,
47
+ title: "Log Severity",
48
+ isAdvancedFilter: true,
49
+ },
50
+ {
51
+ key: "time",
52
+ type: FieldType.DateTime,
53
+ title: "Start and End Date",
54
+ isAdvancedFilter: true,
55
+ },
56
+ {
57
+ key: "attributes",
58
+ type: FieldType.JSON,
59
+ title: "Filter by Attributes",
60
+ jsonKeys: props.logAttributes,
61
+ isAdvancedFilter: true,
62
+ },
63
+ ]}
64
+ />
65
+ </div>
66
+
67
+ <div className="-mx-6 -mb-6 border-t border-slate-200 bg-white/60 px-6 py-3 backdrop-blur">
68
+ {props.toolbar}
69
+ </div>
70
+ </Card>
71
+ );
72
+ };
73
+
74
+ export default LogsFilterCard;
@@ -0,0 +1,109 @@
1
+ import React, { FunctionComponent, ReactElement } from "react";
2
+
3
+ export interface LogsPaginationProps {
4
+ currentPage: number;
5
+ totalItems: number;
6
+ pageSize: number;
7
+ pageSizeOptions: Array<number>;
8
+ onPageChange: (page: number) => void;
9
+ onPageSizeChange: (size: number) => void;
10
+ isDisabled?: boolean;
11
+ }
12
+
13
+ const LogsPagination: FunctionComponent<LogsPaginationProps> = (
14
+ props: LogsPaginationProps,
15
+ ): ReactElement => {
16
+ const totalPages: number = Math.max(
17
+ 1,
18
+ Math.ceil(
19
+ props.totalItems === 0
20
+ ? 1
21
+ : props.totalItems / Math.max(props.pageSize, 1),
22
+ ),
23
+ );
24
+
25
+ const safeCurrentPage: number = Math.min(props.currentPage, totalPages);
26
+
27
+ const firstItemIndex: number =
28
+ props.totalItems === 0 ? 0 : (safeCurrentPage - 1) * props.pageSize + 1;
29
+ const lastItemIndex: number =
30
+ props.totalItems === 0
31
+ ? 0
32
+ : Math.min(props.totalItems, safeCurrentPage * props.pageSize);
33
+
34
+ const disablePrev: boolean =
35
+ props.isDisabled || props.totalItems === 0 || safeCurrentPage <= 1;
36
+ const disableNext: boolean =
37
+ props.isDisabled || props.totalItems === 0 || safeCurrentPage >= totalPages;
38
+
39
+ return (
40
+ <div className="flex flex-col gap-3 border-t border-slate-800 bg-slate-950/60 px-4 py-3 text-xs text-slate-400 md:flex-row md:items-center md:justify-between">
41
+ <div>
42
+ {props.totalItems === 0 ? (
43
+ <span>No results to display.</span>
44
+ ) : (
45
+ <span>
46
+ Showing {firstItemIndex.toLocaleString()}-
47
+ {lastItemIndex.toLocaleString()} of{" "}
48
+ {props.totalItems.toLocaleString()}
49
+ </span>
50
+ )}
51
+ </div>
52
+
53
+ <div className="flex flex-wrap items-center gap-3">
54
+ <label className="flex items-center gap-2 text-slate-500">
55
+ <span className="uppercase tracking-wide text-[10px]">Rows</span>
56
+ <select
57
+ className="rounded-md border border-slate-700 bg-slate-900/80 px-2 py-1 text-xs text-slate-200 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-50"
58
+ value={props.pageSize}
59
+ onChange={(event: React.ChangeEvent<HTMLSelectElement>) => {
60
+ const size: number = Number(event.target.value) || props.pageSize;
61
+ props.onPageSizeChange(size);
62
+ }}
63
+ disabled={props.isDisabled}
64
+ >
65
+ {props.pageSizeOptions.map((option: number) => {
66
+ return (
67
+ <option key={option} value={option}>
68
+ {option}
69
+ </option>
70
+ );
71
+ })}
72
+ </select>
73
+ </label>
74
+
75
+ <div className="inline-flex items-center gap-1 rounded-full border border-slate-800 bg-slate-900/70 p-0.5">
76
+ <button
77
+ type="button"
78
+ className="rounded-full px-3 py-1 text-xs font-medium text-slate-300 transition-colors hover:bg-slate-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-40"
79
+ onClick={() => {
80
+ if (!disablePrev) {
81
+ props.onPageChange(Math.max(1, safeCurrentPage - 1));
82
+ }
83
+ }}
84
+ disabled={disablePrev}
85
+ >
86
+ Previous
87
+ </button>
88
+ <span className="px-3 text-[11px] uppercase tracking-wide text-slate-500">
89
+ Page {safeCurrentPage} / {totalPages}
90
+ </span>
91
+ <button
92
+ type="button"
93
+ className="rounded-full px-3 py-1 text-xs font-medium text-slate-300 transition-colors hover:bg-slate-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-40"
94
+ onClick={() => {
95
+ if (!disableNext) {
96
+ props.onPageChange(Math.min(totalPages, safeCurrentPage + 1));
97
+ }
98
+ }}
99
+ disabled={disableNext}
100
+ >
101
+ Next
102
+ </button>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ );
107
+ };
108
+
109
+ export default LogsPagination;