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

Expand cleanup & progress UI #463

Merged
merged 7 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
(<https://github.com/aws/graph-explorer/pull/434>)
- Added caching and retries to node expansion and neighbor counts
(<https://github.com/aws/graph-explorer/pull/434>)
- Added progress and error UI for neighbor counts
(<https://github.com/aws/graph-explorer/pull/463>)
- Improved scrolling behavior in expand sidebar
(<https://github.com/aws/graph-explorer/pull/436>)
- Add node expansion limit per connection
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
import { cx } from "@emotion/css";
import type { CSSProperties, ReactNode } from "react";
import type { ReactNode } from "react";
import { useWithTheme } from "../../core";
import { LoaderIcon } from "../icons";

import defaultStyles from "./LoadingSpinner.styles";

export type LoadingSpinnerProps = {
style?: CSSProperties;
export interface LoadingSpinnerProps
extends React.HTMLAttributes<HTMLDivElement> {
color?: string;
className?: string;
loadingIcon?: ReactNode;
};
}

export const LoadingSpinner = ({
style,
color,
className,
loadingIcon,
...props
}: LoadingSpinnerProps) => {
const themedStyle = useWithTheme();
return (
<div
className={cx(themedStyle(defaultStyles(color)), className)}
style={style}
{...props}
>
<div>{loadingIcon || <LoaderIcon />}</div>
</div>
Expand Down
98 changes: 3 additions & 95 deletions packages/graph-explorer/src/hooks/useExpandNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import type {
import { activeConnectionSelector, explorerSelector } from "../core/connector";
import useEntities from "./useEntities";
import { useRecoilValue } from "recoil";
import { useMutation, useQueries } from "@tanstack/react-query";
import { neighborsCountQuery } from "../connector/queries";
import { useMutation } from "@tanstack/react-query";
import useDisplayNames from "./useDisplayNames";
import { Vertex } from "../@types/entities";
import { useUpdateAllNodeCounts } from "./useUpdateNodeCounts";

/*

Expand Down Expand Up @@ -47,7 +47,7 @@ const ExpandNodeContext = createContext<ExpandNodeContextType | null>(null);

export function ExpandNodeProvider(props: PropsWithChildren) {
// Wires up node count query in response to new nodes in the graph
useUpdateNodeCounts();
useUpdateAllNodeCounts();

const explorer = useRecoilValue(explorerSelector);
const [_, setEntities] = useEntities();
Expand Down Expand Up @@ -153,98 +153,6 @@ export function ExpandNodeProvider(props: PropsWithChildren) {
);
}

/**
* Hook that watches nodes added to the graph and queries the database for
* neighbor counts. There should be only one instance of this hook in the render
* pipeline since it uses effects for progress and error notifications.
*/
function useUpdateNodeCounts() {
const [entities, setEntities] = useEntities();
const connection = useRecoilValue(activeConnectionSelector);
const explorer = useRecoilValue(explorerSelector);
const { enqueueNotification, clearNotification } = useNotification();

const nodeIds = [...new Set(entities.nodes.map(n => n.data.id))];

const query = useQueries({
queries: nodeIds.map(id =>
neighborsCountQuery(id, connection?.nodeExpansionLimit, explorer)
),
combine: results => {
// Combines data with existing node data and filters out undefined
const data = results
.flatMap(result => (result.data ? [result.data] : []))
.map(data => {
const prevNode = entities.nodes.find(n => n.data.id === data.nodeId);
const node: Vertex | undefined = prevNode
? {
...prevNode,
data: {
...prevNode.data,
neighborsCount: data.totalCount,
neighborsCountByType: data.counts,
},
}
: undefined;
return node;
});

return {
data: data,
pending: results.some(result => result.isPending),
errors: results.map(result => result.error),
hasErrors: results.some(result => result.isError),
};
},
});

// Update the graph with the node counts from the query results
useEffect(() => {
// Ensure we have expanded and finished all count queries
if (query.pending) {
return;
}

// Update node graph with counts
setEntities(prev => ({
nodes: prev.nodes.map(node => {
const nodeWithCounts = query.data.find(
n => n?.data.id === node.data.id
);

return nodeWithCounts ?? node;
}),
edges: prev.edges,
}));
}, [query.data, query.pending, setEntities]);

// Show loading notification
useEffect(() => {
if (!query.pending) {
return;
}
const notificationId = enqueueNotification({
title: "Updating Neighbors",
message: `Updating neighbor counts for new nodes`,
autoHideDuration: null,
});
return () => clearNotification(notificationId);
}, [clearNotification, query.pending, enqueueNotification]);

// Show error notification
useEffect(() => {
if (query.pending || !query.hasErrors) {
return;
}
const notificationId = enqueueNotification({
title: "Some Errors Occurred",
message: `While requesting counts for neighboring nodes, some errors occurred.`,
type: "error",
});
return () => clearNotification(notificationId);
}, [clearNotification, query.pending, query.hasErrors, enqueueNotification]);
}

/**
* Provides a callback to submit a node expansion request, the query
* information, and a callback to reset the request state.
Expand Down
109 changes: 109 additions & 0 deletions packages/graph-explorer/src/hooks/useUpdateNodeCounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { useQueries, useQuery } from "@tanstack/react-query";
import { useEffect } from "react";
import { useRecoilValue } from "recoil";
import { Vertex } from "../@types/entities";
import { useNotification } from "../components/NotificationProvider";
import { neighborsCountQuery } from "../connector/queries";
import { activeConnectionSelector, explorerSelector } from "../core/connector";
import useEntities from "./useEntities";
import { VertexId } from "../connector/useGEFetchTypes";

export function useUpdateNodeCountsQuery(nodeId: VertexId) {
const connection = useRecoilValue(activeConnectionSelector);
const explorer = useRecoilValue(explorerSelector);
return useQuery(
neighborsCountQuery(nodeId, connection?.nodeExpansionLimit, explorer)
);
}

/**
* Hook that watches nodes added to the graph and queries the database for
* neighbor counts. There should be only one instance of this hook in the render
* pipeline since it uses effects for progress and error notifications.
*/
export function useUpdateAllNodeCounts() {
const [entities, setEntities] = useEntities();
const connection = useRecoilValue(activeConnectionSelector);
const explorer = useRecoilValue(explorerSelector);
const { enqueueNotification, clearNotification } = useNotification();

const nodeIds = [...new Set(entities.nodes.map(n => n.data.id))];

const query = useQueries({
queries: nodeIds.map(id =>
neighborsCountQuery(id, connection?.nodeExpansionLimit, explorer)
),
combine: results => {
// Combines data with existing node data and filters out undefined
const data = results
.flatMap(result => (result.data ? [result.data] : []))
.map(data => {
const prevNode = entities.nodes.find(n => n.data.id === data.nodeId);
const node: Vertex | undefined = prevNode
? {
...prevNode,
data: {
...prevNode.data,
neighborsCount: data.totalCount,
neighborsCountByType: data.counts,
},
}
: undefined;
return node;
});

return {
data: data,
pending: results.some(result => result.isPending),
errors: results.map(result => result.error),
hasErrors: results.some(result => result.isError),
};
},
});

// Update the graph with the node counts from the query results
useEffect(() => {
// Ensure we have expanded and finished all count queries
if (query.pending) {
return;
}

// Update node graph with counts
setEntities(prev => ({
nodes: prev.nodes.map(node => {
const nodeWithCounts = query.data.find(
n => n?.data.id === node.data.id
);

return nodeWithCounts ?? node;
}),
edges: prev.edges,
}));
}, [query.data, query.pending, setEntities]);

// Show loading notification
useEffect(() => {
if (!query.pending) {
return;
}
const notificationId = enqueueNotification({
title: "Updating Neighbors",
message: `Updating neighbor counts for new nodes`,
autoHideDuration: null,
});
return () => clearNotification(notificationId);
}, [clearNotification, query.pending, enqueueNotification]);

// Show error notification
useEffect(() => {
if (query.pending || !query.hasErrors) {
return;
}
const notificationId = enqueueNotification({
title: "Some Errors Occurred",
message: `While requesting counts for neighboring nodes, some errors occurred.`,
type: "error",
});
return () => clearNotification(notificationId);
}, [clearNotification, query.pending, query.hasErrors, enqueueNotification]);
}
Loading
Loading