diff --git a/frontend/src/actions/index.js b/frontend/src/actions/index.js index 14084691..ff6d9416 100644 --- a/frontend/src/actions/index.js +++ b/frontend/src/actions/index.js @@ -257,11 +257,14 @@ export const updateTableExpanded = (projectId, expanded) => ({ }); export const TABLE_STATE_COLUMNS_VISIBILITY_UPDATE = 'TABLE_STATE_COLUMNS_VISIBILITY_UPDATE'; -export const updateTableColumnsVisibility = (projectId, hiddenLogKeys, hiddenArgKeys) => ({ +export const updateTableColumnsVisibility = ( + projectId, hiddenLogKeys, hiddenArgKeys, isGrouped +) => ({ type: TABLE_STATE_COLUMNS_VISIBILITY_UPDATE, projectId, hiddenLogKeys, - hiddenArgKeys + hiddenArgKeys, + isGrouped }); export const ASSETS_TABLE_STATE_COLUMNS_VISIBILITY_UPDATE = 'ASSETS_TABLE_STATE_COLUMNS_VISIBILITY_UPDATE'; diff --git a/frontend/src/components/ExperimentsTable.jsx b/frontend/src/components/ExperimentsTable.jsx index af527fbf..5bd381c4 100644 --- a/frontend/src/components/ExperimentsTable.jsx +++ b/frontend/src/components/ExperimentsTable.jsx @@ -5,6 +5,7 @@ import ReactTable from 'react-table'; import * as uiPropTypes from '../store/uiPropTypes'; import { argValue2string, + getGrandParentDirectoryName, getLastLogDict, sortMethod } from '../utils'; @@ -44,6 +45,11 @@ const ExperimentsTable = (props) => { onResultsConfigSelectUpdate(project.id, resultId, !evt.target.checked); }); }; + const generateHandleGroupedResultsConfigSelectChange = (groupedResultKeys) => (evt) => { + groupedResultKeys.forEach((resultId) => { + onResultsConfigSelectUpdate(project.id, resultId, !evt.target.checked); + }); + }; const defaultStyle = { textAlign: 'right' @@ -53,9 +59,78 @@ const ExperimentsTable = (props) => { const expanded = resultList.length === 0 ? {} : tableState.expanded; const { hiddenLogKeys = [], - hiddenArgKeys = [] + hiddenArgKeys = [], + isGrouped = false } = tableState; + const nameColumns = [ + { + Header: 0} + indeterminate={isPartialSelect} + onChange={handleResultsConfigSelectChange} + />, + Cell: (p) => { + const { original } = p; + if (!original) { + return {}; + } + const { id } = original; + return (); + }, + className: 'text-center', + sortable: false, + minWidth: 40, + Aggregated: (row) => { + const groupedResultKeys = row.subRows.map((r) => { + const { _original } = r; + return _original.id; + }); + const groupedVisibleResultCount = groupedResultKeys.filter( + (resultId) => !(resultsConfig[resultId] || {}).hidden).length; + const isGroupedPartialSelect = groupedVisibleResultCount > 0 && + visibleResultCount < groupedResultKeys.length; + return 0} + indeterminate={isGroupedPartialSelect} + onChange={generateHandleGroupedResultsConfigSelectChange(groupedResultKeys)} + />; + } + }, + { + Header: 'name', + id: 'name', + Cell: (p) => { + const { original } = p; + if (original) { + return (); + } + return null; + }, + minWidth: 250 + } + ]; + if (isGrouped) { + nameColumns.unshift({ + Header: '', + id: 'group', + accessor: (p) => getGrandParentDirectoryName(p) + }); + } + const groupedKey = isGrouped ? ['group'] : []; + const logs = logKeys.map((logKey) => ({ Header: logKey, id: `logKey${logKey}`, @@ -67,7 +142,8 @@ const ExperimentsTable = (props) => { return lastLogDict[logKey]; }, style: defaultStyle, - show: !hiddenLogKeys.find((k) => k === logKey) + show: !hiddenLogKeys.find((k) => k === logKey), + aggregate: () => '' })); const argsList = argKeys.map((argKey) => ({ @@ -82,44 +158,14 @@ const ExperimentsTable = (props) => { return argValue2string(argDict[argKey]); }, style: defaultStyle, - show: !hiddenArgKeys.find((k) => k === argKey) + show: !hiddenArgKeys.find((k) => k === argKey), + aggregate: () => '' })); const columns = [ { - Header: 0} - indeterminate={isPartialSelect} - onChange={handleResultsConfigSelectChange} - />, - Cell: (p) => { - const { original } = p; - const { id } = original; - return (); - }, - className: 'text-center', - sortable: false, - minWidth: 40 - }, - { - Header: 'name', - id: 'name', - Cell: (p) => { - const { original } = p; - return (); - }, - minWidth: 250 + Header: ' ', // To prevent from showing "Pivoted" on default (=''), set space on purpose. + columns: nameColumns }, { Header: 'last logs', @@ -146,6 +192,7 @@ const ExperimentsTable = (props) => { id: 'result_id' } ]} + pivotBy={groupedKey} freezeWhenExpanded={expanded === {}} SubComponent={(p) => ( { /> )} getTrProps={(state, rowInfo) => { + if (rowInfo && !rowInfo.original) { + return {}; + } const resultId = rowInfo && rowInfo.original.id; const resultStatus = resultsStatus[resultId] || {}; return { diff --git a/frontend/src/components/ExperimentsTableConfigurator.jsx b/frontend/src/components/ExperimentsTableConfigurator.jsx index 50bd08a2..c2df38f3 100644 --- a/frontend/src/components/ExperimentsTableConfigurator.jsx +++ b/frontend/src/components/ExperimentsTableConfigurator.jsx @@ -19,6 +19,7 @@ class ExperimentsTableConfigurator extends React.Component { this.handleModalShow = this.handleModalShow.bind(this); this.handleModalHide = this.handleModalHide.bind(this); this.handleChange = this.handleChange.bind(this); + this.handleIsGrouped = this.handleIsGrouped.bind(this); this.state = { showModal: false @@ -42,7 +43,8 @@ class ExperimentsTableConfigurator extends React.Component { const { tableState } = projectConfig; const { hiddenLogKeys = [], - hiddenArgKeys = [] + hiddenArgKeys = [], + isGrouped = false } = tableState; if (prefix === 'logKey') { @@ -51,31 +53,51 @@ class ExperimentsTableConfigurator extends React.Component { : hiddenLogKeys.filter((vk) => vk !== event.target.name); this.props.onTableColumnsVisibilityUpdate( - this.props.project.id, nextHiddenLogKeys, hiddenArgKeys); + this.props.project.id, nextHiddenLogKeys, hiddenArgKeys, isGrouped); } else { const nextHiddenArgKeys = !event.target.checked ? hiddenArgKeys.concat(event.target.name) : hiddenArgKeys.filter((vk) => vk !== event.target.name); this.props.onTableColumnsVisibilityUpdate( - this.props.project.id, hiddenLogKeys, nextHiddenArgKeys); + this.props.project.id, hiddenLogKeys, nextHiddenArgKeys, isGrouped); } } + handleIsGrouped(event) { + const { tableState } = this.props.projectConfig; + + this.props.onTableColumnsVisibilityUpdate( + this.props.project.id, tableState.hiddenLogKeys, tableState.hiddenArgKeys, + event.target.checked + ); + } + render() { const { stats, projectConfig } = this.props; const { logKeys, argKeys } = stats; const { tableState } = projectConfig; const { hiddenLogKeys = [], - hiddenArgKeys = [] + hiddenArgKeys = [], + isGrouped = false } = tableState; return (
- +
+ + + Grouping + +
diff --git a/frontend/src/reducers/index.jsx b/frontend/src/reducers/index.jsx index e34b1b2e..07fff871 100644 --- a/frontend/src/reducers/index.jsx +++ b/frontend/src/reducers/index.jsx @@ -426,7 +426,8 @@ const tableStateReducer = (state = {}, action) => { const { expanded = {}, hiddenLogKeys = [], - hiddenArgKeys = [] + hiddenArgKeys = [], + isGrouped = false } = action; switch (action.type) { case ActionTypes.TABLE_STATE_EXPANDED_UPDATE: @@ -438,7 +439,8 @@ const tableStateReducer = (state = {}, action) => { return { ...state, hiddenLogKeys, - hiddenArgKeys + hiddenArgKeys, + isGrouped }; default: return state; diff --git a/frontend/src/store/uiPropTypes.js b/frontend/src/store/uiPropTypes.js index 20266019..d74a0694 100644 --- a/frontend/src/store/uiPropTypes.js +++ b/frontend/src/store/uiPropTypes.js @@ -60,6 +60,7 @@ export const result = PropTypes.shape({ id: resultId, pathName: PropTypes.string, name: PropTypes.string, + group: PropTypes.string, isUnregistered: PropTypes.bool, logs, args, @@ -138,7 +139,8 @@ export const projectConfig = PropTypes.shape({ PropTypes.bool, PropTypes.object ]), hiddenLogKeys: PropTypes.arrayOf(PropTypes.string), - hiddenArgKeys: PropTypes.arrayOf(PropTypes.string) + hiddenArgKeys: PropTypes.arrayOf(PropTypes.string), + isGrouped: PropTypes.bool }).isRequired, resultsConfig: resultsConfig.isRequired, lines: lines.isRequired diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js index b75e5676..183a7aeb 100644 --- a/frontend/src/utils/index.js +++ b/frontend/src/utils/index.js @@ -37,6 +37,10 @@ export const getRelativeResultPathName = (project = {}, result = {}) => ( path.relative(project.pathName || '', result.pathName || '') ); +export const getGrandParentDirectoryName = (result = {}) => ( + path.basename(path.resolve(result.pathName, '..')) +); + export const displayResultNameFull = (project = {}, result = {}) => ( result.name || getRelativeResultPathName(project, result) );