@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.
Files changed (132) hide show
  1. package/Models/DatabaseModels/DockerHost.ts +662 -0
  2. package/Models/DatabaseModels/GlobalConfig.ts +112 -0
  3. package/Models/DatabaseModels/Index.ts +2 -0
  4. package/Server/API/TelemetryAPI.ts +352 -16
  5. package/Server/Infrastructure/ClickhouseConfig.ts +9 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.ts +76 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.ts +133 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.ts +51 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  10. package/Server/Services/DockerHostService.ts +173 -0
  11. package/Server/Services/ExceptionAggregationService.ts +335 -0
  12. package/Server/Services/Index.ts +2 -0
  13. package/Server/Services/LogAggregationService.ts +17 -0
  14. package/Server/Services/MonitorProbeService.ts +42 -21
  15. package/Server/Services/MonitorService.ts +21 -21
  16. package/Server/Services/TraceAggregationService.ts +514 -0
  17. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +73 -1
  18. package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
  19. package/Tests/__mocks__/mermaid.js +18 -0
  20. package/Tests/__mocks__/react-markdown.js +17 -0
  21. package/Tests/__mocks__/react-syntax-highlighter.js +19 -0
  22. package/Tests/__mocks__/remark-gfm.js +8 -0
  23. package/Types/Icon/IconProp.ts +1 -0
  24. package/Types/Monitor/DockerAlertTemplates.ts +507 -0
  25. package/Types/Monitor/DockerMetricCatalog.ts +226 -0
  26. package/Types/Monitor/MonitorStep.ts +33 -0
  27. package/Types/Monitor/MonitorStepDockerMonitor.ts +38 -0
  28. package/Types/Monitor/MonitorType.ts +15 -1
  29. package/Types/Permission.ts +38 -0
  30. package/UI/Components/Icon/Icon.tsx +87 -0
  31. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +7 -132
  32. package/UI/Components/ModelDetail/CardModelDetail.tsx +11 -1
  33. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +285 -0
  34. package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +85 -0
  35. package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +156 -0
  36. package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +160 -0
  37. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +85 -0
  38. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +102 -0
  39. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +280 -0
  40. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +125 -0
  41. package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +114 -0
  42. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +378 -0
  43. package/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.tsx +78 -0
  44. package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +64 -0
  45. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +193 -0
  46. package/UI/Components/TelemetryViewer/types.ts +67 -0
  47. package/build/dist/Models/DatabaseModels/DockerHost.js +686 -0
  48. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -0
  49. package/build/dist/Models/DatabaseModels/GlobalConfig.js +117 -0
  50. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  51. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  52. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  53. package/build/dist/Server/API/TelemetryAPI.js +237 -16
  54. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  55. package/build/dist/Server/Infrastructure/ClickhouseConfig.js +9 -0
  56. package/build/dist/Server/Infrastructure/ClickhouseConfig.js.map +1 -1
  57. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.js +35 -0
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.js.map +1 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.js +52 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.js.map +1 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.js +26 -0
  62. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.js.map +1 -0
  63. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  64. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  65. package/build/dist/Server/Services/DockerHostService.js +162 -0
  66. package/build/dist/Server/Services/DockerHostService.js.map +1 -0
  67. package/build/dist/Server/Services/ExceptionAggregationService.js +224 -0
  68. package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -0
  69. package/build/dist/Server/Services/Index.js +2 -0
  70. package/build/dist/Server/Services/Index.js.map +1 -1
  71. package/build/dist/Server/Services/LogAggregationService.js +11 -0
  72. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  73. package/build/dist/Server/Services/MonitorProbeService.js +28 -14
  74. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  75. package/build/dist/Server/Services/MonitorService.js +19 -17
  76. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  77. package/build/dist/Server/Services/TraceAggregationService.js +364 -0
  78. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -0
  79. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +46 -1
  80. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  81. package/build/dist/Tests/Server/Services/LogAggregationService.test.js +2 -2
  82. package/build/dist/Tests/Server/Services/LogAggregationService.test.js.map +1 -1
  83. package/build/dist/Types/Icon/IconProp.js +1 -0
  84. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  85. package/build/dist/Types/Monitor/DockerAlertTemplates.js +410 -0
  86. package/build/dist/Types/Monitor/DockerAlertTemplates.js.map +1 -0
  87. package/build/dist/Types/Monitor/DockerMetricCatalog.js +192 -0
  88. package/build/dist/Types/Monitor/DockerMetricCatalog.js.map +1 -0
  89. package/build/dist/Types/Monitor/MonitorStep.js +23 -0
  90. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  91. package/build/dist/Types/Monitor/MonitorStepDockerMonitor.js +21 -0
  92. package/build/dist/Types/Monitor/MonitorStepDockerMonitor.js.map +1 -0
  93. package/build/dist/Types/Monitor/MonitorType.js +14 -1
  94. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  95. package/build/dist/Types/Permission.js +36 -0
  96. package/build/dist/Types/Permission.js.map +1 -1
  97. package/build/dist/UI/Components/Icon/Icon.js +13 -0
  98. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  99. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +7 -75
  100. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
  101. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +8 -1
  102. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  103. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +71 -0
  104. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -0
  105. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +39 -0
  106. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -0
  107. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +61 -0
  108. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -0
  109. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +66 -0
  110. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -0
  111. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +41 -0
  112. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -0
  113. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +35 -0
  114. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -0
  115. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +132 -0
  116. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -0
  117. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +65 -0
  118. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -0
  119. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +52 -0
  120. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -0
  121. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +224 -0
  122. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -0
  123. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.js +35 -0
  124. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.js.map +1 -0
  125. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +27 -0
  126. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -0
  127. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +97 -0
  128. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -0
  129. package/build/dist/UI/Components/TelemetryViewer/types.js +6 -0
  130. package/build/dist/UI/Components/TelemetryViewer/types.js.map +1 -0
  131. package/jest.config.json +6 -1
  132. 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 DOMPurify from "dompurify";
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
- // Enhanced markdown preview with proper code block handling
294
- let htmlContent: string = text;
295
-
296
- // Handle code blocks first (before inline code)
297
- htmlContent = htmlContent.replace(
298
- /```([^`]*?)```/g,
299
- (_match: string, code: string) => {
300
- const escapedCode: string = code
301
- .replace(/&/g, "&amp;")
302
- .replace(/</g, "&lt;")
303
- .replace(/>/g, "&gt;")
304
- .replace(/"/g, "&quot;")
305
- .replace(/'/g, "&#39;");
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 prose prose-sm max-w-none">
424
- <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
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;