mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 12:31:54 +00:00
chore(scans): improve scan details (#6665)
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user