refactor(ui): replace HeroUI Snippet with CodeSnippet component (#10319)

This commit is contained in:
Alejandro Bailo
2026-03-13 16:31:39 +01:00
committed by GitHub
parent 0790619020
commit 22f79edec5
12 changed files with 61 additions and 120 deletions

View File

@@ -14,9 +14,9 @@ import { createSamlConfig, updateSamlConfig } from "@/actions/integrations";
import { AddIcon } from "@/components/icons"; import { AddIcon } from "@/components/icons";
import { Button, Card, CardContent, CardHeader } from "@/components/shadcn"; import { Button, Card, CardContent, CardHeader } from "@/components/shadcn";
import { useToast } from "@/components/ui"; import { useToast } from "@/components/ui";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { CustomServerInput } from "@/components/ui/custom"; import { CustomServerInput } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link"; import { CustomLink } from "@/components/ui/custom/custom-link";
import { SnippetChip } from "@/components/ui/entities";
import { FormButtons } from "@/components/ui/form"; import { FormButtons } from "@/components/ui/form";
import { apiBaseUrl } from "@/lib"; import { apiBaseUrl } from "@/lib";
@@ -258,7 +258,11 @@ export const SamlConfigForm = ({
: `${apiBaseUrl}/accounts/saml/your-domain.com/acs/`; : `${apiBaseUrl}/accounts/saml/your-domain.com/acs/`;
return ( return (
<form ref={formRef} action={formAction} className="flex flex-col gap-2"> <form
ref={formRef}
action={formAction}
className="flex min-w-0 flex-col gap-2"
>
<div className="py-1 text-xs"> <div className="py-1 text-xs">
Need help configuring SAML SSO?{" "} Need help configuring SAML SSO?{" "}
<CustomLink <CustomLink
@@ -304,9 +308,9 @@ export const SamlConfigForm = ({
<span className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"> <span className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
ACS URL: ACS URL:
</span> </span>
<SnippetChip <CodeSnippet
value={acsUrl} value={acsUrl}
ariaLabel="Copy ACS URL to clipboard" ariaLabel="Copy ACS URL"
className="h-10 w-full" className="h-10 w-full"
/> />
</div> </div>
@@ -315,9 +319,9 @@ export const SamlConfigForm = ({
<span className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"> <span className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
Audience: Audience:
</span> </span>
<SnippetChip <CodeSnippet
value="urn:prowler.com:sp" value="urn:prowler.com:sp"
ariaLabel="Copy Audience to clipboard" ariaLabel="Copy Audience"
className="h-10 w-full" className="h-10 w-full"
/> />
</div> </div>

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import { Snippet } from "@heroui/snippet";
import Link from "next/link"; import Link from "next/link";
import { AddIcon } from "../icons"; import { AddIcon } from "../icons";
import { Button, Card, CardContent, CardHeader } from "../shadcn"; import { Button, Card, CardContent, CardHeader } from "../shadcn";
import { Separator } from "../shadcn/separator/separator"; import { Separator } from "../shadcn/separator/separator";
import { CodeSnippet } from "../ui/code-snippet/code-snippet";
import { DateWithTime } from "../ui/entities"; import { DateWithTime } from "../ui/entities";
interface InvitationDetailsProps { interface InvitationDetailsProps {
@@ -89,20 +89,7 @@ export const InvitationDetails = ({ attributes }: InvitationDetailsProps) => {
Share this link with the user: Share this link with the user:
</h3> </h3>
<div className="flex w-full flex-col items-start justify-between overflow-hidden"> <CodeSnippet value={invitationLink} className="max-w-full" />
<Snippet
classNames={{
base: "w-full max-w-full",
content: "min-w-0 overflow-hidden",
pre: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap",
}}
hideSymbol
variant="bordered"
className="bg-bg-neutral-secondary max-w-full overflow-hidden py-1"
>
<p className="min-w-0 truncate text-sm">{invitationLink}</p>
</Snippet>
</div>
</CardContent> </CardContent>
</Card> </Card>
<div className="flex w-full items-center justify-end"> <div className="flex w-full items-center justify-end">

View File

@@ -1,8 +1,7 @@
"use client"; "use client";
import { Snippet } from "@heroui/snippet";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { DateWithTime, EntityInfo, InfoField } from "@/components/ui/entities"; import { DateWithTime, EntityInfo, InfoField } from "@/components/ui/entities";
import { StatusBadge } from "@/components/ui/table/status-badge"; import { StatusBadge } from "@/components/ui/table/status-badge";
import { ProviderProps, ProviderType, ScanProps, TaskDetails } from "@/types"; import { ProviderProps, ProviderType, ScanProps, TaskDetails } from "@/types";
@@ -81,18 +80,17 @@ export const ScanDetail = ({
</div> </div>
<InfoField label="Scan ID" variant="simple"> <InfoField label="Scan ID" variant="simple">
<Snippet hideSymbol>{scanDetails.id}</Snippet> <CodeSnippet value={scanDetails.id} />
</InfoField> </InfoField>
{scan.state === "failed" && taskDetails?.attributes.result && ( {scan.state === "failed" && taskDetails?.attributes.result && (
<> <>
{taskDetails.attributes.result.exc_message && ( {taskDetails.attributes.result.exc_message && (
<InfoField label="Error Message" variant="simple"> <InfoField label="Error Message" variant="simple">
<Snippet hideSymbol> <CodeSnippet
<span className="text-xs whitespace-pre-line"> value={taskDetails.attributes.result.exc_message.join("\n")}
{taskDetails.attributes.result.exc_message.join("\n")} multiline
</span> />
</Snippet>
</InfoField> </InfoField>
)} )}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">

View File

@@ -21,6 +21,10 @@ interface CodeSnippetProps {
icon?: ReactNode; icon?: ReactNode;
/** Function to format the displayed text (value is still copied as-is) */ /** Function to format the displayed text (value is still copied as-is) */
formatter?: (value: string) => string; formatter?: (value: string) => string;
/** Enable multiline display (disables truncation, enables word wrap) */
multiline?: boolean;
/** Custom aria-label for the copy button */
ariaLabel?: string;
} }
export const CodeSnippet = ({ export const CodeSnippet = ({
@@ -30,6 +34,8 @@ export const CodeSnippet = ({
hideCopyButton = false, hideCopyButton = false,
icon, icon,
formatter, formatter,
multiline = false,
ariaLabel = "Copy to clipboard",
}: CodeSnippetProps) => { }: CodeSnippetProps) => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@@ -46,7 +52,7 @@ export const CodeSnippet = ({
type="button" type="button"
onClick={handleCopy} onClick={handleCopy}
className="text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 cursor-pointer transition-colors" className="text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 cursor-pointer transition-colors"
aria-label="Copy to clipboard" aria-label={ariaLabel}
> >
{copied ? ( {copied ? (
<Check className="h-3.5 w-3.5" /> <Check className="h-3.5 w-3.5" />
@@ -66,7 +72,7 @@ export const CodeSnippet = ({
"hover:bg-bg-neutral-tertiary text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 cursor-pointer rounded-md p-1 transition-colors", "hover:bg-bg-neutral-tertiary text-text-neutral-secondary hover:text-text-neutral-primary shrink-0 cursor-pointer rounded-md p-1 transition-colors",
className, className,
)} )}
aria-label="Copy to clipboard" aria-label={ariaLabel}
> >
{copied ? ( {copied ? (
<Check className="h-3.5 w-3.5" /> <Check className="h-3.5 w-3.5" />
@@ -80,19 +86,26 @@ export const CodeSnippet = ({
return ( return (
<div <div
className={cn( className={cn(
"bg-bg-neutral-tertiary text-text-neutral-primary border-border-neutral-tertiary flex h-6 w-fit min-w-0 items-center gap-1.5 rounded-full border-2 px-2 py-0.5 text-xs", "bg-bg-neutral-tertiary text-text-neutral-primary border-border-neutral-tertiary flex w-fit min-w-0 items-center gap-1.5 border-2 px-2 py-0.5 text-xs",
multiline ? "h-auto rounded-lg" : "h-6 rounded-full",
className, className,
)} )}
> >
{icon && ( {icon && (
<span className="text-text-neutral-secondary shrink-0">{icon}</span> <span className="text-text-neutral-secondary shrink-0">{icon}</span>
)} )}
<Tooltip> {multiline ? (
<TooltipTrigger asChild> <span className="min-w-0 flex-1 break-all whitespace-pre-wrap">
<code className="min-w-0 flex-1 truncate">{displayValue}</code> {displayValue}
</TooltipTrigger> </span>
<TooltipContent side="top">{value}</TooltipContent> ) : (
</Tooltip> <Tooltip>
<TooltipTrigger asChild>
<span className="min-w-0 flex-1 truncate">{displayValue}</span>
</TooltipTrigger>
<TooltipContent side="top">{value}</TooltipContent>
</Tooltip>
)}
{!hideCopyButton && <CopyButton />} {!hideCopyButton && <CopyButton />}
</div> </div>
); );

View File

@@ -3,4 +3,3 @@ export * from "./entity-info";
export * from "./get-provider-logo"; export * from "./get-provider-logo";
export * from "./info-field"; export * from "./info-field";
export * from "./scan-status"; export * from "./scan-status";
export * from "./snippet-chip";

View File

@@ -1,51 +0,0 @@
import { Snippet } from "@heroui/snippet";
import { cn } from "@heroui/theme";
import { Tooltip } from "@heroui/tooltip";
import React from "react";
import { CopyIcon, DoneIcon } from "@/components/icons";
interface SnippetChipProps {
value: string;
ariaLabel?: string;
icon?: React.ReactNode;
hideCopyButton?: boolean;
formatter?: (value: string) => string;
className?: string;
}
export const SnippetChip = ({
value,
hideCopyButton = false,
ariaLabel = `Copy ${value} to clipboard`,
icon,
formatter,
className,
...props
}: SnippetChipProps) => {
return (
<Snippet
className={cn("h-6", className)}
classNames={{
content: "min-w-0 overflow-hidden",
pre: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap",
base: "border-border-neutral-tertiary bg-bg-neutral-tertiary rounded-lg border py-1",
}}
size="sm"
hideSymbol
copyIcon={<CopyIcon size={16} />}
checkIcon={<DoneIcon size={16} />}
hideCopyButton={hideCopyButton}
codeString={value}
{...props}
>
<div className="flex min-w-0 items-center gap-2" aria-label={ariaLabel}>
{icon}
<Tooltip content={value} placement="top" size="sm">
<span className="min-w-0 flex-1 truncate text-xs">
{formatter ? formatter(value) : value}
</span>
</Tooltip>
</div>
</Snippet>
);
};

View File

@@ -191,7 +191,7 @@ export function DataTablePagination({
{/* Page info and navigation */} {/* Page info and navigation */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-text-neutral-secondary text-xs font-medium"> <span className="text-text-neutral-secondary hidden text-xs font-medium sm:inline">
Page {currentPage} of {totalPages} Page {currentPage} of {totalPages}
</span> </span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">

View File

@@ -1,10 +1,9 @@
"use client"; "use client";
import { Snippet } from "@heroui/snippet";
import { Button } from "@/components/shadcn"; import { Button } from "@/components/shadcn";
import { Modal } from "@/components/shadcn/modal"; import { Modal } from "@/components/shadcn/modal";
import { Alert, AlertDescription } from "@/components/ui/alert/Alert"; import { Alert, AlertDescription } from "@/components/ui/alert/Alert";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
interface ApiKeySuccessModalProps { interface ApiKeySuccessModalProps {
isOpen: boolean; isOpen: boolean;
@@ -36,18 +35,12 @@ export const ApiKeySuccessModal = ({
<p className="text-text-neutral-primary text-sm font-medium"> <p className="text-text-neutral-primary text-sm font-medium">
Your API Key Your API Key
</p> </p>
<Snippet <CodeSnippet
hideSymbol value={apiKey}
classNames={{ multiline
pre: "font-mono text-sm break-all whitespace-pre-wrap p-2 text-text-neutral-primary", ariaLabel="Copy API key"
}} className="w-full px-3 py-2 text-sm"
tooltipProps={{ />
content: "Copy API key",
color: "default",
}}
>
{apiKey}
</Snippet>
</div> </div>
</div> </div>

View File

@@ -32,8 +32,8 @@ export const MembershipItem = ({
setIsOpen={setIsEditOpen} setIsOpen={setIsEditOpen}
/> />
</Modal> </Modal>
<Card variant="inner" className="min-w-[320px] p-2"> <Card variant="inner" className="p-2">
<div className="flex w-full items-center gap-4"> <div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<Chip size="sm" variant="flat" color="secondary"> <Chip size="sm" variant="flat" color="secondary">
{membership.attributes.role} {membership.attributes.role}
</Chip> </Chip>

View File

@@ -1,6 +1,5 @@
"use client"; "use client";
import { Snippet } from "@heroui/snippet";
import { Trash2Icon } from "lucide-react"; import { Trash2Icon } from "lucide-react";
import { revokeApiKey } from "@/actions/api-keys/api-keys"; import { revokeApiKey } from "@/actions/api-keys/api-keys";
@@ -10,6 +9,7 @@ import {
AlertDescription, AlertDescription,
AlertTitle, AlertTitle,
} from "@/components/ui/alert/Alert"; } from "@/components/ui/alert/Alert";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { ModalButtons } from "@/components/ui/custom/custom-modal-buttons"; import { ModalButtons } from "@/components/ui/custom/custom-modal-buttons";
import { FALLBACK_VALUES } from "./api-keys/constants"; import { FALLBACK_VALUES } from "./api-keys/constants";
@@ -67,16 +67,12 @@ export const RevokeApiKeyModal = ({
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<p>Are you sure you want to revoke this API key?</p> <p>Are you sure you want to revoke this API key?</p>
<Snippet <CodeSnippet
hideSymbol value={`${apiKey?.attributes.name || FALLBACK_VALUES.UNNAMED_KEY}\nPrefix: ${apiKey?.attributes.prefix}`}
hideCopyButton={true} hideCopyButton
classNames={{ multiline
pre: "font-mono text-sm break-all whitespace-pre-wrap", className="w-full px-3 py-2 text-sm"
}} />
>
<p>{apiKey?.attributes.name || FALLBACK_VALUES.UNNAMED_KEY}</p>
<p className="mt-1 text-xs">Prefix: {apiKey?.attributes.prefix}</p>
</Snippet>
</div> </div>
{error && ( {error && (

View File

@@ -68,6 +68,7 @@ export const RoleItem = ({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setIsExpanded(!isExpanded)} onClick={() => setIsExpanded(!isExpanded)}
className="px-0"
> >
{isExpanded ? "Hide details" : "Show details"} {isExpanded ? "Hide details" : "Show details"}
</Button> </Button>

View File

@@ -4,13 +4,14 @@ import { Divider } from "@heroui/divider";
import { ProwlerShort } from "@/components/icons"; import { ProwlerShort } from "@/components/icons";
import { Card, CardContent } from "@/components/shadcn"; import { Card, CardContent } from "@/components/shadcn";
import { DateWithTime, InfoField, SnippetChip } from "@/components/ui/entities"; import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { DateWithTime, InfoField } from "@/components/ui/entities";
import { UserDataWithRoles } from "@/types/users"; import { UserDataWithRoles } from "@/types/users";
const TenantIdCopy = ({ id }: { id: string }) => { const TenantIdCopy = ({ id }: { id: string }) => {
return ( return (
<div className="flex max-w-full min-w-0 items-center gap-2 md:flex-col md:items-start md:justify-start"> <div className="flex max-w-full min-w-0 items-center gap-2 md:flex-col md:items-start md:justify-start">
<SnippetChip value={id} className="max-w-full" /> <CodeSnippet value={id} className="max-w-full" />
</div> </div>
); );
}; };