Skip to content

Commit

Permalink
incremental payloads UI
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasheyenbrock committed May 14, 2024
1 parent e3ca774 commit 0f5ab70
Show file tree
Hide file tree
Showing 10 changed files with 422 additions and 87 deletions.
141 changes: 141 additions & 0 deletions packages/graphiql-react/src/editor/components/increments-editors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { useCallback, useEffect, useRef, useState } from 'react';

import { ChevronDownIcon, ChevronUpIcon } from '../../icons';
import { UnStyledButton } from '../../ui';
import {
commonKeys,
DEFAULT_EDITOR_THEME,
DEFAULT_KEY_MAP,
importCodeMirror,
} from '../common';
import { useSynchronizeOption } from '../hooks';
import { IncrementalPayload } from '../tabs';
import { CodeMirrorEditor, CommonEditorProps } from '../types';

import '../style/codemirror.css';
import '../style/fold.css';
import '../style/lint.css';
import '../style/hint.css';
import '../style/info.css';
import '../style/jump.css';
import '../style/auto-insertion.css';
import '../style/editor.css';
import '../style/increments-editors.css';

type UseIncrementsEditorArgs = CommonEditorProps & {
increment: IncrementalPayload;
};

function useIncrementsEditor({
editorTheme = DEFAULT_EDITOR_THEME,
keyMap = DEFAULT_KEY_MAP,
increment,
}: UseIncrementsEditorArgs) {
const [editor, setEditor] = useState<CodeMirrorEditor | null>(null);

Check warning on line 34 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L34

Added line #L34 was not covered by tests

const ref = useRef<HTMLDivElement>(null);

Check warning on line 36 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L36

Added line #L36 was not covered by tests

useEffect(() => {
let isActive = true;
void importCodeMirror(

Check warning on line 40 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L38-L40

Added lines #L38 - L40 were not covered by tests
[
import('codemirror/addon/fold/foldgutter'),
import('codemirror/addon/fold/brace-fold'),
import('codemirror/addon/dialog/dialog'),
import('codemirror/addon/search/search'),
import('codemirror/addon/search/searchcursor'),
import('codemirror/addon/search/jump-to-line'),
// @ts-expect-error
import('codemirror/keymap/sublime'),
import('codemirror-graphql/esm/results/mode'),
import('codemirror-graphql/esm/utils/info-addon'),
],
{ useCommonAddons: false },
).then(CodeMirror => {
// Don't continue if the effect has already been cleaned up
if (!isActive) {
return;

Check warning on line 57 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L57

Added line #L57 was not covered by tests
}

const container = ref.current;

Check warning on line 60 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L60

Added line #L60 was not covered by tests
if (!container) {
return;

Check warning on line 62 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L62

Added line #L62 was not covered by tests
}

const newEditor = CodeMirror(container, {

Check warning on line 65 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L65

Added line #L65 was not covered by tests
value: JSON.stringify(increment.payload, null, 2),
lineWrapping: true,
readOnly: true,
theme: editorTheme,
mode: 'graphql-results',
foldGutter: true,
gutters: ['CodeMirror-foldgutter'],
// @ts-expect-error
info: true,
extraKeys: commonKeys,
});

setEditor(newEditor);

Check warning on line 78 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L78

Added line #L78 was not covered by tests
});

return () => {
isActive = false;

Check warning on line 82 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L81-L82

Added lines #L81 - L82 were not covered by tests
};
}, [editorTheme]);

Check warning on line 84 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View workflow job for this annotation

GitHub Actions / ESLint

React Hook useEffect has a missing dependency: 'increment.payload'. Either include it or remove the dependency array

useSynchronizeOption(editor, 'keyMap', keyMap);

Check warning on line 86 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L86

Added line #L86 was not covered by tests

return ref;

Check warning on line 88 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L88

Added line #L88 was not covered by tests
}

function IncrementEditor(
props: UseIncrementsEditorArgs & { isInitial: boolean },
) {
const [isOpen, setIsOpen] = useState(false);
const incrementEditor = useIncrementsEditor(props);

Check warning on line 95 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L94-L95

Added lines #L94 - L95 were not covered by tests

const toggleEditor = useCallback(() => setIsOpen(current => !current), []);

Check warning on line 97 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L97

Added line #L97 was not covered by tests

return (

Check warning on line 99 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L99

Added line #L99 was not covered by tests
<div
className="graphiql-increment-editor"
style={isOpen ? { height: '30vh' } : {}}
>
<UnStyledButton
className="graphiql-increment-editor-toggle"
onClick={toggleEditor}
>
{props.isInitial ? 'Initial payload' : 'Increment'} (after{' '}
{props.increment.timing / 1000}s)
{isOpen ? (
<ChevronUpIcon className="graphiql-increment-editor-chevron" />
) : (
<ChevronDownIcon className="graphiql-increment-editor-chevron" />
)}
</UnStyledButton>
<div
ref={incrementEditor}
className={`graphiql-editor ${isOpen ? '' : 'hidden'}`}
/>
</div>
);
}

export type IncrementsEditorsProps = CommonEditorProps & {
incrementalPayloads: IncrementalPayload[];
};

export function IncrementsEditors(props: IncrementsEditorsProps) {
return (
<div className="graphiql-increments-editors">
{props.incrementalPayloads.map((increment, index) => (
<IncrementEditor

Check warning on line 132 in packages/graphiql-react/src/editor/components/increments-editors.tsx

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/components/increments-editors.tsx#L132

Added line #L132 was not covered by tests
key={increment.timing}
isInitial={index === 0}
increment={increment}
{...props}
/>
))}
</div>
);
}
3 changes: 3 additions & 0 deletions packages/graphiql-react/src/editor/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export { HeaderEditor } from './header-editor';
export { ImagePreview } from './image-preview';
export { IncrementsEditors } from './increments-editors';
export { QueryEditor } from './query-editor';
export { ResponseEditor } from './response-editor';
export { VariableEditor } from './variable-editor';

export type { IncrementsEditorsProps } from './increments-editors';
4 changes: 3 additions & 1 deletion packages/graphiql-react/src/editor/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export {
HeaderEditor,
ImagePreview,
IncrementsEditors,
QueryEditor,
ResponseEditor,
VariableEditor,
Expand All @@ -26,14 +27,15 @@ export { useQueryEditor } from './query-editor';
export { useResponseEditor } from './response-editor';
export { useVariableEditor } from './variable-editor';

export type { IncrementsEditorsProps } from './components';
export type { EditorContextType, EditorContextProviderProps } from './context';
export type { UseHeaderEditorArgs } from './header-editor';
export type { UseQueryEditorArgs } from './query-editor';
export type {
ResponseTooltipType,
UseResponseEditorArgs,
} from './response-editor';
export type { TabsState } from './tabs';
export type { IncrementalPayload, TabsState } from './tabs';
export type { UseVariableEditorArgs } from './variable-editor';

export type { CommonEditorProps, KeyMap, WriteableEditorProps } from './types';
31 changes: 31 additions & 0 deletions packages/graphiql-react/src/editor/style/increments-editors.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.graphiql-increments-editors {
display: flex;
flex-direction: column;
padding: var(--px-16);
}

.graphiql-increment-editor {
padding: var(--px-4) 0;
display: flex;
flex-direction: column;
position: relative;
}

.graphiql-increment-editor + .graphiql-increment-editor {
border-top: 1px solid
hsla(var(--color-neutral), var(--alpha-background-heavy));
}

.graphiql-increment-editor-toggle,
button.graphiql-increment-editor-toggle {
padding: var(--px-2) var(--px-4);
display: flex;
justify-content: space-between;
align-items: center;
}

.graphiql-increment-editor-chevron {
height: var(--px-12);
width: var(--px-12);
margin-left: var(--px-4);
}
54 changes: 53 additions & 1 deletion packages/graphiql-react/src/editor/tabs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { StorageAPI } from '@graphiql/toolkit';
import { ExecutionResult } from 'graphql';
import { useCallback, useMemo } from 'react';

import debounce from '../utility/debounce';
import { IncrementalResult } from '../utility/incremental';
import { CodeMirrorEditorWithOperationFacts } from './context';
import { CodeMirrorEditor } from './types';

Expand Down Expand Up @@ -47,6 +49,27 @@ export type TabState = TabDefinition & {
* The contents of the response editor of this tab.
*/
response: string | null;
/**
* While being subscribed to a multi-part request (subscription, defer,
* stream, etc.) this list will accumulate all incremental results received
* from the server, including a client-generated timestamp for when the
* increment was received. Each time a new request starts to run, this list
* will be cleared.
*/
incrementalPayloads?: IncrementalPayload[] | null;
};

export type IncrementalPayload = {
/**
* The number of milliseconds that went by between sending the request and
* receiving this increment.
*/
timing: number;
/**
* The execution result (for subscriptions), or the list of incremental
* results (for @defer/@stream).
*/
payload: ExecutionResult | IncrementalResult[];
};

/**
Expand Down Expand Up @@ -125,6 +148,7 @@ export function getDefaultTabState({
headers,
operationName,
response: null,
incrementalPayloads: [],
});
parsed.activeTabIndex = parsed.tabs.length - 1;
}
Expand Down Expand Up @@ -172,7 +196,8 @@ function isTabState(obj: any): obj is TabState {
hasStringOrNullKey(obj, 'variables') &&
hasStringOrNullKey(obj, 'headers') &&
hasStringOrNullKey(obj, 'operationName') &&
hasStringOrNullKey(obj, 'response')
hasStringOrNullKey(obj, 'response') &&
hasIncrementalPayloads(obj)
);
}

Expand All @@ -188,6 +213,31 @@ function hasStringOrNullKey(obj: Record<string, any>, key: string) {
return key in obj && (typeof obj[key] === 'string' || obj[key] === null);
}

function hasIncrementalPayloads(obj: Record<string, any>) {
const { incrementalPayloads } = obj;

Check warning on line 217 in packages/graphiql-react/src/editor/tabs.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/tabs.ts#L217

Added line #L217 was not covered by tests

// Not having any values is fine
if (incrementalPayloads === undefined || incrementalPayloads === null) {
return true;

Check warning on line 221 in packages/graphiql-react/src/editor/tabs.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/tabs.ts#L221

Added line #L221 was not covered by tests
}

// Anything other than an array is bad
if (!Array.isArray(incrementalPayloads)) {
return false;

Check warning on line 226 in packages/graphiql-react/src/editor/tabs.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/tabs.ts#L226

Added line #L226 was not covered by tests
}

return incrementalPayloads.every(

Check warning on line 229 in packages/graphiql-react/src/editor/tabs.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-react/src/editor/tabs.ts#L229

Added line #L229 was not covered by tests
item =>
item &&
typeof item === 'object' &&
'timing' in item &&
typeof item.timing === 'number' &&
'payload' in item &&
item.payload &&
typeof item.payload === 'object',
);
}

export function useSynchronizeActiveTabValues({
queryEditor,
variableEditor,
Expand Down Expand Up @@ -225,6 +275,7 @@ export function serializeTabState(
return JSON.stringify(tabState, (key, value) =>
key === 'hash' ||
key === 'response' ||
key === 'incrementalPayloads' ||
(!shouldPersistHeaders && key === 'headers')
? null
: value,
Expand Down Expand Up @@ -299,6 +350,7 @@ export function createTab({
headers,
operationName: null,
response: null,
incrementalPayloads: [],
};
}

Expand Down
Loading

0 comments on commit 0f5ab70

Please sign in to comment.