From fa5e99ca54810bde30331a5cda1e4e67cec27f3a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 29 May 2024 17:48:09 -0600 Subject: [PATCH 01/15] Create a history class --- src/application/utilities/history.ts | 55 ++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/application/utilities/history.ts diff --git a/src/application/utilities/history.ts b/src/application/utilities/history.ts new file mode 100644 index 000000000..3dfbe197b --- /dev/null +++ b/src/application/utilities/history.ts @@ -0,0 +1,55 @@ +export class History { + 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); + }; +} From 529560479199c833315c0de9d80fa1a62f8509f3 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 29 May 2024 18:13:24 -0600 Subject: [PATCH 02/15] Use history to sync cache state --- src/application/components/Cache/Cache.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/application/components/Cache/Cache.tsx b/src/application/components/Cache/Cache.tsx index 1cc4ef8bd..b469b927e 100644 --- a/src/application/components/Cache/Cache.tsx +++ b/src/application/components/Cache/Cache.tsx @@ -1,5 +1,5 @@ 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"; @@ -13,6 +13,7 @@ import { JSONTreeViewer } from "../JSONTreeViewer"; import clsx from "clsx"; import { CopyButton } from "../CopyButton"; import { EmptyMessage } from "../EmptyMessage"; +import { History } from "../../utilities/history"; const { Sidebar, Main } = SidebarLayout; @@ -41,7 +42,11 @@ function filterCache(cache: Cache, searchTerm: string) { export function Cache() { const [searchTerm, setSearchTerm] = useState(""); - const [cacheId, setCacheId] = useState("ROOT_QUERY"); + const [history] = useState(() => new History("ROOT_QUERY")); + + const cacheId = useSyncExternalStore(history.listen, () => + history.getCurrent() + ); const { loading, data } = useQuery(GET_CACHE); const cache = useMemo( @@ -73,7 +78,7 @@ export function Cache() { history.push(cacheId)} searchTerm={searchTerm} /> @@ -112,7 +117,7 @@ export function Cache() { })} onClick={() => { if (key === "__ref") { - setCacheId(value as string); + history.push(value as string); } }} > From 45c5295330e531b5e0af5216d1a32d8689fb2ab3 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 29 May 2024 18:25:02 -0600 Subject: [PATCH 03/15] Allow for custom delay on tooltip --- src/application/components/Tooltip/Tooltip.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/application/components/Tooltip/Tooltip.tsx b/src/application/components/Tooltip/Tooltip.tsx index eadbb9638..c9c6b2568 100644 --- a/src/application/components/Tooltip/Tooltip.tsx +++ b/src/application/components/Tooltip/Tooltip.tsx @@ -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 ( - + {children} Date: Wed, 29 May 2024 18:26:16 -0600 Subject: [PATCH 04/15] Add buttons to navigate the history stack --- src/application/components/Cache/Cache.tsx | 29 +++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/application/components/Cache/Cache.tsx b/src/application/components/Cache/Cache.tsx index b469b927e..82989bb37 100644 --- a/src/application/components/Cache/Cache.tsx +++ b/src/application/components/Cache/Cache.tsx @@ -2,6 +2,8 @@ import type { ReactNode } from "react"; import { Fragment, useState, useMemo, useSyncExternalStore } from "react"; import type { TypedDocumentNode } from "@apollo/client"; import { gql, useQuery } from "@apollo/client"; +import IconChevronLeft from "@apollo/icons/small/IconChevronLeft.svg"; +import IconChevronRight from "@apollo/icons/small/IconChevronRight.svg"; import { SidebarLayout } from "../Layouts/SidebarLayout"; import { SearchField } from "../SearchField"; @@ -14,6 +16,9 @@ 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"; const { Sidebar, Main } = SidebarLayout; @@ -86,8 +91,30 @@ export function Cache() { ) : null}
+ + +
From b4e25d10db63996ab41bfdbd433b0cdf26411811 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 29 May 2024 18:58:33 -0600 Subject: [PATCH 11/15] Maintain history for lifetime of app --- src/application/components/Cache/Cache.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/application/components/Cache/Cache.tsx b/src/application/components/Cache/Cache.tsx index 41f29ed4c..76781586e 100644 --- a/src/application/components/Cache/Cache.tsx +++ b/src/application/components/Cache/Cache.tsx @@ -45,13 +45,11 @@ function filterCache(cache: Cache, searchTerm: string) { ); } +const history = new History("ROOT_QUERY"); + export function Cache() { const [searchTerm, setSearchTerm] = useState(""); - const [history] = useState(() => new History("ROOT_QUERY")); - - const cacheId = useSyncExternalStore(history.listen, () => - history.getCurrent() - ); + const cacheId = useSyncExternalStore(history.listen, history.getCurrent); const { loading, data } = useQuery(GET_CACHE); const cache = useMemo( From f2e10f85946077c806bf7bfbd63fb14bbd75e279 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 29 May 2024 20:49:34 -0600 Subject: [PATCH 12/15] Create an Alert component --- src/application/components/Alert.tsx | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/application/components/Alert.tsx diff --git a/src/application/components/Alert.tsx b/src/application/components/Alert.tsx new file mode 100644 index 000000000..e7551619c --- /dev/null +++ b/src/application/components/Alert.tsx @@ -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>>; + +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 = { + error: IconErrorSolid, +}; + +export function Alert({ children, className, variant }: AlertProps) { + const Icon = ICONS[variant]; + + return ( +
+ +
{children}
+
+ ); +} From 02460bf864a2e6e2be7ac0cc8a2b5aeb77256328 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 29 May 2024 20:49:46 -0600 Subject: [PATCH 13/15] Show an error banner when the cache item doesn't exist --- src/application/components/Cache/Cache.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/application/components/Cache/Cache.tsx b/src/application/components/Cache/Cache.tsx index 76781586e..62766443f 100644 --- a/src/application/components/Cache/Cache.tsx +++ b/src/application/components/Cache/Cache.tsx @@ -19,6 +19,7 @@ import { History } from "../../utilities/history"; import { Button } from "../Button"; import { ButtonGroup } from "../ButtonGroup"; import { Tooltip } from "../Tooltip"; +import { Alert } from "../Alert"; const { Sidebar, Main } = SidebarLayout; @@ -89,7 +90,7 @@ export function Cache() { ) : null} -
+
{dataExists ? ( <>
@@ -159,7 +160,10 @@ export function Cache() { }} /> ) : dataExists ? ( - <>This doesnt exist :( + + This cache entry was either removed from the cache or does not + exist. + ) : null}
From c01bd13d7e3b72dbc9da800a77c195abf6802da9 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 29 May 2024 21:01:01 -0600 Subject: [PATCH 14/15] Add changeset --- .changeset/sixty-worms-behave.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sixty-worms-behave.md diff --git a/.changeset/sixty-worms-behave.md b/.changeset/sixty-worms-behave.md new file mode 100644 index 000000000..60b5cfa15 --- /dev/null +++ b/.changeset/sixty-worms-behave.md @@ -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. From 57d02283bab6e0801895eaa163f84aa2512b2ad4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 29 May 2024 21:07:36 -0600 Subject: [PATCH 15/15] Inline the EntityList in the Cache tab --- src/application/components/Cache/Cache.tsx | 30 ++++++++++--- .../components/Cache/sidebar/EntityList.tsx | 43 ------------------- 2 files changed, 23 insertions(+), 50 deletions(-) delete mode 100644 src/application/components/Cache/sidebar/EntityList.tsx diff --git a/src/application/components/Cache/Cache.tsx b/src/application/components/Cache/Cache.tsx index 62766443f..b76e022c2 100644 --- a/src/application/components/Cache/Cache.tsx +++ b/src/application/components/Cache/Cache.tsx @@ -7,7 +7,6 @@ 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"; @@ -20,6 +19,10 @@ 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; @@ -65,6 +68,7 @@ export function Cache() { const dataExists = Object.keys(cache).length > 0; const cacheItem = cache[cacheId]; + const cacheIds = getRootCacheIds(filteredCache); return ( @@ -80,12 +84,24 @@ export function Cache() { value={searchTerm} />
- history.push(cacheId)} - searchTerm={searchTerm} - /> + + {cacheIds.map((id) => { + return ( + history.push(id)} + selected={id === cacheId} + className="font-code" + > + {searchTerm ? ( + + ) : ( + id + )} + + ); + })} +
) : null} diff --git a/src/application/components/Cache/sidebar/EntityList.tsx b/src/application/components/Cache/sidebar/EntityList.tsx deleted file mode 100644 index 14378cd02..000000000 --- a/src/application/components/Cache/sidebar/EntityList.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { List } from "../../List"; -import { ListItem } from "../../ListItem"; - -import { getRootCacheIds } from "../common/utils"; -import type { JSONObject } from "../../../types/json"; -import HighlightMatch from "../../HighlightMatch"; - -interface EntityListProps { - data: Record; - selectedCacheId: string; - setCacheId: (cacheId: string) => void; - searchTerm: string; -} - -export function EntityList({ - data, - selectedCacheId, - setCacheId, - searchTerm, -}: EntityListProps) { - const ids = getRootCacheIds(data); - - return ( - - {ids.map((cacheId) => { - return ( - setCacheId(cacheId)} - selected={cacheId === selectedCacheId} - className="font-code" - > - {searchTerm ? ( - - ) : ( - cacheId - )} - - ); - })} - - ); -}