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

fix: persisting headers in playground and styling #1177

Merged
Merged
1 change: 1 addition & 0 deletions demo/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ github.com/wundergraph/cosmo/composition-go v0.0.0-20240124120900-5effe48a4a1d/g
github.com/wundergraph/cosmo/router v0.0.0-20240917083803-9ac159070f12 h1:j11xczYYLj72ylDw7T7UQwX+OAPBhJmfh/sbl0tngfo=
github.com/wundergraph/cosmo/router v0.0.0-20240917083803-9ac159070f12/go.mod h1:hcEocQnviSg+gumcym4MmgTXvdjNy9HuQHP6ns7L3x0=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.83 h1:h55NpJRtcYqLZ6VwQcXWs4XU6+imjOmN1naQWpji/1Y=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.83/go.mod h1:zkPVYJu1iQd0y1fBNj+oXe9uMI/33TSoiXEsKSAESZY=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
Expand Down
3 changes: 2 additions & 1 deletion go.work
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ use (
./router-tests
)

//replace github.com/wundergraph/graphql-go-tools/v2 => ../graphql-go-tools/v2
// replace github.com/wundergraph/graphql-go-tools/v2 => ../../graphql-go-tools/v2

//replace github.com/wundergraph/astjson => ../astjson
33 changes: 30 additions & 3 deletions playground/src/components/playground/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { TraceContext, TraceView } from '@/components/playground/trace-view';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { explorerPlugin } from '@graphiql/plugin-explorer';
import { createGraphiQLFetcher } from '@graphiql/toolkit';
import { GraphiQL } from 'graphiql';
Expand All @@ -19,6 +18,7 @@ import { sentenceCase } from 'change-case';
import { PlanView } from './plan-view';
import { QueryPlan } from './types';
import { useDebounce } from 'use-debounce';
import { useLocalStorage } from '@/lib/use-local-storage';
import 'graphiql/graphiql.css';
import '@graphiql/plugin-explorer/dist/style.css';
import '@/theme.css';
Expand Down Expand Up @@ -249,7 +249,7 @@ const PlaygroundPortal = () => {
fillRule="evenodd"
clipRule="evenodd"
d="M447.099 231.913C405.967 244.337 367.742 264.878 334.682 292.323C320.832 268.71 298.796 251.002 272.754 242.557C313.865 205.575 362.202 177.525 414.709 160.178C467.216 142.832 522.751 136.567 577.803 141.781C632.855 146.994 686.227 163.571 734.544 190.465C746.769 197.27 758.603 204.698 770.004 212.711C770.394 212.542 770.785 212.376 771.179 212.213C785.976 206.085 802.259 204.482 817.967 207.607C833.676 210.733 848.105 218.446 859.429 229.771C870.754 241.096 878.465 255.525 881.589 271.233C884.712 286.941 883.107 303.223 876.976 318.018C870.845 332.814 860.464 345.459 847.146 354.355C833.828 363.252 818.171 367.999 802.154 367.997C791.52 367.997 780.991 365.902 771.167 361.833C761.343 357.763 752.417 351.799 744.898 344.28C737.379 336.76 731.415 327.834 727.347 318.01C723.279 308.186 721.186 297.657 721.187 287.024C721.187 282.871 721.506 278.742 722.135 274.672C713.657 268.849 704.889 263.426 695.859 258.426C658.269 237.612 616.889 224.541 574.163 219.988C531.437 215.434 488.232 219.489 447.099 231.913ZM319.489 348.564C319.489 363.809 315.185 378.728 307.094 391.613L323.693 420.326C307.59 439.476 285.501 452.638 260.995 457.683L244.582 429.298C237.31 429.844 229.959 429.408 222.73 427.971C207.024 424.848 192.597 417.138 181.273 405.816C169.949 394.495 162.237 380.069 159.112 364.365C155.986 348.661 157.588 332.382 163.715 317.588C169.841 302.794 180.217 290.149 193.531 281.251C206.845 272.354 222.498 267.604 238.511 267.601C249.145 267.6 259.674 269.693 269.499 273.761C279.324 277.829 288.251 283.793 295.77 291.311C303.29 298.829 309.255 307.755 313.325 317.578C317.394 327.402 319.489 337.931 319.489 348.564ZM260.998 457.685L400.599 699.132L442.692 772.036L484.794 699.132L537.279 608.237L589.621 698.805L631.691 771.687L673.783 698.794L744.391 576.462H859.708C861.079 564.36 861.767 552.19 861.769 540.01C861.771 527.83 861.08 515.66 859.697 503.558H702.288L694.971 516.229L631.67 625.857L579.327 535.278L537.235 462.374L495.208 535.289L442.692 626.184L323.7 420.328C307.596 439.478 285.506 452.64 260.998 457.685ZM861.77 540.003C861.768 552.183 861.08 564.353 859.709 576.455H937.128V503.551H859.709C861.088 515.653 861.776 527.823 861.77 540.003ZM937.154 503.558H938.332C939.411 515.563 940 527.721 940 540.01C940 760.902 760.967 940 540.027 940C319.088 940 140 760.924 140 540.031C139.942 500.879 145.66 461.933 156.968 424.449C175.493 444.394 200.696 456.845 227.794 459.44C221.851 485.163 218.231 515.061 218.231 540.01C218.231 717.668 362.259 861.764 540.038 861.764C705.462 861.764 841.629 736.99 859.731 576.462H937.154V503.558Z"
fill="white"
className="fill-foreground"
></path>
</svg>
</a>,
Expand All @@ -271,6 +271,31 @@ export const Playground = (input: {
const [schema, setSchema] = useState<GraphQLSchema | null>(null);

const [query, setQuery] = useState<string | undefined>(undefined);

const [storedHeaders, setStoredHeaders] = useLocalStorage('graphiql:headers', '', {
deserializer(value) {
return value;
},
serializer(value) {
return value;
},
});
const [tempHeaders, setTempHeaders] = useState<any>();

useEffect(() => {
if (!storedHeaders || tempHeaders) {
return;
}
setTempHeaders(storedHeaders);
}, [storedHeaders, tempHeaders]);

useEffect(() => {
if (!tempHeaders) {
return;
}
setStoredHeaders(tempHeaders);
}, [tempHeaders]);

const [headers, setHeaders] = useState(`{
"X-WG-TRACE" : "true"
}`);
Expand Down Expand Up @@ -435,7 +460,9 @@ export const Playground = (input: {
showPersistHeadersSettings={false}
fetcher={fetcher}
onEditQuery={setQuery}
headers={headers}
defaultHeaders={`{
"X-WG-TRACE" : "true"
}`}
onEditHeaders={setHeaders}
plugins={[
explorerPlugin({
Expand Down
17 changes: 17 additions & 0 deletions playground/src/lib/use-event-callback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useCallback, useRef } from "react";

import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect";

export function useEventCallback<Args extends unknown[], R>(
fn: (...args: Args) => R
) {
const ref = useRef<typeof fn>(() => {
throw new Error("Cannot call an event handler while rendering.");
});

useIsomorphicLayoutEffect(() => {
ref.current = fn;
}, [fn]);

return useCallback((...args: Args) => ref.current(...args), [ref]);
}
82 changes: 82 additions & 0 deletions playground/src/lib/use-event-listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { RefObject, useEffect, useRef } from "react";

import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect";

// MediaQueryList Event based useEventListener interface
function useEventListener<K extends keyof MediaQueryListEventMap>(
eventName: K,
handler: (event: MediaQueryListEventMap[K]) => void,
element: RefObject<MediaQueryList>,
options?: boolean | AddEventListenerOptions
): void;

// Window Event based useEventListener interface
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
element?: undefined,
options?: boolean | AddEventListenerOptions
): void;

// Element Event based useEventListener interface
function useEventListener<
K extends keyof HTMLElementEventMap,
T extends HTMLElement = HTMLDivElement
>(
eventName: K,
handler: (event: HTMLElementEventMap[K]) => void,
element: RefObject<T>,
options?: boolean | AddEventListenerOptions
): void;

// Document Event based useEventListener interface
function useEventListener<K extends keyof DocumentEventMap>(
eventName: K,
handler: (event: DocumentEventMap[K]) => void,
element: RefObject<Document>,
options?: boolean | AddEventListenerOptions
): void;

function useEventListener<
KW extends keyof WindowEventMap,
KH extends keyof HTMLElementEventMap,
KM extends keyof MediaQueryListEventMap,
T extends HTMLElement | MediaQueryList | void = void
>(
eventName: KW | KH | KM,
handler: (
event:
| WindowEventMap[KW]
| HTMLElementEventMap[KH]
| MediaQueryListEventMap[KM]
| Event
) => void,
element?: RefObject<T>,
options?: boolean | AddEventListenerOptions
) {
// Create a ref that stores handler
const savedHandler = useRef(handler);

useIsomorphicLayoutEffect(() => {
savedHandler.current = handler;
}, [handler]);

useEffect(() => {
// Define the listening target
const targetElement: T | Window = element?.current ?? window;

if (!(targetElement && targetElement.addEventListener)) return;

// Create event listener that calls handler function stored in ref
const listener: typeof handler = (event) => savedHandler.current(event);

targetElement.addEventListener(eventName, listener, options);

// Remove event listener on cleanup
return () => {
targetElement.removeEventListener(eventName, listener, options);
};
}, [eventName, element, options]);
}

export { useEventListener };
4 changes: 4 additions & 0 deletions playground/src/lib/use-isomorphic-layout-effect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { useEffect, useLayoutEffect } from "react";

export const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;
180 changes: 180 additions & 0 deletions playground/src/lib/use-local-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
import { useEventCallback } from './use-event-callback';
import { useEventListener } from './use-event-listener';

declare global {
interface WindowEventMap {
'local-storage': CustomEvent;
}
}

/**
* Options for customizing the behavior of serialization and deserialization.
* @template T - The type of the state to be stored in local storage.
*/
type UseLocalStorageOptions<T> = {
/** A function to serialize the value before storing it. */
serializer?: (value: T) => string;
/** A function to deserialize the stored value. */
deserializer?: (value: string) => T;
/**
* If `true` (default), the hook will initialize reading the local storage. In SSR, you should set it to `false`, returning the initial value initially.
* @default true
*/
initializeWithValue?: boolean;
};

const IS_SERVER = typeof window === 'undefined';

/**
* Custom hook that uses the [`localStorage API`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to persist state across page reloads.
* @template T - The type of the state to be stored in local storage.
* @param {string} key - The key under which the value will be stored in local storage.
* @param {T | (() => T)} initialValue - The initial value of the state or a function that returns the initial value.
* @param {UseLocalStorageOptions<T>} [options] - Options for customizing the behavior of serialization and deserialization (optional).
* @returns {[T, Dispatch<SetStateAction<T>>, () => void]} A tuple containing the stored value, a function to set the value and a function to remove the key from storage.
* @public
* @see [Documentation](https://usehooks-ts.com/react-hook/use-local-storage)
* @example
* ```tsx
* const [count, setCount, removeCount] = useLocalStorage('count', 0);
* // Access the `count` value, the `setCount` function to update it and `removeCount` function to remove the key from storage.
* ```
*/
export function useLocalStorage<T>(
key: string,
initialValue: T | (() => T),
options: UseLocalStorageOptions<T> = {},
): [T, Dispatch<SetStateAction<T>>, () => void] {
const { initializeWithValue = true } = options;

const serializer = useCallback<(value: T) => string>(
(value) => {
if (options.serializer) {
return options.serializer(value);
}

return JSON.stringify(value);
},
[options],
);

const deserializer = useCallback<(value: string) => T>(
(value) => {
if (options.deserializer) {
return options.deserializer(value);
}
// Support 'undefined' as a value
if (value === 'undefined') {
return undefined as unknown as T;
}

const defaultValue = initialValue instanceof Function ? initialValue() : initialValue;

let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch (error) {
console.error('Error parsing JSON:', error);
return defaultValue; // Return initialValue if parsing fails
}

return parsed as T;
},
[options, initialValue],
);

// Get from local storage then
// parse stored json or return initialValue
const readValue = useCallback((): T => {
const initialValueToUse = initialValue instanceof Function ? initialValue() : initialValue;

// Prevent build error "window is undefined" but keep working
if (IS_SERVER) {
return initialValueToUse;
}

try {
const raw = window.localStorage.getItem(key);
return raw ? deserializer(raw) : initialValueToUse;
} catch (error) {
console.warn(`Error reading localStorage key “${key}”:`, error);
return initialValueToUse;
}
}, [initialValue, key, deserializer]);

const [storedValue, setStoredValue] = useState(() => {
if (initializeWithValue) {
return readValue();
}

return initialValue instanceof Function ? initialValue() : initialValue;
});

// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue: Dispatch<SetStateAction<T>> = useEventCallback((value) => {
// Prevent build error "window is undefined" but keeps working
if (IS_SERVER) {
console.warn(`Tried setting localStorage key “${key}” even though environment is not a client`);
}

try {
// Allow value to be a function so we have the same API as useState
const newValue = value instanceof Function ? value(readValue()) : value;

// Save to local storage
window.localStorage.setItem(key, serializer(newValue));

// Save state
setStoredValue(newValue);

// We dispatch a custom event so every useLocalStorage hook are notified
window.dispatchEvent(new Event('local-storage'));
} catch (error) {
console.warn(`Error setting localStorage key “${key}”:`, error);
}
});

const removeValue = useEventCallback(() => {
// Prevent build error "window is undefined" but keeps working
if (IS_SERVER) {
console.warn(`Tried removing localStorage key “${key}” even though environment is not a client`);
}

const defaultValue = initialValue instanceof Function ? initialValue() : initialValue;

// Remove the key from local storage
window.localStorage.removeItem(key);

// Save state with default value
setStoredValue(defaultValue);

// We dispatch a custom event so every similar useLocalStorage hook is notified
window.dispatchEvent(new StorageEvent('local-storage', { key }));
});

useEffect(() => {
setStoredValue(readValue());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [key]);

const handleStorageChange = useCallback(
(event: StorageEvent | CustomEvent) => {
if ((event as StorageEvent).key && (event as StorageEvent).key !== key) {
return;
}
setStoredValue(readValue());
},
[key, readValue],
);

// this only works for other documents, not the current one
useEventListener('storage', handleStorageChange);

// this is a custom event, triggered in writeValueToLocalStorage
// See: useLocalStorage()
useEventListener('local-storage', handleStorageChange);

return [storedValue, setValue, removeValue];
}
2 changes: 1 addition & 1 deletion playground/src/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ body.graphiql-dark [data-radix-popper-content-wrapper] {
.graphiql-history-header,
.graphiql-doc-explorer-title,
.doc-explorer-title {
@apply text-2xl font-bold text-primary-foreground;
@apply text-2xl font-bold text-foreground;
}

.doc-explorer-title {
Expand Down
2 changes: 1 addition & 1 deletion router-tests/Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
SHELL := bash

test:
go test -race ./...
go test -ldflags=-extldflags=-Wl,-ld_classic -race ./...

update-snapshot:
go test -update -race ./...
Expand Down
8 changes: 4 additions & 4 deletions router/internal/graphiql/graphiql.html

Large diffs are not rendered by default.

Loading
Loading