@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,270 @@
1
+ import React, { Fragment, FunctionComponent, ReactElement } from "react";
2
+ import Log from "../../../../Models/AnalyticsModels/Log";
3
+ import TelemetryService from "../../../../Models/DatabaseModels/TelemetryService";
4
+ import Dictionary from "../../../../Types/Dictionary";
5
+ import OneUptimeDate from "../../../../Types/Date";
6
+ import CopyTextButton from "../../CopyTextButton/CopyTextButton";
7
+ import ComponentLoader from "../../ComponentLoader/ComponentLoader";
8
+ import SeverityBadge from "./SeverityBadge";
9
+ import { getSeverityTheme, SeverityTheme } from "./severityTheme";
10
+ import SortOrder from "../../../../Types/BaseDatabase/SortOrder";
11
+ import Icon from "../../Icon/Icon";
12
+ import IconProp from "../../../../Types/Icon/IconProp";
13
+
14
+ export interface LogsTableProps {
15
+ logs: Array<Log>;
16
+ serviceMap: Dictionary<TelemetryService>;
17
+ isLoading: boolean;
18
+ emptyMessage?: string | undefined;
19
+ onRowClick: (log: Log, rowId: string) => void;
20
+ selectedLogId?: string | null;
21
+ renderExpandedContent?: (log: Log) => ReactElement | null;
22
+ sortField?: LogsTableSortField | undefined;
23
+ sortOrder?: SortOrder | undefined;
24
+ onSortChange?: (field: LogsTableSortField) => void;
25
+ }
26
+
27
+ export const resolveLogIdentifier: (log: Log, index: number) => string = (
28
+ log: Log,
29
+ index: number,
30
+ ): string => {
31
+ const possibleIds: Array<unknown> = [
32
+ (log as any).id,
33
+ (log as any)._id,
34
+ (log as any)._objectId,
35
+ log.traceId,
36
+ log.timeUnixNano,
37
+ ];
38
+
39
+ for (const candidate of possibleIds) {
40
+ if (!candidate) {
41
+ continue;
42
+ }
43
+
44
+ try {
45
+ return candidate.toString();
46
+ } catch {
47
+ continue;
48
+ }
49
+ }
50
+
51
+ return `log-row-${index}`;
52
+ };
53
+
54
+ export type LogsTableSortField = "time" | "severityText";
55
+
56
+ const LogsTable: FunctionComponent<LogsTableProps> = (
57
+ props: LogsTableProps,
58
+ ): ReactElement => {
59
+ const showEmptyState: boolean = !props.isLoading && props.logs.length === 0;
60
+ const activeSortField: LogsTableSortField | undefined = props.sortField;
61
+ const activeSortOrder: SortOrder = props.sortOrder || SortOrder.Descending;
62
+
63
+ const resolveSortIcon: (field: LogsTableSortField) => IconProp = (
64
+ field: LogsTableSortField,
65
+ ): IconProp => {
66
+ if (activeSortField !== field) {
67
+ return IconProp.ArrowUpDown;
68
+ }
69
+
70
+ return activeSortOrder === SortOrder.Descending
71
+ ? IconProp.ChevronDown
72
+ : IconProp.ChevronUp;
73
+ };
74
+
75
+ const resolveSortIconClass: (field: LogsTableSortField) => string = (
76
+ field: LogsTableSortField,
77
+ ): string => {
78
+ const base: string = "h-3.5 w-3.5 flex-none transition-colors";
79
+ if (activeSortField === field) {
80
+ return `${base} text-indigo-300`;
81
+ }
82
+
83
+ return `${base} text-slate-600`;
84
+ };
85
+
86
+ return (
87
+ <div className="relative">
88
+ <div className="overflow-x-auto overflow-y-hidden border-b border-slate-900 bg-slate-950">
89
+ <table className="min-w-full divide-y divide-slate-900/80">
90
+ <thead className="bg-slate-950">
91
+ <tr className="text-left text-[11px] font-semibold uppercase tracking-wider text-slate-200">
92
+ <th scope="col" className="px-4 py-3">
93
+ <button
94
+ type="button"
95
+ className={`flex items-center gap-2 text-left font-semibold tracking-wider text-slate-300 transition-colors hover:text-slate-100 focus:outline-none ${
96
+ activeSortField === "time" ? "text-slate-100" : ""
97
+ }`}
98
+ onClick={() => {
99
+ props.onSortChange?.("time");
100
+ }}
101
+ aria-sort={
102
+ activeSortField === "time"
103
+ ? activeSortOrder === SortOrder.Descending
104
+ ? "descending"
105
+ : "ascending"
106
+ : "none"
107
+ }
108
+ >
109
+ <span>Time</span>
110
+ <Icon
111
+ icon={resolveSortIcon("time")}
112
+ className={resolveSortIconClass("time")}
113
+ aria-hidden="true"
114
+ />
115
+ </button>
116
+ </th>
117
+ <th scope="col" className="px-4 py-3">
118
+ Service
119
+ </th>
120
+ <th scope="col" className="px-4 py-3">
121
+ <button
122
+ type="button"
123
+ className={`flex items-center gap-2 text-left font-semibold tracking-wider text-slate-300 transition-colors hover:text-slate-100 focus:outline-none ${
124
+ activeSortField === "severityText" ? "text-slate-100" : ""
125
+ }`}
126
+ onClick={() => {
127
+ props.onSortChange?.("severityText");
128
+ }}
129
+ aria-sort={
130
+ activeSortField === "severityText"
131
+ ? activeSortOrder === SortOrder.Descending
132
+ ? "descending"
133
+ : "ascending"
134
+ : "none"
135
+ }
136
+ >
137
+ <span>Severity</span>
138
+ <Icon
139
+ icon={resolveSortIcon("severityText")}
140
+ className={resolveSortIconClass("severityText")}
141
+ aria-hidden="true"
142
+ />
143
+ </button>
144
+ </th>
145
+ <th scope="col" className="px-4 py-3">
146
+ Message
147
+ </th>
148
+ </tr>
149
+ </thead>
150
+ <tbody className="divide-y divide-slate-900/70 bg-slate-950">
151
+ {props.logs.map((log: Log, index: number) => {
152
+ const rowId: string = resolveLogIdentifier(log, index);
153
+ const serviceId: string = log.serviceId?.toString() || "";
154
+ const service: TelemetryService | undefined =
155
+ props.serviceMap[serviceId];
156
+ const serviceName: string =
157
+ service?.name || serviceId || "Unknown";
158
+ const serviceColor: string =
159
+ (service?.serviceColor && service?.serviceColor.toString()) ||
160
+ "#94a3b8";
161
+
162
+ const message: string = log.body?.toString() || "";
163
+ const traceId: string = log.traceId?.toString() || "";
164
+ const spanId: string = log.spanId?.toString() || "";
165
+
166
+ const isSelected: boolean = props.selectedLogId === rowId;
167
+ const severityTheme: SeverityTheme = getSeverityTheme(
168
+ log.severityText,
169
+ );
170
+
171
+ return (
172
+ <Fragment key={rowId}>
173
+ <tr
174
+ onClick={() => {
175
+ props.onRowClick(log, rowId);
176
+ }}
177
+ className={`group cursor-pointer align-top transition-colors duration-150 hover:bg-slate-900 ${
178
+ isSelected
179
+ ? "bg-slate-900 ring-1 ring-inset ring-indigo-500/40"
180
+ : ""
181
+ }`}
182
+ aria-selected={isSelected}
183
+ aria-expanded={isSelected}
184
+ >
185
+ <td className="whitespace-nowrap px-4 py-3 text-[13px] font-mono text-slate-200">
186
+ {log.time
187
+ ? OneUptimeDate.getDateAsUserFriendlyFormattedString(
188
+ log.time,
189
+ )
190
+ : "—"}
191
+ </td>
192
+ <td className="px-4 py-3">
193
+ <div className="flex items-center gap-3 text-sm text-slate-300">
194
+ <span
195
+ className="h-2.5 w-2.5 flex-none rounded-full border border-slate-900/40 shadow-sm"
196
+ style={{ backgroundColor: serviceColor }}
197
+ aria-hidden="true"
198
+ />
199
+ <span className="truncate" title={serviceName}>
200
+ {serviceName}
201
+ </span>
202
+ </div>
203
+ </td>
204
+ <td className="px-4 py-3">
205
+ <SeverityBadge severity={log.severityText} />
206
+ </td>
207
+ <td className="px-4 py-3">
208
+ <div className="flex items-start justify-between gap-3">
209
+ <div className="flex min-w-0 flex-1 flex-col gap-1">
210
+ <p
211
+ className={`whitespace-pre-wrap break-words text-sm text-slate-200 transition-colors duration-150 group-hover:text-slate-50 ${severityTheme.textClass}`}
212
+ title={message}
213
+ >
214
+ {message || "—"}
215
+ </p>
216
+ {(traceId || spanId) && (
217
+ <div className="flex flex-wrap gap-3 text-[11px] tracking-wide text-slate-500">
218
+ {traceId && <span>Trace: {traceId}</span>}
219
+ {spanId && <span>Span: {spanId}</span>}
220
+ </div>
221
+ )}
222
+ </div>
223
+ <CopyTextButton
224
+ textToBeCopied={message}
225
+ size="xs"
226
+ variant="ghost"
227
+ iconOnly={true}
228
+ title="Copy log message"
229
+ />
230
+ </div>
231
+ </td>
232
+ </tr>
233
+
234
+ {isSelected && props.renderExpandedContent && (
235
+ <tr className="bg-slate-950/70">
236
+ <td colSpan={4} className="px-6 pb-6 pt-3">
237
+ {props.renderExpandedContent(log)}
238
+ </td>
239
+ </tr>
240
+ )}
241
+ </Fragment>
242
+ );
243
+ })}
244
+ </tbody>
245
+ </table>
246
+ </div>
247
+
248
+ {props.isLoading && (
249
+ <div className="absolute inset-0 flex items-center justify-center bg-slate-950/70 backdrop-blur-md">
250
+ <ComponentLoader />
251
+ </div>
252
+ )}
253
+
254
+ {showEmptyState && (
255
+ <div className="flex h-full items-center justify-center px-6 py-12 text-center bg-slate-950">
256
+ <div className="w-full max-w-xl rounded-md border border-slate-900/70 bg-slate-950 p-6 text-left shadow-inner">
257
+ <p className="font-mono text-sm uppercase text-slate-400">
258
+ No logs found
259
+ </p>
260
+ <p className="mt-3 font-mono text-xs text-slate-500">
261
+ {props.emptyMessage || "Adjust filters or check again later."}
262
+ </p>
263
+ </div>
264
+ </div>
265
+ )}
266
+ </div>
267
+ );
268
+ };
269
+
270
+ export default LogsTable;
@@ -0,0 +1,51 @@
1
+ import React, { FunctionComponent, ReactElement } from "react";
2
+ import Button, { ButtonSize, ButtonStyleType } from "../../Button/Button";
3
+
4
+ export interface LogsViewerToolbarProps {
5
+ resultCount: number;
6
+ showApplyButton?: boolean;
7
+ onApplyFilters?: () => void;
8
+ currentPage?: number;
9
+ totalPages?: number;
10
+ className?: string;
11
+ }
12
+
13
+ const LogsViewerToolbar: FunctionComponent<LogsViewerToolbarProps> = (
14
+ props: LogsViewerToolbarProps,
15
+ ): ReactElement => {
16
+ const { currentPage, totalPages } = props;
17
+ const hasPaginationSummary: boolean = Boolean(
18
+ currentPage && totalPages && totalPages > 0,
19
+ );
20
+
21
+ return (
22
+ <div
23
+ className={`flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between ${props.className || ""}`}
24
+ >
25
+ <div className="flex flex-wrap items-center gap-3 text-xs text-slate-400">
26
+ <span className="font-medium text-slate-300">
27
+ {props.resultCount.toLocaleString()} result
28
+ {props.resultCount === 1 ? "" : "s"}
29
+ </span>
30
+ {hasPaginationSummary && (
31
+ <span className="text-slate-500">
32
+ Page {currentPage} of {totalPages}
33
+ </span>
34
+ )}
35
+ </div>
36
+
37
+ <div className="flex flex-wrap items-center gap-2">
38
+ {props.showApplyButton && props.onApplyFilters && (
39
+ <Button
40
+ title="Apply Filters"
41
+ buttonStyle={ButtonStyleType.NORMAL}
42
+ buttonSize={ButtonSize.Small}
43
+ onClick={props.onApplyFilters}
44
+ />
45
+ )}
46
+ </div>
47
+ </div>
48
+ );
49
+ };
50
+
51
+ export default LogsViewerToolbar;
@@ -0,0 +1,28 @@
1
+ import React, { FunctionComponent, ReactElement } from "react";
2
+ import LogSeverity from "../../../../Types/Log/LogSeverity";
3
+ import { getSeverityTheme, SeverityTheme } from "./severityTheme";
4
+
5
+ export interface SeverityBadgeProps {
6
+ severity?: LogSeverity | string | null | undefined;
7
+ }
8
+
9
+ const SeverityBadge: FunctionComponent<SeverityBadgeProps> = (
10
+ props: SeverityBadgeProps,
11
+ ): ReactElement => {
12
+ const label: string = props.severity
13
+ ? props.severity.toString().toUpperCase()
14
+ : "UNKNOWN";
15
+
16
+ const theme: SeverityTheme = getSeverityTheme(props.severity);
17
+
18
+ return (
19
+ <span
20
+ className={`inline-flex items-center gap-2 rounded-full px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide ring-1 ring-inset ${theme.badgeClass}`}
21
+ >
22
+ <span className={`h-1.5 w-1.5 rounded-full ${theme.dotClass}`} />
23
+ <span>{label}</span>
24
+ </span>
25
+ );
26
+ };
27
+
28
+ export default SeverityBadge;
@@ -0,0 +1,69 @@
1
+ import LogSeverity from "../../../../Types/Log/LogSeverity";
2
+
3
+ export interface SeverityTheme {
4
+ badgeClass: string;
5
+ dotClass: string;
6
+ borderClass: string;
7
+ textClass: string;
8
+ }
9
+
10
+ const severityThemeMap: Record<string, SeverityTheme> = {
11
+ [LogSeverity.Fatal]: {
12
+ badgeClass: "bg-rose-950/80 text-rose-100 ring-rose-500/40",
13
+ dotClass: "bg-rose-500",
14
+ borderClass: "border-rose-500/50",
15
+ textClass: "text-rose-100",
16
+ },
17
+ [LogSeverity.Error]: {
18
+ badgeClass: "bg-rose-900/60 text-rose-100 ring-rose-500/30",
19
+ dotClass: "bg-rose-400",
20
+ borderClass: "border-rose-500/40",
21
+ textClass: "text-rose-100",
22
+ },
23
+ [LogSeverity.Warning]: {
24
+ badgeClass: "bg-amber-900/50 text-amber-100 ring-amber-500/30",
25
+ dotClass: "bg-amber-400",
26
+ borderClass: "border-amber-400/40",
27
+ textClass: "text-amber-100",
28
+ },
29
+ [LogSeverity.Information]: {
30
+ badgeClass: "bg-sky-900/50 text-sky-100 ring-sky-500/30",
31
+ dotClass: "bg-sky-400",
32
+ borderClass: "border-sky-400/40",
33
+ textClass: "text-sky-100",
34
+ },
35
+ [LogSeverity.Debug]: {
36
+ badgeClass: "bg-purple-900/50 text-purple-100 ring-purple-500/30",
37
+ dotClass: "bg-purple-400",
38
+ borderClass: "border-purple-500/30",
39
+ textClass: "text-purple-100",
40
+ },
41
+ [LogSeverity.Trace]: {
42
+ badgeClass: "bg-slate-900/60 text-slate-300 ring-slate-500/20",
43
+ dotClass: "bg-slate-400",
44
+ borderClass: "border-slate-600/40",
45
+ textClass: "text-slate-200",
46
+ },
47
+ };
48
+
49
+ const defaultTheme: SeverityTheme = {
50
+ badgeClass: "bg-slate-800/60 text-slate-300 ring-slate-600/20",
51
+ dotClass: "bg-slate-500",
52
+ borderClass: "border-slate-700",
53
+ textClass: "text-slate-200",
54
+ };
55
+
56
+ export const getSeverityTheme: (
57
+ severity?: LogSeverity | string | null,
58
+ ) => SeverityTheme = (
59
+ severity?: LogSeverity | string | null,
60
+ ): SeverityTheme => {
61
+ if (!severity) {
62
+ return defaultTheme;
63
+ }
64
+
65
+ const normalized: string = severity.toString();
66
+ return severityThemeMap[normalized] || defaultTheme;
67
+ };
68
+
69
+ export default severityThemeMap;