Clean up GraphQL explorer

This commit is contained in:
Gregory Schier
2025-07-08 07:44:50 -07:00
parent 6c0f9377cd
commit a3f50a2bb7
6 changed files with 220 additions and 174 deletions

View File

@@ -1,5 +1,3 @@
import type { GraphQLSchema } from 'graphql';
import { atom } from 'jotai'; import { atom } from 'jotai';
export const graphqlSchemaAtom = atom<GraphQLSchema | null>(null); export const showGraphQLDocExplorerAtom = atom<boolean>(false);
export const graphqlDocStateAtom = atom<boolean>(false);

View File

@@ -1,7 +1,7 @@
/* eslint-disable */ /* eslint-disable */
import { Color } from '@yaakapp-internal/plugins'; import { Color } from '@yaakapp-internal/plugins';
import classNames from 'classnames'; import classNames from 'classnames';
import type { GraphQLField, GraphQLInputField, GraphQLType } from 'graphql'; import type { GraphQLField, GraphQLInputField, GraphQLSchema, GraphQLType } from 'graphql';
import { import {
getNamedType, getNamedType,
isEnumType, isEnumType,
@@ -13,99 +13,88 @@ import {
isScalarType, isScalarType,
isUnionType, isUnionType,
} from 'graphql'; } from 'graphql';
import { useAtomValue } from 'jotai'; import { CSSProperties, memo, ReactNode, useState } from 'react';
import { ReactNode, useState } from 'react'; import { showGraphQLDocExplorerAtom } from '../atoms/graphqlSchemaAtom';
import { graphqlDocStateAtom, graphqlSchemaAtom } from '../atoms/graphqlSchemaAtom';
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from '../lib/jotai';
import { CountBadge } from './core/CountBadge'; import { CountBadge } from './core/CountBadge';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { Markdown } from './Markdown'; import { Markdown } from './Markdown';
interface Props {
style?: CSSProperties;
schema: GraphQLSchema;
className?: string;
}
type ExplorerItem = type ExplorerItem =
| { kind: 'type'; type: GraphQLType; from: ExplorerItem } | { kind: 'type'; type: GraphQLType; from: ExplorerItem }
| { kind: 'field'; type: GraphQLField<any, any>; from: ExplorerItem } | { kind: 'field'; type: GraphQLField<any, any>; from: ExplorerItem }
| { kind: 'input_field'; type: GraphQLInputField; from: ExplorerItem } | { kind: 'input_field'; type: GraphQLInputField; from: ExplorerItem }
| null; | null;
export function GraphQLDocsExplorer() { export const GraphQLDocsExplorer = memo(function ({ style, schema, className }: Props) {
const graphqlSchema = useAtomValue(graphqlSchemaAtom);
const [activeItem, setActiveItem] = useState<ExplorerItem>(null); const [activeItem, setActiveItem] = useState<ExplorerItem>(null);
if (!graphqlSchema) { const qryType = schema.getQueryType();
return <div className="p-4">No GraphQL schema available</div>; const mutType = schema.getMutationType();
} const subType = schema.getSubscriptionType();
const qryType = graphqlSchema.getQueryType();
const mutType = graphqlSchema.getMutationType();
const subType = graphqlSchema.getSubscriptionType();
const qryItem: ExplorerItem = qryType ? { kind: 'type', type: qryType, from: null } : null; const qryItem: ExplorerItem = qryType ? { kind: 'type', type: qryType, from: null } : null;
const mutItem: ExplorerItem = mutType ? { kind: 'type', type: mutType, from: null } : null; const mutItem: ExplorerItem = mutType ? { kind: 'type', type: mutType, from: null } : null;
const subItem: ExplorerItem = subType ? { kind: 'type', type: subType, from: null } : null; const subItem: ExplorerItem = subType ? { kind: 'type', type: subType, from: null } : null;
const allTypes = graphqlSchema.getTypeMap(); const allTypes = schema.getTypeMap();
return ( return (
<div className="relative w-full"> <div className={classNames(className, 'py-3 mx-3')} style={style}>
<IconButton <div className="h-full border border-dashed border-border rounded-lg">
icon="x" <GraphQLExplorerHeader item={activeItem} setItem={setActiveItem} />
size="sm" {activeItem == null ? (
className="!absolute right-2 top-0" <div className="flex flex-col gap-3 overflow-y-auto h-full w-full p-3">
title="Close documenation explorer" <Heading>Root Types</Heading>
onClick={() => { <GqlTypeRow
jotaiStore.set(graphqlDocStateAtom, false); name={{ value: 'query', color: 'primary' }}
}} item={qryItem}
/> setItem={setActiveItem}
{activeItem == null ? ( className="!my-0"
<div className="flex flex-col gap-3 overflow-auto h-full"> />
<Heading>Root Types</Heading> <GqlTypeRow
<GqlTypeRow name={{ value: 'mutation', color: 'primary' }}
name={{ value: 'query', color: 'primary' }} item={mutItem}
item={qryItem} setItem={setActiveItem}
setItem={setActiveItem} className="!my-0"
className="!my-0" />
/> <GqlTypeRow
<GqlTypeRow name={{ value: 'subscription', color: 'primary' }}
name={{ value: 'mutation', color: 'primary' }} item={subItem}
item={mutItem} setItem={setActiveItem}
setItem={setActiveItem} className="!my-0"
className="!my-0" />
/> <Subheading count={Object.keys(allTypes).length}>All Schema Types</Subheading>
<GqlTypeRow <DocMarkdown>{schema.description ?? null}</DocMarkdown>
name={{ value: 'subscription', color: 'primary' }} <div className="flex flex-col gap-1">
item={subItem} {Object.keys(allTypes).map((typeName) => {
setItem={setActiveItem} const t = allTypes[typeName]!;
className="!my-0" return (
/> <GqlTypeLink
<Subheading> key={t.name}
All Schema Types <CountBadge count={Object.keys(allTypes).length} /> color="notice"
</Subheading> item={{ kind: 'type', type: t, from: null }}
<Markdown>{graphqlSchema.description ?? null}</Markdown> setItem={setActiveItem}
<div className="flex flex-col gap-1"> />
{Object.keys(allTypes).map((typeName) => { );
const t = allTypes[typeName]!; })}
return ( </div>
<GqlTypeLink
key={t.name}
color="notice"
item={{ kind: 'type', type: t, from: null }}
setItem={setActiveItem}
/>
);
})}
</div> </div>
</div> ) : (
) : ( <div className="overflow-y-auto h-full w-full px-3 grid grid-cols-[minmax(0,1fr)]">
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)] gap-y-3"> <GqlTypeInfo item={activeItem} setItem={setActiveItem} schema={schema} />
<GraphQLExplorerHeader item={activeItem} setItem={setActiveItem} />
<div className="overflow-auto h-full max-h-full">
<GqlTypeInfo item={activeItem} setItem={setActiveItem} />
</div> </div>
</div> )}
)} </div>
</div> </div>
); );
} });
function GraphQLExplorerHeader({ function GraphQLExplorerHeader({
item, item,
@@ -114,12 +103,32 @@ function GraphQLExplorerHeader({
item: ExplorerItem; item: ExplorerItem;
setItem: (t: ExplorerItem) => void; setItem: (t: ExplorerItem) => void;
}) { }) {
if (item == null) return null;
return ( return (
<nav className="flex items-center gap-1"> <nav className="pl-2 pr-1 h-md grid grid-rows-1 grid-cols-[minmax(0,1fr)_auto] items-center gap-2 overflow-x-auto min-w-0 hide-scrollbars">
<Icon icon="chevron_left" color="secondary" /> <div className="w-full">
<GqlTypeLink item={item.from} setItem={setItem} /> {item == null ? (
<div className="flex items-center gap-2">
<Icon icon="house" color="secondary" />
<div className="text-text-subtle whitespace-nowrap _truncate">Schema Documentation</div>
</div>
) : (
<GqlTypeLink
item={item.from}
setItem={setItem}
className="text-text-subtle !font-sans !text-base"
leftSlot={<Icon icon="chevron_left" color="secondary" />}
/>
)}
</div>
<IconButton
icon="x"
size="sm"
className="text-text-subtle"
title="Close documenation explorer"
onClick={() => {
jotaiStore.set(showGraphQLDocExplorerAtom, false);
}}
/>
</nav> </nav>
); );
} }
@@ -127,11 +136,12 @@ function GraphQLExplorerHeader({
function GqlTypeInfo({ function GqlTypeInfo({
item, item,
setItem, setItem,
schema,
}: { }: {
item: ExplorerItem | null; item: ExplorerItem | null;
setItem: (t: ExplorerItem) => void; setItem: (t: ExplorerItem) => void;
schema: GraphQLSchema;
}) { }) {
const graphqlSchema = useAtomValue(graphqlSchemaAtom);
if (item == null) return null; if (item == null) return null;
const name = item.kind === 'type' ? getNamedType(item.type).name : item.type.name; const name = item.kind === 'type' ? getNamedType(item.type).name : item.type.name;
@@ -140,8 +150,8 @@ function GqlTypeInfo({
const heading = ( const heading = (
<div className="mb-3"> <div className="mb-3">
<h1 className="text-2xl font-semibold">{name}</h1> <Heading>{name}</Heading>
<Markdown className="!text-text-subtle italic">{description || 'No description'}</Markdown> <DocMarkdown>{description || 'No description'}</DocMarkdown>
</div> </div>
); );
@@ -150,19 +160,21 @@ function GqlTypeInfo({
} else if (isNonNullType(item.type) || isListType(item.type)) { } else if (isNonNullType(item.type) || isListType(item.type)) {
// kinda a hack, but we'll just unwrap there and show the named type // kinda a hack, but we'll just unwrap there and show the named type
return ( return (
<GqlTypeInfo item={{ ...item, kind: 'type', type: item.type.ofType }} setItem={setItem} /> <GqlTypeInfo
item={{ ...item, kind: 'type', type: item.type.ofType }}
setItem={setItem}
schema={schema}
/>
); );
} else if (isInterfaceType(item.type)) { } else if (isInterfaceType(item.type)) {
const fields = item.type.getFields(); const fields = item.type.getFields();
const possibleTypes = graphqlSchema?.getPossibleTypes(item.type) ?? []; const possibleTypes = schema.getPossibleTypes(item.type) ?? [];
return ( return (
<div> <div>
{heading} {heading}
<Subheading> <Subheading count={Object.keys(fields).length}>Fields</Subheading>
Fields <CountBadge count={Object.keys(fields).length} />
</Subheading>
{Object.keys(fields).map((fieldName) => { {Object.keys(fields).map((fieldName) => {
const field = fields[fieldName]!; const field = fields[fieldName]!;
const fieldItem: ExplorerItem = { kind: 'field', type: field, from: item }; const fieldItem: ExplorerItem = { kind: 'field', type: field, from: item };
@@ -180,7 +192,7 @@ function GqlTypeInfo({
{possibleTypes.length > 0 && ( {possibleTypes.length > 0 && (
<> <>
<Subheading>Implemented By</Subheading> <Subheading>Implemented By</Subheading>
{possibleTypes.map((t) => ( {possibleTypes.map((t: any) => (
<GqlTypeRow <GqlTypeRow
key={t.name} key={t.name}
item={{ kind: 'type', type: t, from: item }} item={{ kind: 'type', type: t, from: item }}
@@ -208,23 +220,15 @@ function GqlTypeInfo({
const values = item.type.getValues(); const values = item.type.getValues();
return ( return (
<div className="flex flex-col gap-3"> <div>
{heading} {heading}
<Subheading>Values</Subheading>
<div> {values.map((v) => (
<Subheading>Type</Subheading> <div key={v.name} className="my-4 font-mono text-editor _truncate">
<GqlTypeRow item={{ kind: 'type', type: item.type, from: item }} setItem={setItem} /> <span className="text-primary">{v.value}</span>
</div> <DocMarkdown>{v.description ?? null}</DocMarkdown>
</div>
<div> ))}
<Subheading>Values</Subheading>
{values.map((v) => (
<div key={v.name} className="my-4">
<span className="text-primary">{v.value}</span>
<Markdown className="!text-text-subtle">{v.description ?? null}</Markdown>
</div>
))}
</div>
</div> </div>
); );
} else if (item.kind === 'field') { } else if (item.kind === 'field') {
@@ -234,7 +238,11 @@ function GqlTypeInfo({
<div> <div>
<Subheading>Type</Subheading> <Subheading>Type</Subheading>
<GqlTypeRow item={{ kind: 'type', type: item.type.type, from: item }} setItem={setItem} /> <GqlTypeRow
className="mt-4"
item={{ kind: 'type', type: item.type.type, from: item }}
setItem={setItem}
/>
</div> </div>
{item.type.args.length > 0 && ( {item.type.args.length > 0 && (
@@ -259,9 +267,7 @@ function GqlTypeInfo({
<div> <div>
{heading} {heading}
<Subheading> <Subheading count={Object.keys(fields).length}>Fields</Subheading>
Fields <CountBadge count={Object.keys(fields).length} />
</Subheading>
{Object.keys(fields).map((fieldName) => { {Object.keys(fields).map((fieldName) => {
const field = fields[fieldName]; const field = fields[fieldName];
if (field == null) return null; if (field == null) return null;
@@ -288,9 +294,7 @@ function GqlTypeInfo({
<div> <div>
{heading} {heading}
<Subheading> <Subheading count={Object.keys(fields).length}>Fields</Subheading>
Fields <CountBadge count={Object.keys(fields).length} />
</Subheading>
{Object.keys(fields).map((fieldName) => { {Object.keys(fields).map((fieldName) => {
const field = fields[fieldName]; const field = fields[fieldName];
if (field == null) return null; if (field == null) return null;
@@ -331,9 +335,7 @@ function GqlTypeInfo({
</> </>
)} )}
<Subheading> <Subheading count={Object.keys(fields).length}>Fields</Subheading>
Fields <CountBadge count={Object.keys(fields).length} />
</Subheading>
{Object.keys(fields).map((fieldName) => { {Object.keys(fields).map((fieldName) => {
const field = fields[fieldName]; const field = fields[fieldName];
if (field == null) return null; if (field == null) return null;
@@ -378,7 +380,7 @@ function GqlTypeRow({
if (item.kind === 'type') { if (item.kind === 'type') {
child = ( child = (
<> <>
<div> <div className="font-mono text-editor">
{name && ( {name && (
<span <span
className={classNames( className={classNames(
@@ -390,16 +392,16 @@ function GqlTypeRow({
name?.color === 'info' && 'text-info', name?.color === 'info' && 'text-info',
)} )}
> >
{name.value}: {name.value}:&nbsp;
</span> </span>
)}{' '} )}
<GqlTypeLink color="notice" item={item} setItem={setItem} /> <GqlTypeLink color="notice" item={item} setItem={setItem} />
</div> </div>
{!hideDescription && ( {!hideDescription && (
<Markdown className="!text-text-subtle"> <DocMarkdown>
{(description === undefined ? getNamedType(item.type).description : description) ?? {(description === undefined ? getNamedType(item.type).description : description) ??
null} null}
</Markdown> </DocMarkdown>
)} )}
</> </>
); );
@@ -411,7 +413,7 @@ function GqlTypeRow({
}; };
child = ( child = (
<div> <div>
<div> <div className="font-mono text-editor">
<GqlTypeLink color="info" item={item} setItem={setItem}> <GqlTypeLink color="info" item={item} setItem={setItem}>
{name?.value} {name?.value}
</GqlTypeLink> </GqlTypeLink>
@@ -421,9 +423,10 @@ function GqlTypeRow({
{item.type.args.map((arg) => ( {item.type.args.map((arg) => (
<div <div
key={`${arg.type}::${arg.name}`} key={`${arg.type}::${arg.name}`}
className={classNames(item.type.args.length == 1 ? 'inline' : 'pl-3')} className={classNames(item.type.args.length == 1 && 'inline-flex')}
> >
<span className="text-primary">{arg.name}:</span>{' '} {item.type.args.length > 1 && <>&nbsp;&nbsp;</>}
<span className="text-primary">{arg.name}:</span>&nbsp;
<GqlTypeLink <GqlTypeLink
color="notice" color="notice"
item={{ kind: 'type', type: arg.type, from: item.from }} item={{ kind: 'type', type: arg.type, from: item.from }}
@@ -431,28 +434,30 @@ function GqlTypeRow({
/> />
</div> </div>
))} ))}
<span className="text-text-subtle">)</span>{' '} <span className="text-text-subtle">)</span>
</> </>
)} )}
<span className="text-text-subtle">:</span>{' '} <span className="text-text-subtle">:</span>{' '}
<GqlTypeLink color="notice" item={returnItem} setItem={setItem} /> <GqlTypeLink color="notice" item={returnItem} setItem={setItem} />
</div> </div>
<Markdown className="!text-text-subtle mt-0.5">{item.type.description ?? null}</Markdown> <DocMarkdown className="!text-text-subtle mt-0.5">
{item.type.description ?? null}
</DocMarkdown>
</div> </div>
); );
} else if (item.kind === 'input_field') { } else if (item.kind === 'input_field') {
child = ( child = (
<> <>
<div> <div className="font-mono text-editor">
{name && <span className="text-primary">{name.value}:</span>}{' '} {name && <span className="text-primary">{name.value}:</span>}{' '}
<GqlTypeLink color="notice" item={item} setItem={setItem} /> <GqlTypeLink color="notice" item={item} setItem={setItem} />
</div> </div>
<Markdown className="!text-text-subtle">{item.type.description ?? null}</Markdown> <DocMarkdown>{item.type.description ?? null}</DocMarkdown>
</> </>
); );
} }
return <div className={className}>{child}</div>; return <div className={classNames(className, 'w-full min-w-0')}>{child}</div>;
} }
function GqlTypeLink({ function GqlTypeLink({
@@ -460,37 +465,52 @@ function GqlTypeLink({
setItem, setItem,
color, color,
children, children,
leftSlot,
className,
}: { }: {
item: ExplorerItem; item: ExplorerItem;
color?: Color; color?: Color;
setItem: (item: ExplorerItem) => void; setItem: (item: ExplorerItem) => void;
children?: string; children?: ReactNode;
leftSlot?: ReactNode;
className?: string;
}) { }) {
if (item?.kind === 'type' && isListType(item.type)) { if (item?.kind === 'type' && isListType(item.type)) {
return ( return (
<> <span className="font-mono text-editor">
<span className="text-text-subtle">[</span> <span className="text-text-subtle">[</span>
<GqlTypeLink item={{ ...item, type: item.type.ofType }} setItem={setItem} color={color}> <GqlTypeLink
{children} item={{ ...item, type: item.type.ofType }}
</GqlTypeLink> setItem={setItem}
color={color}
leftSlot={leftSlot}
children={children}
/>
<span className="text-text-subtle">]</span> <span className="text-text-subtle">]</span>
</> </span>
); );
} else if (item?.kind === 'type' && isNonNullType(item.type)) { } else if (item?.kind === 'type' && isNonNullType(item.type)) {
return ( return (
<> <span className="font-mono text-editor">
<GqlTypeLink item={{ ...item, type: item.type.ofType }} setItem={setItem} color={color}> <GqlTypeLink
{children} item={{ ...item, type: item.type.ofType }}
</GqlTypeLink> setItem={setItem}
color={color}
leftSlot={leftSlot}
children={children}
/>
<span className="text-text-subtle">!</span> <span className="text-text-subtle">!</span>
</> </span>
); );
} }
return ( return (
<button <button
className={classNames( className={classNames(
'hover:underline text-left mr-auto', className,
'hover:underline text-left mr-auto gap-2 max-w-full',
'inline-flex items-center',
'font-mono text-editor _truncate',
color === 'danger' && 'text-danger', color === 'danger' && 'text-danger',
color === 'primary' && 'text-primary', color === 'primary' && 'text-primary',
color === 'success' && 'text-success', color === 'success' && 'text-success',
@@ -500,12 +520,13 @@ function GqlTypeLink({
)} )}
onClick={() => setItem(item)} onClick={() => setItem(item)}
> >
{leftSlot}
<GqlTypeLabel item={item} children={children} /> <GqlTypeLabel item={item} children={children} />
</button> </button>
); );
} }
function GqlTypeLabel({ item, children }: { item: ExplorerItem; children?: string }) { function GqlTypeLabel({ item, children }: { item: ExplorerItem; children?: ReactNode }) {
let inner; let inner;
if (children) { if (children) {
inner = children; inner = children;
@@ -517,13 +538,24 @@ function GqlTypeLabel({ item, children }: { item: ExplorerItem; children?: strin
inner = getNamedType(item.type.type).name; inner = getNamedType(item.type.type).name;
} }
return <>{inner}</>; return <div className="_truncate">{inner}</div>;
} }
function Subheading({ children }: { children: ReactNode }) { function Subheading({ children, count }: { children: ReactNode; count?: number }) {
return <h2 className="font-bold text-lg mt-6 flex items-center">{children}</h2>; return (
<h2 className="font-bold text-lg mt-6 flex items-center">
<div className="_truncate min-w-0">{children}</div>
{count && <CountBadge count={count} />}
</h2>
);
} }
function Heading({ children }: { children: ReactNode }) { function Heading({ children }: { children: ReactNode }) {
return <h1 className="font-bold text-2xl flex items-center">{children}</h1>; return <h1 className="font-bold text-2xl _truncate">{children}</h1>;
}
function DocMarkdown({ children, className }: { children: string | null; className?: string }) {
return (
<Markdown className={classNames(className, '!text-text-subtle italic')}>{children}</Markdown>
);
} }

View File

@@ -3,8 +3,10 @@ import type { HttpRequest } from '@yaakapp-internal/models';
import { updateSchema } from 'cm6-graphql'; import { updateSchema } from 'cm6-graphql';
import { formatSdl } from 'format-graphql'; import { formatSdl } from 'format-graphql';
import { useAtom } from 'jotai';
import { useEffect, useMemo, useRef } from 'react'; import { useEffect, useMemo, useRef } from 'react';
import { useLocalStorage } from 'react-use'; import { useLocalStorage } from 'react-use';
import { showGraphQLDocExplorerAtom } from '../atoms/graphqlSchemaAtom';
import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL'; import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL';
import { useStateWithDeps } from '../hooks/useStateWithDeps'; import { useStateWithDeps } from '../hooks/useStateWithDeps';
import { showDialog } from '../lib/dialog'; import { showDialog } from '../lib/dialog';
@@ -16,8 +18,6 @@ import { Editor } from './core/Editor/Editor';
import { FormattedError } from './core/FormattedError'; import { FormattedError } from './core/FormattedError';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { Separator } from './core/Separator'; import { Separator } from './core/Separator';
import { useAtom } from 'jotai';
import { graphqlDocStateAtom, graphqlSchemaAtom } from '../atoms/graphqlSchemaAtom';
type Props = Pick<EditorProps, 'heightMode' | 'className' | 'forceUpdateKey'> & { type Props = Pick<EditorProps, 'heightMode' | 'className' | 'forceUpdateKey'> & {
baseRequest: HttpRequest; baseRequest: HttpRequest;
@@ -47,8 +47,7 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
return { query: request.body.query ?? '', variables: request.body.variables ?? '' }; return { query: request.body.query ?? '', variables: request.body.variables ?? '' };
}, [extraEditorProps.forceUpdateKey]); }, [extraEditorProps.forceUpdateKey]);
const [, setGraphqlSchemaAtomValue] = useAtom(graphqlSchemaAtom); const [isDocOpen, setGraphqlDocStateAtomValue] = useAtom(showGraphQLDocExplorerAtom);
const [isDocOpen, setGraphqlDocStateAtomValue] = useAtom(graphqlDocStateAtom);
const handleChangeQuery = (query: string) => { const handleChangeQuery = (query: string) => {
const newBody = { query, variables: currentBody.variables || undefined }; const newBody = { query, variables: currentBody.variables || undefined };
@@ -66,8 +65,7 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
useEffect(() => { useEffect(() => {
if (editorViewRef.current == null) return; if (editorViewRef.current == null) return;
updateSchema(editorViewRef.current, schema ?? undefined); updateSchema(editorViewRef.current, schema ?? undefined);
setGraphqlSchemaAtomValue(schema); }, [schema]);
}, [schema, setGraphqlSchemaAtomValue]);
const actions = useMemo<EditorProps['actions']>( const actions = useMemo<EditorProps['actions']>(
() => [ () => [

View File

@@ -1,12 +1,15 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import React from 'react'; import React from 'react';
import type { HttpRequest } from '@yaakapp-internal/models'; import { showGraphQLDocExplorerAtom } from '../atoms/graphqlSchemaAtom';
import { useCurrentGraphQLSchema } from '../hooks/useIntrospectGraphQL';
import type { SlotProps } from './core/SplitLayout';
import { SplitLayout } from './core/SplitLayout'; import { SplitLayout } from './core/SplitLayout';
import { GraphQLDocsExplorer } from './GraphQLDocsExplorer'; import { GraphQLDocsExplorer } from './GraphQLDocsExplorer';
import { HttpRequestPane } from './HttpRequestPane'; import { HttpRequestPane } from './HttpRequestPane';
import { HttpResponsePane } from './HttpResponsePane'; import { HttpResponsePane } from './HttpResponsePane';
import { useAtomValue } from 'jotai';
import { graphqlDocStateAtom } from '../atoms/graphqlSchemaAtom';
interface Props { interface Props {
activeRequest: HttpRequest; activeRequest: HttpRequest;
@@ -15,9 +18,10 @@ interface Props {
export function HttpRequestLayout({ activeRequest, style }: Props) { export function HttpRequestLayout({ activeRequest, style }: Props) {
const { bodyType } = activeRequest; const { bodyType } = activeRequest;
const isDocOpen = useAtomValue(graphqlDocStateAtom); const showGraphQLDocExplorer = useAtomValue(showGraphQLDocExplorerAtom);
const graphQLSchema = useCurrentGraphQLSchema(activeRequest);
return ( const requestResponseSplit = ({ style }: Pick<SlotProps, 'style'>) => (
<SplitLayout <SplitLayout
name="http_layout" name="http_layout"
className="p-3 gap-1.5" className="p-3 gap-1.5"
@@ -29,20 +33,24 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
fullHeight={orientation === 'horizontal'} fullHeight={orientation === 'horizontal'}
/> />
)} )}
secondSlot={ secondSlot={({ style }) => (
bodyType === 'graphql' && isDocOpen <HttpResponsePane activeRequestId={activeRequest.id} style={style} />
? () => ( )}
<SplitLayout
name="http_response_layout"
className="gap-1.5"
firstSlot={({ style }) => (
<HttpResponsePane activeRequestId={activeRequest.id} style={style} />
)}
secondSlot={() => <GraphQLDocsExplorer key={activeRequest.id} />}
/>
)
: ({ style }) => <HttpResponsePane activeRequestId={activeRequest.id} style={style} />
}
/> />
); );
if (bodyType === 'graphql' && showGraphQLDocExplorer && graphQLSchema != null) {
return (
<SplitLayout
name="graphql_layout"
defaultRatio={0.25}
firstSlot={requestResponseSplit}
secondSlot={({ style, orientation }) => (
<GraphQLDocsExplorer key={activeRequest.id} schema={graphQLSchema} className={classNames(orientation == 'horizontal' && '!ml-0')} style={style} />
)}
/>
);
}
return requestResponseSplit({ style });
} }

View File

@@ -8,7 +8,7 @@ import { useContainerSize } from '../../hooks/useContainerQuery';
import { clamp } from '../../lib/clamp'; import { clamp } from '../../lib/clamp';
import { ResizeHandle } from '../ResizeHandle'; import { ResizeHandle } from '../ResizeHandle';
interface SlotProps { export interface SlotProps {
orientation: 'horizontal' | 'vertical'; orientation: 'horizontal' | 'vertical';
style: CSSProperties; style: CSSProperties;
} }
@@ -25,9 +25,10 @@ interface Props {
layout?: 'responsive' | 'vertical' | 'horizontal'; layout?: 'responsive' | 'vertical' | 'horizontal';
} }
const areaL = { gridArea: 'left' }; const baseProperties = { minWidth: 0 };
const areaR = { gridArea: 'right' }; const areaL = { ...baseProperties, gridArea: 'left' };
const areaD = { gridArea: 'drag' }; const areaR = { ...baseProperties, gridArea: 'right' };
const areaD = { ...baseProperties, gridArea: 'drag' };
const STACK_VERTICAL_WIDTH = 500; const STACK_VERTICAL_WIDTH = 500;
@@ -78,7 +79,7 @@ export function SplitLayout({
` `
: ` : `
' ${areaL.gridArea} ${areaD.gridArea} ${areaR.gridArea}' minmax(0,1fr) ' ${areaL.gridArea} ${areaD.gridArea} ${areaR.gridArea}' minmax(0,1fr)
/ ${1 - width}fr 0 ${width}fr / ${1 - width}fr 0 ${width}fr
`, `,
}; };
}, [style, vertical, height, minHeightPx, width]); }, [style, vertical, height, minHeightPx, width]);
@@ -144,7 +145,11 @@ export function SplitLayout({
const containerQueryReady = size.width > 0 || size.height > 0; const containerQueryReady = size.width > 0 || size.height > 0;
return ( return (
<div ref={containerRef} style={styles} className={classNames(className, 'grid w-full h-full overflow-hidden')}> <div
ref={containerRef}
style={styles}
className={classNames(className, 'grid w-full h-full overflow-hidden')}
>
{containerQueryReady && ( {containerQueryReady && (
<> <>
{firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })} {firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}

View File

@@ -125,3 +125,8 @@ export function useIntrospectGraphQL(
return { schema, isLoading, error, refetch, clear }; return { schema, isLoading, error, refetch, clear };
} }
export function useCurrentGraphQLSchema(request: HttpRequest) {
const { schema } = useIntrospectGraphQL(request, { disabled: true });
return schema;
}