mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 04:21:52 +00:00
feat: scan details can be shared now in the URL
This commit is contained in:
@@ -16,10 +16,10 @@ export default async function Scans({
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) {
|
||||
const searchParamsKey = JSON.stringify(searchParams || {});
|
||||
// const providersData = await getProviders({
|
||||
// filters: { "filter[connected]": "true" },
|
||||
// });
|
||||
const filteredParams = { ...searchParams };
|
||||
delete filteredParams.scanId;
|
||||
const searchParamsKey = JSON.stringify(filteredParams);
|
||||
|
||||
const providersData = await getProviders({});
|
||||
|
||||
const providerInfo = providersData?.data?.length
|
||||
@@ -59,9 +59,11 @@ const SSRDataTableScans = async ({
|
||||
const page = parseInt(searchParams.page?.toString() || "1", 10);
|
||||
const sort = searchParams.sort?.toString();
|
||||
|
||||
// Extract all filter parameters
|
||||
// Extract all filter parameters, excluding scanId
|
||||
const filters = Object.fromEntries(
|
||||
Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")),
|
||||
Object.entries(searchParams).filter(
|
||||
([key]) => key.startsWith("filter[") && key !== "scanId",
|
||||
),
|
||||
);
|
||||
|
||||
// Extract query from filters
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./edit-scan-form";
|
||||
export * from "./scan-on-demand-form";
|
||||
export * from "./schedule-form";
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { RocketIcon } from "lucide-react";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { scanOnDemand } from "@/actions/scans";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton, CustomInput } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { onDemandScanFormSchema } from "@/types";
|
||||
|
||||
export const ScanOnDemandForm = ({
|
||||
providerId,
|
||||
scanName,
|
||||
scannerArgs,
|
||||
setIsOpen,
|
||||
}: {
|
||||
providerId: string;
|
||||
scanName?: string;
|
||||
scannerArgs?: { checksToExecute: string[] };
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const formSchema = onDemandScanFormSchema();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
providerId: providerId,
|
||||
scanName: scanName,
|
||||
scannerArgs: scannerArgs,
|
||||
},
|
||||
});
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmitClient = async (values: z.infer<typeof formSchema>) => {
|
||||
const formData = new FormData();
|
||||
|
||||
// Loop through form values and add to formData, converting objects to JSON strings
|
||||
Object.entries(values).forEach(
|
||||
([key, value]) =>
|
||||
value !== undefined &&
|
||||
formData.append(
|
||||
key,
|
||||
typeof value === "object" ? JSON.stringify(value) : value,
|
||||
),
|
||||
);
|
||||
|
||||
const data = await scanOnDemand(formData);
|
||||
|
||||
if (data?.errors && data.errors.length > 0) {
|
||||
const error = data.errors[0];
|
||||
const errorMessage = `${error.detail}`;
|
||||
// show error
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Oops! Something went wrong",
|
||||
description: errorMessage,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Success!",
|
||||
description: "The scan was launched successfully.",
|
||||
});
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmitClient)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<input type="hidden" name="providerId" value={providerId} />
|
||||
|
||||
<div>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="scanName"
|
||||
type="text"
|
||||
label="Scan Name"
|
||||
labelPlacement="outside"
|
||||
placeholder={scanName}
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
isInvalid={!!form.formState.errors.scanName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full justify-center sm:space-x-6">
|
||||
<CustomButton
|
||||
type="button"
|
||||
ariaLabel="Cancel"
|
||||
className="w-full bg-transparent"
|
||||
variant="faded"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
onPress={() => setIsOpen(false)}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
<span>Cancel</span>
|
||||
</CustomButton>
|
||||
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel="Start scan now"
|
||||
className="w-full"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="lg"
|
||||
isLoading={isLoading}
|
||||
startContent={!isLoading && <RocketIcon size={24} />}
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>Start now</span>}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
|
||||
@@ -125,11 +126,16 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
id: "moreInfo",
|
||||
header: "Details",
|
||||
cell: ({ row }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const scanId = searchParams.get("scanId");
|
||||
const isOpen = scanId === row.original.id;
|
||||
|
||||
return (
|
||||
<TriggerSheet
|
||||
triggerComponent={<InfoIcon className="text-primary" size={16} />}
|
||||
title="Scan Details"
|
||||
description="View the scan details"
|
||||
defaultOpen={isOpen}
|
||||
>
|
||||
<DataTableRowDetails entityId={row.original.id} />
|
||||
</TriggerSheet>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getScan } from "@/actions/scans";
|
||||
@@ -7,9 +8,25 @@ import { ScanDetail, SkeletonTableScans } from "@/components/scans/table";
|
||||
import { ScanProps } from "@/types";
|
||||
|
||||
export const DataTableRowDetails = ({ entityId }: { entityId: string }) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [scanDetails, setScanDetails] = useState<ScanProps | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Add scanId to URL
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set("scanId", entityId);
|
||||
router.push(`?${params.toString()}`, { scroll: false });
|
||||
|
||||
// Cleanup function: remove scanId from URL when component unmounts
|
||||
return () => {
|
||||
const newParams = new URLSearchParams(searchParams.toString());
|
||||
newParams.delete("scanId");
|
||||
router.push(`?${newParams.toString()}`, { scroll: false });
|
||||
};
|
||||
}, [entityId, router, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchScanDetails = async () => {
|
||||
try {
|
||||
|
||||
@@ -12,6 +12,7 @@ interface TriggerSheetProps {
|
||||
title: string;
|
||||
description: string;
|
||||
children: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
export function TriggerSheet({
|
||||
@@ -19,9 +20,10 @@ export function TriggerSheet({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
defaultOpen = false,
|
||||
}: TriggerSheetProps) {
|
||||
return (
|
||||
<Sheet>
|
||||
<Sheet defaultOpen={defaultOpen}>
|
||||
<SheetTrigger className="flex items-center gap-2">
|
||||
{triggerComponent}
|
||||
</SheetTrigger>
|
||||
|
||||
Reference in New Issue
Block a user