diff --git a/src/src/components/GroupingItem/GroupingItem.tsx b/src/src/components/GroupingItem/GroupingItem.tsx index 3a5b17b3..fd9952e2 100644 --- a/src/src/components/GroupingItem/GroupingItem.tsx +++ b/src/src/components/GroupingItem/GroupingItem.tsx @@ -9,6 +9,8 @@ import { Icon } from 'components/kit'; import { IconName } from 'components/kit/Icon'; import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; +import { GroupNameEnum } from 'config/grouping/GroupingPopovers'; + import { IGroupingItemProps } from 'types/pages/components/GroupingItem/GroupingItem'; import './GroupingItem.scss'; @@ -50,7 +52,9 @@ function GroupingItem({
' = '>', + '<' = '<', + '>=' = '>=', + '<=' = '<=', +} + function ColorPopoverAdvanced({ onPersistenceChange, onGroupingPaletteChange, onShuffleChange, + onGroupingConditionsChange, + groupingSelectOptions, persistence, paletteIndex, groupingData, }: IGroupingPopoverAdvancedProps): React.FunctionComponentElement { + const [inputValue, setInputValue] = useState(''); + const [selectedField, setSelectedField] = + useState(null); + const [selectedOperator, setSelectedOperator] = useState( + IOperator['=='], + ); + const [selectedValue, setSelectedValue] = useState(''); + const [conditions, setConditions] = useState( + groupingData?.conditions?.color || [], + ); + + const onAddCondition = () => { + const condition: IGroupingCondition = { + fieldName: selectedField?.label || '', + operator: selectedOperator ?? IOperator['=='], + value: selectedValue, + }; + const conditionIndex = conditions.findIndex( + (c) => c.fieldName === condition.fieldName, + ); + const newConditions = + conditionIndex === -1 + ? [...conditions, condition] + : conditions.map((c, index) => + index === conditionIndex ? condition : c, + ); + setConditions(newConditions); + onGroupingConditionsChange?.(newConditions, GroupNameEnum.COLOR); + }; + + const onChangeField = (e: any, value: IGroupingSelectOption | null): void => { + if (!e || e.code !== 'Backspace' || inputValue.length === 0) + handleSelectField(value); + }; + + const onChangeOperator = (e: any, value: IOperator): void => { + handleSelectOperator(value || IOperator['==']); + }; + + const handleSelectField = (value: IGroupingSelectOption | null) => { + const newSelectedField = + selectedField?.value === value?.value ? null : value; + setInputValue(newSelectedField?.label || ''); + setSelectedField(newSelectedField); + }; + + const handleSelectOperator = (value?: IOperator) => { + setSelectedOperator(value ?? IOperator['==']); + }; + + const handleSelectValue = (value: string) => { + setSelectedValue(value); + }; + + const options = useMemo(() => { + const filteredOptions = groupingSelectOptions?.filter((item) => + item.label.toLowerCase().includes(inputValue.toLowerCase()), + ); + return ( + filteredOptions?.sort( + (a, b) => + a.label.toLowerCase().indexOf(inputValue.toLowerCase()) - + b.label.toLowerCase().indexOf(inputValue.toLowerCase()), + ) || [] + ); + }, [groupingSelectOptions, inputValue]); + function onPaletteChange(e: React.ChangeEvent) { let { value } = e.target; if (onGroupingPaletteChange) { @@ -110,6 +198,144 @@ function ColorPopoverAdvanced({ ))}
+
+ + group by condition + + + Group charts by conditions such as{' '} + + run.epochs > 30 + + . + +
+ {/* Add textbox to allow grouping by condition */} + option.group} + getOptionLabel={(option) => option.label} + getOptionSelected={(option, value) => + option.value === selectedField?.value + } + renderInput={(params: any) => ( + { + setInputValue(e.target?.value); + }, + }} + className='TextField__OutLined__Small' + variant='outlined' + placeholder='Select fields' + /> + )} + renderTags={() => null} // No tags for single selection + renderOption={(option, { selected }) => ( +
onChangeField(null, option)} + > + } + checkedIcon={} + style={{ marginRight: 4 }} + checked={selected} + /> + + {option.label} + +
+ )} + /> + {/* Dropdown for operator */} + ( + + )} + /> + {/* Textbox for the condition value */} + handleSelectValue(e.target.value)} + /> +
+ +
+ {conditions.map((condition, index) => ( + + {/* Show condition and button in same line */} + + {condition.fieldName} {condition.operator} {condition.value} + + + + ))} +
+
); diff --git a/src/src/services/models/explorer/createAppModel.ts b/src/src/services/models/explorer/createAppModel.ts index cf28c1f2..a4d231ce 100644 --- a/src/src/services/models/explorer/createAppModel.ts +++ b/src/src/services/models/explorer/createAppModel.ts @@ -212,7 +212,8 @@ import getLegendsData from 'utils/app/getLegendsData'; import onLegendsChange from 'utils/app/onLegendsChange'; import { getSelectedExperiments } from 'utils/app/getSelectedExperiments'; import { removeOldSelectedMetrics } from 'utils/app/removeOldSelectedMetrics'; -import evaluateCondition from 'utils/app/evaluateCondition'; +import { generateGroupValues } from 'utils/app/generateGroupValues'; +import { getConditionStrings } from 'utils/app/getConditionStrings'; import { AppDataTypeEnum, AppNameEnum } from './index'; @@ -1466,30 +1467,21 @@ function createAppModel(appConfig: IAppInitialConfig) { const grouping = configData!.grouping; const { paletteIndex = 0 } = grouping || {}; - const chartConditions: IGroupingCondition[] = - grouping.conditions?.chart || []; - const chartConditionStrings = chartConditions.map( - (condition) => - `${condition.fieldName} ${condition.operator} ${condition.value}`, - ); - - const strokeConditions: IGroupingCondition[] = - grouping.conditions?.stroke || []; - const strokeConditionStrings = strokeConditions.map( - (condition) => - `${condition.fieldName} ${condition.operator} ${condition.value}`, - ); - - const allConditions = chartConditions.concat(strokeConditions); - const allConditionStrings = allConditions.map( - (condition) => - `${condition.fieldName} ${condition.operator} ${condition.value}`, + const colorConditions = grouping.conditions?.color || []; + const colorConditionStrings = getConditionStrings(colorConditions); + const strokeConditions = grouping.conditions?.stroke || []; + const strokeConditionStrings = getConditionStrings(strokeConditions); + const chartConditions = grouping.conditions?.chart || []; + const chartConditionStrings = getConditionStrings(chartConditions); + const allConditions = colorConditions.concat( + strokeConditions, + chartConditions, ); const groupByColor = getFilteredGroupingOptions({ groupName: GroupNameEnum.COLOR, model, - }); + }).concat(colorConditionStrings); const groupByStroke = getFilteredGroupingOptions({ groupName: GroupNameEnum.STROKE, model, @@ -1515,63 +1507,13 @@ function createAppModel(appConfig: IAppInitialConfig) { ]); } - const groupValues: { - [key: string]: IMetricsCollection; - } = {}; - const groupingFields = _.uniq( groupByColor.concat(groupByStroke).concat(groupByChart), ); - for (let i = 0; i < data.length; i++) { - const groupValue: { [key: string]: any } = {}; - groupingFields.forEach((field) => { - groupValue[field] = getValue(data[i], field); - }); - - // Evaluate the conditions and update the row - allConditionStrings.forEach((conditionString, j) => { - // Evaluate the condition - const condition = allConditions[j]; - - // Get everything after the first dot in the field name - const fieldTypeAndName = condition.fieldName.split('.'); - const fieldType = fieldTypeAndName[0]; - const fieldName = fieldTypeAndName.slice(1).join('.'); - - // Flatten default run attributes and store them in a single object - const runAttributes = { - ...data[i].run.params, - ...data[i].run.props, - hash: data[i].run.hash, - name: - fieldType === 'metric' ? data[i].name : data[i].run.props.name, - tags: data[i].run.params.tags, - experiment: data[i].run.props.experiment?.name, - }; - - // Get the relevant attribute's value - const attributeValue = getValue(runAttributes, fieldName); - groupValue[conditionString] = evaluateCondition( - attributeValue, - condition, - ); - }); - - const groupKey = encode(groupValue); - if (groupValues.hasOwnProperty(groupKey)) { - groupValues[groupKey].data.push(data[i]); - } else { - groupValues[groupKey] = { - key: groupKey, - config: groupValue, - color: null, - dasharray: null, - chartIndex: 0, - data: [data[i]], - }; - } - } + const groupValues: { + [key: string]: IMetricsCollection; + } = generateGroupValues(data, allConditions, groupingFields); let colorIndex = 0; let dasharrayIndex = 0; diff --git a/src/src/utils/app/generateGroupValues.ts b/src/src/utils/app/generateGroupValues.ts new file mode 100644 index 00000000..361599f5 --- /dev/null +++ b/src/src/utils/app/generateGroupValues.ts @@ -0,0 +1,74 @@ +import { + IGroupingCondition, + IMetricsCollection, +} from 'types/services/models/metrics/metricsAppModel'; +import { IMetric } from 'types/services/models/metrics/metricModel'; + +import { getValue } from 'utils/helper'; +import { encode } from 'utils/encoder/encoder'; + +import evaluateCondition from './evaluateCondition'; +import { getConditionStrings } from './getConditionStrings'; + +export function generateGroupValues( + data: IMetric[], + allConditions: IGroupingCondition[], + groupingFields: string[], +) { + const groupValues: { + [key: string]: IMetricsCollection; + } = {}; + + const allConditionStrings = getConditionStrings(allConditions); + + for (let i = 0; i < data.length; i++) { + const groupValue: { [key: string]: any } = {}; + groupingFields.forEach((field) => { + groupValue[field] = getValue(data[i], field); + }); + + // Evaluate the conditions and update the row + allConditionStrings.forEach((conditionString, j) => { + // Evaluate the condition + const condition = allConditions[j]; + + // Get everything after the first dot in the field name + const fieldTypeAndName = condition.fieldName.split('.'); + const fieldType = fieldTypeAndName[0]; + const fieldName = fieldTypeAndName.slice(1).join('.'); + + // Flatten default run attributes and store them in a single object + const runAttributes = { + ...data[i].run.params, + ...data[i].run.props, + hash: data[i].run.hash, + name: fieldType === 'metric' ? data[i].name : data[i].run.props.name, + tags: data[i].run.params.tags, + experiment: data[i].run.props.experiment?.name, + }; + + // Get the relevant attribute's value + const attributeValue = getValue(runAttributes, fieldName); + groupValue[conditionString] = evaluateCondition( + attributeValue, + condition, + ); + }); + + const groupKey = encode(groupValue); + if (groupValues.hasOwnProperty(groupKey)) { + groupValues[groupKey].data.push(data[i]); + } else { + groupValues[groupKey] = { + key: groupKey, + config: groupValue, + color: null, + dasharray: null, + chartIndex: 0, + data: [data[i]], + }; + } + } + + return groupValues; +} diff --git a/src/src/utils/app/getConditionStrings.ts b/src/src/utils/app/getConditionStrings.ts new file mode 100644 index 00000000..0c51e685 --- /dev/null +++ b/src/src/utils/app/getConditionStrings.ts @@ -0,0 +1,12 @@ +import { IGroupingCondition } from 'types/services/models/metrics/metricsAppModel'; + +/** + * Get the list of conditions as strings + * @param conditions the list of IGroupingCondition + * @returns the list of conditions as strings + */ +export function getConditionStrings(conditions: IGroupingCondition[]) { + return conditions.map((condition) => { + return `${condition.fieldName} ${condition.operator} ${condition.value}`; + }); +} diff --git a/src/src/utils/app/getLegendsData.tsx b/src/src/utils/app/getLegendsData.tsx index e0e2d001..2ab836c9 100644 --- a/src/src/utils/app/getLegendsData.tsx +++ b/src/src/utils/app/getLegendsData.tsx @@ -39,7 +39,7 @@ function getLegendsData( const groupConfig = groupingConfig[groupName]; const groupedItemPropKeys = - groupName === GroupNameEnum.ROW || groupName === GroupNameEnum.COLOR + groupName === GroupNameEnum.ROW ? groupConfig || [] : groupConfig?.concat( groupingConfig.conditions?.[groupName].map(