@oneuptime/common 8.0.5440 → 8.0.5466
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/StatusPage.ts +80 -0
- package/Models/DatabaseModels/TelemetryUsageBilling.ts +1 -1
- package/Server/API/StatusPageAPI.ts +138 -52
- package/Server/EnvironmentConfig.ts +37 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1761232578396-MigrationName.ts +29 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AnalyticsDatabaseService.ts +71 -11
- package/Server/Services/OpenTelemetryIngestService.ts +1 -39
- package/Server/Services/StatusPageService.ts +117 -0
- package/Server/Services/TelemetryUsageBillingService.ts +268 -15
- package/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.ts +5 -0
- package/Server/Utils/Telemetry/Telemetry.ts +135 -81
- package/Server/Utils/VM/VMRunner.ts +3 -4
- package/Types/Date.ts +5 -0
- package/UI/Components/LogsViewer/LogItem.tsx +12 -4
- package/UI/Components/LogsViewer/LogsViewer.tsx +131 -29
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +2 -2
- package/UI/Components/Table/TableRow.tsx +89 -77
- package/UI/esbuild-config.js +32 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +82 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryUsageBilling.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +157 -74
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +15 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1761232578396-MigrationName.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1761232578396-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +55 -8
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/OpenTelemetryIngestService.js +0 -30
- package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +95 -0
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryUsageBillingService.js +211 -8
- package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.js +4 -0
- package/build/dist/Server/Types/Billing/MeteredPlan/TelemetryMeteredPlan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/Telemetry.js +84 -60
- package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +2 -2
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Types/Date.js +4 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogItem.js +5 -3
- package/build/dist/UI/Components/LogsViewer/LogItem.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +73 -22
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +2 -2
- package/build/dist/UI/Components/Table/TableRow.js +18 -6
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/package.json +4 -4
|
@@ -143,10 +143,10 @@ export default class TelemetryUtil {
|
|
|
143
143
|
let { prefixKeysWithString } = data;
|
|
144
144
|
|
|
145
145
|
if (prefixKeysWithString) {
|
|
146
|
-
prefixKeysWithString = prefixKeysWithString
|
|
146
|
+
prefixKeysWithString = `${prefixKeysWithString}.`;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
const finalObj: Dictionary<AttributeType | Array<AttributeType>> = {};
|
|
150
150
|
const attributes: JSONArray = items;
|
|
151
151
|
|
|
152
152
|
if (!attributes) {
|
|
@@ -154,25 +154,41 @@ export default class TelemetryUtil {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
for (const attribute of attributes) {
|
|
157
|
-
if (attribute["key"]
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
157
|
+
if (!attribute["key"] || typeof attribute["key"] !== "string") {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const keyWithPrefix: string = `${prefixKeysWithString}${attribute["key"]}`;
|
|
162
|
+
|
|
163
|
+
const value:
|
|
164
|
+
| AttributeType
|
|
165
|
+
| Dictionary<AttributeType | Array<AttributeType>>
|
|
166
|
+
| Array<AttributeType>
|
|
167
|
+
| null = this.getAttributeValues(keyWithPrefix, attribute["value"]);
|
|
168
|
+
|
|
169
|
+
if (value === null) {
|
|
170
|
+
finalObj[keyWithPrefix] = null;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
finalObj[keyWithPrefix] = value;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (typeof value === "object") {
|
|
180
|
+
for (const [nestedKey, nestedValue] of Object.entries(
|
|
181
|
+
value as Dictionary<AttributeType | Array<AttributeType>>,
|
|
182
|
+
)) {
|
|
183
|
+
finalObj[nestedKey] = nestedValue as
|
|
184
|
+
| AttributeType
|
|
185
|
+
| Array<AttributeType>;
|
|
174
186
|
}
|
|
187
|
+
|
|
188
|
+
continue;
|
|
175
189
|
}
|
|
190
|
+
|
|
191
|
+
finalObj[keyWithPrefix] = value as AttributeType;
|
|
176
192
|
}
|
|
177
193
|
|
|
178
194
|
return finalObj;
|
|
@@ -181,73 +197,111 @@ export default class TelemetryUtil {
|
|
|
181
197
|
public static getAttributeValues(
|
|
182
198
|
prefixKeysWithString: string,
|
|
183
199
|
value: JSONValue,
|
|
184
|
-
):
|
|
200
|
+
):
|
|
201
|
+
| AttributeType
|
|
202
|
+
| Dictionary<AttributeType | Array<AttributeType>>
|
|
203
|
+
| Array<AttributeType>
|
|
204
|
+
| null {
|
|
185
205
|
let finalObj:
|
|
186
|
-
| Dictionary<AttributeType
|
|
206
|
+
| Dictionary<AttributeType | Array<AttributeType>>
|
|
187
207
|
| AttributeType
|
|
188
|
-
| Array<AttributeType>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
208
|
+
| Array<AttributeType>
|
|
209
|
+
| null = null;
|
|
210
|
+
const jsonValue: JSONObject = value as JSONObject;
|
|
211
|
+
|
|
212
|
+
if (jsonValue && typeof jsonValue === "object") {
|
|
213
|
+
if (Object.prototype.hasOwnProperty.call(jsonValue, "stringValue")) {
|
|
214
|
+
const stringValue: JSONValue = jsonValue["stringValue"];
|
|
215
|
+
finalObj =
|
|
216
|
+
stringValue !== undefined && stringValue !== null
|
|
217
|
+
? (stringValue as string)
|
|
218
|
+
: "";
|
|
219
|
+
} else if (Object.prototype.hasOwnProperty.call(jsonValue, "intValue")) {
|
|
220
|
+
const intValue: JSONValue = jsonValue["intValue"];
|
|
221
|
+
if (intValue !== undefined && intValue !== null) {
|
|
222
|
+
finalObj = intValue as number;
|
|
223
|
+
}
|
|
224
|
+
} else if (
|
|
225
|
+
Object.prototype.hasOwnProperty.call(jsonValue, "doubleValue")
|
|
226
|
+
) {
|
|
227
|
+
const doubleValue: JSONValue = jsonValue["doubleValue"];
|
|
228
|
+
if (doubleValue !== undefined && doubleValue !== null) {
|
|
229
|
+
finalObj = doubleValue as number;
|
|
230
|
+
}
|
|
231
|
+
} else if (Object.prototype.hasOwnProperty.call(jsonValue, "boolValue")) {
|
|
232
|
+
finalObj = jsonValue["boolValue"] as boolean;
|
|
233
|
+
} else if (
|
|
234
|
+
jsonValue["arrayValue"] &&
|
|
235
|
+
(jsonValue["arrayValue"] as JSONObject)["values"]
|
|
236
|
+
) {
|
|
237
|
+
const values: JSONArray = (jsonValue["arrayValue"] as JSONObject)[
|
|
238
|
+
"values"
|
|
239
|
+
] as JSONArray;
|
|
240
|
+
finalObj = values.map((v: JSONObject) => {
|
|
241
|
+
return this.getAttributeValues(
|
|
242
|
+
prefixKeysWithString,
|
|
243
|
+
v,
|
|
244
|
+
) as AttributeType;
|
|
245
|
+
}) as Array<AttributeType>;
|
|
246
|
+
} else if (
|
|
247
|
+
jsonValue["mapValue"] &&
|
|
248
|
+
(jsonValue["mapValue"] as JSONObject)["fields"]
|
|
249
|
+
) {
|
|
250
|
+
const fields: JSONObject = (jsonValue["mapValue"] as JSONObject)[
|
|
251
|
+
"fields"
|
|
252
|
+
] as JSONObject;
|
|
253
|
+
|
|
254
|
+
const flattenedFields: Dictionary<
|
|
255
|
+
AttributeType | Array<AttributeType>
|
|
256
|
+
> = {};
|
|
257
|
+
for (const key in fields) {
|
|
258
|
+
const nestedPrefix: string = `${prefixKeysWithString}.${key}`;
|
|
259
|
+
const nestedValue:
|
|
260
|
+
| AttributeType
|
|
261
|
+
| Dictionary<AttributeType | Array<AttributeType>>
|
|
262
|
+
| Array<AttributeType>
|
|
263
|
+
| null = this.getAttributeValues(nestedPrefix, fields[key]);
|
|
264
|
+
|
|
265
|
+
if (nestedValue === null) {
|
|
266
|
+
flattenedFields[nestedPrefix] = null;
|
|
267
|
+
continue;
|
|
230
268
|
}
|
|
231
|
-
|
|
232
|
-
|
|
269
|
+
|
|
270
|
+
if (Array.isArray(nestedValue)) {
|
|
271
|
+
flattenedFields[nestedPrefix] = nestedValue;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (typeof nestedValue === "object") {
|
|
276
|
+
for (const [nestedKey, nestedEntry] of Object.entries(
|
|
277
|
+
nestedValue as Dictionary<AttributeType | Array<AttributeType>>,
|
|
278
|
+
)) {
|
|
279
|
+
flattenedFields[nestedKey] = nestedEntry as
|
|
280
|
+
| AttributeType
|
|
281
|
+
| Array<AttributeType>;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
flattenedFields[nestedPrefix] = nestedValue as AttributeType;
|
|
233
288
|
}
|
|
289
|
+
|
|
290
|
+
finalObj = flattenedFields;
|
|
291
|
+
} else if (
|
|
292
|
+
jsonValue["kvlistValue"] &&
|
|
293
|
+
(jsonValue["kvlistValue"] as JSONObject)["values"]
|
|
294
|
+
) {
|
|
295
|
+
const values: JSONArray = (jsonValue["kvlistValue"] as JSONObject)[
|
|
296
|
+
"values"
|
|
297
|
+
] as JSONArray;
|
|
298
|
+
finalObj = this.getAttributes({
|
|
299
|
+
prefixKeysWithString,
|
|
300
|
+
items: values,
|
|
301
|
+
});
|
|
302
|
+
} else if ("nullValue" in jsonValue) {
|
|
303
|
+
finalObj = null;
|
|
234
304
|
}
|
|
235
|
-
finalObj = flattenedFields;
|
|
236
|
-
}
|
|
237
|
-
// kvlistValue
|
|
238
|
-
else if (
|
|
239
|
-
value["kvlistValue"] &&
|
|
240
|
-
(value["kvlistValue"] as JSONObject)["values"]
|
|
241
|
-
) {
|
|
242
|
-
const values: JSONArray = (value["kvlistValue"] as JSONObject)[
|
|
243
|
-
"values"
|
|
244
|
-
] as JSONArray;
|
|
245
|
-
finalObj = this.getAttributes({
|
|
246
|
-
prefixKeysWithString,
|
|
247
|
-
items: values,
|
|
248
|
-
}) as Dictionary<AttributeType>;
|
|
249
|
-
} else if (value["nullValue"]) {
|
|
250
|
-
finalObj = null;
|
|
251
305
|
}
|
|
252
306
|
|
|
253
307
|
return finalObj;
|
|
@@ -48,10 +48,9 @@ export default class VMRunner {
|
|
|
48
48
|
|
|
49
49
|
vm.createContext(sandbox); // Contextify the object.
|
|
50
50
|
|
|
51
|
-
const script: string =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
})()` || "";
|
|
51
|
+
const script: string = `(async()=>{
|
|
52
|
+
${code}
|
|
53
|
+
})()`;
|
|
55
54
|
|
|
56
55
|
const returnVal: any = await vm.runInContext(script, sandbox, {
|
|
57
56
|
timeout: options.timeout || 5000,
|
package/Types/Date.ts
CHANGED
|
@@ -1453,4 +1453,9 @@ export default class OneUptimeDate {
|
|
|
1453
1453
|
date = this.fromString(date);
|
|
1454
1454
|
return moment(date).format("YYYY-MM-DD HH:mm:ss");
|
|
1455
1455
|
}
|
|
1456
|
+
|
|
1457
|
+
public static toClickhouseDateTime(date: Date | string): string {
|
|
1458
|
+
const parsedDate: Date = this.fromString(date);
|
|
1459
|
+
return moment(parsedDate).utc().format("YYYY-MM-DD HH:mm:ss");
|
|
1460
|
+
}
|
|
1456
1461
|
}
|
|
@@ -265,7 +265,7 @@ const LogItem: FunctionComponent<ComponentProps> = (
|
|
|
265
265
|
if (isCollapsed) {
|
|
266
266
|
return (
|
|
267
267
|
<div
|
|
268
|
-
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`}
|
|
268
|
+
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 font-mono`}
|
|
269
269
|
onClick={toggleCollapsed}
|
|
270
270
|
role="button"
|
|
271
271
|
aria-expanded={!isCollapsed}
|
|
@@ -311,11 +311,14 @@ const LogItem: FunctionComponent<ComponentProps> = (
|
|
|
311
311
|
className={`${bodyColor} font-mono text-[13px] md:text-sm leading-5 tracking-tight subpixel-antialiased flex-1 min-w-0`}
|
|
312
312
|
>
|
|
313
313
|
{isBodyInJSON ? (
|
|
314
|
-
<div className="truncate" title={logBodyMinified}>
|
|
314
|
+
<div className="truncate font-mono" title={logBodyMinified}>
|
|
315
315
|
{logBodyMinified}
|
|
316
316
|
</div>
|
|
317
317
|
) : (
|
|
318
|
-
<div
|
|
318
|
+
<div
|
|
319
|
+
className="truncate font-mono"
|
|
320
|
+
title={props.log.body?.toString()}
|
|
321
|
+
>
|
|
319
322
|
{props.log.body?.toString()}
|
|
320
323
|
</div>
|
|
321
324
|
)}
|
|
@@ -339,7 +342,12 @@ const LogItem: FunctionComponent<ComponentProps> = (
|
|
|
339
342
|
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`}
|
|
340
343
|
>
|
|
341
344
|
{/* Header with Service Name and Close Indicator */}
|
|
342
|
-
<div
|
|
345
|
+
<div
|
|
346
|
+
className="flex items-center justify-between mb-1 pb-1 border-b border-slate-800/80"
|
|
347
|
+
onClick={() => {
|
|
348
|
+
toggleCollapsed();
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
343
351
|
{serviceName && (
|
|
344
352
|
<div
|
|
345
353
|
className="text-[13px] font-semibold"
|
|
@@ -15,9 +15,11 @@ import React, {
|
|
|
15
15
|
ReactElement,
|
|
16
16
|
Ref,
|
|
17
17
|
useCallback,
|
|
18
|
+
useMemo,
|
|
18
19
|
} from "react";
|
|
19
20
|
import Toggle from "../Toggle/Toggle";
|
|
20
21
|
import Card from "../Card/Card";
|
|
22
|
+
import Icon from "../Icon/Icon";
|
|
21
23
|
import Button, { ButtonSize, ButtonStyleType } from "../Button/Button";
|
|
22
24
|
import IconProp from "../../../Types/Icon/IconProp";
|
|
23
25
|
import ModelAPI from "../../Utils/ModelAPI/ModelAPI";
|
|
@@ -66,8 +68,9 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
66
68
|
typeof window !== "undefined" ? window.innerHeight : 900,
|
|
67
69
|
);
|
|
68
70
|
const [autoScroll, setAutoScroll] = React.useState<boolean>(true);
|
|
69
|
-
const [
|
|
71
|
+
const [showScrollToLatest, setShowScrollToLatest] =
|
|
70
72
|
React.useState<boolean>(false);
|
|
73
|
+
const [isDescending, setIsDescending] = React.useState<boolean>(false);
|
|
71
74
|
// removed wrapLines toggle for a cleaner toolbar
|
|
72
75
|
const logsViewerRef: Ref<HTMLDivElement> = React.useRef<HTMLDivElement>(null);
|
|
73
76
|
const scrollContainerRef: Ref<HTMLDivElement> =
|
|
@@ -89,6 +92,14 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
89
92
|
Dictionary<TelemetryService>
|
|
90
93
|
>({});
|
|
91
94
|
|
|
95
|
+
const displayLogs: Array<Log> = useMemo(() => {
|
|
96
|
+
if (isDescending) {
|
|
97
|
+
return [...props.logs].reverse();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return props.logs;
|
|
101
|
+
}, [props.logs, isDescending]);
|
|
102
|
+
|
|
92
103
|
const loadTelemetryServices: PromiseVoidFunction =
|
|
93
104
|
useCallback(async (): Promise<void> => {
|
|
94
105
|
try {
|
|
@@ -178,33 +189,73 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
178
189
|
};
|
|
179
190
|
}, [loadTelemetryServices]);
|
|
180
191
|
|
|
181
|
-
// Keep scroll
|
|
192
|
+
// Keep scroll aligned with the latest log entry
|
|
182
193
|
|
|
183
|
-
const
|
|
194
|
+
const scrollToLatest: VoidFunction = (): void => {
|
|
184
195
|
const scrollContainer: HTMLDivElement | null = scrollContainerRef.current;
|
|
185
196
|
|
|
186
|
-
if (scrollContainer) {
|
|
187
|
-
|
|
197
|
+
if (!scrollContainer) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (isDescending) {
|
|
202
|
+
scrollContainer.scrollTop = 0;
|
|
203
|
+
return;
|
|
188
204
|
}
|
|
205
|
+
|
|
206
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const applySortDirection: (nextDescending: boolean) => void = (
|
|
210
|
+
nextDescending: boolean,
|
|
211
|
+
) => {
|
|
212
|
+
setShowScrollToLatest(false);
|
|
213
|
+
setIsDescending((previous: boolean) => {
|
|
214
|
+
if (previous === nextDescending) {
|
|
215
|
+
return previous;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Apply scroll alignment after the DOM reorders log entries.
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
const scrollContainer: HTMLDivElement | null =
|
|
221
|
+
scrollContainerRef.current;
|
|
222
|
+
|
|
223
|
+
if (!scrollContainer) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (nextDescending) {
|
|
228
|
+
scrollContainer.scrollTop = 0;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
233
|
+
}, 0);
|
|
234
|
+
|
|
235
|
+
return nextDescending;
|
|
236
|
+
});
|
|
189
237
|
};
|
|
190
238
|
|
|
191
|
-
const handleScroll: VoidFunction = (): void => {
|
|
239
|
+
const handleScroll: VoidFunction = React.useCallback((): void => {
|
|
192
240
|
const scrollContainer: HTMLDivElement | null = scrollContainerRef.current;
|
|
193
|
-
if (scrollContainer) {
|
|
194
|
-
|
|
195
|
-
const isNearBottom: boolean =
|
|
196
|
-
scrollHeight - scrollTop - clientHeight < 100;
|
|
197
|
-
setShowScrollToBottom(!isNearBottom && props.logs.length > 0);
|
|
241
|
+
if (!scrollContainer) {
|
|
242
|
+
return;
|
|
198
243
|
}
|
|
199
|
-
|
|
244
|
+
|
|
245
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
|
|
246
|
+
const isNearLatest: boolean = isDescending
|
|
247
|
+
? scrollTop < 100
|
|
248
|
+
: scrollHeight - scrollTop - clientHeight < 100;
|
|
249
|
+
setShowScrollToLatest(!isNearLatest && displayLogs.length > 0);
|
|
250
|
+
}, [isDescending, displayLogs.length]);
|
|
200
251
|
|
|
201
252
|
React.useEffect(() => {
|
|
202
253
|
if (!autoScroll) {
|
|
203
254
|
return;
|
|
204
255
|
}
|
|
205
256
|
|
|
206
|
-
|
|
207
|
-
}, [props.logs]);
|
|
257
|
+
scrollToLatest();
|
|
258
|
+
}, [props.logs, autoScroll, isDescending]);
|
|
208
259
|
|
|
209
260
|
React.useEffect(() => {
|
|
210
261
|
const scrollContainer: HTMLDivElement | null = scrollContainerRef.current;
|
|
@@ -215,7 +266,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
215
266
|
};
|
|
216
267
|
}
|
|
217
268
|
return () => {}; // Return empty cleanup function if no scrollContainer
|
|
218
|
-
}, []);
|
|
269
|
+
}, [handleScroll]);
|
|
219
270
|
|
|
220
271
|
if (isPageLoading) {
|
|
221
272
|
return <PageLoader isVisible={true} />;
|
|
@@ -305,12 +356,54 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
305
356
|
</div>
|
|
306
357
|
<span className="hidden sm:block h-4 w-px bg-slate-200" />
|
|
307
358
|
<span className="text-xs text-slate-500">
|
|
308
|
-
{
|
|
309
|
-
{
|
|
359
|
+
{displayLogs.length} result
|
|
360
|
+
{displayLogs.length !== 1 ? "s" : ""}
|
|
310
361
|
</span>
|
|
311
362
|
</div>
|
|
312
363
|
|
|
313
364
|
<div className="flex items-center gap-2">
|
|
365
|
+
<div className="inline-flex items-center rounded-full border border-slate-200 bg-white/80 p-1 shadow-sm ring-1 ring-slate-200/60">
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
aria-pressed={isDescending}
|
|
369
|
+
className={`flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold tracking-wide transition-all duration-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 ${
|
|
370
|
+
isDescending
|
|
371
|
+
? "bg-indigo-600 text-white shadow-sm ring-1 ring-indigo-500/40"
|
|
372
|
+
: "text-slate-500 hover:text-indigo-600"
|
|
373
|
+
}`}
|
|
374
|
+
onClick={() => {
|
|
375
|
+
applySortDirection(true);
|
|
376
|
+
}}
|
|
377
|
+
>
|
|
378
|
+
<Icon
|
|
379
|
+
icon={IconProp.BarsArrowDown}
|
|
380
|
+
className={`h-4 w-4 ${
|
|
381
|
+
isDescending ? "text-white/90" : "text-slate-400"
|
|
382
|
+
}`}
|
|
383
|
+
/>
|
|
384
|
+
<span>Newest first</span>
|
|
385
|
+
</button>
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
aria-pressed={!isDescending}
|
|
389
|
+
className={`flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold tracking-wide transition-all duration-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 ${
|
|
390
|
+
!isDescending
|
|
391
|
+
? "bg-indigo-600 text-white shadow-sm ring-1 ring-indigo-500/40"
|
|
392
|
+
: "text-slate-500 hover:text-indigo-600"
|
|
393
|
+
}`}
|
|
394
|
+
onClick={() => {
|
|
395
|
+
applySortDirection(false);
|
|
396
|
+
}}
|
|
397
|
+
>
|
|
398
|
+
<Icon
|
|
399
|
+
icon={IconProp.BarsArrowUp}
|
|
400
|
+
className={`h-4 w-4 ${
|
|
401
|
+
!isDescending ? "text-white/90" : "text-slate-400"
|
|
402
|
+
}`}
|
|
403
|
+
/>
|
|
404
|
+
<span>Oldest first</span>
|
|
405
|
+
</button>
|
|
406
|
+
</div>
|
|
314
407
|
<Button
|
|
315
408
|
title="Apply Filters"
|
|
316
409
|
icon={IconProp.Search}
|
|
@@ -342,7 +435,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
342
435
|
onScroll={handleScroll}
|
|
343
436
|
>
|
|
344
437
|
<ul role="list" className="divide-y divide-slate-800">
|
|
345
|
-
{
|
|
438
|
+
{displayLogs.map((log: Log, i: number) => {
|
|
346
439
|
const traceRouteProps: OptionalTraceRouteProps =
|
|
347
440
|
props.getTraceRoute
|
|
348
441
|
? { getTraceRoute: props.getTraceRoute }
|
|
@@ -364,7 +457,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
364
457
|
})}
|
|
365
458
|
</ul>
|
|
366
459
|
|
|
367
|
-
{
|
|
460
|
+
{displayLogs.length === 0 && (
|
|
368
461
|
<div className="flex items-center justify-center h-full px-4">
|
|
369
462
|
<div className="text-center">
|
|
370
463
|
<h3 className="text-sm font-medium text-slate-300 mb-1">
|
|
@@ -380,12 +473,12 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
380
473
|
</div>
|
|
381
474
|
</div>
|
|
382
475
|
|
|
383
|
-
{/* Floating Scroll to
|
|
384
|
-
{
|
|
476
|
+
{/* Floating Scroll to Latest Button */}
|
|
477
|
+
{showScrollToLatest && (
|
|
385
478
|
<button
|
|
386
|
-
onClick={
|
|
479
|
+
onClick={scrollToLatest}
|
|
387
480
|
className="absolute bottom-3 right-3 bg-slate-700 hover:bg-slate-600 text-white p-2 rounded-md shadow transition-all"
|
|
388
|
-
title="Scroll to bottom"
|
|
481
|
+
title={isDescending ? "Scroll to top" : "Scroll to bottom"}
|
|
389
482
|
>
|
|
390
483
|
<svg
|
|
391
484
|
className="w-5 h-5"
|
|
@@ -393,12 +486,21 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
393
486
|
stroke="currentColor"
|
|
394
487
|
viewBox="0 0 24 24"
|
|
395
488
|
>
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
489
|
+
{isDescending ? (
|
|
490
|
+
<path
|
|
491
|
+
strokeLinecap="round"
|
|
492
|
+
strokeLinejoin="round"
|
|
493
|
+
strokeWidth={2}
|
|
494
|
+
d="M5 14l7-7 7 7m-7-7v18"
|
|
495
|
+
/>
|
|
496
|
+
) : (
|
|
497
|
+
<path
|
|
498
|
+
strokeLinecap="round"
|
|
499
|
+
strokeLinejoin="round"
|
|
500
|
+
strokeWidth={2}
|
|
501
|
+
d="M19 10l-7 7-7-7m7 7V3"
|
|
502
|
+
/>
|
|
503
|
+
)}
|
|
402
504
|
</svg>
|
|
403
505
|
</button>
|
|
404
506
|
)}
|
|
@@ -98,7 +98,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
|
|
98
98
|
|
|
99
99
|
const baseClass: string = isSyntaxHighlighter
|
|
100
100
|
? "mt-4 mb-4 rounded-lg overflow-hidden"
|
|
101
|
-
: "bg-gray-900 text-gray-100 mt-4 mb-4 p-
|
|
101
|
+
: "bg-gray-900 text-gray-100 mt-4 mb-4 p-2 rounded-lg text-sm overflow-x-auto border border-gray-700";
|
|
102
102
|
|
|
103
103
|
return (
|
|
104
104
|
<pre className={baseClass} {...rest}>
|
|
@@ -201,7 +201,7 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
|
|
|
201
201
|
children={content}
|
|
202
202
|
language={match[1]}
|
|
203
203
|
style={a11yDark}
|
|
204
|
-
className="rounded-lg mt-4 mb-4 !bg-gray-900 !p-
|
|
204
|
+
className="rounded-lg mt-4 mb-4 !bg-gray-900 !p-2 text-sm"
|
|
205
205
|
codeTagProps={{ className: "font-mono" }}
|
|
206
206
|
/>
|
|
207
207
|
) : (
|