diff --git a/src/browser/modules/D3Visualization/components/Explorer.jsx b/src/browser/modules/D3Visualization/components/Explorer.jsx index c6238908316..30cf747f929 100644 --- a/src/browser/modules/D3Visualization/components/Explorer.jsx +++ b/src/browser/modules/D3Visualization/components/Explorer.jsx @@ -26,6 +26,8 @@ import neoGraphStyle from '../graphStyle' import { InspectorComponent } from './Inspector' import { LegendComponent } from './Legend' import { StyledFullSizeContainer } from './styled' +import { getMaxFieldItems } from 'shared/modules/settings/settingsDuck' +import { connect } from 'react-redux' const deduplicateNodes = nodes => { return nodes.reduce( @@ -231,6 +233,7 @@ export class ExplorerComponent extends Component { setGraph={this.props.setGraph} /> ({ + maxFieldItems: getMaxFieldItems(state) +}))(ExplorerComponent) diff --git a/src/browser/modules/D3Visualization/components/Inspector.jsx b/src/browser/modules/D3Visualization/components/Inspector.jsx index 02dcdaa151d..470c7f34910 100644 --- a/src/browser/modules/D3Visualization/components/Inspector.jsx +++ b/src/browser/modules/D3Visualization/components/Inspector.jsx @@ -40,6 +40,9 @@ import { GrassEditor } from './GrassEditor' import { RowExpandToggleComponent } from './RowExpandToggle' import ClickableUrls from '../../../components/clickable-urls' import numberToUSLocale from 'shared/utils/number-to-US-locale' +import { StyledTruncatedMessage } from 'browser/modules/Stream/styled' +import { Icon } from 'semantic-ui-react' +import Ellipsis from 'browser-components/Ellipsis' const mapItemProperties = itemProperties => itemProperties @@ -149,6 +152,12 @@ export class InspectorComponent extends Component { + {this.props.hasTruncatedFields && ( + + Record fields have been + truncated.  + + )} {description} diff --git a/src/browser/modules/Sidebar/Settings.jsx b/src/browser/modules/Sidebar/Settings.jsx index 76c40d597fc..5aa01dfb498 100644 --- a/src/browser/modules/Sidebar/Settings.jsx +++ b/src/browser/modules/Sidebar/Settings.jsx @@ -159,6 +159,13 @@ const visualSettings = [ tooltip: "Max number of rows to render in 'Rows' result view" } }, + { + maxFieldItems: { + displayName: 'Record field limit', + tooltip: + 'Max number of items in a field per record being handled. When reached, items get truncated.' + } + }, { autoComplete: { displayName: 'Connect result nodes', diff --git a/src/browser/modules/Stream/CypherFrame/AsciiView.jsx b/src/browser/modules/Stream/CypherFrame/AsciiView.jsx index aeb2b72676a..ee0e2ccff0e 100644 --- a/src/browser/modules/Stream/CypherFrame/AsciiView.jsx +++ b/src/browser/modules/Stream/CypherFrame/AsciiView.jsx @@ -38,8 +38,10 @@ import { stringifyResultArray } from './helpers' import { stringModifier } from 'services/bolt/cypherTypesFormatting' +import { getMaxFieldItems } from 'shared/modules/settings/settingsDuck' +import { connect } from 'react-redux' -export class AsciiView extends Component { +export class AsciiViewComponent extends Component { state = { serializedRows: [], bodyMessage: '' @@ -72,8 +74,8 @@ export class AsciiView extends Component { return !this.equalProps(props) || !shallowEquals(state, this.state) } - makeState(props) { - const { result, maxRows } = props + makeState (props) { + const { result, maxRows, maxFieldItems } = props const { bodyMessage = null } = getBodyAndStatusBarMessages(result, maxRows) || {} this.setState({ bodyMessage }) @@ -82,7 +84,7 @@ export class AsciiView extends Component { const serializedRows = stringifyResultArray( stringModifier, - transformResultRecordsToResultArray(records) + transformResultRecordsToResultArray(records, maxFieldItems) ) || [] this.setState({ serializedRows }) const maxColWidth = asciitable.maxColumnWidth(serializedRows) @@ -110,6 +112,10 @@ export class AsciiView extends Component { } } +export const AsciiView = connect(state => ({ + maxFieldItems: getMaxFieldItems(state) +}))(AsciiViewComponent) + export class AsciiStatusbar extends Component { state = { maxSliderWidth: 140, diff --git a/src/browser/modules/Stream/CypherFrame/AsciiView.test.js b/src/browser/modules/Stream/CypherFrame/AsciiView.test.js index d18671cc25a..8ce025b87ef 100644 --- a/src/browser/modules/Stream/CypherFrame/AsciiView.test.js +++ b/src/browser/modules/Stream/CypherFrame/AsciiView.test.js @@ -22,7 +22,7 @@ import React from 'react' import { render } from '@testing-library/react' import neo4j from 'neo4j-driver' -import { AsciiView, AsciiStatusbar } from './AsciiView' +import { AsciiViewComponent as AsciiView, AsciiStatusbar } from './AsciiView' describe('AsciiViews', () => { describe('AsciiView', () => { diff --git a/src/browser/modules/Stream/CypherFrame/CodeView.jsx b/src/browser/modules/Stream/CypherFrame/CodeView.jsx index 8afb974c4b7..a590817b448 100644 --- a/src/browser/modules/Stream/CypherFrame/CodeView.jsx +++ b/src/browser/modules/Stream/CypherFrame/CodeView.jsx @@ -29,7 +29,10 @@ import { StyledTd, StyledExpandable } from '../styled' -import { TableStatusbar } from './TableView' +import { TableStatusbar, TableStatusbarComponent } from './TableView' +import { getMaxFieldItems } from 'shared/modules/settings/settingsDuck' +import { connect } from 'react-redux' +import { map, take } from 'lodash-es' class ExpandableContent extends Component { state = {} @@ -54,16 +57,33 @@ class ExpandableContent extends Component { } } -export class CodeView extends Component { - shouldComponentUpdate(props) { - return !this.props.result || !deepEquals(props.result, this.props.result) +const fieldLimiterFactory = maxFieldItems => (key, val) => { + if (!maxFieldItems || key !== '_fields') { + return val } - render() { - const { request = {}, query } = this.props + return map(val, field => { + return Array.isArray(field) ? take(field, maxFieldItems) : field + }) +} + +export class CodeViewComponent extends Component { + shouldComponentUpdate (props) { + return !this.props.result || !deepEquals(props.result, this.props.result) + } + render () { + const { request = {}, query, maxFieldItems } = this.props if (request.status !== 'success') return null - const resultJson = JSON.stringify(request.result.records, null, 2) - const summaryJson = JSON.stringify(request.result.summary, null, 2) + const resultJson = JSON.stringify( + request.result.records, + fieldLimiterFactory(maxFieldItems), + 2 + ) + const summaryJson = JSON.stringify( + request.result.summary, + fieldLimiterFactory(maxFieldItems), + 2 + ) return ( @@ -97,4 +117,9 @@ export class CodeView extends Component { } } +export const CodeView = connect(state => ({ + maxFieldItems: getMaxFieldItems(state) +}))(CodeViewComponent) + +export const CodeStatusbarComponent = TableStatusbarComponent export const CodeStatusbar = TableStatusbar diff --git a/src/browser/modules/Stream/CypherFrame/CodeView.test.js b/src/browser/modules/Stream/CypherFrame/CodeView.test.js index 03509d68aff..03453b2d3d3 100644 --- a/src/browser/modules/Stream/CypherFrame/CodeView.test.js +++ b/src/browser/modules/Stream/CypherFrame/CodeView.test.js @@ -22,7 +22,10 @@ import React from 'react' import { render } from '@testing-library/react' import neo4j from 'neo4j-driver' -import { CodeView, CodeStatusbar } from './CodeView' +import { + CodeViewComponent as CodeView, + CodeStatusbarComponent as CodeStatusbar +} from './CodeView' describe('CodeViews', () => { describe('CodeView', () => { diff --git a/src/browser/modules/Stream/CypherFrame/TableView.jsx b/src/browser/modules/Stream/CypherFrame/TableView.jsx index c999bb783a6..94d5104eeea 100644 --- a/src/browser/modules/Stream/CypherFrame/TableView.jsx +++ b/src/browser/modules/Stream/CypherFrame/TableView.jsx @@ -26,7 +26,8 @@ import { HTMLEntities } from 'services/santize.utils' import { StyledStatsBar, PaddedTableViewDiv, - StyledBodyMessage + StyledBodyMessage, + StyledTruncatedMessage } from '../styled' import Ellipsis from 'browser-components/Ellipsis' import { @@ -40,16 +41,21 @@ import { shallowEquals, stringifyMod } from 'services/utils' import { getBodyAndStatusBarMessages, getRecordsToDisplayInTable, - transformResultRecordsToResultArray + transformResultRecordsToResultArray, + resultHasTruncatedFields } from './helpers' import { stringModifier } from 'services/bolt/cypherTypesFormatting' import ClickableUrls, { convertUrlsToHrefTags } from '../../../components/clickable-urls' +import { getMaxFieldItems } from 'shared/modules/settings/settingsDuck' +import { connect } from 'react-redux' +import { Icon } from 'semantic-ui-react' -const renderCell = entry => { +const renderCell = (entry, maxFieldItems) => { if (Array.isArray(entry)) { - const children = entry.map((item, index) => ( + const entryToUse = maxFieldItems ? entry.slice(0, maxFieldItems) : entry + const children = entryToUse.map((item, index) => ( {renderCell(item)} {index === entry.length - 1 ? null : ', '} @@ -75,12 +81,12 @@ export const renderObject = entry => { /> ) } -const buildData = entries => { +const buildData = (entries, maxFieldItems) => { return entries.map(entry => { if (entry !== null) { return ( - {renderCell(entry)} + {renderCell(entry, maxFieldItems)} ) } @@ -91,15 +97,15 @@ const buildData = entries => { ) }) } -const buildRow = item => { +const buildRow = (item, maxFieldItems) => { return ( - {buildData(item)} + {buildData(item, maxFieldItems)} ) } -export class TableView extends Component { +export class TableViewComponent extends Component { state = { columns: [], data: [], @@ -155,7 +161,9 @@ export class TableView extends Component { )) const tableBody = ( - {this.state.data.map(item => buildRow(item))} + + {this.state.data.map(item => buildRow(item, this.props.maxFieldItems))} + ) return ( @@ -170,7 +178,11 @@ export class TableView extends Component { } } -export class TableStatusbar extends Component { +export const TableView = connect(state => ({ + maxFieldItems: getMaxFieldItems(state) +}))(TableViewComponent) + +export class TableStatusbarComponent extends Component { state = { statusBarMessage: '' } @@ -193,14 +205,32 @@ export class TableStatusbar extends Component { this.props.result, this.props.maxRows ) - if (statusBarMessage !== undefined) this.setState({ statusBarMessage }) + const hasTruncatedFields = resultHasTruncatedFields( + props.result, + props.maxFieldItems + ) + if (statusBarMessage !== undefined) { + this.setState({ statusBarMessage, hasTruncatedFields }) + } } render() { return ( - {this.state.statusBarMessage} + + {this.state.hasTruncatedFields && ( + + Record fields have been + truncated.  + + )} + {this.state.statusBarMessage} + ) } } + +export const TableStatusbar = connect(state => ({ + maxFieldItems: getMaxFieldItems(state) +}))(TableStatusbarComponent) diff --git a/src/browser/modules/Stream/CypherFrame/TableView.test.js b/src/browser/modules/Stream/CypherFrame/TableView.test.js index 2140747e5d8..6e2deb63588 100644 --- a/src/browser/modules/Stream/CypherFrame/TableView.test.js +++ b/src/browser/modules/Stream/CypherFrame/TableView.test.js @@ -22,7 +22,11 @@ import React from 'react' import { render } from '@testing-library/react' import neo4j from 'neo4j-driver' -import { TableView, TableStatusbar, renderObject } from './TableView' +import { + TableViewComponent as TableView, + TableStatusbarComponent as TableStatusbar, + renderObject +} from './TableView' describe('TableViews', () => { describe('TableView', () => { diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView.jsx b/src/browser/modules/Stream/CypherFrame/VisualizationView.jsx index 7af32b63763..27815d79b2d 100644 --- a/src/browser/modules/Stream/CypherFrame/VisualizationView.jsx +++ b/src/browser/modules/Stream/CypherFrame/VisualizationView.jsx @@ -29,6 +29,8 @@ import { StyledVisContainer } from './VisualizationView.styled' import { CYPHER_REQUEST } from 'shared/modules/cypher/cypherDuck' import { NEO4J_BROWSER_USER_ACTION_QUERY } from 'services/bolt/txMetadata' +import { getMaxFieldItems } from 'shared/modules/settings/settingsDuck' +import { resultHasTruncatedFields } from 'browser/modules/Stream/CypherFrame/helpers' export class Visualization extends Component { state = { @@ -67,11 +69,18 @@ export class Visualization extends Component { nodes, relationships } = bolt.extractNodesAndRelationshipsFromRecordsForOldVis( - props.result.records + props.result.records, + true, + props.maxFieldItems + ) + const hasTruncatedFields = resultHasTruncatedFields( + props.result, + props.maxFieldItems ) this.setState({ nodes, relationships, + hasTruncatedFields, updated: new Date().getTime() }) } @@ -115,7 +124,8 @@ export class Visualization extends Component { : 0 const resultGraph = bolt.extractNodesAndRelationshipsFromRecordsForOldVis( response.result.records, - false + false, + this.props.maxFieldItems ) this.autoCompleteRelationships( this.graph._nodes, @@ -150,7 +160,8 @@ export class Visualization extends Component { resolve({ ...bolt.extractNodesAndRelationshipsFromRecordsForOldVis( response.result.records, - false + false, + this.props.maxFieldItems ) }) } @@ -171,6 +182,7 @@ export class Visualization extends Component { { return { - graphStyleData: grassActions.getGraphStyleData(state) + graphStyleData: grassActions.getGraphStyleData(state), + maxFieldItems: getMaxFieldItems(state) } } diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/AsciiView.test.js.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/AsciiView.test.js.snap index 3b47b6a7195..b89a18efa66 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/AsciiView.test.js.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/AsciiView.test.js.snap @@ -20,14 +20,14 @@ exports[`AsciiViews AsciiStatusbar displays statusBarMessage if no rows 2`] = ` class="styled__StyledStatsBar-sc-1dtvgs1-23 gWjkIU" >
Max column width:
(no changes, no records)
diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.js.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.js.snap index ccee9f210a8..d70aedd0498 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.js.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.js.snap @@ -7,7 +7,9 @@ exports[`CodeViews CodeStatusbar displays no statusBarMessage 1`] = ` >
+ > + +
`; @@ -34,85 +36,85 @@ exports[`CodeViews CodeView displays request and response info if successful que class="styled__PaddedDiv-sc-1dtvgs1-1 kyHRpW" > diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/TableView.test.js.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/TableView.test.js.snap index 4ed112f7c6d..88de072acbd 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/TableView.test.js.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/TableView.test.js.snap @@ -7,7 +7,9 @@ exports[`TableViews TableStatusbar displays no statusBarMessage 1`] = ` >
+ > + +
`; @@ -32,7 +34,7 @@ exports[`TableViews TableView displays bodyMessage if no rows 1`] = ` class="styled__PaddedDiv-sc-1dtvgs1-1 styled__PaddedTableViewDiv-sc-1dtvgs1-2 clXnC" >
(no changes, no records)
diff --git a/src/browser/modules/Stream/CypherFrame/helpers.js b/src/browser/modules/Stream/CypherFrame/helpers.js index 17ac6742618..b245decaecc 100644 --- a/src/browser/modules/Stream/CypherFrame/helpers.js +++ b/src/browser/modules/Stream/CypherFrame/helpers.js @@ -21,24 +21,46 @@ import neo4j from 'neo4j-driver' import { entries, + flatten, + filter, get, includes, isObjectLike, lowerCase, map, - reduce + some, + reduce, + take } from 'lodash-es' import bolt from 'services/bolt/bolt' import * as viewTypes from 'shared/modules/stream/frameViewTypes' -import { - recursivelyExtractGraphItems, - flattenArray -} from 'services/bolt/boltMappings' +import { recursivelyExtractGraphItems } from 'services/bolt/boltMappings' import { stringifyMod } from 'services/utils' import { stringModifier } from 'services/bolt/cypherTypesFormatting' +/** + * Checks if a results has records which fields will be truncated when displayed + * - O(N2) complexity + * @param {Object} result + * @param {Number} maxFieldItems + * @return {boolean} + */ +export const resultHasTruncatedFields = (result, maxFieldItems) => { + if (!maxFieldItems) { + return false + } + + return some(result.records, record => + some(record.keys, key => { + const val = record.get(key) + + return Array.isArray(val) && val.length > maxFieldItems + }) + ) +} + export function getBodyAndStatusBarMessages(result, maxRows) { if (!result || !result.summary || !result.summary.resultAvailableAfter) { return {} @@ -96,6 +118,18 @@ export const getRecordsToDisplayInTable = (result, maxRows) => { : result.records } +export const flattenArrayDeep = arr => { + let toFlatten = arr + let result = [] + + while (toFlatten.length > 0) { + result = [...result, ...filter(toFlatten, item => !Array.isArray(item))] + toFlatten = flatten(filter(toFlatten, Array.isArray)) + } + + return result +} + export const resultHasNodes = (request, types = bolt.neo4j.types) => { if (!request) return false const { result = {} } = request @@ -106,7 +140,7 @@ export const resultHasNodes = (request, types = bolt.neo4j.types) => { for (let i = 0; i < records.length; i++) { const graphItems = keys.map(key => records[i].get(key)) const items = recursivelyExtractGraphItems(types, graphItems) - const flat = flattenArray(items) + const flat = flattenArrayDeep(items) const nodes = flat.filter( item => item instanceof types.Node || item instanceof types.Path ) @@ -199,10 +233,10 @@ export const stringifyResultArray = (formatter = stringModifier, arr = []) => { * Flattens graph items so only their props are left. * Leaves Neo4j Integers as they were. */ -export const transformResultRecordsToResultArray = records => { +export const transformResultRecordsToResultArray = (records, maxFieldItems) => { return records && records.length ? [records] - .map(extractRecordsToResultArray) + .map(recs => extractRecordsToResultArray(recs, maxFieldItems)) .map( flattenGraphItemsInResultArray.bind(null, neo4j.types, neo4j.isInt) )[0] @@ -213,12 +247,20 @@ export const transformResultRecordsToResultArray = records => { * Transforms an array of neo4j driver records to an array of objects. * Leaves all values as they were, just changing the data structure. */ -export const extractRecordsToResultArray = (records = []) => { +export const extractRecordsToResultArray = (records = [], maxFieldItems) => { records = Array.isArray(records) ? records : [] const keys = records[0] ? [records[0].keys] : undefined return (keys || []).concat( records.map(record => { - return record.keys.map((key, i) => record._fields[i]) + return record.keys.map((key, i) => { + const val = record._fields[i] + + if (!maxFieldItems || !Array.isArray(val)) { + return val + } + + return take(val, maxFieldItems) + }) }) ) } diff --git a/src/browser/modules/Stream/__snapshots__/HistoryRow.test.js.snap b/src/browser/modules/Stream/__snapshots__/HistoryRow.test.js.snap index f1d699d4281..5ecbdf15456 100644 --- a/src/browser/modules/Stream/__snapshots__/HistoryRow.test.js.snap +++ b/src/browser/modules/Stream/__snapshots__/HistoryRow.test.js.snap @@ -3,7 +3,7 @@ exports[`HistoryRow triggers function on click 1`] = `
  • :clear
  • diff --git a/src/browser/modules/Stream/styled.jsx b/src/browser/modules/Stream/styled.jsx index 4b992ecfc93..44d5e28495e 100644 --- a/src/browser/modules/Stream/styled.jsx +++ b/src/browser/modules/Stream/styled.jsx @@ -177,6 +177,9 @@ export const StyledStatsBar = styled.div` padding-left: 24px; width: 100%; ` +export const StyledTruncatedMessage = styled.span` + color: orange; +` export const StyledOneRowStatsBar = styled(StyledStatsBar)` height: 39px; diff --git a/src/shared/modules/settings/__snapshots__/settingsDuck.test.js.snap b/src/shared/modules/settings/__snapshots__/settingsDuck.test.js.snap index 148525a18f2..df73bbaf596 100644 --- a/src/shared/modules/settings/__snapshots__/settingsDuck.test.js.snap +++ b/src/shared/modules/settings/__snapshots__/settingsDuck.test.js.snap @@ -12,6 +12,7 @@ Object { "enableMultiStatementMode": false, "initCmd": ":play start", "initialNodeDisplay": 300, + "maxFieldItems": 500, "maxFrames": 30, "maxHistory": 30, "maxNeighbours": 100, diff --git a/src/shared/modules/settings/settingsDuck.js b/src/shared/modules/settings/settingsDuck.js index b4b499904a5..819a224f4b8 100644 --- a/src/shared/modules/settings/settingsDuck.js +++ b/src/shared/modules/settings/settingsDuck.js @@ -17,6 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +import { get } from 'lodash-es' import { APP_START, USER_CLEAR } from 'shared/modules/app/appDuck' @@ -49,6 +50,8 @@ export const getBrowserSyncConfig = ( export const getMaxNeighbours = state => state[NAME].maxNeighbours || initialState.maxNeighbours export const getMaxRows = state => state[NAME].maxRows || initialState.maxRows +export const getMaxFieldItems = state => + get(state, [NAME, 'maxFieldItems'], initialState.maxFieldItems) export const getInitialNodeDisplay = state => state[NAME].initialNodeDisplay || initialState.initialNodeDisplay export const getScrollToTop = state => state[NAME].scrollToTop @@ -89,6 +92,7 @@ const initialState = { showSampleScripts: true, browserSyncDebugServer: null, maxRows: 1000, + maxFieldItems: 500, shouldReportUdc: true, autoComplete: true, scrollToTop: true, diff --git a/src/shared/services/bolt/bolt.js b/src/shared/services/bolt/bolt.js index 27c2a29504a..cd40087abbf 100644 --- a/src/shared/services/bolt/bolt.js +++ b/src/shared/services/bolt/bolt.js @@ -243,18 +243,21 @@ export default { objectConverter: mappings.extractFromNeoObjects }) }, - extractNodesAndRelationshipsFromRecords: records => { + extractNodesAndRelationshipsFromRecords: (records, maxFieldItems) => { return mappings.extractNodesAndRelationshipsFromRecords( records, - neo4j.types + neo4j.types, + maxFieldItems ) }, extractNodesAndRelationshipsFromRecordsForOldVis: ( records, - filterRels = true + filterRels = true, + maxFieldItems ) => { const intChecker = neo4j.isInt const intConverter = val => val.toString() + return mappings.extractNodesAndRelationshipsFromRecordsForOldVis( records, neo4j.types, @@ -263,7 +266,8 @@ export default { intChecker, intConverter, objectConverter: mappings.extractFromNeoObjects - } + }, + maxFieldItems ) }, extractPlan: (result, calculateTotalDbHits) => { diff --git a/src/shared/services/bolt/boltMappings.js b/src/shared/services/bolt/boltMappings.js index a1ca31639ff..26cd0fe57ef 100644 --- a/src/shared/services/bolt/boltMappings.js +++ b/src/shared/services/bolt/boltMappings.js @@ -19,6 +19,7 @@ */ import updateStatsFields from './updateStatisticsFields' +import { flatten, map, take } from 'lodash-es' import neo4j from 'neo4j-driver' import { stringModifier } from 'services/bolt/cypherTypesFormatting' import { @@ -152,30 +153,19 @@ const collectHits = function(operator) { export function extractNodesAndRelationshipsFromRecords( records, - types = neo4j.types + types = neo4j.types, + maxFieldItems ) { if (records.length === 0) { return { nodes: [], relationships: [] } } - const keys = records[0].keys - let rawNodes = [] - let rawRels = [] - records.forEach(record => { - const graphItems = keys.map(key => record.get(key)) - rawNodes = [ - ...rawNodes, - ...graphItems.filter(item => item instanceof types.Node) - ] - rawRels = [ - ...rawRels, - ...graphItems.filter(item => item instanceof types.Relationship) - ] - const paths = graphItems.filter(item => item instanceof types.Path) - paths.forEach(item => - extractNodesAndRelationshipsFromPath(item, rawNodes, rawRels, types) - ) - }) + const { rawNodes, rawRels } = extractRawNodesAndRelationShipsFromRecords( + records, + types, + maxFieldItems + ) + return { nodes: rawNodes, relationships: rawRels } } @@ -183,33 +173,17 @@ export function extractNodesAndRelationshipsFromRecordsForOldVis( records, types, filterRels, - converters + converters, + maxFieldItems ) { if (records.length === 0) { return { nodes: [], relationships: [] } } - const keys = records[0].keys - let rawNodes = [] - let rawRels = [] - - records.forEach(record => { - let graphItems = keys.map(key => record.get(key)) - graphItems = flattenArray( - recursivelyExtractGraphItems(types, graphItems) - ).filter(item => item !== false) - rawNodes = [ - ...rawNodes, - ...graphItems.filter(item => item instanceof types.Node) - ] - rawRels = [ - ...rawRels, - ...graphItems.filter(item => item instanceof types.Relationship) - ] - const paths = graphItems.filter(item => item instanceof types.Path) - paths.forEach(item => - extractNodesAndRelationshipsFromPath(item, rawNodes, rawRels, types) - ) - }) + const { rawNodes, rawRels } = extractRawNodesAndRelationShipsFromRecords( + records, + types, + maxFieldItems + ) const nodes = rawNodes.map(item => { return { @@ -255,27 +229,66 @@ export const recursivelyExtractGraphItems = (types, item) => { return item } -export const flattenArray = arr => { - return arr.reduce((all, curr) => { - if (Array.isArray(curr)) return all.concat(flattenArray(curr)) - return all.concat(curr) - }, []) -} +export function extractRawNodesAndRelationShipsFromRecords( + records, + types = neo4j.types, + maxFieldItems +) { + const items = new Set() + const paths = new Set() + const segments = new Set() + const rawNodes = new Set() + const rawRels = new Set() -const extractNodesAndRelationshipsFromPath = (item, rawNodes, rawRels) => { - const paths = Array.isArray(item) ? item : [item] - paths.forEach(path => { - let segments = path.segments - // Zero length path. No relationship, end === start - if (!Array.isArray(path.segments) || path.segments.length < 1) { - segments = [{ ...path, end: null }] + for (const record of records) { + for (const key of record.keys) { + items.add(record.get(key)) } - segments.forEach(segment => { - if (segment.start) rawNodes.push(segment.start) - if (segment.end) rawNodes.push(segment.end) - if (segment.relationship) rawRels.push(segment.relationship) - }) - }) + } + + const flatTruncatedItems = flatten( + map([...items], item => + maxFieldItems && Array.isArray(item) ? take(item, maxFieldItems) : item + ) + ) + + for (const item of flatTruncatedItems) { + if (item instanceof types.Relationship) { + rawRels.add(item) + } + if (item instanceof types.Node) { + rawNodes.add(item) + } + if (item instanceof types.Path) { + paths.add(item) + } + } + + for (const path of paths) { + if (path.start) { + rawNodes.add(path.start) + } + if (path.end) { + rawNodes.add(path.end) + } + for (const segment of path.segments) { + segments.add(segment) + } + } + + for (const segment of segments) { + if (segment.start) { + rawNodes.add(segment.start) + } + if (segment.end) { + rawNodes.add(segment.end) + } + if (segment.relationship) { + rawRels.add(segment.relationship) + } + } + + return { rawNodes: [...rawNodes], rawRels: [...rawRels] } } export const retrieveFormattedUpdateStatistics = result => { @@ -290,7 +303,9 @@ export const retrieveFormattedUpdateStatistics = result => { }` ) return statsMessages.join(', ') - } else return null + } else { + return null + } } export const flattenProperties = rows => { diff --git a/src/shared/services/bolt/boltMappings.test.js b/src/shared/services/bolt/boltMappings.test.js index e643a40a71a..3f10b05d969 100644 --- a/src/shared/services/bolt/boltMappings.test.js +++ b/src/shared/services/bolt/boltMappings.test.js @@ -204,6 +204,31 @@ describe('boltMappings', () => { expect(relationships[0].properties).toEqual({}) }) + test('should truncate field items when told to do so', () => { + let startNode = new neo4j.types.Node('1', ['Person'], { + prop1: 'prop1' + }) + let endNode = new neo4j.types.Node('2', ['Movie'], { + prop2: 'prop2' + }) + let boltRecord = { + keys: ['p'], + get: key => [startNode, endNode] + } + + let { nodes, relationships } = extractNodesAndRelationshipsFromRecords( + [boltRecord], + neo4j.types, + 1 + ) + expect(nodes.length).toBe(1) + expect(relationships.length).toBe(0) + let graphNode = nodes[0] + expect(graphNode).toBeDefined() + expect(graphNode.labels).toEqual(['Person']) + expect(graphNode.properties).toEqual({ prop1: 'prop1' }) + }) + test('should map bolt nodes and relationships to graph nodes and relationships', () => { const startNode = new neo4j.types.Node('1', ['Person'], { prop1: 'prop1' @@ -389,7 +414,7 @@ describe('boltMappings', () => { // Then expect(out.nodes.length).toEqual(4) }) - test('should find items in paths with segments', () => { + test('should find items in paths with segments, and only return unique items', () => { // Given const converters = { intChecker: () => false, @@ -423,7 +448,7 @@ describe('boltMappings', () => { ) // Then - expect(out.nodes.length).toEqual(4) + expect(out.nodes.length).toEqual(3) }) test('should find items in paths zero segments', () => { // Given
    Server version xx1
    Server address xx2
    Query MATCH xx0
    Summary
    {, "server": {, "version": "xx1", ...
    Response
    [, {, "res": "xx3" ...