mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
345 lines
10 KiB
TypeScript
345 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { format } from "date-fns";
|
|
import { PlusIcon, Trash2Icon } from "lucide-react";
|
|
import { useState } from "react";
|
|
|
|
import {
|
|
deleteIntegration,
|
|
testIntegrationConnection,
|
|
updateIntegration,
|
|
} from "@/actions/integrations";
|
|
import { JiraIcon } from "@/components/icons/services/IconServices";
|
|
import {
|
|
IntegrationActionButtons,
|
|
IntegrationCardHeader,
|
|
IntegrationSkeleton,
|
|
} from "@/components/integrations/shared";
|
|
import { Button } from "@/components/shadcn";
|
|
import { useToast } from "@/components/ui";
|
|
import { CustomAlertModal } from "@/components/ui/custom";
|
|
import { DataTablePagination } from "@/components/ui/table/data-table-pagination";
|
|
import { triggerTestConnectionWithDelay } from "@/lib/integrations/test-connection-helper";
|
|
import { MetaDataProps } from "@/types";
|
|
import { IntegrationProps } from "@/types/integrations";
|
|
|
|
import { Card, CardContent, CardHeader } from "../../shadcn";
|
|
import { JiraIntegrationForm } from "./jira-integration-form";
|
|
|
|
interface JiraIntegrationsManagerProps {
|
|
integrations: IntegrationProps[];
|
|
metadata?: MetaDataProps;
|
|
}
|
|
|
|
export const JiraIntegrationsManager = ({
|
|
integrations,
|
|
metadata,
|
|
}: JiraIntegrationsManagerProps) => {
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [editingIntegration, setEditingIntegration] =
|
|
useState<IntegrationProps | null>(null);
|
|
const [isDeleting, setIsDeleting] = useState<string | null>(null);
|
|
const [isTesting, setIsTesting] = useState<string | null>(null);
|
|
const [isOperationLoading, setIsOperationLoading] = useState(false);
|
|
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
|
const [integrationToDelete, setIntegrationToDelete] =
|
|
useState<IntegrationProps | null>(null);
|
|
const { toast } = useToast();
|
|
|
|
const handleAddIntegration = () => {
|
|
setEditingIntegration(null);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleEditCredentials = (integration: IntegrationProps) => {
|
|
setEditingIntegration(integration);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleOpenDeleteModal = (integration: IntegrationProps) => {
|
|
setIntegrationToDelete(integration);
|
|
setIsDeleteOpen(true);
|
|
};
|
|
|
|
const handleDeleteIntegration = async (id: string) => {
|
|
setIsDeleting(id);
|
|
try {
|
|
const result = await deleteIntegration(id, "jira");
|
|
|
|
if (result.success) {
|
|
toast({
|
|
title: "Success!",
|
|
description: "Jira integration deleted successfully.",
|
|
});
|
|
} else if (result.error) {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Delete Failed",
|
|
description: result.error,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Error",
|
|
description: "Failed to delete Jira integration. Please try again.",
|
|
});
|
|
} finally {
|
|
setIsDeleting(null);
|
|
setIsDeleteOpen(false);
|
|
setIntegrationToDelete(null);
|
|
}
|
|
};
|
|
|
|
const handleTestConnection = async (id: string) => {
|
|
setIsTesting(id);
|
|
try {
|
|
const result = await testIntegrationConnection(id);
|
|
|
|
if (result.success) {
|
|
toast({
|
|
title: "Connection test successful!",
|
|
description:
|
|
result.message || "Connection test completed successfully.",
|
|
});
|
|
} else if (result.error) {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Connection test failed",
|
|
description: result.error,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Error",
|
|
description: "Failed to test connection. Please try again.",
|
|
});
|
|
} finally {
|
|
setIsTesting(null);
|
|
}
|
|
};
|
|
|
|
const handleToggleEnabled = async (integration: IntegrationProps) => {
|
|
try {
|
|
const newEnabledState = !integration.attributes.enabled;
|
|
const formData = new FormData();
|
|
formData.append(
|
|
"integration_type",
|
|
integration.attributes.integration_type,
|
|
);
|
|
formData.append("enabled", JSON.stringify(newEnabledState));
|
|
|
|
const result = await updateIntegration(integration.id, formData);
|
|
|
|
if (result && "success" in result) {
|
|
toast({
|
|
title: "Success!",
|
|
description: `Integration ${newEnabledState ? "enabled" : "disabled"} successfully.`,
|
|
});
|
|
|
|
// If enabling, trigger test connection automatically
|
|
if (newEnabledState) {
|
|
setIsTesting(integration.id);
|
|
|
|
triggerTestConnectionWithDelay(
|
|
integration.id,
|
|
true,
|
|
"jira",
|
|
toast,
|
|
500,
|
|
() => {
|
|
setIsTesting(null);
|
|
},
|
|
);
|
|
}
|
|
} else if (result && "error" in result) {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Toggle Failed",
|
|
description: result.error,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Error",
|
|
description: "Failed to toggle integration. Please try again.",
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleModalClose = () => {
|
|
setIsModalOpen(false);
|
|
setEditingIntegration(null);
|
|
};
|
|
|
|
const handleFormSuccess = async (
|
|
integrationId?: string,
|
|
shouldTestConnection?: boolean,
|
|
) => {
|
|
// Close the modal immediately
|
|
setIsModalOpen(false);
|
|
setEditingIntegration(null);
|
|
setIsOperationLoading(true);
|
|
|
|
// Set testing state for server-triggered test connections
|
|
if (integrationId && shouldTestConnection) {
|
|
setIsTesting(integrationId);
|
|
}
|
|
|
|
// Trigger test connection if needed
|
|
triggerTestConnectionWithDelay(
|
|
integrationId,
|
|
shouldTestConnection,
|
|
"jira",
|
|
toast,
|
|
200,
|
|
() => {
|
|
// Clear testing state when server-triggered test completes
|
|
setIsTesting(null);
|
|
},
|
|
);
|
|
|
|
// Reset loading state after a short delay to show the skeleton briefly
|
|
setTimeout(() => {
|
|
setIsOperationLoading(false);
|
|
}, 1500);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<CustomAlertModal
|
|
isOpen={isDeleteOpen}
|
|
onOpenChange={setIsDeleteOpen}
|
|
title="Delete Jira Integration"
|
|
description="This action cannot be undone. This will permanently delete your Jira integration."
|
|
>
|
|
<div className="flex w-full justify-end gap-4">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="lg"
|
|
onClick={() => {
|
|
setIsDeleteOpen(false);
|
|
setIntegrationToDelete(null);
|
|
}}
|
|
disabled={isDeleting !== null}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
size="lg"
|
|
disabled={isDeleting !== null}
|
|
onClick={() =>
|
|
integrationToDelete &&
|
|
handleDeleteIntegration(integrationToDelete.id)
|
|
}
|
|
>
|
|
{!isDeleting && <Trash2Icon size={24} />}
|
|
{isDeleting ? "Deleting..." : "Delete"}
|
|
</Button>
|
|
</div>
|
|
</CustomAlertModal>
|
|
|
|
<CustomAlertModal
|
|
isOpen={isModalOpen}
|
|
onOpenChange={setIsModalOpen}
|
|
title={
|
|
editingIntegration
|
|
? "Update Jira Credentials"
|
|
: "Add Jira Integration"
|
|
}
|
|
>
|
|
<JiraIntegrationForm
|
|
integration={editingIntegration}
|
|
onSuccess={handleFormSuccess}
|
|
onCancel={handleModalClose}
|
|
/>
|
|
</CustomAlertModal>
|
|
|
|
<div className="flex flex-col gap-6">
|
|
{/* Header with Add Button */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-lg font-semibold">
|
|
Configured Jira Integrations
|
|
</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-300">
|
|
{integrations.length === 0
|
|
? "Not configured yet"
|
|
: `${integrations.length} integration${integrations.length !== 1 ? "s" : ""} configured`}
|
|
</p>
|
|
</div>
|
|
<Button onClick={handleAddIntegration}>
|
|
<PlusIcon size={16} />
|
|
Add Integration
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Integrations List */}
|
|
{isOperationLoading ? (
|
|
<IntegrationSkeleton
|
|
variant="manager"
|
|
count={integrations.length || 1}
|
|
icon={<JiraIcon size={32} />}
|
|
title="Jira"
|
|
subtitle="Create and manage security issues in Jira."
|
|
/>
|
|
) : integrations.length > 0 ? (
|
|
<div className="grid gap-4">
|
|
{integrations.map((integration) => (
|
|
<Card key={integration.id} variant="base">
|
|
<CardHeader>
|
|
<IntegrationCardHeader
|
|
icon={<JiraIcon size={32} />}
|
|
title={`${integration.attributes.configuration.domain}`}
|
|
connectionStatus={{
|
|
connected: integration.attributes.connected,
|
|
}}
|
|
/>
|
|
</CardHeader>
|
|
|
|
<CardContent className="pt-0">
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div className="text-xs text-gray-500 dark:text-gray-300">
|
|
{integration.attributes.connection_last_checked_at && (
|
|
<p>
|
|
<span className="font-medium">Last checked:</span>{" "}
|
|
{format(
|
|
new Date(
|
|
integration.attributes.connection_last_checked_at,
|
|
),
|
|
"yyyy/MM/dd",
|
|
)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<IntegrationActionButtons
|
|
integration={integration}
|
|
onTestConnection={handleTestConnection}
|
|
onEditCredentials={handleEditCredentials}
|
|
onToggleEnabled={handleToggleEnabled}
|
|
onDelete={handleOpenDeleteModal}
|
|
isTesting={isTesting === integration.id}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
|
|
{metadata && integrations.length > 0 && (
|
|
<div className="mt-6">
|
|
<DataTablePagination metadata={metadata} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
};
|