@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.
- 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/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/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
|
@@ -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;
|