mirror of
https://github.com/graphql-hive/console
synced 2026-05-24 01:28:32 +00:00
Shared component for a read only input with a copy button (#6248)
This commit is contained in:
parent
5e94f5dee5
commit
a039245da8
6 changed files with 82 additions and 113 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
53
packages/web/app/src/components/ui/input-copy.tsx
Normal file
53
packages/web/app/src/components/ui/input-copy.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue