From a768cd01d9ed919e5cc7d31ebec04b804d7a83a5 Mon Sep 17 00:00:00 2001 From: SanjalKatiyar Date: Mon, 7 Oct 2024 19:45:56 +0530 Subject: [PATCH] Add object actions - delete objects --- locales/en/plugin__odf-console.json | 15 ++ .../s3-browser/objects-list/ObjectsList.tsx | 209 ++++++++++++++- .../objects-list/table-components.tsx | 40 ++- .../delete-objects/DeleteObjectsModal.tsx | 242 ++++++++++++++++++ .../delete-objects/DeleteObjectsSummary.tsx | 177 +++++++++++++ .../delete-objects/LazyDeleteModals.ts | 9 + .../delete-objects/delete-objects.scss | 8 + packages/odf/utils/s3-browser.ts | 9 + packages/shared/src/list-page/ListFilter.tsx | 58 +++++ packages/shared/src/list-page/index.ts | 1 + .../src/list-page/paginated-list-page.tsx | 40 ++- packages/shared/src/s3/commands.ts | 5 + packages/shared/src/s3/types.ts | 6 + packages/shared/src/status/icons.tsx | 11 + .../shared/src/table/composable-table.tsx | 8 +- 15 files changed, 804 insertions(+), 34 deletions(-) create mode 100644 packages/odf/modals/s3-browser/delete-objects/DeleteObjectsModal.tsx create mode 100644 packages/odf/modals/s3-browser/delete-objects/DeleteObjectsSummary.tsx create mode 100644 packages/odf/modals/s3-browser/delete-objects/LazyDeleteModals.ts create mode 100644 packages/odf/modals/s3-browser/delete-objects/delete-objects.scss create mode 100644 packages/shared/src/list-page/ListFilter.tsx diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json index 000cc179e..aa094ef12 100644 --- a/locales/en/plugin__odf-console.json +++ b/locales/en/plugin__odf-console.json @@ -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", @@ -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}} in the text input field.": "<0>To confirm deletion, type <1>{{delete}} in the text input field.", + "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", @@ -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}} in namespace <6>{{namespace}}?": "Are you sure you want to delete <2>{{resourceName}} in namespace <6>{{namespace}}?", "Are you sure you want to delete <2>{{resourceName}}?": "Are you sure you want to delete <2>{{resourceName}}?", diff --git a/packages/odf/components/s3-browser/objects-list/ObjectsList.tsx b/packages/odf/components/s3-browser/objects-list/ObjectsList.tsx index a5ece1134..2cf0b796a 100644 --- a/packages/odf/components/s3-browser/objects-list/ObjectsList.tsx +++ b/packages/odf/components/s3-browser/objects-list/ObjectsList.tsx @@ -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'; @@ -19,6 +21,10 @@ import { Level, LevelItem, MenuToggle, + Alert, + AlertVariant, + AlertActionCloseButton, + AlertActionLink, } from '@patternfly/react-core'; import { AngleLeftIcon, AngleRightIcon } from '@patternfly/react-icons'; import { @@ -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'; @@ -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; +}; + +type DeletionAlertsProps = { + deleteResponse: ObjectsDeleteResponse; + foldersPath: string; }; type ContinuationTokens = { @@ -70,6 +91,7 @@ type Trigger = TriggerWithOptionsArgs< string >; +// for navigating (next/previous) through objects list const continuationTokensSetter = ( setContinuationTokens: React.Dispatch< React.SetStateAction @@ -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 + >, + trigger: Trigger, + setSelectedRows: React.Dispatch> +) => { + 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 ): 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, + }, + }), }, ]; @@ -182,6 +242,8 @@ const TableActions: React.FC = ({ foldersPath, bucketName, noobaaS3, + setDeleteResponse, + refreshTokens, }) => { const { t } = useCustomTranslation(); @@ -193,7 +255,7 @@ const TableActions: React.FC = ({
@@ -225,6 +296,94 @@ const TableActions: React.FC = ({ ); }; +const DeletionAlerts: React.FC = ({ + 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 && ( + setErrorResponse([])} /> + } + actionLinks={ + + launcher(LazyDeleteObjectsSummary, { + isOpen: true, + extraProps: { + foldersPath, + errorResponse, + selectedObjects: deleteResponse.selectedObjects, + }, + }) + } + > + {t('View failed objects')} + + } + /> + )} + {!!successCount && ( + setSuccessResponse([])} /> + } + /> + )} + + ); +}; + export const ObjectsList: React.FC<{}> = () => { const { t } = useCustomTranslation(); @@ -259,6 +418,11 @@ export const ObjectsList: React.FC<{}> = () => { next: '', }); const [selectedRows, setSelectedRows] = React.useState([]); + const [deleteResponse, setDeleteResponse] = + React.useState({ + selectedObjects: [] as ObjectCrFormat[], + deleteResponse: {} as DeleteObjectsCommandOutput, + }); const structuredObjects: ObjectCrFormat[] = React.useMemo(() => { const objects: ObjectCrFormat[] = []; @@ -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 (
+

+ {t('Objects are the fundamental entities stored in buckets.')} +

+ {/* ToDo: add upload objects option */} - { if (!!continuationTokens.next && loadedWOError) @@ -321,8 +498,9 @@ export const ObjectsList: React.FC<{}> = () => { foldersPath={foldersPath} bucketName={bucketName} noobaaS3={noobaaS3} + setDeleteResponse={setDeleteResponse} + refreshTokens={refreshTokens} /> - = () => { loaded={!isMutating} loadError={error} isRowSelectable={isRowSelectable} - extraProps={{ launcher, bucketName, foldersPath, noobaaS3 }} + extraProps={{ + launcher, + bucketName, + foldersPath, + noobaaS3, + setDeleteResponse, + refreshTokens, + }} emptyRowMessage={EmptyPage} />
diff --git a/packages/odf/components/s3-browser/objects-list/table-components.tsx b/packages/odf/components/s3-browser/objects-list/table-components.tsx index 6d898a505..439fe215b 100644 --- a/packages/odf/components/s3-browser/objects-list/table-components.tsx +++ b/packages/odf/components/s3-browser/objects-list/table-components.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { S3Commands } from '@odf/shared/s3'; -import { getName } from '@odf/shared/selectors'; import { RowComponentType } from '@odf/shared/table'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; import { sortRows } from '@odf/shared/utils'; @@ -18,8 +17,10 @@ import { import { CubesIcon } from '@patternfly/react-icons'; import { ActionsColumn, Td, IAction } from '@patternfly/react-table'; import { BUCKETS_BASE_ROUTE, PREFIX } from '../../../constants'; +import { SetObjectsDeleteResponse } from '../../../modals/s3-browser/delete-objects/DeleteObjectsModal'; +import { LazyDeleteObjectsModal } from '../../../modals/s3-browser/delete-objects/LazyDeleteModals'; import { ObjectCrFormat } from '../../../types'; -import { getEncodedPrefix } from '../../../utils'; +import { getEncodedPrefix, replacePathFromName } from '../../../utils'; import { DownloadAndPreviewState, onDownload, @@ -47,9 +48,11 @@ const getInlineActionsItems = ( downloadAndPreview: DownloadAndPreviewState, setDownloadAndPreview: React.Dispatch< React.SetStateAction - > + >, + foldersPath: string, + setDeleteResponse: SetObjectsDeleteResponse, + refreshTokens: () => Promise ): IAction[] => [ - // ToDo: add inline delete option { title: t('Download'), onClick: () => @@ -72,7 +75,18 @@ const getInlineActionsItems = ( }, { title: t('Delete'), - onClick: () => undefined, + onClick: () => + launcher(LazyDeleteObjectsModal, { + isOpen: true, + extraProps: { + foldersPath, + bucketName, + objects: [object], + noobaaS3, + setDeleteResponse, + refreshTokens, + }, + }), }, ]; @@ -114,9 +128,16 @@ export const TableRow: React.FC> = ({ isPreviewing: false, }); - const { launcher, bucketName, foldersPath, noobaaS3 } = extraProps; + const { + launcher, + bucketName, + foldersPath, + noobaaS3, + setDeleteResponse, + refreshTokens, + } = extraProps; const isFolder = object.isFolder; - const name = getName(object).replace(foldersPath, ''); + const name = replacePathFromName(object, foldersPath); const prefix = getEncodedPrefix(name, foldersPath); const columnNames = getColumnNames(t); @@ -152,7 +173,10 @@ export const TableRow: React.FC> = ({ object, noobaaS3, downloadAndPreview, - setDownloadAndPreview + setDownloadAndPreview, + foldersPath, + setDeleteResponse, + refreshTokens )} /> )} diff --git a/packages/odf/modals/s3-browser/delete-objects/DeleteObjectsModal.tsx b/packages/odf/modals/s3-browser/delete-objects/DeleteObjectsModal.tsx new file mode 100644 index 000000000..5202b331a --- /dev/null +++ b/packages/odf/modals/s3-browser/delete-objects/DeleteObjectsModal.tsx @@ -0,0 +1,242 @@ +import * as React from 'react'; +import { + DeleteObjectsCommandOutput, + ObjectIdentifier, +} from '@aws-sdk/client-s3'; +import { ButtonBar } from '@odf/shared/generic/ButtonBar'; +import { PaginatedListPage, ListFilter } from '@odf/shared/list-page'; +import { CommonModalProps } from '@odf/shared/modals'; +import { S3Commands } from '@odf/shared/s3'; +import { getName } from '@odf/shared/selectors'; +import { RowComponentType } from '@odf/shared/table'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { sortRows } from '@odf/shared/utils'; +import { TFunction } from 'i18next'; +import { Trans } from 'react-i18next'; +import { + Modal, + ModalVariant, + Button, + ButtonVariant, + TextInput, + TextInputTypes, + FormGroup, + PaginationVariant, +} from '@patternfly/react-core'; +import { Tr, Td, TableVariant } from '@patternfly/react-table'; +import { ObjectCrFormat } from '../../../types'; +import { replacePathFromName } from '../../../utils'; +import './delete-objects.scss'; + +const DELETE = 'delete'; + +export type ObjectsDeleteResponse = { + selectedObjects: ObjectCrFormat[]; + deleteResponse: DeleteObjectsCommandOutput; +}; + +export type SetObjectsDeleteResponse = React.Dispatch< + React.SetStateAction +>; + +type DeleteObjectsModalProps = { + foldersPath: string; + bucketName: string; + noobaaS3: S3Commands; + objects: ObjectCrFormat[]; + setDeleteResponse: SetObjectsDeleteResponse; + refreshTokens: () => Promise; +}; + +const getTextInputLabel = (t: TFunction) => ( + + + To confirm deletion, type {{ delete: DELETE }} in the text input + field. + + +); + +const getColumnNames = (t: TFunction) => [ + t('Object name'), + t('Size'), + t('Last modified'), +]; + +const getHeaderColumns = (t: TFunction) => { + const columnNames = getColumnNames(t); + return [ + { + columnName: columnNames[0], + sortFunction: (a, b, c) => sortRows(a, b, c, 'metadata.name'), + }, + { + columnName: columnNames[1], + sortFunction: (a, b, c) => sortRows(a, b, c, 'apiResponse.size'), + }, + { + columnName: columnNames[2], + sortFunction: (a, b, c) => sortRows(a, b, c, 'apiResponse.lastModified'), + }, + ]; +}; + +const DeleteObjectsTableRow: React.FC> = ({ + row: object, + extraProps, +}) => { + const { t } = useCustomTranslation(); + + const { foldersPath } = extraProps; + const name = replacePathFromName(object, foldersPath); + + const columnNames = getColumnNames(t); + + return ( + + + {name} + + + {object.apiResponse.size} + + + {object.apiResponse.lastModified} + + + ); +}; + +const DeleteObjectsModal: React.FC> = + ({ + closeModal, + isOpen, + extraProps: { + foldersPath, + bucketName, + noobaaS3, + objects: data, + setDeleteResponse, + refreshTokens, + }, + }) => { + const { t } = useCustomTranslation(); + + const [deleteText, setDeleteText] = React.useState(''); + const [inProgress, setInProgress] = React.useState(false); + const [error, setError] = React.useState(); + + const onDelete = async (event) => { + event.preventDefault(); + setInProgress(true); + + try { + const deleteObjectKeys: ObjectIdentifier[] = data.map((object) => ({ + Key: getName(object), + })); + const response = await noobaaS3.deleteObjects({ + Bucket: bucketName, + Delete: { Objects: deleteObjectKeys }, + }); + + setInProgress(false); + setDeleteResponse({ + selectedObjects: data, + deleteResponse: response || ({} as DeleteObjectsCommandOutput), + }); + closeModal(); + // need new continuation tokens after state of bucket has changed (objects deleted) + refreshTokens(); + } catch (err) { + setInProgress(false); + setError(err); + } + }; + + return ( + + {t( + '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.' + )} + + } + variant={ModalVariant.medium} + actions={[ + + + + + + , + ]} + > +
+ + {(filteredData): React.ReactNode => ( + + )} + +
+ + setDeleteText(value)} + type={TextInputTypes.text} + placeholder={DELETE} + /> + +
+ ); + }; + +export default DeleteObjectsModal; diff --git a/packages/odf/modals/s3-browser/delete-objects/DeleteObjectsSummary.tsx b/packages/odf/modals/s3-browser/delete-objects/DeleteObjectsSummary.tsx new file mode 100644 index 000000000..8bbf880c8 --- /dev/null +++ b/packages/odf/modals/s3-browser/delete-objects/DeleteObjectsSummary.tsx @@ -0,0 +1,177 @@ +import * as React from 'react'; +import { DeleteObjectsCommandOutput, _Error } from '@aws-sdk/client-s3'; +import { DASH } from '@odf/shared'; +import { PaginatedListPage } from '@odf/shared/list-page'; +import { CommonModalProps } from '@odf/shared/modals'; +import { getName } from '@odf/shared/selectors'; +import { + RedExclamationCircleIcon, + RedExclamationTriangleIcon, +} from '@odf/shared/status/icons'; +import { RowComponentType } from '@odf/shared/table'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { sortRows } from '@odf/shared/utils'; +import { TFunction } from 'i18next'; +import { + Modal, + ModalVariant, + Button, + ButtonVariant, + PaginationVariant, +} from '@patternfly/react-core'; +import { Tr, Td, TableVariant } from '@patternfly/react-table'; +import { ObjectCrFormat } from '../../../types'; +import { replacePathFromName } from '../../../utils'; +import './delete-objects.scss'; + +type DeleteObjectsSummaryProps = { + errorResponse: DeleteObjectsCommandOutput['Errors']; + selectedObjects: ObjectCrFormat[]; + foldersPath: string; +}; + +type DeleteObjectsMap = { [objectName: string]: ObjectCrFormat }; + +type SummaryRowComponent = React.ComponentType>; + +const getColumnNames = (t: TFunction) => [ + '', // expandable + t('Name'), + t('Size'), + t('Last modified'), + t('Delete status'), +]; + +const getHeaderColumns = (t: TFunction) => { + const columnNames = getColumnNames(t); + return [ + { + columnName: columnNames[0], + }, + { + columnName: columnNames[1], + sortFunction: (a, b, c) => sortRows(a, b, c, 'metadata.name'), + }, + { + columnName: columnNames[2], + sortFunction: (a, b, c) => sortRows(a, b, c, 'apiResponse.size'), + }, + { + columnName: columnNames[3], + sortFunction: (a, b, c) => sortRows(a, b, c, 'apiResponse.lastModified'), + }, + { + columnName: columnNames[4], + }, + ]; +}; + +const ObjectsSummaryTableRow: React.FC> = ({ + row: object, + rowIndex, + extraProps, +}) => { + const { t } = useCustomTranslation(); + + const [isExpanded, setIsExpanded] = React.useState(false); + + const { foldersPath, deleteObjectsMap } = extraProps; + const objectName = object?.Key || DASH; + const objectCrFormat = deleteObjectsMap[objectName] || {}; + const name = replacePathFromName(objectName, foldersPath); + + const columnNames = getColumnNames(t); + + return ( + <> + + setIsExpanded(!isExpanded), + expandId: 'expandable-table', + }} + /> + + {name} + + + {objectCrFormat.apiResponse?.size || DASH} + + + {objectCrFormat.apiResponse?.lastModified || DASH} + + + + {t('Failed')} + + + {isExpanded && ( + + + + {object?.Message || DASH} + + + )} + + ); +}; + +const DeleteObjectsSummary: React.FC< + CommonModalProps +> = ({ + closeModal, + isOpen, + extraProps: { errorResponse, selectedObjects, foldersPath }, +}) => { + const { t } = useCustomTranslation(); + + const deleteObjectsMap: DeleteObjectsMap = React.useMemo( + () => + selectedObjects.reduce((acc, object) => { + acc[getName(object)] = object; + return acc; + }, {} as DeleteObjectsMap), + [selectedObjects] + ); + + return ( + + {t('Close')} + , + ]} + > +
+ +
+
+ ); +}; + +export default DeleteObjectsSummary; diff --git a/packages/odf/modals/s3-browser/delete-objects/LazyDeleteModals.ts b/packages/odf/modals/s3-browser/delete-objects/LazyDeleteModals.ts new file mode 100644 index 000000000..0326d6a50 --- /dev/null +++ b/packages/odf/modals/s3-browser/delete-objects/LazyDeleteModals.ts @@ -0,0 +1,9 @@ +import * as React from 'react'; + +export const LazyDeleteObjectsModal = React.lazy( + () => import('./DeleteObjectsModal') +); + +export const LazyDeleteObjectsSummary = React.lazy( + () => import('./DeleteObjectsSummary') +); diff --git a/packages/odf/modals/s3-browser/delete-objects/delete-objects.scss b/packages/odf/modals/s3-browser/delete-objects/delete-objects.scss new file mode 100644 index 000000000..f45e2db90 --- /dev/null +++ b/packages/odf/modals/s3-browser/delete-objects/delete-objects.scss @@ -0,0 +1,8 @@ +.objects-table { + max-height: 25rem; + overflow-y: auto; +} + +.objects-table-paginate--margin-top { + margin-top: calc(2.25rem * -1); +} diff --git a/packages/odf/utils/s3-browser.ts b/packages/odf/utils/s3-browser.ts index 5f9ac5636..a578e1499 100644 --- a/packages/odf/utils/s3-browser.ts +++ b/packages/odf/utils/s3-browser.ts @@ -1,5 +1,6 @@ import { _Object as Content, CommonPrefix } from '@aws-sdk/client-s3'; import { DASH } from '@odf/shared/constants'; +import { getName } from '@odf/shared/selectors'; import { humanizeBinaryBytes } from '@odf/shared/utils'; import { TFunction } from 'i18next'; import { DELIMITER, BUCKETS_BASE_ROUTE, PREFIX } from '../constants'; @@ -91,3 +92,11 @@ export const convertObjectsDataToCrFormat = ( return structuredObjects; }; + +export const replacePathFromName = ( + object: ObjectCrFormat | string, + foldersPath: string +): string => + typeof object === 'string' + ? object.replace(foldersPath, '') + : getName(object).replace(foldersPath, ''); diff --git a/packages/shared/src/list-page/ListFilter.tsx b/packages/shared/src/list-page/ListFilter.tsx new file mode 100644 index 000000000..f5fae6a9c --- /dev/null +++ b/packages/shared/src/list-page/ListFilter.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { getName } from '@odf/shared/selectors'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; +import * as _ from 'lodash-es'; +import { SearchInput, SearchInputProps } from '@patternfly/react-core'; + +type ListFilterProps = { + data: K8sResourceCommon[]; + loaded: boolean; + dataFilter?: (resource: K8sResourceCommon) => boolean; + textInputProps?: Omit; + children: (filteredData: K8sResourceCommon[]) => React.ReactNode; +}; + +export const ListFilter: React.FC = ({ + data, + loaded, + dataFilter, + textInputProps, + children, +}) => { + const { t } = useCustomTranslation(); + const [input, setInput] = React.useState(''); + + const filteredData = React.useMemo(() => { + if (!input) return data; + const resourceFilter = + dataFilter || + ((resource: K8sResourceCommon): boolean => + _.toLower(getName(resource)).includes(_.toLower(input))); + return (data ?? []).filter(resourceFilter); + }, [input, data, dataFilter]); + + const onChange = ( + inputValue: string | React.FormEvent + ): void => + setInput( + typeof inputValue === 'string' + ? inputValue + : (inputValue.target as HTMLInputElement)?.value + ); + + return ( + <> + {loaded && !_.isEmpty(data) && ( + onChange('')} + /> + )} + {children(filteredData)} + + ); +}; diff --git a/packages/shared/src/list-page/index.ts b/packages/shared/src/list-page/index.ts index d8aa61dda..64827503c 100644 --- a/packages/shared/src/list-page/index.ts +++ b/packages/shared/src/list-page/index.ts @@ -1 +1,2 @@ export * from './paginated-list-page'; +export * from './ListFilter'; diff --git a/packages/shared/src/list-page/paginated-list-page.tsx b/packages/shared/src/list-page/paginated-list-page.tsx index 19eca62d8..8b84aa665 100644 --- a/packages/shared/src/list-page/paginated-list-page.tsx +++ b/packages/shared/src/list-page/paginated-list-page.tsx @@ -8,6 +8,7 @@ import { import { Pagination, PaginationVariant, + PaginationProps, Grid, GridItem, } from '@patternfly/react-core'; @@ -19,12 +20,22 @@ const COUNT_PER_PAGE_NUMBER = 10; export type PaginatedListPageProps = { countPerPage?: number; - filteredData: K8sResourceCommon[]; - CreateButton: React.FC; + filteredData: K8sResourceCommon[] | unknown[]; + CreateButton?: React.FC; Alerts?: React.FC; noData?: boolean; - listPageFilterProps: ListPageFilterProps; + hideFilter?: boolean; + listPageFilterProps?: ListPageFilterProps; composableTableProps: Omit, 'rows'>; + paginationProps?: Omit< + PaginationProps, + | 'itemCount' + | 'widgetId' + | 'perPage' + | 'page' + | 'onSetPage' + | 'onPerPageSelect' + >; }; export const PaginatedListPage: React.FC = ({ @@ -33,8 +44,10 @@ export const PaginatedListPage: React.FC = ({ CreateButton, Alerts, noData, + hideFilter, listPageFilterProps, composableTableProps, + paginationProps, }) => { const [page, setPage] = React.useState(INITIAL_PAGE_NUMBER); const [perPage, setPerPage] = React.useState( @@ -53,24 +66,27 @@ export const PaginatedListPage: React.FC = ({
- - + {!hideFilter && ( + + )} + {!!CreateButton && }
setPage(newPage)} onPerPageSelect={(_event, newPerPage, newPage) => { setPerPage(newPerPage); diff --git a/packages/shared/src/s3/commands.ts b/packages/shared/src/s3/commands.ts index 17461d097..9d0330393 100644 --- a/packages/shared/src/s3/commands.ts +++ b/packages/shared/src/s3/commands.ts @@ -5,6 +5,7 @@ import { CreateBucketCommand, PutBucketTaggingCommand, GetObjectCommand, + DeleteObjectsCommand, } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { @@ -14,6 +15,7 @@ import { PutBucketTags, GetSignedUrl, GetObject, + DeleteObjects, } from './types'; export class S3Commands { @@ -51,4 +53,7 @@ export class S3Commands { getObject: GetObject = (input) => this.s3Client.send(new GetObjectCommand(input)); + + deleteObjects: DeleteObjects = (input) => + this.s3Client.send(new DeleteObjectsCommand(input)); } diff --git a/packages/shared/src/s3/types.ts b/packages/shared/src/s3/types.ts index 1a83fe614..025d06914 100644 --- a/packages/shared/src/s3/types.ts +++ b/packages/shared/src/s3/types.ts @@ -9,6 +9,8 @@ import { PutBucketTaggingCommandOutput, GetObjectCommandInput, GetObjectCommandOutput, + DeleteObjectsCommandInput, + DeleteObjectsCommandOutput, } from '@aws-sdk/client-s3'; // Bucket command types @@ -37,3 +39,7 @@ export type GetSignedUrl = ( export type GetObject = ( input: GetObjectCommandInput ) => Promise; + +export type DeleteObjects = ( + input: DeleteObjectsCommandInput +) => Promise; diff --git a/packages/shared/src/status/icons.tsx b/packages/shared/src/status/icons.tsx index a1c0dc04c..8d05686b7 100644 --- a/packages/shared/src/status/icons.tsx +++ b/packages/shared/src/status/icons.tsx @@ -46,6 +46,17 @@ export const RedExclamationCircleIcon: React.FC = ({ /> ); +export const RedExclamationTriangleIcon: React.FC = ({ + className, + title, +}) => ( + +); + export const YellowExclamationTriangleIcon: React.FC = ({ className, title, diff --git a/packages/shared/src/table/composable-table.tsx b/packages/shared/src/table/composable-table.tsx index ca07463b9..83f9b2a92 100644 --- a/packages/shared/src/table/composable-table.tsx +++ b/packages/shared/src/table/composable-table.tsx @@ -4,6 +4,7 @@ import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; import { SortByDirection, Table, + TableVariant, Tbody, Th, ThProps, @@ -18,7 +19,7 @@ export type TableColumnProps = ThProps & { sortFunction?: (a: T, b: T, c: SortByDirection) => number; }; -export type RowComponentType = { +export type RowComponentType = { row: T; rowIndex?: number; extraProps?: any; @@ -36,6 +37,7 @@ export const ComposableTable: ComposableTableProps = < unfilteredData, noDataMsg, emptyRowMessage, + variant, }) => { const { onSort, @@ -67,6 +69,7 @@ export const ComposableTable: ComposableTableProps = < translate={null} aria-label="Composable table" className="pf-v5-u-mt-md" + variant={variant} > @@ -100,7 +103,7 @@ export const ComposableTable: ComposableTableProps = < // sort is replaced by sortFunction type TableThProps = Omit; -export type TableProps = { +export type TableProps = { rows: T[]; columns: TableColumnProps[]; RowComponent: React.ComponentType>; @@ -110,6 +113,7 @@ export type TableProps = { unfilteredData?: []; noDataMsg?: React.FC; emptyRowMessage?: React.FC; + variant?: TableVariant; }; type ComposableTableProps = (