feat(ui): S3 integrations pagination added (#8450)

This commit is contained in:
Alejandro Bailo
2025-08-05 18:11:32 +02:00
committed by GitHub
parent a9d16bbbce
commit c3d25e6f39
5 changed files with 81 additions and 113 deletions
+1 -4
View File
@@ -1,12 +1,9 @@
import React from "react";
import { getIntegrations } from "@/actions/integrations";
import { S3IntegrationCard } from "@/components/integrations";
import { ContentLayout } from "@/components/ui";
export default async function Integrations() {
const integrations = await getIntegrations();
return (
<ContentLayout title="Integrations" icon="lucide:puzzle">
<div className="space-y-6">
@@ -19,7 +16,7 @@ export default async function Integrations() {
<div className="grid gap-6">
{/* Amazon S3 Integration */}
<S3IntegrationCard integrations={integrations?.data || []} />
<S3IntegrationCard />
</div>
</div>
</ContentLayout>
+36 -4
View File
@@ -5,16 +5,47 @@ import { getProviders } from "@/actions/providers";
import { S3IntegrationsManager } from "@/components/integrations/s3/s3-integrations-manager";
import { ContentLayout } from "@/components/ui";
export default async function S3Integrations() {
interface S3IntegrationsProps {
searchParams: { [key: string]: string | string[] | undefined };
}
export default async function S3Integrations({
searchParams,
}: S3IntegrationsProps) {
const page = parseInt(searchParams.page?.toString() || "1", 10);
const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10);
const sort = searchParams.sort?.toString();
// Extract all filter parameters
const filters = Object.fromEntries(
Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")),
);
const urlSearchParams = new URLSearchParams();
urlSearchParams.set("filter[integration_type]", "amazon_s3");
urlSearchParams.set("page[number]", page.toString());
urlSearchParams.set("page[size]", pageSize.toString());
if (sort) {
urlSearchParams.set("sort", sort);
}
// Add any additional filters
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && key !== "filter[integration_type]") {
const stringValue = Array.isArray(value) ? value[0] : String(value);
urlSearchParams.set(key, stringValue);
}
});
const [integrations, providers] = await Promise.all([
getIntegrations(
new URLSearchParams({ "filter[integration_type]": "amazon_s3" }),
),
getIntegrations(urlSearchParams),
getProviders({ pageSize: 100 }),
]);
const s3Integrations = integrations?.data || [];
const availableProviders = providers?.data || [];
const metadata = integrations?.meta;
return (
<ContentLayout title="Amazon S3">
@@ -53,6 +84,7 @@ export default async function S3Integrations() {
<S3IntegrationsManager
integrations={s3Integrations}
providers={availableProviders}
metadata={metadata}
/>
</div>
</ContentLayout>
@@ -1,42 +1,13 @@
"use client";
import { Card, CardBody, CardHeader, Chip } from "@nextui-org/react";
import { Card, CardBody, CardHeader } from "@nextui-org/react";
import { SettingsIcon } from "lucide-react";
import { AmazonS3Icon } from "@/components/icons/services/IconServices";
import { CustomButton } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link";
import { IntegrationProps } from "@/types/integrations";
import { S3IntegrationCardSkeleton } from "./skeleton-s3-integration-card";
interface S3IntegrationCardProps {
integrations?: IntegrationProps[];
isLoading?: boolean;
}
export const S3IntegrationCard = ({
integrations = [],
isLoading = false,
}: S3IntegrationCardProps) => {
const s3Integrations = integrations.filter(
(integration) => integration.attributes.integration_type === "amazon_s3",
);
const isConfigured = s3Integrations.length > 0;
const connectedCount = s3Integrations.filter(
(integration) => integration.attributes.connected,
).length;
if (isLoading) {
return (
<S3IntegrationCardSkeleton
variant="main"
count={s3Integrations.length || 1}
/>
);
}
export const S3IntegrationCard = () => {
return (
<Card className="dark:bg-gray-800">
<CardHeader className="gap-2">
@@ -62,84 +33,24 @@ export const S3IntegrationCard = ({
</div>
</div>
<div className="flex items-center gap-2 self-end sm:self-center">
{isConfigured && (
<Chip
size="sm"
color={connectedCount > 0 ? "success" : "warning"}
variant="flat"
>
{connectedCount} / {s3Integrations.length} connected
</Chip>
)}
<CustomButton
size="sm"
variant="bordered"
startContent={<SettingsIcon size={14} />}
asLink="/integrations/s3"
ariaLabel={
isConfigured
? "Manage S3 integrations"
: "Configure S3 integration"
}
ariaLabel="Manage S3 integrations"
>
{isConfigured ? "Manage" : "Configure"}
Manage
</CustomButton>
</div>
</div>
</CardHeader>
<CardBody>
<div className="flex flex-col gap-4">
{isConfigured ? (
<>
<div className="space-y-2">
{s3Integrations.map((integration) => (
<div
key={integration.id}
className="flex items-center justify-between rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800"
>
<div className="flex flex-col">
<span className="text-sm font-medium">
{integration.attributes.configuration.bucket_name ||
"Unknown Bucket"}
</span>
<span className="text-xs text-gray-500 dark:text-gray-300">
Output directory:{" "}
{integration.attributes.configuration
.output_directory ||
integration.attributes.configuration.path ||
"/"}
</span>
</div>
<Chip
size="sm"
color={
integration.attributes.connected ? "success" : "danger"
}
variant="dot"
>
{integration.attributes.connected
? "Connected"
: "Disconnected"}
</Chip>
</div>
))}
</div>
</>
) : (
<>
<div className="text-sm">
<span className="font-medium">Status: </span>
<span className="text-gray-500">Not configured</span>
</div>
<div className="space-y-3">
<p className="text-sm text-gray-600 dark:text-gray-300">
Export your security findings to Amazon S3 buckets
automatically.
</p>
</div>
</>
)}
<p className="text-sm text-gray-600 dark:text-gray-300">
Configure and manage your Amazon S3 integrations to automatically
export security findings to your S3 buckets.
</p>
</div>
</CardBody>
</Card>
@@ -49,7 +49,6 @@ export const S3IntegrationForm = ({
const isEditingConfig = editMode === "configuration";
const isEditingCredentials = editMode === "credentials";
// Create the form with updated schema and default values
const form = useForm({
resolver: zodResolver(
// For credentials editing, use creation schema (all fields required)
@@ -141,13 +140,30 @@ export const S3IntegrationForm = ({
return credentials;
};
const buildConfiguration = (values: any) => {
const buildConfiguration = (values: any, isPartial = false) => {
const configuration: any = {};
// Always include all fields for both creation and edit modes
// Backend expects complete configuration object
configuration.bucket_name = values.bucket_name;
configuration.output_directory = values.output_directory || "output";
// For creation mode, include all fields
if (!isPartial) {
configuration.bucket_name = values.bucket_name;
configuration.output_directory = values.output_directory || "output";
} else {
// For edit mode, bucket_name and output_directory are treated as a pair
const originalBucketName =
integration?.attributes.configuration.bucket_name || "";
const originalOutputDirectory =
integration?.attributes.configuration.output_directory || "";
const bucketNameChanged = values.bucket_name !== originalBucketName;
const outputDirectoryChanged =
values.output_directory !== originalOutputDirectory;
// If either field changed, send both (as a pair)
if (bucketNameChanged || outputDirectoryChanged) {
configuration.bucket_name = values.bucket_name;
configuration.output_directory = values.output_directory || "output";
}
}
return configuration;
};
@@ -158,8 +174,10 @@ export const S3IntegrationForm = ({
formData.append("integration_type", values.integration_type);
if (isEditingConfig) {
const configuration = buildConfiguration(values);
formData.append("configuration", JSON.stringify(configuration));
const configuration = buildConfiguration(values, true);
if (Object.keys(configuration).length > 0) {
formData.append("configuration", JSON.stringify(configuration));
}
// Always send providers array, even if empty, to update relationships
formData.append("providers", JSON.stringify(values.providers || []));
} else if (isEditingCredentials) {
@@ -19,6 +19,8 @@ import {
import { AmazonS3Icon } from "@/components/icons/services/IconServices";
import { useToast } from "@/components/ui";
import { CustomAlertModal, CustomButton } from "@/components/ui/custom";
import { DataTablePagination } from "@/components/ui/table/data-table-pagination";
import { MetaDataProps } from "@/types";
import { IntegrationProps } from "@/types/integrations";
import { ProviderProps } from "@/types/providers";
@@ -28,11 +30,13 @@ import { S3IntegrationCardSkeleton } from "./skeleton-s3-integration-card";
interface S3IntegrationsManagerProps {
integrations: IntegrationProps[];
providers: ProviderProps[];
metadata?: MetaDataProps;
}
export const S3IntegrationsManager = ({
integrations,
providers,
metadata,
}: S3IntegrationsManagerProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingIntegration, setEditingIntegration] =
@@ -392,6 +396,12 @@ export const S3IntegrationsManager = ({
))}
</div>
) : null}
{metadata && integrations.length > 0 && (
<div className="mt-6">
<DataTablePagination metadata={metadata} />
</div>
)}
</div>
</>
);