Skip to content

Commit fcb014b

Browse files
nielsdejongbrahmprakashMishraBennuFire
authored
Added styling rules for relationship color (cherrypicked) (#537)
* Added styling rules for relationship color * Removed stray console logs and added comments * Refactor PR * Clean smells * Review commit * updated naming * Refactor --------- Co-authored-by: Brahm Prakash Mishra <[email protected]> Co-authored-by: Bennu <[email protected]>
1 parent 0d1f1ab commit fcb014b

File tree

13 files changed

+186
-68
lines changed

13 files changed

+186
-68
lines changed

src/card/Card.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ const NeoCard = ({
175175
height={report.height}
176176
heightPx={height}
177177
fields={report.fields}
178+
schema={report.schema}
178179
type={report.type}
179180
expanded={expanded}
180181
extensions={extensions}

src/card/CardActions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ export const updateFields = (pagenumber: number, id: number, fields: any) => ({
5050
payload: { pagenumber, id, fields },
5151
});
5252

53+
export const UPDATE_SCHEMA = 'PAGE/CARD/UPDATE_SCHEMA';
54+
export const updateSchema = (pagenumber: number, id: number, schema: any) => ({
55+
type: UPDATE_SCHEMA,
56+
payload: { pagenumber, id, schema },
57+
});
58+
5359
export const UPDATE_SELECTION = 'PAGE/CARD/UPDATE_SELECTION';
5460
export const updateSelection = (pagenumber: number, id: number, selectable: any, field: any) => ({
5561
type: UPDATE_SELECTION,

src/card/CardReducer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
UPDATE_ALL_SELECTIONS,
66
UPDATE_CYPHER_PARAMETERS,
77
UPDATE_FIELDS,
8+
UPDATE_SCHEMA,
89
UPDATE_REPORT_QUERY,
910
UPDATE_REPORT_SETTING,
1011
UPDATE_REPORT_SIZE,
@@ -72,6 +73,11 @@ export const cardReducer = (state = CARD_INITIAL_STATE, action: { type: any; pay
7273
state = update(state, { fields: fields });
7374
return state;
7475
}
76+
case UPDATE_SCHEMA: {
77+
const { schema } = payload;
78+
state = update(state, { schema: schema });
79+
return state;
80+
}
7581
case UPDATE_REPORT_TYPE: {
7682
const { type } = payload;
7783
state = update(state, { type: type });

src/card/CardThunks.ts

Lines changed: 59 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
clearSelection,
1111
updateAllSelections,
1212
updateReportDatabase,
13+
updateSchema,
1314
} from './CardActions';
1415
import { createNotificationThunk } from '../page/PageThunks';
1516
import { getReportTypes } from '../extensions/ExtensionUtils';
@@ -67,68 +68,76 @@ export const updateReportTypeThunk = (id, type) => (dispatch: any, getState: any
6768

6869
dispatch(updateReportType(pagenumber, id, type));
6970
dispatch(updateFields(pagenumber, id, []));
71+
dispatch(updateSchema(pagenumber, id, []));
7072
dispatch(clearSelection(pagenumber, id));
7173
} catch (e) {
7274
dispatch(createNotificationThunk('Cannot update report type', e));
7375
}
7476
};
7577

76-
export const updateFieldsThunk = (id, fields) => (dispatch: any, getState: any) => {
77-
try {
78-
const state = getState();
79-
const { pagenumber } = state.dashboard.settings;
80-
const extensions = Object.fromEntries(Object.entries(state.dashboard.extensions).filter(([_, v]) => v.active));
81-
const oldReport = state.dashboard.pages[pagenumber].reports.find((o) => o.id === id);
78+
export const updateFieldsThunk =
79+
(id, fields, schema = false) =>
80+
(dispatch: any, getState: any) => {
81+
try {
82+
const state = getState();
83+
const { pagenumber } = state.dashboard.settings;
84+
const extensions = Object.fromEntries(Object.entries(state.dashboard.extensions).filter(([_, v]) => v.active));
85+
const oldReport = state.dashboard.pages[pagenumber].reports.find((o) => o.id === id);
8286

83-
if (!oldReport) {
84-
return;
85-
}
86-
const oldFields = oldReport.fields;
87-
const reportType = oldReport.type;
88-
const oldSelection = oldReport.selection;
89-
const reportTypes = getReportTypes(extensions);
90-
const selectableFields = reportTypes[reportType].selection; // The dictionary of selectable fields as defined in the config.
91-
const { autoAssignSelectedProperties } = reportTypes[reportType];
92-
const selectables = selectableFields ? Object.keys(selectableFields) : [];
87+
if (!oldReport) {
88+
return;
89+
}
90+
const oldFields = schema ? oldReport.schema : oldReport.fields;
91+
const reportType = oldReport.type;
92+
const oldSelection = oldReport.selection;
93+
const reportTypes = getReportTypes(extensions);
94+
const selectableFields = reportTypes[reportType].selection; // The dictionary of selectable fields as defined in the config.
95+
const { autoAssignSelectedProperties } = reportTypes[reportType];
96+
const selectables = selectableFields ? Object.keys(selectableFields) : [];
9397

94-
// If the new set of fields is not equal to the current set of fields, we ned to update the field selection.
95-
if (!isEqual(oldFields, fields) || Object.keys(oldSelection).length === 0) {
96-
selectables.forEach((selection, i) => {
97-
if (fields.includes(oldSelection[selection])) {
98-
// If the current selection is still present in the new set of fields, no need to reset.
99-
// Also we ignore this on a node property selector.
100-
/* continue */
101-
} else if (selectableFields[selection].optional) {
102-
// If the fields change, always set optional selections to none.
103-
if (selectableFields[selection].multiple) {
104-
dispatch(updateSelection(pagenumber, id, selection, ['(none)']));
105-
} else {
106-
dispatch(updateSelection(pagenumber, id, selection, '(none)'));
107-
}
108-
} else if (fields.length > 0) {
109-
// For multi selections, select the Nth item of the result fields as a single item array.
110-
if (selectableFields[selection].multiple) {
111-
// only update if the old selection no longer covers the new set of fields...
112-
if (!oldSelection[selection] || !oldSelection[selection].every((v) => fields.includes(v))) {
113-
dispatch(updateSelection(pagenumber, id, selection, [fields[Math.min(i, fields.length - 1)]]));
98+
// If the new set of fields is not equal to the current set of fields, we ned to update the field selection.
99+
if (!isEqual(oldFields, fields) || Object.keys(oldSelection).length === 0) {
100+
selectables.forEach((selection, i) => {
101+
if (fields.includes(oldSelection[selection])) {
102+
// If the current selection is still present in the new set of fields, no need to reset.
103+
// Also we ignore this on a node property selector.
104+
/* continue */
105+
} else if (selectableFields[selection].optional) {
106+
// If the fields change, always set optional selections to none.
107+
if (selectableFields[selection].multiple) {
108+
dispatch(updateSelection(pagenumber, id, selection, ['(none)']));
109+
} else {
110+
dispatch(updateSelection(pagenumber, id, selection, '(none)'));
111+
}
112+
} else if (fields.length > 0) {
113+
// For multi selections, select the Nth item of the result fields as a single item array.
114+
if (selectableFields[selection].multiple) {
115+
// only update if the old selection no longer covers the new set of fields...
116+
if (!oldSelection[selection] || !oldSelection[selection].every((v) => fields.includes(v))) {
117+
dispatch(updateSelection(pagenumber, id, selection, [fields[Math.min(i, fields.length - 1)]]));
118+
}
119+
} else if (selectableFields[selection].type == SELECTION_TYPES.NODE_PROPERTIES) {
120+
// For node property selections, select the most obvious properties of the node to display.
121+
const selection = getSelectionBasedOnFields(fields, oldSelection, autoAssignSelectedProperties);
122+
dispatch(updateAllSelections(pagenumber, id, selection));
123+
} else {
124+
// Else, default the selection to the Nth item of the result set fields.
125+
dispatch(updateSelection(pagenumber, id, selection, fields[Math.min(i, fields.length - 1)]));
114126
}
115-
} else if (selectableFields[selection].type == SELECTION_TYPES.NODE_PROPERTIES) {
116-
// For node property selections, select the most obvious properties of the node to display.
117-
const selection = getSelectionBasedOnFields(fields, oldSelection, autoAssignSelectedProperties);
118-
dispatch(updateAllSelections(pagenumber, id, selection));
119-
} else {
120-
// Else, default the selection to the Nth item of the result set fields.
121-
dispatch(updateSelection(pagenumber, id, selection, fields[Math.min(i, fields.length - 1)]));
122127
}
128+
});
129+
// Set the new set of fields for the report so that we may select them.
130+
131+
if (schema) {
132+
dispatch(updateSchema(pagenumber, id, fields));
133+
} else {
134+
dispatch(updateFields(pagenumber, id, fields));
123135
}
124-
});
125-
// Set the new set of fields for the report so that we may select them.
126-
dispatch(updateFields(pagenumber, id, fields));
136+
}
137+
} catch (e) {
138+
dispatch(createNotificationThunk('Cannot update report fields', e));
127139
}
128-
} catch (e) {
129-
dispatch(createNotificationThunk('Cannot update report fields', e));
130-
}
131-
};
140+
};
132141

133142
export const updateSelectionThunk = (id, selectable, field) => (dispatch: any, getState: any) => {
134143
try {

src/card/settings/CardSettings.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const NeoCardSettings = ({
1919
reportSettings,
2020
reportSettingsOpen,
2121
fields,
22+
schema,
2223
heightPx,
2324
extensions, // A set of enabled extensions.
2425
onQueryUpdate,
@@ -78,6 +79,7 @@ const NeoCardSettings = ({
7879
<NeoCardSettingsFooter
7980
type={type}
8081
fields={fields}
82+
schema={schema}
8183
extensions={extensions}
8284
reportSettings={reportSettings}
8385
reportSettingsOpen={reportSettingsOpen}

src/card/settings/CardSettingsFooter.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const update = (state, mutations) => Object.assign({}, state, mutations);
1919
const NeoCardSettingsFooter = ({
2020
type,
2121
fields,
22+
schema,
2223
reportSettings,
2324
reportSettingsOpen,
2425
extensions,
@@ -124,6 +125,7 @@ const NeoCardSettingsFooter = ({
124125
settingValue={reportSettings[settingToCustomize]}
125126
type={type}
126127
fields={fields}
128+
schema={schema}
127129
customReportStyleModalOpen={customReportStyleModalOpen}
128130
setCustomReportStyleModalOpen={setCustomReportStyleModalOpen}
129131
onReportSettingUpdate={onReportSettingUpdate}

src/chart/Utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,8 @@ export const rgbaToHex = (color: string): string => {
111111
}
112112
return color;
113113
};
114+
115+
export enum EntityType {
116+
Node,
117+
Relationship,
118+
}

src/chart/graph/util/RecordUtils.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { evaluateRulesOnNode } from '../../../extensions/styling/StyleRuleEvaluator';
1+
import { evaluateRulesOnNode, evaluateRulesOnLink } from '../../../extensions/styling/StyleRuleEvaluator';
22
import { extractNodePropertiesFromRecords, mergeNodePropsFieldsLists } from '../../../report/ReportRecordProcessing';
33
import { valueIsArray, valueIsNode, valueIsRelationship, valueIsPath } from '../../ChartUtils';
44
import { GraphChartVisualizationProps } from '../GraphChartVisualization';
@@ -162,10 +162,15 @@ export function buildGraphVisualizationObjectFromRecords(
162162
);
163163
});
164164
});
165-
// Assign proper curvatures to relationships.
166-
// This is needed for pairs of nodes that have multiple relationships between them, or self-loops.
165+
// Assign proper curvatures and colors to relationships.
166+
// Assigning curvature is needed for pairs of nodes that have multiple relationships between them, or self-loops.
167167
const linksList = Object.values(links).map((linkArray) => {
168168
return linkArray.map((link, i) => {
169+
let defaultColor = link.color;
170+
171+
// Assign color from json based on style rule evaluation if specified
172+
let evaluatedColor = evaluateRulesOnLink(link, 'relationship color', defaultColor, styleRules);
173+
link.color = evaluatedColor;
169174
const mirroredNodePair = links[`${link.target},${link.source}`];
170175
return assignCurvatureToLink(link, i, linkArray.length, mirroredNodePair ? mirroredNodePair.length : 0);
171176
});

src/extensions/styling/StyleRuleCreationModal.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ export const RULE_BASED_REPORT_CUSTOMIZATIONS = {
5353
value: 'node label color',
5454
label: 'Node Label Color',
5555
},
56+
{
57+
value: 'relationship color',
58+
label: 'Relationship Color',
59+
on: 'relationship',
60+
},
5661
],
5762
map: [
5863
{
@@ -124,6 +129,7 @@ export const NeoCustomReportStyleModal = ({
124129
settingValue,
125130
type,
126131
fields,
132+
schema,
127133
setCustomReportStyleModalOpen,
128134
onReportSettingUpdate,
129135
}) => {
@@ -157,20 +163,20 @@ export const NeoCustomReportStyleModal = ({
157163
* This will be dynamic based on the type of report we are customizing.
158164
*/
159165
const createFieldVariableSuggestions = () => {
160-
if (!fields) {
166+
if (!schema) {
161167
return [];
162168
}
163169
if (type == 'graph' || type == 'map') {
164-
return fields
170+
return schema
165171
.map((node, index) => {
166172
if (!Array.isArray(node)) {
167173
return undefined;
168174
}
169-
return fields[index].map((property, propertyIndex) => {
175+
return schema[index].map((property, propertyIndex) => {
170176
if (propertyIndex == 0) {
171177
return undefined;
172178
}
173-
return `${fields[index][0]}.${property}`;
179+
return `${schema[index][0]}.${property}`;
174180
});
175181
})
176182
.flat()

src/extensions/styling/StyleRuleEvaluator.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { makeStyles } from '@mui/styles';
2-
import { extensionEnabled } from '../ExtensionUtils';
3-
import React, { useEffect } from 'react';
2+
import { EntityType } from '../../chart/Utils';
43

54
/**
65
* Evaluates the specified rule set on a row returned by the Neo4j driver.
@@ -88,17 +87,30 @@ export const evaluateRulesOnDict = (dict, rules, customizations) => {
8887
* @returns a user-defined value if a rule is met, or the default value if none are.
8988
*/
9089
export const evaluateRulesOnNode = (node, customization, defaultValue, rules) => {
91-
if (!node || !customization || !rules) {
90+
return evaluateRules(node, customization, defaultValue, rules, EntityType.Node);
91+
};
92+
93+
export const evaluateRulesOnLink = (link, customization, defaultValue, rules) => {
94+
return evaluateRules(link, customization, defaultValue, rules, EntityType.Relationship);
95+
};
96+
97+
export const evaluateRules = (entity, customization, defaultValue, rules, entityType) => {
98+
if (!entity || !customization || !rules) {
9299
return defaultValue;
93100
}
101+
94102
for (const [index, rule] of rules.entries()) {
95103
// Only look at rules relevant to the target customization.
96104
if (rule.customization == customization) {
97105
// if the row contains the specified field...
98-
const label = rule.field.split('.')[0];
106+
const typeOrLabel = rule.field.split('.')[0];
99107
const property = rule.field.split('.')[1];
100-
if (node.labels.includes(label)) {
101-
const realValue = node.properties[property] ? node.properties[property] : '';
108+
109+
if (
110+
(entityType === EntityType.Node && entity.labels.includes(typeOrLabel)) ||
111+
(entityType === EntityType.Relationship && entity.type == typeOrLabel)
112+
) {
113+
const realValue = entity?.properties?.[property] || '';
102114
const ruleValue = rule.value;
103115
if (evaluateCondition(realValue, rule.condition, ruleValue)) {
104116
return rule.customizationValue;

0 commit comments

Comments
 (0)