Shared component for a read only input with a copy button (#6248)

This commit is contained in:
Kamil Kisiela 2025-01-02 14:08:56 +01:00 committed by GitHub
parent 5e94f5dee5
commit a039245da8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 82 additions and 113 deletions

View file

@ -1,5 +1,5 @@
import { ReactElement, ReactNode, useMemo, useState } from 'react';
import { CopyIcon, LinkIcon } from 'lucide-react';
import { LinkIcon } from 'lucide-react';
import { useQuery } from 'urql';
import { Button } from '@/components/ui/button';
import {
@ -10,6 +10,7 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { HiveLink } from '@/components/ui/hive-link';
import { InputCopy } from '@/components/ui/input-copy';
import { Link as UiLink } from '@/components/ui/link';
import {
Select,
@ -22,7 +23,7 @@ import { UserMenu } from '@/components/ui/user-menu';
import { graphql } from '@/gql';
import { ProjectType } from '@/gql/graphql';
import { getDocsUrl } from '@/lib/docs-url';
import { useClipboard, useToggle } from '@/lib/hooks';
import { useToggle } from '@/lib/hooks';
import { useResetState } from '@/lib/hooks/use-reset-state';
import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org';
import { cn } from '@/lib/utils';
@ -31,8 +32,6 @@ import { ProjectMigrationToast } from '../project/migration-toast';
import { ResourceNotFoundComponent } from '../resource-not-found';
import { Label } from '../ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
import { useToast } from '../ui/use-toast';
import { TargetSelector } from './target-selector';
export enum Page {
@ -458,9 +457,12 @@ export function ConnectSchemaModal(props: {
) : (
<div className="space-y-2 text-sm">
<p>To access your schema from Hive's CDN, use the following endpoint:</p>
<CodeBlock>
{composeEndpoint(selectedContract?.cdnUrl ?? target.cdnUrl, selectedArtifact)}
</CodeBlock>
<InputCopy
value={composeEndpoint(
selectedContract?.cdnUrl ?? target.cdnUrl,
selectedArtifact,
)}
/>
<p>
To authenticate,{' '}
<UiLink
@ -483,7 +485,7 @@ export function ConnectSchemaModal(props: {
use the CDN access token in your HTTP headers:
<br />
</p>
<CodeBlock>{'X-Hive-CDN-Key: <Your Access Token>'}</CodeBlock>
<InputCopy value="X-Hive-CDN-Key: <Your Access Token>" />
</div>
)}
</>
@ -541,12 +543,14 @@ function FederationModalContent(props: {
following command.
</p>
{authenticateSection}
<CodeBlock className="mt-2">
{`docker run --name hive-gateway --rm -p 4000:4000 \\
<div className="mt-2">
<InputCopy
value={`docker run --name hive-gateway --rm -p 4000:4000 \\
ghcr.io/graphql-hive/gateway supergraph \\
'${props.cdnUrl}' \\
--hive-cdn-key '<hive_cdn_access_key>'`}
</CodeBlock>
/>
</div>
<p>
For more information please refer to our{' '}
<UiLink variant="primary" target="_blank" rel="noreferrer" to={getDocsUrl('/gateway')}>
@ -561,12 +565,12 @@ function FederationModalContent(props: {
following command.
</p>
{authenticateSection}
<CodeBlock>
{`docker run --name hive-gateway --rm \\
<InputCopy
value={`docker run --name hive-gateway --rm \\
--env HIVE_CDN_ENDPOINT="${props.cdnUrl}" \\
--env HIVE_CDN_KEY="<hive_cdn_access_key>"
ghcr.io/graphql-hive/apollo-router`}
</CodeBlock>
/>
<p>
For more information please refer to our{' '}
<UiLink
@ -583,13 +587,15 @@ function FederationModalContent(props: {
<TabsContent value="cdn" className="space-y-2 pt-2" variant="content">
<p>For other tooling you can access the raw supergraph by sending a HTTP request.</p>
<p>To access your schema from Hive's CDN, use the following endpoint:</p>
<CodeBlock>{`${props.cdnUrl}/supergraph`}</CodeBlock>
<InputCopy value={`${props.cdnUrl}/supergraph`} />
<p>Here is an example calling the endpoint using curl.</p>
{authenticateSection}
<CodeBlock className="mt-2">
{`curl -H 'X-Hive-CDN-Key: <hive_cdn_access_key>' \\
<div className="mt-2">
<InputCopy
value={`curl -H 'X-Hive-CDN-Key: <hive_cdn_access_key>' \\
${props.cdnUrl}/supergraph`}
</CodeBlock>
/>
</div>
<p>
For more information please refer to our{' '}
<UiLink
@ -606,42 +612,3 @@ function FederationModalContent(props: {
</Tabs>
);
}
function CodeBlock(props: { children: string; className?: string }) {
const copy = useClipboard();
const toast = useToast();
return (
<div className={props.className}>
<div className="relative">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon-sm"
variant="ghost"
className="absolute right-1 top-1"
onClick={() => {
void copy(props.children).then(() => {
toast.toast({
variant: 'default',
title: 'Command copied to clipboard',
});
});
}}
>
<CopyIcon size="12" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copy command to clipboard</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<pre className="overflow-scroll rounded-md bg-gray-500/10 px-2 py-3 text-xs ring-1 ring-inset ring-black">
<code>{props.children}</code>
</pre>
</div>
</div>
);
}

View file

@ -9,8 +9,9 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { InputCopy } from '@/components/ui/input-copy';
import { Link } from '@/components/ui/link';
import { CopyValue, Tag } from '@/components/v2';
import { Tag } from '@/components/v2';
import { FragmentType, graphql, useFragment } from '@/gql';
import { getDocsUrl } from '@/lib/docs-url';
@ -70,7 +71,7 @@ export const ConnectLabModalContent = (props: {
</div>
) : null}
<span className="text-sm text-white">You can use the following endpoint:</span>
<CopyValue value={props.endpoint} />
<InputCopy value={props.endpoint} />
<span className="text-sm text-white">
To authenticate, use the following HTTP headers, with a token that has `target:read`
scope:

View file

@ -15,9 +15,9 @@ import {
} from '@/components/ui/dialog';
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { InputCopy } from '@/components/ui/input-copy';
import { useToast } from '@/components/ui/use-toast';
import { Accordion } from '@/components/v2/accordion';
import { CopyValue } from '@/components/v2/copy-value';
import { FragmentType, graphql, useFragment } from '@/gql';
import { TargetAccessScope } from '@/gql/graphql';
import { RegistryAccessScope } from '@/lib/access/common';
@ -228,7 +228,7 @@ export function CreatedTokenContent(props: {
<DialogHeader className="flex flex-col gap-5">
<DialogTitle>Token successfully created!</DialogTitle>
<DialogDescription className="flex flex-col gap-5">
<CopyValue value={props.mutation.data.createToken.ok.secret} />
<InputCopy value={props.mutation.data.createToken.ok.secret} />
<Tag color="green">
This is your unique API key and it is non-recoverable. If you lose this key, you will
need to create a new one.

View file

@ -0,0 +1,53 @@
import { useCallback, useEffect, useState } from 'react';
import { CheckIcon, CopyIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useClipboard } from '@/lib/hooks';
export function InputCopy(props: { value: string }) {
const [isCopied, setIsCopied] = useState(false);
const copyToClipboard = useClipboard();
useEffect(() => {
if (!isCopied) return;
const timerId = setTimeout(() => {
setIsCopied(false);
}, 2000);
return () => {
clearTimeout(timerId);
};
}, [isCopied]);
const handleClick = useCallback(async () => {
await copyToClipboard(props.value);
setIsCopied(true);
}, [copyToClipboard]);
return (
<div className="flex w-full max-w-2xl items-center space-x-2">
<div className="relative grow">
<Input
type="text"
value={props.value}
readOnly
className="bg-secondary truncate text-white"
/>
</div>
<Button
type="button"
onClick={handleClick}
variant="outline"
size="icon"
className="bg-secondary size-10 shrink-0"
>
{isCopied ? (
<CheckIcon className="size-4 text-emerald-500" />
) : (
<CopyIcon className="size-4" />
)}
<span className="sr-only">{isCopied ? 'Copied' : 'Copy'}</span>
</Button>
</div>
);
}

View file

@ -1,51 +0,0 @@
import { ReactElement, useCallback, useEffect, useState } from 'react';
import { CheckIcon, CopyIcon } from '@/components/ui/icon';
import { Input } from '@/components/v2';
import { useClipboard } from '@/lib/hooks';
import { Button } from '../ui/button';
export const CopyValue = ({
value,
className,
}: {
value: string;
className?: string;
}): ReactElement => {
const [isCopied, setIsCopied] = useState(false);
const copyToClipboard = useClipboard();
useEffect(() => {
if (!isCopied) return;
const timerId = setTimeout(() => {
setIsCopied(false);
}, 2000);
return () => {
clearTimeout(timerId);
};
}, [isCopied]);
const handleClick = useCallback(async () => {
await copyToClipboard(value);
setIsCopied(true);
}, [value, copyToClipboard]);
return (
<Input
className={className}
value={value}
readOnly
suffix={
<Button
size="icon"
variant="link"
className="p-0 focus:ring-transparent"
onClick={handleClick}
title={isCopied ? 'Copied!' : 'Copy to clipboard'}
>
{isCopied ? <CheckIcon /> : <CopyIcon />}
</Button>
}
/>
);
};

View file

@ -3,7 +3,6 @@ export { Autocomplete } from '@/components/v2/autocomplete';
export { Avatar } from '@/components/v2/avatar';
export { Card } from '@/components/v2/card';
export { Checkbox } from '@/components/v2/checkbox';
export { CopyValue } from '@/components/v2/copy-value';
export { DataWrapper } from '@/components/v2/data-wrapper';
export { DiffEditor } from '@/components/v2/diff-editor';
export { Input } from '@/components/v2/input';