Console 1493 schema explorer displaying argument descriptions (#7282)

This commit is contained in:
Jonathan Brennan 2025-11-24 17:24:42 -06:00 committed by GitHub
parent 714906f989
commit 5fec03c297
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 470 additions and 474 deletions

View file

@ -1,4 +1,4 @@
import React, { ReactElement, ReactNode, useLayoutEffect, useMemo, useRef, useState } from 'react';
import React, { ReactElement, ReactNode, useMemo } from 'react';
import { clsx } from 'clsx';
import { PulseIcon, UsersIcon } from '@/components/ui/icon';
import { Popover, PopoverArrow, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
@ -6,78 +6,24 @@ import { Skeleton } from '@/components/ui/skeleton';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Markdown } from '@/components/v2/markdown';
import { FragmentType, graphql, useFragment } from '@/gql';
import { SupergraphMetadataList_SupergraphMetadataFragmentFragment } from '@/gql/graphql';
import { formatNumber, toDecimal } from '@/lib/hooks';
import { cn } from '@/lib/utils';
import { capitalize } from '@/utils';
import { ChatBubbleIcon } from '@radix-ui/react-icons';
import { Link as NextLink, useRouter } from '@tanstack/react-router';
import { useArgumentListToggle, useSchemaExplorerContext } from './provider';
import { useDescriptionsVisibleToggle } from './provider';
import { SupergraphMetadataList } from './super-graph-metadata';
import { useExplorerFieldFiltering } from './utils';
const noop = () => {};
function useCollapsibleList<T>(list: ReadonlyArray<T>, max: number, defaultValue: boolean) {
const [collapsed, setCollapsed] = React.useState(defaultValue === true && list.length > max);
const expand = React.useCallback(() => {
setCollapsed(false);
}, [setCollapsed]);
if (collapsed) {
return [list.slice(0, max), collapsed, expand] as const;
}
return [list, collapsed, noop] as const;
}
function Description(props: { description: string }) {
return (
<Popover>
<PopoverTrigger asChild>
<button title="Description is available" className="text-gray-500 hover:text-white">
<ChatBubbleIcon className="h-5 w-auto" />
</button>
</PopoverTrigger>
<PopoverContent
className="max-w-screen-sm rounded-md p-4 text-sm shadow-md"
side="right"
sideOffset={5}
>
<PopoverArrow />
<Markdown content={props.description} />
</PopoverContent>
</Popover>
);
}
export function DescriptionInline(props: { description: string }) {
const ref = useRef<HTMLDivElement>(null);
const [canExpand, setCanExpand] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
useLayoutEffect(() => {
if (ref.current) {
setCanExpand(ref.current.scrollHeight > ref.current.clientHeight);
}
}, [props.description]);
export function Description(props: { description: string }) {
const { isDescriptionsVisible } = useDescriptionsVisibleToggle();
return (
<div className="inline-block max-w-screen-sm">
<Markdown
ref={ref}
className={clsx('text-muted-foreground line-clamp-2 text-left text-sm', {
'line-clamp-none': isExpanded,
})}
content={props.description}
/>
{canExpand ? (
<span
className="cursor-pointer text-xs text-orange-500"
onClick={() => setIsExpanded(prev => !prev)}
>
{isExpanded ? 'Show less' : 'Show more'}
</span>
) : null}
<div
className={clsx('mb-2 mt-0 block max-w-screen-sm', {
hidden: !isDescriptionsVisible,
})}
>
<Markdown className={clsx('text-left text-sm text-gray-400')} content={props.description} />
</div>
);
}
@ -142,7 +88,7 @@ export function SchemaExplorerUsageStats(props: {
</li>
</ul>
{Array.isArray(usage.topOperations) ? (
{Array.isArray(usage.topOperations) && (
<table className="mt-4 table-auto">
<thead>
<tr>
@ -177,7 +123,7 @@ export function SchemaExplorerUsageStats(props: {
))}
</tbody>
</table>
) : null}
)}
</div>
)}
</div>
@ -232,36 +178,6 @@ export function SchemaExplorerUsageStats(props: {
);
}
const GraphQLFields_FieldFragment = graphql(`
fragment GraphQLFields_FieldFragment on GraphQLField {
name
description
type
isDeprecated
deprecationReason
usage {
total
...SchemaExplorerUsageStats_UsageFragment
}
args {
...GraphQLArguments_ArgumentFragment
}
supergraphMetadata {
...SupergraphMetadataList_SupergraphMetadataFragment
}
}
`);
const GraphQLArguments_ArgumentFragment = graphql(`
fragment GraphQLArguments_ArgumentFragment on GraphQLArgument {
name
description
type
isDeprecated
deprecationReason
}
`);
const GraphQLInputFields_InputFieldFragment = graphql(`
fragment GraphQLInputFields_InputFieldFragment on GraphQLInputField {
name
@ -331,12 +247,13 @@ export function GraphQLTypeCard(props: {
GraphQLTypeCard_SupergraphMetadataFragment,
props.supergraphMetadata,
);
return (
<div className="rounded-md border-2 border-gray-900">
<div className="flex flex-row justify-between p-4">
<div>
<div className="flex flex-row items-center gap-2">
<div className="font-normal text-gray-500">{props.kind}</div>
<div className="font-normal text-gray-400">{props.kind}</div>
<div className="font-semibold">
<GraphQLTypeAsLink
organizationSlug={props.organizationSlug}
@ -346,10 +263,10 @@ export function GraphQLTypeCard(props: {
/>
</div>
</div>
{props.description ? <DescriptionInline description={props.description} /> : null}
{props.description && <Description description={props.description} />}
</div>
{Array.isArray(props.implements) && props.implements.length > 0 ? (
<div className="flex flex-row items-center text-sm text-gray-500">
{Array.isArray(props.implements) && props.implements.length > 0 && (
<div className="flex flex-row items-center text-sm text-gray-400">
<div className="mx-2">implements</div>
<div className="flex flex-row gap-2">
{props.implements.map(t => (
@ -363,8 +280,8 @@ export function GraphQLTypeCard(props: {
))}
</div>
</div>
) : null}
{props.usage && typeof props.totalRequests !== 'undefined' ? (
)}
{props.usage && typeof props.totalRequests !== 'undefined' && (
<SchemaExplorerUsageStats
kindLabel={props.kind}
totalRequests={props.totalRequests}
@ -373,123 +290,21 @@ export function GraphQLTypeCard(props: {
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
/>
) : null}
{supergraphMetadata ? (
)}
{supergraphMetadata && (
<SupergraphMetadataList
targetSlug={props.targetSlug}
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}
supergraphMetadata={supergraphMetadata}
/>
) : null}
)}
</div>
<div>{props.children}</div>
</div>
);
}
function GraphQLArguments(props: {
parentCoordinate: string;
args: FragmentType<typeof GraphQLArguments_ArgumentFragment>[];
styleDeprecated: boolean;
organizationSlug: string;
projectSlug: string;
targetSlug: string;
}) {
const args = useFragment(GraphQLArguments_ArgumentFragment, props.args);
const [isCollapsedGlobally] = useArgumentListToggle();
const [collapsed, setCollapsed] = React.useState(isCollapsedGlobally);
const hasMoreThanTwo = args.length > 2;
const showAll = hasMoreThanTwo && !collapsed;
React.useEffect(() => {
setCollapsed(isCollapsedGlobally);
}, [isCollapsedGlobally, setCollapsed]);
if (showAll) {
return (
<span className="ml-1 text-gray-500">
<span>(</span>
<div className="pl-4 text-gray-500">
{args.map(arg => {
const coordinate = `${props.parentCoordinate}.${arg.name}`;
return (
<div key={arg.name}>
<DeprecationNote
styleDeprecated={props.styleDeprecated}
deprecationReason={arg.deprecationReason}
>
<LinkToCoordinatePage
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
coordinate={coordinate}
>
{arg.name}
</LinkToCoordinatePage>
</DeprecationNote>
{': '}
<GraphQLTypeAsLink
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
type={arg.type}
/>
{arg.description ? <Description description={arg.description} /> : null}
</div>
);
})}
</div>
<span>)</span>
</span>
);
}
return (
<span className="ml-1 text-gray-500">
<span>(</span>
<span className="space-x-2">
{args.slice(0, 2).map(arg => {
const coordinate = `${props.parentCoordinate}.${arg.name}`;
return (
<span key={arg.name}>
<DeprecationNote
styleDeprecated={props.styleDeprecated}
deprecationReason={arg.deprecationReason}
>
<LinkToCoordinatePage
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
coordinate={coordinate}
>
{arg.name}
</LinkToCoordinatePage>
</DeprecationNote>
{': '}
<GraphQLTypeAsLink
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
type={arg.type}
/>
</span>
);
})}
{hasMoreThanTwo ? (
<span
className="cursor-pointer rounded bg-gray-900 p-1 text-xs text-gray-300 hover:bg-gray-700 hover:text-white"
onClick={() => setCollapsed(prev => !prev)}
>
{props.args.length - 2} hidden
</span>
) : null}
</span>
<span>)</span>
</span>
);
}
export function GraphQLTypeCardListItem(props: {
children: ReactNode;
index: number;
@ -510,163 +325,6 @@ export function GraphQLTypeCardListItem(props: {
);
}
export function GraphQLFields(props: {
typeName: string;
fields: Array<FragmentType<typeof GraphQLFields_FieldFragment>>;
totalRequests?: number;
collapsed?: boolean;
targetSlug: string;
projectSlug: string;
organizationSlug: string;
filterValue?: string;
warnAboutUnusedArguments: boolean;
warnAboutDeprecatedArguments: boolean;
styleDeprecated: boolean;
}) {
const { totalRequests, filterValue /** filterMeta */ } = props;
const fieldsFromFragment = useFragment(GraphQLFields_FieldFragment, props.fields);
const { hasMetadataFilter, metadata: filterMeta } = useSchemaExplorerContext();
const sortedAndFilteredFields = useMemo(() => {
return fieldsFromFragment
.filter(field => {
let matchesFilter = true;
if (filterValue) {
matchesFilter &&= field.name.toLowerCase().includes(filterValue);
}
if (filterMeta.length) {
const matchesMeta =
field.supergraphMetadata &&
(
field.supergraphMetadata as SupergraphMetadataList_SupergraphMetadataFragmentFragment
).metadata?.some(m => hasMetadataFilter(m.name, m.content));
matchesFilter &&= !!matchesMeta;
}
return matchesFilter;
})
.sort(
// Sort by usage DESC, name ASC
(a, b) => b.usage.total - a.usage.total || a.name.localeCompare(b.name),
);
}, [fieldsFromFragment, filterValue, filterMeta]);
const [fields, collapsed, expand] = useCollapsibleList(
sortedAndFilteredFields,
5,
props.collapsed ?? false,
);
return (
<TooltipProvider delayDuration={0}>
<div className="flex flex-col">
{fields.map((field, i) => {
const coordinate = `${props.typeName}.${field.name}`;
const isUsed = field.usage.total > 0;
const hasUnusedArguments = field.args.length > 0;
const showsUnusedSchema = typeof totalRequests !== 'number';
const isDeprecated = field.isDeprecated;
return (
<GraphQLTypeCardListItem key={field.name} index={i}>
<div className="w-full">
<div className="flex w-full flex-row items-center justify-between">
<div>
{props.warnAboutUnusedArguments &&
isUsed &&
hasUnusedArguments &&
showsUnusedSchema ? (
<Tooltip>
<TooltipContent>
This field is used but the presented arguments are not.
</TooltipContent>
<TooltipTrigger>
<span className="mr-1 text-sm text-orange-500">*</span>
</TooltipTrigger>
</Tooltip>
) : null}
{props.warnAboutDeprecatedArguments && !isDeprecated ? (
<Tooltip>
<TooltipContent>
This field is not deprecated but the presented arguments are.
</TooltipContent>
<TooltipTrigger>
<span className="mr-1 text-sm text-orange-500">*</span>
</TooltipTrigger>
</Tooltip>
) : null}
<DeprecationNote
styleDeprecated={props.styleDeprecated}
deprecationReason={field.deprecationReason}
>
<LinkToCoordinatePage
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
coordinate={coordinate}
className="font-semibold"
>
{field.name}
</LinkToCoordinatePage>
</DeprecationNote>
{field.args.length > 0 ? (
<GraphQLArguments
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
styleDeprecated={props.styleDeprecated}
parentCoordinate={coordinate}
args={field.args}
/>
) : null}
<span className="mr-1">:</span>
<GraphQLTypeAsLink
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
className="font-semibold text-gray-400"
type={field.type}
/>
</div>
<div className="flex flex-row items-center">
{field.supergraphMetadata ? (
<div className="ml-1">
<SupergraphMetadataList
targetSlug={props.targetSlug}
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}
supergraphMetadata={field.supergraphMetadata}
/>
</div>
) : null}
{typeof totalRequests === 'number' ? (
<SchemaExplorerUsageStats
totalRequests={totalRequests}
usage={field.usage}
targetSlug={props.targetSlug}
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}
/>
) : null}
</div>
</div>
{field.description ? <DescriptionInline description={field.description} /> : null}
</div>
</GraphQLTypeCardListItem>
);
})}
{collapsed && sortedAndFilteredFields.length > fields.length ? (
<GraphQLTypeCardListItem
index={fields.length}
className="cursor-pointer font-semibold hover:bg-gray-800"
onClick={expand}
>
Show {sortedAndFilteredFields.length - fields.length} more fields
</GraphQLTypeCardListItem>
) : null}
</div>
</TooltipProvider>
);
}
export function GraphQLInputFields(props: {
typeName: string;
fields: FragmentType<typeof GraphQLInputFields_InputFieldFragment>[];
@ -675,33 +333,12 @@ export function GraphQLInputFields(props: {
projectSlug: string;
organizationSlug: string;
styleDeprecated: boolean;
filterValue?: string;
}): ReactElement {
const fields = useFragment(GraphQLInputFields_InputFieldFragment, props.fields);
const { filterValue } = props;
const { hasMetadataFilter, metadata: filterMeta } = useSchemaExplorerContext();
const sortedAndFilteredFields = useMemo(() => {
return fields
.filter(field => {
let matchesFilter = true;
if (filterValue) {
matchesFilter &&= field.name.toLowerCase().includes(filterValue);
}
if (filterMeta.length) {
const matchesMeta =
field.supergraphMetadata &&
(
field.supergraphMetadata as SupergraphMetadataList_SupergraphMetadataFragmentFragment
).metadata?.some(m => hasMetadataFilter(m.name, m.content));
matchesFilter &&= !!matchesMeta;
}
return matchesFilter;
})
.sort(
// Sort by usage DESC, name ASC
(a, b) => b.usage.total - a.usage.total || a.name.localeCompare(b.name),
);
}, [fields, filterValue, filterMeta]);
const sortedAndFilteredFields = useExplorerFieldFiltering({
fields,
});
return (
<div className="flex flex-col">
@ -735,7 +372,7 @@ export function GraphQLInputFields(props: {
type={field.type}
/>
</div>
{typeof props.totalRequests === 'number' ? (
{typeof props.totalRequests === 'number' && (
<SchemaExplorerUsageStats
totalRequests={props.totalRequests}
usage={field.usage}
@ -743,9 +380,9 @@ export function GraphQLInputFields(props: {
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}
/>
) : null}
)}
</div>
{field.description ? <DescriptionInline description={field.description} /> : null}
{field.description && <Description description={field.description} />}
</div>
</GraphQLTypeCardListItem>
);
@ -754,7 +391,7 @@ export function GraphQLInputFields(props: {
);
}
function GraphQLTypeAsLink(props: {
export function GraphQLTypeAsLink(props: {
type: string;
className?: string;
organizationSlug: string;

View file

@ -3,7 +3,7 @@ import { FragmentType, graphql, useFragment } from '@/gql';
import { useRouter } from '@tanstack/react-router';
import {
DeprecationNote,
DescriptionInline,
Description,
GraphQLTypeCard,
GraphQLTypeCardListItem,
LinkToCoordinatePage,
@ -99,16 +99,16 @@ export function GraphQLEnumTypeComponent(props: {
{value.name}
</LinkToCoordinatePage>
</DeprecationNote>
{value.description ? <DescriptionInline description={value.description} /> : null}
{value.description && <Description description={value.description} />}
</div>
{value.supergraphMetadata ? (
{value.supergraphMetadata && (
<SupergraphMetadataList
targetSlug={props.targetSlug}
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}
supergraphMetadata={value.supergraphMetadata}
/>
) : null}
)}
</GraphQLTypeCardListItem>
))}
</div>

View file

@ -27,7 +27,11 @@ import {
useLocation,
useRouter,
} from '@tanstack/react-router';
import { useArgumentListToggle, usePeriodSelector, useSchemaExplorerContext } from './provider';
import {
useDescriptionsVisibleToggle,
usePeriodSelector,
useSchemaExplorerContext,
} from './provider';
const TypeFilter_AllTypes = graphql(`
query TypeFilter_AllTypes(
@ -195,28 +199,28 @@ export function DateRangeFilter() {
);
}
export function ArgumentVisibilityFilter() {
const [collapsed, toggleCollapsed] = useArgumentListToggle();
export function DescriptionsVisibilityFilter() {
const { isDescriptionsVisible, toggleDescriptionsVisible } = useDescriptionsVisibleToggle();
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="bg-secondary flex h-[40px] flex-row items-center gap-x-4 rounded-md border px-3">
<div>
<Label htmlFor="filter-toggle-arguments" className="text-sm font-normal">
All arguments
<Label htmlFor="filter-toggle-descriptions" className="text-sm font-normal">
Show descriptions
</Label>
</div>
<Switch
checked={!collapsed}
onCheckedChange={toggleCollapsed}
id="filter-toggle-arguments"
checked={isDescriptionsVisible}
onCheckedChange={toggleDescriptionsVisible}
id="filter-toggle-descriptions"
/>
</div>
</TooltipTrigger>
<TooltipContent>
List of arguments is collapsed by default. You can toggle this setting to display all
arguments.
Descriptions are not visible by default. You can toggle this setting to display all
descriptions.
</TooltipContent>
</Tooltip>
</TooltipProvider>
@ -320,7 +324,7 @@ export function MetadataFilter(props: { options: Array<{ name: string; values: s
>
{props.options.map(({ name, values }, i) => (
<React.Fragment key={name}>
{i > 0 ? <DropdownMenuSeparator /> : null}
{i > 0 && <DropdownMenuSeparator />}
<DropdownMenuGroup
className="flex cursor-pointer overflow-x-hidden text-sm text-gray-400 hover:underline"
onClick={() => {

View file

@ -0,0 +1,66 @@
import { FragmentType, graphql, useFragment } from '@/gql';
import { DeprecationNote, Description, GraphQLTypeAsLink, LinkToCoordinatePage } from './common';
import { useDescriptionsVisibleToggle } from './provider';
export const GraphQLArguments_ArgumentFragment = graphql(`
fragment GraphQLArguments_ArgumentFragment on GraphQLArgument {
name
description
type
isDeprecated
deprecationReason
}
`);
export function GraphQLArguments(props: {
parentCoordinate: string;
args: FragmentType<typeof GraphQLArguments_ArgumentFragment>[];
styleDeprecated: boolean;
organizationSlug: string;
projectSlug: string;
targetSlug: string;
}) {
const args = useFragment(GraphQLArguments_ArgumentFragment, props.args);
const { isDescriptionsVisible } = useDescriptionsVisibleToggle();
return (
<span className="ml-1 text-gray-400">
<span>(</span>
<div className="pl-4 text-gray-300">
{args.map(arg => {
const coordinate = `${props.parentCoordinate}.${arg.name}`;
return (
<div key={arg.name}>
<DeprecationNote
styleDeprecated={props.styleDeprecated}
deprecationReason={arg.deprecationReason}
>
<LinkToCoordinatePage
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
coordinate={coordinate}
>
{arg.name}
</LinkToCoordinatePage>
</DeprecationNote>
{': '}
<GraphQLTypeAsLink
className="font-medium"
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
type={arg.type}
/>
{arg.description && isDescriptionsVisible && (
<Description description={arg.description} />
)}
</div>
);
})}
</div>
<span>)</span>
</span>
);
}

View file

@ -0,0 +1,154 @@
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { FragmentType, graphql, useFragment } from '@/gql';
import {
DeprecationNote,
Description,
GraphQLTypeAsLink,
GraphQLTypeCardListItem,
LinkToCoordinatePage,
SchemaExplorerUsageStats,
} from './common';
import { GraphQLArguments } from './graphql-arguments';
import { SupergraphMetadataList } from './super-graph-metadata';
import { useExplorerFieldFiltering } from './utils';
const GraphQLFields_FieldFragment = graphql(`
fragment GraphQLFields_FieldFragment on GraphQLField {
name
description
type
isDeprecated
deprecationReason
usage {
total
...SchemaExplorerUsageStats_UsageFragment
}
args {
...GraphQLArguments_ArgumentFragment
}
supergraphMetadata {
...SupergraphMetadataList_SupergraphMetadataFragment
}
}
`);
export function GraphQLFields(props: {
typeName: string;
fields: Array<FragmentType<typeof GraphQLFields_FieldFragment>>;
totalRequests?: number;
targetSlug: string;
projectSlug: string;
organizationSlug: string;
warnAboutUnusedArguments: boolean;
warnAboutDeprecatedArguments: boolean;
styleDeprecated: boolean;
}) {
const { totalRequests } = props;
const fieldsFromFragment = useFragment(GraphQLFields_FieldFragment, props.fields);
const sortedAndFilteredFields = useExplorerFieldFiltering({
fields: fieldsFromFragment,
});
return (
<TooltipProvider delayDuration={0}>
<div className="flex flex-col">
{sortedAndFilteredFields.map((field, i) => {
const coordinate = `${props.typeName}.${field.name}`;
const isUsed = field.usage.total > 0;
const hasArguments = field.args.length > 0;
const showsUnusedSchema = typeof totalRequests !== 'number';
const isDeprecated = field.isDeprecated;
return (
<GraphQLTypeCardListItem key={field.name} index={i}>
<div className="w-full">
<div className="flex w-full flex-row items-baseline justify-between">
<div>
{props.warnAboutUnusedArguments &&
isUsed &&
hasArguments &&
showsUnusedSchema && (
<Tooltip>
<TooltipContent>
This field is used but the presented arguments are not.
</TooltipContent>
<TooltipTrigger>
<span className="mr-1 text-sm text-orange-500">*</span>
</TooltipTrigger>
</Tooltip>
)}
{props.warnAboutDeprecatedArguments && !isDeprecated && (
<Tooltip>
<TooltipContent>
This field is not deprecated but the presented arguments are.
</TooltipContent>
<TooltipTrigger>
<span className="mr-1 text-sm text-orange-500">*</span>
</TooltipTrigger>
</Tooltip>
)}
<DeprecationNote
styleDeprecated={props.styleDeprecated}
deprecationReason={field.deprecationReason}
>
<LinkToCoordinatePage
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
coordinate={coordinate}
className="font-semibold"
>
{field.name}
</LinkToCoordinatePage>
</DeprecationNote>
{field.args.length > 0 && (
<GraphQLArguments
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
styleDeprecated={props.styleDeprecated}
parentCoordinate={coordinate}
args={field.args}
/>
)}
<span className="mr-1">:</span>
<GraphQLTypeAsLink
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
className="font-semibold text-gray-300"
type={field.type}
/>
</div>
<div className="flex flex-row items-center">
{field.supergraphMetadata && (
<div className="ml-1">
<SupergraphMetadataList
targetSlug={props.targetSlug}
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}
supergraphMetadata={field.supergraphMetadata}
/>
</div>
)}
{typeof totalRequests === 'number' && (
<SchemaExplorerUsageStats
totalRequests={totalRequests}
usage={field.usage}
targetSlug={props.targetSlug}
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}
/>
)}
</div>
</div>
{field.description && <Description description={field.description} />}
</div>
</GraphQLTypeCardListItem>
);
})}
</div>
</TooltipProvider>
);
}

View file

@ -1,6 +1,6 @@
import { FragmentType, graphql, useFragment } from '@/gql';
import { useRouter } from '@tanstack/react-router';
import { GraphQLFields, GraphQLTypeCard } from './common';
import { GraphQLTypeCard } from './common';
import { GraphQLFields } from './graphql-fields';
const GraphQLInterfaceTypeComponent_TypeFragment = graphql(`
fragment GraphQLInterfaceTypeComponent_TypeFragment on GraphQLInterfaceType {
@ -29,12 +29,6 @@ export function GraphQLInterfaceTypeComponent(props: {
warnAboutDeprecatedArguments: boolean;
styleDeprecated: boolean;
}) {
const router = useRouter();
const searchObj = router.latestLocation.search;
const search =
'search' in searchObj && typeof searchObj.search === 'string'
? searchObj.search.toLowerCase()
: undefined;
const ttype = useFragment(GraphQLInterfaceTypeComponent_TypeFragment, props.type);
return (
<GraphQLTypeCard
@ -50,7 +44,6 @@ export function GraphQLInterfaceTypeComponent(props: {
<GraphQLFields
typeName={ttype.name}
fields={ttype.fields}
filterValue={search}
totalRequests={props.totalRequests}
targetSlug={props.targetSlug}
projectSlug={props.projectSlug}

View file

@ -1,6 +1,6 @@
import { FragmentType, graphql, useFragment } from '@/gql';
import { useRouter } from '@tanstack/react-router';
import { GraphQLFields, GraphQLTypeCard } from './common';
import { GraphQLTypeCard } from './common';
import { GraphQLFields } from './graphql-fields';
const GraphQLObjectTypeComponent_TypeFragment = graphql(`
fragment GraphQLObjectTypeComponent_TypeFragment on GraphQLObjectType {
@ -22,7 +22,6 @@ const GraphQLObjectTypeComponent_TypeFragment = graphql(`
export function GraphQLObjectTypeComponent(props: {
type: FragmentType<typeof GraphQLObjectTypeComponent_TypeFragment>;
totalRequests?: number;
collapsed?: boolean;
organizationSlug: string;
projectSlug: string;
targetSlug: string;
@ -31,12 +30,6 @@ export function GraphQLObjectTypeComponent(props: {
styleDeprecated: boolean;
}) {
const ttype = useFragment(GraphQLObjectTypeComponent_TypeFragment, props.type);
const router = useRouter();
const searchObj = router.latestLocation.search;
const search =
'search' in searchObj && typeof searchObj.search === 'string'
? searchObj.search.toLowerCase()
: undefined;
return (
<GraphQLTypeCard
@ -52,9 +45,7 @@ export function GraphQLObjectTypeComponent(props: {
<GraphQLFields
typeName={ttype.name}
fields={ttype.fields}
filterValue={search}
totalRequests={props.totalRequests}
collapsed={props.collapsed}
targetSlug={props.targetSlug}
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}

View file

@ -16,8 +16,8 @@ import { useSearchParamsFilter } from '@/lib/hooks/use-search-params-filters';
import { UTCDate } from '@date-fns/utc';
type SchemaExplorerContextType = {
isArgumentListCollapsed: boolean;
setArgumentListCollapsed(isCollapsed: boolean): void;
isDescriptionsVisible: boolean;
setDescriptionsVisible(isCollapsed: boolean): void;
setDataRetentionInDays(days: number): void;
dataRetentionInDays: number;
startDate: Date;
@ -41,8 +41,8 @@ const defaultPeriod: Period = {
};
const SchemaExplorerContext = createContext<SchemaExplorerContextType>({
isArgumentListCollapsed: true,
setArgumentListCollapsed: () => {},
isDescriptionsVisible: true,
setDescriptionsVisible: () => {},
dataRetentionInDays: 7,
startDate: startOfDay(subDays(new UTCDate(), 7)),
period: defaultPeriod,
@ -72,9 +72,9 @@ export function SchemaExplorerProvider({ children }: { children: ReactNode }): R
[dataRetentionInDays],
);
const [isArgumentListCollapsed, setArgumentListCollapsed] = useLocalStorageJson(
const [isDescriptionsVisible, setDescriptionsVisible] = useLocalStorageJson(
'hive:schema-explorer:collapsed',
z.boolean().default(true),
z.boolean().default(false),
);
const [period, setPeriod] = useLocalStorageJson(
'hive:schema-explorer:period-1',
@ -86,8 +86,8 @@ export function SchemaExplorerProvider({ children }: { children: ReactNode }): R
return (
<SchemaExplorerContext.Provider
value={{
isArgumentListCollapsed,
setArgumentListCollapsed,
isDescriptionsVisible,
setDescriptionsVisible,
period,
setPeriod(period) {
setPeriod(period);
@ -142,13 +142,13 @@ export function useSchemaExplorerContext() {
return useContext(SchemaExplorerContext);
}
export function useArgumentListToggle() {
const { isArgumentListCollapsed, setArgumentListCollapsed } = useSchemaExplorerContext();
const toggle = useCallback(() => {
setArgumentListCollapsed(!isArgumentListCollapsed);
}, [setArgumentListCollapsed, isArgumentListCollapsed]);
export function useDescriptionsVisibleToggle() {
const { isDescriptionsVisible, setDescriptionsVisible } = useSchemaExplorerContext();
const toggleDescriptionsVisible = useCallback(() => {
setDescriptionsVisible(!isDescriptionsVisible);
}, [setDescriptionsVisible, isDescriptionsVisible]);
return [isArgumentListCollapsed, toggle] as const;
return { isDescriptionsVisible, toggleDescriptionsVisible };
}
export function usePeriodSelector() {

View file

@ -34,9 +34,9 @@ export function GraphQLScalarTypeComponent(props: {
>
<div className="flex flex-row justify-between p-4">
<div className="max-w-2xl grow text-sm">
{typeof ttype.description === 'string' ? <Markdown content={ttype.description} /> : null}
{typeof ttype.description === 'string' && <Markdown content={ttype.description} />}
</div>
{typeof props.totalRequests === 'number' ? (
{typeof props.totalRequests === 'number' && (
<SchemaExplorerUsageStats
totalRequests={props.totalRequests}
usage={ttype.usage}
@ -44,7 +44,7 @@ export function GraphQLScalarTypeComponent(props: {
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}
/>
) : null}
)}
</div>
</GraphQLTypeCard>
);

View file

@ -60,7 +60,7 @@ function SubgraphChip(props: {
>
{props.text}
<PackageIcon size={10} className="ml-1 inline-block" />
{props.metadata?.length ? <span className="inline-block text-[8px] font-bold">*</span> : null}
{props.metadata?.length && <span className="inline-block text-[8px] font-bold">*</span>}
</Link>
);
@ -196,7 +196,7 @@ export function SupergraphMetadataList(props: {
<div className="flex w-full justify-end">
{meta}
{previewItems}{' '}
{allItems ? (
{allItems && (
<Tooltip
content={
<>
@ -220,7 +220,7 @@ export function SupergraphMetadataList(props: {
+ {allItems.length - previewItems.length} more
</span>
</Tooltip>
) : null}
)}
</div>
);
}

View file

@ -47,7 +47,7 @@ export function GraphQLUnionTypeComponent(props: {
{ttype.members.map((member, i) => (
<GraphQLTypeCardListItem key={member.name} index={i}>
<div>{member.name}</div>
{typeof props.totalRequests === 'number' ? (
{typeof props.totalRequests === 'number' && (
<SchemaExplorerUsageStats
totalRequests={props.totalRequests}
usage={member.usage}
@ -55,15 +55,15 @@ export function GraphQLUnionTypeComponent(props: {
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}
/>
) : null}
{member.supergraphMetadata ? (
)}
{member.supergraphMetadata && (
<SupergraphMetadataList
targetSlug={props.targetSlug}
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}
supergraphMetadata={member.supergraphMetadata}
/>
) : null}
)}
</GraphQLTypeCardListItem>
))}
</div>

View file

@ -0,0 +1,41 @@
import { useMemo } from 'react';
import { useRouter } from '@tanstack/react-router';
import {
GraphQlFields_FieldFragmentFragment,
GraphQlInputFields_InputFieldFragmentFragment,
SupergraphMetadataList_SupergraphMetadataFragmentFragment,
} from '../../../gql/graphql';
import { useSchemaExplorerContext } from './provider';
export function useExplorerFieldFiltering<
T extends GraphQlFields_FieldFragmentFragment | GraphQlInputFields_InputFieldFragmentFragment,
>({ fields }: { fields: T[] }) {
const { hasMetadataFilter, metadata: filterMeta } = useSchemaExplorerContext();
const router = useRouter();
const searchObj = router.latestLocation.search;
const search =
'search' in searchObj && typeof searchObj.search === 'string'
? searchObj.search.toLowerCase()
: undefined;
return useMemo(() => {
return fields
.filter(field => {
let doesMatchFilter = true;
if (search) {
doesMatchFilter &&= field.name.toLowerCase().includes(search);
}
if (filterMeta.length) {
const doesMatchMeta =
field.supergraphMetadata &&
(
field.supergraphMetadata as SupergraphMetadataList_SupergraphMetadataFragmentFragment
).metadata?.some(m => hasMetadataFilter(m.name, m.content));
doesMatchFilter &&= !!doesMatchMeta;
}
return doesMatchFilter;
})
.sort((a, b) => b.usage.total - a.usage.total || a.name.localeCompare(b.name));
}, [fields, search, filterMeta, hasMetadataFilter]);
}

View file

@ -7,8 +7,8 @@ import {
} from '@/components/target/explorer/common';
import { GraphQLEnumTypeComponent } from '@/components/target/explorer/enum-type';
import {
ArgumentVisibilityFilter,
DateRangeFilter,
DescriptionsVisibilityFilter,
FieldByNameFilter,
MetadataFilter,
SchemaVariantFilter,
@ -237,7 +237,7 @@ function TypeExplorerPageContent(props: {
/>
<FieldByNameFilter />
<DateRangeFilter />
<ArgumentVisibilityFilter />
<DescriptionsVisibilityFilter />
<SchemaVariantFilter
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}

View file

@ -7,8 +7,8 @@ import {
GraphQLTypeCardSkeleton,
} from '@/components/target/explorer/common';
import {
ArgumentVisibilityFilter,
DateRangeFilter,
DescriptionsVisibilityFilter,
FieldByNameFilter,
MetadataFilter,
SchemaVariantFilter,
@ -60,7 +60,6 @@ function SchemaView(props: {
<GraphQLObjectTypeComponent
type={query}
totalRequests={totalRequests}
collapsed
targetSlug={props.targetSlug}
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}
@ -73,7 +72,6 @@ function SchemaView(props: {
<GraphQLObjectTypeComponent
type={mutation}
totalRequests={totalRequests}
collapsed
targetSlug={props.targetSlug}
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}
@ -86,7 +84,6 @@ function SchemaView(props: {
<GraphQLObjectTypeComponent
type={subscription}
totalRequests={totalRequests}
collapsed
targetSlug={props.targetSlug}
projectSlug={props.projectSlug}
organizationSlug={props.organizationSlug}
@ -214,7 +211,7 @@ function ExplorerPageContent(props: {
/>
<FieldByNameFilter />
<DateRangeFilter />
<ArgumentVisibilityFilter />
<DescriptionsVisibilityFilter />
<SchemaVariantFilter
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}

View file

@ -158,7 +158,17 @@ type Review @key(fields: "id") {
}
extend type Query {
notification(id: ID!): Notification
notification(
"""
Unique identifier of the notification to retrieve
"""
id: ID!
): Notification
notifications: [Notification!]!
searchNotifications(filter: NotificationFilterInput!): [Notification!]!
searchNotifications(
"""
Filter criteria including search text, notification types, and date range
"""
filter: NotificationFilterInput!
): [Notification!]!
}

View file

@ -65,11 +65,11 @@ The **Product** type represents physical or _digital items_ available in the cat
*Related types: `ProductItf`, `ProductDimension`, `ShippingClass`*
"""
type Product implements ProductItf & SkuItf {
type Product implements ProductItf & SkuItf @key(fields: "upc") @key(fields: "sku package") {
"""
Universal Product Code. A standardized numeric global identifier
"""
upc: String!
upc: String! @shareable
"""
SKUs are unique to the company and are used internally. Alphanumeric.
"""
@ -195,26 +195,87 @@ type Query {
"""
Get a specific product by its UPC
"""
product(upc: String!): ProductItf
product(
"""
Universal Product Code of the product to retrieve
"""
upc: String!
): ProductItf
"""
Get the top N products (default: 5)
"""
topProducts(first: Int = 5): [ProductItf!]!
topProducts(
"""
Number of products to return (default: 5)
"""
first: Int = 5
): [ProductItf!]!
"""
Search and filter products based on criteria
"""
searchProducts(filter: ProductFilterInput!): [ProductItf!]!
searchProducts(
"""
## Product Search Filter
Comprehensive filter criteria for searching and filtering products in the catalog. The **ProductFilterInput** supports multiple filtering dimensions that can be _combined_ for precise results.
### Available Filter Options
- **Search Text** (`searchText`) - Full-text search across product names and descriptions
- **Categories** (`categories`) - Filter by one or more product categories
- **Price Range** (`priceRange`) - Min/max price boundaries (inclusive)
- **Shipping Class** (`shippingClass`) - Filter by delivery speed (STANDARD, EXPRESS, OVERNIGHT)
### Common Usage Patterns
1. Text search only: `{ searchText: "laptop" }`
2. Category + price: `{ categories: ["Electronics"], priceRange: { min: 100, max: 500 } }`
3. Multiple filters: `{ searchText: "tablet", categories: ["Electronics", "Computers"], shippingClass: EXPRESS }`
> **Tip**: All filter fields are optional - combine them as needed for your search requirements
---
*See `ProductFilterInput` type for complete field definitions and constraints*
"""
filter: ProductFilterInput!
): [ProductItf!]!
"""
Get products filtered by category and price range
"""
products(category: String, minPrice: Float, maxPrice: Float): [ProductItf!]!
products(
"""
Optional category name to filter products
"""
category: String
"""
Minimum price threshold (inclusive)
"""
minPrice: Float
"""
Maximum price threshold (inclusive)
"""
maxPrice: Float
): [ProductItf!]!
"""
Get products filtered by physical dimensions and shipping class
"""
productsByDimensions(
"""
Minimum weight in pounds (inclusive)
"""
minWeight: Float
"""
Maximum weight in pounds (inclusive)
"""
maxWeight: Float
"""
Size description to match (e.g., 'Small', 'Large', '10x5x3')
"""
size: String
"""
Shipping class filter (STANDARD, EXPRESS, or OVERNIGHT)
"""
shippingClass: ShippingClass
): [ProductItf!]!
}

View file

@ -68,16 +68,39 @@ type Query {
"""
Get a specific review by ID
"""
review(id: Int!): Review
review(
"""
Unique identifier of the review to retrieve
"""
id: Int!
): Review
"""
Get reviews filtered by product and rating range
"""
reviews(productUpc: String, minRating: Int, maxRating: Int): [Review]
reviews(
"""
Optional Universal Product Code to filter reviews by product
"""
productUpc: String
"""
Minimum rating threshold (inclusive)
"""
minRating: Int
"""
Maximum rating threshold (inclusive)
"""
maxRating: Int
): [Review]
}
type Mutation {
"""
Create a new product review
"""
createReview(input: CreateReviewInput!): Review!
createReview(
"""
Review creation data including product UPC, rating, and optional body text
"""
input: CreateReviewInput!
): Review!
}

View file

@ -96,11 +96,30 @@ input UpdateUserInput {
extend type Query {
me: User
user(id: ID!): User
user(
"""
Unique identifier of the user to retrieve
"""
id: ID!
): User
users: [User]
}
extend type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User
createUser(
"""
User creation data including email, name, and optional alias
"""
input: CreateUserInput!
): User!
updateUser(
"""
Unique identifier of the user to update
"""
id: ID!
"""
User update data including optional name and alias changes
"""
input: UpdateUserInput!
): User
}