@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
|
@@ -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>
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
2
|
import LiveLogsToggle from "./LiveLogsToggle";
|
|
3
3
|
import LogTimeRangePicker from "./LogTimeRangePicker";
|
|
4
|
-
import
|
|
4
|
+
import ColumnSelector from "./ColumnSelector";
|
|
5
|
+
import SavedViewsDropdown from "./SavedViewsDropdown";
|
|
6
|
+
import {
|
|
7
|
+
LiveLogsOptions,
|
|
8
|
+
LogsSavedViewOption,
|
|
9
|
+
LogsTableColumnOption,
|
|
10
|
+
LogsViewMode,
|
|
11
|
+
} from "../types";
|
|
5
12
|
import RangeStartAndEndDateTime from "../../../../Types/Time/RangeStartAndEndDateTime";
|
|
6
13
|
|
|
7
14
|
export interface LogsViewerToolbarProps {
|
|
@@ -12,6 +19,18 @@ export interface LogsViewerToolbarProps {
|
|
|
12
19
|
liveOptions?: LiveLogsOptions;
|
|
13
20
|
timeRange?: RangeStartAndEndDateTime;
|
|
14
21
|
onTimeRangeChange?: (value: RangeStartAndEndDateTime) => void;
|
|
22
|
+
onCreateSavedView?: (() => void) | undefined;
|
|
23
|
+
savedViews?: Array<LogsSavedViewOption> | undefined;
|
|
24
|
+
selectedSavedViewId?: string | null | undefined;
|
|
25
|
+
onSavedViewSelect?: ((viewId: string) => void) | undefined;
|
|
26
|
+
onEditSavedView?: ((viewId: string) => void) | undefined;
|
|
27
|
+
onDeleteSavedView?: ((viewId: string) => void) | undefined;
|
|
28
|
+
onUpdateCurrentSavedView?: (() => void) | undefined;
|
|
29
|
+
availableColumns?: Array<LogsTableColumnOption> | undefined;
|
|
30
|
+
selectedColumns?: Array<string> | undefined;
|
|
31
|
+
onSelectedColumnsChange?: ((columns: Array<string>) => void) | undefined;
|
|
32
|
+
viewMode?: LogsViewMode | undefined;
|
|
33
|
+
onViewModeChange?: ((mode: LogsViewMode) => void) | undefined;
|
|
15
34
|
}
|
|
16
35
|
|
|
17
36
|
const LogsViewerToolbar: FunctionComponent<LogsViewerToolbarProps> = (
|
|
@@ -26,19 +45,102 @@ const LogsViewerToolbar: FunctionComponent<LogsViewerToolbarProps> = (
|
|
|
26
45
|
<div
|
|
27
46
|
className={`flex items-center justify-between gap-3 ${props.className || ""}`}
|
|
28
47
|
>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
{/* Left group: View management + stats */}
|
|
49
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
50
|
+
{props.viewMode && props.onViewModeChange && (
|
|
51
|
+
<div className="inline-flex rounded-md shadow-sm" role="group">
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
className={`inline-flex items-center gap-1.5 rounded-l-md border px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
55
|
+
props.viewMode === "list"
|
|
56
|
+
? "border-indigo-500 bg-indigo-50 text-indigo-700"
|
|
57
|
+
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
|
|
58
|
+
}`}
|
|
59
|
+
onClick={() => {
|
|
60
|
+
props.onViewModeChange!("list");
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<svg
|
|
64
|
+
className="h-3.5 w-3.5"
|
|
65
|
+
fill="none"
|
|
66
|
+
viewBox="0 0 24 24"
|
|
67
|
+
strokeWidth={1.5}
|
|
68
|
+
stroke="currentColor"
|
|
69
|
+
>
|
|
70
|
+
<path
|
|
71
|
+
strokeLinecap="round"
|
|
72
|
+
strokeLinejoin="round"
|
|
73
|
+
d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 0 1 0 3.75H5.625a1.875 1.875 0 0 1 0-3.75Z"
|
|
74
|
+
/>
|
|
75
|
+
</svg>
|
|
76
|
+
List
|
|
77
|
+
</button>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
className={`inline-flex items-center gap-1.5 rounded-r-md border px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
81
|
+
props.viewMode === "analytics"
|
|
82
|
+
? "border-indigo-500 bg-indigo-50 text-indigo-700"
|
|
83
|
+
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
|
|
84
|
+
}`}
|
|
85
|
+
onClick={() => {
|
|
86
|
+
props.onViewModeChange!("analytics");
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<svg
|
|
90
|
+
className="h-3.5 w-3.5"
|
|
91
|
+
fill="none"
|
|
92
|
+
viewBox="0 0 24 24"
|
|
93
|
+
strokeWidth={1.5}
|
|
94
|
+
stroke="currentColor"
|
|
95
|
+
>
|
|
96
|
+
<path
|
|
97
|
+
strokeLinecap="round"
|
|
98
|
+
strokeLinejoin="round"
|
|
99
|
+
d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z"
|
|
100
|
+
/>
|
|
101
|
+
</svg>
|
|
102
|
+
Analytics
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{props.savedViews && props.onSavedViewSelect && (
|
|
108
|
+
<SavedViewsDropdown
|
|
109
|
+
savedViews={props.savedViews}
|
|
110
|
+
selectedSavedViewId={props.selectedSavedViewId}
|
|
111
|
+
onSelect={props.onSavedViewSelect}
|
|
112
|
+
onCreate={props.onCreateSavedView}
|
|
113
|
+
onEdit={props.onEditSavedView}
|
|
114
|
+
onDelete={props.onDeleteSavedView}
|
|
115
|
+
onUpdateCurrent={props.onUpdateCurrentSavedView}
|
|
116
|
+
/>
|
|
38
117
|
)}
|
|
118
|
+
|
|
119
|
+
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
120
|
+
<span className="font-medium text-gray-700">
|
|
121
|
+
{props.resultCount.toLocaleString()} result
|
|
122
|
+
{props.resultCount === 1 ? "" : "s"}
|
|
123
|
+
</span>
|
|
124
|
+
{hasPaginationSummary && (
|
|
125
|
+
<span className="text-gray-400">
|
|
126
|
+
Page {currentPage} of {totalPages}
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
39
130
|
</div>
|
|
40
131
|
|
|
41
|
-
|
|
132
|
+
{/* Right group: Display controls */}
|
|
133
|
+
<div className="flex flex-wrap items-center justify-end gap-2">
|
|
134
|
+
{props.availableColumns &&
|
|
135
|
+
props.selectedColumns &&
|
|
136
|
+
props.onSelectedColumnsChange && (
|
|
137
|
+
<ColumnSelector
|
|
138
|
+
availableColumns={props.availableColumns}
|
|
139
|
+
selectedColumns={props.selectedColumns}
|
|
140
|
+
onChange={props.onSelectedColumnsChange}
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
|
|
42
144
|
{props.timeRange && props.onTimeRangeChange && (
|
|
43
145
|
<LogTimeRangePicker
|
|
44
146
|
value={props.timeRange}
|