@oneuptime/common 8.0.5479 → 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.
- package/Server/Utils/Execute.ts +8 -13
- package/UI/Components/LogsViewer/LogsViewer.tsx +331 -367
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +343 -0
- package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +74 -0
- package/UI/Components/LogsViewer/components/LogsPagination.tsx +109 -0
- package/UI/Components/LogsViewer/components/LogsTable.tsx +270 -0
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +51 -0
- package/UI/Components/LogsViewer/components/SeverityBadge.tsx +28 -0
- package/UI/Components/LogsViewer/components/severityTheme.ts +69 -0
- package/build/dist/Server/Utils/Execute.js +1 -1
- package/build/dist/Server/Utils/Execute.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +211 -201
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +151 -0
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +40 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +49 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +130 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +20 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/SeverityBadge.js +13 -0
- package/build/dist/UI/Components/LogsViewer/components/SeverityBadge.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/severityTheme.js +54 -0
- package/build/dist/UI/Components/LogsViewer/components/severityTheme.js.map +1 -0
- package/package.json +1 -1
- package/UI/Components/LogsViewer/LogItem.tsx +0 -503
- package/build/dist/UI/Components/LogsViewer/LogItem.js +0 -221
- package/build/dist/UI/Components/LogsViewer/LogItem.js.map +0 -1
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
import Query from "../../../Types/BaseDatabase/Query";
|
|
2
|
-
import DropdownUtil from "../../Utils/Dropdown";
|
|
3
|
-
import ComponentLoader from "../ComponentLoader/ComponentLoader";
|
|
4
|
-
import FiltersForm from "../Filters/FiltersForm";
|
|
5
|
-
import FieldType from "../Types/FieldType";
|
|
6
|
-
import LogItem from "./LogItem";
|
|
7
|
-
import {
|
|
8
|
-
PromiseVoidFunction,
|
|
9
|
-
VoidFunction,
|
|
10
|
-
} from "../../../Types/FunctionTypes";
|
|
11
|
-
import Log from "../../../Models/AnalyticsModels/Log";
|
|
12
|
-
import LogSeverity from "../../../Types/Log/LogSeverity";
|
|
13
1
|
import React, {
|
|
14
2
|
FunctionComponent,
|
|
15
3
|
ReactElement,
|
|
16
|
-
Ref,
|
|
17
4
|
useCallback,
|
|
5
|
+
useEffect,
|
|
18
6
|
useMemo,
|
|
7
|
+
useState,
|
|
19
8
|
} from "react";
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
import Button, { ButtonSize, ButtonStyleType } from "../Button/Button";
|
|
24
|
-
import IconProp from "../../../Types/Icon/IconProp";
|
|
9
|
+
import Query from "../../../Types/BaseDatabase/Query";
|
|
10
|
+
import { PromiseVoidFunction } from "../../../Types/FunctionTypes";
|
|
11
|
+
import Log from "../../../Models/AnalyticsModels/Log";
|
|
25
12
|
import ModelAPI from "../../Utils/ModelAPI/ModelAPI";
|
|
26
13
|
import Route from "../../../Types/API/Route";
|
|
27
14
|
import URL from "../../../Types/API/URL";
|
|
@@ -37,6 +24,16 @@ import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
|
|
|
37
24
|
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
38
25
|
import ListResult from "../../../Types/BaseDatabase/ListResult";
|
|
39
26
|
import Dictionary from "../../../Types/Dictionary";
|
|
27
|
+
import LogsFilterCard from "./components/LogsFilterCard";
|
|
28
|
+
import LogsViewerToolbar, {
|
|
29
|
+
LogsViewerToolbarProps,
|
|
30
|
+
} from "./components/LogsViewerToolbar";
|
|
31
|
+
import LogsTable, {
|
|
32
|
+
LogsTableSortField,
|
|
33
|
+
resolveLogIdentifier,
|
|
34
|
+
} from "./components/LogsTable";
|
|
35
|
+
import LogsPagination from "./components/LogsPagination";
|
|
36
|
+
import LogDetailsPanel from "./components/LogDetailsPanel";
|
|
40
37
|
|
|
41
38
|
export interface ComponentProps {
|
|
42
39
|
logs: Array<Log>;
|
|
@@ -47,58 +44,195 @@ export interface ComponentProps {
|
|
|
47
44
|
noLogsMessage?: string | undefined;
|
|
48
45
|
getTraceRoute?: (traceId: string, log: Log) => Route | URL | undefined;
|
|
49
46
|
getSpanRoute?: (spanId: string, log: Log) => Route | URL | undefined;
|
|
47
|
+
totalCount?: number | undefined;
|
|
48
|
+
page?: number | undefined;
|
|
49
|
+
pageSize?: number | undefined;
|
|
50
|
+
onPageChange?: (page: number) => void;
|
|
51
|
+
onPageSizeChange?: (size: number) => void;
|
|
52
|
+
sortField?: LogsTableSortField | undefined;
|
|
53
|
+
sortOrder?: SortOrder | undefined;
|
|
54
|
+
onSortChange?: (field: LogsTableSortField, order: SortOrder) => void;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
type
|
|
53
|
-
|
|
57
|
+
export type LogsSortField = LogsTableSortField;
|
|
58
|
+
|
|
59
|
+
const DEFAULT_PAGE_SIZE: number = 100;
|
|
60
|
+
const PAGE_SIZE_OPTIONS: Array<number> = [100, 250, 500, 1000];
|
|
61
|
+
|
|
62
|
+
const severityWeight: Record<string, number> = {
|
|
63
|
+
fatal: 6,
|
|
64
|
+
error: 5,
|
|
65
|
+
warn: 4,
|
|
66
|
+
warning: 4,
|
|
67
|
+
info: 3,
|
|
68
|
+
notice: 3,
|
|
69
|
+
debug: 2,
|
|
70
|
+
trace: 1,
|
|
54
71
|
};
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
|
|
73
|
+
const getSeverityWeight: (severity: string | undefined) => number = (
|
|
74
|
+
severity: string | undefined,
|
|
75
|
+
): number => {
|
|
76
|
+
if (!severity) {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const normalized: string = severity.toString().toLowerCase();
|
|
81
|
+
return severityWeight[normalized] || 0;
|
|
58
82
|
};
|
|
59
83
|
|
|
60
84
|
const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
61
85
|
props: ComponentProps,
|
|
62
86
|
): ReactElement => {
|
|
63
|
-
const [filterData, setFilterData] =
|
|
64
|
-
|
|
87
|
+
const [filterData, setFilterData] = useState<Query<Log>>(props.filterData);
|
|
88
|
+
|
|
89
|
+
const [logAttributes, setLogAttributes] = useState<Array<string>>([]);
|
|
90
|
+
const [attributesLoaded, setAttributesLoaded] = useState<boolean>(false);
|
|
91
|
+
const [attributesLoading, setAttributesLoading] = useState<boolean>(false);
|
|
92
|
+
const [attributesError, setAttributesError] = useState<string>("");
|
|
93
|
+
const [areAdvancedFiltersVisible, setAreAdvancedFiltersVisible] =
|
|
94
|
+
useState<boolean>(false);
|
|
95
|
+
|
|
96
|
+
const [isPageLoading, setIsPageLoading] = useState<boolean>(true);
|
|
97
|
+
const [pageError, setPageError] = useState<string>("");
|
|
98
|
+
|
|
99
|
+
const [serviceMap, setServiceMap] = useState<Dictionary<TelemetryService>>(
|
|
100
|
+
{},
|
|
65
101
|
);
|
|
66
102
|
|
|
67
|
-
const [
|
|
68
|
-
|
|
103
|
+
const [selectedLogId, setSelectedLogId] = useState<string | null>(null);
|
|
104
|
+
|
|
105
|
+
const [internalPage, setInternalPage] = useState<number>(1);
|
|
106
|
+
const [internalPageSize, setInternalPageSize] =
|
|
107
|
+
useState<number>(DEFAULT_PAGE_SIZE);
|
|
108
|
+
const [localSortField, setLocalSortField] =
|
|
109
|
+
useState<LogsTableSortField>("time");
|
|
110
|
+
const [localSortOrder, setLocalSortOrder] = useState<SortOrder>(
|
|
111
|
+
SortOrder.Descending,
|
|
69
112
|
);
|
|
70
|
-
const [autoScroll, setAutoScroll] = React.useState<boolean>(true);
|
|
71
|
-
const [showScrollToLatest, setShowScrollToLatest] =
|
|
72
|
-
React.useState<boolean>(false);
|
|
73
|
-
const [isDescending, setIsDescending] = React.useState<boolean>(false);
|
|
74
|
-
// removed wrapLines toggle for a cleaner toolbar
|
|
75
|
-
const logsViewerRef: Ref<HTMLDivElement> = React.useRef<HTMLDivElement>(null);
|
|
76
|
-
const scrollContainerRef: Ref<HTMLDivElement> =
|
|
77
|
-
React.useRef<HTMLDivElement>(null);
|
|
78
|
-
|
|
79
|
-
const [logAttributes, setLogAttributes] = React.useState<Array<string>>([]);
|
|
80
|
-
const [attributesLoaded, setAttributesLoaded] =
|
|
81
|
-
React.useState<boolean>(false);
|
|
82
|
-
const [attributesLoading, setAttributesLoading] =
|
|
83
|
-
React.useState<boolean>(false);
|
|
84
|
-
const [attributesError, setAttributesError] = React.useState<string>("");
|
|
85
|
-
const [areAdvancedFiltersVisible, setAreAdvancedFiltersVisible] =
|
|
86
|
-
React.useState<boolean>(false);
|
|
87
113
|
|
|
88
|
-
|
|
89
|
-
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
setFilterData(props.filterData);
|
|
116
|
+
}, [props.filterData]);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (props.sortField) {
|
|
120
|
+
setLocalSortField(props.sortField);
|
|
121
|
+
}
|
|
122
|
+
}, [props.sortField]);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (props.sortOrder) {
|
|
126
|
+
setLocalSortOrder(props.sortOrder);
|
|
127
|
+
}
|
|
128
|
+
}, [props.sortOrder]);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (props.pageSize) {
|
|
132
|
+
setInternalPageSize(props.pageSize);
|
|
133
|
+
}
|
|
134
|
+
}, [props.pageSize]);
|
|
135
|
+
|
|
136
|
+
const currentPage: number = props.page ?? internalPage;
|
|
137
|
+
const pageSize: number = props.pageSize ?? internalPageSize;
|
|
138
|
+
|
|
139
|
+
const totalItems: number = props.totalCount ?? props.logs.length;
|
|
140
|
+
|
|
141
|
+
const totalPages: number = Math.max(
|
|
142
|
+
1,
|
|
143
|
+
Math.ceil(totalItems / Math.max(pageSize, 1)),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const sortField: LogsTableSortField = props.sortField ?? localSortField;
|
|
147
|
+
const sortOrder: SortOrder = props.sortOrder ?? localSortOrder;
|
|
148
|
+
|
|
149
|
+
const shouldClientSort: boolean = !props.onSortChange;
|
|
150
|
+
|
|
151
|
+
const sortedLogs: Array<Log> = useMemo(() => {
|
|
152
|
+
if (!shouldClientSort) {
|
|
153
|
+
return props.logs;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const cloned: Array<Log> = [...props.logs];
|
|
157
|
+
|
|
158
|
+
cloned.sort((a: Log, b: Log) => {
|
|
159
|
+
if (sortField === "time") {
|
|
160
|
+
const aTime: number =
|
|
161
|
+
Number(a.timeUnixNano) || (a.time ? new Date(a.time).getTime() : 0);
|
|
162
|
+
const bTime: number =
|
|
163
|
+
Number(b.timeUnixNano) || (b.time ? new Date(b.time).getTime() : 0);
|
|
164
|
+
|
|
165
|
+
if (aTime === bTime) {
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return sortOrder === SortOrder.Descending
|
|
170
|
+
? bTime - aTime
|
|
171
|
+
: aTime - bTime;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const aSeverity: number = getSeverityWeight(a.severityText?.toString());
|
|
175
|
+
const bSeverity: number = getSeverityWeight(b.severityText?.toString());
|
|
176
|
+
|
|
177
|
+
if (aSeverity === bSeverity) {
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return sortOrder === SortOrder.Descending
|
|
182
|
+
? bSeverity - aSeverity
|
|
183
|
+
: aSeverity - bSeverity;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return cloned;
|
|
187
|
+
}, [props.logs, shouldClientSort, sortField, sortOrder]);
|
|
188
|
+
|
|
189
|
+
const shouldClientPaginate: boolean = props.totalCount === undefined;
|
|
190
|
+
|
|
191
|
+
const paginatedLogs: Array<Log> = useMemo(() => {
|
|
192
|
+
if (!shouldClientPaginate) {
|
|
193
|
+
return sortedLogs;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (sortedLogs.length === 0) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const safePage: number = Math.min(Math.max(currentPage, 1), totalPages);
|
|
201
|
+
const startIndex: number = (safePage - 1) * pageSize;
|
|
202
|
+
return sortedLogs.slice(startIndex, startIndex + pageSize);
|
|
203
|
+
}, [sortedLogs, shouldClientPaginate, currentPage, totalPages, pageSize]);
|
|
204
|
+
|
|
205
|
+
const displayedLogs: Array<Log> = shouldClientPaginate
|
|
206
|
+
? paginatedLogs
|
|
207
|
+
: sortedLogs;
|
|
208
|
+
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (!shouldClientPaginate || props.page !== undefined) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
90
213
|
|
|
91
|
-
|
|
92
|
-
Dictionary<TelemetryService>
|
|
93
|
-
>({});
|
|
214
|
+
const safePage: number = Math.min(Math.max(internalPage, 1), totalPages);
|
|
94
215
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return [...props.logs].reverse();
|
|
216
|
+
if (safePage !== internalPage) {
|
|
217
|
+
setInternalPage(safePage);
|
|
98
218
|
}
|
|
219
|
+
}, [shouldClientPaginate, props.page, internalPage, totalPages]);
|
|
99
220
|
|
|
100
|
-
|
|
101
|
-
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (!selectedLogId) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const stillExists: boolean = displayedLogs.some(
|
|
227
|
+
(log: Log, index: number) => {
|
|
228
|
+
return resolveLogIdentifier(log, index) === selectedLogId;
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (!stillExists) {
|
|
233
|
+
setSelectedLogId(null);
|
|
234
|
+
}
|
|
235
|
+
}, [displayedLogs, selectedLogId]);
|
|
102
236
|
|
|
103
237
|
const loadTelemetryServices: PromiseVoidFunction =
|
|
104
238
|
useCallback(async (): Promise<void> => {
|
|
@@ -123,7 +257,11 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
123
257
|
const services: Dictionary<TelemetryService> = {};
|
|
124
258
|
|
|
125
259
|
telemetryServices.data.forEach((service: TelemetryService) => {
|
|
126
|
-
|
|
260
|
+
if (!service.id) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
services[service.id.toString()] = service;
|
|
127
265
|
});
|
|
128
266
|
|
|
129
267
|
setServiceMap(services);
|
|
@@ -142,7 +280,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
142
280
|
setAttributesLoading(true);
|
|
143
281
|
setAttributesError("");
|
|
144
282
|
|
|
145
|
-
const
|
|
283
|
+
const attributeResponse: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
|
146
284
|
await API.post({
|
|
147
285
|
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
|
148
286
|
"/telemetry/logs/get-attributes",
|
|
@@ -153,11 +291,11 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
153
291
|
},
|
|
154
292
|
});
|
|
155
293
|
|
|
156
|
-
if (
|
|
157
|
-
throw
|
|
294
|
+
if (attributeResponse instanceof HTTPErrorResponse) {
|
|
295
|
+
throw attributeResponse;
|
|
158
296
|
}
|
|
159
297
|
|
|
160
|
-
const attributes: Array<string> = (
|
|
298
|
+
const attributes: Array<string> = (attributeResponse.data[
|
|
161
299
|
"attributes"
|
|
162
300
|
] || []) as Array<string>;
|
|
163
301
|
setLogAttributes(attributes);
|
|
@@ -173,352 +311,178 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
173
311
|
}
|
|
174
312
|
}, []);
|
|
175
313
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
React.useEffect(() => {
|
|
314
|
+
useEffect(() => {
|
|
179
315
|
void loadTelemetryServices();
|
|
180
|
-
|
|
181
|
-
const handleResize: any = (): void => {
|
|
182
|
-
setScreenHeight(window.innerHeight);
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
window.addEventListener("resize", handleResize);
|
|
186
|
-
|
|
187
|
-
return () => {
|
|
188
|
-
window.removeEventListener("resize", handleResize);
|
|
189
|
-
};
|
|
190
316
|
}, [loadTelemetryServices]);
|
|
191
317
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const scrollContainer: HTMLDivElement | null = scrollContainerRef.current;
|
|
196
|
-
|
|
197
|
-
if (!scrollContainer) {
|
|
198
|
-
return;
|
|
318
|
+
const resetPage: () => void = (): void => {
|
|
319
|
+
if (props.onPageChange) {
|
|
320
|
+
props.onPageChange(1);
|
|
199
321
|
}
|
|
200
322
|
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
return;
|
|
323
|
+
if (props.page === undefined) {
|
|
324
|
+
setInternalPage(1);
|
|
204
325
|
}
|
|
205
|
-
|
|
206
|
-
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
207
326
|
};
|
|
208
327
|
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (previous === nextDescending) {
|
|
215
|
-
return previous;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Apply scroll alignment after the DOM reorders log entries.
|
|
219
|
-
setTimeout(() => {
|
|
220
|
-
const scrollContainer: HTMLDivElement | null =
|
|
221
|
-
scrollContainerRef.current;
|
|
328
|
+
const handleApplyFilters: () => void = (): void => {
|
|
329
|
+
resetPage();
|
|
330
|
+
setSelectedLogId(null);
|
|
331
|
+
props.onFilterChanged(filterData);
|
|
332
|
+
};
|
|
222
333
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
334
|
+
const handlePageChange: (page: number) => void = (page: number): void => {
|
|
335
|
+
if (props.onPageChange) {
|
|
336
|
+
props.onPageChange(page);
|
|
337
|
+
}
|
|
226
338
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
339
|
+
if (props.page === undefined) {
|
|
340
|
+
setInternalPage(page);
|
|
341
|
+
}
|
|
231
342
|
|
|
232
|
-
|
|
233
|
-
}, 0);
|
|
343
|
+
setSelectedLogId(null);
|
|
234
344
|
|
|
235
|
-
|
|
236
|
-
});
|
|
345
|
+
setSelectedLogId(null);
|
|
237
346
|
};
|
|
238
347
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return;
|
|
348
|
+
const handlePageSizeChange: (size: number) => void = (size: number): void => {
|
|
349
|
+
if (props.onPageSizeChange) {
|
|
350
|
+
props.onPageSizeChange(size);
|
|
243
351
|
}
|
|
244
352
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
? scrollTop < 100
|
|
248
|
-
: scrollHeight - scrollTop - clientHeight < 100;
|
|
249
|
-
setShowScrollToLatest(!isNearLatest && displayLogs.length > 0);
|
|
250
|
-
}, [isDescending, displayLogs.length]);
|
|
251
|
-
|
|
252
|
-
React.useEffect(() => {
|
|
253
|
-
if (!autoScroll) {
|
|
254
|
-
return;
|
|
353
|
+
if (props.pageSize === undefined) {
|
|
354
|
+
setInternalPageSize(size);
|
|
255
355
|
}
|
|
256
356
|
|
|
257
|
-
|
|
258
|
-
|
|
357
|
+
resetPage();
|
|
358
|
+
setSelectedLogId(null);
|
|
359
|
+
};
|
|
259
360
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
361
|
+
const handleSortChange: (field: LogsTableSortField) => void = (
|
|
362
|
+
field: LogsTableSortField,
|
|
363
|
+
): void => {
|
|
364
|
+
const isSameField: boolean = sortField === field;
|
|
365
|
+
const nextOrder: SortOrder = isSameField
|
|
366
|
+
? sortOrder === SortOrder.Descending
|
|
367
|
+
? SortOrder.Ascending
|
|
368
|
+
: SortOrder.Descending
|
|
369
|
+
: SortOrder.Descending;
|
|
370
|
+
|
|
371
|
+
setLocalSortField(field);
|
|
372
|
+
setLocalSortOrder(nextOrder);
|
|
373
|
+
|
|
374
|
+
props.onSortChange?.(field, nextOrder);
|
|
375
|
+
|
|
376
|
+
resetPage();
|
|
377
|
+
setSelectedLogId(null);
|
|
378
|
+
};
|
|
270
379
|
|
|
271
380
|
if (isPageLoading) {
|
|
272
381
|
return <PageLoader isVisible={true} />;
|
|
273
382
|
}
|
|
383
|
+
|
|
274
384
|
if (pageError) {
|
|
275
385
|
return <ErrorMessage message={pageError} />;
|
|
276
386
|
}
|
|
387
|
+
|
|
388
|
+
const toolbarProps: LogsViewerToolbarProps = {
|
|
389
|
+
resultCount: totalItems,
|
|
390
|
+
currentPage,
|
|
391
|
+
totalPages,
|
|
392
|
+
};
|
|
393
|
+
|
|
277
394
|
return (
|
|
278
|
-
<div>
|
|
395
|
+
<div className="space-y-6">
|
|
279
396
|
{props.showFilters && (
|
|
280
397
|
<div className="mb-6">
|
|
281
|
-
<
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
setFilterData(filterData);
|
|
289
|
-
}}
|
|
290
|
-
onAdvancedFiltersToggle={(show: boolean) => {
|
|
291
|
-
setAreAdvancedFiltersVisible(show);
|
|
398
|
+
<LogsFilterCard
|
|
399
|
+
filterData={filterData}
|
|
400
|
+
onFilterChanged={(updated: Query<Log>) => {
|
|
401
|
+
setFilterData(updated);
|
|
402
|
+
}}
|
|
403
|
+
onAdvancedFiltersToggle={(show: boolean) => {
|
|
404
|
+
setAreAdvancedFiltersVisible(show);
|
|
292
405
|
|
|
293
|
-
|
|
406
|
+
if (show && !attributesLoaded && !attributesLoading) {
|
|
407
|
+
void loadAttributes();
|
|
408
|
+
}
|
|
409
|
+
}}
|
|
410
|
+
isFilterLoading={areAdvancedFiltersVisible && attributesLoading}
|
|
411
|
+
filterError={
|
|
412
|
+
areAdvancedFiltersVisible && attributesError
|
|
413
|
+
? attributesError
|
|
414
|
+
: undefined
|
|
415
|
+
}
|
|
416
|
+
onFilterRefreshClick={
|
|
417
|
+
areAdvancedFiltersVisible && attributesError
|
|
418
|
+
? () => {
|
|
294
419
|
void loadAttributes();
|
|
295
420
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
areAdvancedFiltersVisible && attributesError
|
|
305
|
-
? () => {
|
|
306
|
-
void loadAttributes();
|
|
307
|
-
}
|
|
308
|
-
: undefined
|
|
309
|
-
}
|
|
310
|
-
filters={[
|
|
311
|
-
{
|
|
312
|
-
key: "body",
|
|
313
|
-
type: FieldType.Text,
|
|
314
|
-
title: "Search Log",
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
key: "severityText",
|
|
318
|
-
filterDropdownOptions:
|
|
319
|
-
DropdownUtil.getDropdownOptionsFromEnum(LogSeverity),
|
|
320
|
-
type: FieldType.Dropdown,
|
|
321
|
-
title: "Log Severity",
|
|
322
|
-
isAdvancedFilter: true,
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
key: "time",
|
|
326
|
-
type: FieldType.DateTime,
|
|
327
|
-
title: "Start and End Date",
|
|
328
|
-
isAdvancedFilter: true,
|
|
329
|
-
},
|
|
330
|
-
{
|
|
331
|
-
key: "attributes",
|
|
332
|
-
type: FieldType.JSON,
|
|
333
|
-
title: "Filter by Attributes",
|
|
334
|
-
jsonKeys: logAttributes,
|
|
335
|
-
isAdvancedFilter: true,
|
|
336
|
-
},
|
|
337
|
-
]}
|
|
421
|
+
: undefined
|
|
422
|
+
}
|
|
423
|
+
logAttributes={logAttributes}
|
|
424
|
+
toolbar={
|
|
425
|
+
<LogsViewerToolbar
|
|
426
|
+
{...toolbarProps}
|
|
427
|
+
showApplyButton={true}
|
|
428
|
+
onApplyFilters={handleApplyFilters}
|
|
338
429
|
/>
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
{/* Enhanced Controls Section */}
|
|
342
|
-
<div className="-mx-6 -mb-6 px-6 py-3 border-t border-slate-200 bg-white/50">
|
|
343
|
-
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
344
|
-
<div className="flex items-center gap-4 flex-wrap">
|
|
345
|
-
<div className="flex items-center gap-2">
|
|
346
|
-
<Toggle
|
|
347
|
-
title=""
|
|
348
|
-
value={autoScroll}
|
|
349
|
-
onChange={(checked: boolean) => {
|
|
350
|
-
return setAutoScroll(checked);
|
|
351
|
-
}}
|
|
352
|
-
/>
|
|
353
|
-
<span className="text-xs text-slate-600">
|
|
354
|
-
{autoScroll ? "Live" : "Paused"}
|
|
355
|
-
</span>
|
|
356
|
-
</div>
|
|
357
|
-
<span className="hidden sm:block h-4 w-px bg-slate-200" />
|
|
358
|
-
<span className="text-xs text-slate-500">
|
|
359
|
-
{displayLogs.length} result
|
|
360
|
-
{displayLogs.length !== 1 ? "s" : ""}
|
|
361
|
-
</span>
|
|
362
|
-
</div>
|
|
363
|
-
|
|
364
|
-
<div className="flex items-center gap-2">
|
|
365
|
-
<div className="inline-flex items-center rounded-full border border-slate-200 bg-white/80 p-1 shadow-sm ring-1 ring-slate-200/60">
|
|
366
|
-
<button
|
|
367
|
-
type="button"
|
|
368
|
-
aria-pressed={isDescending}
|
|
369
|
-
className={`flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold tracking-wide transition-all duration-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 ${
|
|
370
|
-
isDescending
|
|
371
|
-
? "bg-indigo-600 text-white shadow-sm ring-1 ring-indigo-500/40"
|
|
372
|
-
: "text-slate-500 hover:text-indigo-600"
|
|
373
|
-
}`}
|
|
374
|
-
onClick={() => {
|
|
375
|
-
applySortDirection(true);
|
|
376
|
-
}}
|
|
377
|
-
>
|
|
378
|
-
<Icon
|
|
379
|
-
icon={IconProp.BarsArrowDown}
|
|
380
|
-
className={`h-4 w-4 ${
|
|
381
|
-
isDescending ? "text-white/90" : "text-slate-400"
|
|
382
|
-
}`}
|
|
383
|
-
/>
|
|
384
|
-
<span>Newest first</span>
|
|
385
|
-
</button>
|
|
386
|
-
<button
|
|
387
|
-
type="button"
|
|
388
|
-
aria-pressed={!isDescending}
|
|
389
|
-
className={`flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold tracking-wide transition-all duration-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 ${
|
|
390
|
-
!isDescending
|
|
391
|
-
? "bg-indigo-600 text-white shadow-sm ring-1 ring-indigo-500/40"
|
|
392
|
-
: "text-slate-500 hover:text-indigo-600"
|
|
393
|
-
}`}
|
|
394
|
-
onClick={() => {
|
|
395
|
-
applySortDirection(false);
|
|
396
|
-
}}
|
|
397
|
-
>
|
|
398
|
-
<Icon
|
|
399
|
-
icon={IconProp.BarsArrowUp}
|
|
400
|
-
className={`h-4 w-4 ${
|
|
401
|
-
!isDescending ? "text-white/90" : "text-slate-400"
|
|
402
|
-
}`}
|
|
403
|
-
/>
|
|
404
|
-
<span>Oldest first</span>
|
|
405
|
-
</button>
|
|
406
|
-
</div>
|
|
407
|
-
<Button
|
|
408
|
-
title="Apply Filters"
|
|
409
|
-
icon={IconProp.Search}
|
|
410
|
-
buttonStyle={ButtonStyleType.NORMAL}
|
|
411
|
-
buttonSize={ButtonSize.Small}
|
|
412
|
-
onClick={() => {
|
|
413
|
-
return props.onFilterChanged(filterData);
|
|
414
|
-
}}
|
|
415
|
-
/>
|
|
416
|
-
</div>
|
|
417
|
-
</div>
|
|
418
|
-
</div>
|
|
419
|
-
</Card>
|
|
430
|
+
}
|
|
431
|
+
/>
|
|
420
432
|
</div>
|
|
421
433
|
)}
|
|
422
|
-
{!props.isLoading && (
|
|
423
|
-
<div className="relative">
|
|
424
|
-
<div
|
|
425
|
-
ref={logsViewerRef}
|
|
426
|
-
className="rounded-md border border-slate-700 bg-slate-900 overflow-hidden"
|
|
427
|
-
style={{
|
|
428
|
-
height: Math.max(360, screenHeight - 360),
|
|
429
|
-
}}
|
|
430
|
-
>
|
|
431
|
-
{/* Custom Scrollbar Container */}
|
|
432
|
-
<div
|
|
433
|
-
ref={scrollContainerRef}
|
|
434
|
-
className={`h-full overflow-y-auto p-2 sm:p-3 antialiased`}
|
|
435
|
-
onScroll={handleScroll}
|
|
436
|
-
>
|
|
437
|
-
<ul role="list" className="divide-y divide-slate-800">
|
|
438
|
-
{displayLogs.map((log: Log, i: number) => {
|
|
439
|
-
const traceRouteProps: OptionalTraceRouteProps =
|
|
440
|
-
props.getTraceRoute
|
|
441
|
-
? { getTraceRoute: props.getTraceRoute }
|
|
442
|
-
: {};
|
|
443
|
-
const spanRouteProps: OptionalSpanRouteProps =
|
|
444
|
-
props.getSpanRoute
|
|
445
|
-
? { getSpanRoute: props.getSpanRoute }
|
|
446
|
-
: {};
|
|
447
|
-
return (
|
|
448
|
-
<li key={i} className="py-1 first:pt-0 last:pb-0">
|
|
449
|
-
<LogItem
|
|
450
|
-
serviceMap={serviceMap}
|
|
451
|
-
log={log}
|
|
452
|
-
{...traceRouteProps}
|
|
453
|
-
{...spanRouteProps}
|
|
454
|
-
/>
|
|
455
|
-
</li>
|
|
456
|
-
);
|
|
457
|
-
})}
|
|
458
|
-
</ul>
|
|
459
|
-
|
|
460
|
-
{displayLogs.length === 0 && (
|
|
461
|
-
<div className="flex items-center justify-center h-full px-4">
|
|
462
|
-
<div className="text-center">
|
|
463
|
-
<h3 className="text-sm font-medium text-slate-300 mb-1">
|
|
464
|
-
No logs found
|
|
465
|
-
</h3>
|
|
466
|
-
<p className="text-slate-500 text-xs">
|
|
467
|
-
{props.noLogsMessage ||
|
|
468
|
-
"Adjust filters or check again later."}
|
|
469
|
-
</p>
|
|
470
|
-
</div>
|
|
471
|
-
</div>
|
|
472
|
-
)}
|
|
473
|
-
</div>
|
|
474
|
-
</div>
|
|
475
434
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
className="absolute bottom-3 right-3 bg-slate-700 hover:bg-slate-600 text-white p-2 rounded-md shadow transition-all"
|
|
481
|
-
title={isDescending ? "Scroll to top" : "Scroll to bottom"}
|
|
482
|
-
>
|
|
483
|
-
<svg
|
|
484
|
-
className="w-5 h-5"
|
|
485
|
-
fill="none"
|
|
486
|
-
stroke="currentColor"
|
|
487
|
-
viewBox="0 0 24 24"
|
|
488
|
-
>
|
|
489
|
-
{isDescending ? (
|
|
490
|
-
<path
|
|
491
|
-
strokeLinecap="round"
|
|
492
|
-
strokeLinejoin="round"
|
|
493
|
-
strokeWidth={2}
|
|
494
|
-
d="M5 14l7-7 7 7m-7-7v18"
|
|
495
|
-
/>
|
|
496
|
-
) : (
|
|
497
|
-
<path
|
|
498
|
-
strokeLinecap="round"
|
|
499
|
-
strokeLinejoin="round"
|
|
500
|
-
strokeWidth={2}
|
|
501
|
-
d="M19 10l-7 7-7-7m7 7V3"
|
|
502
|
-
/>
|
|
503
|
-
)}
|
|
504
|
-
</svg>
|
|
505
|
-
</button>
|
|
506
|
-
)}
|
|
507
|
-
</div>
|
|
508
|
-
)}
|
|
509
|
-
{props.isLoading && (
|
|
510
|
-
<div
|
|
511
|
-
className="rounded-md border border-slate-700 bg-slate-900 overflow-hidden"
|
|
512
|
-
style={{ height: Math.max(360, screenHeight - 360) }}
|
|
513
|
-
>
|
|
514
|
-
<div className="flex items-center justify-center h-full">
|
|
515
|
-
<div className="text-center">
|
|
516
|
-
<ComponentLoader />
|
|
517
|
-
<p className="text-slate-400 text-sm mt-4">Loading logs...</p>
|
|
518
|
-
</div>
|
|
435
|
+
<div className="overflow-hidden rounded-2xl border border-slate-800/70 bg-slate-950/60 shadow-xl">
|
|
436
|
+
{!props.showFilters && (
|
|
437
|
+
<div className="border-b border-slate-800/70 bg-slate-950/70 px-4 py-3">
|
|
438
|
+
<LogsViewerToolbar {...toolbarProps} />
|
|
519
439
|
</div>
|
|
520
|
-
|
|
521
|
-
|
|
440
|
+
)}
|
|
441
|
+
|
|
442
|
+
<LogsTable
|
|
443
|
+
logs={displayedLogs}
|
|
444
|
+
serviceMap={serviceMap}
|
|
445
|
+
isLoading={props.isLoading}
|
|
446
|
+
emptyMessage={props.noLogsMessage}
|
|
447
|
+
onRowClick={(_log: Log, rowId: string) => {
|
|
448
|
+
setSelectedLogId((currentSelected: string | null) => {
|
|
449
|
+
if (currentSelected === rowId) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return rowId;
|
|
454
|
+
});
|
|
455
|
+
}}
|
|
456
|
+
selectedLogId={selectedLogId}
|
|
457
|
+
sortField={sortField}
|
|
458
|
+
sortOrder={sortOrder}
|
|
459
|
+
onSortChange={handleSortChange}
|
|
460
|
+
renderExpandedContent={(log: Log) => {
|
|
461
|
+
return (
|
|
462
|
+
<LogDetailsPanel
|
|
463
|
+
log={log}
|
|
464
|
+
serviceMap={serviceMap}
|
|
465
|
+
onClose={() => {
|
|
466
|
+
setSelectedLogId(null);
|
|
467
|
+
}}
|
|
468
|
+
getTraceRoute={props.getTraceRoute}
|
|
469
|
+
getSpanRoute={props.getSpanRoute}
|
|
470
|
+
variant="embedded"
|
|
471
|
+
/>
|
|
472
|
+
);
|
|
473
|
+
}}
|
|
474
|
+
/>
|
|
475
|
+
|
|
476
|
+
<LogsPagination
|
|
477
|
+
currentPage={currentPage}
|
|
478
|
+
totalItems={totalItems}
|
|
479
|
+
pageSize={pageSize}
|
|
480
|
+
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
|
481
|
+
onPageChange={handlePageChange}
|
|
482
|
+
onPageSizeChange={handlePageSizeChange}
|
|
483
|
+
isDisabled={props.isLoading || totalItems === 0}
|
|
484
|
+
/>
|
|
485
|
+
</div>
|
|
522
486
|
</div>
|
|
523
487
|
);
|
|
524
488
|
};
|