@oneuptime/common 8.0.5174 → 8.0.5179

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.
@@ -1,3 +1,11 @@
1
+ /**
2
+ * LogItem UI
3
+ * - Collapsed row: compact line with timestamp, service, severity badge, message preview, quick copy.
4
+ * - Expanded panel: left severity-colored border, header with service, severity, timestamp; sections for Message, JSON body, Trace/Span IDs, and Attributes, each with copy buttons.
5
+ * - Accessibility: entire item is keyboard-activatable (Enter/Space) and exposes aria-expanded.
6
+ * - Styling hooks: severityBadgeClasses and leftBorderColor derive from props.log.severityText for consistent theming.
7
+ * - Non-breaking: props and behavior remain backward compatible.
8
+ */
1
9
  import CopyTextButton from "../CopyTextButton/CopyTextButton";
2
10
  import OneUptimeDate from "../../../Types/Date";
3
11
  import Dictionary from "../../../Types/Dictionary";
@@ -7,6 +15,8 @@ import LogSeverity from "../../../Types/Log/LogSeverity";
7
15
  import TelemetryService from "../../../Models/DatabaseModels/TelemetryService";
8
16
  import React, { FunctionComponent, ReactElement, useEffect } from "react";
9
17
  import { Logger } from "../../Utils/Logger";
18
+ import Icon from "../Icon/Icon";
19
+ import IconProp from "../../../Types/Icon/IconProp";
10
20
 
11
21
  export interface ComponentProps {
12
22
  log: Log;
@@ -28,6 +38,8 @@ const LogItem: FunctionComponent<ComponentProps> = (
28
38
  }, []);
29
39
 
30
40
  let bodyColor: string = "text-slate-200";
41
+ let leftBorderColor: string = "border-l-slate-700";
42
+ let severityDotClass: string = "bg-slate-400";
31
43
 
32
44
  type GetCopyButtonFunction = (textToBeCopied: string) => ReactElement;
33
45
 
@@ -35,87 +47,141 @@ const LogItem: FunctionComponent<ComponentProps> = (
35
47
  return (
36
48
  <CopyTextButton
37
49
  textToBeCopied={textToBeCopied}
38
- className="ml-5 font-medium px-3 my-0.5 py-0.5 text-xs bg-slate-900 text-slate-300 rounded hover:bg-slate-600 border-slate-700 border-solid border-0"
50
+ size="xs"
51
+ variant="ghost"
52
+ iconOnly={true}
53
+ title="Copy"
54
+ className="flex-none"
39
55
  />
40
56
  );
41
57
  };
42
58
 
43
59
  if (props.log.severityText === LogSeverity.Warning) {
44
60
  bodyColor = "text-amber-400";
61
+ leftBorderColor = "border-l-amber-500/60";
62
+ severityDotClass = "bg-amber-500";
45
63
  } else if (props.log.severityText === LogSeverity.Error) {
46
64
  bodyColor = "text-rose-400";
65
+ leftBorderColor = "border-l-rose-500/60";
66
+ severityDotClass = "bg-rose-500";
47
67
  } else if (
48
68
  props.log.severityText === LogSeverity.Trace ||
49
69
  props.log.severityText === LogSeverity.Debug
50
70
  ) {
51
71
  bodyColor = "text-slate-400";
72
+ leftBorderColor =
73
+ props.log.severityText === LogSeverity.Debug
74
+ ? "border-l-purple-500/60"
75
+ : "border-l-slate-500/60";
76
+ severityDotClass =
77
+ props.log.severityText === LogSeverity.Debug
78
+ ? "bg-purple-500"
79
+ : "bg-slate-500";
80
+ } else if (props.log.severityText === LogSeverity.Information) {
81
+ leftBorderColor = "border-l-blue-500/60";
82
+ severityDotClass = "bg-blue-500";
83
+ } else if (props.log.severityText === LogSeverity.Fatal) {
84
+ leftBorderColor = "border-l-rose-700/70";
85
+ severityDotClass = "bg-rose-700";
52
86
  }
53
87
 
54
88
  let logBody: string = props.log.body?.toString() || "";
89
+ let logBodyMinified: string = "";
55
90
 
56
91
  let isBodyInJSON: boolean = false;
57
92
 
58
93
  try {
59
- logBody = JSON.stringify(JSON.parse(logBody), null, 2);
94
+ const parsed: any = JSON.parse(logBody);
95
+ logBody = JSON.stringify(parsed, null, 2);
96
+ logBodyMinified = JSON.stringify(parsed);
60
97
  isBodyInJSON = true;
61
98
  } catch (e) {
62
99
  Logger.error(e as Error);
63
100
  isBodyInJSON = false;
64
101
  }
65
102
 
103
+ const toggleCollapsed: () => void = (): void => {
104
+ setIsCollapsed((v: boolean) => {
105
+ return !v;
106
+ });
107
+ };
108
+
109
+ const handleKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void = (
110
+ e: React.KeyboardEvent<HTMLDivElement>,
111
+ ): void => {
112
+ if (e.key === "Enter" || e.key === " ") {
113
+ e.preventDefault();
114
+ toggleCollapsed();
115
+ }
116
+ };
117
+
66
118
  if (isCollapsed) {
67
119
  return (
68
120
  <div
69
- className="text-slate-200 flex cursor-pointer hover:border-slate-700 px-2 border-transparent border-2 rounded-md"
70
- onClick={() => {
71
- setIsCollapsed(false);
72
- }}
121
+ className={`group relative text-slate-200 flex items-center gap-2 cursor-pointer hover:bg-slate-800/40 px-2 py-0.5 border border-transparent border-l ${leftBorderColor} rounded-sm transition-colors duration-100`}
122
+ onClick={toggleCollapsed}
123
+ role="button"
124
+ aria-expanded={!isCollapsed}
125
+ tabIndex={0}
126
+ onKeyDown={handleKeyDown}
73
127
  >
128
+ {/* Timestamp and Service Name */}
74
129
  {props.log.time && (
75
- <div
76
- className="text-slate-500 courier-prime flex-none"
77
- style={{
78
- width: "230px !important",
79
- color: serviceColor,
80
- }}
81
- >
82
- {serviceName} &nbsp;{" "}
83
- </div>
84
- )}
85
- {props.log.severityText === LogSeverity.Information && (
86
- <div className="text-sky-400 courier-prime flex-none">
87
- [INFO] &nbsp;
88
- </div>
89
- )}
90
- {props.log.severityText === LogSeverity.Warning && (
91
- <div className="text-amber-400 courier-prime flex-none">
92
- [WARN] &nbsp;
93
- </div>
94
- )}
95
- {props.log.severityText === LogSeverity.Trace && (
96
- <div className="text-slate-400 courier-prime flex-none">
97
- [TRACE] &nbsp;
98
- </div>
99
- )}
100
- {props.log.severityText === LogSeverity.Debug && (
101
- <div className="text-slate-400 courier-prime flex-none">
102
- [DEBUG] &nbsp;
103
- </div>
104
- )}
105
- {props.log.severityText === LogSeverity.Error && (
106
- <div className="text-rose-400 courier-prime flex-none">
107
- [ERROR] &nbsp;
108
- </div>
109
- )}
110
- {props.log.severityText === LogSeverity.Fatal && (
111
- <div className="text-rose-400 courier-prime flex-none">
112
- [FATAL] &nbsp;
130
+ <div className="flex items-center space-x-2 flex-none w-52">
131
+ <div className="text-[10px] text-slate-400 font-mono tabular-nums">
132
+ {OneUptimeDate.getDateAsUserFriendlyFormattedString(
133
+ props.log.time,
134
+ )
135
+ .split(" ")
136
+ .slice(1)
137
+ .join(" ")}
138
+ </div>
139
+ <div
140
+ className="text-[11px] font-medium truncate"
141
+ style={{ color: serviceColor }}
142
+ title={serviceName}
143
+ >
144
+ {serviceName}
145
+ </div>
113
146
  </div>
114
147
  )}
115
148
 
116
- <div className={`${bodyColor} courier-prime`}>
117
- {isBodyInJSON && <pre className="whitespace-pre-wrap">{logBody}</pre>}
118
- {!isBodyInJSON && props.log.body?.toString()}
149
+ {/* Severity Badge */}
150
+ <div className="flex-none mr-1">
151
+ <span className="inline-flex items-center gap-2">
152
+ <span
153
+ className={`inline-block w-1.5 h-1.5 rounded-full ${severityDotClass}`}
154
+ aria-hidden="true"
155
+ />
156
+ <span className="text-[9px] uppercase tracking-wide text-slate-400">
157
+ {(props.log.severityText || "").toString() || "UNKNOWN"}
158
+ </span>
159
+ </span>
160
+ </div>
161
+
162
+ {/* Log Message */}
163
+ <div
164
+ className={`${bodyColor} font-mono text-[13px] md:text-sm leading-5 tracking-tight subpixel-antialiased flex-1 min-w-0`}
165
+ >
166
+ {isBodyInJSON ? (
167
+ <div className="truncate" title={logBodyMinified}>
168
+ {logBodyMinified}
169
+ </div>
170
+ ) : (
171
+ <div className="truncate" title={props.log.body?.toString()}>
172
+ {props.log.body?.toString()}
173
+ </div>
174
+ )}
175
+ </div>
176
+
177
+ {/* Quick copy button (message) */}
178
+ <div className="opacity-0 group-hover:opacity-100 transition-opacity ml-1">
179
+ {getCopyButton(props.log.body?.toString() || "")}
180
+ </div>
181
+
182
+ {/* Expand Indicator */}
183
+ <div className="flex-none ml-1 text-slate-500 group-hover:text-slate-300 transition-transform duration-200">
184
+ <Icon icon={IconProp.ChevronDown} className="w-3 h-3" />
119
185
  </div>
120
186
  </div>
121
187
  );
@@ -123,145 +189,164 @@ const LogItem: FunctionComponent<ComponentProps> = (
123
189
 
124
190
  return (
125
191
  <div
126
- className="text-slate-200 cursor-pointer hover:border-slate-700 px-2 border-transparent border-2 rounded-md mb-1"
127
- onClick={() => {
128
- setIsCollapsed(true);
129
- }}
192
+ className={`group relative text-slate-200 bg-slate-950/70 border ${leftBorderColor} border-l border-slate-900 rounded-sm p-2 hover:border-slate-700 transition-colors`}
130
193
  >
131
- {serviceName && (
132
- <div
133
- className="text-slate-500 courier-prime"
134
- style={{
135
- color: serviceColor,
136
- }}
137
- >
138
- {serviceName} &nbsp;{" "}
139
- </div>
140
- )}
141
- {props.log.time && (
142
- <div className="flex">
143
- <div className="font-medium text-slate-200 courier-prime mr-2">
144
- DATE TIME:
145
- </div>
146
- <div className="text-slate-500 courier-prime">
147
- {OneUptimeDate.getDateAsUserFriendlyFormattedString(props.log.time)}{" "}
148
- &nbsp;{" "}
149
- </div>
150
- </div>
151
- )}
152
- {props.log.severityText === LogSeverity.Information && (
153
- <div className="flex">
154
- <div className="font-medium text-slate-200 courier-prime mr-2">
155
- SEVERITY:
156
- </div>
157
- <div className="text-sky-400 courier-prime">[INFO] &nbsp;</div>
158
- </div>
159
- )}
160
- {props.log.severityText === LogSeverity.Unspecified && (
161
- <div className="flex">
162
- <div className="font-medium text-slate-200 courier-prime mr-2">
163
- SEVERITY:
164
- </div>
165
- <div className="text-sky-400 courier-prime">[UNKNOWN] &nbsp;</div>
166
- </div>
167
- )}
168
- {props.log.severityText === LogSeverity.Warning && (
169
- <div className="flex">
170
- <div className="font-medium text-slate-200 courier-prime mr-2">
171
- SEVERITY:
172
- </div>
173
- <div className="text-amber-400 courier-prime">[WARN] &nbsp;</div>
174
- </div>
175
- )}
176
- {props.log.severityText === LogSeverity.Trace && (
177
- <div className="flex">
178
- <div className="font-medium text-slate-200 courier-prime mr-2">
179
- SEVERITY:
180
- </div>
181
- <div className="text-slate-400 courier-prime">[TRACE] &nbsp;</div>
182
- </div>
183
- )}
184
- {props.log.severityText === LogSeverity.Debug && (
185
- <div className="flex">
186
- <div className="font-medium text-slate-200 courier-prime mr-2">
187
- SEVERITY:
194
+ {/* Header with Service Name and Close Indicator */}
195
+ <div className="flex items-center justify-between mb-1 pb-1 border-b border-slate-800/80">
196
+ {serviceName && (
197
+ <div
198
+ className="text-[13px] font-semibold"
199
+ style={{ color: serviceColor }}
200
+ >
201
+ {serviceName}
188
202
  </div>
189
- <div className="text-slate-400 courier-prime">[DEBUG] &nbsp;</div>
203
+ )}
204
+ <div className="flex items-center gap-2">
205
+ <span className="inline-flex items-center gap-2">
206
+ <span
207
+ className={`inline-block w-1.5 h-1.5 rounded-full ${severityDotClass}`}
208
+ aria-hidden="true"
209
+ />
210
+ <span className="text-[9px] uppercase tracking-wide text-slate-400">
211
+ {(props.log.severityText || "").toString() || "UNKNOWN"}
212
+ </span>
213
+ </span>
214
+ {props.log.time && (
215
+ <div className="text-[11px] text-slate-400 font-mono tabular-nums">
216
+ {OneUptimeDate.getDateAsUserFriendlyFormattedString(
217
+ props.log.time,
218
+ )}
219
+ </div>
220
+ )}
221
+ <button
222
+ type="button"
223
+ title="Collapse"
224
+ aria-label="Collapse"
225
+ onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
226
+ e.stopPropagation();
227
+ toggleCollapsed();
228
+ }}
229
+ className="flex-none text-slate-500 hover:text-slate-300 transition-colors"
230
+ >
231
+ <Icon icon={IconProp.ChevronUp} className="w-3.5 h-3.5" />
232
+ </button>
190
233
  </div>
191
- )}
192
- {props.log.severityText === LogSeverity.Error && (
193
- <div className="flex">
194
- <div className="font-medium text-slate-200 courier-prime mr-2">
195
- SEVERITY:
234
+ </div>
235
+
236
+ {/* Meta row (compact) */}
237
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-1.5 mb-1.5 text-[11px] text-slate-400">
238
+ {props.log.time && (
239
+ <div className="flex items-center gap-2">
240
+ <span className="uppercase tracking-wide">Timestamp</span>
241
+ <span className="text-slate-300 font-mono tabular-nums">
242
+ {OneUptimeDate.getDateAsUserFriendlyFormattedString(
243
+ props.log.time,
244
+ )}
245
+ </span>
196
246
  </div>
197
- <div className="text-rose-400 courier-prime">[ERROR] &nbsp;</div>
198
- </div>
199
- )}
200
- {props.log.severityText === LogSeverity.Fatal && (
201
- <div className="flex">
202
- <div className="font-medium text-slate-200 courier-prime mr-2">
203
- SEVERITY:
247
+ )}
248
+ {props.log.severityText && (
249
+ <div className="flex items-center gap-2">
250
+ <span className="uppercase tracking-wide">Severity</span>
251
+ <span className="inline-flex items-center gap-2 text-slate-300">
252
+ <span
253
+ className={`inline-block w-1.5 h-1.5 rounded-full ${severityDotClass}`}
254
+ />
255
+ <span className="text-[10px] uppercase">
256
+ {(props.log.severityText || "").toString()}
257
+ </span>
258
+ </span>
204
259
  </div>
205
- <div className="text-rose-400 courier-prime">[FATAL] &nbsp;</div>
206
- </div>
207
- )}
260
+ )}
261
+ </div>
208
262
 
209
- <div>
210
- <div className="flex">
211
- <div className="font-medium text-slate-200 courier-prime mr-2">
212
- MESSAGE:&nbsp;
263
+ {/* Message */}
264
+ <div className="mb-1.5">
265
+ <div className="flex items-center justify-between text-[10px] font-semibold text-slate-400 uppercase tracking-wide mb-1">
266
+ Message
267
+ <div className="flex items-center gap-2 normal-case">
268
+ {getCopyButton(
269
+ isBodyInJSON ? logBody : props.log.body?.toString() || "",
270
+ )}
213
271
  </div>
272
+ </div>
273
+ <div className="bg-slate-950 rounded p-1.5 border border-slate-800">
214
274
  {!isBodyInJSON && (
215
- <div className={`${bodyColor} courier-prime`}>
275
+ <div className={`${bodyColor} font-mono text-sm leading-snug`}>
216
276
  {props.log.body?.toString()}
217
277
  </div>
218
278
  )}
279
+ {isBodyInJSON && (
280
+ <pre
281
+ className={`${bodyColor} font-mono text-sm leading-snug whitespace-pre overflow-auto max-h-40 w-full block`}
282
+ >
283
+ {logBody}
284
+ </pre>
285
+ )}
219
286
  </div>
220
- {isBodyInJSON && (
221
- <pre className={`${bodyColor} courier-prime whitespace-pre-wrap`}>
222
- {logBody}
223
- </pre>
224
- )}
225
287
  </div>
226
288
 
227
- {props.log.traceId && (
228
- <div className="flex">
229
- <div className="font-medium text-slate-200 courier-prime mr-2">
230
- TRACE:&nbsp;&nbsp;&nbsp;
231
- </div>
232
- <div className={`${bodyColor} courier-prime`}>
233
- {props.log.traceId?.toString()}
234
- </div>
235
- {getCopyButton(props.log.traceId?.toString() || "")}
236
- </div>
237
- )}
289
+ {/* Trace and Span Information */}
290
+ {(props.log.traceId || props.log.spanId) && (
291
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-1.5 mb-1.5">
292
+ {props.log.traceId && (
293
+ <div className="flex flex-col space-y-2">
294
+ <div className="text-[10px] font-semibold text-slate-400 uppercase tracking-wide">
295
+ Trace ID
296
+ </div>
297
+ <div className="flex items-center space-x-2">
298
+ <div
299
+ className={`${bodyColor} font-mono text-sm bg-slate-950 px-2.5 py-1.5 rounded border border-slate-800 flex-1 truncate`}
300
+ >
301
+ {props.log.traceId?.toString()}
302
+ </div>
303
+ {getCopyButton(props.log.traceId?.toString() || "")}
304
+ </div>
305
+ </div>
306
+ )}
238
307
 
239
- {props.log.spanId && (
240
- <div className="flex">
241
- <div className="font-medium text-slate-200 courier-prime mr-2">
242
- SPAN:&nbsp;&nbsp;&nbsp;&nbsp;
243
- </div>
244
- <div className={`${bodyColor} courier-prime`}>
245
- {props.log.spanId?.toString()}
246
- </div>
247
- {getCopyButton(props.log.spanId?.toString() || "")}
308
+ {props.log.spanId && (
309
+ <div className="flex flex-col space-y-2">
310
+ <div className="text-[10px] font-semibold text-slate-400 uppercase tracking-wide">
311
+ Span ID
312
+ </div>
313
+ <div className="flex items-center space-x-2">
314
+ <div
315
+ className={`${bodyColor} font-mono text-sm bg-slate-950 px-2.5 py-1.5 rounded border border-slate-800 flex-1 truncate`}
316
+ >
317
+ {props.log.spanId?.toString()}
318
+ </div>
319
+ {getCopyButton(props.log.spanId?.toString() || "")}
320
+ </div>
321
+ </div>
322
+ )}
248
323
  </div>
249
324
  )}
250
325
 
326
+ {/* Attributes */}
251
327
  {props.log.attributes && (
252
328
  <div>
253
- <div className="flex">
254
- <div className="font-medium text-slate-200 courier-prime mr-2">
255
- ATTRIBUTES:
256
- </div>
257
- </div>
258
- <pre className={`${bodyColor} courier-prime whitespace-pre-wrap`}>
259
- {JSON.stringify(
260
- JSONFunctions.unflattenObject(props.log.attributes || {}),
261
- null,
262
- 2,
329
+ <div className="flex items-center justify-between text-[10px] font-semibold text-slate-400 uppercase tracking-wide mb-1">
330
+ Attributes
331
+ {getCopyButton(
332
+ JSON.stringify(
333
+ JSONFunctions.unflattenObject(props.log.attributes || {}),
334
+ null,
335
+ 2,
336
+ ) || "",
263
337
  )}
264
- </pre>
338
+ </div>
339
+ <div className="bg-slate-950 rounded p-1.5 border border-slate-800">
340
+ <pre
341
+ className={`${bodyColor} font-mono text-sm leading-snug whitespace-pre overflow-auto max-h-40 w-full block`}
342
+ >
343
+ {JSON.stringify(
344
+ JSONFunctions.unflattenObject(props.log.attributes || {}),
345
+ null,
346
+ 2,
347
+ )}
348
+ </pre>
349
+ </div>
265
350
  </div>
266
351
  )}
267
352
  </div>