Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fuzzy-plums-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'hive': patch
---

Added directions for publishing on no schema component
58 changes: 58 additions & 0 deletions packages/web/app/src/components/ui/code.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useRef, type ComponentProps, type FC } from 'react';
import cn from 'clsx';
import { useHover } from '@/lib/hooks/use-hover';
import { useTimed } from '@/lib/hooks/use-timed';
import { CheckIcon, CopyIcon } from './icon';

export const Code: FC<ComponentProps<'code'>> = ({ children, className, ...props }) => {
const [copied, startCopyTimer] = useTimed(1500);
const [ref, hovering] = useHover();
const codeRef = useRef<HTMLElement | null>(null);
// in case this browser does not support this newer API...
const navigatorClipboardSupport = typeof navigator.clipboard?.writeText === 'function';
return (
<span
ref={ref}
className="relative flex cursor-text items-center gap-2 break-all rounded-md border border-gray-600 bg-black p-4 pr-14 font-mono text-sm"
// Make this element able to be focused by setting tabIndex.
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
onFocus={() => {
if (codeRef.current) {
const selection = window.getSelection();
const range = document.createRange();
range.setStart(codeRef.current, 0);
range.setEnd(codeRef.current, codeRef.current.childNodes.length);
selection?.removeAllRanges();
selection?.addRange(range);
}
}}
>
<code
ref={codeRef}
className={cn('whitespace-pre-line', className)}
// always show code blocks in ltr
dir="ltr"
{...props}
>
{children}
</code>
<button
hidden={!navigatorClipboardSupport}
data-hovering={hovering || copied}
className="absolute right-3 top-2 cursor-pointer rounded-md border border-gray-600 p-2 opacity-0 hover:text-orange-600 data-[hovering=true]:opacity-100 data-[hovering=true]:transition-opacity"
onClick={async ev => {
const value = children?.valueOf().toString();
if (value) {
ev.preventDefault();
await navigator.clipboard.writeText(value);
startCopyTimer();
}
}}
title="Copy to clipboard"
>
{copied ? <CheckIcon size={16} /> : <CopyIcon size={16} />}
</button>
</span>
);
};
71 changes: 62 additions & 9 deletions packages/web/app/src/components/ui/empty-list.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ReactElement } from 'react';
import { ReactElement, ReactNode } from 'react';
import magnifier from '../../../public/images/figures/magnifier.svg?url';
import { ProjectType } from '@/gql/graphql';
import { cn } from '@/lib/utils';
import { Card } from './card';
import { Code } from './code';
import { DocsLink } from './docs-note';
import { Heading } from './heading';

Expand All @@ -10,15 +12,17 @@ export const EmptyList = ({
description,
docsUrl,
className,
children,
}: {
title: string;
description: string;
docsUrl?: string | null;
children?: ReactNode | null;
className?: string;
}): ReactElement => {
return (
<Card
className={cn('flex grow cursor-default flex-col items-center gap-y-2 py-4', className)}
className={cn('flex grow cursor-default flex-col items-center gap-y-2 p-4', className)}
data-cy="empty-list"
>
<img
Expand All @@ -30,6 +34,7 @@ export const EmptyList = ({
/>
<Heading className="text-center">{title}</Heading>
<span className="text-center text-sm font-medium text-gray-500">{description}</span>
{children}
{docsUrl && <DocsLink href={docsUrl}>Read about it in the documentation</DocsLink>}
</Card>
);
Expand All @@ -43,13 +48,61 @@ export const noSchema = (
/>
);

export const noSchemaVersion = (
<EmptyList
title="Hive is waiting for your first schema"
description="You can publish a schema with Hive CLI and Hive Client"
docsUrl="/features/schema-registry#publish-a-schema"
/>
);
export const NoSchemaVersion = ({
projectType = null,
recommendedAction = 'none',
}: {
projectType: ProjectType | null;
recommendedAction: 'publish' | 'check' | 'none';
}): ReactElement => {
let children: ReactElement | null = null;
if (recommendedAction !== 'none') {
const isDistributed =
projectType === ProjectType.Federation || projectType === ProjectType.Stitching;

if (recommendedAction === 'check') {
children = (
<>
<div className="flex w-full justify-center py-2 text-xs text-gray-500">
It's recommended to check that the schema is valid and compatible with the state of the
registry before publishing.
</div>
<div className="flex w-full justify-center">
<Code>
{`hive schema:check ${isDistributed ? '--service <service-name> --url <url> ' : ''}<path/schema.graphql>`}
</Code>
</div>
</>
);
} else if (recommendedAction === 'publish') {
children = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify schema checking is working correctly

Since this is a critical user-facing instruction, let's verify that the schema:check command parameters shown here match what's expected by the Hive CLI.


🏁 Script executed:

#!/bin/bash
# Search for schema:check usage examples in documentation or tests
rg -A 2 "schema:check" --type md

Length of output: 2515


Critical: Schema Check Command Parameters Mismatch

  • In the UI component at packages/web/app/src/components/ui/empty-list.tsx (lines 63–77), the schema check command for distributed schemas conditionally includes --url <url>.
  • However, our CLI documentation (see packages/libraries/cli/README.md) only indicates the use of --service <value> (alongside other optional flags like --registry.endpoint) without mentioning a --url flag.
  • Action required: Please review whether the --url <url> flag is still valid for the Hive CLI. If not, update the command snippet in the UI accordingly (for example, replacing --url <url> with the appropriate flag such as --registry.endpoint <value>, or removing it if unnecessary).

<>
{isDistributed && (
<div className="flex w-full justify-center py-2 text-xs text-gray-500">
For distributed systems, it's recommended to publish the schema after the service is
deployed.
</div>
)}
<div className="flex w-full justify-center">
<Code>
{`hive schema:publish ${isDistributed ? '--service <service-name> --url <url> ' : ''}<path/schema.graphql>`}
</Code>
</div>
</>
);
}
}

return (
<EmptyList
title="Hive is waiting for your first schema"
description="You can publish a schema with Hive CLI and Hive Client"
docsUrl="/features/schema-registry#publish-a-schema"
>
{children}
</EmptyList>
);
};

export const noValidSchemaVersion = (
<EmptyList
Expand Down
6 changes: 3 additions & 3 deletions packages/web/app/src/components/ui/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,11 @@ export const ArrowDownIcon = ({ className }: IconProps): ReactElement => (
</svg>
);

export const CheckIcon = ({ className }: IconProps): ReactElement => (
export const CheckIcon = ({ className, size }: IconProps & { size?: number }): ReactElement => (
<svg
viewBox="0 0 24 24"
width="24"
height="24"
width={size ?? 24}
height={size ?? 24}
stroke="currentColor"
xmlns="http://www.w3.org/2000/svg"
className={className}
Expand Down
33 changes: 33 additions & 0 deletions packages/web/app/src/lib/hooks/use-hover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useCallback, useRef, useState } from 'react';

export function useHover() {
const [hovering, setHovering] = useState(false);
const previousNode = useRef<Node | null>(null);

const handleMouseEnter = useCallback(() => {
setHovering(true);
}, []);

const handleMouseLeave = useCallback(() => {
setHovering(false);
}, []);

const customRef = useCallback(
(node: HTMLElement) => {
if (previousNode.current?.nodeType === Node.ELEMENT_NODE) {
previousNode.current.removeEventListener('mouseenter', handleMouseEnter);
previousNode.current.removeEventListener('mouseleave', handleMouseLeave);
}

if (node?.nodeType === Node.ELEMENT_NODE) {
node.addEventListener('mouseenter', handleMouseEnter);
node.addEventListener('mouseleave', handleMouseLeave);
}

previousNode.current = node;
},
[handleMouseEnter, handleMouseLeave],
);

return [customRef, hovering] as const;
}
24 changes: 24 additions & 0 deletions packages/web/app/src/lib/hooks/use-timed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect, useState } from 'react';

export function useTimed(wait: number = 1000) {
const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
useEffect(() => {
return () => {
if (timer) {
clearTimeout(timer);
}
};
}, [timer]);

const handler = () => {
if (timer) {
clearTimeout(timer);
}
setTimer(
setTimeout(() => {
setTimer(null);
}, wait),
);
};
return [timer !== null, handler] as const;
}
11 changes: 9 additions & 2 deletions packages/web/app/src/pages/target-apps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Page, TargetLayout } from '@/components/layouts/target';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { CardDescription } from '@/components/ui/card';
import { EmptyList, noSchemaVersion } from '@/components/ui/empty-list';
import { EmptyList, NoSchemaVersion } from '@/components/ui/empty-list';
import { Meta } from '@/components/ui/meta';
import { SubPageLayoutHeader } from '@/components/ui/page-content-layout';
import { QueryError } from '@/components/ui/query-error';
Expand Down Expand Up @@ -61,6 +61,10 @@ const TargetAppsViewQuery = graphql(`
id
__typename
}
project {
id
type
}
viewerCanViewAppDeployments
appDeployments(first: 20, after: $after) {
pageInfo {
Expand Down Expand Up @@ -254,7 +258,10 @@ function TargetAppsView(props: {
</div>
</div>
) : !data.data?.target?.latestSchemaVersion ? (
noSchemaVersion
<NoSchemaVersion
recommendedAction="publish"
projectType={data.data?.target?.project?.type ?? null}
/>
) : !data.data.target.appDeployments ? (
<EmptyList
title="Hive is waiting for your first app deployment"
Expand Down
15 changes: 13 additions & 2 deletions packages/web/app/src/pages/target-checks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Page, TargetLayout } from '@/components/layouts/target';
import { BadgeRounded } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { DocsLink } from '@/components/ui/docs-note';
import { EmptyList } from '@/components/ui/empty-list';
import { EmptyList, NoSchemaVersion } from '@/components/ui/empty-list';
import { Label } from '@/components/ui/label';
import { Meta } from '@/components/ui/meta';
import { Subtitle, Title } from '@/components/ui/page';
Expand Down Expand Up @@ -196,6 +196,10 @@ const ChecksPageQuery = graphql(`
}
) {
id
project {
id
type
}
schemaChecks(first: 1) {
edges {
node {
Expand Down Expand Up @@ -359,7 +363,14 @@ function ChecksPageContent(props: {
) : (
<div>
<div className="cursor-default text-sm">
{hasActiveSchemaCheck ? 'List is empty' : 'Your schema check list is empty'}
{hasActiveSchemaCheck ? (
'List is empty'
) : (
<NoSchemaVersion
projectType={query.data?.target?.project.type ?? null}
recommendedAction="check"
/>
)}
</div>
<DocsLink href="/features/schema-registry#check-a-schema">
{hasActiveSchemaCheck
Expand Down
11 changes: 9 additions & 2 deletions packages/web/app/src/pages/target-explorer-deprecated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { SchemaExplorerProvider } from '@/components/target/explorer/provider';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { DateRangePicker, presetLast7Days } from '@/components/ui/date-range-picker';
import { noSchemaVersion } from '@/components/ui/empty-list';
import { NoSchemaVersion } from '@/components/ui/empty-list';
import { Link } from '@/components/ui/link';
import { Meta } from '@/components/ui/meta';
import { Subtitle, Title } from '@/components/ui/page';
Expand Down Expand Up @@ -165,6 +165,10 @@ const DeprecatedSchemaExplorer_DeprecatedSchemaQuery = graphql(`
targetSlug: $targetSlug
}
) {
project {
id
type
}
id
slug
latestSchemaVersion {
Expand Down Expand Up @@ -303,7 +307,10 @@ function DeprecatedSchemaExplorer(props: {
/>
</>
) : (
noSchemaVersion
<NoSchemaVersion
recommendedAction="publish"
projectType={query.data?.target?.project?.type ?? null}
/>
)}
</>
)}
Expand Down
11 changes: 9 additions & 2 deletions packages/web/app/src/pages/target-explorer-type.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '@/components/target/explorer/provider';
import { GraphQLScalarTypeComponent } from '@/components/target/explorer/scalar-type';
import { GraphQLUnionTypeComponent } from '@/components/target/explorer/union-type';
import { noSchemaVersion } from '@/components/ui/empty-list';
import { NoSchemaVersion } from '@/components/ui/empty-list';
import { Meta } from '@/components/ui/meta';
import { Subtitle, Title } from '@/components/ui/page';
import { QueryError } from '@/components/ui/query-error';
Expand Down Expand Up @@ -162,6 +162,10 @@ const TargetExplorerTypenamePageQuery = graphql(`
}
}
}
project {
id
type
}
}
operationsStats(
selector: {
Expand Down Expand Up @@ -263,7 +267,10 @@ function TypeExplorerPageContent(props: {
styleDeprecated
/>
) : type ? (
noSchemaVersion
<NoSchemaVersion
recommendedAction="publish"
projectType={query.data?.target?.project?.type ?? null}
/>
) : (
<div>Not found</div>
)}
Expand Down
Loading
Loading