@oneuptime/common 10.0.55 → 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 (129) 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/MonitorService.ts +21 -21
  15. package/Server/Services/TraceAggregationService.ts +514 -0
  16. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +73 -1
  17. package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
  18. package/Tests/__mocks__/mermaid.js +18 -0
  19. package/Tests/__mocks__/react-markdown.js +17 -0
  20. package/Tests/__mocks__/react-syntax-highlighter.js +19 -0
  21. package/Tests/__mocks__/remark-gfm.js +8 -0
  22. package/Types/Icon/IconProp.ts +1 -0
  23. package/Types/Monitor/DockerAlertTemplates.ts +507 -0
  24. package/Types/Monitor/DockerMetricCatalog.ts +226 -0
  25. package/Types/Monitor/MonitorStep.ts +33 -0
  26. package/Types/Monitor/MonitorStepDockerMonitor.ts +38 -0
  27. package/Types/Monitor/MonitorType.ts +15 -1
  28. package/Types/Permission.ts +38 -0
  29. package/UI/Components/Icon/Icon.tsx +87 -0
  30. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +7 -132
  31. package/UI/Components/ModelDetail/CardModelDetail.tsx +11 -1
  32. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +285 -0
  33. package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +85 -0
  34. package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +156 -0
  35. package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +160 -0
  36. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +85 -0
  37. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +102 -0
  38. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +280 -0
  39. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +125 -0
  40. package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +114 -0
  41. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +378 -0
  42. package/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.tsx +78 -0
  43. package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +64 -0
  44. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +193 -0
  45. package/UI/Components/TelemetryViewer/types.ts +67 -0
  46. package/build/dist/Models/DatabaseModels/DockerHost.js +686 -0
  47. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -0
  48. package/build/dist/Models/DatabaseModels/GlobalConfig.js +117 -0
  49. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  50. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  51. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  52. package/build/dist/Server/API/TelemetryAPI.js +237 -16
  53. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  54. package/build/dist/Server/Infrastructure/ClickhouseConfig.js +9 -0
  55. package/build/dist/Server/Infrastructure/ClickhouseConfig.js.map +1 -1
  56. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.js +35 -0
  57. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1774000000002-MigrationName.js.map +1 -0
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.js +52 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775766676723-MigrationName.js.map +1 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.js +26 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1775900000000-AddGlobalSmtpOAuth.js.map +1 -0
  62. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  63. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  64. package/build/dist/Server/Services/DockerHostService.js +162 -0
  65. package/build/dist/Server/Services/DockerHostService.js.map +1 -0
  66. package/build/dist/Server/Services/ExceptionAggregationService.js +224 -0
  67. package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -0
  68. package/build/dist/Server/Services/Index.js +2 -0
  69. package/build/dist/Server/Services/Index.js.map +1 -1
  70. package/build/dist/Server/Services/LogAggregationService.js +11 -0
  71. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  72. package/build/dist/Server/Services/MonitorService.js +19 -17
  73. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  74. package/build/dist/Server/Services/TraceAggregationService.js +364 -0
  75. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -0
  76. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +46 -1
  77. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  78. package/build/dist/Tests/Server/Services/LogAggregationService.test.js +2 -2
  79. package/build/dist/Tests/Server/Services/LogAggregationService.test.js.map +1 -1
  80. package/build/dist/Types/Icon/IconProp.js +1 -0
  81. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  82. package/build/dist/Types/Monitor/DockerAlertTemplates.js +410 -0
  83. package/build/dist/Types/Monitor/DockerAlertTemplates.js.map +1 -0
  84. package/build/dist/Types/Monitor/DockerMetricCatalog.js +192 -0
  85. package/build/dist/Types/Monitor/DockerMetricCatalog.js.map +1 -0
  86. package/build/dist/Types/Monitor/MonitorStep.js +23 -0
  87. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  88. package/build/dist/Types/Monitor/MonitorStepDockerMonitor.js +21 -0
  89. package/build/dist/Types/Monitor/MonitorStepDockerMonitor.js.map +1 -0
  90. package/build/dist/Types/Monitor/MonitorType.js +14 -1
  91. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  92. package/build/dist/Types/Permission.js +36 -0
  93. package/build/dist/Types/Permission.js.map +1 -1
  94. package/build/dist/UI/Components/Icon/Icon.js +13 -0
  95. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  96. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +7 -75
  97. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
  98. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +8 -1
  99. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  100. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +71 -0
  101. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -0
  102. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +39 -0
  103. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -0
  104. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +61 -0
  105. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -0
  106. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +66 -0
  107. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -0
  108. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +41 -0
  109. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -0
  110. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +35 -0
  111. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -0
  112. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +132 -0
  113. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -0
  114. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +65 -0
  115. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -0
  116. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +52 -0
  117. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -0
  118. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +224 -0
  119. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -0
  120. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.js +35 -0
  121. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchHelp.js.map +1 -0
  122. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +27 -0
  123. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -0
  124. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +97 -0
  125. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -0
  126. package/build/dist/UI/Components/TelemetryViewer/types.js +6 -0
  127. package/build/dist/UI/Components/TelemetryViewer/types.js.map +1 -0
  128. package/jest.config.json +6 -1
  129. package/package.json +1 -1
@@ -0,0 +1,156 @@
1
+ import React, {
2
+ FunctionComponent,
3
+ ReactElement,
4
+ ReactNode,
5
+ useEffect,
6
+ } from "react";
7
+ import Icon from "../../Icon/Icon";
8
+ import IconProp from "../../../../Types/Icon/IconProp";
9
+
10
+ export interface TelemetryDetailPanelTab {
11
+ id: string;
12
+ label: string;
13
+ content: ReactNode;
14
+ badge?: string | number | undefined;
15
+ }
16
+
17
+ export interface TelemetryDetailPanelProps {
18
+ isOpen: boolean;
19
+ title: ReactNode;
20
+ subtitle?: ReactNode;
21
+ onClose: () => void;
22
+ tabs: Array<TelemetryDetailPanelTab>;
23
+ activeTabId: string;
24
+ onTabChange: (tabId: string) => void;
25
+ headerActions?: ReactNode;
26
+ variant?: "floating" | "embedded";
27
+ widthClassName?: string;
28
+ }
29
+
30
+ const TelemetryDetailPanel: FunctionComponent<TelemetryDetailPanelProps> = (
31
+ props: TelemetryDetailPanelProps,
32
+ ): ReactElement | null => {
33
+ // Close on Escape
34
+ useEffect(() => {
35
+ if (!props.isOpen) {
36
+ return;
37
+ }
38
+ const handler: (e: KeyboardEvent) => void = (e: KeyboardEvent): void => {
39
+ if (e.key === "Escape") {
40
+ props.onClose();
41
+ }
42
+ };
43
+ document.addEventListener("keydown", handler);
44
+ return () => {
45
+ document.removeEventListener("keydown", handler);
46
+ };
47
+ }, [props.isOpen, props.onClose]);
48
+
49
+ if (!props.isOpen) {
50
+ return null;
51
+ }
52
+
53
+ const variant: "floating" | "embedded" = props.variant || "floating";
54
+ const widthClassName: string = props.widthClassName || "w-[38rem]";
55
+
56
+ const activeTab: TelemetryDetailPanelTab | undefined = props.tabs.find(
57
+ (t: TelemetryDetailPanelTab) => {
58
+ return t.id === props.activeTabId;
59
+ },
60
+ );
61
+
62
+ const body: ReactElement = (
63
+ <div
64
+ className={`flex h-full flex-col bg-white ${
65
+ variant === "floating"
66
+ ? `fixed right-0 top-0 z-40 ${widthClassName} border-l border-gray-200 shadow-2xl`
67
+ : "rounded-lg border border-gray-200"
68
+ }`}
69
+ >
70
+ {/* Header */}
71
+ <div className="flex items-start justify-between gap-3 border-b border-gray-100 px-4 py-3">
72
+ <div className="min-w-0 flex-1">
73
+ <div className="truncate text-sm font-semibold text-gray-900">
74
+ {props.title}
75
+ </div>
76
+ {props.subtitle && (
77
+ <div className="mt-0.5 truncate text-xs text-gray-500">
78
+ {props.subtitle}
79
+ </div>
80
+ )}
81
+ </div>
82
+ <div className="flex items-center gap-1">
83
+ {props.headerActions}
84
+ <button
85
+ type="button"
86
+ className="rounded-md p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
87
+ onClick={props.onClose}
88
+ title="Close (Esc)"
89
+ >
90
+ <Icon icon={IconProp.Close} className="h-4 w-4" />
91
+ </button>
92
+ </div>
93
+ </div>
94
+
95
+ {/* Tabs */}
96
+ {props.tabs.length > 1 && (
97
+ <div className="flex items-center gap-1 border-b border-gray-100 px-2 pt-1.5">
98
+ {props.tabs.map((tab: TelemetryDetailPanelTab) => {
99
+ const isActive: boolean = tab.id === props.activeTabId;
100
+ return (
101
+ <button
102
+ key={tab.id}
103
+ type="button"
104
+ className={`relative inline-flex items-center gap-1.5 rounded-t-md px-3 py-1.5 text-xs font-medium transition-colors ${
105
+ isActive
106
+ ? "text-indigo-700"
107
+ : "text-gray-500 hover:text-gray-700"
108
+ }`}
109
+ onClick={() => {
110
+ props.onTabChange(tab.id);
111
+ }}
112
+ >
113
+ {tab.label}
114
+ {tab.badge !== undefined && tab.badge !== null && (
115
+ <span
116
+ className={`inline-flex h-4 min-w-[1rem] items-center justify-center rounded-full px-1 text-[10px] font-semibold ${
117
+ isActive
118
+ ? "bg-indigo-100 text-indigo-700"
119
+ : "bg-gray-100 text-gray-500"
120
+ }`}
121
+ >
122
+ {tab.badge}
123
+ </span>
124
+ )}
125
+ {isActive && (
126
+ <span className="absolute inset-x-2 bottom-0 h-0.5 rounded-t bg-indigo-500" />
127
+ )}
128
+ </button>
129
+ );
130
+ })}
131
+ </div>
132
+ )}
133
+
134
+ {/* Body */}
135
+ <div className="flex-1 overflow-y-auto">{activeTab?.content}</div>
136
+ </div>
137
+ );
138
+
139
+ if (variant === "floating") {
140
+ return (
141
+ <>
142
+ {/* Click-outside backdrop (transparent) */}
143
+ <div
144
+ className="fixed inset-0 z-30"
145
+ onClick={props.onClose}
146
+ aria-hidden="true"
147
+ />
148
+ {body}
149
+ </>
150
+ );
151
+ }
152
+
153
+ return body;
154
+ };
155
+
156
+ export default TelemetryDetailPanel;
@@ -0,0 +1,160 @@
1
+ import React, {
2
+ FunctionComponent,
3
+ ReactElement,
4
+ useState,
5
+ useMemo,
6
+ } from "react";
7
+ import { FacetValue } from "../types";
8
+ import TelemetryFacetValueRow from "./TelemetryFacetValueRow";
9
+ import Icon from "../../Icon/Icon";
10
+ import IconProp from "../../../../Types/Icon/IconProp";
11
+
12
+ export interface TelemetryFacetSectionProps {
13
+ title: string;
14
+ values: Array<FacetValue>;
15
+ initialVisibleCount?: number;
16
+ onIncludeValue: (key: string, value: string) => void;
17
+ onExcludeValue: (key: string, value: string) => void;
18
+ facetKey: string;
19
+ valueDisplayMap?: Record<string, string> | undefined;
20
+ valueColorMap?: Record<string, string> | undefined;
21
+ activeValues?: Set<string> | undefined;
22
+ defaultExpanded?: boolean;
23
+ }
24
+
25
+ const DEFAULT_VISIBLE_COUNT: number = 5;
26
+ const SEARCH_THRESHOLD: number = 6;
27
+
28
+ const TelemetryFacetSection: FunctionComponent<TelemetryFacetSectionProps> = (
29
+ props: TelemetryFacetSectionProps,
30
+ ): ReactElement => {
31
+ const [isExpanded, setIsExpanded] = useState<boolean>(
32
+ props.defaultExpanded !== false,
33
+ );
34
+ const [showAll, setShowAll] = useState<boolean>(false);
35
+ const [searchText, setSearchText] = useState<string>("");
36
+
37
+ const showSearch: boolean = props.values.length >= SEARCH_THRESHOLD;
38
+
39
+ const filteredValues: Array<FacetValue> = useMemo(() => {
40
+ if (!searchText.trim()) {
41
+ return props.values;
42
+ }
43
+ const query: string = searchText.toLowerCase().trim();
44
+ return props.values.filter((facet: FacetValue) => {
45
+ const displayName: string =
46
+ props.valueDisplayMap?.[facet.value] ?? facet.value;
47
+ return displayName.toLowerCase().includes(query);
48
+ });
49
+ }, [props.values, props.valueDisplayMap, searchText]);
50
+
51
+ const visibleCount: number =
52
+ props.initialVisibleCount ?? DEFAULT_VISIBLE_COUNT;
53
+
54
+ const displayedValues: Array<FacetValue> = searchText.trim()
55
+ ? filteredValues
56
+ : showAll
57
+ ? filteredValues
58
+ : filteredValues.slice(0, visibleCount);
59
+
60
+ const hasMore: boolean =
61
+ !searchText.trim() && filteredValues.length > visibleCount;
62
+
63
+ const maxCount: number =
64
+ props.values.length > 0
65
+ ? Math.max(
66
+ ...props.values.map((v: FacetValue) => {
67
+ return v.count;
68
+ }),
69
+ )
70
+ : 0;
71
+
72
+ const activeCount: number = props.activeValues ? props.activeValues.size : 0;
73
+
74
+ return (
75
+ <div className="border-b border-gray-100 py-2">
76
+ <button
77
+ type="button"
78
+ className="flex w-full items-center justify-between px-2 py-1 text-left"
79
+ onClick={() => {
80
+ setIsExpanded(!isExpanded);
81
+ }}
82
+ >
83
+ <div className="flex items-center gap-1.5">
84
+ <span className="text-[11px] font-semibold uppercase tracking-wider text-gray-500">
85
+ {props.title}
86
+ </span>
87
+ {activeCount > 0 && (
88
+ <span className="inline-flex h-4 min-w-[1rem] items-center justify-center rounded-full bg-indigo-100 px-1 text-[10px] font-semibold text-indigo-600">
89
+ {activeCount}
90
+ </span>
91
+ )}
92
+ </div>
93
+ <Icon
94
+ icon={isExpanded ? IconProp.ChevronDown : IconProp.ChevronRight}
95
+ className="h-3 w-3 text-gray-400"
96
+ />
97
+ </button>
98
+
99
+ {isExpanded && (
100
+ <div className="mt-1 px-1">
101
+ {showSearch && (
102
+ <div className="mb-1 px-1">
103
+ <input
104
+ type="text"
105
+ placeholder={`Search ${props.title.toLowerCase()}...`}
106
+ value={searchText}
107
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
108
+ setSearchText(e.target.value);
109
+ }}
110
+ className="w-full rounded border border-gray-200 bg-gray-50 px-2 py-1 text-[11px] text-gray-700 placeholder-gray-400 outline-none focus:border-indigo-300 focus:bg-white focus:ring-1 focus:ring-indigo-200"
111
+ />
112
+ </div>
113
+ )}
114
+
115
+ {displayedValues.map((facet: FacetValue) => {
116
+ return (
117
+ <TelemetryFacetValueRow
118
+ key={facet.value}
119
+ value={facet.value}
120
+ displayValue={props.valueDisplayMap?.[facet.value]}
121
+ count={facet.count}
122
+ maxCount={maxCount}
123
+ color={props.valueColorMap?.[facet.value]}
124
+ isActive={props.activeValues?.has(facet.value) || false}
125
+ onInclude={(value: string) => {
126
+ props.onIncludeValue(props.facetKey, value);
127
+ }}
128
+ onExclude={(value: string) => {
129
+ props.onExcludeValue(props.facetKey, value);
130
+ }}
131
+ />
132
+ );
133
+ })}
134
+
135
+ {displayedValues.length === 0 && (
136
+ <p className="px-1 py-2 text-[11px] text-gray-400">
137
+ {searchText.trim() ? "No matches found" : "No values found"}
138
+ </p>
139
+ )}
140
+
141
+ {hasMore && (
142
+ <button
143
+ type="button"
144
+ className="mt-1 px-1 text-[11px] font-medium text-indigo-500 hover:text-indigo-600"
145
+ onClick={() => {
146
+ setShowAll(!showAll);
147
+ }}
148
+ >
149
+ {showAll
150
+ ? "Show less"
151
+ : `+${props.values.length - visibleCount} more`}
152
+ </button>
153
+ )}
154
+ </div>
155
+ )}
156
+ </div>
157
+ );
158
+ };
159
+
160
+ export default TelemetryFacetSection;
@@ -0,0 +1,85 @@
1
+ import React, { FunctionComponent, ReactElement, useMemo } from "react";
2
+ import { FacetData, FacetValue, ActiveFilter, FacetConfig } from "../types";
3
+ import TelemetryFacetSection from "./TelemetryFacetSection";
4
+ import ComponentLoader from "../../ComponentLoader/ComponentLoader";
5
+
6
+ export interface TelemetryFacetSidebarProps {
7
+ facetData: FacetData;
8
+ isLoading: boolean;
9
+ // Declarative facet config — ordering, titles, colors, display maps.
10
+ facetConfigs: Array<FacetConfig>;
11
+ onIncludeFilter: (facetKey: string, value: string) => void;
12
+ onExcludeFilter: (facetKey: string, value: string) => void;
13
+ activeFilters?: Array<ActiveFilter> | undefined;
14
+ headerLabel?: string | undefined;
15
+ }
16
+
17
+ const TelemetryFacetSidebar: FunctionComponent<TelemetryFacetSidebarProps> = (
18
+ props: TelemetryFacetSidebarProps,
19
+ ): ReactElement => {
20
+ const orderedConfigs: Array<FacetConfig> = useMemo(() => {
21
+ const copy: Array<FacetConfig> = [...props.facetConfigs];
22
+ copy.sort((a: FacetConfig, b: FacetConfig): number => {
23
+ const aPriority: number = a.priority ?? 100;
24
+ const bPriority: number = b.priority ?? 100;
25
+ if (aPriority !== bPriority) {
26
+ return aPriority - bPriority;
27
+ }
28
+ return a.title.localeCompare(b.title);
29
+ });
30
+ return copy;
31
+ }, [props.facetConfigs]);
32
+
33
+ const activeValuesByKey: Record<string, Set<string>> = useMemo(() => {
34
+ const map: Record<string, Set<string>> = {};
35
+
36
+ if (props.activeFilters) {
37
+ for (const filter of props.activeFilters) {
38
+ if (!map[filter.facetKey]) {
39
+ map[filter.facetKey] = new Set<string>();
40
+ }
41
+ map[filter.facetKey]!.add(filter.value);
42
+ }
43
+ }
44
+
45
+ return map;
46
+ }, [props.activeFilters]);
47
+
48
+ return (
49
+ <div className="flex h-full w-56 flex-none flex-col overflow-y-auto rounded-lg border border-gray-200 bg-white">
50
+ <div className="border-b border-gray-100 px-3 py-2.5">
51
+ <h3 className="text-[11px] font-semibold uppercase tracking-widest text-gray-400">
52
+ {props.headerLabel || "Filters"}
53
+ </h3>
54
+ </div>
55
+
56
+ {props.isLoading && Object.keys(props.facetData).length === 0 && (
57
+ <div className="flex flex-1 items-center justify-center py-8">
58
+ <ComponentLoader />
59
+ </div>
60
+ )}
61
+
62
+ <div className="flex-1 overflow-y-auto">
63
+ {orderedConfigs.map((config: FacetConfig) => {
64
+ const values: Array<FacetValue> = props.facetData[config.key] || [];
65
+
66
+ return (
67
+ <TelemetryFacetSection
68
+ key={config.key}
69
+ facetKey={config.key}
70
+ title={config.title}
71
+ values={values}
72
+ onIncludeValue={props.onIncludeFilter}
73
+ onExcludeValue={props.onExcludeFilter}
74
+ valueDisplayMap={config.valueDisplayMap}
75
+ valueColorMap={config.valueColorMap}
76
+ activeValues={activeValuesByKey[config.key]}
77
+ />
78
+ );
79
+ })}
80
+ </div>
81
+ </div>
82
+ );
83
+ };
84
+
85
+ export default TelemetryFacetSidebar;
@@ -0,0 +1,102 @@
1
+ import React, { FunctionComponent, ReactElement } from "react";
2
+ import Icon from "../../Icon/Icon";
3
+ import IconProp from "../../../../Types/Icon/IconProp";
4
+
5
+ export interface TelemetryFacetValueRowProps {
6
+ value: string;
7
+ displayValue?: string | undefined;
8
+ count: number;
9
+ maxCount: number;
10
+ color?: string | undefined;
11
+ isActive?: boolean | undefined;
12
+ onInclude: (value: string) => void;
13
+ onExclude: (value: string) => void;
14
+ }
15
+
16
+ const TelemetryFacetValueRow: FunctionComponent<TelemetryFacetValueRowProps> = (
17
+ props: TelemetryFacetValueRowProps,
18
+ ): ReactElement => {
19
+ const barWidth: number =
20
+ props.maxCount > 0
21
+ ? Math.max(4, Math.round((props.count / props.maxCount) * 100))
22
+ : 0;
23
+
24
+ const displayLabel: string = props.displayValue || props.value || "(empty)";
25
+ const isActive: boolean = props.isActive || false;
26
+
27
+ return (
28
+ <div className="group flex items-center gap-2 py-0.5">
29
+ <button
30
+ type="button"
31
+ className={`flex min-w-0 flex-1 items-center gap-2 rounded px-1.5 py-0.5 text-left transition-colors ${
32
+ isActive ? "bg-indigo-50 ring-1 ring-indigo-200" : "hover:bg-gray-50"
33
+ }`}
34
+ onClick={() => {
35
+ props.onInclude(props.value);
36
+ }}
37
+ title={
38
+ isActive
39
+ ? `Remove filter: ${displayLabel}`
40
+ : `Filter to ${displayLabel}`
41
+ }
42
+ >
43
+ {isActive ? (
44
+ <span className="flex h-3.5 w-3.5 flex-none items-center justify-center rounded bg-indigo-500">
45
+ <Icon icon={IconProp.Check} className="h-2.5 w-2.5 text-white" />
46
+ </span>
47
+ ) : props.color ? (
48
+ <span
49
+ className="h-2.5 w-2.5 flex-none rounded-full"
50
+ style={{ backgroundColor: props.color }}
51
+ />
52
+ ) : (
53
+ <span className="h-2.5 w-2.5 flex-none rounded-full bg-gray-300" />
54
+ )}
55
+ <span
56
+ className={`min-w-0 truncate text-[12px] ${
57
+ isActive ? "font-medium text-indigo-700" : "text-gray-700"
58
+ }`}
59
+ >
60
+ {displayLabel}
61
+ </span>
62
+ </button>
63
+
64
+ <div className="flex items-center gap-1.5">
65
+ <div className="w-12">
66
+ <div className="h-1.5 w-full rounded-full bg-gray-100">
67
+ <div
68
+ className={`h-1.5 rounded-full transition-all ${isActive ? "opacity-100" : "opacity-70"}`}
69
+ style={{
70
+ width: `${barWidth}%`,
71
+ backgroundColor: isActive
72
+ ? "#6366f1"
73
+ : props.color || "#9ca3af",
74
+ }}
75
+ />
76
+ </div>
77
+ </div>
78
+ <span
79
+ className={`min-w-[2rem] text-right font-mono text-[10px] tabular-nums ${
80
+ isActive ? "font-medium text-indigo-600" : "text-gray-400"
81
+ }`}
82
+ >
83
+ {props.count.toLocaleString()}
84
+ </span>
85
+ </div>
86
+
87
+ <button
88
+ type="button"
89
+ className="hidden h-5 w-5 items-center justify-center rounded text-[10px] text-gray-400 transition-colors hover:bg-red-50 hover:text-red-500 group-hover:flex"
90
+ onClick={(e: React.MouseEvent) => {
91
+ e.stopPropagation();
92
+ props.onExclude(props.value);
93
+ }}
94
+ title={`Exclude ${displayLabel}`}
95
+ >
96
+ -
97
+ </button>
98
+ </div>
99
+ );
100
+ };
101
+
102
+ export default TelemetryFacetValueRow;