mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 21:11:53 +00:00
feat(v5): tweaks UI for v5 release - 2 (#5979)
This commit is contained in:
@@ -45,6 +45,7 @@ export const getFindings = async ({
|
||||
revalidatePath("/findings");
|
||||
return parsedData;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching findings:", error);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,12 @@ import {
|
||||
import { Header } from "@/components/ui";
|
||||
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
|
||||
import { createDict } from "@/lib";
|
||||
import { FindingProps, SearchParamsProps } from "@/types/components";
|
||||
import {
|
||||
FindingProps,
|
||||
ProviderProps,
|
||||
ScanProps,
|
||||
SearchParamsProps,
|
||||
} from "@/types/components";
|
||||
|
||||
export default async function Findings({
|
||||
searchParams,
|
||||
@@ -56,24 +61,28 @@ export default async function Findings({
|
||||
const scansData = await getScans({});
|
||||
|
||||
// Extract provider UIDs
|
||||
const providerUIDs = providersData?.data
|
||||
?.map((provider: any) => provider.attributes.uid)
|
||||
.filter(Boolean);
|
||||
const providerUIDs = Array.from(
|
||||
new Set(
|
||||
providersData?.data
|
||||
?.map((provider: ProviderProps) => provider.attributes.uid)
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
|
||||
// Extract scan UUIDs with "completed" state and more than one resource
|
||||
const completedScans = scansData?.data
|
||||
?.filter(
|
||||
(scan: any) =>
|
||||
scan.attributes.state === "completed" &&
|
||||
scan.attributes.unique_resource_count > 1 &&
|
||||
scan.attributes.name, // Ensure it has a name
|
||||
scan.attributes.unique_resource_count > 1,
|
||||
)
|
||||
.map((scan: any) => ({
|
||||
.map((scan: ScanProps) => ({
|
||||
id: scan.id,
|
||||
name: scan.attributes.name,
|
||||
}));
|
||||
|
||||
const completedScanIds = completedScans?.map((scan: any) => scan.id) || [];
|
||||
const completedScanIds =
|
||||
completedScans?.map((scan: ScanProps) => scan.id) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -97,12 +106,12 @@ export default async function Findings({
|
||||
},
|
||||
{
|
||||
key: "provider_uid__in",
|
||||
labelCheckboxGroup: "Account",
|
||||
labelCheckboxGroup: "Provider UID",
|
||||
values: providerUIDs,
|
||||
},
|
||||
{
|
||||
key: "scan__in",
|
||||
labelCheckboxGroup: "Scans",
|
||||
labelCheckboxGroup: "Scan ID",
|
||||
values: completedScanIds,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -56,6 +56,7 @@ export default function Home({
|
||||
</div>
|
||||
|
||||
<div className="col-span-12">
|
||||
<Spacer y={16} />
|
||||
<Suspense
|
||||
key={searchParamsKey}
|
||||
fallback={<SkeletonTableNewFindings />}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getProviders } from "@/actions/providers";
|
||||
import { getProvider, getProviders } from "@/actions/providers";
|
||||
import { getScans } from "@/actions/scans";
|
||||
import { FilterControls, filterScans } from "@/components/filters";
|
||||
import { ButtonRefreshData } from "@/components/scans";
|
||||
import { filterScans } from "@/components/filters";
|
||||
import {
|
||||
ButtonRefreshData,
|
||||
NoProvidersAdded,
|
||||
NoProvidersConnected,
|
||||
} from "@/components/scans";
|
||||
import { LaunchScanWorkflow } from "@/components/scans/launch-workflow";
|
||||
import { SkeletonTableScans } from "@/components/scans/table";
|
||||
import { ColumnGetScans } from "@/components/scans/table/scans";
|
||||
@@ -21,48 +25,72 @@ export default async function Scans({
|
||||
delete filteredParams.scanId;
|
||||
const searchParamsKey = JSON.stringify(filteredParams);
|
||||
|
||||
const providersData = await getProviders({});
|
||||
const providersData = await getProviders({
|
||||
filters: {
|
||||
"filter[connected]": true,
|
||||
},
|
||||
});
|
||||
|
||||
const providerInfo = providersData?.data?.length
|
||||
? providersData.data.map((provider: ProviderProps) => ({
|
||||
providerId: provider.id,
|
||||
alias: provider.attributes.alias,
|
||||
providerType: provider.attributes.provider,
|
||||
uid: provider.attributes.uid,
|
||||
connected: provider.attributes.connection.connected,
|
||||
}))
|
||||
: [];
|
||||
const providerInfo = providersData.data.map((provider: ProviderProps) => ({
|
||||
providerId: provider.id,
|
||||
alias: provider.attributes.alias,
|
||||
providerType: provider.attributes.provider,
|
||||
uid: provider.attributes.uid,
|
||||
connected: provider.attributes.connection.connected,
|
||||
}));
|
||||
|
||||
// const executingScans = await getExecutingScans();
|
||||
const providersCountConnected = await getProviders({});
|
||||
const thereIsNoProviders =
|
||||
!providersCountConnected?.data || providersCountConnected.data.length === 0;
|
||||
|
||||
const thereIsNoProvidersConnected = providersCountConnected?.data?.every(
|
||||
(provider: ProviderProps) => !provider.attributes.connection.connected,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="Scans" icon="lucide:scan-search" />
|
||||
|
||||
<Spacer y={4} />
|
||||
<FilterControls search providers />
|
||||
<Spacer y={8} />
|
||||
<LaunchScanWorkflow providers={providerInfo} />
|
||||
<Spacer y={8} />
|
||||
<div className="flex flex-row justify-between">
|
||||
<DataTableFilterCustom filters={filterScans || []} />
|
||||
<ButtonRefreshData
|
||||
onPress={async () => {
|
||||
"use server";
|
||||
await getScans({});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{thereIsNoProviders && (
|
||||
<>
|
||||
<Spacer y={4} />
|
||||
<NoProvidersAdded />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Spacer y={8} />
|
||||
|
||||
<div className="grid grid-cols-12 items-start gap-4">
|
||||
<div className="col-span-12">
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonTableScans />}>
|
||||
<SSRDataTableScans searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
{!thereIsNoProviders && (
|
||||
<>
|
||||
{thereIsNoProvidersConnected ? (
|
||||
<>
|
||||
<Spacer y={8} />
|
||||
<NoProvidersConnected />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LaunchScanWorkflow providers={providerInfo} />
|
||||
<Spacer y={8} />
|
||||
</>
|
||||
)}
|
||||
<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">
|
||||
<DataTableFilterCustom filters={filterScans || []} />
|
||||
<ButtonRefreshData
|
||||
onPress={async () => {
|
||||
"use server";
|
||||
await getScans({});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Spacer y={8} />
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonTableScans />}>
|
||||
<SSRDataTableScans searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -85,22 +113,40 @@ const SSRDataTableScans = async ({
|
||||
// Extract query from filters
|
||||
const query = (filters["filter[search]"] as string) || "";
|
||||
|
||||
// Fetch scans data
|
||||
const scansData = await getScans({ query, page, sort, filters });
|
||||
|
||||
// Handle expanded scans data
|
||||
const expandedScansData = await Promise.all(
|
||||
scansData?.data?.map(async (scan: any) => {
|
||||
const providerId = scan.relationships?.provider?.data?.id;
|
||||
|
||||
if (!providerId) {
|
||||
return { ...scan, providerInfo: null };
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("id", providerId);
|
||||
|
||||
const providerData = await getProvider(formData);
|
||||
|
||||
if (providerData?.data) {
|
||||
const { provider, uid, alias } = providerData.data.attributes;
|
||||
return {
|
||||
...scan,
|
||||
providerInfo: { provider, uid, alias },
|
||||
};
|
||||
}
|
||||
|
||||
return { ...scan, providerInfo: null };
|
||||
}) || [],
|
||||
);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={ColumnGetScans}
|
||||
data={scansData?.data || []}
|
||||
data={expandedScansData || []}
|
||||
metadata={scansData?.meta}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// const getExecutingScans = async () => {
|
||||
// const scansData = await getScans({});
|
||||
|
||||
// return scansData?.data?.some(
|
||||
// (scan: ScanProps) =>
|
||||
// scan.attributes.state === "executing" && scan.attributes.progress < 100,
|
||||
// );
|
||||
// };
|
||||
|
||||
@@ -8,11 +8,11 @@ export const filterProviders = [
|
||||
];
|
||||
|
||||
export const filterScans = [
|
||||
// {
|
||||
// key: "provider_type__in",
|
||||
// labelCheckboxGroup: "Provider",
|
||||
// values: ["aws", "azure", "gcp", "kubernetes"],
|
||||
// },
|
||||
{
|
||||
key: "provider_type__in",
|
||||
labelCheckboxGroup: "Cloud Provider",
|
||||
values: ["aws", "azure", "gcp", "kubernetes"],
|
||||
},
|
||||
{
|
||||
key: "state",
|
||||
labelCheckboxGroup: "State",
|
||||
@@ -51,7 +51,7 @@ export const filterFindings = [
|
||||
},
|
||||
{
|
||||
key: "provider_type__in",
|
||||
labelCheckboxGroup: "Provider",
|
||||
labelCheckboxGroup: "Cloud Provider",
|
||||
values: ["aws", "azure", "gcp", "kubernetes"],
|
||||
},
|
||||
// Add more filter categories as needed
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { DataTableRowDetails } from "@/components/findings/table";
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import { DateWithTime } from "@/components/ui/entities";
|
||||
import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
|
||||
import { TriggerSheet } from "@/components/ui/sheet";
|
||||
import {
|
||||
DataTableColumnHeader,
|
||||
@@ -181,16 +181,20 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "account",
|
||||
header: "Account",
|
||||
accessorKey: "cloudProvider",
|
||||
header: "Cloud provider",
|
||||
cell: ({ row }) => {
|
||||
const account = getProviderData(row, "uid");
|
||||
const provider = getProviderData(row, "provider");
|
||||
const alias = getProviderData(row, "alias");
|
||||
const uid = getProviderData(row, "uid");
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="max-w-96 truncate text-small">
|
||||
{typeof account === "string" ? account : "Invalid account"}
|
||||
</p>
|
||||
<EntityInfoShort
|
||||
cloudProvider={provider as "aws" | "azure" | "gcp" | "kubernetes"}
|
||||
entityAlias={alias as string}
|
||||
entityId={uid as string}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ export const FindingsBySeverityChart = ({
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card className="dark:bg-prowler-blue-400">
|
||||
<Card className="h-full dark:bg-prowler-blue-400">
|
||||
<CardBody>
|
||||
<div className="my-auto">
|
||||
<ChartContainer config={chartConfig}>
|
||||
|
||||
@@ -91,7 +91,7 @@ export const FindingsByStatusChart: React.FC<FindingsByStatusChartProps> = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<Card className="dark:bg-prowler-blue-400">
|
||||
<Card className="h-full dark:bg-prowler-blue-400">
|
||||
<CardBody>
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<ChartContainer
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { DataTableRowDetails } from "@/components/findings/table";
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import { DateWithTime } from "@/components/ui/entities";
|
||||
import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
|
||||
import { TriggerSheet } from "@/components/ui/sheet";
|
||||
import { SeverityBadge, StatusFindingBadge } from "@/components/ui/table";
|
||||
import { FindingProps } from "@/types";
|
||||
@@ -132,16 +132,20 @@ export const ColumnNewFindingsToDate: ColumnDef<FindingProps>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "account",
|
||||
header: "Account",
|
||||
accessorKey: "cloudProvider",
|
||||
header: "Cloud provider",
|
||||
cell: ({ row }) => {
|
||||
const account = getProviderData(row, "uid");
|
||||
const provider = getProviderData(row, "provider");
|
||||
const alias = getProviderData(row, "alias");
|
||||
const uid = getProviderData(row, "uid");
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="max-w-96 truncate text-small">
|
||||
{typeof account === "string" ? account : "Invalid account"}
|
||||
</p>
|
||||
<EntityInfoShort
|
||||
cloudProvider={provider as "aws" | "azure" | "gcp" | "kubernetes"}
|
||||
entityAlias={alias as string}
|
||||
entityId={uid as string}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -44,9 +44,9 @@ export const ProvidersOverview = ({
|
||||
|
||||
if (!providersOverview || !Array.isArray(providersOverview.data)) {
|
||||
return (
|
||||
<Card className="dark:bg-prowler-blue-400">
|
||||
<Card className="h-full dark:bg-prowler-blue-400">
|
||||
<CardBody>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<div className="my-auto grid grid-cols-1 gap-3">
|
||||
<div className="grid grid-cols-4 border-b pb-2 text-xs font-semibold">
|
||||
<span className="text-center">Provider</span>
|
||||
<span className="flex flex-col items-center text-center">
|
||||
@@ -92,9 +92,9 @@ export const ProvidersOverview = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="dark:bg-prowler-blue-400">
|
||||
<Card className="h-full dark:bg-prowler-blue-400">
|
||||
<CardBody>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<div className="my-auto grid grid-cols-1 gap-3">
|
||||
<div className="grid grid-cols-4 border-b pb-2 text-xs font-semibold">
|
||||
<span className="text-center">Provider</span>
|
||||
<span className="flex flex-col items-center text-center">
|
||||
@@ -175,7 +175,7 @@ export const ProvidersOverview = ({
|
||||
</div>
|
||||
<div className="mt-4 flex w-full items-center justify-end">
|
||||
<CustomButton
|
||||
asLink="/providers/connect-account"
|
||||
asLink="/providers"
|
||||
ariaLabel="Go to Providers page"
|
||||
variant="solid"
|
||||
color="action"
|
||||
|
||||
@@ -42,8 +42,7 @@ export function DataTableRowActions<ProviderProps>({
|
||||
<CustomAlertModal
|
||||
isOpen={isEditOpen}
|
||||
onOpenChange={setIsEditOpen}
|
||||
title="Edit Provider"
|
||||
description={"Edit the provider details"}
|
||||
title="Edit Provider Alias"
|
||||
>
|
||||
<EditForm
|
||||
providerId={providerId}
|
||||
@@ -93,7 +92,7 @@ export function DataTableRowActions<ProviderProps>({
|
||||
startContent={<EditDocumentBulkIcon className={iconClasses} />}
|
||||
onClick={() => setIsEditOpen(true)}
|
||||
>
|
||||
Edit Provider
|
||||
Edit Provider Alias
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownSection title="Danger zone">
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from "./button-refresh-data";
|
||||
export * from "./link-to-findings-from-scan";
|
||||
export * from "./no-providers-added";
|
||||
export * from "./no-providers-connected";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { scanOnDemand } from "@/actions/scans";
|
||||
import { RocketIcon, ScheduleIcon } from "@/components/icons";
|
||||
import { RocketIcon } from "@/components/icons";
|
||||
import { CustomButton, CustomInput } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { toast } from "@/components/ui/toast";
|
||||
@@ -79,26 +79,26 @@ export const LaunchScanWorkflow = ({
|
||||
onSubmit={form.handleSubmit(onSubmitClient)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="w-full">
|
||||
<p className="pb-1 text-sm font-medium text-default-700">
|
||||
Launch Scan
|
||||
</p>
|
||||
<SelectScanProvider
|
||||
providers={providers}
|
||||
control={form.control}
|
||||
name="providerId"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{form.watch("providerId") && (
|
||||
<div className="grid grid-cols-12 gap-6">
|
||||
<div className="col-span-4">
|
||||
<p className="pb-1 text-sm font-medium text-default-700">
|
||||
Launch Scan
|
||||
</p>
|
||||
<SelectScanProvider
|
||||
providers={providers}
|
||||
control={form.control}
|
||||
name="providerId"
|
||||
/>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{form.watch("providerId") && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="col-span-2"
|
||||
>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
@@ -112,10 +112,41 @@ export const LaunchScanWorkflow = ({
|
||||
isInvalid={!!form.formState.errors.scanName}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="col-span-4 flex items-end gap-4"
|
||||
>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel="Start scan now"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="lg"
|
||||
isLoading={isLoading}
|
||||
startContent={!isLoading && <RocketIcon size={24} />}
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>Start now</span>}
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
onPress={() => form.reset()}
|
||||
className="w-fit border-gray-200 bg-transparent"
|
||||
ariaLabel="Clear form"
|
||||
variant="bordered"
|
||||
size="md"
|
||||
radius="lg"
|
||||
>
|
||||
Cancel
|
||||
</CustomButton>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/*
|
||||
<div className="flex flex-col justify-start">
|
||||
<AnimatePresence>
|
||||
{form.watch("providerId") && (
|
||||
@@ -139,54 +170,7 @@ export const LaunchScanWorkflow = ({
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{form.watch("providerId") && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="col-span-2 flex justify-start gap-4"
|
||||
>
|
||||
<CustomButton
|
||||
onPress={() => form.reset()}
|
||||
className="w-fit border-gray-200 bg-transparent"
|
||||
ariaLabel="Clear form"
|
||||
variant="bordered"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
>
|
||||
Cancel
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel="Schedule scan"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="lg"
|
||||
isLoading={isLoading}
|
||||
startContent={!isLoading && <ScheduleIcon size={24} />}
|
||||
isDisabled={true}
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>Schedule</span>}
|
||||
</CustomButton>
|
||||
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel="Start scan now"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="lg"
|
||||
isLoading={isLoading}
|
||||
startContent={!isLoading && <RocketIcon size={24} />}
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>Start now</span>}
|
||||
</CustomButton>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div> */}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -55,8 +55,8 @@ export const SelectScanProvider = <
|
||||
<>
|
||||
<FormControl>
|
||||
<Select
|
||||
aria-label="Select a Provider"
|
||||
placeholder="Choose a provider"
|
||||
aria-label="Select a scan job"
|
||||
placeholder="Choose a scan job"
|
||||
labelPlacement="outside"
|
||||
size="md"
|
||||
selectedKeys={field.value ? new Set([field.value]) : new Set()}
|
||||
@@ -74,7 +74,7 @@ export const SelectScanProvider = <
|
||||
{selectedItem.alias}
|
||||
</div>
|
||||
) : (
|
||||
"Choose a provider"
|
||||
"Choose a scan job"
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
import { Card, CardBody } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
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">
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardBody } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
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">
|
||||
<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.
|
||||
</p>
|
||||
<CustomButton
|
||||
asLink="/providers"
|
||||
ariaLabel="Go to Cloud accounts page"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="lg"
|
||||
>
|
||||
Review Cloud Accounts
|
||||
</CustomButton>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { ColumnDef } from "@tanstack/react-table";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import { DateWithTime } from "@/components/ui/entities";
|
||||
import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
|
||||
import { TriggerSheet } from "@/components/ui/sheet";
|
||||
import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table";
|
||||
import { ScanProps } from "@/types";
|
||||
@@ -19,14 +19,49 @@ const getScanData = (row: { original: ScanProps }) => {
|
||||
|
||||
export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
{
|
||||
accessorKey: "accountName",
|
||||
header: () => <p className="pr-8">Account name</p>,
|
||||
id: "moreInfo",
|
||||
header: "Details",
|
||||
cell: ({ row }) => {
|
||||
console.log(row.original);
|
||||
const searchParams = useSearchParams();
|
||||
const scanId = searchParams.get("scanId");
|
||||
const isOpen = scanId === row.original.id;
|
||||
|
||||
return <span className="font-medium">providerinfo</span>;
|
||||
return (
|
||||
<div className="flex w-9 items-center justify-center">
|
||||
<TriggerSheet
|
||||
triggerComponent={<InfoIcon className="text-primary" size={16} />}
|
||||
title="Scan Details"
|
||||
description="View the scan details"
|
||||
defaultOpen={isOpen}
|
||||
>
|
||||
<DataTableRowDetails entityId={row.original.id} />
|
||||
</TriggerSheet>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "cloudProvider",
|
||||
header: () => <p className="pr-8">Cloud provider</p>,
|
||||
cell: ({ row }) => {
|
||||
const providerInfo = row.original.providerInfo;
|
||||
|
||||
if (!providerInfo) {
|
||||
return <span className="font-medium">No provider info</span>;
|
||||
}
|
||||
|
||||
const { provider, uid, alias } = providerInfo;
|
||||
|
||||
return (
|
||||
<EntityInfoShort
|
||||
cloudProvider={provider as "aws" | "azure" | "gcp" | "kubernetes"}
|
||||
entityAlias={alias}
|
||||
entityId={uid}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "started_at",
|
||||
header: () => <p className="pr-8">Started at</p>,
|
||||
@@ -88,22 +123,21 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "scheduled_at",
|
||||
accessorKey: "next_scan_at",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={"Scheduled at"}
|
||||
param="scheduled_at"
|
||||
title={"Next execution"}
|
||||
param="next_scan_at"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { scheduled_at },
|
||||
attributes: { next_scan_at },
|
||||
} = getScanData(row);
|
||||
return <DateWithTime dateTime={scheduled_at} />;
|
||||
return <DateWithTime dateTime={next_scan_at} />;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "completed_at",
|
||||
header: ({ column }) => (
|
||||
@@ -129,16 +163,9 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
const {
|
||||
attributes: { trigger },
|
||||
} = getScanData(row);
|
||||
return <p>{trigger}</p>;
|
||||
return <p className="text-tiny font-medium uppercase">{trigger}</p>;
|
||||
},
|
||||
},
|
||||
// {
|
||||
// accessorKey: "id",
|
||||
// header: () => <span>ID</span>,
|
||||
// cell: ({ row }) => {
|
||||
// return <SnippetId entityId={row.original.id} />;
|
||||
// },
|
||||
// },
|
||||
{
|
||||
accessorKey: "scanName",
|
||||
header: ({ column }) => (
|
||||
@@ -153,30 +180,9 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
return <span className="font-medium">-</span>;
|
||||
}
|
||||
|
||||
return <span className="font-medium">{name}</span>;
|
||||
return <span className="text-xs font-medium">{name}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
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>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
|
||||
@@ -18,13 +18,13 @@ export const DateWithTime: React.FC<DateWithTimeProps> = ({
|
||||
const formattedTime = format(date, "p 'UTC'");
|
||||
|
||||
return (
|
||||
<div className="max-w-fit">
|
||||
<div className="mw-fit">
|
||||
<div
|
||||
className={`flex ${inline ? "flex-row items-center gap-2" : "flex-col"}`}
|
||||
>
|
||||
<span className="text-sm font-semibold">{formattedDate}</span>
|
||||
<span className="text-xs font-semibold">{formattedDate}</span>
|
||||
{showTime && (
|
||||
<span className="text-xs text-gray-500">{formattedTime}</span>
|
||||
<span className="text-tiny text-gray-500">{formattedTime}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from "react";
|
||||
|
||||
import { getProviderLogo } from "./get-provider-logo";
|
||||
import { SnippetId } from "./snippet-id";
|
||||
import { SnippetLabel } from "./snippet-label";
|
||||
|
||||
interface EntityInfoProps {
|
||||
cloudProvider: "aws" | "azure" | "gcp" | "kubernetes";
|
||||
@@ -16,11 +15,13 @@ export const EntityInfoShort: React.FC<EntityInfoProps> = ({
|
||||
entityId,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between space-x-4">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div className="flex w-full items-center justify-between space-x-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="flex-shrink-0">{getProviderLogo(cloudProvider)}</div>
|
||||
<div className="flex flex-col">
|
||||
<SnippetLabel label={entityAlias ?? ""} />
|
||||
<div className="flex flex-col space-y-1">
|
||||
{entityAlias && (
|
||||
<span className="text-tiny text-default-500">{entityAlias}</span>
|
||||
)}
|
||||
<SnippetId entityId={entityId ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ interface SnippetIdProps {
|
||||
export const SnippetId: React.FC<SnippetIdProps> = ({ entityId, ...props }) => {
|
||||
return (
|
||||
<Snippet
|
||||
className="flex h-6 items-center py-0"
|
||||
className="flex h-4 items-center py-0"
|
||||
color="default"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
@@ -22,7 +22,7 @@ export const SnippetId: React.FC<SnippetIdProps> = ({ entityId, ...props }) => {
|
||||
>
|
||||
<p className="flex items-center space-x-2">
|
||||
<IdIcon size={18} />
|
||||
<span className="no-scrollbar text-md w-28 overflow-hidden overflow-x-scroll text-ellipsis whitespace-nowrap text-sm">
|
||||
<span className="no-scrollbar w-14 overflow-hidden overflow-x-scroll text-ellipsis whitespace-nowrap text-xs">
|
||||
{entityId}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@@ -131,7 +131,7 @@ export const SidebarWrap = () => {
|
||||
ariaLabel="Documentation"
|
||||
variant="flat"
|
||||
className={clsx(
|
||||
"justify-start truncate bg-transparent text-default-500 data-[hover=true]:text-foreground",
|
||||
"justify-start truncate bg-transparent text-default-500 data-[hover=true]:text-foreground dark:bg-transparent",
|
||||
{
|
||||
"justify-center": isCompact,
|
||||
},
|
||||
@@ -168,7 +168,7 @@ export const SidebarWrap = () => {
|
||||
ariaLabel="Support"
|
||||
variant="flat"
|
||||
className={clsx(
|
||||
"justify-start truncate bg-transparent text-default-500 data-[hover=true]:text-foreground",
|
||||
"justify-start truncate bg-transparent text-default-500 data-[hover=true]:text-foreground dark:bg-transparent",
|
||||
{
|
||||
"justify-center": isCompact,
|
||||
},
|
||||
|
||||
@@ -80,10 +80,20 @@ export const DataTableColumnHeader = <TData, TValue>({
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="h-10 w-full justify-between whitespace-nowrap bg-transparent px-0 text-left align-middle text-tiny font-semibold text-foreground-500 outline-none dark:text-slate-400"
|
||||
className="h-10 w-fit max-w-[110px] whitespace-nowrap bg-transparent px-0 text-left align-middle text-tiny font-semibold text-foreground-500 outline-none dark:text-slate-400"
|
||||
onClick={getToggleSortingHandler}
|
||||
>
|
||||
<span>{title}</span>
|
||||
<span
|
||||
className="block whitespace-normal break-normal"
|
||||
style={{
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: 2,
|
||||
width: "90px",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
{renderSortIcon()}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ export const StatusBadge = ({
|
||||
|
||||
return (
|
||||
<Chip
|
||||
className="gap-1 border-none px-2 py-4 capitalize text-default-600"
|
||||
className="gap-1 border-none px-2 py-2 capitalize text-default-600"
|
||||
size={size}
|
||||
variant="flat"
|
||||
color={color}
|
||||
|
||||
@@ -75,7 +75,7 @@ const TableHead = React.forwardRef<
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 whitespace-nowrap bg-default-100 px-4 text-left align-middle text-tiny font-semibold text-foreground-500 outline-none first:rounded-l-lg last:rounded-r-lg data-[focus-visible=true]:z-10 data-[hover=true]:text-foreground-400 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-offset-2 data-[focus-visible=true]:outline-focus dark:bg-prowler-blue-800 dark:text-slate-400 rtl:text-right rtl:first:rounded-l-[unset] rtl:first:rounded-r-lg rtl:last:rounded-l-lg rtl:last:rounded-r-[unset] [&:has([role=checkbox])]:pr-0",
|
||||
"h-10 whitespace-nowrap bg-default-100 px-2 text-left align-middle text-tiny font-semibold text-foreground-500 outline-none first:rounded-l-lg last:rounded-r-lg data-[focus-visible=true]:z-10 data-[hover=true]:text-foreground-400 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-offset-2 data-[focus-visible=true]:outline-focus dark:bg-prowler-blue-800 dark:text-slate-400 rtl:text-right rtl:first:rounded-l-[unset] rtl:first:rounded-r-lg rtl:last:rounded-l-lg rtl:last:rounded-r-[unset] [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -90,7 +90,7 @@ const TableCell = React.forwardRef<
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-4 py-2.5 align-middle [&:has([role=checkbox])]:pr-0",
|
||||
"px-2 py-2.5 align-middle [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -327,6 +327,7 @@ export interface ScanProps {
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
scheduled_at: string;
|
||||
next_scan_at: string;
|
||||
};
|
||||
relationships: {
|
||||
provider: {
|
||||
@@ -342,6 +343,11 @@ export interface ScanProps {
|
||||
};
|
||||
};
|
||||
};
|
||||
providerInfo?: {
|
||||
provider: "aws" | "azure" | "gcp" | "kubernetes";
|
||||
uid: string;
|
||||
alias: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FindingProps {
|
||||
|
||||
Reference in New Issue
Block a user