Skip to content

Commit 84cd7ed

Browse files
Merge pull request #881 from neo4j-labs/bug/array-spaghetti-bowl
Rendering of multiple parameter select
2 parents 16b3d5a + 04f048a commit 84cd7ed

File tree

6 files changed

+278
-51
lines changed

6 files changed

+278
-51
lines changed

cypress/e2e/render/array.cy.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { stringArrayCypherQuery, intArrayCypherQuery, pathArrayCypherQuery } from '../../fixtures/cypher_queries';
2+
import {
3+
enableReportActions,
4+
createReportOfType,
5+
closeSettings,
6+
toggleTableTranspose,
7+
openReportActionsMenu,
8+
selectReportOfType,
9+
openAdvancedSettings,
10+
updateDropdownAdvancedSetting,
11+
} from '../utils';
12+
13+
const WAITING_TIME = 20000;
14+
const CARD_SELECTOR = 'main .react-grid-item:eq(2)';
15+
// Ignore warnings that may appear when using the Cypress dev server
16+
Cypress.on('uncaught:exception', (err, runnable) => {
17+
console.log(err, runnable);
18+
return false;
19+
});
20+
21+
describe('Testing array rendering', () => {
22+
beforeEach('open neodash', () => {
23+
cy.viewport(1920, 1080);
24+
cy.visit('/', {
25+
onBeforeLoad(win) {
26+
win.localStorage.clear();
27+
},
28+
});
29+
30+
cy.get('#form-dialog-title', { WAITING_TIME: WAITING_TIME })
31+
.should('contain', 'NeoDash - Neo4j Dashboard Builder')
32+
.click();
33+
34+
cy.get('#form-dialog-title').then(($div) => {
35+
const text = $div.text();
36+
if (text == 'NeoDash - Neo4j Dashboard Builder') {
37+
cy.wait(500);
38+
// Create new dashboard
39+
cy.contains('New Dashboard').click();
40+
}
41+
});
42+
43+
cy.get('#form-dialog-title', { WAITING_TIME: WAITING_TIME }).should('contain', 'Connect to Neo4j');
44+
45+
cy.get('#url').clear().type('localhost');
46+
cy.get('#dbusername').clear().type('neo4j');
47+
cy.get('#dbpassword').type('test1234');
48+
cy.get('button').contains('Connect').click();
49+
cy.wait(100);
50+
});
51+
52+
it('creates a table that contains string arrays', () => {
53+
cy.checkInitialState();
54+
enableReportActions();
55+
createReportOfType('Table', stringArrayCypherQuery, true, true);
56+
57+
// Standard array, displays strings joined with comma and whitespace
58+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`).should('have.text', 'initial, list');
59+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', 'other, list');
60+
61+
// Now, transpose the table
62+
toggleTableTranspose(CARD_SELECTOR, true);
63+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-columnHeaderTitle:eq(1)`, { timeout: WAITING_TIME }).should(
64+
'have.text',
65+
'initial,list'
66+
);
67+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', 'other, list');
68+
69+
// Transpose back
70+
// And add a report action
71+
toggleTableTranspose(CARD_SELECTOR, false);
72+
openReportActionsMenu(CARD_SELECTOR);
73+
cy.get('.ndl-modal').find('button[aria-label="add"]').click();
74+
cy.get('.ndl-modal').find('input:eq(2)').type('column');
75+
cy.get('.ndl-modal').find('input:eq(5)').type('test_param');
76+
cy.get('.ndl-modal').find('input:eq(6)').type('column');
77+
cy.get('.ndl-modal').find('button').contains('Save').click();
78+
closeSettings(CARD_SELECTOR);
79+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`)
80+
.find('button')
81+
.should('be.visible')
82+
.should('have.text', 'initial, list')
83+
.click();
84+
85+
// Previous step's click set a parameter from the array
86+
// Test that parameter rendering works
87+
cy.get(`${CARD_SELECTOR} .MuiCardHeader-root`).find('input').type('$neodash_test_param').blur();
88+
cy.get(`${CARD_SELECTOR} .MuiCardHeader-root`).find('input').should('have.value', 'initial, list');
89+
});
90+
91+
it('creates a table that contains int arrays', () => {
92+
cy.checkInitialState();
93+
createReportOfType('Table', intArrayCypherQuery, true, true);
94+
95+
// Standard array, displays strings joined with comma and whitespace
96+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`).should('have.text', '1, 2');
97+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', '3, 4');
98+
99+
// Now, transpose the table
100+
toggleTableTranspose(CARD_SELECTOR, true);
101+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-columnHeaderTitle:eq(1)`, { timeout: WAITING_TIME }).should(
102+
'have.text',
103+
'1,2'
104+
);
105+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(1)`).should('have.text', '3, 4');
106+
});
107+
108+
it('creates a table that contains nodes and rels', () => {
109+
cy.checkInitialState();
110+
createReportOfType('Table', pathArrayCypherQuery, true, true);
111+
112+
// Standard array, displays a path with two nodes and a relationship
113+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0)`).should('have.text', 'PersonACTED_INMovie');
114+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) button`).should('have.length', 2);
115+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) button:eq(0)`).should('have.text', 'Person');
116+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) button:eq(1)`).should('have.text', 'Movie');
117+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) .MuiChip-root`).should('have.length', 1);
118+
cy.get(`${CARD_SELECTOR} .MuiDataGrid-cell:eq(0) .MuiChip-root`).should('have.text', 'ACTED_IN');
119+
});
120+
121+
it('creates a single value report which is an array', () => {
122+
cy.checkInitialState();
123+
createReportOfType('Single Value', stringArrayCypherQuery, true, true);
124+
cy.get(CARD_SELECTOR).should('have.text', 'initial, list');
125+
});
126+
127+
it('creates a multi parameter select', () => {
128+
cy.checkInitialState();
129+
selectReportOfType('Parameter Select');
130+
cy.get('main .react-grid-item:eq(2) label[for="Selection Type"]').siblings('div').click();
131+
// Set up the parameter select
132+
cy.contains('Node Property').click();
133+
cy.wait(100);
134+
cy.contains('Node Label').click();
135+
cy.contains('Node Label').siblings('div').find('input').type('Movie');
136+
cy.wait(1000);
137+
cy.get('.MuiAutocomplete-popper').contains('Movie').click();
138+
cy.contains('Property Name').click();
139+
cy.contains('Property Name').siblings('div').find('input').type('title');
140+
cy.wait(1000);
141+
cy.get('.MuiAutocomplete-popper').contains('title').click();
142+
// Enable multiple selection
143+
closeSettings(CARD_SELECTOR);
144+
updateDropdownAdvancedSetting(CARD_SELECTOR, 'Multiple Selection', 'on');
145+
// Finally, select a few values in the parameter select
146+
cy.get(CARD_SELECTOR).contains('Movie title').click();
147+
cy.get(CARD_SELECTOR).contains('Movie title').siblings('div').find('input').type('a');
148+
cy.get('.MuiAutocomplete-popper').contains('Apollo 13').click();
149+
cy.get(CARD_SELECTOR).contains('Movie title').siblings('div').find('input').type('t');
150+
cy.get('.MuiAutocomplete-popper').contains('The Matrix').click();
151+
cy.get(CARD_SELECTOR).contains('Apollo 13').should('be.visible');
152+
cy.get(CARD_SELECTOR).contains('The Matrix').should('be.visible');
153+
});
154+
});

cypress/e2e/start_page.cy.js

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
gaugeChartCypherQuery,
1111
formCypherQuery,
1212
} from '../fixtures/cypher_queries';
13+
import { createReportOfType, selectReportOfType, enableAdvancedVisualizations, enableFormsExtension } from './utils';
1314

1415
const WAITING_TIME = 20000;
1516
// Ignore warnings that may appear when using the Cypress dev server
@@ -293,46 +294,3 @@ describe('NeoDash E2E Tests', () => {
293294
}
294295
});
295296
});
296-
297-
function enableAdvancedVisualizations() {
298-
cy.get('main button[aria-label="Extensions').should('be.visible').click();
299-
cy.get('#checkbox-advanced-charts').should('be.visible').click();
300-
cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click();
301-
cy.wait(200);
302-
}
303-
304-
function enableFormsExtension() {
305-
cy.get('main button[aria-label="Extensions').should('be.visible').click();
306-
cy.get('#checkbox-forms').scrollIntoView();
307-
cy.get('#checkbox-forms').should('be.visible').click();
308-
cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click();
309-
cy.wait(200);
310-
}
311-
312-
function selectReportOfType(type) {
313-
cy.get('main .react-grid-item button[aria-label="add report"]').should('be.visible').click();
314-
cy.get('main .react-grid-item')
315-
.contains('No query specified.')
316-
.parentsUntil('.react-grid-item')
317-
.find('button[aria-label="settings"]', { timeout: 2000 })
318-
.should('be.visible')
319-
.click();
320-
cy.get('main .react-grid-item:eq(2) #type', { timeout: 2000 }).should('be.visible').click();
321-
cy.contains(type).click();
322-
cy.wait(100);
323-
}
324-
325-
function createReportOfType(type, query, fast = false, run = true) {
326-
selectReportOfType(type);
327-
if (fast) {
328-
cy.get('main .react-grid-item:eq(2) .ReactCodeMirror').type(query, { delay: 1, parseSpecialCharSequences: false });
329-
} else {
330-
cy.get('main .react-grid-item:eq(2) .ReactCodeMirror').type(query, { parseSpecialCharSequences: false });
331-
}
332-
cy.wait(400);
333-
334-
cy.get('main .react-grid-item:eq(2)').contains('Advanced settings').click();
335-
if (run) {
336-
cy.get('main .react-grid-item:eq(2) button[aria-label="run"]').click();
337-
}
338-
}

cypress/e2e/utils.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
export function enableReportActions() {
2+
cy.get('main button[aria-label="Extensions').should('be.visible').click();
3+
cy.get('#checkbox-actions').scrollIntoView();
4+
cy.get('#checkbox-actions').should('be.visible').click();
5+
cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click();
6+
cy.wait(200);
7+
}
8+
9+
export function enableAdvancedVisualizations() {
10+
cy.get('main button[aria-label="Extensions').should('be.visible').click();
11+
cy.get('#checkbox-advanced-charts').should('be.visible').click();
12+
cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click();
13+
cy.wait(200);
14+
}
15+
16+
export function enableFormsExtension() {
17+
cy.get('main button[aria-label="Extensions').should('be.visible').click();
18+
cy.get('#checkbox-forms').scrollIntoView();
19+
cy.get('#checkbox-forms').should('be.visible').click();
20+
cy.get('.ndl-dialog-close').scrollIntoView().should('be.visible').click();
21+
cy.wait(200);
22+
}
23+
24+
export function selectReportOfType(type) {
25+
cy.get('main .react-grid-item button[aria-label="add report"]').should('be.visible').click();
26+
cy.get('main .react-grid-item')
27+
.contains('No query specified.')
28+
.parentsUntil('.react-grid-item')
29+
.find('button[aria-label="settings"]', { timeout: 2000 })
30+
.should('be.visible')
31+
.click();
32+
cy.get('main .react-grid-item:eq(2) #type', { timeout: 2000 }).should('be.visible').click();
33+
cy.contains(type).click();
34+
cy.wait(100);
35+
}
36+
37+
export function createReportOfType(type, query, fast = false, run = true) {
38+
selectReportOfType(type);
39+
if (fast) {
40+
cy.get('main .react-grid-item:eq(2) .ReactCodeMirror').type(query, { delay: 1, parseSpecialCharSequences: false });
41+
} else {
42+
cy.get('main .react-grid-item:eq(2) .ReactCodeMirror').type(query, { parseSpecialCharSequences: false });
43+
}
44+
cy.wait(400);
45+
46+
if (run) {
47+
closeSettings('main .react-grid-item:eq(2)');
48+
}
49+
}
50+
51+
export function openSettings(cardSelector) {
52+
cy.get(cardSelector).find('button[aria-label="settings"]', { WAITING_TIME: 2000 }).click();
53+
}
54+
55+
export function closeSettings(cardSelector) {
56+
cy.get(`${cardSelector} button[aria-label="run"]`).click();
57+
}
58+
59+
export function openAdvancedSettings(cardSelector) {
60+
openSettings(cardSelector);
61+
cy.get(cardSelector).contains('Advanced settings').click();
62+
}
63+
64+
export function closeAdvancedSettings(cardSelector) {
65+
cy.get(cardSelector).contains('Advanced settings').click();
66+
closeSettings(cardSelector);
67+
}
68+
69+
export function openReportActionsMenu(cardSelector) {
70+
openSettings(cardSelector);
71+
cy.get(cardSelector).find('button[aria-label="custom actions"]').click();
72+
}
73+
74+
export function updateDropdownAdvancedSetting(cardSelector, settingLabel, targetValue) {
75+
openAdvancedSettings(cardSelector);
76+
cy.get(`${cardSelector} .ndl-dropdown`).contains(settingLabel).siblings('div').click();
77+
cy.contains(targetValue).click();
78+
closeAdvancedSettings(cardSelector);
79+
}
80+
81+
export function toggleTableTranspose(cardSelector, enable) {
82+
let transpose = enable ? 'on' : 'off';
83+
updateDropdownAdvancedSetting(cardSelector, 'Transpose Rows & Columns', transpose);
84+
}

cypress/fixtures/cypher_queries.js

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/chart/table/TableChart.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,9 @@ export const NeoTableChart = (props: ChartProps) => {
184184
Object.assign(
185185
{ id: i, Field: key },
186186
...records.map((record, j) => ({
187-
[`${record._fields[0]}_${j + 1}`]: RenderSubValue(record._fields[i + 1]),
187+
// Note the true here is for the rendered to know we are inside a transposed table
188+
// It will be needed for rendering the records properly, if they are arrays
189+
[`${record._fields[0]}_${j + 1}`]: RenderSubValue(record._fields[i + 1], true),
188190
}))
189191
)
190192
);

src/report/ReportRecordProcessing.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -246,15 +246,36 @@ function RenderPath(value) {
246246
});
247247
}
248248

249-
function RenderArray(value) {
249+
/**
250+
* Renders an array of values.
251+
*
252+
* @param value - The array of values to render.
253+
* @param transposedTable - Optional. Specifies whether the table should be transposed. Default is false.
254+
* @returns The rendered array of values.
255+
*/
256+
function RenderArray(value, transposedTable = false) {
257+
let mapped = [];
258+
// If the first value is neither a Node nor a Relationship object
259+
// It is safe to assume that all values should be renedered as strings
250260
if (value.length > 0 && !valueIsNode(value[0]) && !valueIsRelationship(value[0])) {
251-
return RenderString(value.join(', '));
261+
// If this request comes up from a transposed table
262+
// The returned value must be a single value, not an array
263+
// Otherwise, it will cast to [Object object], [Object object]
264+
if (transposedTable) {
265+
return RenderString(value.join(', '));
266+
}
267+
// Nominal case of a list of values renderable as strings
268+
// These should be joined by commas, and not inside <span> tags
269+
mapped = value.map((v, i) => {
270+
return RenderSubValue(v) + (i < value.length - 1 ? ', ' : '');
271+
});
252272
}
253-
const mapped = value.map((v, i) => {
273+
// Render Node and Relationship objects, which will look like a Path
274+
mapped = value.map((v, i) => {
254275
return (
255276
<span key={String(`k${i}`) + v}>
256277
{RenderSubValue(v)}
257-
{i < value.length - 1 && !valueIsNode(v) && !valueIsRelationship(v) ? <span>,&nbsp;</span> : <></>}
278+
{i < value.length - 1 && !valueIsNode(v) && !valueIsRelationship(v) ? <span>, </span> : <></>}
258279
</span>
259280
);
260281
});
@@ -320,15 +341,15 @@ function RenderNumber(value) {
320341
return number;
321342
}
322343

323-
export function RenderSubValue(value) {
344+
export function RenderSubValue(value, transposedTable = false) {
324345
if (value == undefined) {
325346
return '';
326347
}
327348
const type = getRecordType(value);
328349
const columnProperties = rendererForType[type];
329350
if (columnProperties) {
330351
if (columnProperties.renderValue) {
331-
return columnProperties.renderValue({ value: value });
352+
return columnProperties.renderValue({ value: value, transposedTable: transposedTable });
332353
} else if (columnProperties.valueGetter) {
333354
return columnProperties.valueGetter({ value: value });
334355
}
@@ -366,7 +387,7 @@ export const rendererForType: any = {
366387
},
367388
array: {
368389
type: 'string',
369-
renderValue: (c) => RenderArray(c.value),
390+
renderValue: (c) => RenderArray(c.value, c.transposedTable),
370391
},
371392
string: {
372393
type: 'string',

0 commit comments

Comments
 (0)