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

Add history navigation to cache tab #1394

Merged
merged 16 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
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: [
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now we only need the error variant, so I've only implemented this one. We can add others as we need them.

"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");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instantiating this outside of the component allows us to keep the history when changing tabs, otherwise this resets anytime the Cache component unmounts.


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>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component was fairly small and "EntityList" wasn't a super great name for this anyways, so I went ahead and inlined the implementation here and removed the old component.

{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}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice UX detail 💯

<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);
};
}
Loading