Skip to content

Commit

Permalink
Merge pull request #46 from thefrontside/pc/virtualized-infinite-scroll
Browse files Browse the repository at this point in the history
Virtualized infinite-scroll
  • Loading branch information
taras authored Oct 4, 2022
2 parents a59dc99 + b2d1610 commit ab83bb6
Show file tree
Hide file tree
Showing 20 changed files with 354 additions and 94 deletions.
3 changes: 3 additions & 0 deletions cli/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"deno.enable": false
}
2 changes: 1 addition & 1 deletion cli/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<link rel="icon" href="favicon.ico" />
</head>
<body>
<div id="main"></div>
<main></main>
<script type="module" src="src/index.tsx"></script>
</body>
</html>
9 changes: 2 additions & 7 deletions cli/app/src/components/Factory/Factory.css
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
#main {
main {
display: grid;
grid-template-rows: [top] 2.125rem [main] 1fr;
grid-template-rows: [top] 2.125rem [main] auto;
}

.app {
grid-row: main;
margin-top: .5rem;
}

.app section:first-of-type {
display: flex;
justify-content: flex-start;
}
10 changes: 8 additions & 2 deletions cli/app/src/components/Factory/Factory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import { StrictMode, Suspense } from "react";
import { StyledEngineProvider } from "@mui/material/styles";
import { GraphInspector } from "../GraphInspector/GraphInspector";
import { Topbar } from "../Topbar/Topbar";
import { createClient, dedupExchange, Exchange, fetchExchange, Provider } from "urql";
import {
createClient,
dedupExchange,
Exchange,
fetchExchange,
Provider,
} from "urql";
import { cacheExchange } from "@urql/exchange-graphcache";
import { relayPagination } from '@urql/exchange-graphcache/extras';
import { relayPagination } from "@urql/exchange-graphcache/extras";

const client = createClient({
url: "/graphql",
Expand Down
88 changes: 88 additions & 0 deletions cli/app/src/components/GraphInspector/DynamicRowVirtualizer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useCallback, useEffect, useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import type { VertexNode } from "../../../../graphql/types";
import { VirtualRow } from "./VirtualRow";

interface DynamicRowVirtualizerProps {
nodes: VertexNode[];
typename: string;
hasNextPage: boolean;
fetching: boolean;
fetchNextPage: () => void;
update: number;
}

const RowSize = 30;

export function DynamicRowVirtualizer(
{ nodes, typename, hasNextPage, fetching, fetchNextPage, update }:
DynamicRowVirtualizerProps,
): JSX.Element {
const expanderRef = useRef<HTMLDivElement>();

const rowVirtualizer = useVirtualizer({
count: nodes.length,
getScrollElement: () => expanderRef.current,
// we need useCallback to force the update
estimateSize: useCallback(() => nodes[0].fields.length * RowSize, [update]),
enableSmoothScroll: false,
getItemKey: (index) => nodes[index].id,
// nuking this for now. Default does too much
scrollToFn: () => ({}),
});

useEffect(() => {
const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse();

if (!lastItem) {
return;
}

if (
lastItem.index >= nodes.length - 1 &&
hasNextPage &&
!fetching
) {
fetchNextPage();
}
}, [
hasNextPage,
fetchNextPage,
nodes.length,
fetching,
rowVirtualizer.getVirtualItems(),
]);

return (
<div
ref={expanderRef}
style={{
height: `${window.innerHeight - 250}px`,
width: `100%`,
overflow: "auto",
}}
>
<div
style={{
height: rowVirtualizer.getTotalSize(),
width: "100%",
position: "relative",
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const vertexNode = nodes[virtualRow.index];

return (
<VirtualRow
key={`${nodes[virtualRow.index].id}`}
vertexNode={vertexNode}
typename={typename}
virtualRow={virtualRow}
update={update}
/>
);
})}
</div>
</div>
);
}
100 changes: 55 additions & 45 deletions cli/app/src/components/GraphInspector/GraphInspector.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,49 @@
import "./GraphInspector.css";
import type { SyntheticEvent } from "react";
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
import { useCallback, useEffect, useReducer, useState, useRef } from "react";
import TreeView from "@mui/lab/TreeView";
import { Node } from "./Node";
import { allQuery, node } from "./queries";
import { graphReducer } from "./graphReducer";
import { VertexNode } from "../../../../graphql/types";
import { MinusSquare, PlusSquare } from "./icons";
import { StyledTreeItem } from "./StyledTreeItem";
import { fetchGraphQL } from "../../graphql/fetchGraphql";
import { Loader } from "../Loader/Loader";
import { useQuery } from 'urql';
import type { Page } from '../../../../graphql/relay';
import { useQuery } from "urql";
import type { Page } from "../../../../graphql/relay";
import { DynamicRowVirtualizer } from "./DynamicRowVirtualizer";

const emptyGraph = { graph: {} };

const limit = 5;

export function GraphInspector(): JSX.Element {
// TODO: call setAfter when scrolling
const [after] = useState('');
const [after, setAfter] = useState("");
const [typename, setTypename] = useState<string | undefined>();
// TODO: this really needs to go at some point and the rangeExtractor prop of
// useVirtualized seems a better way to go
// https://tanstack.com/virtual/v3/docs/api/virtualizer#rangeextractor
// measureElement might be useful here too
// https://tanstack.com/virtual/v3/docs/api/virtualizer#measureelement
const [update, forceUpdate] = useReducer((x) => x + 1, 0);

const [result] = useQuery<{ all: Page<VertexNode> }, {
typename: string;
first: number;
after: string;
}>({
query: allQuery,
pause: !typename,
pause: !typename && !after,
variables: {
typename,
first: limit,
after
after,
},
});

const [{ graph }, dispatch] = useReducer(graphReducer, emptyGraph);
const expandedNodes = useRef(new Set<string>());

const { data, error } = result;
const { data, error, fetching } = result;

useEffect(() => {
const edges = data?.all?.edges ?? [];
Expand All @@ -51,10 +56,10 @@ export function GraphInspector(): JSX.Element {
type: "ALL",
payload: {
typename,
nodes: edges.map(edge => edge.node),
nodes: edges.map((edge) => edge.node),
},
});
}, [data, typename])
}, [data, typename]);

const handleChange = useCallback(
async (_: SyntheticEvent, nodeIds: string[]) => {
Expand Down Expand Up @@ -115,11 +120,17 @@ export function GraphInspector(): JSX.Element {
}
}

setTimeout(forceUpdate, 300);

return;
}

setAfter("");
setTypename(nodeId);
}, [],

setTimeout(forceUpdate, 300);
},
[],
);

useEffect(() => {
Expand All @@ -146,40 +157,39 @@ export function GraphInspector(): JSX.Element {
}

if (error) {
return <p className="error">Oh no... {error?.message}</p>
return <p className="error">Oh no... {error?.message}</p>;
}

return (
<TreeView
aria-label="graph inspector"
defaultCollapseIcon={<MinusSquare />}
defaultExpandIcon={<PlusSquare />}
onNodeToggle={handleChange}
multiSelect={false}
>
{Object.values(graph).map(({ typename, label, nodes }) => (
<StyledTreeItem
key={typename}
nodeId={typename}
label={<div className="root">{label}</div>}
>
{nodes.length > 0
? nodes.map((vertexNode, i) => (
<StyledTreeItem
key={vertexNode.id}
nodeId={vertexNode.id}
label={
<Node
parentId={`${typename}.nodes.${i}`}
node={vertexNode}
/>
}
/>
)
)
: <Loader />}
</StyledTreeItem>
))}
</TreeView>
<div>
<TreeView
aria-label="graph inspector"
defaultCollapseIcon={<MinusSquare />}
defaultExpandIcon={<PlusSquare />}
onNodeToggle={handleChange}
multiSelect={false}
>
{Object.values(graph).map(({ typename, label, nodes }) => (
<StyledTreeItem
key={typename}
nodeId={typename}
label={<div className="root">{label}</div>}
>
{nodes.length > 0
? (
<DynamicRowVirtualizer
hasNextPage={!!data?.all?.pageInfo?.hasNextPage}
nodes={nodes}
typename={typename}
fetching={fetching}
fetchNextPage={() => setAfter(data.all.pageInfo.endCursor)}
update={update}
/>
)
: <div>loading....</div>}
</StyledTreeItem>
))}
</TreeView>
</div>
);
}
10 changes: 4 additions & 6 deletions cli/app/src/components/GraphInspector/StyledTreeItem.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { alpha, styled } from "@mui/material/styles";
import TreeItem, { treeItemClasses, TreeItemProps } from "@mui/lab/TreeItem";
import { FC } from 'react';
import { FC } from "react";

export const StyledTreeItem: FC<TreeItemProps> = styled((props: TreeItemProps) => (
<TreeItem {...props} />
))(({ theme }) => ({
export const StyledTreeItem: FC<TreeItemProps> = styled((
props: TreeItemProps,
) => <TreeItem {...props} />)(({ theme }) => ({
[`& .${treeItemClasses.iconContainer}`]: {
"& .close": {
opacity: 0.3,
},
},
[`& .${treeItemClasses.group}`]: {
marginLeft: `15px !important`,
paddingLeft: `18px !important`,
borderLeft: `1px dashed ${alpha(theme.palette.text.primary, 0.4)}`,
},
[`& .${treeItemClasses.selected}`]: {
Expand Down
47 changes: 47 additions & 0 deletions cli/app/src/components/GraphInspector/VirtualRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { StyledTreeItem } from "./StyledTreeItem";
import { Node } from "./Node";
import { VirtualItem } from "@tanstack/react-virtual";
import type { VertexNode } from "../../../../graphql/types";
import { useEffect, useRef } from "react";

interface VirtualRowProps {
virtualRow: VirtualItem<unknown>;
vertexNode: VertexNode;
typename: string;
update: number;
}

export function VirtualRow(
{ virtualRow, vertexNode, typename, update }: VirtualRowProps,
): JSX.Element {
const elementRef = useRef<HTMLDivElement>(null);

useEffect(() => {
virtualRow.measureElement(elementRef.current);
}, [update]);

return (
<div
key={vertexNode.id}
ref={elementRef}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
>
<StyledTreeItem
key={vertexNode.id}
nodeId={vertexNode.id}
label={
<Node
parentId={`${typename}.nodes.${virtualRow.index}`}
node={vertexNode}
/>
}
/>
</div>
);
}
Loading

0 comments on commit ab83bb6

Please sign in to comment.