mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
Console 1493 schema explorer displaying argument descriptions (#7282)
This commit is contained in:
parent
714906f989
commit
5fec03c297
18 changed files with 470 additions and 474 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
41
packages/web/app/src/components/target/explorer/utils.ts
Normal file
41
packages/web/app/src/components/target/explorer/utils.ts
Normal 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]);
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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!]!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!]!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue