@oneuptime/common 10.0.27 → 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/API/TelemetryAPI.ts +1 -1
- 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 +1 -1
- 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/FacetSection.tsx +45 -7
- 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/API/TelemetryAPI.js +1 -1
- 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 +1 -1
- 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/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/FacetSection.js +26 -6
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -1
- 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
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
FunctionComponent,
|
|
3
|
+
ReactElement,
|
|
4
|
+
useState,
|
|
5
|
+
useMemo,
|
|
6
|
+
} from "react";
|
|
2
7
|
import { FacetValue } from "../types";
|
|
3
8
|
import FacetValueRow from "./FacetValueRow";
|
|
4
9
|
import Icon from "../../Icon/Icon";
|
|
@@ -17,21 +22,40 @@ export interface FacetSectionProps {
|
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
const DEFAULT_VISIBLE_COUNT: number = 5;
|
|
25
|
+
const SEARCH_THRESHOLD: number = 6;
|
|
20
26
|
|
|
21
27
|
const FacetSection: FunctionComponent<FacetSectionProps> = (
|
|
22
28
|
props: FacetSectionProps,
|
|
23
29
|
): ReactElement => {
|
|
24
30
|
const [isExpanded, setIsExpanded] = useState<boolean>(true);
|
|
25
31
|
const [showAll, setShowAll] = useState<boolean>(false);
|
|
32
|
+
const [searchText, setSearchText] = useState<string>("");
|
|
33
|
+
|
|
34
|
+
const showSearch: boolean = props.values.length >= SEARCH_THRESHOLD;
|
|
35
|
+
|
|
36
|
+
const filteredValues: Array<FacetValue> = useMemo(() => {
|
|
37
|
+
if (!searchText.trim()) {
|
|
38
|
+
return props.values;
|
|
39
|
+
}
|
|
40
|
+
const query: string = searchText.toLowerCase().trim();
|
|
41
|
+
return props.values.filter((facet: FacetValue) => {
|
|
42
|
+
const displayName: string =
|
|
43
|
+
props.valueDisplayMap?.[facet.value] ?? facet.value;
|
|
44
|
+
return displayName.toLowerCase().includes(query);
|
|
45
|
+
});
|
|
46
|
+
}, [props.values, props.valueDisplayMap, searchText]);
|
|
26
47
|
|
|
27
48
|
const visibleCount: number =
|
|
28
49
|
props.initialVisibleCount ?? DEFAULT_VISIBLE_COUNT;
|
|
29
50
|
|
|
30
|
-
const displayedValues: Array<FacetValue> =
|
|
31
|
-
?
|
|
32
|
-
:
|
|
51
|
+
const displayedValues: Array<FacetValue> = searchText.trim()
|
|
52
|
+
? filteredValues
|
|
53
|
+
: showAll
|
|
54
|
+
? filteredValues
|
|
55
|
+
: filteredValues.slice(0, visibleCount);
|
|
33
56
|
|
|
34
|
-
const hasMore: boolean =
|
|
57
|
+
const hasMore: boolean =
|
|
58
|
+
!searchText.trim() && filteredValues.length > visibleCount;
|
|
35
59
|
|
|
36
60
|
const maxCount: number =
|
|
37
61
|
props.values.length > 0
|
|
@@ -71,6 +95,20 @@ const FacetSection: FunctionComponent<FacetSectionProps> = (
|
|
|
71
95
|
|
|
72
96
|
{isExpanded && (
|
|
73
97
|
<div className="mt-1 px-1">
|
|
98
|
+
{showSearch && (
|
|
99
|
+
<div className="mb-1 px-1">
|
|
100
|
+
<input
|
|
101
|
+
type="text"
|
|
102
|
+
placeholder={`Search ${props.title.toLowerCase()}...`}
|
|
103
|
+
value={searchText}
|
|
104
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
105
|
+
setSearchText(e.target.value);
|
|
106
|
+
}}
|
|
107
|
+
className="w-full rounded border border-gray-200 bg-gray-50 px-2 py-1 text-[11px] text-gray-700 placeholder-gray-400 outline-none focus:border-indigo-300 focus:bg-white focus:ring-1 focus:ring-indigo-200"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
|
|
74
112
|
{displayedValues.map((facet: FacetValue) => {
|
|
75
113
|
return (
|
|
76
114
|
<FacetValueRow
|
|
@@ -91,9 +129,9 @@ const FacetSection: FunctionComponent<FacetSectionProps> = (
|
|
|
91
129
|
);
|
|
92
130
|
})}
|
|
93
131
|
|
|
94
|
-
{
|
|
132
|
+
{displayedValues.length === 0 && (
|
|
95
133
|
<p className="px-1 py-2 text-[11px] text-gray-400">
|
|
96
|
-
No values found
|
|
134
|
+
{searchText.trim() ? "No matches found" : "No values found"}
|
|
97
135
|
</p>
|
|
98
136
|
)}
|
|
99
137
|
|
|
@@ -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);
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import React, { FunctionComponent, ReactElement, useMemo } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
FacetData,
|
|
4
|
+
FacetValue,
|
|
5
|
+
ActiveFilter,
|
|
6
|
+
LogsSavedViewOption,
|
|
7
|
+
} from "../types";
|
|
3
8
|
import FacetSection from "./FacetSection";
|
|
4
9
|
import Service from "../../../../Models/DatabaseModels/Service";
|
|
5
10
|
import Dictionary from "../../../../Types/Dictionary";
|
|
@@ -14,6 +19,9 @@ export interface LogsFacetSidebarProps {
|
|
|
14
19
|
onIncludeFilter: (facetKey: string, value: string) => void;
|
|
15
20
|
onExcludeFilter: (facetKey: string, value: string) => void;
|
|
16
21
|
activeFilters?: Array<ActiveFilter> | undefined;
|
|
22
|
+
savedViews?: Array<LogsSavedViewOption> | undefined;
|
|
23
|
+
selectedSavedViewId?: string | null | undefined;
|
|
24
|
+
onSavedViewSelect?: ((viewId: string) => void) | undefined;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
const SEVERITY_ORDER: Array<string> = [
|
|
@@ -137,6 +145,43 @@ const LogsFacetSidebar: FunctionComponent<LogsFacetSidebarProps> = (
|
|
|
137
145
|
)}
|
|
138
146
|
|
|
139
147
|
<div className="flex-1 overflow-y-auto">
|
|
148
|
+
{props.savedViews && props.savedViews.length > 0 && (
|
|
149
|
+
<div className="border-b border-gray-100 px-3 py-3">
|
|
150
|
+
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-gray-400">
|
|
151
|
+
Saved Views
|
|
152
|
+
</p>
|
|
153
|
+
|
|
154
|
+
<div className="space-y-1.5">
|
|
155
|
+
{props.savedViews.map((view: LogsSavedViewOption) => {
|
|
156
|
+
const isSelected: boolean =
|
|
157
|
+
view.id === props.selectedSavedViewId;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<button
|
|
161
|
+
key={view.id}
|
|
162
|
+
type="button"
|
|
163
|
+
className={`flex w-full items-center justify-between rounded-md px-2.5 py-2 text-left text-sm transition-colors ${
|
|
164
|
+
isSelected
|
|
165
|
+
? "bg-indigo-50 text-indigo-700"
|
|
166
|
+
: "text-gray-600 hover:bg-gray-50 hover:text-gray-800"
|
|
167
|
+
}`}
|
|
168
|
+
onClick={() => {
|
|
169
|
+
props.onSavedViewSelect?.(view.id);
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<span className="truncate">{view.name}</span>
|
|
173
|
+
{view.isDefault && (
|
|
174
|
+
<span className="ml-2 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-700">
|
|
175
|
+
Default
|
|
176
|
+
</span>
|
|
177
|
+
)}
|
|
178
|
+
</button>
|
|
179
|
+
);
|
|
180
|
+
})}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
140
185
|
{facetKeys.map((key: string) => {
|
|
141
186
|
const values: Array<FacetValue> = props.facetData[key] || [];
|
|
142
187
|
|
|
@@ -25,8 +25,8 @@ const LogsFilterCard: FunctionComponent<LogsFilterCardProps> = (
|
|
|
25
25
|
];
|
|
26
26
|
|
|
27
27
|
return (
|
|
28
|
-
<div className="flex
|
|
29
|
-
<div
|
|
28
|
+
<div className="flex flex-col gap-3">
|
|
29
|
+
<div>
|
|
30
30
|
<LogSearchBar
|
|
31
31
|
value={props.searchQuery}
|
|
32
32
|
onChange={props.onSearchQueryChange}
|
|
@@ -36,7 +36,7 @@ const LogsFilterCard: FunctionComponent<LogsFilterCardProps> = (
|
|
|
36
36
|
onFieldValueSelect={props.onFieldValueSelect}
|
|
37
37
|
/>
|
|
38
38
|
</div>
|
|
39
|
-
<div
|
|
39
|
+
<div>{props.toolbar}</div>
|
|
40
40
|
</div>
|
|
41
41
|
);
|
|
42
42
|
};
|
|
@@ -10,6 +10,11 @@ import { getSeverityTheme, SeverityTheme } from "./severityTheme";
|
|
|
10
10
|
import SortOrder from "../../../../Types/BaseDatabase/SortOrder";
|
|
11
11
|
import Icon from "../../Icon/Icon";
|
|
12
12
|
import IconProp from "../../../../Types/Icon/IconProp";
|
|
13
|
+
import {
|
|
14
|
+
getLogsAttributeKeyFromColumnId,
|
|
15
|
+
isLogsAttributeColumnId,
|
|
16
|
+
normalizeLogsTableColumns,
|
|
17
|
+
} from "../types";
|
|
13
18
|
|
|
14
19
|
export interface LogsTableProps {
|
|
15
20
|
logs: Array<Log>;
|
|
@@ -22,6 +27,7 @@ export interface LogsTableProps {
|
|
|
22
27
|
sortField?: LogsTableSortField | undefined;
|
|
23
28
|
sortOrder?: SortOrder | undefined;
|
|
24
29
|
onSortChange?: (field: LogsTableSortField) => void;
|
|
30
|
+
selectedColumns?: Array<string> | undefined;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export const resolveLogIdentifier: (log: Log, index: number) => string = (
|
|
@@ -53,12 +59,39 @@ export const resolveLogIdentifier: (log: Log, index: number) => string = (
|
|
|
53
59
|
|
|
54
60
|
export type LogsTableSortField = "time" | "severityText";
|
|
55
61
|
|
|
62
|
+
const stringifyLogValue: (value: unknown) => string = (
|
|
63
|
+
value: unknown,
|
|
64
|
+
): string => {
|
|
65
|
+
if (value === undefined || value === null) {
|
|
66
|
+
return "-";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
return value || "-";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
74
|
+
return value.toString();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
return JSON.stringify(value);
|
|
79
|
+
} catch {
|
|
80
|
+
return String(value);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
56
84
|
const LogsTable: FunctionComponent<LogsTableProps> = (
|
|
57
85
|
props: LogsTableProps,
|
|
58
86
|
): ReactElement => {
|
|
59
87
|
const showEmptyState: boolean = !props.isLoading && props.logs.length === 0;
|
|
60
88
|
const activeSortField: LogsTableSortField | undefined = props.sortField;
|
|
61
89
|
const activeSortOrder: SortOrder = props.sortOrder || SortOrder.Descending;
|
|
90
|
+
const selectedColumns: Array<string> = normalizeLogsTableColumns(
|
|
91
|
+
props.selectedColumns,
|
|
92
|
+
);
|
|
93
|
+
const showTraceColumn: boolean = selectedColumns.includes("traceId");
|
|
94
|
+
const showSpanColumn: boolean = selectedColumns.includes("spanId");
|
|
62
95
|
|
|
63
96
|
const resolveSortIcon: (field: LogsTableSortField) => IconProp = (
|
|
64
97
|
field: LogsTableSortField,
|
|
@@ -83,68 +116,117 @@ const LogsTable: FunctionComponent<LogsTableProps> = (
|
|
|
83
116
|
return `${base} text-gray-300`;
|
|
84
117
|
};
|
|
85
118
|
|
|
119
|
+
const getHeaderCell: (columnId: string) => ReactElement = (
|
|
120
|
+
columnId: string,
|
|
121
|
+
): ReactElement => {
|
|
122
|
+
if (columnId === "time") {
|
|
123
|
+
return (
|
|
124
|
+
<th scope="col" className="px-4 py-2.5" key={columnId}>
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
className={`flex items-center gap-2 text-left font-semibold tracking-wider text-gray-500 transition-colors hover:text-gray-700 focus:outline-none ${
|
|
128
|
+
activeSortField === "time" ? "text-gray-700" : ""
|
|
129
|
+
}`}
|
|
130
|
+
onClick={() => {
|
|
131
|
+
props.onSortChange?.("time");
|
|
132
|
+
}}
|
|
133
|
+
aria-sort={
|
|
134
|
+
activeSortField === "time"
|
|
135
|
+
? activeSortOrder === SortOrder.Descending
|
|
136
|
+
? "descending"
|
|
137
|
+
: "ascending"
|
|
138
|
+
: "none"
|
|
139
|
+
}
|
|
140
|
+
>
|
|
141
|
+
<span>Time</span>
|
|
142
|
+
<Icon
|
|
143
|
+
icon={resolveSortIcon("time")}
|
|
144
|
+
className={resolveSortIconClass("time")}
|
|
145
|
+
aria-hidden="true"
|
|
146
|
+
/>
|
|
147
|
+
</button>
|
|
148
|
+
</th>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (columnId === "severity") {
|
|
153
|
+
return (
|
|
154
|
+
<th scope="col" className="px-4 py-2.5" key={columnId}>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
className={`flex items-center gap-2 text-left font-semibold tracking-wider text-gray-500 transition-colors hover:text-gray-700 focus:outline-none ${
|
|
158
|
+
activeSortField === "severityText" ? "text-gray-700" : ""
|
|
159
|
+
}`}
|
|
160
|
+
onClick={() => {
|
|
161
|
+
props.onSortChange?.("severityText");
|
|
162
|
+
}}
|
|
163
|
+
aria-sort={
|
|
164
|
+
activeSortField === "severityText"
|
|
165
|
+
? activeSortOrder === SortOrder.Descending
|
|
166
|
+
? "descending"
|
|
167
|
+
: "ascending"
|
|
168
|
+
: "none"
|
|
169
|
+
}
|
|
170
|
+
>
|
|
171
|
+
<span>Severity</span>
|
|
172
|
+
<Icon
|
|
173
|
+
icon={resolveSortIcon("severityText")}
|
|
174
|
+
className={resolveSortIconClass("severityText")}
|
|
175
|
+
aria-hidden="true"
|
|
176
|
+
/>
|
|
177
|
+
</button>
|
|
178
|
+
</th>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (columnId === "service") {
|
|
183
|
+
return (
|
|
184
|
+
<th scope="col" className="px-4 py-2.5" key={columnId}>
|
|
185
|
+
Service
|
|
186
|
+
</th>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (columnId === "message") {
|
|
191
|
+
return (
|
|
192
|
+
<th scope="col" className="px-4 py-2.5" key={columnId}>
|
|
193
|
+
Message
|
|
194
|
+
</th>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (columnId === "traceId") {
|
|
199
|
+
return (
|
|
200
|
+
<th scope="col" className="px-4 py-2.5" key={columnId}>
|
|
201
|
+
Trace ID
|
|
202
|
+
</th>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (columnId === "spanId") {
|
|
207
|
+
return (
|
|
208
|
+
<th scope="col" className="px-4 py-2.5" key={columnId}>
|
|
209
|
+
Span ID
|
|
210
|
+
</th>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<th scope="col" className="px-4 py-2.5" key={columnId}>
|
|
216
|
+
{getLogsAttributeKeyFromColumnId(columnId) || columnId}
|
|
217
|
+
</th>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
86
221
|
return (
|
|
87
222
|
<div className="relative">
|
|
88
223
|
<div className="overflow-x-auto bg-white">
|
|
89
224
|
<table className="min-w-full">
|
|
90
225
|
<thead className="bg-gray-50/80">
|
|
91
226
|
<tr className="text-left text-[11px] font-semibold uppercase tracking-wider text-gray-500">
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
className={`flex items-center gap-2 text-left font-semibold tracking-wider text-gray-500 transition-colors hover:text-gray-700 focus:outline-none ${
|
|
96
|
-
activeSortField === "time" ? "text-gray-700" : ""
|
|
97
|
-
}`}
|
|
98
|
-
onClick={() => {
|
|
99
|
-
props.onSortChange?.("time");
|
|
100
|
-
}}
|
|
101
|
-
aria-sort={
|
|
102
|
-
activeSortField === "time"
|
|
103
|
-
? activeSortOrder === SortOrder.Descending
|
|
104
|
-
? "descending"
|
|
105
|
-
: "ascending"
|
|
106
|
-
: "none"
|
|
107
|
-
}
|
|
108
|
-
>
|
|
109
|
-
<span>Time</span>
|
|
110
|
-
<Icon
|
|
111
|
-
icon={resolveSortIcon("time")}
|
|
112
|
-
className={resolveSortIconClass("time")}
|
|
113
|
-
aria-hidden="true"
|
|
114
|
-
/>
|
|
115
|
-
</button>
|
|
116
|
-
</th>
|
|
117
|
-
<th scope="col" className="px-4 py-2.5">
|
|
118
|
-
<span>Service</span>
|
|
119
|
-
</th>
|
|
120
|
-
<th scope="col" className="px-4 py-2.5">
|
|
121
|
-
<button
|
|
122
|
-
type="button"
|
|
123
|
-
className={`flex items-center gap-2 text-left font-semibold tracking-wider text-gray-500 transition-colors hover:text-gray-700 focus:outline-none ${
|
|
124
|
-
activeSortField === "severityText" ? "text-gray-700" : ""
|
|
125
|
-
}`}
|
|
126
|
-
onClick={() => {
|
|
127
|
-
props.onSortChange?.("severityText");
|
|
128
|
-
}}
|
|
129
|
-
aria-sort={
|
|
130
|
-
activeSortField === "severityText"
|
|
131
|
-
? activeSortOrder === SortOrder.Descending
|
|
132
|
-
? "descending"
|
|
133
|
-
: "ascending"
|
|
134
|
-
: "none"
|
|
135
|
-
}
|
|
136
|
-
>
|
|
137
|
-
<span>Severity</span>
|
|
138
|
-
<Icon
|
|
139
|
-
icon={resolveSortIcon("severityText")}
|
|
140
|
-
className={resolveSortIconClass("severityText")}
|
|
141
|
-
aria-hidden="true"
|
|
142
|
-
/>
|
|
143
|
-
</button>
|
|
144
|
-
</th>
|
|
145
|
-
<th scope="col" className="px-4 py-2.5">
|
|
146
|
-
Message
|
|
147
|
-
</th>
|
|
227
|
+
{selectedColumns.map((columnId: string) => {
|
|
228
|
+
return getHeaderCell(columnId);
|
|
229
|
+
})}
|
|
148
230
|
</tr>
|
|
149
231
|
</thead>
|
|
150
232
|
<tbody className="divide-y divide-gray-100">
|
|
@@ -181,59 +263,162 @@ const LogsTable: FunctionComponent<LogsTableProps> = (
|
|
|
181
263
|
aria-selected={isSelected}
|
|
182
264
|
aria-expanded={isSelected}
|
|
183
265
|
>
|
|
184
|
-
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
</td>
|
|
191
|
-
<td className="px-4 py-2">
|
|
192
|
-
<div className="flex items-center gap-3 text-sm text-gray-700">
|
|
193
|
-
<span
|
|
194
|
-
className="h-2.5 w-2.5 flex-none rounded-full shadow-sm"
|
|
195
|
-
style={{ backgroundColor: serviceColor }}
|
|
196
|
-
aria-hidden="true"
|
|
197
|
-
/>
|
|
198
|
-
<span className="truncate" title={serviceName}>
|
|
199
|
-
{serviceName}
|
|
200
|
-
</span>
|
|
201
|
-
</div>
|
|
202
|
-
</td>
|
|
203
|
-
<td className="px-4 py-2">
|
|
204
|
-
<SeverityBadge severity={log.severityText} />
|
|
205
|
-
</td>
|
|
206
|
-
<td className="px-4 py-2">
|
|
207
|
-
<div className="flex items-start justify-between gap-3">
|
|
208
|
-
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
209
|
-
<p
|
|
210
|
-
className="whitespace-pre-wrap break-words text-sm text-gray-800"
|
|
211
|
-
title={message}
|
|
266
|
+
{selectedColumns.map((columnId: string) => {
|
|
267
|
+
if (columnId === "time") {
|
|
268
|
+
return (
|
|
269
|
+
<td
|
|
270
|
+
className="whitespace-nowrap px-4 py-2 text-[13px] font-mono text-gray-600"
|
|
271
|
+
key={columnId}
|
|
212
272
|
>
|
|
213
|
-
{
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
273
|
+
{log.time
|
|
274
|
+
? OneUptimeDate.getDateAsUserFriendlyFormattedString(
|
|
275
|
+
log.time,
|
|
276
|
+
)
|
|
277
|
+
: "-"}
|
|
278
|
+
</td>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (columnId === "service") {
|
|
283
|
+
return (
|
|
284
|
+
<td className="px-4 py-2" key={columnId}>
|
|
285
|
+
<div className="flex items-center gap-3 text-sm text-gray-700">
|
|
286
|
+
<span
|
|
287
|
+
className="h-2.5 w-2.5 flex-none rounded-full shadow-sm"
|
|
288
|
+
style={{ backgroundColor: serviceColor }}
|
|
289
|
+
aria-hidden="true"
|
|
290
|
+
/>
|
|
291
|
+
<span className="truncate" title={serviceName}>
|
|
292
|
+
{serviceName}
|
|
293
|
+
</span>
|
|
294
|
+
</div>
|
|
295
|
+
</td>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (columnId === "severity") {
|
|
300
|
+
return (
|
|
301
|
+
<td className="px-4 py-2" key={columnId}>
|
|
302
|
+
<SeverityBadge severity={log.severityText} />
|
|
303
|
+
</td>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (columnId === "message") {
|
|
308
|
+
return (
|
|
309
|
+
<td className="px-4 py-2" key={columnId}>
|
|
310
|
+
<div className="flex items-start justify-between gap-3">
|
|
311
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
312
|
+
<p
|
|
313
|
+
className="whitespace-pre-wrap break-words text-sm text-gray-800"
|
|
314
|
+
title={message}
|
|
315
|
+
>
|
|
316
|
+
{message || "-"}
|
|
317
|
+
</p>
|
|
318
|
+
{((traceId && !showTraceColumn) ||
|
|
319
|
+
(spanId && !showSpanColumn)) && (
|
|
320
|
+
<div className="flex flex-wrap gap-3 text-[11px] tracking-wide text-gray-400">
|
|
321
|
+
{traceId && !showTraceColumn && (
|
|
322
|
+
<span>Trace: {traceId}</span>
|
|
323
|
+
)}
|
|
324
|
+
{spanId && !showSpanColumn && (
|
|
325
|
+
<span>Span: {spanId}</span>
|
|
326
|
+
)}
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
330
|
+
<CopyTextButton
|
|
331
|
+
textToBeCopied={message}
|
|
332
|
+
size="xs"
|
|
333
|
+
variant="ghost"
|
|
334
|
+
iconOnly={true}
|
|
335
|
+
title="Copy log message"
|
|
336
|
+
className="opacity-0 transition-opacity group-hover:opacity-100"
|
|
337
|
+
/>
|
|
219
338
|
</div>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
339
|
+
</td>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (columnId === "traceId") {
|
|
344
|
+
return (
|
|
345
|
+
<td
|
|
346
|
+
className="max-w-xs px-4 py-2 text-sm text-gray-600"
|
|
347
|
+
key={columnId}
|
|
348
|
+
>
|
|
349
|
+
<span
|
|
350
|
+
className="block truncate font-mono"
|
|
351
|
+
title={traceId}
|
|
352
|
+
>
|
|
353
|
+
{traceId || "-"}
|
|
354
|
+
</span>
|
|
355
|
+
</td>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (columnId === "spanId") {
|
|
360
|
+
return (
|
|
361
|
+
<td
|
|
362
|
+
className="max-w-xs px-4 py-2 text-sm text-gray-600"
|
|
363
|
+
key={columnId}
|
|
364
|
+
>
|
|
365
|
+
<span
|
|
366
|
+
className="block truncate font-mono"
|
|
367
|
+
title={spanId}
|
|
368
|
+
>
|
|
369
|
+
{spanId || "-"}
|
|
370
|
+
</span>
|
|
371
|
+
</td>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (isLogsAttributeColumnId(columnId)) {
|
|
376
|
+
const attributeKey: string | null =
|
|
377
|
+
getLogsAttributeKeyFromColumnId(columnId);
|
|
378
|
+
const attributeValue: unknown =
|
|
379
|
+
attributeKey &&
|
|
380
|
+
typeof log.attributes === "object" &&
|
|
381
|
+
log.attributes
|
|
382
|
+
? (log.attributes as Record<string, unknown>)[
|
|
383
|
+
attributeKey
|
|
384
|
+
]
|
|
385
|
+
: undefined;
|
|
386
|
+
|
|
387
|
+
const displayValue: string =
|
|
388
|
+
stringifyLogValue(attributeValue);
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<td
|
|
392
|
+
className="max-w-xs px-4 py-2 text-sm text-gray-600"
|
|
393
|
+
key={columnId}
|
|
394
|
+
>
|
|
395
|
+
<span
|
|
396
|
+
className="block truncate"
|
|
397
|
+
title={displayValue}
|
|
398
|
+
>
|
|
399
|
+
{displayValue}
|
|
400
|
+
</span>
|
|
401
|
+
</td>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<td
|
|
407
|
+
className="px-4 py-2 text-sm text-gray-600"
|
|
408
|
+
key={columnId}
|
|
409
|
+
>
|
|
410
|
+
-
|
|
411
|
+
</td>
|
|
412
|
+
);
|
|
413
|
+
})}
|
|
232
414
|
</tr>
|
|
233
415
|
|
|
234
416
|
{isSelected && props.renderExpandedContent && (
|
|
235
417
|
<tr className="bg-white">
|
|
236
|
-
<td
|
|
418
|
+
<td
|
|
419
|
+
colSpan={selectedColumns.length}
|
|
420
|
+
className="px-6 pb-6 pt-3"
|
|
421
|
+
>
|
|
237
422
|
{props.renderExpandedContent(log)}
|
|
238
423
|
</td>
|
|
239
424
|
</tr>
|