Compare commits

..

1 Commits

Author SHA1 Message Date
Srikanth Chekuri
85a3923564 chore: add time filter 2025-03-26 13:13:31 +05:30
13 changed files with 106 additions and 1174 deletions

View File

@@ -207,7 +207,7 @@ export const PasswordReset = Loadable(
export const SomethingWentWrong = Loadable(
() =>
import(
/* webpackChunkName: "SomethingWentWrong" */ 'pages/SomethingWentWrong'
/* webpackChunkName: "ErrorBoundaryFallback" */ 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'
),
);
@@ -299,10 +299,3 @@ export const MetricsExplorer = Loadable(
export const ApiMonitoring = Loadable(
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
);
export const DynamicVariableTest = Loadable(
() =>
import(
/* webpackChunkName: "DynamicVariableTest" */ 'pages/DynamicVariableTest'
),
);

View File

@@ -15,7 +15,6 @@ import {
CustomDomainSettings,
DashboardPage,
DashboardWidget,
DynamicVariableTest,
EditAlertChannelsAlerts,
EditRulesPage,
ErrorDetails,
@@ -506,13 +505,6 @@ const routes: AppRoutes[] = [
key: 'API_MONITORING',
isPrivate: true,
},
{
path: ROUTES.DYNAMIC_VARIABLE_TEST,
exact: true,
component: DynamicVariableTest,
key: 'DYNAMIC_VARIABLE_TEST',
isPrivate: true,
},
];
export const SUPPORT_ROUTE: AppRoutes = {

View File

@@ -1,231 +0,0 @@
.multi-select-container {
position: relative;
width: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif;
}
.multi-select-label {
margin-bottom: 4px;
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
}
.multi-select-input {
width: 100%;
min-height: 40px;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 4px 8px;
display: flex;
align-items: center;
cursor: text;
background-color: #fff;
transition: all 0.3s;
&:hover {
border-color: #40a9ff;
}
&:focus,
&.multi-select-input-focused {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
outline: 0;
}
}
.multi-select-chips {
display: flex;
flex-wrap: wrap;
width: 100%;
gap: 4px;
align-items: center;
}
.multi-select-chip {
background-color: #f0f0f0;
border-radius: 4px;
padding: 2px 8px;
display: flex;
align-items: center;
gap: 4px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
line-height: 22px;
.multi-select-chip-remove {
cursor: pointer;
font-size: 12px;
background: none;
border: none;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
color: rgba(0, 0, 0, 0.45);
&:hover {
color: #ff4d4f;
}
}
}
.multi-select-search {
flex: 1;
min-width: 50px;
border: none;
outline: none;
background: transparent;
&:focus {
border: none;
box-shadow: none;
}
// Override Ant Design's styles
.ant-input {
background: transparent;
&:focus {
box-shadow: none;
}
}
}
.multi-select-clear-all {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: #ff4d4f;
}
}
.multi-select-dropdown {
position: absolute;
top: 100%;
left: 0;
width: 100%;
max-height: 400px;
border: 1px solid #d9d9d9;
border-radius: 6px;
margin-top: 4px;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1050;
overflow: hidden;
display: flex;
flex-direction: column;
}
.multi-select-option {
padding: 8px 12px;
cursor: pointer;
&:hover {
background-color: #f5f5f5;
}
}
.multi-select-divider {
height: 1px;
background-color: #f0f0f0;
margin: 0;
}
.multi-select-section-label {
padding: 8px 12px;
font-weight: 500;
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
background-color: #fafafa;
}
.multi-select-loading {
padding: 12px;
display: flex;
align-items: center;
gap: 12px;
justify-content: center;
}
.multi-select-no-results {
padding: 12px;
text-align: center;
color: rgba(0, 0, 0, 0.45);
}
.multi-select-options-container,
.multi-select-section-content {
overflow-y: auto;
/* For WebKit browsers */
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-track {
background: #f0f0f0;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: #aaa;
}
/* For Firefox */
scrollbar-width: thin;
scrollbar-color: #ccc #f0f0f0;
}
.multi-select-error {
border-color: #ff4d4f;
&:hover,
&:focus,
&.multi-select-input-focused {
border-color: #ff7875;
box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2);
}
}
.multi-select-error-text {
color: #ff4d4f;
font-size: 14px;
line-height: 1.5;
margin-top: 4px;
}
.multi-select-disabled {
.multi-select-input {
background-color: #f5f5f5;
cursor: not-allowed;
color: rgba(0, 0, 0, 0.25);
border-color: #d9d9d9;
&:hover {
border-color: #d9d9d9;
}
}
.multi-select-chip {
color: rgba(0, 0, 0, 0.25);
background-color: #eee;
}
}

View File

@@ -1,595 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './MultiSelect.styles.scss';
import { CloseOutlined, SearchOutlined } from '@ant-design/icons';
import { Checkbox, Input, Spin } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import { InputRef } from 'antd/lib/input';
import { useCallback, useEffect, useRef, useState } from 'react';
export interface MultiSelectOption {
label: string;
value: string;
selected?: boolean;
disabled?: boolean;
}
export interface MultiSelectSection {
title: string;
options: MultiSelectOption[];
}
export interface MultiSelectProps {
/** Array of options to display in the dropdown */
options: MultiSelectOption[];
/** Callback when selected values change */
onChange: (selectedValues: string[]) => void;
/** Currently selected values */
value?: string[];
/** Placeholder text for the search input */
placeholder?: string;
/** Whether the component is in loading state */
loading?: boolean;
/** Allow users to add custom values */
allowCustomValues?: boolean;
/** Callback when search text changes - can be used for server filtering */
onSearch?: (searchText: string) => void;
/** Custom class name */
className?: string;
/** Additional sections to display (e.g., "Related Values") */
additionalSections?: MultiSelectSection[];
/** Show "Select All" option */
showSelectAll?: boolean;
/** Maximum height of dropdown in pixels */
dropdownMaxHeight?: number;
/** Maximum width of dropdown in pixels (defaults to matching input width) */
dropdownMaxWidth?: number;
/** Disable the component */
disabled?: boolean;
/** Error message to display */
error?: string;
/** Label text */
label?: string;
/** Allow users to clear all selections */
allowClear?: boolean;
/** Maximum height of a section */
sectionMaxHeight?: number;
}
function MultiSelect({
options,
onChange,
value = [],
placeholder = 'Search...',
loading = false,
allowCustomValues = true,
onSearch,
className = '',
additionalSections = [],
showSelectAll = true,
dropdownMaxHeight = 400,
dropdownMaxWidth,
disabled = false,
error,
label,
allowClear = true,
sectionMaxHeight = 150,
}: MultiSelectProps): JSX.Element {
const [isDropdownOpen, setIsDropdownOpen] = useState<boolean>(false);
const [searchText, setSearchText] = useState<string>('');
const [selectedValues, setSelectedValues] = useState<string[]>(value);
const [displayOptions, setDisplayOptions] = useState<MultiSelectOption[]>(
options,
);
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<InputRef>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const [focusedChipIndex, setFocusedChipIndex] = useState<number>(-1);
const chipRefs = useRef<(HTMLDivElement | null)[]>([]);
// Handle save action - memoize with useCallback
const handleSave = useCallback((): void => {
setIsDropdownOpen(false);
setSearchText('');
onChange(selectedValues);
}, [onChange, selectedValues]);
// Synchronize value prop with internal state
useEffect(() => {
setSelectedValues(value);
}, [value]);
// Filter and sort options based on search text
useEffect(() => {
// Filter options based on search text
const filteredOptions = options.filter((option) =>
option.label.toLowerCase().includes(searchText.toLowerCase()),
);
// Add custom value option if no matches found and allowCustomValues is true
if (
allowCustomValues &&
searchText &&
!filteredOptions.some(
(option) => option.label.toLowerCase() === searchText.toLowerCase(),
) &&
!filteredOptions.some(
(option) => option.value.toLowerCase() === searchText.toLowerCase(),
)
) {
filteredOptions.unshift({
label: `Add "${searchText}"`,
value: searchText,
});
}
// Sort options: selected first, then matching search term
const sortedOptions = [...filteredOptions].sort((a, b) => {
// First by selection status
if (selectedValues.includes(a.value) && !selectedValues.includes(b.value))
return -1;
if (!selectedValues.includes(a.value) && selectedValues.includes(b.value))
return 1;
// Then by match position (exact matches or starts with come first)
const aLower = a.label.toLowerCase();
const bLower = b.label.toLowerCase();
const searchLower = searchText.toLowerCase();
if (aLower === searchLower && bLower !== searchLower) return -1;
if (aLower !== searchLower && bLower === searchLower) return 1;
if (aLower.startsWith(searchLower) && !bLower.startsWith(searchLower))
return -1;
if (!aLower.startsWith(searchLower) && bLower.startsWith(searchLower))
return 1;
return 0;
});
setDisplayOptions(sortedOptions);
}, [options, searchText, selectedValues, allowCustomValues]);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent): void => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
handleSave();
}
};
document.addEventListener('mousedown', handleClickOutside);
return (): void => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [selectedValues, handleSave]);
// Adjust dropdown position if needed
useEffect(() => {
if (isDropdownOpen && dropdownRef.current && containerRef.current) {
const containerRect = containerRef.current.getBoundingClientRect();
const dropdownHeight = dropdownRef.current.offsetHeight;
const viewportHeight = window.innerHeight;
// Check if dropdown extends beyond viewport bottom
if (
containerRect.bottom + dropdownHeight > viewportHeight &&
containerRect.top > dropdownHeight
) {
dropdownRef.current.style.top = 'auto';
dropdownRef.current.style.bottom = '100%';
dropdownRef.current.style.marginTop = '0';
dropdownRef.current.style.marginBottom = '4px';
}
}
}, [isDropdownOpen, displayOptions]);
// Handle search input change
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const text = e.target.value;
setSearchText(text);
if (onSearch) {
onSearch(text);
}
};
// Handle selection change
const handleSelectionChange = (
option: MultiSelectOption,
e: CheckboxChangeEvent,
): void => {
const { checked } = e.target;
let newSelectedValues: string[];
if (checked) {
newSelectedValues = [...selectedValues, option.value];
} else {
newSelectedValues = selectedValues.filter((val) => val !== option.value);
}
setSelectedValues(newSelectedValues);
};
// Handle "All" checkbox change
const handleSelectAll = (e: CheckboxChangeEvent): void => {
if (e.target.checked) {
const allValues = options
.filter((option) => !option.disabled)
.map((option) => option.value);
setSelectedValues(allValues);
} else {
setSelectedValues([]);
}
};
// Remove a selected item
const handleRemoveItem = useCallback(
(value: string): void => {
const newSelectedValues = selectedValues.filter((val) => val !== value);
setSelectedValues(newSelectedValues);
},
[selectedValues],
);
// Handle clicking the input area
const handleInputClick = (): void => {
if (!disabled) {
setIsDropdownOpen(true);
inputRef.current?.focus();
}
};
// Handle clear all selections
const handleClearAll = (): void => {
setSelectedValues([]);
setSearchText('');
inputRef.current?.focus();
};
// Get display value of a selection (chips)
const getSelectedOptions = (): MultiSelectOption[] =>
selectedValues.map((value) => {
const option = options.find((opt) => opt.value === value);
return {
label: option?.label || value,
value,
};
});
const selectedOptions = getSelectedOptions();
const allSelectableOptions = options.filter((option) => !option.disabled);
const allSelected =
allSelectableOptions.length > 0 &&
selectedValues.length === allSelectableOptions.length;
const containerClasses = [
'multi-select-container',
className,
disabled ? 'multi-select-disabled' : '',
error ? 'multi-select-error' : '',
]
.filter(Boolean)
.join(' ');
const inputClasses = [
'multi-select-input',
isDropdownOpen ? 'multi-select-input-focused' : '',
]
.filter(Boolean)
.join(' ');
// Reset chip refs array when selected options change
useEffect(() => {
chipRefs.current = Array(selectedOptions.length).fill(null);
}, [selectedOptions.length]);
// Handle chip keyboard navigation
const handleChipKeyDown = useCallback(
(e: React.KeyboardEvent, index: number) => {
e.stopPropagation(); // Prevent bubbling to container
switch (e.key) {
case 'ArrowLeft':
e.preventDefault();
// Move focus to previous chip
if (index > 0) {
setFocusedChipIndex(index - 1);
}
break;
case 'ArrowRight':
e.preventDefault();
// Move focus to next chip or input
if (index < selectedOptions.length - 1) {
setFocusedChipIndex(index + 1);
} else {
// Focus the input when at the last chip
setFocusedChipIndex(-1);
inputRef.current?.focus();
}
break;
case 'Delete':
case 'Backspace':
e.preventDefault();
// Remove current chip
handleRemoveItem(selectedOptions[index].value);
// Adjust focus after deletion
if (selectedOptions.length > 1) {
// Focus previous chip if not at beginning
const newIndex = Math.min(index, selectedOptions.length - 2);
setFocusedChipIndex(newIndex);
} else {
// If this was the last chip, focus input
setFocusedChipIndex(-1);
inputRef.current?.focus();
}
break;
case 'Escape':
e.preventDefault();
// Return focus to input
setFocusedChipIndex(-1);
inputRef.current?.focus();
break;
default:
// No-op for unhandled keys
break;
}
},
[selectedOptions, handleRemoveItem],
);
// Handle key events in the input
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
// Add custom value on Enter if it doesn't exist
if (
allowCustomValues &&
searchText &&
!options.some(
(option) => option.value.toLowerCase() === searchText.toLowerCase(),
) &&
!options.some(
(option) => option.label.toLowerCase() === searchText.toLowerCase(),
)
) {
const newSelectedValues = [...selectedValues, searchText];
setSelectedValues(newSelectedValues);
setSearchText('');
} else if (isDropdownOpen) {
handleSave();
}
} else if (e.key === 'Escape') {
handleSave();
} else if (
e.key === 'Backspace' &&
!searchText &&
selectedValues.length > 0
) {
// Remove the last selected item when pressing backspace in an empty input
const newSelectedValues = [...selectedValues];
newSelectedValues.pop();
setSelectedValues(newSelectedValues);
} else if (e.key === 'Tab' && isDropdownOpen) {
// Close dropdown but keep focus within component
e.preventDefault();
handleSave();
}
// Add navigation TO chips when in input field
if (e.key === 'ArrowLeft' && !searchText && selectedOptions.length > 0) {
e.preventDefault();
setFocusedChipIndex(selectedOptions.length - 1);
}
};
// Focus the appropriate chip when focusedChipIndex changes
useEffect(() => {
if (focusedChipIndex >= 0 && chipRefs.current[focusedChipIndex]) {
chipRefs.current[focusedChipIndex]?.focus();
}
}, [focusedChipIndex]);
return (
<div className={containerClasses} ref={containerRef}>
{label && <div className="multi-select-label">{label}</div>}
<div
className={inputClasses}
onClick={handleInputClick}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
handleInputClick();
}
}}
role="combobox"
aria-expanded={isDropdownOpen}
aria-haspopup="listbox"
aria-controls="multi-select-dropdown"
aria-owns="multi-select-dropdown"
tabIndex={disabled ? -1 : 0}
>
<div className="multi-select-chips">
{selectedOptions.map((option, index) => (
<div
key={option.value}
className={`multi-select-chip ${
focusedChipIndex === index ? 'multi-select-chip-focused' : ''
}`}
ref={(el): void => {
chipRefs.current[index] = el;
}}
role="button"
tabIndex={0}
onKeyDown={(e): void => handleChipKeyDown(e, index)}
onFocus={(): void => setFocusedChipIndex(index)}
onClick={(e): void => e.stopPropagation()}
aria-label={`Selected option: ${option.label}`}
>
{option.label}
{!disabled && (
<button
type="button"
className="multi-select-chip-remove"
onClick={(e): void => {
e.stopPropagation();
handleRemoveItem(option.value);
}}
aria-label={`Remove ${option.label}`}
tabIndex={-1} // Don't make the inner button tabbable
>
<CloseOutlined />
</button>
)}
</div>
))}
<Input
ref={inputRef}
className="multi-select-search"
placeholder={selectedOptions.length === 0 ? placeholder : ''}
value={searchText}
onChange={handleSearchChange}
onKeyDown={handleKeyDown}
onFocus={(): void => setIsDropdownOpen(true)}
suffix={<SearchOutlined />}
bordered={false}
disabled={disabled}
/>
{allowClear && selectedValues.length > 0 && !disabled && (
<button
type="button"
className="multi-select-clear-all"
onClick={(e): void => {
e.stopPropagation();
handleClearAll();
}}
aria-label="Clear all selections"
>
<CloseOutlined />
</button>
)}
</div>
</div>
{error && <div className="multi-select-error-text">{error}</div>}
{isDropdownOpen && !disabled && (
<div
className="multi-select-dropdown"
ref={dropdownRef}
style={{
maxHeight: `${dropdownMaxHeight}px`,
maxWidth: dropdownMaxWidth ? `${dropdownMaxWidth}px` : undefined,
}}
id="multi-select-dropdown"
role="listbox"
aria-multiselectable="true"
>
{loading ? (
<div className="multi-select-loading">
<Spin size="small" />
<span>We are updating the values ...</span>
</div>
) : (
<>
{showSelectAll && (
<>
<div className="multi-select-option">
<Checkbox checked={allSelected} onChange={handleSelectAll}>
ALL
</Checkbox>
</div>
<div className="multi-select-divider" />
</>
)}
<div
className="multi-select-options-container"
style={{ maxHeight: `${sectionMaxHeight}px` }}
>
{displayOptions.length > 0 ? (
displayOptions.map((option) => (
<div
key={option.value}
className="multi-select-option"
role="option"
aria-selected={selectedValues.includes(option.value)}
>
<Checkbox
checked={selectedValues.includes(option.value)}
onChange={(e): void => handleSelectionChange(option, e)}
disabled={option.disabled}
>
{option.label}
</Checkbox>
</div>
))
) : (
<div className="multi-select-no-results">
{allowCustomValues && searchText
? `Add "${searchText}"`
: 'No results found'}
</div>
)}
</div>
{additionalSections.map(
(section) =>
section.options.length > 0 && (
<div key={`section-${section.title}`}>
<div className="multi-select-divider" />
<div className="multi-select-section-label">{section.title}</div>
<div
className="multi-select-section-content"
style={{ maxHeight: `${sectionMaxHeight}px` }}
>
{section.options.map((option) => (
<div
key={option.value}
className="multi-select-option"
role="option"
aria-selected={selectedValues.includes(option.value)}
>
<Checkbox
checked={selectedValues.includes(option.value)}
onChange={(e): void => handleSelectionChange(option, e)}
disabled={option.disabled}
>
{option.label}
</Checkbox>
</div>
))}
</div>
</div>
),
)}
</>
)}
</div>
)}
</div>
);
}
// Define defaultProps to fix linter warnings
MultiSelect.defaultProps = {
value: [],
placeholder: 'Search...',
loading: false,
allowCustomValues: true,
onSearch: undefined,
className: '',
additionalSections: [],
showSelectAll: true,
dropdownMaxHeight: 400,
dropdownMaxWidth: undefined,
disabled: false,
error: undefined,
label: undefined,
allowClear: true,
sectionMaxHeight: 150,
};
export default MultiSelect;

View File

@@ -1,8 +0,0 @@
import MultiSelect from './MultiSelect';
export type {
MultiSelectOption,
MultiSelectProps,
MultiSelectSection,
} from './MultiSelect';
export default MultiSelect;

View File

@@ -75,7 +75,6 @@ const ROUTES = {
METRICS_EXPLORER_BASE: '/metrics-explorer',
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
HOME_PAGE: '/',
DYNAMIC_VARIABLE_TEST: '/dynamic-variable-test',
} as const;
export default ROUTES;

View File

@@ -1,211 +0,0 @@
import './styles.scss';
import { Button, Card, Col, Divider, Row, Switch, Typography } from 'antd';
import MultiSelect, {
MultiSelectOption,
MultiSelectSection,
} from 'components/MultiSelect';
import { useState } from 'react';
const { Title, Text, Paragraph } = Typography;
// Sample data for the component
const sampleOptions: MultiSelectOption[] = [
{ label: 'abc', value: 'abc' },
{ label: 'acbewc', value: 'acbewc' },
{ label: 'custom-value', value: 'custom-value' },
{ label: 'option1', value: 'option1' },
{ label: 'option2', value: 'option2' },
{ label: 'another-option', value: 'another-option' },
{ label: 'test-option', value: 'test-option' },
{ label: 'disabled-option', value: 'disabled-option', disabled: true },
];
// Sample related values for the "Related Values" section
const relatedValues: MultiSelectOption[] = [
{ label: 'gke-mgmt-pl-generator-e2st4-sp-f1c1bde8-skbl', value: 'gke-1' },
{ label: 'gke-mgmt-pl-generator-e2st4-sp-f1c1bde8-skb2', value: 'gke-2' },
{ label: 'gke-mgmt-pl-generator-e2st4-sp-f1c1bde8-skb3', value: 'gke-3' },
];
// Sample all values for the "All Values" section
const allValues: MultiSelectOption[] = Array.from({ length: 20 }, (_, i) => ({
label: `gke-mgmt-pl-generator-e2st4-sp-f1c1bde8-7a7w-${i + 1}`,
value: `all-${i + 1}`,
}));
// Creating sections
const sections: MultiSelectSection[] = [
{
title: 'Related Values',
options: relatedValues,
},
{
title: 'ALL Values',
options: allValues,
},
];
function DynamicVariableTestPage(): JSX.Element {
const [selectedValues, setSelectedValues] = useState<string[]>([
'abc',
'acbewc',
]);
const [loadingDemo, setLoadingDemo] = useState<boolean>(false);
const [allowCustom, setAllowCustom] = useState<boolean>(true);
const [showError, setShowError] = useState<boolean>(false);
const [disabled, setDisabled] = useState<boolean>(false);
const handleChange = (values: string[]): void => {
setSelectedValues(values);
};
const toggleLoading = (): void => {
setLoadingDemo((prev) => !prev);
};
return (
<div className="dynamic-variable-page">
<Card>
<Title level={3}>Dynamic Variable MultiSelect</Title>
<Paragraph>
This page demonstrates the MultiSelect component with various features. The
component is now fully reusable and production-ready with support for
dynamic data from APIs, proper error states, accessibility, and UI
improvements.
</Paragraph>
<Divider />
<Row gutter={[16, 16]}>
<Col xs={24} md={12}>
<Title level={5}>Basic MultiSelect</Title>
<Text>This example shows the basic usage with pre-selected values.</Text>
<div className="multiselect-demo-container">
<MultiSelect
options={sampleOptions}
value={selectedValues}
onChange={handleChange}
placeholder="Search or add values..."
label="Select options"
/>
</div>
<div className="selected-values-display">
<Title level={5}>Selected Values:</Title>
<pre>{JSON.stringify(selectedValues, null, 2)}</pre>
</div>
</Col>
<Col xs={24} md={12}>
<Title level={5}>With Sections & All Values</Title>
<Text>This example shows the component with additional sections.</Text>
<div className="multiselect-demo-container">
<MultiSelect
options={sampleOptions}
value={selectedValues}
onChange={handleChange}
placeholder="Search or add values..."
additionalSections={sections}
sectionMaxHeight={120}
/>
</div>
</Col>
</Row>
<Divider />
<Row gutter={[16, 16]}>
<Col xs={24} md={12}>
<Title level={5}>Loading State</Title>
<Text>This example demonstrates the loading state.</Text>
<div className="multiselect-demo-container">
<MultiSelect
options={sampleOptions}
value={[]}
onChange={(): void => {}}
loading={loadingDemo}
placeholder="This shows loading state..."
/>
<Button onClick={toggleLoading} style={{ marginTop: 16 }}>
{loadingDemo ? 'Stop Loading' : 'Simulate Loading'}
</Button>
</div>
</Col>
<Col xs={24} md={12}>
<Title level={5}>Custom Values Configuration</Title>
<Text>Toggle to enable/disable custom values.</Text>
<div className="multiselect-demo-container">
<MultiSelect
options={sampleOptions}
value={[]}
onChange={(): void => {}}
allowCustomValues={allowCustom}
placeholder={
allowCustom ? 'Custom values allowed...' : 'Only predefined values...'
}
/>
<div style={{ marginTop: 16 }}>
<Switch
checked={allowCustom}
onChange={setAllowCustom}
checkedChildren="Custom values on"
unCheckedChildren="Custom values off"
/>
</div>
</div>
</Col>
</Row>
<Divider />
<Row gutter={[16, 16]}>
<Col xs={24} md={12}>
<Title level={5}>Error State</Title>
<Text>This example shows the component with an error.</Text>
<div className="multiselect-demo-container">
<MultiSelect
options={sampleOptions}
value={[]}
onChange={(): void => {}}
placeholder="Select some options..."
error={showError ? 'Please select at least one option' : undefined}
/>
<Button
onClick={(): void => setShowError(!showError)}
style={{ marginTop: 16 }}
type={showError ? 'primary' : 'default'}
>
{showError ? 'Hide Error' : 'Show Error'}
</Button>
</div>
</Col>
<Col xs={24} md={12}>
<Title level={5}>Disabled State</Title>
<Text>This example shows the disabled state of the component.</Text>
<div className="multiselect-demo-container">
<MultiSelect
options={sampleOptions}
value={['abc', 'option1']}
onChange={(): void => {}}
placeholder="This component is disabled..."
disabled={disabled}
/>
<div style={{ marginTop: 16 }}>
<Switch
checked={disabled}
onChange={setDisabled}
checkedChildren="Disabled"
unCheckedChildren="Enabled"
/>
</div>
</div>
</Col>
</Row>
</Card>
</div>
);
}
export default DynamicVariableTestPage;

View File

@@ -1,20 +0,0 @@
.dynamic-variable-page {
padding: 24px;
max-width: 800px;
margin: 0 auto;
}
.multiselect-demo-container {
margin-top: 16px;
width: 100%;
}
.selected-values-display {
margin-top: 24px;
pre {
background-color: #f5f5f5;
padding: 16px;
border-radius: 6px;
}
}

View File

@@ -120,5 +120,4 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
API_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'],
WORKSPACE_ACCESS_RESTRICTED: ['ADMIN', 'EDITOR', 'VIEWER'],
METRICS_EXPLORER_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
DYNAMIC_VARIABLE_TEST: ['ADMIN', 'EDITOR', 'VIEWER'],
};

View File

@@ -5982,10 +5982,10 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, req *metrics_
}
firstQueryLimit := req.Limit
dataPointsOrder := false
samplesOrder := false
var orderByClauseFirstQuery string
if req.OrderBy.ColumnName == "samples" {
dataPointsOrder = true
samplesOrder = true
orderByClauseFirstQuery = fmt.Sprintf("ORDER BY timeseries %s", req.OrderBy.Order)
if req.Limit < 50 {
firstQueryLimit = 50
@@ -5995,30 +5995,33 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, req *metrics_
}
// Determine which tables to use
start, end, tsTable, localTsTable := utils.WhichTSTableToUse(req.Start, req.EndD)
sampleTable, countExp := utils.WhichSampleTableToUse(req.Start, req.EndD)
start, end, tsTable, localTsTable := utils.WhichTSTableToUse(req.Start, req.End)
sampleTable, countExp := utils.WhichSampleTableToUse(req.Start, req.End)
metricsQuery := fmt.Sprintf(
`SELECT
t.metric_name AS metric_name,
ANY_VALUE(t.description) AS description,
ANY_VALUE(t.type) AS metric_type,
ANY_VALUE(t.unit) AS metric_unit,
uniq(t.fingerprint) AS timeseries,
metric_name,
ANY_VALUE(description) AS description,
ANY_VALUE(type) AS metric_type,
ANY_VALUE(unit) AS metric_unit,
uniq(fingerprint) AS timeseries,
uniq(metric_name) OVER() AS total
FROM %s.%s AS t
FROM %s.%s
WHERE unix_milli BETWEEN ? AND ?
AND NOT startsWith(metric_name, 'signoz_')
AND __normalized = true
%s
GROUP BY t.metric_name
AND NOT startsWith(metric_name, 'signoz_')
AND __normalized = true
%s
GROUP BY metric_name
%s
LIMIT %d OFFSET %d;`,
signozMetricDBName, tsTable, whereClause, orderByClauseFirstQuery, firstQueryLimit, req.Offset)
args = append(args, start, end)
valueCtx := context.WithValue(ctx, "clickhouse_max_threads", constants.MetricsExplorerClickhouseThreads)
begin := time.Now()
rows, err := r.db.Query(valueCtx, metricsQuery, args...)
duration := time.Since(begin)
zap.L().Info("Time taken to execute metrics query to fetch metrics with high time series", zap.String("query", metricsQuery), zap.Any("args", args), zap.Duration("duration", duration))
if err != nil {
zap.L().Error("Error executing metrics query", zap.Error(err))
return &metrics_explorer.SummaryListMetricsResponse{}, &model.ApiError{Typ: "ClickHouseError", Err: err}
@@ -6049,12 +6052,14 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, req *metrics_
// Build a comma-separated list of quoted metric names.
metricsList := "'" + strings.Join(metricNames, "', '") + "'"
// If samples are being sorted by datapoints, update the ORDER clause.
if dataPointsOrder {
if samplesOrder {
orderByClauseFirstQuery = fmt.Sprintf("ORDER BY s.samples %s", req.OrderBy.Order)
} else {
orderByClauseFirstQuery = ""
}
// reset the args for main query
args = make([]interface{}, 0)
var sampleQuery string
var sb strings.Builder
@@ -6062,20 +6067,19 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, req *metrics_
sb.WriteString(fmt.Sprintf(
`SELECT
s.samples,
s.metric_name,
s.lastReceived
s.metric_name
FROM (
SELECT
SELECT
dm.metric_name,
%s AS samples,
MAX(dm.unix_milli) AS lastReceived
%s AS samples
FROM %s.%s AS dm
WHERE dm.metric_name IN (%s)
AND dm.fingerprint IN (
SELECT fingerprint
FROM %s.%s
WHERE metric_name IN (%s)
AND __normalized = true
AND __normalized = true
AND unix_milli BETWEEN ? AND ?
%s
GROUP BY fingerprint
)
@@ -6089,26 +6093,27 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, req *metrics_
metricsList,
whereClause,
))
args = append(args, start, end)
args = append(args, req.Start, req.End)
} else {
// If no filters, it is a simpler query.
sb.WriteString(fmt.Sprintf(
`SELECT
s.samples,
s.metric_name,
s.lastReceived
FROM (
SELECT
metric_name,
%s AS samples,
MAX(unix_milli) AS lastReceived
FROM %s.%s
WHERE metric_name IN (%s)
AND unix_milli BETWEEN ? AND ?
GROUP BY metric_name
) AS s `,
s.samples,
s.metric_name
FROM (
SELECT
metric_name,
%s AS samples
FROM %s.%s
WHERE metric_name IN (%s)
AND unix_milli BETWEEN ? AND ?
GROUP BY metric_name
) AS s `,
countExp,
signozMetricDBName, sampleTable,
metricsList))
args = append(args, req.Start, req.End)
}
// Append ORDER BY clause if provided.
@@ -6120,9 +6125,10 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, req *metrics_
sb.WriteString(fmt.Sprintf("LIMIT %d;", req.Limit))
sampleQuery = sb.String()
// Append the time boundaries for sampleQuery.
args = append(args, start, end)
begin = time.Now()
rows, err = r.db.Query(valueCtx, sampleQuery, args...)
duration = time.Since(begin)
zap.L().Info("Time taken to execute samples query", zap.String("query", sampleQuery), zap.Any("args", args), zap.Duration("duration", duration))
if err != nil {
zap.L().Error("Error executing samples query", zap.Error(err))
return &response, &model.ApiError{Typ: "ClickHouseError", Err: err}
@@ -6130,18 +6136,15 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, req *metrics_
defer rows.Close()
samplesMap := make(map[string]uint64)
lastReceivedMap := make(map[string]int64)
for rows.Next() {
var samples uint64
var metricName string
var lastReceived int64
if err := rows.Scan(&samples, &metricName, &lastReceived); err != nil {
if err := rows.Scan(&samples, &metricName); err != nil {
zap.L().Error("Error scanning sample row", zap.Error(err))
return &response, &model.ApiError{Typ: "ClickHouseError", Err: err}
}
samplesMap[metricName] = samples
lastReceivedMap[metricName] = lastReceived
}
if err := rows.Err(); err != nil {
zap.L().Error("Error iterating over sample rows", zap.Error(err))
@@ -6167,16 +6170,13 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, req *metrics_
}
if samples, exists := samplesMap[response.Metrics[i].MetricName]; exists {
response.Metrics[i].Samples = samples
if lastReceived, exists := lastReceivedMap[response.Metrics[i].MetricName]; exists {
response.Metrics[i].LastReceived = lastReceived
}
filteredMetrics = append(filteredMetrics, response.Metrics[i])
}
}
response.Metrics = filteredMetrics
// If ordering by samples, sort in-memory.
if dataPointsOrder {
if samplesOrder {
sort.Slice(response.Metrics, func(i, j int) bool {
return response.Metrics[i].Samples > response.Metrics[j].Samples
})
@@ -6194,7 +6194,7 @@ func (r *ClickHouseReader) GetMetricsTimeSeriesPercentage(ctx context.Context, r
if len(conditions) > 0 {
whereClause = "AND " + strings.Join(conditions, " AND ")
}
start, end, tsTable, _ := utils.WhichTSTableToUse(req.Start, req.EndD)
start, end, tsTable, _ := utils.WhichTSTableToUse(req.Start, req.End)
// Construct the query without backticks
query := fmt.Sprintf(`
@@ -6204,17 +6204,17 @@ func (r *ClickHouseReader) GetMetricsTimeSeriesPercentage(ctx context.Context, r
(total_value * 100.0 / total_time_series) AS percentage
FROM (
SELECT
metric_name,
uniq(fingerprint) AS total_value,
(SELECT uniq(fingerprint)
FROM %s.%s
WHERE unix_milli BETWEEN ? AND ? AND __normalized = true) AS total_time_series
FROM %s.%s
WHERE unix_milli BETWEEN ? AND ? AND NOT startsWith(metric_name, 'signoz_') AND __normalized = true %s
GROUP BY metric_name
)
ORDER BY percentage DESC
LIMIT %d;`,
metric_name,
uniq(fingerprint) AS total_value,
(SELECT uniq(fingerprint)
FROM %s.%s
WHERE unix_milli BETWEEN ? AND ? AND __normalized = true) AS total_time_series
FROM %s.%s
WHERE unix_milli BETWEEN ? AND ? AND NOT startsWith(metric_name, 'signoz_') AND __normalized = true %s
GROUP BY metric_name
)
ORDER BY percentage DESC
LIMIT %d;`,
signozMetricDBName,
tsTable,
signozMetricDBName,
@@ -6224,26 +6224,29 @@ func (r *ClickHouseReader) GetMetricsTimeSeriesPercentage(ctx context.Context, r
)
args = append(args,
start, end, // For total_cardinality subquery
start, end, // For total_time_series subquery
start, end, // For main query
)
valueCtx := context.WithValue(ctx, "clickhouse_max_threads", constants.MetricsExplorerClickhouseThreads)
begin := time.Now()
rows, err := r.db.Query(valueCtx, query, args...)
duration := time.Since(begin)
zap.L().Info("Time taken to execute time series percentage query", zap.String("query", query), zap.Any("args", args), zap.Duration("duration", duration))
if err != nil {
zap.L().Error("Error executing cardinality query", zap.Error(err), zap.String("query", query))
zap.L().Error("Error executing time series percentage query", zap.Error(err), zap.String("query", query))
return nil, &model.ApiError{Typ: "ClickHouseError", Err: err}
}
defer rows.Close()
var heatmap []metrics_explorer.TreeMapResponseItem
var treeMap []metrics_explorer.TreeMapResponseItem
for rows.Next() {
var item metrics_explorer.TreeMapResponseItem
if err := rows.Scan(&item.MetricName, &item.TotalValue, &item.Percentage); err != nil {
zap.L().Error("Error scanning row", zap.Error(err))
return nil, &model.ApiError{Typ: "ClickHouseError", Err: err}
}
heatmap = append(heatmap, item)
treeMap = append(treeMap, item)
}
if err := rows.Err(); err != nil {
@@ -6251,7 +6254,7 @@ func (r *ClickHouseReader) GetMetricsTimeSeriesPercentage(ctx context.Context, r
return nil, &model.ApiError{Typ: "ClickHouseError", Err: err}
}
return &heatmap, nil
return &treeMap, nil
}
func (r *ClickHouseReader) GetMetricsSamplesPercentage(ctx context.Context, req *metrics_explorer.TreeMapMetricsRequest) (*[]metrics_explorer.TreeMapResponseItem, *model.ApiError) {
@@ -6264,27 +6267,30 @@ func (r *ClickHouseReader) GetMetricsSamplesPercentage(ctx context.Context, req
}
// Determine time range and tables to use
start, end, tsTable, localTsTable := utils.WhichTSTableToUse(req.Start, req.EndD)
sampleTable, countExp := utils.WhichSampleTableToUse(req.Start, req.EndD)
start, end, tsTable, localTsTable := utils.WhichTSTableToUse(req.Start, req.End)
sampleTable, countExp := utils.WhichSampleTableToUse(req.Start, req.End)
queryLimit := 50 + req.Limit
metricsQuery := fmt.Sprintf(
`SELECT
ts.metric_name AS metric_name,
uniq(ts.fingerprint) AS timeSeries
FROM %s.%s AS ts
WHERE NOT startsWith(ts.metric_name, 'signoz_')
AND __normalized = true
AND unix_milli BETWEEN ? AND ?
%s
GROUP BY ts.metric_name
metricsQuery := fmt.Sprintf(`
SELECT
metric_name,
uniq(fingerprint) AS timeSeries
FROM %s.%s
WHERE NOT startsWith(metric_name, 'signoz_')
AND __normalized = true
AND unix_milli BETWEEN ? AND ?
%s
GROUP BY metric_name
ORDER BY timeSeries DESC
LIMIT %d;`,
signozMetricDBName, tsTable, whereClause, queryLimit,
)
valueCtx := context.WithValue(ctx, "clickhouse_max_threads", constants.MetricsExplorerClickhouseThreads)
begin := time.Now()
rows, err := r.db.Query(valueCtx, metricsQuery, start, end)
duration := time.Since(begin)
zap.L().Info("Time taken to execute metrics query to reduce search space", zap.String("query", metricsQuery), zap.Any("start", start), zap.Any("end", end), zap.Duration("duration", duration))
if err != nil {
zap.L().Error("Error executing metrics query", zap.Error(err))
return nil, &model.ApiError{Typ: "ClickHouseError", Err: err}
@@ -6345,12 +6351,13 @@ func (r *ClickHouseReader) GetMetricsSamplesPercentage(ctx context.Context, req
if whereClause != "" {
sb.WriteString(fmt.Sprintf(
` AND dm.fingerprint IN (
SELECT ts.fingerprint
FROM %s.%s AS ts
WHERE ts.metric_name IN (%s)
AND __normalized = true
%s
GROUP BY ts.fingerprint
SELECT fingerprint
FROM %s.%s
WHERE metric_name IN (%s)
AND unix_milli BETWEEN ? AND ?
AND __normalized = true
%s
GROUP BY fingerprint
)`,
signozMetricDBName, localTsTable, metricsList, whereClause,
))
@@ -6370,10 +6377,18 @@ func (r *ClickHouseReader) GetMetricsSamplesPercentage(ctx context.Context, req
sampleQuery := sb.String()
// Add start and end time to args (only for sample table)
args = append(args, start, end, start, end, req.Limit)
args = append(args,
req.Start, req.End, // For total_samples subquery
req.Start, req.End, // For main query
start, end, // For where clause time series fingerprint query
req.Limit,
)
begin = time.Now()
// Execute the sample percentage query
rows, err = r.db.Query(valueCtx, sampleQuery, args...)
duration = time.Since(begin)
zap.L().Info("Time taken to execute samples percentage query", zap.String("query", sampleQuery), zap.Any("args", args), zap.Duration("duration", duration))
if err != nil {
zap.L().Error("Error executing samples query", zap.Error(err))
return nil, &model.ApiError{Typ: "ClickHouseError", Err: err}

View File

@@ -225,21 +225,21 @@ func (receiver *SummaryService) GetMetricsTreemap(ctx context.Context, params *m
var response metrics_explorer.TreeMap
switch params.Treemap {
case metrics_explorer.TimeSeriesTeeMap:
cardinality, apiError := receiver.reader.GetMetricsTimeSeriesPercentage(ctx, params)
ts, apiError := receiver.reader.GetMetricsTimeSeriesPercentage(ctx, params)
if apiError != nil {
return nil, apiError
}
if cardinality != nil {
response.TimeSeries = *cardinality
if ts != nil {
response.TimeSeries = *ts
}
return &response, nil
case metrics_explorer.SamplesTreeMap:
dataPoints, apiError := receiver.reader.GetMetricsSamplesPercentage(ctx, params)
samples, apiError := receiver.reader.GetMetricsSamplesPercentage(ctx, params)
if apiError != nil {
return nil, apiError
}
if dataPoints != nil {
response.Samples = *dataPoints
if samples != nil {
response.Samples = *samples
}
return &response, nil
default:

View File

@@ -89,18 +89,17 @@ func (aH *APIHandler) GetTreeMap(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
params, apiError := explorer.ParseTreeMapMetricsParams(r)
if apiError != nil {
zap.L().Error("error parsing heatmap metric params", zap.Error(apiError.Err))
zap.L().Error("error parsing tree map metric params", zap.Error(apiError.Err))
RespondError(w, apiError, nil)
return
}
result, apiError := aH.SummaryService.GetMetricsTreemap(ctx, params)
if apiError != nil {
zap.L().Error("error getting heatmap data", zap.Error(apiError.Err))
zap.L().Error("error getting tree map data", zap.Error(apiError.Err))
RespondError(w, apiError, nil)
return
}
aH.Respond(w, result)
}
func (aH *APIHandler) GetRelatedMetrics(w http.ResponseWriter, r *http.Request) {

View File

@@ -9,7 +9,7 @@ type SummaryListMetricsRequest struct {
Limit int `json:"limit"`
OrderBy v3.OrderBy `json:"orderBy"`
Start int64 `json:"start"`
EndD int64 `json:"end"`
End int64 `json:"end"`
Filters v3.FilterSet `json:"filters"`
}
@@ -24,7 +24,7 @@ type TreeMapMetricsRequest struct {
Limit int `json:"limit"`
Treemap TreeMapType `json:"treemap"`
Start int64 `json:"start"`
EndD int64 `json:"end"`
End int64 `json:"end"`
Filters v3.FilterSet `json:"filters"`
}