diff --git a/frontend/src/actions/index.js b/frontend/src/actions/index.js index 5f6c1866..5303e15e 100644 --- a/frontend/src/actions/index.js +++ b/frontend/src/actions/index.js @@ -253,6 +253,17 @@ export const updateResultSelect = (projectId, resultId, selected) => ({ selected, }); +// result filter + +export const RESULT_FILTER_UPDATE = 'RESULT_FILTER_UPDATE'; + +export const updateResultFilter = (projectId, filterKey, filterText) => ({ + type: RESULT_FILTER_UPDATE, + projectId, + filterKey, + filterText, +}); + // lines config export const LINES_CONFIG_LINE_UPDATE = 'LINES_CONFIG_LINE_UPDATE'; diff --git a/frontend/src/components/ExperimentsTable.jsx b/frontend/src/components/ExperimentsTable.jsx index c0b7103a..979a88ee 100644 --- a/frontend/src/components/ExperimentsTable.jsx +++ b/frontend/src/components/ExperimentsTable.jsx @@ -14,6 +14,7 @@ import { import ResultName from './experiments_table_cell/ResultName'; import ToggleResult from './experiments_table_cell/ToggleResult'; import SubComponent from './experiments_table_cell/SubComponent'; +import ResultFilter from './experiments_table_cell/ResultFilter'; import VisibilityCheckbox from './VisibilityCheckbox'; import TableConfigurator from './TableConfigurator'; @@ -24,12 +25,14 @@ const ExperimentsTable = (props) => { project, results, resultsStatus, + resultFilter, stats, projectConfig, globalConfig, onResultsConfigSelectUpdate, onResultUpdate, onResultSelect, + onResultFilterUpdate, onCommandSubmit, onTableExpandedUpdate, onTableColumnsVisibilityUpdate, @@ -90,6 +93,7 @@ const ExperimentsTable = (props) => { }, className: 'text-center', sortable: false, + filterable: false, minWidth: 40, Aggregated: (row) => { const groupedResultKeys = row.subRows.map((r) => { @@ -129,6 +133,14 @@ const ExperimentsTable = (props) => { return null; }, minWidth: 250, + Filter: ( + + ), }, ]; if (isGrouped) { @@ -136,6 +148,14 @@ const ExperimentsTable = (props) => { Header: '', id: 'group', accessor: (p) => getGrandParentDirectoryName(p), + Filter: ( + + ), }); } const groupedKey = isGrouped ? ['group'] : []; @@ -153,6 +173,7 @@ const ExperimentsTable = (props) => { style: defaultStyle, show: !(knownLogKeysConfig[logKey] || {}).hidden, aggregate: () => '', + filterable: false, })); const argsList = sortKeys(argKeys, knownArgKeysConfig).map((argKey) => ({ @@ -169,6 +190,7 @@ const ExperimentsTable = (props) => { style: defaultStyle, show: !(knownArgKeysConfig[argKey] || {}).hidden, aggregate: () => '', + filterable: false, })); const columns = [ @@ -203,6 +225,7 @@ const ExperimentsTable = (props) => { expanded={expanded} onExpandedChange={(nextExpanded) => onTableExpandedUpdate(project.id, nextExpanded)} pageSize={resultList.length} + filterable defaultSortMethod={sortMethod} defaultSorted={[ { @@ -254,12 +277,14 @@ ExperimentsTable.propTypes = { project: uiPropTypes.project.isRequired, results: uiPropTypes.results.isRequired, resultsStatus: uiPropTypes.resultsStatus, + resultFilter: uiPropTypes.resultFilter, projectConfig: uiPropTypes.projectConfig.isRequired, globalConfig: uiPropTypes.globalConfig.isRequired, stats: uiPropTypes.stats.isRequired, onResultsConfigSelectUpdate: PropTypes.func.isRequired, onResultUpdate: PropTypes.func.isRequired, onResultSelect: PropTypes.func.isRequired, + onResultFilterUpdate: PropTypes.func.isRequired, onCommandSubmit: PropTypes.func.isRequired, onTableExpandedUpdate: PropTypes.func.isRequired, onTableColumnsVisibilityUpdate: PropTypes.func.isRequired, @@ -267,6 +292,7 @@ ExperimentsTable.propTypes = { ExperimentsTable.defaultProps = { resultsStatus: {}, + resultFilter: {}, }; export default ExperimentsTable; diff --git a/frontend/src/components/experiments_table_cell/ResultFilter.jsx b/frontend/src/components/experiments_table_cell/ResultFilter.jsx new file mode 100644 index 00000000..cc96c106 --- /dev/null +++ b/frontend/src/components/experiments_table_cell/ResultFilter.jsx @@ -0,0 +1,35 @@ +import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; + +const ResultFilter = ({ projectId, filterKey, filterText, onResultFilterUpdate }) => { + const onChange = useCallback( + (e) => { + onResultFilterUpdate(projectId, filterKey, e.target.value); + }, + [projectId, filterKey, onResultFilterUpdate] + ); + + return ( +
+ +
+ ); +}; + +ResultFilter.propTypes = { + projectId: PropTypes.number.isRequired, + filterKey: PropTypes.string.isRequired, + filterText: PropTypes.string, + onResultFilterUpdate: PropTypes.func.isRequired, +}; + +ResultFilter.defaultProps = { + filterText: '', +}; + +export default ResultFilter; diff --git a/frontend/src/containers/PlotContainer.jsx b/frontend/src/containers/PlotContainer.jsx index 45dc5de6..cb206287 100644 --- a/frontend/src/containers/PlotContainer.jsx +++ b/frontend/src/containers/PlotContainer.jsx @@ -14,6 +14,7 @@ import { updateAxisScale, toggleLogKeySelect, updateResultSelect, + updateResultFilter, updateResultsConfigSelect, updateXAxisKey, updateAxisScaleRangeType, @@ -32,7 +33,12 @@ import LogVisualizer from '../components/LogVisualizer'; import SideBar from '../components/SideBar'; import ResultTypeSelector from '../components/ResultTypeSelector'; import { defaultProjectStatus, defaultProjectConfig } from '../constants'; -import { startPolling, stopPolling } from '../utils'; +import { + startPolling, + stopPolling, + getGrandParentDirectoryName, + displayResultNameFull, +} from '../utils'; class PlotContainer extends React.Component { componentDidMount() { @@ -145,12 +151,14 @@ class PlotContainer extends React.Component { project={project} results={results} resultsStatus={projectStatus.resultsStatus} + resultFilter={projectStatus.resultFilter} stats={stats} projectConfig={projectConfig} globalConfig={globalConfig} onResultsConfigSelectUpdate={this.props.updateResultsConfigSelect} onResultUpdate={this.props.updateResult} onResultSelect={this.props.updateResultSelect} + onResultFilterUpdate={this.props.updateResultFilter} onCommandSubmit={this.props.createCommand} onTableExpandedUpdate={this.props.updateTableExpanded} onTableColumnsVisibilityUpdate={this.handleExperimentsTableColumnsVisibilityUpdate} @@ -187,6 +195,7 @@ PlotContainer.propTypes = { updateAxisScale: PropTypes.func.isRequired, toggleLogKeySelect: PropTypes.func.isRequired, updateResultSelect: PropTypes.func.isRequired, + updateResultFilter: PropTypes.func.isRequired, updateResultsConfigSelect: PropTypes.func.isRequired, updateXAxisKey: PropTypes.func.isRequired, updateAxisScaleRangeType: PropTypes.func.isRequired, @@ -197,6 +206,33 @@ PlotContainer.propTypes = { updateTargetResultType: PropTypes.func.isRequired, }; +const getTargetTextForFilter = (project, result, filterKey) => { + switch (filterKey) { + case 'group': + return getGrandParentDirectoryName(result); + case 'name': + return displayResultNameFull(project, result); + default: + return result[filterKey]; + } +}; + +const filterResults = (project, results, resultFilter) => { + const filteredResults = Object.keys(results).reduce((pre, resultId) => { + const result = results[resultId]; + const isMatched = Object.keys(resultFilter).every((filterKey) => { + const filterText = resultFilter[filterKey]; + const targetText = getTargetTextForFilter(project, result, filterKey); + return !targetText || targetText.includes(filterText); + }); + if (isMatched) { + return { ...pre, [resultId]: result }; + } + return pre; + }, {}); + return filteredResults; +}; + const mapStateToProps = (state, ownProps) => { const projectId = Number(ownProps.params.projectId); const { entities, status, config } = state; @@ -205,11 +241,15 @@ const mapStateToProps = (state, ownProps) => { const projectStatus = status.projectsStatus[projectId] || defaultProjectStatus; const projectConfig = config.projectsConfig[projectId] || defaultProjectConfig; const globalConfig = config.global; + const { resultFilter = {} } = projectStatus; const { stats } = status; + + const filteredResults = filterResults(project, results, resultFilter); + return { projectId, project, - results, + results: filteredResults, projectStatus, projectConfig, globalConfig, @@ -230,6 +270,7 @@ export default connect( updateAxisScale, toggleLogKeySelect, updateResultSelect, + updateResultFilter, updateResultsConfigSelect, updateXAxisKey, updateAxisScaleRangeType, diff --git a/frontend/src/reducers/index.jsx b/frontend/src/reducers/index.jsx index c898fd70..684e9d45 100644 --- a/frontend/src/reducers/index.jsx +++ b/frontend/src/reducers/index.jsx @@ -225,9 +225,24 @@ const resultsStatusReducer = (state = {}, action) => { return state; }; +const resultFilterReducer = (state = {}, action) => { + switch (action.type) { + case ActionTypes.RESULT_FILTER_UPDATE: { + const { filterKey, filterText } = action; + return { + ...state, + [filterKey]: filterText, + }; + } + default: + return state; + } +}; + const projectStatusReducer = combineReducers({ chartDownloadStatus: chartDownloadStatusReducer, resultsStatus: resultsStatusReducer, + resultFilter: resultFilterReducer, }); const projectsStatusReducer = (state = {}, action) => { diff --git a/frontend/src/store/uiPropTypes.js b/frontend/src/store/uiPropTypes.js index fd30aaf6..417f85ef 100644 --- a/frontend/src/store/uiPropTypes.js +++ b/frontend/src/store/uiPropTypes.js @@ -173,6 +173,8 @@ export const resultStatus = PropTypes.shape({ export const resultsStatus = PropTypes.objectOf(resultStatus); +export const resultFilter = PropTypes.objectOf(PropTypes.string); + export const projectStatus = PropTypes.shape({ chartDownloadStatus: PropTypes.oneOf(Object.values(CHART_DOWNLOAD_STATUS)), resultsStatus,