chore(scans): improve scan details (#6665)

This commit is contained in:
Pablo Lara
2025-01-23 13:20:41 +01:00
committed by Pepe Fagoaga
parent 9d04a1bc52
commit 34b5f483d7
12 changed files with 268 additions and 269 deletions
@@ -14,7 +14,7 @@ export default function ProviderLayout({ children }: ProviderLayoutProps) {
return (
<>
<NavigationHeader
title="Connect your cloud account"
title="Connect a Cloud Provider"
icon="icon-park-outline:close-small"
href="/providers"
/>
+2 -2
View File
@@ -22,10 +22,10 @@ export default async function Providers({
return (
<>
<Header title="Providers" icon="fluent:cloud-sync-24-regular" />
<Header title="Cloud Providers" icon="fluent:cloud-sync-24-regular" />
<Spacer y={4} />
<FilterControls search providers />
<FilterControls search />
<Spacer y={8} />
<div className="flex items-center gap-4 md:justify-end">
<ManageGroupsButton />
+6 -3
View File
@@ -51,8 +51,6 @@ export default async function Scans({
return (
<>
<Header title="Scans" icon="lucide:scan-search" />
{thereIsNoProviders && (
<>
<Spacer y={4} />
@@ -64,18 +62,23 @@ export default async function Scans({
<>
{thereIsNoProvidersConnected ? (
<>
<Header title="Scans" icon="lucide:scan-search" />
<Spacer y={8} />
<NoProvidersConnected />
<Spacer y={4} />
<Spacer y={8} />
</>
) : (
<>
<Header title="Scans" icon="lucide:scan-search" />
<LaunchScanWorkflow providers={providerInfo} />
<Spacer y={4} />
<ScanWarningBar />
<Spacer y={8} />
</>
)}
<div className="grid grid-cols-12 items-start gap-4">
<div className="col-span-12">
<div className="flex flex-row items-center justify-between">
@@ -7,13 +7,13 @@ export const AddProviderButton = () => {
return (
<CustomButton
asLink="/providers/connect-account"
ariaLabel="Add Account"
ariaLabel="Add Cloud Provider"
variant="solid"
color="action"
size="md"
endContent={<AddIcon size={20} />}
>
Add Account
Add Cloud Provider
</CustomButton>
);
};
@@ -116,9 +116,13 @@ export const TestConnectionForm = ({
error: connected ? null : error || "Unknown error",
});
if (connected) {
if (connected && isUpdated) return router.push("/providers");
if (connected && !isUpdated) {
try {
// Schedule daily scan by default
const data = await scheduleDaily(formData);
if (data.error) {
setApiErrorMessage(data.error);
form.setError("providerId", {
@@ -126,19 +130,8 @@ export const TestConnectionForm = ({
message: data.error,
});
} else {
const urlParams = new URLSearchParams(window.location.search);
const isUpdated = urlParams.get("updated") === "true";
if (!isUpdated) {
setIsRedirecting(true);
router.push("/scans");
return;
} else {
setConnectionStatus({
connected: true,
error: null,
});
}
setIsRedirecting(true);
router.push("/scans");
}
} catch (error) {
form.setError("providerId", {
@@ -203,8 +196,8 @@ export const TestConnectionForm = ({
<p className="text-xl font-medium text-primary">
Scan initiated successfully
</p>
<p className="mt-2 text-gray-500">
Redirecting to scans dashboard...
<p className="mt-2 text-small font-bold text-gray-500">
Redirecting to scans job details...
</p>
</div>
</div>
@@ -219,10 +212,14 @@ export const TestConnectionForm = ({
>
<div className="text-left">
<div className="mb-2 text-xl font-medium">
Check connection and launch scan
{!isUpdated
? "Check connection and launch scan"
: "Check connection"}
</div>
<p className="py-2 text-small text-default-500">
A successful connection will launch a daily scheduled scan.
{!isUpdated
? "A successful connection will launch a daily scheduled scan."
: "A successful connection will redirect you to the providers page."}
</p>
</div>
@@ -267,6 +264,12 @@ export const TestConnectionForm = ({
</p>
)} */}
{isUpdated && !connectionStatus?.error && (
<p className="py-2 text-small text-default-500">
Check the new credentials and test the connection.
</p>
)}
<input type="hidden" name="providerId" value={providerId} />
<div className="flex w-full justify-end sm:space-x-6">
@@ -307,27 +310,18 @@ export const TestConnectionForm = ({
type={
isUpdated && connectionStatus?.connected ? "button" : "submit"
}
onPress={
isUpdated && connectionStatus?.connected
? () => router.push("/providers")
: undefined
}
ariaLabel={"Save"}
className="w-1/2"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
endContent={!isLoading && <RocketIcon size={24} />}
endContent={!isLoading && !isUpdated && <RocketIcon size={24} />}
>
{isLoading ? (
<>Loading</>
) : (
<span>
{isUpdated && connectionStatus?.connected
? "Go to providers"
: "Launch scan"}
</span>
<span>{isUpdated ? "Check connection" : "Launch scan"}</span>
)}
</CustomButton>
)}
@@ -8,21 +8,21 @@ import { VerticalSteps } from "./vertical-steps";
const steps = [
{
title: "Add your cloud account",
title: "Add your cloud provider",
description:
"Select the cloud provider for the account to connect and specify whether to use an IAM role or credentials for access.",
href: "/providers/connect-account",
},
{
title: "Add credentials to your cloud account",
title: "Add credentials to your cloud provider",
description:
"Provide the credentials required to connect to the cloud account.",
"Provide the credentials required to connect to the cloud provider.",
href: "/providers/add-credentials",
},
{
title: "Check connection and launch scan",
description:
"Verify the connection to ensure that the provided credentials are valid for accessing the cloud account and initiating a scan.",
"Verify the connection to ensure that the provided credentials are valid for accessing the cloud provider and initiating a scan.",
href: "/providers/test-connection",
},
];
@@ -39,10 +39,10 @@ export const WorkflowAddProvider = () => {
return (
<section className="max-w-sm">
<h1 className="mb-2 text-xl font-medium" id="getting-started">
Add a cloud account
Add a cloud provider
</h1>
<p className="mb-5 text-small text-default-500">
Complete the steps to configure the cloud account, enabling the launch
Complete the steps to configure the cloud provider, enabling the launch
of the first scan once completed.
</p>
<Progress
+32 -19
View File
@@ -1,30 +1,43 @@
"use client";
import { Card, CardBody } from "@nextui-org/react";
import React from "react";
import { InfoIcon } from "../icons/Icons";
import { CustomButton } from "../ui/custom";
export const NoProvidersAdded = () => {
return (
<div className="flex h-screen items-center justify-center">
<Card shadow="sm" className="w-full max-w-md dark:bg-prowler-blue-400">
<CardBody className="space-y-6 p-6 text-center">
<h2 className="text-xl font-bold">No Cloud Accounts Configured</h2>
<p className="text-md text-gray-600">
You don&apos;t have any cloud accounts configured yet. This is the
first step to get started.
</p>
<CustomButton
asLink="/providers/connect-account"
ariaLabel="Go to Add Cloud Account page"
variant="solid"
color="action"
size="lg"
>
Add Cloud Account
</CustomButton>
</CardBody>
</Card>
<div className="dark:bg-prowler-blue-900 flex min-h-screen items-center justify-center">
<div className="mx-auto w-full max-w-7xl px-4">
<Card className="mx-auto w-full max-w-3xl rounded-lg dark:bg-prowler-blue-400">
<CardBody className="flex flex-col items-center space-y-4 p-6 text-center sm:p-8">
<div className="flex flex-col items-center space-y-4">
<InfoIcon className="h-10 w-10 text-gray-800 dark:text-white" />
<h2 className="text-2xl font-bold text-gray-800 dark:text-white">
No Cloud Providers Configured
</h2>
</div>
<div className="flex flex-col items-center space-y-3">
<p className="text-md leading-relaxed text-gray-600 dark:text-gray-300">
No cloud providers have been configured. Start by setting up a
cloud provider.
</p>
</div>
<CustomButton
asLink="/providers/connect-account"
ariaLabel="Go to Add Cloud Provider page"
className="w-full max-w-xs justify-center"
variant="solid"
color="action"
size="lg"
>
Get Started
</CustomButton>
</CardBody>
</Card>
</div>
</div>
);
};
+25 -13
View File
@@ -1,31 +1,43 @@
"use client";
import { Card, CardBody } from "@nextui-org/react";
import React from "react";
import { InfoIcon } from "../icons/Icons";
import { CustomButton } from "../ui/custom";
export const NoProvidersConnected = () => {
return (
<div className="flex items-center justify-center">
<Card shadow="sm" className="w-full max-w-md dark:bg-prowler-blue-400">
<CardBody className="space-y-6 p-6 text-center">
<h2 className="text-xl font-bold">No Cloud Accounts Connected</h2>
<p className="text-md text-gray-600">
All your cloud accounts are currently disconnected. Please review
their configuration to proceed.
<div className="flex items-center justify-start rounded-lg border-1 border-system-warning-light px-4 py-6 shadow-box dark:bg-prowler-blue-400">
<div className="flex w-full flex-col items-start gap-6 md:flex-row md:items-center md:justify-between md:gap-8">
<div className="flex flex-col space-y-3">
<div className="flex items-center justify-start gap-3">
<InfoIcon className="h-6 w-6 text-gray-800 dark:text-white" />
<h2 className="text-lg font-bold text-gray-800 dark:text-white">
No Connected Cloud Providers
</h2>
</div>
<p className="text-sm text-gray-600 dark:text-gray-300">
No cloud providers are currently connected. Connecting a cloud
provider is required to launch on-demand scans.
</p>
<p className="text-sm text-gray-600 dark:text-gray-300">
Once the cloud providers are correctly configured, this message will
disappear, and on-demand scans can be launched.
</p>
</div>
<div className="w-full md:w-auto md:flex-shrink-0">
<CustomButton
asLink="/providers"
ariaLabel="Go to Cloud accounts page"
className="w-full justify-center md:w-fit"
ariaLabel="Go to Cloud providers page"
variant="solid"
color="action"
size="lg"
size="md"
>
Review Cloud Accounts
Review Cloud Providers
</CustomButton>
</CardBody>
</Card>
</div>
</div>
</div>
);
};
+139 -188
View File
@@ -1,214 +1,165 @@
"use client";
import { Card, CardBody, CardHeader, Divider } from "@nextui-org/react";
import { Snippet } from "@nextui-org/react";
import { DateWithTime, SnippetId } from "@/components/ui/entities";
import { ConnectionTrue } from "@/components/icons";
import { ConnectionFalse } from "@/components/icons/Icons";
import {
DateWithTime,
EntityInfoShort,
InfoField,
} from "@/components/ui/entities";
import { StatusBadge } from "@/components/ui/table/status-badge";
import { ScanProps, TaskDetails } from "@/types";
import { ProviderProps, ScanProps, TaskDetails } from "@/types";
interface ScanDetailsProps {
const renderValue = (value: string | null | undefined) => {
return value && value.trim() !== "" ? value : "-";
};
const formatDuration = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
const parts = [];
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0) parts.push(`${minutes}m`);
if (remainingSeconds > 0 || parts.length === 0)
parts.push(`${remainingSeconds}s`);
return parts.join(" ");
};
const Section = ({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) => (
<div className="flex flex-col gap-4 rounded-lg p-4 shadow dark:bg-prowler-blue-400">
<h3 className="text-md font-medium text-gray-800 dark:text-prowler-theme-pale/90">
{title}
</h3>
{children}
</div>
);
export const ScanDetail = ({
scanDetails,
}: {
scanDetails: ScanProps & {
taskDetails?: TaskDetails;
providerDetails?: ProviderProps;
};
}
export const ScanDetail = ({ scanDetails }: ScanDetailsProps) => {
const scanOnDemand = scanDetails.attributes;
}) => {
const scan = scanDetails.attributes;
const taskDetails = scanDetails.taskDetails;
const providerDetails = scanDetails.providerDetails?.attributes;
return (
<div className="flex flex-col gap-6 rounded-lg">
{/* Header */}
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-800 dark:text-prowler-theme-pale/90">
Scan Details
</h2>
<StatusBadge
size="lg"
status={scanOnDemand.state}
loadingProgress={scanOnDemand.progress}
status={scan.state}
loadingProgress={scan.progress}
/>
</div>
<Divider className="border-gray-300 dark:border-gray-600" />
{/* Details Section */}
<div className="flex flex-col gap-4 rounded-lg p-4 shadow dark:bg-prowler-blue-400">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="flex flex-col gap-4">
<DetailItem label="Scan Name" value={scanOnDemand.name} />
<DetailItem
label="ID"
value={<SnippetId label="Type" entityId={scanDetails.id} />}
/>
<DetailItem label="Trigger" value={scanOnDemand.trigger} />
<DetailItem
label="Resource Count"
value={scanOnDemand.unique_resource_count.toString()}
/>
<DetailItem label="Progress" value={`${scanOnDemand.progress}%`} />
<DetailItem
label="Duration"
value={`${scanOnDemand.duration} seconds`}
/>
</div>
<div className="flex flex-col gap-4">
<DateItem
label="Started At"
value={
scanOnDemand.started_at ? (
<DateWithTime dateTime={scanOnDemand.started_at.toString()} />
) : (
"Not Started"
)
}
/>
<DateItem
label="Completed At"
value={
scanOnDemand.completed_at ? (
<DateWithTime
dateTime={scanOnDemand.completed_at.toString()}
/>
) : (
"Not Completed"
)
}
/>
<DateItem
label="Scheduled At"
value={
scanOnDemand.scheduled_at ? (
<DateWithTime
dateTime={scanOnDemand.scheduled_at.toString()}
/>
) : (
"Not Scheduled"
)
}
/>
<DetailItem
label="Provider ID"
value={
<SnippetId
label="Provider ID"
entityId={scanDetails.relationships.provider.data.id}
/>
}
/>
<DetailItem
label="Task ID"
value={
scanDetails.relationships.task?.data?.id ? (
<SnippetId
label="Task ID"
entityId={scanDetails.relationships.task.data.id}
/>
) : (
"N/A"
)
}
/>
</div>
{/* Scan Details */}
<Section title="Scan Details">
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<InfoField label="Scan Name">{renderValue(scan.name)}</InfoField>
<InfoField label="Resources Scanned">
{scan.unique_resource_count}
</InfoField>
<InfoField label="Progress">{scan.progress}%</InfoField>
</div>
</div>
{/* Scan Arguments Section */}
{/* <Card className="rounded-lg p-4 shadow dark:bg-prowler-blue-400">
<CardHeader className="pb-4">
<h3 className="text-lg font-bold text-gray-800 dark:text-prowler-theme-pale/90">
Scan Arguments
</h3>
</CardHeader>
<Divider className="border-gray-300 dark:border-gray-600" />
<CardBody className="pt-4">
<div className="flex flex-col gap-2">
<span className="text-sm font-semibold text-gray-600 dark:text-gray-300">
Checks
</span>
<span className="text-gray-800 dark:text-prowler-theme-pale/90">
{(scanOnDemand.scanner_args as any)?.checks_to_execute?.join(
", ",
) || "N/A"}
</span>
</div>
</CardBody>
</Card> */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<InfoField label="Trigger">{renderValue(scan.trigger)}</InfoField>
<InfoField label="State">{renderValue(scan.state)}</InfoField>
<InfoField label="Duration">
{formatDuration(scan.duration)}
</InfoField>
</div>
{/* Task Details Section */}
{taskDetails && (
<Card className="rounded-lg p-4 shadow dark:bg-prowler-blue-400">
<CardHeader className="pb-4">
<h3 className="text-lg font-bold text-gray-800 dark:text-prowler-theme-pale/90">
State Details
</h3>
</CardHeader>
<Divider className="border-gray-300 dark:border-gray-600" />
<CardBody className="pt-4">
<div className="flex flex-col gap-4">
<DetailItem label="State" value={taskDetails.attributes.state} />
<DetailItem
label="Completed At"
value={taskDetails.attributes.completed_at || "N/A"}
/>
{taskDetails.attributes.result && (
<>
<DetailItem
label="Error Type"
value={taskDetails.attributes.result.exc_type || "N/A"}
/>
{taskDetails.attributes.result.exc_message && (
<DetailItem
label="Error Message"
value={taskDetails.attributes.result.exc_message.join(
", ",
)}
/>
)}
</>
)}
<DetailItem
label="Checks to Execute"
value={
taskDetails.attributes.task_args.checks_to_execute?.join(
", ",
) || "N/A"
}
/>
{scan.state === "failed" && taskDetails?.attributes.result && (
<>
{taskDetails.attributes.result.exc_message && (
<InfoField label="Error Message" variant="simple">
<Snippet hideSymbol>
<span className="whitespace-pre-line text-xs">
{taskDetails.attributes.result.exc_message.join("\n")}
</span>
</Snippet>
</InfoField>
)}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<InfoField label="Error Type">
{renderValue(taskDetails.attributes.result.exc_type)}
</InfoField>
<InfoField label="Scan ID" variant="simple">
<Snippet hideSymbol>
{renderValue(taskDetails?.attributes.task_args.scan_id)}
</Snippet>
</InfoField>
</div>
</CardBody>
</Card>
)}
</>
)}
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<InfoField label="Started At">
<DateWithTime inline dateTime={scan.started_at || "-"} />
</InfoField>
<InfoField label="Completed At">
<DateWithTime inline dateTime={scan.completed_at || "-"} />
</InfoField>
{scan.next_scan_at && (
<InfoField label="Scheduled At">
<DateWithTime inline dateTime={scan.next_scan_at} />
</InfoField>
)}
</div>
</Section>
{/* Provider Details */}
<Section title="Provider Details">
{providerDetails ? (
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<EntityInfoShort
cloudProvider={
providerDetails.provider as
| "aws"
| "azure"
| "gcp"
| "kubernetes"
}
entityAlias={providerDetails.alias}
entityId={providerDetails.uid}
/>
<InfoField label="Connection Status" variant="simple">
{providerDetails.connection.connected ? (
<ConnectionTrue className="text-system-success" size={24} />
) : (
<ConnectionFalse className="text-danger" size={24} />
)}
</InfoField>
<InfoField label="Last Checked">
<DateWithTime
inline
dateTime={providerDetails.connection.last_checked_at}
/>
</InfoField>
</div>
) : (
<span className="text-sm text-gray-500">
No provider details available
</span>
)}
</Section>
</div>
);
};
const DateItem = ({
label,
value,
}: {
label: string;
value: React.ReactNode;
}) => (
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-gray-600 dark:text-gray-300">
{label}:
</p>
<p className="text-gray-800 dark:text-prowler-theme-pale/90">{value}</p>
</div>
);
const DetailItem = ({
label,
value,
}: {
label: string;
value: React.ReactNode;
}) => (
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-gray-600 dark:text-gray-300">
{label}:
</p>
<p className="text-gray-800 dark:text-prowler-theme-pale/90">{value}</p>
</div>
);
@@ -3,9 +3,11 @@
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { getProvider } from "@/actions/providers";
import { getScan } from "@/actions/scans";
import { getTask } from "@/actions/task";
import { ScanDetail, SkeletonTableScans } from "@/components/scans/table";
import { ScanDetail } from "@/components/scans/table";
import { Alert } from "@/components/ui/alert/Alert";
import { checkTaskStatus } from "@/lib";
import { ScanProps } from "@/types";
@@ -35,6 +37,15 @@ export const DataTableRowDetails = ({ entityId }: { entityId: string }) => {
const result = await getScan(entityId);
const taskId = result.data.relationships.task?.data?.id;
const providerId = result.data.relationships.provider?.data?.id;
let providerDetails = null;
if (providerId) {
const formData = new FormData();
formData.append("id", providerId);
const providerResult = await getProvider(formData);
providerDetails = providerResult.data;
}
if (taskId) {
const taskResult = await checkTaskStatus(taskId);
@@ -44,10 +55,14 @@ export const DataTableRowDetails = ({ entityId }: { entityId: string }) => {
setScanDetails({
...result.data,
taskDetails: task.data,
providerDetails: providerDetails,
});
}
} else {
setScanDetails(result.data);
setScanDetails({
...result.data,
providerDetails: providerDetails,
});
}
} catch (error) {
// eslint-disable-next-line no-console
@@ -61,7 +76,12 @@ export const DataTableRowDetails = ({ entityId }: { entityId: string }) => {
}, [entityId]);
if (isLoading) {
return <SkeletonTableScans />;
return (
<Alert className="text-center text-small font-bold text-gray-500">
Scan details are loading and will be available once the scan is
completed.
</Alert>
);
}
if (!scanDetails) {
@@ -32,7 +32,9 @@ export const CustomAlertModal: React.FC<CustomAlertModalProps> = ({
<>
<ModalHeader className="flex flex-col py-0">{title}</ModalHeader>
<ModalBody>
{description}
<p className="text-small text-gray-600 dark:text-gray-300">
{description}
</p>
{children}
</ModalBody>
</>
+6 -2
View File
@@ -1,13 +1,17 @@
import clsx from "clsx";
interface InfoFieldProps {
label: string;
children: React.ReactNode;
variant?: "default" | "simple";
className?: string;
}
export const InfoField = ({
label,
children,
variant = "default",
className,
}: InfoFieldProps) => {
if (variant === "simple") {
return (
@@ -23,11 +27,11 @@ export const InfoField = ({
}
return (
<div className="flex flex-col gap-1">
<div className={clsx("flex flex-col gap-1", className)}>
<span className="text-xs font-bold text-gray-500 dark:text-prowler-theme-pale/70">
{label}
</span>
<div className="rounded-lg bg-gray-50 px-3 py-2 text-small text-gray-900 dark:bg-prowler-blue-400 dark:text-prowler-theme-pale">
<div className="rounded-lg bg-gray-50 px-3 py-2 text-small text-gray-900 dark:bg-slate-800 dark:text-prowler-theme-pale">
{children}
</div>
</div>