mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
Merge pull request #97 from prowler-cloud/PRWLR-5450-Providers-Page-Add-Provider-Remove-Start-now
feat(providers) providers page add provider remove start now
This commit is contained in:
@@ -51,29 +51,30 @@ export default async function Findings({
|
||||
const resourceDict = createDict("resources", findingsData);
|
||||
|
||||
// Get unique regions and services
|
||||
const allRegionsAndServices = findingsData?.data
|
||||
?.flatMap((finding: FindingProps) => {
|
||||
const resource =
|
||||
resourceDict[finding.relationships?.resources?.data?.[0]?.id];
|
||||
return {
|
||||
region: resource?.attributes?.region,
|
||||
service: resource?.attributes?.service,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
const allRegionsAndServices =
|
||||
findingsData?.data
|
||||
?.flatMap((finding: FindingProps) => {
|
||||
const resource =
|
||||
resourceDict[finding.relationships?.resources?.data?.[0]?.id];
|
||||
return {
|
||||
region: resource?.attributes?.region,
|
||||
service: resource?.attributes?.service,
|
||||
};
|
||||
})
|
||||
.filter(Boolean) || [];
|
||||
|
||||
const uniqueRegions = Array.from(
|
||||
new Set<string>(
|
||||
allRegionsAndServices
|
||||
.map((item: { region: string }) => item.region)
|
||||
.filter(Boolean),
|
||||
.filter(Boolean) || [],
|
||||
),
|
||||
);
|
||||
const uniqueServices = Array.from(
|
||||
new Set<string>(
|
||||
allRegionsAndServices
|
||||
.map((item: { service: string }) => item.service)
|
||||
.filter(Boolean),
|
||||
.filter(Boolean) || [],
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Suspense } from "react";
|
||||
import { getProviders } from "@/actions/providers";
|
||||
import { getScans } from "@/actions/scans";
|
||||
import { filterScans } from "@/components/filters";
|
||||
import { ButtonRefreshData } from "@/components/scans";
|
||||
import { LaunchScanWorkflow } from "@/components/scans/launch-workflow";
|
||||
import { SkeletonTableScans } from "@/components/scans/table";
|
||||
import { ColumnGetScans } from "@/components/scans/table/scans";
|
||||
@@ -32,6 +33,8 @@ export default async function Scans({
|
||||
}))
|
||||
: [];
|
||||
|
||||
// const executingScans = await getExecutingScans();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="Scans" icon="lucide:scan-search" />
|
||||
@@ -39,7 +42,16 @@ export default async function Scans({
|
||||
<Spacer y={4} />
|
||||
<LaunchScanWorkflow providers={providerInfo} />
|
||||
<Spacer y={8} />
|
||||
<DataTableFilterCustom filters={filterScans || []} />
|
||||
<div className="flex flex-row justify-between">
|
||||
<DataTableFilterCustom filters={filterScans || []} />
|
||||
<ButtonRefreshData
|
||||
onPress={async () => {
|
||||
"use server";
|
||||
await getScans({});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Spacer y={8} />
|
||||
|
||||
<div className="grid grid-cols-12 items-start gap-4">
|
||||
@@ -81,3 +93,12 @@ const SSRDataTableScans = async ({
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// const getExecutingScans = async () => {
|
||||
// const scansData = await getScans({});
|
||||
|
||||
// return scansData?.data?.some(
|
||||
// (scan: ScanProps) =>
|
||||
// scan.attributes.state === "executing" && scan.attributes.progress < 100,
|
||||
// );
|
||||
// };
|
||||
|
||||
@@ -60,7 +60,9 @@ export const ConnectAccountForm = () => {
|
||||
if (data?.errors && data.errors.length > 0) {
|
||||
data.errors.forEach((error: ApiError) => {
|
||||
const errorMessage = error.detail;
|
||||
switch (error.source.pointer) {
|
||||
const pointer = error.source?.pointer;
|
||||
|
||||
switch (pointer) {
|
||||
case "/data/attributes/provider":
|
||||
form.setError("providerType", {
|
||||
type: "server",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { scanOnDemand } from "@/actions/scans/scans";
|
||||
import { RocketIcon, ScheduleIcon } from "@/components/icons";
|
||||
import { AddIcon } from "@/components/icons";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { ProviderProps } from "@/types"; // Asegúrate de importar la interfaz correcta
|
||||
@@ -51,7 +51,7 @@ export const LaunchScanForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
// const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmitClient = async (values: FormValues) => {
|
||||
const formData = new FormData();
|
||||
@@ -94,10 +94,11 @@ export const LaunchScanForm = ({
|
||||
>
|
||||
<div className="text-left">
|
||||
<div className="text-2xl font-bold leading-9 text-default-foreground">
|
||||
Launch scan
|
||||
Scan started
|
||||
</div>
|
||||
<div className="py-2 text-default-500">
|
||||
Launch the scan now or schedule it for a later date and time.
|
||||
The scan has just started. From now on, a new scan will be launched
|
||||
every 24 hours, starting from this moment.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -116,7 +117,7 @@ export const LaunchScanForm = ({
|
||||
<input type="hidden" name="providerId" value={providerId} />
|
||||
<input type="hidden" name="providerType" value={providerType} />
|
||||
|
||||
<div className="flex w-full justify-end sm:space-x-6">
|
||||
{/* <div className="flex w-full justify-end sm:space-x-6">
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel={"Save"}
|
||||
@@ -142,6 +143,18 @@ export const LaunchScanForm = ({
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>Start now</span>}
|
||||
</CustomButton>
|
||||
</div> */}
|
||||
<div className="flex w-full items-center justify-end">
|
||||
<CustomButton
|
||||
asLink="/scans"
|
||||
ariaLabel="Go to Scans page"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="md"
|
||||
endContent={<AddIcon size={20} />}
|
||||
>
|
||||
Go to Scans
|
||||
</CustomButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
checkConnectionProvider,
|
||||
deleteCredentials,
|
||||
} from "@/actions/providers";
|
||||
import { scanOnDemand } from "@/actions/scans";
|
||||
import { getTask } from "@/actions/task/tasks";
|
||||
import { CheckIcon, SaveIcon } from "@/components/icons";
|
||||
import { useToast } from "@/components/ui";
|
||||
@@ -115,9 +116,26 @@ export const TestConnectionForm = ({
|
||||
});
|
||||
|
||||
if (connected) {
|
||||
router.push(
|
||||
`/providers/launch-scan?type=${providerType}&id=${providerId}`,
|
||||
);
|
||||
try {
|
||||
const data = await scanOnDemand(formData);
|
||||
|
||||
if (data.error) {
|
||||
setApiErrorMessage(data.error);
|
||||
form.setError("providerId", {
|
||||
type: "server",
|
||||
message: data.error,
|
||||
});
|
||||
} else {
|
||||
router.push(
|
||||
`/providers/launch-scan?type=${providerType}&id=${providerId}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
form.setError("providerId", {
|
||||
type: "server",
|
||||
message: "An unexpected error occurred. Please try again.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setConnectionStatus({
|
||||
connected: false,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { RefreshCcwIcon } from "lucide-react";
|
||||
import { useTransition } from "react";
|
||||
|
||||
import { CustomButton } from "../ui/custom";
|
||||
|
||||
export const ButtonRefreshData = ({
|
||||
onPress,
|
||||
}: {
|
||||
onPress: () => Promise<void>;
|
||||
}) => {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
return (
|
||||
<CustomButton
|
||||
ariaLabel="Refresh scan page"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="md"
|
||||
endContent={!isPending && <RefreshCcwIcon size={24} />}
|
||||
isLoading={isPending}
|
||||
onPress={() => {
|
||||
startTransition(async () => {
|
||||
await onPress();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./button-refresh-data";
|
||||
@@ -127,9 +127,11 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
const {
|
||||
attributes: { name },
|
||||
} = getScanData(row);
|
||||
if (name.length === 0) {
|
||||
|
||||
if (!name || name.length === 0) {
|
||||
return <span className="font-medium">-</span>;
|
||||
}
|
||||
|
||||
return <span className="font-medium">{name}</span>;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,9 +4,10 @@ import React, { ReactNode } from "react";
|
||||
interface CustomAlertModalProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
title: string;
|
||||
description: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CustomAlertModal: React.FC<CustomAlertModalProps> = ({
|
||||
@@ -15,13 +16,14 @@ export const CustomAlertModal: React.FC<CustomAlertModalProps> = ({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
size="xl"
|
||||
className="dark:bg-prowler-blue-800"
|
||||
className={`dark:bg-prowler-blue-800 ${className || ""}`}
|
||||
backdrop="blur"
|
||||
>
|
||||
<ModalContent className="py-4">
|
||||
|
||||
@@ -42,7 +42,7 @@ interface CustomButtonProps {
|
||||
| "danger"
|
||||
| "transparent";
|
||||
onPress?: (e: PressEvent) => void;
|
||||
children: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
startContent?: React.ReactNode;
|
||||
endContent?: React.ReactNode;
|
||||
size?: "sm" | "md" | "lg";
|
||||
|
||||
@@ -44,55 +44,57 @@ export const CustomDropdownFilter: React.FC<CustomDropdownFilterProps> = ({
|
||||
return currentFilters;
|
||||
}, [searchParams, filter]);
|
||||
|
||||
const memoizedFilterValues = useMemo(
|
||||
() => filter?.values || [],
|
||||
[filter?.values],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (filter && getActiveFilter[filter.key]) {
|
||||
const activeValues = getActiveFilter[filter.key].split(",");
|
||||
const newSelection = new Set(activeValues);
|
||||
if (newSelection.size === allFilterKeys.length) {
|
||||
if (newSelection.size === memoizedFilterValues.length) {
|
||||
newSelection.add("all");
|
||||
}
|
||||
setGroupSelected(newSelection);
|
||||
} else {
|
||||
setGroupSelected(new Set());
|
||||
}
|
||||
}, [getActiveFilter, filter, allFilterKeys]);
|
||||
}, [getActiveFilter, filter?.key, memoizedFilterValues]);
|
||||
|
||||
const onSelectionChange = useCallback(
|
||||
(keys: string[]) => {
|
||||
const newSelection = new Set(keys);
|
||||
setGroupSelected((prevGroupSelected) => {
|
||||
const newSelection = new Set(keys);
|
||||
|
||||
if (
|
||||
newSelection.size === allFilterKeys.length &&
|
||||
!newSelection.has("all")
|
||||
) {
|
||||
setGroupSelected(new Set(["all", ...allFilterKeys]));
|
||||
} else if (groupSelected.has("all")) {
|
||||
newSelection.delete("all");
|
||||
const remainingValues = allFilterKeys.filter((key) =>
|
||||
newSelection.has(key),
|
||||
);
|
||||
setGroupSelected(new Set(remainingValues));
|
||||
} else {
|
||||
setGroupSelected(newSelection);
|
||||
}
|
||||
if (
|
||||
newSelection.size === allFilterKeys.length &&
|
||||
!newSelection.has("all")
|
||||
) {
|
||||
return new Set(["all", ...allFilterKeys]);
|
||||
} else if (prevGroupSelected.has("all")) {
|
||||
newSelection.delete("all");
|
||||
return new Set(allFilterKeys.filter((key) => newSelection.has(key)));
|
||||
}
|
||||
return newSelection;
|
||||
});
|
||||
|
||||
if (onFilterChange && filter) {
|
||||
const selectedValues = Array.from(newSelection).filter(
|
||||
(key) => key !== "all",
|
||||
);
|
||||
const selectedValues = keys.filter((key) => key !== "all");
|
||||
onFilterChange(filter.key, selectedValues);
|
||||
}
|
||||
},
|
||||
[allFilterKeys, groupSelected, onFilterChange, filter],
|
||||
[allFilterKeys, onFilterChange, filter],
|
||||
);
|
||||
|
||||
const handleSelectAllClick = useCallback(() => {
|
||||
if (groupSelected.has("all")) {
|
||||
setGroupSelected(new Set());
|
||||
} else {
|
||||
setGroupSelected(new Set(["all", ...allFilterKeys]));
|
||||
}
|
||||
}, [groupSelected, allFilterKeys]);
|
||||
setGroupSelected((prevGroupSelected) => {
|
||||
if (prevGroupSelected.has("all")) {
|
||||
return new Set();
|
||||
}
|
||||
return new Set(["all", ...allFilterKeys]);
|
||||
});
|
||||
}, [allFilterKeys]);
|
||||
|
||||
const onClearFilter = useCallback(
|
||||
(filterKey: string) => {
|
||||
@@ -170,7 +172,7 @@ export const CustomDropdownFilter: React.FC<CustomDropdownFilterProps> = ({
|
||||
hideScrollBar
|
||||
className="flex max-h-96 max-w-56 flex-col gap-y-2 py-2"
|
||||
>
|
||||
{allFilterKeys.map((value) => (
|
||||
{memoizedFilterValues.map((value) => (
|
||||
<Checkbox className="font-normal" key={value} value={value}>
|
||||
{value}
|
||||
</Checkbox>
|
||||
|
||||
@@ -106,9 +106,11 @@ export function DataTable<TData, TValue>({
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex w-full items-center space-x-2 py-4">
|
||||
<DataTablePagination metadata={metadata} />
|
||||
</div>
|
||||
{metadata && (
|
||||
<div className="flex w-full items-center space-x-2 py-4">
|
||||
<DataTablePagination metadata={metadata} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user