Skip to content

Commit

Permalink
Fix edge type display (#716)
Browse files Browse the repository at this point in the history
  • Loading branch information
kmcginnes authored Dec 10, 2024
1 parent 897dd38 commit b0f8ce2
Show file tree
Hide file tree
Showing 13 changed files with 325 additions and 19 deletions.
5 changes: 5 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# graph-explorer Change Log

## Release 1.12.1

- **Fixed** issue where the edge's display name value was not being displayed
properly ([#716](https://github.com/aws/graph-explorer/pull/716))

## Release 1.12.0

This release is mostly a maintenance release, with a few new features and bug
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "graph-explorer",
"version": "1.12.0",
"version": "1.12.1",
"description": "Graph Explorer",
"author": "amazon",
"license": "Apache-2.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/graph-explorer-proxy-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "graph-explorer-proxy-server",
"version": "1.12.0",
"version": "1.12.1",
"description": "Server to facilitate communication between the browser and the supported graph database.",
"main": "dist/node-server.js",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/graph-explorer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "graph-explorer",
"version": "1.12.0",
"version": "1.12.1",
"description": "Graph Explorer",
"engines": {
"node": ">=22.11.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,24 @@ export const vertexTypeAttributesSelector = selectorFamily({
},
});

export const edgeTypeAttributesSelector = selectorFamily({
key: "edge-type-attributes",
get:
(edgeTypes: string[]) =>
({ get }) => {
const attributesByNameMap = new Map(
edgeTypes
.values()
.map(et => get(edgeTypeConfigSelector(et)))
.filter(et => et != null)
.flatMap(et => et.attributes)
.map(attr => [attr.name, attr])
);

return attributesByNameMap.values().toArray();
},
});

export const vertexTypeConfigSelector = selectorFamily({
key: "vertex-type-config",
get:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { RawConfiguration, VertexTypeConfig } from "../ConfigurationProvider";
import { SchemaInference } from "./schema";
import { UserStyling } from "./userPreferences";
import { createRandomName } from "@shared/utils/testing";
import { RESERVED_TYPES_PROPERTY } from "@/utils";

describe("mergedConfiguration", () => {
it("should produce empty defaults when empty object is passed", () => {
Expand Down Expand Up @@ -231,6 +232,32 @@ describe("mergedConfiguration", () => {

expect(actualEtConfig?.displayLabel).toEqual(customDisplayLabel);
});

it("should patch displayNameAttribute to be 'types' when it was 'type'", () => {
const etConfig = createRandomEdgeTypeConfig();

const config: RawConfiguration = createRandomRawConfiguration();
const styling: UserStyling = {
edges: [
{
type: etConfig.type,
displayNameAttribute: "type",
},
],
};
const schema = createRandomSchema();
schema.edges = [etConfig];

const result = mergeConfiguration(schema, config, styling);

const actualEtConfig = result.schema?.edges.find(
e => e.type === etConfig.type
);

expect(actualEtConfig?.displayNameAttribute).toEqual(
RESERVED_TYPES_PROPERTY
);
});
});

/** Sorts the configs by type name */
Expand Down
12 changes: 11 additions & 1 deletion packages/graph-explorer/src/core/StateProvider/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ const mergeEdge = (
const et =
preferences?.type || configEdge?.type || schemaEdge?.type || "unknown";

return {
const config: EdgeTypeConfig = {
// Defaults
...getDefaultEdgeTypeConfig(et),
// Automatic schema override
Expand All @@ -209,6 +209,15 @@ const mergeEdge = (
...(preferences || {}),
attributes,
};

if (config.displayNameAttribute === "type") {
// Patch displayNameAttribute to be "types" when it was "type" ensuring
// backwards compatibility if the user had customized the
// displayNameAttribute to be the edge type prior to this release.
config.displayNameAttribute = RESERVED_TYPES_PROPERTY;
}

return config;
};

export const allVertexTypeConfigsSelector = selector({
Expand Down Expand Up @@ -267,6 +276,7 @@ export const defaultEdgeTypeConfig = {
targetArrowStyle: "triangle",
lineStyle: "solid",
lineColor: "#b3b3b3",
displayNameAttribute: RESERVED_TYPES_PROPERTY,
} satisfies Omit<EdgeTypeConfig, "type">;

export function getDefaultEdgeTypeConfig(edgeType: string): EdgeTypeConfig {
Expand Down
241 changes: 241 additions & 0 deletions packages/graph-explorer/src/core/StateProvider/displayEdge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import { Edge } from "@/@types/entities";
import {
createRandomEdge,
createRandomEdgeTypeConfig,
createRandomRawConfiguration,
createRandomSchema,
createRandomVertex,
renderHookWithRecoilRoot,
} from "@/utils/testing";
import { useDisplayEdgeFromEdge } from "./displayEdge";
import { formatDate, sanitizeText } from "@/utils";
import { createRandomDate } from "@shared/utils/testing";
import { DisplayAttribute } from "./displayAttribute";
import { mapToDisplayEdgeTypeConfig } from "./displayTypeConfigs";
import {
activeConfigurationAtom,
configurationAtom,
getDefaultEdgeTypeConfig,
} from "./configuration";
import { Schema } from "../ConfigurationProvider";
import { MutableSnapshot } from "recoil";
import { schemaAtom } from "./schema";
import { ConnectionConfig } from "@shared/types";

describe("useDisplayEdgeFromEdge", () => {
it("should keep the same ID", () => {
const edge = createEdge();
expect(act(edge).id).toEqual(edge.id);
});

it("should be an edge", () => {
const edge = createEdge();
expect(act(edge).entityType).toEqual("edge");
});

it("should have a display ID equal to the edge ID", () => {
const edge = createEdge();
expect(act(edge).displayId).toEqual(edge.id);
});

it("should have the display name be the types", () => {
const edge = createEdge();
expect(act(edge).displayName).toEqual(sanitizeText(edge.type));
});

it("should have display name that matches the attribute value", () => {
const edge = createEdge();
const schema = createRandomSchema();
// Get the first attribute
const attribute = Object.entries(edge.attributes).map(([name, value]) => ({
name,
value,
}))[0];

const etConfig = createRandomEdgeTypeConfig();
etConfig.type = edge.type;
etConfig.displayNameAttribute = attribute.name;
schema.edges.push(etConfig);

expect(
act(edge, withSchemaAndConnection(schema, "gremlin")).displayName
).toEqual(`${attribute.value}`);
});

it("should have display name that matches the types when displayNameAttribute is 'type'", () => {
const edge = createEdge();
const schema = createRandomSchema();

const etConfig = createRandomEdgeTypeConfig();
delete etConfig.displayLabel;
etConfig.type = edge.type;
etConfig.displayNameAttribute = "type";
schema.edges.push(etConfig);

expect(
act(edge, withSchemaAndConnection(schema, "gremlin")).displayName
).toEqual(sanitizeText(edge.type));
});

it("should have the default type config when edge type is not in the schema", () => {
const edge = createEdge();
const etConfig = getDefaultEdgeTypeConfig(edge.type);
const displayConfig = mapToDisplayEdgeTypeConfig(etConfig);
expect(act(edge).typeConfig).toEqual(displayConfig);
});

it("should use the type config from the merged schema", () => {
const edge = createEdge();
const etConfig = createRandomEdgeTypeConfig();
etConfig.type = edge.type;
const schema = createRandomSchema();
schema.edges.push(etConfig);

const expectedTypeConfig = mapToDisplayEdgeTypeConfig(etConfig);

expect(
act(edge, withSchemaAndConnection(schema, "gremlin")).typeConfig
).toEqual(expectedTypeConfig);
});

it("should have display types that list all types in gremlin", () => {
const edge = createEdge();
const schema = createRandomSchema();

const etConfig = createRandomEdgeTypeConfig();
delete etConfig.displayLabel;
etConfig.type = edge.type;
schema.edges.push(etConfig);

edge.type = etConfig.type;

expect(
act(edge, withSchemaAndConnection(schema, "gremlin")).displayTypes
).toEqual(`${sanitizeText(etConfig.type)}`);
});

it("should have display types that list all types in sparql", () => {
const edge = createEdge();
edge.type = "http://www.example.com/class#bar";
const schema = createRandomSchema();
schema.prefixes = [
{
prefix: "example-class",
uri: "http://www.example.com/class#",
},
];

const etConfig = createRandomEdgeTypeConfig();
delete etConfig.displayLabel;
etConfig.type = edge.type;
schema.edges.push(etConfig);

edge.type = etConfig.type;

expect(
act(edge, withSchemaAndConnection(schema, "sparql")).displayTypes
).toEqual(`example-class:bar`);
});

it("should have sorted attributes", () => {
const edge = createEdge();
const attributes: DisplayAttribute[] = Object.entries(edge.attributes)
.map(([key, value]) => ({
name: key,
displayLabel: sanitizeText(key),
displayValue: String(value),
}))
.toSorted((a, b) => a.displayLabel.localeCompare(b.displayLabel));

expect(act(edge).attributes).toEqual(attributes);
});

it("should format date values in attribute when type is Date", () => {
const edge = createEdge();
const schema = createRandomSchema();
const etConfig = createRandomEdgeTypeConfig();
etConfig.type = edge.type;
etConfig.attributes.push({
name: "created",
displayLabel: sanitizeText("created"),
dataType: "Date",
});
schema.edges.push(etConfig);

edge.attributes = {
...edge.attributes,
created: createRandomDate().toISOString(),
};

const actualAttribute = act(edge, withSchema(schema)).attributes.find(
attr => attr.name === "created"
);
expect(actualAttribute?.displayValue).toEqual(
formatDate(new Date(edge.attributes.created))
);
});

it("should format date values in attribute when type is g:Date", () => {
const edge = createEdge();
const schema = createRandomSchema();
const etConfig = createRandomEdgeTypeConfig();
etConfig.type = edge.type;
etConfig.attributes.push({
name: "created",
displayLabel: sanitizeText("created"),
dataType: "g:Date",
});
schema.edges.push(etConfig);

edge.attributes = {
...edge.attributes,
created: createRandomDate().toISOString(),
};

const actualAttribute = act(edge, withSchema(schema)).attributes.find(
attr => attr.name === "created"
);
expect(actualAttribute?.displayValue).toEqual(
formatDate(new Date(edge.attributes.created))
);
});

// Helpers

function createEdge() {
return createRandomEdge(createRandomVertex(), createRandomVertex());
}

function act(
edge: Edge,
initializeState?: (mutableSnapshot: MutableSnapshot) => void
) {
const { result } = renderHookWithRecoilRoot(
() => useDisplayEdgeFromEdge(edge),
initializeState
);
return result.current;
}

function withSchema(schema: Schema) {
const config = createRandomRawConfiguration();
return (snapshot: MutableSnapshot) => {
snapshot.set(configurationAtom, new Map([[config.id, config]]));
snapshot.set(schemaAtom, new Map([[config.id, schema]]));
snapshot.set(activeConfigurationAtom, config.id);
};
}

function withSchemaAndConnection(
schema: Schema,
queryEngine: ConnectionConfig["queryEngine"]
) {
const config = createRandomRawConfiguration();
config.connection!.queryEngine = queryEngine;
return (snapshot: MutableSnapshot) => {
snapshot.set(configurationAtom, new Map([[config.id, config]]));
snapshot.set(schemaAtom, new Map([[config.id, schema]]));
snapshot.set(activeConfigurationAtom, config.id);
};
}
});
Loading

0 comments on commit b0f8ce2

Please sign in to comment.