From 1c3793adbfadcbf8d5150d996a1b02089298ef6d Mon Sep 17 00:00:00 2001 From: Riley Bauer Date: Fri, 4 Jan 2019 11:24:30 -0800 Subject: [PATCH 1/6] Adds simple filtering by 'name' to CustomTable --- frontend/mock-backend/mock-api-middleware.ts | 86 ++++++++++++------- frontend/src/atoms/Input.tsx | 5 +- frontend/src/components/CustomTable.tsx | 88 ++++++++++++++++++-- frontend/src/lib/Apis.ts | 2 +- frontend/src/pages/ExperimentList.tsx | 3 +- frontend/src/pages/NewRun.tsx | 2 + frontend/src/pages/PipelineList.tsx | 4 +- frontend/src/pages/RecurringRunsManager.tsx | 8 +- frontend/src/pages/ResourceSelector.test.tsx | 1 + frontend/src/pages/ResourceSelector.tsx | 7 +- frontend/src/pages/RunList.tsx | 3 +- 11 files changed, 159 insertions(+), 50 deletions(-) diff --git a/frontend/mock-backend/mock-api-middleware.ts b/frontend/mock-backend/mock-api-middleware.ts index ef58c0eb2b6..3e0ed791ac5 100644 --- a/frontend/mock-backend/mock-api-middleware.ts +++ b/frontend/mock-backend/mock-api-middleware.ts @@ -26,6 +26,7 @@ import { ApiRun, ApiListRunsResponse, ApiResourceType } from '../src/apis/run'; import { ApiListExperimentsResponse, ApiExperiment } from '../src/apis/experiment'; import RunUtils from '../src/lib/RunUtils'; import { Response } from 'express-serve-static-core'; +import { ApiFilter, PredicateOp } from '../src/apis/filter/api'; const rocMetadataJsonPath = './eval-output/metadata.json'; const rocMetadataJsonPath2 = './eval-output/metadata2.json'; @@ -42,6 +43,15 @@ const v1beta1Prefix = '/apis/v1beta1'; let tensorboardPod = ''; +// This is a copy of the BaseResource defined within src/pages/ResourceSelector +interface BaseResource { + id?: string; + created_at?: Date; + description?: string; + name?: string; + error?: string; +} + // tslint:disable-next-line:no-default-export export default (app: express.Application) => { @@ -92,13 +102,8 @@ export default (app: express.Application) => { }; let jobs: ApiJob[] = fixedData.jobs; - if (req.query.filter_by) { - // NOTE: We do not mock fuzzy matching. E.g. 'jb' doesn't match 'job' - // This may need to be updated when the backend implements filtering. - jobs = fixedData.jobs.filter((j) => - j.name!.toLocaleLowerCase().indexOf( - decodeURIComponent(req.query.filter_by).toLocaleLowerCase()) > -1); - + if (req.query.filter) { + jobs = filterResources(fixedData.jobs, req.query.filter); } const { desc, key } = getSortKeyAndOrder(ExperimentSortKeys.CREATED_AT, req.query.sort_by); @@ -134,12 +139,8 @@ export default (app: express.Application) => { }; let experiments: ApiExperiment[] = fixedData.experiments; - if (req.query.filterBy) { - // NOTE: We do not mock fuzzy matching. E.g. 'ep' doesn't match 'experiment' - experiments = fixedData.experiments.filter((exp) => - exp.name!.toLocaleLowerCase().indexOf( - decodeURIComponent(req.query.filterBy).toLocaleLowerCase()) > -1); - + if (req.query.filter) { + experiments = filterResources(fixedData.experiments, req.query.filter); } const { desc, key } = getSortKeyAndOrder(ExperimentSortKeys.NAME, req.query.sortBy); @@ -272,11 +273,8 @@ export default (app: express.Application) => { return; } - if (req.query.filter_by) { - // NOTE: We do not mock fuzzy matching. E.g. 'jb' doesn't match 'job' - // This may need to be updated when the backend implements filtering. - runs = runs.filter((r) => r.name!.toLocaleLowerCase().indexOf( - decodeURIComponent(req.query.filter_by).toLocaleLowerCase()) > -1); + if (req.query.filter) { + runs = filterResources(runs, req.query.filter); } const { desc, key } = getSortKeyAndOrder(RunSortKeys.CREATED_AT, req.query.sort_by); @@ -313,11 +311,8 @@ export default (app: express.Application) => { let runs: ApiRun[] = fixedData.runs.map((r) => r.run!); - if (req.query.filter_by) { - // NOTE: We do not mock fuzzy matching. E.g. 'rn' doesn't match 'run' - // This may need to be updated when the backend implements filtering. - runs = runs.filter((r) => r.name!.toLocaleLowerCase().indexOf( - decodeURIComponent(req.query.filter_by).toLocaleLowerCase()) > -1); + if (req.query.filter) { + runs = filterResources(runs, req.query.filter); } if (req.query['resource_reference_key.type'] === ApiResourceType.EXPERIMENT) { @@ -409,6 +404,41 @@ export default (app: express.Application) => { }, 1000); }); + function filterResources(resources: BaseResource[], filterString?: string): BaseResource[] { + if (!filterString) { + return resources; + } + const filter: ApiFilter = JSON.parse(decodeURIComponent(filterString)); + ((filter && filter.predicates) || []).forEach(p => { + resources = resources.filter(r => { + switch(p.op) { + // case PredicateOp.CONTAINS + // return r.name!.toLocaleLowerCase().indexOf( + // decodeURIComponent(req.query.filter).toLocaleLowerCase()) > -1); + case PredicateOp.EQUALS: + if (p.key !== 'name') { + throw new Error(`Key: ${p.key} is not yet supported by the mock API server`); + } + return r.name!.toLocaleLowerCase() === (p.string_value || '').toLocaleLowerCase(); + case PredicateOp.NOTEQUALS: + // Fall through + case PredicateOp.GREATERTHAN: + // Fall through + case PredicateOp.GREATERTHANEQUALS: + // Fall through + case PredicateOp.LESSTHAN: + // Fall through + case PredicateOp.LESSTHANEQUALS: + // Fall through + throw new Error(`Op: ${p.op} is not yet supported by the mock API server`); + default: + throw new Error(`Unknown Predicate op: ${p.op}`); + } + }); + }); + return resources; + } + app.get(v1beta1Prefix + '/pipelines', (req, res) => { res.header('Content-Type', 'application/json'); const response: ApiListPipelinesResponse = { @@ -417,18 +447,12 @@ export default (app: express.Application) => { }; let pipelines: ApiPipeline[] = fixedData.pipelines; - if (req.query.filter_by) { - // NOTE: We do not mock fuzzy matching. E.g. 'jb' doesn't match 'job' - // This may need to be updated depending on how the backend implements filtering. - pipelines = fixedData.pipelines.filter((p) => - p.name!.toLocaleLowerCase().indexOf( - decodeURIComponent(req.query.filter_by).toLocaleLowerCase()) > -1); - + if (req.query.filter) { + pipelines = filterResources(fixedData.pipelines, req.query.filter); } const { desc, key } = getSortKeyAndOrder(PipelineSortKeys.CREATED_AT, req.query.sort_by); - pipelines.sort((a, b) => { let result = 1; if (a[key]! < b[key]!) { diff --git a/frontend/src/atoms/Input.tsx b/frontend/src/atoms/Input.tsx index 2a36cb6633a..f23396c8b6f 100644 --- a/frontend/src/atoms/Input.tsx +++ b/frontend/src/atoms/Input.tsx @@ -20,16 +20,17 @@ import { commonCss } from '../Css'; interface InputProps extends OutlinedTextFieldProps { height?: number | string; + maxWidth?: number | string; width?: number; } export default (props: InputProps) => { - const { height, variant, width, ...rest } = props; + const { height, maxWidth, variant, width, ...rest } = props; return ( {props.children} diff --git a/frontend/src/components/CustomTable.tsx b/frontend/src/components/CustomTable.tsx index 692b9d0e86b..95fe46a4681 100644 --- a/frontend/src/components/CustomTable.tsx +++ b/frontend/src/components/CustomTable.tsx @@ -20,19 +20,23 @@ import Checkbox, { CheckboxProps } from '@material-ui/core/Checkbox'; import ChevronLeft from '@material-ui/icons/ChevronLeft'; import ChevronRight from '@material-ui/icons/ChevronRight'; import CircularProgress from '@material-ui/core/CircularProgress'; +import FilterIcon from '@material-ui/icons/FilterList'; import IconButton from '@material-ui/core/IconButton'; +import Input from '../atoms/Input'; import MenuItem from '@material-ui/core/MenuItem'; import Radio from '@material-ui/core/Radio'; import Separator from '../atoms/Separator'; import TableSortLabel from '@material-ui/core/TableSortLabel'; -import TextField from '@material-ui/core/TextField'; +import TextField, { TextFieldProps } from '@material-ui/core/TextField'; import Tooltip from '@material-ui/core/Tooltip'; import WarningIcon from '@material-ui/icons/WarningRounded'; import { ListRequest } from '../lib/Apis'; -import { TextFieldProps } from '@material-ui/core/TextField'; import { classes, stylesheet } from 'typestyle'; import { fonts, fontsize, dimension, commonCss, color, padding } from '../Css'; import { logger } from '../lib/Utils'; +import { ApiFilter, PredicateOp } from '../apis/filter/api'; +import { debounce } from 'lodash'; +import { InputAdornment } from '@material-ui/core'; export enum ExpandState { COLLAPSED, @@ -106,6 +110,12 @@ export const css = stylesheet({ boxSizing: 'border-box', height: '40px !important', }, + filterBorderRadius: { + borderRadius: 8, + }, + filterBox: { + margin: '16px 0', + }, footer: { borderBottom: '1px solid ' + color.divider, fontFamily: fonts.secondary, @@ -118,7 +128,6 @@ export const css = stylesheet({ display: 'flex', flex: '0 0 40px', lineHeight: '40px', // must declare px - marginTop: 20, }, icon: { color: color.alert, @@ -127,6 +136,12 @@ export const css = stylesheet({ verticalAlign: 'sub', width: 18, }, + noLeftPadding: { + paddingLeft: 0, + }, + noMargin: { + margin: 0, + }, row: { $nest: { '&:hover': { @@ -158,6 +173,7 @@ interface CustomTableProps { disableSelection?: boolean; disableSorting?: boolean; emptyMessage?: string; + filterLabel?: string; getExpandComponent?: (index: number) => React.ReactNode; initialSortColumn?: string; initialSortOrder?: 'asc' | 'desc'; @@ -171,6 +187,8 @@ interface CustomTableProps { interface CustomTableState { currentPage: number; + filterString: string; + filterStringEncoded: string; isBusy: boolean; maxPageIndex: number; sortOrder: 'asc' | 'desc'; @@ -182,11 +200,16 @@ interface CustomTableState { export default class CustomTable extends React.Component { private _isMounted = true; + private _debouncedRequest = + debounce((filterString: string) => this._requestFilter(filterString), 300); + constructor(props: CustomTableProps) { super(props); this.state = { currentPage: 0, + filterString: '', + filterStringEncoded: '', isBusy: false, maxPageIndex: Number.MAX_SAFE_INTEGER, pageSize: 10, @@ -241,10 +264,11 @@ export default class CustomTable extends React.Component total += (c.flex || 1), 0); const widths = this.props.columns.map(c => (c.flex || 1) / totalFlex * 100); @@ -252,6 +276,24 @@ export default class CustomTable extends React.Component + {/* Filter/Search bar */} +
+ + + + ) + }} /> +
+ {/* Header */}
@@ -380,6 +422,7 @@ export default class CustomTable extends React.Component { // Override the current state with incoming request const request: ListRequest = Object.assign({ + filterBy: this.state.filterStringEncoded, orderAscending: this.state.sortOrder === 'asc', pageSize: this.state.pageSize, pageToken: this.state.tokenList[this.state.currentPage], @@ -389,9 +432,10 @@ export default class CustomTable extends React.Component (event: any) => { + const value = (event.target as TextFieldProps).value; + this.setStateSafe({ [name]: value } as any, + () => { + if (name === 'filterString') { + this._debouncedRequest(value as string); + } + }); + } + + private _requestFilter(filterString?: string): void { + const filterStringEncoded = filterString ? this._createAndEncodeFilter(filterString) : ''; + this.setStateSafe({ filterString, filterStringEncoded }, + async () => this._resetToFirstPage(await this.reload({ filter: filterStringEncoded })) + ); + } + private setStateSafe(newState: Partial, cb?: () => void): void { if (this._isMounted) { this.setState(newState as any, cb); diff --git a/frontend/src/lib/Apis.ts b/frontend/src/lib/Apis.ts index f6d6bf23df8..2930c824f41 100644 --- a/frontend/src/lib/Apis.ts +++ b/frontend/src/lib/Apis.ts @@ -22,7 +22,7 @@ import { StoragePath } from './WorkflowParser'; const v1beta1Prefix = 'apis/v1beta1'; export interface ListRequest { - filterBy?: string; + filter?: string; orderAscending?: boolean; pageSize?: number; pageToken?: string; diff --git a/frontend/src/pages/ExperimentList.tsx b/frontend/src/pages/ExperimentList.tsx index 995aab8a047..eb445b1b821 100644 --- a/frontend/src/pages/ExperimentList.tsx +++ b/frontend/src/pages/ExperimentList.tsx @@ -125,6 +125,7 @@ class ExperimentList extends Page<{}, ExperimentListState> { disableSelection={true} initialSortColumn={ExperimentSortKeys.CREATED_AT} reload={this._reload.bind(this)} toggleExpansion={this._toggleRowExpand.bind(this)} getExpandComponent={this._getExpandedExperimentComponent.bind(this)} + filterLabel='Filter experiments' emptyMessage='No experiments found. Click "Create experiment" to start.' />
); @@ -143,7 +144,7 @@ class ExperimentList extends Page<{}, ExperimentListState> { let displayExperiments: DisplayExperiment[]; try { response = await Apis.experimentServiceApi.listExperiment( - request.pageToken, request.pageSize, request.sortBy); + request.pageToken, request.pageSize, request.sortBy, request.filter); displayExperiments = response.experiments || []; displayExperiments.forEach((exp) => exp.expandState = ExpandState.COLLAPSED); } catch (err) { diff --git a/frontend/src/pages/NewRun.tsx b/frontend/src/pages/NewRun.tsx index 9b0b36f57d6..0651f26881b 100644 --- a/frontend/src/pages/NewRun.tsx +++ b/frontend/src/pages/NewRun.tsx @@ -188,6 +188,7 @@ class NewRun extends Page<{}, NewRunState> { { const response = await Apis.pipelineServiceApi.listPipelines(...args); return { resources: response.pipelines || [], nextPageToken: response.next_page_token || '' }; @@ -216,6 +217,7 @@ class NewRun extends Page<{}, NewRunState> { { const response = await Apis.experimentServiceApi.listExperiment(...args); return { resources: response.experiments || [], nextPageToken: response.next_page_token || '' }; diff --git a/frontend/src/pages/PipelineList.tsx b/frontend/src/pages/PipelineList.tsx index 8ad2dc4be33..811fdb52701 100644 --- a/frontend/src/pages/PipelineList.tsx +++ b/frontend/src/pages/PipelineList.tsx @@ -105,7 +105,7 @@ class PipelineList extends Page<{}, PipelineListState> {
{ let response: ApiListPipelinesResponse | null = null; try { response = await Apis.pipelineServiceApi.listPipelines( - request.pageToken, request.pageSize, request.sortBy); + request.pageToken, request.pageSize, request.sortBy, request.filter); this.clearBanner(); } catch (err) { await this.showPageError('Error: failed to retrieve list of pipelines.', err); diff --git a/frontend/src/pages/RecurringRunsManager.tsx b/frontend/src/pages/RecurringRunsManager.tsx index 48257093355..c94a6e1419f 100644 --- a/frontend/src/pages/RecurringRunsManager.tsx +++ b/frontend/src/pages/RecurringRunsManager.tsx @@ -83,9 +83,10 @@ class RecurringRunsManager extends React.Component this.setState({ selectedIds: ids })} initialSortColumn={JobSortKeys.CREATED_AT} - reload={this._loadRuns.bind(this)} emptyMessage={'No recurring runs found in this experiment.'} - disableSelection={true} /> + updateSelection={ids => this.setState({ selectedIds: ids })} + initialSortColumn={JobSortKeys.CREATED_AT} reload={this._loadRuns.bind(this)} + filterLabel='Filter recurring runs' disableSelection={true} + emptyMessage={'No recurring runs found in this experiment.'}/> ); } @@ -105,6 +106,7 @@ class RecurringRunsManager extends React.Component { return { columns: selectorColumns, emptyMessage: testEmptyMessage, + filterLabel: 'test filter label', history: {} as any, initialSortColumn: 'created_at', listApi: listResourceSpy as any, diff --git a/frontend/src/pages/ResourceSelector.tsx b/frontend/src/pages/ResourceSelector.tsx index 89b73aa7887..54cf9038f17 100644 --- a/frontend/src/pages/ResourceSelector.tsx +++ b/frontend/src/pages/ResourceSelector.tsx @@ -39,6 +39,7 @@ export interface ResourceSelectorProps extends RouteComponentProps { listApi: (...args: any[]) => Promise; columns: Column[]; emptyMessage: string; + filterLabel: string; initialSortColumn: any; selectionChanged: (resource: BaseResource) => void; title: string; @@ -68,13 +69,13 @@ class ResourceSelector extends React.Component @@ -110,7 +111,7 @@ class ResourceSelector extends React.Component { initialSortColumn={RunSortKeys.CREATED_AT} ref={this._tableRef} updateSelection={this.props.onSelectionChange} reload={this._loadRuns.bind(this)} disablePaging={this.props.disablePaging} disableSorting={this.props.disableSorting} - disableSelection={this.props.disableSelection} + disableSelection={this.props.disableSelection} filterLabel='Filter runs' emptyMessage={`No runs found${this.props.experimentIdMask ? ' for this experiment' : ''}.`} />
); @@ -288,6 +288,7 @@ class RunList extends React.PureComponent { request.sortBy, this.props.experimentIdMask ? ApiResourceType.EXPERIMENT.toString() : undefined, this.props.experimentIdMask, + request.filter, ); displayRuns = (response.runs || []).map(r => ({ metadata: r })); From f472bd03143ca48cedecdaba710a612571bfe5c8 Mon Sep 17 00:00:00 2001 From: Riley Bauer Date: Fri, 4 Jan 2019 14:01:21 -0800 Subject: [PATCH 2/6] Update tests --- frontend/src/components/CustomTable.test.tsx | 98 +- frontend/src/components/CustomTable.tsx | 41 +- .../__snapshots__/CustomTable.test.tsx.snap | 688 ++++++ frontend/src/pages/ExperimentList.test.tsx | 6 +- frontend/src/pages/ExperimentList.tsx | 2 +- frontend/src/pages/PipelineList.test.tsx | 6 +- .../src/pages/RecurringRunsManager.test.tsx | 3 +- frontend/src/pages/ResourceSelector.test.tsx | 2 +- frontend/src/pages/RunList.test.tsx | 6 +- frontend/src/pages/RunList.tsx | 9 +- .../ExperimentList.test.tsx.snap | 4 + .../pages/__snapshots__/NewRun.test.tsx.snap | 26 + .../__snapshots__/PipelineList.test.tsx.snap | 4 + .../RecurringRunsManager.test.tsx.snap | 353 +-- .../ResourceSelector.test.tsx.snap | 1 + .../pages/__snapshots__/RunList.test.tsx.snap | 1956 +++++++++++++---- 16 files changed, 2513 insertions(+), 692 deletions(-) diff --git a/frontend/src/components/CustomTable.test.tsx b/frontend/src/components/CustomTable.test.tsx index aebd2669396..4070ff1077e 100644 --- a/frontend/src/components/CustomTable.test.tsx +++ b/frontend/src/components/CustomTable.test.tsx @@ -112,7 +112,13 @@ describe('CustomTable', () => { it('calls reload function with an empty page token to get rows', () => { const reload = jest.fn(); shallow(); - expect(reload).toHaveBeenLastCalledWith({ pageToken: '', pageSize: 10, orderAscending: false, sortBy: '' }); + expect(reload).toHaveBeenLastCalledWith({ + filter: '', + orderAscending: false, + pageSize: 10, + pageToken: '', + sortBy: '', + }); }); it('calls reload function with sort key of clicked column, while keeping same page', () => { @@ -127,11 +133,21 @@ describe('CustomTable', () => { }]; const reload = jest.fn(); const tree = shallow(); - expect(reload).toHaveBeenLastCalledWith({ pageToken: '', pageSize: 10, sortBy: 'col1sortkey desc', orderAscending: false }); + expect(reload).toHaveBeenLastCalledWith({ + filter: '', + orderAscending: false, + pageSize: 10, + pageToken: '', + sortBy: 'col1sortkey desc', + }); tree.find('WithStyles(TableSortLabel)').at(1).simulate('click'); expect(reload).toHaveBeenLastCalledWith({ - orderAscending: true, pageSize: 10, pageToken: '', sortBy: 'col2sortkey', + filter: '', + orderAscending: true, + pageSize: 10, + pageToken: '', + sortBy: 'col2sortkey', }); }); @@ -147,16 +163,30 @@ describe('CustomTable', () => { }]; const reload = jest.fn(); const tree = shallow(); - expect(reload).toHaveBeenLastCalledWith({ pageToken: '', orderAscending: false, pageSize: 10, sortBy: 'col1sortkey desc' }); + expect(reload).toHaveBeenLastCalledWith({ + filter: '', + orderAscending: false, + pageSize: 10, + pageToken: '', + sortBy: 'col1sortkey desc', + }); tree.find('WithStyles(TableSortLabel)').at(1).simulate('click'); expect(reload).toHaveBeenLastCalledWith({ - orderAscending: true, pageSize: 10, pageToken: '', sortBy: 'col2sortkey', + filter: '', + orderAscending: true, + pageSize: 10, + pageToken: '', + sortBy: 'col2sortkey', }); tree.setProps({ sortBy: 'col1sortkey' }); tree.find('WithStyles(TableSortLabel)').at(1).simulate('click'); expect(reload).toHaveBeenLastCalledWith({ - orderAscending: false, pageSize: 10, pageToken: '', sortBy: 'col2sortkey desc' + filter: '', + orderAscending: false, + pageSize: 10, + pageToken: '', + sortBy: 'col2sortkey desc', }); }); @@ -170,10 +200,22 @@ describe('CustomTable', () => { }]; const reload = jest.fn(); const tree = shallow(); - expect(reload).toHaveBeenLastCalledWith({ pageToken: '', pageSize: 10, orderAscending: false, sortBy: '' }); + expect(reload).toHaveBeenLastCalledWith({ + filter: '', + orderAscending: false, + pageSize: 10, + pageToken: '', + sortBy: '', + }); tree.find('WithStyles(TableSortLabel)').at(0).simulate('click'); - expect(reload).toHaveBeenLastCalledWith({ pageToken: '', pageSize: 10, orderAscending: false, sortBy: '' }); + expect(reload).toHaveBeenLastCalledWith({ + filter: '', + orderAscending: false, + pageSize: 10, + pageToken: '', + sortBy: '', + }); }); it('logs error if row has more cells than columns', () => { @@ -309,7 +351,13 @@ describe('CustomTable', () => { await TestUtils.flushPromises(); tree.find('WithStyles(IconButton)').at(1).simulate('click'); - expect(spy).toHaveBeenLastCalledWith({ pageToken: 'some token', pageSize: 10, orderAscending: false, sortBy: '' }); + expect(spy).toHaveBeenLastCalledWith({ + filter: '', + orderAscending: false, + pageSize: 10, + pageToken: 'some token', + sortBy: '', + }); }); it('renders new rows after clicking next page, and enables previous page button', async () => { @@ -320,7 +368,13 @@ describe('CustomTable', () => { tree.find('WithStyles(IconButton)').at(1).simulate('click'); await TestUtils.flushPromises(); - expect(spy).toHaveBeenLastCalledWith({ pageToken: 'some token', pageSize: 10, sortBy: '', orderAscending: false }); + expect(spy).toHaveBeenLastCalledWith({ + filter: '', + orderAscending: false, + pageSize: 10, + pageToken: 'some token', + sortBy: '', + }); expect(tree.state()).toHaveProperty('currentPage', 1); tree.setProps({ rows: [rows[1]] }); expect(tree).toMatchSnapshot(); @@ -338,7 +392,13 @@ describe('CustomTable', () => { tree.find('WithStyles(IconButton)').at(0).simulate('click'); await TestUtils.flushPromises(); - expect(spy).toHaveBeenLastCalledWith({ pageToken: '', orderAscending: false, sortBy: '', pageSize: 10 }); + expect(spy).toHaveBeenLastCalledWith({ + filter: '', + orderAscending: false, + pageSize: 10, + pageToken: '', + sortBy: '', + }); tree.setProps({ rows }); expect(tree.find('WithStyles(IconButton)').at(0).prop('disabled')).toBeTruthy(); @@ -353,7 +413,13 @@ describe('CustomTable', () => { tree.find('.' + css.rowsPerPage).simulate('change', { target: { value: 1234 } }); await TestUtils.flushPromises(); - expect(spy).toHaveBeenLastCalledWith({ pageSize: 1234, pageToken: '', orderAscending: false, sortBy: '' }); + expect(spy).toHaveBeenLastCalledWith({ + filter: '', + orderAscending: false, + pageSize: 1234, + pageToken: '', + sortBy: '', + }); expect(tree.state()).toHaveProperty('tokenList', ['', 'some token']); }); @@ -364,7 +430,13 @@ describe('CustomTable', () => { tree.find('.' + css.rowsPerPage).simulate('change', { target: { value: 1234 } }); await reloadResult; - expect(spy).toHaveBeenLastCalledWith({ pageSize: 1234, pageToken: '', orderAscending: false, sortBy: '' }); + expect(spy).toHaveBeenLastCalledWith({ + filter: '', + orderAscending: false, + pageSize: 1234, + pageToken: '', + sortBy: '', + }); expect(tree.state()).toHaveProperty('tokenList', ['']); }); diff --git a/frontend/src/components/CustomTable.tsx b/frontend/src/components/CustomTable.tsx index 95fe46a4681..dc300efb6ed 100644 --- a/frontend/src/components/CustomTable.tsx +++ b/frontend/src/components/CustomTable.tsx @@ -165,6 +165,9 @@ export const css = stylesheet({ selectionToggle: { marginRight: 12, }, + verticalAlignInitial: { + verticalAlign: 'initial', + }, }); interface CustomTableProps { @@ -177,6 +180,7 @@ interface CustomTableProps { getExpandComponent?: (index: number) => React.ReactNode; initialSortColumn?: string; initialSortOrder?: 'asc' | 'desc'; + noFilterBox?: boolean; reload: (request: ListRequest) => Promise; rows: Row[]; selectedIds?: string[]; @@ -277,22 +281,24 @@ export default class CustomTable extends React.Component {/* Filter/Search bar */} -
- - - - ) - }} /> -
+ {!this.props.noFilterBox && ( +
+ + + + ) + }} /> +
+ )} {/* Header */}
Rows per page: {[10, 20, 50, 100].map((size, i) => ( @@ -422,7 +429,7 @@ export default class CustomTable extends React.Component { // Override the current state with incoming request const request: ListRequest = Object.assign({ - filterBy: this.state.filterStringEncoded, + filter: this.state.filterStringEncoded, orderAscending: this.state.sortOrder === 'asc', pageSize: this.state.pageSize, pageToken: this.state.tokenList[this.state.currentPage], diff --git a/frontend/src/components/__snapshots__/CustomTable.test.tsx.snap b/frontend/src/components/__snapshots__/CustomTable.test.tsx.snap index fc8980cfe01..2e988a507e6 100644 --- a/frontend/src/components/__snapshots__/CustomTable.test.tsx.snap +++ b/frontend/src/components/__snapshots__/CustomTable.test.tsx.snap @@ -4,6 +4,44 @@ exports[`CustomTable displays warning icon with tooltip if row has error 1`] = `
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -177,6 +215,11 @@ exports[`CustomTable displays warning icon with tooltip if row has error 1`] = ` } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -228,6 +271,44 @@ exports[`CustomTable renders a collapsed row 1`] = `
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -362,6 +443,11 @@ exports[`CustomTable renders a collapsed row 1`] = ` } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -413,6 +499,44 @@ exports[`CustomTable renders a collapsed row when selection is disabled 1`] = `
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -533,6 +657,11 @@ exports[`CustomTable renders a collapsed row when selection is disabled 1`] = ` } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -584,6 +713,44 @@ exports[`CustomTable renders a table with sorting disabled 1`] = `
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -732,6 +899,11 @@ exports[`CustomTable renders a table with sorting disabled 1`] = ` } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -783,6 +955,44 @@ exports[`CustomTable renders an expanded row 1`] = `
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -907,6 +1117,11 @@ exports[`CustomTable renders an expanded row 1`] = ` } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -958,6 +1173,44 @@ exports[`CustomTable renders an expanded row with expanded component below it 1`
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -1099,6 +1352,11 @@ exports[`CustomTable renders an expanded row with expanded component below it 1` } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -1150,6 +1408,44 @@ exports[`CustomTable renders columns with specified widths 1`] = `
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -1231,6 +1527,11 @@ exports[`CustomTable renders columns with specified widths 1`] = ` } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -1282,6 +1583,44 @@ exports[`CustomTable renders empty message on no rows 1`] = `
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -1325,6 +1664,11 @@ exports[`CustomTable renders empty message on no rows 1`] = ` } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -1376,6 +1720,44 @@ exports[`CustomTable renders new rows after clicking next page, and enables prev
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -1500,6 +1882,11 @@ exports[`CustomTable renders new rows after clicking next page, and enables prev } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -1551,6 +1938,44 @@ exports[`CustomTable renders new rows after clicking previous page, and enables
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -1717,6 +2142,11 @@ exports[`CustomTable renders new rows after clicking previous page, and enables } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -1768,6 +2198,44 @@ exports[`CustomTable renders some columns with descending sort order on first co
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -1850,6 +2318,11 @@ exports[`CustomTable renders some columns with descending sort order on first co } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -1901,6 +2374,44 @@ exports[`CustomTable renders some columns with equal widths without rows 1`] = `
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -1982,6 +2493,11 @@ exports[`CustomTable renders some columns with equal widths without rows 1`] = ` } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -2033,6 +2549,44 @@ exports[`CustomTable renders some rows 1`] = `
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -2199,6 +2753,11 @@ exports[`CustomTable renders some rows 1`] = ` } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -2250,6 +2809,44 @@ exports[`CustomTable renders some rows using a custom renderer 1`] = `
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -2420,6 +3017,11 @@ exports[`CustomTable renders some rows using a custom renderer 1`] = ` } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -2471,6 +3073,44 @@ exports[`CustomTable renders without rows or columns 1`] = `
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -2508,6 +3148,11 @@ exports[`CustomTable renders without rows or columns 1`] = ` } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} @@ -2559,6 +3204,44 @@ exports[`CustomTable renders without the checkboxes if disableSelection is true
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
@@ -2699,6 +3382,11 @@ exports[`CustomTable renders without the checkboxes if disableSelection is true } } className="rowsPerPage" + classes={ + Object { + "root": "verticalAlignInitial", + } + } onChange={[Function]} required={false} select={true} diff --git a/frontend/src/pages/ExperimentList.test.tsx b/frontend/src/pages/ExperimentList.test.tsx index ca7391fe207..86442bbdbe1 100644 --- a/frontend/src/pages/ExperimentList.test.tsx +++ b/frontend/src/pages/ExperimentList.test.tsx @@ -126,7 +126,7 @@ describe('ExperimentList', () => { it('calls Apis to list experiments, sorted by creation time in descending order', async () => { const tree = await mountWithNExperiments(1, 1); - expect(listExperimentsSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc'); + expect(listExperimentsSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc', ''); expect(listRunsSpy).toHaveBeenLastCalledWith(undefined, 5, 'created_at desc', ApiResourceType.EXPERIMENT.toString(), 'test-experiment-id0'); expect(tree.state()).toHaveProperty('displayExperiments', [{ @@ -146,7 +146,7 @@ describe('ExperimentList', () => { expect(refreshBtn).toBeDefined(); await refreshBtn!.action(); expect(listExperimentsSpy.mock.calls.length).toBe(2); - expect(listExperimentsSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc'); + expect(listExperimentsSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc', ''); expect(updateBannerSpy).toHaveBeenLastCalledWith({}); tree.unmount(); }); @@ -189,7 +189,7 @@ describe('ExperimentList', () => { TestUtils.makeErrorResponseOnce(listExperimentsSpy, 'bad stuff happened'); await refreshBtn!.action(); expect(listExperimentsSpy.mock.calls.length).toBe(2); - expect(listExperimentsSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc'); + expect(listExperimentsSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc', ''); expect(updateBannerSpy).toHaveBeenLastCalledWith(expect.objectContaining({ additionalInfo: 'bad stuff happened', message: 'Error: failed to retrieve list of experiments. Click Details for more information.', diff --git a/frontend/src/pages/ExperimentList.tsx b/frontend/src/pages/ExperimentList.tsx index eb445b1b821..6f6304d7ee5 100644 --- a/frontend/src/pages/ExperimentList.tsx +++ b/frontend/src/pages/ExperimentList.tsx @@ -242,7 +242,7 @@ class ExperimentList extends Page<{}, ExperimentListState> { const experiment = this.state.displayExperiments[experimentIndex]; const runIds = (experiment.last5Runs || []).map((r) => r.id!); return null} {...this.props} - disablePaging={true} selectedIds={this.state.selectedRunIds} + disablePaging={true} selectedIds={this.state.selectedRunIds} noFilterBox={true} onSelectionChange={this._runSelectionChanged.bind(this)} disableSorting={true} />; } } diff --git a/frontend/src/pages/PipelineList.test.tsx b/frontend/src/pages/PipelineList.test.tsx index 4b47cc49675..b64887531ea 100644 --- a/frontend/src/pages/PipelineList.test.tsx +++ b/frontend/src/pages/PipelineList.test.tsx @@ -121,7 +121,7 @@ describe('PipelineList', () => { listPipelinesSpy.mockImplementationOnce(() => ({ pipelines: [{ name: 'pipeline1' }] })); tree = TestUtils.mountWithRouter(); await listPipelinesSpy; - expect(listPipelinesSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc'); + expect(listPipelinesSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc', ''); expect(tree.state()).toHaveProperty('pipelines', [{ name: 'pipeline1' }]); }); @@ -133,7 +133,7 @@ describe('PipelineList', () => { expect(refreshBtn).toBeDefined(); await refreshBtn!.action(); expect(listPipelinesSpy.mock.calls.length).toBe(2); - expect(listPipelinesSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc'); + expect(listPipelinesSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc', ''); expect(updateBannerSpy).toHaveBeenLastCalledWith({}); }); @@ -157,7 +157,7 @@ describe('PipelineList', () => { TestUtils.makeErrorResponseOnce(listPipelinesSpy, 'bad stuff happened'); await refreshBtn!.action(); expect(listPipelinesSpy.mock.calls.length).toBe(2); - expect(listPipelinesSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc'); + expect(listPipelinesSpy).toHaveBeenLastCalledWith('', 10, 'created_at desc', ''); expect(updateBannerSpy).toHaveBeenLastCalledWith(expect.objectContaining({ additionalInfo: 'bad stuff happened', message: 'Error: failed to retrieve list of pipelines. Click Details for more information.', diff --git a/frontend/src/pages/RecurringRunsManager.test.tsx b/frontend/src/pages/RecurringRunsManager.test.tsx index cbf516a316e..7f42198b209 100644 --- a/frontend/src/pages/RecurringRunsManager.test.tsx +++ b/frontend/src/pages/RecurringRunsManager.test.tsx @@ -89,7 +89,8 @@ describe('RecurringRunsManager', () => { undefined, undefined, ApiResourceType.EXPERIMENT, - 'test-experiment'); + 'test-experiment', + undefined); expect(tree.state('runs')).toEqual(JOBS); expect(tree).toMatchSnapshot(); tree.unmount(); diff --git a/frontend/src/pages/ResourceSelector.test.tsx b/frontend/src/pages/ResourceSelector.test.tsx index 35894b8ba4a..ad6834083e6 100644 --- a/frontend/src/pages/ResourceSelector.test.tsx +++ b/frontend/src/pages/ResourceSelector.test.tsx @@ -97,7 +97,7 @@ describe('ResourceSelector', () => { await (tree.instance() as TestResourceSelector)._load({}); expect(listResourceSpy).toHaveBeenCalledTimes(1); - expect(listResourceSpy).toHaveBeenLastCalledWith(undefined, undefined, undefined); + expect(listResourceSpy).toHaveBeenLastCalledWith(undefined, undefined, undefined, undefined); expect(tree.state('resources')).toEqual(RESOURCES); expect(tree).toMatchSnapshot(); }); diff --git a/frontend/src/pages/RunList.test.tsx b/frontend/src/pages/RunList.test.tsx index e4772647d7c..c6010f24c79 100644 --- a/frontend/src/pages/RunList.test.tsx +++ b/frontend/src/pages/RunList.test.tsx @@ -85,7 +85,7 @@ describe('RunList', () => { const props = generateProps(); const tree = shallow(); await (tree.instance() as RunListTest)._loadRuns({}); - expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith(undefined, undefined, undefined, undefined, undefined); + expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith(undefined, undefined, undefined, undefined, undefined, undefined); expect(props.onError).not.toHaveBeenCalled(); expect(tree).toMatchSnapshot(); }); @@ -97,7 +97,7 @@ describe('RunList', () => { await (tree.instance() as RunList).refresh(); tree.update(); expect(Apis.runServiceApi.listRuns).toHaveBeenCalledTimes(2); - expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith('', 10, RunSortKeys.CREATED_AT + ' desc', undefined, undefined); + expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith('', 10, RunSortKeys.CREATED_AT + ' desc', undefined, undefined, ''); expect(props.onError).not.toHaveBeenCalled(); expect(tree).toMatchSnapshot(); }); @@ -187,7 +187,7 @@ describe('RunList', () => { await (tree.instance() as RunListTest)._loadRuns({}); expect(props.onError).not.toHaveBeenCalled(); expect(Apis.runServiceApi.listRuns).toHaveBeenLastCalledWith( - undefined, undefined, undefined, ApiResourceType.EXPERIMENT.toString(), 'experiment1'); + undefined, undefined, undefined, ApiResourceType.EXPERIMENT.toString(), 'experiment1', undefined); }); it('loads given list of runs only', async () => { diff --git a/frontend/src/pages/RunList.tsx b/frontend/src/pages/RunList.tsx index 06dc66c50ae..5c9ec3fb4d9 100644 --- a/frontend/src/pages/RunList.tsx +++ b/frontend/src/pages/RunList.tsx @@ -74,10 +74,11 @@ export interface RunListProps extends RouteComponentProps { disableSelection?: boolean; disableSorting?: boolean; experimentIdMask?: string; - runIdListMask?: string[]; + noFilterBox?: boolean; onError: (message: string, error: Error) => void; - selectedIds?: string[]; onSelectionChange?: (selectedRunIds: string[]) => void; + runIdListMask?: string[]; + selectedIds?: string[]; } interface RunListState { @@ -164,10 +165,10 @@ class RunList extends React.PureComponent { return (
); diff --git a/frontend/src/pages/__snapshots__/ExperimentList.test.tsx.snap b/frontend/src/pages/__snapshots__/ExperimentList.test.tsx.snap index c03edab8ded..9296def1d86 100644 --- a/frontend/src/pages/__snapshots__/ExperimentList.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/ExperimentList.test.tsx.snap @@ -26,6 +26,7 @@ exports[`ExperimentList renders a list of one experiment 1`] = ` } disableSelection={true} emptyMessage="No experiments found. Click \\"Create experiment\\" to start." + filterLabel="Filter experiments" getExpandComponent={[Function]} initialSortColumn="created_at" reload={[Function]} @@ -74,6 +75,7 @@ exports[`ExperimentList renders a list of one experiment with error 1`] = ` } disableSelection={true} emptyMessage="No experiments found. Click \\"Create experiment\\" to start." + filterLabel="Filter experiments" getExpandComponent={[Function]} initialSortColumn="created_at" reload={[Function]} @@ -109,6 +111,7 @@ exports[`ExperimentList renders a list of one experiment with no description 1`] } disableSelection={true} emptyMessage="No experiments found. Click \\"Create experiment\\" to start." + filterLabel="Filter experiments" getExpandComponent={[Function]} initialSortColumn="created_at" reload={[Function]} @@ -144,6 +147,7 @@ exports[`ExperimentList renders an empty list with empty state message 1`] = ` } disableSelection={true} emptyMessage="No experiments found. Click \\"Create experiment\\" to start." + filterLabel="Filter experiments" getExpandComponent={[Function]} initialSortColumn="created_at" reload={[Function]} diff --git a/frontend/src/pages/__snapshots__/NewRun.test.tsx.snap b/frontend/src/pages/__snapshots__/NewRun.test.tsx.snap index 761dd38199c..1b60d36562e 100644 --- a/frontend/src/pages/__snapshots__/NewRun.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/NewRun.test.tsx.snap @@ -67,6 +67,7 @@ exports[`NewRun arriving from pipeline details page indicates that a pipeline is ] } emptyMessage="No pipelines found. Upload a pipeline and then try again." + filterLabel="Filter pipelines" history={ Object { "push": [MockFunction], @@ -192,6 +193,7 @@ exports[`NewRun arriving from pipeline details page indicates that a pipeline is ] } emptyMessage="No experiments found. Create an experiment and then try again." + filterLabel="Filter experiments" history={ Object { "push": [MockFunction], @@ -450,6 +452,7 @@ exports[`NewRun changes the exit button's text if query params indicate this is ] } emptyMessage="No pipelines found. Upload a pipeline and then try again." + filterLabel="Filter pipelines" history={ Object { "push": [MockFunction], @@ -579,6 +582,7 @@ exports[`NewRun changes the exit button's text if query params indicate this is ] } emptyMessage="No experiments found. Create an experiment and then try again." + filterLabel="Filter experiments" history={ Object { "push": [MockFunction], @@ -867,6 +871,7 @@ exports[`NewRun cloning from a run shows pipeline selector when switching from e ] } emptyMessage="No pipelines found. Upload a pipeline and then try again." + filterLabel="Filter pipelines" history={ Object { "push": [MockFunction], @@ -992,6 +997,7 @@ exports[`NewRun cloning from a run shows pipeline selector when switching from e ] } emptyMessage="No experiments found. Create an experiment and then try again." + filterLabel="Filter experiments" history={ Object { "push": [MockFunction], @@ -1242,6 +1248,7 @@ exports[`NewRun cloning from a run shows switching controls when run has embedde ] } emptyMessage="No pipelines found. Upload a pipeline and then try again." + filterLabel="Filter pipelines" history={ Object { "push": [MockFunction], @@ -1367,6 +1374,7 @@ exports[`NewRun cloning from a run shows switching controls when run has embedde ] } emptyMessage="No experiments found. Create an experiment and then try again." + filterLabel="Filter experiments" history={ Object { "push": [MockFunction], @@ -1623,6 +1631,7 @@ exports[`NewRun fetches the associated pipeline if one is present in the query p ] } emptyMessage="No pipelines found. Upload a pipeline and then try again." + filterLabel="Filter pipelines" history={ Object { "push": [MockFunction], @@ -1748,6 +1757,7 @@ exports[`NewRun fetches the associated pipeline if one is present in the query p ] } emptyMessage="No experiments found. Create an experiment and then try again." + filterLabel="Filter experiments" history={ Object { "push": [MockFunction], @@ -2006,6 +2016,7 @@ exports[`NewRun renders the new run page 1`] = ` ] } emptyMessage="No pipelines found. Upload a pipeline and then try again." + filterLabel="Filter pipelines" history={ Object { "push": [MockFunction], @@ -2135,6 +2146,7 @@ exports[`NewRun renders the new run page 1`] = ` ] } emptyMessage="No experiments found. Create an experiment and then try again." + filterLabel="Filter experiments" history={ Object { "push": [MockFunction], @@ -2397,6 +2409,7 @@ exports[`NewRun starting a new recurring run includes additional trigger input f ] } emptyMessage="No pipelines found. Upload a pipeline and then try again." + filterLabel="Filter pipelines" history={ Object { "push": [MockFunction], @@ -2522,6 +2535,7 @@ exports[`NewRun starting a new recurring run includes additional trigger input f ] } emptyMessage="No experiments found. Create an experiment and then try again." + filterLabel="Filter experiments" history={ Object { "push": [MockFunction], @@ -2782,6 +2796,7 @@ exports[`NewRun starting a new run copies pipeline from run in the start API cal ] } emptyMessage="No pipelines found. Upload a pipeline and then try again." + filterLabel="Filter pipelines" history={ Object { "push": [MockFunction] { @@ -2924,6 +2939,7 @@ exports[`NewRun starting a new run copies pipeline from run in the start API cal ] } emptyMessage="No experiments found. Create an experiment and then try again." + filterLabel="Filter experiments" history={ Object { "push": [MockFunction] { @@ -3197,6 +3213,7 @@ exports[`NewRun starting a new run updates the pipeline in state when a user fil ] } emptyMessage="No pipelines found. Upload a pipeline and then try again." + filterLabel="Filter pipelines" history={ Object { "push": [MockFunction] { @@ -3339,6 +3356,7 @@ exports[`NewRun starting a new run updates the pipeline in state when a user fil ] } emptyMessage="No experiments found. Create an experiment and then try again." + filterLabel="Filter experiments" history={ Object { "push": [MockFunction] { @@ -3648,6 +3666,7 @@ exports[`NewRun starting a new run updates the pipeline params as user selects d ] } emptyMessage="No pipelines found. Upload a pipeline and then try again." + filterLabel="Filter pipelines" history={ Object { "push": [MockFunction], @@ -3777,6 +3796,7 @@ exports[`NewRun starting a new run updates the pipeline params as user selects d ] } emptyMessage="No experiments found. Create an experiment and then try again." + filterLabel="Filter experiments" history={ Object { "push": [MockFunction], @@ -4039,6 +4059,7 @@ exports[`NewRun starting a new run updates the pipeline params as user selects d ] } emptyMessage="No pipelines found. Upload a pipeline and then try again." + filterLabel="Filter pipelines" history={ Object { "push": [MockFunction], @@ -4168,6 +4189,7 @@ exports[`NewRun starting a new run updates the pipeline params as user selects d ] } emptyMessage="No experiments found. Create an experiment and then try again." + filterLabel="Filter experiments" history={ Object { "push": [MockFunction], @@ -4466,6 +4488,7 @@ exports[`NewRun starting a new run updates the pipeline params as user selects d ] } emptyMessage="No pipelines found. Upload a pipeline and then try again." + filterLabel="Filter pipelines" history={ Object { "push": [MockFunction], @@ -4595,6 +4618,7 @@ exports[`NewRun starting a new run updates the pipeline params as user selects d ] } emptyMessage="No experiments found. Create an experiment and then try again." + filterLabel="Filter experiments" history={ Object { "push": [MockFunction], @@ -4857,6 +4881,7 @@ exports[`NewRun updates the run's state with the associated experiment if one is ] } emptyMessage="No pipelines found. Upload a pipeline and then try again." + filterLabel="Filter pipelines" history={ Object { "push": [MockFunction], @@ -4986,6 +5011,7 @@ exports[`NewRun updates the run's state with the associated experiment if one is ] } emptyMessage="No experiments found. Create an experiment and then try again." + filterLabel="Filter experiments" history={ Object { "push": [MockFunction], diff --git a/frontend/src/pages/__snapshots__/PipelineList.test.tsx.snap b/frontend/src/pages/__snapshots__/PipelineList.test.tsx.snap index 2f80bcb4dd2..50071655d17 100644 --- a/frontend/src/pages/__snapshots__/PipelineList.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/PipelineList.test.tsx.snap @@ -25,6 +25,7 @@ exports[`PipelineList renders a list of one pipeline 1`] = ` ] } emptyMessage="No pipelines found. Click \\"Upload pipeline\\" to start." + filterLabel="Filter pipelines" initialSortColumn="created_at" reload={[Function]} rows={ @@ -74,6 +75,7 @@ exports[`PipelineList renders a list of one pipeline with error 1`] = ` ] } emptyMessage="No pipelines found. Click \\"Upload pipeline\\" to start." + filterLabel="Filter pipelines" initialSortColumn="created_at" reload={[Function]} rows={ @@ -123,6 +125,7 @@ exports[`PipelineList renders a list of one pipeline with no description or crea ] } emptyMessage="No pipelines found. Click \\"Upload pipeline\\" to start." + filterLabel="Filter pipelines" initialSortColumn="created_at" reload={[Function]} rows={ @@ -172,6 +175,7 @@ exports[`PipelineList renders an empty list with empty state message 1`] = ` ] } emptyMessage="No pipelines found. Click \\"Upload pipeline\\" to start." + filterLabel="Filter pipelines" initialSortColumn="created_at" reload={[Function]} rows={Array []} diff --git a/frontend/src/pages/__snapshots__/RecurringRunsManager.test.tsx.snap b/frontend/src/pages/__snapshots__/RecurringRunsManager.test.tsx.snap index ed036be97e9..1888eddfbf7 100644 --- a/frontend/src/pages/__snapshots__/RecurringRunsManager.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/RecurringRunsManager.test.tsx.snap @@ -30,6 +30,7 @@ exports[`RecurringRunsManager calls API to load recurring runs 1`] = ` } disableSelection={true} emptyMessage="No recurring runs found in this experiment." + filterLabel="Filter recurring runs" initialSortColumn="created_at" reload={[Function]} rows={ @@ -74,32 +75,32 @@ exports[`RecurringRunsManager reloads the list of runs after enable/disabling 1` className="root" classes={ Object { - "colorInherit": "MuiButton-colorInherit-115", - "contained": "MuiButton-contained-105", - "containedPrimary": "MuiButton-containedPrimary-106", - "containedSecondary": "MuiButton-containedSecondary-107", - "disabled": "MuiButton-disabled-114", - "extendedFab": "MuiButton-extendedFab-112", - "fab": "MuiButton-fab-111", - "flat": "MuiButton-flat-99", - "flatPrimary": "MuiButton-flatPrimary-100", - "flatSecondary": "MuiButton-flatSecondary-101", - "focusVisible": "MuiButton-focusVisible-113", - "fullWidth": "MuiButton-fullWidth-119", - "label": "MuiButton-label-95", - "mini": "MuiButton-mini-116", - "outlined": "MuiButton-outlined-102", - "outlinedPrimary": "MuiButton-outlinedPrimary-103", - "outlinedSecondary": "MuiButton-outlinedSecondary-104", - "raised": "MuiButton-raised-108", - "raisedPrimary": "MuiButton-raisedPrimary-109", - "raisedSecondary": "MuiButton-raisedSecondary-110", - "root": "MuiButton-root-94", - "sizeLarge": "MuiButton-sizeLarge-118", - "sizeSmall": "MuiButton-sizeSmall-117", - "text": "MuiButton-text-96", - "textPrimary": "MuiButton-textPrimary-97", - "textSecondary": "MuiButton-textSecondary-98", + "colorInherit": "MuiButton-colorInherit-152", + "contained": "MuiButton-contained-142", + "containedPrimary": "MuiButton-containedPrimary-143", + "containedSecondary": "MuiButton-containedSecondary-144", + "disabled": "MuiButton-disabled-151", + "extendedFab": "MuiButton-extendedFab-149", + "fab": "MuiButton-fab-148", + "flat": "MuiButton-flat-136", + "flatPrimary": "MuiButton-flatPrimary-137", + "flatSecondary": "MuiButton-flatSecondary-138", + "focusVisible": "MuiButton-focusVisible-150", + "fullWidth": "MuiButton-fullWidth-156", + "label": "MuiButton-label-132", + "mini": "MuiButton-mini-153", + "outlined": "MuiButton-outlined-139", + "outlinedPrimary": "MuiButton-outlinedPrimary-140", + "outlinedSecondary": "MuiButton-outlinedSecondary-141", + "raised": "MuiButton-raised-145", + "raisedPrimary": "MuiButton-raisedPrimary-146", + "raisedSecondary": "MuiButton-raisedSecondary-147", + "root": "MuiButton-root-131", + "sizeLarge": "MuiButton-sizeLarge-155", + "sizeSmall": "MuiButton-sizeSmall-154", + "text": "MuiButton-text-133", + "textPrimary": "MuiButton-textPrimary-134", + "textSecondary": "MuiButton-textSecondary-135", } } color="primary" @@ -114,22 +115,22 @@ exports[`RecurringRunsManager reloads the list of runs after enable/disabling 1` variant="text" >
`; +exports[`CustomTable renders with default filter label 1`] = ` +
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
+
+
+ +
+
+
+
+ + Rows per page: + + + + 10 + + + 20 + + + 50 + + + 100 + + + + + + + + +
+
+`; + +exports[`CustomTable renders with provided filter label 1`] = ` +
+
+ + + , + } + } + className="filterBox" + height={48} + label="test filter label" + maxWidth="100%" + onChange={[Function]} + value="" + variant="outlined" + /> +
+
+
+ +
+
+
+
+ + Rows per page: + + + + 10 + + + 20 + + + 50 + + + 100 + + + + + + + + +
+
+`; + +exports[`CustomTable renders without filter box 1`] = ` +
+
+
+ +
+
+
+
+ + Rows per page: + + + + 10 + + + 20 + + + 50 + + + 100 + + + + + + + + +
+
+`; + exports[`CustomTable renders without rows or columns 1`] = `
Date: Tue, 8 Jan 2019 10:58:56 -0800 Subject: [PATCH 4/6] Filter using 'is_substring' rather than 'equal' --- frontend/mock-backend/mock-api-middleware.ts | 10 ++++++---- frontend/src/apis/filter/api.ts | 3 ++- frontend/src/components/CustomTable.test.tsx | 2 +- frontend/src/components/CustomTable.tsx | 3 +-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/mock-backend/mock-api-middleware.ts b/frontend/mock-backend/mock-api-middleware.ts index 3e0ed791ac5..09762aecbb9 100644 --- a/frontend/mock-backend/mock-api-middleware.ts +++ b/frontend/mock-backend/mock-api-middleware.ts @@ -412,14 +412,16 @@ export default (app: express.Application) => { ((filter && filter.predicates) || []).forEach(p => { resources = resources.filter(r => { switch(p.op) { - // case PredicateOp.CONTAINS - // return r.name!.toLocaleLowerCase().indexOf( - // decodeURIComponent(req.query.filter).toLocaleLowerCase()) > -1); case PredicateOp.EQUALS: if (p.key !== 'name') { throw new Error(`Key: ${p.key} is not yet supported by the mock API server`); } - return r.name!.toLocaleLowerCase() === (p.string_value || '').toLocaleLowerCase(); + return r.name && r.name.toLocaleLowerCase() === (p.string_value || '').toLocaleLowerCase(); + case PredicateOp.ISSUBSTRING: + if (p.key !== 'name') { + throw new Error(`Key: ${p.key} is not yet supported by the mock API server`); + } + return r.name && r.name.toLocaleLowerCase().includes((p.string_value || '').toLocaleLowerCase()); case PredicateOp.NOTEQUALS: // Fall through case PredicateOp.GREATERTHAN: diff --git a/frontend/src/apis/filter/api.ts b/frontend/src/apis/filter/api.ts index 5b7a7ceae12..36c95227760 100644 --- a/frontend/src/apis/filter/api.ts +++ b/frontend/src/apis/filter/api.ts @@ -209,7 +209,8 @@ export enum PredicateOp { GREATERTHANEQUALS = 'GREATER_THAN_EQUALS', LESSTHAN = 'LESS_THAN', LESSTHANEQUALS = 'LESS_THAN_EQUALS', - IN = 'IN' + IN = 'IN', + ISSUBSTRING = 'IS_SUBSTRING' } diff --git a/frontend/src/components/CustomTable.test.tsx b/frontend/src/components/CustomTable.test.tsx index f376e4b9711..acd2e4aed51 100644 --- a/frontend/src/components/CustomTable.test.tsx +++ b/frontend/src/components/CustomTable.test.tsx @@ -553,7 +553,7 @@ describe('CustomTable', () => { const expectedEncodedFilter = encodeURIComponent(JSON.stringify({ predicates: [{ key: 'name', - op: PredicateOp.EQUALS, + op: PredicateOp.ISSUBSTRING, string_value: 'test filter', }] })); diff --git a/frontend/src/components/CustomTable.tsx b/frontend/src/components/CustomTable.tsx index d4c4cb30141..420385c766e 100644 --- a/frontend/src/components/CustomTable.tsx +++ b/frontend/src/components/CustomTable.tsx @@ -480,8 +480,7 @@ export default class CustomTable extends React.Component Date: Thu, 10 Jan 2019 13:55:42 -0800 Subject: [PATCH 5/6] Clean up and some comments --- frontend/mock-backend/mock-api-middleware.ts | 17 ++++++++--------- frontend/src/components/CustomTable.test.tsx | 10 +++++----- frontend/src/components/CustomTable.tsx | 20 ++++++++++---------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/frontend/mock-backend/mock-api-middleware.ts b/frontend/mock-backend/mock-api-middleware.ts index 09762aecbb9..cbc1a2b4090 100644 --- a/frontend/mock-backend/mock-api-middleware.ts +++ b/frontend/mock-backend/mock-api-middleware.ts @@ -15,18 +15,17 @@ import * as express from 'express'; import * as fs from 'fs'; import * as _path from 'path'; -import proxyMiddleware from '../server/proxy-middleware'; -import { ExperimentSortKeys, RunSortKeys, PipelineSortKeys } from '../src/lib/Apis'; - +import RunUtils from '../src/lib/RunUtils'; import helloWorldRuntime from './integration-test-runtime'; -import { data as fixedData, namedPipelines } from './fixed-data'; -import { ApiPipeline, ApiListPipelinesResponse } from '../src/apis/pipeline'; -import { ApiListJobsResponse, ApiJob } from '../src/apis/job'; -import { ApiRun, ApiListRunsResponse, ApiResourceType } from '../src/apis/run'; +import proxyMiddleware from '../server/proxy-middleware'; +import { ApiFilter, PredicateOp } from '../src/apis/filter/api'; import { ApiListExperimentsResponse, ApiExperiment } from '../src/apis/experiment'; -import RunUtils from '../src/lib/RunUtils'; +import { ApiListJobsResponse, ApiJob } from '../src/apis/job'; +import { ApiListPipelinesResponse, ApiPipeline } from '../src/apis/pipeline'; +import { ApiListRunsResponse, ApiResourceType, ApiRun } from '../src/apis/run'; +import { ExperimentSortKeys, PipelineSortKeys, RunSortKeys } from '../src/lib/Apis'; import { Response } from 'express-serve-static-core'; -import { ApiFilter, PredicateOp } from '../src/apis/filter/api'; +import { data as fixedData, namedPipelines } from './fixed-data'; const rocMetadataJsonPath = './eval-output/metadata.json'; const rocMetadataJsonPath2 = './eval-output/metadata2.json'; diff --git a/frontend/src/components/CustomTable.test.tsx b/frontend/src/components/CustomTable.test.tsx index acd2e4aed51..91efeafdc84 100644 --- a/frontend/src/components/CustomTable.test.tsx +++ b/frontend/src/components/CustomTable.test.tsx @@ -15,10 +15,10 @@ */ import * as React from 'react'; -import CustomTable, { Column, Row, css, ExpandState } from './CustomTable'; +import CustomTable, { Column, ExpandState, Row, css } from './CustomTable'; import TestUtils from '../TestUtils'; -import { shallow } from 'enzyme'; import { PredicateOp } from '../apis/filter'; +import { shallow } from 'enzyme'; const props = { columns: [], @@ -291,7 +291,7 @@ describe('CustomTable', () => { expect(spy).toHaveBeenLastCalledWith(['row1']); }); - it('does not add items to selection when multiple clicked', () => { + it('does not add items to selection when multiple rows are clicked', () => { // Keeping track of selection is the parent's job. const spy = jest.fn(); const tree = shallow(); @@ -300,7 +300,7 @@ describe('CustomTable', () => { expect(spy).toHaveBeenLastCalledWith(['row2']); }); - it('passes both selectedIds and newly selected row when clicked', () => { + it('passes both selectedIds and the newly selected row to updateSelection when a row is clicked', () => { // Keeping track of selection is the parent's job. const selectedIds = ['previouslySelectedRow']; const spy = jest.fn(); @@ -539,7 +539,7 @@ describe('CustomTable', () => { it('updates the filter string in state when the filter box input changes', async () => { const tree = shallow(); - (tree.instance() as CustomTable).handleChange('filterString')({ target: { value: 'test filter' } }); + (tree.instance() as CustomTable).handleFilterChange({ target: { value: 'test filter' } }); await TestUtils.flushPromises(); expect(tree.state('filterString')).toEqual('test filter'); }); diff --git a/frontend/src/components/CustomTable.tsx b/frontend/src/components/CustomTable.tsx index 420385c766e..7a028de71bd 100644 --- a/frontend/src/components/CustomTable.tsx +++ b/frontend/src/components/CustomTable.tsx @@ -204,7 +204,7 @@ interface CustomTableState { export default class CustomTable extends React.Component { private _isMounted = true; - private _debouncedRequest = + private _debouncedFilterRequest = debounce((filterString: string) => this._requestFilter(filterString), 300); constructor(props: CustomTableProps) { @@ -269,7 +269,7 @@ export default class CustomTable extends React.Component (event: any) => { + public handleFilterChange = (event: any) => { const value = event.target.value; - this.setStateSafe({ [name]: value } as any, - async () => { - if (name === 'filterString') { - await this._debouncedRequest(value as string); - } - }); + // Set state here so that the UI will be updated even if the actual filter request is debounced + this.setStateSafe( + { filterString: value } as any, + async () => await this._debouncedFilterRequest(value as string) + ); } // Exposed for testing From 2e99ce8681414112dbdb98b4a363e24b15e20ac5 Mon Sep 17 00:00:00 2001 From: Riley Bauer Date: Thu, 10 Jan 2019 14:39:39 -0800 Subject: [PATCH 6/6] Add snapshot to handleFilterChange test --- frontend/src/components/CustomTable.test.tsx | 1 + .../__snapshots__/CustomTable.test.tsx.snap | 260 ++++++++++++++++++ 2 files changed, 261 insertions(+) diff --git a/frontend/src/components/CustomTable.test.tsx b/frontend/src/components/CustomTable.test.tsx index 91efeafdc84..2d4bb859510 100644 --- a/frontend/src/components/CustomTable.test.tsx +++ b/frontend/src/components/CustomTable.test.tsx @@ -542,6 +542,7 @@ describe('CustomTable', () => { (tree.instance() as CustomTable).handleFilterChange({ target: { value: 'test filter' } }); await TestUtils.flushPromises(); expect(tree.state('filterString')).toEqual('test filter'); + expect(tree).toMatchSnapshot(); }); it('reloads the table with the encoded filter object', async () => { diff --git a/frontend/src/components/__snapshots__/CustomTable.test.tsx.snap b/frontend/src/components/__snapshots__/CustomTable.test.tsx.snap index 91fc5154a53..34d8f84aef0 100644 --- a/frontend/src/components/__snapshots__/CustomTable.test.tsx.snap +++ b/frontend/src/components/__snapshots__/CustomTable.test.tsx.snap @@ -3788,3 +3788,263 @@ exports[`CustomTable renders without the checkboxes if disableSelection is true
`; + +exports[`CustomTable updates the filter string in state when the filter box input changes 1`] = ` +
+
+ + + , + } + } + className="filterBox" + height={48} + label="Filter" + maxWidth="100%" + onChange={[Function]} + value="test filter" + variant="outlined" + /> +
+
+
+ +
+
+ + + col1 + + +
+
+ + + col2 + + +
+
+
+
+
+
+ +
+
+ cell1 +
+
+ cell2 +
+
+
+
+
+
+ +
+
+ cell1 +
+
+ cell2 +
+
+
+
+
+ + Rows per page: + + + + 10 + + + 20 + + + 50 + + + 100 + + + + + + + + +
+
+`;