@oneuptime/common 10.0.28 → 10.0.30
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/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/LogSavedView.ts +466 -0
- package/Server/API/TelemetryAPI.ts +146 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1772355000000-AddLogSavedView.ts +48 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1773344537755-MigrationName.ts +91 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/LogAggregationService.ts +387 -0
- package/Server/Services/LogSavedViewService.ts +109 -0
- package/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.ts +15 -0
- package/Server/Utils/Express.ts +1 -0
- package/Server/Utils/OpenAPI.ts +28 -0
- package/Server/Utils/StartServer.ts +20 -1
- package/UI/Components/LogsViewer/LogsViewer.tsx +204 -64
- package/UI/Components/LogsViewer/components/ColumnSelector.tsx +270 -0
- package/UI/Components/LogsViewer/components/LiveLogsToggle.tsx +3 -3
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +2 -2
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +699 -0
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +46 -1
- package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +3 -3
- package/UI/Components/LogsViewer/components/LogsTable.tsx +288 -103
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +113 -11
- package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +175 -0
- package/UI/Components/LogsViewer/types.ts +96 -0
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LogSavedView.js +496 -0
- package/build/dist/Models/DatabaseModels/LogSavedView.js.map +1 -0
- package/build/dist/Server/API/TelemetryAPI.js +88 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1772355000000-AddLogSavedView.js +44 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1772355000000-AddLogSavedView.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773344537755-MigrationName.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773344537755-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +249 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/LogSavedViewService.js +82 -0
- package/build/dist/Server/Services/LogSavedViewService.js.map +1 -0
- package/build/dist/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.js +15 -0
- package/build/dist/Server/Types/Workflow/Components/BaseModel/OnTriggerBaseModel.js.map +1 -1
- package/build/dist/Server/Utils/Express.js +1 -0
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Server/Utils/OpenAPI.js +24 -0
- package/build/dist/Server/Utils/OpenAPI.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +17 -2
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +77 -8
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js +115 -0
- package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js +3 -3
- package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +2 -2
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +379 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +27 -13
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +3 -3
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +118 -49
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +35 -11
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +58 -0
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/types.js +60 -1
- package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -11,6 +11,7 @@ import "./Environment";
|
|
|
11
11
|
import Express, {
|
|
12
12
|
ExpressApplication,
|
|
13
13
|
ExpressJson,
|
|
14
|
+
ExpressRaw,
|
|
14
15
|
ExpressRequest,
|
|
15
16
|
ExpressResponse,
|
|
16
17
|
ExpressStatic,
|
|
@@ -114,8 +115,26 @@ app.use((req: ExpressRequest, _res: ExpressResponse, next: NextFunction) => {
|
|
|
114
115
|
next();
|
|
115
116
|
});
|
|
116
117
|
|
|
118
|
+
/*
|
|
119
|
+
* Parse protobuf (binary) bodies for OTLP ingestion before JSON/gzip middleware.
|
|
120
|
+
* The .NET OpenTelemetry SDK (and others) send telemetry data as application/x-protobuf.
|
|
121
|
+
* Without this, express.json() skips protobuf requests and req.body remains undefined.
|
|
122
|
+
*/
|
|
123
|
+
const protobufBodyParserMiddleware: RequestHandler = ExpressRaw({
|
|
124
|
+
type: ["application/x-protobuf", "application/protobuf"],
|
|
125
|
+
limit: "50mb",
|
|
126
|
+
});
|
|
127
|
+
|
|
117
128
|
app.use((req: OneUptimeRequest, res: ExpressResponse, next: NextFunction) => {
|
|
118
|
-
|
|
129
|
+
const contentType: string | undefined = req.headers["content-type"];
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
contentType &&
|
|
133
|
+
(contentType.includes("application/x-protobuf") ||
|
|
134
|
+
contentType.includes("application/protobuf"))
|
|
135
|
+
) {
|
|
136
|
+
protobufBodyParserMiddleware(req, res, next);
|
|
137
|
+
} else if (req.headers["content-encoding"] === "gzip") {
|
|
119
138
|
const buffers: any = [];
|
|
120
139
|
|
|
121
140
|
req.on("data", (chunk: any) => {
|
|
@@ -42,9 +42,18 @@ import {
|
|
|
42
42
|
HistogramBucket,
|
|
43
43
|
FacetData,
|
|
44
44
|
ActiveFilter,
|
|
45
|
+
CORE_LOGS_TABLE_COLUMN_OPTIONS,
|
|
46
|
+
DEFAULT_LOGS_TABLE_COLUMNS,
|
|
47
|
+
getLogsAttributeColumnId,
|
|
48
|
+
LogsSavedViewOption,
|
|
49
|
+
LogsTableColumnOption,
|
|
50
|
+
LogsViewMode,
|
|
51
|
+
normalizeLogsTableColumns,
|
|
45
52
|
} from "./types";
|
|
53
|
+
import LogsAnalyticsView from "./components/LogsAnalyticsView";
|
|
46
54
|
import { queryStringToFilter } from "../../../Types/Log/LogQueryToFilter";
|
|
47
55
|
import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
|
|
56
|
+
import TimeRange from "../../../Types/Time/TimeRange";
|
|
48
57
|
|
|
49
58
|
export interface ComponentProps {
|
|
50
59
|
logs: Array<Log>;
|
|
@@ -79,11 +88,29 @@ export interface ComponentProps {
|
|
|
79
88
|
onFieldValueSelect?: ((fieldKey: string, value: string) => void) | undefined;
|
|
80
89
|
timeRange?: RangeStartAndEndDateTime | undefined;
|
|
81
90
|
onTimeRangeChange?: ((value: RangeStartAndEndDateTime) => void) | undefined;
|
|
91
|
+
selectedColumns?: Array<string> | undefined;
|
|
92
|
+
onSelectedColumnsChange?: ((columns: Array<string>) => void) | undefined;
|
|
93
|
+
savedViews?: Array<LogsSavedViewOption> | undefined;
|
|
94
|
+
selectedSavedViewId?: string | null;
|
|
95
|
+
onSavedViewSelect?: ((viewId: string) => void) | undefined;
|
|
96
|
+
onCreateSavedView?: (() => void) | undefined;
|
|
97
|
+
onEditSavedView?: ((viewId: string) => void) | undefined;
|
|
98
|
+
onDeleteSavedView?: ((viewId: string) => void) | undefined;
|
|
99
|
+
onUpdateCurrentSavedView?: (() => void) | undefined;
|
|
100
|
+
viewMode?: LogsViewMode | undefined;
|
|
101
|
+
onViewModeChange?: ((mode: LogsViewMode) => void) | undefined;
|
|
102
|
+
analyticsServiceIds?: Array<string> | undefined;
|
|
103
|
+
analyticsAppliedFacetFilters?: Map<string, Set<string>> | undefined;
|
|
82
104
|
}
|
|
83
105
|
|
|
84
106
|
export type LogsSortField = LogsTableSortField;
|
|
85
107
|
export type { LiveLogsOptions } from "./types";
|
|
86
|
-
export type {
|
|
108
|
+
export type {
|
|
109
|
+
HistogramBucket,
|
|
110
|
+
FacetData,
|
|
111
|
+
ActiveFilter,
|
|
112
|
+
LogsViewMode,
|
|
113
|
+
} from "./types";
|
|
87
114
|
|
|
88
115
|
const DEFAULT_PAGE_SIZE: number = 100;
|
|
89
116
|
const PAGE_SIZE_OPTIONS: Array<number> = [100, 250, 500, 1000];
|
|
@@ -99,6 +126,32 @@ const severityWeight: Record<string, number> = {
|
|
|
99
126
|
trace: 1,
|
|
100
127
|
};
|
|
101
128
|
|
|
129
|
+
function getEmptyMessageWithTimeRange(
|
|
130
|
+
timeRange: RangeStartAndEndDateTime | undefined,
|
|
131
|
+
): string {
|
|
132
|
+
if (!timeRange) {
|
|
133
|
+
return "Adjust filters or check again later.";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (timeRange.range === TimeRange.CUSTOM && timeRange.startAndEndDate) {
|
|
137
|
+
const startDate: Date = timeRange.startAndEndDate.startValue;
|
|
138
|
+
const endDate: Date = timeRange.startAndEndDate.endValue;
|
|
139
|
+
const fmt: (d: Date) => string = (d: Date): string => {
|
|
140
|
+
return d.toLocaleString("en-US", {
|
|
141
|
+
month: "short",
|
|
142
|
+
day: "numeric",
|
|
143
|
+
hour: "2-digit",
|
|
144
|
+
minute: "2-digit",
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return `Time range: ${fmt(startDate)} – ${fmt(endDate)}. Try adjusting filters or expanding the time range.`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const rangeLabel: string = timeRange.range.toLowerCase();
|
|
152
|
+
return `Time range: ${rangeLabel}. Try adjusting filters or expanding the time range.`;
|
|
153
|
+
}
|
|
154
|
+
|
|
102
155
|
const getSeverityWeight: (severity: string | undefined) => number = (
|
|
103
156
|
severity: string | undefined,
|
|
104
157
|
): number => {
|
|
@@ -151,6 +204,12 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
151
204
|
const [localSortOrder, setLocalSortOrder] = useState<SortOrder>(
|
|
152
205
|
SortOrder.Descending,
|
|
153
206
|
);
|
|
207
|
+
const [internalSelectedColumns, setInternalSelectedColumns] = useState<
|
|
208
|
+
Array<string>
|
|
209
|
+
>(DEFAULT_LOGS_TABLE_COLUMNS);
|
|
210
|
+
|
|
211
|
+
const [internalViewMode, setInternalViewMode] =
|
|
212
|
+
useState<LogsViewMode>("list");
|
|
154
213
|
|
|
155
214
|
useEffect(() => {
|
|
156
215
|
setFilterData(props.filterData);
|
|
@@ -174,6 +233,14 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
174
233
|
}
|
|
175
234
|
}, [props.pageSize]);
|
|
176
235
|
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (props.selectedColumns) {
|
|
238
|
+
setInternalSelectedColumns(
|
|
239
|
+
normalizeLogsTableColumns(props.selectedColumns),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}, [props.selectedColumns]);
|
|
243
|
+
|
|
177
244
|
const currentPage: number = props.page ?? internalPage;
|
|
178
245
|
const pageSize: number = props.pageSize ?? internalPageSize;
|
|
179
246
|
|
|
@@ -519,6 +586,26 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
519
586
|
};
|
|
520
587
|
}, [props.onFieldValueSelect, serviceMap]);
|
|
521
588
|
|
|
589
|
+
const selectedColumns: Array<string> = props.selectedColumns
|
|
590
|
+
? normalizeLogsTableColumns(props.selectedColumns)
|
|
591
|
+
: internalSelectedColumns;
|
|
592
|
+
|
|
593
|
+
const availableColumns: Array<LogsTableColumnOption> = useMemo(() => {
|
|
594
|
+
const attributeColumns: Array<LogsTableColumnOption> = [...logAttributes]
|
|
595
|
+
.sort((left: string, right: string) => {
|
|
596
|
+
return left.localeCompare(right);
|
|
597
|
+
})
|
|
598
|
+
.map((attributeKey: string): LogsTableColumnOption => {
|
|
599
|
+
return {
|
|
600
|
+
id: getLogsAttributeColumnId(attributeKey),
|
|
601
|
+
label: attributeKey,
|
|
602
|
+
attributeKey,
|
|
603
|
+
};
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
return [...CORE_LOGS_TABLE_COLUMN_OPTIONS, ...attributeColumns];
|
|
607
|
+
}, [logAttributes]);
|
|
608
|
+
|
|
522
609
|
if (isPageLoading) {
|
|
523
610
|
return <PageLoader isVisible={true} />;
|
|
524
611
|
}
|
|
@@ -527,10 +614,47 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
527
614
|
return <ErrorMessage message={pageError} />;
|
|
528
615
|
}
|
|
529
616
|
|
|
617
|
+
const currentViewMode: LogsViewMode = props.viewMode ?? internalViewMode;
|
|
618
|
+
|
|
619
|
+
const handleViewModeChange: (mode: LogsViewMode) => void = (
|
|
620
|
+
mode: LogsViewMode,
|
|
621
|
+
): void => {
|
|
622
|
+
if (!props.viewMode) {
|
|
623
|
+
setInternalViewMode(mode);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
props.onViewModeChange?.(mode);
|
|
627
|
+
};
|
|
628
|
+
|
|
530
629
|
const toolbarProps: LogsViewerToolbarProps = {
|
|
531
630
|
resultCount: totalItems,
|
|
532
631
|
currentPage,
|
|
533
632
|
totalPages,
|
|
633
|
+
viewMode: currentViewMode,
|
|
634
|
+
onViewModeChange: handleViewModeChange,
|
|
635
|
+
savedViews: props.savedViews,
|
|
636
|
+
selectedSavedViewId: props.selectedSavedViewId,
|
|
637
|
+
onSavedViewSelect: props.onSavedViewSelect,
|
|
638
|
+
onCreateSavedView: props.onCreateSavedView,
|
|
639
|
+
onEditSavedView: props.onEditSavedView,
|
|
640
|
+
onDeleteSavedView: props.onDeleteSavedView,
|
|
641
|
+
onUpdateCurrentSavedView: props.onUpdateCurrentSavedView,
|
|
642
|
+
...(props.onSelectedColumnsChange || props.selectedColumns
|
|
643
|
+
? {
|
|
644
|
+
availableColumns,
|
|
645
|
+
selectedColumns,
|
|
646
|
+
onSelectedColumnsChange: (columns: Array<string>) => {
|
|
647
|
+
const nextColumns: Array<string> =
|
|
648
|
+
normalizeLogsTableColumns(columns);
|
|
649
|
+
|
|
650
|
+
if (!props.selectedColumns) {
|
|
651
|
+
setInternalSelectedColumns(nextColumns);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
props.onSelectedColumnsChange?.(nextColumns);
|
|
655
|
+
},
|
|
656
|
+
}
|
|
657
|
+
: {}),
|
|
534
658
|
...(props.liveOptions ? { liveOptions: props.liveOptions } : {}),
|
|
535
659
|
...(props.timeRange && props.onTimeRangeChange
|
|
536
660
|
? {
|
|
@@ -577,73 +701,89 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
577
701
|
/>
|
|
578
702
|
)}
|
|
579
703
|
|
|
580
|
-
{/* Main content: sidebar + table */}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
<div className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
|
|
595
|
-
{!props.showFilters && (
|
|
596
|
-
<div className="border-b border-gray-100 bg-gray-50/50 px-4 py-3">
|
|
597
|
-
<LogsViewerToolbar {...toolbarProps} />
|
|
598
|
-
</div>
|
|
599
|
-
)}
|
|
600
|
-
|
|
601
|
-
<LogsTable
|
|
602
|
-
logs={displayedLogs}
|
|
704
|
+
{/* Main content: analytics view or sidebar + table */}
|
|
705
|
+
{currentViewMode === "analytics" ? (
|
|
706
|
+
<LogsAnalyticsView
|
|
707
|
+
timeRange={props.timeRange || { range: TimeRange.PAST_ONE_HOUR }}
|
|
708
|
+
serviceIds={props.analyticsServiceIds}
|
|
709
|
+
appliedFacetFilters={props.analyticsAppliedFacetFilters || new Map()}
|
|
710
|
+
logAttributes={logAttributes}
|
|
711
|
+
/>
|
|
712
|
+
) : (
|
|
713
|
+
<div className="flex gap-3">
|
|
714
|
+
{showSidebar && props.facetData && (
|
|
715
|
+
<LogsFacetSidebar
|
|
716
|
+
facetData={props.facetData}
|
|
717
|
+
isLoading={props.facetLoading || false}
|
|
603
718
|
serviceMap={serviceMap}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
return rowId;
|
|
613
|
-
});
|
|
614
|
-
}}
|
|
615
|
-
selectedLogId={selectedLogId}
|
|
616
|
-
sortField={sortField}
|
|
617
|
-
sortOrder={sortOrder}
|
|
618
|
-
onSortChange={handleSortChange}
|
|
619
|
-
renderExpandedContent={(log: Log) => {
|
|
620
|
-
return (
|
|
621
|
-
<LogDetailsPanel
|
|
622
|
-
log={log}
|
|
623
|
-
serviceMap={serviceMap}
|
|
624
|
-
onClose={() => {
|
|
625
|
-
setSelectedLogId(null);
|
|
626
|
-
}}
|
|
627
|
-
getTraceRoute={props.getTraceRoute}
|
|
628
|
-
getSpanRoute={props.getSpanRoute}
|
|
629
|
-
variant="embedded"
|
|
630
|
-
/>
|
|
631
|
-
);
|
|
632
|
-
}}
|
|
633
|
-
/>
|
|
634
|
-
|
|
635
|
-
<LogsPagination
|
|
636
|
-
currentPage={currentPage}
|
|
637
|
-
totalItems={totalItems}
|
|
638
|
-
pageSize={pageSize}
|
|
639
|
-
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
|
640
|
-
onPageChange={handlePageChange}
|
|
641
|
-
onPageSizeChange={handlePageSizeChange}
|
|
642
|
-
isDisabled={props.isLoading || totalItems === 0}
|
|
719
|
+
onIncludeFilter={props.onFacetInclude || (() => {})}
|
|
720
|
+
onExcludeFilter={props.onFacetExclude || (() => {})}
|
|
721
|
+
activeFilters={props.activeFilters}
|
|
722
|
+
savedViews={props.savedViews}
|
|
723
|
+
selectedSavedViewId={props.selectedSavedViewId}
|
|
724
|
+
onSavedViewSelect={props.onSavedViewSelect}
|
|
643
725
|
/>
|
|
726
|
+
)}
|
|
727
|
+
|
|
728
|
+
<div className="min-w-0 flex-1">
|
|
729
|
+
<div className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
|
|
730
|
+
{!props.showFilters && (
|
|
731
|
+
<div className="border-b border-gray-100 bg-gray-50/50 px-4 py-3">
|
|
732
|
+
<LogsViewerToolbar {...toolbarProps} />
|
|
733
|
+
</div>
|
|
734
|
+
)}
|
|
735
|
+
|
|
736
|
+
<LogsTable
|
|
737
|
+
logs={displayedLogs}
|
|
738
|
+
serviceMap={serviceMap}
|
|
739
|
+
isLoading={props.isLoading}
|
|
740
|
+
emptyMessage={
|
|
741
|
+
props.noLogsMessage ||
|
|
742
|
+
getEmptyMessageWithTimeRange(props.timeRange)
|
|
743
|
+
}
|
|
744
|
+
onRowClick={(_log: Log, rowId: string) => {
|
|
745
|
+
setSelectedLogId((currentSelected: string | null) => {
|
|
746
|
+
if (currentSelected === rowId) {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return rowId;
|
|
751
|
+
});
|
|
752
|
+
}}
|
|
753
|
+
selectedLogId={selectedLogId}
|
|
754
|
+
sortField={sortField}
|
|
755
|
+
sortOrder={sortOrder}
|
|
756
|
+
onSortChange={handleSortChange}
|
|
757
|
+
selectedColumns={selectedColumns}
|
|
758
|
+
renderExpandedContent={(log: Log) => {
|
|
759
|
+
return (
|
|
760
|
+
<LogDetailsPanel
|
|
761
|
+
log={log}
|
|
762
|
+
serviceMap={serviceMap}
|
|
763
|
+
onClose={() => {
|
|
764
|
+
setSelectedLogId(null);
|
|
765
|
+
}}
|
|
766
|
+
getTraceRoute={props.getTraceRoute}
|
|
767
|
+
getSpanRoute={props.getSpanRoute}
|
|
768
|
+
variant="embedded"
|
|
769
|
+
/>
|
|
770
|
+
);
|
|
771
|
+
}}
|
|
772
|
+
/>
|
|
773
|
+
|
|
774
|
+
<LogsPagination
|
|
775
|
+
currentPage={currentPage}
|
|
776
|
+
totalItems={totalItems}
|
|
777
|
+
pageSize={pageSize}
|
|
778
|
+
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
|
779
|
+
onPageChange={handlePageChange}
|
|
780
|
+
onPageSizeChange={handlePageSizeChange}
|
|
781
|
+
isDisabled={props.isLoading || totalItems === 0}
|
|
782
|
+
/>
|
|
783
|
+
</div>
|
|
644
784
|
</div>
|
|
645
785
|
</div>
|
|
646
|
-
|
|
786
|
+
)}
|
|
647
787
|
</div>
|
|
648
788
|
);
|
|
649
789
|
};
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
ChangeEvent,
|
|
3
|
+
FunctionComponent,
|
|
4
|
+
ReactElement,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
} from "react";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_LOGS_TABLE_COLUMNS,
|
|
10
|
+
LogsTableColumnOption,
|
|
11
|
+
normalizeLogsTableColumns,
|
|
12
|
+
} from "../types";
|
|
13
|
+
import useComponentOutsideClick from "../../../Types/UseComponentOutsideClick";
|
|
14
|
+
|
|
15
|
+
export interface ColumnSelectorProps {
|
|
16
|
+
availableColumns: Array<LogsTableColumnOption>;
|
|
17
|
+
selectedColumns: Array<string>;
|
|
18
|
+
onChange: (columns: Array<string>) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const triggerButtonClassName: string =
|
|
22
|
+
"inline-flex items-center gap-1.5 rounded-md border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-colors hover:border-gray-300 hover:bg-gray-50";
|
|
23
|
+
|
|
24
|
+
const actionButtonClassName: string =
|
|
25
|
+
"rounded-md px-2 py-1 text-xs font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700";
|
|
26
|
+
|
|
27
|
+
const ColumnSelector: FunctionComponent<ColumnSelectorProps> = (
|
|
28
|
+
props: ColumnSelectorProps,
|
|
29
|
+
): ReactElement => {
|
|
30
|
+
const { ref, isComponentVisible, setIsComponentVisible } =
|
|
31
|
+
useComponentOutsideClick(false);
|
|
32
|
+
const [searchQuery, setSearchQuery] = useState<string>("");
|
|
33
|
+
|
|
34
|
+
const selectedColumnIds: Array<string> = useMemo(() => {
|
|
35
|
+
return normalizeLogsTableColumns(props.selectedColumns);
|
|
36
|
+
}, [props.selectedColumns]);
|
|
37
|
+
|
|
38
|
+
const availableColumnsById: Map<string, LogsTableColumnOption> =
|
|
39
|
+
useMemo(() => {
|
|
40
|
+
return new Map(
|
|
41
|
+
props.availableColumns.map((column: LogsTableColumnOption) => {
|
|
42
|
+
return [column.id, column] as [string, LogsTableColumnOption];
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
}, [props.availableColumns]);
|
|
46
|
+
|
|
47
|
+
const selectedColumns: Array<LogsTableColumnOption> = useMemo(() => {
|
|
48
|
+
return selectedColumnIds.map((columnId: string): LogsTableColumnOption => {
|
|
49
|
+
return (
|
|
50
|
+
availableColumnsById.get(columnId) || {
|
|
51
|
+
id: columnId,
|
|
52
|
+
label: columnId,
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
}, [availableColumnsById, selectedColumnIds]);
|
|
57
|
+
|
|
58
|
+
const availableColumns: Array<LogsTableColumnOption> = useMemo(() => {
|
|
59
|
+
const normalizedSearchQuery: string = searchQuery.trim().toLowerCase();
|
|
60
|
+
|
|
61
|
+
return props.availableColumns.filter((column: LogsTableColumnOption) => {
|
|
62
|
+
if (selectedColumnIds.includes(column.id)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!normalizedSearchQuery) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return column.label.toLowerCase().includes(normalizedSearchQuery);
|
|
71
|
+
});
|
|
72
|
+
}, [props.availableColumns, searchQuery, selectedColumnIds]);
|
|
73
|
+
|
|
74
|
+
const updateColumns: (columns: Array<string>) => void = (
|
|
75
|
+
columns: Array<string>,
|
|
76
|
+
): void => {
|
|
77
|
+
props.onChange(normalizeLogsTableColumns(columns));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const moveColumn: (columnId: string, direction: -1 | 1) => void = (
|
|
81
|
+
columnId: string,
|
|
82
|
+
direction: -1 | 1,
|
|
83
|
+
): void => {
|
|
84
|
+
const currentIndex: number = selectedColumnIds.indexOf(columnId);
|
|
85
|
+
|
|
86
|
+
if (currentIndex === -1) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const nextIndex: number = currentIndex + direction;
|
|
91
|
+
|
|
92
|
+
if (nextIndex < 0 || nextIndex >= selectedColumnIds.length) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const nextColumns: Array<string> = [...selectedColumnIds];
|
|
97
|
+
const currentColumn: string = nextColumns[currentIndex] as string;
|
|
98
|
+
nextColumns[currentIndex] = nextColumns[nextIndex] as string;
|
|
99
|
+
nextColumns[nextIndex] = currentColumn;
|
|
100
|
+
|
|
101
|
+
updateColumns(nextColumns);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const removeColumn: (columnId: string) => void = (columnId: string): void => {
|
|
105
|
+
if (selectedColumnIds.length <= 1) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
updateColumns(
|
|
110
|
+
selectedColumnIds.filter((selectedColumnId: string) => {
|
|
111
|
+
return selectedColumnId !== columnId;
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const addColumn: (columnId: string) => void = (columnId: string): void => {
|
|
117
|
+
updateColumns([...selectedColumnIds, columnId]);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="relative" ref={ref}>
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
className={triggerButtonClassName}
|
|
125
|
+
onClick={() => {
|
|
126
|
+
setIsComponentVisible(!isComponentVisible);
|
|
127
|
+
}}
|
|
128
|
+
aria-haspopup="dialog"
|
|
129
|
+
aria-expanded={isComponentVisible}
|
|
130
|
+
>
|
|
131
|
+
<span>Columns</span>
|
|
132
|
+
<span className="text-xs text-gray-400">
|
|
133
|
+
{selectedColumnIds.length}
|
|
134
|
+
</span>
|
|
135
|
+
</button>
|
|
136
|
+
|
|
137
|
+
{isComponentVisible && (
|
|
138
|
+
<div className="absolute right-0 z-20 mt-2 w-96 rounded-lg border border-gray-200 bg-white p-4 shadow-xl">
|
|
139
|
+
<div className="flex items-center justify-between gap-3">
|
|
140
|
+
<div>
|
|
141
|
+
<h3 className="text-sm font-semibold text-gray-900">Columns</h3>
|
|
142
|
+
<p className="text-xs text-gray-500">
|
|
143
|
+
Add, remove, and reorder visible columns.
|
|
144
|
+
</p>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
className="text-xs font-medium text-gray-500 transition-colors hover:text-gray-700"
|
|
150
|
+
onClick={() => {
|
|
151
|
+
updateColumns(DEFAULT_LOGS_TABLE_COLUMNS);
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
Reset
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div className="mt-4">
|
|
159
|
+
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-gray-400">
|
|
160
|
+
Selected
|
|
161
|
+
</p>
|
|
162
|
+
|
|
163
|
+
<div className="space-y-2">
|
|
164
|
+
{selectedColumns.map(
|
|
165
|
+
(column: LogsTableColumnOption, index: number) => {
|
|
166
|
+
const isFirst: boolean = index === 0;
|
|
167
|
+
const isLast: boolean = index === selectedColumns.length - 1;
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
key={column.id}
|
|
172
|
+
className="flex items-center justify-between rounded-md border border-gray-200 px-3 py-2"
|
|
173
|
+
>
|
|
174
|
+
<span className="min-w-0 truncate text-sm text-gray-700">
|
|
175
|
+
{column.label}
|
|
176
|
+
</span>
|
|
177
|
+
|
|
178
|
+
<div className="flex items-center gap-1">
|
|
179
|
+
<button
|
|
180
|
+
type="button"
|
|
181
|
+
className={actionButtonClassName}
|
|
182
|
+
onClick={() => {
|
|
183
|
+
moveColumn(column.id, -1);
|
|
184
|
+
}}
|
|
185
|
+
disabled={isFirst}
|
|
186
|
+
>
|
|
187
|
+
Up
|
|
188
|
+
</button>
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
className={actionButtonClassName}
|
|
192
|
+
onClick={() => {
|
|
193
|
+
moveColumn(column.id, 1);
|
|
194
|
+
}}
|
|
195
|
+
disabled={isLast}
|
|
196
|
+
>
|
|
197
|
+
Down
|
|
198
|
+
</button>
|
|
199
|
+
<button
|
|
200
|
+
type="button"
|
|
201
|
+
className={actionButtonClassName}
|
|
202
|
+
onClick={() => {
|
|
203
|
+
removeColumn(column.id);
|
|
204
|
+
}}
|
|
205
|
+
disabled={selectedColumns.length <= 1}
|
|
206
|
+
>
|
|
207
|
+
Remove
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
},
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div className="mt-4">
|
|
218
|
+
<div className="mb-2 flex items-center justify-between gap-3">
|
|
219
|
+
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-400">
|
|
220
|
+
Available
|
|
221
|
+
</p>
|
|
222
|
+
|
|
223
|
+
<input
|
|
224
|
+
value={searchQuery}
|
|
225
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
|
226
|
+
setSearchQuery(event.target.value);
|
|
227
|
+
}}
|
|
228
|
+
placeholder="Search columns"
|
|
229
|
+
className="w-40 rounded-md border border-gray-200 px-2 py-1 text-xs text-gray-600 focus:border-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-100"
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div className="max-h-72 space-y-2 overflow-y-auto pr-1">
|
|
234
|
+
{availableColumns.length === 0 && (
|
|
235
|
+
<div className="rounded-md border border-dashed border-gray-200 px-3 py-4 text-sm text-gray-500">
|
|
236
|
+
No matching columns available.
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
{availableColumns.map((column: LogsTableColumnOption) => {
|
|
241
|
+
return (
|
|
242
|
+
<div
|
|
243
|
+
key={column.id}
|
|
244
|
+
className="flex items-center justify-between rounded-md border border-gray-100 px-3 py-2"
|
|
245
|
+
>
|
|
246
|
+
<span className="min-w-0 truncate text-sm text-gray-700">
|
|
247
|
+
{column.label}
|
|
248
|
+
</span>
|
|
249
|
+
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
className={actionButtonClassName}
|
|
253
|
+
onClick={() => {
|
|
254
|
+
addColumn(column.id);
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
Add
|
|
258
|
+
</button>
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
})}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export default ColumnSelector;
|
|
@@ -9,10 +9,10 @@ const LiveLogsToggle: FunctionComponent<LiveLogsToggleProps> = (
|
|
|
9
9
|
const { isLive, onToggle, isDisabled } = props;
|
|
10
10
|
|
|
11
11
|
const baseClasses: string =
|
|
12
|
-
"inline-flex items-center gap-
|
|
12
|
+
"inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-emerald-200";
|
|
13
13
|
const activeClasses: string = isLive
|
|
14
14
|
? "border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100"
|
|
15
|
-
: "border-gray-200 bg-white text-gray-
|
|
15
|
+
: "border-gray-200 bg-white text-gray-700 hover:bg-gray-50";
|
|
16
16
|
const disabledClasses: string = isDisabled
|
|
17
17
|
? "cursor-not-allowed opacity-50"
|
|
18
18
|
: "cursor-pointer";
|
|
@@ -36,7 +36,7 @@ const LiveLogsToggle: FunctionComponent<LiveLogsToggleProps> = (
|
|
|
36
36
|
isLive ? "bg-emerald-500 animate-pulse" : "bg-gray-300"
|
|
37
37
|
}`}
|
|
38
38
|
/>
|
|
39
|
-
<span
|
|
39
|
+
<span>Live</span>
|
|
40
40
|
</button>
|
|
41
41
|
);
|
|
42
42
|
|
|
@@ -119,10 +119,10 @@ const LogTimeRangePicker: FunctionComponent<LogTimeRangePickerProps> = (
|
|
|
119
119
|
<div ref={containerRef} className="relative">
|
|
120
120
|
<button
|
|
121
121
|
type="button"
|
|
122
|
-
className={`flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition-colors ${
|
|
122
|
+
className={`inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium shadow-sm transition-colors ${
|
|
123
123
|
isOpen
|
|
124
124
|
? "border-indigo-300 bg-indigo-50 text-indigo-700"
|
|
125
|
-
: "border-gray-200 bg-white text-gray-
|
|
125
|
+
: "border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50"
|
|
126
126
|
}`}
|
|
127
127
|
onClick={() => {
|
|
128
128
|
setIsOpen(!isOpen);
|