Skip to content

Commit df3e5a2

Browse files
jdollen1ru4lcoderabbitai[bot]
authored
feat: add directions for publishing to no schema component (#6602)
Co-authored-by: Laurin Quast <[email protected]> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 535283c commit df3e5a2

14 files changed

+256
-28
lines changed

.changeset/fuzzy-plums-repeat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'hive': patch
3+
---
4+
5+
Added directions for publishing on no schema component
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useRef, type ComponentProps, type FC } from 'react';
2+
import cn from 'clsx';
3+
import { useHover } from '@/lib/hooks/use-hover';
4+
import { useTimed } from '@/lib/hooks/use-timed';
5+
import { CheckIcon, CopyIcon } from './icon';
6+
7+
export const Code: FC<ComponentProps<'code'>> = ({ children, className, ...props }) => {
8+
const [copied, startCopyTimer] = useTimed(1500);
9+
const [ref, hovering] = useHover();
10+
const codeRef = useRef<HTMLElement | null>(null);
11+
// in case this browser does not support this newer API...
12+
const navigatorClipboardSupport = typeof navigator.clipboard?.writeText === 'function';
13+
return (
14+
<span
15+
ref={ref}
16+
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"
17+
// Make this element able to be focused by setting tabIndex.
18+
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
19+
tabIndex={0}
20+
onFocus={() => {
21+
if (codeRef.current) {
22+
const selection = window.getSelection();
23+
const range = document.createRange();
24+
range.setStart(codeRef.current, 0);
25+
range.setEnd(codeRef.current, codeRef.current.childNodes.length);
26+
selection?.removeAllRanges();
27+
selection?.addRange(range);
28+
}
29+
}}
30+
>
31+
<code
32+
ref={codeRef}
33+
className={cn('whitespace-pre-line', className)}
34+
// always show code blocks in ltr
35+
dir="ltr"
36+
{...props}
37+
>
38+
{children}
39+
</code>
40+
<button
41+
hidden={!navigatorClipboardSupport}
42+
data-hovering={hovering || copied}
43+
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"
44+
onClick={async ev => {
45+
const value = children?.valueOf().toString();
46+
if (value) {
47+
ev.preventDefault();
48+
await navigator.clipboard.writeText(value);
49+
startCopyTimer();
50+
}
51+
}}
52+
title="Copy to clipboard"
53+
>
54+
{copied ? <CheckIcon size={16} /> : <CopyIcon size={16} />}
55+
</button>
56+
</span>
57+
);
58+
};

packages/web/app/src/components/ui/empty-list.tsx

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { ReactElement } from 'react';
1+
import { ReactElement, ReactNode } from 'react';
22
import magnifier from '../../../public/images/figures/magnifier.svg?url';
3+
import { ProjectType } from '@/gql/graphql';
34
import { cn } from '@/lib/utils';
45
import { Card } from './card';
6+
import { Code } from './code';
57
import { DocsLink } from './docs-note';
68
import { Heading } from './heading';
79

@@ -10,15 +12,17 @@ export const EmptyList = ({
1012
description,
1113
docsUrl,
1214
className,
15+
children,
1316
}: {
1417
title: string;
1518
description: string;
1619
docsUrl?: string | null;
20+
children?: ReactNode | null;
1721
className?: string;
1822
}): ReactElement => {
1923
return (
2024
<Card
21-
className={cn('flex grow cursor-default flex-col items-center gap-y-2 py-4', className)}
25+
className={cn('flex grow cursor-default flex-col items-center gap-y-2 p-4', className)}
2226
data-cy="empty-list"
2327
>
2428
<img
@@ -30,6 +34,7 @@ export const EmptyList = ({
3034
/>
3135
<Heading className="text-center">{title}</Heading>
3236
<span className="text-center text-sm font-medium text-gray-500">{description}</span>
37+
{children}
3338
{docsUrl && <DocsLink href={docsUrl}>Read about it in the documentation</DocsLink>}
3439
</Card>
3540
);
@@ -43,13 +48,61 @@ export const noSchema = (
4348
/>
4449
);
4550

46-
export const noSchemaVersion = (
47-
<EmptyList
48-
title="Hive is waiting for your first schema"
49-
description="You can publish a schema with Hive CLI and Hive Client"
50-
docsUrl="/features/schema-registry#publish-a-schema"
51-
/>
52-
);
51+
export const NoSchemaVersion = ({
52+
projectType = null,
53+
recommendedAction = 'none',
54+
}: {
55+
projectType: ProjectType | null;
56+
recommendedAction: 'publish' | 'check' | 'none';
57+
}): ReactElement => {
58+
let children: ReactElement | null = null;
59+
if (recommendedAction !== 'none') {
60+
const isDistributed =
61+
projectType === ProjectType.Federation || projectType === ProjectType.Stitching;
62+
63+
if (recommendedAction === 'check') {
64+
children = (
65+
<>
66+
<div className="flex w-full justify-center py-2 text-xs text-gray-500">
67+
It's recommended to check that the schema is valid and compatible with the state of the
68+
registry before publishing.
69+
</div>
70+
<div className="flex w-full justify-center">
71+
<Code>
72+
{`hive schema:check ${isDistributed ? '--service <service-name> --url <url> ' : ''} --target "<org>/<project>/<target>" <path/schema.graphql>`}
73+
</Code>
74+
</div>
75+
</>
76+
);
77+
} else if (recommendedAction === 'publish') {
78+
children = (
79+
<>
80+
{isDistributed && (
81+
<div className="flex w-full justify-center py-2 text-xs text-gray-500">
82+
For distributed systems, it's recommended to publish the schema after the service is
83+
deployed.
84+
</div>
85+
)}
86+
<div className="flex w-full justify-center">
87+
<Code>
88+
{`hive schema:publish ${isDistributed ? '--service <service-name> --url <url> ' : ''} --target "<org>/<project>/<target>" <path/schema.graphql>`}
89+
</Code>
90+
</div>
91+
</>
92+
);
93+
}
94+
}
95+
96+
return (
97+
<EmptyList
98+
title="Hive is waiting for your first schema"
99+
description="You can publish a schema with Hive CLI and Hive Client"
100+
docsUrl="/features/schema-registry#publish-a-schema"
101+
>
102+
{children}
103+
</EmptyList>
104+
);
105+
};
53106

54107
export const noValidSchemaVersion = (
55108
<EmptyList

packages/web/app/src/components/ui/icon.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,11 @@ export const ArrowDownIcon = ({ className }: IconProps): ReactElement => (
231231
</svg>
232232
);
233233

234-
export const CheckIcon = ({ className }: IconProps): ReactElement => (
234+
export const CheckIcon = ({ className, size }: IconProps & { size?: number }): ReactElement => (
235235
<svg
236236
viewBox="0 0 24 24"
237-
width="24"
238-
height="24"
237+
width={size ?? 24}
238+
height={size ?? 24}
239239
stroke="currentColor"
240240
xmlns="http://www.w3.org/2000/svg"
241241
className={className}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { useCallback, useRef, useState } from 'react';
2+
3+
export function useHover() {
4+
const [hovering, setHovering] = useState(false);
5+
const previousNode = useRef<Node | null>(null);
6+
7+
const handleMouseEnter = useCallback(() => {
8+
setHovering(true);
9+
}, []);
10+
11+
const handleMouseLeave = useCallback(() => {
12+
setHovering(false);
13+
}, []);
14+
15+
const customRef = useCallback(
16+
(node: HTMLElement) => {
17+
if (previousNode.current?.nodeType === Node.ELEMENT_NODE) {
18+
previousNode.current.removeEventListener('mouseenter', handleMouseEnter);
19+
previousNode.current.removeEventListener('mouseleave', handleMouseLeave);
20+
}
21+
22+
if (node?.nodeType === Node.ELEMENT_NODE) {
23+
node.addEventListener('mouseenter', handleMouseEnter);
24+
node.addEventListener('mouseleave', handleMouseLeave);
25+
}
26+
27+
previousNode.current = node;
28+
},
29+
[handleMouseEnter, handleMouseLeave],
30+
);
31+
32+
return [customRef, hovering] as const;
33+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useEffect, useState } from 'react';
2+
3+
export function useTimed(wait: number = 1000) {
4+
const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
5+
useEffect(() => {
6+
return () => {
7+
if (timer) {
8+
clearTimeout(timer);
9+
}
10+
};
11+
}, [timer]);
12+
13+
const handler = () => {
14+
if (timer) {
15+
clearTimeout(timer);
16+
}
17+
setTimer(
18+
setTimeout(() => {
19+
setTimer(null);
20+
}, wait),
21+
);
22+
};
23+
return [timer !== null, handler] as const;
24+
}

packages/web/app/src/pages/target-apps.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Page, TargetLayout } from '@/components/layouts/target';
66
import { Badge } from '@/components/ui/badge';
77
import { Button } from '@/components/ui/button';
88
import { CardDescription } from '@/components/ui/card';
9-
import { EmptyList, noSchemaVersion } from '@/components/ui/empty-list';
9+
import { EmptyList, NoSchemaVersion } from '@/components/ui/empty-list';
1010
import { Meta } from '@/components/ui/meta';
1111
import { SubPageLayoutHeader } from '@/components/ui/page-content-layout';
1212
import { QueryError } from '@/components/ui/query-error';
@@ -61,6 +61,10 @@ const TargetAppsViewQuery = graphql(`
6161
id
6262
__typename
6363
}
64+
project {
65+
id
66+
type
67+
}
6468
viewerCanViewAppDeployments
6569
appDeployments(first: 20, after: $after) {
6670
pageInfo {
@@ -254,7 +258,10 @@ function TargetAppsView(props: {
254258
</div>
255259
</div>
256260
) : !data.data?.target?.latestSchemaVersion ? (
257-
noSchemaVersion
261+
<NoSchemaVersion
262+
recommendedAction="publish"
263+
projectType={data.data?.target?.project?.type ?? null}
264+
/>
258265
) : !data.data.target.appDeployments ? (
259266
<EmptyList
260267
title="Hive is waiting for your first app deployment"

packages/web/app/src/pages/target-checks.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Page, TargetLayout } from '@/components/layouts/target';
44
import { BadgeRounded } from '@/components/ui/badge';
55
import { Button } from '@/components/ui/button';
66
import { DocsLink } from '@/components/ui/docs-note';
7-
import { EmptyList } from '@/components/ui/empty-list';
7+
import { EmptyList, NoSchemaVersion } from '@/components/ui/empty-list';
88
import { Label } from '@/components/ui/label';
99
import { Meta } from '@/components/ui/meta';
1010
import { Subtitle, Title } from '@/components/ui/page';
@@ -196,6 +196,10 @@ const ChecksPageQuery = graphql(`
196196
}
197197
) {
198198
id
199+
project {
200+
id
201+
type
202+
}
199203
schemaChecks(first: 1) {
200204
edges {
201205
node {
@@ -359,7 +363,14 @@ function ChecksPageContent(props: {
359363
) : (
360364
<div>
361365
<div className="cursor-default text-sm">
362-
{hasActiveSchemaCheck ? 'List is empty' : 'Your schema check list is empty'}
366+
{hasActiveSchemaCheck ? (
367+
'List is empty'
368+
) : (
369+
<NoSchemaVersion
370+
projectType={query.data?.target?.project.type ?? null}
371+
recommendedAction="check"
372+
/>
373+
)}
363374
</div>
364375
<DocsLink href="/features/schema-registry#check-a-schema">
365376
{hasActiveSchemaCheck

packages/web/app/src/pages/target-explorer-deprecated.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { SchemaExplorerProvider } from '@/components/target/explorer/provider';
77
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
88
import { Button } from '@/components/ui/button';
99
import { DateRangePicker, presetLast7Days } from '@/components/ui/date-range-picker';
10-
import { noSchemaVersion } from '@/components/ui/empty-list';
10+
import { NoSchemaVersion } from '@/components/ui/empty-list';
1111
import { Link } from '@/components/ui/link';
1212
import { Meta } from '@/components/ui/meta';
1313
import { Subtitle, Title } from '@/components/ui/page';
@@ -165,6 +165,10 @@ const DeprecatedSchemaExplorer_DeprecatedSchemaQuery = graphql(`
165165
targetSlug: $targetSlug
166166
}
167167
) {
168+
project {
169+
id
170+
type
171+
}
168172
id
169173
slug
170174
latestSchemaVersion {
@@ -303,7 +307,10 @@ function DeprecatedSchemaExplorer(props: {
303307
/>
304308
</>
305309
) : (
306-
noSchemaVersion
310+
<NoSchemaVersion
311+
recommendedAction="publish"
312+
projectType={query.data?.target?.project?.type ?? null}
313+
/>
307314
)}
308315
</>
309316
)}

packages/web/app/src/pages/target-explorer-type.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
} from '@/components/target/explorer/provider';
2020
import { GraphQLScalarTypeComponent } from '@/components/target/explorer/scalar-type';
2121
import { GraphQLUnionTypeComponent } from '@/components/target/explorer/union-type';
22-
import { noSchemaVersion } from '@/components/ui/empty-list';
22+
import { NoSchemaVersion } from '@/components/ui/empty-list';
2323
import { Meta } from '@/components/ui/meta';
2424
import { Subtitle, Title } from '@/components/ui/page';
2525
import { QueryError } from '@/components/ui/query-error';
@@ -162,6 +162,10 @@ const TargetExplorerTypenamePageQuery = graphql(`
162162
}
163163
}
164164
}
165+
project {
166+
id
167+
type
168+
}
165169
}
166170
operationsStats(
167171
selector: {
@@ -263,7 +267,10 @@ function TypeExplorerPageContent(props: {
263267
styleDeprecated
264268
/>
265269
) : type ? (
266-
noSchemaVersion
270+
<NoSchemaVersion
271+
recommendedAction="publish"
272+
projectType={query.data?.target?.project?.type ?? null}
273+
/>
267274
) : (
268275
<div>Not found</div>
269276
)}

0 commit comments

Comments
 (0)