@oneuptime/common 10.0.54 → 10.0.56
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/DockerHost.ts +662 -0
- package/Models/DatabaseModels/GlobalConfig.ts +112 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Server/API/TelemetryAPI.ts +352 -16
- package/Server/Infrastructure/ClickhouseConfig.ts +9 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.ts +76 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.ts +133 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/DockerHostService.ts +173 -0
- package/Server/Services/ExceptionAggregationService.ts +335 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/LogAggregationService.ts +17 -0
- package/Server/Services/MonitorProbeService.ts +42 -21
- package/Server/Services/MonitorService.ts +21 -21
- package/Server/Services/TraceAggregationService.ts +514 -0
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +73 -1
- package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
- package/Tests/__mocks__/mermaid.js +18 -0
- package/Tests/__mocks__/react-markdown.js +17 -0
- package/Tests/__mocks__/react-syntax-highlighter.js +19 -0
- package/Tests/__mocks__/remark-gfm.js +8 -0
- package/Types/Icon/IconProp.ts +1 -0
- package/Types/Monitor/DockerAlertTemplates.ts +507 -0
- package/Types/Monitor/DockerMetricCatalog.ts +226 -0
- package/Types/Monitor/MonitorStep.ts +33 -0
- package/Types/Monitor/MonitorStepDockerMonitor.ts +38 -0
- package/Types/Monitor/MonitorType.ts +15 -1
- package/Types/Permission.ts +38 -0
- package/UI/Components/Icon/Icon.tsx +87 -0
- package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +7 -132
- package/UI/Components/ModelDetail/CardModelDetail.tsx +11 -1
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +285 -0
- package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +85 -0
- package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +156 -0
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +160 -0
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +85 -0
- package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +102 -0
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +280 -0
- package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +125 -0
- package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +114 -0
- package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +378 -0
- package/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.tsx +78 -0
- package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +64 -0
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +193 -0
- package/UI/Components/TelemetryViewer/types.ts +67 -0
- package/build/dist/Models/DatabaseModels/DockerHost.js +686 -0
- package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +117 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +237 -16
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/ClickhouseConfig.js +9 -0
- package/build/dist/Server/Infrastructure/ClickhouseConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.js +35 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.js +52 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +162 -0
- package/build/dist/Server/Services/DockerHostService.js.map +1 -0
- package/build/dist/Server/Services/ExceptionAggregationService.js +224 -0
- package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -0
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +11 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MonitorProbeService.js +28 -14
- package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +19 -17
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +364 -0
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +46 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Tests/Server/Services/LogAggregationService.test.js +2 -2
- package/build/dist/Tests/Server/Services/LogAggregationService.test.js.map +1 -1
- package/build/dist/Types/Icon/IconProp.js +1 -0
- package/build/dist/Types/Icon/IconProp.js.map +1 -1
- package/build/dist/Types/Monitor/DockerAlertTemplates.js +410 -0
- package/build/dist/Types/Monitor/DockerAlertTemplates.js.map +1 -0
- package/build/dist/Types/Monitor/DockerMetricCatalog.js +192 -0
- package/build/dist/Types/Monitor/DockerMetricCatalog.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorStep.js +23 -0
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepDockerMonitor.js +21 -0
- package/build/dist/Types/Monitor/MonitorStepDockerMonitor.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorType.js +14 -1
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/Permission.js +36 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +13 -0
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +7 -75
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +8 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +71 -0
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +39 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +61 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +66 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +41 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +35 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +132 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +65 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +52 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +224 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.js +35 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +27 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +97 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/types.js +6 -0
- package/build/dist/UI/Components/TelemetryViewer/types.js.map +1 -0
- package/jest.config.json +6 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Icon from "../Icon/Icon";
|
|
2
2
|
import IconProp from "../../../Types/Icon/IconProp";
|
|
3
3
|
import TinyFormDocumentation from "../TinyFormDocumentation/TinyFormDocumentation";
|
|
4
|
-
import
|
|
4
|
+
import MarkdownViewer from "./MarkdownViewer";
|
|
5
5
|
import React, {
|
|
6
6
|
FunctionComponent,
|
|
7
7
|
ReactElement,
|
|
@@ -290,138 +290,13 @@ const MarkdownEditor: FunctionComponent<ComponentProps> = (
|
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
const renderPreview: () => ReactElement = (): ReactElement => {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
htmlContent = htmlContent.replace(
|
|
298
|
-
/```([^`]*?)```/g,
|
|
299
|
-
(_match: string, code: string) => {
|
|
300
|
-
const escapedCode: string = code
|
|
301
|
-
.replace(/&/g, "&")
|
|
302
|
-
.replace(/</g, "<")
|
|
303
|
-
.replace(/>/g, ">")
|
|
304
|
-
.replace(/"/g, """)
|
|
305
|
-
.replace(/'/g, "'");
|
|
306
|
-
return `<pre class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto mb-4"><code class="text-sm font-mono whitespace-pre">${escapedCode}</code></pre>`;
|
|
307
|
-
},
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
// Handle inline code (after code blocks to avoid conflicts)
|
|
311
|
-
htmlContent = htmlContent.replace(
|
|
312
|
-
/`([^`]+)`/g,
|
|
313
|
-
'<code class="bg-gray-100 text-gray-800 px-1.5 py-0.5 rounded text-sm font-mono">$1</code>',
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
// Handle other markdown elements
|
|
317
|
-
htmlContent = htmlContent
|
|
318
|
-
.replace(/\*\*(.*?)\*\*/g, '<strong class="font-bold">$1</strong>')
|
|
319
|
-
.replace(/\*(.*?)\*/g, '<em class="italic">$1</em>')
|
|
320
|
-
.replace(/<u>(.*?)<\/u>/g, '<u class="underline">$1</u>')
|
|
321
|
-
.replace(/~~(.*?)~~/g, '<s class="line-through">$1</s>')
|
|
322
|
-
.replace(
|
|
323
|
-
/^# (.*$)/gm,
|
|
324
|
-
'<h1 class="text-3xl font-bold mb-4 mt-6 first:mt-0">$1</h1>',
|
|
325
|
-
)
|
|
326
|
-
.replace(
|
|
327
|
-
/^## (.*$)/gm,
|
|
328
|
-
'<h2 class="text-2xl font-bold mb-3 mt-5 first:mt-0">$1</h2>',
|
|
329
|
-
)
|
|
330
|
-
.replace(
|
|
331
|
-
/^### (.*$)/gm,
|
|
332
|
-
'<h3 class="text-xl font-bold mb-2 mt-4 first:mt-0">$1</h3>',
|
|
333
|
-
)
|
|
334
|
-
.replace(
|
|
335
|
-
/^#### (.*$)/gm,
|
|
336
|
-
'<h4 class="text-lg font-bold mb-2 mt-3 first:mt-0">$1</h4>',
|
|
337
|
-
)
|
|
338
|
-
.replace(
|
|
339
|
-
/^- \[ \] (.*$)/gm,
|
|
340
|
-
'<li class="ml-6 mb-1 flex items-center"><input type="checkbox" class="mr-2" disabled> $1</li>',
|
|
341
|
-
)
|
|
342
|
-
.replace(
|
|
343
|
-
/^- \[x\] (.*$)/gm,
|
|
344
|
-
'<li class="ml-6 mb-1 flex items-center"><input type="checkbox" class="mr-2" checked disabled> $1</li>',
|
|
345
|
-
)
|
|
346
|
-
.replace(
|
|
347
|
-
/^- (.*$)/gm,
|
|
348
|
-
'<li class="ml-6 mb-1 list-disc list-inside">$1</li>',
|
|
349
|
-
)
|
|
350
|
-
.replace(
|
|
351
|
-
/^\d+\. (.*$)/gm,
|
|
352
|
-
'<li class="ml-6 mb-1 list-decimal list-inside">$1</li>',
|
|
353
|
-
)
|
|
354
|
-
.replace(
|
|
355
|
-
/^> (.*$)/gm,
|
|
356
|
-
'<blockquote class="border-l-4 border-blue-400 pl-4 py-2 mb-4 bg-blue-50 italic text-gray-700">$1</blockquote>',
|
|
357
|
-
)
|
|
358
|
-
.replace(/^---$/gm, '<hr class="border-t-2 border-gray-300 my-6">')
|
|
359
|
-
.replace(
|
|
360
|
-
/!\[([^\]]*)\]\(([^)]+)\)/g,
|
|
361
|
-
'<img src="$2" alt="$1" class="max-w-full h-auto rounded-lg shadow-sm my-4">',
|
|
362
|
-
)
|
|
363
|
-
.replace(
|
|
364
|
-
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
365
|
-
'<a href="$2" class="text-blue-600 hover:text-blue-800 underline font-medium" target="_blank" rel="noopener noreferrer">$1</a>',
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
// Handle tables
|
|
369
|
-
htmlContent = htmlContent.replace(
|
|
370
|
-
/^\|(.+)\|\n\|(-+\|)+\n((?:\|.+\|\n?)*)/gm,
|
|
371
|
-
(
|
|
372
|
-
_match: string,
|
|
373
|
-
headerRow: string,
|
|
374
|
-
_separatorRow: string,
|
|
375
|
-
bodyRows: string,
|
|
376
|
-
) => {
|
|
377
|
-
const headers: string = headerRow
|
|
378
|
-
.split("|")
|
|
379
|
-
.filter((cell: string) => {
|
|
380
|
-
return cell.trim();
|
|
381
|
-
})
|
|
382
|
-
.map((cell: string) => {
|
|
383
|
-
return `<th class="px-4 py-2 bg-gray-50 font-semibold text-left border-b border-gray-300">${cell.trim()}</th>`;
|
|
384
|
-
})
|
|
385
|
-
.join("");
|
|
386
|
-
|
|
387
|
-
const rows: string = bodyRows
|
|
388
|
-
.split("\n")
|
|
389
|
-
.filter((row: string) => {
|
|
390
|
-
return row.trim();
|
|
391
|
-
})
|
|
392
|
-
.map((row: string) => {
|
|
393
|
-
const cells: string = row
|
|
394
|
-
.split("|")
|
|
395
|
-
.filter((cell: string) => {
|
|
396
|
-
return cell.trim();
|
|
397
|
-
})
|
|
398
|
-
.map((cell: string) => {
|
|
399
|
-
return `<td class="px-4 py-2 border-b border-gray-200">${cell.trim()}</td>`;
|
|
400
|
-
})
|
|
401
|
-
.join("");
|
|
402
|
-
return `<tr>${cells}</tr>`;
|
|
403
|
-
})
|
|
404
|
-
.join("");
|
|
405
|
-
|
|
406
|
-
return `<table class="w-full border-collapse border border-gray-300 my-4 rounded-lg overflow-hidden"><thead><tr>${headers}</tr></thead><tbody>${rows}</tbody></table>`;
|
|
407
|
-
},
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
// Handle line breaks (convert \n to <br> but avoid double breaks)
|
|
411
|
-
htmlContent = htmlContent
|
|
412
|
-
.replace(/\n\n/g, '</p><p class="mb-4">')
|
|
413
|
-
.replace(/\n/g, "<br>");
|
|
414
|
-
|
|
415
|
-
// Wrap in paragraphs if there's content
|
|
416
|
-
if (htmlContent.trim()) {
|
|
417
|
-
htmlContent = `<p class="mb-4">${htmlContent}</p>`;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const sanitizedContent: string = DOMPurify.sanitize(htmlContent);
|
|
421
|
-
|
|
293
|
+
/*
|
|
294
|
+
* Render the preview using the same MarkdownViewer that renders the
|
|
295
|
+
* final published output, so the preview is guaranteed to match.
|
|
296
|
+
*/
|
|
422
297
|
return (
|
|
423
|
-
<div className="p-4 min-h-32 bg-white
|
|
424
|
-
<
|
|
298
|
+
<div className="p-4 min-h-32 bg-white">
|
|
299
|
+
<MarkdownViewer text={text} />
|
|
425
300
|
</div>
|
|
426
301
|
);
|
|
427
302
|
};
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
UserPermission,
|
|
23
23
|
UserTenantAccessPermission,
|
|
24
24
|
} from "../../../Types/Permission";
|
|
25
|
-
import React, { ReactElement, useEffect, useState } from "react";
|
|
25
|
+
import React, { ReactElement, useEffect, useRef, useState } from "react";
|
|
26
26
|
|
|
27
27
|
export interface ComponentProps<TBaseModel extends BaseModel> {
|
|
28
28
|
cardProps: CardProps;
|
|
@@ -40,6 +40,7 @@ export interface ComponentProps<TBaseModel extends BaseModel> {
|
|
|
40
40
|
createOrUpdateApiUrl?: URL | undefined;
|
|
41
41
|
documentationLink?: Route | URL | undefined;
|
|
42
42
|
videoLink?: Route | URL | undefined;
|
|
43
|
+
onBeforeEdit?: (() => boolean) | undefined;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
const CardModelDetail: <TBaseModel extends BaseModel>(
|
|
@@ -55,6 +56,12 @@ const CardModelDetail: <TBaseModel extends BaseModel>(
|
|
|
55
56
|
const [refresher, setRefresher] = useState<boolean>(false);
|
|
56
57
|
const model: TBaseModel = new props.modelDetailProps.modelType();
|
|
57
58
|
|
|
59
|
+
const onBeforeEditRef: React.MutableRefObject<(() => boolean) | undefined> =
|
|
60
|
+
useRef<(() => boolean) | undefined>(props.onBeforeEdit);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
onBeforeEditRef.current = props.onBeforeEdit;
|
|
63
|
+
}, [props.onBeforeEdit]);
|
|
64
|
+
|
|
58
65
|
useEffect(() => {
|
|
59
66
|
setRefresher(!refresher);
|
|
60
67
|
}, [props.refresher]);
|
|
@@ -112,6 +119,9 @@ const CardModelDetail: <TBaseModel extends BaseModel>(
|
|
|
112
119
|
title: props.editButtonText || `Edit ${model.singularName}`,
|
|
113
120
|
buttonStyle: ButtonStyleType.NORMAL,
|
|
114
121
|
onClick: () => {
|
|
122
|
+
if (onBeforeEditRef.current && onBeforeEditRef.current() === false) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
115
125
|
setShowModal(true);
|
|
116
126
|
},
|
|
117
127
|
icon: IconProp.Edit,
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import React, { ReactElement, ReactNode } from "react";
|
|
2
|
+
import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
|
|
3
|
+
import {
|
|
4
|
+
FacetData,
|
|
5
|
+
FacetConfig,
|
|
6
|
+
ActiveFilter,
|
|
7
|
+
HistogramBucket,
|
|
8
|
+
HistogramSeriesOption,
|
|
9
|
+
LiveOptions,
|
|
10
|
+
SearchHelpRow,
|
|
11
|
+
} from "./types";
|
|
12
|
+
import TelemetryTimeRangePicker from "./components/TelemetryTimeRangePicker";
|
|
13
|
+
import TelemetrySearchBar, {
|
|
14
|
+
TelemetrySearchBarRef,
|
|
15
|
+
} from "./components/TelemetrySearchBar";
|
|
16
|
+
import TelemetryFacetSidebar from "./components/TelemetryFacetSidebar";
|
|
17
|
+
import TelemetryActiveFilterChips from "./components/TelemetryActiveFilterChips";
|
|
18
|
+
import TelemetryHistogram from "./components/TelemetryHistogram";
|
|
19
|
+
import TelemetryPagination from "./components/TelemetryPagination";
|
|
20
|
+
import ComponentLoader from "../ComponentLoader/ComponentLoader";
|
|
21
|
+
import ErrorMessage from "../ErrorMessage/ErrorMessage";
|
|
22
|
+
import Icon from "../Icon/Icon";
|
|
23
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
24
|
+
|
|
25
|
+
export interface TelemetryViewerProps<T> {
|
|
26
|
+
// -- Data --
|
|
27
|
+
items: Array<T>;
|
|
28
|
+
isLoading: boolean;
|
|
29
|
+
error?: string | undefined;
|
|
30
|
+
onRefresh?: (() => void) | undefined;
|
|
31
|
+
emptyMessage?: string | undefined;
|
|
32
|
+
|
|
33
|
+
// -- Layout --
|
|
34
|
+
/** Render one item row in the main list. */
|
|
35
|
+
renderRow: (item: T, index: number) => ReactElement;
|
|
36
|
+
/** Unique key accessor per row (used for React keys). */
|
|
37
|
+
getRowKey: (item: T, index: number) => string;
|
|
38
|
+
|
|
39
|
+
// -- Toolbar: search --
|
|
40
|
+
searchValue: string;
|
|
41
|
+
onSearchChange: (value: string) => void;
|
|
42
|
+
onSearchSubmit: () => void;
|
|
43
|
+
searchPlaceholder?: string | undefined;
|
|
44
|
+
searchSuggestions?: Array<string> | undefined;
|
|
45
|
+
searchValueSuggestions?: Record<string, Array<string>> | undefined;
|
|
46
|
+
searchFieldAliasMap?: Record<string, string> | undefined;
|
|
47
|
+
onSearchFieldValueSelect?:
|
|
48
|
+
| ((fieldKey: string, value: string) => void)
|
|
49
|
+
| undefined;
|
|
50
|
+
searchHelpRows?: Array<SearchHelpRow> | undefined;
|
|
51
|
+
searchHelpCombinedExample?: string | undefined;
|
|
52
|
+
searchBarRef?: React.Ref<TelemetrySearchBarRef> | undefined;
|
|
53
|
+
|
|
54
|
+
// -- Toolbar: time --
|
|
55
|
+
timeRange: RangeStartAndEndDateTime;
|
|
56
|
+
onTimeRangeChange: (value: RangeStartAndEndDateTime) => void;
|
|
57
|
+
|
|
58
|
+
// -- Toolbar: live + actions --
|
|
59
|
+
live?: LiveOptions | undefined;
|
|
60
|
+
toolbarLeadingActions?: ReactNode;
|
|
61
|
+
toolbarTrailingActions?: ReactNode;
|
|
62
|
+
|
|
63
|
+
// -- Sidebar facets --
|
|
64
|
+
showFacetSidebar?: boolean;
|
|
65
|
+
facetData?: FacetData | undefined;
|
|
66
|
+
facetConfigs?: Array<FacetConfig> | undefined;
|
|
67
|
+
facetLoading?: boolean;
|
|
68
|
+
onFacetInclude?: ((facetKey: string, value: string) => void) | undefined;
|
|
69
|
+
onFacetExclude?: ((facetKey: string, value: string) => void) | undefined;
|
|
70
|
+
|
|
71
|
+
// -- Active filters --
|
|
72
|
+
activeFilters?: Array<ActiveFilter> | undefined;
|
|
73
|
+
onRemoveFilter?: ((facetKey: string, value: string) => void) | undefined;
|
|
74
|
+
onClearAllFilters?: (() => void) | undefined;
|
|
75
|
+
|
|
76
|
+
// -- Histogram --
|
|
77
|
+
showHistogram?: boolean;
|
|
78
|
+
histogramBuckets?: Array<HistogramBucket> | undefined;
|
|
79
|
+
histogramSeries?: Array<HistogramSeriesOption> | undefined;
|
|
80
|
+
histogramTitle?: string | undefined;
|
|
81
|
+
histogramLoading?: boolean;
|
|
82
|
+
onHistogramTimeRangeSelect?:
|
|
83
|
+
| ((startTime: Date, endTime: Date) => void)
|
|
84
|
+
| undefined;
|
|
85
|
+
|
|
86
|
+
// -- Pagination --
|
|
87
|
+
page: number;
|
|
88
|
+
pageSize: number;
|
|
89
|
+
totalCount: number;
|
|
90
|
+
pageSizeOptions?: Array<number> | undefined;
|
|
91
|
+
onPageChange: (page: number) => void;
|
|
92
|
+
onPageSizeChange: (size: number) => void;
|
|
93
|
+
itemLabel?: string | undefined;
|
|
94
|
+
|
|
95
|
+
// -- Detail panel overlay (rendered by caller above everything) --
|
|
96
|
+
detailPanel?: ReactNode;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const DEFAULT_PAGE_SIZE_OPTIONS: Array<number> = [25, 50, 100, 200];
|
|
100
|
+
|
|
101
|
+
function TelemetryViewerInner<T>(props: TelemetryViewerProps<T>): ReactElement {
|
|
102
|
+
const showFacets: boolean =
|
|
103
|
+
(props.showFacetSidebar ?? true) &&
|
|
104
|
+
props.facetConfigs !== undefined &&
|
|
105
|
+
props.facetConfigs.length > 0;
|
|
106
|
+
|
|
107
|
+
const showHistogram: boolean = props.showHistogram ?? true;
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="flex h-full w-full flex-col gap-3">
|
|
111
|
+
{/* Toolbar */}
|
|
112
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
113
|
+
<div className="min-w-0 flex-1">
|
|
114
|
+
<TelemetrySearchBar
|
|
115
|
+
ref={props.searchBarRef}
|
|
116
|
+
value={props.searchValue}
|
|
117
|
+
onChange={props.onSearchChange}
|
|
118
|
+
onSubmit={props.onSearchSubmit}
|
|
119
|
+
placeholder={props.searchPlaceholder}
|
|
120
|
+
suggestions={props.searchSuggestions}
|
|
121
|
+
valueSuggestions={props.searchValueSuggestions}
|
|
122
|
+
fieldAliasMap={props.searchFieldAliasMap}
|
|
123
|
+
onFieldValueSelect={props.onSearchFieldValueSelect}
|
|
124
|
+
helpRows={props.searchHelpRows}
|
|
125
|
+
helpCombinedExample={props.searchHelpCombinedExample}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{props.toolbarLeadingActions}
|
|
130
|
+
|
|
131
|
+
<TelemetryTimeRangePicker
|
|
132
|
+
value={props.timeRange}
|
|
133
|
+
onChange={props.onTimeRangeChange}
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
{props.live && (
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
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 ${
|
|
140
|
+
props.live.isLive
|
|
141
|
+
? "border-emerald-300 bg-emerald-50 text-emerald-700"
|
|
142
|
+
: "border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50"
|
|
143
|
+
} ${props.live.isDisabled ? "cursor-not-allowed opacity-50" : ""}`}
|
|
144
|
+
disabled={props.live.isDisabled}
|
|
145
|
+
onClick={() => {
|
|
146
|
+
props.live?.onToggle(!props.live.isLive);
|
|
147
|
+
}}
|
|
148
|
+
title={
|
|
149
|
+
props.live.isLive ? "Pause live updates" : "Enable live updates"
|
|
150
|
+
}
|
|
151
|
+
>
|
|
152
|
+
<span
|
|
153
|
+
className={`h-2 w-2 rounded-full ${
|
|
154
|
+
props.live.isLive
|
|
155
|
+
? "animate-pulse bg-emerald-500"
|
|
156
|
+
: "bg-gray-300"
|
|
157
|
+
}`}
|
|
158
|
+
/>
|
|
159
|
+
<span>{props.live.isLive ? "Live" : "Paused"}</span>
|
|
160
|
+
</button>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
{props.onRefresh && (
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
className="inline-flex items-center gap-1.5 rounded-md border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm transition-colors hover:border-gray-300 hover:bg-gray-50"
|
|
167
|
+
onClick={props.onRefresh}
|
|
168
|
+
title="Refresh"
|
|
169
|
+
>
|
|
170
|
+
<Icon icon={IconProp.Refresh} className="h-3.5 w-3.5" />
|
|
171
|
+
<span>Refresh</span>
|
|
172
|
+
</button>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{props.toolbarTrailingActions}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* Active filter chips */}
|
|
179
|
+
{props.activeFilters && props.activeFilters.length > 0 && (
|
|
180
|
+
<TelemetryActiveFilterChips
|
|
181
|
+
filters={props.activeFilters}
|
|
182
|
+
onRemove={(key: string, value: string) => {
|
|
183
|
+
props.onRemoveFilter?.(key, value);
|
|
184
|
+
}}
|
|
185
|
+
onClearAll={() => {
|
|
186
|
+
props.onClearAllFilters?.();
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{/* Histogram */}
|
|
192
|
+
{showHistogram && props.histogramBuckets && props.histogramSeries && (
|
|
193
|
+
<TelemetryHistogram
|
|
194
|
+
buckets={props.histogramBuckets}
|
|
195
|
+
isLoading={props.histogramLoading || false}
|
|
196
|
+
series={props.histogramSeries}
|
|
197
|
+
title={props.histogramTitle}
|
|
198
|
+
onTimeRangeSelect={props.onHistogramTimeRangeSelect}
|
|
199
|
+
/>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{/* Main area: facets + list */}
|
|
203
|
+
<div className="flex min-h-0 flex-1 gap-3">
|
|
204
|
+
{showFacets && (
|
|
205
|
+
<TelemetryFacetSidebar
|
|
206
|
+
facetData={props.facetData || {}}
|
|
207
|
+
isLoading={props.facetLoading || false}
|
|
208
|
+
facetConfigs={props.facetConfigs || []}
|
|
209
|
+
activeFilters={props.activeFilters}
|
|
210
|
+
onIncludeFilter={(key: string, value: string) => {
|
|
211
|
+
props.onFacetInclude?.(key, value);
|
|
212
|
+
}}
|
|
213
|
+
onExcludeFilter={(key: string, value: string) => {
|
|
214
|
+
props.onFacetExclude?.(key, value);
|
|
215
|
+
}}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
<div className="flex min-w-0 flex-1 flex-col rounded-lg border border-gray-200 bg-white">
|
|
220
|
+
{props.error && (
|
|
221
|
+
<div className="p-4">
|
|
222
|
+
<ErrorMessage message={props.error} />
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
{!props.error && (
|
|
227
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
228
|
+
{props.isLoading && props.items.length === 0 ? (
|
|
229
|
+
<div className="flex h-48 items-center justify-center">
|
|
230
|
+
<ComponentLoader />
|
|
231
|
+
</div>
|
|
232
|
+
) : props.items.length === 0 ? (
|
|
233
|
+
<div className="flex h-48 flex-col items-center justify-center gap-2 px-6 text-center">
|
|
234
|
+
<Icon
|
|
235
|
+
icon={IconProp.Search}
|
|
236
|
+
className="h-8 w-8 text-gray-300"
|
|
237
|
+
/>
|
|
238
|
+
<p className="text-sm font-medium text-gray-500">
|
|
239
|
+
{props.emptyMessage || "No results"}
|
|
240
|
+
</p>
|
|
241
|
+
<p className="text-xs text-gray-400">
|
|
242
|
+
Try adjusting filters or time range.
|
|
243
|
+
</p>
|
|
244
|
+
</div>
|
|
245
|
+
) : (
|
|
246
|
+
<ul className="divide-y divide-gray-100">
|
|
247
|
+
{props.items.map((item: T, index: number) => {
|
|
248
|
+
return (
|
|
249
|
+
<li key={props.getRowKey(item, index)}>
|
|
250
|
+
{props.renderRow(item, index)}
|
|
251
|
+
</li>
|
|
252
|
+
);
|
|
253
|
+
})}
|
|
254
|
+
</ul>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
|
|
259
|
+
<TelemetryPagination
|
|
260
|
+
currentPage={props.page}
|
|
261
|
+
totalItems={props.totalCount}
|
|
262
|
+
pageSize={props.pageSize}
|
|
263
|
+
pageSizeOptions={props.pageSizeOptions || DEFAULT_PAGE_SIZE_OPTIONS}
|
|
264
|
+
onPageChange={props.onPageChange}
|
|
265
|
+
onPageSizeChange={props.onPageSizeChange}
|
|
266
|
+
isDisabled={props.isLoading}
|
|
267
|
+
itemLabel={props.itemLabel}
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{/* Detail panel overlay */}
|
|
273
|
+
{props.detailPanel}
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Generic functional component wrapper so TypeScript can infer <T>.
|
|
279
|
+
const TelemetryViewer: <T>(props: TelemetryViewerProps<T>) => ReactElement =
|
|
280
|
+
TelemetryViewerInner as unknown as <T>(
|
|
281
|
+
props: TelemetryViewerProps<T>,
|
|
282
|
+
) => ReactElement;
|
|
283
|
+
|
|
284
|
+
export default TelemetryViewer;
|
|
285
|
+
export type { TelemetrySearchBarRef } from "./components/TelemetrySearchBar";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
import { ActiveFilter } from "../types";
|
|
3
|
+
import Icon from "../../Icon/Icon";
|
|
4
|
+
import IconProp from "../../../../Types/Icon/IconProp";
|
|
5
|
+
|
|
6
|
+
export interface TelemetryActiveFilterChipsProps {
|
|
7
|
+
filters: Array<ActiveFilter>;
|
|
8
|
+
onRemove: (facetKey: string, value: string) => void;
|
|
9
|
+
onClearAll: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const TelemetryActiveFilterChips: FunctionComponent<
|
|
13
|
+
TelemetryActiveFilterChipsProps
|
|
14
|
+
> = (props: TelemetryActiveFilterChipsProps): ReactElement | null => {
|
|
15
|
+
if (props.filters.length === 0) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const readOnlyFilters: Array<ActiveFilter> = props.filters.filter(
|
|
20
|
+
(f: ActiveFilter) => {
|
|
21
|
+
return f.readOnly;
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
const removableFilters: Array<ActiveFilter> = props.filters.filter(
|
|
25
|
+
(f: ActiveFilter) => {
|
|
26
|
+
return !f.readOnly;
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex flex-wrap items-center gap-1.5 px-0.5">
|
|
32
|
+
{readOnlyFilters.map((filter: ActiveFilter) => {
|
|
33
|
+
const chipKey: string = `readonly:${filter.facetKey}:${filter.value}`;
|
|
34
|
+
return (
|
|
35
|
+
<span
|
|
36
|
+
key={chipKey}
|
|
37
|
+
className="inline-flex items-center gap-1 rounded-md border border-gray-300 bg-gray-100 py-0.5 pl-2 pr-2 text-xs text-gray-700"
|
|
38
|
+
title={`${filter.displayKey}: ${filter.displayValue} (applied filter)`}
|
|
39
|
+
>
|
|
40
|
+
<Icon icon={IconProp.Lock} className="h-2.5 w-2.5 text-gray-400" />
|
|
41
|
+
<span className="font-medium text-gray-500">
|
|
42
|
+
{filter.displayKey}:
|
|
43
|
+
</span>
|
|
44
|
+
<span>{filter.displayValue}</span>
|
|
45
|
+
</span>
|
|
46
|
+
);
|
|
47
|
+
})}
|
|
48
|
+
{removableFilters.map((filter: ActiveFilter) => {
|
|
49
|
+
const chipKey: string = `${filter.facetKey}:${filter.value}`;
|
|
50
|
+
return (
|
|
51
|
+
<span
|
|
52
|
+
key={chipKey}
|
|
53
|
+
className="inline-flex items-center gap-1 rounded-md border border-indigo-200 bg-indigo-50 py-0.5 pl-2 pr-1 text-xs text-indigo-700"
|
|
54
|
+
>
|
|
55
|
+
<span className="font-medium text-indigo-500">
|
|
56
|
+
{filter.displayKey}:
|
|
57
|
+
</span>
|
|
58
|
+
<span>{filter.displayValue}</span>
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className="ml-0.5 inline-flex h-4 w-4 items-center justify-center rounded text-indigo-400 transition-colors hover:bg-indigo-100 hover:text-indigo-600"
|
|
62
|
+
onClick={() => {
|
|
63
|
+
props.onRemove(filter.facetKey, filter.value);
|
|
64
|
+
}}
|
|
65
|
+
title={`Remove ${filter.displayKey}: ${filter.displayValue}`}
|
|
66
|
+
>
|
|
67
|
+
<Icon icon={IconProp.Close} className="h-2.5 w-2.5" />
|
|
68
|
+
</button>
|
|
69
|
+
</span>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
{removableFilters.length > 1 && (
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
className="rounded px-1.5 py-0.5 text-[11px] font-medium text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
|
|
76
|
+
onClick={props.onClearAll}
|
|
77
|
+
>
|
|
78
|
+
Clear all
|
|
79
|
+
</button>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default TelemetryActiveFilterChips;
|