mirror of
https://github.com/coder/coder.git
synced 2025-07-18 14:17:22 +00:00
feat: add copy button for workspace name in breadcrumb (#17822)
Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
This commit is contained in:
@ -1,6 +1,5 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { visuallyHidden } from "@mui/utils";
|
||||
import { type FC, type KeyboardEvent, type MouseEvent, useRef } from "react";
|
||||
import type { FC } from "react";
|
||||
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
|
||||
import { CopyButton } from "../CopyButton/CopyButton";
|
||||
|
||||
@ -21,33 +20,8 @@ export const CodeExample: FC<CodeExampleProps> = ({
|
||||
// the secure option, not remember to opt in
|
||||
secret = true,
|
||||
}) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const triggerButton = (event: KeyboardEvent | MouseEvent) => {
|
||||
const clickTriggeredOutsideButton =
|
||||
event.target instanceof HTMLElement &&
|
||||
!buttonRef.current?.contains(event.target);
|
||||
|
||||
if (clickTriggeredOutsideButton) {
|
||||
buttonRef.current?.click();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
css={styles.container}
|
||||
className={className}
|
||||
onClick={triggerButton}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
triggerButton(event);
|
||||
}
|
||||
}}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === " ") {
|
||||
triggerButton(event);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div css={styles.container} className={className}>
|
||||
<code css={[styles.code, secret && styles.secret]}>
|
||||
{secret ? (
|
||||
<>
|
||||
@ -60,7 +34,7 @@ export const CodeExample: FC<CodeExampleProps> = ({
|
||||
* readily available in the HTML itself
|
||||
*/}
|
||||
<span aria-hidden>{obfuscateText(code)}</span>
|
||||
<span css={{ ...visuallyHidden }}>
|
||||
<span className="sr-only">
|
||||
Encrypted text. Please access via the copy button.
|
||||
</span>
|
||||
</>
|
||||
@ -69,7 +43,7 @@ export const CodeExample: FC<CodeExampleProps> = ({
|
||||
)}
|
||||
</code>
|
||||
|
||||
<CopyButton ref={buttonRef} text={code} />
|
||||
<CopyButton text={code} label="Copy code" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,77 +1,44 @@
|
||||
import { type Interpolation, type Theme, css } from "@emotion/react";
|
||||
import IconButton from "@mui/material/Button";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { Button, type ButtonProps } from "components/Button/Button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { type ReactNode, forwardRef } from "react";
|
||||
import { FileCopyIcon } from "../Icons/FileCopyIcon";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
|
||||
interface CopyButtonProps {
|
||||
children?: ReactNode;
|
||||
type CopyButtonProps = ButtonProps & {
|
||||
text: string;
|
||||
ctaCopy?: string;
|
||||
wrapperStyles?: Interpolation<Theme>;
|
||||
buttonStyles?: Interpolation<Theme>;
|
||||
tooltipTitle?: string;
|
||||
}
|
||||
|
||||
const Language = {
|
||||
tooltipTitle: "Copy to clipboard",
|
||||
ariaLabel: "Copy to clipboard",
|
||||
label: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy button used inside the CodeBlock component internally
|
||||
*/
|
||||
export const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
text,
|
||||
ctaCopy,
|
||||
wrapperStyles,
|
||||
buttonStyles,
|
||||
tooltipTitle = Language.tooltipTitle,
|
||||
} = props;
|
||||
const { showCopiedSuccess, copyToClipboard } = useClipboard({
|
||||
textToCopy: text,
|
||||
});
|
||||
export const CopyButton: FC<CopyButtonProps> = ({
|
||||
text,
|
||||
label,
|
||||
...buttonProps
|
||||
}) => {
|
||||
const { showCopiedSuccess, copyToClipboard } = useClipboard({
|
||||
textToCopy: text,
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltipTitle} placement="top">
|
||||
<div css={[{ display: "flex" }, wrapperStyles]}>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
css={[styles.button, buttonStyles]}
|
||||
size="small"
|
||||
aria-label={Language.ariaLabel}
|
||||
variant="text"
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="subtle"
|
||||
onClick={copyToClipboard}
|
||||
{...buttonProps}
|
||||
>
|
||||
{showCopiedSuccess ? (
|
||||
<CheckIcon css={styles.copyIcon} />
|
||||
) : (
|
||||
<FileCopyIcon css={styles.copyIcon} />
|
||||
)}
|
||||
{ctaCopy && <div css={{ marginLeft: 8 }}>{ctaCopy}</div>}
|
||||
</IconButton>
|
||||
</div>
|
||||
{showCopiedSuccess ? <CheckIcon /> : <CopyIcon />}
|
||||
<span className="sr-only">{label}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const styles = {
|
||||
button: (theme) => css`
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
min-width: 32px;
|
||||
|
||||
&:hover {
|
||||
background: ${theme.palette.background.paper};
|
||||
}
|
||||
`,
|
||||
copyIcon: css`
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
`,
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
@ -134,7 +134,11 @@ export const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
|
||||
Copy your one-time code:
|
||||
<div css={styles.copyCode}>
|
||||
<span css={styles.code}>{externalAuthDevice.user_code}</span>
|
||||
<CopyButton text={externalAuthDevice.user_code} />
|
||||
{" "}
|
||||
<CopyButton
|
||||
text={externalAuthDevice.user_code}
|
||||
label="Copy user code"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
Then open the link below and paste it:
|
||||
|
@ -1,10 +0,0 @@
|
||||
import SvgIcon, { type SvgIconProps } from "@mui/material/SvgIcon";
|
||||
|
||||
export const FileCopyIcon = (props: SvgIconProps): JSX.Element => (
|
||||
<SvgIcon {...props} viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M12.7412 2.2807H4.32014C3.5447 2.2807 2.91663 2.90877 2.91663 3.68421V13.5088H4.32014V3.68421H12.7412V2.2807ZM14.8465 5.08772H7.12716C6.35172 5.08772 5.72365 5.71579 5.72365 6.49123V16.3158C5.72365 17.0912 6.35172 17.7193 7.12716 17.7193H14.8465C15.6219 17.7193 16.25 17.0912 16.25 16.3158V6.49123C16.25 5.71579 15.6219 5.08772 14.8465 5.08772ZM14.8465 16.3158H7.12716V6.49123H14.8465V16.3158Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
);
|
@ -151,15 +151,7 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
||||
</Tooltip>
|
||||
<CopyButton
|
||||
text={buildInfo.deployment_id}
|
||||
buttonStyles={css`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
`}
|
||||
label="Copy deployment ID"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -181,7 +173,7 @@ const GithubStar: FC<SvgIconProps> = (props) => (
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"></path>
|
||||
<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { workspaceQuota } from "api/queries/workspaceQuota";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { AvatarData } from "components/Avatar/AvatarData";
|
||||
import { CopyButton } from "components/CopyButton/CopyButton";
|
||||
import {
|
||||
Topbar,
|
||||
TopbarAvatar,
|
||||
@ -346,50 +347,57 @@ const WorkspaceBreadcrumb: FC<WorkspaceBreadcrumbProps> = ({
|
||||
templateDisplayName,
|
||||
}) => {
|
||||
return (
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
<span css={styles.breadcrumbSegment}>
|
||||
<TopbarAvatar src={templateIconUrl} fallback={templateDisplayName} />
|
||||
<span css={[styles.breadcrumbText, { fontWeight: 500 }]}>
|
||||
{workspaceName}
|
||||
</span>
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
|
||||
<HelpTooltipContent
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
>
|
||||
<AvatarData
|
||||
title={
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={rootTemplateUrl}
|
||||
css={{ color: "inherit" }}
|
||||
>
|
||||
{templateDisplayName}
|
||||
</Link>
|
||||
}
|
||||
subtitle={
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`${rootTemplateUrl}/versions/${encodeURIComponent(templateVersionName)}`}
|
||||
css={{ color: "inherit" }}
|
||||
>
|
||||
Version: {latestBuildVersionName}
|
||||
</Link>
|
||||
}
|
||||
avatar={
|
||||
<Avatar
|
||||
variant="icon"
|
||||
<div className="flex items-center">
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
<span css={styles.breadcrumbSegment}>
|
||||
<TopbarAvatar
|
||||
src={templateIconUrl}
|
||||
fallback={templateDisplayName}
|
||||
/>
|
||||
}
|
||||
imgFallbackText={templateDisplayName}
|
||||
/>
|
||||
</HelpTooltipContent>
|
||||
</Popover>
|
||||
|
||||
<span css={[styles.breadcrumbText, { fontWeight: 500 }]}>
|
||||
{workspaceName}
|
||||
</span>
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
|
||||
<HelpTooltipContent
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
>
|
||||
<AvatarData
|
||||
title={
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={rootTemplateUrl}
|
||||
css={{ color: "inherit" }}
|
||||
>
|
||||
{templateDisplayName}
|
||||
</Link>
|
||||
}
|
||||
subtitle={
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`${rootTemplateUrl}/versions/${encodeURIComponent(templateVersionName)}`}
|
||||
css={{ color: "inherit" }}
|
||||
>
|
||||
Version: {latestBuildVersionName}
|
||||
</Link>
|
||||
}
|
||||
avatar={
|
||||
<Avatar
|
||||
variant="icon"
|
||||
src={templateIconUrl}
|
||||
fallback={templateDisplayName}
|
||||
/>
|
||||
}
|
||||
imgFallbackText={templateDisplayName}
|
||||
/>
|
||||
</HelpTooltipContent>
|
||||
</Popover>
|
||||
<CopyButton text={workspaceName} label="Copy workspace name" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user