diff --git a/.github/workflows/master-deployment.yml b/.github/workflows/master-deployment.yml index c7c3afe16..6826027eb 100644 --- a/.github/workflows/master-deployment.yml +++ b/.github/workflows/master-deployment.yml @@ -79,7 +79,7 @@ jobs: context: . file: ./Dockerfile push: true - tags: ${{ secrets.DOCKER_HUB_LABS_USERNAME }}/neodash:latest,${{ secrets.DOCKER_HUB_LABS_USERNAME }}/neodash:2.4.6 + tags: ${{ secrets.DOCKER_HUB_LABS_USERNAME }}/neodash:latest,${{ secrets.DOCKER_HUB_LABS_USERNAME }}/neodash:2.4.7 build-docker-legacy: needs: build-test runs-on: neodash-runners @@ -103,7 +103,7 @@ jobs: context: . file: ./Dockerfile push: true - tags: ${{ secrets.DOCKER_HUB_USERNAME }}/neodash:latest,${{ secrets.DOCKER_HUB_USERNAME }}/neodash:2.4.6 + tags: ${{ secrets.DOCKER_HUB_USERNAME }}/neodash:latest,${{ secrets.DOCKER_HUB_USERNAME }}/neodash:2.4.7 deploy-gallery: runs-on: neodash-runners strategy: diff --git a/Dockerfile b/Dockerfile index 1e78a8511..5f8abb829 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,4 +44,4 @@ USER nginx EXPOSE $NGINX_PORT HEALTHCHECK cmd curl --fail "http://localhost:$NGINX_PORT" || exit 1 -LABEL version="2.4.6" +LABEL version="2.4.7" diff --git a/changelog.md b/changelog.md index aceee2e41..a41402a34 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,15 @@ +## NeoDash 2.4.7 +This is a minor release containing a few critical fixes and general code quality improvements: + +- Fix multiple parameter select ([881](/~https://github.com/neo4j-labs/neodash/pull/881)). +- Fix parameter casting error when loading dashboards([874](/~https://github.com/neo4j-labs/neodash/pull/874)). +- Fix the fraud demo in the [Example Gallery](https://neodash-gallery.graphapp.io/). + +Thanks to all the contributors for this release: +- [alfredorubin96](/~https://github.com/alfredorubin96), +- [MariusC](/~https://github.com/mariusconjeaud), +- [elizarp](/~https://github.com/elizarp). + ## NeoDash 2.4.6 This is a minor release containing a few critical fixes and some extra style customizations: diff --git a/cypress/e2e/render/array.cy.js b/cypress/e2e/render/array.cy.js new file mode 100644 index 000000000..108d4aceb --- /dev/null +++ b/cypress/e2e/render/array.cy.js @@ -0,0 +1,154 @@ +import { stringArrayCypherQuery, intArrayCypherQuery, pathArrayCypherQuery } from '../../fixtures/cypher_queries'; +import { + enableReportActions, + createReportOfType, + closeSettings, + toggleTableTranspose, + openReportActionsMenu, + selectReportOfType, + openAdvancedSettings, + updateDropdownAdvancedSetting, +} from '../utils'; + +const WAITING_TIME = 20000; +const CARD_SELECTOR = 'main .react-grid-item:eq(2)'; +// Ignore warnings that may appear when using the Cypress dev server +Cypress.on('uncaught:exception', (err, runnable) => { + console.log(err, runnable); + return false; +}); + +describe('Testing array rendering', () => { + beforeEach('open neodash', () => { + cy.viewport(1920, 1080); + cy.visit('/', { + onBeforeLoad(win) { + win.localStorage.clear(); + }, + }); + + cy.get('#form-dialog-title', { WAITING_TIME: WAITING_TIME }) + .should('contain', 'NeoDash - Neo4j Dashboard Builder') + .click(); + + cy.get('#form-dialog-title').then(($div) => { + const text = $div.text(); + if (text == 'NeoDash - Neo4j Dashboard Builder') { + cy.wait(500); + // Create new dashboard + cy.contains('New Dashboard').click(); + } + }); + + cy.get('#form-dialog-title', { WAITING_TIME: WAITING_TIME }).should('contain', 'Connect to Neo4j'); + + cy.get('#url').clear().type('localhost'); + cy.get('#dbusername').clear().type('neo4j'); + cy.get('#dbpassword').type('test1234'); + cy.get('button').contains('Connect').click(); + cy.wait(100); + }); + + it('creates a table that contains string arrays', () => { + cy.checkInitialState(); + enableReportActions(); + createReportOfType('Table', stringArrayCypherQuery, true, true); + + // Standard array, displays strings joined with comma and whitespace + cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`).should('have.text', 'initial, list'); + cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', 'other, list'); + + // Now, transpose the table + toggleTableTranspose(CARD_SELECTOR, true); + cy.get(`${CARD_SELECTOR} .MuiDataGrid-columnHeaderTitle:eq(1)`, { timeout: WAITING_TIME }).should( + 'have.text', + 'initial,list' + ); + cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', 'other, list'); + + // Transpose back + // And add a report action + toggleTableTranspose(CARD_SELECTOR, false); + openReportActionsMenu(CARD_SELECTOR); + cy.get('.ndl-modal').find('button[aria-label="add"]').click(); + cy.get('.ndl-modal').find('input:eq(2)').type('column'); + cy.get('.ndl-modal').find('input:eq(5)').type('test_param'); + cy.get('.ndl-modal').find('input:eq(6)').type('column'); + cy.get('.ndl-modal').find('button').contains('Save').click(); + closeSettings(CARD_SELECTOR); + cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`) + .find('button') + .should('be.visible') + .should('have.text', 'initial, list') + .click(); + + // Previous step's click set a parameter from the array + // Test that parameter rendering works + cy.get(`${CARD_SELECTOR} .MuiCardHeader-root`).find('input').type('$neodash_test_param').blur(); + cy.get(`${CARD_SELECTOR} .MuiCardHeader-root`).find('input').should('have.value', 'initial, list'); + }); + + it('creates a table that contains int arrays', () => { + cy.checkInitialState(); + createReportOfType('Table', intArrayCypherQuery, true, true); + + // Standard array, displays strings joined with comma and whitespace + cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`).should('have.text', '1, 2'); + cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', '3, 4'); + + // Now, transpose the table + toggleTableTranspose(CARD_SELECTOR, true); + cy.get(`${CARD_SELECTOR} .MuiDataGrid-columnHeaderTitle:eq(1)`, { timeout: WAITING_TIME }).should( + 'have.text', + '1,2' + ); + cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', '3, 4'); + }); + + it('creates a table that contains nodes and rels', () => { + cy.checkInitialState(); + createReportOfType('Table', pathArrayCypherQuery, true, true); + + // Standard array, displays a path with two nodes and a relationship + cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`).should('have.text', 'PersonACTED_INMovie'); + cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) button`).should('have.length', 2); + cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) button:eq(0)`).should('have.text', 'Person'); + cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) button:eq(1)`).should('have.text', 'Movie'); + cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) .MuiChip-root`).should('have.length', 1); + cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) .MuiChip-root`).should('have.text', 'ACTED_IN'); + }); + + it('creates a single value report which is an array', () => { + cy.checkInitialState(); + createReportOfType('Single Value', stringArrayCypherQuery, true, true); + cy.get(CARD_SELECTOR).should('have.text', 'initial, list'); + }); + + it('creates a multi parameter select', () => { + cy.checkInitialState(); + selectReportOfType('Parameter Select'); + cy.get('main .react-grid-item:eq(2) label[for="Selection Type"]').siblings('div').click(); + // Set up the parameter select + cy.contains('Node Property').click(); + cy.wait(100); + cy.contains('Node Label').click(); + cy.contains('Node Label').siblings('div').find('input').type('Movie'); + cy.wait(1000); + cy.get('.MuiAutocomplete-popper').contains('Movie').click(); + cy.contains('Property Name').click(); + cy.contains('Property Name').siblings('div').find('input').type('title'); + cy.wait(1000); + cy.get('.MuiAutocomplete-popper').contains('title').click(); + // Enable multiple selection + closeSettings(CARD_SELECTOR); + updateDropdownAdvancedSetting(CARD_SELECTOR, 'Multiple Selection', 'on'); + // Finally, select a few values in the parameter select + cy.get(CARD_SELECTOR).contains('Movie title').click(); + cy.get(CARD_SELECTOR).contains('Movie title').siblings('div').find('input').type('a'); + cy.get('.MuiAutocomplete-popper').contains('Apollo 13').click(); + cy.get(CARD_SELECTOR).contains('Movie title').siblings('div').find('input').type('t'); + cy.get('.MuiAutocomplete-popper').contains('The Matrix').click(); + cy.get(CARD_SELECTOR).contains('Apollo 13').should('be.visible'); + cy.get(CARD_SELECTOR).contains('The Matrix').should('be.visible'); + }); +}); diff --git a/cypress/e2e/start_page.cy.js b/cypress/e2e/start_page.cy.js index 2f6bfa873..c6e9f7c4b 100644 --- a/cypress/e2e/start_page.cy.js +++ b/cypress/e2e/start_page.cy.js @@ -10,6 +10,7 @@ import { gaugeChartCypherQuery, formCypherQuery, } from '../fixtures/cypher_queries'; +import { createReportOfType, selectReportOfType, enableAdvancedVisualizations, enableFormsExtension } from './utils'; const WAITING_TIME = 20000; // Ignore warnings that may appear when using the Cypress dev server @@ -293,46 +294,3 @@ describe('NeoDash E2E Tests', () => { } }); }); - -function enableAdvancedVisualizations() { - cy.get('main button[aria-label="Extensions').should('be.visible').click(); - cy.get('#checkbox-advanced-charts').should('be.visible').click(); - cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click(); - cy.wait(200); -} - -function enableFormsExtension() { - cy.get('main button[aria-label="Extensions').should('be.visible').click(); - cy.get('#checkbox-forms').scrollIntoView(); - cy.get('#checkbox-forms').should('be.visible').click(); - cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click(); - cy.wait(200); -} - -function selectReportOfType(type) { - cy.get('main .react-grid-item button[aria-label="add report"]').should('be.visible').click(); - cy.get('main .react-grid-item') - .contains('No query specified.') - .parentsUntil('.react-grid-item') - .find('button[aria-label="settings"]', { timeout: 2000 }) - .should('be.visible') - .click(); - cy.get('main .react-grid-item:eq(2) #type', { timeout: 2000 }).should('be.visible').click(); - cy.contains(type).click(); - cy.wait(100); -} - -function createReportOfType(type, query, fast = false, run = true) { - selectReportOfType(type); - if (fast) { - cy.get('main .react-grid-item:eq(2) .ReactCodeMirror').type(query, { delay: 1, parseSpecialCharSequences: false }); - } else { - cy.get('main .react-grid-item:eq(2) .ReactCodeMirror').type(query, { parseSpecialCharSequences: false }); - } - cy.wait(400); - - cy.get('main .react-grid-item:eq(2)').contains('Advanced settings').click(); - if (run) { - cy.get('main .react-grid-item:eq(2) button[aria-label="run"]').click(); - } -} diff --git a/cypress/e2e/utils.js b/cypress/e2e/utils.js new file mode 100644 index 000000000..ae5639cf9 --- /dev/null +++ b/cypress/e2e/utils.js @@ -0,0 +1,84 @@ +export function enableReportActions() { + cy.get('main button[aria-label="Extensions').should('be.visible').click(); + cy.get('#checkbox-actions').scrollIntoView(); + cy.get('#checkbox-actions').should('be.visible').click(); + cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click(); + cy.wait(200); +} + +export function enableAdvancedVisualizations() { + cy.get('main button[aria-label="Extensions').should('be.visible').click(); + cy.get('#checkbox-advanced-charts').should('be.visible').click(); + cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click(); + cy.wait(200); +} + +export function enableFormsExtension() { + cy.get('main button[aria-label="Extensions').should('be.visible').click(); + cy.get('#checkbox-forms').scrollIntoView(); + cy.get('#checkbox-forms').should('be.visible').click(); + cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click(); + cy.wait(200); +} + +export function selectReportOfType(type) { + cy.get('main .react-grid-item button[aria-label="add report"]').should('be.visible').click(); + cy.get('main .react-grid-item') + .contains('No query specified.') + .parentsUntil('.react-grid-item') + .find('button[aria-label="settings"]', { timeout: 2000 }) + .should('be.visible') + .click(); + cy.get('main .react-grid-item:eq(2) #type', { timeout: 2000 }).should('be.visible').click(); + cy.contains(type).click(); + cy.wait(100); +} + +export function createReportOfType(type, query, fast = false, run = true) { + selectReportOfType(type); + if (fast) { + cy.get('main .react-grid-item:eq(2) .ReactCodeMirror').type(query, { delay: 1, parseSpecialCharSequences: false }); + } else { + cy.get('main .react-grid-item:eq(2) .ReactCodeMirror').type(query, { parseSpecialCharSequences: false }); + } + cy.wait(400); + + if (run) { + closeSettings('main .react-grid-item:eq(2)'); + } +} + +export function openSettings(cardSelector) { + cy.get(cardSelector).find('button[aria-label="settings"]', { WAITING_TIME: 2000 }).click(); +} + +export function closeSettings(cardSelector) { + cy.get(`${cardSelector} button[aria-label="run"]`).click(); +} + +export function openAdvancedSettings(cardSelector) { + openSettings(cardSelector); + cy.get(cardSelector).contains('Advanced settings').click(); +} + +export function closeAdvancedSettings(cardSelector) { + cy.get(cardSelector).contains('Advanced settings').click(); + closeSettings(cardSelector); +} + +export function openReportActionsMenu(cardSelector) { + openSettings(cardSelector); + cy.get(cardSelector).find('button[aria-label="custom actions"]').click(); +} + +export function updateDropdownAdvancedSetting(cardSelector, settingLabel, targetValue) { + openAdvancedSettings(cardSelector); + cy.get(`${cardSelector} .ndl-dropdown`).contains(settingLabel).siblings('div').click(); + cy.contains(targetValue).click(); + closeAdvancedSettings(cardSelector); +} + +export function toggleTableTranspose(cardSelector, enable) { + let transpose = enable ? 'on' : 'off'; + updateDropdownAdvancedSetting(cardSelector, 'Transpose Rows & Columns', transpose); +} diff --git a/cypress/fixtures/cypher_queries.js b/cypress/fixtures/cypher_queries.js index 4f009b7ae..c47c6c2be 100644 --- a/cypress/fixtures/cypher_queries.js +++ b/cypress/fixtures/cypher_queries.js @@ -1,3 +1,4 @@ +// Cypher queries - for component testing export const defaultCypherQuery = 'MATCH (n) RETURN n LIMIT 25'; export const tableCypherQuery = 'MATCH (n:Movie) RETURN n.title AS title, n.released AS released, id(n) AS __id LIMIT 8'; @@ -13,6 +14,13 @@ export const sankeyChartCypherQuery = "WITH [ { path: { start: {labels: ['Person'], identity: 1, properties: {name: 'Jim'}}, end: {identity: 11}, length: 1, segments: [ { start: {labels: ['Person'], identity: 1, properties: {name: 'Jim'}}, relationship: {type: 'RATES', start: 1, end: 11, identity: 10001, properties: {value: 4.5}}, end: {labels: ['Movie'], identity: 11,properties: {title: 'The Matrix', released: 1999}} } ] }, person: 'Jim', movie: 'The Matrix', value: 4.5 }, { path: { start: {labels: ['Person'], identity: 2, properties: {name: 'Mike'}}, end: {identity: 11}, length: 1, segments: [ { start: {labels: ['Person'], identity: 2, properties: {name: 'Mike'}}, relationship: {type: 'RATES', start: 2, end: 11, identity: 10002, properties: {value: 3.8}}, end: {labels: ['Movie'], identity: 11,properties: {title: 'The Matrix', released: 1999}} } ] }, person: 'Mike', movie: 'The Matrix', value: 3.8 } ] as data UNWIND data as row RETURN row.path as Path"; export const gaugeChartCypherQuery = 'RETURN 69'; export const formCypherQuery = 'MATCH (n:Movie) WHERE n.title = $neodash_movie_title SET n.rating = 92'; + +// Cypher queries - for renderer testing +export const stringArrayCypherQuery = "RETURN ['initial', 'list'] AS column, ['other', 'list'] AS otherColumn"; +export const intArrayCypherQuery = 'RETURN [1, 2] AS column, [3, 4] AS otherColumn'; +export const pathArrayCypherQuery = 'MATCH p=(:Person)-[:ACTED_IN]->(:Movie) WITH p LIMIT 1 RETURN p'; + +// Other content fixtures export const iFrameText = 'https://www.wikipedia.org/'; export const markdownText = '# Hello'; export const loadDashboardURL = diff --git a/docs/modules/ROOT/pages/developer-guide/deploy-a-build.adoc b/docs/modules/ROOT/pages/developer-guide/deploy-a-build.adoc index 6fadeb4b7..05ed41604 100644 --- a/docs/modules/ROOT/pages/developer-guide/deploy-a-build.adoc +++ b/docs/modules/ROOT/pages/developer-guide/deploy-a-build.adoc @@ -37,7 +37,7 @@ Depending on the webserver type and version, this could be different directory. As an example - to copy the files to an nginx webserver using `scp`: ```bash -scp neodash-2.4.6 username@host:/usr/share/nginx/html +scp neodash-2.4.7 username@host:/usr/share/nginx/html ``` NeoDash should now be visible by visiting your (sub)domain in the browser. diff --git a/gallery/dashboards/fraud.json b/gallery/dashboards/fraud.json index b75717ba0..b96145042 100644 --- a/gallery/dashboards/fraud.json +++ b/gallery/dashboards/fraud.json @@ -1,282 +1,324 @@ { - "title": "Financial Crimes Enforcement Dashboard 🕵️", - "version": "2.1", - "settings": { - "pagenumber": 0, - "editable": true, - "parameters": { - "neodash_entity_name": null, - "neodash_country_name": null - }, - "fullscreenEnabled": true + "uuid": "b3236f88-ff8b-492d-8a84-d620a3dd629d", + "title": "Financial Crimes Enforcement Dashboard 🕵️", + "version": "2.4", + "settings": { + "pagenumber": 0, + "editable": true, + "parameters": { + "neodash_entity_name": null, + "neodash_country_name": null }, - "pages": [ - { - "title": "Countries", - "reports": [ - { - "x": 0, - "y": 0, - "title": "About this dashboard", - "query": "This is an example dashboard on financial crime data. It uses the `fincen` dataset from \n[https://demo.neo4jlabs.com/](https://demo.neo4jlabs.com/).\n\nThis dashboard's purpose is to provide examples on how to use and customize all the different NeoDash report types.\n\nIt consists of three pages:\n- **Countries**: high-level data on specific countries.\n- **Investigate Entity**: a way to drill down into a specific entity.\n- **Statistics**: high-level statistics about the data.\n\nTry out the Documentation 📄 button on the left for basic examples of the different visualization reports.", - "width": "4", - "type": "text", - "height": 2, - "selection": {}, - "settings": {} + "fullscreenEnabled": true + }, + "pages": [ + { + "title": "Countries", + "reports": [ + { + "x": 0, + "y": 0, + "title": "About this dashboard", + "query": "This is an example dashboard on financial crime data. It uses the `fincen` dataset from \n[https://demo.neo4jlabs.com/](https://demo.neo4jlabs.com/).\n\nThis dashboard's purpose is to provide examples on how to use and customize all the different NeoDash report types.\n\nIt consists of three pages:\n- **Countries**: high-level data on specific countries.\n- **Investigate Entity**: a way to drill down into a specific entity.\n- **Statistics**: high-level statistics about the data.\n\nTry out the Documentation 📄 button on the left for basic examples of the different visualization reports.", + "width": 8, + "type": "text", + "height": 4, + "selection": {}, + "settings": {}, + "id": "bd17ccad-c12e-4e5a-8e45-48504b071698" + }, + { + "x": 8, + "y": 0, + "title": "How much does each entity benefit in total? (Hint: try clicking the table headers to sort/filter data)", + "query": "MATCH Path=(e:Entity)-[:COUNTRY]->(c:Country), (f:Filing)-[:BENEFITS]->(e)\nRETURN Path, e.name as Entity, c.name as Country, suM(f.amount) as `Total Benefit ($)`\nLIMIT 1000", + "width": 16, + "type": "table", + "height": 4, + "selection": {}, + "settings": {}, + "id": "6db3061c-12c5-4a92-a1a1-bf7e40c068a4" + }, + { + "x": 0, + "y": 4, + "title": "Where in Europe does the Netherlands send money to?", + "query": "MATCH (c1:Country)--(:Entity)<-[:ORIGINATOR]-(f:Filing)-[:BENEFITS]->(:Entity)--(c2:Country)\nWHERE c1.name = \"Netherlands\"\nAND point.distance(c2.location, point({latitude: 53, longitude: 9})) < 3000000\nWITH c1, c2, sum(f.amount) as amount ORDER BY amount DESC\nRETURN c1, c2, apoc.create.vRelationship(c1, \"TRANSFER\", {amount: amount}, c2) ", + "width": 12, + "type": "map", + "height": 4, + "selection": { + "Country": "(no label)", + "TRANSFER": "(label)" }, - { - "x": 4, - "y": 0, - "title": "How much does each entity benefit in total? (Hint: try clicking the table headers to sort/filter data)", - "query": "MATCH Path=(e:Entity)-[:COUNTRY]->(c:Country), (f:Filing)-[:BENEFITS]->(e)\nRETURN Path, e.name as Entity, c.name as Country, suM(f.amount) as `Total Benefit ($)`\nLIMIT 1000", - "width": "8", - "type": "table", - "height": 2, - "selection": {}, - "settings": {} + "settings": { + "defaultRelColor": "rgba(120,120,120,0.5)", + "defaultRelWidth": 5, + "defaultNodeSize": "medium", + "nodeColorScheme": "category10" }, - { - "x": 0, - "y": 2, - "title": "Where in Europe does the Netherlands send money to?", - "query": "MATCH (c1:Country)--(:Entity)<-[:ORIGINATOR]-(f:Filing)-[:BENEFITS]->(:Entity)--(c2:Country)\nWHERE c1.name = \"Netherlands\"\nAND distance(c2.location, point({latitude: 53, longitude: 9})) < 3000000\nWITH c1, c2, sum(f.amount) as amount ORDER BY amount DESC\nRETURN c1, c2, apoc.create.vRelationship(c1, \"TRANSFER\", {amount: amount}, c2) ", - "width": "6", - "type": "map", - "height": 2, - "selection": { - "Country": "(no label)" - }, - "settings": { - "defaultRelColor": "rgba(120,120,120,0.5)", - "defaultRelWidth": 5, - "defaultNodeSize": "medium", - "nodeColorScheme": "category10" - } + "id": "5484e81c-52b2-416d-8b7f-fa112887fbec", + "schema": [ + ["Country", "code", "name", "location", "tld"], + ["TRANSFER", "amount"] + ] + }, + { + "x": 12, + "y": 4, + "title": "Which entities are involved?", + "query": "MATCH (c1:Country)--(:Entity)<-[:ORIGINATOR]-(f:Filing)-[:BENEFITS]->(:Entity)--(c2:Country)\nWHERE c1.name = \"Netherlands\"\nAND point.distance(c2.location, point({latitude: 53, longitude: 9})) < 3000000\nWITH c1, c2, sum(f.amount) as amount\nWITH c1, c2, apoc.create.vRelationship(c1, \"TRANSFER\", {amount: amount}, c2) as t\n\nMATCH path=(c2:Country)-[r]-(e:Entity)\nRETURN c1, t, c2, collect(path)[0..10]", + "width": 12, + "type": "graph", + "height": 4, + "selection": { + "Country": "name", + "TRANSFER": "(label)", + "Entity": "name" }, - { - "x": 6, - "y": 2, - "title": "Which entities are involved?", - "query": "MATCH (c1:Country)--(:Entity)<-[:ORIGINATOR]-(f:Filing)-[:BENEFITS]->(:Entity)--(c2:Country)\nWHERE c1.name = \"Netherlands\"\nAND distance(c2.location, point({latitude: 53, longitude: 9})) < 3000000\nWITH c1, c2, sum(f.amount) as amount\nWITH c1, c2, apoc.create.vRelationship(c1, \"TRANSFER\", {amount: amount}, c2) as t\n\nMATCH path=(c2:Country)-[r]-(e:Entity)\nRETURN c1, t, c2, collect(path)[0..10]", - "width": "6", - "type": "graph", - "height": 2, - "selection": { - "Country": "name", - "Entity": "name" - }, - "settings": { - "nodePositions": {} - } - } - ] - }, - { - "title": "Entities", - "reports": [ - { - "x": 0, - "y": 0, - "title": "Entity Investigator 🔎", - "query": "You can use this page to explore information about a single entity in the dataset. All reports are automatically updated based on the selected entity.\n\n**Hint**: Try typing **ING Bank NV** \nin the \"Entity name\" box to the right of this text.\n\n\n", - "width": 3, - "type": "text", - "height": 2, - "selection": {}, - "settings": {} + "settings": { + "nodePositions": {} }, - { - "x": 3, - "y": 0, - "title": "Select an entity to view reports", - "query": "MATCH (n:`Entity`) \nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`name` as value LIMIT 5", - "width": 3, - "type": "select", - "height": 2, - "selection": {}, - "settings": { - "type": "Node Property", - "entityType": "Entity", - "propertyType": "name", - "parameterName": "neodash_entity_name" - } + "id": "a4f84cff-996b-4bfd-b5de-2d8f0c9aa8b1", + "schema": [ + ["Country", "code", "name", "location", "tld"], + ["TRANSFER", "amount"], + ["Entity", "name", "location", "id", "country"] + ] + } + ] + }, + { + "title": "Entities", + "reports": [ + { + "x": 0, + "y": 0, + "title": "Entity Investigator 🔎", + "query": "You can use this page to explore information about a single entity in the dataset. All reports are automatically updated based on the selected entity.\n\n**Hint**: Try typing **ING Bank NV** \nin the \"Entity name\" box to the right of this text.\n\n\n", + "width": 6, + "type": "text", + "height": 4, + "selection": {}, + "settings": {}, + "id": "e33482d3-a7a2-4090-8868-0ed3931bc99e" + }, + { + "x": 6, + "y": 0, + "title": "Select an entity to view reports", + "query": "MATCH (n:`Entity`) \nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`name` as value, n.`name` as display ORDER BY size(toString(value)) ASC LIMIT 5", + "width": 5, + "type": "select", + "height": 4, + "selection": {}, + "settings": { + "type": "Node Property", + "entityType": "Entity", + "propertyType": "name", + "parameterName": "neodash_entity_name" + }, + "id": "5a5a46cb-d586-4831-88bc-b193edfa9e9c" + }, + { + "x": 11, + "y": 0, + "title": "Details ", + "query": " MATCH (e:Entity)\nWHERE e.name = $neodash_entity_name\nWITH e LIMIT 1\nMATCH (c:Country)--(e)--(f:Filing)\nWITH e, c, sum(f.amount) AS totalAmount, min(f.begin) AS startOperation\nWITH e, c, totalAmount, startOperation\nRETURN e.name as `Entity full name`,\n c.name as `Country of origin`,\n \"$\" + toInteger(totalAmount/1000000) + \" million\" as `Total filings`,\n toString(date(startOperation)) as `Start of operations`\n", + "width": 7, + "type": "table", + "height": 4, + "selection": {}, + "settings": { + "compact": false + }, + "id": "6e2e57b7-09c8-46cf-b33e-19fd3645693b", + "schema": [] + }, + { + "x": 18, + "y": 0, + "title": "Entity interactions", + "query": "MATCH path=(e:Entity)<--()-->(e2:Entity)\nWHERE e.name = $neodash_entity_name\nWITH DISTINCT e, e2\nRETURN e, e2, apoc.create.vRelationship(e, \"INTERACTS\", {}, e2) \n\n\n", + "width": 6, + "type": "map", + "height": 4, + "selection": { + "Entity": "(no label)", + "INTERACTS": "(label)" }, - { - "x": 6, - "y": 0, - "title": "Details ", - "query": "MATCH (e:Entity)\nWHERE e.name = $neodash_entity_name\nWITH e LIMIT 1\nMATCH (c:Country)--(e)--(f:Filing)\nWITH [\"Entity full name: \" + e.name, \n \"Country of origin: \"+c.name, \n \"Total filings: $\"+ toInteger(sum(f.amount)/1000000) + \" million\",\n \"Start of operations: \"+ toString(date((min(f.begin))))\n] as data\nUNWIND data as Information\nRETURN Information\n", - "width": "3", - "type": "table", - "height": 2, - "selection": {}, - "settings": {} + "settings": { + "hideSelections": true }, - { - "x": 9, - "y": 0, - "title": "Entity interactions", - "query": "MATCH path=(e:Entity)<--()-->(e2:Entity)\nWHERE e.name = $neodash_entity_name\nWITH DISTINCT e, e2\nRETURN e, e2, apoc.create.vRelationship(e, \"INTERACTS\", {}, e2) \n\n\n", - "width": 3, - "type": "map", - "height": 2, - "selection": { - "Entity": "(no label)" - }, - "settings": { - "hideSelections": true - } + "id": "6070f205-23ce-42bb-abc1-96ec3839e531", + "schema": [["Entity", "name", "location", "id", "country"], ["INTERACTS"]] + }, + { + "x": 0, + "y": 4, + "title": "Who receives most money from this entity?", + "query": "MATCH path=(e:Entity)<--(f:Filing)-->(e2:Entity)\nWHERE e.name = $neodash_entity_name\nWITH DISTINCT e, f, e2\nRETURN e2.name as `Other`, sum(f.amount) as Amount\nORDER BY Amount ASC", + "width": 12, + "type": "bar", + "height": 4, + "selection": { + "index": "Other", + "value": "Amount", + "key": "(none)" }, - { - "x": 0, - "y": 2, - "title": "Who receives most money from this entity?", - "query": "MATCH path=(e:Entity)<--(f:Filing)-->(e2:Entity)\nWHERE e.name = $neodash_entity_name\nWITH DISTINCT e, f, e2\nRETURN e2.name as `Other`, sum(f.amount) as Amount\nORDER BY Amount ASC", - "width": "6", - "type": "bar", - "height": 2, - "selection": { - "index": "Other", - "value": "Amount", - "key": "(none)" - }, - "settings": { - "valueScale": "linear", - "marginLeft": 90, - "marginBottom": 100, - "marginRight": 50, - "colors": "paired", - "groupMode": "grouped" - } + "settings": { + "valueScale": "linear", + "marginLeft": 90, + "marginBottom": 100, + "marginRight": 50, + "colors": "paired", + "groupMode": "grouped" }, - { - "x": 6, - "y": 2, - "title": "Details on a filing by the entity", - "query": "MATCH path=(e:Entity)<--(f:Filing)\nWHERE e.name = $neodash_entity_name\nRETURN f LIMIT 1\n", - "width": 3, - "type": "json", - "height": 2, - "selection": {}, - "settings": {} + "id": "7023ae0c-68af-4fa6-8c59-3ba91d7980aa" + }, + { + "x": 12, + "y": 4, + "title": "Details on a filing by the entity", + "query": "MATCH path=(e:Entity)<--(f:Filing)\nWHERE e.name = $neodash_entity_name\nRETURN f LIMIT 1\n", + "width": 6, + "type": "json", + "height": 4, + "selection": {}, + "settings": {}, + "id": "ceb4d37e-8d9c-45c9-9062-485d40bed6cd" + }, + { + "x": 18, + "y": 4, + "title": "Number of Filings", + "query": "MATCH (e:Entity)--(:Filing)\nWHERE e.name = $neodash_entity_name\nRETURN COUNT(*)\n\n", + "width": 6, + "type": "value", + "height": 4, + "selection": {}, + "settings": { + "fontSize": 80 + }, + "id": "0efecd19-10ea-4475-b649-19b3b1b1e511" + } + ] + }, + { + "title": "Statistics", + "reports": [ + { + "x": 0, + "y": 0, + "title": "Total number of nodes", + "query": "MATCH (n)\nRETURN COUNT(n)", + "width": 6, + "type": "value", + "height": 4, + "selection": {}, + "settings": { + "textAlign": "center", + "fontSize": 80, + "marginTop": 50 }, - { - "x": 9, - "y": 2, - "title": "Number of Filings", - "query": "MATCH (e:Entity)--(:Filing)\nWHERE e.name = $neodash_entity_name\nRETURN COUNT(*)\n\n", - "width": 3, - "type": "value", - "height": 2, - "selection": {}, - "settings": { - "fontSize": 80 - } - } - ] - }, - { - "title": "Statistics", - "reports": [ - { - "x": 0, - "y": 0, - "title": "Total number of nodes", - "query": "MATCH (n)\nRETURN COUNT(n)", - "width": 3, - "type": "value", - "height": 2, - "selection": {}, - "settings": { - "textAlign": "center", - "fontSize": 80, - "marginTop": 50 - } + "id": "073fc2f0-c1a8-4206-ac48-53171ed98696" + }, + { + "x": 6, + "y": 0, + "title": "Total number of relationships", + "query": "MATCH (n)-[e]->(m)\nRETURN COUNT(e)\n\n\n", + "width": 6, + "type": "value", + "height": 4, + "selection": {}, + "settings": { + "fontSize": 80, + "marginTop": 50, + "textAlign": "center" }, - { - "x": 3, - "y": 0, - "title": "Total number of relationships", - "query": "MATCH (n)-[e]->(m)\nRETURN COUNT(e)\n\n\n", - "width": 3, - "type": "value", - "height": 2, - "selection": {}, - "settings": { - "fontSize": 80, - "marginTop": 50, - "textAlign": "center" - } + "id": "294a7cc4-bd1e-4d6f-b440-16e9ad9a95f8" + }, + { + "x": 12, + "y": 0, + "title": "Number of nodes by label", + "query": "MATCH (n)\nRETURN labels(n), count(*) as count\nORDER BY count ASC\n\n\n", + "width": 6, + "type": "pie", + "height": 4, + "selection": { + "index": "labels(n)", + "value": "count", + "key": "(none)" }, - { - "x": 6, - "y": 0, - "title": "Number of nodes by label", - "query": "MATCH (n)\nRETURN labels(n), count(*) as count\nORDER BY count ASC\n\n\n", - "width": 3, - "type": "pie", - "height": 2, - "selection": { - "index": "labels(n)", - "value": "count", - "key": "(none)" - }, - "settings": { - "colors": "pastel1", - "marginBottom": 60 - } + "settings": { + "colors": "pastel1", + "marginBottom": 60 }, - { - "x": 9, - "y": 0, - "title": "Number of relationship types", - "query": "MATCH (n)-[e]->(m)\nRETURN type(e),count(*) as count\nORDER BY count ASC\n\n\n\n\n\n\n", - "width": 3, - "type": "pie", - "height": 2, - "selection": { - "index": "type(e)", - "value": "count", - "key": "(none)" - }, - "settings": { - "colors": "pastel1", - "marginBottom": 60, - "marginLeft": 120, - "marginRight": 120 - } + "id": "bb995076-94b4-4e58-822b-19d4f8c62ac8" + }, + { + "x": 18, + "y": 0, + "title": "Number of relationship types", + "query": "MATCH (n)-[e]->(m)\nRETURN type(e),count(*) as count\nORDER BY count ASC\n\n\n\n\n\n\n", + "width": 6, + "type": "pie", + "height": 4, + "selection": { + "index": "type(e)", + "value": "count", + "key": "(none)" }, - { - "x": 0, - "y": 2, - "title": "Number of filing per year", - "query": "MATCH (f:Filing)\nWHERE f.begin IS NOT NULL\nWITH f, date(f.begin).year as Year\nRETURN Year, COUNT(f) as Total\nORDER BY Year ASC\n", - "width": 6, - "type": "line", - "height": 2, - "selection": { - "x": "Year", - "value": [ - "Total" - ] - }, - "settings": { - "marginLeft": 60 - } + "settings": { + "colors": "pastel1", + "marginBottom": 60, + "marginLeft": 120, + "marginRight": 120 }, - { - "x": 6, - "y": 2, - "title": "Example: using iFrames to embed custom visualizations (3D graph)", - "query": "https://vasturiano.github.io/react-force-graph/example/basic/", - "width": 6, - "type": "iframe", - "height": 2, - "selection": {}, - "settings": {} - } - ] - } - ] - } \ No newline at end of file + "id": "6b604fad-6104-49e7-9aea-0a878a887e49" + }, + { + "x": 0, + "y": 4, + "title": "Number of filing per year", + "query": "MATCH (f:Filing)\nWHERE f.begin IS NOT NULL\nWITH f, date(f.begin).year as Year\nRETURN Year, COUNT(f) as Total\nORDER BY Year ASC\n", + "width": 12, + "type": "line", + "height": 4, + "selection": { + "x": "Year", + "value": ["Total"] + }, + "settings": { + "marginLeft": 60 + }, + "id": "93ce2eab-4f7e-4e6e-a71e-257ebf5f68a9" + }, + { + "x": 12, + "y": 4, + "title": "Example: using iFrames to embed custom visualizations (3D graph)", + "query": "https://vasturiano.github.io/react-force-graph/example/basic/", + "width": 12, + "type": "iframe", + "height": 4, + "selection": {}, + "settings": {}, + "id": "94463630-ed46-4cbe-b140-316151e23ed1" + } + ] + } + ], + "extensions": { + "advanced-charts": { + "active": true + }, + "styling": { + "active": true + }, + "active": true, + "activeReducers": [] + } +} diff --git a/package.json b/package.json index f97d7fa5a..e5f7357dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "neodash", - "version": "2.4.6", + "version": "2.4.7", "description": "NeoDash - Neo4j Dashboard Builder", "neo4jDesktop": { "apiVersion": "^1.2.0" diff --git a/release-notes.md b/release-notes.md index d8b929222..a5f0da386 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,7 +1,11 @@ -## NeoDash 2.4.6 -This is a minor release containing a few critical fixes and some extra style customizations: +## NeoDash 2.4.7 +This is a minor release containing a few critical fixes and general code quality improvements: -- Fix bad text wrapping for arrays in tables ([868](/~https://github.com/neo4j-labs/neodash/pull/868)). -- Make wrapping in table optional, disabled by default ([872](/~https://github.com/neo4j-labs/neodash/pull/872)). -- Fixed issues where cross database dashboard sharing always reverted back to the default database ([873](/~https://github.com/neo4j-labs/neodash/pull/873)). -- Added option to define style config using environment variables for the Docker image ([876](/~https://github.com/neo4j-labs/neodash/pull/876)). \ No newline at end of file +- Fix multiple parameter select ([881](/~https://github.com/neo4j-labs/neodash/pull/881)). +- Fix parameter casting error when loading dashboards([874](/~https://github.com/neo4j-labs/neodash/pull/874)). +- Fix the fraud demo in the [Example Gallery](https://neodash-gallery.graphapp.io/). + +Thanks to all the contributors for this release: +- [alfredorubin96](/~https://github.com/alfredorubin96), +- [MariusC](/~https://github.com/mariusconjeaud), +- [elizarp](/~https://github.com/elizarp). \ No newline at end of file diff --git a/src/application/ApplicationThunks.ts b/src/application/ApplicationThunks.ts index e3ddc8ce0..5076e855c 100644 --- a/src/application/ApplicationThunks.ts +++ b/src/application/ApplicationThunks.ts @@ -14,7 +14,6 @@ import { createNotificationThunk } from '../page/PageThunks'; import { runCypherQuery } from '../report/ReportQueryRunner'; import { setPageNumberThunk, - updateParametersToNeo4jTypeThunk, updateGlobalParametersThunk, updateSessionParameterThunk, } from '../settings/SettingsThunks'; @@ -507,8 +506,6 @@ export const loadApplicationConfigThunk = () => async (dispatch: any, getState: ); } } - // At the load of a dashboard, we want to ensure correct casting types - dispatch(updateParametersToNeo4jTypeThunk()); // SSO - specific case starts here. if (state.application.waitForSSO) { @@ -650,8 +647,8 @@ export const initializeApplicationAsStandaloneThunk = } else { dispatch(setDashboardToLoadAfterConnecting(`name:${config.standaloneDashboardName}`)); } - dispatch(setParametersToLoadAfterConnecting(paramsToSetAfterConnecting)); + dispatch(updateGlobalParametersThunk(paramsToSetAfterConnecting)); if (clearNotificationAfterLoad) { dispatch(clearNotification()); diff --git a/src/chart/ChartUtils.ts b/src/chart/ChartUtils.ts index 1ff72b7f6..c88afe17a 100644 --- a/src/chart/ChartUtils.ts +++ b/src/chart/ChartUtils.ts @@ -220,7 +220,7 @@ export function replaceDashboardParameters(str, parameters) { let type = getRecordType(val); // Arrays weren't playing nicely with RenderSubValue(). Each object would be passed separately and return [oject Object]. - if (type === 'string' || type == 'link' ) { + if (type === 'string' || type == 'link') { return val; } else if (type === 'array') { return RenderSubValue(val.join(', ')); @@ -415,7 +415,7 @@ export function isCastableToNeo4jDate(value: object) { return false; } let keys = Object.keys(value); - return keys.length == 3 && keys.includes('day') && keys.includes('month') && keys.includes('year'); + return keys.includes('day') && keys.includes('month') && keys.includes('year'); } /** @@ -425,7 +425,7 @@ export function isCastableToNeo4jDate(value: object) { */ export function castToNeo4jDate(value: object) { if (isCastableToNeo4jDate(value)) { - return new Neo4jDate(value.year, value.month, value.day); + return new Neo4jDate(toNumber(value.year), toNumber(value.month), toNumber(value.day)); } throw new Error(`Invalid input for castToNeo4jDate: ${value}`); } diff --git a/src/chart/table/TableChart.tsx b/src/chart/table/TableChart.tsx index 58e54e71f..d64633056 100644 --- a/src/chart/table/TableChart.tsx +++ b/src/chart/table/TableChart.tsx @@ -184,7 +184,9 @@ export const NeoTableChart = (props: ChartProps) => { Object.assign( { id: i, Field: key }, ...records.map((record, j) => ({ - [`${record._fields[0]}_${j + 1}`]: RenderSubValue(record._fields[i + 1]), + // Note the true here is for the rendered to know we are inside a transposed table + // It will be needed for rendering the records properly, if they are arrays + [`${record._fields[0]}_${j + 1}`]: RenderSubValue(record._fields[i + 1], true), })) ) ); diff --git a/src/config/ReportConfig.tsx b/src/config/ReportConfig.tsx index 97b6bc913..a8e716d52 100644 --- a/src/config/ReportConfig.tsx +++ b/src/config/ReportConfig.tsx @@ -421,28 +421,6 @@ const _REPORT_TYPES = { type: SELECTION_TYPES.NUMBER, default: 0.25, }, - expandHeightForLegend: { - label: 'Expand Height For Legend', - type: SELECTION_TYPES.LIST, - values: [true, false], - default: false, - }, - innerPadding: { - label: 'Inner Padding', - type: SELECTION_TYPES.NUMBER, - default: 0, - }, - legendPosition: { - label: 'Legend Position', - type: SELECTION_TYPES.LIST, - values: ['Horizontal', 'Vertical'], - default: 'Vertical', - }, - padding: { - label: 'Padding', - type: SELECTION_TYPES.NUMBER, - default: 0.25, - }, }, }, pie: { diff --git a/src/dashboard/DashboardThunks.ts b/src/dashboard/DashboardThunks.ts index 2ab54e614..3ef4e62bf 100644 --- a/src/dashboard/DashboardThunks.ts +++ b/src/dashboard/DashboardThunks.ts @@ -3,13 +3,12 @@ import { updateDashboardSetting } from '../settings/SettingsActions'; import { addPage, movePage, removePage, resetDashboardState, setDashboard, setDashboardUuid } from './DashboardActions'; import { QueryStatus, runCypherQuery } from '../report/ReportQueryRunner'; import { setDraft, setParametersToLoadAfterConnecting, setWelcomeScreenOpen } from '../application/ApplicationActions'; -import { updateGlobalParametersThunk, updateParametersToNeo4jTypeThunk } from '../settings/SettingsThunks'; +import { updateGlobalParametersThunk } from '../settings/SettingsThunks'; import { createUUID } from '../utils/uuid'; import { createLogThunk } from '../application/logging/LoggingThunk'; import { applicationGetConnectionUser, applicationIsStandalone } from '../application/ApplicationSelectors'; import { applicationGetLoggingSettings } from '../application/logging/LoggingSelectors'; import { NEODASH_VERSION, VERSION_TO_MIGRATE } from './DashboardReducer'; -import { Date as Neo4jDate } from 'neo4j-driver-core/lib/temporal-types.js'; export const removePageThunk = (number) => (dispatch: any, getState: any) => { try { @@ -113,16 +112,6 @@ export const loadDashboardThunk = (uuid, text) => (dispatch: any, getState: any) throw `Invalid dashboard version: ${dashboard.version}. Try restarting the application, or retrieve your cached dashboard using a debug report.`; } - // Cast dashboard parameters from serialized format to correct types - Object.keys(dashboard.settings.parameters).forEach((key) => { - const value = dashboard.settings.parameters[key]; - - // Serialized Date to Neo4jDate - if (value && value.year && value.month && value.day) { - dashboard.settings.parameters[key] = new Neo4jDate(value.year, value.month, value.day); - } - }); - // Reverse engineer the minimal set of fields from the selection loaded. dashboard.pages.forEach((p) => { p.reports.forEach((r) => { @@ -140,9 +129,8 @@ export const loadDashboardThunk = (uuid, text) => (dispatch: any, getState: any) const { application } = getState(); dispatch(updateGlobalParametersThunk(application.parametersToLoadAfterConnecting)); + dispatch(updateGlobalParametersThunk(dashboard.settings.parameters)); dispatch(setParametersToLoadAfterConnecting(null)); - dispatch(updateParametersToNeo4jTypeThunk()); - // Pre-2.3.4 dashboards might now always have a UUID. Set it if not present. if (!dashboard.uuid) { dispatch(setDashboardUuid(uuid)); diff --git a/src/extensions/advancedcharts/Utils.ts b/src/extensions/advancedcharts/Utils.ts index aa7c1f840..647223918 100644 --- a/src/extensions/advancedcharts/Utils.ts +++ b/src/extensions/advancedcharts/Utils.ts @@ -1,7 +1,6 @@ import { valueIsArray } from '../../chart/ChartUtils'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { getPageNumbersAndNames } from '../../dashboard/DashboardSelectors'; -import { updateDashboardSetting } from '../../settings/SettingsActions'; export const getRule = (e, rules, type) => { let r = getRuleWithFieldPropertyName(e, rules, type, null); diff --git a/src/modal/AboutModal.tsx b/src/modal/AboutModal.tsx index 826b032d5..bda365175 100644 --- a/src/modal/AboutModal.tsx +++ b/src/modal/AboutModal.tsx @@ -3,7 +3,7 @@ import { Button, Dialog, TextLink } from '@neo4j-ndl/react'; import { BookOpenIconOutline, BeakerIconOutline } from '@neo4j-ndl/react/icons'; import { Section, SectionTitle, SectionContent } from './ModalUtils'; -export const version = '2.4.6'; +export const version = '2.4.7'; export const NeoAboutModal = ({ open, handleClose, getDebugState }) => { const downloadDebugFile = () => { diff --git a/src/report/ReportRecordProcessing.tsx b/src/report/ReportRecordProcessing.tsx index f30908577..15159b911 100644 --- a/src/report/ReportRecordProcessing.tsx +++ b/src/report/ReportRecordProcessing.tsx @@ -246,15 +246,36 @@ function RenderPath(value) { }); } -function RenderArray(value) { +/** + * Renders an array of values. + * + * @param value - The array of values to render. + * @param transposedTable - Optional. Specifies whether the table should be transposed. Default is false. + * @returns The rendered array of values. + */ +function RenderArray(value, transposedTable = false) { + let mapped = []; + // If the first value is neither a Node nor a Relationship object + // It is safe to assume that all values should be renedered as strings if (value.length > 0 && !valueIsNode(value[0]) && !valueIsRelationship(value[0])) { - return RenderString(value.join(', ')); + // If this request comes up from a transposed table + // The returned value must be a single value, not an array + // Otherwise, it will cast to [Object object], [Object object] + if (transposedTable) { + return RenderString(value.join(', ')); + } + // Nominal case of a list of values renderable as strings + // These should be joined by commas, and not inside tags + mapped = value.map((v, i) => { + return RenderSubValue(v) + (i < value.length - 1 ? ', ' : ''); + }); } - const mapped = value.map((v, i) => { + // Render Node and Relationship objects, which will look like a Path + mapped = value.map((v, i) => { return ( {RenderSubValue(v)} - {i < value.length - 1 && !valueIsNode(v) && !valueIsRelationship(v) ? : <>} + {i < value.length - 1 && !valueIsNode(v) && !valueIsRelationship(v) ? , : <>} ); }); @@ -320,7 +341,7 @@ function RenderNumber(value) { return number; } -export function RenderSubValue(value) { +export function RenderSubValue(value, transposedTable = false) { if (value == undefined) { return ''; } @@ -328,7 +349,7 @@ export function RenderSubValue(value) { const columnProperties = rendererForType[type]; if (columnProperties) { if (columnProperties.renderValue) { - return columnProperties.renderValue({ value: value }); + return columnProperties.renderValue({ value: value, transposedTable: transposedTable }); } else if (columnProperties.valueGetter) { return columnProperties.valueGetter({ value: value }); } @@ -366,7 +387,7 @@ export const rendererForType: any = { }, array: { type: 'string', - renderValue: (c) => RenderArray(c.value), + renderValue: (c) => RenderArray(c.value, c.transposedTable), }, string: { type: 'string', diff --git a/src/settings/SettingsThunks.ts b/src/settings/SettingsThunks.ts index 267911ae5..9a225089d 100644 --- a/src/settings/SettingsThunks.ts +++ b/src/settings/SettingsThunks.ts @@ -1,6 +1,6 @@ import { setSessionParameters } from '../application/ApplicationActions'; import { hardResetCardSettings } from '../card/CardActions'; -import { castToNeo4jDate, isCastableToNeo4jDate, valueIsNode } from '../chart/ChartUtils'; +import { castToNeo4jDate, isCastableToNeo4jDate, toNumber, valueIsNode } from '../chart/ChartUtils'; import { createNotificationThunk } from '../page/PageThunks'; import { updateDashboardSetting } from './SettingsActions'; @@ -73,6 +73,7 @@ export const updateGlobalParametersThunk = (newParameters) => (dispatch: any, ge } }); dispatch(updateDashboardSetting('parameters', { ...parameters })); + dispatch(updateParametersToNeo4jTypeThunk()); } } catch (e) { dispatch(createNotificationThunk('Unable to update global parameters', e)); @@ -86,12 +87,13 @@ export const updateParametersToNeo4jTypeThunk = () => (dispatch: any, getState: try { const { settings } = getState().dashboard; const parameters = settings.parameters ? settings.parameters : {}; - // if new parameters are set... // iterate over the key value pairs in parameters Object.keys(parameters).forEach((key) => { if (isCastableToNeo4jDate(parameters[key])) { parameters[key] = castToNeo4jDate(parameters[key]); + } else if (parameters[key] && typeof toNumber(parameters[key]) === 'number') { + parameters[key] = toNumber(parameters[key]); } else if (parameters[key] == undefined) { delete parameters[key]; }