Skip to content

Commit

Permalink
Add history navigation to cache tab (#1394)
Browse files Browse the repository at this point in the history
  • Loading branch information
jerelmiller authored Jun 6, 2024
1 parent 8fac152 commit 84a9634
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 62 deletions.
5 changes: 5 additions & 0 deletions .changeset/sixty-worms-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"apollo-client-devtools": minor
---

Add forward and back buttons to the cache tab to navigate the history of cache entries you've visited.
46 changes: 46 additions & 0 deletions src/application/components/Alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import type { ElementType, ReactNode } from "react";
import type { OmitNull } from "../types/utils";
import IconErrorSolid from "@apollo/icons/large/IconErrorSolid.svg";
import { twMerge } from "tailwind-merge";

type Variants = OmitNull<Required<VariantProps<typeof alert>>>;

interface AlertProps extends Variants {
children?: ReactNode;
className?: string;
}

const alert = cva(
["px-4", "py-3", "border-l-4", "rounded", "flex", "gap-2", "items-start"],
{
variants: {
variant: {
error: [
"border-l-error",
"dark:border-l-error-dark",
"bg-error",
"dark:bg-error-dark",
"text-error",
"dark:text-error-dark",
],
},
},
}
);

const ICONS: Record<Variants["variant"], ElementType> = {
error: IconErrorSolid,
};

export function Alert({ children, className, variant }: AlertProps) {
const Icon = ICONS[variant];

return (
<div className={twMerge(alert({ variant }), className)}>
<Icon className="size-6" />
<div className="flex-1 font-body text-md font-normal">{children}</div>
</div>
);
}
93 changes: 76 additions & 17 deletions src/application/components/Cache/Cache.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import type { ReactNode } from "react";
import { Fragment, useState, useMemo } from "react";
import { Fragment, useState, useMemo, useSyncExternalStore } from "react";
import type { TypedDocumentNode } from "@apollo/client";
import { gql, useQuery } from "@apollo/client";
import IconArrowLeft from "@apollo/icons/small/IconArrowLeft.svg";
import IconArrowRight from "@apollo/icons/small/IconArrowRight.svg";

import { SidebarLayout } from "../Layouts/SidebarLayout";
import { SearchField } from "../SearchField";
import { EntityList } from "./sidebar/EntityList";
import { Loading } from "./common/Loading";
import type { GetCache, GetCacheVariables } from "../../types/gql";
import type { JSONObject } from "../../types/json";
import { JSONTreeViewer } from "../JSONTreeViewer";
import clsx from "clsx";
import { CopyButton } from "../CopyButton";
import { EmptyMessage } from "../EmptyMessage";
import { History } from "../../utilities/history";
import { Button } from "../Button";
import { ButtonGroup } from "../ButtonGroup";
import { Tooltip } from "../Tooltip";
import { Alert } from "../Alert";
import { List } from "../List";
import { ListItem } from "../ListItem";
import { getRootCacheIds } from "./common/utils";
import HighlightMatch from "../HighlightMatch";

const { Sidebar, Main } = SidebarLayout;

Expand All @@ -39,9 +49,11 @@ function filterCache(cache: Cache, searchTerm: string) {
);
}

const history = new History("ROOT_QUERY");

export function Cache() {
const [searchTerm, setSearchTerm] = useState("");
const [cacheId, setCacheId] = useState("ROOT_QUERY");
const cacheId = useSyncExternalStore(history.listen, history.getCurrent);

const { loading, data } = useQuery(GET_CACHE);
const cache = useMemo(
Expand All @@ -55,6 +67,8 @@ export function Cache() {
);

const dataExists = Object.keys(cache).length > 0;
const cacheItem = cache[cacheId];
const cacheIds = getRootCacheIds(filteredCache);

return (
<SidebarLayout>
Expand All @@ -70,20 +84,61 @@ export function Cache() {
value={searchTerm}
/>
<div className="overflow-auto h-full">
<EntityList
data={filteredCache}
selectedCacheId={cacheId}
setCacheId={setCacheId}
searchTerm={searchTerm}
/>
<List>
{cacheIds.map((id) => {
return (
<ListItem
key={id}
onClick={() => history.push(id)}
selected={id === cacheId}
className="font-code"
>
{searchTerm ? (
<HighlightMatch searchTerm={searchTerm} value={id} />
) : (
id
)}
</ListItem>
);
})}
</List>
</div>
</Fragment>
) : null}
</Sidebar>
<Main className="!overflow-auto">
<Main className="!overflow-auto flex flex-col">
{dataExists ? (
<div className="flex items-start justify-between mb-2 gap-2">
<div>
<>
<div className="flex items-start justify-between">
<ButtonGroup>
<Tooltip content="Go back" delayDuration={500}>
<Button
aria-label="Go back"
icon={<IconArrowLeft />}
size="xs"
variant="secondary"
disabled={!history.canGoBack()}
onClick={() => history.back()}
/>
</Tooltip>
<Tooltip content="Go forward" delayDuration={500}>
<Button
aria-label="Go forward"
icon={<IconArrowRight />}
size="xs"
variant="secondary"
disabled={!history.canGoForward()}
onClick={() => history.forward()}
/>
</Tooltip>
</ButtonGroup>
<CopyButton
size="sm"
text={JSON.stringify(cache[cacheId])}
className={clsx({ invisible: !cacheItem })}
/>
</div>
<div className="my-2">
<div className="text-xs font-bold uppercase">Cache ID</div>
<h1
className="font-code font-medium text-xl text-heading dark:text-heading-dark break-all"
Expand All @@ -92,17 +147,16 @@ export function Cache() {
{cacheId}
</h1>
</div>
<CopyButton size="md" text={JSON.stringify(cache[cacheId])} />
</div>
</>
) : (
<EmptyMessage className="m-auto mt-20" />
)}

{loading ? (
<Loading />
) : dataExists ? (
) : cacheItem ? (
<JSONTreeViewer
data={cache[cacheId]}
data={cacheItem}
hideRoot={true}
valueRenderer={(valueAsString, value, key) => {
return (
Expand All @@ -112,7 +166,7 @@ export function Cache() {
})}
onClick={() => {
if (key === "__ref") {
setCacheId(value as string);
history.push(value as string);
}
}}
>
Expand All @@ -121,6 +175,11 @@ export function Cache() {
);
}}
/>
) : dataExists ? (
<Alert variant="error" className="mt-4">
This cache entry was either removed from the cache or does not
exist.
</Alert>
) : null}
</Main>
</SidebarLayout>
Expand Down
43 changes: 0 additions & 43 deletions src/application/components/Cache/sidebar/EntityList.tsx

This file was deleted.

10 changes: 8 additions & 2 deletions src/application/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ interface TooltipProps {
asChild?: boolean;
content: ReactNode;
children?: ReactNode;
delayDuration?: number;
side?: "top" | "bottom" | "left" | "right";
}

export function Tooltip({ content, children, side = "bottom" }: TooltipProps) {
export function Tooltip({
content,
children,
delayDuration,
side = "bottom",
}: TooltipProps) {
return (
<Root>
<Root delayDuration={delayDuration}>
<Trigger asChild>{children}</Trigger>
<Portal>
<Content
Expand Down
55 changes: 55 additions & 0 deletions src/application/utilities/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export class History<T> {
private stack: T[] = [];
private currentIndex = 0;
private listeners = new Set<(value: T) => void>();

constructor(initialValue: T) {
this.stack.push(initialValue);
}

getCurrent = () => {
return this.stack[this.currentIndex];
};

go = (delta: number) => {
const previousIndex = this.currentIndex;
this.currentIndex = Math.min(
Math.max(0, this.currentIndex + delta),
this.stack.length - 1
);

if (this.currentIndex !== previousIndex) {
this.listeners.forEach((listener) => {
listener(this.getCurrent());
});
}
};

back = () => {
this.go(-1);
};

forward = () => {
this.go(1);
};

canGoBack = () => {
return this.currentIndex > 0;
};

canGoForward = () => {
return this.currentIndex < this.stack.length - 1;
};

push = (value: T) => {
this.stack.splice(this.currentIndex + 1);
this.stack.push(value);
this.forward();
};

listen = (listener: (value: T) => void) => {
this.listeners.add(listener);

return () => this.listeners.delete(listener);
};
}

0 comments on commit 84a9634

Please sign in to comment.