Skip to content

Commit

Permalink
Expand cleanup & progress UI (#463)
Browse files Browse the repository at this point in the history
* Move useUpdateNodeCounts to new file

* Rename to be more clear

* Add hook useUpdateNodeCountsQuery

* Better props for LoadingSpinner

* Fix label of show more button

* Add progress and error UI to sidebar

* Update changelog
  • Loading branch information
kmcginnes authored Jun 26, 2024
1 parent a9eee23 commit bfd1e56
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 186 deletions.
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

0 comments on commit bfd1e56

Please sign in to comment.