From 80d9cde60b92521bdd4b4262a2b66c32ce51a218 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 19 Feb 2025 16:10:29 +0100 Subject: [PATCH] feat(scans): update the progress for executing scans (#6972) --- ui/actions/scans/scans.ts | 91 ++++++++++++++----- ui/app/(prowler)/scans/page.tsx | 22 ++--- .../findings/table/finding-detail.tsx | 9 +- ui/components/icons/Icons.tsx | 24 +++++ .../invitations/invitation-details.tsx | 2 +- .../table/finding-detail.tsx | 12 ++- .../workflow/credentials-role-helper.tsx | 6 +- ui/components/scans/auto-refresh.tsx | 24 +++++ ui/components/scans/button-refresh-data.tsx | 30 ------ ui/components/scans/index.ts | 2 +- ui/components/scans/table/scan-detail.tsx | 19 ++-- .../scans/table/scans/column-get-scans.tsx | 10 +- ui/components/ui/table/status-badge.tsx | 34 +++---- 13 files changed, 188 insertions(+), 97 deletions(-) create mode 100644 ui/components/scans/auto-refresh.tsx delete mode 100644 ui/components/scans/button-refresh-data.tsx diff --git a/ui/actions/scans/scans.ts b/ui/actions/scans/scans.ts index 3ac7d897d6..a1064a1085 100644 --- a/ui/actions/scans/scans.ts +++ b/ui/actions/scans/scans.ts @@ -48,6 +48,42 @@ export const getScans = async ({ } }; +export const getScansByState = async () => { + const session = await auth(); + + const keyServer = process.env.API_BASE_URL; + const url = new URL(`${keyServer}/scans`); + + // Request only the necessary fields to optimize the response + url.searchParams.append("fields[scans]", "state"); + + try { + const response = await fetch(url.toString(), { + headers: { + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + + if (!response.ok) { + try { + const errorData = await response.json(); + throw new Error(errorData?.message || "Failed to fetch scans by state"); + } catch { + throw new Error("Failed to fetch scans by state"); + } + } + + const data = await response.json(); + + return parseStringify(data); + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error fetching scans by state:", error); + return undefined; + } +}; + export const getScan = async (scanId: string) => { const session = await auth(); @@ -77,11 +113,30 @@ export const scanOnDemand = async (formData: FormData) => { const keyServer = process.env.API_BASE_URL; const providerId = formData.get("providerId"); - const scanName = formData.get("scanName"); + const scanName = formData.get("scanName") || undefined; + + if (!providerId) { + return { error: "Provider ID is required" }; + } const url = new URL(`${keyServer}/scans`); try { + const requestBody = { + data: { + type: "scans", + attributes: scanName ? { name: scanName } : {}, + relationships: { + provider: { + data: { + type: "providers", + id: providerId, + }, + }, + }, + }, + }; + const response = await fetch(url.toString(), { method: "POST", headers: { @@ -89,32 +144,26 @@ export const scanOnDemand = async (formData: FormData) => { Accept: "application/vnd.api+json", Authorization: `Bearer ${session?.accessToken}`, }, - body: JSON.stringify({ - data: { - type: "scans", - attributes: { - name: scanName, - }, - relationships: { - provider: { - data: { - type: "providers", - id: providerId, - }, - }, - }, - }, - }), + body: JSON.stringify(requestBody), }); + + if (!response.ok) { + try { + const errorData = await response.json(); + throw new Error(errorData?.message || "Failed to start scan"); + } catch { + throw new Error("Failed to start scan"); + } + } + const data = await response.json(); + revalidatePath("/scans"); return parseStringify(data); } catch (error) { // eslint-disable-next-line no-console - console.error(error); - return { - error: getErrorMessage(error), - }; + console.error("Error starting scan:", error); + return { error: getErrorMessage(error) }; } }; diff --git a/ui/app/(prowler)/scans/page.tsx b/ui/app/(prowler)/scans/page.tsx index db38bd4e97..05435149da 100644 --- a/ui/app/(prowler)/scans/page.tsx +++ b/ui/app/(prowler)/scans/page.tsx @@ -2,10 +2,10 @@ import { Spacer } from "@nextui-org/react"; import { Suspense } from "react"; import { getProvider, getProviders } from "@/actions/providers"; -import { getScans } from "@/actions/scans"; +import { getScans, getScansByState } from "@/actions/scans"; import { FilterControls, filterScans } from "@/components/filters"; import { - ButtonRefreshData, + AutoRefresh, NoProvidersAdded, NoProvidersConnected, } from "@/components/scans"; @@ -14,7 +14,7 @@ import { SkeletonTableScans } from "@/components/scans/table"; import { ColumnGetScans } from "@/components/scans/table/scans"; import { Header } from "@/components/ui"; import { DataTable, DataTableFilterCustom } from "@/components/ui/table"; -import { ProviderProps, SearchParamsProps } from "@/types"; +import { ProviderProps, ScanProps, SearchParamsProps } from "@/types"; export default async function Scans({ searchParams, @@ -32,7 +32,7 @@ export default async function Scans({ }); const providerInfo = - providersData?.data.map((provider: ProviderProps) => ({ + providersData?.data?.map((provider: ProviderProps) => ({ providerId: provider.id, alias: provider.attributes.alias, providerType: provider.attributes.provider, @@ -48,6 +48,12 @@ export default async function Scans({ (provider: ProviderProps) => !provider.attributes.connection.connected, ); + // Get scans data to check for executing scans + const scansData = await getScansByState(); + const hasExecutingScan = scansData?.data?.some( + (scan: ScanProps) => scan.attributes.state === "executing", + ); + return ( <> {thereIsNoProviders && ( @@ -70,7 +76,7 @@ export default async function Scans({ ) : ( <>
- + @@ -82,12 +88,6 @@ export default async function Scans({ - { - "use server"; - await getScans({}); - }} - /> }> diff --git a/ui/components/findings/table/finding-detail.tsx b/ui/components/findings/table/finding-detail.tsx index 71f2de2eb1..bc1d9e6992 100644 --- a/ui/components/findings/table/finding-detail.tsx +++ b/ui/components/findings/table/finding-detail.tsx @@ -101,7 +101,10 @@ export const FindingDetail = ({
- + {attributes.check_id} @@ -160,7 +163,7 @@ export const FindingDetail = ({ {/* CLI Command section */} {attributes.check_metadata.remediation.code.cli && ( - + {attributes.check_metadata.remediation.code.cli} @@ -191,7 +194,7 @@ export const FindingDetail = ({ {/* Resource Details */}
- + {renderValue(resource.uid)} diff --git a/ui/components/icons/Icons.tsx b/ui/components/icons/Icons.tsx index b8f448afd3..963f7d9b4f 100644 --- a/ui/components/icons/Icons.tsx +++ b/ui/components/icons/Icons.tsx @@ -808,3 +808,27 @@ export const InfoIcon: React.FC = ({ ); + +export const SpinnerIcon: React.FC = ({ + size = 24, + width, + height, + className, + ...props +}) => ( + + + +); diff --git a/ui/components/invitations/invitation-details.tsx b/ui/components/invitations/invitation-details.tsx index 17525e1eca..f91aef5f19 100644 --- a/ui/components/invitations/invitation-details.tsx +++ b/ui/components/invitations/invitation-details.tsx @@ -98,7 +98,7 @@ export const InvitationDetails = ({ attributes }: InvitationDetailsProps) => { }} hideSymbol variant="bordered" - className="overflow-hidden text-ellipsis whitespace-nowrap" + className="overflow-hidden text-ellipsis whitespace-nowrap bg-gray-50 py-1 dark:bg-slate-800" >

{invitationLink} diff --git a/ui/components/overview/new-findings-table/table/finding-detail.tsx b/ui/components/overview/new-findings-table/table/finding-detail.tsx index d2febae564..71927e520b 100644 --- a/ui/components/overview/new-findings-table/table/finding-detail.tsx +++ b/ui/components/overview/new-findings-table/table/finding-detail.tsx @@ -105,7 +105,11 @@ export const FindingDetail = ({ {remediation.code.cli && (

CLI Command:

- +

{remediation.code.cli}

@@ -148,7 +152,11 @@ export const FindingDetail = ({

Resource ID

- +

{resource.uid}

diff --git a/ui/components/providers/workflow/credentials-role-helper.tsx b/ui/components/providers/workflow/credentials-role-helper.tsx index 282a64e38e..90bad1b5e7 100644 --- a/ui/components/providers/workflow/credentials-role-helper.tsx +++ b/ui/components/providers/workflow/credentials-role-helper.tsx @@ -33,7 +33,11 @@ export const CredentialsRoleHelper = () => {

The External ID will also be required:

- +

{session?.tenantId}

diff --git a/ui/components/scans/auto-refresh.tsx b/ui/components/scans/auto-refresh.tsx new file mode 100644 index 0000000000..218d5ccc32 --- /dev/null +++ b/ui/components/scans/auto-refresh.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; + +interface AutoRefreshProps { + hasExecutingScan: boolean; +} + +export function AutoRefresh({ hasExecutingScan }: AutoRefreshProps) { + const router = useRouter(); + + useEffect(() => { + if (!hasExecutingScan) return; + + const interval = setInterval(() => { + router.refresh(); + }, 5000); + + return () => clearInterval(interval); + }, [hasExecutingScan, router]); + + return null; +} diff --git a/ui/components/scans/button-refresh-data.tsx b/ui/components/scans/button-refresh-data.tsx deleted file mode 100644 index 41d05e3981..0000000000 --- a/ui/components/scans/button-refresh-data.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; - -import { RefreshCcwIcon } from "lucide-react"; -import { useTransition } from "react"; - -import { CustomButton } from "../ui/custom"; - -export const ButtonRefreshData = ({ - onPress, -}: { - onPress: () => Promise; -}) => { - const [isPending, startTransition] = useTransition(); - - return ( - } - isLoading={isPending} - onPress={() => { - startTransition(async () => { - await onPress(); - }); - }} - /> - ); -}; diff --git a/ui/components/scans/index.ts b/ui/components/scans/index.ts index aa42edfe3c..9da9587028 100644 --- a/ui/components/scans/index.ts +++ b/ui/components/scans/index.ts @@ -1,4 +1,4 @@ -export * from "./button-refresh-data"; +export * from "./auto-refresh"; export * from "./link-to-findings-from-scan"; export * from "./no-providers-added"; export * from "./no-providers-connected"; diff --git a/ui/components/scans/table/scan-detail.tsx b/ui/components/scans/table/scan-detail.tsx index 7307ff53f0..2db2bb20fc 100644 --- a/ui/components/scans/table/scan-detail.tsx +++ b/ui/components/scans/table/scan-detail.tsx @@ -62,7 +62,8 @@ export const ScanDetail = ({ {/* Header */}
@@ -86,11 +87,20 @@ export const ScanDetail = ({
+ + + {renderValue(taskDetails?.attributes.task_args.scan_id)} + + + {scan.state === "failed" && taskDetails?.attributes.result && ( <> {taskDetails.attributes.result.exc_message && ( - + {taskDetails.attributes.result.exc_message.join("\n")} @@ -101,11 +111,6 @@ export const ScanDetail = ({ {renderValue(taskDetails.attributes.result.exc_type)} - - - {renderValue(taskDetails?.attributes.task_args.scan_id)} - -
)} diff --git a/ui/components/scans/table/scans/column-get-scans.tsx b/ui/components/scans/table/scans/column-get-scans.tsx index 72a70f9311..5dcddd18cb 100644 --- a/ui/components/scans/table/scans/column-get-scans.tsx +++ b/ui/components/scans/table/scans/column-get-scans.tsx @@ -87,10 +87,12 @@ export const ColumnGetScans: ColumnDef[] = [ attributes: { state }, } = getScanData(row); return ( - +
+ +
); }, }, diff --git a/ui/components/ui/table/status-badge.tsx b/ui/components/ui/table/status-badge.tsx index d9a241ede6..7ac6a99bef 100644 --- a/ui/components/ui/table/status-badge.tsx +++ b/ui/components/ui/table/status-badge.tsx @@ -1,6 +1,9 @@ -import { Chip, CircularProgress } from "@nextui-org/react"; +import { Chip } from "@nextui-org/react"; +import clsx from "clsx"; import React from "react"; +import { SpinnerIcon } from "@/components/icons"; + export type Status = | "available" | "scheduled" @@ -25,39 +28,38 @@ export const StatusBadge = ({ status, size = "sm", loadingProgress, + className, ...props }: { status: Status; size?: "sm" | "md" | "lg"; loadingProgress?: number; + className?: string; }) => { const color = statusColorMap[status as keyof typeof statusColorMap]; return ( {status === "executing" ? ( -
- - executing +
+ + + {loadingProgress}% + + executing
) : ( - status + {status} )} );