@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.
Files changed (67) hide show
  1. package/Models/DatabaseModels/Index.ts +2 -0
  2. package/Models/DatabaseModels/LogSavedView.ts +466 -0
  3. package/Server/API/TelemetryAPI.ts +1 -1
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1772355000000-AddLogSavedView.ts +48 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1773344537755-MigrationName.ts +91 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  7. package/Server/Services/LogAggregationService.ts +1 -1
  8. package/Server/Services/LogSavedViewService.ts +109 -0
  9. package/Server/Utils/Express.ts +1 -0
  10. package/Server/Utils/OpenAPI.ts +28 -0
  11. package/Server/Utils/StartServer.ts +20 -1
  12. package/UI/Components/LogsViewer/LogsViewer.tsx +104 -1
  13. package/UI/Components/LogsViewer/components/ColumnSelector.tsx +270 -0
  14. package/UI/Components/LogsViewer/components/FacetSection.tsx +45 -7
  15. package/UI/Components/LogsViewer/components/LiveLogsToggle.tsx +3 -3
  16. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +2 -2
  17. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +46 -1
  18. package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +3 -3
  19. package/UI/Components/LogsViewer/components/LogsTable.tsx +288 -103
  20. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +53 -11
  21. package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +175 -0
  22. package/UI/Components/LogsViewer/types.ts +94 -0
  23. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  24. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  25. package/build/dist/Models/DatabaseModels/LogSavedView.js +496 -0
  26. package/build/dist/Models/DatabaseModels/LogSavedView.js.map +1 -0
  27. package/build/dist/Server/API/TelemetryAPI.js +1 -1
  28. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  29. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1772355000000-AddLogSavedView.js +44 -0
  30. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1772355000000-AddLogSavedView.js.map +1 -0
  31. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773344537755-MigrationName.js +38 -0
  32. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773344537755-MigrationName.js.map +1 -0
  33. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  34. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  35. package/build/dist/Server/Services/LogAggregationService.js +1 -1
  36. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  37. package/build/dist/Server/Services/LogSavedViewService.js +82 -0
  38. package/build/dist/Server/Services/LogSavedViewService.js.map +1 -0
  39. package/build/dist/Server/Utils/Express.js +1 -0
  40. package/build/dist/Server/Utils/Express.js.map +1 -1
  41. package/build/dist/Server/Utils/OpenAPI.js +24 -0
  42. package/build/dist/Server/Utils/OpenAPI.js.map +1 -1
  43. package/build/dist/Server/Utils/StartServer.js +17 -2
  44. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  45. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +64 -5
  46. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  47. package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js +115 -0
  48. package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js.map +1 -0
  49. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +26 -6
  50. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -1
  51. package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js +3 -3
  52. package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js.map +1 -1
  53. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +2 -2
  54. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  55. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +27 -13
  56. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  57. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +3 -3
  58. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
  59. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +118 -49
  60. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  61. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +18 -11
  62. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  63. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +58 -0
  64. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -0
  65. package/build/dist/UI/Components/LogsViewer/types.js +60 -1
  66. package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
  67. package/package.json +2 -2
@@ -1,4 +1,9 @@
1
- import React, { FunctionComponent, ReactElement, useState } from "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> = showAll
31
- ? props.values
32
- : props.values.slice(0, visibleCount);
51
+ const displayedValues: Array<FacetValue> = searchText.trim()
52
+ ? filteredValues
53
+ : showAll
54
+ ? filteredValues
55
+ : filteredValues.slice(0, visibleCount);
33
56
 
34
- const hasMore: boolean = props.values.length > visibleCount;
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
- {props.values.length === 0 && (
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-2 rounded-lg border px-3 py-1.5 text-sm font-medium transition-all focus:outline-none focus:ring-2 focus:ring-emerald-200";
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-600 hover:bg-gray-50";
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 className="font-semibold">Live</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-600 hover:border-gray-300 hover:bg-gray-50"
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 { FacetData, FacetValue, ActiveFilter } from "../types";
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 items-start gap-3">
29
- <div className="min-w-0 flex-1">
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 className="flex-none pt-0.5">{props.toolbar}</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
- <th scope="col" className="px-4 py-2.5">
93
- <button
94
- type="button"
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
- <td className="whitespace-nowrap px-4 py-2 text-[13px] font-mono text-gray-600">
185
- {log.time
186
- ? OneUptimeDate.getDateAsUserFriendlyFormattedString(
187
- log.time,
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
- {message || "-"}
214
- </p>
215
- {(traceId || spanId) && (
216
- <div className="flex flex-wrap gap-3 text-[11px] tracking-wide text-gray-400">
217
- {traceId && <span>Trace: {traceId}</span>}
218
- {spanId && <span>Span: {spanId}</span>}
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
- </div>
222
- <CopyTextButton
223
- textToBeCopied={message}
224
- size="xs"
225
- variant="ghost"
226
- iconOnly={true}
227
- title="Copy log message"
228
- className="opacity-0 group-hover:opacity-100 transition-opacity"
229
- />
230
- </div>
231
- </td>
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 colSpan={4} className="px-6 pb-6 pt-3">
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>