mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
188 lines
5.6 KiB
TypeScript
188 lines
5.6 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useState } from "react";
|
|
|
|
import { MutedIcon } from "@/components/icons";
|
|
import { Button } from "@/components/shadcn";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/shadcn/popover";
|
|
import { DOCS_URLS } from "@/lib/external-urls";
|
|
import { cn } from "@/lib/utils";
|
|
import { FINDING_DELTA, type FindingDelta } from "@/types";
|
|
|
|
export const DeltaValues = FINDING_DELTA;
|
|
|
|
export type DeltaType = Exclude<FindingDelta, null>;
|
|
|
|
interface NotificationIndicatorProps {
|
|
delta?: DeltaType;
|
|
isMuted?: boolean;
|
|
mutedReason?: string;
|
|
showDeltaWhenMuted?: boolean;
|
|
reserveMutedSlot?: boolean;
|
|
}
|
|
|
|
export const NotificationIndicator = ({
|
|
delta,
|
|
isMuted = false,
|
|
mutedReason,
|
|
showDeltaWhenMuted = false,
|
|
reserveMutedSlot = false,
|
|
}: NotificationIndicatorProps) => {
|
|
const hasDelta = delta === DeltaValues.NEW || delta === DeltaValues.CHANGED;
|
|
|
|
if (showDeltaWhenMuted && reserveMutedSlot) {
|
|
return (
|
|
<div
|
|
data-slot="notification-indicator"
|
|
className="flex shrink-0 items-center gap-1"
|
|
>
|
|
<div
|
|
data-slot="notification-muted-slot"
|
|
className="flex w-5 shrink-0 items-center justify-center"
|
|
>
|
|
{isMuted ? <MutedIndicator mutedReason={mutedReason} /> : null}
|
|
</div>
|
|
<div
|
|
data-slot="notification-delta-slot"
|
|
className="flex w-2 shrink-0 items-center justify-center"
|
|
>
|
|
{hasDelta ? <DeltaIndicator delta={delta} /> : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isMuted && hasDelta && showDeltaWhenMuted) {
|
|
return (
|
|
<div className="flex shrink-0 items-center gap-1">
|
|
<MutedIndicator mutedReason={mutedReason} />
|
|
<DeltaIndicator delta={delta} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Uses Popover (not Tooltip) because the content has an interactive link.
|
|
// Radix Tooltip does not support interactive content — clicks fall through.
|
|
if (isMuted) {
|
|
return <MutedIndicator mutedReason={mutedReason} />;
|
|
}
|
|
|
|
// Show dot with tooltip for new or changed findings
|
|
if (hasDelta) {
|
|
return <DeltaIndicator delta={delta} />;
|
|
}
|
|
|
|
// No indicator - return minimal width placeholder
|
|
return <div className="w-2 shrink-0" />;
|
|
};
|
|
|
|
function DeltaIndicator({
|
|
delta,
|
|
}: {
|
|
delta: typeof DeltaValues.NEW | typeof DeltaValues.CHANGED;
|
|
}) {
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<button
|
|
type="button"
|
|
onClick={(e) => e.stopPropagation()}
|
|
onMouseEnter={() => setOpen(true)}
|
|
onMouseLeave={() => setOpen(false)}
|
|
className="flex w-2 shrink-0 cursor-pointer items-center justify-center bg-transparent p-0"
|
|
>
|
|
<div
|
|
className={cn(
|
|
"size-1.5 rounded-full",
|
|
delta === DeltaValues.NEW ? "bg-bg-data-high" : "bg-bg-data-low",
|
|
)}
|
|
/>
|
|
</button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="border-border-neutral-tertiary bg-bg-neutral-tertiary w-auto rounded-lg px-2 py-1.5 shadow-lg"
|
|
sideOffset={4}
|
|
onMouseEnter={() => setOpen(true)}
|
|
onMouseLeave={() => setOpen(false)}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="flex items-center gap-1 text-xs">
|
|
<span>
|
|
{delta === DeltaValues.NEW
|
|
? "New finding."
|
|
: "Status changed since the previous scan."}
|
|
</span>
|
|
<Button
|
|
aria-label="Learn more about findings"
|
|
variant="link"
|
|
size="default"
|
|
className="text-button-primary h-auto min-w-0 p-0 text-xs"
|
|
asChild
|
|
>
|
|
<a
|
|
href={DOCS_URLS.FINDINGS_ANALYSIS}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
Learn more
|
|
</a>
|
|
</Button>
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|
|
|
|
/** Muted indicator with hover-triggered Popover for interactive link. */
|
|
function MutedIndicator({ mutedReason }: { mutedReason?: string }) {
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="flex w-5 shrink-0 cursor-pointer items-center justify-center bg-transparent p-0"
|
|
onClick={(e) => e.stopPropagation()}
|
|
onMouseEnter={() => setOpen(true)}
|
|
onMouseLeave={() => setOpen(false)}
|
|
>
|
|
<MutedIcon className="text-bg-data-muted size-3" />
|
|
</button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="border-border-neutral-tertiary bg-bg-neutral-tertiary w-auto rounded-lg px-2 py-1.5 shadow-lg"
|
|
sideOffset={4}
|
|
onMouseEnter={() => setOpen(true)}
|
|
onMouseLeave={() => setOpen(false)}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<Link
|
|
href="/mutelist"
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="text-button-tertiary hover:text-button-tertiary-hover flex items-center gap-1 text-xs underline-offset-4"
|
|
>
|
|
{mutedReason ? (
|
|
<>
|
|
<span className="text-text-neutral-primary">Mute rule:</span>
|
|
<span className="max-w-[150px] truncate">{mutedReason}</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<span className="text-text-neutral-primary">Mute rule:</span>
|
|
<span>view rules</span>
|
|
</>
|
|
)}
|
|
</Link>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|