Skip to content

Commit

Permalink
Improve node and edge filter options (#289)
Browse files Browse the repository at this point in the history
  • Loading branch information
kmcginnes authored Apr 11, 2024
1 parent 2e9d1fd commit 361c4ba
Show file tree
Hide file tree
Showing 18 changed files with 462 additions and 113 deletions.
19 changes: 19 additions & 0 deletions packages/graph-explorer/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Jest current file",
"skipFiles": ["<node_internals>/**"],
"runtimeExecutable": "sh",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["${relativeFile}", "--coverage=false"],
"console": "integratedTerminal",
"internalConsoleOptions": "openOnFirstSessionStart"
}
]
}
1 change: 1 addition & 0 deletions packages/graph-explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"@babel/preset-env": "^7.23.2",
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.22.15",
"@jest/globals": "29.0.0",
"@react-aria/overlays": "3.9.1",
"@react-stately/radio": "^3.8.2",
"@react-types/button": "^3.7.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,42 +362,42 @@ export const defaultBadgeStyles =
}
&.${pfx}-placement-bottom-right {
bottom: -4px;
right: 0;
bottom: -6px;
right: -6px;
&.${pfx}-variant-undetermined {
bottom: 4px;
right: 2px;
bottom: 0;
right: 0;
}
}
&.${pfx}-placement-bottom-left {
bottom: -4px;
left: 0;
bottom: -6px;
left: -2px;
&.${pfx}-variant-undetermined {
bottom: 4px;
left: 2px;
bottom: 0;
left: 0;
}
}
&.${pfx}-placement-top-right {
top: -4px;
right: 0;
top: -6px;
right: -6px;
&.${pfx}-variant-undetermined {
top: 4px;
right: 2px;
top: 0;
right: 0;
}
}
&.${pfx}-placement-top-left {
top: -4px;
left: 0;
top: -6px;
left: -2px;
&.${pfx}-variant-undetermined {
top: 4px;
left: 2px;
top: 0;
left: 0;
}
}
}
Expand Down
25 changes: 5 additions & 20 deletions packages/graph-explorer/src/components/Sidebar/SidebarButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { cx } from "@emotion/css";
import type { ReactNode } from "react";
import { withClassNamePrefix } from "../../core";
import IconButton from "../IconButton";
import IconButton, { IconButtonProps } from "../IconButton";

export type NavItem = {
icon: ReactNode;
Expand All @@ -10,40 +10,25 @@ export type NavItem = {
};

export type SidebarButtonProps = {
tooltipText?: string;
icon: ReactNode;
onPress(): void;
classNamePrefix?: string;
className?: string;
active?: boolean;
};
} & Omit<IconButtonProps, "variant" | "rounded" | "tooltipPlacement">;

const SidebarButton = ({
tooltipText,
classNamePrefix = "ft",
className,
active,
icon,
onPress,
...props
}: SidebarButtonProps) => {
const pfx = withClassNamePrefix(classNamePrefix);

return (
<IconButton
tooltipText={tooltipText}
tooltipPlacement={"left-center"}
variant={"text"}
rounded={true}
classNamePrefix={classNamePrefix}
className={cx(
pfx("sidebar-button"),
{
[pfx("active")]: active,
},
className
)}
icon={icon}
onPress={onPress}
className={cx(pfx("sidebar-button"), active && pfx("active"), className)}
{...props}
/>
);
};
Expand Down
28 changes: 15 additions & 13 deletions packages/graph-explorer/src/core/ConfigurationProvider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,20 @@ export type ConnectionConfig = {
fetchTimeoutMs?: number;
};

export type Schema = {
totalVertices: number;
vertices: Array<VertexTypeConfig>;
totalEdges: number;
edges: Array<EdgeTypeConfig>;
lastUpdate?: Date;
triedToSync?: boolean;
lastSyncFail?: boolean;
/**
* List of RDF prefixes (only for SPARQL)
*/
prefixes?: Array<PrefixTypeConfig>;
};

export type RawConfiguration = {
/**
* Unique identifier for this config
Expand All @@ -166,19 +180,7 @@ export type RawConfiguration = {
/**
* Database schema: types, names, labels, icons, ...
*/
schema?: {
totalVertices: number;
vertices: Array<VertexTypeConfig>;
totalEdges: number;
edges: Array<EdgeTypeConfig>;
lastUpdate?: Date;
triedToSync?: boolean;
lastSyncFail?: boolean;
/**
* List of RDF prefixes (only for SPARQL)
*/
prefixes?: Array<PrefixTypeConfig>;
};
schema?: Schema;
/**
* Mark as created from a file
*/
Expand Down
18 changes: 18 additions & 0 deletions packages/graph-explorer/src/core/StateProvider/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,21 @@ const mergeEdge = (
attributes,
};
};

/** Same as `useConfig().vertexTypes */
export const vertexTypesSelector = selector({
key: "config-vertex-types",
get: ({ get }) => {
const configuration = get(mergedConfigurationSelector);
return configuration?.schema?.vertices?.map(vt => vt.type) || [];
},
});

/** Same as `useConfig().edgeTypes */
export const edgeTypesSelector = selector({
key: "config-edge-types",
get: ({ get }) => {
const configuration = get(mergedConfigurationSelector);
return configuration?.schema?.edges?.map(vt => vt.type) || [];
},
});
18 changes: 18 additions & 0 deletions packages/graph-explorer/src/core/StateProvider/filterCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { selector } from "recoil";
import { edgesTypesFilteredAtom } from "./edges";
import { nodesTypesFilteredAtom } from "./nodes";

/**
* The count of filtered node and edge types
*/
export const totalFilteredCount = selector({
key: "nodes-and-edges-filtered-count",
get: ({ get }) => {
// Get all the filtered entities
const filteredNodeTypes = get(nodesTypesFilteredAtom);
const filteredEdgeTypes = get(edgesTypesFilteredAtom);

// Determine how many entity types are not checked
return filteredNodeTypes.size + filteredEdgeTypes.size;
},
});
1 change: 0 additions & 1 deletion packages/graph-explorer/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from "./ConfigurationProvider";
export * from "./ThemeProvider";
export * from "./ConnectedProvider";
4 changes: 2 additions & 2 deletions packages/graph-explorer/src/hooks/useEntities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,12 @@ const useEntities = ({ disableFilters }: { disableFilters?: boolean } = {}): [
let filteredEdges = edges;
if (!disableFilters) {
filteredNodes = nodes.filter(node => {
return vertexTypes.has(node.data.type);
return vertexTypes.has(node.data.type) === false;
});

filteredEdges = edges.filter(edge => {
return (
connectionTypes.has(edge.data.type) &&
connectionTypes.has(edge.data.type) === false &&
filteredNodes.some(node => node.data.id === edge.data.source) &&
filteredNodes.some(node => node.data.id === edge.data.target)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { expect, jest } from "@jest/globals";
import { act, renderHook } from "@testing-library/react-hooks";
import useFiltersConfig from "./useFiltersConfig";
import { useTestSchema } from "../../utils/testing/useTestSchema";
import { createRandomSchema } from "../../utils/testing/randomData";
import { TestableRootProviders } from "../../utils/testing/TestableRootProviders";
import { sample, sortBy } from "lodash";
import { Schema } from "../../core";

jest.mock("localforage", () => ({
config: jest.fn(),
getItem: jest.fn(),
setItem: jest.fn(),
}));

/** Creates a config with the schema and makes it active, then renders the `useFiltersConfig` hook. */
function renderFilterConfigHook(schema: Schema) {
return renderHook(
() => {
useTestSchema(schema);
return useFiltersConfig();
},
{
wrapper: TestableRootProviders,
}
);
}

describe("useFiltersConfig", () => {
beforeEach(() => {
jest.resetAllMocks();
});

it("should have all entities selected", () => {
const schema = createRandomSchema();

const { result } = renderFilterConfigHook(schema);

expect(result.current.selectedVertexTypes).toEqual(
new Set(schema.vertices.map(v => v.type))
);
expect(result.current.selectedConnectionTypes).toEqual(
new Set(schema.edges.map(v => v.type))
);
});

it("should have all vertices in checkboxes", () => {
const schema = createRandomSchema();
const expectedCheckboxIds = schema.vertices.map(v => v.type);

const { result } = renderFilterConfigHook(schema);

expect(result.current.vertexTypes.map(vt => vt.id)).toEqual(
expect.arrayContaining(expectedCheckboxIds)
);
});

it("should sort vertex checkboxes alphabetically", () => {
const schema = createRandomSchema();

const { result } = renderFilterConfigHook(schema);

expect(result.current.vertexTypes).toEqual(
sortBy(result.current.vertexTypes, vt => vt.text)
);
});

it("should have all edges in checkboxes", () => {
const schema = createRandomSchema();
const expectedCheckboxIds = schema.edges.map(v => v.type);

const { result } = renderFilterConfigHook(schema);

expect(result.current.connectionTypes.map(vt => vt.id)).toEqual(
expect.arrayContaining(expectedCheckboxIds)
);
});

it("should sort edge checkboxes alphabetically", () => {
const schema = createRandomSchema();

const { result } = renderFilterConfigHook(schema);

expect(result.current.connectionTypes).toEqual(
sortBy(result.current.connectionTypes, vt => vt.text)
);
});

it("should unselect vertex when toggled", () => {
const schema = createRandomSchema();
const changingVertex = sample(schema.vertices)!;

const { result } = renderFilterConfigHook(schema);

// Ensure vertex is selected initially
expect(result.current.selectedVertexTypes.has(changingVertex.type)).toEqual(
true
);

// Deselect vertex
act(() => {
result.current.onChangeVertexTypes(changingVertex.type, false);
});

// Ensure vertex is no longer selected
expect(result.current.selectedVertexTypes.has(changingVertex.type)).toEqual(
false
);
});

it("should unselect edge when toggled", () => {
const schema = createRandomSchema();
const changingEdge = sample(schema.edges)!;

const { result } = renderFilterConfigHook(schema);

// Ensure edge is selected initially
expect(
result.current.selectedConnectionTypes.has(changingEdge.type)
).toEqual(true);

// Deselect edge
act(() => {
result.current.onChangeConnectionTypes(changingEdge.type, false);
});

// Ensure edge is no longer selected
expect(
result.current.selectedConnectionTypes.has(changingEdge.type)
).toEqual(false);
});
});
Loading

0 comments on commit 361c4ba

Please sign in to comment.