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 type { Interpolation, Theme } from "@emotion/react";
|
||||||
import { visuallyHidden } from "@mui/utils";
|
import type { FC } from "react";
|
||||||
import { type FC, type KeyboardEvent, type MouseEvent, useRef } from "react";
|
|
||||||
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
|
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
|
||||||
import { CopyButton } from "../CopyButton/CopyButton";
|
import { CopyButton } from "../CopyButton/CopyButton";
|
||||||
|
|
||||||
@ -21,33 +20,8 @@ export const CodeExample: FC<CodeExampleProps> = ({
|
|||||||
// the secure option, not remember to opt in
|
// the secure option, not remember to opt in
|
||||||
secret = true,
|
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 (
|
return (
|
||||||
<div
|
<div css={styles.container} className={className}>
|
||||||
css={styles.container}
|
|
||||||
className={className}
|
|
||||||
onClick={triggerButton}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
triggerButton(event);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyUp={(event) => {
|
|
||||||
if (event.key === " ") {
|
|
||||||
triggerButton(event);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<code css={[styles.code, secret && styles.secret]}>
|
<code css={[styles.code, secret && styles.secret]}>
|
||||||
{secret ? (
|
{secret ? (
|
||||||
<>
|
<>
|
||||||
@ -60,7 +34,7 @@ export const CodeExample: FC<CodeExampleProps> = ({
|
|||||||
* readily available in the HTML itself
|
* readily available in the HTML itself
|
||||||
*/}
|
*/}
|
||||||
<span aria-hidden>{obfuscateText(code)}</span>
|
<span aria-hidden>{obfuscateText(code)}</span>
|
||||||
<span css={{ ...visuallyHidden }}>
|
<span className="sr-only">
|
||||||
Encrypted text. Please access via the copy button.
|
Encrypted text. Please access via the copy button.
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
@ -69,7 +43,7 @@ export const CodeExample: FC<CodeExampleProps> = ({
|
|||||||
)}
|
)}
|
||||||
</code>
|
</code>
|
||||||
|
|
||||||
<CopyButton ref={buttonRef} text={code} />
|
<CopyButton text={code} label="Copy code" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,77 +1,44 @@
|
|||||||
import { type Interpolation, type Theme, css } from "@emotion/react";
|
import { Button, type ButtonProps } from "components/Button/Button";
|
||||||
import IconButton from "@mui/material/Button";
|
import {
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "components/Tooltip/Tooltip";
|
||||||
import { useClipboard } from "hooks/useClipboard";
|
import { useClipboard } from "hooks/useClipboard";
|
||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||||
import { type ReactNode, forwardRef } from "react";
|
import type { FC } from "react";
|
||||||
import { FileCopyIcon } from "../Icons/FileCopyIcon";
|
|
||||||
|
|
||||||
interface CopyButtonProps {
|
type CopyButtonProps = ButtonProps & {
|
||||||
children?: ReactNode;
|
|
||||||
text: string;
|
text: string;
|
||||||
ctaCopy?: string;
|
label: string;
|
||||||
wrapperStyles?: Interpolation<Theme>;
|
|
||||||
buttonStyles?: Interpolation<Theme>;
|
|
||||||
tooltipTitle?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Language = {
|
|
||||||
tooltipTitle: "Copy to clipboard",
|
|
||||||
ariaLabel: "Copy to clipboard",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const CopyButton: FC<CopyButtonProps> = ({
|
||||||
* Copy button used inside the CodeBlock component internally
|
text,
|
||||||
*/
|
label,
|
||||||
export const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
|
...buttonProps
|
||||||
(props, ref) => {
|
}) => {
|
||||||
const {
|
const { showCopiedSuccess, copyToClipboard } = useClipboard({
|
||||||
text,
|
textToCopy: text,
|
||||||
ctaCopy,
|
});
|
||||||
wrapperStyles,
|
|
||||||
buttonStyles,
|
|
||||||
tooltipTitle = Language.tooltipTitle,
|
|
||||||
} = props;
|
|
||||||
const { showCopiedSuccess, copyToClipboard } = useClipboard({
|
|
||||||
textToCopy: text,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={tooltipTitle} placement="top">
|
<TooltipProvider>
|
||||||
<div css={[{ display: "flex" }, wrapperStyles]}>
|
<Tooltip>
|
||||||
<IconButton
|
<TooltipTrigger asChild>
|
||||||
ref={ref}
|
<Button
|
||||||
css={[styles.button, buttonStyles]}
|
size="icon"
|
||||||
size="small"
|
variant="subtle"
|
||||||
aria-label={Language.ariaLabel}
|
|
||||||
variant="text"
|
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
{showCopiedSuccess ? (
|
{showCopiedSuccess ? <CheckIcon /> : <CopyIcon />}
|
||||||
<CheckIcon css={styles.copyIcon} />
|
<span className="sr-only">{label}</span>
|
||||||
) : (
|
</Button>
|
||||||
<FileCopyIcon css={styles.copyIcon} />
|
</TooltipTrigger>
|
||||||
)}
|
<TooltipContent>{label}</TooltipContent>
|
||||||
{ctaCopy && <div css={{ marginLeft: 8 }}>{ctaCopy}</div>}
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
</TooltipProvider>
|
||||||
},
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
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>>;
|
|
||||||
|
@ -134,7 +134,11 @@ export const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
|
|||||||
Copy your one-time code:
|
Copy your one-time code:
|
||||||
<div css={styles.copyCode}>
|
<div css={styles.copyCode}>
|
||||||
<span css={styles.code}>{externalAuthDevice.user_code}</span>
|
<span css={styles.code}>{externalAuthDevice.user_code}</span>
|
||||||
<CopyButton text={externalAuthDevice.user_code} />
|
{" "}
|
||||||
|
<CopyButton
|
||||||
|
text={externalAuthDevice.user_code}
|
||||||
|
label="Copy user code"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
Then open the link below and paste it:
|
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>
|
</Tooltip>
|
||||||
<CopyButton
|
<CopyButton
|
||||||
text={buildInfo.deployment_id}
|
text={buildInfo.deployment_id}
|
||||||
buttonStyles={css`
|
label="Copy deployment ID"
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -181,7 +173,7 @@ const GithubStar: FC<SvgIconProps> = (props) => (
|
|||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
{...props}
|
{...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>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { workspaceQuota } from "api/queries/workspaceQuota";
|
|||||||
import type * as TypesGen from "api/typesGenerated";
|
import type * as TypesGen from "api/typesGenerated";
|
||||||
import { Avatar } from "components/Avatar/Avatar";
|
import { Avatar } from "components/Avatar/Avatar";
|
||||||
import { AvatarData } from "components/Avatar/AvatarData";
|
import { AvatarData } from "components/Avatar/AvatarData";
|
||||||
|
import { CopyButton } from "components/CopyButton/CopyButton";
|
||||||
import {
|
import {
|
||||||
Topbar,
|
Topbar,
|
||||||
TopbarAvatar,
|
TopbarAvatar,
|
||||||
@ -346,50 +347,57 @@ const WorkspaceBreadcrumb: FC<WorkspaceBreadcrumbProps> = ({
|
|||||||
templateDisplayName,
|
templateDisplayName,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Popover mode="hover">
|
<div className="flex items-center">
|
||||||
<PopoverTrigger>
|
<Popover mode="hover">
|
||||||
<span css={styles.breadcrumbSegment}>
|
<PopoverTrigger>
|
||||||
<TopbarAvatar src={templateIconUrl} fallback={templateDisplayName} />
|
<span css={styles.breadcrumbSegment}>
|
||||||
<span css={[styles.breadcrumbText, { fontWeight: 500 }]}>
|
<TopbarAvatar
|
||||||
{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}
|
src={templateIconUrl}
|
||||||
fallback={templateDisplayName}
|
fallback={templateDisplayName}
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
imgFallbackText={templateDisplayName}
|
<span css={[styles.breadcrumbText, { fontWeight: 500 }]}>
|
||||||
/>
|
{workspaceName}
|
||||||
</HelpTooltipContent>
|
</span>
|
||||||
</Popover>
|
</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