From b82ccdb04f84da3a030f6eb771794d53f49286f6 Mon Sep 17 00:00:00 2001 From: Jonathan Giannuzzi Date: Thu, 21 Dec 2023 18:47:11 +0000 Subject: [PATCH 01/79] Customize Aim UI (#31) --- src/package.json | 6 +- src/public/index.html | 25 ++- src/src/App.tsx | 2 +- src/src/assets/logo.svg | 30 +-- .../AutocompleteInput/AutocompleteInput.tsx | 2 +- src/src/components/AxesPropsPopover/config.ts | 4 +- src/src/components/CodeBlock/CodeBlock.tsx | 2 +- .../CommunityPopup/CommunityPopup.tsx | 8 +- src/src/components/SideBar/SideBar.tsx | 38 ++-- src/src/components/SplitPane/SplitPane.tsx | 6 +- .../kit/DictVisualizer/DictVisualizer.tsx | 8 +- src/src/config/config.ts | 30 ++- src/src/config/table/tableConfigs.ts | 87 +-------- .../modules/core/api/experimentsApi/index.ts | 11 ++ .../core/engine/explorer-engine/index.ts | 4 +- .../core/engine/visualizations/index.ts | 5 +- src/src/pages/Dashboard/Dashboard.tsx | 2 - .../AimIntegrations/AimIntegrations.tsx | 177 +++++++----------- .../ProjectStatistics/ProjectStatistics.tsx | 56 ------ .../ProjectStatisticsStore.ts | 9 +- .../components/QuickStart/QuickStart.tsx | 21 ++- src/src/pages/Experiment/Experiment.tsx | 16 +- src/src/pages/Experiment/ExperimentStore.ts | 22 +++ .../ExperimentSettingsTab.d.ts | 1 + .../ExperimentSettingsTab.scss | 15 ++ .../ExperimentSettingsTab.tsx | 41 ++++ .../pages/Experiment/useExperimentState.tsx | 1 + src/src/pages/RunDetail/RunDetail.tsx | 89 --------- .../RunOverviewSidebar/RunOverviewSidebar.tsx | 35 ---- src/src/routes/routes.tsx | 8 +- src/src/utils/app/updateUrlParam.ts | 7 +- src/src/utils/document/documentTitle.ts | 6 +- src/src/utils/formatByAlignment.ts | 3 +- src/src/utils/getStateFromLocalStorage.ts | 4 +- src/src/utils/storage.ts | 16 +- 35 files changed, 287 insertions(+), 510 deletions(-) diff --git a/src/package.json b/src/package.json index f12efe1a..47c2cbb9 100644 --- a/src/package.json +++ b/src/package.json @@ -69,7 +69,7 @@ }, "scripts": { "start": "react-app-rewired --max_old_space_size=4096 start", - "build": "react-app-rewired --max_old_space_size=4096 build && gzipper c -i js,css,html ./build && node tasks/index-html-template-generator.js", + "build": "GENERATE_SOURCEMAP=false react-app-rewired --max_old_space_size=2048 build", "test": "react-app-rewired test ", "test:coverage": "react-app-rewired test --collectCoverage", "test:watch": "react-app-rewired test --watchAll", @@ -77,14 +77,14 @@ "lint": "eslint src/. --ext .js,.jsx,.ts,.tsx", "format:fix": "eslint src/. --ext .js,.jsx,.ts,.tsx --quiet --fix", "preinstall": "rimraf public/vs", - "postinstall": "cp -R node_modules/monaco-editor/min/vs public/vs", + "postinstall": "cp -R node_modules/monaco-editor/min/vs public/vs && find public/vs -type f | xargs sed -i -e '/^\\/\\/# sourceMappingURL=/d'", "analyze-bundles": "node tasks/bundle-analyzer.js", "crc-kit": "func() { node tasks/cli/index.js create-component --name=\"$1\" --path=./src/components/kit/ --lint; }; func", "crc": "func() { node tasks/cli/index.js create-component --name=\"$1\" --path=./src/components/ --lint; }; func", "storybook": "start-storybook -p 6006 -s public", "build-storybook": "build-storybook -s public" }, - "homepage": "/static-files/", + "homepage": "/static/aim/", "browserslist": { "production": [ ">0.2%", diff --git a/src/public/index.html b/src/public/index.html index 74776c49..e55cce01 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -75,14 +75,12 @@ diff --git a/src/src/App.tsx b/src/src/App.tsx index 2572a4b7..401fe605 100644 --- a/src/src/App.tsx +++ b/src/src/App.tsx @@ -27,7 +27,7 @@ const isVisibleCacheBanner = checkIsBasePathInCachedEnv(basePath) && inIframe(); // loading monaco from node modules instead of CDN loader.config({ paths: { - vs: `${getBasePath()}/static-files/vs`, + vs: '/static/aim/vs', }, }); diff --git a/src/src/assets/logo.svg b/src/src/assets/logo.svg index 1e1243af..373a99ab 100644 --- a/src/src/assets/logo.svg +++ b/src/src/assets/logo.svg @@ -1,29 +1 @@ - - - - - Aim Logo - - +FastTrackML \ No newline at end of file diff --git a/src/src/components/AutocompleteInput/AutocompleteInput.tsx b/src/src/components/AutocompleteInput/AutocompleteInput.tsx index fd3115ab..113226ea 100644 --- a/src/src/components/AutocompleteInput/AutocompleteInput.tsx +++ b/src/src/components/AutocompleteInput/AutocompleteInput.tsx @@ -186,7 +186,7 @@ function AutocompleteInput({ // : formattedValue; onChange(formattedValue, ev); } - if (ev.changes[0].text === '\n') { + if (/^\r?\n$/.test(ev.changes[0].text)) { formattedValue = hasSelection ? editorValue.replace(/[\n\r]/g, '') : formattedValue; diff --git a/src/src/components/AxesPropsPopover/config.ts b/src/src/components/AxesPropsPopover/config.ts index 9a0048d2..e2622f25 100644 --- a/src/src/components/AxesPropsPopover/config.ts +++ b/src/src/components/AxesPropsPopover/config.ts @@ -7,11 +7,11 @@ export const METRICS_ALIGNMENT_LIST: { }[] = [ { value: AlignmentOptionsEnum.STEP, - label: 'Step', + label: 'Logging Iteration', }, { value: AlignmentOptionsEnum.EPOCH, - label: 'Epoch', + label: 'Step', }, { value: AlignmentOptionsEnum.RELATIVE_TIME, diff --git a/src/src/components/CodeBlock/CodeBlock.tsx b/src/src/components/CodeBlock/CodeBlock.tsx index 1c24655e..4220209e 100644 --- a/src/src/components/CodeBlock/CodeBlock.tsx +++ b/src/src/components/CodeBlock/CodeBlock.tsx @@ -29,7 +29,7 @@ function CodeBlock({ diff --git a/src/src/components/CommunityPopup/CommunityPopup.tsx b/src/src/components/CommunityPopup/CommunityPopup.tsx index 19e3ebad..51ca7a41 100644 --- a/src/src/components/CommunityPopup/CommunityPopup.tsx +++ b/src/src/components/CommunityPopup/CommunityPopup.tsx @@ -6,6 +6,8 @@ import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap'; import { trackEvent } from 'services/analytics'; +import { getItem, setItem } from 'utils/storage'; + import { Button, Text } from '../kit'; import { ICommunityPopupProps } from './'; @@ -20,7 +22,7 @@ function CommunityPopup(props: ICommunityPopupProps) { let timeoutIdRef = React.useRef(); React.useEffect(() => { - const popupSeenStorage = localStorage.getItem(COMMUNITY_POPUP_SEEN); + const popupSeenStorage = getItem(COMMUNITY_POPUP_SEEN); if (popupSeenStorage === 'true') { setOpen(false); @@ -33,12 +35,12 @@ function CommunityPopup(props: ICommunityPopupProps) { }, []); const onSkip = React.useCallback(() => { - localStorage.setItem(COMMUNITY_POPUP_SEEN, 'true'); + setItem(COMMUNITY_POPUP_SEEN, 'true'); setOpen(false); }, []); const onJoin = React.useCallback(() => { - localStorage.setItem(COMMUNITY_POPUP_SEEN, 'true'); + setItem(COMMUNITY_POPUP_SEEN, 'true'); window.open(COMMUNITY_URL, '_blank'); trackEvent(ANALYTICS_EVENT_KEYS.sidebar.discord); setOpen(false); diff --git a/src/src/components/SideBar/SideBar.tsx b/src/src/components/SideBar/SideBar.tsx index e94ab360..5ab6a148 100644 --- a/src/src/components/SideBar/SideBar.tsx +++ b/src/src/components/SideBar/SideBar.tsx @@ -1,18 +1,16 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { NavLink } from 'react-router-dom'; import { Drawer, Tooltip } from '@material-ui/core'; import logoImg from 'assets/logo.svg'; -import { ReactComponent as DiscordIcon } from 'assets/icons/discord.svg'; import { Icon, Text } from 'components/kit'; import { IconName } from 'components/kit/Icon'; import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; -import CommunityPopup from 'components/CommunityPopup'; import { PathEnum } from 'config/enums/routesEnum'; -import { AIM_VERSION } from 'config/config'; +import { getBaseHost, getPrefix } from 'config/config'; import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap'; import { DOCUMENTATIONS } from 'config/references'; @@ -25,6 +23,16 @@ import { getItem } from 'utils/storage'; import './Sidebar.scss'; function SideBar(): React.FunctionComponentElement { + const [version, setVersion] = React.useState('unknown'); + + useEffect(() => { + fetch(`${getBaseHost()}/version`).then((response) => { + response.text().then((version) => { + setVersion(version); + }); + }); + }, []); + function getPathFromStorage(route: PathEnum): PathEnum | string { const path = getItem(`${route.slice(1)}Url`) ?? ''; if (path !== '' && path.startsWith(route)) { @@ -83,21 +91,11 @@ function SideBar(): React.FunctionComponentElement {
- - - - trackEvent(ANALYTICS_EVENT_KEYS.sidebar.discord) - } - > - - - - + + + + + { - v{AIM_VERSION} + {version}
diff --git a/src/src/components/SplitPane/SplitPane.tsx b/src/src/components/SplitPane/SplitPane.tsx index 6477775f..7cb632a1 100644 --- a/src/src/components/SplitPane/SplitPane.tsx +++ b/src/src/components/SplitPane/SplitPane.tsx @@ -4,6 +4,8 @@ import classNames from 'classnames'; import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; +import { getItem, setItem } from 'utils/storage'; + import { SplitPaneProps, SplitPaneContext } from '.'; import './SplitPane.scss'; @@ -41,7 +43,7 @@ function SplitPane(props: SplitPaneProps) { onDragEnd(endSizes); } if (useLocalStorage) { - localStorage.setItem(`${id}-panesSizes`, JSON.stringify(endSizes)); + setItem(`${id}-panesSizes`, JSON.stringify(endSizes)); } }, [onDragEnd, id, useLocalStorage], @@ -50,7 +52,7 @@ function SplitPane(props: SplitPaneProps) { const getSizes = React.useCallback( (useLocalStorage: boolean, id: string, sizes?: number[]) => { if (useLocalStorage) { - const savedSizes = localStorage.getItem(`${id}-panesSizes`); + const savedSizes = getItem(`${id}-panesSizes`); if (savedSizes) { return JSON.parse(savedSizes); } diff --git a/src/src/components/kit/DictVisualizer/DictVisualizer.tsx b/src/src/components/kit/DictVisualizer/DictVisualizer.tsx index b44e6717..165d85d8 100644 --- a/src/src/components/kit/DictVisualizer/DictVisualizer.tsx +++ b/src/src/components/kit/DictVisualizer/DictVisualizer.tsx @@ -206,7 +206,7 @@ function flattenDict( closedValue: '[...]', sub: `${nestedItemsLength} item${nestedItemsLength === 1 ? '' : 's'}`, color: typeToColor('array'), - copyContent: formatValue(dict), + copyContent: JSON.stringify(JSON.parse(formatValue(dict)), null, 2), }); } else { let nestedItemsLength = Object.keys(dict).length; @@ -219,7 +219,7 @@ function flattenDict( closedValue: '{...}', sub: `${nestedItemsLength} item${nestedItemsLength === 1 ? '' : 's'}`, color: typeToColor('object'), - copyContent: formatValue(dict), + copyContent: JSON.stringify(JSON.parse(formatValue(dict)), null, 2), }); } } @@ -240,7 +240,7 @@ function flattenDict( closedValue: '[...]', sub: `${item.length} item${item.length === 1 ? '' : 's'}`, color: typeToColor('array'), - copyContent: value, + copyContent: JSON.stringify(JSON.parse(value), null, 2), }); if (item.length > 0) { rows.push(...flattenDict(item as unknown[], level + 1, id)); @@ -266,7 +266,7 @@ function flattenDict( closedValue: '{...}', sub: `${nestedItemsLength} item${nestedItemsLength === 1 ? '' : 's'}`, color: typeToColor('object'), - copyContent: value, + copyContent: JSON.stringify(JSON.parse(value), null, 2), }); if (nestedItemsLength > 0) { rows.push( diff --git a/src/src/config/config.ts b/src/src/config/config.ts index fc196c7b..1e5a3e2e 100644 --- a/src/src/config/config.ts +++ b/src/src/config/config.ts @@ -2,6 +2,7 @@ import { version } from '../../package.json'; interface GlobalScope extends Window { API_BASE_PATH?: string; + PREFIX?: string; API_AUTH_TOKEN?: string; } @@ -16,6 +17,10 @@ try { const isDEVModeOn: boolean = process.env.NODE_ENV === 'development'; +function getBaseHost(): string { + return isDEVModeOn ? 'http://localhost:5000' : ''; +} + function getBasePath(isApiBasePath: boolean = true): string { if (globalScope.API_BASE_PATH === '{{ base_path }}') { return isApiBasePath ? '' : '/'; @@ -23,19 +28,28 @@ function getBasePath(isApiBasePath: boolean = true): string { return `${globalScope.API_BASE_PATH}`; } -let API_HOST: string = isDEVModeOn - ? `http://127.0.0.1:43800${getBasePath()}/api` - : `${getBasePath()}/api`; +let API_HOST: string = `${getBaseHost()}${getBasePath()}/api`; function getAPIHost() { return API_HOST; } +function getPrefix(): string { + return `${globalScope.PREFIX}`; +} + +function getTrackingURI(): string { + let host = getBaseHost(); + if (host === '') { + const { protocol, hostname, port } = window.location; + host = `${protocol}//${hostname}${port ? `:${port}` : ''}`; + } + return `${host}${getPrefix()}`; +} + function setAPIBasePath(basePath: string) { globalScope.API_BASE_PATH = basePath; - API_HOST = isDEVModeOn - ? `http://127.0.0.1:43800${getBasePath()}/api` - : `${getBasePath()}/api`; + API_HOST = `${getBaseHost()}${getBasePath()}/api`; } function setAPIAuthToken(authToken: string) { @@ -58,9 +72,11 @@ export function checkIsBasePathInCachedEnv(basePath: string) { } export { - isDEVModeOn, + getBaseHost, getBasePath, getAPIHost, + getPrefix, + getTrackingURI, setAPIBasePath, setAPIAuthToken, getAPIAuthToken, diff --git a/src/src/config/table/tableConfigs.ts b/src/src/config/table/tableConfigs.ts index 083797ab..9d336647 100644 --- a/src/src/config/table/tableConfigs.ts +++ b/src/src/config/table/tableConfigs.ts @@ -88,22 +88,6 @@ export const TABLE_DEFAULT_CONFIG: Record = { }, height: '0.5', }, - [AppNameEnum.IMAGES]: { - metricsValueKey: MetricsValueKeyEnum.LAST, - resizeMode: ResizeModeEnum.Resizable, - rowHeight: RowHeightSize.md, - sortFields: [], - hiddenMetrics: [], - hiddenColumns: ['hash', 'description'], - nonHidableColumns: new Set(['#', 'run', 'actions']), - columnsWidths: {}, - columnsOrder: { - left: ['run'], - middle: [], - right: [], - }, - height: '0.5', - }, [AppNameEnum.SCATTERS]: { metricsValueKey: MetricsValueKeyEnum.LAST, resizeMode: ResizeModeEnum.Resizable, @@ -133,68 +117,11 @@ export const AVOID_COLUMNS_TO_HIDE_LIST = new Set([ ]); export const EXPLORE_SELECTED_RUNS_CONFIG: Record = { - [AppNameEnum.RUNS]: [ - AppNameEnum.RUNS, - AppNameEnum.METRICS, - AppNameEnum.IMAGES, - AppNameEnum.FIGURES, - AppNameEnum.AUDIOS, - AppNameEnum.TEXT, - ], - [AppNameEnum.METRICS]: [ - AppNameEnum.RUNS, - AppNameEnum.METRICS, - AppNameEnum.IMAGES, - AppNameEnum.FIGURES, - AppNameEnum.AUDIOS, - AppNameEnum.TEXT, - ], - [AppNameEnum.PARAMS]: [ - AppNameEnum.RUNS, - AppNameEnum.METRICS, - AppNameEnum.IMAGES, - AppNameEnum.FIGURES, - AppNameEnum.AUDIOS, - AppNameEnum.TEXT, - ], - [AppNameEnum.SCATTERS]: [ - AppNameEnum.RUNS, - AppNameEnum.METRICS, - AppNameEnum.IMAGES, - AppNameEnum.FIGURES, - AppNameEnum.AUDIOS, - AppNameEnum.TEXT, - ], - [AppNameEnum.IMAGES]: [ - AppNameEnum.RUNS, - AppNameEnum.METRICS, - AppNameEnum.IMAGES, - AppNameEnum.FIGURES, - AppNameEnum.AUDIOS, - AppNameEnum.TEXT, - ], - dashboard: [ - AppNameEnum.RUNS, - AppNameEnum.METRICS, - AppNameEnum.IMAGES, - AppNameEnum.FIGURES, - AppNameEnum.AUDIOS, - AppNameEnum.TEXT, - ], - experiment: [ - AppNameEnum.RUNS, - AppNameEnum.METRICS, - AppNameEnum.IMAGES, - AppNameEnum.FIGURES, - AppNameEnum.AUDIOS, - AppNameEnum.TEXT, - ], - run: [ - AppNameEnum.RUNS, - AppNameEnum.METRICS, - AppNameEnum.IMAGES, - AppNameEnum.FIGURES, - AppNameEnum.AUDIOS, - AppNameEnum.TEXT, - ], + [AppNameEnum.RUNS]: [AppNameEnum.RUNS, AppNameEnum.METRICS], + [AppNameEnum.METRICS]: [AppNameEnum.RUNS, AppNameEnum.METRICS], + [AppNameEnum.PARAMS]: [AppNameEnum.RUNS, AppNameEnum.METRICS], + [AppNameEnum.SCATTERS]: [AppNameEnum.RUNS, AppNameEnum.METRICS], + dashboard: [AppNameEnum.RUNS, AppNameEnum.METRICS], + experiment: [AppNameEnum.RUNS, AppNameEnum.METRICS], + run: [AppNameEnum.RUNS, AppNameEnum.METRICS], }; diff --git a/src/src/modules/core/api/experimentsApi/index.ts b/src/src/modules/core/api/experimentsApi/index.ts index 606154e1..3c4a0bf5 100644 --- a/src/src/modules/core/api/experimentsApi/index.ts +++ b/src/src/modules/core/api/experimentsApi/index.ts @@ -63,6 +63,16 @@ async function updateExperimentById( ).body; } +/** + * function getExperimentById + * this call is used for deleting an experiment by id. + * @param id - experiment id + * @returns {Promise} + */ +async function deleteExperimentById(id: string): Promise { + return api.makeAPIDeleteRequest(`${ENDPOINTS.EXPERIMENTS.GET}${id}`); +} + /** * function createExperiment * this call is used for create an experiment. @@ -200,6 +210,7 @@ export { searchExperiment, getExperimentById, updateExperimentById, + deleteExperimentById, createExperiment, getRunsOfExperiment, getExperimentContributions, diff --git a/src/src/modules/core/engine/explorer-engine/index.ts b/src/src/modules/core/engine/explorer-engine/index.ts index 432db8b9..6c7e6f7d 100644 --- a/src/src/modules/core/engine/explorer-engine/index.ts +++ b/src/src/modules/core/engine/explorer-engine/index.ts @@ -13,6 +13,8 @@ import getQueryParamsFromState from 'modules/core/utils/getQueryParamsFromState' import { AimFlatObjectBase } from 'types/core/AimObjects'; import { SequenceTypesEnum } from 'types/core/enums'; +import { setItem } from 'utils/storage'; + import createPipelineEngine, { IPipelineEngine } from '../pipeline'; import createInstructionsEngine, { IInstructionsEngine } from '../instructions'; import { INotificationsState, PipelineStatusEnum } from '../types'; @@ -354,7 +356,7 @@ function createEngine( const removeHistoryListener = config.persist && browserHistory.listen((update: Update) => { - localStorage.setItem( + setItem( `${basePath}Url`, update.location.pathname + update.location.search, ); diff --git a/src/src/modules/core/engine/visualizations/index.ts b/src/src/modules/core/engine/visualizations/index.ts index cafeba8d..e066c4e6 100644 --- a/src/src/modules/core/engine/visualizations/index.ts +++ b/src/src/modules/core/engine/visualizations/index.ts @@ -15,6 +15,7 @@ import getUrlSearchParam from 'modules/core/utils/getUrlSearchParam'; import getStateFromLocalStorage from 'utils/getStateFromLocalStorage'; import { encode } from 'utils/encoder/encoder'; +import { removeItem, setItem } from 'utils/storage'; import { ControlsConfigs } from '../explorer/state/controls'; import { PersistenceTypesEnum } from '../types'; @@ -224,12 +225,12 @@ function createVisualizationEngine( boxMethods.reset = () => { originalMethods.reset(); - localStorage.removeItem(boxPersistenceKey); + removeItem(boxPersistenceKey); }; boxMethods.update = (newValue: Partial) => { originalMethods.update(newValue); - localStorage.setItem(boxPersistenceKey, encode(newValue)); + setItem(boxPersistenceKey, encode(newValue)); }; } diff --git a/src/src/pages/Dashboard/Dashboard.tsx b/src/src/pages/Dashboard/Dashboard.tsx index 1dbda66f..6899d409 100644 --- a/src/src/pages/Dashboard/Dashboard.tsx +++ b/src/src/pages/Dashboard/Dashboard.tsx @@ -6,7 +6,6 @@ import { Spinner, Text } from 'components/kit'; import ProjectContributions from './components/ProjectContributions/ProjectContributions'; import ExploreSection from './components/ExploreSection/ExploreSection'; -import DashboardRight from './components/DashboardRight/DashboardRight'; import DashboardContributionsFeed from './components/DashboardContributionsFeed'; import ProjectStatistics from './components/ProjectStatistics'; import useProjectContributions from './components/ProjectContributions/useProjectContributions'; @@ -54,7 +53,6 @@ function Dashboard(): React.FunctionComponentElement { )} {!isLoading && !totalRunsCount && } - ); diff --git a/src/src/pages/Dashboard/components/AimIntegrations/AimIntegrations.tsx b/src/src/pages/Dashboard/components/AimIntegrations/AimIntegrations.tsx index 67bb80d2..cccc2c12 100644 --- a/src/src/pages/Dashboard/components/AimIntegrations/AimIntegrations.tsx +++ b/src/src/pages/Dashboard/components/AimIntegrations/AimIntegrations.tsx @@ -4,13 +4,12 @@ import { Accordion, AccordionDetails, AccordionSummary, - Link, } from '@material-ui/core'; import { Icon, Text } from 'components/kit'; import CodeBlock from 'components/CodeBlock/CodeBlock'; -import { DOCUMENTATIONS } from 'config/references'; +import { getTrackingURI } from 'config/config'; import './AimIntegrations.scss'; @@ -25,133 +24,103 @@ function AimIntegrations() { const integrations = [ { title: 'Integrate PyTorch Lightning', - docsLink: DOCUMENTATIONS.INTEGRATIONS.PYTORCH_LIGHTNING, - code: `from aim.pytorch_lightning import AimLogger + code: `import pytorch_lightning as pl +import mlflow -# ... -trainer = pl.Trainer(logger=AimLogger(experiment='experiment_name')) -# ...`, - }, - { - title: 'Integrate Hugging Face', - docsLink: DOCUMENTATIONS.INTEGRATIONS.HUGGING_FACE, - code: `from aim.hugging_face import AimCallback +# Set FastTrackML tracking server +mlflow.set_tracking_uri("${getTrackingURI()}") + +# Enable autologging +mlflow.pytorch.autolog() # ... -aim_callback = AimCallback(repo='/path/to/logs/dir', experiment='mnli') -trainer = Trainer( - model=model, - args=training_args, - train_dataset=train_dataset if training_args.do_train else None, - eval_dataset=eval_dataset if training_args.do_eval else None, - callbacks=[aim_callback], - # ... -) +trainer = pl.Trainer() +trainer.fit(model, dm) +trainer.test() # ...`, }, { title: 'Integrate Keras & tf.keras', - docsLink: DOCUMENTATIONS.INTEGRATIONS.KERAS, - code: `import aim + code: `from tensorflow import keras +import mlflow -# ... -model.fit(x_train, y_train, epochs=epochs, callbacks=[ - aim.keras.AimCallback(repo='/path/to/logs/dir', experiment='experiment_name') - - # Use aim.tensorflow.AimCallback in case of tf.keras - aim.tensorflow.AimCallback(repo='/path/to/logs/dir', experiment='experiment_name') -]) -# ...`, - }, - { - title: 'Integrate KerasTuner', - docsLink: DOCUMENTATIONS.INTEGRATIONS.KERAS_TUNER, - code: `from aim.keras_tuner import AimCallback +# Set FastTrackML tracking server +mlflow.set_tracking_uri("${getTrackingURI()}") + +# Enable autologging +mlflow.tensorflow.autolog() # ... -tuner.search( - train_ds, - validation_data=test_ds, - callbacks=[AimCallback(tuner=tuner, repo='.', experiment='keras_tuner_test')], -) +results = keras_model.fit( + x_train, y_train, epochs=20, batch_size=128, validation_data=(x_val, y_val)) # ...`, }, { title: 'Integrate XGBoost', - docsLink: DOCUMENTATIONS.INTEGRATIONS.XGBOOST, - code: `from aim.xgboost import AimCallback + code: `import mlflow +import xgboost as xgb -# ... -aim_callback = AimCallback(repo='/path/to/logs/dir', experiment='experiment_name') -bst = xgb.train(param, xg_train, num_round, watchlist, callbacks=[aim_callback]) -# ...`, - }, - { - title: 'Integrate CatBoost', - docsLink: DOCUMENTATIONS.INTEGRATIONS.CATBOOST, - code: `from aim.catboost import AimLogger +# Set FastTrackML tracking server +mlflow.set_tracking_uri("${getTrackingURI()}") -# ... -model.fit(train_data, train_labels, log_cout=AimLogger(loss_function='Logloss'), logging_level="Info") -# ...`, +# Enable autologging +mlflow.xgboost.autolog() + +# Start MLflow session +with mlflow.start_run(): + # ... + model = xgb.train(params, dtrain, evals=[(dtrain, "train")]) + # ...`, }, { title: 'Integrate fastai', - docsLink: DOCUMENTATIONS.INTEGRATIONS.FASTAI, - code: `from aim.fastai import AimCallback + code: `from fastai.learner import Learner +import mlflow + +# Set FastTrackML tracking server +mlflow.set_tracking_uri(${getTrackingURI()}) + +# Enable autologging +mlflow.fastai.autolog() # ... -learn = cnn_learner(dls, resnet18, pretrained=True, - loss_func=CrossEntropyLossFlat(), - metrics=accuracy, model_dir="/tmp/model/", - cbs=AimCallback(repo='.', experiment='fastai_test')) -# ...`, + +# Create Learner model +learn = Learner(get_data_loaders(), Model(), loss_func=nn.MSELoss(), splitter=splitter) + +# Start MLflow session +with mlflow.start_run(): + # ... + learn.fit_one_cycle(args.epochs, args.lr) + # ...`, }, { title: 'Integrate LightGBM', - docsLink: DOCUMENTATIONS.INTEGRATIONS.LIGHT_GBM, - code: `from aim.lightgbm import AimCallback + code: `import lightgbm as lgb +import mlflow -# ... -aim_callback = AimCallback(experiment='lgb_test') -aim_callback.experiment['hparams'] = params - -gbm = lgb.train(params, - lgb_train, - num_boost_round=20, - valid_sets=lgb_eval, - callbacks=[aim_callback, lgb.early_stopping(stopping_rounds=5)]) -# ...`, - }, +# Set FastTrackML tracking server +mlflow.set_tracking_uri("${getTrackingURI()}") - { - title: 'Integrate PyTorch Ignite', - docsLink: DOCUMENTATIONS.INTEGRATIONS.PYTORCH_IGNITE, - code: `from aim.pytorch_ignite import AimLogger +# Enable autologging +mlflow.lightgbm.autolog() # ... -aim_logger = AimLogger() - -aim_logger.log_params({ - "model": model.__class__.__name__, - "pytorch_version": str(torch.__version__), - "ignite_version": str(ignite.__version__), -}) - -aim_logger.attach_output_handler( - trainer, - event_name=Events.ITERATION_COMPLETED, - tag="train", - output_transform=lambda loss: {'loss': loss} -) -# ...`, + +# Start MLflow session +with mlflow.start_run(): + # ... + model = lgb.train( + params, train_set, num_boost_round=10, valid_sets=[train_set], valid_names=["train"] + ) + # ...`, }, ]; return (
- Integrate Aim with your favorite ML framework + Integrate FastTrackML with your favorite ML framework
{integrations.map((item, i) => ( @@ -173,24 +142,6 @@ aim_logger.attach_output_handler( - - See documentation{' '} - - here - - . - ))} diff --git a/src/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.tsx b/src/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.tsx index d2bf9177..35ef65b5 100644 --- a/src/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.tsx +++ b/src/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.tsx @@ -22,62 +22,6 @@ const statisticsInitialMap: Record = { iconBgColor: '#7A4CE0', navLink: routes.METRICS.path, }, - systemMetrics: { - label: 'Sys. metrics', - count: 0, - icon: 'metrics', - iconBgColor: '#AF4EAB', - navLink: `${routes.METRICS.path}?select=${encode({ - advancedQuery: "metric.name.startswith('__system__') == True", - advancedMode: true, - })}`, - }, - [SequenceTypesEnum.Figures]: { - label: 'Figures', - icon: 'figures', - count: 0, - iconBgColor: '#18AB6D', - navLink: routes.FIGURES_EXPLORER.path, - }, - [SequenceTypesEnum.Images]: { - label: 'Images', - icon: 'images', - count: 0, - iconBgColor: '#F17922', - navLink: routes.IMAGE_EXPLORE.path, - }, - [SequenceTypesEnum.Audios]: { - label: 'Audios', - icon: 'audios', - count: 0, - iconBgColor: '#FCB500', - navLink: routes.AUDIOS_EXPLORER.path, - badge: { - value: 'New', - style: { backgroundColor: '#1473e6', color: '#fff' }, - }, - }, - [SequenceTypesEnum.Texts]: { - label: 'Texts', - icon: 'text', - count: 0, - iconBgColor: '#E149A0', - navLink: routes.TEXT_EXPLORER.path, - badge: { - value: 'New', - style: { backgroundColor: '#1473e6', color: '#fff' }, - }, - }, - [SequenceTypesEnum.Distributions]: { - label: 'Distributions', - icon: 'distributions', - count: 0, - iconBgColor: '#0394B4', - navLink: '', - badge: { - value: 'Explorer coming soon', - }, - }, }; const runsCountingInitialMap: Record<'archived' | 'runs', IProjectStatistic> = { diff --git a/src/src/pages/Dashboard/components/ProjectStatistics/ProjectStatisticsStore.ts b/src/src/pages/Dashboard/components/ProjectStatistics/ProjectStatisticsStore.ts index a9cca3a1..effffccd 100644 --- a/src/src/pages/Dashboard/components/ProjectStatistics/ProjectStatisticsStore.ts +++ b/src/src/pages/Dashboard/components/ProjectStatistics/ProjectStatisticsStore.ts @@ -6,14 +6,7 @@ import { SequenceTypesEnum } from 'types/core/enums'; function projectStatisticsEngine() { const { fetchData, state, destroy } = createResource(() => getParams({ - sequence: [ - SequenceTypesEnum.Metric, - SequenceTypesEnum.Images, - SequenceTypesEnum.Figures, - SequenceTypesEnum.Texts, - SequenceTypesEnum.Audios, - SequenceTypesEnum.Distributions, - ], + sequence: [SequenceTypesEnum.Metric], exclude_params: true, }), ); diff --git a/src/src/pages/Dashboard/components/QuickStart/QuickStart.tsx b/src/src/pages/Dashboard/components/QuickStart/QuickStart.tsx index 4608789d..d9d32442 100644 --- a/src/src/pages/Dashboard/components/QuickStart/QuickStart.tsx +++ b/src/src/pages/Dashboard/components/QuickStart/QuickStart.tsx @@ -5,6 +5,7 @@ import { Link } from '@material-ui/core'; import { Text } from 'components/kit'; import CodeBlock from 'components/CodeBlock/CodeBlock'; +import { getTrackingURI } from 'config/config'; import { DOCUMENTATIONS } from 'config/references'; import './QuickStart.scss'; @@ -28,24 +29,24 @@ function QuickStart() { tint={100} className='QuickStart__section__title' > - Integrate Aim with your code + Integrate FastTrackML with your code - import( - /* webpackChunkName: "ExperimentOverviewTab" */ './components/ExperimentNotesTab' - ), -); - const ExperimentSettingsTab = React.lazy( () => import( @@ -60,7 +53,6 @@ const ExperimentSettingsTab = React.lazy( const tabs: Record = { overview: 'Overview', runs: 'Runs', - notes: 'Notes', settings: 'Settings', }; @@ -75,6 +67,7 @@ function Experiment(): React.FunctionComponentElement { experimentsState, getExperimentsData, updateExperiment, + deleteExperiment, } = useExperimentState(experimentId); const { notificationState, onNotificationDelete } = useNotificationContainer(); @@ -104,16 +97,11 @@ function Experiment(): React.FunctionComponentElement { }, Component: ExperimentRunsTab, }, - notes: { - props: { - experimentId, - }, - Component: ExperimentNotesTab, - }, settings: { props: { experimentName: experimentData?.name ?? '', updateExperiment, + deleteExperiment, description: experimentData?.description ?? '', }, Component: ExperimentSettingsTab, diff --git a/src/src/pages/Experiment/ExperimentStore.ts b/src/src/pages/Experiment/ExperimentStore.ts index 42bcf8fd..62f32774 100644 --- a/src/src/pages/Experiment/ExperimentStore.ts +++ b/src/src/pages/Experiment/ExperimentStore.ts @@ -1,9 +1,12 @@ +import _ from 'lodash-es'; + import { notificationContainerStore } from 'components/NotificationContainer'; import { getExperimentById, getExperiments, updateExperimentById, + deleteExperimentById, IExperimentData, } from 'modules/core/api/experimentsApi'; import createResource from 'modules/core/utils/createResource'; @@ -50,6 +53,24 @@ function experimentEngine() { }); } + function deleteExperiment(successCallback: () => void = _.noop) { + const experimentData = experimentState.getState().data; + if (experimentData !== null && experimentData !== undefined) { + deleteExperimentById(experimentData.id) + .then(() => { + analytics.trackEvent('[Experiment] Delete Experiment'); + successCallback(); + }) + .catch((err) => { + notificationContainerStore.onNotificationAdd({ + id: Date.now(), + messages: [err.message || 'Something went wrong'], + severity: 'error', + }); + }); + } + } + return { fetchExperimentData, experimentState, @@ -58,6 +79,7 @@ function experimentEngine() { experimentsState, destroyExperiments, updateExperiment, + deleteExperiment, }; } diff --git a/src/src/pages/Experiment/components/ExperimentSettingsTab/ExperimentSettingsTab.d.ts b/src/src/pages/Experiment/components/ExperimentSettingsTab/ExperimentSettingsTab.d.ts index e4d2f900..07bb67d8 100644 --- a/src/src/pages/Experiment/components/ExperimentSettingsTab/ExperimentSettingsTab.d.ts +++ b/src/src/pages/Experiment/components/ExperimentSettingsTab/ExperimentSettingsTab.d.ts @@ -2,4 +2,5 @@ export interface IExperimentSettingsTabProps { experimentName: string; description: string; updateExperiment: (name: string, description: string) => void; + deleteExperiment: (successCallback: () => void) => void; } diff --git a/src/src/pages/Experiment/components/ExperimentSettingsTab/ExperimentSettingsTab.scss b/src/src/pages/Experiment/components/ExperimentSettingsTab/ExperimentSettingsTab.scss index b558b8d5..1f8e3348 100644 --- a/src/src/pages/Experiment/components/ExperimentSettingsTab/ExperimentSettingsTab.scss +++ b/src/src/pages/Experiment/components/ExperimentSettingsTab/ExperimentSettingsTab.scss @@ -4,5 +4,20 @@ padding: $space-lg 0; &__actionCardsCnt { margin: 0 toRem(164px); + + &__btn__delete { + .MuiButton-label { + display: initial; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + background-color: $error-color; + + &:hover { + background-color: $error-color; + } + } } } diff --git a/src/src/pages/Experiment/components/ExperimentSettingsTab/ExperimentSettingsTab.tsx b/src/src/pages/Experiment/components/ExperimentSettingsTab/ExperimentSettingsTab.tsx index 2fd3b8cc..9b8aa6b0 100644 --- a/src/src/pages/Experiment/components/ExperimentSettingsTab/ExperimentSettingsTab.tsx +++ b/src/src/pages/Experiment/components/ExperimentSettingsTab/ExperimentSettingsTab.tsx @@ -1,7 +1,10 @@ import React, { memo } from 'react'; +import { useHistory } from 'react-router-dom'; +import ConfirmModal from 'components/ConfirmModal/ConfirmModal'; import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; import NameAndDescriptionCard from 'components/NameAndDescriptionCard'; +import { ActionCard, Icon } from 'components/kit'; import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap'; @@ -15,7 +18,24 @@ function ExperimentSettingsTab({ experimentName, description, updateExperiment, + deleteExperiment, }: IExperimentSettingsTabProps): React.FunctionComponentElement { + const history = useHistory(); + const [openDeleteModal, setOpenDeleteModal] = React.useState(false); + + function onExperimentDelete() { + deleteExperiment(() => { + history.push('/experiments'); + }); + } + + function handleDeleteModalOpen() { + setOpenDeleteModal(true); + } + + function handleDeleteModalClose() { + setOpenDeleteModal(false); + } React.useEffect(() => { analytics.pageView(ANALYTICS_EVENT_KEYS.experiment.tabs.settings.tabView); }, []); @@ -34,7 +54,28 @@ function ExperimentSettingsTab({ defaultDescription={description ?? ''} onSave={onSave} /> +
+ } + title='Delete experiment' + statusType='error' + confirmBtnText='Delete' + />
); diff --git a/src/src/pages/Experiment/useExperimentState.tsx b/src/src/pages/Experiment/useExperimentState.tsx index b3931e8f..b5632de9 100644 --- a/src/src/pages/Experiment/useExperimentState.tsx +++ b/src/src/pages/Experiment/useExperimentState.tsx @@ -30,6 +30,7 @@ function useExperimentState(experimentId: string) { experimentsState, getExperimentsData: engine.fetchExperimentsData, updateExperiment: engine.updateExperiment, + deleteExperiment: engine.deleteExperiment, }; } diff --git a/src/src/pages/RunDetail/RunDetail.tsx b/src/src/pages/RunDetail/RunDetail.tsx index 3822c7cb..ce44b72c 100644 --- a/src/src/pages/RunDetail/RunDetail.tsx +++ b/src/src/pages/RunDetail/RunDetail.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import _ from 'lodash-es'; import classNames from 'classnames'; import moment from 'moment'; import { @@ -43,11 +42,6 @@ import RunSelectPopoverContent from './RunSelectPopoverContent'; import './RunDetail.scss'; -const RunDetailNotesTab = React.lazy( - () => - import(/* webpackChunkName: "RunDetailNotesTab" */ './RunDetailNotesTab'), -); - const RunDetailParamsTab = React.lazy( () => import(/* webpackChunkName: "RunDetailParamsTab" */ './RunDetailParamsTab'), @@ -64,35 +58,14 @@ const RunDetailMetricsAndSystemTab = React.lazy( /* webpackChunkName: "RunDetailMetricsAndSystemTab" */ './RunDetailMetricsAndSystemTab' ), ); -const TraceVisualizationContainer = React.lazy( - () => - import( - /* webpackChunkName: "TraceVisualizationContainer" */ './TraceVisualizationContainer' - ), -); const RunOverviewTab = React.lazy( () => import(/* webpackChunkName: "RunOverviewTab" */ './RunOverviewTab'), ); -const RunLogsTab = React.lazy( - () => import(/* webpackChunkName: "RunLogsTab" */ './RunLogsTab'), -); -const RunLogRecords = React.lazy( - () => import(/* webpackChunkName: "RunLogRecords" */ './RunLogRecords'), -); const tabs: Record = { overview: 'Overview', run_parameters: 'Run Params', - notes: 'Notes', - logs: 'Logs', - messages: 'Messages', metrics: 'Metrics', - system: 'System', - distributions: 'Distributions', - images: 'Images', - audios: 'Audios', - texts: 'Texts', - figures: 'Figures', settings: 'Settings', }; @@ -136,21 +109,6 @@ function RunDetail(): React.FunctionComponentElement { isRunInfoLoading={runData?.isRunInfoLoading} /> ), - logs: ( - - ), - messages: ( - - ), metrics: ( { isRunBatchLoading={runData?.isRunBatchLoading} /> ), - system: ( - - ), - distributions: ( - - ), - images: ( - - ), - audios: ( - - ), - texts: ( - - ), - figures: ( - - ), settings: ( { runHash={runHash} /> ), - notes: , }; function getRunsOfExperiment( diff --git a/src/src/pages/RunDetail/RunOverviewTab/components/RunOverviewSidebar/RunOverviewSidebar.tsx b/src/src/pages/RunDetail/RunOverviewTab/components/RunOverviewSidebar/RunOverviewSidebar.tsx index b17f9cbe..0cbd65b0 100644 --- a/src/src/pages/RunDetail/RunOverviewTab/components/RunOverviewSidebar/RunOverviewSidebar.tsx +++ b/src/src/pages/RunDetail/RunOverviewTab/components/RunOverviewSidebar/RunOverviewSidebar.tsx @@ -54,46 +54,11 @@ function RunOverviewSidebar({ const systemMetricsLength: number = traces.metric.filter((m) => m.name.startsWith('__system__')).length || 0; return [ - { - name: 'Notes', - path: `${path}/notes`, - value: info.notes || 0, - }, { name: 'Metrics', path: `${path}/metrics`, value: traces?.metric?.length - systemMetricsLength || 0, }, - { - name: 'System', - path: `${path}/system`, - value: systemMetricsLength, - }, - { - name: 'Distributions', - path: `${path}/distributions`, - value: traces?.distributions?.length || 0, - }, - { - name: 'Images', - path: `${path}/images`, - value: traces?.images?.length || 0, - }, - { - name: 'Audios', - path: `${path}/audios`, - value: traces?.audios?.length || 0, - }, - { - name: 'Texts', - path: `${path}/texts`, - value: traces?.texts?.length || 0, - }, - { - name: 'Figures', - path: `${path}/figures`, - value: traces?.figures?.length || 0, - }, ]; // eslint-disable-next-line react-hooks/exhaustive-deps }, [traces]); diff --git a/src/src/routes/routes.tsx b/src/src/routes/routes.tsx index 75c659b2..ac46be9a 100644 --- a/src/src/routes/routes.tsx +++ b/src/src/routes/routes.tsx @@ -125,7 +125,7 @@ const routes = { TEXT_EXPLORER: { path: PathEnum.Text_Explorer, component: TextExplorer, - showInSidebar: true, + showInSidebar: false, icon: 'text', displayName: 'Text', isExact: true, @@ -134,7 +134,7 @@ const routes = { IMAGE_EXPLORE: { path: PathEnum.Images_Explore, component: ImagesExplore, - showInSidebar: true, + showInSidebar: false, displayName: 'Images', icon: 'images', isExact: true, @@ -151,7 +151,7 @@ const routes = { FIGURES_EXPLORER: { path: PathEnum.Figures_Explorer, component: FiguresExplore, - showInSidebar: true, + showInSidebar: false, icon: 'figures', displayName: 'Figures', isExact: true, @@ -160,7 +160,7 @@ const routes = { AUDIOS_EXPLORER: { path: PathEnum.Audios_Explorer, component: AudiosExplorer, - showInSidebar: true, + showInSidebar: false, icon: 'audios', displayName: 'Audios', isExact: true, diff --git a/src/src/utils/app/updateUrlParam.ts b/src/src/utils/app/updateUrlParam.ts index ac491cbc..4f0a64df 100644 --- a/src/src/utils/app/updateUrlParam.ts +++ b/src/src/utils/app/updateUrlParam.ts @@ -1,3 +1,5 @@ +import { getPrefix } from 'config/config'; + import getUrlWithParam from 'utils/getUrlWithParam'; import { setItem } from 'utils/storage'; @@ -22,9 +24,12 @@ export default function updateUrlParam({ } const isExistBasePath = (window as any).API_BASE_PATH !== '{{ base_path }}'; + const hasPrefix = getPrefix() !== '/'; const appId: string = - window.location.pathname.split('/')[isExistBasePath ? 3 : 2]; + window.location.pathname.split('/')[ + (isExistBasePath ? 3 : 2) + (hasPrefix ? 2 : 0) + ]; if (!appId) { let fullURL = url; diff --git a/src/src/utils/document/documentTitle.ts b/src/src/utils/document/documentTitle.ts index cd4e0fb2..0359dee7 100644 --- a/src/src/utils/document/documentTitle.ts +++ b/src/src/utils/document/documentTitle.ts @@ -8,12 +8,12 @@ export function getDocumentTitle(pathname: string): { if (paths[0]) { return { title: _.capitalize(paths[0]), withPrefix: true }; } - return { title: 'Aim', withPrefix: false }; + return { title: 'FastTrackML (modern)', withPrefix: false }; } export function setDocumentTitle( - title: string = 'Aim', + title: string = 'FastTrackML (modern)', withPrefix: boolean = false, ): void { - document.title = title + (withPrefix ? ' | Aim' : ''); + document.title = title + (withPrefix ? ' | FastTrackML (modern)' : ''); } diff --git a/src/src/utils/formatByAlignment.ts b/src/src/utils/formatByAlignment.ts index 8b93a306..95c499e2 100644 --- a/src/src/utils/formatByAlignment.ts +++ b/src/src/utils/formatByAlignment.ts @@ -44,8 +44,9 @@ function formatValueByAlignment({ function getKeyByAlignment(alignmentConfig?: IAlignmentConfig): string { switch (alignmentConfig?.type) { case AlignmentOptionsEnum.STEP: + return 'logging iteration'; case AlignmentOptionsEnum.EPOCH: - return alignmentConfig?.type; + return 'step'; case AlignmentOptionsEnum.ABSOLUTE_TIME: case AlignmentOptionsEnum.RELATIVE_TIME: return alignmentConfig?.type.replace('_', ' '); diff --git a/src/src/utils/getStateFromLocalStorage.ts b/src/src/utils/getStateFromLocalStorage.ts index 66b763e5..3dfd93c9 100644 --- a/src/src/utils/getStateFromLocalStorage.ts +++ b/src/src/utils/getStateFromLocalStorage.ts @@ -1,7 +1,9 @@ +import { getItem } from 'utils/storage'; + import { decode } from './encoder/encoder'; export default function getStateFromLocalStorage(key: string) { - const data: any = localStorage.getItem(key); + const data: any = getItem(key); if (data) { return JSON.parse(decode(data)); } diff --git a/src/src/utils/storage.ts b/src/src/utils/storage.ts index 0d87b57f..4e716068 100644 --- a/src/src/utils/storage.ts +++ b/src/src/utils/storage.ts @@ -1,12 +1,14 @@ +import { getPrefix } from 'config/config'; + export function setItem(key: string, value: any) { try { - localStorage.setItem(key, value); + localStorage.setItem(getNamespacedKey(key), value); } catch (error) {} } export function getItem(key: string) { try { - return localStorage.getItem(key); + return localStorage.getItem(getNamespacedKey(key)); } catch (error) { return null; } @@ -14,7 +16,7 @@ export function getItem(key: string) { export function removeItem(key: string) { try { - localStorage.removeItem(key); + localStorage.removeItem(getNamespacedKey(key)); } catch (error) {} } @@ -23,3 +25,11 @@ export function clear() { localStorage.clear(); } catch (error) {} } + +function getNamespacedKey(key: string) { + let prefix = getPrefix(); + if (prefix === '/') { + return key; + } + return prefix.replace(new RegExp('/ns/([^/]+)/'), '$1') + ':' + key; +} From f361332997ab321372f4ad0bfbda12fde815c553 Mon Sep 17 00:00:00 2001 From: Jonathan Giannuzzi Date: Tue, 2 Jan 2024 12:43:11 +0000 Subject: [PATCH 02/79] Add support for system metrics (#33) --- src/src/config/systemMetrics/systemMetrics.ts | 64 +++++++++++++++--- .../ProjectStatistics/ProjectStatistics.tsx | 12 +++- src/src/pages/RunDetail/RunDetail.tsx | 10 +++ .../RunOverviewSidebar/RunOverviewSidebar.tsx | 7 +- .../RunsTableGrid/RunsTableGrid.tsx | 27 +++++--- .../services/models/runs/runDetailAppModel.ts | 2 +- .../types/utils/formatSystemMetricName.d.ts | 66 ++++++++++++++++--- 7 files changed, 159 insertions(+), 29 deletions(-) diff --git a/src/src/config/systemMetrics/systemMetrics.ts b/src/src/config/systemMetrics/systemMetrics.ts index d7703311..2d2ffebf 100644 --- a/src/src/config/systemMetrics/systemMetrics.ts +++ b/src/src/config/systemMetrics/systemMetrics.ts @@ -1,12 +1,60 @@ import { systemMetricsDictType } from 'types/utils/formatSystemMetricName'; export const systemMetricsDict: systemMetricsDictType = { - __system__cpu: 'CPU (%)', - __system__p_memory_percent: 'Process Memory (%)', - __system__memory_percent: 'Memory (%)', - __system__disk_percent: 'Disk (%)', - __system__gpu: 'GPU (%)', - __system__gpu_memory_percent: 'GPU Memory (%)', - __system__gpu_power_watts: 'GPU Power (W)', - __system__gpu_temp: 'GPU Temperature (°C)', + 'system/cpu_utilization_percentage': 'CPU (%)', + 'system/disk_available_megabytes': 'Disk Available (MB)', + 'system/disk_usage_megabytes': 'Disk (MB)', + 'system/disk_usage_percentage': 'Disk (%)', + 'system/gpu_0_memory_usage_megabytes': 'GPU 0 Memory (MB)', + 'system/gpu_0_memory_usage_percentage': 'GPU 0 Memory (%)', + 'system/gpu_0_utilization_percentage': 'GPU 0 (%)', + 'system/gpu_1_memory_usage_megabytes': 'GPU 1 Memory (MB)', + 'system/gpu_1_memory_usage_percentage': 'GPU 1 Memory (%)', + 'system/gpu_1_utilization_percentage': 'GPU 1 (%)', + 'system/gpu_2_memory_usage_megabytes': 'GPU 2 Memory (MB)', + 'system/gpu_2_memory_usage_percentage': 'GPU 2 Memory (%)', + 'system/gpu_2_utilization_percentage': 'GPU 2 (%)', + 'system/gpu_3_memory_usage_megabytes': 'GPU 3 Memory (MB)', + 'system/gpu_3_memory_usage_percentage': 'GPU 3 Memory (%)', + 'system/gpu_3_utilization_percentage': 'GPU 3 (%)', + 'system/gpu_4_memory_usage_megabytes': 'GPU 4 Memory (MB)', + 'system/gpu_4_memory_usage_percentage': 'GPU 4 Memory (%)', + 'system/gpu_4_utilization_percentage': 'GPU 4 (%)', + 'system/gpu_5_memory_usage_megabytes': 'GPU 5 Memory (MB)', + 'system/gpu_5_memory_usage_percentage': 'GPU 5 Memory (%)', + 'system/gpu_5_utilization_percentage': 'GPU 5 (%)', + 'system/gpu_6_memory_usage_megabytes': 'GPU 6 Memory (MB)', + 'system/gpu_6_memory_usage_percentage': 'GPU 6 Memory (%)', + 'system/gpu_6_utilization_percentage': 'GPU 6 (%)', + 'system/gpu_7_memory_usage_megabytes': 'GPU 7 Memory (MB)', + 'system/gpu_7_memory_usage_percentage': 'GPU 7 Memory (%)', + 'system/gpu_7_utilization_percentage': 'GPU 7 (%)', + 'system/gpu_8_memory_usage_megabytes': 'GPU 8 Memory (MB)', + 'system/gpu_8_memory_usage_percentage': 'GPU 8 Memory (%)', + 'system/gpu_8_utilization_percentage': 'GPU 8 (%)', + 'system/gpu_9_memory_usage_megabytes': 'GPU 9 Memory (MB)', + 'system/gpu_9_memory_usage_percentage': 'GPU 9 Memory (%)', + 'system/gpu_9_utilization_percentage': 'GPU 9 (%)', + 'system/gpu_10_memory_usage_megabytes': 'GPU 10 Memory (MB)', + 'system/gpu_10_memory_usage_percentage': 'GPU 10 Memory (%)', + 'system/gpu_10_utilization_percentage': 'GPU 10 (%)', + 'system/gpu_11_memory_usage_megabytes': 'GPU 11 Memory (MB)', + 'system/gpu_11_memory_usage_percentage': 'GPU 11 Memory (%)', + 'system/gpu_11_utilization_percentage': 'GPU 11 (%)', + 'system/gpu_12_memory_usage_megabytes': 'GPU 12 Memory (MB)', + 'system/gpu_12_memory_usage_percentage': 'GPU 12 Memory (%)', + 'system/gpu_12_utilization_percentage': 'GPU 12 (%)', + 'system/gpu_13_memory_usage_megabytes': 'GPU 13 Memory (MB)', + 'system/gpu_13_memory_usage_percentage': 'GPU 13 Memory (%)', + 'system/gpu_13_utilization_percentage': 'GPU 13 (%)', + 'system/gpu_14_memory_usage_megabytes': 'GPU 14 Memory (MB)', + 'system/gpu_14_memory_usage_percentage': 'GPU 14 Memory (%)', + 'system/gpu_14_utilization_percentage': 'GPU 14 (%)', + 'system/gpu_15_memory_usage_megabytes': 'GPU 15 Memory (MB)', + 'system/gpu_15_memory_usage_percentage': 'GPU 15 Memory (%)', + 'system/gpu_15_utilization_percentage': 'GPU 15 (%)', + 'system/network_receive_megabytes': 'Network Rx (MB)', + 'system/network_transmit_megabytes': 'Network Tx (MB)', + 'system/system_memory_usage_megabytes': 'Memory (MB)', + 'system/system_memory_usage_percentage': 'Memory (%)', }; diff --git a/src/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.tsx b/src/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.tsx index 35ef65b5..65041889 100644 --- a/src/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.tsx +++ b/src/src/pages/Dashboard/components/ProjectStatistics/ProjectStatistics.tsx @@ -22,6 +22,16 @@ const statisticsInitialMap: Record = { iconBgColor: '#7A4CE0', navLink: routes.METRICS.path, }, + systemMetrics: { + label: 'Sys. metrics', + count: 0, + icon: 'metrics', + iconBgColor: '#AF4EAB', + navLink: `${routes.METRICS.path}?select=${encode({ + advancedQuery: "metric.name.startswith('system/')", + advancedMode: true, + })}`, + }, }; const runsCountingInitialMap: Record<'archived' | 'runs', IProjectStatistic> = { @@ -59,7 +69,7 @@ function ProjectStatistics() { let systemMetricsCount = 0; let sequenceItemsCount = 0; for (let [itemKey, itemData] of Object.entries(seqData)) { - if (itemKey.startsWith('__system__')) { + if (itemKey.startsWith('system/')) { systemMetricsCount += itemData.length; } else { sequenceItemsCount += itemData.length; diff --git a/src/src/pages/RunDetail/RunDetail.tsx b/src/src/pages/RunDetail/RunDetail.tsx index ce44b72c..6f0c5756 100644 --- a/src/src/pages/RunDetail/RunDetail.tsx +++ b/src/src/pages/RunDetail/RunDetail.tsx @@ -66,6 +66,7 @@ const tabs: Record = { overview: 'Overview', run_parameters: 'Run Params', metrics: 'Metrics', + system: 'System', settings: 'Settings', }; @@ -117,6 +118,15 @@ function RunDetail(): React.FunctionComponentElement { isRunBatchLoading={runData?.isRunBatchLoading} /> ), + system: ( + + ), settings: ( { const path = url.split('/').slice(0, -1).join('/'); const systemMetricsLength: number = - traces.metric.filter((m) => m.name.startsWith('__system__')).length || 0; + traces.metric.filter((m) => m.name.startsWith('system/')).length || 0; return [ { name: 'Metrics', path: `${path}/metrics`, value: traces?.metric?.length - systemMetricsLength || 0, }, + { + name: 'System', + path: `${path}/system`, + value: systemMetricsLength, + }, ]; // eslint-disable-next-line react-hooks/exhaustive-deps }, [traces]); diff --git a/src/src/pages/Runs/components/RunsTableGrid/RunsTableGrid.tsx b/src/src/pages/Runs/components/RunsTableGrid/RunsTableGrid.tsx index 5f69822f..1e14e6f8 100644 --- a/src/src/pages/Runs/components/RunsTableGrid/RunsTableGrid.tsx +++ b/src/src/pages/Runs/components/RunsTableGrid/RunsTableGrid.tsx @@ -14,7 +14,6 @@ import { ITableColumn } from 'types/pages/metrics/components/TableColumns/TableC import { ITagInfo } from 'types/pages/tags/Tags'; import { formatSystemMetricName } from 'utils/formatSystemMetricName'; -import alphabeticalSortComparator from 'utils/alphabeticalSortComparator'; import { getMetricHash } from 'utils/app/getMetricHash'; import { getMetricLabel } from 'utils/app/getMetricLabel'; import { isSystemMetric } from 'utils/isSystemMetric'; @@ -149,14 +148,24 @@ function getRunsTableColumns( }; isSystem ? systemMetricsList.push(column) : metricsList.push(column); }); - acc = [ - ...acc, - ...metricsList.sort(alphabeticalSortComparator({ orderBy: 'key' })), - ...systemMetricsList.sort( - alphabeticalSortComparator({ orderBy: 'key' }), - ), - ]; - return acc; + acc = [...acc, ...metricsList, ...systemMetricsList]; + return acc.sort((a: ITableColumn, b: ITableColumn) => { + const aIsSystem = isSystemMetric(a['key']); + const bIsSystem = isSystemMetric(b['key']); + if (aIsSystem && !bIsSystem) { + return -1; + } else if (!aIsSystem && bIsSystem) { + return 1; + } + const aLabel = (a['label'] as string).toUpperCase(); + const bLabel = (b['label'] as string).toUpperCase(); + if (aLabel < bLabel) { + return -1; + } else if (aLabel > bLabel) { + return 1; + } + return 0; + }); }, []), runColumns.map((param) => ({ key: param, diff --git a/src/src/services/models/runs/runDetailAppModel.ts b/src/src/services/models/runs/runDetailAppModel.ts index 1a60db76..149ba775 100644 --- a/src/src/services/models/runs/runDetailAppModel.ts +++ b/src/src/services/models/runs/runDetailAppModel.ts @@ -147,7 +147,7 @@ function processRunBatchData( }), sortKey: `${run.name}${contextName}`, }; - if (run.name.startsWith('__system__')) { + if (run.name.startsWith('system/')) { runSystemBatch.push(metric); } else { runMetricsBatch.push(metric); diff --git a/src/src/types/utils/formatSystemMetricName.d.ts b/src/src/types/utils/formatSystemMetricName.d.ts index e28f266b..17966c78 100644 --- a/src/src/types/utils/formatSystemMetricName.d.ts +++ b/src/src/types/utils/formatSystemMetricName.d.ts @@ -1,10 +1,58 @@ export type systemMetricsDictType = { - __system__cpu: string; - __system__p_memory_percent: string; - __system__memory_percent: string; - __system__disk_percent: string; - __system__gpu: string; - __system__gpu_memory_percent: string; - __system__gpu_power_watts: string; - __system__gpu_temp: string; -}; + 'system/cpu_utilization_percentage': string; + 'system/disk_available_megabytes': string; + 'system/disk_usage_megabytes': string; + 'system/disk_usage_percentage': string; + 'system/gpu_0_memory_usage_megabytes': string; + 'system/gpu_0_memory_usage_percentage': string; + 'system/gpu_0_utilization_percentage': string; + 'system/gpu_1_memory_usage_megabytes': string; + 'system/gpu_1_memory_usage_percentage': string; + 'system/gpu_1_utilization_percentage': string; + 'system/gpu_10_memory_usage_megabytes': string; + 'system/gpu_10_memory_usage_percentage': string; + 'system/gpu_10_utilization_percentage': string; + 'system/gpu_11_memory_usage_megabytes': string; + 'system/gpu_11_memory_usage_percentage': string; + 'system/gpu_11_utilization_percentage': string; + 'system/gpu_12_memory_usage_megabytes': string; + 'system/gpu_12_memory_usage_percentage': string; + 'system/gpu_12_utilization_percentage': string; + 'system/gpu_13_memory_usage_megabytes': string; + 'system/gpu_13_memory_usage_percentage': string; + 'system/gpu_13_utilization_percentage': string; + 'system/gpu_14_memory_usage_megabytes': string; + 'system/gpu_14_memory_usage_percentage': string; + 'system/gpu_14_utilization_percentage': string; + 'system/gpu_15_memory_usage_megabytes': string; + 'system/gpu_15_memory_usage_percentage': string; + 'system/gpu_15_utilization_percentage': string; + 'system/gpu_2_memory_usage_megabytes': string; + 'system/gpu_2_memory_usage_percentage': string; + 'system/gpu_2_utilization_percentage': string; + 'system/gpu_3_memory_usage_megabytes': string; + 'system/gpu_3_memory_usage_percentage': string; + 'system/gpu_3_utilization_percentage': string; + 'system/gpu_4_memory_usage_megabytes': string; + 'system/gpu_4_memory_usage_percentage': string; + 'system/gpu_4_utilization_percentage': string; + 'system/gpu_5_memory_usage_megabytes': string; + 'system/gpu_5_memory_usage_percentage': string; + 'system/gpu_5_utilization_percentage': string; + 'system/gpu_6_memory_usage_megabytes': string; + 'system/gpu_6_memory_usage_percentage': string; + 'system/gpu_6_utilization_percentage': string; + 'system/gpu_7_memory_usage_megabytes': string; + 'system/gpu_7_memory_usage_percentage': string; + 'system/gpu_7_utilization_percentage': string; + 'system/gpu_8_memory_usage_megabytes': string; + 'system/gpu_8_memory_usage_percentage': string; + 'system/gpu_8_utilization_percentage': string; + 'system/gpu_9_memory_usage_megabytes': string; + 'system/gpu_9_memory_usage_percentage': string; + 'system/gpu_9_utilization_percentage': string; + 'system/network_receive_megabytes': string; + 'system/network_transmit_megabytes': string; + 'system/system_memory_usage_megabytes': string; + 'system/system_memory_usage_percentage': string; +}; \ No newline at end of file From f6ce90b8a2e07b2221df010b0851d1ecc213f320 Mon Sep 17 00:00:00 2001 From: Jonathan Giannuzzi Date: Tue, 2 Jan 2024 15:46:52 +0000 Subject: [PATCH 03/79] Update caniuse-lite from 1.0.30001434 to 1.0.30001572 (#34) --- src/package-lock.json | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index 5f23879b..ec7fb241 100755 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "ui_v2", - "version": "3.17.5", + "version": "3.19.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ui_v2", - "version": "3.17.5", + "version": "3.19.3", "hasInstallScript": true, "dependencies": { "@aksel/structjs": "^1.0.0", @@ -11583,9 +11583,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001434", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", - "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==", + "version": "1.0.30001572", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz", + "integrity": "sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==", "funding": [ { "type": "opencollective", @@ -11594,6 +11594,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -46036,9 +46040,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001434", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", - "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==" + "version": "1.0.30001572", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz", + "integrity": "sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==" }, "canvas-fit": { "version": "1.5.0", From 9f5460e43be95dced92bb845856b98822eed27bb Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Thu, 18 Jan 2024 10:52:09 +0100 Subject: [PATCH 04/79] Namespace Selector in AIM UI (#35) Adds a new Namespace selector component on the AIM UI --- src/src/components/SideBar/SideBar.tsx | 70 +++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/src/src/components/SideBar/SideBar.tsx b/src/src/components/SideBar/SideBar.tsx index 5ab6a148..249214f9 100644 --- a/src/src/components/SideBar/SideBar.tsx +++ b/src/src/components/SideBar/SideBar.tsx @@ -1,7 +1,10 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { NavLink } from 'react-router-dom'; import { Drawer, Tooltip } from '@material-ui/core'; +import Select from '@material-ui/core/Select'; +import MenuItem from '@material-ui/core/MenuItem'; +import AccountTreeIcon from '@material-ui/icons/AccountTree'; import logoImg from 'assets/logo.svg'; @@ -24,6 +27,20 @@ import './Sidebar.scss'; function SideBar(): React.FunctionComponentElement { const [version, setVersion] = React.useState('unknown'); + const [namespaces, setNamespaces] = useState([]); + const [selectedNamespace, setSelectedNamespace] = useState(''); + const [tooltipOpen, setTooltipOpen] = useState(false); + const handleTooltipClose = () => { + setTooltipOpen(false); + }; + const handleTooltipOpen = () => { + setTooltipOpen(true); + }; + const style = { + fontSize: '0.875rem', + textAlign: 'right' as const, + paddingLeft: '5px', + }; useEffect(() => { fetch(`${getBaseHost()}/version`).then((response) => { @@ -33,6 +50,32 @@ function SideBar(): React.FunctionComponentElement { }); }, []); + useEffect(() => { + fetch(`${getBaseHost()}/admin/namespaces/list`) + .then((response) => response.json()) + .then((data) => + setNamespaces(data.map((item: { code: any }) => item.code)), + ); + + fetch(`${getBaseHost()}${getPrefix()}admin/namespaces/current`) + .then((response) => response.json()) + .then((data) => { + const selected = data.code; + setSelectedNamespace(selected); + }); + }, []); + + function selectNamespace(event: React.ChangeEvent<{ value: unknown }>) { + const selectedNamespace = event.target.value as string; + let newUrl = + selectedNamespace === 'default' + ? `${getBaseHost()}${routes.DASHBOARD.path}aim/` + : `${getBaseHost()}/ns/${selectedNamespace}${ + routes.DASHBOARD.path + }aim/`; + window.location.href = newUrl; + } + function getPathFromStorage(route: PathEnum): PathEnum | string { const path = getItem(`${route.slice(1)}Url`) ?? ''; if (path !== '' && path.startsWith(route)) { @@ -68,7 +111,7 @@ function SideBar(): React.FunctionComponentElement { key={index} to={() => getPathFromStorage(path)} exact={true} - isActive={(m, location) => + isActive={(m: any, location: { pathname: string }) => location.pathname.split('/')[1] === path.split('/')[1] } activeClassName='Sidebar__NavLink--active' @@ -91,6 +134,29 @@ function SideBar(): React.FunctionComponentElement {
+ + + From 94bb832e1d752e355388cfdc8f9a43f2248b43ef Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:05:09 +0000 Subject: [PATCH 05/79] Export all matched runs in Aim UI (#37) Download the whole runs table even if it is not completely displayed. --- src/src/components/Table/Table.tsx | 23 ++- .../models/explorer/paramsModelMethods.ts | 3 - .../models/explorer/runsModelMethods.ts | 156 ++++++++++++------ 3 files changed, 122 insertions(+), 60 deletions(-) diff --git a/src/src/components/Table/Table.tsx b/src/src/components/Table/Table.tsx index 723601cc..964bbd2d 100644 --- a/src/src/components/Table/Table.tsx +++ b/src/src/components/Table/Table.tsx @@ -6,6 +6,8 @@ import { isEmpty, isEqual, isNil } from 'lodash-es'; import { useResizeObserver } from 'hooks'; import _ from 'lodash-es'; +import CircularProgress from '@material-ui/core/CircularProgress'; + import { Button, Icon, Text } from 'components/kit'; import ControlPopover from 'components/ControlPopover/ControlPopover'; import IllustrationBlock from 'components/IllustrationBlock/IllustrationBlock'; @@ -143,6 +145,16 @@ const Table = React.forwardRef(function Table( width: 0, availableSpace: 0, }); + const [isExporting, setIsExporting] = React.useState(false); + + const handleExport = async () => { + setIsExporting(true); + try { + await onExport(); + } finally { + setIsExporting(false); + } + }; let groups = !Array.isArray(rowData); @@ -863,8 +875,15 @@ const Table = React.forwardRef(function Table( fullWidth variant='outlined' size='small' - onClick={onExport} - startIcon={} + onClick={handleExport} + startIcon={ + isExporting ? ( + + ) : ( + + ) + } + disabled={isExporting} > Export diff --git a/src/src/services/models/explorer/paramsModelMethods.ts b/src/src/services/models/explorer/paramsModelMethods.ts index b9eb97b2..bd1e84d6 100644 --- a/src/src/services/models/explorer/paramsModelMethods.ts +++ b/src/src/services/models/explorer/paramsModelMethods.ts @@ -223,9 +223,6 @@ function getParamsModelMethods( runsRequestRef.abort(); } const configData = { ...model.getState()?.config }; - if (queryString) { - configData.select.query = queryString; - } runsRequestRef = runsService.getRunsData(configData?.select?.query); setRequestProgress(model); return { diff --git a/src/src/services/models/explorer/runsModelMethods.ts b/src/src/services/models/explorer/runsModelMethods.ts index 45d85e7f..015abdf5 100644 --- a/src/src/services/models/explorer/runsModelMethods.ts +++ b/src/src/services/models/explorer/runsModelMethods.ts @@ -183,6 +183,44 @@ function getRunsModelMethods( onRunsTagsChange({ runHash, tags, model, updateModelData }); } + function getAllRunsData(queryString?: string): { + call: (exceptionHandler: (detail: any) => void) => Promise; + abort: () => void; + } { + if (runsRequestRef) { + runsRequestRef.abort(); + } + runsRequestRef = runsService.getRunsData(queryString); + return { + call: async () => { + try { + const stream = await runsRequestRef.call((detail) => { + exceptionHandler({ detail, model }); + }); + let bufferPairs = decodeBufferPairs(stream as ReadableStream); + let decodedPairs = decodePathsVals(bufferPairs); + let objects = iterFoldTree(decodedPairs, 1); + const runsData: IRun[] = []; + + for await (let [keys, val] of objects) { + const data = { ...(val as any), hash: keys[0] }; + if (!data.hash.startsWith('progress')) { + const runData: any = val; + runsData.push({ ...runData, hash: keys[0] } as any); + } + } + return runsData; + } catch (ex: Error | any) { + if (ex.name === 'AbortError') { + // eslint-disable-next-line no-console + console.error(`${ex.name}, ${ex.message}`); + } + } + }, + abort: runsRequestRef.abort, + }; + } + function getRunsData( shouldUrlUpdate?: boolean, shouldResetSelectedRows?: boolean, @@ -809,67 +847,75 @@ function getRunsModelMethods( } } - function onExportTableData(): void { + function onExportTableData(): Promise { // @TODO need to get data and params from state not from processData - const { data, params, metricsColumns } = processData( - model.getState()?.rawData, - ); - const tableData = getDataAsTableRows(data, metricsColumns, params, true); - const configData = model.getState()?.config; - const tableColumns: ITableColumn[] = getRunsTableColumns( - metricsColumns, - params, - configData?.table.columnsOrder!, - configData?.table.hiddenColumns!, - ); - const excludedFields: string[] = ['#', 'actions']; - const filteredHeader: string[] = tableColumns.reduce( - (acc: string[], column: ITableColumn) => - acc.concat( - excludedFields.indexOf(column.key) === -1 && !column.isHidden - ? column.key - : [], - ), - [], + const runsDataToExport = getAllRunsData( + model.getState()?.config?.select?.query, ); + const exceptionHandler = (detail: any) => { + // eslint-disable-next-line no-console + console.error('An error occurred:', detail); + }; - let emptyRow: { [key: string]: string } = {}; - filteredHeader.forEach((column: string) => { - emptyRow[column] = '--'; - }); + return runsDataToExport.call(exceptionHandler).then((rawData) => { + const { data, params, metricsColumns } = processData(rawData); + const tableData = getDataAsTableRows(data, metricsColumns, params, true); + const configData = model.getState()?.config; + const tableColumns: ITableColumn[] = getRunsTableColumns( + metricsColumns, + params, + configData?.table.columnsOrder!, + configData?.table.hiddenColumns!, + ); + const excludedFields: string[] = ['#', 'actions']; + const filteredHeader: string[] = tableColumns.reduce( + (acc: string[], column: ITableColumn) => + acc.concat( + excludedFields.indexOf(column.key) === -1 && !column.isHidden + ? column.key + : [], + ), + [], + ); - const groupedRows: IMetricTableRowData[][] = - data.length > 1 - ? Object.keys(tableData.rows).map( - (groupedRowKey: string) => tableData.rows[groupedRowKey].items, - ) - : [ - Array.isArray(tableData.rows) - ? tableData.rows - : tableData.rows[Object.keys(tableData.rows)[0]].items, - ]; - - const dataToExport: { [key: string]: string }[] = []; - - groupedRows?.forEach( - (groupedRow: IMetricTableRowData[], groupedRowIndex: number) => { - groupedRow?.forEach((row: IMetricTableRowData) => { - const filteredRow = getFilteredRow({ - columnKeys: filteredHeader, - row, + let emptyRow: { [key: string]: string } = {}; + filteredHeader.forEach((column: string) => { + emptyRow[column] = '--'; + }); + + const groupedRows: IMetricTableRowData[][] = + data.length > 1 + ? Object.keys(tableData.rows).map( + (groupedRowKey: string) => tableData.rows[groupedRowKey].items, + ) + : [ + Array.isArray(tableData.rows) + ? tableData.rows + : tableData.rows[Object.keys(tableData.rows)[0]].items, + ]; + + const dataToExport: { [key: string]: string }[] = []; + + groupedRows?.forEach( + (groupedRow: IMetricTableRowData[], groupedRowIndex: number) => { + groupedRow?.forEach((row: IMetricTableRowData) => { + const filteredRow = getFilteredRow({ + columnKeys: filteredHeader, + row, + }); + dataToExport.push(filteredRow); }); - dataToExport.push(filteredRow); - }); - if (groupedRows?.length - 1 !== groupedRowIndex) { - dataToExport.push(emptyRow); - } - }, - ); - const blob = new Blob([JsonToCSV(dataToExport)], { - type: 'text/csv;charset=utf-8;', + if (groupedRows?.length - 1 !== groupedRowIndex) { + dataToExport.push(emptyRow); + } + }, + ); + const blob = new Blob([JsonToCSV(dataToExport)], { + type: 'text/csv;charset=utf-8;', + }); + saveAs(blob, `runs-${moment().format(DATE_EXPORTING_FORMAT)}.csv`); + analytics.trackEvent(ANALYTICS_EVENT_KEYS[appName].table.exports.csv); }); - saveAs(blob, `runs-${moment().format(DATE_EXPORTING_FORMAT)}.csv`); - analytics.trackEvent(ANALYTICS_EVENT_KEYS[appName].table.exports.csv); } function onModelNotificationDelete(id: number): void { From c043868eb24ee5702f462d3a916f88de777258ef Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Thu, 1 Feb 2024 16:52:39 +0000 Subject: [PATCH 06/79] Preliminary ParamsScalePopover component (#36) * Add ParamsScalePopover interfaces * Add preliminary ParamsScalePopover component * Add onParamsScaleTypeChange logic * Add ParamsScalePopover to Params Controls * Improve ParamsScalePopover logic * Load selected params into component * Store params scale type state * Unify props into single variable * Fix onParamsScaleTypeChange logic to handle selectedParams state * Add scale support for high plot charts * Add analytics * Remove todos * Fix popover styling bug --- src/src/components/HighPlot/HighPlot.tsx | 2 + .../ParamsScalePopover.scss | 17 ++++++ .../ParamsScalePopover/ParamsScalePopover.tsx | 57 +++++++++++++++++++ src/src/config/analytics/analyticsKeysMap.ts | 2 + .../config/controls/controlsDefaultConfig.ts | 1 + src/src/pages/Params/Params.tsx | 11 ++++ src/src/pages/Params/ParamsContainer.tsx | 2 + .../Params/components/Controls/Controls.tsx | 42 ++++++++++++++ .../models/explorer/paramsModelMethods.ts | 4 ++ .../types/components/HighPlot/HighPlot.d.ts | 5 +- .../ParamsScalePopover.d.ts | 7 +++ src/src/types/pages/params/Params.d.ts | 2 + .../params/components/Controls/Controls.d.ts | 3 + .../models/explorer/createAppModel.d.ts | 4 +- src/src/types/utils/d3/drawParallelAxes.d.ts | 1 + src/src/utils/app/onParamsScaleTypeChange.ts | 42 ++++++++++++++ src/src/utils/d3/drawParallelAxes.ts | 5 +- 17 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 src/src/components/ParamsScalePopover/ParamsScalePopover.scss create mode 100644 src/src/components/ParamsScalePopover/ParamsScalePopover.tsx create mode 100644 src/src/types/components/ParamsScalePopover/ParamsScalePopover.d.ts create mode 100644 src/src/utils/app/onParamsScaleTypeChange.ts diff --git a/src/src/components/HighPlot/HighPlot.tsx b/src/src/components/HighPlot/HighPlot.tsx index 7f5f5dd5..35026bd0 100644 --- a/src/src/components/HighPlot/HighPlot.tsx +++ b/src/src/components/HighPlot/HighPlot.tsx @@ -45,6 +45,7 @@ const HighPlot = React.forwardRef(function HighPlot( bottom: 30, left: 60, }, + scaleStates, } = props; // boxes @@ -117,6 +118,7 @@ const HighPlot = React.forwardRef(function HighPlot( axesRef, dimensions: data.dimensions, plotBoxRef, + scaleStates, }); if (attributesRef?.current.xScale && attributesRef.current.yScale) { diff --git a/src/src/components/ParamsScalePopover/ParamsScalePopover.scss b/src/src/components/ParamsScalePopover/ParamsScalePopover.scss new file mode 100644 index 00000000..fe7cfeaa --- /dev/null +++ b/src/src/components/ParamsScalePopover/ParamsScalePopover.scss @@ -0,0 +1,17 @@ +@use 'src/styles/abstracts' as *; + +.ParamsScalePopover { + width: 21.5rem; + padding: 1rem; + + &__subtitle { + text-transform: uppercase; + } + + &__select { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: $space-xs; + } +} diff --git a/src/src/components/ParamsScalePopover/ParamsScalePopover.tsx b/src/src/components/ParamsScalePopover/ParamsScalePopover.tsx new file mode 100644 index 00000000..9874d19d --- /dev/null +++ b/src/src/components/ParamsScalePopover/ParamsScalePopover.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import { Text, ToggleButton } from 'components/kit'; +import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; + +import { IParamsScalePopoverProps } from 'types/components/ParamsScalePopover/ParamsScalePopover'; +import { ISelectOption } from 'types/services/models/explorer/createAppModel'; + +import { ScaleEnum } from 'utils/d3'; + +import './ParamsScalePopover.scss'; + +function ParamsScalePopover( + props: IParamsScalePopoverProps, +): React.FunctionComponentElement { + function handleScaleChange(val: string | number, id: any) { + const newParams = props.selectedParams.map((param) => { + if (param.key === id) { + param.scale = val as ScaleEnum; + } + return param; + }); + + props.onParamsScaleTypeChange(newParams); + } + + return ( + +
+ + Select Params Scale: + + {props.selectedParams.map((param: ISelectOption) => ( +
+ +
+ ))} +
+
+ ); +} + +export default React.memo(ParamsScalePopover); diff --git a/src/src/config/analytics/analyticsKeysMap.ts b/src/src/config/analytics/analyticsKeysMap.ts index b87a0430..b27cb13c 100644 --- a/src/src/config/analytics/analyticsKeysMap.ts +++ b/src/src/config/analytics/analyticsKeysMap.ts @@ -112,6 +112,8 @@ export const ANALYTICS_EVENT_KEYS = { }, changeColorIndicatorMode: '[ParamsExplorer][Chart][Controls] Change color indicator mode', // to value + changeParamsScale: + '[ParamsExplorer][Chart][Controls] Change params scale', // to value }, }, groupings: { diff --git a/src/src/config/controls/controlsDefaultConfig.ts b/src/src/config/controls/controlsDefaultConfig.ts index 0187d92b..4dbe1a56 100644 --- a/src/src/config/controls/controlsDefaultConfig.ts +++ b/src/src/config/controls/controlsDefaultConfig.ts @@ -76,6 +76,7 @@ export const CONTROLS_DEFAULT_CONFIG = { selectedFields: [], }, sortFields: [], + defaultParamsScaleType: ScaleEnum.Linear, }, images: { alignmentType: MediaItemAlignmentEnum.Height, diff --git a/src/src/pages/Params/Params.tsx b/src/src/pages/Params/Params.tsx index 33562445..122b9ee8 100644 --- a/src/src/pages/Params/Params.tsx +++ b/src/src/pages/Params/Params.tsx @@ -73,6 +73,7 @@ const Params = ({ hiddenColumns, liveUpdateConfig, selectFormData, + selectedParams, onTableRowHover, onTableRowClick, hideSystemMetrics, @@ -113,7 +114,13 @@ const Params = ({ sortOptions, onRunsTagsChange, onRowsVisibilityChange, + onParamsScaleTypeChange, }: IParamsProps): React.FunctionComponentElement => { + let scaleStates = selectedParams.reduce((acc, param) => { + (acc as any)[param.key] = param.scale; + return acc; + }, {}); + const [isProgressBarVisible, setIsProgressBarVisible] = React.useState(false); const chartProps: any[] = React.useMemo(() => { @@ -123,6 +130,7 @@ const Params = ({ onAxisBrushExtentChange, brushExtents, chartTitle: chartTitleData[chartData.data[0]?.chartIndex], + scaleStates, })); }, [ highPlotData, @@ -131,6 +139,7 @@ const Params = ({ chartTitleData, onAxisBrushExtentChange, brushExtents, + scaleStates, ]); return ( @@ -230,6 +239,8 @@ const Params = ({ } onColorIndicatorChange={onColorIndicatorChange} onChangeTooltip={onChangeTooltip} + onParamsScaleTypeChange={onParamsScaleTypeChange} + selectedParams={selectedParams} /> } /> diff --git a/src/src/pages/Params/ParamsContainer.tsx b/src/src/pages/Params/ParamsContainer.tsx index 7c2f39a0..ccb772b9 100644 --- a/src/src/pages/Params/ParamsContainer.tsx +++ b/src/src/pages/Params/ParamsContainer.tsx @@ -154,6 +154,7 @@ function ParamsContainer(): React.FunctionComponentElement { tableRowHeight={paramsData?.config?.table?.rowHeight!} columnsWidths={paramsData?.config?.table?.columnsWidths!} selectFormData={paramsData?.selectFormData!} + selectedParams={paramsData?.config?.select?.options!} onColorIndicatorChange={paramsAppModel.onColorIndicatorChange} onCurveInterpolationChange={paramsAppModel.onCurveInterpolationChange} onParamsSelectChange={paramsAppModel.onParamsSelectChange} @@ -192,6 +193,7 @@ function ParamsContainer(): React.FunctionComponentElement { archiveRuns={paramsAppModel.archiveRuns} deleteRuns={paramsAppModel.deleteRuns} onRowsVisibilityChange={paramsAppModel.onRowsVisibilityChange} + onParamsScaleTypeChange={paramsAppModel.onParamsScaleTypeChange} /> ); } diff --git a/src/src/pages/Params/components/Controls/Controls.tsx b/src/src/pages/Params/components/Controls/Controls.tsx index 014e4e2a..af6b4707 100644 --- a/src/src/pages/Params/components/Controls/Controls.tsx +++ b/src/src/pages/Params/components/Controls/Controls.tsx @@ -1,9 +1,11 @@ import React from 'react'; +import classNames from 'classnames'; import { Tooltip } from '@material-ui/core'; import ControlPopover from 'components/ControlPopover/ControlPopover'; import TooltipContentPopover from 'components/TooltipContentPopover/TooltipContentPopover'; +import ParamsScalePopover from 'components/ParamsScalePopover/ParamsScalePopover'; import { Icon } from 'components/kit'; import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; @@ -26,6 +28,15 @@ function Controls( CONTROLS_DEFAULT_CONFIG.params.tooltip.selectedFields.length ); }, [props.tooltip]); + + const paramsScaleChanged: boolean = React.useMemo(() => { + const changed = props.selectedParams.some( + (param) => + param.scale !== CONTROLS_DEFAULT_CONFIG.params.defaultParamsScaleType, + ); + return changed; + }, [props.selectedParams]); + return (
@@ -61,6 +72,37 @@ function Controls( />
+
+ + ( + +
+ +
+
+ )} + component={ + + } + /> +
+
void; readOnly?: boolean; margin?: { top: number; right: number; bottom: number; left: number }; + scaleStates?: { + [key: string]: ScaleEnum; + }; } diff --git a/src/src/types/components/ParamsScalePopover/ParamsScalePopover.d.ts b/src/src/types/components/ParamsScalePopover/ParamsScalePopover.d.ts new file mode 100644 index 00000000..ed472e70 --- /dev/null +++ b/src/src/types/components/ParamsScalePopover/ParamsScalePopover.d.ts @@ -0,0 +1,7 @@ +import { ISelectOption } from 'types/services/models/explorer/createAppModel'; +import { ScaleEnum } from 'utils/d3'; + +export interface IParamsScalePopoverProps { + onParamsScaleTypeChange: (params: ISelectOption[]) => void; + selectedParams: ISelectOption[]; +} diff --git a/src/src/types/pages/params/Params.d.ts b/src/src/types/pages/params/Params.d.ts index 718c3ba0..421c7020 100644 --- a/src/src/types/pages/params/Params.d.ts +++ b/src/src/types/pages/params/Params.d.ts @@ -81,6 +81,7 @@ export interface IParamsProps extends Partial { }; columnsOrder: IColumnsOrder; sameValueColumns: string[] | []; + selectedParams: ISelectOption[]; onNotificationDelete: (id: number) => void; onCurveInterpolationChange: () => void; onActivePointChange: ( @@ -124,4 +125,5 @@ export interface IParamsProps extends Partial { archiveRuns: (ids: string[], archived: boolean) => void; deleteRuns: (ids: string[]) => void; onRowsVisibilityChange: (metricKeys: string[]) => void; + onParamsScaleTypeChange: (params: ISelectOption[]) => void; } diff --git a/src/src/types/pages/params/components/Controls/Controls.d.ts b/src/src/types/pages/params/components/Controls/Controls.d.ts index 1a189246..cdfc5589 100644 --- a/src/src/types/pages/params/components/Controls/Controls.d.ts +++ b/src/src/types/pages/params/components/Controls/Controls.d.ts @@ -4,6 +4,7 @@ import { } from 'types/services/models/metrics/metricsAppModel'; import { CurveEnum } from 'utils/d3'; +import { ISelectOption } from 'types/services/models/explorer/createAppModel'; export interface IControlProps { curveInterpolation: CurveEnum; @@ -13,4 +14,6 @@ export interface IControlProps { onColorIndicatorChange: () => void; onCurveInterpolationChange: () => void; onChangeTooltip: (tooltip: Partial) => void; + onParamsScaleTypeChange: (args: ISelectOption[]) => void; + selectedParams: ISelectOption[]; } diff --git a/src/src/types/services/models/explorer/createAppModel.d.ts b/src/src/types/services/models/explorer/createAppModel.d.ts index 0ad3b1d9..d6be7e15 100644 --- a/src/src/types/services/models/explorer/createAppModel.d.ts +++ b/src/src/types/services/models/explorer/createAppModel.d.ts @@ -26,7 +26,7 @@ import { ITrendlineOptions, } from 'types/services/models/scatter/scatterAppModel'; -import { ChartTypeEnum, CurveEnum, HighlightEnum } from 'utils/d3'; +import { ChartTypeEnum, CurveEnum, HighlightEnum, ScaleEnum } from 'utils/d3'; import { IImagesExploreAppModelState } from '../imagesExplore/imagesExploreAppModel'; @@ -102,6 +102,7 @@ export interface ISelectOption { option_name: string; context: { [key: string]: unknown } | null | any; }; + scale?: ScaleEnum; } export interface ISelectConfig { @@ -152,6 +153,7 @@ export interface IHighPlotConfig { [key: string]: [number, number] | [string, string]; }; }; + // paramsScaleType: IParamsScaleStates; } export interface ILineChartConfig { diff --git a/src/src/types/utils/d3/drawParallelAxes.d.ts b/src/src/types/utils/d3/drawParallelAxes.d.ts index 3483522b..3d8babd7 100644 --- a/src/src/types/utils/d3/drawParallelAxes.d.ts +++ b/src/src/types/utils/d3/drawParallelAxes.d.ts @@ -26,4 +26,5 @@ export interface IDrawParallelAxesProps { axesRef: React.MutableRefObject<>; dimensions: IDimensionsType; plotBoxRef: React.MutableRefObject<>; + scaleStates: React.MutableRefObject<>; } diff --git a/src/src/utils/app/onParamsScaleTypeChange.ts b/src/src/utils/app/onParamsScaleTypeChange.ts new file mode 100644 index 00000000..78e7caca --- /dev/null +++ b/src/src/utils/app/onParamsScaleTypeChange.ts @@ -0,0 +1,42 @@ +import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap'; + +import * as analytics from 'services/analytics'; + +import { IModel, State } from 'types/services/models/model'; +import { + IAppModelConfig, + ISelectOption, +} from 'types/services/models/explorer/createAppModel'; + +export default function onParamsScaleTypeChange({ + args, + model, + appName, + updateModelData, +}: { + args: ISelectOption[]; + model: IModel; + appName: string; + updateModelData: ( + configData: IAppModelConfig | any, + shouldURLUpdate?: boolean, + ) => void; +}): void { + let configData = model?.getState()?.config; + if (configData?.chart) { + configData = { + ...configData, + select: { + ...configData.select, + options: args, + }, + }; + model.setState({ config: configData }); + updateModelData(configData, true); + } + // todo create analytics keys and change this + analytics.trackEvent( + // @ts-ignore + `${ANALYTICS_EVENT_KEYS[appName].chart.controls.changeParamsScale} to "${args}"`, + ); +} diff --git a/src/src/utils/d3/drawParallelAxes.ts b/src/src/utils/d3/drawParallelAxes.ts index d5c883a1..2804c18e 100644 --- a/src/src/utils/d3/drawParallelAxes.ts +++ b/src/src/utils/d3/drawParallelAxes.ts @@ -20,6 +20,7 @@ function drawParallelAxes({ axesRef, dimensions, plotBoxRef, + scaleStates, }: IDrawParallelAxesProps): void { if (!axesNodeRef?.current && !dimensions && _.isEmpty(dimensions)) { return; @@ -55,10 +56,10 @@ function drawParallelAxes({ dimensions[keyOfDimension]; const first = 0; const last = keysOfDimensions.length - 1; - const tmpYScale = getAxisScale({ domainData, - scaleType, + scaleType: + scaleType == ScaleEnum.Point ? scaleType : scaleStates[keyOfDimension], rangeData: [height - margin.top - margin.bottom, 0], }); yScale[keyOfDimension] = tmpYScale; From 2ce717b8db9b0241c8ac0d10599677740e26b35c Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Fri, 2 Feb 2024 18:40:30 +0000 Subject: [PATCH 07/79] Fetch runs CSV from server on export runs (#40) Fetch csv file of exported runs from server instead of creating it on the client. --- src/src/services/api/runs/runsService.ts | 8 ++ .../models/explorer/runsModelMethods.ts | 125 +++--------------- 2 files changed, 25 insertions(+), 108 deletions(-) diff --git a/src/src/services/api/runs/runsService.ts b/src/src/services/api/runs/runsService.ts index 777001f1..5b037e1b 100644 --- a/src/src/services/api/runs/runsService.ts +++ b/src/src/services/api/runs/runsService.ts @@ -28,6 +28,13 @@ function getRunsData(query?: string, limit?: number, offset?: string) { }); } +function getCsvData(query?: string) { + return API.getStream(endpoints.GET_RUNS, { + q: query || '', + action: 'export', + }); +} + function getRunLogs(id: string, record_range?: string) { return API.getStream(endpoints.GET_RUN_LOGS(id), { record_range: record_range ?? '', @@ -111,6 +118,7 @@ const runsService = { getBatch, getBatchByStep, getRunsData, + getCsvData, getRunInfo, getRunLogs, getRunMetricsBatch, diff --git a/src/src/services/models/explorer/runsModelMethods.ts b/src/src/services/models/explorer/runsModelMethods.ts index 015abdf5..7874e036 100644 --- a/src/src/services/models/explorer/runsModelMethods.ts +++ b/src/src/services/models/explorer/runsModelMethods.ts @@ -46,7 +46,6 @@ import { ITagInfo } from 'types/pages/tags/Tags'; import { aggregateGroupData } from 'utils/aggregateGroupData'; import exceptionHandler from 'utils/app/exceptionHandler'; import { getFilteredGroupingOptions } from 'utils/app/getFilteredGroupingOptions'; -import getFilteredRow from 'utils/app/getFilteredRow'; import { getGroupingPersistIndex } from 'utils/app/getGroupingPersistIndex'; import onColumnsOrderChange from 'utils/app/onColumnsOrderChange'; import onColumnsVisibilityChange from 'utils/app/onColumnsVisibilityChange'; @@ -63,7 +62,6 @@ import { } from 'utils/encoder/streamEncoding'; import { formatValue } from 'utils/formatValue'; import getObjectPaths from 'utils/getObjectPaths'; -import JsonToCSV from 'utils/JsonToCSV'; import { setItem } from 'utils/storage'; import { encode } from 'utils/encoder/encoder'; import onNotificationDelete from 'utils/app/onNotificationDelete'; @@ -183,44 +181,6 @@ function getRunsModelMethods( onRunsTagsChange({ runHash, tags, model, updateModelData }); } - function getAllRunsData(queryString?: string): { - call: (exceptionHandler: (detail: any) => void) => Promise; - abort: () => void; - } { - if (runsRequestRef) { - runsRequestRef.abort(); - } - runsRequestRef = runsService.getRunsData(queryString); - return { - call: async () => { - try { - const stream = await runsRequestRef.call((detail) => { - exceptionHandler({ detail, model }); - }); - let bufferPairs = decodeBufferPairs(stream as ReadableStream); - let decodedPairs = decodePathsVals(bufferPairs); - let objects = iterFoldTree(decodedPairs, 1); - const runsData: IRun[] = []; - - for await (let [keys, val] of objects) { - const data = { ...(val as any), hash: keys[0] }; - if (!data.hash.startsWith('progress')) { - const runData: any = val; - runsData.push({ ...runData, hash: keys[0] } as any); - } - } - return runsData; - } catch (ex: Error | any) { - if (ex.name === 'AbortError') { - // eslint-disable-next-line no-console - console.error(`${ex.name}, ${ex.message}`); - } - } - }, - abort: runsRequestRef.abort, - }; - } - function getRunsData( shouldUrlUpdate?: boolean, shouldResetSelectedRows?: boolean, @@ -847,75 +807,24 @@ function getRunsModelMethods( } } - function onExportTableData(): Promise { - // @TODO need to get data and params from state not from processData - const runsDataToExport = getAllRunsData( - model.getState()?.config?.select?.query, - ); - const exceptionHandler = (detail: any) => { - // eslint-disable-next-line no-console - console.error('An error occurred:', detail); - }; - - return runsDataToExport.call(exceptionHandler).then((rawData) => { - const { data, params, metricsColumns } = processData(rawData); - const tableData = getDataAsTableRows(data, metricsColumns, params, true); - const configData = model.getState()?.config; - const tableColumns: ITableColumn[] = getRunsTableColumns( - metricsColumns, - params, - configData?.table.columnsOrder!, - configData?.table.hiddenColumns!, - ); - const excludedFields: string[] = ['#', 'actions']; - const filteredHeader: string[] = tableColumns.reduce( - (acc: string[], column: ITableColumn) => - acc.concat( - excludedFields.indexOf(column.key) === -1 && !column.isHidden - ? column.key - : [], - ), - [], - ); - - let emptyRow: { [key: string]: string } = {}; - filteredHeader.forEach((column: string) => { - emptyRow[column] = '--'; - }); - - const groupedRows: IMetricTableRowData[][] = - data.length > 1 - ? Object.keys(tableData.rows).map( - (groupedRowKey: string) => tableData.rows[groupedRowKey].items, - ) - : [ - Array.isArray(tableData.rows) - ? tableData.rows - : tableData.rows[Object.keys(tableData.rows)[0]].items, - ]; - - const dataToExport: { [key: string]: string }[] = []; - - groupedRows?.forEach( - (groupedRow: IMetricTableRowData[], groupedRowIndex: number) => { - groupedRow?.forEach((row: IMetricTableRowData) => { - const filteredRow = getFilteredRow({ - columnKeys: filteredHeader, - row, - }); - dataToExport.push(filteredRow); - }); - if (groupedRows?.length - 1 !== groupedRowIndex) { - dataToExport.push(emptyRow); - } - }, - ); - const blob = new Blob([JsonToCSV(dataToExport)], { - type: 'text/csv;charset=utf-8;', - }); - saveAs(blob, `runs-${moment().format(DATE_EXPORTING_FORMAT)}.csv`); - analytics.trackEvent(ANALYTICS_EVENT_KEYS[appName].table.exports.csv); + async function onExportTableData(): Promise { + const query = model.getState()?.config?.select?.query; + const request = runsService.getCsvData(query); + const readableStream = await request.call((detail) => { + exceptionHandler({ detail, model }); + }); + const reader = readableStream.getReader(); + const data = []; + let readResult = await reader.read(); + while (!readResult.done) { + data.push(readResult.value); + readResult = await reader.read(); + } + const blob = new Blob(data, { + type: 'text/csv;charset=utf-8;', }); + saveAs(blob, `runs-${moment().format(DATE_EXPORTING_FORMAT)}.csv`); + analytics.trackEvent(ANALYTICS_EVENT_KEYS[appName].table.exports.csv); } function onModelNotificationDelete(id: number): void { From fced1a0b3e70f9d4f8679380d772d469d5393b40 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Sat, 3 Feb 2024 04:02:46 +0000 Subject: [PATCH 08/79] Disable Scaling for Point Params (#39) * Add ParamsScalePopover interfaces * Add preliminary ParamsScalePopover component * Add onParamsScaleTypeChange logic * Add ParamsScalePopover to Params Controls * Improve ParamsScalePopover logic * Load selected params into component * Store params scale type state * Unify props into single variable * Fix onParamsScaleTypeChange logic to handle selectedParams state * Add scale support for high plot charts * Add analytics * Remove todos * Fix popover styling bug * Disable controls for Point params * Fix chart scale state not changing * Add tooltip overriding for ToggleButton * Fix ParamsScalePopover title/button styling issue * Add short comment for scale state functions --- .../ParamsScalePopover.scss | 2 +- .../ParamsScalePopover/ParamsScalePopover.tsx | 8 ++- .../kit/ToggleButton/ToggleButton.d.ts | 2 + .../kit/ToggleButton/ToggleButton.tsx | 6 ++- src/src/pages/Params/Params.tsx | 53 ++++++++++++++++--- .../models/explorer/paramsModelMethods.ts | 7 ++- src/src/utils/d3/drawParallelAxes.ts | 2 +- 7 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/src/components/ParamsScalePopover/ParamsScalePopover.scss b/src/src/components/ParamsScalePopover/ParamsScalePopover.scss index fe7cfeaa..a2d54cdf 100644 --- a/src/src/components/ParamsScalePopover/ParamsScalePopover.scss +++ b/src/src/components/ParamsScalePopover/ParamsScalePopover.scss @@ -1,7 +1,7 @@ @use 'src/styles/abstracts' as *; .ParamsScalePopover { - width: 21.5rem; + width: 25rem; padding: 1rem; &__subtitle { diff --git a/src/src/components/ParamsScalePopover/ParamsScalePopover.tsx b/src/src/components/ParamsScalePopover/ParamsScalePopover.tsx index 9874d19d..65554b9a 100644 --- a/src/src/components/ParamsScalePopover/ParamsScalePopover.tsx +++ b/src/src/components/ParamsScalePopover/ParamsScalePopover.tsx @@ -38,7 +38,11 @@ function ParamsScalePopover( {props.selectedParams.map((param: ISelectOption) => (
))} diff --git a/src/src/components/kit/ToggleButton/ToggleButton.d.ts b/src/src/components/kit/ToggleButton/ToggleButton.d.ts index c4916611..33931e9d 100644 --- a/src/src/components/kit/ToggleButton/ToggleButton.d.ts +++ b/src/src/components/kit/ToggleButton/ToggleButton.d.ts @@ -9,4 +9,6 @@ export default interface IToggleButtonProps extends ButtonProps { rightValue: number | string; value: string | number; title: string; + disabled?: boolean; + tooltipOverride?: string; } diff --git a/src/src/components/kit/ToggleButton/ToggleButton.tsx b/src/src/components/kit/ToggleButton/ToggleButton.tsx index fb0c25b4..7a2c1f23 100644 --- a/src/src/components/kit/ToggleButton/ToggleButton.tsx +++ b/src/src/components/kit/ToggleButton/ToggleButton.tsx @@ -20,6 +20,8 @@ function ToggleButton({ value, id, className, + disabled, + tooltipOverride, }: IToggleButtonProps): React.FunctionComponentElement { function handleToggle(e: any): void { const { id, value } = e.currentTarget; @@ -29,7 +31,7 @@ function ToggleButton({ return (
- +
{title}
@@ -43,6 +45,7 @@ function ToggleButton({ size='small' color={value === leftValue ? 'primary' : 'inherit'} onClick={handleToggle} + disabled={disabled} > {leftLabel} @@ -53,6 +56,7 @@ function ToggleButton({ size='small' color={value === rightValue ? 'primary' : 'inherit'} onClick={handleToggle} + disabled={disabled} > {rightLabel} diff --git a/src/src/pages/Params/Params.tsx b/src/src/pages/Params/Params.tsx index 122b9ee8..77929e2c 100644 --- a/src/src/pages/Params/Params.tsx +++ b/src/src/pages/Params/Params.tsx @@ -32,8 +32,9 @@ import AppBar from 'pages/Metrics/components/MetricsBar/MetricsBar'; import { AppNameEnum } from 'services/models/explorer'; import { IParamsProps } from 'types/pages/params/Params'; +import { ISelectOption } from 'types/services/models/explorer/createAppModel'; -import { ChartTypeEnum } from 'utils/d3'; +import { ChartTypeEnum, ScaleEnum } from 'utils/d3'; import SelectForm from './components/SelectForm/SelectForm'; import Controls from './components/Controls/Controls'; @@ -116,21 +117,58 @@ const Params = ({ onRowsVisibilityChange, onParamsScaleTypeChange, }: IParamsProps): React.FunctionComponentElement => { - let scaleStates = selectedParams.reduce((acc, param) => { - (acc as any)[param.key] = param.scale; - return acc; - }, {}); + let scaleStates = getDefaultScaleStates(highPlotData); + let newScaleStates = scaleStates; + + // Updates selectedParams prop to render disabled button for point params + // Also updates newScaleStates to render the newly selected scales in chart + function updateParamsState() { + selectedParams.map((param) => { + if (scaleStates[param.key] === ScaleEnum.Point) { + param.scale = scaleStates[param.key]; + return param; + } else { + return param; + } + }); + newScaleStates = selectedParams.reduce((acc: {}, param: ISelectOption) => { + (acc as any)[param.key] = param.scale; + return acc; + }, {}); + } + + // Obtains the original scale for each parameter + // (point for string params, linear for numeric params) + function getDefaultScaleStates(highPlotData: any) { + const dimensions: { [key: string]: { scaleType: string } } = + highPlotData?.[0]?.dimensions; + + if (!dimensions) { + return {}; + } + const dimensionsArray = Object.entries(dimensions).map(([key, value]) => ({ + key, + ...value, + })); + const result = dimensionsArray.map((dimension) => { + return { + [dimension.key]: dimension.scaleType, + }; + }); + return _.assign({}, ...result); + } const [isProgressBarVisible, setIsProgressBarVisible] = React.useState(false); const chartProps: any[] = React.useMemo(() => { + updateParamsState(); return (highPlotData || []).map((chartData: any) => ({ curveInterpolation, isVisibleColorIndicator, onAxisBrushExtentChange, brushExtents, chartTitle: chartTitleData[chartData.data[0]?.chartIndex], - scaleStates, + scaleStates: newScaleStates, })); }, [ highPlotData, @@ -139,7 +177,8 @@ const Params = ({ chartTitleData, onAxisBrushExtentChange, brushExtents, - scaleStates, + selectedParams, + updateParamsState, ]); return ( diff --git a/src/src/services/models/explorer/paramsModelMethods.ts b/src/src/services/models/explorer/paramsModelMethods.ts index 3a430fcb..2c2204ca 100644 --- a/src/src/services/models/explorer/paramsModelMethods.ts +++ b/src/src/services/models/explorer/paramsModelMethods.ts @@ -542,9 +542,14 @@ function getParamsModelMethods( ({ type, label, value }: ISelectOption) => { const dimension = dimensionsObject[chartIndex]; if (!dimension[label] && type === 'params') { + const paramValue = getValue(run.run.params, label, '-'); + const scaleType = + typeof paramValue === 'number' + ? ScaleEnum.Linear + : ScaleEnum.Point; dimension[label] = { values: new Set(), - scaleType: ScaleEnum.Linear, + scaleType: scaleType, displayName: label, dimensionType: 'param', }; diff --git a/src/src/utils/d3/drawParallelAxes.ts b/src/src/utils/d3/drawParallelAxes.ts index 2804c18e..2bae8bb0 100644 --- a/src/src/utils/d3/drawParallelAxes.ts +++ b/src/src/utils/d3/drawParallelAxes.ts @@ -59,7 +59,7 @@ function drawParallelAxes({ const tmpYScale = getAxisScale({ domainData, scaleType: - scaleType == ScaleEnum.Point ? scaleType : scaleStates[keyOfDimension], + scaleType === ScaleEnum.Point ? scaleType : scaleStates[keyOfDimension], rangeData: [height - margin.top - margin.bottom, 0], }); yScale[keyOfDimension] = tmpYScale; From ef4110c3a36e4fd8b41e1e6c83f60bb64ce0b56e Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Fri, 16 Feb 2024 09:27:25 +0100 Subject: [PATCH 09/79] Remove white shadow in metrics charts (#42) Remove the white shadow around the active runs metrics as it significantly slowed down performance. --- src/src/components/LineChart/LineChart.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/src/components/LineChart/LineChart.scss b/src/src/components/LineChart/LineChart.scss index 0f8a6054..eda1936f 100644 --- a/src/src/components/LineChart/LineChart.scss +++ b/src/src/components/LineChart/LineChart.scss @@ -143,7 +143,6 @@ opacity: 1; paint-order: stroke; cursor: pointer; - filter: drop-shadow(0 0 0.5px #ffffff) drop-shadow(0 0 0.5px #ffffff) drop-shadow(0 0 0.5px #ffffff); } @keyframes inProgressIndicator { From efedea55796f3a9320975753d89a27e32f575371 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:41:19 +0100 Subject: [PATCH 10/79] Metrics/Params selection dropdown (#43) Add a select all checkbox and regex search support for metrics and params selection dropdown. --- .../SelectFormPopper/SelectFormPopper.d.ts | 27 ++++ .../SelectFormPopper/SelectFormPopper.tsx | 152 ++++++++++++++++++ .../components/SelectForm/SelectForm.scss | 19 +++ .../components/SelectForm/SelectForm.tsx | 130 ++++++--------- .../components/SelectForm/SelectForm.scss | 19 +++ .../components/SelectForm/SelectForm.tsx | 118 ++++++-------- 6 files changed, 312 insertions(+), 153 deletions(-) create mode 100644 src/src/components/SelectFormPopper/SelectFormPopper.d.ts create mode 100644 src/src/components/SelectFormPopper/SelectFormPopper.tsx diff --git a/src/src/components/SelectFormPopper/SelectFormPopper.d.ts b/src/src/components/SelectFormPopper/SelectFormPopper.d.ts new file mode 100644 index 00000000..9d7ff18e --- /dev/null +++ b/src/src/components/SelectFormPopper/SelectFormPopper.d.ts @@ -0,0 +1,27 @@ +// @ts-nocheck +/* eslint-disable react/prop-types */ +import React from 'react'; + +import { + ISelectConfig, + ISelectOption, +} from 'types/services/models/explorer/createAppModel'; + +export interface ISelectFormPopperProps { + id: string | undefined; + type: 'metrics' | 'params'; + open: boolean; + anchorEl: HTMLElement | null; + options: ISelectOption[]; + selectedData?: ISelectConfig; + onSelect: (event: React.ChangeEvent<{}>, value: ISelectOption[]) => void; + searchValue: string; + handleSearchInputChange: (event: React.ChangeEvent) => void; + handleClose(event: any, reason: any): void; + regexError: string | null; + setRegexError: (value: string | null) => void; + isRegexSearch: boolean; + setIsRegexSearch: (value: boolean) => void; + className: string; + classes: { [key: string]: string }; +} \ No newline at end of file diff --git a/src/src/components/SelectFormPopper/SelectFormPopper.tsx b/src/src/components/SelectFormPopper/SelectFormPopper.tsx new file mode 100644 index 00000000..7e141eeb --- /dev/null +++ b/src/src/components/SelectFormPopper/SelectFormPopper.tsx @@ -0,0 +1,152 @@ +// @ts-nocheck +/* eslint-disable react/prop-types */ +import React from 'react'; + +import ToggleButton from '@material-ui/lab/ToggleButton'; +import { + Checkbox, + InputBase, + Popper, + Snackbar, + Tooltip, +} from '@material-ui/core'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import MuiAlert from '@material-ui/lab/Alert'; +import CheckBoxOutlineBlank from '@material-ui/icons/CheckBoxOutlineBlank'; +import CheckBoxIcon from '@material-ui/icons/CheckBox'; + +import { Text } from 'components/kit'; + +import { ISelectOption } from 'types/services/models/explorer/createAppModel'; + +const SelectFormPopper: React.FC = ({ + id, + open, + anchorEl, + options, + selectedData, + onSelect, + searchValue, + handleSearchInputChange, + handleClose, + regexError, + setRegexError, + isRegexSearch, + setIsRegexSearch, + className, + classes, +}) => { + return ( + + option.group} + getOptionLabel={(option) => option.label} + renderTags={() => null} + disableClearable={true} + ListboxProps={{ + style: { + height: 400, + }, + }} + renderInput={(params) => ( +
+ + } + checkedIcon={} + checked={selectedData?.options.length === options.length} + onChange={(event) => { + if (event.target.checked) { + onSelect(event, options); + } else { + onSelect(event, []); + } + }} + size='small' + /> + + + setRegexError(null)} + > + setRegexError(null)} + > + {regexError} + + + + { + setIsRegexSearch(!isRegexSearch); + }} + className='RegexToggle' + > + .* + + +
+ )} + renderOption={(option) => { + let selected: boolean = !!selectedData?.options.find( + (item: ISelectOption) => item.key === option.key, + )?.key; + return ( +
+ } + checkedIcon={} + checked={selected} + size='small' + /> + + {option.label} + +
+ ); + }} + /> +
+ ); +}; + +export default SelectFormPopper; diff --git a/src/src/pages/Metrics/components/SelectForm/SelectForm.scss b/src/src/pages/Metrics/components/SelectForm/SelectForm.scss index 30b5cd16..c2eda0b7 100644 --- a/src/src/pages/Metrics/components/SelectForm/SelectForm.scss +++ b/src/src/pages/Metrics/components/SelectForm/SelectForm.scss @@ -123,3 +123,22 @@ } } } + +.RegexToggle { + border: none; + width: 2rem; + height: 2rem; + margin: 10px; +} + +.Popper__SelectForm__select{ + @extend .Metrics__SelectForm__metric__select; +} + +.Popper__SelectForm__option{ + @extend .Metrics__SelectForm__option !optional; +} + +.Popper__SelectForm__option__label{ + @extend .Metrics__SelectForm__option__label !optional; +} diff --git a/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx b/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx index 2b7253c6..4c3d083d 100644 --- a/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx +++ b/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx @@ -1,23 +1,13 @@ import React from 'react'; import classNames from 'classnames'; -import { - Box, - Checkbox, - Divider, - InputBase, - Popper, - Tooltip, -} from '@material-ui/core'; -import Autocomplete from '@material-ui/lab/Autocomplete'; -import { - CheckBox as CheckBoxIcon, - CheckBoxOutlineBlank, -} from '@material-ui/icons'; +import { Box, Divider, Tooltip } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; import { Button, Icon, Badge, Text } from 'components/kit'; import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; import AutocompleteInput from 'components/AutocompleteInput'; +import SelectFormPopper from 'components/SelectFormPopper/SelectFormPopper'; import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap'; @@ -29,6 +19,12 @@ import { ISelectOption } from 'types/services/models/explorer/createAppModel'; import './SelectForm.scss'; +const useStyles = makeStyles({ + popper: { + width: '100% !important', + }, +}); + function SelectForm({ requestIsPending, isDisabled = false, @@ -42,6 +38,8 @@ function SelectForm({ }: ISelectFormProps): React.FunctionComponentElement { const [anchorEl, setAnchorEl] = React.useState(null); const [searchValue, setSearchValue] = React.useState(''); + const [isRegexSearch, setIsRegexSearch] = React.useState(false); + const [regexError, setRegexError] = React.useState(null); const searchRef: any = React.useRef>(null); const autocompleteRef: any = React.useRef>(null); const advancedAutocompleteRef: any = @@ -51,6 +49,7 @@ function SelectForm({ searchRef.current?.abort(); }; }, []); + const classes = useStyles(); function handleMetricSearch(): void { if (requestIsPending) { @@ -82,7 +81,7 @@ function SelectForm({ event: React.ChangeEvent<{}>, value: ISelectOption[], ): void { - if (event.type === 'click') { + if (event.type === 'click' || event.type === 'change') { const lookup = value.reduce( (acc: { [key: string]: number }, curr: ISelectOption) => { acc[curr.key] = ++acc[curr.key] || 0; @@ -133,12 +132,27 @@ function SelectForm({ } const options = React.useMemo(() => { - return ( - selectFormData?.options?.filter( - (option) => option.label.indexOf(searchValue) !== -1, - ) ?? [] - ); - }, [searchValue, selectFormData?.options]); + if (isRegexSearch) { + try { + const regex = new RegExp(searchValue, 'i'); + setRegexError(null); + return ( + selectFormData?.options?.filter((option) => + regex.test(option.label), + ) ?? [] + ); + } catch (error) { + setRegexError('Invalid Regex'); + return []; + } + } else { + return ( + selectFormData?.options?.filter( + (option) => option.label.indexOf(searchValue) !== -1, + ) ?? [] + ); + } + }, [searchValue, selectFormData?.options, isRegexSearch]); const open: boolean = !!anchorEl; const id = open ? 'select-metric' : undefined; @@ -177,72 +191,24 @@ function SelectForm({ Metrics - - option.group} - getOptionLabel={(option) => option.label} - renderTags={() => null} - disableClearable={true} - ListboxProps={{ - style: { - height: 400, - }, - }} - renderInput={(params) => ( - - )} - renderOption={(option) => { - let selected: boolean = - !!selectedMetricsData?.options.find( - (item: ISelectOption) => item.key === option.key, - )?.key; - return ( -
- } - checkedIcon={} - checked={selected} - size='small' - /> - - {option.label} - -
- ); - }} - /> -
+ classes={classes} + /> { const [anchorEl, setAnchorEl] = React.useState(null); const [searchValue, setSearchValue] = React.useState(''); + const [isRegexSearch, setIsRegexSearch] = React.useState(false); + const [regexError, setRegexError] = React.useState(null); const searchRef = React.useRef(null); const autocompleteRef: any = React.useRef>(null); + const classes = useStyles(); React.useEffect(() => { return () => { searchRef.current?.abort(); @@ -64,7 +70,7 @@ function SelectForm({ event: React.ChangeEvent<{}>, value: ISelectOption[], ): void { - if (event.type === 'click') { + if (event.type === 'click' || event.type === 'change') { const lookup = value.reduce( (acc: { [key: string]: number }, curr: ISelectOption) => { acc[curr.key] = ++acc[curr.key] || 0; @@ -109,11 +115,26 @@ function SelectForm({ } const options = React.useMemo(() => { - return ( - selectFormData?.options?.filter( - (option) => option.label.indexOf(searchValue) !== -1, - ) ?? [] - ); + if (isRegexSearch) { + try { + const regex = new RegExp(searchValue, 'i'); + setRegexError(null); + return ( + selectFormData?.options?.filter((option) => + regex.test(option.label), + ) ?? [] + ); + } catch (error) { + setRegexError('Invalid Regex'); + return []; + } + } else { + return ( + selectFormData?.options?.filter( + (option) => option.label.indexOf(searchValue) !== -1, + ) ?? [] + ); + } }, [searchValue, selectFormData?.options]); const open: boolean = !!anchorEl; @@ -141,69 +162,24 @@ function SelectForm({ Run Params - - option.group} - getOptionLabel={(option) => option.label} - renderTags={() => null} - disableClearable={true} - ListboxProps={{ - style: { - height: 400, - }, - }} - renderInput={(params) => ( - - )} - renderOption={(option) => { - let selected: boolean = - !!selectedParamsData?.options.find( - (item: ISelectOption) => item.key === option.key, - )?.key; - return ( -
- } - checkedIcon={} - checked={selected} - /> - - {option.label} - -
- ); - }} - /> -
+ classes={classes} + /> Date: Mon, 4 Mar 2024 16:58:30 +0100 Subject: [PATCH 11/79] Fix Metric/Params selection dropdown search text disappears with left key (#45) --- src/src/components/SelectFormPopper/SelectFormPopper.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/src/components/SelectFormPopper/SelectFormPopper.tsx b/src/src/components/SelectFormPopper/SelectFormPopper.tsx index 7e141eeb..2fa26c31 100644 --- a/src/src/components/SelectFormPopper/SelectFormPopper.tsx +++ b/src/src/components/SelectFormPopper/SelectFormPopper.tsx @@ -36,6 +36,11 @@ const SelectFormPopper: React.FC = ({ className, classes, }) => { + const handleKeyDown = (event) => { + if (event.key === 'ArrowLeft') { + event.stopPropagation(); + } + }; return ( = ({ ...params.inputProps, value: searchValue, onChange: handleSearchInputChange, + onKeyDown: handleKeyDown, }} spellCheck={false} placeholder='Search' From dd98f683867268be62e7dbed269d01cd2eb746d4 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:50:48 +0000 Subject: [PATCH 12/79] Experiment Selection Header (#41) * Add ExperimentNavigationPopoverCompact * Add conditional rendering for compact header/popover * Add ExperimentHeader to MetricsBar * Fix ExperimentHeader styling * Add metrics filtering * Add filtering handler * Add params filter by experiment * Add scatters filtering by experiment * Fix date alignment * Abridge long experiment names * Refactor onSelectExperiment handler * Extend query string parser for experimentNames * Refactor createAppModel internals * Refactor experiment id to experimentNames * Add ExperimentBar component * Add ExperimentSelectionPopover component * Add chips for multiple experiment names * Add styling for experiment bar chips * Remove compact experiment header * Add default experiment bar text * Remove unused props * Fix shortening issue * Show no results when empty selectedExperiments * Add util for getting selected experiments * Update selectedExperiments toggle function * Refactor component tree to simplify global state --- src/src/components/AppBar/AppBar.scss | 9 +- .../ExperimentBar/ExperimentBar.d.ts | 10 ++ .../ExperimentBar/ExperimentBar.scss | 116 ++++++++++++++ .../ExperimentBar/ExperimentBar.tsx | 123 +++++++++++++++ .../components/ExperimentBar/index.tsx | 5 + .../ExperimentHeader/ExperimentHeader.d.ts | 2 + .../ExperimentHeader/ExperimentHeader.scss | 14 ++ .../ExperimentNavigationPopover.tsx | 11 +- .../ExperimentSelectionPopover.d.ts | 9 ++ .../ExperimentSelectionPopover.scss | 124 +++++++++++++++ .../ExperimentSelectionPopover.tsx | 141 ++++++++++++++++++ .../ExperimentSelectionPopover/index.tsx | 5 + src/src/pages/Metrics/Metrics.tsx | 3 + src/src/pages/Metrics/MetricsContainer.tsx | 3 + .../components/MetricsBar/MetricsBar.tsx | 30 ++++ src/src/pages/Params/Params.tsx | 3 + src/src/pages/Params/ParamsContainer.tsx | 3 + src/src/pages/Scatters/Scatters.tsx | 3 + src/src/pages/Scatters/ScattersContainer.tsx | 3 + src/src/services/models/explorer/config.ts | 2 + .../models/explorer/metricsModelMethods.ts | 6 + .../models/explorer/paramsModelMethods.ts | 10 +- .../models/explorer/scattersModelMethods.ts | 10 +- src/src/types/pages/metrics/Metrics.d.ts | 1 + .../components/MetricsBar/MetricsBar.d.ts | 1 + src/src/types/pages/params/Params.d.ts | 1 + src/src/types/pages/scatters/Scatters.d.ts | 1 + .../models/explorer/createAppModel.d.ts | 1 + src/src/utils/app/getQueryStringFromSelect.ts | 66 +++++--- .../utils/app/getSelectedExperimentNames.ts | 8 + .../app/onSelectExperimentNamesChange.ts | 40 +++++ src/src/utils/storage.ts | 3 +- 32 files changed, 738 insertions(+), 29 deletions(-) create mode 100644 src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.d.ts create mode 100644 src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.scss create mode 100644 src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.tsx create mode 100644 src/src/pages/Experiment/components/ExperimentBar/index.tsx create mode 100644 src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.d.ts create mode 100644 src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.scss create mode 100644 src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx create mode 100644 src/src/pages/Experiment/components/ExperimentSelectionPopover/index.tsx create mode 100644 src/src/utils/app/getSelectedExperimentNames.ts create mode 100644 src/src/utils/app/onSelectExperimentNamesChange.ts diff --git a/src/src/components/AppBar/AppBar.scss b/src/src/components/AppBar/AppBar.scss index 0a60bc20..10bb864f 100644 --- a/src/src/components/AppBar/AppBar.scss +++ b/src/src/components/AppBar/AppBar.scss @@ -4,11 +4,12 @@ display: flex; align-items: center; min-height: 2.5rem; - max-height: 2.5rem; border-bottom: $border-main; border-left: $border-main; border-right: $border-main; padding-left: 1.5rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; background: $white; z-index: 1; &__title { @@ -19,10 +20,16 @@ &__content { display: flex; + flex: 6; align-items: center; + justify-content: right; &--disabled { pointer-events: none; opacity: 0.5; } + + .ExperimentBar__headerContainer { + margin-right: auto; + } } } diff --git a/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.d.ts b/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.d.ts new file mode 100644 index 00000000..ebb28eef --- /dev/null +++ b/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.d.ts @@ -0,0 +1,10 @@ +import { IExperimentData } from 'modules/core/api/experimentsApi/types'; + +export interface IExperimentBarProps { + isExperimentLoading: boolean; + isExperimentsLoading: boolean; + experimentsData: IExperimentData[] | null; + selectedExperimentNames: string[]; + getExperimentsData: () => void; + onSelectExperimentNamesChange: (experimentName: string) => void; +} diff --git a/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.scss b/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.scss new file mode 100644 index 00000000..b22cc1c6 --- /dev/null +++ b/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.scss @@ -0,0 +1,116 @@ +@use 'src/styles/abstracts' as *; + +.ExperimentBar { + &__headerContainer { + height: fit-content; + display: flex; + align-items: center; + &__appBarBox { + display: flex; + justify-content: space-between; + &__navigationContainer { + width: 100%; + } + &__actionContainer { + width: 4.5rem; + display: flex; + justify-content: center; + align-items: center; + border-left: $border-main; + margin-bottom: $space-unit; + i { + color: $pico-70; + } + a { + text-decoration: unset; + } + } + } + &__appBarTitleBox { + display: flex; + flex-direction: column; + font-size: $text-md; + font-weight: $font-600; + color: $text-color; + align-items: flex-start; + height: fit-content; + max-width: 100%; + width: auto; + &__appBarTitleBoxWrapper { + display: flex; + align-items: center; + flex-direction: row; + max-width: 100%; + width: 100%; + } + &__chipContainer { + display: flex; + align-items: baseline; + flex-wrap: wrap; + &__chip { + margin-left: $space-xxs; + font-size: smaller; + font-weight: $font-600; + border-color: $cuddle-50; + background-color: $cuddle-10; + color: $pico-80; + } + } + &__title { + margin-bottom: $space-xxs; + } + &__text { + margin-left: $space-xxs; + font-size: $text-md; + } + &__date { + display: flex; + i { + margin-right: $space-xxs; + color: $pico-70; + } + } + &__Skeleton { + margin-right: $space-xs; + } + &__buttonSelectToggler { + width: 1.5rem; + height: 1.5rem; + &.opened { + background: $primary-color-10; + } + .MuiButton-label { + padding-top: $space-xxxxs; + } + } + &__container { + display: flex; + align-items: flex-start; + flex-direction: column; + margin-right: $space-xxxs; + cursor: pointer; + max-width: calc(100% - 6.5rem); + + &__pageName { + margin-right: 0.4375em; + text-transform: capitalize; + text-decoration: none; + } + &__runHash { + margin-left: 0.4375em; + text-transform: lowercase; + } + } + span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + } + } + .StatusLabel { + margin-left: $space-xs; + } + } +} \ No newline at end of file diff --git a/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.tsx b/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.tsx new file mode 100644 index 00000000..663e20ad --- /dev/null +++ b/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import classNames from 'classnames'; + +import { Skeleton } from '@material-ui/lab'; +import { Chip, Tooltip } from '@material-ui/core'; + +import { Button, Icon, Text } from 'components/kit'; +import ControlPopover from 'components/ControlPopover/ControlPopover'; +import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; + +import ExperimentSelectionPopover from '../ExperimentSelectionPopover'; + +import { IExperimentBarProps } from '.'; + +import './ExperimentBar.scss'; + +function ExperimentBar({ + isExperimentLoading, + isExperimentsLoading, + experimentsData, + selectedExperimentNames, + getExperimentsData, + onSelectExperimentNamesChange, +}: IExperimentBarProps): React.FunctionComponentElement { + function shortenExperimentName(name?: string): string { + if (!name) { + return 'default'; + } else if (name.length > 18) { + return `${name.slice(0, 8)}...${name.slice(-8)}`; + } + return name; + } + + return ( + +
+
+
+ ( +
+ {!isExperimentLoading ? ( + <> +
+ + + {selectedExperimentNames.length === 0 ? ( + + Select Experiments + + ) : null} + +
+ {selectedExperimentNames.map((experimentName) => ( + + + onSelectExperimentNamesChange(experimentName) + } + /> + + ))} +
+
+ + ) : ( +
+ +
+ )} +
+ )} + component={ + + } + /> +
+
+
+
+ ); +} +export default ExperimentBar; diff --git a/src/src/pages/Experiment/components/ExperimentBar/index.tsx b/src/src/pages/Experiment/components/ExperimentBar/index.tsx new file mode 100644 index 00000000..7ca82109 --- /dev/null +++ b/src/src/pages/Experiment/components/ExperimentBar/index.tsx @@ -0,0 +1,5 @@ +import ExperimentBar from './ExperimentBar'; + +export * from './ExperimentBar.d'; + +export default ExperimentBar; diff --git a/src/src/pages/Experiment/components/ExperimentHeader/ExperimentHeader.d.ts b/src/src/pages/Experiment/components/ExperimentHeader/ExperimentHeader.d.ts index af88ddb5..2b8cb0ef 100644 --- a/src/src/pages/Experiment/components/ExperimentHeader/ExperimentHeader.d.ts +++ b/src/src/pages/Experiment/components/ExperimentHeader/ExperimentHeader.d.ts @@ -7,4 +7,6 @@ export interface IExperimentHeaderProps { experimentsData: IExperimentData[] | null; experimentId: string; getExperimentsData: () => void; + isCompact?: boolean; + onExperimentChange?: (experimentId: string) => void; } diff --git a/src/src/pages/Experiment/components/ExperimentHeader/ExperimentHeader.scss b/src/src/pages/Experiment/components/ExperimentHeader/ExperimentHeader.scss index d202fc4f..083a4d9e 100644 --- a/src/src/pages/Experiment/components/ExperimentHeader/ExperimentHeader.scss +++ b/src/src/pages/Experiment/components/ExperimentHeader/ExperimentHeader.scss @@ -98,3 +98,17 @@ } } } + +.compact { + .ExperimentHeader__headerContainer { + &__appBarBox { + &__navigationContainer { + max-width: 100%; + width: 100%; + } + } + } + min-width: 30rem; + background-color: transparent; + padding-top: 0.5rem; +} diff --git a/src/src/pages/Experiment/components/ExperimentNavigationPopover/ExperimentNavigationPopover.tsx b/src/src/pages/Experiment/components/ExperimentNavigationPopover/ExperimentNavigationPopover.tsx index 0000174a..437235c7 100644 --- a/src/src/pages/Experiment/components/ExperimentNavigationPopover/ExperimentNavigationPopover.tsx +++ b/src/src/pages/Experiment/components/ExperimentNavigationPopover/ExperimentNavigationPopover.tsx @@ -20,6 +20,15 @@ function ExperimentNavigationPopover({ }: IExperimentNavigationPopoverProps): React.FunctionComponentElement { const { pathname } = useLocation(); + function shortenExperimentName(name?: string): string { + if (!name) { + return 'default'; + } else if (name.length > 57) { + return `${name.slice(0, 57)}...`; + } + return name; + } + React.useEffect(() => { if (!experimentsData) { getExperimentsData(); @@ -55,7 +64,7 @@ function ExperimentNavigationPopover({ weight={500} className='experimentBox__experimentName' > - {experiment?.name ?? 'default'} + {shortenExperimentName(experiment?.name)}
void; + onSelectExperimentNamesChange: (experimentName: string) => void; +} diff --git a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.scss b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.scss new file mode 100644 index 00000000..147e4b9a --- /dev/null +++ b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.scss @@ -0,0 +1,124 @@ +@use 'src/styles/abstracts' as *; + +.ExperimentSelectionPopover { + height: 20rem; + width: 28rem; + &__contentContainer { + height: calc(100% - 2.25rem); + display: flex; + &__experimentsListContainer { + background: $white; + width: 100%; + height: 100%; + position: relative; + padding: $space-xxs $space-xs 0 $space-xs; + &::before { + content: ''; + position: absolute; + background: linear-gradient( + 180deg, + rgba(248, 250, 253, 0) 0%, + #f8fafd 75.52% + ); + height: $space-xs; + width: 100%; + bottom: 0; + left: 0; + z-index: 1; + } + &__experimentList { + overflow-y: auto; + height: 100%; + .experimentBox { + position: relative; + width: 100%; + text-decoration: none; + height: auto; + padding: $space-xs; + display: flex; + align-items: flex-start; + cursor: pointer; + text-decoration: unset; + margin-bottom: $space-xxxxs; + position: relative; + flex-direction: column; + border-radius: 0.25rem; + text-align: left; + + .MuiButton-label { + align-items: flex-start; + flex-direction: column; + line-height: normal; + text-transform: none; + } + + &:hover { + background: $cuddle-20; + } + &__experimentName { + width: 100%; + word-break: break-all; + margin-bottom: 0.375rem; + @include monospaceFontFamily(); + } + &__date { + display: flex; + align-items: center; + @include monospaceFontFamily(14); + i { + margin-right: $space-xxs; + } + } + &.selected { + background: $primary-color-10; + &:hover { + border-radius: 0.3125rem 0 0 0.3125rem; + } + } + &:last-child { + margin-bottom: 0; + } + } + } + } + } + &__headerContainer { + background: $primary-color-5; + width: 100%; + height: 2.25rem; + display: flex; + border-bottom: $border-main; + i { + transform: rotate(90deg); + align-items: center; + display: flex; + } + &__titleContainer { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + &:first-child { + border-right: $border-main; + } + } + } + + &__loaderContainer { + height: 100%; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + } + .MuiPaper-root { + width: 37.5rem; + background: $white; + border: $border-main; + box-sizing: border-box; + box-shadow: 0 0.25rem 0.375rem rgba(144, 175, 218, 0.2); + border-radius: 0.3125rem 0.3125rem 0 0; + height: 20rem; + } +} diff --git a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx new file mode 100644 index 00000000..aff4a1cd --- /dev/null +++ b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import classNames from 'classnames'; +import moment from 'moment'; + +import { Button } from '@material-ui/core'; + +import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; +import { Icon, Spinner, Text } from 'components/kit'; + +import { DATE_WITH_SECONDS } from 'config/dates/dates'; + +import { IExperimentSelectionPopoverProps } from '.'; + +import './ExperimentSelectionPopover.scss'; + +function ExperimentSelectionPopover({ + experimentsData, + selectedExperimentNames, + isExperimentsLoading, + getExperimentsData, + onSelectExperimentNamesChange, +}: IExperimentSelectionPopoverProps): React.FunctionComponentElement { + React.useEffect(() => { + if (!experimentsData) { + getExperimentsData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Add forceUpdate to force re-render (to update selected experiments) + const [, updateState] = React.useState<{}>(); + const forceUpdate = React.useCallback(() => updateState({}), []); + + // TODO: Add shortening in the middle rather than at the end + function shortenExperimentName(name?: string): string { + if (!name) { + return 'default'; + } else if (name.length > 57) { + return `${name.slice(0, 57)}...`; + } + return name; + } + + function handleExperimentClick(experimentName: string): void { + onSelectExperimentNamesChange(experimentName); + // TODO: Figure out a better way to re-render + forceUpdate(); + } + + function experimentInList( + experimentName: string, + selectedExperimentNames: string[], + ): boolean { + return selectedExperimentNames.includes(experimentName); + } + + return ( + +
+
+
+ + Experiments + +
+
+
+
+
+ {!isExperimentsLoading ? ( + experimentsData?.map((experiment) => ( + + )) + ) : ( +
+ +
+ )} +
+
+
+
+
+ ); +} +export default ExperimentSelectionPopover; diff --git a/src/src/pages/Experiment/components/ExperimentSelectionPopover/index.tsx b/src/src/pages/Experiment/components/ExperimentSelectionPopover/index.tsx new file mode 100644 index 00000000..e17abf8d --- /dev/null +++ b/src/src/pages/Experiment/components/ExperimentSelectionPopover/index.tsx @@ -0,0 +1,5 @@ +import ExperimentSelectionPopover from './ExperimentSelectionPopover'; + +export * from './ExperimentSelectionPopover.d'; + +export default ExperimentSelectionPopover; diff --git a/src/src/pages/Metrics/Metrics.tsx b/src/src/pages/Metrics/Metrics.tsx index 43b622d0..bdccdbd9 100644 --- a/src/src/pages/Metrics/Metrics.tsx +++ b/src/src/pages/Metrics/Metrics.tsx @@ -91,6 +91,9 @@ function Metrics( liveUpdateConfig={props.liveUpdateConfig} onLiveUpdateConfigChange={props.onLiveUpdateConfigChange} title={pageTitlesEnum.METRICS_EXPLORER} + onSelectExperimentNamesChange={ + props.onSelectExperimentNamesChange + } />
{ onAlignmentTypeChange={metricAppModel.onAlignmentTypeChange} onDensityTypeChange={metricAppModel.onDensityTypeChange} onMetricsSelectChange={metricAppModel.onMetricsSelectChange} + onSelectExperimentNamesChange={ + metricAppModel.onSelectExperimentNamesChange + } onSelectRunQueryChange={metricAppModel.onSelectRunQueryChange} onSelectAdvancedQueryChange={metricAppModel.onSelectAdvancedQueryChange} toggleSelectAdvancedMode={metricAppModel.toggleSelectAdvancedMode} diff --git a/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx b/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx index cb3bd1a5..8280c3ca 100644 --- a/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx +++ b/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx @@ -13,8 +13,13 @@ import ConfirmModal from 'components/ConfirmModal/ConfirmModal'; import { DOCUMENTATIONS } from 'config/references'; +import ExperimentBar from 'pages/Experiment/components/ExperimentBar'; +import useExperimentState from 'pages/Experiment/useExperimentState'; + import { IMetricsBarProps } from 'types/pages/metrics/components/MetricsBar/MetricsBar'; +import { getSelectedExperimentNames } from 'utils/app/getSelectedExperimentNames'; + import './MetricsBar.scss'; function MetricsBar({ @@ -26,11 +31,24 @@ function MetricsBar({ onBookmarkUpdate, onResetConfigData, onLiveUpdateConfigChange, + onSelectExperimentNamesChange, }: IMetricsBarProps): React.FunctionComponentElement { const [popover, setPopover] = React.useState(''); const route = useRouteMatch(); + // Fetch all experiments along with default + const { experimentState, experimentsState, getExperimentsData } = + useExperimentState('0'); + + const { data: experimentData, loading: isExperimentLoading } = + experimentState; + + const { data: experimentsData, loading: isExperimentsLoading } = + experimentsState; + + const selectedExperimentNames = getSelectedExperimentNames(); + function handleBookmarkClick(value: string): void { setPopover(value); } @@ -44,9 +62,21 @@ function MetricsBar({ handleClosePopover(); } + function handleExperimentNamesChange(experimentName: string): void { + onSelectExperimentNamesChange(experimentName); + } + return ( + => { let scaleStates = getDefaultScaleStates(highPlotData); let newScaleStates = scaleStates; @@ -179,6 +180,7 @@ const Params = ({ brushExtents, selectedParams, updateParamsState, + newScaleStates, ]); return ( @@ -194,6 +196,7 @@ const Params = ({ onResetConfigData={onResetConfigData} liveUpdateConfig={liveUpdateConfig} onLiveUpdateConfigChange={onLiveUpdateConfigChange} + onSelectExperimentNamesChange={onSelectExperimentNamesChange} title={pageTitlesEnum.PARAMS_EXPLORER} />
diff --git a/src/src/pages/Params/ParamsContainer.tsx b/src/src/pages/Params/ParamsContainer.tsx index ccb772b9..5d056ff2 100644 --- a/src/src/pages/Params/ParamsContainer.tsx +++ b/src/src/pages/Params/ParamsContainer.tsx @@ -165,6 +165,9 @@ function ParamsContainer(): React.FunctionComponentElement { onActivePointChange={paramsAppModel.onActivePointChange} onGroupingApplyChange={paramsAppModel.onGroupingApplyChange} onGroupingPersistenceChange={paramsAppModel.onGroupingPersistenceChange} + onSelectExperimentNamesChange={ + paramsAppModel.onSelectExperimentNamesChange + } onSelectRunQueryChange={paramsAppModel.onSelectRunQueryChange} onBookmarkCreate={paramsAppModel.onBookmarkCreate} onBookmarkUpdate={paramsAppModel.onBookmarkUpdate} diff --git a/src/src/pages/Scatters/Scatters.tsx b/src/src/pages/Scatters/Scatters.tsx index d3e77e78..c6055b9b 100644 --- a/src/src/pages/Scatters/Scatters.tsx +++ b/src/src/pages/Scatters/Scatters.tsx @@ -62,6 +62,9 @@ function Scatters( onResetConfigData={props.onResetConfigData} liveUpdateConfig={props.liveUpdateConfig} onLiveUpdateConfigChange={props.onLiveUpdateConfigChange} + onSelectExperimentNamesChange={ + props.onSelectExperimentNamesChange + } title='Scatters explorer' />
diff --git a/src/src/pages/Scatters/ScattersContainer.tsx b/src/src/pages/Scatters/ScattersContainer.tsx index 8e62270f..f67e3589 100644 --- a/src/src/pages/Scatters/ScattersContainer.tsx +++ b/src/src/pages/Scatters/ScattersContainer.tsx @@ -177,6 +177,9 @@ function ScattersContainer(): React.FunctionComponentElement { onNotificationAdd={scattersAppModel.onNotificationAdd} onNotificationDelete={scattersAppModel.onNotificationDelete} onResetConfigData={scattersAppModel.onResetConfigData} + onSelectExperimentNamesChange={ + scattersAppModel.onSelectExperimentNamesChange + } onSelectOptionsChange={scattersAppModel.onSelectOptionsChange} onSelectRunQueryChange={scattersAppModel.onSelectRunQueryChange} onExportTableData={scattersAppModel.onExportTableData} diff --git a/src/src/services/models/explorer/config.ts b/src/src/services/models/explorer/config.ts index 2a3620c4..199f1880 100644 --- a/src/src/services/models/explorer/config.ts +++ b/src/src/services/models/explorer/config.ts @@ -160,6 +160,7 @@ function initializeAppModel(appConfig: IAppInitialConfig): InitialAppModelType { query: '', advancedMode: false, advancedQuery: '', + selectedExperimentNames: [], }; } return config; @@ -281,6 +282,7 @@ function initializeAppModel(appConfig: IAppInitialConfig): InitialAppModelType { query: '', advancedMode: false, advancedQuery: '', + selectedExperimentNames: [], }; } //TODO solve the problem with keeping table config after switching from Scatters explore to Params explore. But the solution is temporal diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index d2b8d5bd..e58702df 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -91,6 +91,7 @@ import onGroupingSelectChange from 'utils/app/onGroupingSelectChange'; import onHighlightModeChange from 'utils/app/onHighlightModeChange'; import onIgnoreOutliersChange from 'utils/app/onIgnoreOutliersChange'; import onSelectOptionsChange from 'utils/app/onSelectOptionsChange'; +import onSelectExperimentNamesChange from 'utils/app/onSelectExperimentNamesChange'; import onMetricVisibilityChange from 'utils/app/onMetricsVisibilityChange'; import onRowHeightChange from 'utils/app/onRowHeightChange'; import onRowVisibilityChange from 'utils/app/onRowVisibilityChange'; @@ -1638,6 +1639,11 @@ function getMetricsAppModelMethods( onMetricsSelectChange(data: D & Partial): void { onSelectOptionsChange({ data, model }); }, + onSelectExperimentNamesChange(experimentName: string): void { + // Handle experiment change, then re-fetch metrics data + onSelectExperimentNamesChange({ experimentName, model }); + getMetricsData(true, true).call(); + }, onSelectRunQueryChange(query: string): void { onSelectRunQueryChange({ query, model }); }, diff --git a/src/src/services/models/explorer/paramsModelMethods.ts b/src/src/services/models/explorer/paramsModelMethods.ts index 2c2204ca..249b47fb 100644 --- a/src/src/services/models/explorer/paramsModelMethods.ts +++ b/src/src/services/models/explorer/paramsModelMethods.ts @@ -54,6 +54,7 @@ import { getFilteredGroupingOptions } from 'utils/app/getFilteredGroupingOptions import getFilteredRow from 'utils/app/getFilteredRow'; import { getGroupingPersistIndex } from 'utils/app/getGroupingPersistIndex'; import getGroupingSelectOptions from 'utils/app/getGroupingSelectOptions'; +import getQueryStringFromSelect from 'utils/app/getQueryStringFromSelect'; import getRunData from 'utils/app/getRunData'; import onChangeTooltip from 'utils/app/onChangeTooltip'; import onColorIndicatorChange from 'utils/app/onColorIndicatorChange'; @@ -72,6 +73,7 @@ import onParamsScaleTypeChange from 'utils/app/onParamsScaleTypeChange'; import onRowHeightChange from 'utils/app/onRowHeightChange'; import onRowVisibilityChange from 'utils/app/onRowVisibilityChange'; import onSelectRunQueryChange from 'utils/app/onSelectRunQueryChange'; +import onSelectExperimentNamesChange from 'utils/app/onSelectExperimentNamesChange'; import onSortFieldsChange from 'utils/app/onSortFieldsChange'; import { onTableDiffShow } from 'utils/app/onTableDiffShow'; import { onTableResizeEnd } from 'utils/app/onTableResizeEnd'; @@ -224,7 +226,8 @@ function getParamsModelMethods( runsRequestRef.abort(); } const configData = { ...model.getState()?.config }; - runsRequestRef = runsService.getRunsData(configData?.select?.query); + const query = getQueryStringFromSelect(configData?.select, true); + runsRequestRef = runsService.getRunsData(query); setRequestProgress(model); return { call: async () => { @@ -1562,6 +1565,11 @@ function getParamsModelMethods( onParamsSelectChange(data: D & Partial): void { onSelectOptionsChange({ data, model }); }, + onSelectExperimentNamesChange(experimentName: string): void { + // Handle experiment change, then re-fetch params data + onSelectExperimentNamesChange({ experimentName, model }); + getParamsData(true, true).call(); + }, onSelectRunQueryChange(query: string): void { onSelectRunQueryChange({ query, model }); }, diff --git a/src/src/services/models/explorer/scattersModelMethods.ts b/src/src/services/models/explorer/scattersModelMethods.ts index 575d9e8c..ef7a8b8a 100644 --- a/src/src/services/models/explorer/scattersModelMethods.ts +++ b/src/src/services/models/explorer/scattersModelMethods.ts @@ -61,6 +61,7 @@ import { getFilteredGroupingOptions } from 'utils/app/getFilteredGroupingOptions import getFilteredRow from 'utils/app/getFilteredRow'; import { getGroupingPersistIndex } from 'utils/app/getGroupingPersistIndex'; import getGroupingSelectOptions from 'utils/app/getGroupingSelectOptions'; +import getQueryStringFromSelect from 'utils/app/getQueryStringFromSelect'; import getRunData from 'utils/app/getRunData'; import onChangeTooltip from 'utils/app/onChangeTooltip'; import onColumnsOrderChange from 'utils/app/onColumnsOrderChange'; @@ -83,6 +84,7 @@ import onTableResizeModeChange from 'utils/app/onTableResizeModeChange'; import onTableRowClick from 'utils/app/onTableRowClick'; import onTableRowHover from 'utils/app/onTableRowHover'; import onTableSortChange from 'utils/app/onTableSortChange'; +import onSelectExperimentNamesChange from 'utils/app/onSelectExperimentNamesChange'; import updateColumnsWidths from 'utils/app/updateColumnsWidths'; import updateSortFields from 'utils/app/updateTableSortFields'; import contextToString from 'utils/contextToString'; @@ -999,7 +1001,8 @@ function getScattersModelMethods( } const configData = { ...model.getState()?.config }; - runsRequestRef = runsService.getRunsData(configData?.select?.query); + const query = getQueryStringFromSelect(configData?.select, true); + runsRequestRef = runsService.getRunsData(query); setRequestProgress(model); return { call: async () => { @@ -1493,6 +1496,11 @@ function getScattersModelMethods( onSelectOptionsChange(data: D & Partial): void { onSelectOptionsChange({ data, model }); }, + onSelectExperimentNamesChange(experimentName: string): void { + // Handle experiment change, then re-fetch scatters data + onSelectExperimentNamesChange({ experimentName, model }); + getScattersData(true, true).call(); + }, onSelectRunQueryChange(query: string): void { onSelectRunQueryChange({ query, model }); }, diff --git a/src/src/types/pages/metrics/Metrics.d.ts b/src/src/types/pages/metrics/Metrics.d.ts index 4a7d0517..266c253b 100644 --- a/src/src/types/pages/metrics/Metrics.d.ts +++ b/src/src/types/pages/metrics/Metrics.d.ts @@ -130,6 +130,7 @@ export interface IMetricProps extends Partial { onAlignmentTypeChange: (type: XAlignmentEnum) => void; onDensityTypeChange: (type: DensityOptions) => void; onMetricsSelectChange: (options: ISelectOption[]) => void; + onSelectExperimentNamesChange: (experimentName: string) => void; onSelectRunQueryChange: (query: string) => void; onSelectAdvancedQueryChange: (query: string) => void; onRowsVisibilityChange: (metricKeys: string[]) => void; diff --git a/src/src/types/pages/metrics/components/MetricsBar/MetricsBar.d.ts b/src/src/types/pages/metrics/components/MetricsBar/MetricsBar.d.ts index 6ac87851..f2458108 100644 --- a/src/src/types/pages/metrics/components/MetricsBar/MetricsBar.d.ts +++ b/src/src/types/pages/metrics/components/MetricsBar/MetricsBar.d.ts @@ -12,4 +12,5 @@ export interface IMetricsBarProps { enabled?: boolean; }) => void; title: string; + onSelectExperimentNamesChange: (experimentName: string) => void; } diff --git a/src/src/types/pages/params/Params.d.ts b/src/src/types/pages/params/Params.d.ts index 421c7020..b0a5f7fd 100644 --- a/src/src/types/pages/params/Params.d.ts +++ b/src/src/types/pages/params/Params.d.ts @@ -90,6 +90,7 @@ export interface IParamsProps extends Partial { ) => void; onColorIndicatorChange: () => void; onParamsSelectChange: (options: ISelectOption[]) => void; + onSelectExperimentNamesChange: (experimentName: string) => void; onSelectRunQueryChange: (query: string) => void; onGroupingSelectChange: (params: IOnGroupingSelectChangeParams) => void; onGroupingModeChange: (params: IOnGroupingModeChangeParams) => void; diff --git a/src/src/types/pages/scatters/Scatters.d.ts b/src/src/types/pages/scatters/Scatters.d.ts index 92857a05..2f372668 100644 --- a/src/src/types/pages/scatters/Scatters.d.ts +++ b/src/src/types/pages/scatters/Scatters.d.ts @@ -93,6 +93,7 @@ export interface IScattersProps extends Partial { onNotificationAdd: (notification: INotification) => void; onNotificationDelete: (id: number) => void; onResetConfigData: () => void; + onSelectExperimentNamesChange: (experimentName: string) => void; onSelectOptionsChange: (options: ISelectOption[]) => void; onSelectRunQueryChange: (query: string) => void; onExportTableData: (e: React.ChangeEvent) => void; diff --git a/src/src/types/services/models/explorer/createAppModel.d.ts b/src/src/types/services/models/explorer/createAppModel.d.ts index d6be7e15..416bde93 100644 --- a/src/src/types/services/models/explorer/createAppModel.d.ts +++ b/src/src/types/services/models/explorer/createAppModel.d.ts @@ -110,6 +110,7 @@ export interface ISelectConfig { query: string; advancedMode?: boolean; advancedQuery?: string; + selectedExperimentNames?: string[]; } export interface ITableConfig { diff --git a/src/src/utils/app/getQueryStringFromSelect.ts b/src/src/utils/app/getQueryStringFromSelect.ts index 6c429bb2..9fea464d 100644 --- a/src/src/utils/app/getQueryStringFromSelect.ts +++ b/src/src/utils/app/getQueryStringFromSelect.ts @@ -5,8 +5,11 @@ import { jsValidVariableRegex } from 'utils/getObjectPaths'; import { formatValue } from '../formatValue'; +import { getSelectedExperimentNames } from './getSelectedExperimentNames'; + export default function getQueryStringFromSelect( selectData: ISelectConfig, + excludeMetrics?: boolean, error?: ISyntaxErrorDetails, ) { let query = '()'; @@ -20,35 +23,50 @@ export default function getQueryStringFromSelect( selectData.query?.trim() && !error?.message ? `(${selectData.query.trim()})` : ''; - const selections = selectData.options?.length - ? `(${selectData.options - .map((option) => { - const metricName = option.value?.option_name.replaceAll('"', '\\"'); - return `(metric.name == "${metricName}"${ - option.value?.context === null - ? '' - : ' and ' + - Object.keys(option.value?.context) - .map((item) => { - const contextName = !jsValidVariableRegex.test(item) - ? `['${item.replaceAll('"', '\\"')}']` - : `.${item}`; - const value = (option.value?.context as any)[item]; - return `metric.context${contextName} == ${formatValue( - value, - )}`; - }) - .join(' and ') - })`; - }) - .join(' or ')})` - : ''; + + let selections = ''; + if (!excludeMetrics) { + selections = selectData.options?.length + ? `(${selectData.options + .map((option) => { + const metricName = option.value?.option_name.replaceAll( + '"', + '\\"', + ); + return `(metric.name == "${metricName}"${ + option.value?.context === null + ? '' + : ' and ' + + Object.keys(option.value?.context) + .map((item) => { + const contextName = !jsValidVariableRegex.test(item) + ? `['${item.replaceAll('"', '\\"')}']` + : `.${item}`; + const value = (option.value?.context as any)[item]; + return `metric.context${contextName} == ${formatValue( + value, + )}`; + }) + .join(' and ') + })`; + }) + .join(' or ')})` + : ''; + } + + const selectedExperiments = getSelectedExperimentNames(); + + const experimentNames = `run.experiment in ["${selectedExperiments.join( + '", "', + )}"]`; if (simpleInput && selections) { query = `${simpleInput} and ${selections}`; } else { query = `${simpleInput}${selections}`; } + + query = query ? `${query} and ${experimentNames}` : experimentNames; } - return query.trim() || '()'; + return excludeMetrics ? query.trim() || '' : query.trim() || '()'; } diff --git a/src/src/utils/app/getSelectedExperimentNames.ts b/src/src/utils/app/getSelectedExperimentNames.ts new file mode 100644 index 00000000..6d7acd7c --- /dev/null +++ b/src/src/utils/app/getSelectedExperimentNames.ts @@ -0,0 +1,8 @@ +import { getItem } from 'utils/storage'; + +export function getSelectedExperimentNames(): string[] { + const rawNames = getItem('selectedExperimentNames') ?? ''; + const selectedExperimentNames = + rawNames.length > 0 ? rawNames.split(',') : []; + return selectedExperimentNames.length > 0 ? selectedExperimentNames : []; +} diff --git a/src/src/utils/app/onSelectExperimentNamesChange.ts b/src/src/utils/app/onSelectExperimentNamesChange.ts new file mode 100644 index 00000000..98ab27f9 --- /dev/null +++ b/src/src/utils/app/onSelectExperimentNamesChange.ts @@ -0,0 +1,40 @@ +import { IModel, State } from 'types/services/models/model'; + +import { setItem } from 'utils/storage'; + +import { getSelectedExperimentNames } from './getSelectedExperimentNames'; + +export default function onSelectExperimentNamesChange({ + experimentName, + model, +}: { + experimentName: string; + model: IModel; +}) { + const configData = model.getState()?.config; + const selectedExperimentNames = getSelectedExperimentNames(); + + // If the experiment is already selected, remove it from the list + // Otherwise, add it + const index = selectedExperimentNames.indexOf(experimentName); + + if (index > -1) { + selectedExperimentNames.splice(index, 1); + } else { + selectedExperimentNames.push(experimentName); + } + + if (selectedExperimentNames.length === 0) { + setItem('selectedExperimentNames', ''); + } else { + setItem('selectedExperimentNames', selectedExperimentNames); + } + + if (configData?.select) { + const newConfig = { + ...configData, + select: { ...configData.select, selectedExperimentNames }, + }; + model.setState({ config: newConfig }); + } +} diff --git a/src/src/utils/storage.ts b/src/src/utils/storage.ts index 4e716068..756a4c31 100644 --- a/src/src/utils/storage.ts +++ b/src/src/utils/storage.ts @@ -2,7 +2,8 @@ import { getPrefix } from 'config/config'; export function setItem(key: string, value: any) { try { - localStorage.setItem(getNamespacedKey(key), value); + let namespacedKey = getNamespacedKey(key); + localStorage.setItem(namespacedKey, value); } catch (error) {} } From 10e3820b197764aadea686c3207250fdea38176b Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:20:55 +0000 Subject: [PATCH 13/79] Experiment Selection Header Searchbox + Quick Action Buttons (#48) * Add ExperimentNavigationPopoverCompact * Add conditional rendering for compact header/popover * Add ExperimentHeader to MetricsBar * Fix ExperimentHeader styling * Add metrics filtering * Add filtering handler * Add params filter by experiment * Add scatters filtering by experiment * Fix date alignment * Abridge long experiment names * Refactor onSelectExperiment handler * Extend query string parser for experimentNames * Refactor createAppModel internals * Refactor experiment id to experimentNames * Add ExperimentBar component * Add ExperimentSelectionPopover component * Add chips for multiple experiment names * Add styling for experiment bar chips * Remove compact experiment header * Add default experiment bar text * Remove unused props * Fix shortening issue * Show no results when empty selectedExperiments * Add util for getting selected experiments * Update selectedExperiments toggle function * Refactor component tree to simplify global state * Add onToggleAllExperiments to model * Fix missing dependency warning * Add toggle function to view * Add regex filtering to experiment header * Update onToggleAllExperiments * Add experiment selection checkboxes and new toggle logic * Update experiment selection styling * Improve experiment name slicing UX * Add conditional tooltip to main checkbox * Fix import spacing --- .../ExperimentBar/ExperimentBar.d.ts | 1 + .../ExperimentBar/ExperimentBar.tsx | 2 + .../ExperimentSelectionPopover.d.ts | 1 + .../ExperimentSelectionPopover.scss | 56 ++++- .../ExperimentSelectionPopover.tsx | 200 ++++++++++++++---- src/src/pages/Metrics/Metrics.tsx | 1 + src/src/pages/Metrics/MetricsContainer.tsx | 1 + .../components/MetricsBar/MetricsBar.tsx | 2 + src/src/pages/Params/Params.tsx | 2 + src/src/pages/Params/ParamsContainer.tsx | 1 + .../components/SelectForm/SelectForm.tsx | 2 +- src/src/pages/Scatters/Scatters.tsx | 1 + src/src/pages/Scatters/ScattersContainer.tsx | 1 + .../models/explorer/metricsModelMethods.ts | 5 + .../models/explorer/paramsModelMethods.ts | 5 + .../models/explorer/scattersModelMethods.ts | 5 + src/src/types/pages/metrics/Metrics.d.ts | 1 + .../components/MetricsBar/MetricsBar.d.ts | 1 + src/src/types/pages/params/Params.d.ts | 1 + src/src/types/pages/scatters/Scatters.d.ts | 1 + src/src/utils/app/onToggleAllExperiments.ts | 16 ++ 21 files changed, 263 insertions(+), 43 deletions(-) create mode 100644 src/src/utils/app/onToggleAllExperiments.ts diff --git a/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.d.ts b/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.d.ts index ebb28eef..5dd65a8a 100644 --- a/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.d.ts +++ b/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.d.ts @@ -7,4 +7,5 @@ export interface IExperimentBarProps { selectedExperimentNames: string[]; getExperimentsData: () => void; onSelectExperimentNamesChange: (experimentName: string) => void; + onToggleAllExperiments: (experimentNames: string[]) => void; } diff --git a/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.tsx b/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.tsx index 663e20ad..a1f4ab69 100644 --- a/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.tsx +++ b/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.tsx @@ -21,6 +21,7 @@ function ExperimentBar({ selectedExperimentNames, getExperimentsData, onSelectExperimentNamesChange, + onToggleAllExperiments, }: IExperimentBarProps): React.FunctionComponentElement { function shortenExperimentName(name?: string): string { if (!name) { @@ -111,6 +112,7 @@ function ExperimentBar({ selectedExperimentNames={selectedExperimentNames} getExperimentsData={getExperimentsData} onSelectExperimentNamesChange={onSelectExperimentNamesChange} + onToggleAllExperiments={onToggleAllExperiments} /> } /> diff --git a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.d.ts b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.d.ts index fe8ff931..5f084f1e 100644 --- a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.d.ts +++ b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.d.ts @@ -6,4 +6,5 @@ export interface IExperimentSelectionPopoverProps { isExperimentsLoading: boolean; getExperimentsData: () => void; onSelectExperimentNamesChange: (experimentName: string) => void; + onToggleAllExperiments: (experimentNames: string[]) => void; } diff --git a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.scss b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.scss index 147e4b9a..3078e44d 100644 --- a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.scss +++ b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.scss @@ -34,27 +34,29 @@ width: 100%; text-decoration: none; height: auto; - padding: $space-xs; + padding-left: 0; display: flex; align-items: flex-start; cursor: pointer; text-decoration: unset; margin-bottom: $space-xxxxs; position: relative; - flex-direction: column; border-radius: 0.25rem; text-align: left; - .MuiButton-label { align-items: flex-start; - flex-direction: column; + flex-direction: row; line-height: normal; text-transform: none; + justify-content: flex-start; } - &:hover { background: $cuddle-20; } + &__rightContainer { + display: flex; + flex-direction: column; + } &__experimentName { width: 100%; word-break: break-all; @@ -98,9 +100,26 @@ height: 100%; display: flex; align-items: center; - justify-content: center; + justify-content: space-between; &:first-child { border-right: $border-main; + justify-self: start; + padding-left: $space-sm; + } + } + &__buttons { + justify-self: end; + padding: $space-xxs; + } + &__selectAllButton, &__removeAllButton { + background-color: $primary-color; + color: white; + margin-right: $space-xxxxs; + border-radius: 0.25rem; + padding: 0.25rem; + height: 1.5rem; + &:hover { + background-color: $primary-dark; } } } @@ -121,4 +140,29 @@ border-radius: 0.3125rem 0.3125rem 0 0; height: 20rem; } + &__searchContainer { + border: $border-main; + width: 100%; + border-radius: $radius-main; + color: #586069; + background-color: #ffffff; + margin-bottom: $space-xxxxs; + display: flex; + flex-direction: row; + justify-content: space-between; + &__inputBase { + margin: 0; + font-size: $text-md; + width: 100%; + } + .RegexToggle { + border: none; + width: 2rem; + height: 2rem; + margin: $space-xxxs; + } + } + .error { + border: 1px solid red; + } } diff --git a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx index aff4a1cd..6184e8c5 100644 --- a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx +++ b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx @@ -2,13 +2,18 @@ import React from 'react'; import classNames from 'classnames'; import moment from 'moment'; -import { Button } from '@material-ui/core'; +import ToggleButton from '@material-ui/lab/ToggleButton'; +import { Button, Checkbox, InputBase, Tooltip } from '@material-ui/core'; +import CheckBoxOutlineBlank from '@material-ui/icons/CheckBoxOutlineBlank'; +import CheckBoxIcon from '@material-ui/icons/CheckBox'; import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; import { Icon, Spinner, Text } from 'components/kit'; import { DATE_WITH_SECONDS } from 'config/dates/dates'; +import { IExperimentData } from 'modules/core/api/experimentsApi'; + import { IExperimentSelectionPopoverProps } from '.'; import './ExperimentSelectionPopover.scss'; @@ -19,6 +24,7 @@ function ExperimentSelectionPopover({ isExperimentsLoading, getExperimentsData, onSelectExperimentNamesChange, + onToggleAllExperiments, }: IExperimentSelectionPopoverProps): React.FunctionComponentElement { React.useEffect(() => { if (!experimentsData) { @@ -31,12 +37,23 @@ function ExperimentSelectionPopover({ const [, updateState] = React.useState<{}>(); const forceUpdate = React.useCallback(() => updateState({}), []); - // TODO: Add shortening in the middle rather than at the end + const [searchValue, setSearchValue] = React.useState(''); + const [isRegexSearch, setIsRegexSearch] = React.useState(false); + const [invalidRegex, setInvalidRegex] = React.useState(false); + const [visibleExperiments, setVisibleExperiments] = React.useState< + IExperimentData[] + >([]); + + React.useEffect(() => { + setVisibleExperiments(experimentsData || []); + }, [experimentsData]); + function shortenExperimentName(name?: string): string { if (!name) { return 'default'; - } else if (name.length > 57) { - return `${name.slice(0, 57)}...`; + } else if (name.length > 56) { + // Slice the name in the middle + return `${name.slice(0, 27)}...${name.slice(-26)}`; } return name; } @@ -54,6 +71,55 @@ function ExperimentSelectionPopover({ return selectedExperimentNames.includes(experimentName); } + function handleSearchInputChange( + e: React.ChangeEvent, + ): void { + setSearchValue(e.target.value); + + if (isRegexSearch) { + try { + const regex = new RegExp(e.target.value, 'i'); + setInvalidRegex(false); + const options = experimentsData?.filter((experiment) => + regex.test(experiment.name), + ); + setVisibleExperiments(options || []); + } catch (error) { + setInvalidRegex(true); + } + } else { + const options = experimentsData?.filter((experiment) => + experiment.name.toLowerCase().includes(e.target.value.toLowerCase()), + ); + setVisibleExperiments(options || []); + } + } + + function toggleAllExperiments(checked: boolean): void { + const visibleExperimentNames = visibleExperiments?.map( + (experiment) => experiment.name, + ); + // If all experiments are selected, deselect all + // otherwise, select all that are unselected + if (checked) { + onToggleAllExperiments(visibleExperimentNames); + } else { + const unselectedExperiments = visibleExperimentNames?.filter( + (experimentName) => !selectedExperimentNames.includes(experimentName), + ); + onToggleAllExperiments(unselectedExperiments); + } + } + + function allExperimentsSelected(): boolean { + return ( + visibleExperiments.length > 0 && + visibleExperiments.every((experiment) => + selectedExperimentNames.includes(experiment.name), + ) + ); + } + return (
@@ -67,8 +133,55 @@ function ExperimentSelectionPopover({
+
+ + } + checkedIcon={} + checked={allExperimentsSelected()} + onChange={() => { + const checked = allExperimentsSelected(); + toggleAllExperiments(checked); + }} + size='small' + /> + + + + + { + setIsRegexSearch(!isRegexSearch); + }} + className='RegexToggle' + > + .* + + +
{!isExperimentsLoading ? ( - experimentsData?.map((experiment) => ( + visibleExperiments?.map((experiment) => ( )) diff --git a/src/src/pages/Metrics/Metrics.tsx b/src/src/pages/Metrics/Metrics.tsx index bdccdbd9..516a353e 100644 --- a/src/src/pages/Metrics/Metrics.tsx +++ b/src/src/pages/Metrics/Metrics.tsx @@ -94,6 +94,7 @@ function Metrics( onSelectExperimentNamesChange={ props.onSelectExperimentNamesChange } + onToggleAllExperiments={props.onToggleAllExperiments} />
{ onSelectExperimentNamesChange={ metricAppModel.onSelectExperimentNamesChange } + onToggleAllExperiments={metricAppModel.onToggleAllExperiments} onSelectRunQueryChange={metricAppModel.onSelectRunQueryChange} onSelectAdvancedQueryChange={metricAppModel.onSelectAdvancedQueryChange} toggleSelectAdvancedMode={metricAppModel.toggleSelectAdvancedMode} diff --git a/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx b/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx index 8280c3ca..f450759d 100644 --- a/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx +++ b/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx @@ -32,6 +32,7 @@ function MetricsBar({ onResetConfigData, onLiveUpdateConfigChange, onSelectExperimentNamesChange, + onToggleAllExperiments, }: IMetricsBarProps): React.FunctionComponentElement { const [popover, setPopover] = React.useState(''); @@ -76,6 +77,7 @@ function MetricsBar({ selectedExperimentNames={selectedExperimentNames} getExperimentsData={getExperimentsData} onSelectExperimentNamesChange={handleExperimentNamesChange} + onToggleAllExperiments={onToggleAllExperiments} /> => { let scaleStates = getDefaultScaleStates(highPlotData); let newScaleStates = scaleStates; @@ -197,6 +198,7 @@ const Params = ({ liveUpdateConfig={liveUpdateConfig} onLiveUpdateConfigChange={onLiveUpdateConfigChange} onSelectExperimentNamesChange={onSelectExperimentNamesChange} + onToggleAllExperiments={onToggleAllExperiments} title={pageTitlesEnum.PARAMS_EXPLORER} />
diff --git a/src/src/pages/Params/ParamsContainer.tsx b/src/src/pages/Params/ParamsContainer.tsx index 5d056ff2..4c004f4b 100644 --- a/src/src/pages/Params/ParamsContainer.tsx +++ b/src/src/pages/Params/ParamsContainer.tsx @@ -168,6 +168,7 @@ function ParamsContainer(): React.FunctionComponentElement { onSelectExperimentNamesChange={ paramsAppModel.onSelectExperimentNamesChange } + onToggleAllExperiments={paramsAppModel.onToggleAllExperiments} onSelectRunQueryChange={paramsAppModel.onSelectRunQueryChange} onBookmarkCreate={paramsAppModel.onBookmarkCreate} onBookmarkUpdate={paramsAppModel.onBookmarkUpdate} diff --git a/src/src/pages/Params/components/SelectForm/SelectForm.tsx b/src/src/pages/Params/components/SelectForm/SelectForm.tsx index 85caed40..fea7a3b8 100644 --- a/src/src/pages/Params/components/SelectForm/SelectForm.tsx +++ b/src/src/pages/Params/components/SelectForm/SelectForm.tsx @@ -135,7 +135,7 @@ function SelectForm({ ) ?? [] ); } - }, [searchValue, selectFormData?.options]); + }, [searchValue, selectFormData?.options, isRegexSearch]); const open: boolean = !!anchorEl; const id = open ? 'select-metric' : undefined; diff --git a/src/src/pages/Scatters/Scatters.tsx b/src/src/pages/Scatters/Scatters.tsx index c6055b9b..f764f1c3 100644 --- a/src/src/pages/Scatters/Scatters.tsx +++ b/src/src/pages/Scatters/Scatters.tsx @@ -65,6 +65,7 @@ function Scatters( onSelectExperimentNamesChange={ props.onSelectExperimentNamesChange } + onToggleAllExperiments={props.onToggleAllExperiments} title='Scatters explorer' />
diff --git a/src/src/pages/Scatters/ScattersContainer.tsx b/src/src/pages/Scatters/ScattersContainer.tsx index f67e3589..18e43ef6 100644 --- a/src/src/pages/Scatters/ScattersContainer.tsx +++ b/src/src/pages/Scatters/ScattersContainer.tsx @@ -180,6 +180,7 @@ function ScattersContainer(): React.FunctionComponentElement { onSelectExperimentNamesChange={ scattersAppModel.onSelectExperimentNamesChange } + onToggleAllExperiments={scattersAppModel.onToggleAllExperiments} onSelectOptionsChange={scattersAppModel.onSelectOptionsChange} onSelectRunQueryChange={scattersAppModel.onSelectRunQueryChange} onExportTableData={scattersAppModel.onExportTableData} diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index e58702df..338e3c2c 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -101,6 +101,7 @@ import onSmoothingChange from 'utils/app/onSmoothingChange'; import { onTableDiffShow } from 'utils/app/onTableDiffShow'; import { onTableResizeEnd } from 'utils/app/onTableResizeEnd'; import onTableResizeModeChange from 'utils/app/onTableResizeModeChange'; +import onToggleAllExperiments from 'utils/app/onToggleAllExperiments'; import onTableRowClick from 'utils/app/onTableRowClick'; import onTableRowHover from 'utils/app/onTableRowHover'; import onTableSortChange from 'utils/app/onTableSortChange'; @@ -1644,6 +1645,10 @@ function getMetricsAppModelMethods( onSelectExperimentNamesChange({ experimentName, model }); getMetricsData(true, true).call(); }, + onToggleAllExperiments(experimentNames: string[]): void { + onToggleAllExperiments({ experimentNames, model }); + getMetricsData(true, true).call(); + }, onSelectRunQueryChange(query: string): void { onSelectRunQueryChange({ query, model }); }, diff --git a/src/src/services/models/explorer/paramsModelMethods.ts b/src/src/services/models/explorer/paramsModelMethods.ts index 249b47fb..2e1b9422 100644 --- a/src/src/services/models/explorer/paramsModelMethods.ts +++ b/src/src/services/models/explorer/paramsModelMethods.ts @@ -78,6 +78,7 @@ import onSortFieldsChange from 'utils/app/onSortFieldsChange'; import { onTableDiffShow } from 'utils/app/onTableDiffShow'; import { onTableResizeEnd } from 'utils/app/onTableResizeEnd'; import onTableResizeModeChange from 'utils/app/onTableResizeModeChange'; +import onToggleAllExperiments from 'utils/app/onToggleAllExperiments'; import onTableRowClick from 'utils/app/onTableRowClick'; import onTableRowHover from 'utils/app/onTableRowHover'; import onTableSortChange from 'utils/app/onTableSortChange'; @@ -1570,6 +1571,10 @@ function getParamsModelMethods( onSelectExperimentNamesChange({ experimentName, model }); getParamsData(true, true).call(); }, + onToggleAllExperiments(experimentNames: string[]): void { + onToggleAllExperiments({ experimentNames, model }); + getParamsData(true, true).call(); + }, onSelectRunQueryChange(query: string): void { onSelectRunQueryChange({ query, model }); }, diff --git a/src/src/services/models/explorer/scattersModelMethods.ts b/src/src/services/models/explorer/scattersModelMethods.ts index ef7a8b8a..92172a83 100644 --- a/src/src/services/models/explorer/scattersModelMethods.ts +++ b/src/src/services/models/explorer/scattersModelMethods.ts @@ -84,6 +84,7 @@ import onTableResizeModeChange from 'utils/app/onTableResizeModeChange'; import onTableRowClick from 'utils/app/onTableRowClick'; import onTableRowHover from 'utils/app/onTableRowHover'; import onTableSortChange from 'utils/app/onTableSortChange'; +import onToggleAllExperiments from 'utils/app/onToggleAllExperiments'; import onSelectExperimentNamesChange from 'utils/app/onSelectExperimentNamesChange'; import updateColumnsWidths from 'utils/app/updateColumnsWidths'; import updateSortFields from 'utils/app/updateTableSortFields'; @@ -1501,6 +1502,10 @@ function getScattersModelMethods( onSelectExperimentNamesChange({ experimentName, model }); getScattersData(true, true).call(); }, + onToggleAllExperiments(experimentNames: string[]): void { + onToggleAllExperiments({ experimentNames, model }); + getScattersData(true, true).call(); + }, onSelectRunQueryChange(query: string): void { onSelectRunQueryChange({ query, model }); }, diff --git a/src/src/types/pages/metrics/Metrics.d.ts b/src/src/types/pages/metrics/Metrics.d.ts index 266c253b..6dab70b9 100644 --- a/src/src/types/pages/metrics/Metrics.d.ts +++ b/src/src/types/pages/metrics/Metrics.d.ts @@ -131,6 +131,7 @@ export interface IMetricProps extends Partial { onDensityTypeChange: (type: DensityOptions) => void; onMetricsSelectChange: (options: ISelectOption[]) => void; onSelectExperimentNamesChange: (experimentName: string) => void; + onToggleAllExperiments: (experimentNames: string[]) => void; onSelectRunQueryChange: (query: string) => void; onSelectAdvancedQueryChange: (query: string) => void; onRowsVisibilityChange: (metricKeys: string[]) => void; diff --git a/src/src/types/pages/metrics/components/MetricsBar/MetricsBar.d.ts b/src/src/types/pages/metrics/components/MetricsBar/MetricsBar.d.ts index f2458108..32a8276f 100644 --- a/src/src/types/pages/metrics/components/MetricsBar/MetricsBar.d.ts +++ b/src/src/types/pages/metrics/components/MetricsBar/MetricsBar.d.ts @@ -13,4 +13,5 @@ export interface IMetricsBarProps { }) => void; title: string; onSelectExperimentNamesChange: (experimentName: string) => void; + onToggleAllExperiments: (experimentNames: string[]) => void; } diff --git a/src/src/types/pages/params/Params.d.ts b/src/src/types/pages/params/Params.d.ts index b0a5f7fd..a04f071f 100644 --- a/src/src/types/pages/params/Params.d.ts +++ b/src/src/types/pages/params/Params.d.ts @@ -91,6 +91,7 @@ export interface IParamsProps extends Partial { onColorIndicatorChange: () => void; onParamsSelectChange: (options: ISelectOption[]) => void; onSelectExperimentNamesChange: (experimentName: string) => void; + onToggleAllExperiments: (experimentNames: string[]) => void; onSelectRunQueryChange: (query: string) => void; onGroupingSelectChange: (params: IOnGroupingSelectChangeParams) => void; onGroupingModeChange: (params: IOnGroupingModeChangeParams) => void; diff --git a/src/src/types/pages/scatters/Scatters.d.ts b/src/src/types/pages/scatters/Scatters.d.ts index 2f372668..4bae80fd 100644 --- a/src/src/types/pages/scatters/Scatters.d.ts +++ b/src/src/types/pages/scatters/Scatters.d.ts @@ -94,6 +94,7 @@ export interface IScattersProps extends Partial { onNotificationDelete: (id: number) => void; onResetConfigData: () => void; onSelectExperimentNamesChange: (experimentName: string) => void; + onToggleAllExperiments: (experimentNames: string[]) => void; onSelectOptionsChange: (options: ISelectOption[]) => void; onSelectRunQueryChange: (query: string) => void; onExportTableData: (e: React.ChangeEvent) => void; diff --git a/src/src/utils/app/onToggleAllExperiments.ts b/src/src/utils/app/onToggleAllExperiments.ts new file mode 100644 index 00000000..dd9930e3 --- /dev/null +++ b/src/src/utils/app/onToggleAllExperiments.ts @@ -0,0 +1,16 @@ +import { IModel, State } from 'types/services/models/model'; + +import onSelectExperimentNamesChange from './onSelectExperimentNamesChange'; + +export default function onToggleAllExperiments({ + experimentNames, + model, +}: { + experimentNames: string[]; + model: IModel; +}) { + // Toggle all experiments in list one by one + experimentNames.forEach((experimentName) => { + onSelectExperimentNamesChange({ experimentName, model }); + }); +} From 1559e456177d4d3ece023dd293ca47920a876c00 Mon Sep 17 00:00:00 2001 From: Geoffrey Wilson Date: Wed, 20 Mar 2024 10:42:21 -0400 Subject: [PATCH 14/79] Live update disable (#50) * Pickup "live_updates_enabled" field and show/hide accordingly --- .../LiveUpdateSettings/LiveUpdateSettings.tsx | 41 +++++++++++-------- .../services/models/projects/projectsModel.ts | 2 + 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/src/components/LiveUpdateSettings/LiveUpdateSettings.tsx b/src/src/components/LiveUpdateSettings/LiveUpdateSettings.tsx index fb9591ca..5c8dc03f 100644 --- a/src/src/components/LiveUpdateSettings/LiveUpdateSettings.tsx +++ b/src/src/components/LiveUpdateSettings/LiveUpdateSettings.tsx @@ -13,24 +13,29 @@ export interface ILiveUpdateSettingsProp { function LiveUpdateSettings( props: ILiveUpdateSettingsProp, -): React.FunctionComponentElement { - return ( - -
- - Live Update: - - { - props.onLiveUpdateConfigChange({ enabled: !props.enabled }); - }} - size='small' - color='primary' - /> -
-
- ); +): React.FunctionComponentElement | null { + //@ts-ignore + if (window.live_updates_enabled == 1) { + return ( + +
+ + Live Update: + + { + props.onLiveUpdateConfigChange({ enabled: !props.enabled }); + }} + size='small' + color='primary' + /> +
+
+ ); + } + props.onLiveUpdateConfigChange({ enabled: false }); + return null; } export default React.memo(LiveUpdateSettings); diff --git a/src/src/services/models/projects/projectsModel.ts b/src/src/services/models/projects/projectsModel.ts index 4422c903..a892f888 100644 --- a/src/src/services/models/projects/projectsModel.ts +++ b/src/src/services/models/projects/projectsModel.ts @@ -24,6 +24,8 @@ function getProjectsData() { }).then((data: IProject) => { //@ts-ignore window.telemetry_enabled = data.telemetry_enabled; + //@ts-ignore + window.live_updates_enabled = data.live_updates_enabled; model.setState({ project: data, }); From 1ca286c02d473e5cdb75ef47fc3ee563825cd00c Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Thu, 21 Mar 2024 01:58:37 +0900 Subject: [PATCH 15/79] Experiment Regex Fixes (#49) * Fix invalid regex when toggling * Add conditional filter execution on toggle --- .../ExperimentSelectionPopover.tsx | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx index 6184e8c5..42f6cea3 100644 --- a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx +++ b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx @@ -77,24 +77,33 @@ function ExperimentSelectionPopover({ setSearchValue(e.target.value); if (isRegexSearch) { - try { - const regex = new RegExp(e.target.value, 'i'); - setInvalidRegex(false); - const options = experimentsData?.filter((experiment) => - regex.test(experiment.name), - ); - setVisibleExperiments(options || []); - } catch (error) { - setInvalidRegex(true); - } + handleRegexSearch(e.target.value); } else { + handleSimpleSearch(e.target.value); + } + } + + function handleRegexSearch(search: string): void { + try { + const regex = new RegExp(search, 'i'); + setInvalidRegex(false); const options = experimentsData?.filter((experiment) => - experiment.name.toLowerCase().includes(e.target.value.toLowerCase()), + regex.test(experiment.name), ); setVisibleExperiments(options || []); + } catch (error) { + setInvalidRegex(true); } } + function handleSimpleSearch(search: string): void { + setInvalidRegex(false); + const options = experimentsData?.filter((experiment) => + experiment.name.toLowerCase().includes(search.toLowerCase()), + ); + setVisibleExperiments(options || []); + } + function toggleAllExperiments(checked: boolean): void { const visibleExperimentNames = visibleExperiments?.map( (experiment) => experiment.name, @@ -173,6 +182,12 @@ function ExperimentSelectionPopover({ selected={isRegexSearch} onChange={() => { setIsRegexSearch(!isRegexSearch); + + if (!isRegexSearch) { + handleRegexSearch(searchValue); + } else { + handleSimpleSearch(searchValue); + } }} className='RegexToggle' > From c1cc49e7a182a796d868d31c8643afcc84861987 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Thu, 21 Mar 2024 07:56:01 -0700 Subject: [PATCH 16/79] Send List of Metric-Context to SearchMetrics endpoint (#46) Replace metrics and context query with a list of metric (key, partial context) tuples sent to the SearchMetrics endpoint. --- src/src/services/api/api.ts | 16 ++-- .../models/explorer/metricsModelMethods.ts | 89 +++++++++++-------- src/src/utils/app/getMetricsListFromSelect.ts | 39 ++++++++ src/src/utils/app/getSelectOptions.ts | 3 +- 4 files changed, 102 insertions(+), 45 deletions(-) create mode 100644 src/src/utils/app/getMetricsListFromSelect.ts diff --git a/src/src/services/api/api.ts b/src/src/services/api/api.ts index 93615f5a..bb221579 100644 --- a/src/src/services/api/api.ts +++ b/src/src/services/api/api.ts @@ -96,13 +96,19 @@ function getStream( options?: RequestInit, apiHost: string = getAPIHost(), ) { + const queryString = Object.entries(params || {}) + .map(([key, value]) => { + if (Array.isArray(value)) { + return value.map((v) => `${key}=${v}`).join('&'); + } else { + return `${key}=${value}`; + } + }) + .join('&'); + return createAPIRequestWrapper( `${url}${ - options?.method === 'POST' - ? '' - : params - ? '?' + new URLSearchParams(params).toString() - : '' + options?.method === 'POST' ? '' : queryString ? '?' + queryString : '' }`, { method: 'GET', diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index 338e3c2c..2a73c015 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -74,6 +74,7 @@ import getFilteredRow from 'utils/app/getFilteredRow'; import { getGroupingPersistIndex } from 'utils/app/getGroupingPersistIndex'; import getGroupingSelectOptions from 'utils/app/getGroupingSelectOptions'; import getQueryStringFromSelect from 'utils/app/getQueryStringFromSelect'; +import getMetricsListFromSelect from 'utils/app/getMetricsListFromSelect'; import getRunData from 'utils/app/getRunData'; import onAggregationConfigChange from 'utils/app/onAggregationConfigChange'; import onAlignmentMetricChange from 'utils/app/onAlignmentMetricChange'; @@ -217,7 +218,7 @@ function getMetricsAppModelMethods( ); model.setState({ selectFormData: { - options: getSelectOptions(data, true), + options: getSelectOptions(data, false, false), suggestions: getSuggestionsByExplorer(appName, data), advancedSuggestions: { ...getSuggestionsByExplorer(appName, data), @@ -283,55 +284,65 @@ function getMetricsAppModelMethods( configData.select.query = queryString; } } - let query = getQueryStringFromSelect(configData?.select); - metricsRequestRef = metricsService.getMetricsData({ + let metrics = getMetricsListFromSelect(configData?.select); + let query = getQueryStringFromSelect(configData?.select, true); + + let params: { + q: string; + p?: any; + x_axis?: any; + [key: string]: any; + } = { q: query, p: configData?.chart?.densityType, ...(metric ? { x_axis: metric } : {}), + }; + + metrics.forEach((tuple, index) => { + const [metric, context] = tuple; + params[`m[${index}][metric]`] = metric; + params[`m[${index}][context]`] = context; }); + metricsRequestRef = metricsService.getMetricsData(params); setRequestProgress(model); return { call: async () => { - if (query === '()') { - resetModelState(configData, shouldResetSelectedRows!); - } else { - model.setState({ - requestStatus: RequestStatusEnum.Pending, - queryIsEmpty: false, - selectedRows: shouldResetSelectedRows - ? {} - : model.getState()?.selectedRows, + model.setState({ + requestStatus: RequestStatusEnum.Pending, + queryIsEmpty: false, + selectedRows: shouldResetSelectedRows + ? {} + : model.getState()?.selectedRows, + }); + liveUpdateInstance?.stop().then(); + try { + const stream = await metricsRequestRef.call((detail) => { + exceptionHandler({ detail, model }); + resetModelState(configData, shouldResetSelectedRows!); }); - liveUpdateInstance?.stop().then(); - try { - const stream = await metricsRequestRef.call((detail) => { - exceptionHandler({ detail, model }); - resetModelState(configData, shouldResetSelectedRows!); - }); - const runData = await getRunData(stream, (progress) => - setRequestProgress(model, progress), - ); - if (shouldUrlUpdate) { - updateURL({ configData, appName }); - } - saveRecentSearches(appName, query); - updateData(runData); - } catch (ex: Error | any) { - if (ex.name === 'AbortError') { - // Abort Error - } else { - // eslint-disable-next-line no-console - console.log('Unhandled error: ', ex); - } + const runData = await getRunData(stream, (progress) => + setRequestProgress(model, progress), + ); + if (shouldUrlUpdate) { + updateURL({ configData, appName }); + } + saveRecentSearches(appName, query); + updateData(runData); + } catch (ex: Error | any) { + if (ex.name === 'AbortError') { + // Abort Error + } else { + // eslint-disable-next-line no-console + console.log('Unhandled error: ', ex); } - - liveUpdateInstance?.start({ - q: query, - p: configData?.chart?.densityType, - ...(metric && { x_axis: metric }), - }); } + + liveUpdateInstance?.start({ + q: query, + p: configData?.chart?.densityType, + ...(metric && { x_axis: metric }), + }); }, abort: metricsRequestRef.abort, }; diff --git a/src/src/utils/app/getMetricsListFromSelect.ts b/src/src/utils/app/getMetricsListFromSelect.ts new file mode 100644 index 00000000..50ceb67f --- /dev/null +++ b/src/src/utils/app/getMetricsListFromSelect.ts @@ -0,0 +1,39 @@ +import { ISyntaxErrorDetails } from 'types/components/NotificationContainer/NotificationContainer'; +import { ISelectConfig } from 'types/services/models/explorer/createAppModel'; + +import { jsValidVariableRegex } from 'utils/getObjectPaths'; + +import { formatValue } from '../formatValue'; + +export default function getMetricsListFromSelect( + selectData: ISelectConfig, + error?: ISyntaxErrorDetails, +): Array<[string, string]> { + const metricsList: Array<[string, string]> = []; + + if (selectData === undefined) { + return metricsList; + } + + selectData.options?.forEach((option) => { + const metricName = option.value?.option_name ?? ''; + const context: string = + option.value?.context && Object.keys(option.value?.context).length > 0 + ? Object.keys(option.value?.context) + .map((item) => { + const contextKey = !jsValidVariableRegex.test(item) + ? `['${item}']` + : `${item}`; + const contextValue = (option.value?.context as any)[item]; + return `{${formatValue(contextKey)}: ${formatValue( + contextValue, + )}}`; + }) + .join(', ') + : '{}'; + + metricsList.push([metricName, context]); + }); + + return metricsList; +} diff --git a/src/src/utils/app/getSelectOptions.ts b/src/src/utils/app/getSelectOptions.ts index b3bb34c1..0f270dce 100644 --- a/src/src/utils/app/getSelectOptions.ts +++ b/src/src/utils/app/getSelectOptions.ts @@ -15,6 +15,7 @@ import { getMetricLabel } from './getMetricLabel'; export default function getSelectOptions( projectsData: IProjectParamsMetrics, addHighLevelMetrics: boolean = false, + includeParams: boolean = true, ): ISelectOption[] { const comparator = alphabeticalSortComparator({ orderBy: 'label', @@ -72,7 +73,7 @@ export default function getSelectOptions( } } } - if (projectsData?.params && !addHighLevelMetrics) { + if (projectsData?.params && !addHighLevelMetrics && includeParams) { const paramPaths = getObjectPaths(projectsData.params, projectsData.params); paramPaths.forEach((paramPath, index) => { const indexOf = From 70549c0a02e8005e413e043dfb591ae53a9e7cc1 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Fri, 22 Mar 2024 06:00:32 -0700 Subject: [PATCH 17/79] Remove advanced search mode from Metric search (#51) --- src/src/pages/Metrics/Metrics.tsx | 2 - src/src/pages/Metrics/MetricsContainer.tsx | 2 - .../components/SelectForm/SelectForm.tsx | 218 +++++++----------- .../models/explorer/metricsModelMethods.ts | 15 +- src/src/types/pages/metrics/Metrics.d.ts | 2 - .../components/SelectForm/SelectForm.d.ts | 2 - 6 files changed, 87 insertions(+), 154 deletions(-) diff --git a/src/src/pages/Metrics/Metrics.tsx b/src/src/pages/Metrics/Metrics.tsx index 516a353e..ed695ecf 100644 --- a/src/src/pages/Metrics/Metrics.tsx +++ b/src/src/pages/Metrics/Metrics.tsx @@ -106,8 +106,6 @@ function Metrics( selectedMetricsData={props.selectedMetricsData} onMetricsSelectChange={props.onMetricsSelectChange} onSelectRunQueryChange={props.onSelectRunQueryChange} - onSelectAdvancedQueryChange={props.onSelectAdvancedQueryChange} - toggleSelectAdvancedMode={props.toggleSelectAdvancedMode} onSearchQueryCopy={props.onSearchQueryCopy} /> { } onToggleAllExperiments={metricAppModel.onToggleAllExperiments} onSelectRunQueryChange={metricAppModel.onSelectRunQueryChange} - onSelectAdvancedQueryChange={metricAppModel.onSelectAdvancedQueryChange} - toggleSelectAdvancedMode={metricAppModel.toggleSelectAdvancedMode} onExportTableData={metricAppModel.onExportTableData} onRowHeightChange={metricAppModel.onRowHeightChange} onSortChange={metricAppModel.onSortChange} diff --git a/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx b/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx index 4c3d083d..08edab2a 100644 --- a/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx +++ b/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx @@ -32,8 +32,6 @@ function SelectForm({ selectFormData, onMetricsSelectChange, onSelectRunQueryChange, - onSelectAdvancedQueryChange, - toggleSelectAdvancedMode, onSearchQueryCopy, }: ISelectFormProps): React.FunctionComponentElement { const [anchorEl, setAnchorEl] = React.useState(null); @@ -42,8 +40,6 @@ function SelectForm({ const [regexError, setRegexError] = React.useState(null); const searchRef: any = React.useRef>(null); const autocompleteRef: any = React.useRef>(null); - const advancedAutocompleteRef: any = - React.useRef>(null); React.useEffect(() => { return () => { searchRef.current?.abort(); @@ -55,14 +51,9 @@ function SelectForm({ if (requestIsPending) { return; } - let query = selectedMetricsData?.advancedMode - ? advancedAutocompleteRef?.current?.getValue() - : autocompleteRef?.current?.getValue(); - if (selectedMetricsData?.advancedMode) { - onSelectAdvancedQueryChange(advancedAutocompleteRef.current.getValue()); - } else { - onSelectRunQueryChange(autocompleteRef.current.getValue()); - } + let query = autocompleteRef?.current?.getValue(); + + onSelectRunQueryChange(autocompleteRef.current.getValue()); searchRef.current = metricAppModel.getMetricsData(true, true, query ?? ''); searchRef.current.call(); trackEvent(ANALYTICS_EVENT_KEYS.metrics.searchClick); @@ -100,10 +91,6 @@ function SelectForm({ onMetricsSelectChange(fieldData); } - function toggleEditMode(): void { - toggleSelectAdvancedMode(); - } - function handleClick(event: React.ChangeEvent) { setAnchorEl(event.currentTarget); } @@ -122,7 +109,6 @@ function SelectForm({ function handleResetSelectForm(): void { onMetricsSelectChange([]); onSelectRunQueryChange(''); - onSelectAdvancedQueryChange(''); } function handleSearchInputChange( @@ -166,106 +152,88 @@ function SelectForm({ justifyContent='space-between' alignItems='center' > - {selectedMetricsData?.advancedMode ? ( -
- -
- ) : ( - <> - - - - - {selectedMetricsData?.options.length === 0 && ( - - No metrics are selected - - )} - - {selectedMetricsData?.options?.map((tag: ISelectOption) => { - return ( - - ); - })} - - - {selectedMetricsData?.options && - selectedMetricsData.options.length > 1 && ( - - )} - - )} - - {selectedMetricsData?.advancedMode ? null : ( -
- + + + -
- )} + {selectedMetricsData?.options.length === 0 && ( + + No metrics are selected + + )} + + {selectedMetricsData?.options?.map((tag: ISelectOption) => { + return ( + + ); + })} + + + {selectedMetricsData?.options && + selectedMetricsData.options.length > 1 && ( + + )} + +
+ +
- -
- -
-
diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index 2a73c015..eb71e2f8 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -108,7 +108,6 @@ import onTableRowHover from 'utils/app/onTableRowHover'; import onTableSortChange from 'utils/app/onTableSortChange'; import onZoomChange from 'utils/app/onZoomChange'; import setAggregationEnabled from 'utils/app/setAggregationEnabled'; -import toggleSelectAdvancedMode from 'utils/app/toggleSelectAdvancedMode'; import updateColumnsWidths from 'utils/app/updateColumnsWidths'; import updateSortFields from 'utils/app/updateTableSortFields'; import contextToString from 'utils/contextToString'; @@ -278,12 +277,9 @@ function getMetricsAppModelMethods( const metric = configData?.chart?.alignmentConfig?.metric; if (queryString) { - if (configData.select.advancedMode) { - configData.select.advancedQuery = queryString; - } else { - configData.select.query = queryString; - } + configData.select.query = queryString; } + let metrics = getMetricsListFromSelect(configData?.select); let query = getQueryStringFromSelect(configData?.select, true); @@ -972,7 +968,6 @@ function getMetricsAppModelMethods( data, selectFormData: { ...modelState?.selectFormData, - [configData.select?.advancedMode ? 'advancedError' : 'error']: null, }, lineChartData: getDataAsLines(data), chartTitleData: getChartTitleData>( @@ -1663,12 +1658,6 @@ function getMetricsAppModelMethods( onSelectRunQueryChange(query: string): void { onSelectRunQueryChange({ query, model }); }, - onSelectAdvancedQueryChange(query: string): void { - onSelectAdvancedQueryChange({ query, model }); - }, - toggleSelectAdvancedMode(): void { - toggleSelectAdvancedMode({ model, appName }); - }, }); } if (components?.charts?.[0]) { diff --git a/src/src/types/pages/metrics/Metrics.d.ts b/src/src/types/pages/metrics/Metrics.d.ts index 6dab70b9..d3198438 100644 --- a/src/src/types/pages/metrics/Metrics.d.ts +++ b/src/src/types/pages/metrics/Metrics.d.ts @@ -133,9 +133,7 @@ export interface IMetricProps extends Partial { onSelectExperimentNamesChange: (experimentName: string) => void; onToggleAllExperiments: (experimentNames: string[]) => void; onSelectRunQueryChange: (query: string) => void; - onSelectAdvancedQueryChange: (query: string) => void; onRowsVisibilityChange: (metricKeys: string[]) => void; - toggleSelectAdvancedMode: () => void; onExportTableData: (e: React.ChangeEvent) => void; onRowHeightChange: (height: RowHeightSize) => void; onSortReset: () => void; diff --git a/src/src/types/pages/metrics/components/SelectForm/SelectForm.d.ts b/src/src/types/pages/metrics/components/SelectForm/SelectForm.d.ts index 84e2a519..d95c8ecd 100644 --- a/src/src/types/pages/metrics/components/SelectForm/SelectForm.d.ts +++ b/src/src/types/pages/metrics/components/SelectForm/SelectForm.d.ts @@ -16,7 +16,5 @@ export interface ISelectFormProps { }; onMetricsSelectChange: (options: ISelectOption[]) => void; onSelectRunQueryChange: (query: string) => void; - onSelectAdvancedQueryChange: (query: string) => void; - toggleSelectAdvancedMode: () => void; onSearchQueryCopy: () => void; } From d00c169b2301480181c12701610f1c2def0b06d4 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Thu, 28 Mar 2024 12:40:30 +0100 Subject: [PATCH 18/79] change endpoint from admin/namespaces to chooser/namespaces (#53) --- src/src/components/SideBar/SideBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/src/components/SideBar/SideBar.tsx b/src/src/components/SideBar/SideBar.tsx index 249214f9..e67f873f 100644 --- a/src/src/components/SideBar/SideBar.tsx +++ b/src/src/components/SideBar/SideBar.tsx @@ -51,13 +51,13 @@ function SideBar(): React.FunctionComponentElement { }, []); useEffect(() => { - fetch(`${getBaseHost()}/admin/namespaces/list`) + fetch(`${getBaseHost()}/chooser/namespaces`) .then((response) => response.json()) .then((data) => setNamespaces(data.map((item: { code: any }) => item.code)), ); - fetch(`${getBaseHost()}${getPrefix()}admin/namespaces/current`) + fetch(`${getBaseHost()}${getPrefix()}chooser/namespaces/current`) .then((response) => response.json()) .then((data) => { const selected = data.code; From 2f907fd270d93583d7b28fad0336faebe455984a Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Thu, 28 Mar 2024 20:22:34 -0700 Subject: [PATCH 19/79] Fix various experiment header issues (#52) * Fix rerender issue with no selected params * Fix metrics error bug * Fix select form popper styling * Update createAppModel.ts --- .../components/MetricsBar/MetricsBar.tsx | 6 +- .../components/SelectForm/SelectForm.scss | 7 +- .../components/SelectForm/SelectForm.scss | 7 +- .../models/explorer/metricsModelMethods.ts | 69 ++++++++++--------- .../app/onSelectExperimentNamesChange.ts | 5 +- 5 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx b/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx index f450759d..cbfbdd44 100644 --- a/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx +++ b/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx @@ -35,6 +35,9 @@ function MetricsBar({ onToggleAllExperiments, }: IMetricsBarProps): React.FunctionComponentElement { const [popover, setPopover] = React.useState(''); + const [selectedExperimentNames, setSelectedExperimentNames] = React.useState< + string[] + >(getSelectedExperimentNames()); const route = useRouteMatch(); @@ -48,8 +51,6 @@ function MetricsBar({ const { data: experimentsData, loading: isExperimentsLoading } = experimentsState; - const selectedExperimentNames = getSelectedExperimentNames(); - function handleBookmarkClick(value: string): void { setPopover(value); } @@ -65,6 +66,7 @@ function MetricsBar({ function handleExperimentNamesChange(experimentName: string): void { onSelectExperimentNamesChange(experimentName); + setSelectedExperimentNames(getSelectedExperimentNames()); } return ( diff --git a/src/src/pages/Metrics/components/SelectForm/SelectForm.scss b/src/src/pages/Metrics/components/SelectForm/SelectForm.scss index c2eda0b7..1e15e8dc 100644 --- a/src/src/pages/Metrics/components/SelectForm/SelectForm.scss +++ b/src/src/pages/Metrics/components/SelectForm/SelectForm.scss @@ -131,14 +131,15 @@ margin: 10px; } -.Popper__SelectForm__select{ +.Popper__SelectForm__select { @extend .Metrics__SelectForm__metric__select; } -.Popper__SelectForm__option{ +.Popper__SelectForm__option { @extend .Metrics__SelectForm__option !optional; } -.Popper__SelectForm__option__label{ +.Popper__SelectForm__option__label { @extend .Metrics__SelectForm__option__label !optional; + margin-left: $space-xs; } diff --git a/src/src/pages/Params/components/SelectForm/SelectForm.scss b/src/src/pages/Params/components/SelectForm/SelectForm.scss index d9801ea4..dc49836f 100644 --- a/src/src/pages/Params/components/SelectForm/SelectForm.scss +++ b/src/src/pages/Params/components/SelectForm/SelectForm.scss @@ -117,14 +117,15 @@ margin: 10px; } -.Popper__SelectForm__select{ +.Popper__SelectForm__select { @extend .SelectForm__param__select; } -.Popper__SelectForm__option{ +.Popper__SelectForm__option { @extend .SelectForm__option !optional; } -.Popper__SelectForm__option__label{ +.Popper__SelectForm__option__label { @extend .SelectForm__option__label !optional; + margin-left: $space-xs; } diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index eb71e2f8..524fc470 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -96,7 +96,6 @@ import onSelectExperimentNamesChange from 'utils/app/onSelectExperimentNamesChan import onMetricVisibilityChange from 'utils/app/onMetricsVisibilityChange'; import onRowHeightChange from 'utils/app/onRowHeightChange'; import onRowVisibilityChange from 'utils/app/onRowVisibilityChange'; -import onSelectAdvancedQueryChange from 'utils/app/onSelectAdvancedQueryChange'; import onSelectRunQueryChange from 'utils/app/onSelectRunQueryChange'; import onSmoothingChange from 'utils/app/onSmoothingChange'; import { onTableDiffShow } from 'utils/app/onTableDiffShow'; @@ -304,41 +303,45 @@ function getMetricsAppModelMethods( setRequestProgress(model); return { call: async () => { - model.setState({ - requestStatus: RequestStatusEnum.Pending, - queryIsEmpty: false, - selectedRows: shouldResetSelectedRows - ? {} - : model.getState()?.selectedRows, - }); - liveUpdateInstance?.stop().then(); - try { - const stream = await metricsRequestRef.call((detail) => { - exceptionHandler({ detail, model }); - resetModelState(configData, shouldResetSelectedRows!); + if (_.isEmpty(configData?.select?.options)) { + resetModelState(configData, shouldResetSelectedRows!); + } else { + model.setState({ + requestStatus: RequestStatusEnum.Pending, + queryIsEmpty: false, + selectedRows: shouldResetSelectedRows + ? {} + : model.getState()?.selectedRows, }); - const runData = await getRunData(stream, (progress) => - setRequestProgress(model, progress), - ); - if (shouldUrlUpdate) { - updateURL({ configData, appName }); - } - saveRecentSearches(appName, query); - updateData(runData); - } catch (ex: Error | any) { - if (ex.name === 'AbortError') { - // Abort Error - } else { - // eslint-disable-next-line no-console - console.log('Unhandled error: ', ex); + liveUpdateInstance?.stop().then(); + try { + const stream = await metricsRequestRef.call((detail) => { + exceptionHandler({ detail, model }); + resetModelState(configData, shouldResetSelectedRows!); + }); + const runData = await getRunData(stream, (progress) => + setRequestProgress(model, progress), + ); + if (shouldUrlUpdate) { + updateURL({ configData, appName }); + } + saveRecentSearches(appName, query); + updateData(runData); + } catch (ex: Error | any) { + if (ex.name === 'AbortError') { + // Abort Error + } else { + // eslint-disable-next-line no-console + console.log('Unhandled error: ', ex); + } } - } - liveUpdateInstance?.start({ - q: query, - p: configData?.chart?.densityType, - ...(metric && { x_axis: metric }), - }); + liveUpdateInstance?.start({ + q: query, + p: configData?.chart?.densityType, + ...(metric && { x_axis: metric }), + }); + } }, abort: metricsRequestRef.abort, }; diff --git a/src/src/utils/app/onSelectExperimentNamesChange.ts b/src/src/utils/app/onSelectExperimentNamesChange.ts index 98ab27f9..9037f6b5 100644 --- a/src/src/utils/app/onSelectExperimentNamesChange.ts +++ b/src/src/utils/app/onSelectExperimentNamesChange.ts @@ -33,7 +33,10 @@ export default function onSelectExperimentNamesChange({ if (configData?.select) { const newConfig = { ...configData, - select: { ...configData.select, selectedExperimentNames }, + select: { + ...configData.select, + selectedExperimentNames, + }, }; model.setState({ config: newConfig }); } From b03f8492433631bd6e036c05a2d9eed4309501df Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Tue, 2 Apr 2024 00:22:36 +0900 Subject: [PATCH 20/79] Fix initial experiment state namespace bug (#57) --- .../pages/Experiment/useExperimentState.tsx | 9 +++++++-- .../components/MetricsBar/MetricsBar.tsx | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/src/pages/Experiment/useExperimentState.tsx b/src/src/pages/Experiment/useExperimentState.tsx index b5632de9..768a792d 100644 --- a/src/src/pages/Experiment/useExperimentState.tsx +++ b/src/src/pages/Experiment/useExperimentState.tsx @@ -5,7 +5,7 @@ import { IExperimentData } from 'modules/core/api/experimentsApi'; import experimentEngine from './ExperimentStore'; -function useExperimentState(experimentId: string) { +function useExperimentState(experimentId?: string) { const { current: engine } = React.useRef(experimentEngine); const experimentState: IResourceState = engine.experimentState((state) => state); @@ -21,7 +21,12 @@ function useExperimentState(experimentId: string) { }, []); React.useEffect(() => { - engine.fetchExperimentData(experimentId as any); + if (experimentId) { + engine.fetchExperimentData(experimentId as any); + } else { + // Fetch the default experiment in the namespace + engine.fetchExperimentsData(); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [experimentId]); diff --git a/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx b/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx index cbfbdd44..e9c6f099 100644 --- a/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx +++ b/src/src/pages/Metrics/components/MetricsBar/MetricsBar.tsx @@ -13,6 +13,10 @@ import ConfirmModal from 'components/ConfirmModal/ConfirmModal'; import { DOCUMENTATIONS } from 'config/references'; +import { IResourceState } from 'modules/core/utils/createResource'; +import { IExperimentData } from 'modules/core/api/experimentsApi'; + +import createExperimentEngine from 'pages/Dashboard/components/ExploreSection/ExperimentsCard/ExperimentsStore'; import ExperimentBar from 'pages/Experiment/components/ExperimentBar'; import useExperimentState from 'pages/Experiment/useExperimentState'; @@ -41,9 +45,22 @@ function MetricsBar({ const route = useRouteMatch(); + const { current: experimentsEngine } = React.useRef(createExperimentEngine); + + const experimentsStore: IResourceState = + experimentsEngine.experimentsState((state) => state); + + React.useEffect(() => { + experimentsEngine.fetchExperiments(); + return () => { + experimentsEngine.destroy(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // Fetch all experiments along with default const { experimentState, experimentsState, getExperimentsData } = - useExperimentState('0'); + useExperimentState(experimentsStore.data?.[0]?.id); const { data: experimentData, loading: isExperimentLoading } = experimentState; From 2a3f82bbfad6143b8e17e7457996f1aee45ef24f Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:09:49 -0700 Subject: [PATCH 21/79] Link Run from Aim UI to MLFlow UI (#55) * Create link for experiment to classic ui * Link run from Aim UI to MLFlow UI * use icon * add icon --------- Co-authored-by: fabio vincenzi Co-authored-by: Geoffrey Wilson --- src/src/components/SideBar/SideBar.tsx | 19 ++-- .../ExperimentHeader/ExperimentHeader.tsx | 32 ++++++- .../ExperimentSelectionPopover.tsx | 94 +++++++++++++------ src/src/pages/RunDetail/RunDetail.tsx | 36 ++++++- src/src/services/api/api.ts | 39 +++++--- .../api/namespaces/namespacesService.ts | 34 +++++++ 6 files changed, 202 insertions(+), 52 deletions(-) create mode 100644 src/src/services/api/namespaces/namespacesService.ts diff --git a/src/src/components/SideBar/SideBar.tsx b/src/src/components/SideBar/SideBar.tsx index e67f873f..3a2fb280 100644 --- a/src/src/components/SideBar/SideBar.tsx +++ b/src/src/components/SideBar/SideBar.tsx @@ -19,6 +19,7 @@ import { DOCUMENTATIONS } from 'config/references'; import routes, { IRoute } from 'routes/routes'; +import namespacesService from 'services/api/namespaces/namespacesService'; import { trackEvent } from 'services/analytics'; import { getItem } from 'utils/storage'; @@ -51,18 +52,20 @@ function SideBar(): React.FunctionComponentElement { }, []); useEffect(() => { - fetch(`${getBaseHost()}/chooser/namespaces`) - .then((response) => response.json()) - .then((data) => - setNamespaces(data.map((item: { code: any }) => item.code)), - ); - - fetch(`${getBaseHost()}${getPrefix()}chooser/namespaces/current`) - .then((response) => response.json()) + namespacesService + .fetchCurrentNamespace() + .call() .then((data) => { const selected = data.code; setSelectedNamespace(selected); }); + + namespacesService + .fetchNamespacesList() + .call() + .then((data) => + setNamespaces(data.map((item: { code: any }) => item.code)), + ); }, []); function selectNamespace(event: React.ChangeEvent<{ value: unknown }>) { diff --git a/src/src/pages/Experiment/components/ExperimentHeader/ExperimentHeader.tsx b/src/src/pages/Experiment/components/ExperimentHeader/ExperimentHeader.tsx index 59cd4f6d..08d8dd83 100644 --- a/src/src/pages/Experiment/components/ExperimentHeader/ExperimentHeader.tsx +++ b/src/src/pages/Experiment/components/ExperimentHeader/ExperimentHeader.tsx @@ -1,16 +1,20 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import classNames from 'classnames'; +import { Link as RouteLink } from 'react-router-dom'; import { NavLink, useRouteMatch } from 'react-router-dom'; import moment from 'moment'; import { Skeleton } from '@material-ui/lab'; -import { Tooltip } from '@material-ui/core'; +import { Link, Tooltip } from '@material-ui/core'; import { Button, Icon, Text } from 'components/kit'; import ControlPopover from 'components/ControlPopover/ControlPopover'; import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; import { DATE_WITH_SECONDS } from 'config/dates/dates'; +import { getBaseHost } from 'config/config'; + +import namespacesService from 'services/api/namespaces/namespacesService'; import ExperimentNavigationPopover from '../ExperimentNavigationPopover'; @@ -27,6 +31,12 @@ function ExperimentHeader({ getExperimentsData, }: IExperimentHeaderProps): React.FunctionComponentElement { const { url } = useRouteMatch(); + const [selectedNamespace, setSelectedNamespace] = useState(''); + useEffect(() => { + namespacesService.fetchCurrentNamespacePath().then((data) => { + setSelectedNamespace(data); + }); + }, []); return ( @@ -108,6 +118,24 @@ function ExperimentHeader({ (experimentData?.creation_time || 0) * 1000, ).format(DATE_WITH_SECONDS)}`} + +
+ + Open in Classic UI +
+ ) : ( ([]); + const [selectedNamespace, setSelectedNamespace] = React.useState(''); + React.useEffect(() => { + namespacesService.fetchCurrentNamespacePath().then((data) => { + setSelectedNamespace(data); + }); + }, []); React.useEffect(() => { setVisibleExperiments(experimentsData || []); @@ -236,35 +245,64 @@ function ExperimentSelectionPopover({ > {shortenExperimentName(experiment?.name)} -
- - +
+ + + {`${moment(experiment.creation_time * 1000).format( + DATE_WITH_SECONDS, + )}`} + +
+ e.stopPropagation()} + style={{ marginLeft: '1rem' }} > - {`${moment(experiment.creation_time * 1000).format( - DATE_WITH_SECONDS, - )}`} -
+
+ + Open in Classic UI +
+
diff --git a/src/src/pages/RunDetail/RunDetail.tsx b/src/src/pages/RunDetail/RunDetail.tsx index 6f0c5756..2decc20c 100644 --- a/src/src/pages/RunDetail/RunDetail.tsx +++ b/src/src/pages/RunDetail/RunDetail.tsx @@ -13,7 +13,13 @@ import { } from 'react-router-dom'; import { useModel } from 'hooks'; -import { Paper, Tab, Tabs, Tooltip } from '@material-ui/core'; +import { + Paper, + Tab, + Tabs, + Tooltip, + Link as MaterialLink, +} from '@material-ui/core'; import { Skeleton } from '@material-ui/lab'; import { Button, Icon, Text } from 'components/kit'; @@ -27,6 +33,7 @@ import Spinner from 'components/kit/Spinner'; import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap'; import { DATE_WITH_SECONDS } from 'config/dates/dates'; import { PathEnum } from 'config/enums/routesEnum'; +import { getBaseHost } from 'config/config'; import CompareSelectedRunsPopover from 'pages/Metrics/components/Table/CompareSelectedRunsPopover'; @@ -34,6 +41,7 @@ import runDetailAppModel from 'services/models/runs/runDetailAppModel'; import * as analytics from 'services/analytics'; import notesModel from 'services/models/notes/notesModel'; import { AppNameEnum } from 'services/models/explorer'; +import namespacesService from 'services/api/namespaces/namespacesService'; import { setDocumentTitle } from 'utils/document/documentTitle'; import { processDurationTime } from 'utils/processDurationTime'; @@ -83,6 +91,12 @@ function RunDetail(): React.FunctionComponentElement { const [isRunSelectDropdownOpen, setIsRunSelectDropdownOpen] = React.useState(false); const [activeTab, setActiveTab] = React.useState(pathname); + const [selectedNamespace, setSelectedNamespace] = React.useState(''); + React.useEffect(() => { + namespacesService.fetchCurrentNamespacePath().then((data) => { + setSelectedNamespace(data); + }); + }, []); function redirect(): void { const splitPathname: string[] = pathname.split('/'); @@ -320,6 +334,26 @@ function RunDetail(): React.FunctionComponentElement { : dateNow, )}`} + +
+ + Open in Classic UI +
+
) : ( ( return { call: (exceptionHandler?: (error: ResponseDataType) => any) => new Promise((resolve: (data: ResponseDataType) => void, reject) => { - fetch(`${apiHost}/${url}`, { ...options, signal }) + // prevent creating urls with double forward slashes. + let urlSep = '/'; + if (url.charAt(0) == urlSep) { + urlSep = ''; + } + fetch(`${apiHost}${urlSep}${url}`, { ...options, signal }) .then(async (response) => { try { if (response.status >= 400) { @@ -53,17 +58,7 @@ function createAPIRequestWrapper( exceptionHandler(body); } - return await checkCredentials( - response, - url, - () => - createAPIRequestWrapper( - url, - options, - stream, - apiHost, - ).call(exceptionHandler), - ); + return; } const data = stream ? response.body : await response.json(); @@ -148,6 +143,23 @@ function getStream1( ); } +function getFromBaseHost( + url: string, + params?: {}, + options?: RequestInit, +) { + return createAPIRequestWrapper( + `${url}${params ? '?' + new URLSearchParams(params).toString() : ''}`, + { + method: 'GET', + ...options, + headers: getRequestHeaders(), + }, + false, + getBaseHost(), + ); +} + function get( url: string, params?: {}, @@ -375,6 +387,7 @@ async function checkCredentials( const API = { get, + getFromBaseHost, getStream, getStream1, post, diff --git a/src/src/services/api/namespaces/namespacesService.ts b/src/src/services/api/namespaces/namespacesService.ts new file mode 100644 index 00000000..3b4c3e82 --- /dev/null +++ b/src/src/services/api/namespaces/namespacesService.ts @@ -0,0 +1,34 @@ +import { getPrefix } from 'config/config'; + +import { IAppData } from 'types/services/models/metrics/metricsAppModel'; +import { IApiRequest } from 'types/services/services'; + +import API from '../api'; + +const endpoints = { + NAMESPACES: '/chooser/namespaces', + CURRENT_NAMESPACE: `${getPrefix()}chooser/namespaces/current`, +}; + +function fetchNamespacesList(): IApiRequest { + return API.getFromBaseHost(endpoints.NAMESPACES); +} + +function fetchCurrentNamespace(): IApiRequest { + return API.getFromBaseHost(endpoints.CURRENT_NAMESPACE); +} + +async function fetchCurrentNamespacePath(): Promise { + const data = await fetchCurrentNamespace().call(); + const selected = data.code; + return selected === 'default' ? '' : `/ns/${data.code}`; +} + +const namespacesService = { + endpoints, + fetchNamespacesList, + fetchCurrentNamespace, + fetchCurrentNamespacePath, +}; + +export default namespacesService; From 4b4f20917aba6ffd99f837f61429acd88be06f03 Mon Sep 17 00:00:00 2001 From: Geoffrey Wilson Date: Wed, 3 Apr 2024 09:06:18 -0700 Subject: [PATCH 22/79] refresh Metrics drop down with periodic refresh of project-params (#56) --- .../models/explorer/metricsModelMethods.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index 524fc470..7ef262bc 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -206,7 +206,22 @@ function getMetricsAppModelMethods( if (!appId) { setModelDefaultAppConfigData(); } + // fetch project params now and update every 30s + fetchProjectParamsAndUpdateState(); + setInterval(fetchProjectParamsAndUpdateState, 30000); + const liveUpdateState = model.getState()?.config?.liveUpdate; + + if (liveUpdateState?.enabled) { + liveUpdateInstance = new LiveUpdateService( + appName, + updateData, + liveUpdateState.delay, + ); + } + } + + function fetchProjectParamsAndUpdateState() { projectsService .getProjectParams(['metric']) .call() @@ -230,15 +245,6 @@ function getMetricsAppModelMethods( }, }); }); - const liveUpdateState = model.getState()?.config?.liveUpdate; - - if (liveUpdateState?.enabled) { - liveUpdateInstance = new LiveUpdateService( - appName, - updateData, - liveUpdateState.delay, - ); - } } function updateData(newData: ISequence[]): void { From 4c2e66850eda71459856f552f7abc89d8b3553d8 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Thu, 4 Apr 2024 00:57:16 -0700 Subject: [PATCH 23/79] Change Search Metric to be a POST (#58) --- .../services/api/metrics/metricsService.ts | 11 ++++++--- .../models/explorer/metricsModelMethods.ts | 24 ++++++------------- .../models/metrics/metricsAppModel.d.ts | 9 +++++++ src/src/utils/app/getMetricsListFromSelect.ts | 18 ++++---------- 4 files changed, 28 insertions(+), 34 deletions(-) diff --git a/src/src/services/api/metrics/metricsService.ts b/src/src/services/api/metrics/metricsService.ts index 3f72d511..78c3a208 100644 --- a/src/src/services/api/metrics/metricsService.ts +++ b/src/src/services/api/metrics/metricsService.ts @@ -1,5 +1,8 @@ import { IApiRequest } from 'types/services/services'; -import { IAlignMetricsDataParams } from 'types/services/models/metrics/metricsAppModel'; +import { + IAlignMetricsDataParams, + IMetricsDataParams, +} from 'types/services/models/metrics/metricsAppModel'; import API from '../api'; // import generateMetrics from './metricsMock'; @@ -10,8 +13,10 @@ const endpoints = { GET_ALIGNED_METRICS: 'runs/search/metric/align', }; -function getMetricsData(params: {}): IApiRequest { - return API.getStream(endpoints.GET_METRICS, params); +function getMetricsData(params: IMetricsDataParams): IApiRequest { + return API.getStream>(endpoints.GET_METRICS, params, { + method: 'POST', + }); // We will not remove this part yet, though we will need to refactor mock data structure // due to API schema changes diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index 7ef262bc..b64b23f2 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -39,6 +39,7 @@ import { IGroupingSelectOption, IMetricAppModelState, IMetricsCollection, + IMetricsDataParams, IMetricTableRowData, IOnGroupingModeChangeParams, IOnGroupingSelectChangeParams, @@ -288,24 +289,13 @@ function getMetricsAppModelMethods( let metrics = getMetricsListFromSelect(configData?.select); let query = getQueryStringFromSelect(configData?.select, true); - let params: { - q: string; - p?: any; - x_axis?: any; - [key: string]: any; - } = { - q: query, - p: configData?.chart?.densityType, - ...(metric ? { x_axis: metric } : {}), + const reqBody: IMetricsDataParams = { + metrics: metrics, + steps: configData?.chart?.densityType, + query: query, + x_axis: JSON.stringify(metric ? { x_axis: metric } : {}), }; - - metrics.forEach((tuple, index) => { - const [metric, context] = tuple; - params[`m[${index}][metric]`] = metric; - params[`m[${index}][context]`] = context; - }); - - metricsRequestRef = metricsService.getMetricsData(params); + metricsRequestRef = metricsService.getMetricsData(reqBody); setRequestProgress(model); return { call: async () => { diff --git a/src/src/types/services/models/metrics/metricsAppModel.d.ts b/src/src/types/services/models/metrics/metricsAppModel.d.ts index 0d088e51..1f4aed9c 100644 --- a/src/src/types/services/models/metrics/metricsAppModel.d.ts +++ b/src/src/types/services/models/metrics/metricsAppModel.d.ts @@ -256,6 +256,15 @@ export interface IAlignMetricsDataParams { }[]; } +export interface IMetricsDataParams { + report_progress?: boolean; + metrics: Array<{ key: string; context: string }>; + query: string; + steps: number; + x_axis: string; + skip_system?: boolean; +} + export interface ISmoothing { algorithm: SmoothingAlgorithmEnum; factor: number; diff --git a/src/src/utils/app/getMetricsListFromSelect.ts b/src/src/utils/app/getMetricsListFromSelect.ts index 50ceb67f..7c2454f1 100644 --- a/src/src/utils/app/getMetricsListFromSelect.ts +++ b/src/src/utils/app/getMetricsListFromSelect.ts @@ -8,8 +8,8 @@ import { formatValue } from '../formatValue'; export default function getMetricsListFromSelect( selectData: ISelectConfig, error?: ISyntaxErrorDetails, -): Array<[string, string]> { - const metricsList: Array<[string, string]> = []; +): Array<{ key: string; context: string }> { + const metricsList: Array<{ key: string; context: string }> = []; if (selectData === undefined) { return metricsList; @@ -19,20 +19,10 @@ export default function getMetricsListFromSelect( const metricName = option.value?.option_name ?? ''; const context: string = option.value?.context && Object.keys(option.value?.context).length > 0 - ? Object.keys(option.value?.context) - .map((item) => { - const contextKey = !jsValidVariableRegex.test(item) - ? `['${item}']` - : `${item}`; - const contextValue = (option.value?.context as any)[item]; - return `{${formatValue(contextKey)}: ${formatValue( - contextValue, - )}}`; - }) - .join(', ') + ? formatValue(option.value?.context) : '{}'; - metricsList.push([metricName, context]); + metricsList.push({ key: metricName, context: context }); }); return metricsList; From 7ba8118f19a4303909f58f691b37a6eb60fb7268 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Sat, 6 Apr 2024 00:45:02 +0900 Subject: [PATCH 24/79] Fix experiment header UI refresh issue (#61) --- .../ExperimentBar/ExperimentBar.tsx | 17 ++++-- .../ExperimentSelectionPopover.d.ts | 3 +- .../ExperimentSelectionPopover.tsx | 54 +++++++++++++------ 3 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.tsx b/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.tsx index a1f4ab69..01e7e001 100644 --- a/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.tsx +++ b/src/src/pages/Experiment/components/ExperimentBar/ExperimentBar.tsx @@ -32,6 +32,16 @@ function ExperimentBar({ return name; } + // Selected experiments are the ones that are checked + const [selectedExperiments, setSelectedExperiments] = React.useState< + string[] + >(selectedExperimentNames); + + // Update selected experiments in UI when selectedExperimentNames change + React.useEffect(() => { + setSelectedExperiments(selectedExperimentNames); + }, [selectedExperimentNames]); + return (
@@ -67,14 +77,14 @@ function ExperimentBar({ - {selectedExperimentNames.length === 0 ? ( + {selectedExperiments.length === 0 ? ( Select Experiments ) : null}
- {selectedExperimentNames.map((experimentName) => ( + {selectedExperiments.map((experimentName) => ( void; + setSelectedExperiments: (selectedExperiments: string[]) => void; onSelectExperimentNamesChange: (experimentName: string) => void; onToggleAllExperiments: (experimentNames: string[]) => void; } diff --git a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx index 6cdf5187..746c05fd 100644 --- a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx +++ b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx @@ -23,9 +23,10 @@ import './ExperimentSelectionPopover.scss'; function ExperimentSelectionPopover({ experimentsData, - selectedExperimentNames, + selectedExperiments, isExperimentsLoading, getExperimentsData, + setSelectedExperiments, onSelectExperimentNamesChange, onToggleAllExperiments, }: IExperimentSelectionPopoverProps): React.FunctionComponentElement { @@ -36,16 +37,15 @@ function ExperimentSelectionPopover({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Add forceUpdate to force re-render (to update selected experiments) - const [, updateState] = React.useState<{}>(); - const forceUpdate = React.useCallback(() => updateState({}), []); - const [searchValue, setSearchValue] = React.useState(''); const [isRegexSearch, setIsRegexSearch] = React.useState(false); const [invalidRegex, setInvalidRegex] = React.useState(false); + + // Visible experiments are the ones that match the search query const [visibleExperiments, setVisibleExperiments] = React.useState< IExperimentData[] >([]); + const [selectedNamespace, setSelectedNamespace] = React.useState(''); React.useEffect(() => { namespacesService.fetchCurrentNamespacePath().then((data) => { @@ -68,9 +68,17 @@ function ExperimentSelectionPopover({ } function handleExperimentClick(experimentName: string): void { + // Update model onSelectExperimentNamesChange(experimentName); - // TODO: Figure out a better way to re-render - forceUpdate(); + + // Update view + if (selectedExperiments.includes(experimentName)) { + setSelectedExperiments( + selectedExperiments.filter((name) => name !== experimentName), + ); + } else { + setSelectedExperiments([...selectedExperiments, experimentName]); + } } function experimentInList( @@ -120,12 +128,27 @@ function ExperimentSelectionPopover({ // If all experiments are selected, deselect all // otherwise, select all that are unselected if (checked) { + // Update model onToggleAllExperiments(visibleExperimentNames); + + // Update view + setSelectedExperiments( + selectedExperiments.filter( + (experimentName) => !visibleExperimentNames.includes(experimentName), + ), + ); } else { + // Update model const unselectedExperiments = visibleExperimentNames?.filter( - (experimentName) => !selectedExperimentNames.includes(experimentName), + (experimentName) => !selectedExperiments.includes(experimentName), ); onToggleAllExperiments(unselectedExperiments); + + // Update view + setSelectedExperiments([ + ...selectedExperiments, + ...unselectedExperiments, + ]); } } @@ -133,7 +156,7 @@ function ExperimentSelectionPopover({ return ( visibleExperiments.length > 0 && visibleExperiments.every((experiment) => - selectedExperimentNames.includes(experiment.name), + selectedExperiments.includes(experiment.name), ) ); } @@ -212,7 +235,7 @@ function ExperimentSelectionPopover({ className={classNames('experimentBox', { selected: experimentInList( experiment.name, - selectedExperimentNames, + selectedExperiments, ), })} > @@ -223,7 +246,7 @@ function ExperimentSelectionPopover({ checkedIcon={} checked={experimentInList( experiment.name, - selectedExperimentNames, + selectedExperiments, )} size='small' className='experimentBox__checkbox' @@ -233,10 +256,7 @@ function ExperimentSelectionPopover({ Date: Mon, 8 Apr 2024 13:50:03 -0700 Subject: [PATCH 25/79] Project Params Hotfix (#62) * Hotfix: Add support for experimentNames in getProjectParams * Fetch project params on selectedExperiment change --- src/src/services/api/projects/projectsService.ts | 13 ++++++++----- .../services/models/explorer/metricsModelMethods.ts | 4 +++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/src/services/api/projects/projectsService.ts b/src/src/services/api/projects/projectsService.ts index 5aca1cd6..dde2118d 100644 --- a/src/src/services/api/projects/projectsService.ts +++ b/src/src/services/api/projects/projectsService.ts @@ -24,14 +24,17 @@ function fetchActivityData(): IApiRequest { function getProjectParams( sequences: string[] = ['metric'], + selectedExperimentNames: string[] = [], ): IApiRequest { - const query = sequences.reduce( - (acc: string, sequence: string, index: number) => { + const query = + sequences.reduce((acc: string, sequence: string, index: number) => { acc += `${index === 0 ? '?' : '&'}sequence=${sequence}`; return acc; - }, - '', - ); + }, '') + + selectedExperimentNames.reduce((acc: string, experimentName: string) => { + acc += `&experiment_names=${experimentName}`; + return acc; + }, ''); return API.get(endpoints.GET_PROJECTS_PARAMS + query); } diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index b64b23f2..e9c560dc 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -223,8 +223,10 @@ function getMetricsAppModelMethods( } function fetchProjectParamsAndUpdateState() { + const selectedExperimentNames = + model.getState()?.config?.select.selectedExperimentNames; projectsService - .getProjectParams(['metric']) + .getProjectParams(['metric'], selectedExperimentNames) .call() .then((data) => { const advancedSuggestions: Record = getAdvancedSuggestion( From d890342e71593eed08ec5629916376d3d4b4d0d6 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:33:19 +0200 Subject: [PATCH 26/79] Do not call projects/params endpoint if no experiment is selected (#63) --- src/src/services/api/projects/projectsService.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/src/services/api/projects/projectsService.ts b/src/src/services/api/projects/projectsService.ts index dde2118d..b66a556b 100644 --- a/src/src/services/api/projects/projectsService.ts +++ b/src/src/services/api/projects/projectsService.ts @@ -26,6 +26,22 @@ function getProjectParams( sequences: string[] = ['metric'], selectedExperimentNames: string[] = [], ): IApiRequest { + if (selectedExperimentNames.length === 0) { + const controller = new AbortController(); + return { + call: () => + new Promise((resolve: (data: IProjectParamsMetrics) => any) => { + // Simulating an empty response + const data: IProjectParamsMetrics = { + metric: {}, + images: {}, + params: {}, + }; + resolve(data); + }), + abort: () => controller.abort(), + }; + } const query = sequences.reduce((acc: string, sequence: string, index: number) => { acc += `${index === 0 ? '?' : '&'}sequence=${sequence}`; From 74eae2707376b9adf071f409ffdabc65a6985ac8 Mon Sep 17 00:00:00 2001 From: Shugaba Wuta <57352948+Shugaba-Wuta@users.noreply.github.com> Date: Wed, 10 Apr 2024 15:11:41 +0100 Subject: [PATCH 27/79] feat removed tags page from ui-aim (#59) --- src/src/routes/routes.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/src/routes/routes.tsx b/src/src/routes/routes.tsx index ac46be9a..514eefee 100644 --- a/src/src/routes/routes.tsx +++ b/src/src/routes/routes.tsx @@ -28,9 +28,9 @@ const Bookmarks = React.lazy( const Dashboard = React.lazy( () => import(/* webpackChunkName: "dashboard" */ 'pages/Dashboard/Dashboard'), ); -const TagsContainer = React.lazy( - () => import(/* webpackChunkName: "tags" */ 'pages/Tags/TagsContainer'), -); +// const TagsContainer = React.lazy( +// () => import(/* webpackChunkName: "tags" */ 'pages/Tags/TagsContainer'), +// ); const Scatters = React.lazy( () => import( @@ -192,15 +192,15 @@ const routes = { isExact: true, title: pageTitlesEnum.BOOKMARKS, }, - TAGS: { - path: PathEnum.Tags, - component: TagsContainer, - showInSidebar: true, - displayName: 'Tags', - icon: 'tags', - isExact: true, - title: pageTitlesEnum.TAGS, - }, + // TAGS: { + // path: PathEnum.Tags, + // component: TagsContainer, + // showInSidebar: true, + // displayName: 'Tags', + // icon: 'tags', + // isExact: true, + // title: pageTitlesEnum.TAGS, + // }, RUN_DETAIL: { path: PathEnum.Run_Detail, component: RunDetail, From 772586e7cda441b8351a27129d0cec395cca8e9e Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:23:38 +0200 Subject: [PATCH 28/79] fetch params on initialize and when experiment selection changes (#66) --- .../models/explorer/paramsModelMethods.ts | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/src/services/models/explorer/paramsModelMethods.ts b/src/src/services/models/explorer/paramsModelMethods.ts index 2e1b9422..819ce0cd 100644 --- a/src/src/services/models/explorer/paramsModelMethods.ts +++ b/src/src/services/models/explorer/paramsModelMethods.ts @@ -167,21 +167,12 @@ function getParamsModelMethods( chartPanelRef: { current: null }, }; } - projectsService - .getProjectParams(['metric']) - .call() - .then((data) => { - model.setState({ - selectFormData: { - options: getSelectOptions(data), - suggestions: getSuggestionsByExplorer(appName, data), - }, - }); - }); model.setState({ ...state }); if (!appId) { setModelDefaultAppConfigData(); } + fetchProjectParamsAndUpdateState(); + const liveUpdateState = model.getState()?.config?.liveUpdate; if (liveUpdateState?.enabled) { @@ -193,6 +184,22 @@ function getParamsModelMethods( } } + function fetchProjectParamsAndUpdateState() { + const selectedExperimentNames = + model.getState()?.config?.select?.selectedExperimentNames; + projectsService + .getProjectParams(['metric'], selectedExperimentNames) + .call() + .then((data) => { + model.setState({ + selectFormData: { + options: getSelectOptions(data), + suggestions: getSuggestionsByExplorer(appName, data), + }, + }); + }); + } + function updateData(newData: IRun[]): void { const configData = model.getState()?.config; if (configData) { @@ -1569,6 +1576,7 @@ function getParamsModelMethods( onSelectExperimentNamesChange(experimentName: string): void { // Handle experiment change, then re-fetch params data onSelectExperimentNamesChange({ experimentName, model }); + fetchProjectParamsAndUpdateState(); getParamsData(true, true).call(); }, onToggleAllExperiments(experimentNames: string[]): void { From 0f81e87e5476f51884ec75ced17883195eda163b Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Mon, 15 Apr 2024 06:44:12 -0700 Subject: [PATCH 29/79] Fix projects/params poll (#67) --- src/src/pages/Metrics/MetricsContainer.tsx | 7 +++++++ src/src/pages/Params/ParamsContainer.tsx | 7 +++++++ src/src/services/models/explorer/metricsModelMethods.ts | 2 +- src/src/services/models/explorer/paramsModelMethods.ts | 1 + 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/src/pages/Metrics/MetricsContainer.tsx b/src/src/pages/Metrics/MetricsContainer.tsx index c6fa8725..3ba4ebfb 100644 --- a/src/src/pages/Metrics/MetricsContainer.tsx +++ b/src/src/pages/Metrics/MetricsContainer.tsx @@ -62,6 +62,13 @@ function MetricsContainer(): React.FunctionComponentElement { } }, [metricsData?.rawData]); + React.useEffect(() => { + const pollingTimer = setInterval(() => { + metricAppModel.fetchProjectParamsAndUpdateState(); + }, 30000); + return () => clearInterval(pollingTimer); + }, []); + React.useEffect(() => { metricAppModel.initialize(route.params.appId); let appRequestRef: IApiRequest; diff --git a/src/src/pages/Params/ParamsContainer.tsx b/src/src/pages/Params/ParamsContainer.tsx index 4c004f4b..d888cc9c 100644 --- a/src/src/pages/Params/ParamsContainer.tsx +++ b/src/src/pages/Params/ParamsContainer.tsx @@ -64,6 +64,13 @@ function ParamsContainer(): React.FunctionComponentElement { } }, [paramsData?.rawData]); + React.useEffect(() => { + const pollingTimer = setInterval(() => { + paramsAppModel.fetchProjectParamsAndUpdateState(); + }, 30000); + return () => clearInterval(pollingTimer); + }, []); + React.useEffect(() => { paramsAppModel.initialize(route.params.appId); let appRequestRef: IApiRequest; diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index e9c560dc..c7ec1ac8 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -209,7 +209,6 @@ function getMetricsAppModelMethods( } // fetch project params now and update every 30s fetchProjectParamsAndUpdateState(); - setInterval(fetchProjectParamsAndUpdateState, 30000); const liveUpdateState = model.getState()?.config?.liveUpdate; @@ -1583,6 +1582,7 @@ function getMetricsAppModelMethods( destroy, deleteRuns, archiveRuns, + fetchProjectParamsAndUpdateState, }; if (grouping) { diff --git a/src/src/services/models/explorer/paramsModelMethods.ts b/src/src/services/models/explorer/paramsModelMethods.ts index 819ce0cd..db10b2c2 100644 --- a/src/src/services/models/explorer/paramsModelMethods.ts +++ b/src/src/services/models/explorer/paramsModelMethods.ts @@ -1485,6 +1485,7 @@ function getParamsModelMethods( onShuffleChange, deleteRuns, archiveRuns, + fetchProjectParamsAndUpdateState, }; if (grouping) { From 994564ffbb61dfe3b6661c44d9308c9bf56b11e0 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:53:14 +0200 Subject: [PATCH 30/79] Implement virtualization for Metric Autocomplete (#64) --- .../SelectFormPopper/SelectFormPopper.tsx | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/src/components/SelectFormPopper/SelectFormPopper.tsx b/src/src/components/SelectFormPopper/SelectFormPopper.tsx index 2fa26c31..3b6d111a 100644 --- a/src/src/components/SelectFormPopper/SelectFormPopper.tsx +++ b/src/src/components/SelectFormPopper/SelectFormPopper.tsx @@ -1,6 +1,7 @@ // @ts-nocheck /* eslint-disable react/prop-types */ import React from 'react'; +import { VariableSizeList as List } from 'react-window'; import ToggleButton from '@material-ui/lab/ToggleButton'; import { @@ -19,6 +20,51 @@ import { Text } from 'components/kit'; import { ISelectOption } from 'types/services/models/explorer/createAppModel'; +const Row = React.forwardRef(({ index, children, setSize }, ref) => { + const rowRef = React.useRef(); + React.useEffect(() => { + setSize(index, rowRef.current.getBoundingClientRect().height); + }, [setSize, index]); + return
{children[index]}
; +}); + +const ListboxComponent = React.forwardRef(function ListboxComponent( + props, + ref, +) { + const { children, role, ...other } = props; + const itemCount = Array.isArray(children) ? children.length : 0; + const listRef = React.useRef(); + const sizeMap = React.useRef({}); + const setSize = React.useCallback((index, size) => { + sizeMap.current = { ...sizeMap.current, [index]: size }; + listRef.current.resetAfterIndex(index); + }, []); + const getSize = (index) => { + return sizeMap.current[index] || 50; + }; + + return ( +
+ + {({ index, style }) => ( +
+ + {children} + +
+ )} +
+
+ ); +}); + const SelectFormPopper: React.FC = ({ id, open, @@ -68,9 +114,11 @@ const SelectFormPopper: React.FC = ({ disableClearable={true} ListboxProps={{ style: { - height: 400, + maxHeight: 400, }, }} + disableListWrap + ListboxComponent={ListboxComponent} renderInput={(params) => (
From 2c3fd2dc4e78c306781f61cceb6813c9f85276f2 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Wed, 17 Apr 2024 06:55:20 -0700 Subject: [PATCH 31/79] Use the displayed experiments for requests in every tab (#68) --- src/src/pages/Metrics/MetricsContainer.tsx | 1 + src/src/pages/Params/ParamsContainer.tsx | 1 + src/src/pages/Scatters/ScattersContainer.tsx | 8 +++++++ src/src/services/models/explorer/config.ts | 2 -- .../models/explorer/metricsModelMethods.ts | 6 ++--- .../models/explorer/paramsModelMethods.ts | 5 ++-- .../models/explorer/scattersModelMethods.ts | 23 +++++++++++-------- .../app/onSelectExperimentNamesChange.ts | 11 --------- 8 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/src/pages/Metrics/MetricsContainer.tsx b/src/src/pages/Metrics/MetricsContainer.tsx index 3ba4ebfb..1c5c14c4 100644 --- a/src/src/pages/Metrics/MetricsContainer.tsx +++ b/src/src/pages/Metrics/MetricsContainer.tsx @@ -63,6 +63,7 @@ function MetricsContainer(): React.FunctionComponentElement { }, [metricsData?.rawData]); React.useEffect(() => { + metricAppModel.fetchProjectParamsAndUpdateState(); const pollingTimer = setInterval(() => { metricAppModel.fetchProjectParamsAndUpdateState(); }, 30000); diff --git a/src/src/pages/Params/ParamsContainer.tsx b/src/src/pages/Params/ParamsContainer.tsx index d888cc9c..1f7f5fce 100644 --- a/src/src/pages/Params/ParamsContainer.tsx +++ b/src/src/pages/Params/ParamsContainer.tsx @@ -65,6 +65,7 @@ function ParamsContainer(): React.FunctionComponentElement { }, [paramsData?.rawData]); React.useEffect(() => { + paramsAppModel.fetchProjectParamsAndUpdateState(); const pollingTimer = setInterval(() => { paramsAppModel.fetchProjectParamsAndUpdateState(); }, 30000); diff --git a/src/src/pages/Scatters/ScattersContainer.tsx b/src/src/pages/Scatters/ScattersContainer.tsx index 18e43ef6..55df57bb 100644 --- a/src/src/pages/Scatters/ScattersContainer.tsx +++ b/src/src/pages/Scatters/ScattersContainer.tsx @@ -54,6 +54,14 @@ function ScattersContainer(): React.FunctionComponentElement { } }, chartElemRef); + React.useEffect(() => { + scattersAppModel.fetchProjectParamsAndUpdateState(); + const pollingTimer = setInterval(() => { + scattersAppModel.fetchProjectParamsAndUpdateState(); + }, 30000); + return () => clearInterval(pollingTimer); + }, []); + React.useEffect(() => { if (tableRef.current && chartPanelRef.current) { setComponentRefs({ diff --git a/src/src/services/models/explorer/config.ts b/src/src/services/models/explorer/config.ts index 199f1880..2a3620c4 100644 --- a/src/src/services/models/explorer/config.ts +++ b/src/src/services/models/explorer/config.ts @@ -160,7 +160,6 @@ function initializeAppModel(appConfig: IAppInitialConfig): InitialAppModelType { query: '', advancedMode: false, advancedQuery: '', - selectedExperimentNames: [], }; } return config; @@ -282,7 +281,6 @@ function initializeAppModel(appConfig: IAppInitialConfig): InitialAppModelType { query: '', advancedMode: false, advancedQuery: '', - selectedExperimentNames: [], }; } //TODO solve the problem with keeping table config after switching from Scatters explore to Params explore. But the solution is temporal diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index c7ec1ac8..388eba11 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -152,6 +152,7 @@ import { onCopyToClipBoard } from 'utils/onCopyToClipBoard'; import saveRecentSearches from 'utils/saveRecentSearches'; import getLegendsData from 'utils/app/getLegendsData'; import onLegendsChange from 'utils/app/onLegendsChange'; +import { getSelectedExperimentNames } from 'utils/app/getSelectedExperimentNames'; import { InitialAppModelType } from './config'; @@ -207,8 +208,6 @@ function getMetricsAppModelMethods( if (!appId) { setModelDefaultAppConfigData(); } - // fetch project params now and update every 30s - fetchProjectParamsAndUpdateState(); const liveUpdateState = model.getState()?.config?.liveUpdate; @@ -222,8 +221,7 @@ function getMetricsAppModelMethods( } function fetchProjectParamsAndUpdateState() { - const selectedExperimentNames = - model.getState()?.config?.select.selectedExperimentNames; + const selectedExperimentNames = getSelectedExperimentNames(); projectsService .getProjectParams(['metric'], selectedExperimentNames) .call() diff --git a/src/src/services/models/explorer/paramsModelMethods.ts b/src/src/services/models/explorer/paramsModelMethods.ts index db10b2c2..03333248 100644 --- a/src/src/services/models/explorer/paramsModelMethods.ts +++ b/src/src/services/models/explorer/paramsModelMethods.ts @@ -114,6 +114,7 @@ import onRowsVisibilityChange from 'utils/app/onRowsVisibilityChange'; import { getMetricsInitialRowData } from 'utils/app/getMetricsInitialRowData'; import { getMetricHash } from 'utils/app/getMetricHash'; import { getMetricLabel } from 'utils/app/getMetricLabel'; +import { getSelectedExperimentNames } from 'utils/app/getSelectedExperimentNames'; import { InitialAppModelType } from './config'; @@ -171,7 +172,6 @@ function getParamsModelMethods( if (!appId) { setModelDefaultAppConfigData(); } - fetchProjectParamsAndUpdateState(); const liveUpdateState = model.getState()?.config?.liveUpdate; @@ -185,8 +185,7 @@ function getParamsModelMethods( } function fetchProjectParamsAndUpdateState() { - const selectedExperimentNames = - model.getState()?.config?.select?.selectedExperimentNames; + const selectedExperimentNames = getSelectedExperimentNames(); projectsService .getProjectParams(['metric'], selectedExperimentNames) .call() diff --git a/src/src/services/models/explorer/scattersModelMethods.ts b/src/src/services/models/explorer/scattersModelMethods.ts index 92172a83..17450de5 100644 --- a/src/src/services/models/explorer/scattersModelMethods.ts +++ b/src/src/services/models/explorer/scattersModelMethods.ts @@ -118,6 +118,7 @@ import onRowsVisibilityChange from 'utils/app/onRowsVisibilityChange'; import { getMetricsInitialRowData } from 'utils/app/getMetricsInitialRowData'; import { getMetricHash } from 'utils/app/getMetricHash'; import { getMetricLabel } from 'utils/app/getMetricLabel'; +import { getSelectedExperimentNames } from 'utils/app/getSelectedExperimentNames'; import { InitialAppModelType } from './config'; @@ -176,9 +177,19 @@ function getScattersModelMethods( setModelDefaultAppConfigData(); } const liveUpdateState = model.getState()?.config?.liveUpdate; + if (liveUpdateState?.enabled) { + liveUpdateInstance = new LiveUpdateService( + appName, + updateData, + liveUpdateState.delay, + ); + } + } + function fetchProjectParamsAndUpdateState() { + const selectedExperimentNames = getSelectedExperimentNames(); projectsService - .getProjectParams(['metric']) + .getProjectParams(['metric'], selectedExperimentNames) .call() .then((data: IProjectParamsMetrics) => { model.setState({ @@ -188,14 +199,6 @@ function getScattersModelMethods( }, }); }); - - if (liveUpdateState?.enabled) { - liveUpdateInstance = new LiveUpdateService( - appName, - updateData, - liveUpdateState.delay, - ); - } } function updateData(newData: IRun[]): void { @@ -1448,6 +1451,7 @@ function getScattersModelMethods( changeLiveUpdateConfig, archiveRuns, deleteRuns, + fetchProjectParamsAndUpdateState, }; if (grouping) { @@ -1500,6 +1504,7 @@ function getScattersModelMethods( onSelectExperimentNamesChange(experimentName: string): void { // Handle experiment change, then re-fetch scatters data onSelectExperimentNamesChange({ experimentName, model }); + fetchProjectParamsAndUpdateState(); getScattersData(true, true).call(); }, onToggleAllExperiments(experimentNames: string[]): void { diff --git a/src/src/utils/app/onSelectExperimentNamesChange.ts b/src/src/utils/app/onSelectExperimentNamesChange.ts index 9037f6b5..6ab305fc 100644 --- a/src/src/utils/app/onSelectExperimentNamesChange.ts +++ b/src/src/utils/app/onSelectExperimentNamesChange.ts @@ -29,15 +29,4 @@ export default function onSelectExperimentNamesChange({ } else { setItem('selectedExperimentNames', selectedExperimentNames); } - - if (configData?.select) { - const newConfig = { - ...configData, - select: { - ...configData.select, - selectedExperimentNames, - }, - }; - model.setState({ config: newConfig }); - } } From 76021af185ed0039ab5fdefbaaeb8ad4b66b9cf8 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:00:09 +0200 Subject: [PATCH 32/79] Fetch projects/params when select/deselect all in checked on the experiment --- src/src/services/models/explorer/metricsModelMethods.ts | 1 + src/src/services/models/explorer/paramsModelMethods.ts | 1 + src/src/services/models/explorer/scattersModelMethods.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index 388eba11..af814e59 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -1652,6 +1652,7 @@ function getMetricsAppModelMethods( }, onToggleAllExperiments(experimentNames: string[]): void { onToggleAllExperiments({ experimentNames, model }); + fetchProjectParamsAndUpdateState(); getMetricsData(true, true).call(); }, onSelectRunQueryChange(query: string): void { diff --git a/src/src/services/models/explorer/paramsModelMethods.ts b/src/src/services/models/explorer/paramsModelMethods.ts index 03333248..cb8dba5c 100644 --- a/src/src/services/models/explorer/paramsModelMethods.ts +++ b/src/src/services/models/explorer/paramsModelMethods.ts @@ -1581,6 +1581,7 @@ function getParamsModelMethods( }, onToggleAllExperiments(experimentNames: string[]): void { onToggleAllExperiments({ experimentNames, model }); + fetchProjectParamsAndUpdateState(); getParamsData(true, true).call(); }, onSelectRunQueryChange(query: string): void { diff --git a/src/src/services/models/explorer/scattersModelMethods.ts b/src/src/services/models/explorer/scattersModelMethods.ts index 17450de5..3de5d991 100644 --- a/src/src/services/models/explorer/scattersModelMethods.ts +++ b/src/src/services/models/explorer/scattersModelMethods.ts @@ -1509,6 +1509,7 @@ function getScattersModelMethods( }, onToggleAllExperiments(experimentNames: string[]): void { onToggleAllExperiments({ experimentNames, model }); + fetchProjectParamsAndUpdateState(); getScattersData(true, true).call(); }, onSelectRunQueryChange(query: string): void { From acf4aea9489c53c58a2ab131dcbbff059ac8d1f3 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Wed, 17 Apr 2024 08:53:17 -0700 Subject: [PATCH 33/79] Run Selector UX Improvements (#60) * Flip metrics and run query positions * Add scrollbar to selected metrics * Add scrollbar to selected params and improve layout --------- Co-authored-by: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> --- src/src/pages/Metrics/Metrics.scss | 1 - .../components/SelectForm/SelectForm.scss | 34 ++-- .../components/SelectForm/SelectForm.tsx | 191 +++++++++--------- .../components/SelectForm/SelectForm.scss | 22 +- .../components/SelectForm/SelectForm.tsx | 42 ++-- 5 files changed, 152 insertions(+), 138 deletions(-) diff --git a/src/src/pages/Metrics/Metrics.scss b/src/src/pages/Metrics/Metrics.scss index 94ad5cca..16859aab 100644 --- a/src/src/pages/Metrics/Metrics.scss +++ b/src/src/pages/Metrics/Metrics.scss @@ -60,7 +60,6 @@ display: flex; border-bottom: $border-main; min-height: 6rem; - max-height: 6rem; } .Metrics__table__aggregationColumn__cell { diff --git a/src/src/pages/Metrics/components/SelectForm/SelectForm.scss b/src/src/pages/Metrics/components/SelectForm/SelectForm.scss index 1e15e8dc..1d0b1756 100644 --- a/src/src/pages/Metrics/components/SelectForm/SelectForm.scss +++ b/src/src/pages/Metrics/components/SelectForm/SelectForm.scss @@ -11,16 +11,22 @@ flex-direction: column; justify-content: space-between; padding-right: 1rem; - height: 4.5rem; + min-height: 4.5rem; /* TODO [GA]: Override MUI default styles in a nice way */ .MuiBox-root { justify-content: flex-start; } } + + &__metricsButton { + margin-top: $space-xs; + margin-bottom: $space-xs; + } + &__container__search { width: 103px; display: flex; - justify-content: space-between; + justify-content: baseline; flex-direction: column; } &__Popper { @@ -46,6 +52,7 @@ } &__TextField { position: relative; + width: 100%; } &__textarea { flex: 1; @@ -56,6 +63,7 @@ } &__search__button { width: 100%; + margin-top: $space-xs; } &__search__actions { display: flex; @@ -77,11 +85,11 @@ width: 24px; background: #e8f1fc; border-radius: 6px; - display: flex; align-items: center; justify-content: center; cursor: pointer; position: relative; + display: inline-flex; &.disabled { opacity: 0.5; } @@ -90,12 +98,6 @@ position: absolute; width: 20px; height: 34px; - background: rgba(255, 255, 255, 0.5); - background: linear-gradient( - 90deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 1) 92% - ); left: -20px; top: -5px; border-radius: 0; @@ -109,14 +111,20 @@ } } &__tags { - display: flex; align-items: center; - overflow: auto; - max-width: calc(100vw - 40rem); min-width: 12rem; + padding-top: 1px; + max-height: 3.2rem; + display: block; + + overflow-y: scroll; + scrollbar-color: auto; + scrollbar-width: thin; + overflow-x: hidden; .Badge { - margin-right: 0.5rem; + margin-right: $space-xxxs; + margin-bottom: 1px; &:last-child { margin-right: 20px; } diff --git a/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx b/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx index 08edab2a..afdbd1ac 100644 --- a/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx +++ b/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx @@ -152,106 +152,95 @@ function SelectForm({ justifyContent='space-between' alignItems='center' > - - - - - {selectedMetricsData?.options.length === 0 && ( - - No metrics are selected - - )} - - {selectedMetricsData?.options?.map((tag: ISelectOption) => { - return ( - - ); - })} - - - {selectedMetricsData?.options && - selectedMetricsData.options.length > 1 && ( - - )} +
-
- + + -
+ + {selectedMetricsData?.options.length === 0 && ( + + No metrics are selected + + )} + + {selectedMetricsData?.options?.map((tag: ISelectOption) => { + return ( + + ); + })} + {selectedMetricsData?.options && + selectedMetricsData.options.length > 1 && ( + + )} + +
-
@@ -278,6 +267,22 @@ function SelectForm({
+
diff --git a/src/src/pages/Params/components/SelectForm/SelectForm.scss b/src/src/pages/Params/components/SelectForm/SelectForm.scss index dc49836f..04c16797 100644 --- a/src/src/pages/Params/components/SelectForm/SelectForm.scss +++ b/src/src/pages/Params/components/SelectForm/SelectForm.scss @@ -18,14 +18,18 @@ } .SelectForm__tags { - display: flex; align-items: center; - overflow: auto; - max-width: calc(100vw - 40rem); - min-width: 15rem; + min-width: 12rem; + max-height: 3.2rem; + + overflow-y: scroll; + scrollbar-color: auto; + scrollbar-width: thin; + overflow-x: hidden; .Badge { - margin-right: 0.5rem; + margin-right: $space-xxxs; + margin-bottom: 2px; &:last-child { margin-right: 20px; } @@ -78,11 +82,11 @@ width: 24px; background: #e8f1fc; border-radius: 6px; - display: flex; align-items: center; justify-content: center; cursor: pointer; position: relative; + display: inline-flex; &.disabled { opacity: 0.5; } @@ -91,12 +95,6 @@ position: absolute; width: 20px; height: 34px; - background: rgba(255, 255, 255, 0.5); - background: linear-gradient( - 90deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 1) 92% - ); left: -20px; top: -5px; border-radius: 0; diff --git a/src/src/pages/Params/components/SelectForm/SelectForm.tsx b/src/src/pages/Params/components/SelectForm/SelectForm.tsx index fea7a3b8..34c20185 100644 --- a/src/src/pages/Params/components/SelectForm/SelectForm.tsx +++ b/src/src/pages/Params/components/SelectForm/SelectForm.tsx @@ -143,7 +143,7 @@ function SelectForm({
- + - + + + )} )} - {selectedParamsData?.options && - selectedParamsData.options.length > 1 && ( - - - - )} - {selectedExperiments.length === 0 ? ( + {selectedExperimentsState.length === 0 ? ( Select Experiments ) : null}
- {selectedExperiments.map((experimentName) => ( + {selectedExperimentsState.map((experiment) => ( - onSelectExperimentNamesChange(experimentName) + onSelectExperimentsChange(experiment) } /> @@ -119,10 +120,10 @@ function ExperimentBar({ } diff --git a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.d.ts b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.d.ts index fdfd4300..ca66c159 100644 --- a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.d.ts +++ b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.d.ts @@ -1,11 +1,14 @@ -import { IExperimentData } from 'modules/core/api/experimentsApi/types'; +import { + IExperimentData, + IExperimentDataShort, +} from 'modules/core/api/experimentsApi/types'; export interface IExperimentSelectionPopoverProps { experimentsData: IExperimentData[] | null; - selectedExperiments: string[]; + selectedExperiments: IExperimentDataShort[]; isExperimentsLoading: boolean; getExperimentsData: () => void; - setSelectedExperiments: (selectedExperiments: string[]) => void; - onSelectExperimentNamesChange: (experimentName: string) => void; - onToggleAllExperiments: (experimentNames: string[]) => void; + setSelectedExperiments: (selectedExperiments: IExperimentDataShort[]) => void; + onSelectExperimentsChange: (experiment: IExperimentDataShort) => void; + onToggleAllExperiments: (experiments: IExperimentDataShort[]) => void; } diff --git a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx index 746c05fd..93f648c2 100644 --- a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx +++ b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx @@ -13,7 +13,10 @@ import { Icon, Spinner, Text } from 'components/kit'; import { DATE_WITH_SECONDS } from 'config/dates/dates'; import { getBaseHost } from 'config/config'; -import { IExperimentData } from 'modules/core/api/experimentsApi'; +import { + IExperimentData, + IExperimentDataShort, +} from 'modules/core/api/experimentsApi'; import namespacesService from 'services/api/namespaces/namespacesService'; @@ -27,7 +30,7 @@ function ExperimentSelectionPopover({ isExperimentsLoading, getExperimentsData, setSelectedExperiments, - onSelectExperimentNamesChange, + onSelectExperimentsChange, onToggleAllExperiments, }: IExperimentSelectionPopoverProps): React.FunctionComponentElement { React.useEffect(() => { @@ -67,25 +70,24 @@ function ExperimentSelectionPopover({ return name; } - function handleExperimentClick(experimentName: string): void { + function handleExperimentClick(experiment: IExperimentDataShort): void { // Update model - onSelectExperimentNamesChange(experimentName); + onSelectExperimentsChange(experiment); // Update view - if (selectedExperiments.includes(experimentName)) { + if (selectedExperiments.some((e) => e.id === experiment.id)) { setSelectedExperiments( - selectedExperiments.filter((name) => name !== experimentName), + selectedExperiments.filter((e) => e.name !== experiment.name), ); } else { - setSelectedExperiments([...selectedExperiments, experimentName]); + setSelectedExperiments([...selectedExperiments, experiment]); } } - function experimentInList( - experimentName: string, - selectedExperimentNames: string[], - ): boolean { - return selectedExperimentNames.includes(experimentName); + function experimentIsSelected(experiment: IExperimentDataShort): boolean { + return selectedExperiments.some( + (selectedExperiment) => selectedExperiment.id === experiment.id, + ); } function handleSearchInputChange( @@ -122,25 +124,24 @@ function ExperimentSelectionPopover({ } function toggleAllExperiments(checked: boolean): void { - const visibleExperimentNames = visibleExperiments?.map( - (experiment) => experiment.name, - ); // If all experiments are selected, deselect all // otherwise, select all that are unselected if (checked) { // Update model - onToggleAllExperiments(visibleExperimentNames); + onToggleAllExperiments(visibleExperiments); // Update view setSelectedExperiments( selectedExperiments.filter( - (experimentName) => !visibleExperimentNames.includes(experimentName), + (experiment) => + !visibleExperiments.some((e) => e.id === experiment.id), ), ); } else { // Update model - const unselectedExperiments = visibleExperimentNames?.filter( - (experimentName) => !selectedExperiments.includes(experimentName), + const unselectedExperiments = visibleExperiments?.filter( + (experiment) => + !selectedExperiments.some((e) => e.id === experiment.id), ); onToggleAllExperiments(unselectedExperiments); @@ -153,10 +154,16 @@ function ExperimentSelectionPopover({ } function allExperimentsSelected(): boolean { + // Create a set of selected experiment IDs + const selectedExperimentIds = new Set( + selectedExperiments.map((experiment) => experiment.id), + ); + + // Check if all visible experiments are selected by their IDs return ( visibleExperiments.length > 0 && visibleExperiments.every((experiment) => - selectedExperiments.includes(experiment.name), + selectedExperimentIds.has(experiment.id), ) ); } @@ -231,12 +238,9 @@ function ExperimentSelectionPopover({ visibleExperiments?.map((experiment) => (
- -
- -
-
); From 28f3ef8de48d1bf573ba730b3da5f2208cca4d66 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Thu, 2 May 2024 14:49:26 -0700 Subject: [PATCH 40/79] Fix search query enter bug (#75) --- .../AutocompleteInput/AutocompleteInput.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/src/components/AutocompleteInput/AutocompleteInput.tsx b/src/src/components/AutocompleteInput/AutocompleteInput.tsx index 113226ea..e0f139d5 100644 --- a/src/src/components/AutocompleteInput/AutocompleteInput.tsx +++ b/src/src/components/AutocompleteInput/AutocompleteInput.tsx @@ -47,7 +47,21 @@ function AutocompleteInput({ monacoConfig.theme.config, ); monaco.editor.setTheme(monacoConfig.theme.name); + + // Prevent editor from adding spaces on enter after an open parenthesis + monaco.languages.setLanguageConfiguration('python', { + onEnterRules: [ + { + beforeText: new RegExp(/(\s*\(\s*)$/), + action: { + indentAction: monaco.languages.IndentAction.None, + appendText: '', + }, + }, + ], + }); } + const onResize = _.debounce(() => { setContainerWidth(window.innerWidth); }, 500); From 2f6229730ea6ec8f7ec2db925b5a5cc271d344ff Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Fri, 3 May 2024 11:06:26 -0700 Subject: [PATCH 41/79] Table Column Filtering and Performance Improvements (#44) * Add visibility change handler * Add type definitions for column filters * Add internal column filtering logic * Add HideColumnsPopover * Improve table scrolling speeds * Plug new props into each app container * Fix HideColumnsPopover re-rendering problems * Fix Params and Scatters unselected metric filtering --------- Co-authored-by: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> --- src/src/components/Table/Table.tsx | 60 ++++---- src/src/config/analytics/analyticsKeysMap.ts | 8 ++ src/src/config/table/tableConfigs.ts | 10 ++ src/src/pages/Metrics/Metrics.tsx | 4 + src/src/pages/Metrics/MetricsContainer.tsx | 6 + .../HideColumnsPopover.scss | 27 ++++ .../HideColumnsPopover/HideColumnsPopover.tsx | 131 ++++++++++++++++++ src/src/pages/Params/Params.tsx | 6 + src/src/pages/Params/ParamsContainer.tsx | 4 + .../ParamsTableGrid/ParamsTableGrid.tsx | 2 +- src/src/pages/Scatters/Scatters.tsx | 4 + src/src/pages/Scatters/ScattersContainer.tsx | 6 + src/src/services/models/explorer/config.ts | 6 + .../models/explorer/metricsModelMethods.ts | 50 ++++++- .../models/explorer/paramsModelMethods.ts | 60 +++++++- .../models/explorer/runsModelMethods.ts | 14 +- .../models/explorer/scattersModelMethods.ts | 61 +++++++- src/src/types/components/Table/Table.d.ts | 4 +- src/src/types/pages/metrics/Metrics.d.ts | 4 +- src/src/types/pages/params/Params.d.ts | 3 + src/src/types/pages/scatters/Scatters.d.ts | 4 +- .../models/explorer/createAppModel.d.ts | 3 +- .../app/onDefaultColumnsVisibilityChange.ts | 46 ++++++ 23 files changed, 485 insertions(+), 38 deletions(-) create mode 100644 src/src/pages/Metrics/components/Table/HideColumnsPopover/HideColumnsPopover.scss create mode 100644 src/src/pages/Metrics/components/Table/HideColumnsPopover/HideColumnsPopover.tsx create mode 100644 src/src/utils/app/onDefaultColumnsVisibilityChange.ts diff --git a/src/src/components/Table/Table.tsx b/src/src/components/Table/Table.tsx index 964bbd2d..a10dc03f 100644 --- a/src/src/components/Table/Table.tsx +++ b/src/src/components/Table/Table.tsx @@ -2,7 +2,7 @@ /* eslint-disable react/prop-types */ import React from 'react'; -import { isEmpty, isEqual, isNil } from 'lodash-es'; +import { isEmpty, isEqual, isNil, debounce } from 'lodash-es'; import { useResizeObserver } from 'hooks'; import _ from 'lodash-es'; @@ -27,6 +27,7 @@ import HideRowsPopover from 'pages/Metrics/components/Table/HideRowsPopover/Hide import RowHeightPopover from 'pages/Metrics/components/Table/RowHeightPopover/RowHeightPopover'; import CompareSelectedRunsPopover from 'pages/Metrics/components/Table/CompareSelectedRunsPopover'; import MetricsValueKeyPopover from 'pages/Metrics/components/Table/MetricsValueKeyPopover'; +import HideColumnsPopover from 'pages/Metrics/components/Table/HideColumnsPopover/HideColumnsPopover'; import { ITableProps } from 'types/components/Table/Table'; @@ -54,6 +55,7 @@ const Table = React.forwardRef(function Table( onTableResizeModeChange, onMetricsValueKeyChange, metricsValueKey, + onDefaultColumnsVisibilityChange, custom, data, columns, @@ -74,6 +76,7 @@ const Table = React.forwardRef(function Table( updateColumnsWidths, sortFields, hiddenRows, + unselectedColumnState, isLoading, showRowClickBehaviour = true, showResizeContainerActionBar = true, @@ -382,19 +385,21 @@ const Table = React.forwardRef(function Table( const groupRow = dataRef.current[groupKey]; if (!!groupRow && !!groupRow.data) { if (colKey === 'value') { + const { min, line, max, stdDevValue, stdErrValue } = + groupRow.data.aggregation.area; groupHeaderRowCell.children[0].children[0].children[0].textContent = - groupRow.data.aggregation.area.min; + min; groupHeaderRowCell.children[0].children[0].children[1].textContent = - groupRow.data.aggregation.line; + line; groupHeaderRowCell.children[0].children[0].children[2].textContent = - groupRow.data.aggregation.area.max; + max; if (!isNil(groupRow.data.aggregation.area.stdDevValue)) { groupHeaderRowCell.children[0].children[0].children[3].textContent = - groupRow.data.aggregation.area.stdDevValue; + stdDevValue; } if (!isNil(groupRow.data.aggregation.area.stdErrValue)) { groupHeaderRowCell.children[0].children[0].children[3].textContent = - groupRow.data.aggregation.area.stdErrValue; + stdErrValue; } } else { groupHeaderRowCell.textContent = groupRow.data[colKey]; @@ -607,22 +612,9 @@ const Table = React.forwardRef(function Table( React.useEffect(() => { if (custom && !!tableContainerRef.current) { - const windowEdges = calculateWindow({ - scrollTop: tableContainerRef.current.scrollTop, - offsetHeight: tableContainerRef.current.offsetHeight, - scrollHeight: tableContainerRef.current.scrollHeight, - itemHeight: rowHeight, - groupMargin: - ROW_CELL_SIZE_CONFIG[rowHeight]?.groupMargin ?? - ROW_CELL_SIZE_CONFIG[RowHeightSize.md].groupMargin, - }); - - startIndex.current = windowEdges.startIndex; - endIndex.current = windowEdges.endIndex; - - virtualizedUpdate(); - - tableContainerRef.current.onscroll = ({ target }) => { + // Debounce the scroll event to avoid performance issues + const handleScroll = debounce(() => { + const target = tableContainerRef.current; const windowEdges = calculateWindow({ scrollTop: target.scrollTop, offsetHeight: target.offsetHeight, @@ -654,15 +646,16 @@ const Table = React.forwardRef(function Table( } } setListWindowMeasurements(); + }, 30); + + tableContainerRef.current.addEventListener('scroll', handleScroll); + + return () => { + if (tableContainerRef.current) { + tableContainerRef.current.removeEventListener('scroll', handleScroll); + } }; } - - return () => { - if (custom && tableContainerRef.current) { - // eslint-disable-next-line react-hooks/exhaustive-deps - tableContainerRef.current.onscroll = null; - } - }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [custom, rowData]); @@ -806,6 +799,15 @@ const Table = React.forwardRef(function Table( data={dataRef.current} /> )} + {onDefaultColumnsVisibilityChange && ( + + )} {onSort && ( = { resizeMode: ResizeModeEnum.Resizable, rowHeight: RowHeightSize.md, sortFields: [], + unselectedColumnState: UnselectedColumnState.FORCE_HIDE, hideSystemMetrics: true, hiddenMetrics: [], hiddenColumns: ['hash', 'description'], @@ -60,6 +67,7 @@ export const TABLE_DEFAULT_CONFIG: Record = { resizeMode: ResizeModeEnum.Resizable, rowHeight: RowHeightSize.md, sortFields: [], + unselectedColumnState: UnselectedColumnState.FORCE_HIDE, hiddenMetrics: [], hiddenColumns: ['hash', 'description'], nonHidableColumns: new Set(['#', 'run', 'actions']), @@ -76,6 +84,7 @@ export const TABLE_DEFAULT_CONFIG: Record = { resizeMode: ResizeModeEnum.Resizable, rowHeight: RowHeightSize.md, sortFields: [], + unselectedColumnState: UnselectedColumnState.FORCE_HIDE, hiddenMetrics: [], hiddenColumns: ['hash', 'description'], nonHidableColumns: new Set(['#', 'run', 'actions']), @@ -93,6 +102,7 @@ export const TABLE_DEFAULT_CONFIG: Record = { resizeMode: ResizeModeEnum.Resizable, rowHeight: RowHeightSize.md, sortFields: [], + unselectedColumnState: UnselectedColumnState.FORCE_HIDE, hiddenMetrics: [], hiddenColumns: ['hash', 'description'], nonHidableColumns: new Set(['#', 'run', 'actions']), diff --git a/src/src/pages/Metrics/Metrics.tsx b/src/src/pages/Metrics/Metrics.tsx index 56fb2edd..035433c5 100644 --- a/src/src/pages/Metrics/Metrics.tsx +++ b/src/src/pages/Metrics/Metrics.tsx @@ -259,6 +259,7 @@ function Metrics( resizeMode={props.resizeMode} columnsWidths={props.columnsWidths} selectedRows={props.selectedRows} + unselectedColumnState={props.unselectedColumnState} hideSystemMetrics={props.hideSystemMetrics} appName={AppNameEnum.METRICS} hiddenChartRows={props.lineChartData?.length === 0} @@ -272,6 +273,9 @@ function Metrics( onColumnsVisibilityChange={ props.onColumnsVisibilityChange } + onDefaultColumnsVisibilityChange={ + props.onDefaultColumnsVisibilityChange + } onTableDiffShow={props.onTableDiffShow} onRowHeightChange={props.onRowHeightChange} onRowsChange={props.onMetricVisibilityChange} diff --git a/src/src/pages/Metrics/MetricsContainer.tsx b/src/src/pages/Metrics/MetricsContainer.tsx index 70d93f0b..5df2a791 100644 --- a/src/src/pages/Metrics/MetricsContainer.tsx +++ b/src/src/pages/Metrics/MetricsContainer.tsx @@ -159,6 +159,9 @@ function MetricsContainer(): React.FunctionComponentElement { hiddenMetrics={metricsData?.config?.table?.hiddenMetrics!} hideSystemMetrics={metricsData?.config?.table?.hideSystemMetrics!} hiddenColumns={metricsData?.config?.table?.hiddenColumns!} + unselectedColumnState={ + metricsData?.config?.table?.unselectedColumnState! + } chartPanelOffsetHeight={chartPanelOffsetHeight} selectedRows={metricsData?.selectedRows!} groupingSelectOptions={metricsData?.groupingSelectOptions!} @@ -209,6 +212,9 @@ function MetricsContainer(): React.FunctionComponentElement { onMetricVisibilityChange={metricAppModel.onMetricVisibilityChange} onColumnsOrderChange={metricAppModel.onColumnsOrderChange} onColumnsVisibilityChange={metricAppModel.onColumnsVisibilityChange} + onDefaultColumnsVisibilityChange={ + metricAppModel.onDefaultColumnsVisibilityChange + } onTableDiffShow={metricAppModel.onTableDiffShow} onTableResizeModeChange={metricAppModel.onTableResizeModeChange} onRowsVisibilityChange={metricAppModel.onRowsVisibilityChange} diff --git a/src/src/pages/Metrics/components/Table/HideColumnsPopover/HideColumnsPopover.scss b/src/src/pages/Metrics/components/Table/HideColumnsPopover/HideColumnsPopover.scss new file mode 100644 index 00000000..cb0a3cee --- /dev/null +++ b/src/src/pages/Metrics/components/Table/HideColumnsPopover/HideColumnsPopover.scss @@ -0,0 +1,27 @@ +@use 'src/styles/abstracts' as *; + +.HideColumnsPopover { + padding: 0.5rem; + width: 16.5rem; + &__item { + text-transform: capitalize; + } +} + +.HideColumnsPopover__trigger { + cursor: pointer; + display: flex; + align-items: center; + border: 0.0625rem solid transparent; + transition: all 0.18s ease-out; + border-radius: $radius-main; + padding: 0 0.375rem !important; + margin-right: 0.375rem !important; + i { + margin-right: 0.75rem; + } + &.opened { + border: $border-main-active; + background-color: $primary-color-10; + } +} diff --git a/src/src/pages/Metrics/components/Table/HideColumnsPopover/HideColumnsPopover.tsx b/src/src/pages/Metrics/components/Table/HideColumnsPopover/HideColumnsPopover.tsx new file mode 100644 index 00000000..cba7cf84 --- /dev/null +++ b/src/src/pages/Metrics/components/Table/HideColumnsPopover/HideColumnsPopover.tsx @@ -0,0 +1,131 @@ +import React from 'react'; + +import { MenuItem, Tooltip } from '@material-ui/core'; + +import { Button, Icon, Text } from 'components/kit'; +import ControlPopover from 'components/ControlPopover/ControlPopover'; +import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary'; + +import { UnselectedColumnState } from 'config/table/tableConfigs'; + +import { AppNameEnum } from 'services/models/explorer'; + +import './HideColumnsPopover.scss'; + +function HideColumnsPopover({ + unselectedColumnState, + onDefaultColumnsVisibilityChange, + appName, +}: { + unselectedColumnState: UnselectedColumnState; + onDefaultColumnsVisibilityChange: (value: UnselectedColumnState) => void; + appName: AppNameEnum; +}) { + const columnStateChanged: boolean = React.useMemo(() => { + return unselectedColumnState !== UnselectedColumnState.DEFAULT; + }, [unselectedColumnState]); + + const [unselectedColumnLocal, setUnselectedColumnLocal] = + React.useState(unselectedColumnState); + + // Triggers re-rendering on unselectedColumnState change + React.useEffect(() => { + setUnselectedColumnLocal(unselectedColumnState); + }, [unselectedColumnState]); + + function handleDefaultColumnsVisibilityChange( + value: UnselectedColumnState, + ): void { + onDefaultColumnsVisibilityChange(value); + setUnselectedColumnLocal(value); + } + + return ( + + ( + +
+ +
+
+ )} + component={ +
+ + + handleDefaultColumnsVisibilityChange( + UnselectedColumnState.FORCE_HIDE, + ) + } + > + Always Hide Unselected + + + + + handleDefaultColumnsVisibilityChange( + UnselectedColumnState.FORCE_SHOW, + ) + } + > + Always Show Unselected + + + + + handleDefaultColumnsVisibilityChange( + UnselectedColumnState.DEFAULT, + ) + } + > + Default + + +
+ } + /> +
+ ); +} + +export default React.memo(HideColumnsPopover); diff --git a/src/src/pages/Params/Params.tsx b/src/src/pages/Params/Params.tsx index 7876db40..730a0364 100644 --- a/src/src/pages/Params/Params.tsx +++ b/src/src/pages/Params/Params.tsx @@ -72,6 +72,7 @@ const Params = ({ resizeMode, notifyData, hiddenColumns, + unselectedColumnState, liveUpdateConfig, selectFormData, selectedParams, @@ -97,6 +98,7 @@ const Params = ({ onTableResizeModeChange, onNotificationDelete, onColumnsVisibilityChange, + onDefaultColumnsVisibilityChange, onTableDiffShow, onSortReset, onAxisBrushExtentChange, @@ -327,6 +329,7 @@ const Params = ({ hiddenRows={hiddenMetrics} hiddenColumns={hiddenColumns} hideSystemMetrics={hideSystemMetrics} + unselectedColumnState={unselectedColumnState} resizeMode={resizeMode} columnsWidths={columnsWidths} selectedRows={selectedRows} @@ -339,6 +342,9 @@ const Params = ({ onSort={onSortFieldsChange} onExport={onExportTableData} onColumnsVisibilityChange={onColumnsVisibilityChange} + onDefaultColumnsVisibilityChange={ + onDefaultColumnsVisibilityChange + } onManageColumns={onColumnsOrderChange} onRowHeightChange={onRowHeightChange} onRowsChange={onParamVisibilityChange} diff --git a/src/src/pages/Params/ParamsContainer.tsx b/src/src/pages/Params/ParamsContainer.tsx index be30c2cb..85afa293 100644 --- a/src/src/pages/Params/ParamsContainer.tsx +++ b/src/src/pages/Params/ParamsContainer.tsx @@ -155,6 +155,7 @@ function ParamsContainer(): React.FunctionComponentElement { sortOptions={paramsData?.sortOptions!} hiddenColumns={paramsData?.config?.table?.hiddenColumns!} hideSystemMetrics={paramsData?.config?.table?.hideSystemMetrics!} + unselectedColumnState={paramsData?.config?.table?.unselectedColumnState!} columnsOrder={paramsData?.config?.table?.columnsOrder!} resizeMode={paramsData?.config?.table?.resizeMode!} hiddenMetrics={paramsData?.config?.table?.hiddenMetrics!} @@ -191,6 +192,9 @@ function ParamsContainer(): React.FunctionComponentElement { onRunsTagsChange={paramsAppModel.onRunsTagsChange} onColumnsOrderChange={paramsAppModel.onColumnsOrderChange} onColumnsVisibilityChange={paramsAppModel.onColumnsVisibilityChange} + onDefaultColumnsVisibilityChange={ + paramsAppModel.onDefaultColumnsVisibilityChange + } onTableResizeModeChange={paramsAppModel.onTableResizeModeChange} onTableDiffShow={paramsAppModel.onTableDiffShow} onSortReset={paramsAppModel.onSortReset} diff --git a/src/src/pages/Params/components/ParamsTableGrid/ParamsTableGrid.tsx b/src/src/pages/Params/components/ParamsTableGrid/ParamsTableGrid.tsx index 60dae1e3..28892170 100644 --- a/src/src/pages/Params/components/ParamsTableGrid/ParamsTableGrid.tsx +++ b/src/src/pages/Params/components/ParamsTableGrid/ParamsTableGrid.tsx @@ -327,7 +327,7 @@ function getParamsTableColumns( ...col, isHidden: !TABLE_DEFAULT_CONFIG.params.nonHidableColumns.has(col.key) && - hiddenColumns.includes(col.key), + hiddenColumns.includes(col.label ?? col.key), })); const columnsOrder = order?.left.concat(order.middle).concat(order.right); diff --git a/src/src/pages/Scatters/Scatters.tsx b/src/src/pages/Scatters/Scatters.tsx index e586539f..3370445e 100644 --- a/src/src/pages/Scatters/Scatters.tsx +++ b/src/src/pages/Scatters/Scatters.tsx @@ -197,6 +197,7 @@ function Scatters( hiddenRows={props.hiddenMetrics} hiddenColumns={props.hiddenColumns} hideSystemMetrics={props.hideSystemMetrics} + unselectedColumnState={props.unselectedColumnState} resizeMode={props.resizeMode} columnsWidths={props.columnsWidths} selectedRows={props.selectedRows} @@ -214,6 +215,9 @@ function Scatters( onColumnsVisibilityChange={ props.onColumnsVisibilityChange } + onDefaultColumnsVisibilityChange={ + props.onDefaultColumnsVisibilityChange + } onTableDiffShow={props.onTableDiffShow} onRowHeightChange={props.onRowHeightChange} onRowsChange={props.onParamVisibilityChange} diff --git a/src/src/pages/Scatters/ScattersContainer.tsx b/src/src/pages/Scatters/ScattersContainer.tsx index a2652128..bcefb7bc 100644 --- a/src/src/pages/Scatters/ScattersContainer.tsx +++ b/src/src/pages/Scatters/ScattersContainer.tsx @@ -154,6 +154,9 @@ function ScattersContainer(): React.FunctionComponentElement { hiddenMetrics={scattersData?.config?.table?.hiddenMetrics!} hideSystemMetrics={scattersData?.config?.table?.hideSystemMetrics!} hiddenColumns={scattersData?.config?.table?.hiddenColumns!} + unselectedColumnState={ + scattersData?.config?.table?.unselectedColumnState! + } groupingSelectOptions={scattersData?.groupingSelectOptions!} sortOptions={scattersData?.sortOptions!} projectsDataMetrics={projectsData?.metrics!} @@ -196,6 +199,9 @@ function ScattersContainer(): React.FunctionComponentElement { onParamVisibilityChange={scattersAppModel.onParamVisibilityChange} onColumnsOrderChange={scattersAppModel.onColumnsOrderChange} onColumnsVisibilityChange={scattersAppModel.onColumnsVisibilityChange} + onDefaultColumnsVisibilityChange={ + scattersAppModel.onDefaultColumnsVisibilityChange + } onTableDiffShow={scattersAppModel.onTableDiffShow} onTableResizeModeChange={scattersAppModel.onTableResizeModeChange} onRunsTagsChange={scattersAppModel.onRunsTagsChange} diff --git a/src/src/services/models/explorer/config.ts b/src/src/services/models/explorer/config.ts index 2a3620c4..a7222110 100644 --- a/src/src/services/models/explorer/config.ts +++ b/src/src/services/models/explorer/config.ts @@ -80,6 +80,8 @@ function initializeAppModel(appConfig: IAppInitialConfig): InitialAppModelType { resizeMode: TABLE_DEFAULT_CONFIG.metrics.resizeMode, rowHeight: TABLE_DEFAULT_CONFIG.metrics.rowHeight, sortFields: [...TABLE_DEFAULT_CONFIG.metrics.sortFields], + unselectedColumnState: + TABLE_DEFAULT_CONFIG.metrics.unselectedColumnState, hiddenMetrics: [...TABLE_DEFAULT_CONFIG.metrics.hiddenMetrics], hiddenColumns: [...TABLE_DEFAULT_CONFIG.metrics.hiddenColumns], columnsWidths: { tags: 300 }, @@ -201,6 +203,8 @@ function initializeAppModel(appConfig: IAppInitialConfig): InitialAppModelType { config.table = { metricsValueKey: TABLE_DEFAULT_CONFIG.runs.metricsValueKey, rowHeight: TABLE_DEFAULT_CONFIG.runs.rowHeight, + unselectedColumnState: + TABLE_DEFAULT_CONFIG.metrics.unselectedColumnState, hideSystemMetrics: TABLE_DEFAULT_CONFIG.runs.hideSystemMetrics, hiddenMetrics: TABLE_DEFAULT_CONFIG.runs.hiddenMetrics, hiddenColumns: TABLE_DEFAULT_CONFIG.runs.hiddenColumns, @@ -249,6 +253,8 @@ function initializeAppModel(appConfig: IAppInitialConfig): InitialAppModelType { if (components.charts.indexOf(ChartTypeEnum.ScatterPlot) !== -1) { config.table = { ...config?.table!, + unselectedColumnState: + TABLE_DEFAULT_CONFIG.metrics.unselectedColumnState, resizeMode: TABLE_DEFAULT_CONFIG.scatters.resizeMode, }; config.chart = { diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index 6aa7c26e..a1131d5f 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -7,7 +7,11 @@ import { IAxesScaleRange } from 'components/AxesPropsPopover'; import COLORS from 'config/colors/colors'; import DASH_ARRAYS from 'config/dash-arrays/dashArrays'; import { ResizeModeEnum } from 'config/enums/tableEnums'; -import { RowHeightSize } from 'config/table/tableConfigs'; +import { + RowHeightSize, + TABLE_DEFAULT_CONFIG, + UnselectedColumnState, +} from 'config/table/tableConfigs'; import { DensityOptions } from 'config/enums/densityEnum'; import { RequestStatusEnum } from 'config/enums/requestStatusEnum'; import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap'; @@ -86,6 +90,7 @@ import onAxesScaleTypeChange from 'utils/app/onAxesScaleTypeChange'; import onChangeTooltip from 'utils/app/onChangeTooltip'; import onColumnsOrderChange from 'utils/app/onColumnsOrderChange'; import onColumnsVisibilityChange from 'utils/app/onColumnsVisibilityChange'; +import onDefaultColumnsVisibilityChange from 'utils/app/onDefaultColumnsVisibilityChange'; import onGroupingApplyChange from 'utils/app/onGroupingApplyChange'; import onGroupingModeChange from 'utils/app/onGroupingModeChange'; import onGroupingPaletteChange from 'utils/app/onGroupingPaletteChange'; @@ -159,6 +164,8 @@ import { removeOldSelectedMetrics } from 'utils/app/removeOldSelectedMetrics'; import { InitialAppModelType } from './config'; +import { AppNameEnum } from './index'; + // ************ Metrics App Model Methods function getMetricsAppModelMethods( @@ -855,6 +862,39 @@ function getMetricsAppModelMethods( groupingSelectOptions, ); + const unselectedColumnState = configData.table?.unselectedColumnState; + + if (unselectedColumnState !== UnselectedColumnState.DEFAULT) { + const selected = configData.select?.options.map( + (option: ISelectOption) => option.label, + ); + let hiddenColumns = configData.table?.hiddenColumns; + + const defaultHiddenColumns = + TABLE_DEFAULT_CONFIG?.[AppNameEnum.METRICS]?.hiddenColumns; + + if (unselectedColumnState === UnselectedColumnState.FORCE_HIDE) { + // Push unique values to hiddenColumns + hiddenColumns = _.uniq( + hiddenColumns?.concat( + params.filter((item: string) => !selected.includes(item)), + ), + ); + } else { + // Remove unselected values from hiddenColumns + hiddenColumns = defaultHiddenColumns; + } + + configData = { + ...configData, + table: { + ...configData.table, + rowHeight: configData.table?.rowHeight!, + hiddenColumns, + }, + }; + } + const tableColumns = getMetricsTableColumns( params, groupingSelectOptions, @@ -1761,6 +1801,14 @@ function getMetricsAppModelMethods( updateModelData, }); }, + onDefaultColumnsVisibilityChange(state: UnselectedColumnState): void { + onDefaultColumnsVisibilityChange({ + unselectedColumnState: state, + model, + appName, + updateModelData, + }); + }, onTableDiffShow(): void { onTableDiffShow({ model, appName, updateModelData }); }, diff --git a/src/src/services/models/explorer/paramsModelMethods.ts b/src/src/services/models/explorer/paramsModelMethods.ts index 2c2e41f3..71dc40cc 100644 --- a/src/src/services/models/explorer/paramsModelMethods.ts +++ b/src/src/services/models/explorer/paramsModelMethods.ts @@ -5,7 +5,11 @@ import _ from 'lodash-es'; import COLORS from 'config/colors/colors'; import DASH_ARRAYS from 'config/dash-arrays/dashArrays'; import { MetricsValueKeyEnum, ResizeModeEnum } from 'config/enums/tableEnums'; -import { RowHeightSize } from 'config/table/tableConfigs'; +import { + RowHeightSize, + TABLE_DEFAULT_CONFIG, + UnselectedColumnState, +} from 'config/table/tableConfigs'; import { RequestStatusEnum } from 'config/enums/requestStatusEnum'; import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap'; import { DATE_EXPORTING_FORMAT, TABLE_DATE_FORMAT } from 'config/dates/dates'; @@ -63,6 +67,7 @@ import onColorIndicatorChange from 'utils/app/onColorIndicatorChange'; import onColumnsOrderChange from 'utils/app/onColumnsOrderChange'; import onColumnsVisibilityChange from 'utils/app/onColumnsVisibilityChange'; import onCurveInterpolationChange from 'utils/app/onCurveInterpolationChange'; +import onDefaultColumnsVisibilityChange from 'utils/app/onDefaultColumnsVisibilityChange'; import onGroupingApplyChange from 'utils/app/onGroupingApplyChange'; import onGroupingModeChange from 'utils/app/onGroupingModeChange'; import onGroupingPaletteChange from 'utils/app/onGroupingPaletteChange'; @@ -719,6 +724,51 @@ function getParamsModelMethods( ); const sortFields = modelState?.config?.table.sortFields; + const unselectedColumnState = configData.table?.unselectedColumnState; + + if (unselectedColumnState !== UnselectedColumnState.DEFAULT) { + const selected = modelState?.config?.select?.options.map( + (option: ISelectOption) => option.label, + ); + + let hiddenColumns = configData.table?.hiddenColumns; + + const defaultHiddenColumns = + TABLE_DEFAULT_CONFIG?.[AppNameEnum.PARAMS]?.hiddenColumns; + + if (unselectedColumnState === UnselectedColumnState.FORCE_HIDE) { + // Extract the combined keys from metricsColumns dictionary + const metricList = Object.keys(metricsColumns).reduce( + (acc: string[], key: string) => { + const metricKeys = Object.keys(metricsColumns[key]); + return acc.concat( + metricKeys.map((metricKey) => `${key} ${metricKey}`.trim()), + ); + }, + [], + ); + + const metricsAndParams = metricList.concat(params); + hiddenColumns = _.uniq( + defaultHiddenColumns?.concat( + metricsAndParams.filter((item: string) => !selected.includes(item)), + ), + ); + } else { + // Remove unselected values from hiddenColumns + hiddenColumns = defaultHiddenColumns; + } + + configData = { + ...configData, + table: { + ...configData.table, + rowHeight: modelState?.config?.table?.rowHeight!, + hiddenColumns, + }, + }; + } + const tableColumns = getParamsTableColumns( sortOptions, metricsColumns, @@ -1672,6 +1722,14 @@ function getParamsModelMethods( updateModelData, }); }, + onDefaultColumnsVisibilityChange(state: UnselectedColumnState): void { + onDefaultColumnsVisibilityChange({ + unselectedColumnState: state, + model, + appName, + updateModelData, + }); + }, onTableResizeModeChange(mode: ResizeModeEnum): void { onTableResizeModeChange({ mode, model, appName }); }, diff --git a/src/src/services/models/explorer/runsModelMethods.ts b/src/src/services/models/explorer/runsModelMethods.ts index 6fd03310..77f30f38 100644 --- a/src/src/services/models/explorer/runsModelMethods.ts +++ b/src/src/services/models/explorer/runsModelMethods.ts @@ -4,7 +4,10 @@ import _ from 'lodash-es'; import COLORS from 'config/colors/colors'; import DASH_ARRAYS from 'config/dash-arrays/dashArrays'; -import { RowHeightSize } from 'config/table/tableConfigs'; +import { + RowHeightSize, + UnselectedColumnState, +} from 'config/table/tableConfigs'; import { RequestStatusEnum } from 'config/enums/requestStatusEnum'; import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap'; import { DATE_EXPORTING_FORMAT, TABLE_DATE_FORMAT } from 'config/dates/dates'; @@ -51,6 +54,7 @@ import { getFilteredGroupingOptions } from 'utils/app/getFilteredGroupingOptions import { getGroupingPersistIndex } from 'utils/app/getGroupingPersistIndex'; import onColumnsOrderChange from 'utils/app/onColumnsOrderChange'; import onColumnsVisibilityChange from 'utils/app/onColumnsVisibilityChange'; +import onDefaultColumnsVisibilityChange from 'utils/app/onDefaultColumnsVisibilityChange'; import onRowHeightChange from 'utils/app/onRowHeightChange'; import onSelectRunQueryChange from 'utils/app/onSelectRunQueryChange'; import onSelectExperimentsChange from 'utils/app/onSelectExperimentsChange'; @@ -1144,6 +1148,14 @@ function getRunsModelMethods( updateModelData, }); }, + onDefaultColumnsVisibilityChange(state: UnselectedColumnState): void { + onDefaultColumnsVisibilityChange({ + unselectedColumnState: state, + model, + appName, + updateModelData, + }); + }, onTableDiffShow(): void { onTableDiffShow({ model, appName, updateModelData }); }, diff --git a/src/src/services/models/explorer/scattersModelMethods.ts b/src/src/services/models/explorer/scattersModelMethods.ts index bc4de5f7..49b21cc8 100644 --- a/src/src/services/models/explorer/scattersModelMethods.ts +++ b/src/src/services/models/explorer/scattersModelMethods.ts @@ -7,7 +7,11 @@ import { IPoint } from 'components/ScatterPlot'; import COLORS from 'config/colors/colors'; import DASH_ARRAYS from 'config/dash-arrays/dashArrays'; import { MetricsValueKeyEnum, ResizeModeEnum } from 'config/enums/tableEnums'; -import { RowHeightSize } from 'config/table/tableConfigs'; +import { + RowHeightSize, + TABLE_DEFAULT_CONFIG, + UnselectedColumnState, +} from 'config/table/tableConfigs'; import { RequestStatusEnum } from 'config/enums/requestStatusEnum'; import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap'; import { DATE_EXPORTING_FORMAT, TABLE_DATE_FORMAT } from 'config/dates/dates'; @@ -68,6 +72,7 @@ import getRunData from 'utils/app/getRunData'; import onChangeTooltip from 'utils/app/onChangeTooltip'; import onColumnsOrderChange from 'utils/app/onColumnsOrderChange'; import onColumnsVisibilityChange from 'utils/app/onColumnsVisibilityChange'; +import onDefaultColumnsVisibilityChange from 'utils/app/onDefaultColumnsVisibilityChange'; import onGroupingApplyChange from 'utils/app/onGroupingApplyChange'; import onGroupingModeChange from 'utils/app/onGroupingModeChange'; import onGroupingPaletteChange from 'utils/app/onGroupingPaletteChange'; @@ -248,6 +253,52 @@ function getScattersModelMethods( ); const sortFields = modelState?.config?.table.sortFields; + const unselectedColumnState = configData.table?.unselectedColumnState; + + if (unselectedColumnState !== UnselectedColumnState.DEFAULT) { + const selected = modelState?.config?.select?.options.map( + (option: ISelectOption) => option.label, + ); + + let hiddenColumns = configData.table?.hiddenColumns; + + const defaultHiddenColumns = + TABLE_DEFAULT_CONFIG?.[AppNameEnum.PARAMS]?.hiddenColumns; + + if (unselectedColumnState === UnselectedColumnState.FORCE_HIDE) { + // Extract the combined keys from metricsColumns dictionary + const metricList = Object.keys(metricsColumns).reduce( + (acc: string[], key: string) => { + const metricKeys = Object.keys(metricsColumns[key]); + return acc.concat( + metricKeys.map((metricKey) => `${key} ${metricKey}`.trim()), + ); + }, + [], + ); + + const metricsAndParams = metricList.concat(params); + hiddenColumns = _.uniq( + defaultHiddenColumns?.concat( + metricsAndParams.filter((item: string) => !selected.includes(item)), + ), + ); + } else { + // Remove unselected values from hiddenColumns + hiddenColumns = defaultHiddenColumns; + } + + configData = { + ...configData, + table: { + ...configData.table, + rowHeight: modelState?.config?.table?.rowHeight!, + unselectedColumnState, + hiddenColumns, + }, + }; + } + const tableColumns = getParamsTableColumns( sortOptions, metricsColumns, @@ -1571,6 +1622,14 @@ function getScattersModelMethods( updateModelData, }); }, + onDefaultColumnsVisibilityChange(state: UnselectedColumnState): void { + onDefaultColumnsVisibilityChange({ + unselectedColumnState: state, + model, + appName, + updateModelData, + }); + }, onTableResizeModeChange(mode: ResizeModeEnum): void { onTableResizeModeChange({ mode, model, appName }); }, diff --git a/src/src/types/components/Table/Table.d.ts b/src/src/types/components/Table/Table.d.ts index 6aa49da4..b2f8ae95 100644 --- a/src/src/types/components/Table/Table.d.ts +++ b/src/src/types/components/Table/Table.d.ts @@ -1,6 +1,6 @@ import React from 'react'; -import { RowHeight, RowHeightSize } from 'config/table/tableConfigs'; +import { RowHeight, RowHeightSize, UnselectedColumnState } from 'config/table/tableConfigs'; import { MetricsValueKeyEnum, ResizeModeEnum, @@ -27,6 +27,7 @@ export interface ITableProps { estimatedRowHeight?: number; onManageColumns?: (order: IColumnsOrderData) => void; onColumnsVisibilityChange?: (hiddenColumns: string[] | string) => void; + onDefaultColumnsVisibilityChange?: (state: UnselectedColumnState) => void; hiddenChartRows?: boolean; onTableDiffShow?: () => void; onSort?: (field: string, value: 'asc' | 'desc' | 'none') => void; @@ -53,6 +54,7 @@ export interface ITableProps { rowHeightMode?: any; columnsOrder?: IColumnsOrder; hiddenColumns?: string[]; + unselectedColumnState?: UnselectedColumnState; hideSystemMetrics?: boolean; updateColumns?: any; columnsWidths?: any; diff --git a/src/src/types/pages/metrics/Metrics.d.ts b/src/src/types/pages/metrics/Metrics.d.ts index af10a29e..faf479c5 100644 --- a/src/src/types/pages/metrics/Metrics.d.ts +++ b/src/src/types/pages/metrics/Metrics.d.ts @@ -1,7 +1,7 @@ import React from 'react'; import { RouteChildrenProps } from 'react-router-dom'; -import { RowHeightSize } 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'; @@ -85,6 +85,7 @@ export interface IMetricProps extends Partial { hiddenMetrics: string[]; hiddenColumns: string[]; hideSystemMetrics: boolean; + unselectedColumnState: UnselectedColumnState; sameValueColumns?: string[] | []; groupingSelectOptions: IGroupingSelectOption[]; sortOptions: IGroupingSelectOption[]; @@ -142,6 +143,7 @@ export interface IMetricProps extends Partial { onMetricVisibilityChange: (metricKeys: string[]) => void; onColumnsOrderChange: (order: any) => void; onColumnsVisibilityChange: (hiddenColumns: string[] | string) => void; + onDefaultColumnsVisibilityChange: (state: UnselectedColumnState) => void; onTableDiffShow: () => void; onTableResizeModeChange: (mode: ResizeModeEnum) => void; updateColumnsWidths: (key: string, width: number, isReset: boolean) => void; diff --git a/src/src/types/pages/params/Params.d.ts b/src/src/types/pages/params/Params.d.ts index b88d0a53..41eee410 100644 --- a/src/src/types/pages/params/Params.d.ts +++ b/src/src/types/pages/params/Params.d.ts @@ -3,6 +3,7 @@ import { RouteChildrenProps } from 'react-router-dom'; import { ITableRef } from 'components/Table/Table'; +import { UnselectedColumnState } from 'config/table/tableConfigs'; import { ResizeModeEnum } from 'config/enums/tableEnums'; import { RequestStatusEnum } from 'config/enums/requestStatusEnum'; @@ -51,6 +52,7 @@ export interface IParamsProps extends Partial { sortOptions: IGroupingSelectOption[]; hiddenMetrics: string[]; hideSystemMetrics: boolean; + unselectedColumnState: UnselectedColumnState; sortFields: [string, 'asc' | 'desc' | boolean][]; focusedState: IFocusedState; isVisibleColorIndicator: boolean; @@ -107,6 +109,7 @@ export interface IParamsProps extends Partial { onChangeTooltip: (tooltip: Partial) => void; onExportTableData: (e: React.ChangeEvent) => void; onColumnsVisibilityChange: (order: any) => void; + onDefaultColumnsVisibilityChange: (state: UnselectedColumnState) => void; onTableDiffShow: () => void; onTableResizeModeChange: (mode: ResizeModeEnum) => void; onSortReset: () => void; diff --git a/src/src/types/pages/scatters/Scatters.d.ts b/src/src/types/pages/scatters/Scatters.d.ts index d85061fa..5bd5754a 100644 --- a/src/src/types/pages/scatters/Scatters.d.ts +++ b/src/src/types/pages/scatters/Scatters.d.ts @@ -1,7 +1,7 @@ import React from 'react'; import { RouteChildrenProps } from 'react-router-dom'; -import { RowHeightSize } from 'config/table/tableConfigs'; +import { RowHeightSize, UnselectedColumnState } from 'config/table/tableConfigs'; import { ResizeModeEnum } from 'config/enums/tableEnums'; import { RequestStatusEnum } from 'config/enums/requestStatusEnum'; @@ -59,6 +59,7 @@ export interface IScattersProps extends Partial { sortFields: [string, 'asc' | 'desc' | boolean][]; hiddenMetrics: string[]; hiddenColumns: string[]; + unselectedColumnState: UnselectedColumnState; hideSystemMetrics: boolean; groupingSelectOptions: IGroupingSelectOption[]; sortOptions: IGroupingSelectOption[]; @@ -105,6 +106,7 @@ export interface IScattersProps extends Partial { onParamVisibilityChange: (metricKeys: string[]) => void; onColumnsOrderChange: (order: any) => void; onColumnsVisibilityChange: (hiddenColumns: string[] | string) => void; + onDefaultColumnsVisibilityChange: (state: UnselectedColumnState) => void; onTableDiffShow: () => void; onTableResizeModeChange: (mode: ResizeModeEnum) => void; updateColumnsWidths: (key: string, width: number, isReset: boolean) => void; diff --git a/src/src/types/services/models/explorer/createAppModel.d.ts b/src/src/types/services/models/explorer/createAppModel.d.ts index d6be7e15..43a6cd23 100644 --- a/src/src/types/services/models/explorer/createAppModel.d.ts +++ b/src/src/types/services/models/explorer/createAppModel.d.ts @@ -1,5 +1,5 @@ import { MetricsValueKeyEnum, ResizeModeEnum } from 'config/enums/tableEnums'; -import { RowHeightSize } from 'config/table/tableConfigs'; +import { RowHeightSize, UnselectedColumnState } from 'config/table/tableConfigs'; import { DensityOptions } from 'config/enums/densityEnum'; import { GroupNameEnum } from 'config/grouping/GroupingPopovers'; @@ -117,6 +117,7 @@ export interface ITableConfig { resizeMode?: ResizeModeEnum; rowHeight: RowHeightSize; sortFields?: SortField[]; + unselectedColumnState?: UnselectedColumnState; hiddenMetrics?: string[]; hiddenColumns?: string[]; hideSystemMetrics?: boolean; diff --git a/src/src/utils/app/onDefaultColumnsVisibilityChange.ts b/src/src/utils/app/onDefaultColumnsVisibilityChange.ts new file mode 100644 index 00000000..dcd675ae --- /dev/null +++ b/src/src/utils/app/onDefaultColumnsVisibilityChange.ts @@ -0,0 +1,46 @@ +import { UnselectedColumnState } from 'config/table/tableConfigs'; +import { ANALYTICS_EVENT_KEYS } from 'config/analytics/analyticsKeysMap'; + +import * as analytics from 'services/analytics'; + +import { IModel, State } from 'types/services/models/model'; +import { IAppModelConfig } from 'types/services/models/explorer/createAppModel'; + +import { encode } from 'utils/encoder/encoder'; +import { setItem } from 'utils/storage'; + +export default function onDefaultColumnsVisibilityChange({ + unselectedColumnState, + model, + appName, + updateModelData, +}: { + unselectedColumnState: UnselectedColumnState; + model: IModel; + appName: string; + updateModelData: ( + configData?: IAppModelConfig | any, + shouldURLUpdate?: boolean, + ) => void; +}): void { + const configData = model.getState()?.config; + if (configData?.table) { + const table = { + ...configData.table, + unselectedColumnState, + }; + const config = { + ...configData, + table, + }; + model.setState({ config }); + setItem(`${appName}Table`, encode(table)); + updateModelData(config); + } + analytics.trackEvent( + `${ + // @ts-ignore + ANALYTICS_EVENT_KEYS[appName].table.changeDefaultColumnsVisibility + } to "${unselectedColumnState.toString()}"`, + ); +} From 99898bed7d0e5793f553daf02a98af377b239928 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Mon, 6 May 2024 16:09:46 +0200 Subject: [PATCH 42/79] Change context from string to object (#76) --- .../services/models/metrics/metricsAppModel.d.ts | 6 +++++- src/src/utils/app/getMetricsListFromSelect.ts | 14 ++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/src/types/services/models/metrics/metricsAppModel.d.ts b/src/src/types/services/models/metrics/metricsAppModel.d.ts index 1f4aed9c..d40a4792 100644 --- a/src/src/types/services/models/metrics/metricsAppModel.d.ts +++ b/src/src/types/services/models/metrics/metricsAppModel.d.ts @@ -256,9 +256,13 @@ export interface IAlignMetricsDataParams { }[]; } +export interface IContext { + [key: string]: any; +} + export interface IMetricsDataParams { report_progress?: boolean; - metrics: Array<{ key: string; context: string }>; + metrics: Array<{ key: string; context: IContext }>; query: string; steps: number; x_axis: string; diff --git a/src/src/utils/app/getMetricsListFromSelect.ts b/src/src/utils/app/getMetricsListFromSelect.ts index 7c2454f1..c218b1ee 100644 --- a/src/src/utils/app/getMetricsListFromSelect.ts +++ b/src/src/utils/app/getMetricsListFromSelect.ts @@ -1,15 +1,12 @@ import { ISyntaxErrorDetails } from 'types/components/NotificationContainer/NotificationContainer'; import { ISelectConfig } from 'types/services/models/explorer/createAppModel'; - -import { jsValidVariableRegex } from 'utils/getObjectPaths'; - -import { formatValue } from '../formatValue'; +import { IContext } from 'types/services/models/metrics/metricsAppModel'; export default function getMetricsListFromSelect( selectData: ISelectConfig, error?: ISyntaxErrorDetails, -): Array<{ key: string; context: string }> { - const metricsList: Array<{ key: string; context: string }> = []; +): Array<{ key: string; context: IContext }> { + const metricsList: Array<{ key: string; context: IContext }> = []; if (selectData === undefined) { return metricsList; @@ -17,10 +14,7 @@ export default function getMetricsListFromSelect( selectData.options?.forEach((option) => { const metricName = option.value?.option_name ?? ''; - const context: string = - option.value?.context && Object.keys(option.value?.context).length > 0 - ? formatValue(option.value?.context) - : '{}'; + const context: IContext = option.value?.context ?? {}; metricsList.push({ key: metricName, context: context }); }); From 4d030044b3ed6400f3c575067b64a120300924ef Mon Sep 17 00:00:00 2001 From: Geoffrey Wilson Date: Fri, 10 May 2024 09:05:08 -0400 Subject: [PATCH 43/79] Limit metrics (#77) * limit metrics selection by specified length * change limit to 50 --- src/src/pages/Metrics/Metrics.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/src/pages/Metrics/Metrics.tsx b/src/src/pages/Metrics/Metrics.tsx index 035433c5..dbc07852 100644 --- a/src/src/pages/Metrics/Metrics.tsx +++ b/src/src/pages/Metrics/Metrics.tsx @@ -30,6 +30,7 @@ import { AppNameEnum } from 'services/models/explorer'; import { ILine } from 'types/components/LineChart/LineChart'; import { IMetricProps } from 'types/pages/metrics/Metrics'; +import { ISelectOption } from 'types/services/models/explorer/createAppModel'; import { ChartTypeEnum, CurveEnum } from 'utils/d3'; @@ -39,6 +40,8 @@ import SelectForm from './components/SelectForm/SelectForm'; import './Metrics.scss'; +const MaxMetrics = 50; + function Metrics( props: IMetricProps, ): React.FunctionComponentElement { @@ -78,6 +81,18 @@ function Metrics( props.axesScaleRange, ]); + const metricsSelectChange = (selectedMetrics: ISelectOption[]): void => { + if (selectedMetrics.length > MaxMetrics) { + props.onNotificationAdd({ + id: Date.now(), + severity: 'warning', + messages: [`Maximum number of metrics is ${MaxMetrics}`], + }); + return; + } + props.onMetricsSelectChange(selectedMetrics); + }; + return (
@@ -102,7 +117,7 @@ function Metrics( isDisabled={isProgressBarVisible} selectFormData={props.selectFormData} selectedMetricsData={props.selectedMetricsData} - onMetricsSelectChange={props.onMetricsSelectChange} + onMetricsSelectChange={metricsSelectChange} onSelectRunQueryChange={props.onSelectRunQueryChange} onSearchQueryCopy={props.onSearchQueryCopy} /> From 6346f8f338e950788b7da267d7b45081e9b81ff3 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Mon, 13 May 2024 11:52:15 +0200 Subject: [PATCH 44/79] Show only the visible columns in Manage Columns popover (#78) --- .../ManageColumnsPopover.tsx | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/src/pages/Metrics/components/Table/ManageColumnsPopover/ManageColumnsPopover.tsx b/src/src/pages/Metrics/components/Table/ManageColumnsPopover/ManageColumnsPopover.tsx index 8d3e20f6..7e7de038 100644 --- a/src/src/pages/Metrics/components/Table/ManageColumnsPopover/ManageColumnsPopover.tsx +++ b/src/src/pages/Metrics/components/Table/ManageColumnsPopover/ManageColumnsPopover.tsx @@ -21,6 +21,7 @@ import { IManageColumnsPopoverProps } from './ManageColumns'; import './ManageColumnsPopover.scss'; +let softHidden: string[] = []; const initialData = { columns: { left: { @@ -73,6 +74,12 @@ function ManageColumnsPopover({ } }; + function toggleSoftHidden(key: string) { + softHidden.includes(key) + ? softHidden.splice(softHidden.indexOf(key), 1) + : softHidden.push(key); + } + function onDragStart(result: any) { setDraggingItemId(result.draggableId); } @@ -171,13 +178,20 @@ function ManageColumnsPopover({ React.useEffect(() => { const newState = { ...state }; const leftList = columnsData.filter( - (item: ITableColumn) => item.pin === 'left', + (item: ITableColumn) => + item.pin === 'left' && + (!item.isHidden || softHidden.includes(item.key)), ); const rightList = columnsData.filter( - (item: ITableColumn) => item.pin === 'right', + (item: ITableColumn) => + item.pin === 'right' && + (!item.isHidden || softHidden.includes(item.key)), ); const middleList = columnsData.filter( - (item: ITableColumn) => item.pin !== 'left' && item.pin !== 'right', + (item: ITableColumn) => + item.pin !== 'left' && + item.pin !== 'right' && + (!item.isHidden || softHidden.includes(item.key)), ); newState.columns.left.list = leftList; newState.columns.middle.list = middleList; @@ -285,15 +299,16 @@ function ManageColumnsPopover({ popoverWidth={popoverWidth} appName={appName} isHidden={isColumnHidden(column.key)} - onClick={() => + onClick={() => { + toggleSoftHidden(column.key); onColumnsVisibilityChange( hiddenColumns?.includes(column.key) ? hiddenColumns?.filter( (col: string) => col !== column.key, ) : hiddenColumns?.concat([column.key]), - ) - } + ); + }} draggingItemId={draggingItemId} /> ), @@ -341,15 +356,16 @@ function ManageColumnsPopover({ hasSearchableItems searchKey={searchKey} isHidden={isColumnHidden(column.key)} - onClick={() => + onClick={() => { + toggleSoftHidden(column.key); onColumnsVisibilityChange( hiddenColumns?.includes(column.key) ? hiddenColumns?.filter( (col: string) => col !== column.key, ) : hiddenColumns?.concat([column.key]), - ) - } + ); + }} draggingItemId={draggingItemId} /> ), @@ -383,15 +399,16 @@ function ManageColumnsPopover({ appName={appName} popoverWidth={popoverWidth} isHidden={isColumnHidden(column.key)} - onClick={() => + onClick={() => { + toggleSoftHidden(column.key); onColumnsVisibilityChange( hiddenColumns.includes(column.key) ? hiddenColumns.filter( (col: string) => col !== column.key, ) : hiddenColumns.concat([column.key]), - ) - } + ); + }} draggingItemId={draggingItemId} /> ); From 4133f4199e2654f8280c1f7d84c1c9023ca1c50d Mon Sep 17 00:00:00 2001 From: Geoffrey Wilson Date: Mon, 13 May 2024 14:34:26 -0400 Subject: [PATCH 45/79] Autofocus the experiment search (#81) --- .../ExperimentSelectionPopover/ExperimentSelectionPopover.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx index 93f648c2..88c5f1dc 100644 --- a/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx +++ b/src/src/pages/Experiment/components/ExperimentSelectionPopover/ExperimentSelectionPopover.tsx @@ -214,6 +214,7 @@ function ExperimentSelectionPopover({ onChange={handleSearchInputChange} inputProps={{ 'aria-label': 'search' }} className='ExperimentSelectionPopover__searchContainer__inputBase' + autoFocus={true} /> Date: Fri, 16 Aug 2024 15:10:14 -0700 Subject: [PATCH 46/79] (bugfix) Get v3.19.3 working with v3.17.5 server --- src/src/pages/Runs/RunsTable.tsx | 3 +- src/src/services/api/runs/runsService.ts | 2 +- .../models/explorer/paramsModelMethods.ts | 19 +++++++++---- .../models/explorer/runsModelMethods.ts | 17 +++++++---- .../models/explorer/scattersModelMethods.ts | 28 ++++++++++++++----- 5 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/src/pages/Runs/RunsTable.tsx b/src/src/pages/Runs/RunsTable.tsx index 6f91339c..76da6b4c 100644 --- a/src/src/pages/Runs/RunsTable.tsx +++ b/src/src/pages/Runs/RunsTable.tsx @@ -81,7 +81,8 @@ function RunsTable({ columnsOrder={columnsOrder} columnsWidths={columnsWidths} // Table actions - onMetricsValueKeyChange={onMetricsValueKeyChange} + // TODO: Re-enable the metrics value display options + // onMetricsValueKeyChange={onMetricsValueKeyChange} onManageColumns={onManageColumns} onColumnsVisibilityChange={onColumnsVisibilityChange} onTableDiffShow={onTableDiffShow} diff --git a/src/src/services/api/runs/runsService.ts b/src/src/services/api/runs/runsService.ts index 117246a9..3521dfb7 100644 --- a/src/src/services/api/runs/runsService.ts +++ b/src/src/services/api/runs/runsService.ts @@ -28,7 +28,7 @@ function getRunsData( ) { return API.getStream(endpoints.GET_RUNS, { q: query || '', - experiment_names: selectedExperimentNames, + experiment_names: selectedExperimentNames || [], ...(limit ? { limit } : {}), ...(offset ? { offset } : {}), }); diff --git a/src/src/services/models/explorer/paramsModelMethods.ts b/src/src/services/models/explorer/paramsModelMethods.ts index 71dc40cc..4ffb05e0 100644 --- a/src/src/services/models/explorer/paramsModelMethods.ts +++ b/src/src/services/models/explorer/paramsModelMethods.ts @@ -389,7 +389,9 @@ function getParamsModelMethods( const metricsRowValues = getMetricsInitialRowData(metricsColumns); metric.run.traces.metric.forEach((trace: any) => { const metricHash = getMetricHash(trace.name, trace.context); - metricsRowValues[metricHash] = formatValue(trace.values.last); + // TODO: Implement Support for the new metric value API format + // metricsRowValues[metricHash] = formatValue(trace.values.last); + metricsRowValues[metricHash] = formatValue(trace.last_value.last); }); const rowValues: any = { rowMeta: { @@ -1004,11 +1006,18 @@ function getParamsModelMethods( }; const metricHash = getMetricHash(trace.name, trace.context as any); metricsValues[metricHash] = { - min: trace.values.min, - max: trace.values.max, - last: trace.values.last, - first: trace.values.first, + min: '-', + max: '-', + last: '-', + first: '-', }; + // TODO: Implement Support for the new metric value API format + // metricsValues[metricHash] = { + // min: trace.values.min, + // max: trace.values.max, + // last: trace.values.last, + // first: trace.values.first, + // }; }); const paramKey = encode({ runHash: run.hash }); diff --git a/src/src/services/models/explorer/runsModelMethods.ts b/src/src/services/models/explorer/runsModelMethods.ts index 77f30f38..96c8525c 100644 --- a/src/src/services/models/explorer/runsModelMethods.ts +++ b/src/src/services/models/explorer/runsModelMethods.ts @@ -459,11 +459,16 @@ function getRunsModelMethods( [contextToString(trace.context) as string]: '-', }; const metricHash = getMetricHash(trace.name, trace.context as any); + // TODO: Implement Support for the new metric value API format metricsValues[metricHash] = { - min: trace.values.min, - max: trace.values.max, - last: trace.values.last, - first: trace.values.first, + //min: trace.values.min, + min: '-', + // max: trace.values.max, + max: '-', + last: '-', + //last: trace.values.last, + first: '-', + //first: trace.values.first, }; }); runHashArray.push(run.hash); @@ -707,8 +712,10 @@ function getRunsModelMethods( const metricsRowValues = getMetricsInitialRowData(metricsColumns); metric.run.traces.metric.forEach((trace: any) => { const metricHash = getMetricHash(trace.name, trace.context); + // TODO: Implement Support for the new metric value API format metricsRowValues[metricHash] = formatValue( - trace.values[metricsValueKey], + trace.last_value.last, + // trace.values[metricsValueKey], ); }); diff --git a/src/src/services/models/explorer/scattersModelMethods.ts b/src/src/services/models/explorer/scattersModelMethods.ts index 49b21cc8..2f5a0f48 100644 --- a/src/src/services/models/explorer/scattersModelMethods.ts +++ b/src/src/services/models/explorer/scattersModelMethods.ts @@ -380,12 +380,16 @@ function getScattersModelMethods( }; } if (type === 'metrics') { - run.run.traces.metric.forEach((trace: IParamTrace) => { + // TODO: Implement Support for the new metric value API format + //run.run.traces.metric.forEach((trace: IParamTrace) => { + // Change the type so that we can access fields of the old API type + run.run.traces.metric.forEach((trace: any) => { if ( trace.name === value?.option_name && _.isEqual(trace.context, value?.context) ) { - let lastValue = trace.values.last; + // TODO: Revert this back to trace.values.last; + let lastValue = trace.last_value.last; const formattedLastValue = formatValue(lastValue, '-'); values[i] = lastValue; if (formattedLastValue !== '-') { @@ -560,7 +564,9 @@ function getScattersModelMethods( const metricsRowValues = getMetricsInitialRowData(metricsColumns); metric.run.traces.metric.forEach((trace: any) => { const metricHash = getMetricHash(trace.name, trace.context as any); - metricsRowValues[metricHash] = formatValue(trace.values.last); + // TODO: Implement Support for the new metric value API format + metricsRowValues[metricHash] = formatValue(trace.last_value.last); + //metricsRowValues[metricHash] = formatValue(trace.values.last); }); const rowValues: any = { rowMeta: { @@ -747,12 +753,20 @@ function getScattersModelMethods( ...metricsColumns[trace.name], [contextToString(trace.context) as string]: '-', }; + // TODO: Implement Support for the new metric value API format + const traceOldAPI: any = trace; const metricHash = getMetricHash(trace.name, trace.context as any); + // metricsValues[metricHash] = { + // min: trace.values.min, + // max: trace.values.max, + // last: trace.last, + // first: trace.values.first, + // }; metricsValues[metricHash] = { - min: trace.values.min, - max: trace.values.max, - last: trace.values.last, - first: trace.values.first, + min: '-', + max: '-', + last: traceOldAPI.last_value.last, + first: '-', }; }); const paramKey = encode({ runHash: run.hash }); From d4d6a4977f878937f1ba424c1a7049261ed442b7 Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Tue, 14 May 2024 01:22:18 -0700 Subject: [PATCH 47/79] pass selected experiments to getRunsData for params and scatters (#80) S3 Testing and adjustments (#1386) * Set UsePathStyle to false when creating S3 instance * Restore comment * Add preliminary AWS S3 tracking documentation * Revert UsePathStyle for integration test * Revert UsePathStyle setting * Fix aws tracking docs --- src/src/services/models/explorer/paramsModelMethods.ts | 9 +++++++-- src/src/utils/app/getQueryStringFromSelect.ts | 8 -------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/src/services/models/explorer/paramsModelMethods.ts b/src/src/services/models/explorer/paramsModelMethods.ts index 4ffb05e0..d7e82065 100644 --- a/src/src/services/models/explorer/paramsModelMethods.ts +++ b/src/src/services/models/explorer/paramsModelMethods.ts @@ -246,8 +246,13 @@ function getParamsModelMethods( } const configData = { ...model.getState()?.config }; const query = getQueryStringFromSelect(configData?.select, true); - runsRequestRef = runsService.getRunsData(query); - setRequestProgress(model); + const selectedExperimentNames = getSelectedExperiments().map((e) => e.name); + runsRequestRef = runsService.getRunsData( + query, + undefined, + undefined, + selectedExperimentNames, + ); return { call: async () => { if (_.isEmpty(configData?.select?.options)) { diff --git a/src/src/utils/app/getQueryStringFromSelect.ts b/src/src/utils/app/getQueryStringFromSelect.ts index c009ed6a..b2eaa68a 100644 --- a/src/src/utils/app/getQueryStringFromSelect.ts +++ b/src/src/utils/app/getQueryStringFromSelect.ts @@ -54,19 +54,11 @@ export default function getQueryStringFromSelect( : ''; } - const selectedExperiments = getSelectedExperiments(); - - const experimentNames = `run.experiment in ["${selectedExperiments - .map((e) => e.name) - .join('", "')}"]`; - if (simpleInput && selections) { query = `${simpleInput} and ${selections}`; } else { query = `${simpleInput}${selections}`; } - - query = query ? `${query} and ${experimentNames}` : experimentNames; } return excludeMetrics ? query.trim() || '' : query.trim() || '()'; } From 63d4cefff1b740678d38ea37165dcc658c6bc0ac Mon Sep 17 00:00:00 2001 From: Geoffrey Wilson Date: Tue, 14 May 2024 10:03:36 -0400 Subject: [PATCH 48/79] Metrics counter (#79) * Display count of metrics selected * Move counter to button label --- src/src/pages/Metrics/components/SelectForm/SelectForm.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx b/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx index afdbd1ac..d77278ef 100644 --- a/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx +++ b/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx @@ -142,6 +142,11 @@ function SelectForm({ const open: boolean = !!anchorEl; const id = open ? 'select-metric' : undefined; + let metricsButtonText = 'Metrics'; + let selectedCount = selectedMetricsData?.options?.length ?? 0; + if (selectedCount > 0) { + metricsButtonText += ` (${selectedCount})`; + } return (
@@ -177,7 +182,7 @@ function SelectForm({ className='Metrics__SelectForm__metricsButton' > - Metrics + {metricsButtonText} Date: Wed, 15 May 2024 10:31:06 +0200 Subject: [PATCH 49/79] Metric tooltip dynamic size (#83) --- .../ChartPanel/ChartPopover/ChartPopover.scss | 4 +++- .../ChartPanel/ChartPopover/ChartPopover.tsx | 14 +++++++++----- .../ChartPanel/PopoverContent/PopoverContent.scss | 3 +-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/src/components/ChartPanel/ChartPopover/ChartPopover.scss b/src/src/components/ChartPanel/ChartPopover/ChartPopover.scss index 6c6589b0..857a77c5 100644 --- a/src/src/components/ChartPanel/ChartPopover/ChartPopover.scss +++ b/src/src/components/ChartPanel/ChartPopover/ChartPopover.scss @@ -1,5 +1,7 @@ @use 'src/styles/abstracts' as *; - +.CustomChartPopover_content{ + width: auto !important; +} .ChartPopover { pointer-events: none; z-index: 5 !important; diff --git a/src/src/components/ChartPanel/ChartPopover/ChartPopover.tsx b/src/src/components/ChartPanel/ChartPopover/ChartPopover.tsx index c302e2f7..5fed3f5b 100644 --- a/src/src/components/ChartPanel/ChartPopover/ChartPopover.tsx +++ b/src/src/components/ChartPanel/ChartPopover/ChartPopover.tsx @@ -152,11 +152,15 @@ function ChartPopover(props: IChartPopover): JSX.Element { }} transformOrigin={{ vertical: 'top', horizontal: 'left' }} classes={{ - paper: classNames('ChartPopover__content', { - ChartPopover__content__active: props.focusedState?.active, - ChartPopover__content__pinned: isPopoverPinned, - hide: !popoverPos, - }), + paper: classNames( + 'ChartPopover__content', + 'CustomChartPopover_content', + { + ChartPopover__content__active: props.focusedState?.active, + ChartPopover__content__pinned: isPopoverPinned, + hide: !popoverPos, + }, + ), }} > diff --git a/src/src/components/ChartPanel/PopoverContent/PopoverContent.scss b/src/src/components/ChartPanel/PopoverContent/PopoverContent.scss index 13c69543..af99285b 100644 --- a/src/src/components/ChartPanel/PopoverContent/PopoverContent.scss +++ b/src/src/components/ChartPanel/PopoverContent/PopoverContent.scss @@ -55,13 +55,12 @@ overflow: unset; .PopoverContent__boxWrapper { display: flex; - max-width: 20rem; flex: 1; &.pinned { flex-direction: column; overflow-y: auto; - width: toRem(250px); + width: auto; .PopoverContent__box { overflow-y: unset; } From fbcd0ff8517c9c5b0d51a2112a667a45dd6aae51 Mon Sep 17 00:00:00 2001 From: Software Developer <7852635+dsuhinin@users.noreply.github.com> Date: Tue, 21 May 2024 17:45:53 +0200 Subject: [PATCH 50/79] Make it possible to preselect run hash in Metrics Explorer (#85) --- .gitignore | 2 ++ .../components/SelectForm/SelectForm.tsx | 12 ++++++- src/src/pages/RunDetail/RunDetail.tsx | 36 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7a2976b6..b68b0baa 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ embed/ +.idea/ +.vscode/ diff --git a/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx b/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx index d77278ef..50128712 100644 --- a/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx +++ b/src/src/pages/Metrics/components/SelectForm/SelectForm.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useLocation } from 'react-router-dom'; import classNames from 'classnames'; import { Box, Divider, Tooltip } from '@material-ui/core'; @@ -147,6 +148,13 @@ function SelectForm({ if (selectedCount > 0) { metricsButtonText += ` (${selectedCount})`; } + + let selectedRunHash = useLocation()?.runProps?.hash; + let prefilledQuery = ''; + if (selectedRunHash !== undefined) { + prefilledQuery = `run.hash == "${selectedRunHash}"`; + } + return (
@@ -161,7 +169,9 @@ function SelectForm({ { setActiveTab(newValue); } + function handleOnClickViewInMetricsExplorer(experiment: any) { + if (experiment !== undefined) { + const selectedExperiments = getSelectedExperiments(); + if ( + !selectedExperiments.find( + (e) => e.id === experiment.id && e.name === experiment.name, + ) + ) { + onSelectExperimentChange(experiment); + } + } + } + function onRunsSelectToggle() { setIsRunSelectDropdownOpen(!isRunSelectDropdownOpen); } @@ -354,6 +369,27 @@ function RunDetail(): React.FunctionComponentElement { Open in Classic UI
+ + handleOnClickViewInMetricsExplorer( + runData?.runInfo?.experiment, + ) + } + > +
+ + View in Metrics Explorer +
+
) : ( Date: Wed, 22 May 2024 07:21:20 -0700 Subject: [PATCH 51/79] Add support for grouping charts by multiple conditions (#82) * Add ParamsScalePopover interfaces * Add preliminary ParamsScalePopover component * Add onParamsScaleTypeChange logic * Add ParamsScalePopover to Params Controls * Improve ParamsScalePopover logic * Load selected params into component * Store params scale type state * Unify props into single variable * Fix onParamsScaleTypeChange logic to handle selectedParams state * Add scale support for high plot charts * Add analytics * Remove todos * Fix popover styling bug * Disable controls for Point params * Fix chart scale state not changing * Add tooltip overriding for ToggleButton * Fix ParamsScalePopover title/button styling issue * Add preliminary ChartPopoverAdvanced * Add ChartPopoverAdvanced component * Add grouping condition types and functions * Add onGroupingConditionsChange helper * Add evaluateCondtion helper * Update getLegendsData * Add chart title conditions support * Add support for conditions to createAppModel * Fix missing props issue * Fix button styling * Improve code style --------- Co-authored-by: Geoffrey Wilson --- src/src/components/Grouping/Grouping.tsx | 4 + src/src/config/grouping/GroupingPopovers.ts | 3 +- .../GroupingPopover/GroupingPopover.tsx | 1 + src/src/pages/Metrics/Metrics.tsx | 1 + src/src/pages/Metrics/MetricsContainer.tsx | 1 + .../ChartPopover/ChartPopoverAdvanced.scss | 58 +++++ .../ChartPopover/ChartPopoverAdvanced.tsx | 246 ++++++++++++++++++ .../models/explorer/metricsModelMethods.ts | 51 +++- .../GroupingPopover/GroupingPopover.d.ts | 4 + .../pages/components/Grouping/Grouping.d.ts | 1 + src/src/types/pages/metrics/Metrics.d.ts | 5 +- .../models/explorer/createAppModel.d.ts | 2 + .../models/metrics/metricsAppModel.d.ts | 6 + src/src/utils/app/evaluateCondition.ts | 34 +++ src/src/utils/app/getChartTitleData.ts | 19 +- src/src/utils/app/getLegendsData.tsx | 21 +- .../utils/app/onGroupingConditionsChange.ts | 33 +++ 17 files changed, 479 insertions(+), 11 deletions(-) create mode 100644 src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.scss create mode 100644 src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.tsx create mode 100644 src/src/utils/app/evaluateCondition.ts create mode 100644 src/src/utils/app/onGroupingConditionsChange.ts diff --git a/src/src/components/Grouping/Grouping.tsx b/src/src/components/Grouping/Grouping.tsx index 6b66295c..c0331ef5 100644 --- a/src/src/components/Grouping/Grouping.tsx +++ b/src/src/components/Grouping/Grouping.tsx @@ -23,6 +23,7 @@ function Grouping({ onGroupingPersistenceChange, onGroupingApplyChange, onShuffleChange, + onGroupingConditionsChange, groupingPopovers = GroupingPopovers, isDisabled = false, }: IGroupingProps): React.FunctionComponentElement { @@ -63,6 +64,9 @@ function Grouping({ onGroupingPaletteChange, paletteIndex: groupingData?.paletteIndex, })} + groupingSelectOptions={groupingSelectOptions} + onSelect={onGroupingSelectChange} + onGroupingConditionsChange={onGroupingConditionsChange} /> ) } diff --git a/src/src/config/grouping/GroupingPopovers.ts b/src/src/config/grouping/GroupingPopovers.ts index 4c4f3a0e..75b08a1f 100644 --- a/src/src/config/grouping/GroupingPopovers.ts +++ b/src/src/config/grouping/GroupingPopovers.ts @@ -1,3 +1,4 @@ +import ChartPopoverAdvanced from 'pages/Metrics/components/ChartPopover/ChartPopoverAdvanced'; import ColorPopoverAdvanced from 'pages/Metrics/components/ColorPopoverAdvanced/ColorPopoverAdvanced'; import StrokePopoverAdvanced from 'pages/Metrics/components/StrokePopover/StrokePopoverAdvanced'; @@ -24,7 +25,7 @@ const GroupingPopovers: IGroupingPopovers[] = [ { groupName: GroupNameEnum.CHART, title: 'Divide into charts', - inputLabel: 'Select fields to divide into charts', + AdvancedComponent: ChartPopoverAdvanced, }, { groupName: GroupNameEnum.ROW, diff --git a/src/src/modules/BaseExplorer/components/Grouping/GroupingPopover/GroupingPopover.tsx b/src/src/modules/BaseExplorer/components/Grouping/GroupingPopover/GroupingPopover.tsx index 55d2dec7..72cce82c 100644 --- a/src/src/modules/BaseExplorer/components/Grouping/GroupingPopover/GroupingPopover.tsx +++ b/src/src/modules/BaseExplorer/components/Grouping/GroupingPopover/GroupingPopover.tsx @@ -253,6 +253,7 @@ function GroupingPopover(props: IGroupingPopoverProps) { + {/* Add the advancedComponent and give it the engine as props */} {advancedComponent} diff --git a/src/src/pages/Metrics/Metrics.tsx b/src/src/pages/Metrics/Metrics.tsx index dbc07852..13d1837f 100644 --- a/src/src/pages/Metrics/Metrics.tsx +++ b/src/src/pages/Metrics/Metrics.tsx @@ -137,6 +137,7 @@ function Metrics( onGroupingReset={props.onGroupingReset} onGroupingApplyChange={props.onGroupingApplyChange} onGroupingPersistenceChange={props.onGroupingPersistenceChange} + onGroupingConditionsChange={props.onGroupingConditionsChange} onShuffleChange={props.onShuffleChange} />
diff --git a/src/src/pages/Metrics/MetricsContainer.tsx b/src/src/pages/Metrics/MetricsContainer.tsx index 5df2a791..d4965d93 100644 --- a/src/src/pages/Metrics/MetricsContainer.tsx +++ b/src/src/pages/Metrics/MetricsContainer.tsx @@ -193,6 +193,7 @@ function MetricsContainer(): React.FunctionComponentElement { onActivePointChange={metricAppModel.onActivePointChange} onGroupingApplyChange={metricAppModel.onGroupingApplyChange} onGroupingPersistenceChange={metricAppModel.onGroupingPersistenceChange} + onGroupingConditionsChange={metricAppModel.onGroupingConditionsChange} onBookmarkCreate={metricAppModel.onBookmarkCreate} onBookmarkUpdate={metricAppModel.onBookmarkUpdate} onNotificationAdd={metricAppModel.onNotificationAdd} diff --git a/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.scss b/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.scss new file mode 100644 index 00000000..36e81ca2 --- /dev/null +++ b/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.scss @@ -0,0 +1,58 @@ +@use 'src/styles/abstracts' as *; + +.ChartPopoverAdvanced { + width: 25rem; + + &__Switcher__button__container { + height: 1.75rem; + display: flex; + align-items: center; + .ChartPopoverAdvanced__span { + margin-left: 0.875rem; + } + } + &__conditionalFilter { + padding: 1rem; + border-bottom: $border-main; + } + + &__conditionalFilter__p { + margin: 0.5rem 0 1rem 0; + } + + &__preferred__colors { + padding: 1rem; + h3 { + margin-bottom: 0.5rem; + } + .ChartPopoverAdvanced__span { + flex: 1; + } + } + &__container__select__fieldName { + width: 100%; + flex: 3; + } + &__container__select__value { + flex: 2; + .MuiInputBase-inputMarginDense { + padding: 10px; + font-size: 14px; + } + } + &__container__select__button { + margin-top: 0.5rem; + margin-bottom: 0.5rem; + } + &__conditionalFilter__box { + margin-bottom: 0.5rem; + + &__p { + font-family: 'Courier New', Courier, monospace; + } + + &__button { + min-width: 32px; + } + } +} diff --git a/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.tsx b/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.tsx new file mode 100644 index 00000000..a48d2693 --- /dev/null +++ b/src/src/pages/Metrics/components/ChartPopover/ChartPopoverAdvanced.tsx @@ -0,0 +1,246 @@ +import { useState, useMemo } from 'react'; + +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, Text } from 'components/kit'; +import ErrorBoundary from 'components/ErrorBoundary'; + +import { + IGroupingCondition, + IGroupingSelectOption, +} from 'types/services/models/metrics/metricsAppModel'; +import { IGroupingPopoverAdvancedProps } from 'types/components/GroupingPopover/GroupingPopover'; + +import './ChartPopoverAdvanced.scss'; + +export enum IOperator { + '==' = '==', + '!=' = '!=', + '>' = '>', + '<' = '<', + '>=' = '>=', + '<=' = '<=', +} + +function ChartPopoverAdvanced({ + onGroupingConditionsChange, + groupingData, + 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 || [], + ); + + 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); + }; + + 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]); + + return ( + +
+
+ + 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} + + + + ))} +
+
+
+
+ ); +} + +export default ChartPopoverAdvanced; diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index a1131d5f..50d21863 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -52,6 +52,7 @@ import { ISmoothing, ITooltip, LegendsConfig, + IGroupingCondition, } from 'types/services/models/metrics/metricsAppModel'; import { IMetricTrace, @@ -91,7 +92,9 @@ import onChangeTooltip from 'utils/app/onChangeTooltip'; import onColumnsOrderChange from 'utils/app/onColumnsOrderChange'; import onColumnsVisibilityChange from 'utils/app/onColumnsVisibilityChange'; import onDefaultColumnsVisibilityChange from 'utils/app/onDefaultColumnsVisibilityChange'; +import evaluateCondition from 'utils/app/evaluateCondition'; import onGroupingApplyChange from 'utils/app/onGroupingApplyChange'; +import onGroupingConditionsChange from 'utils/app/onGroupingConditionsChange'; import onGroupingModeChange from 'utils/app/onGroupingModeChange'; import onGroupingPaletteChange from 'utils/app/onGroupingPaletteChange'; import onGroupingPersistenceChange from 'utils/app/onGroupingPersistenceChange'; @@ -1059,6 +1062,12 @@ function getMetricsAppModelMethods( const configData = model.getState()!.config; const grouping = configData!.grouping; const { paletteIndex = 0 } = grouping || {}; + + const conditions: IGroupingCondition[] = grouping.conditions || []; + const conditionStrings = conditions.map( + (condition) => + `${condition.fieldName} ${condition.operator} ${condition.value}`, + ); const groupByColor = getFilteredGroupingOptions({ groupName: GroupNameEnum.COLOR, model, @@ -1067,10 +1076,11 @@ function getMetricsAppModelMethods( groupName: GroupNameEnum.STROKE, model, }); + const groupByChart = getFilteredGroupingOptions({ groupName: GroupNameEnum.CHART, model, - }); + }).concat(conditionStrings); if ( groupByColor.length === 0 && groupByStroke.length === 0 && @@ -1096,10 +1106,39 @@ function getMetricsAppModelMethods( ); for (let i = 0; i < data.length; i++) { - const groupValue: { [key: string]: string } = {}; + const groupValue: { [key: string]: any } = {}; groupingFields.forEach((field) => { groupValue[field] = getValue(data[i], field); }); + + // Evaluate the conditions and update the row + conditionStrings.forEach((conditionString, j) => { + // Evaluate the condition + const condition = conditions[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]); @@ -1682,6 +1721,14 @@ function getMetricsAppModelMethods( setAggregationEnabled, }); }, + onGroupingConditionsChange(conditions: IGroupingCondition[]): void { + onGroupingConditionsChange({ + conditions, + model, + appName, + updateModelData, + }); + }, onShuffleChange(name: 'color' | 'stroke'): void { onShuffleChange({ name, model, updateModelData }); }, diff --git a/src/src/types/components/GroupingPopover/GroupingPopover.d.ts b/src/src/types/components/GroupingPopover/GroupingPopover.d.ts index e47d8c47..7034b227 100644 --- a/src/src/types/components/GroupingPopover/GroupingPopover.d.ts +++ b/src/src/types/components/GroupingPopover/GroupingPopover.d.ts @@ -25,4 +25,8 @@ export interface IGroupingPopoverAdvancedProps { onPersistenceChange: IMetricProps['onGroupingPersistenceChange']; onGroupingPaletteChange?: IMetricProps['onGroupingPaletteChange']; onShuffleChange: IMetricProps['onShuffleChange']; + groupingSelectOptions?: 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 3d246387..3b924a08 100644 --- a/src/src/types/pages/components/Grouping/Grouping.d.ts +++ b/src/src/types/pages/components/Grouping/Grouping.d.ts @@ -20,6 +20,7 @@ export interface IGroupingProps { onGroupingApplyChange: IMetricProps['onGroupingApplyChange']; onGroupingPersistenceChange: IMetricProps['onGroupingPersistenceChange']; onShuffleChange: IMetricProps['onShuffleChange']; + onGroupingConditionsChange?: IMetricProps['onGroupingConditionsChange']; groupingPopovers?: IGroupingPopovers[]; isDisabled?: boolean; } diff --git a/src/src/types/pages/metrics/Metrics.d.ts b/src/src/types/pages/metrics/Metrics.d.ts index faf479c5..4cd6ffd9 100644 --- a/src/src/types/pages/metrics/Metrics.d.ts +++ b/src/src/types/pages/metrics/Metrics.d.ts @@ -6,6 +6,8 @@ import { ResizeModeEnum } from 'config/enums/tableEnums'; import { DensityOptions } from 'config/enums/densityEnum'; import { RequestStatusEnum } from 'config/enums/requestStatusEnum'; +import { IExperimentDataShort } from 'modules/core/api/experimentsApi'; + import { IGroupingConfig, ISelectConfig, @@ -30,6 +32,7 @@ import { ISmoothing, LegendsDataType, LegendsConfig, + IGroupingCondition, } from 'types/services/models/metrics/metricsAppModel'; import { ITableColumn } from 'types/components/TableColumns/TableColumns'; import { IChartPanelRef } from 'types/components/ChartPanel/ChartPanel'; @@ -47,7 +50,6 @@ import { ITagInfo } from 'types/tags/Tags'; import { HighlightEnum } from 'utils/d3'; import { IRequestProgress } from 'utils/app/setRequestProgress'; -import { IExperimentDataShort } from 'modules/core/api/experimentsApi'; export interface IMetricProps extends Partial { tableRef: React.RefObject; @@ -123,6 +125,7 @@ export interface IMetricProps extends Partial { onGroupingReset: (groupName: GroupNameEnum) => void; onGroupingApplyChange: (groupName: GroupNameEnum) => void; onGroupingPersistenceChange: (groupName: 'color' | 'stroke') => void; + onGroupingConditionsChange: (conditions: IGroupingCondition[]) => 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 43a6cd23..475408b0 100644 --- a/src/src/types/services/models/explorer/createAppModel.d.ts +++ b/src/src/types/services/models/explorer/createAppModel.d.ts @@ -18,6 +18,7 @@ import { ITooltipConfig, ISmoothing, LegendsConfig, + IGroupingCondition, } from 'types/services/models/metrics/metricsAppModel'; import { IParamsAppModelState } from 'types/services/models/params/paramsAppModel'; import { IRunsAppModelState } from 'types/services/models/runs/runsAppModel'; @@ -90,6 +91,7 @@ export interface IGroupingConfig { stroke: number; }; paletteIndex?: number; + conditions?: IGroupingCondition[]; } export interface ISelectOption { diff --git a/src/src/types/services/models/metrics/metricsAppModel.d.ts b/src/src/types/services/models/metrics/metricsAppModel.d.ts index d40a4792..cd373f75 100644 --- a/src/src/types/services/models/metrics/metricsAppModel.d.ts +++ b/src/src/types/services/models/metrics/metricsAppModel.d.ts @@ -286,3 +286,9 @@ export interface LegendColumnDataType { export interface LegendsDataType { [key: string]: Record; } + +export interface IGroupingCondition { + fieldName: string; + operator: IOperator; + value: string | number; +} diff --git a/src/src/utils/app/evaluateCondition.ts b/src/src/utils/app/evaluateCondition.ts new file mode 100644 index 00000000..e359b2f8 --- /dev/null +++ b/src/src/utils/app/evaluateCondition.ts @@ -0,0 +1,34 @@ +import { IGroupingCondition } from 'types/services/models/metrics/metricsAppModel'; + +export default function evaluateCondition( + value: string, + condition: IGroupingCondition, +): boolean { + const { operator, value: conditionValue } = condition; + + if (operator === '==') { + return value === conditionValue; + } + + if (operator === '!=') { + return value !== conditionValue; + } + + if (operator === '>=') { + return Number(value) >= Number(conditionValue); + } + + if (operator === '<=') { + return Number(value) <= Number(conditionValue); + } + + if (operator === '>') { + return Number(value) > Number(conditionValue); + } + + if (operator === '<') { + return Number(value) < Number(conditionValue); + } + + return false; +} diff --git a/src/src/utils/app/getChartTitleData.ts b/src/src/utils/app/getChartTitleData.ts index 67c40e58..00dd84bf 100644 --- a/src/src/utils/app/getChartTitleData.ts +++ b/src/src/utils/app/getChartTitleData.ts @@ -6,6 +6,7 @@ import { DATE_WITH_SECONDS } from 'config/dates/dates'; import { IChartTitle, IChartTitleData, + IGroupingCondition, IGroupingSelectOption, IMetricsCollection, } from 'types/services/models/metrics/metricsAppModel'; @@ -28,9 +29,19 @@ export default function getChartTitleData({ } const groupData = model.getState()?.config?.grouping; let chartTitleData: IChartTitleData = {}; + + // Get the list of conditions as strings + const conditions: IGroupingCondition[] = groupData.conditions?.map( + (condition: IGroupingCondition) => + `${condition.fieldName} ${condition.operator} ${condition.value}`, + ); + + // Add the conditions to the chart names array + const chartNames = groupData.chart.concat(conditions || []); + processedData.forEach((metricsCollection) => { if (!chartTitleData[metricsCollection.chartIndex]) { - chartTitleData[metricsCollection.chartIndex] = groupData.chart.reduce( + chartTitleData[metricsCollection.chartIndex] = chartNames.reduce( (acc: IChartTitle, groupItemKey: string) => { if (metricsCollection.config?.hasOwnProperty(groupItemKey)) { const value = metricsCollection.config[groupItemKey]; @@ -45,8 +56,10 @@ export default function getChartTitleData({ : value, ); } else { - acc[getValueByField(groupingSelectOptions || [], groupItemKey)] = - formatValue(value); + acc[ + getValueByField(groupingSelectOptions || [], groupItemKey) || + groupItemKey + ] = formatValue(value); } } return acc; diff --git a/src/src/utils/app/getLegendsData.tsx b/src/src/utils/app/getLegendsData.tsx index e9d2f5e8..e8f4c934 100644 --- a/src/src/utils/app/getLegendsData.tsx +++ b/src/src/utils/app/getLegendsData.tsx @@ -36,16 +36,28 @@ function getLegendsData( for (let groupName of groupingNames) { const legendRowData: Record = {}; const groupPropKey = groupingPropKeys[groupName]; - const groupedItemPropKeys = groupingConfig[groupName] || []; + + const groupConfig = groupingConfig[groupName]; + const groupedItemPropKeys = + groupName !== GroupNameEnum.CHART + ? groupConfig || [] + : groupConfig?.concat( + groupingConfig.conditions?.map( + (condition) => + `${condition.fieldName} ${condition.operator} ${condition.value}`, + ) || [], + ) || []; if (groupedItemPropKeys.length > 0) { for (const item of processedData) { const config: Record = {}; for (const propKey of groupedItemPropKeys) { - const key = getValueByField(groupingSelectOptions, propKey); + const key = + getValueByField(groupingSelectOptions, propKey) || propKey; const value = item.config?.[propKey]; config[key] = formatValue(value); } + const legendRow = { [groupPropKey]: item[groupPropKey], config, @@ -55,9 +67,10 @@ function getLegendsData( legendRowData[hashed] = legendRow; } } - const keys = groupedItemPropKeys.map((item) => - getValueByField(groupingSelectOptions, item), + const keys = groupedItemPropKeys.map( + (item) => getValueByField(groupingSelectOptions, item) || item, ); + const uniqueRows = Object.values(legendRowData); const groupedByColumns: Record = {}; for (let key of keys) { diff --git a/src/src/utils/app/onGroupingConditionsChange.ts b/src/src/utils/app/onGroupingConditionsChange.ts new file mode 100644 index 00000000..ec65b3d7 --- /dev/null +++ b/src/src/utils/app/onGroupingConditionsChange.ts @@ -0,0 +1,33 @@ +import * as analytics from 'services/analytics'; + +import { IModel, State } from 'types/services/models/model'; +import { IAppModelConfig } from 'types/services/models/explorer/createAppModel'; +import { IGroupingCondition } from 'types/services/models/metrics/metricsAppModel'; + +import resetChartZoom from './resetChartZoom'; + +export default function onGroupingConditionsChange({ + conditions, + model, + appName, + updateModelData, +}: { + conditions: IGroupingCondition[]; + model: IModel; + appName: string; + updateModelData: ( + configData: IAppModelConfig | any, + shouldURLUpdate?: boolean, + ) => void; +}) { + let configData = model.getState()?.config; + + if (configData?.grouping) { + configData.grouping = { ...configData.grouping, conditions }; + configData = resetChartZoom({ configData, appName }); + updateModelData(configData, true); + } + analytics.trackEvent( + `[${appName}Explorer] Group by condition: ${conditions}`, + ); +} From 3abb28773f5cef15ec4b68d8b59c5838f0fa6539 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Wed, 22 May 2024 14:15:44 -0700 Subject: [PATCH 52/79] Metric Tooltip Rounding (#84) * Add metric rounding for chart popover * Fix params and scatters rounding * Extract rounding to util function * Add y-axis label rounding --- .../PopoverContent/PopoverContent.tsx | 9 +++++---- src/src/utils/d3/drawHoverAttributes.ts | 20 +++++++++++-------- src/src/utils/roundToSignificantDigits.ts | 16 +++++++++++++++ 3 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 src/src/utils/roundToSignificantDigits.ts diff --git a/src/src/components/ChartPanel/PopoverContent/PopoverContent.tsx b/src/src/components/ChartPanel/PopoverContent/PopoverContent.tsx index 21cfa1ec..20d0999e 100644 --- a/src/src/components/ChartPanel/PopoverContent/PopoverContent.tsx +++ b/src/src/components/ChartPanel/PopoverContent/PopoverContent.tsx @@ -27,6 +27,7 @@ import { formatSystemMetricName } from 'utils/formatSystemMetricName'; import getValueByField from 'utils/getValueByField'; import { isMetricHash } from 'utils/isMetricHash'; import { decode } from 'utils/encoder/encoder'; +import { roundToSignificantDigits } from 'utils/roundToSignificantDigits'; import './PopoverContent.scss'; @@ -75,7 +76,7 @@ const PopoverContent = React.forwardRef(function PopoverContent( {contextToString(context)} - {focusedState?.yValue} + {roundToSignificantDigits(Number(focusedState?.yValue))}
@@ -127,7 +128,7 @@ const PopoverContent = React.forwardRef(function PopoverContent( {context || null}
- Value: {focusedState?.yValue} + Value: {roundToSignificantDigits(Number(focusedState?.yValue))}
@@ -168,7 +169,7 @@ const PopoverContent = React.forwardRef(function PopoverContent( Y: - {focusedState?.yValue} + {roundToSignificantDigits(Number(focusedState?.yValue))}
@@ -176,7 +177,7 @@ const PopoverContent = React.forwardRef(function PopoverContent( X: - {focusedState?.xValue} + {roundToSignificantDigits(Number(focusedState?.xValue))}
diff --git a/src/src/utils/d3/drawHoverAttributes.ts b/src/src/utils/d3/drawHoverAttributes.ts index d6c1b587..6ba84496 100644 --- a/src/src/utils/d3/drawHoverAttributes.ts +++ b/src/src/utils/d3/drawHoverAttributes.ts @@ -16,6 +16,7 @@ import { IFocusedState } from 'types/services/models/metrics/metricsAppModel'; import { AggregationAreaMethods } from 'utils/aggregateGroupData'; import getRoundedValue from 'utils/roundValue'; +import { roundToSignificantDigits } from 'utils/roundToSignificantDigits'; import { formatValueByAlignment } from '../formatByAlignment'; @@ -183,12 +184,13 @@ function drawHoverAttributes(args: IDrawHoverAttributesArgs): void { } const x = attrRef.current.xScale(xValue); + // prettier-ignore const left = x - xAxisValueWidth / 2 < 0 ? axisLeftEdge + xAxisValueWidth / 2 : x + axisLeftEdge + xAxisValueWidth / 2 > axisRightEdge - ? axisRightEdge - xAxisValueWidth / 2 - : x + axisLeftEdge; + ? axisRightEdge - xAxisValueWidth / 2 + : x + axisLeftEdge; const top = height - margin.bottom + 1; if (xAxisLabelNodeRef.current && xAxisValueWidth) { @@ -235,34 +237,36 @@ function drawHoverAttributes(args: IDrawHoverAttributesArgs): void { const yAxisValueHeight = yAxisLabelNodeRef.current?.node()?.offsetHeight || 0; const y = attrRef.current.yScale(yValue); + // prettier-ignore const top = y - yAxisValueHeight / 2 < 0 ? axisTopEdge + yAxisValueHeight / 2 : y + axisTopEdge + yAxisValueHeight / 2 > axisBottomEdge - ? axisBottomEdge - yAxisValueHeight / 2 - : y + axisTopEdge; + ? axisBottomEdge - yAxisValueHeight / 2 + : y + axisTopEdge; const right = width - margin.left; const maxWidth = margin.left - 5; + const yValueRounded = roundToSignificantDigits(Number(yValue)); if (yAxisLabelNodeRef.current && yAxisValueHeight) { // update y-axis label yAxisLabelNodeRef.current - .attr('title', yValue) + .attr('title', yValueRounded) .style('top', `${top}px`) .style('right', `${right}px`) .style('max-width', `${maxWidth}px`) - .text(yValue); + .text(yValueRounded); } else { // create y-axis label yAxisLabelNodeRef.current = visArea .append('div') .attr('class', 'ChartMouseValue ChartMouseValueYAxis') - .attr('title', yValue) + .attr('title', yValueRounded) .style('top', `${top}px`) .style('right', `${right}px`) .style('max-width', `${maxWidth}px`) - .text(yValue); + .text(yValueRounded); } } } diff --git a/src/src/utils/roundToSignificantDigits.ts b/src/src/utils/roundToSignificantDigits.ts new file mode 100644 index 00000000..d011cdde --- /dev/null +++ b/src/src/utils/roundToSignificantDigits.ts @@ -0,0 +1,16 @@ +/** + * Rounds a number to a specified number of significant digits. + * @param value the number which needs to be rounded + * @param sig the number of significant digits + * @returns the rounded number + */ +export const roundToSignificantDigits = (value: number, sig: number = 5) => { + if (value === 0) { + return 0; + } + + const power = sig - Math.ceil(Math.log10(Math.abs(value))); + const magnitude = Math.pow(10, power); + const shifted = Math.round(value * magnitude); + return shifted / magnitude; +}; From cacae46c14ef8710f0849bd7b0f9f7421b5d1be8 Mon Sep 17 00:00:00 2001 From: Juan Escalada <97265671+jescalada@users.noreply.github.com> Date: Wed, 29 May 2024 07:04:34 -0700 Subject: [PATCH 53/79] Fix dynamic grouping table cells --- src/src/services/models/explorer/metricsModelMethods.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/src/services/models/explorer/metricsModelMethods.ts b/src/src/services/models/explorer/metricsModelMethods.ts index 50d21863..eb3e2a14 100644 --- a/src/src/services/models/explorer/metricsModelMethods.ts +++ b/src/src/services/models/explorer/metricsModelMethods.ts @@ -427,7 +427,7 @@ function getMetricsAppModelMethods( if (metricsCollection.config !== null) { const groupConfigData: { [key: string]: unknown } = {}; for (let key in metricsCollection.config) { - groupConfigData[getValueByField(groupingSelectOptions, key)] = + groupConfigData[getValueByField(groupingSelectOptions, key) || key] = metricsCollection.config[key]; } const groupHeaderRow = { From edf5cd821c1863026a3188b84cefa420b481481e Mon Sep 17 00:00:00 2001 From: fabiovincenzi <93596376+fabiovincenzi@users.noreply.github.com> Date: Wed, 29 May 2024 08:04:15 -0700 Subject: [PATCH 54/79] Individual Plot Axis Properties (#88) --- .../AxesPropsPopover/AxesPropsPopover.d.ts | 10 +- .../AxesPropsPopover/AxesPropsPopover.scss | 1 + .../AxesPropsPopover/AxesPropsPopover.tsx | 92 ++++++++++++++----- src/src/components/ChartPanel/ChartPanel.tsx | 2 +- .../MultipleAxesPropsPopover.d.ts | 5 + .../MultipleAxesPropsPopover.scss | 25 +++++ .../MultipleAxesPropsPopover.tsx | 91 ++++++++++++++++++ .../MultipleAxesPropsPopover/index.ts | 5 + .../config/controls/controlsDefaultConfig.ts | 6 ++ .../Metrics/hooks/useAlignMetricsData.tsx | 1 - src/src/pages/Metrics/Metrics.tsx | 46 +++++----- src/src/pages/Metrics/MetricsContainer.tsx | 4 +- .../Metrics/components/Controls/Controls.tsx | 40 +++++--- src/src/services/models/explorer/config.ts | 17 +++- .../models/explorer/metricsModelMethods.ts | 41 ++++++++- .../components/ChartPanel/ChartPanel.d.ts | 2 +- src/src/types/pages/metrics/Metrics.d.ts | 8 +- .../metrics/components/Controls/Controls.d.ts | 4 +- .../models/explorer/createAppModel.d.ts | 3 +- src/src/utils/app/onAlignmentTypeChange.ts | 24 +++-- src/src/utils/app/onAxesScaleRangeChange.ts | 32 ++++--- src/src/utils/d3/drawArea.ts | 13 +-- src/src/utils/d3/utils.ts | 15 +++ 23 files changed, 375 insertions(+), 112 deletions(-) create mode 100644 src/src/components/MultipleAxesPropsPopover/MultipleAxesPropsPopover.d.ts create mode 100644 src/src/components/MultipleAxesPropsPopover/MultipleAxesPropsPopover.scss create mode 100644 src/src/components/MultipleAxesPropsPopover/MultipleAxesPropsPopover.tsx create mode 100644 src/src/components/MultipleAxesPropsPopover/index.ts create mode 100644 src/src/utils/d3/utils.ts diff --git a/src/src/components/AxesPropsPopover/AxesPropsPopover.d.ts b/src/src/components/AxesPropsPopover/AxesPropsPopover.d.ts index 753e26e8..1cf70801 100644 --- a/src/src/components/AxesPropsPopover/AxesPropsPopover.d.ts +++ b/src/src/components/AxesPropsPopover/AxesPropsPopover.d.ts @@ -2,15 +2,19 @@ import { IMetricProps } from 'types/pages/metrics/Metrics'; import { IAlignmentConfig } from 'types/services/models/metrics/metricsAppModel'; import { ISelectOption } from 'types/services/models/explorer/createAppModel'; -export interface IAxesPropsPopoverProps { +export interface IBaseAxesPopoverProps { onAlignmentMetricChange: IMetricProps['onAlignmentMetricChange']; onAlignmentTypeChange: IMetricProps['onAlignmentTypeChange']; - alignmentConfig: IAlignmentConfig; + alignmentConfigs: IAlignmentConfig[]; selectFormOptions: ISelectOption[]; - axesScaleRange: IAxesScaleRange; + axesScaleRanges: IAxesScaleRange[]; onAxesScaleRangeChange: IMetricProps['onAxesScaleRangeChange']; } +export interface IAxesPropsPopoverProps extends IBaseAxesPopoverProps { + selectedIds: number[]; +} + export interface IAxesScaleRange { yAxis: { min?: number; max?: number }; xAxis: { min?: number; max?: number }; diff --git a/src/src/components/AxesPropsPopover/AxesPropsPopover.scss b/src/src/components/AxesPropsPopover/AxesPropsPopover.scss index 14570ea4..5946f69d 100644 --- a/src/src/components/AxesPropsPopover/AxesPropsPopover.scss +++ b/src/src/components/AxesPropsPopover/AxesPropsPopover.scss @@ -3,6 +3,7 @@ .AxesPropsPopover { width: 24.375rem; padding: $space-xs; + padding-top: 0px; &__subtitle { padding-block: $space-xs; text-transform: uppercase; diff --git a/src/src/components/AxesPropsPopover/AxesPropsPopover.tsx b/src/src/components/AxesPropsPopover/AxesPropsPopover.tsx index 40196df0..f969ba4e 100644 --- a/src/src/components/AxesPropsPopover/AxesPropsPopover.tsx +++ b/src/src/components/AxesPropsPopover/AxesPropsPopover.tsx @@ -29,12 +29,13 @@ import { import './AxesPropsPopover.scss'; function AxesPropsPopover({ + selectedIds, onAlignmentTypeChange, onAlignmentMetricChange, onAxesScaleRangeChange, - alignmentConfig, + alignmentConfigs, selectFormOptions, - axesScaleRange, + axesScaleRanges, }: IAxesPropsPopoverProps): React.FunctionComponentElement { const [yScaleRange, setYScaleRange] = React.useState({}); const [isYScaleRangeValid, setIsYScaleRangeValid] = @@ -55,11 +56,16 @@ function AxesPropsPopover({ if (option.group === 'METRIC') { onAlignmentMetricChange(option.value); } else { - onAlignmentTypeChange(option.value as AlignmentOptionsEnum); + selectedIds.forEach((selectedId) => { + onAlignmentTypeChange( + selectedId, + option.value as AlignmentOptionsEnum, + ); + }); } } }, - [onAlignmentMetricChange, onAlignmentTypeChange], + [onAlignmentMetricChange, onAlignmentTypeChange, selectedIds], ); const alignmentOptions: ISelectOption[] = React.useMemo(() => { @@ -83,10 +89,19 @@ function AxesPropsPopover({ }, [selectFormOptions]); const selected = React.useMemo(() => { - return alignmentConfig?.type === AlignmentOptionsEnum.CUSTOM_METRIC - ? alignmentConfig.metric - : alignmentConfig.type; - }, [alignmentConfig]); + const filtered = alignmentConfigs.filter((alignment, index) => + selectedIds.includes(index), + ); + if (filtered.length === 0) { + return null; + } + return filtered.every( + (alignmentConfig) => + alignmentConfig?.type === AlignmentOptionsEnum.CUSTOM_METRIC, + ) + ? filtered[0].metric + : filtered[0].type; + }, [alignmentConfigs, selectedIds]); const axesProps = React.useMemo( () => ({ @@ -127,8 +142,10 @@ function AxesPropsPopover({ [key]: metadata.isValid, }); if (metadata.isValid) { - onAxesScaleRangeChange({ - [axisType]: { ...scaleRange, [key]: value }, + selectedIds.forEach((selectedId) => { + onAxesScaleRangeChange(selectedId, { + [axisType]: { ...scaleRange, [key]: value }, + }); }); } } @@ -138,11 +155,13 @@ function AxesPropsPopover({ const onResetRange = React.useCallback( (axisType: 'xAxis' | 'yAxis') => { - onAxesScaleRangeChange({ - [axisType]: { min: undefined, max: undefined }, + selectedIds.forEach((selectedId) => { + onAxesScaleRangeChange(selectedId, { + [axisType]: { min: undefined, max: undefined }, + }); }); }, - [onAxesScaleRangeChange], + [onAxesScaleRangeChange, selectedIds], ); const validationPatterns = React.useMemo( @@ -166,17 +185,34 @@ function AxesPropsPopover({ ); React.useEffect(() => { - setXScaleRange((prevState) => - _.isEqual(axesScaleRange.xAxis, prevState) - ? prevState - : axesScaleRange.xAxis, - ); - setYScaleRange((prevState) => - _.isEqual(axesScaleRange.yAxis, prevState) - ? prevState - : axesScaleRange.yAxis, - ); - }, [axesScaleRange]); + selectedIds.forEach((selectedId) => { + setXScaleRange((prevState) => { + if (axesScaleRanges[selectedId] !== undefined) { + return _.isEqual(axesScaleRanges[selectedId].xAxis, prevState) + ? prevState + : axesScaleRanges[selectedId].xAxis; + } else { + return prevState; + } + }); + setYScaleRange((prevState) => { + if (axesScaleRanges[selectedId] !== undefined) { + return _.isEqual(axesScaleRanges[selectedId].yAxis, prevState) + ? prevState + : axesScaleRanges[selectedId].yAxis; + } else { + return prevState; + } + }); + }); + }, [selectedIds, axesScaleRanges]); + + React.useEffect(() => { + if (selectedIds.length > 1) { + onResetRange('xAxis'); + onResetRange('yAxis'); + } + }, [selectedIds]); const xResetBtnDisabled = React.useMemo( () => xScaleRange.min === undefined && xScaleRange.max === undefined, @@ -186,6 +222,7 @@ function AxesPropsPopover({ () => yScaleRange.min === undefined && yScaleRange.max === undefined, [yScaleRange], ); + const isDisabled = selectedIds.length === 0; return (
@@ -195,11 +232,12 @@ function AxesPropsPopover({
@@ -227,6 +265,7 @@ function AxesPropsPopover({ onChange={onScaleRangeChange} validationPatterns={validationPatterns.min(xScaleRange.max)} isValid={isXScaleRangeValid.min} + disabled={isDisabled} />