From 503ed45a990a6f6431d3ae1ccfa7eabdb119152f Mon Sep 17 00:00:00 2001 From: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:51:22 +0530 Subject: [PATCH] fix: fixed threshold for columns with units (#6079) * fix: fixed threshold for columns with units * fix: added interunit and category conversion for handling threshold across different unit types * fix: added invalid comparison text and removed unsupported unit categories * fix: cleanup and multiple threshold and state change handling * fix: restrict category select to only columnUnit group * fix: restricted column name from threshold option * fix: removed console log * fix: resolved comments and some refactoring * fix: resolved comments and some refactoring --- .../container/GridTableComponent/index.tsx | 12 +- .../src/container/GridTableComponent/utils.ts | 38 +++- .../Threshold/Threshold.styles.scss | 10 ++ .../RightContainer/Threshold/Threshold.tsx | 21 ++- .../Threshold/ThresholdSelector.tsx | 47 ++--- .../RightContainer/Threshold/types.ts | 3 + .../NewWidget/RightContainer/constants.ts | 8 - .../RightContainer/dataFormatCategories.ts | 165 ++++++++++++++++++ .../NewWidget/RightContainer/index.tsx | 1 + frontend/src/container/NewWidget/utils.ts | 48 ++++- 10 files changed, 313 insertions(+), 40 deletions(-) diff --git a/frontend/src/container/GridTableComponent/index.tsx b/frontend/src/container/GridTableComponent/index.tsx index dfa90b8255..63084be5f3 100644 --- a/frontend/src/container/GridTableComponent/index.tsx +++ b/frontend/src/container/GridTableComponent/index.tsx @@ -97,13 +97,19 @@ function GridTableComponent({ const newColumnData = columns.map((e) => ({ ...e, - render: (text: string): ReactNode => { - const isNumber = !Number.isNaN(Number(text)); + render: (text: string, ...rest: any): ReactNode => { + let textForThreshold = text; + if (columnUnits && columnUnits?.[e.title as string]) { + textForThreshold = rest[0][`${e.title}_without_unit`]; + } + const isNumber = !Number.isNaN(Number(textForThreshold)); + if (thresholds && isNumber) { const { hasMultipleMatches, threshold } = findMatchingThreshold( thresholds, e.title as string, - Number(text), + Number(textForThreshold), + columnUnits?.[e.title as string], ); const idx = thresholds.findIndex( diff --git a/frontend/src/container/GridTableComponent/utils.ts b/frontend/src/container/GridTableComponent/utils.ts index acd58af62d..52a4a7810b 100644 --- a/frontend/src/container/GridTableComponent/utils.ts +++ b/frontend/src/container/GridTableComponent/utils.ts @@ -1,5 +1,6 @@ /* eslint-disable sonarjs/cognitive-complexity */ import { ColumnsType, ColumnType } from 'antd/es/table'; +import { convertUnit } from 'container/NewWidget/RightContainer/dataFormatCategories'; import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types'; import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config'; import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces'; @@ -30,10 +31,39 @@ function evaluateCondition( } } +/** + * Evaluates whether a given value meets a specified threshold condition. + * It first converts the value to the appropriate unit if a threshold unit is provided, + * and then checks the condition using the specified operator. + * + * @param value - The value to be evaluated. + * @param thresholdValue - The threshold value to compare against. + * @param thresholdOperator - The operator used for comparison (e.g., '>', '<', '=='). + * @param thresholdUnit - The unit to which the value should be converted. + * @param columnUnit - The current unit of the value. + * @returns A boolean indicating whether the value meets the threshold condition. + */ +function evaluateThresholdWithConvertedValue( + value: number, + thresholdValue: number, + thresholdOperator?: string, + thresholdUnit?: string, + columnUnit?: string, +): boolean { + const convertedValue = convertUnit(value, columnUnit, thresholdUnit); + + if (convertedValue) { + return evaluateCondition(thresholdOperator, convertedValue, thresholdValue); + } + + return evaluateCondition(thresholdOperator, value, thresholdValue); +} + export function findMatchingThreshold( thresholds: ThresholdProps[], label: string, value: number, + columnUnit?: string, ): { threshold: ThresholdProps; hasMultipleMatches: boolean; @@ -45,10 +75,12 @@ export function findMatchingThreshold( if ( threshold.thresholdValue !== undefined && threshold.thresholdTableOptions === label && - evaluateCondition( - threshold.thresholdOperator, + evaluateThresholdWithConvertedValue( value, - threshold.thresholdValue, + threshold?.thresholdValue, + threshold.thresholdOperator, + threshold.thresholdUnit, + columnUnit, ) ) { matchingThresholds.push(threshold); diff --git a/frontend/src/container/NewWidget/RightContainer/Threshold/Threshold.styles.scss b/frontend/src/container/NewWidget/RightContainer/Threshold/Threshold.styles.scss index 091df7e750..15b3d05f4e 100644 --- a/frontend/src/container/NewWidget/RightContainer/Threshold/Threshold.styles.scss +++ b/frontend/src/container/NewWidget/RightContainer/Threshold/Threshold.styles.scss @@ -297,6 +297,16 @@ box-shadow: none; } } + + .invalid-unit { + color: var(--bg-vanilla-400); + font-family: 'Giest Mono'; + font-size: 11px; + font-style: normal; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.48px; + } } .threshold-card-container:hover { diff --git a/frontend/src/container/NewWidget/RightContainer/Threshold/Threshold.tsx b/frontend/src/container/NewWidget/RightContainer/Threshold/Threshold.tsx index dbcdda1d20..0bd0e47acf 100644 --- a/frontend/src/container/NewWidget/RightContainer/Threshold/Threshold.tsx +++ b/frontend/src/container/NewWidget/RightContainer/Threshold/Threshold.tsx @@ -3,17 +3,18 @@ import './Threshold.styles.scss'; import { Button, Input, InputNumber, Select, Space, Typography } from 'antd'; import { PANEL_TYPES } from 'constants/queryBuilder'; +import { unitOptions } from 'container/NewWidget/utils'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { Check, Pencil, Trash2, X } from 'lucide-react'; -import { useRef, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import { useDrag, useDrop, XYCoord } from 'react-dnd'; import { operatorOptions, panelTypeVsDragAndDrop, showAsOptions, - unitOptions, } from '../constants'; +import { convertUnit } from '../dataFormatCategories'; import ColorSelector from './ColorSelector'; import CustomColor from './CustomColor'; import ShowCaseValue from './ShowCaseValue'; @@ -40,6 +41,7 @@ function Threshold({ thresholdLabel = '', tableOptions, thresholdTableOptions = '', + columnUnits, }: ThresholdProps): JSX.Element { const [isEditMode, setIsEditMode] = useState(isEditEnabled); const [operator, setOperator] = useState( @@ -192,6 +194,13 @@ function Threshold({ const allowDragAndDrop = panelTypeVsDragAndDrop[selectedGraph]; + const isInvalidUnitComparison = useMemo( + () => + unit !== 'none' && + convertUnit(value, unit, columnUnits?.[tableSelectedOption]) === null, + [unit, value, columnUnits, tableSelectedOption], + ); + return (
)}
+ {isInvalidUnitComparison && ( + + Threshold unit ({unit}) is not valid in comparison with the column unit ( + {columnUnits?.[tableSelectedOption] || 'none'}) + + )} {isEditMode && (
diff --git a/frontend/src/container/NewWidget/RightContainer/Threshold/types.ts b/frontend/src/container/NewWidget/RightContainer/Threshold/types.ts index 820a621d84..6f09f2136e 100644 --- a/frontend/src/container/NewWidget/RightContainer/Threshold/types.ts +++ b/frontend/src/container/NewWidget/RightContainer/Threshold/types.ts @@ -1,5 +1,6 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; import { Dispatch, ReactNode, SetStateAction } from 'react'; +import { ColumnUnit } from 'types/api/dashboard/getAll'; export type ThresholdOperators = '>' | '<' | '>=' | '<=' | '='; @@ -19,6 +20,7 @@ export type ThresholdProps = { moveThreshold: (dragIndex: number, hoverIndex: number) => void; selectedGraph: PANEL_TYPES; tableOptions?: Array<{ value: string; label: string }>; + columnUnits?: ColumnUnit; }; export type ShowCaseValueProps = { @@ -36,4 +38,5 @@ export type ThresholdSelectorProps = { thresholds: ThresholdProps[]; setThresholds: Dispatch>; selectedGraph: PANEL_TYPES; + columnUnits: ColumnUnit; }; diff --git a/frontend/src/container/NewWidget/RightContainer/constants.ts b/frontend/src/container/NewWidget/RightContainer/constants.ts index 03cee96d21..1fad229ed6 100644 --- a/frontend/src/container/NewWidget/RightContainer/constants.ts +++ b/frontend/src/container/NewWidget/RightContainer/constants.ts @@ -1,8 +1,5 @@ import { DefaultOptionType } from 'antd/es/select'; import { PANEL_TYPES } from 'constants/queryBuilder'; -import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config'; - -import { getCategorySelectOptionByName } from './alertFomatCategories'; export const operatorOptions: DefaultOptionType[] = [ { value: '>', label: '>' }, @@ -11,11 +8,6 @@ export const operatorOptions: DefaultOptionType[] = [ { value: '<=', label: '<=' }, ]; -export const unitOptions = categoryToSupport.map((category) => ({ - label: category, - options: getCategorySelectOptionByName(category), -})); - export const showAsOptions: DefaultOptionType[] = [ { value: 'Text', label: 'Text' }, { value: 'Background', label: 'Background' }, diff --git a/frontend/src/container/NewWidget/RightContainer/dataFormatCategories.ts b/frontend/src/container/NewWidget/RightContainer/dataFormatCategories.ts index c1b4944d53..ea5bc2ab55 100644 --- a/frontend/src/container/NewWidget/RightContainer/dataFormatCategories.ts +++ b/frontend/src/container/NewWidget/RightContainer/dataFormatCategories.ts @@ -438,3 +438,168 @@ export const dataTypeCategories: DataTypeCategories = [ export const flattenedCategories = flattenDeep( dataTypeCategories.map((category) => category.formats), ); + +type ConversionFactors = { + [key: string]: { + [key: string]: number | null; + }; +}; + +// Object containing conversion factors for various categories and formats +const conversionFactors: ConversionFactors = { + [CategoryNames.Time]: { + [TimeFormats.Hertz]: 1, + [TimeFormats.Nanoseconds]: 1e-9, + [TimeFormats.Microseconds]: 1e-6, + [TimeFormats.Milliseconds]: 1e-3, + [TimeFormats.Seconds]: 1, + [TimeFormats.Minutes]: 60, + [TimeFormats.Hours]: 3600, + [TimeFormats.Days]: 86400, + [TimeFormats.DurationMs]: 1e-3, + [TimeFormats.DurationS]: 1, + [TimeFormats.DurationHms]: null, // Requires special handling + [TimeFormats.DurationDhms]: null, // Requires special handling + [TimeFormats.Timeticks]: null, // Requires special handling + [TimeFormats.ClockMs]: 1e-3, + [TimeFormats.ClockS]: 1, + }, + [CategoryNames.Throughput]: { + [ThroughputFormats.CountsPerSec]: 1, + [ThroughputFormats.OpsPerSec]: 1, + [ThroughputFormats.RequestsPerSec]: 1, + [ThroughputFormats.ReadsPerSec]: 1, + [ThroughputFormats.WritesPerSec]: 1, + [ThroughputFormats.IOOpsPerSec]: 1, + [ThroughputFormats.CountsPerMin]: 1 / 60, + [ThroughputFormats.OpsPerMin]: 1 / 60, + [ThroughputFormats.ReadsPerMin]: 1 / 60, + [ThroughputFormats.WritesPerMin]: 1 / 60, + }, + [CategoryNames.Data]: { + [DataFormats.BytesIEC]: 1, + [DataFormats.BytesSI]: 1, + [DataFormats.BitsIEC]: 0.125, + [DataFormats.BitsSI]: 0.125, + [DataFormats.KibiBytes]: 1024, + [DataFormats.KiloBytes]: 1000, + [DataFormats.MebiBytes]: 1048576, + [DataFormats.MegaBytes]: 1000000, + [DataFormats.GibiBytes]: 1073741824, + [DataFormats.GigaBytes]: 1000000000, + [DataFormats.TebiBytes]: 1099511627776, + [DataFormats.TeraBytes]: 1000000000000, + [DataFormats.PebiBytes]: 1125899906842624, + [DataFormats.PetaBytes]: 1000000000000000, + }, + [CategoryNames.DataRate]: { + [DataRateFormats.PacketsPerSec]: null, // Cannot convert directly to other data rates + [DataRateFormats.BytesPerSecIEC]: 1, + [DataRateFormats.BytesPerSecSI]: 1, + [DataRateFormats.BitsPerSecIEC]: 0.125, + [DataRateFormats.BitsPerSecSI]: 0.125, + [DataRateFormats.KibiBytesPerSec]: 1024, + [DataRateFormats.KibiBitsPerSec]: 128, + [DataRateFormats.KiloBytesPerSec]: 1000, + [DataRateFormats.KiloBitsPerSec]: 125, + [DataRateFormats.MebiBytesPerSec]: 1048576, + [DataRateFormats.MebiBitsPerSec]: 131072, + [DataRateFormats.MegaBytesPerSec]: 1000000, + [DataRateFormats.MegaBitsPerSec]: 125000, + [DataRateFormats.GibiBytesPerSec]: 1073741824, + [DataRateFormats.GibiBitsPerSec]: 134217728, + [DataRateFormats.GigaBytesPerSec]: 1000000000, + [DataRateFormats.GigaBitsPerSec]: 125000000, + [DataRateFormats.TebiBytesPerSec]: 1099511627776, + [DataRateFormats.TebiBitsPerSec]: 137438953472, + [DataRateFormats.TeraBytesPerSec]: 1000000000000, + [DataRateFormats.TeraBitsPerSec]: 125000000000, + [DataRateFormats.PebiBytesPerSec]: 1125899906842624, + [DataRateFormats.PebiBitsPerSec]: 140737488355328, + [DataRateFormats.PetaBytesPerSec]: 1000000000000000, + [DataRateFormats.PetaBitsPerSec]: 125000000000000, + }, + [CategoryNames.Miscellaneous]: { + [MiscellaneousFormats.None]: null, + [MiscellaneousFormats.String]: null, + [MiscellaneousFormats.Short]: null, + [MiscellaneousFormats.Percent]: 1, + [MiscellaneousFormats.PercentUnit]: 100, + [MiscellaneousFormats.Humidity]: 1, + [MiscellaneousFormats.Decibel]: null, + [MiscellaneousFormats.Hexadecimal0x]: null, + [MiscellaneousFormats.Hexadecimal]: null, + [MiscellaneousFormats.ScientificNotation]: null, + [MiscellaneousFormats.LocaleFormat]: null, + [MiscellaneousFormats.Pixels]: null, + }, + [CategoryNames.Boolean]: { + [BooleanFormats.TRUE_FALSE]: null, // Not convertible + [BooleanFormats.YES_NO]: null, // Not convertible + [BooleanFormats.ON_OFF]: null, // Not convertible + }, +}; + +// Function to get the conversion factor between two units in a specific category +function getConversionFactor( + fromUnit: string, + toUnit: string, + category: CategoryNames, +): number | null { + // Retrieves the conversion factors for the specified category + const categoryFactors = conversionFactors[category]; + if (!categoryFactors) { + return null; // Returns null if the category does not exist + } + const fromFactor = categoryFactors[fromUnit]; + const toFactor = categoryFactors[toUnit]; + if ( + fromFactor === undefined || + toFactor === undefined || + fromFactor === null || + toFactor === null + ) { + return null; // Returns null if either unit does not exist or is not convertible + } + return fromFactor / toFactor; // Returns the conversion factor ratio +} + +// Function to convert a value from one unit to another +export function convertUnit( + value: number, + fromUnitId?: string, + toUnitId?: string, +): number | null { + let fromUnit: string | undefined; + let toUnit: string | undefined; + + // Finds the category that contains the specified units and extracts fromUnit and toUnit using array methods + const category = dataTypeCategories.find((category) => + category.formats.some((format) => { + if (format.id === fromUnitId) fromUnit = format.id; + if (format.id === toUnitId) toUnit = format.id; + return fromUnit && toUnit; // Break out early if both units are found + }), + ); + + if (!category || !fromUnit || !toUnit) return null; // Return null if category or units are not found + + // Gets the conversion factor for the specified units + const conversionFactor = getConversionFactor( + fromUnit, + toUnit, + category.name as any, + ); + if (conversionFactor === null) return null; // Return null if conversion is not possible + + return value * conversionFactor; +} + +// Function to get the category name for a given unit ID +export const getCategoryName = (unitId: string): CategoryNames | null => { + // Finds the category that contains the specified unit ID + const foundCategory = dataTypeCategories.find((category) => + category.formats.some((format) => format.id === unitId), + ); + return foundCategory ? (foundCategory.name as CategoryNames) : null; +}; diff --git a/frontend/src/container/NewWidget/RightContainer/index.tsx b/frontend/src/container/NewWidget/RightContainer/index.tsx index 43e3b5611d..55968c5aee 100644 --- a/frontend/src/container/NewWidget/RightContainer/index.tsx +++ b/frontend/src/container/NewWidget/RightContainer/index.tsx @@ -311,6 +311,7 @@ function RightContainer({ setThresholds={setThresholds} yAxisUnit={yAxisUnit} selectedGraph={selectedGraph} + columnUnits={columnUnits} /> )} diff --git a/frontend/src/container/NewWidget/utils.ts b/frontend/src/container/NewWidget/utils.ts index f8eef3157d..cb498ef932 100644 --- a/frontend/src/container/NewWidget/utils.ts +++ b/frontend/src/container/NewWidget/utils.ts @@ -1,3 +1,4 @@ +import { DefaultOptionType } from 'antd/es/select'; import { omitIdFromQuery } from 'components/ExplorerCard/utils'; import { initialQueryBuilderFormValuesMap, @@ -8,12 +9,19 @@ import { listViewInitialTraceQuery, PANEL_TYPES_INITIAL_QUERY, } from 'container/NewDashboard/ComponentsSlider/constants'; -import { cloneDeep, isEqual, set, unset } from 'lodash-es'; +import { categoryToSupport } from 'container/QueryBuilder/filters/BuilderUnitsFilter/config'; +import { cloneDeep, isEmpty, isEqual, set, unset } from 'lodash-es'; import { Widgets } from 'types/api/dashboard/getAll'; import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; import { DataSource } from 'types/common/queryBuilder'; +import { + dataTypeCategories, + getCategoryName, +} from './RightContainer/dataFormatCategories'; +import { CategoryNames } from './RightContainer/types'; + export const getIsQueryModified = ( currentQuery: Query, stagedQuery: Query | null, @@ -529,3 +537,41 @@ export const PANEL_TYPE_TO_QUERY_TYPES: Record = { EQueryType.PROM, ], }; + +/** + * Retrieves a list of category select options based on the provided category name. + * If the category is found, it maps the formats to an array of objects containing + * the label and value for each format. + */ +export const getCategorySelectOptionByName = ( + name?: CategoryNames | string, +): DefaultOptionType[] => + dataTypeCategories + .find((category) => category.name === name) + ?.formats.map((format) => ({ + label: format.name, + value: format.id, + })) || []; + +/** + * Generates unit options based on the provided column unit. + * It first retrieves the category name associated with the column unit. + * If the category is empty, it maps all supported categories to their respective + * select options. If a valid category is found, it filters the supported categories + * to return only the options for the matched category. + */ +export const unitOptions = (columnUnit: string): DefaultOptionType[] => { + const category = getCategoryName(columnUnit); + if (isEmpty(category)) { + return categoryToSupport.map((category) => ({ + label: category, + options: getCategorySelectOptionByName(category), + })); + } + return categoryToSupport + .filter((supportedCategory) => supportedCategory === category) + .map((filteredCategory) => ({ + label: filteredCategory, + options: getCategorySelectOptionByName(filteredCategory), + })); +};