From fb3bca7e0de824dc9ab849769963ce7aae2d8734 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:53:21 +0200 Subject: [PATCH 01/11] Undefined AlignmentConfig (#91) --- src/src/services/models/explorer/createAppModel.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/src/services/models/explorer/createAppModel.ts b/src/src/services/models/explorer/createAppModel.ts index 56fbc4b3..c50e3a99 100644 --- a/src/src/services/models/explorer/createAppModel.ts +++ b/src/src/services/models/explorer/createAppModel.ts @@ -1079,7 +1079,7 @@ function createAppModel(appConfig: IAppInitialConfig) { x_axis_iters, } = filterMetricsData( trace, - configData?.chart?.alignmentConfig.type, + configData?.chart?.alignmentConfigs[0].type, configData?.chart?.axesScaleType, ); @@ -1435,7 +1435,8 @@ function createAppModel(appConfig: IAppInitialConfig) { function alignData( data: IMetricsCollection[], type: AlignmentOptionsEnum = model.getState()!.config!.chart - ?.alignmentConfig.type, + ?.alignmentConfigs[0].type, + chartId: number = 0, ): IMetricsCollection[] { const alignmentObj: { [key: string]: Function } = { [AlignmentOptionsEnum.STEP]: alignByStep, @@ -1447,7 +1448,11 @@ function createAppModel(appConfig: IAppInitialConfig) { throw new Error('Unknown value for X axis alignment'); }, }; - const alignment = alignmentObj[type] || alignmentObj.default; + const alignmentConfig = + model.getState()!.config!.chart?.alignmentConfigs[chartId]; + const alignment = + alignmentObj[alignmentConfig.type] || alignmentObj.default; + return alignment(data, model); } From 48319f378f57401af1dcd744aafddebb59a8f2c7 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Mon, 3 Jun 2024 09:40:01 -0700 Subject: [PATCH 02/11] Catch CreateResourceError and redirect to login (#90) --- src/src/modules/core/utils/createResource.ts | 20 ++++++++++++++++++-- src/src/services/NetworkService/types.ts | 5 +++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/src/modules/core/utils/createResource.ts b/src/src/modules/core/utils/createResource.ts index 357032a4..45f4a33c 100644 --- a/src/src/modules/core/utils/createResource.ts +++ b/src/src/modules/core/utils/createResource.ts @@ -1,12 +1,21 @@ import { RequestOptions } from 'https'; import create from 'zustand'; +import { getPrefix } from 'config/config'; + +import { ErrorCode } from 'services/NetworkService'; + export interface IResourceState { data: T | null; loading: boolean; error: any; } +interface CreateResourceError { + error_code: string; + message: string; +} + const defaultState = { data: null, loading: true, @@ -18,8 +27,15 @@ function createResource(getter: any) { async function fetchData(args?: GetterArgs) { state.setState({ loading: true }); - const data = await getter(args); - state.setState({ data, loading: false }); + try { + const data = await getter(args); + state.setState({ data, loading: false }); + } catch (error: CreateResourceError | any) { + if (error?.error_code === ErrorCode.RESOURCE_DOES_NOT_EXIST) { + window.location.href = getPrefix(); + } + state.setState({ error, loading: false }); + } } function destroy() { state.destroy(); diff --git a/src/src/services/NetworkService/types.ts b/src/src/services/NetworkService/types.ts index 5c35af9e..c07d6001 100644 --- a/src/src/services/NetworkService/types.ts +++ b/src/src/services/NetworkService/types.ts @@ -18,6 +18,11 @@ export enum HttpErrorMessages { INVALID_RESPONSE_DATA = 'Invalid Response Data', SERVER_IS_UNAVAILABLE = 'The server is unavailable.', RESPONSE_PARSING_ERROR = 'Unable to parse response.', + RESOURCE_DOES_NOT_EXIST = 'Resource does not exist.', +} + +export enum ErrorCode { + RESOURCE_DOES_NOT_EXIST = 'RESOURCE_DOES_NOT_EXIST', } export type RequestOptions = { From e3d2df125a5c41ca98775b232d755c78630836b7 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Wed, 5 Jun 2024 14:31:59 -0700 Subject: [PATCH 03/11] Add conditional grouping by stroke (#92) * Add multiple categories of grouping conditions * Add conditional grouping to stroke popover * Add stroke grouping logic --- .../ChartPopover/ChartPopoverAdvanced.tsx | 11 +- .../StrokePopover/StrokePopoverAdvanced.scss | 1 + .../StrokePopover/StrokePopoverAdvanced.tsx | 234 +++++++++++++++++- .../models/explorer/createAppModel.ts | 39 ++- src/src/types/pages/metrics/Metrics.d.ts | 10 +- .../models/explorer/createAppModel.d.ts | 11 +- src/src/utils/app/getChartTitleData.ts | 2 +- src/src/utils/app/getLegendsData.tsx | 4 +- .../utils/app/onGroupingConditionsChange.ts | 9 +- 9 files changed, 300 insertions(+), 21 deletions(-) diff --git a/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.tsx b/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.tsx index a48d2693..d4db2331 100644 --- a/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.tsx +++ b/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.tsx @@ -11,6 +11,8 @@ import { Button, Box } from '@material-ui/core'; import { Icon, Text } from 'components/kit'; import ErrorBoundary from 'components/ErrorBoundary'; +import { GroupNameEnum } from 'config/grouping/GroupingPopovers'; + import { IGroupingCondition, IGroupingSelectOption, @@ -41,7 +43,7 @@ function ChartPopoverAdvanced({ ); const [selectedValue, setSelectedValue] = useState(''); const [conditions, setConditions] = useState( - groupingData?.conditions || [], + groupingData?.conditions?.chart || [], ); const onAddCondition = () => { @@ -60,7 +62,7 @@ function ChartPopoverAdvanced({ index === conditionIndex ? condition : c, ); setConditions(newConditions); - onGroupingConditionsChange?.(newConditions); + onGroupingConditionsChange?.(newConditions, GroupNameEnum.CHART); }; const onChangeField = (e: any, value: IGroupingSelectOption | null): void => { @@ -226,7 +228,10 @@ function ChartPopoverAdvanced({ newConditions.splice(index, 1); setConditions(newConditions); if (onGroupingConditionsChange) { - onGroupingConditionsChange(newConditions); + onGroupingConditionsChange( + newConditions, + GroupNameEnum.CHART, + ); } }} variant='text' diff --git a/src/src/pages/Metrics/components/StrokePopover/StrokePopoverAdvanced.scss b/src/src/pages/Metrics/components/StrokePopover/StrokePopoverAdvanced.scss index 543ac865..93216f80 100644 --- a/src/src/pages/Metrics/components/StrokePopover/StrokePopoverAdvanced.scss +++ b/src/src/pages/Metrics/components/StrokePopover/StrokePopoverAdvanced.scss @@ -3,6 +3,7 @@ .StrokePopoverAdvanced { &__container { padding: 1rem; + border-bottom: $border-main; &__p { margin: 1rem 0; } diff --git a/src/src/pages/Metrics/components/StrokePopover/StrokePopoverAdvanced.tsx b/src/src/pages/Metrics/components/StrokePopover/StrokePopoverAdvanced.tsx index 18157cb3..a745e30a 100644 --- a/src/src/pages/Metrics/components/StrokePopover/StrokePopoverAdvanced.tsx +++ b/src/src/pages/Metrics/components/StrokePopover/StrokePopoverAdvanced.tsx @@ -1,18 +1,110 @@ -import React from 'react'; +import { useMemo, useState } from 'react'; -import { Button, Switcher, Text } from 'components/kit'; +import { Checkbox, TextField } from '@material-ui/core'; +import { + CheckBox as CheckBoxIcon, + CheckBoxOutlineBlank, +} from '@material-ui/icons'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import { Button, Box } from '@material-ui/core'; + +import { Icon, Switcher, Text } from 'components/kit'; import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; +import { GroupNameEnum } from 'config/grouping/GroupingPopovers'; + +import { + IGroupingCondition, + IGroupingSelectOption, +} from 'types/services/models/metrics/metricsAppModel'; import { IGroupingPopoverAdvancedProps } from 'types/components/GroupingPopover/GroupingPopover'; import './StrokePopoverAdvanced.scss'; +export enum IOperator { + '==' = '==', + '!=' = '!=', + '>' = '>', + '<' = '<', + '>=' = '>=', + '<=' = '<=', +} + function StrokePopoverAdvanced({ onPersistenceChange, onShuffleChange, persistence, groupingData, + onGroupingConditionsChange, + groupingSelectOptions, }: 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?.stroke || [], + ); + + 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.STROKE); + }; + + 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 isShuffleDisabled(): boolean { //ToDo reverse mode // if (groupingData?.reverseMode.stroke || groupingData?.stroke.length) { @@ -60,6 +152,144 @@ function StrokePopoverAdvanced({ )} +
+ + group by condition + + + Group strokes 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 c50e3a99..cf28c1f2 100644 --- a/src/src/services/models/explorer/createAppModel.ts +++ b/src/src/services/models/explorer/createAppModel.ts @@ -275,6 +275,11 @@ function createAppModel(appConfig: IAppInitialConfig) { stroke: 10, }, paletteIndex: 0, + conditions: { + color: [], + stroke: [], + chart: [], + }, }; } if (components?.table) { @@ -1461,11 +1466,26 @@ function createAppModel(appConfig: IAppInitialConfig) { const grouping = configData!.grouping; const { paletteIndex = 0 } = grouping || {}; - const conditions: IGroupingCondition[] = grouping.conditions || []; - const conditionStrings = conditions.map( + 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 groupByColor = getFilteredGroupingOptions({ groupName: GroupNameEnum.COLOR, model, @@ -1473,12 +1493,11 @@ function createAppModel(appConfig: IAppInitialConfig) { const groupByStroke = getFilteredGroupingOptions({ groupName: GroupNameEnum.STROKE, model, - }); - + }).concat(strokeConditionStrings); const groupByChart = getFilteredGroupingOptions({ groupName: GroupNameEnum.CHART, model, - }).concat(conditionStrings); + }).concat(chartConditionStrings); if ( groupByColor.length === 0 && @@ -1511,9 +1530,9 @@ function createAppModel(appConfig: IAppInitialConfig) { }); // Evaluate the conditions and update the row - conditionStrings.forEach((conditionString, j) => { + allConditionStrings.forEach((conditionString, j) => { // Evaluate the condition - const condition = conditions[j]; + const condition = allConditions[j]; // Get everything after the first dot in the field name const fieldTypeAndName = condition.fieldName.split('.'); @@ -2125,9 +2144,13 @@ function createAppModel(appConfig: IAppInitialConfig) { setAggregationEnabled, }); }, - onGroupingConditionsChange(conditions: IGroupingCondition[]): void { + onGroupingConditionsChange( + conditions: IGroupingCondition[], + groupName: GroupNameEnum, + ): void { onGroupingConditionsChange({ conditions, + groupName, model, appName, updateModelData, diff --git a/src/src/types/pages/metrics/Metrics.d.ts b/src/src/types/pages/metrics/Metrics.d.ts index 757bfd5c..2f8b26f9 100644 --- a/src/src/types/pages/metrics/Metrics.d.ts +++ b/src/src/types/pages/metrics/Metrics.d.ts @@ -1,7 +1,10 @@ import React from 'react'; import { RouteChildrenProps } from 'react-router-dom'; -import { RowHeightSize, UnselectedColumnState } from 'config/table/tableConfigs'; +import { + RowHeightSize, + UnselectedColumnState, +} from 'config/table/tableConfigs'; import { ResizeModeEnum } from 'config/enums/tableEnums'; import { DensityOptions } from 'config/enums/densityEnum'; import { RequestStatusEnum } from 'config/enums/requestStatusEnum'; @@ -125,7 +128,10 @@ export interface IMetricProps extends Partial { onGroupingReset: (groupName: GroupNameEnum) => void; onGroupingApplyChange: (groupName: GroupNameEnum) => void; onGroupingPersistenceChange: (groupName: 'color' | 'stroke') => void; - onGroupingConditionsChange: (conditions: IGroupingCondition[]) => void; + onGroupingConditionsChange: ( + conditions: IGroupingCondition[], + groupName: GroupNameEnum, + ) => void; onBookmarkCreate: (params: IBookmarkFormState) => void; onBookmarkUpdate: (id: string) => void; onNotificationAdd: (notification: INotification) => void; diff --git a/src/src/types/services/models/explorer/createAppModel.d.ts b/src/src/types/services/models/explorer/createAppModel.d.ts index 03eef02f..49bb6499 100644 --- a/src/src/types/services/models/explorer/createAppModel.d.ts +++ b/src/src/types/services/models/explorer/createAppModel.d.ts @@ -1,5 +1,8 @@ import { ResizeModeEnum } from 'config/enums/tableEnums'; -import { RowHeightSize, UnselectedColumnState } from 'config/table/tableConfigs'; +import { + RowHeightSize, + UnselectedColumnState, +} from 'config/table/tableConfigs'; import { DensityOptions } from 'config/enums/densityEnum'; import { GroupNameEnum } from 'config/grouping/GroupingPopovers'; @@ -91,7 +94,11 @@ export interface IGroupingConfig { stroke: number; }; paletteIndex?: number; - conditions?: IGroupingCondition[]; + conditions?: { + color: IGroupingCondition[]; + stroke: IGroupingCondition[]; + chart: IGroupingCondition[]; + }; } export interface ISelectOption { diff --git a/src/src/utils/app/getChartTitleData.ts b/src/src/utils/app/getChartTitleData.ts index 00dd84bf..fba8df98 100644 --- a/src/src/utils/app/getChartTitleData.ts +++ b/src/src/utils/app/getChartTitleData.ts @@ -31,7 +31,7 @@ export default function getChartTitleData({ let chartTitleData: IChartTitleData = {}; // Get the list of conditions as strings - const conditions: IGroupingCondition[] = groupData.conditions?.map( + const conditions: IGroupingCondition[] = groupData.conditions?.chart?.map( (condition: IGroupingCondition) => `${condition.fieldName} ${condition.operator} ${condition.value}`, ); diff --git a/src/src/utils/app/getLegendsData.tsx b/src/src/utils/app/getLegendsData.tsx index e8f4c934..e0e2d001 100644 --- a/src/src/utils/app/getLegendsData.tsx +++ b/src/src/utils/app/getLegendsData.tsx @@ -39,10 +39,10 @@ function getLegendsData( const groupConfig = groupingConfig[groupName]; const groupedItemPropKeys = - groupName !== GroupNameEnum.CHART + groupName === GroupNameEnum.ROW || groupName === GroupNameEnum.COLOR ? groupConfig || [] : groupConfig?.concat( - groupingConfig.conditions?.map( + groupingConfig.conditions?.[groupName].map( (condition) => `${condition.fieldName} ${condition.operator} ${condition.value}`, ) || [], diff --git a/src/src/utils/app/onGroupingConditionsChange.ts b/src/src/utils/app/onGroupingConditionsChange.ts index ec65b3d7..947ba1ab 100644 --- a/src/src/utils/app/onGroupingConditionsChange.ts +++ b/src/src/utils/app/onGroupingConditionsChange.ts @@ -1,3 +1,5 @@ +import { GroupNameEnum } from 'config/grouping/GroupingPopovers'; + import * as analytics from 'services/analytics'; import { IModel, State } from 'types/services/models/model'; @@ -8,11 +10,13 @@ import resetChartZoom from './resetChartZoom'; export default function onGroupingConditionsChange({ conditions, + groupName, model, appName, updateModelData, }: { conditions: IGroupingCondition[]; + groupName: GroupNameEnum; model: IModel; appName: string; updateModelData: ( @@ -23,7 +27,10 @@ export default function onGroupingConditionsChange({ let configData = model.getState()?.config; if (configData?.grouping) { - configData.grouping = { ...configData.grouping, conditions }; + configData.grouping.conditions = { + ...configData.grouping.conditions, + [groupName]: conditions, + }; configData = resetChartZoom({ configData, appName }); updateModelData(configData, true); } From 8943fd80057fb7da621ced0d142cd053250bff6e Mon Sep 17 00:00:00 2001 From: Software Developer <7852635+dsuhinin@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:50:36 +0200 Subject: [PATCH 04/11] Enable Run Logs tab --- src/src/pages/RunDetail/RunDetail.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/src/pages/RunDetail/RunDetail.tsx b/src/src/pages/RunDetail/RunDetail.tsx index 6eda999b..2bc5422f 100644 --- a/src/src/pages/RunDetail/RunDetail.tsx +++ b/src/src/pages/RunDetail/RunDetail.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import _ from 'lodash-es'; import classNames from 'classnames'; import moment from 'moment'; import { @@ -56,6 +57,9 @@ const RunDetailParamsTab = React.lazy( () => import(/* webpackChunkName: "RunDetailParamsTab" */ './RunDetailParamsTab'), ); +const RunLogsTab = React.lazy( + () => import(/* webpackChunkName: "RunDetailParamsTab" */ './RunLogsTab'), +); const RunDetailSettingsTab = React.lazy( () => import( @@ -75,6 +79,7 @@ const RunOverviewTab = React.lazy( const tabs: Record = { overview: 'Overview', run_parameters: 'Run Params', + logs: 'Logs', metrics: 'Metrics', system: 'System', settings: 'Settings', @@ -126,6 +131,15 @@ function RunDetail(): React.FunctionComponentElement { isRunInfoLoading={runData?.isRunInfoLoading} /> ), + logs: ( + + ), metrics: ( Date: Fri, 7 Jun 2024 11:57:57 +0200 Subject: [PATCH 05/11] Enable Run Logs tab --- src/src/pages/RunDetail/RunDetail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/pages/RunDetail/RunDetail.tsx b/src/src/pages/RunDetail/RunDetail.tsx index 2bc5422f..3212b9ca 100644 --- a/src/src/pages/RunDetail/RunDetail.tsx +++ b/src/src/pages/RunDetail/RunDetail.tsx @@ -58,7 +58,7 @@ const RunDetailParamsTab = React.lazy( import(/* webpackChunkName: "RunDetailParamsTab" */ './RunDetailParamsTab'), ); const RunLogsTab = React.lazy( - () => import(/* webpackChunkName: "RunDetailParamsTab" */ './RunLogsTab'), + () => import(/* webpackChunkName: "RunLogsTab" */ './RunLogsTab'), ); const RunDetailSettingsTab = React.lazy( () => From 71b8966c6ca01e52526705a2b80ca21c23a4071d Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Sat, 8 Jun 2024 09:26:39 +0200 Subject: [PATCH 06/11] add options for metrics to autocomplete (#94) --- src/src/config/monacoConfig/monacoConfig.ts | 12 ++++++++++- src/src/utils/showAutocompletion.ts | 22 +++++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/src/config/monacoConfig/monacoConfig.ts b/src/src/config/monacoConfig/monacoConfig.ts index 185d555c..978f3ae3 100644 --- a/src/src/config/monacoConfig/monacoConfig.ts +++ b/src/src/config/monacoConfig/monacoConfig.ts @@ -67,7 +67,16 @@ export const getSuggestionsByExplorer = ( explorerName: AppNameEnum, data: Record, ): Record => { - const defaultSuggestions = { + const metricNames = data?.metric ? Object.keys(data.metric) : []; + + const metricDict: Record = metricNames.reduce( + (acc: Record, metricName: string) => { + acc[metricName] = { last: 0 }; + return acc; + }, + {}, + ); + const defaultSuggestions: Record = { run: { active: false, hash: '', @@ -78,6 +87,7 @@ export const getSuggestionsByExplorer = ( created_at: 0, finalized_at: 0, duration: 0, + metrics: metricDict, ...(data?.params || {}), }, }; diff --git a/src/src/utils/showAutocompletion.ts b/src/src/utils/showAutocompletion.ts index 6d99395f..616fed1d 100644 --- a/src/src/utils/showAutocompletion.ts +++ b/src/src/utils/showAutocompletion.ts @@ -126,12 +126,16 @@ function getSuggestions(monaco: Monaco, options: Record) { } // flatten strings of array of accessible options paths without example type const filteredOptions = getObjectPaths(options, options).map((option) => { + const remappedOption = option.replace( + /\.metrics\.([^."]+)(\.[^.]+)?$/, + '.metrics["$1"]$2', + ); const indexOf = - option.indexOf('.__example_type__') !== -1 || - option[option.length - 1] === '.' - ? option.indexOf('.__example_type__') - : option.length; - return option.slice(0, indexOf); + remappedOption.indexOf('.__example_type__') !== -1 || + remappedOption[option.length - 1] === '.' + ? remappedOption.indexOf('.__example_type__') + : remappedOption.length; + return remappedOption.slice(0, indexOf); }); // If the last character typed is a period then we need to look at member objects of the `options` object const isMember = activeTyping.charAt(activeTyping.length - 1) === '.'; @@ -168,13 +172,19 @@ function getSuggestions(monaco: Monaco, options: Record) { endColumn: word.endColumn, }; + // Check if the prefix ends with ".metrics" + const metricsContextRegex = /\.metrics.$/; + // Get all the child properties of the last token for (const prop in lastToken) { // Do not show properites that begin with "__" if (lastToken.hasOwnProperty(prop) && !prop.startsWith('__')) { // Create completion object - const key = !jsValidVariableRegex.test(prop) ? `["${prop}"]` : prop; + const key = + !jsValidVariableRegex.test(prop) || metricsContextRegex.test(prefix) + ? `["${prop}"]` + : prop; let detailType = getDetailType(getValue(options, prefix + key)); const completionItem = { From b937ff70bff56ddcf0a8a1dea443f42d78607aa8 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Mon, 10 Jun 2024 11:53:09 +0200 Subject: [PATCH 07/11] retrieve right description from tags (#97) --- src/src/services/models/runs/runDetailAppModel.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/src/services/models/runs/runDetailAppModel.ts b/src/src/services/models/runs/runDetailAppModel.ts index 7aaf6c41..9e817f2c 100644 --- a/src/src/services/models/runs/runDetailAppModel.ts +++ b/src/src/services/models/runs/runDetailAppModel.ts @@ -64,6 +64,7 @@ function getExperimentsData() { } function getRunInfo(runHash: string): IApiRequest { + const DESCRIPTION_TAG = 'mlflow.note.content'; if (getRunsInfoRequestRef) { getRunsInfoRequestRef.abort(); } @@ -74,6 +75,10 @@ function getRunInfo(runHash: string): IApiRequest { const data = await getRunsInfoRequestRef.call((detail: any) => { exceptionHandler({ detail, model }); }); + data.props.description = + DESCRIPTION_TAG in data.params.tags + ? data.params.tags[DESCRIPTION_TAG] + : ''; model.setState({ runParams: data.params, runTraces: data.traces, @@ -332,7 +337,7 @@ function editRunNameAndDescription( description, }, }); - if (res.id) { + if (res.ID) { onNotificationAdd({ id: Date.now(), severity: 'success', From 9c25ad66463191f6841e5ff0caafabaa178dc665 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Wed, 12 Jun 2024 09:14:13 -0700 Subject: [PATCH 08/11] Add conditional grouping by color (#93) * Add multiple categories of grouping conditions * Add conditional grouping to stroke popover * Add stroke grouping logic * Add conditional grouping section to Color Popover * Fix active condtions not displaying * Modularize conditional parsing logic * Add color grouping logic --- .../components/GroupingItem/GroupingItem.tsx | 6 +- .../ColorPopoverAdvanced.scss | 1 + .../ColorPopoverAdvanced.tsx | 232 +++++++++++++++++- .../models/explorer/createAppModel.ts | 88 ++----- src/src/utils/app/generateGroupValues.ts | 74 ++++++ src/src/utils/app/getConditionStrings.ts | 12 + src/src/utils/app/getLegendsData.tsx | 2 +- 7 files changed, 337 insertions(+), 78 deletions(-) create mode 100644 src/src/utils/app/generateGroupValues.ts create mode 100644 src/src/utils/app/getConditionStrings.ts 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( From 94bce1558f9908ca04ba6ef79440962580fcc31a Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Mon, 17 Jun 2024 08:13:19 -0700 Subject: [PATCH 09/11] Add Metrics filtering for Conditional Grouping (#86) * Add conditionalGroupingOptions prop * Add metrics support to ChartPopover (Metrics app) * Add condtionalGroupingOptions state * Add TODO for Params and Scatters * Fix context conditional grouping and repeated metrics --------- Co-authored-by: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> --- src/src/components/Grouping/Grouping.tsx | 3 ++ .../components/GroupingItem/GroupingItem.tsx | 2 ++ src/src/pages/Metrics/Metrics.tsx | 1 + src/src/pages/Metrics/MetricsContainer.tsx | 1 + .../ChartPopover/ChartPopoverAdvanced.scss | 5 ++++ .../ChartPopover/ChartPopoverAdvanced.tsx | 28 +++++++++++++------ src/src/pages/Params/Params.tsx | 1 + src/src/pages/Scatters/Scatters.tsx | 1 + .../models/explorer/createAppModel.ts | 28 +++++++++++++++++++ .../GroupingPopover/GroupingPopover.d.ts | 2 ++ .../pages/components/Grouping/Grouping.d.ts | 1 + .../components/GroupingItem/GroupingItem.d.ts | 1 + src/src/types/pages/metrics/Metrics.d.ts | 1 + .../services/models/metrics/metricModel.d.ts | 1 + .../models/metrics/metricsAppModel.d.ts | 1 + src/src/utils/app/generateGroupValues.ts | 1 + 16 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/src/components/Grouping/Grouping.tsx b/src/src/components/Grouping/Grouping.tsx index c0331ef5..42fafb2e 100644 --- a/src/src/components/Grouping/Grouping.tsx +++ b/src/src/components/Grouping/Grouping.tsx @@ -16,6 +16,7 @@ import './Grouping.scss'; function Grouping({ groupingData, groupingSelectOptions, + conditionalGroupingOptions, onGroupingSelectChange, onGroupingModeChange, onGroupingPaletteChange, @@ -46,6 +47,7 @@ function Grouping({ groupName={groupName as GroupNameEnum} groupingData={groupingData} groupingSelectOptions={groupingSelectOptions} + conditionalGroupingOptions={conditionalGroupingOptions} onSelect={onGroupingSelectChange} onGroupingModeChange={onGroupingModeChange} isDisabled={isDisabled} @@ -65,6 +67,7 @@ function Grouping({ paletteIndex: groupingData?.paletteIndex, })} groupingSelectOptions={groupingSelectOptions} + conditionalGroupingOptions={conditionalGroupingOptions} onSelect={onGroupingSelectChange} onGroupingConditionsChange={onGroupingConditionsChange} /> diff --git a/src/src/components/GroupingItem/GroupingItem.tsx b/src/src/components/GroupingItem/GroupingItem.tsx index fd9952e2..3d31ed24 100644 --- a/src/src/components/GroupingItem/GroupingItem.tsx +++ b/src/src/components/GroupingItem/GroupingItem.tsx @@ -31,6 +31,7 @@ function GroupingItem({ onSelect, onGroupingModeChange, groupingSelectOptions, + conditionalGroupingOptions, isDisabled, }: IGroupingItemProps): React.FunctionComponentElement { return ( @@ -70,6 +71,7 @@ function GroupingItem({ inputLabel={inputLabel} groupingData={groupingData} groupingSelectOptions={groupingSelectOptions} + conditionalGroupingOptions={conditionalGroupingOptions} advancedComponent={advancedComponent} onSelect={onSelect} onGroupingModeChange={onGroupingModeChange} diff --git a/src/src/pages/Metrics/Metrics.tsx b/src/src/pages/Metrics/Metrics.tsx index 45f4652c..378ea1d0 100644 --- a/src/src/pages/Metrics/Metrics.tsx +++ b/src/src/pages/Metrics/Metrics.tsx @@ -133,6 +133,7 @@ function Metrics( isDisabled={isProgressBarVisible} groupingData={props.groupingData} groupingSelectOptions={props.groupingSelectOptions} + conditionalGroupingOptions={props.conditionalGroupingOptions} onGroupingSelectChange={props.onGroupingSelectChange} onGroupingModeChange={props.onGroupingModeChange} onGroupingPaletteChange={props.onGroupingPaletteChange} diff --git a/src/src/pages/Metrics/MetricsContainer.tsx b/src/src/pages/Metrics/MetricsContainer.tsx index 850b9187..fb9e8bc2 100644 --- a/src/src/pages/Metrics/MetricsContainer.tsx +++ b/src/src/pages/Metrics/MetricsContainer.tsx @@ -165,6 +165,7 @@ function MetricsContainer(): React.FunctionComponentElement { chartPanelOffsetHeight={chartPanelOffsetHeight} selectedRows={metricsData?.selectedRows!} groupingSelectOptions={metricsData?.groupingSelectOptions!} + conditionalGroupingOptions={metricsData?.conditionalGroupingOptions!} sortOptions={metricsData?.sortOptions!} resizeMode={metricsData?.config?.table?.resizeMode!} columnsWidths={metricsData?.config?.table?.columnsWidths!} diff --git a/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.scss b/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.scss index 36e81ca2..1bf458f8 100644 --- a/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.scss +++ b/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.scss @@ -14,6 +14,11 @@ &__conditionalFilter { padding: 1rem; border-bottom: $border-main; + + &__icon { + margin-left: 0.5rem; + color: $text-color; + } } &__conditionalFilter__p { diff --git a/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.tsx b/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.tsx index d4db2331..819ae4b3 100644 --- a/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.tsx +++ b/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.tsx @@ -1,6 +1,6 @@ import { useState, useMemo } from 'react'; -import { Checkbox, TextField } from '@material-ui/core'; +import { Checkbox, TextField, Tooltip } from '@material-ui/core'; import { CheckBox as CheckBoxIcon, CheckBoxOutlineBlank, @@ -33,7 +33,7 @@ export enum IOperator { function ChartPopoverAdvanced({ onGroupingConditionsChange, groupingData, - groupingSelectOptions, + conditionalGroupingOptions, }: IGroupingPopoverAdvancedProps): React.FunctionComponentElement { const [inputValue, setInputValue] = useState(''); const [selectedField, setSelectedField] = @@ -90,7 +90,7 @@ function ChartPopoverAdvanced({ }; const options = useMemo(() => { - const filteredOptions = groupingSelectOptions?.filter((item) => + const filteredOptions = conditionalGroupingOptions?.filter((item) => item.label.toLowerCase().includes(inputValue.toLowerCase()), ); return ( @@ -100,7 +100,7 @@ function ChartPopoverAdvanced({ b.label.toLowerCase().indexOf(inputValue.toLowerCase()), ) || [] ); - }, [groupingSelectOptions, inputValue]); + }, [conditionalGroupingOptions, inputValue]); return ( @@ -108,20 +108,32 @@ function ChartPopoverAdvanced({
group by condition + +
+ +
+
- Group charts by conditions such as{' '} + Group charts by conditions, e.g.{' '} run.epochs > 30 .
- {/* Add textbox to allow grouping by condition */} + {/* Textbox for selecting fields */} )} renderTags={() => null} // No tags for single selection @@ -214,7 +226,7 @@ function ChartPopoverAdvanced({ key={index} className='ChartPopoverAdvanced__conditionalFilter__box flex fac fjb' > - {/* Show condition and button in same line */} + {/* Show condition and Remove button in same line */} { + return { + group: 'metric', + label: `metric.${metric?.config?.name}`, + value: `${metric?.config?.name}`, + }; + }), + 'value', + ), + ); const sortOptions = [ ...groupingSelectOptions, { @@ -1277,6 +1290,7 @@ function createAppModel(appConfig: IAppInitialConfig) { tableColumns, sameValueColumns: tableData.sameValueColumns, groupingSelectOptions, + conditionalGroupingOptions, sortOptions, selectedRows, }); @@ -1309,6 +1323,19 @@ function createAppModel(appConfig: IAppInitialConfig) { sequenceName: 'metric', }), ]; + // Conditional grouping allows grouping by regular select options and also metrics + const conditionalGroupingOptions = groupingSelectOptions.concat( + _.uniqBy( + data.map((metric) => { + return { + group: 'metric', + label: `metric.${metric?.config?.name}`, + value: `${metric?.config?.name}`, + }; + }), + 'value', + ), + ); const sortOptions = [ ...groupingSelectOptions, { @@ -1433,6 +1460,7 @@ function createAppModel(appConfig: IAppInitialConfig) { tableColumns: tableColumns, sameValueColumns: tableData.sameValueColumns, groupingSelectOptions, + conditionalGroupingOptions, sortOptions, selectedRows, }); diff --git a/src/src/types/components/GroupingPopover/GroupingPopover.d.ts b/src/src/types/components/GroupingPopover/GroupingPopover.d.ts index 7034b227..92b7a638 100644 --- a/src/src/types/components/GroupingPopover/GroupingPopover.d.ts +++ b/src/src/types/components/GroupingPopover/GroupingPopover.d.ts @@ -13,6 +13,7 @@ export interface IGroupingPopoverProps { groupingData: IGroupingConfig; advancedComponent?: React.FunctionComponentElement | null; groupingSelectOptions: IGroupingSelectOption[]; + conditionalGroupingOptions?: IGroupingSelectOption[]; onSelect: IMetricProps['onGroupingSelectChange']; onGroupingModeChange: IMetricProps['onGroupingModeChange']; inputLabel?: string; @@ -26,6 +27,7 @@ export interface IGroupingPopoverAdvancedProps { onGroupingPaletteChange?: IMetricProps['onGroupingPaletteChange']; onShuffleChange: IMetricProps['onShuffleChange']; groupingSelectOptions?: IGroupingSelectOption[]; + conditionalGroupingOptions?: IGroupingSelectOption[]; onSelect?: IMetricProps['onGroupingSelectChange']; inputLabel?: string; onGroupingConditionsChange?: IMetricProps['onGroupingConditionsChange']; diff --git a/src/src/types/pages/components/Grouping/Grouping.d.ts b/src/src/types/pages/components/Grouping/Grouping.d.ts index 3b924a08..d3f327b3 100644 --- a/src/src/types/pages/components/Grouping/Grouping.d.ts +++ b/src/src/types/pages/components/Grouping/Grouping.d.ts @@ -13,6 +13,7 @@ import { IMetricProps } from 'types/pages/metrics/Metrics'; export interface IGroupingProps { groupingData: IGroupingConfig; groupingSelectOptions: IGroupingSelectOption[]; + conditionalGroupingOptions?: IGroupingSelectOption[]; onGroupingSelectChange: IMetricProps['onGroupingSelectChange']; onGroupingModeChange: IMetricProps['onGroupingModeChange']; onGroupingPaletteChange: IMetricProps['onGroupingPaletteChange']; diff --git a/src/src/types/pages/components/GroupingItem/GroupingItem.d.ts b/src/src/types/pages/components/GroupingItem/GroupingItem.d.ts index e722cc12..7f9db3e7 100644 --- a/src/src/types/pages/components/GroupingItem/GroupingItem.d.ts +++ b/src/src/types/pages/components/GroupingItem/GroupingItem.d.ts @@ -14,6 +14,7 @@ export interface IGroupingItemProps extends IGroupingPopoverProps { groupingData: IGroupingConfig; advancedComponent?: React.FunctionComponentElement; groupingSelectOptions: IGroupingSelectOption[]; + conditionalGroupingOptions?: IGroupingSelectOption[]; onReset: () => void; onVisibilityChange: () => void; } diff --git a/src/src/types/pages/metrics/Metrics.d.ts b/src/src/types/pages/metrics/Metrics.d.ts index 2f8b26f9..3c9adf52 100644 --- a/src/src/types/pages/metrics/Metrics.d.ts +++ b/src/src/types/pages/metrics/Metrics.d.ts @@ -93,6 +93,7 @@ export interface IMetricProps extends Partial { unselectedColumnState: UnselectedColumnState; sameValueColumns?: string[] | []; groupingSelectOptions: IGroupingSelectOption[]; + conditionalGroupingOptions: IGroupingSelectOption[]; sortOptions: IGroupingSelectOption[]; requestStatus: RequestStatusEnum; requestProgress: IRequestProgress; diff --git a/src/src/types/services/models/metrics/metricModel.d.ts b/src/src/types/services/models/metrics/metricModel.d.ts index 74d5aa64..b687bd06 100644 --- a/src/src/types/services/models/metrics/metricModel.d.ts +++ b/src/src/types/services/models/metrics/metricModel.d.ts @@ -18,4 +18,5 @@ export interface IMetric { x_axis_iters?: Float64Array; x_axis_values?: Float64Array; isHidden: boolean; + lastValue?: number; } diff --git a/src/src/types/services/models/metrics/metricsAppModel.d.ts b/src/src/types/services/models/metrics/metricsAppModel.d.ts index cd373f75..639fa6f5 100644 --- a/src/src/types/services/models/metrics/metricsAppModel.d.ts +++ b/src/src/types/services/models/metrics/metricsAppModel.d.ts @@ -56,6 +56,7 @@ export interface IMetricAppModelState { params: string[]; notifyData: INotification[]; groupingSelectOptions: IGroupingSelectOption[]; + conditionalGroupingOptions: IGroupingSelectOption[]; sortOptions: IGroupingSelectOption[]; selectFormData?: { options: ISelectOption[]; diff --git a/src/src/utils/app/generateGroupValues.ts b/src/src/utils/app/generateGroupValues.ts index 361599f5..5b4d4ee4 100644 --- a/src/src/utils/app/generateGroupValues.ts +++ b/src/src/utils/app/generateGroupValues.ts @@ -45,6 +45,7 @@ export function generateGroupValues( name: fieldType === 'metric' ? data[i].name : data[i].run.props.name, tags: data[i].run.params.tags, experiment: data[i].run.props.experiment?.name, + context: data[i].context, }; // Get the relevant attribute's value From 9373a9313b3953ff607386ca3ed75f7d794bf1c1 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Wed, 19 Jun 2024 09:33:02 -0700 Subject: [PATCH 10/11] Add E2E testing framework (#98) * Add playwright to project * Add playright CI workflow * Add rudimentary test for Dashboard * Move playwright into base directory * Update playwright.yml * Update playwright.yml * Update playwright.yml * Update playwright.yml * Update playwright.yml * Update playwright.yml * Update playwright.yml * Update playwright.yml * Fix bad yml * Add wait-on for dev server pipeline * Add failing test to check pipeline * Remove failing test * Add tests for dashboard quick navigation buttons * Remove example test and add to readme * Update playwright.yml --- .github/workflows/playwright.yml | 33 ++++++++++++++ README.md | 13 ++++++ src/.gitignore | 4 ++ src/e2e/Dashboard.spec.ts | 35 +++++++++++++++ src/package-lock.json | 68 ++++++++++++++++++++++++++++ src/package.json | 2 + src/playwright.config.ts | 77 ++++++++++++++++++++++++++++++++ 7 files changed, 232 insertions(+) create mode 100644 .github/workflows/playwright.yml create mode 100644 src/e2e/Dashboard.spec.ts create mode 100644 src/playwright.config.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..e012fbc5 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,33 @@ +name: Playwright Tests +on: + push: + branches: [ main, master, release/* ] + pull_request: + branches: [ main, release/* ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 16 + - name: Install dependencies + run: cd src && npm ci --legacy-peer-deps + - name: Install wait-on + run: npm install -g wait-on + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Dev Server and E2E Tests + run: | + cd src + npm start & + npx wait-on http://localhost:3000 + npx playwright test src/e2e + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/README.md b/README.md index 315520b0..84650e9f 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,18 @@ An embed directory will be created with the built UI, ready to be embedded in th Once you're happy with them, create a pull request **against the `release/vX.Y.Z` branch*** that you started from (***not `main`!***). Once merged, the CI will run and build the UI. It will then push it to a new tag that is compatible with the Go module rules. For example, the first customization to `v3.16.2` of Aim will end up in a tag named `v0.31602.1`. +### How to run E2E tests? + +The E2E tests can be run locally by following these steps: +1. Start the FastTrackML server +2. Run the Aim UI in development mode (on localhost:3000) +3. In another terminal, run `cd src/e2e` +4. Run `npx playwright test` to run the test suite + +New tests can be added directly to the `src/e2e` directory. You may also run `npx playwright show-report` to see the test results. + +For a guide on how to write a test, see [Playwright's example tests](https://github.com/microsoft/playwright/blob/main/examples/todomvc/tests/integration.spec.ts). + ### How is this all enforced? A GitHub app has been created with the `contents:write` permissions on this repo. Its App ID and private key are stored as secrets under the `restricted` environment. This environment is limited to the `main` and `release/v*` branches @@ -239,3 +251,4 @@ do gh api /repos/G-Research/fasttrackml-ui-aim/rulesets/$rule | jq '[{name: .name, target: .target, conditions: .conditions, rules: .rules, bypass_actors: .bypass_actors}]' done | jq -s add ``` + diff --git a/src/.gitignore b/src/.gitignore index 79829f37..37410fe9 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -25,3 +25,7 @@ yarn-debug.log* yarn-error.log* Desktop +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/src/e2e/Dashboard.spec.ts b/src/e2e/Dashboard.spec.ts new file mode 100644 index 00000000..21b2f37e --- /dev/null +++ b/src/e2e/Dashboard.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '@playwright/test'; + +const BASE_URL = 'http://localhost:3000'; + +test.describe('Dashboard', () => { + test.beforeEach(async ({ page }) => { + await page.goto(BASE_URL); + }); + + test('has title', async ({ page }) => { + await expect(page).toHaveTitle('FastTrackML (modern)'); + }); + + test('active runs link redirects correctly', async ({ page }) => { + await page.getByText('Active Runs').click({ force: true }); + + await page.getByRole('code', { name: 'runs.active == True' }); + }); + + test('archived runs link redirects correctly', async ({ page }) => { + await page.getByText('Archived Runs').click({ force: true }); + + await page.getByRole('code', { name: 'runs.active == False' }); + }); + + test("last week's runs link redirects correctly", async ({ page }) => { + await page.getByText("Last Week's Runs").click({ force: true }); + + // The text varies depending on the current date: + // Example: datetime(2024, 6, 3) <= run.created_at < datetime(2024, 6, 10) + await page.getByRole('code', { + name: /datetime\(\d+, \d+, \d+\) <= run\.created_at < datetime\(\d+, \d+, \d+\)/, + }); + }); +}); diff --git a/src/package-lock.json b/src/package-lock.json index f0d04c3a..ee510eee 100755 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -49,6 +49,7 @@ "memoize-one": "^5.2.1", "moment": "^2.29.4", "monaco-editor": "^0.33.0", + "playwright": "^1.44.1", "plotly.js": "^2.7.0", "prop-types": "^15.7.2", "prosemirror-tables": "^1.1.1", @@ -73,6 +74,7 @@ "zustand": "^4.1.1" }, "devDependencies": { + "@playwright/test": "^1.44.1", "@storybook/addon-actions": "^6.5.12", "@storybook/addon-essentials": "^6.5.12", "@storybook/addon-interactions": "^6.5.12", @@ -3618,6 +3620,21 @@ "node": ">=10" } }, + "node_modules/@playwright/test": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", + "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", + "dev": true, + "dependencies": { + "playwright": "1.44.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@plotly/d3": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.0.tgz", @@ -25409,6 +25426,34 @@ "node": ">=4" } }, + "node_modules/playwright": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", + "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "dependencies": { + "playwright-core": "1.44.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", @@ -40022,6 +40067,15 @@ } } }, + "@playwright/test": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", + "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", + "dev": true, + "requires": { + "playwright": "1.44.1" + } + }, "@plotly/d3": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.0.tgz", @@ -56820,6 +56874,20 @@ } } }, + "playwright": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", + "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.44.1" + } + }, + "playwright-core": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==" + }, "please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", diff --git a/src/package.json b/src/package.json index 13e2ede2..50e5b3e8 100644 --- a/src/package.json +++ b/src/package.json @@ -43,6 +43,7 @@ "memoize-one": "^5.2.1", "moment": "^2.29.4", "monaco-editor": "^0.33.0", + "playwright": "^1.44.1", "plotly.js": "^2.7.0", "prop-types": "^15.7.2", "prosemirror-tables": "^1.1.1", @@ -107,6 +108,7 @@ ] }, "devDependencies": { + "@playwright/test": "^1.44.1", "@storybook/addon-actions": "^6.5.12", "@storybook/addon-essentials": "^6.5.12", "@storybook/addon-interactions": "^6.5.12", diff --git a/src/playwright.config.ts b/src/playwright.config.ts new file mode 100644 index 00000000..bfe3e830 --- /dev/null +++ b/src/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); From b430d0e19596f86880bc683a614ac330c6b6dec0 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:42:19 -0700 Subject: [PATCH 11/11] Add backend integration for E2E CI pipeline (#99) * Add playwright to project * Add playright CI workflow * Add rudimentary test for Dashboard * Move playwright into base directory * Update playwright.yml * Update playwright.yml * Update playwright.yml * Update playwright.yml * Update playwright.yml * Update playwright.yml * Update playwright.yml * Update playwright.yml * Fix bad yml * Add wait-on for dev server pipeline * Add failing test to check pipeline * Remove failing test * Add tests for dashboard quick navigation buttons * Remove example test and add to readme * Add backend clone and run before tests * Fix docker execution * Fix docker TTY issue * Add wait-on for backend server * Add k6 installation and database seeding * Rename Dashboard logic tests * Add preliminary dashboard test * Fix DashboardContent test * Add dev mode to docker pipeline * Fix pipeline issues (remove unused commands) * Remove logging and debugging screenshots * Fix pipeline * Fix pipeline * Access load script by getting raw file instead of pulling entire repo * Remove duplicate tests --- .github/workflows/playwright.yml | 42 ++++++++++++++++--- src/e2e/Dashboard/DashboardContent.spec.ts | 19 +++++++++ .../DashboardLogic.spec.ts} | 0 3 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 src/e2e/Dashboard/DashboardContent.spec.ts rename src/e2e/{Dashboard.spec.ts => Dashboard/DashboardLogic.spec.ts} (100%) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index e012fbc5..c7560c6d 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,31 +1,61 @@ name: Playwright Tests + on: push: - branches: [ main, master, release/* ] + branches: [ main, release/* ] pull_request: branches: [ main, release/* ] + jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest + steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 with: node-version: 16 - - name: Install dependencies + + - name: Install frontend dependencies run: cd src && npm ci --legacy-peer-deps + - name: Install wait-on run: npm install -g wait-on - - name: Install Playwright Browsers + + - name: Install Playwright browsers run: npx playwright install --with-deps - - name: Run Dev Server and E2E Tests + + - name: Build and run backend + run: | + docker run --rm -p 5000:5000 -e FML_DEV_MODE=true gresearch/fasttrackml:main & + npx wait-on http://localhost:5000 + + - name: Install k6 + run: | + sudo apt-get update + sudo apt-get install -y gnupg software-properties-common ca-certificates curl + curl -s https://dl.k6.io/key.gpg | sudo apt-key add - + echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list + sudo apt-get update + sudo apt-get install -y k6 + + - name: Seed database with k6 + run: | + wget https://raw.githubusercontent.com/G-Research/fasttrackml/main/docs/example/k6_load.js + k6 run k6_load.js + + - name: Start frontend dev server and run E2E tests run: | cd src npm start & npx wait-on http://localhost:3000 npx playwright test src/e2e - - uses: actions/upload-artifact@v4 + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 if: always() with: name: playwright-report diff --git a/src/e2e/Dashboard/DashboardContent.spec.ts b/src/e2e/Dashboard/DashboardContent.spec.ts new file mode 100644 index 00000000..189789eb --- /dev/null +++ b/src/e2e/Dashboard/DashboardContent.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from '@playwright/test'; + +const BASE_URL = 'http://localhost:3000'; + +test.describe('Dashboard', () => { + test.beforeEach(async ({ page }) => { + await page.goto(BASE_URL); + await page.waitForLoadState('networkidle'); + }); + + // Test that the total number of runs is two. This assumes that the + // test database has been seeded with two runs by running `k6 run k6_load.js` + test('has two runs', async ({ page }) => { + const textContent = await page.textContent( + 'p.ProjectStatistics__totalRuns', + ); + expect(textContent).toBe('Total runs: 2'); + }); +}); diff --git a/src/e2e/Dashboard.spec.ts b/src/e2e/Dashboard/DashboardLogic.spec.ts similarity index 100% rename from src/e2e/Dashboard.spec.ts rename to src/e2e/Dashboard/DashboardLogic.spec.ts