From 4b53fc7f05a137cd64423a25ce9fc730626591fa Mon Sep 17 00:00:00 2001 From: milan-deepfence Date: Thu, 11 Jan 2024 16:26:58 +0530 Subject: [PATCH 1/2] wip delete multiple integration --- .../integrations/components/IntegrationTable.tsx | 7 ++++++- .../components/useIntegrationTableColumn.tsx | 13 ++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/deepfence_frontend/apps/dashboard/src/features/integrations/components/IntegrationTable.tsx b/deepfence_frontend/apps/dashboard/src/features/integrations/components/IntegrationTable.tsx index 438391b872..9fc0f267dc 100644 --- a/deepfence_frontend/apps/dashboard/src/features/integrations/components/IntegrationTable.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/integrations/components/IntegrationTable.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { useParams } from 'react-router-dom'; -import { Table, TableNoDataElement } from 'ui-components'; +import { RowSelectionState, Table, TableNoDataElement } from 'ui-components'; import { ModelIntegrationListResp } from '@/api/generated'; import { @@ -19,6 +19,7 @@ export const IntegrationTable = ({ }) => { const columns = useIntegrationTableColumn(onTableAction); const { data: list } = useListIntegrations(); + const [rowSelectionState, setRowSelectionState] = useState({}); const { data = [], message } = list ?? {}; const params = useParams() as { @@ -44,6 +45,10 @@ export const IntegrationTable = ({ onPageResize={(newSize) => { setPageSize(newSize); }} + enableRowSelection + rowSelectionState={rowSelectionState} + onRowSelectionChange={setRowSelectionState} + getRowId={(row) => `${row.id}`} noDataElement={ } diff --git a/deepfence_frontend/apps/dashboard/src/features/integrations/components/useIntegrationTableColumn.tsx b/deepfence_frontend/apps/dashboard/src/features/integrations/components/useIntegrationTableColumn.tsx index d8017d572a..e4059106e3 100644 --- a/deepfence_frontend/apps/dashboard/src/features/integrations/components/useIntegrationTableColumn.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/integrations/components/useIntegrationTableColumn.tsx @@ -1,7 +1,13 @@ import { has, isEmpty } from 'lodash-es'; import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import { createColumnHelper, Dropdown, DropdownItem, Tooltip } from 'ui-components'; +import { + createColumnHelper, + Dropdown, + DropdownItem, + getRowSelectionColumn, + Tooltip, +} from 'ui-components'; import { ModelIntegrationListResp } from '@/api/generated'; import { EllipsisIcon } from '@/components/icons/common/Ellipsis'; @@ -462,6 +468,11 @@ export const useIntegrationTableColumn = ( const columns = useMemo(() => { const columns = [ + getRowSelectionColumn(columnHelper, { + size: 30, + minSize: 20, + maxSize: 45, + }), columnHelper.display({ id: 'actions', enableSorting: false, From 79a36918c3be39d1cbf9cb8bae4b7afaff367eb3 Mon Sep 17 00:00:00 2001 From: milan-deepfence Date: Wed, 31 Jan 2024 16:54:20 +0530 Subject: [PATCH 2/2] bulk delete added for integration --- .../apps/dashboard/api-spec.json | 61 ++++++++- .../apps/dashboard/src/api/api.ts | 1 + .../api/generated/.openapi-generator/FILES | 1 + .../src/api/generated/apis/IntegrationApi.ts | 77 ++++++++++-- .../models/ModelDeleteIntegrationReq.ts | 66 ++++++++++ .../models/ModelIntegrationAddReq.ts | 10 +- .../src/api/generated/models/index.ts | 1 + .../components/IntegrationTable.tsx | 5 +- .../integrations/pages/IntegrationAdd.tsx | 119 +++++++++++++++--- 9 files changed, 309 insertions(+), 32 deletions(-) create mode 100644 deepfence_frontend/apps/dashboard/src/api/generated/models/ModelDeleteIntegrationReq.ts diff --git a/deepfence_frontend/apps/dashboard/api-spec.json b/deepfence_frontend/apps/dashboard/api-spec.json index 97fbea5b1b..084f0c444d 100644 --- a/deepfence_frontend/apps/dashboard/api-spec.json +++ b/deepfence_frontend/apps/dashboard/api-spec.json @@ -3639,11 +3639,56 @@ "security": [{ "bearer_token": [] }] } }, + "/deepfence/integration/delete": { + "patch": { + "tags": ["Integration"], + "summary": "Delete Integrations", + "description": "Delete integrations", + "operationId": "deleteIntegrations", + "requestBody": { + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ModelDeleteIntegrationReq" } + } + } + }, + "responses": { + "204": { "description": "No Content" }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ApiDocsBadRequestResponse" } + } + } + }, + "401": { "description": "Unauthorized" }, + "403": { "description": "Forbidden" }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ApiDocsFailureResponse" } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ApiDocsFailureResponse" } + } + } + } + }, + "security": [{ "bearer_token": [] }] + } + }, "/deepfence/integration/{integration_id}": { "delete": { "tags": ["Integration"], - "summary": "Delete Integration", - "description": "Delete integration", + "summary": "Delete Single Integration", + "description": "Delete single integration", "operationId": "deleteIntegration", "parameters": [ { @@ -13066,6 +13111,17 @@ "vulnerability_scan_status": { "type": "string" } } }, + "ModelDeleteIntegrationReq": { + "required": ["integration_ids"], + "type": "object", + "properties": { + "integration_ids": { + "type": "array", + "items": { "type": "integer" }, + "nullable": true + } + } + }, "ModelDeleteRegistryBulkReq": { "required": ["registry_ids"], "type": "object", @@ -13439,6 +13495,7 @@ } }, "ModelIntegrationAddReq": { + "required": ["integration_type", "notification_type"], "type": "object", "properties": { "config": { "type": "object", "additionalProperties": {}, "nullable": true }, diff --git a/deepfence_frontend/apps/dashboard/src/api/api.ts b/deepfence_frontend/apps/dashboard/src/api/api.ts index 046171142f..ac90d852d0 100644 --- a/deepfence_frontend/apps/dashboard/src/api/api.ts +++ b/deepfence_frontend/apps/dashboard/src/api/api.ts @@ -309,6 +309,7 @@ export function getIntegrationApiClient() { updateIntegration: integrationApi.updateIntegration.bind(integrationApi), listIntegration: integrationApi.listIntegration.bind(integrationApi), deleteIntegration: integrationApi.deleteIntegration.bind(integrationApi), + bulkDeleteIntegration: integrationApi.deleteIntegrations.bind(integrationApi), }; } diff --git a/deepfence_frontend/apps/dashboard/src/api/generated/.openapi-generator/FILES b/deepfence_frontend/apps/dashboard/src/api/generated/.openapi-generator/FILES index ba89a98bbe..07a06d8e20 100644 --- a/deepfence_frontend/apps/dashboard/src/api/generated/.openapi-generator/FILES +++ b/deepfence_frontend/apps/dashboard/src/api/generated/.openapi-generator/FILES @@ -107,6 +107,7 @@ models/ModelComplianceScanTriggerReq.ts models/ModelConnection.ts models/ModelContainer.ts models/ModelContainerImage.ts +models/ModelDeleteIntegrationReq.ts models/ModelDeleteRegistryBulkReq.ts models/ModelDownloadReportResponse.ts models/ModelDownloadScanResultsResponse.ts diff --git a/deepfence_frontend/apps/dashboard/src/api/generated/apis/IntegrationApi.ts b/deepfence_frontend/apps/dashboard/src/api/generated/apis/IntegrationApi.ts index 8502f76544..cd7306630a 100644 --- a/deepfence_frontend/apps/dashboard/src/api/generated/apis/IntegrationApi.ts +++ b/deepfence_frontend/apps/dashboard/src/api/generated/apis/IntegrationApi.ts @@ -17,6 +17,7 @@ import * as runtime from '../runtime'; import type { ApiDocsBadRequestResponse, ApiDocsFailureResponse, + ModelDeleteIntegrationReq, ModelIntegrationAddReq, ModelIntegrationListResp, ModelIntegrationUpdateReq, @@ -27,6 +28,8 @@ import { ApiDocsBadRequestResponseToJSON, ApiDocsFailureResponseFromJSON, ApiDocsFailureResponseToJSON, + ModelDeleteIntegrationReqFromJSON, + ModelDeleteIntegrationReqToJSON, ModelIntegrationAddReqFromJSON, ModelIntegrationAddReqToJSON, ModelIntegrationListRespFromJSON, @@ -45,6 +48,10 @@ export interface DeleteIntegrationRequest { integrationId: string; } +export interface DeleteIntegrationsRequest { + modelDeleteIntegrationReq?: ModelDeleteIntegrationReq; +} + export interface UpdateIntegrationRequest { integrationId: string; modelIntegrationUpdateReq?: ModelIntegrationUpdateReq; @@ -74,8 +81,8 @@ export interface IntegrationApiInterface { addIntegration(requestParameters: AddIntegrationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; /** - * Delete integration - * @summary Delete Integration + * Delete single integration + * @summary Delete Single Integration * @param {string} integrationId * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -84,11 +91,27 @@ export interface IntegrationApiInterface { deleteIntegrationRaw(requestParameters: DeleteIntegrationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; /** - * Delete integration - * Delete Integration + * Delete single integration + * Delete Single Integration */ deleteIntegration(requestParameters: DeleteIntegrationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + /** + * Delete integrations + * @summary Delete Integrations + * @param {ModelDeleteIntegrationReq} [modelDeleteIntegrationReq] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof IntegrationApiInterface + */ + deleteIntegrationsRaw(requestParameters: DeleteIntegrationsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * Delete integrations + * Delete Integrations + */ + deleteIntegrations(requestParameters: DeleteIntegrationsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; + /** * List all the added Integrations * @summary List Integrations @@ -168,8 +191,8 @@ export class IntegrationApi extends runtime.BaseAPI implements IntegrationApiInt } /** - * Delete integration - * Delete Integration + * Delete single integration + * Delete Single Integration */ async deleteIntegrationRaw(requestParameters: DeleteIntegrationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { if (requestParameters.integrationId === null || requestParameters.integrationId === undefined) { @@ -199,13 +222,51 @@ export class IntegrationApi extends runtime.BaseAPI implements IntegrationApiInt } /** - * Delete integration - * Delete Integration + * Delete single integration + * Delete Single Integration */ async deleteIntegration(requestParameters: DeleteIntegrationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { await this.deleteIntegrationRaw(requestParameters, initOverrides); } + /** + * Delete integrations + * Delete Integrations + */ + async deleteIntegrationsRaw(requestParameters: DeleteIntegrationsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && this.configuration.accessToken) { + const token = this.configuration.accessToken; + const tokenString = await token("bearer_token", []); + + if (tokenString) { + headerParameters["Authorization"] = `Bearer ${tokenString}`; + } + } + const response = await this.request({ + path: `/deepfence/integration/delete`, + method: 'PATCH', + headers: headerParameters, + query: queryParameters, + body: ModelDeleteIntegrationReqToJSON(requestParameters.modelDeleteIntegrationReq), + }, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + * Delete integrations + * Delete Integrations + */ + async deleteIntegrations(requestParameters: DeleteIntegrationsRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + await this.deleteIntegrationsRaw(requestParameters, initOverrides); + } + /** * List all the added Integrations * List Integrations diff --git a/deepfence_frontend/apps/dashboard/src/api/generated/models/ModelDeleteIntegrationReq.ts b/deepfence_frontend/apps/dashboard/src/api/generated/models/ModelDeleteIntegrationReq.ts new file mode 100644 index 0000000000..f3834f6c22 --- /dev/null +++ b/deepfence_frontend/apps/dashboard/src/api/generated/models/ModelDeleteIntegrationReq.ts @@ -0,0 +1,66 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Deepfence ThreatMapper + * Deepfence Runtime API provides programmatic control over Deepfence microservice securing your container, kubernetes and cloud deployments. The API abstracts away underlying infrastructure details like cloud provider, container distros, container orchestrator and type of deployment. This is one uniform API to manage and control security alerts, policies and response to alerts for microservices running anywhere i.e. managed pure greenfield container deployments or a mix of containers, VMs and serverless paradigms like AWS Fargate. + * + * The version of the OpenAPI document: 2.0.0 + * Contact: community@deepfence.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface ModelDeleteIntegrationReq + */ +export interface ModelDeleteIntegrationReq { + /** + * + * @type {Array} + * @memberof ModelDeleteIntegrationReq + */ + integration_ids: Array | null; +} + +/** + * Check if a given object implements the ModelDeleteIntegrationReq interface. + */ +export function instanceOfModelDeleteIntegrationReq(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "integration_ids" in value; + + return isInstance; +} + +export function ModelDeleteIntegrationReqFromJSON(json: any): ModelDeleteIntegrationReq { + return ModelDeleteIntegrationReqFromJSONTyped(json, false); +} + +export function ModelDeleteIntegrationReqFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelDeleteIntegrationReq { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'integration_ids': json['integration_ids'], + }; +} + +export function ModelDeleteIntegrationReqToJSON(value?: ModelDeleteIntegrationReq | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'integration_ids': value.integration_ids, + }; +} + diff --git a/deepfence_frontend/apps/dashboard/src/api/generated/models/ModelIntegrationAddReq.ts b/deepfence_frontend/apps/dashboard/src/api/generated/models/ModelIntegrationAddReq.ts index 6d1a3647f7..c3bb43f333 100644 --- a/deepfence_frontend/apps/dashboard/src/api/generated/models/ModelIntegrationAddReq.ts +++ b/deepfence_frontend/apps/dashboard/src/api/generated/models/ModelIntegrationAddReq.ts @@ -43,13 +43,13 @@ export interface ModelIntegrationAddReq { * @type {string} * @memberof ModelIntegrationAddReq */ - integration_type?: string; + integration_type: string; /** * * @type {string} * @memberof ModelIntegrationAddReq */ - notification_type?: string; + notification_type: string; } /** @@ -57,6 +57,8 @@ export interface ModelIntegrationAddReq { */ export function instanceOfModelIntegrationAddReq(value: object): boolean { let isInstance = true; + isInstance = isInstance && "integration_type" in value; + isInstance = isInstance && "notification_type" in value; return isInstance; } @@ -73,8 +75,8 @@ export function ModelIntegrationAddReqFromJSONTyped(json: any, ignoreDiscriminat 'config': !exists(json, 'config') ? undefined : json['config'], 'filters': !exists(json, 'filters') ? undefined : ModelIntegrationFiltersFromJSON(json['filters']), - 'integration_type': !exists(json, 'integration_type') ? undefined : json['integration_type'], - 'notification_type': !exists(json, 'notification_type') ? undefined : json['notification_type'], + 'integration_type': json['integration_type'], + 'notification_type': json['notification_type'], }; } diff --git a/deepfence_frontend/apps/dashboard/src/api/generated/models/index.ts b/deepfence_frontend/apps/dashboard/src/api/generated/models/index.ts index 2a19b3f158..82642892e2 100644 --- a/deepfence_frontend/apps/dashboard/src/api/generated/models/index.ts +++ b/deepfence_frontend/apps/dashboard/src/api/generated/models/index.ts @@ -81,6 +81,7 @@ export * from './ModelComplianceScanTriggerReq'; export * from './ModelConnection'; export * from './ModelContainer'; export * from './ModelContainerImage'; +export * from './ModelDeleteIntegrationReq'; export * from './ModelDeleteRegistryBulkReq'; export * from './ModelDownloadReportResponse'; export * from './ModelDownloadScanResultsResponse'; diff --git a/deepfence_frontend/apps/dashboard/src/features/integrations/components/IntegrationTable.tsx b/deepfence_frontend/apps/dashboard/src/features/integrations/components/IntegrationTable.tsx index 9fc0f267dc..9de53393ed 100644 --- a/deepfence_frontend/apps/dashboard/src/features/integrations/components/IntegrationTable.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/integrations/components/IntegrationTable.tsx @@ -14,12 +14,15 @@ const DEFAULT_PAGE_SIZE = 10; export const IntegrationTable = ({ onTableAction, + rowSelectionState, + setRowSelectionState, }: { onTableAction: (row: ModelIntegrationListResp, actionType: ActionEnumType) => void; + rowSelectionState: RowSelectionState; + setRowSelectionState: React.Dispatch>; }) => { const columns = useIntegrationTableColumn(onTableAction); const { data: list } = useListIntegrations(); - const [rowSelectionState, setRowSelectionState] = useState({}); const { data = [], message } = list ?? {}; const params = useParams() as { diff --git a/deepfence_frontend/apps/dashboard/src/features/integrations/pages/IntegrationAdd.tsx b/deepfence_frontend/apps/dashboard/src/features/integrations/pages/IntegrationAdd.tsx index 817f8856d6..af8e2d40ac 100644 --- a/deepfence_frontend/apps/dashboard/src/features/integrations/pages/IntegrationAdd.tsx +++ b/deepfence_frontend/apps/dashboard/src/features/integrations/pages/IntegrationAdd.tsx @@ -1,10 +1,11 @@ import { useSuspenseQuery } from '@suspensive/react-query'; import { isNil } from 'lodash-es'; -import { Suspense, useCallback, useMemo, useState } from 'react'; +import { Suspense, useCallback, useEffect, useMemo, useState } from 'react'; import { ActionFunctionArgs, useFetcher, useParams } from 'react-router-dom'; import { Button, Modal, + RowSelectionState, SlidingModal, SlidingModalCloseButton, SlidingModalHeader, @@ -174,6 +175,7 @@ const getConfigBodyNotificationType = (formData: FormData, integrationType: stri } }; type ActionData = { + action: ActionEnumType; message?: string; success?: boolean; deleteSuccess?: boolean; @@ -181,15 +183,16 @@ type ActionData = { } | null; const action = async ({ request, params }: ActionFunctionArgs): Promise => { - const _integrationType = params.integrationType?.toString(); + const _integrationType = params.integrationType?.toString() ?? ''; const formData = await request.formData(); - const _notificationType = formData.get('_notificationType')?.toString(); + const _notificationType = formData.get('_notificationType')?.toString() ?? ''; const _actionType = formData.get('_actionType')?.toString(); const integrationId = formData.get('integrationId')?.toString() ?? ''; if (!_actionType) { return { message: 'Action Type is required', + action: _actionType as ActionEnumType, }; } @@ -355,6 +358,7 @@ const action = async ({ request, params }: ActionFunctionArgs): Promise Number(id)), + }, }); if (!r.ok) { if (r.error.response.status === 400) { const { message } = await getResponseErrors(r.error); return { + action: _actionType as ActionEnumType, success: false, message: message ?? 'Error in deleting integrations', }; } else if (r.error.response.status === 403) { const message = await get403Message(r.error); return { + action: _actionType as ActionEnumType, message, success: false, }; @@ -428,6 +440,7 @@ const action = async ({ request, params }: ActionFunctionArgs): Promise { const DeleteConfirmationModal = ({ showDialog, - integrationId, + ids, setShowDialog, + onDeleteSuccess, }: { showDialog: boolean; - integrationId: number | undefined; + ids: string[]; setShowDialog: React.Dispatch>; + onDeleteSuccess: () => void; }) => { const fetcher = useFetcher(); @@ -456,14 +471,24 @@ const DeleteConfirmationModal = ({ (actionType: string) => { const formData = new FormData(); formData.append('_actionType', actionType); - formData.append('id', integrationId?.toString() ?? ''); - + ids.forEach((item) => formData.append('id[]', item)); fetcher.submit(formData, { method: 'post', }); }, - [fetcher, integrationId], + [fetcher, ids], ); + + useEffect(() => { + if ( + fetcher.state === 'idle' && + fetcher.data?.deleteSuccess && + fetcher.data.action === ActionEnumType.DELETE + ) { + onDeleteSuccess(); + } + }, [fetcher]); + return ( { ); }; + +const BulkActions = ({ + selectedRows, + setRowSelectionState, +}: { + selectedRows: RowSelectionState; + setRowSelectionState: React.Dispatch>; +}) => { + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + + const integrationIdsToDelete = useMemo(() => { + return Object.keys(selectedRows).map((id) => { + return id; + }); + }, [selectedRows]); + + return ( + <> + {showDeleteDialog && ( + { + setRowSelectionState({}); + }} + /> + )} + + + ); +}; + const IntegrationAdd = () => { const { integrationType } = useParams() as { integrationType: string; @@ -559,7 +627,9 @@ const IntegrationAdd = () => { const [showEditIntegrationModal, setShowEditIntegrationModal] = useState(false); - const [integrationIdToDelete, setIntegrationIdToDelete] = useState(); + const [integrationIdsToDelete, setIntegrationIdsToDelete] = useState([]); + + const [rowSelectionState, setRowSelectionState] = useState({}); const params = useParams() as { integrationType: string; @@ -568,7 +638,11 @@ const IntegrationAdd = () => { const onTableAction = useCallback( (row: ModelIntegrationListResp, actionType: string) => { if (actionType === ActionEnumType.DELETE) { - setIntegrationIdToDelete(row.id); + if (!row.id) { + console.error('Row is missing integration id'); + return; + } + setIntegrationIdsToDelete([row.id.toString()]); setShowDeleteDialog(true); } else if (actionType === ActionEnumType.EDIT) { setIntegrationToEdit(row); @@ -599,6 +673,10 @@ const IntegrationAdd = () => { > Add new integration + {isEmailIntegration && ( @@ -649,14 +727,21 @@ const IntegrationAdd = () => {
}> - +
{showDeleteDialog && ( { + setRowSelectionState({}); + }} /> )}