Skip to content

Commit

Permalink
Merge pull request #1126 from oskarhane/fix-numbers-in-table
Browse files Browse the repository at this point in the history
Fix integer rendering in table recursively + JSON export
  • Loading branch information
oskarhane authored Jun 25, 2020
2 parents 1e11acd + af9bede commit bb59893
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 93 deletions.
4 changes: 3 additions & 1 deletion e2e_tests/integration/types.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,18 @@ describe('Types in Browser', () => {
if (Cypress.config('serverVersion') >= 3.4) {
it('presents large integers correctly', () => {
cy.executeCommand(':clear')
const query = 'RETURN 2467500000 AS bigNumber'
const query = 'RETURN 2467500000 AS bigNumber, {{}x: 9907199254740991}'
cy.executeCommand(query)
cy.waitForCommandResult()
cy.resultContains('2467500000')
cy.resultContains('9907199254740991')

// Go to ascii view
cy.get('[data-testid="cypherFrameSidebarAscii"]')
.first()
.click()
cy.resultContains('│2467500000')
cy.resultContains('9907199254740991')
})
it('presents the point type correctly', () => {
cy.executeCommand(':clear')
Expand Down
13 changes: 4 additions & 9 deletions src/browser/modules/Frame/FrameTitlebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,9 @@ import {
transformResultRecordsToResultArray,
recordToJSONMapper
} from 'browser/modules/Stream/CypherFrame/helpers'
import { csvFormat } from 'services/bolt/cypherTypesFormatting'
import { csvFormat, stringModifier } from 'services/bolt/cypherTypesFormatting'
import arrayHasItems from 'shared/utils/array-has-items'

const JSON_EXPORT_INDENT = 2
import { stringifyMod } from 'services/utils'

class FrameTitlebar extends Component {
hasData() {
Expand Down Expand Up @@ -115,15 +114,11 @@ class FrameTitlebar extends Component {
}

exportJSON(records) {
const data = JSON.stringify(
map(records, recordToJSONMapper),
null,
JSON_EXPORT_INDENT
)
const exportData = map(records, recordToJSONMapper)
const data = stringifyMod(exportData, stringModifier, true)
const blob = new Blob([data], {
type: 'text/plain;charset=utf-8'
})

saveAs(blob, 'records.json')
}

Expand Down
3 changes: 2 additions & 1 deletion src/browser/modules/Stream/CypherFrame/AsciiView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ export class AsciiViewComponent extends Component {
const serializedRows =
stringifyResultArray(
stringModifier,
transformResultRecordsToResultArray(records, maxFieldItems)
transformResultRecordsToResultArray(records, maxFieldItems),
true
) || []
this.setState({ serializedRows })
const maxColWidth = asciitable.maxColumnWidth(serializedRows)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ exports[`RelatableViews RelatableView does not display bodyMessage if rows, and
class="relatable__table-cell relatable__table-body-cell"
role="cell"
>
"String with HTML <strong>in</strong> it"
<span>
"String with HTML &lt;strong&gt;in&lt;/strong&gt; it"
</span>
</td>
</tr>
</tbody>
Expand Down
24 changes: 17 additions & 7 deletions src/browser/modules/Stream/CypherFrame/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import bolt from 'services/bolt/bolt'

import * as viewTypes from 'shared/modules/stream/frameViewTypes'
import { recursivelyExtractGraphItems } from 'services/bolt/boltMappings'
import { stringifyMod } from 'services/utils'
import { stringifyMod, unescapeDoubleQuotesForDisplay } from 'services/utils'
import { stringModifier } from 'services/bolt/cypherTypesFormatting'

/**
Expand Down Expand Up @@ -226,11 +226,16 @@ export const initialView = (props, state = {}) => {
* It takes a replacer without enforcing quoting rules to it.
* Used so we can have Neo4j integers as string without quotes.
*/
export const stringifyResultArray = (formatter = stringModifier, arr = []) => {
export const stringifyResultArray = (
formatter = stringModifier,
arr = [],
unescapeDoubleQuotes = false
) => {
return arr.map(col => {
if (!col) return col
return col.map(fVal => {
return stringifyMod(fVal, formatter)
const res = stringifyMod(fVal, formatter)
return unescapeDoubleQuotes ? unescapeDoubleQuotesForDisplay(res) : res
})
})
}
Expand Down Expand Up @@ -353,13 +358,16 @@ const arrayifyPath = (types = neo4j.types, path) => {

/**
* Converts a raw Neo4j record into a JSON friendly format, mimicking APOC output
* Note: This preserves Neo4j integers as objects because they can't be guaranteed
* to be converted to numbers and keeping the precision.
* It's up to the serializer to identify them and write them as fake numbers (strings without quotes)
* @param {Record} record
* @return {*}
*/
export function recordToJSONMapper(record) {
const keys = get(record, 'keys', [])

return reduce(
const recordObj = reduce(
keys,
(agg, key) => {
const field = record.get(key)
Expand All @@ -371,6 +379,7 @@ export function recordToJSONMapper(record) {
},
{}
)
return recordObj
}

/**
Expand All @@ -379,6 +388,10 @@ export function recordToJSONMapper(record) {
* @return {*}
*/
export function mapNeo4jValuesToPlainValues(values) {
if (neo4j.isInt(values)) {
return values
}

if (!isObjectLike(values)) {
return values
}
Expand Down Expand Up @@ -425,8 +438,6 @@ function neo4jValueToPlainValue(value) {
case neo4j.types.LocalTime:
case neo4j.types.Time:
return value.toString()
case neo4j.types.Integer: // not exposed in typings but still there
return value.inSafeRange() ? value.toInt() : value.toNumber()
default:
return value
}
Expand All @@ -445,7 +456,6 @@ function isNeo4jValue(value) {
case neo4j.types.LocalDateTime:
case neo4j.types.LocalTime:
case neo4j.types.Time:
case neo4j.types.Integer: // not exposed in typings but still there
return true
default:
return false
Expand Down
29 changes: 20 additions & 9 deletions src/browser/modules/Stream/CypherFrame/helpers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ describe('helpers', () => {
neo4j.isInt,
step1
)
const res = stringifyResultArray(stringModifier, step2)
const res = stringifyResultArray(stringModifier, step2, true)
// Then
expect(res).toEqual([
['""neoInt""', '""int""', '""any""', '""backslash""'],
Expand Down Expand Up @@ -713,7 +713,7 @@ describe('helpers', () => {
neo4j.isInt,
step1
)
const res = stringifyResultArray(stringModifier, step2)
const res = stringifyResultArray(stringModifier, step2, true)
// Then
expect(res).toEqual([
['""x""', '""y""', '""n""'],
Expand All @@ -730,15 +730,21 @@ describe('helpers', () => {
describe('recordToJSONMapper', () => {
describe('Nodes', () => {
test('handles integer values', () => {
const node = new neo4j.types.Node(1, ['foo'], { bar: new neo4j.int(3) })
const node = new neo4j.types.Node(1, ['foo'], {
bar: new neo4j.int(3),
baz: new neo4j.int(1416268800000),
bax: new neo4j.int(9907199254740991) // Larger than Number.MAX_SAFE_INTEGER, but still in 64 bit int range
})
const record = new neo4j.types.Record(['n'], [node])
const expected = {
n: {
identity: 1,
elementType: 'node',
labels: ['foo'],
properties: {
bar: 3
bar: new neo4j.int(3),
baz: new neo4j.int(1416268800000),
bax: new neo4j.int(9907199254740991)
}
}
}
Expand Down Expand Up @@ -911,7 +917,7 @@ describe('helpers', () => {
elementType: 'relationship',
type: 'foo',
properties: {
bar: 3
bar: new neo4j.int(3)
}
}
}
Expand Down Expand Up @@ -1109,7 +1115,7 @@ describe('helpers', () => {
elementType: 'node',
labels: ['foo'],
properties: {
bar: 3
bar: new neo4j.int(3)
}
},
r1: {
Expand Down Expand Up @@ -1159,7 +1165,7 @@ describe('helpers', () => {
elementType: 'node',
labels: ['foo'],
properties: {
bar: 3
bar: new neo4j.int(3)
}
},
end: {
Expand All @@ -1177,7 +1183,7 @@ describe('helpers', () => {
elementType: 'node',
labels: ['foo'],
properties: {
bar: 3
bar: new neo4j.int(3)
}
},
relationship: {
Expand Down Expand Up @@ -1224,7 +1230,12 @@ describe('helpers', () => {
)
const expected = {
foo: {
data: [1, 'car', { srid: 1, x: 10, y: 5, z: 15 }, '1970-01-01']
data: [
new neo4j.int(1),
'car',
{ srid: 1, x: 10, y: 5, z: 15 },
'1970-01-01'
]
}
}

Expand Down
100 changes: 40 additions & 60 deletions src/browser/modules/Stream/CypherFrame/relatable-view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,31 @@
*
*/

import React, { useCallback, useMemo } from 'react'
import React, { useMemo } from 'react'
import { isInt } from 'neo4j-driver'
import Relatable from '@relate-by-ui/relatable'
import {
entries,
filter,
get,
head,
join,
map,
memoize,
slice
} from 'lodash-es'
import { get, head, map, slice } from 'lodash-es'
import { Icon } from 'semantic-ui-react'
import { connect } from 'react-redux'

import { HTMLEntities } from 'services/santize.utils'
import {
getBodyAndStatusBarMessages,
mapNeo4jValuesToPlainValues,
resultHasTruncatedFields
} from './helpers'
import arrayHasItems from 'shared/utils/array-has-items'
import {
getMaxFieldItems,
getMaxRows
} from 'shared/modules/settings/settingsDuck'

import { stringModifier } from 'services/bolt/cypherTypesFormatting'
import ClickableUrls, {
convertUrlsToHrefTags
} from '../../../components/clickable-urls'
import { StyledStatsBar, StyledTruncatedMessage } from '../styled'
import Ellipsis from '../../../components/Ellipsis'
import { RelatableStyleWrapper, StyledJsonPre } from './relatable-view.styled'
import { isPoint, isInt } from 'neo4j-driver'
import { stringifyMod, unescapeDoubleQuotesForDisplay } from 'services/utils'

const RelatableView = connect(state => ({
maxRows: getMaxRows(state),
Expand Down Expand Up @@ -94,57 +85,46 @@ function getColumns(records, maxFieldItems) {

function CypherCell({ cell }) {
const { value } = cell
const mapper = useCallback(
value => {
const memo = memoize(mapNeo4jValuesToPlainValues, value => {
const { elementType, identity } = value || {}

return elementType ? `${elementType}:${identity}` : identity
})

return memo(value)
},
[memoize, mapNeo4jValuesToPlainValues]
)
const mapped = mapper(value)

if (isInt(value)) {
return value.toString()
}

if (Number.isInteger(value)) {
return `${value}.0`
}

if (typeof mapped === 'string') {
return `"${mapped}"`
}

if (isPoint(value)) {
const pairs = filter(
entries(mapped),
([, val]) => val !== null && val !== undefined
)

return `point({${join(
map(pairs, pair => join(pair, ':')),
', '
)}})`
}
return renderCell(value)
}

if (mapped && typeof mapped === 'object') {
const renderCell = entry => {
if (Array.isArray(entry)) {
const children = entry.map((item, index) => (
<span key={index}>
{renderCell(item)}
{index === entry.length - 1 ? null : ', '}
</span>
))
return <span>[{children}]</span>
} else if (typeof entry === 'object') {
return renderObject(entry)
} else {
return (
<StyledJsonPre
dangerouslySetInnerHTML={{
__html: convertUrlsToHrefTags(
HTMLEntities(JSON.stringify(mapped, null, 2))
)
}}
<ClickableUrls
text={unescapeDoubleQuotesForDisplay(
stringifyMod(entry, stringModifier, true)
)}
/>
)
}

return <ClickableUrls text={mapped} />
}
const renderObject = entry => {
if (isInt(entry)) return entry.toString()
if (entry === null) return <em>null</em>
return (
<StyledJsonPre
dangerouslySetInnerHTML={{
__html: convertUrlsToHrefTags(
HTMLEntities(
unescapeDoubleQuotesForDisplay(
stringifyMod(entry, stringModifier, true)
)
)
)
}}
/>
)
}

export function RelatableBodyMessage({ maxRows, result }) {
Expand Down
Loading

0 comments on commit bb59893

Please sign in to comment.