Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rendering of multiple parameter select #881

Merged
merged 10 commits into from
May 7, 2024
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
Loading