mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-29 14:12:03 +08:00
feat: added checkbox selection in dashboard variables (#5191)
* feat: added checkbox selection in dashboard variables * feat: added checkbox selection - handling with only and all * feat: added checkbox selection - style changes * fix: fixed deselecting all options * feat: fixed all showing up in single select * feat: improve styles * feat: fixed single select getting all values and array issues * feat: updated test case * feat: added max tag shown logic with count length and info on hover for overflowed content
This commit is contained in:
parent
0fade428ef
commit
a65d5095a0
@ -40,12 +40,46 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.variable-select {
|
.variable-select {
|
||||||
.ant-select-dropdown {
|
.ant-select-item {
|
||||||
max-width: 300px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-label {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-checkbox-label {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 24px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-value {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.option-text {
|
||||||
|
max-width: 180px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-tag-label {
|
||||||
|
padding-left: 8px;
|
||||||
|
right: 40px;
|
||||||
|
font-weight: normal;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-styles {
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
.variable-item {
|
.variable-item {
|
||||||
.variable-name {
|
.variable-name {
|
||||||
|
@ -123,6 +123,8 @@ describe('VariableItem', () => {
|
|||||||
const customVariableData = {
|
const customVariableData = {
|
||||||
...mockCustomVariableData,
|
...mockCustomVariableData,
|
||||||
allSelected: true,
|
allSelected: true,
|
||||||
|
showALLOption: true,
|
||||||
|
multiSelect: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
@ -1,15 +1,29 @@
|
|||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
import './DashboardVariableSelection.styles.scss';
|
import './DashboardVariableSelection.styles.scss';
|
||||||
|
|
||||||
import { orange } from '@ant-design/colors';
|
import { orange } from '@ant-design/colors';
|
||||||
import { WarningOutlined } from '@ant-design/icons';
|
import { WarningOutlined } from '@ant-design/icons';
|
||||||
import { Input, Popover, Select, Typography } from 'antd';
|
import {
|
||||||
|
Checkbox,
|
||||||
|
Input,
|
||||||
|
Popover,
|
||||||
|
Select,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
|
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||||
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||||
import { debounce, isArray, isString } from 'lodash-es';
|
import { debounce, isArray, isString } from 'lodash-es';
|
||||||
import map from 'lodash-es/map';
|
import map from 'lodash-es/map';
|
||||||
import { memo, useEffect, useMemo, useState } from 'react';
|
import { ChangeEvent, memo, useEffect, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
|
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
|
||||||
@ -23,6 +37,11 @@ const ALL_SELECT_VALUE = '__ALL__';
|
|||||||
|
|
||||||
const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g;
|
const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g;
|
||||||
|
|
||||||
|
enum ToggleTagValue {
|
||||||
|
Only = 'Only',
|
||||||
|
All = 'All',
|
||||||
|
}
|
||||||
|
|
||||||
interface VariableItemProps {
|
interface VariableItemProps {
|
||||||
variableData: IDashboardVariable;
|
variableData: IDashboardVariable;
|
||||||
existingVariables: Record<string, IDashboardVariable>;
|
existingVariables: Record<string, IDashboardVariable>;
|
||||||
@ -37,8 +56,12 @@ interface VariableItemProps {
|
|||||||
|
|
||||||
const getSelectValue = (
|
const getSelectValue = (
|
||||||
selectedValue: IDashboardVariable['selectedValue'],
|
selectedValue: IDashboardVariable['selectedValue'],
|
||||||
|
variableData: IDashboardVariable,
|
||||||
): string | string[] => {
|
): string | string[] => {
|
||||||
if (Array.isArray(selectedValue)) {
|
if (Array.isArray(selectedValue)) {
|
||||||
|
if (!variableData.multiSelect && selectedValue.length === 1) {
|
||||||
|
return selectedValue[0]?.toString() || '';
|
||||||
|
}
|
||||||
return selectedValue.map((item) => item.toString());
|
return selectedValue.map((item) => item.toString());
|
||||||
}
|
}
|
||||||
return selectedValue?.toString() || '';
|
return selectedValue?.toString() || '';
|
||||||
@ -193,7 +216,7 @@ function VariableItem({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = (value: string | string[]): void => {
|
const handleChange = (value: string | string[]): void => {
|
||||||
if (variableData.name)
|
if (variableData.name) {
|
||||||
if (
|
if (
|
||||||
value === ALL_SELECT_VALUE ||
|
value === ALL_SELECT_VALUE ||
|
||||||
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
|
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
|
||||||
@ -203,25 +226,29 @@ function VariableItem({
|
|||||||
} else {
|
} else {
|
||||||
onValueUpdate(variableData.name, variableData.id, value, false);
|
onValueUpdate(variableData.name, variableData.id, value, false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// do not debounce the above function as we do not need debounce in select variables
|
// do not debounce the above function as we do not need debounce in select variables
|
||||||
const debouncedHandleChange = debounce(handleChange, 500);
|
const debouncedHandleChange = debounce(handleChange, 500);
|
||||||
|
|
||||||
const { selectedValue } = variableData;
|
const { selectedValue } = variableData;
|
||||||
const selectedValueStringified = useMemo(() => getSelectValue(selectedValue), [
|
const selectedValueStringified = useMemo(
|
||||||
selectedValue,
|
() => getSelectValue(selectedValue, variableData),
|
||||||
]);
|
[selectedValue, variableData],
|
||||||
|
);
|
||||||
|
|
||||||
const selectValue = variableData.allSelected
|
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
|
||||||
? 'ALL'
|
|
||||||
: selectedValueStringified;
|
|
||||||
|
|
||||||
const mode =
|
const selectValue =
|
||||||
|
variableData.allSelected && enableSelectAll
|
||||||
|
? 'ALL'
|
||||||
|
: selectedValueStringified;
|
||||||
|
|
||||||
|
const mode: 'multiple' | undefined =
|
||||||
variableData.multiSelect && !variableData.allSelected
|
variableData.multiSelect && !variableData.allSelected
|
||||||
? 'multiple'
|
? 'multiple'
|
||||||
: undefined;
|
: undefined;
|
||||||
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch options for CUSTOM Type
|
// Fetch options for CUSTOM Type
|
||||||
@ -231,6 +258,117 @@ function VariableItem({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [variableData.type, variableData.customValue]);
|
}, [variableData.type, variableData.customValue]);
|
||||||
|
|
||||||
|
const checkAll = (e: MouseEvent): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
const isChecked =
|
||||||
|
variableData.allSelected || selectValue.includes(ALL_SELECT_VALUE);
|
||||||
|
|
||||||
|
if (isChecked) {
|
||||||
|
handleChange([]);
|
||||||
|
} else {
|
||||||
|
handleChange(ALL_SELECT_VALUE);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOptionSelect = (
|
||||||
|
e: CheckboxChangeEvent,
|
||||||
|
option: string | number | boolean,
|
||||||
|
): void => {
|
||||||
|
const newSelectedValue = Array.isArray(selectedValue)
|
||||||
|
? ((selectedValue.filter(
|
||||||
|
(val) => val.toString() !== option.toString(),
|
||||||
|
) as unknown) as string[])
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!e.target.checked &&
|
||||||
|
Array.isArray(selectedValueStringified) &&
|
||||||
|
selectedValueStringified.includes(option.toString())
|
||||||
|
) {
|
||||||
|
if (newSelectedValue.length === 0) {
|
||||||
|
handleChange(ALL_SELECT_VALUE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newSelectedValue.length === 1) {
|
||||||
|
handleChange(newSelectedValue[0].toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleChange(newSelectedValue);
|
||||||
|
} else if (!e.target.checked && selectedValue === option.toString()) {
|
||||||
|
handleChange(ALL_SELECT_VALUE);
|
||||||
|
} else if (newSelectedValue.length === optionsData.length - 1) {
|
||||||
|
handleChange(ALL_SELECT_VALUE);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [optionState, setOptionState] = useState({
|
||||||
|
tag: '',
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
function currentToggleTagValue({
|
||||||
|
option,
|
||||||
|
}: {
|
||||||
|
option: string;
|
||||||
|
}): ToggleTagValue {
|
||||||
|
if (
|
||||||
|
option.toString() === selectValue ||
|
||||||
|
(Array.isArray(selectValue) &&
|
||||||
|
selectValue?.includes(option.toString()) &&
|
||||||
|
selectValue.length === 1)
|
||||||
|
) {
|
||||||
|
return ToggleTagValue.All;
|
||||||
|
}
|
||||||
|
return ToggleTagValue.Only;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToggle(e: ChangeEvent, option: string): void {
|
||||||
|
e.stopPropagation();
|
||||||
|
const mode = currentToggleTagValue({ option: option as string });
|
||||||
|
const isChecked =
|
||||||
|
variableData.allSelected ||
|
||||||
|
option.toString() === selectValue ||
|
||||||
|
(Array.isArray(selectValue) && selectValue?.includes(option.toString()));
|
||||||
|
|
||||||
|
if (isChecked) {
|
||||||
|
if (mode === ToggleTagValue.Only) {
|
||||||
|
handleChange(option.toString());
|
||||||
|
} else if (!variableData.multiSelect) {
|
||||||
|
handleChange(option.toString());
|
||||||
|
} else {
|
||||||
|
handleChange(ALL_SELECT_VALUE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleChange(option.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function retProps(
|
||||||
|
option: string,
|
||||||
|
): {
|
||||||
|
onMouseOver: () => void;
|
||||||
|
onMouseOut: () => void;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
onMouseOver: (): void =>
|
||||||
|
setOptionState({
|
||||||
|
tag: option.toString(),
|
||||||
|
visible: true,
|
||||||
|
}),
|
||||||
|
onMouseOut: (): void =>
|
||||||
|
setOptionState({
|
||||||
|
tag: option.toString(),
|
||||||
|
visible: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureValidOption = (option: string): boolean =>
|
||||||
|
!(
|
||||||
|
currentToggleTagValue({ option }) === ToggleTagValue.All && !enableSelectAll
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="variable-item">
|
<div className="variable-item">
|
||||||
<Typography.Text className="variable-name" ellipsis>
|
<Typography.Text className="variable-name" ellipsis>
|
||||||
@ -264,19 +402,35 @@ function VariableItem({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
bordered={false}
|
bordered={false}
|
||||||
placeholder="Select value"
|
placeholder="Select value"
|
||||||
placement="bottomRight"
|
placement="bottomLeft"
|
||||||
mode={mode}
|
mode={mode}
|
||||||
dropdownMatchSelectWidth={false}
|
|
||||||
style={SelectItemStyle}
|
style={SelectItemStyle}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
showSearch
|
showSearch
|
||||||
data-testid="variable-select"
|
data-testid="variable-select"
|
||||||
className="variable-select"
|
className="variable-select"
|
||||||
|
popupClassName="dropdown-styles"
|
||||||
|
maxTagCount={4}
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
|
// eslint-disable-next-line react/no-unstable-nested-components
|
||||||
|
tagRender={(props): JSX.Element => (
|
||||||
|
<Tag closable onClose={props.onClose}>
|
||||||
|
{props.value}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
// eslint-disable-next-line react/no-unstable-nested-components
|
||||||
|
maxTagPlaceholder={(omittedValues): JSX.Element => (
|
||||||
|
<Tooltip title={omittedValues.map(({ value }) => value).join(', ')}>
|
||||||
|
<span>+ {omittedValues.length} </span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{enableSelectAll && (
|
{enableSelectAll && (
|
||||||
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
|
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
|
||||||
ALL
|
<div className="all-label" onClick={(e): void => checkAll(e as any)}>
|
||||||
|
<Checkbox checked={variableData.allSelected} />
|
||||||
|
ALL
|
||||||
|
</div>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
)}
|
)}
|
||||||
{map(optionsData, (option) => (
|
{map(optionsData, (option) => (
|
||||||
@ -285,7 +439,45 @@ function VariableItem({
|
|||||||
key={option.toString()}
|
key={option.toString()}
|
||||||
value={option}
|
value={option}
|
||||||
>
|
>
|
||||||
{option.toString()}
|
<div
|
||||||
|
className={variableData.multiSelect ? 'dropdown-checkbox-label' : ''}
|
||||||
|
>
|
||||||
|
{variableData.multiSelect && (
|
||||||
|
<Checkbox
|
||||||
|
onChange={(e): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
handleOptionSelect(e, option);
|
||||||
|
}}
|
||||||
|
checked={
|
||||||
|
variableData.allSelected ||
|
||||||
|
option.toString() === selectValue ||
|
||||||
|
(Array.isArray(selectValue) &&
|
||||||
|
selectValue?.includes(option.toString()))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="dropdown-value"
|
||||||
|
{...retProps(option as string)}
|
||||||
|
onClick={(e): void => handleToggle(e as any, option as string)}
|
||||||
|
>
|
||||||
|
<Tooltip title={option.toString()} placement="bottomRight">
|
||||||
|
<Typography.Text ellipsis className="option-text">
|
||||||
|
{option.toString()}
|
||||||
|
</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{variableData.multiSelect &&
|
||||||
|
optionState.tag === option.toString() &&
|
||||||
|
optionState.visible &&
|
||||||
|
ensureValidOption(option as string) && (
|
||||||
|
<Typography.Text className="toggle-tag-label">
|
||||||
|
{currentToggleTagValue({ option: option as string })}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
@ -42,4 +42,5 @@ export const VariableValue = styled(Typography)`
|
|||||||
export const SelectItemStyle = {
|
export const SelectItemStyle = {
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
fontSize: '0.8rem',
|
fontSize: '0.8rem',
|
||||||
|
width: '100%',
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user