@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
@@ -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 Toggle from "../Toggle/Toggle";
21
- import Card from "../Card/Card";
22
- import Icon from "../Icon/Icon";
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 OptionalTraceRouteProps = {
53
- getTraceRoute?: (traceId: string, log: Log) => Route | URL | undefined;
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
- type OptionalSpanRouteProps = {
57
- getSpanRoute?: (spanId: string, log: Log) => Route | URL | undefined;
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] = React.useState<Query<Log>>(
64
- props.filterData,
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 [screenHeight, setScreenHeight] = React.useState<number>(
68
- typeof window !== "undefined" ? window.innerHeight : 900,
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
- const [isPageLoading, setIsPageLoading] = React.useState<boolean>(true);
89
- const [pageError, setPageError] = React.useState<string>("");
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
- const [serviceMap, setServiceMap] = React.useState<
92
- Dictionary<TelemetryService>
93
- >({});
214
+ const safePage: number = Math.min(Math.max(internalPage, 1), totalPages);
94
215
 
95
- const displayLogs: Array<Log> = useMemo(() => {
96
- if (isDescending) {
97
- return [...props.logs].reverse();
216
+ if (safePage !== internalPage) {
217
+ setInternalPage(safePage);
98
218
  }
219
+ }, [shouldClientPaginate, props.page, internalPage, totalPages]);
99
220
 
100
- return props.logs;
101
- }, [props.logs, isDescending]);
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
- services[service.id!.toString()!] = service;
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 attributeRepsonse: HTTPResponse<JSONObject> | HTTPErrorResponse =
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 (attributeRepsonse instanceof HTTPErrorResponse) {
157
- throw attributeRepsonse;
294
+ if (attributeResponse instanceof HTTPErrorResponse) {
295
+ throw attributeResponse;
158
296
  }
159
297
 
160
- const attributes: Array<string> = (attributeRepsonse.data[
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
- // Update the screen height when the window is resized
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
- // Keep scroll aligned with the latest log entry
193
-
194
- const scrollToLatest: VoidFunction = (): void => {
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 (isDescending) {
202
- scrollContainer.scrollTop = 0;
203
- return;
323
+ if (props.page === undefined) {
324
+ setInternalPage(1);
204
325
  }
205
-
206
- scrollContainer.scrollTop = scrollContainer.scrollHeight;
207
326
  };
208
327
 
209
- const applySortDirection: (nextDescending: boolean) => void = (
210
- nextDescending: boolean,
211
- ) => {
212
- setShowScrollToLatest(false);
213
- setIsDescending((previous: boolean) => {
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
- if (!scrollContainer) {
224
- return;
225
- }
334
+ const handlePageChange: (page: number) => void = (page: number): void => {
335
+ if (props.onPageChange) {
336
+ props.onPageChange(page);
337
+ }
226
338
 
227
- if (nextDescending) {
228
- scrollContainer.scrollTop = 0;
229
- return;
230
- }
339
+ if (props.page === undefined) {
340
+ setInternalPage(page);
341
+ }
231
342
 
232
- scrollContainer.scrollTop = scrollContainer.scrollHeight;
233
- }, 0);
343
+ setSelectedLogId(null);
234
344
 
235
- return nextDescending;
236
- });
345
+ setSelectedLogId(null);
237
346
  };
238
347
 
239
- const handleScroll: VoidFunction = React.useCallback((): void => {
240
- const scrollContainer: HTMLDivElement | null = scrollContainerRef.current;
241
- if (!scrollContainer) {
242
- return;
348
+ const handlePageSizeChange: (size: number) => void = (size: number): void => {
349
+ if (props.onPageSizeChange) {
350
+ props.onPageSizeChange(size);
243
351
  }
244
352
 
245
- const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
246
- const isNearLatest: boolean = isDescending
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
- scrollToLatest();
258
- }, [props.logs, autoScroll, isDescending]);
357
+ resetPage();
358
+ setSelectedLogId(null);
359
+ };
259
360
 
260
- React.useEffect(() => {
261
- const scrollContainer: HTMLDivElement | null = scrollContainerRef.current;
262
- if (scrollContainer) {
263
- scrollContainer.addEventListener("scroll", handleScroll);
264
- return () => {
265
- return scrollContainer.removeEventListener("scroll", handleScroll);
266
- };
267
- }
268
- return () => {}; // Return empty cleanup function if no scrollContainer
269
- }, [handleScroll]);
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
- <Card>
282
- <div className="-mt-8">
283
- <FiltersForm<Log>
284
- id="logs-filter"
285
- showFilter={props.showFilters}
286
- filterData={props.filterData}
287
- onFilterChanged={(filterData: Query<Log>) => {
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
- if (show && !attributesLoaded && !attributesLoading) {
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
- isFilterLoading={areAdvancedFiltersVisible && attributesLoading}
298
- filterError={
299
- areAdvancedFiltersVisible && attributesError
300
- ? attributesError
301
- : undefined
302
- }
303
- onFilterRefreshClick={
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
- </div>
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
- {/* Floating Scroll to Latest Button */}
477
- {showScrollToLatest && (
478
- <button
479
- onClick={scrollToLatest}
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
- </div>
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
  };