mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-29 23:04:23 +00:00
Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com> Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
198 lines
5.1 KiB
TypeScript
198 lines
5.1 KiB
TypeScript
"use client";
|
|
|
|
import type { PopoverDOM } from "driver.js";
|
|
import { XIcon } from "lucide-react";
|
|
import { flushSync } from "react-dom";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
|
|
import {
|
|
Button,
|
|
Card,
|
|
CardAction,
|
|
CardContent,
|
|
CardDescription,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle,
|
|
Progress,
|
|
} from "@/components/shadcn";
|
|
|
|
interface TourPopoverProps {
|
|
title: string;
|
|
description: string;
|
|
progressText: string;
|
|
progressValue: number;
|
|
previousLabel: string;
|
|
nextLabel: string;
|
|
closeLabel: string;
|
|
showPrevious: boolean;
|
|
showNext: boolean;
|
|
showClose: boolean;
|
|
previousDisabled: boolean;
|
|
onPrevious: () => void;
|
|
onNext: () => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
let activeRoot: Root | null = null;
|
|
let activeWrapper: HTMLElement | null = null;
|
|
|
|
function isDisplayed(element: HTMLElement): boolean {
|
|
return element.style.display !== "none";
|
|
}
|
|
|
|
function parseProgressValue(progressText: string): number {
|
|
const match = progressText.match(/(\d+)\D+(\d+)/);
|
|
if (!match) return 0;
|
|
|
|
const current = Number(match[1]);
|
|
const total = Number(match[2]);
|
|
if (total <= 0) return 0;
|
|
|
|
return Math.min(100, Math.max(0, (current / total) * 100));
|
|
}
|
|
|
|
function getText(element: HTMLElement, fallback: string): string {
|
|
const text = element.textContent?.trim();
|
|
return text && text.length > 0 ? text : fallback;
|
|
}
|
|
|
|
function hideDriverPopoverDom(popover: PopoverDOM) {
|
|
popover.title.hidden = true;
|
|
popover.description.hidden = true;
|
|
popover.footer.hidden = true;
|
|
popover.closeButton.hidden = true;
|
|
popover.arrow.hidden = true;
|
|
}
|
|
|
|
function getOrCreateMount(popover: PopoverDOM): HTMLElement {
|
|
const existing = popover.wrapper.querySelector<HTMLElement>(
|
|
"[data-tour-popover-root]",
|
|
);
|
|
if (existing) {
|
|
existing.style.width = "100%";
|
|
return existing;
|
|
}
|
|
|
|
const mount = document.createElement("div");
|
|
mount.dataset.tourPopoverRoot = "";
|
|
mount.style.width = "100%";
|
|
popover.wrapper.appendChild(mount);
|
|
return mount;
|
|
}
|
|
|
|
function TourPopover({
|
|
title,
|
|
description,
|
|
progressText,
|
|
progressValue,
|
|
previousLabel,
|
|
nextLabel,
|
|
closeLabel,
|
|
showPrevious,
|
|
showNext,
|
|
showClose,
|
|
previousDisabled,
|
|
onPrevious,
|
|
onNext,
|
|
onClose,
|
|
}: TourPopoverProps) {
|
|
return (
|
|
<Card variant="inner" className="gap-0 shadow-lg">
|
|
<CardHeader className="mb-4">
|
|
<CardTitle className="text-base leading-snug">{title}</CardTitle>
|
|
{showClose ? (
|
|
<CardAction>
|
|
<Button
|
|
aria-label={closeLabel}
|
|
size="icon-xs"
|
|
variant="bare"
|
|
onClick={onClose}
|
|
>
|
|
<XIcon aria-hidden="true" />
|
|
</Button>
|
|
</CardAction>
|
|
) : null}
|
|
{description ? (
|
|
<CardDescription className="mt-2 text-xs leading-relaxed">
|
|
{description}
|
|
</CardDescription>
|
|
) : null}
|
|
</CardHeader>
|
|
<CardContent className="mt-2">
|
|
{progressText ? (
|
|
<>
|
|
<CardDescription className="mt-2 text-xs leading-relaxed">
|
|
{progressText}
|
|
</CardDescription>
|
|
<Progress value={progressValue} />
|
|
</>
|
|
) : null}
|
|
</CardContent>
|
|
<CardFooter className="mt-4 justify-end gap-2 px-0">
|
|
{showPrevious ? (
|
|
<Button
|
|
disabled={previousDisabled}
|
|
size="sm"
|
|
// Match the app's modal secondary action (e.g. Launch Scan's Cancel).
|
|
variant="outline"
|
|
onClick={onPrevious}
|
|
>
|
|
{previousLabel}
|
|
</Button>
|
|
) : null}
|
|
{showNext ? (
|
|
<Button size="sm" onClick={onNext}>
|
|
{nextLabel}
|
|
</Button>
|
|
) : null}
|
|
</CardFooter>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export function unmountActiveTourPopover() {
|
|
activeRoot?.unmount();
|
|
activeRoot = null;
|
|
activeWrapper = null;
|
|
}
|
|
|
|
export function renderTourPopover(popover: PopoverDOM) {
|
|
if (activeWrapper !== popover.wrapper) {
|
|
unmountActiveTourPopover();
|
|
}
|
|
|
|
hideDriverPopoverDom(popover);
|
|
|
|
const mount = getOrCreateMount(popover);
|
|
activeRoot = activeRoot ?? createRoot(mount);
|
|
activeWrapper = popover.wrapper;
|
|
|
|
const progressText = isDisplayed(popover.progress)
|
|
? getText(popover.progress, "")
|
|
: "";
|
|
|
|
flushSync(() => {
|
|
activeRoot?.render(
|
|
<TourPopover
|
|
title={getText(popover.title, "")}
|
|
description={getText(popover.description, "")}
|
|
progressText={progressText}
|
|
progressValue={parseProgressValue(progressText)}
|
|
previousLabel={getText(popover.previousButton, "Back")}
|
|
nextLabel={getText(popover.nextButton, "Next")}
|
|
closeLabel={popover.closeButton.getAttribute("aria-label") ?? "Close"}
|
|
showPrevious={isDisplayed(popover.previousButton)}
|
|
showNext={isDisplayed(popover.nextButton)}
|
|
showClose={isDisplayed(popover.closeButton)}
|
|
previousDisabled={popover.previousButton.disabled}
|
|
onPrevious={() => {
|
|
if (!popover.previousButton.disabled) popover.previousButton.click();
|
|
}}
|
|
onNext={() => popover.nextButton.click()}
|
|
onClose={() => popover.closeButton.click()}
|
|
/>,
|
|
);
|
|
});
|
|
}
|