[Feat]: threshold in table (#4002)

* feat: threshold in table

* refactor: updated the message

* chore: some css fixes
This commit is contained in:
Rajat Dabade 2023-11-23 15:32:06 +05:30 committed by GitHub
parent e7f9c3981b
commit 4009ac83fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 259 additions and 33 deletions

View File

@ -1,4 +1,5 @@
export enum Events { export enum Events {
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE', UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE', UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
TABLE_COLUMNS_DATA = 'TABLE_COLUMNS_DATA',
} }

View File

@ -26,7 +26,12 @@ const GridPanelSwitch = forwardRef<
yAxisUnit, yAxisUnit,
thresholds, thresholds,
}, },
[PANEL_TYPES.TABLE]: { ...GRID_TABLE_CONFIG, data: panelData, query }, [PANEL_TYPES.TABLE]: {
...GRID_TABLE_CONFIG,
data: panelData,
query,
thresholds,
},
[PANEL_TYPES.LIST]: null, [PANEL_TYPES.LIST]: null,
[PANEL_TYPES.TRACE]: null, [PANEL_TYPES.TRACE]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null, [PANEL_TYPES.EMPTY_WIDGET]: null,

View File

@ -1,20 +1,86 @@
import { ExclamationCircleFilled } from '@ant-design/icons';
import { Space, Tooltip } from 'antd';
import { Events } from 'constants/events';
import { QueryTable } from 'container/QueryTable'; import { QueryTable } from 'container/QueryTable';
import { memo } from 'react'; import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery';
import { memo, ReactNode, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { eventEmitter } from 'utils/getEventEmitter';
import { WrapperStyled } from './styles'; import { WrapperStyled } from './styles';
import { GridTableComponentProps } from './types'; import { GridTableComponentProps } from './types';
import { findMatchingThreshold } from './utils';
function GridTableComponent({ function GridTableComponent({
data, data,
query, query,
thresholds,
...props ...props
}: GridTableComponentProps): JSX.Element { }: GridTableComponentProps): JSX.Element {
const { t } = useTranslation(['valueGraph']);
const { columns, dataSource } = useMemo(
() =>
createTableColumnsFromQuery({
query,
queryTableData: data,
}),
[data, query],
);
const newColumnData = columns.map((e) => ({
...e,
render: (text: string): ReactNode => {
const isNumber = !Number.isNaN(Number(text));
if (thresholds && isNumber) {
const { hasMultipleMatches, threshold } = findMatchingThreshold(
thresholds,
e.title as string,
Number(text),
);
const idx = thresholds.findIndex(
(t) => t.thresholdTableOptions === e.title,
);
if (idx !== -1) {
return (
<div
style={
threshold.thresholdFormat === 'Background'
? { backgroundColor: threshold.thresholdColor }
: { color: threshold.thresholdColor }
}
>
<Space>
{text}
{hasMultipleMatches && (
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
<ExclamationCircleFilled className="value-graph-icon" />
</Tooltip>
)}
</Space>
</div>
);
}
}
return <div>{text}</div>;
},
}));
useEffect(() => {
eventEmitter.emit(Events.TABLE_COLUMNS_DATA, {
columns: newColumnData,
dataSource,
});
}, [dataSource, newColumnData]);
return ( return (
<WrapperStyled> <WrapperStyled>
<QueryTable <QueryTable
query={query} query={query}
queryTableData={data} queryTableData={data}
loading={false} loading={false}
columns={newColumnData}
dataSource={dataSource}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...props} {...props}
/> />

View File

@ -1,10 +1,23 @@
import { TableProps } from 'antd'; import { TableProps } from 'antd';
import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces'; import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces';
import {
ThresholdOperators,
ThresholdProps,
} from 'container/NewWidget/RightContainer/Threshold/types';
import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
export type GridTableComponentProps = { query: Query } & Pick< export type GridTableComponentProps = {
LogsExplorerTableProps, query: Query;
'data' thresholds?: ThresholdProps[];
> & } & Pick<LogsExplorerTableProps, 'data'> &
Omit<TableProps<RowData>, 'columns' | 'dataSource'>; Omit<TableProps<RowData>, 'columns' | 'dataSource'>;
export type RequiredThresholdProps = Omit<
ThresholdProps,
'thresholdTableOptions' | 'thresholdOperator' | 'thresholdValue'
> & {
thresholdTableOptions: string;
thresholdOperator: ThresholdOperators;
thresholdValue: number;
};

View File

@ -0,0 +1,58 @@
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
// Helper function to evaluate the condition based on the operator
function evaluateCondition(
operator: string | undefined,
value: number,
thresholdValue: number,
): boolean {
switch (operator) {
case '>':
return value > thresholdValue;
case '<':
return value < thresholdValue;
case '>=':
return value >= thresholdValue;
case '<=':
return value <= thresholdValue;
case '==':
return value === thresholdValue;
default:
return false;
}
}
export function findMatchingThreshold(
thresholds: ThresholdProps[],
label: string,
value: number,
): {
threshold: ThresholdProps;
hasMultipleMatches: boolean;
} {
const matchingThresholds: ThresholdProps[] = [];
let hasMultipleMatches = false;
thresholds.forEach((threshold) => {
if (
threshold.thresholdValue !== undefined &&
threshold.thresholdTableOptions === label &&
evaluateCondition(
threshold.thresholdOperator,
value,
threshold.thresholdValue,
)
) {
matchingThresholds.push(threshold);
}
});
if (matchingThresholds.length > 1) {
hasMultipleMatches = true;
}
return {
threshold: matchingThresholds[0],
hasMultipleMatches,
};
}

View File

@ -1,6 +1,7 @@
.show-case-container { .show-case-container {
padding: 5px 15px; padding: 5px 15px;
border-radius: 5px; border-radius: 5px;
display: inline-block;
} }
.show-case-dark { .show-case-dark {

View File

@ -1,3 +1,4 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './Threshold.styles.scss'; import './Threshold.styles.scss';
import { CheckOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'; import { CheckOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
@ -40,6 +41,8 @@ function Threshold({
moveThreshold, moveThreshold,
selectedGraph, selectedGraph,
thresholdLabel = '', thresholdLabel = '',
tableOptions,
thresholdTableOptions = '',
}: ThresholdProps): JSX.Element { }: ThresholdProps): JSX.Element {
const [isEditMode, setIsEditMode] = useState<boolean>(isEditEnabled); const [isEditMode, setIsEditMode] = useState<boolean>(isEditEnabled);
const [operator, setOperator] = useState<string | number>( const [operator, setOperator] = useState<string | number>(
@ -52,6 +55,9 @@ function Threshold({
thresholdFormat, thresholdFormat,
); );
const [label, setLabel] = useState<string>(thresholdLabel); const [label, setLabel] = useState<string>(thresholdLabel);
const [tableSelectedOption, setTableSelectedOption] = useState<string>(
thresholdTableOptions,
);
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
@ -72,6 +78,7 @@ function Threshold({
thresholdUnit: unit, thresholdUnit: unit,
thresholdValue: value, thresholdValue: value,
thresholdLabel: label, thresholdLabel: label,
thresholdTableOptions: tableSelectedOption,
}; };
} }
return threshold; return threshold;
@ -104,6 +111,10 @@ function Threshold({
setFormat(value); setFormat(value);
}; };
const handleTableOptionsChange = (value: string): void => {
setTableSelectedOption(value);
};
const deleteHandler = (): void => { const deleteHandler = (): void => {
if (thresholdDeleteHandler) { if (thresholdDeleteHandler) {
thresholdDeleteHandler(index); thresholdDeleteHandler(index);
@ -203,7 +214,11 @@ function Threshold({
/> />
</div> </div>
<div> <div>
<Space> <Space
direction={
selectedGraph === PANEL_TYPES.TABLE ? 'vertical' : 'horizontal'
}
>
{selectedGraph === PANEL_TYPES.TIME_SERIES && ( {selectedGraph === PANEL_TYPES.TIME_SERIES && (
<> <>
<Typography.Text>Label</Typography.Text> <Typography.Text>Label</Typography.Text>
@ -219,10 +234,31 @@ function Threshold({
)} )}
</> </>
)} )}
{selectedGraph === PANEL_TYPES.VALUE && ( {(selectedGraph === PANEL_TYPES.VALUE ||
selectedGraph === PANEL_TYPES.TABLE) && (
<> <>
<Typography.Text>If value is</Typography.Text> <Typography.Text>
If value {selectedGraph === PANEL_TYPES.TABLE ? 'in' : 'is'}
</Typography.Text>
{isEditMode ? ( {isEditMode ? (
<>
{selectedGraph === PANEL_TYPES.TABLE && (
<Space>
<Select
style={{
minWidth: '150px',
backgroundColor,
borderRadius: '5px',
}}
defaultValue={tableSelectedOption}
options={tableOptions}
bordered={!isDarkMode}
showSearch
onChange={handleTableOptionsChange}
/>
<Typography.Text>is</Typography.Text>
</Space>
)}
<Select <Select
style={{ minWidth: '73px', backgroundColor }} style={{ minWidth: '73px', backgroundColor }}
defaultValue={operator} defaultValue={operator}
@ -230,8 +266,17 @@ function Threshold({
onChange={handleOperatorChange} onChange={handleOperatorChange}
bordered={!isDarkMode} bordered={!isDarkMode}
/> />
</>
) : ( ) : (
<>
{selectedGraph === PANEL_TYPES.TABLE && (
<Space>
<ShowCaseValue width="150px" value={tableSelectedOption} />
<Typography.Text>is</Typography.Text>
</Space>
)}
<ShowCaseValue width="49px" value={operator} /> <ShowCaseValue width="49px" value={operator} />
</>
)} )}
</> </>
)} )}
@ -280,7 +325,7 @@ function Threshold({
</> </>
) : ( ) : (
<> <>
<ShowCaseValue width="100px" value={<CustomColor color={color} />} /> <ShowCaseValue width="120px" value={<CustomColor color={color} />} />
<ShowCaseValue width="100px" value={format} /> <ShowCaseValue width="100px" value={format} />
</> </>
)} )}

View File

@ -1,9 +1,13 @@
import './ThresholdSelector.styles.scss'; import './ThresholdSelector.styles.scss';
import { Button, Typography } from 'antd'; import { Button, Typography } from 'antd';
import { useCallback } from 'react'; import { ColumnsType } from 'antd/es/table';
import { Events } from 'constants/events';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { useCallback, useEffect, useState } from 'react';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import { eventEmitter } from 'utils/getEventEmitter';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import Threshold from './Threshold'; import Threshold from './Threshold';
@ -15,6 +19,22 @@ function ThresholdSelector({
yAxisUnit, yAxisUnit,
selectedGraph, selectedGraph,
}: ThresholdSelectorProps): JSX.Element { }: ThresholdSelectorProps): JSX.Element {
const [tableOptions, setTableOptions] = useState<
Array<{ value: string; label: string }>
>([]);
useEffect(() => {
eventEmitter.on(
Events.TABLE_COLUMNS_DATA,
(data: { columns: ColumnsType<RowData>; dataSource: RowData[] }) => {
const newTableOptions = data.columns.map((e) => ({
value: e.title as string,
label: e.title as string,
}));
setTableOptions([...newTableOptions]);
},
);
}, []);
const moveThreshold = useCallback( const moveThreshold = useCallback(
(dragIndex: number, hoverIndex: number) => { (dragIndex: number, hoverIndex: number) => {
setThresholds((prevCards) => { setThresholds((prevCards) => {
@ -44,6 +64,7 @@ function ThresholdSelector({
moveThreshold, moveThreshold,
keyIndex: thresholds.length, keyIndex: thresholds.length,
selectedGraph, selectedGraph,
thresholdTableOptions: tableOptions[0]?.value || '',
}, },
]); ]);
}; };
@ -75,6 +96,8 @@ function ThresholdSelector({
moveThreshold={moveThreshold} moveThreshold={moveThreshold}
selectedGraph={selectedGraph} selectedGraph={selectedGraph}
thresholdLabel={threshold.thresholdLabel} thresholdLabel={threshold.thresholdLabel}
tableOptions={tableOptions}
thresholdTableOptions={threshold.thresholdTableOptions}
/> />
))} ))}
<Button className="threshold-selector-button" onClick={addThresholdHandler}> <Button className="threshold-selector-button" onClick={addThresholdHandler}>

View File

@ -1,7 +1,7 @@
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { Dispatch, ReactNode, SetStateAction } from 'react'; import { Dispatch, ReactNode, SetStateAction } from 'react';
type ThresholdOperators = '>' | '<' | '>=' | '<=' | '='; export type ThresholdOperators = '>' | '<' | '>=' | '<=' | '=';
export type ThresholdProps = { export type ThresholdProps = {
index: string; index: string;
@ -14,9 +14,11 @@ export type ThresholdProps = {
thresholdFormat?: 'Text' | 'Background'; thresholdFormat?: 'Text' | 'Background';
isEditEnabled?: boolean; isEditEnabled?: boolean;
thresholdLabel?: string; thresholdLabel?: string;
thresholdTableOptions?: string;
setThresholds?: Dispatch<SetStateAction<ThresholdProps[]>>; setThresholds?: Dispatch<SetStateAction<ThresholdProps[]>>;
moveThreshold: (dragIndex: number, hoverIndex: number) => void; moveThreshold: (dragIndex: number, hoverIndex: number) => void;
selectedGraph: PANEL_TYPES; selectedGraph: PANEL_TYPES;
tableOptions?: Array<{ value: string; label: string }>;
}; };
export type ShowCaseValueProps = { export type ShowCaseValueProps = {

View File

@ -24,7 +24,7 @@ export const showAsOptions: DefaultOptionType[] = [
export const panelTypeVsThreshold: { [key in PANEL_TYPES]: boolean } = { export const panelTypeVsThreshold: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.TIME_SERIES]: true, [PANEL_TYPES.TIME_SERIES]: true,
[PANEL_TYPES.VALUE]: true, [PANEL_TYPES.VALUE]: true,
[PANEL_TYPES.TABLE]: false, [PANEL_TYPES.TABLE]: true,
[PANEL_TYPES.LIST]: false, [PANEL_TYPES.LIST]: false,
[PANEL_TYPES.TRACE]: false, [PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false, [PANEL_TYPES.EMPTY_WIDGET]: false,

View File

@ -16,4 +16,6 @@ export type QueryTableProps = Omit<
modifyColumns?: (columns: ColumnsType<RowData>) => ColumnsType<RowData>; modifyColumns?: (columns: ColumnsType<RowData>) => ColumnsType<RowData>;
renderColumnCell?: Record<string, (record: RowData) => ReactNode>; renderColumnCell?: Record<string, (record: RowData) => ReactNode>;
downloadOption?: DownloadOptions; downloadOption?: DownloadOptions;
columns?: ColumnsType<RowData>;
dataSource?: RowData[];
}; };

View File

@ -17,25 +17,35 @@ export function QueryTable({
modifyColumns, modifyColumns,
renderColumnCell, renderColumnCell,
downloadOption, downloadOption,
columns,
dataSource,
...props ...props
}: QueryTableProps): JSX.Element { }: QueryTableProps): JSX.Element {
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {}; const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
const { servicename } = useParams<IServiceName>(); const { servicename } = useParams<IServiceName>();
const { loading } = props; const { loading } = props;
const { columns, dataSource } = useMemo( const { columns: newColumns, dataSource: newDataSource } = useMemo(() => {
() => if (columns && dataSource) {
createTableColumnsFromQuery({ return { columns, dataSource };
}
return createTableColumnsFromQuery({
query, query,
queryTableData, queryTableData,
renderActionCell, renderActionCell,
renderColumnCell, renderColumnCell,
}), });
[query, queryTableData, renderActionCell, renderColumnCell], }, [
); columns,
dataSource,
query,
queryTableData,
renderActionCell,
renderColumnCell,
]);
const downloadableData = createDownloadableData(dataSource); const downloadableData = createDownloadableData(newDataSource);
const tableColumns = modifyColumns ? modifyColumns(columns) : columns; const tableColumns = modifyColumns ? modifyColumns(newColumns) : newColumns;
return ( return (
<div className="query-table"> <div className="query-table">