Skip to content

Commit

Permalink
Experiment Selection Header Searchbox + Quick Action Buttons (#48)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jescalada authored Mar 15, 2024
1 parent b162df6 commit 8e547de
Show file tree
Hide file tree
Showing 19 changed files with 261 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface IExperimentBarProps {
selectedExperimentNames: string[];
getExperimentsData: () => void;
onSelectExperimentNamesChange: (experimentName: string) => void;
onToggleAllExperiments: (experimentNames: string[]) => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function ExperimentBar({
selectedExperimentNames,
getExperimentsData,
onSelectExperimentNamesChange,
onToggleAllExperiments,
}: IExperimentBarProps): React.FunctionComponentElement<React.ReactNode> {
function shortenExperimentName(name?: string): string {
if (!name) {
Expand Down Expand Up @@ -111,6 +112,7 @@ function ExperimentBar({
selectedExperimentNames={selectedExperimentNames}
getExperimentsData={getExperimentsData}
onSelectExperimentNamesChange={onSelectExperimentNamesChange}
onToggleAllExperiments={onToggleAllExperiments}
/>
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export interface IExperimentSelectionPopoverProps {
isExperimentsLoading: boolean;
getExperimentsData: () => void;
onSelectExperimentNamesChange: (experimentName: string) => void;
onToggleAllExperiments: (experimentNames: string[]) => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
}
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -19,6 +24,7 @@ function ExperimentSelectionPopover({
isExperimentsLoading,
getExperimentsData,
onSelectExperimentNamesChange,
onToggleAllExperiments,
}: IExperimentSelectionPopoverProps): React.FunctionComponentElement<React.ReactNode> {
React.useEffect(() => {
if (!experimentsData) {
Expand All @@ -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<string>('');
const [isRegexSearch, setIsRegexSearch] = React.useState(false);
const [invalidRegex, setInvalidRegex] = React.useState<boolean>(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;
}
Expand All @@ -54,6 +71,55 @@ function ExperimentSelectionPopover({
return selectedExperimentNames.includes(experimentName);
}

function handleSearchInputChange(
e: React.ChangeEvent<HTMLInputElement>,
): 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 (
<ErrorBoundary>
<div className='ExperimentSelectionPopover'>
Expand All @@ -67,8 +133,55 @@ function ExperimentSelectionPopover({
<div className='ExperimentSelectionPopover__contentContainer'>
<div className='ExperimentSelectionPopover__contentContainer__experimentsListContainer'>
<div className='ExperimentSelectionPopover__contentContainer__experimentsListContainer__experimentList ScrollBar__hidden'>
<div
className={
invalidRegex
? 'ExperimentSelectionPopover__searchContainer error'
: 'ExperimentSelectionPopover__searchContainer'
}
>
<Tooltip
title={
allExperimentsSelected()
? 'Deselect all visible'
: 'Select all visible'
}
>
<Checkbox
color='primary'
icon={<CheckBoxOutlineBlank />}
checkedIcon={<CheckBoxIcon />}
checked={allExperimentsSelected()}
onChange={() => {
const checked = allExperimentsSelected();
toggleAllExperiments(checked);
}}
size='small'
/>
</Tooltip>

<InputBase
placeholder='Search'
value={searchValue}
onChange={handleSearchInputChange}
inputProps={{ 'aria-label': 'search' }}
className='ExperimentSelectionPopover__searchContainer__inputBase'
/>
<Tooltip title='Use Regular Expression'>
<ToggleButton
value='check'
selected={isRegexSearch}
onChange={() => {
setIsRegexSearch(!isRegexSearch);
}}
className='RegexToggle'
>
.*
</ToggleButton>
</Tooltip>
</div>
{!isExperimentsLoading ? (
experimentsData?.map((experiment) => (
visibleExperiments?.map((experiment) => (
<Button
key={experiment.id}
onClick={() => handleExperimentClick(experiment.name)}
Expand All @@ -79,50 +192,65 @@ function ExperimentSelectionPopover({
),
})}
>
<Text
size={16}
tint={
experimentInList(
<div className='experimentBox__leftContainer'>
<Checkbox
color='primary'
icon={<CheckBoxOutlineBlank />}
checkedIcon={<CheckBoxIcon />}
checked={experimentInList(
experiment.name,
selectedExperimentNames,
)
? 100
: 80
}
weight={500}
className='experimentBox__experimentName'
>
{shortenExperimentName(experiment?.name)}
</Text>
<div className='experimentBox__date'>
<Icon
name='calendar'
color={
experimentInList(
experiment.name,
selectedExperimentNames,
)
? '#414B6D'
: '#606986'
}
fontSize={12}
)}
size='small'
className='experimentBox__checkbox'
/>
</div>
<div className='experimentBox__rightContainer'>
<Text
size={14}
size={16}
tint={
experimentInList(
experiment.name,
selectedExperimentNames,
)
? 80
: 70
? 100
: 80
}
weight={500}
className='experimentBox__experimentName'
>
{`${moment(experiment.creation_time * 1000).format(
DATE_WITH_SECONDS,
)}`}
{shortenExperimentName(experiment?.name)}
</Text>
<div className='experimentBox__date'>
<Icon
name='calendar'
color={
experimentInList(
experiment.name,
selectedExperimentNames,
)
? '#414B6D'
: '#606986'
}
fontSize={12}
/>
<Text
size={14}
tint={
experimentInList(
experiment.name,
selectedExperimentNames,
)
? 80
: 70
}
weight={500}
>
{`${moment(experiment.creation_time * 1000).format(
DATE_WITH_SECONDS,
)}`}
</Text>
</div>
</div>
</Button>
))
Expand Down
1 change: 1 addition & 0 deletions src/src/pages/Metrics/Metrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function Metrics(
onSelectExperimentNamesChange={
props.onSelectExperimentNamesChange
}
onToggleAllExperiments={props.onToggleAllExperiments}
/>
<div className='Metrics__SelectForm__Grouping__container'>
<SelectForm
Expand Down
Loading

0 comments on commit 8e547de

Please sign in to comment.