@oneuptime/common 10.0.20 → 10.0.21
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/API/TelemetryAPI.ts +208 -0
- package/Server/API/UserCallAPI.ts +29 -0
- package/Server/API/UserEmailAPI.ts +29 -0
- package/Server/API/UserSmsAPI.ts +29 -0
- package/Server/API/UserWhatsAppAPI.ts +29 -0
- package/Server/Services/LogAggregationService.ts +251 -0
- package/Server/Utils/VM/VMRunner.ts +10 -0
- package/Types/Log/LogQueryParser.ts +252 -0
- package/Types/Log/LogQueryToFilter.ts +131 -0
- package/UI/Components/CopyTextButton/CopyTextButton.tsx +3 -3
- package/UI/Components/LogsViewer/LogsViewer.tsx +166 -93
- package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +58 -0
- package/UI/Components/LogsViewer/components/FacetSection.tsx +119 -0
- package/UI/Components/LogsViewer/components/FacetValueRow.tsx +102 -0
- package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +122 -0
- package/UI/Components/LogsViewer/components/LiveLogsToggle.tsx +4 -4
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +22 -26
- package/UI/Components/LogsViewer/components/LogSearchBar.tsx +360 -0
- package/UI/Components/LogsViewer/components/LogSearchHelp.tsx +128 -0
- package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +64 -0
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +199 -0
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +172 -0
- package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +27 -57
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +268 -0
- package/UI/Components/LogsViewer/components/LogsPagination.tsx +12 -10
- package/UI/Components/LogsViewer/components/LogsTable.tsx +33 -32
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +16 -18
- package/UI/Components/LogsViewer/components/severityColors.ts +31 -0
- package/UI/Components/LogsViewer/components/severityTheme.ts +25 -25
- package/UI/Components/LogsViewer/types.ts +20 -0
- package/build/dist/Server/API/TelemetryAPI.js +136 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/API/UserCallAPI.js +17 -0
- package/build/dist/Server/API/UserCallAPI.js.map +1 -1
- package/build/dist/Server/API/UserEmailAPI.js +17 -0
- package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
- package/build/dist/Server/API/UserSmsAPI.js +17 -0
- package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
- package/build/dist/Server/API/UserWhatsAppAPI.js +17 -0
- package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +163 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js +10 -0
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Types/Log/LogQueryParser.js +200 -0
- package/build/dist/Types/Log/LogQueryParser.js.map +1 -0
- package/build/dist/Types/Log/LogQueryToFilter.js +96 -0
- package/build/dist/Types/Log/LogQueryToFilter.js.map +1 -0
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +3 -3
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +64 -42
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +24 -0
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +46 -0
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +35 -0
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +64 -0
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js +4 -4
- package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +19 -21
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +230 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchHelp.js +84 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchHelp.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +27 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +100 -0
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +104 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +14 -35
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +127 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +9 -9
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +31 -30
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +7 -8
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/severityColors.js +22 -0
- package/build/dist/UI/Components/LogsViewer/components/severityColors.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/severityTheme.js +25 -25
- package/build/dist/UI/Components/LogsViewer/components/severityTheme.js.map +1 -1
- package/package.json +1 -1
|
@@ -34,7 +34,17 @@ import LogsTable, {
|
|
|
34
34
|
} from "./components/LogsTable";
|
|
35
35
|
import LogsPagination from "./components/LogsPagination";
|
|
36
36
|
import LogDetailsPanel from "./components/LogDetailsPanel";
|
|
37
|
-
import
|
|
37
|
+
import LogsHistogram from "./components/LogsHistogram";
|
|
38
|
+
import LogsFacetSidebar from "./components/LogsFacetSidebar";
|
|
39
|
+
import ActiveFilterChips from "./components/ActiveFilterChips";
|
|
40
|
+
import {
|
|
41
|
+
LiveLogsOptions,
|
|
42
|
+
HistogramBucket,
|
|
43
|
+
FacetData,
|
|
44
|
+
ActiveFilter,
|
|
45
|
+
} from "./types";
|
|
46
|
+
import { queryStringToFilter } from "../../../Types/Log/LogQueryToFilter";
|
|
47
|
+
import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
|
|
38
48
|
|
|
39
49
|
export interface ComponentProps {
|
|
40
50
|
logs: Array<Log>;
|
|
@@ -54,10 +64,26 @@ export interface ComponentProps {
|
|
|
54
64
|
sortOrder?: SortOrder | undefined;
|
|
55
65
|
onSortChange?: (field: LogsTableSortField, order: SortOrder) => void;
|
|
56
66
|
liveOptions?: LiveLogsOptions | undefined;
|
|
67
|
+
histogramBuckets?: Array<HistogramBucket>;
|
|
68
|
+
histogramLoading?: boolean;
|
|
69
|
+
onHistogramTimeRangeSelect?: (startTime: Date, endTime: Date) => void;
|
|
70
|
+
facetData?: FacetData;
|
|
71
|
+
facetLoading?: boolean;
|
|
72
|
+
onFacetInclude?: (facetKey: string, value: string) => void;
|
|
73
|
+
onFacetExclude?: (facetKey: string, value: string) => void;
|
|
74
|
+
showFacetSidebar?: boolean;
|
|
75
|
+
activeFilters?: Array<ActiveFilter> | undefined;
|
|
76
|
+
onRemoveFilter?: ((facetKey: string, value: string) => void) | undefined;
|
|
77
|
+
onClearAllFilters?: (() => void) | undefined;
|
|
78
|
+
valueSuggestions?: Record<string, Array<string>> | undefined;
|
|
79
|
+
onFieldValueSelect?: ((fieldKey: string, value: string) => void) | undefined;
|
|
80
|
+
timeRange?: RangeStartAndEndDateTime | undefined;
|
|
81
|
+
onTimeRangeChange?: ((value: RangeStartAndEndDateTime) => void) | undefined;
|
|
57
82
|
}
|
|
58
83
|
|
|
59
84
|
export type LogsSortField = LogsTableSortField;
|
|
60
85
|
export type { LiveLogsOptions } from "./types";
|
|
86
|
+
export type { HistogramBucket, FacetData, ActiveFilter } from "./types";
|
|
61
87
|
|
|
62
88
|
const DEFAULT_PAGE_SIZE: number = 100;
|
|
63
89
|
const PAGE_SIZE_OPTIONS: Array<number> = [100, 250, 500, 1000];
|
|
@@ -88,13 +114,11 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
88
114
|
props: ComponentProps,
|
|
89
115
|
): ReactElement => {
|
|
90
116
|
const [filterData, setFilterData] = useState<Query<Log>>(props.filterData);
|
|
117
|
+
const [searchQuery, setSearchQuery] = useState<string>("");
|
|
91
118
|
|
|
92
119
|
const [logAttributes, setLogAttributes] = useState<Array<string>>([]);
|
|
93
120
|
const [attributesLoaded, setAttributesLoaded] = useState<boolean>(false);
|
|
94
121
|
const [attributesLoading, setAttributesLoading] = useState<boolean>(false);
|
|
95
|
-
const [attributesError, setAttributesError] = useState<string>("");
|
|
96
|
-
const [areAdvancedFiltersVisible, setAreAdvancedFiltersVisible] =
|
|
97
|
-
useState<boolean>(false);
|
|
98
122
|
|
|
99
123
|
const [isPageLoading, setIsPageLoading] = useState<boolean>(true);
|
|
100
124
|
const [pageError, setPageError] = useState<string>("");
|
|
@@ -278,7 +302,6 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
278
302
|
useCallback(async (): Promise<void> => {
|
|
279
303
|
try {
|
|
280
304
|
setAttributesLoading(true);
|
|
281
|
-
setAttributesError("");
|
|
282
305
|
|
|
283
306
|
const attributeResponse: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
|
284
307
|
await API.post({
|
|
@@ -300,12 +323,9 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
300
323
|
] || []) as Array<string>;
|
|
301
324
|
setLogAttributes(attributes);
|
|
302
325
|
setAttributesLoaded(true);
|
|
303
|
-
} catch
|
|
326
|
+
} catch {
|
|
304
327
|
setLogAttributes([]);
|
|
305
328
|
setAttributesLoaded(false);
|
|
306
|
-
setAttributesError(
|
|
307
|
-
`We couldn't load log attributes. Filters may be limited. ${API.getFriendlyErrorMessage(err as Error)}`,
|
|
308
|
-
);
|
|
309
329
|
} finally {
|
|
310
330
|
setAttributesLoading(false);
|
|
311
331
|
}
|
|
@@ -315,6 +335,13 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
315
335
|
void loadServices();
|
|
316
336
|
}, [loadServices]);
|
|
317
337
|
|
|
338
|
+
// Load attributes eagerly for search bar suggestions
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
if (!attributesLoaded && !attributesLoading) {
|
|
341
|
+
void loadAttributes();
|
|
342
|
+
}
|
|
343
|
+
}, [attributesLoaded, attributesLoading, loadAttributes]);
|
|
344
|
+
|
|
318
345
|
const resetPage: () => void = (): void => {
|
|
319
346
|
if (props.onPageChange) {
|
|
320
347
|
props.onPageChange(1);
|
|
@@ -325,10 +352,18 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
325
352
|
}
|
|
326
353
|
};
|
|
327
354
|
|
|
328
|
-
const
|
|
355
|
+
const handleSearchSubmit: () => void = (): void => {
|
|
356
|
+
const queryFilter: Record<string, unknown> = queryStringToFilter(
|
|
357
|
+
searchQuery,
|
|
358
|
+
) as Record<string, unknown>;
|
|
359
|
+
const mergedFilter: Query<Log> = {
|
|
360
|
+
...filterData,
|
|
361
|
+
...queryFilter,
|
|
362
|
+
} as Query<Log>;
|
|
363
|
+
|
|
329
364
|
resetPage();
|
|
330
365
|
setSelectedLogId(null);
|
|
331
|
-
props.onFilterChanged(
|
|
366
|
+
props.onFilterChanged(mergedFilter);
|
|
332
367
|
};
|
|
333
368
|
|
|
334
369
|
const handlePageChange: (page: number) => void = (page: number): void => {
|
|
@@ -341,8 +376,6 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
341
376
|
}
|
|
342
377
|
|
|
343
378
|
setSelectedLogId(null);
|
|
344
|
-
|
|
345
|
-
setSelectedLogId(null);
|
|
346
379
|
};
|
|
347
380
|
|
|
348
381
|
const handlePageSizeChange: (size: number) => void = (size: number): void => {
|
|
@@ -377,6 +410,28 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
377
410
|
setSelectedLogId(null);
|
|
378
411
|
};
|
|
379
412
|
|
|
413
|
+
/*
|
|
414
|
+
* Enrich active filters with resolved display values (e.g. service names)
|
|
415
|
+
* Must be before early returns to maintain consistent hook call order.
|
|
416
|
+
*/
|
|
417
|
+
const enrichedActiveFilters: Array<ActiveFilter> = useMemo(() => {
|
|
418
|
+
if (!props.activeFilters) {
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return props.activeFilters.map((filter: ActiveFilter): ActiveFilter => {
|
|
423
|
+
if (filter.facetKey === "serviceId" && serviceMap[filter.value]) {
|
|
424
|
+
const service: Service | undefined = serviceMap[filter.value];
|
|
425
|
+
return {
|
|
426
|
+
...filter,
|
|
427
|
+
displayValue: service?.name || filter.value,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return filter;
|
|
432
|
+
});
|
|
433
|
+
}, [props.activeFilters, serviceMap]);
|
|
434
|
+
|
|
380
435
|
if (isPageLoading) {
|
|
381
436
|
return <PageLoader isVisible={true} />;
|
|
382
437
|
}
|
|
@@ -390,99 +445,117 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
390
445
|
currentPage,
|
|
391
446
|
totalPages,
|
|
392
447
|
...(props.liveOptions ? { liveOptions: props.liveOptions } : {}),
|
|
448
|
+
...(props.timeRange && props.onTimeRangeChange
|
|
449
|
+
? {
|
|
450
|
+
timeRange: props.timeRange,
|
|
451
|
+
onTimeRangeChange: props.onTimeRangeChange,
|
|
452
|
+
}
|
|
453
|
+
: {}),
|
|
393
454
|
};
|
|
394
455
|
|
|
456
|
+
const showSidebar: boolean =
|
|
457
|
+
props.showFacetSidebar !== false && Boolean(props.facetData);
|
|
458
|
+
|
|
395
459
|
return (
|
|
396
|
-
<div className="space-y-
|
|
460
|
+
<div className="space-y-2">
|
|
397
461
|
{props.showFilters && (
|
|
398
|
-
<div
|
|
462
|
+
<div>
|
|
399
463
|
<LogsFilterCard
|
|
400
|
-
filterData={filterData}
|
|
401
|
-
onFilterChanged={(updated: Query<Log>) => {
|
|
402
|
-
setFilterData(updated);
|
|
403
|
-
}}
|
|
404
|
-
onAdvancedFiltersToggle={(show: boolean) => {
|
|
405
|
-
setAreAdvancedFiltersVisible(show);
|
|
406
|
-
|
|
407
|
-
if (show && !attributesLoaded && !attributesLoading) {
|
|
408
|
-
void loadAttributes();
|
|
409
|
-
}
|
|
410
|
-
}}
|
|
411
|
-
isFilterLoading={areAdvancedFiltersVisible && attributesLoading}
|
|
412
|
-
filterError={
|
|
413
|
-
areAdvancedFiltersVisible && attributesError
|
|
414
|
-
? attributesError
|
|
415
|
-
: undefined
|
|
416
|
-
}
|
|
417
|
-
onFilterRefreshClick={
|
|
418
|
-
areAdvancedFiltersVisible && attributesError
|
|
419
|
-
? () => {
|
|
420
|
-
void loadAttributes();
|
|
421
|
-
}
|
|
422
|
-
: undefined
|
|
423
|
-
}
|
|
424
464
|
logAttributes={logAttributes}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
465
|
+
searchQuery={searchQuery}
|
|
466
|
+
onSearchQueryChange={setSearchQuery}
|
|
467
|
+
onSearchSubmit={handleSearchSubmit}
|
|
468
|
+
valueSuggestions={props.valueSuggestions}
|
|
469
|
+
onFieldValueSelect={props.onFieldValueSelect}
|
|
470
|
+
toolbar={<LogsViewerToolbar {...toolbarProps} />}
|
|
432
471
|
/>
|
|
433
472
|
</div>
|
|
434
473
|
)}
|
|
435
474
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
<LogsTable
|
|
444
|
-
logs={displayedLogs}
|
|
445
|
-
serviceMap={serviceMap}
|
|
446
|
-
isLoading={props.isLoading}
|
|
447
|
-
emptyMessage={props.noLogsMessage}
|
|
448
|
-
onRowClick={(_log: Log, rowId: string) => {
|
|
449
|
-
setSelectedLogId((currentSelected: string | null) => {
|
|
450
|
-
if (currentSelected === rowId) {
|
|
451
|
-
return null;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return rowId;
|
|
455
|
-
});
|
|
456
|
-
}}
|
|
457
|
-
selectedLogId={selectedLogId}
|
|
458
|
-
sortField={sortField}
|
|
459
|
-
sortOrder={sortOrder}
|
|
460
|
-
onSortChange={handleSortChange}
|
|
461
|
-
renderExpandedContent={(log: Log) => {
|
|
462
|
-
return (
|
|
463
|
-
<LogDetailsPanel
|
|
464
|
-
log={log}
|
|
465
|
-
serviceMap={serviceMap}
|
|
466
|
-
onClose={() => {
|
|
467
|
-
setSelectedLogId(null);
|
|
468
|
-
}}
|
|
469
|
-
getTraceRoute={props.getTraceRoute}
|
|
470
|
-
getSpanRoute={props.getSpanRoute}
|
|
471
|
-
variant="embedded"
|
|
472
|
-
/>
|
|
473
|
-
);
|
|
474
|
-
}}
|
|
475
|
+
{/* Active filter chips */}
|
|
476
|
+
{enrichedActiveFilters.length > 0 && props.onRemoveFilter && (
|
|
477
|
+
<ActiveFilterChips
|
|
478
|
+
filters={enrichedActiveFilters}
|
|
479
|
+
onRemove={props.onRemoveFilter}
|
|
480
|
+
onClearAll={props.onClearAllFilters || (() => {})}
|
|
475
481
|
/>
|
|
482
|
+
)}
|
|
476
483
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
onPageSizeChange={handlePageSizeChange}
|
|
484
|
-
isDisabled={props.isLoading || totalItems === 0}
|
|
484
|
+
{/* Histogram */}
|
|
485
|
+
{props.histogramBuckets && (
|
|
486
|
+
<LogsHistogram
|
|
487
|
+
buckets={props.histogramBuckets}
|
|
488
|
+
isLoading={props.histogramLoading || false}
|
|
489
|
+
onTimeRangeSelect={props.onHistogramTimeRangeSelect}
|
|
485
490
|
/>
|
|
491
|
+
)}
|
|
492
|
+
|
|
493
|
+
{/* Main content: sidebar + table */}
|
|
494
|
+
<div className="flex gap-3">
|
|
495
|
+
{showSidebar && props.facetData && (
|
|
496
|
+
<LogsFacetSidebar
|
|
497
|
+
facetData={props.facetData}
|
|
498
|
+
isLoading={props.facetLoading || false}
|
|
499
|
+
serviceMap={serviceMap}
|
|
500
|
+
onIncludeFilter={props.onFacetInclude || (() => {})}
|
|
501
|
+
onExcludeFilter={props.onFacetExclude || (() => {})}
|
|
502
|
+
activeFilters={props.activeFilters}
|
|
503
|
+
/>
|
|
504
|
+
)}
|
|
505
|
+
|
|
506
|
+
<div className="min-w-0 flex-1">
|
|
507
|
+
<div className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
|
|
508
|
+
{!props.showFilters && (
|
|
509
|
+
<div className="border-b border-gray-100 bg-gray-50/50 px-4 py-3">
|
|
510
|
+
<LogsViewerToolbar {...toolbarProps} />
|
|
511
|
+
</div>
|
|
512
|
+
)}
|
|
513
|
+
|
|
514
|
+
<LogsTable
|
|
515
|
+
logs={displayedLogs}
|
|
516
|
+
serviceMap={serviceMap}
|
|
517
|
+
isLoading={props.isLoading}
|
|
518
|
+
emptyMessage={props.noLogsMessage}
|
|
519
|
+
onRowClick={(_log: Log, rowId: string) => {
|
|
520
|
+
setSelectedLogId((currentSelected: string | null) => {
|
|
521
|
+
if (currentSelected === rowId) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return rowId;
|
|
526
|
+
});
|
|
527
|
+
}}
|
|
528
|
+
selectedLogId={selectedLogId}
|
|
529
|
+
sortField={sortField}
|
|
530
|
+
sortOrder={sortOrder}
|
|
531
|
+
onSortChange={handleSortChange}
|
|
532
|
+
renderExpandedContent={(log: Log) => {
|
|
533
|
+
return (
|
|
534
|
+
<LogDetailsPanel
|
|
535
|
+
log={log}
|
|
536
|
+
serviceMap={serviceMap}
|
|
537
|
+
onClose={() => {
|
|
538
|
+
setSelectedLogId(null);
|
|
539
|
+
}}
|
|
540
|
+
getTraceRoute={props.getTraceRoute}
|
|
541
|
+
getSpanRoute={props.getSpanRoute}
|
|
542
|
+
variant="embedded"
|
|
543
|
+
/>
|
|
544
|
+
);
|
|
545
|
+
}}
|
|
546
|
+
/>
|
|
547
|
+
|
|
548
|
+
<LogsPagination
|
|
549
|
+
currentPage={currentPage}
|
|
550
|
+
totalItems={totalItems}
|
|
551
|
+
pageSize={pageSize}
|
|
552
|
+
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
|
553
|
+
onPageChange={handlePageChange}
|
|
554
|
+
onPageSizeChange={handlePageSizeChange}
|
|
555
|
+
isDisabled={props.isLoading || totalItems === 0}
|
|
556
|
+
/>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
486
559
|
</div>
|
|
487
560
|
</div>
|
|
488
561
|
);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
import { ActiveFilter } from "../types";
|
|
3
|
+
import Icon from "../../Icon/Icon";
|
|
4
|
+
import IconProp from "../../../../Types/Icon/IconProp";
|
|
5
|
+
|
|
6
|
+
export interface ActiveFilterChipsProps {
|
|
7
|
+
filters: Array<ActiveFilter>;
|
|
8
|
+
onRemove: (facetKey: string, value: string) => void;
|
|
9
|
+
onClearAll: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ActiveFilterChips: FunctionComponent<ActiveFilterChipsProps> = (
|
|
13
|
+
props: ActiveFilterChipsProps,
|
|
14
|
+
): ReactElement | null => {
|
|
15
|
+
if (props.filters.length === 0) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex flex-wrap items-center gap-1.5 px-0.5">
|
|
21
|
+
{props.filters.map((filter: ActiveFilter) => {
|
|
22
|
+
const chipKey: string = `${filter.facetKey}:${filter.value}`;
|
|
23
|
+
return (
|
|
24
|
+
<span
|
|
25
|
+
key={chipKey}
|
|
26
|
+
className="inline-flex items-center gap-1 rounded-md border border-indigo-200 bg-indigo-50 py-0.5 pl-2 pr-1 text-xs text-indigo-700"
|
|
27
|
+
>
|
|
28
|
+
<span className="font-medium text-indigo-500">
|
|
29
|
+
{filter.displayKey}:
|
|
30
|
+
</span>
|
|
31
|
+
<span>{filter.displayValue}</span>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
className="ml-0.5 inline-flex h-4 w-4 items-center justify-center rounded text-indigo-400 transition-colors hover:bg-indigo-100 hover:text-indigo-600"
|
|
35
|
+
onClick={() => {
|
|
36
|
+
props.onRemove(filter.facetKey, filter.value);
|
|
37
|
+
}}
|
|
38
|
+
title={`Remove ${filter.displayKey}: ${filter.displayValue}`}
|
|
39
|
+
>
|
|
40
|
+
<Icon icon={IconProp.Close} className="h-2.5 w-2.5" />
|
|
41
|
+
</button>
|
|
42
|
+
</span>
|
|
43
|
+
);
|
|
44
|
+
})}
|
|
45
|
+
{props.filters.length > 1 && (
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
className="rounded px-1.5 py-0.5 text-[11px] font-medium text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
|
|
49
|
+
onClick={props.onClearAll}
|
|
50
|
+
>
|
|
51
|
+
Clear all
|
|
52
|
+
</button>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default ActiveFilterChips;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement, useState } from "react";
|
|
2
|
+
import { FacetValue } from "../types";
|
|
3
|
+
import FacetValueRow from "./FacetValueRow";
|
|
4
|
+
import Icon from "../../Icon/Icon";
|
|
5
|
+
import IconProp from "../../../../Types/Icon/IconProp";
|
|
6
|
+
|
|
7
|
+
export interface FacetSectionProps {
|
|
8
|
+
title: string;
|
|
9
|
+
values: Array<FacetValue>;
|
|
10
|
+
initialVisibleCount?: number;
|
|
11
|
+
onIncludeValue: (key: string, value: string) => void;
|
|
12
|
+
onExcludeValue: (key: string, value: string) => void;
|
|
13
|
+
facetKey: string;
|
|
14
|
+
valueDisplayMap?: Record<string, string> | undefined;
|
|
15
|
+
valueColorMap?: Record<string, string> | undefined;
|
|
16
|
+
activeValues?: Set<string> | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_VISIBLE_COUNT: number = 5;
|
|
20
|
+
|
|
21
|
+
const FacetSection: FunctionComponent<FacetSectionProps> = (
|
|
22
|
+
props: FacetSectionProps,
|
|
23
|
+
): ReactElement => {
|
|
24
|
+
const [isExpanded, setIsExpanded] = useState<boolean>(true);
|
|
25
|
+
const [showAll, setShowAll] = useState<boolean>(false);
|
|
26
|
+
|
|
27
|
+
const visibleCount: number =
|
|
28
|
+
props.initialVisibleCount ?? DEFAULT_VISIBLE_COUNT;
|
|
29
|
+
|
|
30
|
+
const displayedValues: Array<FacetValue> = showAll
|
|
31
|
+
? props.values
|
|
32
|
+
: props.values.slice(0, visibleCount);
|
|
33
|
+
|
|
34
|
+
const hasMore: boolean = props.values.length > visibleCount;
|
|
35
|
+
|
|
36
|
+
const maxCount: number =
|
|
37
|
+
props.values.length > 0
|
|
38
|
+
? Math.max(
|
|
39
|
+
...props.values.map((v: FacetValue) => {
|
|
40
|
+
return v.count;
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
: 0;
|
|
44
|
+
|
|
45
|
+
const activeCount: number = props.activeValues ? props.activeValues.size : 0;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="border-b border-gray-100 py-2">
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
className="flex w-full items-center justify-between px-2 py-1 text-left"
|
|
52
|
+
onClick={() => {
|
|
53
|
+
setIsExpanded(!isExpanded);
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<div className="flex items-center gap-1.5">
|
|
57
|
+
<span className="text-[11px] font-semibold uppercase tracking-wider text-gray-500">
|
|
58
|
+
{props.title}
|
|
59
|
+
</span>
|
|
60
|
+
{activeCount > 0 && (
|
|
61
|
+
<span className="inline-flex h-4 min-w-[1rem] items-center justify-center rounded-full bg-indigo-100 px-1 text-[10px] font-semibold text-indigo-600">
|
|
62
|
+
{activeCount}
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
<Icon
|
|
67
|
+
icon={isExpanded ? IconProp.ChevronDown : IconProp.ChevronRight}
|
|
68
|
+
className="h-3 w-3 text-gray-400"
|
|
69
|
+
/>
|
|
70
|
+
</button>
|
|
71
|
+
|
|
72
|
+
{isExpanded && (
|
|
73
|
+
<div className="mt-1 px-1">
|
|
74
|
+
{displayedValues.map((facet: FacetValue) => {
|
|
75
|
+
return (
|
|
76
|
+
<FacetValueRow
|
|
77
|
+
key={facet.value}
|
|
78
|
+
value={facet.value}
|
|
79
|
+
displayValue={props.valueDisplayMap?.[facet.value]}
|
|
80
|
+
count={facet.count}
|
|
81
|
+
maxCount={maxCount}
|
|
82
|
+
color={props.valueColorMap?.[facet.value]}
|
|
83
|
+
isActive={props.activeValues?.has(facet.value) || false}
|
|
84
|
+
onInclude={(value: string) => {
|
|
85
|
+
props.onIncludeValue(props.facetKey, value);
|
|
86
|
+
}}
|
|
87
|
+
onExclude={(value: string) => {
|
|
88
|
+
props.onExcludeValue(props.facetKey, value);
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
|
|
94
|
+
{props.values.length === 0 && (
|
|
95
|
+
<p className="px-1 py-2 text-[11px] text-gray-400">
|
|
96
|
+
No values found
|
|
97
|
+
</p>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{hasMore && (
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
className="mt-1 px-1 text-[11px] font-medium text-indigo-500 hover:text-indigo-600"
|
|
104
|
+
onClick={() => {
|
|
105
|
+
setShowAll(!showAll);
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{showAll
|
|
109
|
+
? "Show less"
|
|
110
|
+
: `+${props.values.length - visibleCount} more`}
|
|
111
|
+
</button>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default FacetSection;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
import Icon from "../../Icon/Icon";
|
|
3
|
+
import IconProp from "../../../../Types/Icon/IconProp";
|
|
4
|
+
|
|
5
|
+
export interface FacetValueRowProps {
|
|
6
|
+
value: string;
|
|
7
|
+
displayValue?: string | undefined;
|
|
8
|
+
count: number;
|
|
9
|
+
maxCount: number;
|
|
10
|
+
color?: string | undefined;
|
|
11
|
+
isActive?: boolean | undefined;
|
|
12
|
+
onInclude: (value: string) => void;
|
|
13
|
+
onExclude: (value: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const FacetValueRow: FunctionComponent<FacetValueRowProps> = (
|
|
17
|
+
props: FacetValueRowProps,
|
|
18
|
+
): ReactElement => {
|
|
19
|
+
const barWidth: number =
|
|
20
|
+
props.maxCount > 0
|
|
21
|
+
? Math.max(4, Math.round((props.count / props.maxCount) * 100))
|
|
22
|
+
: 0;
|
|
23
|
+
|
|
24
|
+
const displayLabel: string = props.displayValue || props.value || "(empty)";
|
|
25
|
+
const isActive: boolean = props.isActive || false;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="group flex items-center gap-2 py-0.5">
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
className={`flex min-w-0 flex-1 items-center gap-2 rounded px-1.5 py-0.5 text-left transition-colors ${
|
|
32
|
+
isActive ? "bg-indigo-50 ring-1 ring-indigo-200" : "hover:bg-gray-50"
|
|
33
|
+
}`}
|
|
34
|
+
onClick={() => {
|
|
35
|
+
props.onInclude(props.value);
|
|
36
|
+
}}
|
|
37
|
+
title={
|
|
38
|
+
isActive
|
|
39
|
+
? `Remove filter: ${displayLabel}`
|
|
40
|
+
: `Filter to ${displayLabel}`
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
{isActive ? (
|
|
44
|
+
<span className="flex h-3.5 w-3.5 flex-none items-center justify-center rounded bg-indigo-500">
|
|
45
|
+
<Icon icon={IconProp.Check} className="h-2.5 w-2.5 text-white" />
|
|
46
|
+
</span>
|
|
47
|
+
) : props.color ? (
|
|
48
|
+
<span
|
|
49
|
+
className="h-2.5 w-2.5 flex-none rounded-full"
|
|
50
|
+
style={{ backgroundColor: props.color }}
|
|
51
|
+
/>
|
|
52
|
+
) : (
|
|
53
|
+
<span className="h-2.5 w-2.5 flex-none rounded-full bg-gray-300" />
|
|
54
|
+
)}
|
|
55
|
+
<span
|
|
56
|
+
className={`min-w-0 truncate text-[12px] ${
|
|
57
|
+
isActive ? "font-medium text-indigo-700" : "text-gray-700"
|
|
58
|
+
}`}
|
|
59
|
+
>
|
|
60
|
+
{displayLabel}
|
|
61
|
+
</span>
|
|
62
|
+
</button>
|
|
63
|
+
|
|
64
|
+
<div className="flex items-center gap-1.5">
|
|
65
|
+
<div className="w-12">
|
|
66
|
+
<div className="h-1.5 w-full rounded-full bg-gray-100">
|
|
67
|
+
<div
|
|
68
|
+
className={`h-1.5 rounded-full transition-all ${isActive ? "opacity-100" : "opacity-70"}`}
|
|
69
|
+
style={{
|
|
70
|
+
width: `${barWidth}%`,
|
|
71
|
+
backgroundColor: isActive
|
|
72
|
+
? "#6366f1"
|
|
73
|
+
: props.color || "#9ca3af",
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<span
|
|
79
|
+
className={`min-w-[2rem] text-right font-mono text-[10px] tabular-nums ${
|
|
80
|
+
isActive ? "font-medium text-indigo-600" : "text-gray-400"
|
|
81
|
+
}`}
|
|
82
|
+
>
|
|
83
|
+
{props.count.toLocaleString()}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className="hidden h-5 w-5 items-center justify-center rounded text-[10px] text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500 group-hover:flex"
|
|
90
|
+
onClick={(e: React.MouseEvent) => {
|
|
91
|
+
e.stopPropagation();
|
|
92
|
+
props.onExclude(props.value);
|
|
93
|
+
}}
|
|
94
|
+
title={`Exclude ${displayLabel}`}
|
|
95
|
+
>
|
|
96
|
+
-
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export default FacetValueRow;
|