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

Release 2.3.1 #540

Merged
merged 15 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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 <brahm.praksh_mishra@mercedes-benz.com>
Co-authored-by: Bennu <agudeloharold13@gmail.com>
  • Loading branch information
3 people authored Aug 1, 2023
commit fcb014b37f8755fe6207557dc6909c118662f777
1 change: 1 addition & 0 deletions src/card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ const NeoCard = ({
height={report.height}
heightPx={height}
fields={report.fields}
schema={report.schema}
type={report.type}
expanded={expanded}
extensions={extensions}
Expand Down
6 changes: 6 additions & 0 deletions src/card/CardActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ export const updateFields = (pagenumber: number, id: number, fields: any) => ({
payload: { pagenumber, id, fields },
});

export const UPDATE_SCHEMA = 'PAGE/CARD/UPDATE_SCHEMA';
export const updateSchema = (pagenumber: number, id: number, schema: any) => ({
type: UPDATE_SCHEMA,
payload: { pagenumber, id, schema },
});

export const UPDATE_SELECTION = 'PAGE/CARD/UPDATE_SELECTION';
export const updateSelection = (pagenumber: number, id: number, selectable: any, field: any) => ({
type: UPDATE_SELECTION,
Expand Down
6 changes: 6 additions & 0 deletions src/card/CardReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
UPDATE_ALL_SELECTIONS,
UPDATE_CYPHER_PARAMETERS,
UPDATE_FIELDS,
UPDATE_SCHEMA,
UPDATE_REPORT_QUERY,
UPDATE_REPORT_SETTING,
UPDATE_REPORT_SIZE,
Expand Down Expand Up @@ -72,6 +73,11 @@ export const cardReducer = (state = CARD_INITIAL_STATE, action: { type: any; pay
state = update(state, { fields: fields });
return state;
}
case UPDATE_SCHEMA: {
const { schema } = payload;
state = update(state, { schema: schema });
return state;
}
case UPDATE_REPORT_TYPE: {
const { type } = payload;
state = update(state, { type: type });
Expand Down
109 changes: 59 additions & 50 deletions src/card/CardThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
clearSelection,
updateAllSelections,
updateReportDatabase,
updateSchema,
} from './CardActions';
import { createNotificationThunk } from '../page/PageThunks';
import { getReportTypes } from '../extensions/ExtensionUtils';
Expand Down Expand Up @@ -67,68 +68,76 @@ export const updateReportTypeThunk = (id, type) => (dispatch: any, getState: any

dispatch(updateReportType(pagenumber, id, type));
dispatch(updateFields(pagenumber, id, []));
dispatch(updateSchema(pagenumber, id, []));
dispatch(clearSelection(pagenumber, id));
} catch (e) {
dispatch(createNotificationThunk('Cannot update report type', e));
}
};

export const updateFieldsThunk = (id, fields) => (dispatch: any, getState: any) => {
try {
const state = getState();
const { pagenumber } = state.dashboard.settings;
const extensions = Object.fromEntries(Object.entries(state.dashboard.extensions).filter(([_, v]) => v.active));
const oldReport = state.dashboard.pages[pagenumber].reports.find((o) => o.id === id);
export const updateFieldsThunk =
(id, fields, schema = false) =>
(dispatch: any, getState: any) => {
try {
const state = getState();
const { pagenumber } = state.dashboard.settings;
const extensions = Object.fromEntries(Object.entries(state.dashboard.extensions).filter(([_, v]) => v.active));
const oldReport = state.dashboard.pages[pagenumber].reports.find((o) => o.id === id);

if (!oldReport) {
return;
}
const oldFields = oldReport.fields;
const reportType = oldReport.type;
const oldSelection = oldReport.selection;
const reportTypes = getReportTypes(extensions);
const selectableFields = reportTypes[reportType].selection; // The dictionary of selectable fields as defined in the config.
const { autoAssignSelectedProperties } = reportTypes[reportType];
const selectables = selectableFields ? Object.keys(selectableFields) : [];
if (!oldReport) {
return;
}
const oldFields = schema ? oldReport.schema : oldReport.fields;
const reportType = oldReport.type;
const oldSelection = oldReport.selection;
const reportTypes = getReportTypes(extensions);
const selectableFields = reportTypes[reportType].selection; // The dictionary of selectable fields as defined in the config.
const { autoAssignSelectedProperties } = reportTypes[reportType];
const selectables = selectableFields ? Object.keys(selectableFields) : [];

// If the new set of fields is not equal to the current set of fields, we ned to update the field selection.
if (!isEqual(oldFields, fields) || Object.keys(oldSelection).length === 0) {
selectables.forEach((selection, i) => {
if (fields.includes(oldSelection[selection])) {
// If the current selection is still present in the new set of fields, no need to reset.
// Also we ignore this on a node property selector.
/* continue */
} else if (selectableFields[selection].optional) {
// If the fields change, always set optional selections to none.
if (selectableFields[selection].multiple) {
dispatch(updateSelection(pagenumber, id, selection, ['(none)']));
} else {
dispatch(updateSelection(pagenumber, id, selection, '(none)'));
}
} else if (fields.length > 0) {
// For multi selections, select the Nth item of the result fields as a single item array.
if (selectableFields[selection].multiple) {
// only update if the old selection no longer covers the new set of fields...
if (!oldSelection[selection] || !oldSelection[selection].every((v) => fields.includes(v))) {
dispatch(updateSelection(pagenumber, id, selection, [fields[Math.min(i, fields.length - 1)]]));
// If the new set of fields is not equal to the current set of fields, we ned to update the field selection.
if (!isEqual(oldFields, fields) || Object.keys(oldSelection).length === 0) {
selectables.forEach((selection, i) => {
if (fields.includes(oldSelection[selection])) {
// If the current selection is still present in the new set of fields, no need to reset.
// Also we ignore this on a node property selector.
/* continue */
} else if (selectableFields[selection].optional) {
// If the fields change, always set optional selections to none.
if (selectableFields[selection].multiple) {
dispatch(updateSelection(pagenumber, id, selection, ['(none)']));
} else {
dispatch(updateSelection(pagenumber, id, selection, '(none)'));
}
} else if (fields.length > 0) {
// For multi selections, select the Nth item of the result fields as a single item array.
if (selectableFields[selection].multiple) {
// only update if the old selection no longer covers the new set of fields...
if (!oldSelection[selection] || !oldSelection[selection].every((v) => fields.includes(v))) {
dispatch(updateSelection(pagenumber, id, selection, [fields[Math.min(i, fields.length - 1)]]));
}
} else if (selectableFields[selection].type == SELECTION_TYPES.NODE_PROPERTIES) {
// For node property selections, select the most obvious properties of the node to display.
const selection = getSelectionBasedOnFields(fields, oldSelection, autoAssignSelectedProperties);
dispatch(updateAllSelections(pagenumber, id, selection));
} else {
// Else, default the selection to the Nth item of the result set fields.
dispatch(updateSelection(pagenumber, id, selection, fields[Math.min(i, fields.length - 1)]));
}
} else if (selectableFields[selection].type == SELECTION_TYPES.NODE_PROPERTIES) {
// For node property selections, select the most obvious properties of the node to display.
const selection = getSelectionBasedOnFields(fields, oldSelection, autoAssignSelectedProperties);
dispatch(updateAllSelections(pagenumber, id, selection));
} else {
// Else, default the selection to the Nth item of the result set fields.
dispatch(updateSelection(pagenumber, id, selection, fields[Math.min(i, fields.length - 1)]));
}
});
// Set the new set of fields for the report so that we may select them.

if (schema) {
dispatch(updateSchema(pagenumber, id, fields));
} else {
dispatch(updateFields(pagenumber, id, fields));
}
});
// Set the new set of fields for the report so that we may select them.
dispatch(updateFields(pagenumber, id, fields));
}
} catch (e) {
dispatch(createNotificationThunk('Cannot update report fields', e));
}
} catch (e) {
dispatch(createNotificationThunk('Cannot update report fields', e));
}
};
};

export const updateSelectionThunk = (id, selectable, field) => (dispatch: any, getState: any) => {
try {
Expand Down
2 changes: 2 additions & 0 deletions src/card/settings/CardSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const NeoCardSettings = ({
reportSettings,
reportSettingsOpen,
fields,
schema,
heightPx,
extensions, // A set of enabled extensions.
onQueryUpdate,
Expand Down Expand Up @@ -78,6 +79,7 @@ const NeoCardSettings = ({
<NeoCardSettingsFooter
type={type}
fields={fields}
schema={schema}
extensions={extensions}
reportSettings={reportSettings}
reportSettingsOpen={reportSettingsOpen}
Expand Down
2 changes: 2 additions & 0 deletions src/card/settings/CardSettingsFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const update = (state, mutations) => Object.assign({}, state, mutations);
const NeoCardSettingsFooter = ({
type,
fields,
schema,
reportSettings,
reportSettingsOpen,
extensions,
Expand Down Expand Up @@ -124,6 +125,7 @@ const NeoCardSettingsFooter = ({
settingValue={reportSettings[settingToCustomize]}
type={type}
fields={fields}
schema={schema}
customReportStyleModalOpen={customReportStyleModalOpen}
setCustomReportStyleModalOpen={setCustomReportStyleModalOpen}
onReportSettingUpdate={onReportSettingUpdate}
Expand Down
5 changes: 5 additions & 0 deletions src/chart/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,8 @@ export const rgbaToHex = (color: string): string => {
}
return color;
};

export enum EntityType {
Node,
Relationship,
}
11 changes: 8 additions & 3 deletions src/chart/graph/util/RecordUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { evaluateRulesOnNode } from '../../../extensions/styling/StyleRuleEvaluator';
import { evaluateRulesOnNode, evaluateRulesOnLink } from '../../../extensions/styling/StyleRuleEvaluator';
import { extractNodePropertiesFromRecords, mergeNodePropsFieldsLists } from '../../../report/ReportRecordProcessing';
import { valueIsArray, valueIsNode, valueIsRelationship, valueIsPath } from '../../ChartUtils';
import { GraphChartVisualizationProps } from '../GraphChartVisualization';
Expand Down Expand Up @@ -162,10 +162,15 @@ export function buildGraphVisualizationObjectFromRecords(
);
});
});
// Assign proper curvatures to relationships.
// This is needed for pairs of nodes that have multiple relationships between them, or self-loops.
// Assign proper curvatures and colors to relationships.
// Assigning curvature is needed for pairs of nodes that have multiple relationships between them, or self-loops.
const linksList = Object.values(links).map((linkArray) => {
return linkArray.map((link, i) => {
let defaultColor = link.color;

// Assign color from json based on style rule evaluation if specified
let evaluatedColor = evaluateRulesOnLink(link, 'relationship color', defaultColor, styleRules);
link.color = evaluatedColor;
const mirroredNodePair = links[`${link.target},${link.source}`];
return assignCurvatureToLink(link, i, linkArray.length, mirroredNodePair ? mirroredNodePair.length : 0);
});
Expand Down
14 changes: 10 additions & 4 deletions src/extensions/styling/StyleRuleCreationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export const RULE_BASED_REPORT_CUSTOMIZATIONS = {
value: 'node label color',
label: 'Node Label Color',
},
{
value: 'relationship color',
label: 'Relationship Color',
on: 'relationship',
},
],
map: [
{
Expand Down Expand Up @@ -124,6 +129,7 @@ export const NeoCustomReportStyleModal = ({
settingValue,
type,
fields,
schema,
setCustomReportStyleModalOpen,
onReportSettingUpdate,
}) => {
Expand Down Expand Up @@ -157,20 +163,20 @@ export const NeoCustomReportStyleModal = ({
* This will be dynamic based on the type of report we are customizing.
*/
const createFieldVariableSuggestions = () => {
if (!fields) {
if (!schema) {
return [];
}
if (type == 'graph' || type == 'map') {
return fields
return schema
.map((node, index) => {
if (!Array.isArray(node)) {
return undefined;
}
return fields[index].map((property, propertyIndex) => {
return schema[index].map((property, propertyIndex) => {
if (propertyIndex == 0) {
return undefined;
}
return `${fields[index][0]}.${property}`;
return `${schema[index][0]}.${property}`;
});
})
.flat()
Expand Down
24 changes: 18 additions & 6 deletions src/extensions/styling/StyleRuleEvaluator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { makeStyles } from '@mui/styles';
import { extensionEnabled } from '../ExtensionUtils';
import React, { useEffect } from 'react';
import { EntityType } from '../../chart/Utils';

/**
* Evaluates the specified rule set on a row returned by the Neo4j driver.
Expand Down Expand Up @@ -88,17 +87,30 @@ export const evaluateRulesOnDict = (dict, rules, customizations) => {
* @returns a user-defined value if a rule is met, or the default value if none are.
*/
export const evaluateRulesOnNode = (node, customization, defaultValue, rules) => {
if (!node || !customization || !rules) {
return evaluateRules(node, customization, defaultValue, rules, EntityType.Node);
};

export const evaluateRulesOnLink = (link, customization, defaultValue, rules) => {
return evaluateRules(link, customization, defaultValue, rules, EntityType.Relationship);
};

export const evaluateRules = (entity, customization, defaultValue, rules, entityType) => {
if (!entity || !customization || !rules) {
return defaultValue;
}

for (const [index, rule] of rules.entries()) {
// Only look at rules relevant to the target customization.
if (rule.customization == customization) {
// if the row contains the specified field...
const label = rule.field.split('.')[0];
const typeOrLabel = rule.field.split('.')[0];
const property = rule.field.split('.')[1];
if (node.labels.includes(label)) {
const realValue = node.properties[property] ? node.properties[property] : '';

if (
(entityType === EntityType.Node && entity.labels.includes(typeOrLabel)) ||
(entityType === EntityType.Relationship && entity.type == typeOrLabel)
) {
const realValue = entity?.properties?.[property] || '';
const ruleValue = rule.value;
if (evaluateCondition(realValue, rule.condition, ruleValue)) {
return rule.customizationValue;
Expand Down
20 changes: 17 additions & 3 deletions src/report/Report.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { EXTENSIONS } from '../extensions/ExtensionConfig';
import { getPageNumber } from '../settings/SettingsSelectors';
import { getPrepopulateReportExtension } from '../extensions/state/ExtensionSelectors';
import { deleteSessionStoragePrepopulationReportFunction } from '../extensions/state/ExtensionActions';
import { updateFieldsThunk } from '../card/CardThunks';

const DEFAULT_LOADING_ICON = <LoadingSpinner size='large' className='centered' style={{ marginTop: '-30px' }} />;

Expand All @@ -36,6 +37,7 @@ export const NeoReport = ({
setFields = (f) => {
fields = f;
}, // The callback to update the set of query fields after query execution.
setSchema,
setGlobalParameter = () => {}, // callback to update global (dashboard) parameters.
getGlobalParameter = (_: string) => {
return '';
Expand Down Expand Up @@ -118,7 +120,10 @@ export const NeoReport = ({
useNodePropsAsFields,
useReturnValuesAsFields,
HARD_ROW_LIMITING,
queryTimeLimit
queryTimeLimit,
(schema) => {
setSchema(id, schema);
}
);
} else {
runCypherQuery(
Expand All @@ -134,7 +139,10 @@ export const NeoReport = ({
useNodePropsAsFields,
useReturnValuesAsFields,
HARD_ROW_LIMITING,
queryTimeLimit
queryTimeLimit,
(schema) => {
setSchema(id, schema);
}
);
}
};
Expand Down Expand Up @@ -206,7 +214,10 @@ export const NeoReport = ({
false,
false,
HARD_ROW_LIMITING,
queryTimeLimit
queryTimeLimit,
(schema) => {
setSchema(id, schema);
}
);
},
[database]
Expand Down Expand Up @@ -330,6 +341,9 @@ const mapDispatchToProps = (dispatch) => ({
getCustomDispatcher: () => {
return dispatch;
},
setSchema: (id: any, schema: any) => {
dispatch(updateFieldsThunk(id, schema, true));
},
});

export default connect(mapStateToProps, mapDispatchToProps)(NeoReport);
Loading