Skip to content

Commit

Permalink
Add object actions - delete objects
Browse files Browse the repository at this point in the history
  • Loading branch information
SanjalKatiyar committed Oct 9, 2024
1 parent 43d1d39 commit a768cd0
Show file tree
Hide file tree
Showing 15 changed files with 804 additions and 34 deletions.
15 changes: 15 additions & 0 deletions locales/en/plugin__odf-console.json
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,12 @@
"Delete objects": "Delete objects",
"Actions": "Actions",
"Create folder": "Create folder",
"Failed to delete {{ errorCount }} object from the bucket. View deletion summary for details.": "Failed to delete {{ errorCount }} object from the bucket. View deletion summary for details.",
"Failed to delete {{ errorCount }} objects from the bucket. View deletion summary for details.": "Failed to delete {{ errorCount }} objects from the bucket. View deletion summary for details.",
"View failed objects": "View failed objects",
"Successfully deleted {{ successCount }} object from the bucket.": "Successfully deleted {{ successCount }} object from the bucket.",
"Successfully deleted {{ successCount }} objects from the bucket.": "Successfully deleted {{ successCount }} objects from the bucket.",
"Objects are the fundamental entities stored in buckets.": "Objects are the fundamental entities stored in buckets.",
"Size": "Size",
"Last modified": "Last modified",
"Download": "Download",
Expand Down Expand Up @@ -1304,6 +1310,14 @@
"Organize objects within a bucket by creating virtual folders for easier management and navigation of objects.": "Organize objects within a bucket by creating virtual folders for easier management and navigation of objects.",
"Folders structure and group objects logically by using prefixes in object keys, without enforcing any physical hierarchy.": "Folders structure and group objects logically by using prefixes in object keys, without enforcing any physical hierarchy.",
"Folder name": "Folder name",
"<0>To confirm deletion, type <1>{{delete}}</1> in the text input field.</0>": "<0>To confirm deletion, type <1>{{delete}}</1> in the text input field.</0>",
"Object name": "Object name",
"Delete object?": "Delete object?",
"Deleted objects will no longer be visible in the bucket. If versioning is enabled a delete marker is created, you can recover object from previous versions. For unversioned objects, deletion is final and cannot be undone.": "Deleted objects will no longer be visible in the bucket. If versioning is enabled a delete marker is created, you can recover object from previous versions. For unversioned objects, deletion is final and cannot be undone.",
"Delete object": "Delete object",
"Delete status": "Delete status",
"Failed": "Failed",
"Object delete summary": "Object delete summary",
"Expires after": "Expires after",
"minus": "minus",
"plus": "plus",
Expand Down Expand Up @@ -1415,6 +1429,7 @@
"Values": "Values",
"Select the values": "Select the values",
"Add label expression": "Add label expression",
"Search by name...": "Search by name...",
"Delete {{kind}}?": "Delete {{kind}}?",
"Are you sure you want to delete <2>{{resourceName}}</2> in namespace <6>{{namespace}}</6>?": "Are you sure you want to delete <2>{{resourceName}}</2> in namespace <6>{{namespace}}</6>?",
"Are you sure you want to delete <2>{{resourceName}}</2>?": "Are you sure you want to delete <2>{{resourceName}}</2>?",
Expand Down
209 changes: 197 additions & 12 deletions packages/odf/components/s3-browser/objects-list/ObjectsList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as React from 'react';
import {
ListObjectsV2CommandOutput,
DeleteObjectsCommandOutput,
_Object as Content,
CommonPrefix,
} from '@aws-sdk/client-s3';
import { pluralize } from '@odf/core/components/utils';
import { S3Commands } from '@odf/shared/s3';
import { SelectableTable } from '@odf/shared/table';
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
Expand All @@ -19,6 +21,10 @@ import {
Level,
LevelItem,
MenuToggle,
Alert,
AlertVariant,
AlertActionCloseButton,
AlertActionLink,
} from '@patternfly/react-core';
import { AngleLeftIcon, AngleRightIcon } from '@patternfly/react-icons';
import {
Expand All @@ -27,6 +33,14 @@ import {
CustomActionsToggleProps,
} from '@patternfly/react-table';
import { LIST_OBJECTS, DELIMITER, MAX_KEYS, PREFIX } from '../../../constants';
import {
ObjectsDeleteResponse,
SetObjectsDeleteResponse,
} from '../../../modals/s3-browser/delete-objects/DeleteObjectsModal';
import {
LazyDeleteObjectsModal,
LazyDeleteObjectsSummary,
} from '../../../modals/s3-browser/delete-objects/LazyDeleteModals';
import { ObjectCrFormat } from '../../../types';
import { getPath, convertObjectsDataToCrFormat } from '../../../utils';
import { NoobaaS3Context } from '../noobaa-context';
Expand All @@ -50,11 +64,18 @@ type PaginationProps = {

type TableActionsProps = {
launcher: LaunchModal;
selectedRows: unknown[];
selectedRows: ObjectCrFormat[];
loadedWOError: boolean;
foldersPath: string;
bucketName: string;
noobaaS3: S3Commands;
setDeleteResponse: SetObjectsDeleteResponse;
refreshTokens: () => Promise<void>;
};

type DeletionAlertsProps = {
deleteResponse: ObjectsDeleteResponse;
foldersPath: string;
};

type ContinuationTokens = {
Expand All @@ -70,6 +91,7 @@ type Trigger = TriggerWithOptionsArgs<
string
>;

// for navigating (next/previous) through objects list
const continuationTokensSetter = (
setContinuationTokens: React.Dispatch<
React.SetStateAction<ContinuationTokens>
Expand Down Expand Up @@ -117,15 +139,53 @@ const fetchObjects = async (
}
};

// for refreshing (re-feching) objects from start, once state of bucket has changed (objects added/deleted)
const continuationTokensRefresher = async (
setContinuationTokens: React.Dispatch<
React.SetStateAction<ContinuationTokens>
>,
trigger: Trigger,
setSelectedRows: React.Dispatch<React.SetStateAction<ObjectCrFormat[]>>
) => {
try {
const response: ListObjectsV2CommandOutput = await trigger();
setContinuationTokens({
previous: [''],
current: '',
next: response.NextContinuationToken,
});
setSelectedRows([]);
} catch (err) {
// no need to handle any error here, use "error" object directly from the "useSWRMutation" hook
// eslint-disable-next-line no-console
console.error(err);
}
};

const getBulkActionsItems = (
t: TFunction,
_launcher: LaunchModal,
_selectedRows: unknown[]
launcher: LaunchModal,
selectedRows: ObjectCrFormat[],
foldersPath: string,
bucketName: string,
noobaaS3: S3Commands,
setDeleteResponse: SetObjectsDeleteResponse,
refreshTokens: () => Promise<void>
): IAction[] => [
// ToDo: add bulk delete option
{
title: t('Delete objects'),
onClick: () => undefined,
onClick: () =>
launcher(LazyDeleteObjectsModal, {
isOpen: true,
extraProps: {
foldersPath,
bucketName,
objects: selectedRows,
noobaaS3,
setDeleteResponse,
refreshTokens,
},
}),
},
];

Expand Down Expand Up @@ -182,6 +242,8 @@ const TableActions: React.FC<PaginationProps & TableActionsProps> = ({
foldersPath,
bucketName,
noobaaS3,
setDeleteResponse,
refreshTokens,
}) => {
const { t } = useCustomTranslation();

Expand All @@ -193,7 +255,7 @@ const TableActions: React.FC<PaginationProps & TableActionsProps> = ({
<div className="pf-v5-u-display-flex pf-v5-u-flex-direction-row">
<Button
variant={ButtonVariant.secondary}
className="pf-v5-u-mr-xs"
className="pf-v5-u-mr-sm"
isDisabled={anySelection || !loadedWOError}
onClick={() =>
launcher(LazyCreateFolderModal, {
Expand All @@ -207,9 +269,18 @@ const TableActions: React.FC<PaginationProps & TableActionsProps> = ({
<ActionsColumn
isDisabled={!anySelection || !loadedWOError}
translate={null}
items={getBulkActionsItems(t, launcher, selectedRows)}
items={getBulkActionsItems(
t,
launcher,
selectedRows,
foldersPath,
bucketName,
noobaaS3,
setDeleteResponse,
refreshTokens
)}
actionsToggle={CustomActionsToggle}
className="pf-v5-u-ml-xs"
className="pf-v5-u-ml-sm"
/>
</div>
</LevelItem>
Expand All @@ -225,6 +296,94 @@ const TableActions: React.FC<PaginationProps & TableActionsProps> = ({
);
};

const DeletionAlerts: React.FC<DeletionAlertsProps> = ({
deleteResponse,
foldersPath,
}) => {
const { t } = useCustomTranslation();

const launcher = useModal();

const [errorResponse, setErrorResponse] = React.useState<
DeleteObjectsCommandOutput['Errors']
>([]);
const [successResponse, setSuccessResponse] = React.useState<
DeleteObjectsCommandOutput['Deleted']
>([]);

React.useEffect(() => {
setErrorResponse(deleteResponse?.deleteResponse?.Errors || []);
setSuccessResponse(deleteResponse?.deleteResponse?.Deleted || []);
}, [deleteResponse]);

const errorCount = errorResponse.length;
const successCount = successResponse.length;
return (
<>
{!!errorCount && (
<Alert
variant={AlertVariant.danger}
title={pluralize(
errorCount,
t(
'Failed to delete {{ errorCount }} object from the bucket. View deletion summary for details.',
{ errorCount }
),
t(
'Failed to delete {{ errorCount }} objects from the bucket. View deletion summary for details.',
{ errorCount }
),
false
)}
isInline
className="pf-v5-u-mb-sm pf-v5-u-mt-lg"
actionClose={
<AlertActionCloseButton onClose={() => setErrorResponse([])} />
}
actionLinks={
<AlertActionLink
onClick={() =>
launcher(LazyDeleteObjectsSummary, {
isOpen: true,
extraProps: {
foldersPath,
errorResponse,
selectedObjects: deleteResponse.selectedObjects,
},
})
}
>
{t('View failed objects')}
</AlertActionLink>
}
/>
)}
{!!successCount && (
<Alert
variant={AlertVariant.success}
title={pluralize(
successCount,
t(
'Successfully deleted {{ successCount }} object from the bucket.',
{ successCount }
),
t(
'Successfully deleted {{ successCount }} objects from the bucket.',
{ successCount }
),
false
)}
isInline
className="pf-v5-u-mb-lg pf-v5-u-mt-sm"
actionClose={
<AlertActionCloseButton onClose={() => setSuccessResponse([])} />
}
/>
)}
</>
);
};

export const ObjectsList: React.FC<{}> = () => {
const { t } = useCustomTranslation();

Expand Down Expand Up @@ -259,6 +418,11 @@ export const ObjectsList: React.FC<{}> = () => {
next: '',
});
const [selectedRows, setSelectedRows] = React.useState<ObjectCrFormat[]>([]);
const [deleteResponse, setDeleteResponse] =
React.useState<ObjectsDeleteResponse>({
selectedObjects: [] as ObjectCrFormat[],
deleteResponse: {} as DeleteObjectsCommandOutput,
});

const structuredObjects: ObjectCrFormat[] = React.useMemo(() => {
const objects: ObjectCrFormat[] = [];
Expand All @@ -277,16 +441,29 @@ export const ObjectsList: React.FC<{}> = () => {
return objects;
}, [data, loadedWOError, t]);

const refreshTokens = () =>
continuationTokensRefresher(
setContinuationTokens,
trigger,
setSelectedRows
);

// initial fetch on first mount or on route update (drilling in/out of the folder view)
React.useEffect(() => {
fetchObjects(setContinuationTokens, trigger, true, setSelectedRows);
refreshTokens();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [foldersPath]);

return (
<div className="pf-v5-u-m-lg">
<p className="pf-v5-u-mb-sm">
{t('Objects are the fundamental entities stored in buckets.')}
</p>
<DeletionAlerts
deleteResponse={deleteResponse}
foldersPath={foldersPath}
/>
{/* ToDo: add upload objects option */}

<TableActions
onNext={async () => {
if (!!continuationTokens.next && loadedWOError)
Expand Down Expand Up @@ -321,8 +498,9 @@ export const ObjectsList: React.FC<{}> = () => {
foldersPath={foldersPath}
bucketName={bucketName}
noobaaS3={noobaaS3}
setDeleteResponse={setDeleteResponse}
refreshTokens={refreshTokens}
/>

<SelectableTable
className="pf-v5-u-mt-lg"
columns={getColumns(t)}
Expand All @@ -333,7 +511,14 @@ export const ObjectsList: React.FC<{}> = () => {
loaded={!isMutating}
loadError={error}
isRowSelectable={isRowSelectable}
extraProps={{ launcher, bucketName, foldersPath, noobaaS3 }}
extraProps={{
launcher,
bucketName,
foldersPath,
noobaaS3,
setDeleteResponse,
refreshTokens,
}}
emptyRowMessage={EmptyPage}
/>
</div>
Expand Down
Loading

0 comments on commit a768cd0

Please sign in to comment.