diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss index f610198f4a..f7fcb83a53 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/DashboardVariableSelection.styles.scss @@ -40,12 +40,46 @@ } .variable-select { - .ant-select-dropdown { - max-width: 300px; + .ant-select-item { + 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 { .variable-item { .variable-name { diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx index cd8b23ea46..14f20347d0 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.test.tsx @@ -123,6 +123,8 @@ describe('VariableItem', () => { const customVariableData = { ...mockCustomVariableData, allSelected: true, + showALLOption: true, + multiSelect: true, }; render( diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx index 2a14aa19e5..e0393ea163 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/VariableItem.tsx @@ -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 { orange } from '@ant-design/colors'; 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 { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser'; import sortValues from 'lib/dashbaordVariables/sortVariableValues'; import { debounce, isArray, isString } from 'lodash-es'; 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 { IDashboardVariable } from 'types/api/dashboard/getAll'; import { VariableResponseProps } from 'types/api/dashboard/variables/query'; @@ -23,6 +37,11 @@ const ALL_SELECT_VALUE = '__ALL__'; const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g; +enum ToggleTagValue { + Only = 'Only', + All = 'All', +} + interface VariableItemProps { variableData: IDashboardVariable; existingVariables: Record; @@ -37,8 +56,12 @@ interface VariableItemProps { const getSelectValue = ( selectedValue: IDashboardVariable['selectedValue'], + variableData: IDashboardVariable, ): string | string[] => { if (Array.isArray(selectedValue)) { + if (!variableData.multiSelect && selectedValue.length === 1) { + return selectedValue[0]?.toString() || ''; + } return selectedValue.map((item) => item.toString()); } return selectedValue?.toString() || ''; @@ -193,7 +216,7 @@ function VariableItem({ }); const handleChange = (value: string | string[]): void => { - if (variableData.name) + if (variableData.name) { if ( value === ALL_SELECT_VALUE || (Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) || @@ -203,25 +226,29 @@ function VariableItem({ } else { onValueUpdate(variableData.name, variableData.id, value, false); } + } }; // do not debounce the above function as we do not need debounce in select variables const debouncedHandleChange = debounce(handleChange, 500); const { selectedValue } = variableData; - const selectedValueStringified = useMemo(() => getSelectValue(selectedValue), [ - selectedValue, - ]); + const selectedValueStringified = useMemo( + () => getSelectValue(selectedValue, variableData), + [selectedValue, variableData], + ); - const selectValue = variableData.allSelected - ? 'ALL' - : selectedValueStringified; + const enableSelectAll = variableData.multiSelect && variableData.showALLOption; - const mode = + const selectValue = + variableData.allSelected && enableSelectAll + ? 'ALL' + : selectedValueStringified; + + const mode: 'multiple' | undefined = variableData.multiSelect && !variableData.allSelected ? 'multiple' : undefined; - const enableSelectAll = variableData.multiSelect && variableData.showALLOption; useEffect(() => { // Fetch options for CUSTOM Type @@ -231,6 +258,117 @@ function VariableItem({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [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 (
@@ -264,19 +402,35 @@ function VariableItem({ onChange={handleChange} bordered={false} placeholder="Select value" - placement="bottomRight" + placement="bottomLeft" mode={mode} - dropdownMatchSelectWidth={false} style={SelectItemStyle} loading={isLoading} showSearch data-testid="variable-select" className="variable-select" + popupClassName="dropdown-styles" + maxTagCount={4} getPopupContainer={popupContainer} + // eslint-disable-next-line react/no-unstable-nested-components + tagRender={(props): JSX.Element => ( + + {props.value} + + )} + // eslint-disable-next-line react/no-unstable-nested-components + maxTagPlaceholder={(omittedValues): JSX.Element => ( + value).join(', ')}> + + {omittedValues.length} + + )} > {enableSelectAll && ( - ALL +
checkAll(e as any)}> + + ALL +
)} {map(optionsData, (option) => ( @@ -285,7 +439,45 @@ function VariableItem({ key={option.toString()} value={option} > - {option.toString()} +
+ {variableData.multiSelect && ( + { + e.stopPropagation(); + e.preventDefault(); + handleOptionSelect(e, option); + }} + checked={ + variableData.allSelected || + option.toString() === selectValue || + (Array.isArray(selectValue) && + selectValue?.includes(option.toString())) + } + /> + )} +
handleToggle(e as any, option as string)} + > + + + {option.toString()} + + + + {variableData.multiSelect && + optionState.tag === option.toString() && + optionState.visible && + ensureValidOption(option as string) && ( + + {currentToggleTagValue({ option: option as string })} + + )} +
+
))} diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts b/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts index 5c5de3e97e..02530dbd0f 100644 --- a/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/styles.ts @@ -42,4 +42,5 @@ export const VariableValue = styled(Typography)` export const SelectItemStyle = { minWidth: 120, fontSize: '0.8rem', + width: '100%', };