diff --git a/packages/xod-client/src/debugger/reducer.js b/packages/xod-client/src/debugger/reducer.js index 726fd73e..f7610efc 100644 --- a/packages/xod-client/src/debugger/reducer.js +++ b/packages/xod-client/src/debugger/reducer.js @@ -33,7 +33,12 @@ import { import * as EAT from '../editor/actionTypes'; -import { UPLOAD_MSG_TYPE, LOG_TAB_TYPE, SESSION_TYPE, NEW_SHEET } from './constants'; +import { + UPLOAD_MSG_TYPE, + LOG_TAB_TYPE, + SESSION_TYPE, + NEW_SHEET, +} from './constants'; import * as MSG from './messages'; import { STATUS } from '../utils/constants'; import { isXodErrorMessage } from './debugProtocol'; @@ -140,10 +145,15 @@ const addMessagesOrIncrementSkippedLines = R.curry( const updateInteractiveNodeValues = R.curry((messageList, state) => { const MapToRekey = R.prop('nodeIdsMap', state); + const tableLogNodeIds = R.compose( + R.unnest, + R.values, + R.prop('tableLogSources') + )(state); // All node ids that is not represented in `tableLogNodeIds` is `watch` nodes const [tableLogMessages, watchNodeMessages] = R.compose( R.map(R.fromPairs), - R.partition(R.compose(isAmong(state.tableLogNodeIds), R.nth(0))), + R.partition(R.compose(isAmong(tableLogNodeIds), R.nth(0))), R.toPairs, renameKeys(MapToRekey), R.groupBy(R.prop('nodeId')), @@ -203,6 +213,14 @@ const updateInteractiveNodeValues = R.curry((messageList, state) => { )(state); }); +const updateTableLogSources = R.curry((patchPath, tableLogNodeIds, state_) => + R.over( + R.lensPath(['tableLogSources', patchPath]), + R.compose(R.uniq, R.concat(tableLogNodeIds), R.defaultTo([])), + state_ + ) +); + /* eslint-disable no-bitwise */ const filterPinKeysByBitmask = R.curry((pinKeys, bitmask) => R.addIndex(R.filter)((val, idx) => bitmask & (1 << idx), pinKeys) @@ -522,7 +540,10 @@ export default (state = initialState, action) => { R.assoc('isSkippingNewSerialLogLines', false), R.assoc('numberOfSkippedSerialLogLines', 0), R.assoc('nodeIdsMap', invertedNodeIdsMap), - R.assoc('tableLogNodeIds', action.payload.tableLogNodeIds), + updateTableLogSources( + action.payload.patchPath, + action.payload.tableLogNodeIds + ), R.assoc('nodePinKeysMap', action.payload.nodePinKeysMap), rekeyAndAssocPinsAffectedByErrorRaisers( invertedNodeIdsMap, @@ -648,7 +669,11 @@ export default (state = initialState, action) => { R.assoc('isPreparingSimulation', false), R.assoc('isOutdated', false), R.assoc('uploadProgress', null), - R.assoc('tableLogNodeIds', action.payload.tableLogNodeIds), + updateTableLogSources( + action.payload.patchPath, + action.payload.tableLogNodeIds + ), + R.assoc('nodeIdsMap', invertedNodeIdsMap), R.assoc('nodePinKeysMap', action.payload.nodePinKeysMap), R.assoc('globals', action.payload.globals), diff --git a/packages/xod-client/src/debugger/selectors.js b/packages/xod-client/src/debugger/selectors.js index a903d392..d308ff19 100644 --- a/packages/xod-client/src/debugger/selectors.js +++ b/packages/xod-client/src/debugger/selectors.js @@ -11,6 +11,7 @@ import { DEBUGGER_TAB_ID } from '../editor/constants'; import { SESSION_TYPE } from './constants'; import { createMemoizedSelector } from '../utils/selectorTools'; +import { getTableLogSourceLabels } from './utils'; export const getDebuggerState = R.prop('debugger'); @@ -308,7 +309,19 @@ export const getTableLogValues = R.compose( getDebuggerState ); -export const getTableLogSources = R.compose(R.keys, getTableLogValues); +const getTableLogSourcesRaw = R.compose( + R.prop('tableLogSources'), + getDebuggerState +); + +// :: State -> [{ nodeId: NodeId, label: String }] +export const getTableLogSources = state => + R.compose( + R.unnest, + R.values, + R.mapObjIndexed(getTableLogSourceLabels(state.project)), + getTableLogSourcesRaw + )(state); export const getTableLogsByNodeId = R.curry((nodeId, state) => R.compose(R.pathOr([], ['tableLogValues', nodeId]), getDebuggerState)(state) diff --git a/packages/xod-client/src/debugger/state.js b/packages/xod-client/src/debugger/state.js index ed4e5f0c..7213be27 100644 --- a/packages/xod-client/src/debugger/state.js +++ b/packages/xod-client/src/debugger/state.js @@ -39,7 +39,10 @@ export default { currentStage: LOG_TAB_TYPE.COMPILER, nodeIdsMap: {}, watchNodeValues: {}, - tableLogNodeIds: [], + tableLogSources: { + // PatchPath : [NodeId] + // where PatchPath is a path of root patch that was compiled + }, tableLogValues: { // NodeId : [ /* Experiments history */ [[String]] ] }, diff --git a/packages/xod-client/src/debugger/utils.js b/packages/xod-client/src/debugger/utils.js index 756dca34..f7efab5e 100644 --- a/packages/xod-client/src/debugger/utils.js +++ b/packages/xod-client/src/debugger/utils.js @@ -1,6 +1,6 @@ import * as R from 'ramda'; import * as XP from 'xod-project'; -import { foldMaybe } from 'xod-func-tools'; +import { foldMaybe, mapIndexed } from 'xod-func-tools'; import { UPLOAD_MSG_TYPE } from './constants'; @@ -38,3 +38,93 @@ export const getTetheringInetNodeId = R.curry( ); export const isErrorMessage = R.propEq('type', UPLOAD_MSG_TYPE.ERROR); + +// :: Project -> [NodeId] -> PatchPath -> [{ nodeId: NodeId, label: String }] +export const getTableLogSourceLabels = R.curry( + (project, sourceNodeIds, rootPatchPath) => + R.compose( + R.map( + R.applySpec({ + nodeId: R.prop('nodeId'), + label: R.compose( + R.join(' > '), + R.concat([rootPatchPath]), + R.pluck('finalLabel'), + R.prop('chunks') + ), + }) + ), + R.reduce((acc, nextSource) => { + const reducedChunks = R.pluck('chunks', acc); + const result = R.over( + R.lensProp('chunks'), + mapIndexed((chunk, idx) => + R.compose( + R.ifElse( + R.equals(0), + () => R.assoc('finalLabel', chunk.label, chunk), + num => + R.assoc('finalLabel', `${chunk.label} #${num + 1}`, chunk) + ), + R.length, + R.uniqBy(R.both(R.prop('label'), R.prop('nodeId'))), + R.filter( + rc => + rc.nodeId !== chunk.nodeId && + rc.label === chunk.label && + rc.parent === chunk.parent + ), + R.reject(R.isNil), + R.pluck(idx) + )(reducedChunks) + ), + nextSource + ); + return [...acc, result]; + }, []), + R.map(nodeId => { + // Convert chained NodeId (`a~b~c`) into [NodeId] (['a', 'b', 'c']) + const splittedNodeId = R.split('~', nodeId); + return R.compose( + foldMaybe( + { + nodeId, + chunks: [ + { + nodeId, + label: [`DELETED (${nodeId})`], + }, + ], + }, + R.applySpec({ + nodeId: R.always(nodeId), + chunks: R.identity, + }) + ), + R.map( + mapIndexed((chunk, idx) => + R.compose( + R.assoc('parent', R.__, chunk), + R.join('~'), + R.concat([rootPatchPath]), + R.take(idx) + )(splittedNodeId) + ) + ), + XP.mapNestedNodes( + node => { + const nodeType = XP.getNodeType(node); + const label = XP.getNodeLabel(node); + return { + nodeType, + nodeId: XP.getNodeId(node), + label: label.length ? label : XP.getBaseName(nodeType), + }; + }, + project, + rootPatchPath + ) + )(splittedNodeId); + }) + )(sourceNodeIds) +); diff --git a/packages/xod-client/src/editor/containers/TableLog.jsx b/packages/xod-client/src/editor/containers/TableLog.jsx index c66904f7..f14c8565 100644 --- a/packages/xod-client/src/editor/containers/TableLog.jsx +++ b/packages/xod-client/src/editor/containers/TableLog.jsx @@ -83,7 +83,7 @@ class TableLog extends React.Component { onSaveLogClicked() { const tsv = gridToTsv(this.getSheet()); - const defaultFilename = `log-${this.props.nodeId}-${ + const defaultFilename = `log-${this.getNodeLabel()}-${ this.props.sheetIndex }.txt`; @@ -116,6 +116,13 @@ class TableLog extends React.Component { return this.props.document.length; } + getNodeLabel() { + return R.compose( + R.propOr('unknown', 'label'), + R.find(R.propEq('nodeId', this.props.nodeId)) + )(this.props.sources); + } + isEmptySheet() { return this.getSheet().length === 0; } @@ -136,9 +143,9 @@ class TableLog extends React.Component { disabled={hasNoSources} > {hasNoSources ? : null} - {this.props.sources.map(nodeId => ( + {this.props.sources.map(({ nodeId, label }) => ( ))} @@ -202,7 +209,7 @@ TableLog.propTypes = { // from parent tabId: PropTypes.string.isRequired, // connect - sources: PropTypes.arrayOf(PropTypes.string).isRequired, + sources: PropTypes.arrayOf(PropTypes.object).isRequired, document: PropTypes.array.isRequired, nodeId: PropTypes.string.isRequired, sheetIndex: PropTypes.number.isRequired, diff --git a/packages/xod-client/test/tableLogSources.spec.js b/packages/xod-client/test/tableLogSources.spec.js new file mode 100644 index 00000000..f8d290b6 --- /dev/null +++ b/packages/xod-client/test/tableLogSources.spec.js @@ -0,0 +1,263 @@ +import * as R from 'ramda'; +import { assert } from 'chai'; +import { defaultizeProject } from 'xod-project/test/helpers'; + +import { getTableLogSourceLabels } from '../src/debugger/utils'; + +describe('generate labels for table log sources', () => { + const assertSameSources = (project, rootPatchPath, expectedLabelsForIds) => { + const ids = R.keys(expectedLabelsForIds); + const result = getTableLogSourceLabels(project, ids, rootPatchPath); + const expectedResult = R.compose( + R.map( + R.applySpec({ + nodeId: R.nth(0), + label: R.nth(1), + }) + ), + R.toPairs + )(expectedLabelsForIds); + assert.deepEqual(result, expectedResult); + }; + + it('should contain `DELETED (nodeId)` for a source that not exists in the patch', () => { + const project = defaultizeProject({ + patches: { + '@/main': {}, + 'xod/debug/table-log': {}, + }, + }); + + assertSameSources(project, '@/main', { + 'a~n~t': '@/main > DELETED (a~n~t)', + t: '@/main > DELETED (t)', + }); + }); + it('should contain node type base names if the node is unlabeled', () => { + const project = defaultizeProject({ + patches: { + '@/main': { + nodes: { + a: { + type: '@/nested-1', + }, + b: { + type: '@/nested-2', + }, + t: { + type: 'xod/debug/table-log', + }, + }, + }, + '@/nested-1': { + nodes: { + n: { + type: '@/nested-2', + }, + }, + }, + '@/nested-2': { + nodes: { + t: { + type: 'xod/debug/table-log', + }, + }, + }, + 'xod/debug/table-log': {}, + }, + }); + + assertSameSources(project, '@/main', { + 'a~n~t': '@/main > nested-1 > nested-2 > table-log', + 'b~t': '@/main > nested-2 > table-log', + t: '@/main > table-log', + }); + }); + it('should contain node labels if it is set', () => { + const project = defaultizeProject({ + patches: { + '@/main': { + nodes: { + a: { + type: '@/nested-1', + label: 'Nested One', + }, + b: { + type: '@/nested-2', + label: 'Nested Two', + }, + c: { + type: '@/nested-label', + label: 'Nested Label', + }, + t: { + type: 'xod/debug/table-log', + label: 'My Table Log', + }, + }, + }, + '@/nested-1': { + nodes: { + n: { + type: '@/nested-2', + }, + }, + }, + '@/nested-2': { + nodes: { + t: { + type: 'xod/debug/table-log', + }, + }, + }, + '@/nested-label': { + nodes: { + n: { + type: '@/nested-2', + label: 'Log', + }, + }, + }, + 'xod/debug/table-log': {}, + }, + }); + + assertSameSources(project, '@/main', { + 'a~n~t': '@/main > Nested One > nested-2 > table-log', + 'b~t': '@/main > Nested Two > table-log', + 'c~n~t': '@/main > Nested Label > Log > table-log', + t: '@/main > My Table Log', + }); + }); + it('should add numbers to the duplicates on a correct nesting level', () => { + const project = defaultizeProject({ + patches: { + '@/main': { + nodes: { + a: { + type: '@/nested-1', + label: 'Nested', + }, + a2: { + type: '@/nested-1', + label: 'Nested', + }, + a3: { + type: '@/nested-1', + }, + b: { + type: '@/nested-label', + }, + b2: { + type: '@/nested-label', + }, + d: { + type: '@/nested-double', + }, + d2: { + type: '@/nested-double', + }, + d3: { + type: '@/nested-double', + label: 'Double Inside', + }, + nc: { + type: '@/nested-complex', + }, + nc2: { + type: '@/nested-complex', + }, + t: { + type: 'xod/debug/table-log', + label: 'My Table Log', + }, + t2: { + type: 'xod/debug/table-log', + label: 'My Table Log', + }, + t3: { + type: 'xod/debug/table-log', + label: 'My Table Log', + }, + }, + }, + '@/nested-1': { + nodes: { + n: { + type: '@/nested-2', + }, + }, + }, + '@/nested-2': { + nodes: { + t: { + type: 'xod/debug/table-log', + }, + }, + }, + '@/nested-label': { + nodes: { + n: { + type: '@/nested-2', + label: 'Log', + }, + }, + }, + '@/nested-double': { + nodes: { + t: { type: 'xod/debug/table-log' }, + t2: { type: 'xod/debug/table-log' }, + }, + }, + '@/nested-complex': { + nodes: { + n: { type: '@/nested-1' }, + n2: { type: '@/nested-1' }, + nn: { type: '@/nested-2' }, + nn2: { type: '@/nested-2' }, + nd: { type: '@/nested-double' }, + nd2: { type: '@/nested-double' }, + }, + }, + 'xod/debug/table-log': {}, + }, + }); + + assertSameSources(project, '@/main', { + 'a~n~t': '@/main > Nested > nested-2 > table-log', + 'a2~n~t': '@/main > Nested #2 > nested-2 > table-log', + 'a3~n~t': '@/main > nested-1 > nested-2 > table-log', + 'b~n~t': '@/main > nested-label > Log > table-log', + 'b2~n~t': '@/main > nested-label #2 > Log > table-log', + 'd~t': '@/main > nested-double > table-log', + 'd~t2': '@/main > nested-double > table-log #2', + 'd2~t': '@/main > nested-double #2 > table-log', + 'd2~t2': '@/main > nested-double #2 > table-log #2', + 'd3~t': '@/main > Double Inside > table-log', + 'd3~t2': '@/main > Double Inside > table-log #2', + 'nc~n~n~t': '@/main > nested-complex > nested-1 > nested-2 > table-log', + 'nc~n2~n~t': + '@/main > nested-complex > nested-1 #2 > nested-2 > table-log', + 'nc~nn~t': '@/main > nested-complex > nested-2 > table-log', + 'nc~nn2~t': '@/main > nested-complex > nested-2 #2 > table-log', + 'nc~nd~t': '@/main > nested-complex > nested-double > table-log', + 'nc~nd~t2': '@/main > nested-complex > nested-double > table-log #2', + 'nc~nd2~t': '@/main > nested-complex > nested-double #2 > table-log', + 'nc~nd2~t2': '@/main > nested-complex > nested-double #2 > table-log #2', + 'nc2~n~n~t': + '@/main > nested-complex #2 > nested-1 > nested-2 > table-log', + 'nc2~n2~n~t': + '@/main > nested-complex #2 > nested-1 #2 > nested-2 > table-log', + 'nc2~nn~t': '@/main > nested-complex #2 > nested-2 > table-log', + 'nc2~nn2~t': '@/main > nested-complex #2 > nested-2 #2 > table-log', + 'nc2~nd~t': '@/main > nested-complex #2 > nested-double > table-log', + 'nc2~nd~t2': '@/main > nested-complex #2 > nested-double > table-log #2', + 'nc2~nd2~t': '@/main > nested-complex #2 > nested-double #2 > table-log', + 'nc2~nd2~t2': + '@/main > nested-complex #2 > nested-double #2 > table-log #2', + t: '@/main > My Table Log', + t2: '@/main > My Table Log #2', + t3: '@/main > My Table Log #3', + }); + }); +});