Skip to content

Commit

Permalink
Merge pull request #881 from neo4j-labs/bug/array-spaghetti-bowl
Browse files Browse the repository at this point in the history
Rendering of multiple parameter select
  • Loading branch information
mariusconjeaud authored May 7, 2024
2 parents 16b3d5a + 04f048a commit 84cd7ed
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 51 deletions.
154 changes: 154 additions & 0 deletions cypress/e2e/render/array.cy.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
44 changes: 1 addition & 43 deletions cypress/e2e/start_page.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
}
84 changes: 84 additions & 0 deletions cypress/e2e/utils.js
Original file line number Diff line number Diff line change
@@ -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);
}
8 changes: 8 additions & 0 deletions cypress/fixtures/cypher_queries.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/chart/table/TableChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}))
)
);
Expand Down
35 changes: 28 additions & 7 deletions src/report/ReportRecordProcessing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <span> 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 (
<span key={String(`k${i}`) + v}>
{RenderSubValue(v)}
{i < value.length - 1 && !valueIsNode(v) && !valueIsRelationship(v) ? <span>,&nbsp;</span> : <></>}
{i < value.length - 1 && !valueIsNode(v) && !valueIsRelationship(v) ? <span>, </span> : <></>}
</span>
);
});
Expand Down Expand Up @@ -320,15 +341,15 @@ function RenderNumber(value) {
return number;
}

export function RenderSubValue(value) {
export function RenderSubValue(value, transposedTable = false) {
if (value == undefined) {
return '';
}
const type = getRecordType(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 });
}
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit 84cd7ed

Please sign in to comment.