@oneuptime/common 10.0.28 → 10.0.29
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/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/LogSavedViewService.ts +109 -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 +104 -1
- 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/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 +53 -11
- package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +175 -0
- package/UI/Components/LogsViewer/types.ts +94 -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/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/LogSavedViewService.js +82 -0
- package/build/dist/Server/Services/LogSavedViewService.js.map +1 -0
- 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 +64 -5
- 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/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 +18 -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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import DatabaseService from "./DatabaseService";
|
|
2
|
+
import Model from "../../Models/DatabaseModels/LogSavedView";
|
|
3
|
+
import CreateBy from "../Types/Database/CreateBy";
|
|
4
|
+
import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
|
|
5
|
+
import UpdateBy from "../Types/Database/UpdateBy";
|
|
6
|
+
import ObjectID from "../../Types/ObjectID";
|
|
7
|
+
import QueryHelper from "../Types/Database/QueryHelper";
|
|
8
|
+
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
9
|
+
|
|
10
|
+
export class Service extends DatabaseService<Model> {
|
|
11
|
+
public constructor() {
|
|
12
|
+
super(Model);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
protected override async onBeforeCreate(
|
|
16
|
+
createBy: CreateBy<Model>,
|
|
17
|
+
): Promise<OnCreate<Model>> {
|
|
18
|
+
if (createBy.data.isDefault === undefined && createBy.data.projectId) {
|
|
19
|
+
const existingDefaultView: Model | null = await this.findOneBy({
|
|
20
|
+
query: {
|
|
21
|
+
projectId: createBy.data.projectId,
|
|
22
|
+
isDefault: true,
|
|
23
|
+
},
|
|
24
|
+
select: {
|
|
25
|
+
_id: true,
|
|
26
|
+
},
|
|
27
|
+
props: {
|
|
28
|
+
isRoot: true,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
createBy.data.isDefault = !existingDefaultView;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (createBy.data.projectId) {
|
|
36
|
+
await this.unsetOtherDefaultsIfNeeded({
|
|
37
|
+
projectId: createBy.data.projectId,
|
|
38
|
+
isDefault: createBy.data.isDefault || false,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { createBy, carryForward: null };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected override async onBeforeUpdate(
|
|
46
|
+
updateBy: UpdateBy<Model>,
|
|
47
|
+
): Promise<OnUpdate<Model>> {
|
|
48
|
+
if (updateBy.data.isDefault !== true) {
|
|
49
|
+
return { updateBy, carryForward: null };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const itemsToUpdate: Array<Model> = await this.findBy({
|
|
53
|
+
query: updateBy.query,
|
|
54
|
+
select: {
|
|
55
|
+
_id: true,
|
|
56
|
+
projectId: true,
|
|
57
|
+
},
|
|
58
|
+
props: {
|
|
59
|
+
isRoot: true,
|
|
60
|
+
},
|
|
61
|
+
limit: LIMIT_MAX,
|
|
62
|
+
skip: 0,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
for (const item of itemsToUpdate) {
|
|
66
|
+
if (item.projectId) {
|
|
67
|
+
await this.unsetOtherDefaultsIfNeeded({
|
|
68
|
+
projectId: item.projectId,
|
|
69
|
+
isDefault: true,
|
|
70
|
+
excludeIds: item._id ? [item._id] : [],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { updateBy, carryForward: null };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async unsetOtherDefaultsIfNeeded(data: {
|
|
79
|
+
projectId?: ObjectID;
|
|
80
|
+
isDefault?: boolean;
|
|
81
|
+
excludeIds?: Array<string>;
|
|
82
|
+
}): Promise<void> {
|
|
83
|
+
if (!data.projectId || !data.isDefault) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await this.updateBy({
|
|
88
|
+
query: {
|
|
89
|
+
projectId: data.projectId,
|
|
90
|
+
isDefault: true,
|
|
91
|
+
...(data.excludeIds && data.excludeIds.length > 0
|
|
92
|
+
? {
|
|
93
|
+
_id: QueryHelper.notInOrNull(data.excludeIds),
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
96
|
+
},
|
|
97
|
+
data: {
|
|
98
|
+
isDefault: false,
|
|
99
|
+
},
|
|
100
|
+
props: {
|
|
101
|
+
isRoot: true,
|
|
102
|
+
},
|
|
103
|
+
limit: LIMIT_MAX,
|
|
104
|
+
skip: 0,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default new Service();
|
package/Server/Utils/Express.ts
CHANGED
|
@@ -21,6 +21,7 @@ export type NextFunction = express.NextFunction;
|
|
|
21
21
|
export const ExpressStatic: GenericFunction = express.static;
|
|
22
22
|
export const ExpressJson: GenericFunction = express.json;
|
|
23
23
|
export const ExpressUrlEncoded: GenericFunction = express.urlencoded;
|
|
24
|
+
export const ExpressRaw: GenericFunction = express.raw;
|
|
24
25
|
|
|
25
26
|
export type ProbeRequest = {
|
|
26
27
|
id: ObjectID;
|
package/Server/Utils/OpenAPI.ts
CHANGED
|
@@ -257,6 +257,20 @@ export default class OpenAPIUtil {
|
|
|
257
257
|
query: { $ref: `#/components/schemas/${querySchemaName}` },
|
|
258
258
|
select: { $ref: `#/components/schemas/${selectSchemaName}` },
|
|
259
259
|
sort: { $ref: `#/components/schemas/${sortSchemaName}` },
|
|
260
|
+
limit: {
|
|
261
|
+
type: "number",
|
|
262
|
+
description:
|
|
263
|
+
"Maximum number of items to return. Defaults to 10.",
|
|
264
|
+
default: 10,
|
|
265
|
+
minimum: 1,
|
|
266
|
+
},
|
|
267
|
+
skip: {
|
|
268
|
+
type: "number",
|
|
269
|
+
description:
|
|
270
|
+
"Number of items to skip for pagination. Defaults to 0.",
|
|
271
|
+
default: 0,
|
|
272
|
+
minimum: 0,
|
|
273
|
+
},
|
|
260
274
|
},
|
|
261
275
|
},
|
|
262
276
|
},
|
|
@@ -891,6 +905,20 @@ export default class OpenAPIUtil {
|
|
|
891
905
|
select: { $ref: `#/components/schemas/${selectSchemaName}` },
|
|
892
906
|
sort: { $ref: `#/components/schemas/${sortSchemaName}` },
|
|
893
907
|
groupBy: { $ref: `#/components/schemas/${groupBySchemaName}` },
|
|
908
|
+
limit: {
|
|
909
|
+
type: "number",
|
|
910
|
+
description:
|
|
911
|
+
"Maximum number of items to return. Defaults to 10.",
|
|
912
|
+
default: 10,
|
|
913
|
+
minimum: 1,
|
|
914
|
+
},
|
|
915
|
+
skip: {
|
|
916
|
+
type: "number",
|
|
917
|
+
description:
|
|
918
|
+
"Number of items to skip for pagination. Defaults to 0.",
|
|
919
|
+
default: 0,
|
|
920
|
+
minimum: 0,
|
|
921
|
+
},
|
|
894
922
|
},
|
|
895
923
|
},
|
|
896
924
|
},
|
|
@@ -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,16 @@ 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
|
+
normalizeLogsTableColumns,
|
|
45
51
|
} from "./types";
|
|
46
52
|
import { queryStringToFilter } from "../../../Types/Log/LogQueryToFilter";
|
|
47
53
|
import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
|
|
54
|
+
import TimeRange from "../../../Types/Time/TimeRange";
|
|
48
55
|
|
|
49
56
|
export interface ComponentProps {
|
|
50
57
|
logs: Array<Log>;
|
|
@@ -79,6 +86,15 @@ export interface ComponentProps {
|
|
|
79
86
|
onFieldValueSelect?: ((fieldKey: string, value: string) => void) | undefined;
|
|
80
87
|
timeRange?: RangeStartAndEndDateTime | undefined;
|
|
81
88
|
onTimeRangeChange?: ((value: RangeStartAndEndDateTime) => void) | undefined;
|
|
89
|
+
selectedColumns?: Array<string> | undefined;
|
|
90
|
+
onSelectedColumnsChange?: ((columns: Array<string>) => void) | undefined;
|
|
91
|
+
savedViews?: Array<LogsSavedViewOption> | undefined;
|
|
92
|
+
selectedSavedViewId?: string | null;
|
|
93
|
+
onSavedViewSelect?: ((viewId: string) => void) | undefined;
|
|
94
|
+
onCreateSavedView?: (() => void) | undefined;
|
|
95
|
+
onEditSavedView?: ((viewId: string) => void) | undefined;
|
|
96
|
+
onDeleteSavedView?: ((viewId: string) => void) | undefined;
|
|
97
|
+
onUpdateCurrentSavedView?: (() => void) | undefined;
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
export type LogsSortField = LogsTableSortField;
|
|
@@ -99,6 +115,32 @@ const severityWeight: Record<string, number> = {
|
|
|
99
115
|
trace: 1,
|
|
100
116
|
};
|
|
101
117
|
|
|
118
|
+
function getEmptyMessageWithTimeRange(
|
|
119
|
+
timeRange: RangeStartAndEndDateTime | undefined,
|
|
120
|
+
): string {
|
|
121
|
+
if (!timeRange) {
|
|
122
|
+
return "Adjust filters or check again later.";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (timeRange.range === TimeRange.CUSTOM && timeRange.startAndEndDate) {
|
|
126
|
+
const startDate: Date = timeRange.startAndEndDate.startValue;
|
|
127
|
+
const endDate: Date = timeRange.startAndEndDate.endValue;
|
|
128
|
+
const fmt: (d: Date) => string = (d: Date): string => {
|
|
129
|
+
return d.toLocaleString("en-US", {
|
|
130
|
+
month: "short",
|
|
131
|
+
day: "numeric",
|
|
132
|
+
hour: "2-digit",
|
|
133
|
+
minute: "2-digit",
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return `Time range: ${fmt(startDate)} – ${fmt(endDate)}. Try adjusting filters or expanding the time range.`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const rangeLabel: string = timeRange.range.toLowerCase();
|
|
141
|
+
return `Time range: ${rangeLabel}. Try adjusting filters or expanding the time range.`;
|
|
142
|
+
}
|
|
143
|
+
|
|
102
144
|
const getSeverityWeight: (severity: string | undefined) => number = (
|
|
103
145
|
severity: string | undefined,
|
|
104
146
|
): number => {
|
|
@@ -151,6 +193,9 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
151
193
|
const [localSortOrder, setLocalSortOrder] = useState<SortOrder>(
|
|
152
194
|
SortOrder.Descending,
|
|
153
195
|
);
|
|
196
|
+
const [internalSelectedColumns, setInternalSelectedColumns] = useState<
|
|
197
|
+
Array<string>
|
|
198
|
+
>(DEFAULT_LOGS_TABLE_COLUMNS);
|
|
154
199
|
|
|
155
200
|
useEffect(() => {
|
|
156
201
|
setFilterData(props.filterData);
|
|
@@ -174,6 +219,14 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
174
219
|
}
|
|
175
220
|
}, [props.pageSize]);
|
|
176
221
|
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (props.selectedColumns) {
|
|
224
|
+
setInternalSelectedColumns(
|
|
225
|
+
normalizeLogsTableColumns(props.selectedColumns),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}, [props.selectedColumns]);
|
|
229
|
+
|
|
177
230
|
const currentPage: number = props.page ?? internalPage;
|
|
178
231
|
const pageSize: number = props.pageSize ?? internalPageSize;
|
|
179
232
|
|
|
@@ -519,6 +572,26 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
519
572
|
};
|
|
520
573
|
}, [props.onFieldValueSelect, serviceMap]);
|
|
521
574
|
|
|
575
|
+
const selectedColumns: Array<string> = props.selectedColumns
|
|
576
|
+
? normalizeLogsTableColumns(props.selectedColumns)
|
|
577
|
+
: internalSelectedColumns;
|
|
578
|
+
|
|
579
|
+
const availableColumns: Array<LogsTableColumnOption> = useMemo(() => {
|
|
580
|
+
const attributeColumns: Array<LogsTableColumnOption> = [...logAttributes]
|
|
581
|
+
.sort((left: string, right: string) => {
|
|
582
|
+
return left.localeCompare(right);
|
|
583
|
+
})
|
|
584
|
+
.map((attributeKey: string): LogsTableColumnOption => {
|
|
585
|
+
return {
|
|
586
|
+
id: getLogsAttributeColumnId(attributeKey),
|
|
587
|
+
label: attributeKey,
|
|
588
|
+
attributeKey,
|
|
589
|
+
};
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
return [...CORE_LOGS_TABLE_COLUMN_OPTIONS, ...attributeColumns];
|
|
593
|
+
}, [logAttributes]);
|
|
594
|
+
|
|
522
595
|
if (isPageLoading) {
|
|
523
596
|
return <PageLoader isVisible={true} />;
|
|
524
597
|
}
|
|
@@ -531,6 +604,29 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
531
604
|
resultCount: totalItems,
|
|
532
605
|
currentPage,
|
|
533
606
|
totalPages,
|
|
607
|
+
savedViews: props.savedViews,
|
|
608
|
+
selectedSavedViewId: props.selectedSavedViewId,
|
|
609
|
+
onSavedViewSelect: props.onSavedViewSelect,
|
|
610
|
+
onCreateSavedView: props.onCreateSavedView,
|
|
611
|
+
onEditSavedView: props.onEditSavedView,
|
|
612
|
+
onDeleteSavedView: props.onDeleteSavedView,
|
|
613
|
+
onUpdateCurrentSavedView: props.onUpdateCurrentSavedView,
|
|
614
|
+
...(props.onSelectedColumnsChange || props.selectedColumns
|
|
615
|
+
? {
|
|
616
|
+
availableColumns,
|
|
617
|
+
selectedColumns,
|
|
618
|
+
onSelectedColumnsChange: (columns: Array<string>) => {
|
|
619
|
+
const nextColumns: Array<string> =
|
|
620
|
+
normalizeLogsTableColumns(columns);
|
|
621
|
+
|
|
622
|
+
if (!props.selectedColumns) {
|
|
623
|
+
setInternalSelectedColumns(nextColumns);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
props.onSelectedColumnsChange?.(nextColumns);
|
|
627
|
+
},
|
|
628
|
+
}
|
|
629
|
+
: {}),
|
|
534
630
|
...(props.liveOptions ? { liveOptions: props.liveOptions } : {}),
|
|
535
631
|
...(props.timeRange && props.onTimeRangeChange
|
|
536
632
|
? {
|
|
@@ -587,6 +683,9 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
587
683
|
onIncludeFilter={props.onFacetInclude || (() => {})}
|
|
588
684
|
onExcludeFilter={props.onFacetExclude || (() => {})}
|
|
589
685
|
activeFilters={props.activeFilters}
|
|
686
|
+
savedViews={props.savedViews}
|
|
687
|
+
selectedSavedViewId={props.selectedSavedViewId}
|
|
688
|
+
onSavedViewSelect={props.onSavedViewSelect}
|
|
590
689
|
/>
|
|
591
690
|
)}
|
|
592
691
|
|
|
@@ -602,7 +701,10 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
602
701
|
logs={displayedLogs}
|
|
603
702
|
serviceMap={serviceMap}
|
|
604
703
|
isLoading={props.isLoading}
|
|
605
|
-
emptyMessage={
|
|
704
|
+
emptyMessage={
|
|
705
|
+
props.noLogsMessage ||
|
|
706
|
+
getEmptyMessageWithTimeRange(props.timeRange)
|
|
707
|
+
}
|
|
606
708
|
onRowClick={(_log: Log, rowId: string) => {
|
|
607
709
|
setSelectedLogId((currentSelected: string | null) => {
|
|
608
710
|
if (currentSelected === rowId) {
|
|
@@ -616,6 +718,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
616
718
|
sortField={sortField}
|
|
617
719
|
sortOrder={sortOrder}
|
|
618
720
|
onSortChange={handleSortChange}
|
|
721
|
+
selectedColumns={selectedColumns}
|
|
619
722
|
renderExpandedContent={(log: Log) => {
|
|
620
723
|
return (
|
|
621
724
|
<LogDetailsPanel
|
|
@@ -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);
|