From 21c4758e962ae4e07b7940cc93c5e769443cb6df Mon Sep 17 00:00:00 2001 From: Yi-Ya Chen Date: Mon, 23 Dec 2024 13:58:44 +0800 Subject: [PATCH] feat: bulk backup action Signed-off-by: Yi-Ya Chen --- src/models/backingImage.js | 2 +- .../backingImage/BackingImageBulkActions.js | 276 +++++++++++------- .../backingImage/BackingImageBulkActions.less | 41 +++ src/routes/backingImage/index.js | 6 +- 4 files changed, 211 insertions(+), 114 deletions(-) create mode 100644 src/routes/backingImage/BackingImageBulkActions.less diff --git a/src/models/backingImage.js b/src/models/backingImage.js index ff5a5e2b1..8e5e0f994 100644 --- a/src/models/backingImage.js +++ b/src/models/backingImage.js @@ -212,7 +212,7 @@ export default { for (const bi of payload) { const url = bi.actions.backupBackingImageCreate if (url) { - yield call(execAction, url) + yield call(execAction, url, bi) } } } diff --git a/src/routes/backingImage/BackingImageBulkActions.js b/src/routes/backingImage/BackingImageBulkActions.js index d6837de1f..17428370b 100644 --- a/src/routes/backingImage/BackingImageBulkActions.js +++ b/src/routes/backingImage/BackingImageBulkActions.js @@ -1,139 +1,193 @@ -import React from 'react' +import React, { useState } from 'react' import PropTypes from 'prop-types' -import { Button, Modal } from 'antd' +import { Form, Button, Modal, Select, Icon } from 'antd' import { hasReadyBackingDisk, diskStatusColorMap } from '../../utils/status' +import styles from './BackingImageBulkActions.less' -const confirm = Modal.confirm +function BulkActions({ + selectedRows, + backupProps, + deleteBackingImages, + downloadSelectedBackingImages, + backupSelectedBackingImages, + backupTargets = [], + form = {}, +}) { + const [isBackupModalVisible, setIsBackupModalVisible] = useState(false) - -function bulkActions({ selectedRows, backupProps, deleteBackingImages, downloadSelectedBackingImages, backupSelectedBackingImages }) { + const { getFieldDecorator, getFieldValue } = form const { backupTargetAvailable } = backupProps + const readyColor = diskStatusColorMap.ready + const readyTextStyle = { + display: 'inline-block', + width: 'max-content', + padding: '0 4px', + color: '#27AE5F', + border: `1px solid ${readyColor.color}`, + backgroundColor: readyColor.bg, + textTransform: 'capitalize', + } + const getImageLabel = selectedRows.length === 1 ? 'image' : 'images' + const readyImages = selectedRows.filter((row) => hasReadyBackingDisk(row)) + + const handleDelete = () => { + const title = ( + <> +

+ Are you sure you want to delete the following {selectedRows.length} backing {getImageLabel}? +

+ + + ) + Modal.confirm({ + width: 'fit-content', + okText: 'Delete', + okType: 'danger', + title, + content: null, + onOk: () => deleteBackingImages(selectedRows), + }) + } + + const handleDownload = () => { + const downloadableImages = selectedRows.filter((row) => hasReadyBackingDisk(row)) + + const title = ( + <> +

+ The following backing {getImageLabel} with Ready status disk will be downloaded. +

+ +

+ Note: You need to allow Automatic Downloads in browser settings to download multiple files at once. +

+ + ) + Modal.confirm({ + width: 'fit-content', + okText: 'Download', + title, + content: null, + onOk: () => downloadSelectedBackingImages(downloadableImages), + }) + } + + const handleBackup = () => { + setIsBackupModalVisible(true) + } + + const handleBackupOk = () => { + const backupTarget = backupTargets.find((bkTarget) => bkTarget.name === getFieldValue('backupTargetName')) + const backupImages = readyImages.map((backingImage) => ({ + ...backingImage, + backingImageName: backingImage.name, + backupTargetName: backupTarget.name, + backupTargetURL: backupTarget.backupTargetURL, + })) + + backupSelectedBackingImages(backupImages) + setIsBackupModalVisible(false) + } - const handleClick = (action) => { - const count = selectedRows.length - switch (action) { - case 'delete': - confirm({ - width: 'fit-content', - okText: 'Delete', - okType: 'danger', - title: (<> -

Are you sure to delete below {count} backing {count === 1 ? 'image' : 'images' } ?

- - ), - onOk() { - deleteBackingImages(selectedRows) - }, - }) - break - case 'download': { - const downloadableImages = selectedRows.filter(row => hasReadyBackingDisk(row)) - const readyColor = diskStatusColorMap.ready - const readyTextStyle = { - display: 'inline-block', - width: 'max-content', - padding: '0 4px', - marginRight: '5px', - color: '#27AE5F', - border: `1px solid ${readyColor.color}`, - backgroundColor: readyColor.bg, - textTransform: 'capitalize', - } - confirm({ - okText: 'Download', - width: 'fit-content', - title: (<> -

Below backing {count === 1 ? 'image' : 'images' } with Ready status disk will be downloaded

- -

Note. You need allow Automatic Downloads permission
in browser settings to download multiple files at once.

- ), - onOk() { - downloadSelectedBackingImages(downloadableImages) - }, - }) - break - } - case 'backup': { - const backupImages = selectedRows.filter(row => hasReadyBackingDisk(row)) - const readyColor = diskStatusColorMap.ready - const readyTextStyle = { - display: 'inline-block', - width: 'max-content', - padding: '0 4px', - marginRight: '5px', - color: '#27AE5F', - border: `1px solid ${readyColor.color}`, - backgroundColor: readyColor.bg, - textTransform: 'capitalize', - } - confirm({ - okText: 'Backup', - width: 'fit-content', - title: (<> -

Are you sure to backup below Ready status backing {count === 1 ? 'image' : 'images'} ?

- - ), - onOk() { - backupSelectedBackingImages(backupImages) - }, - }) - break - } - default: - } + const handleBackupCancel = () => { + setIsBackupModalVisible(false) } const allActions = [ { key: 'delete', name: 'Delete', - disabled() { return selectedRows.length === 0 } + disabled: () => selectedRows.length === 0, + onClick: handleDelete, }, { key: 'download', name: 'Download', - disabled() { - return selectedRows.length === 0 - || selectedRows.every(row => !hasReadyBackingDisk(row)) - || selectedRows.some(row => row.dataEngine === 'v2') - } + disabled: () => selectedRows.length === 0 + || readyImages.length === 0 + || selectedRows.some((row) => row.dataEngine === 'v2'), + onClick: handleDownload, }, { key: 'backup', - name: 'Back Up', - disabled() { - return selectedRows.length === 0 - || backupTargetAvailable === false - || selectedRows.every(row => !hasReadyBackingDisk(row)) - } + name: 'Backup', + disabled: () => selectedRows.length === 0 + || readyImages.length === 0 + || !backupTargetAvailable, + onClick: handleBackup, }, ] return ( -
- { allActions.map(item => { - return ( -
- -
- ) - }) } -
+ <> +
+ {allActions.map(({ key, name, disabled, onClick }) => ( + + ))} +
+ + +
+

+ Are you sure you want to backup the following Ready status backing {readyImages.length === 1 ? 'image' : 'images'}? +

+
    {readyImages.map((item) =>
  • {item.name}
  • )}
+ + {getFieldDecorator('backupTargetName', { + initialValue: backupTargets.find((bk) => bk.name === 'default')?.name || '', + })( + + )} + +
+
+ ) } -bulkActions.propTypes = { - selectedRows: PropTypes.array, - deleteBackingImages: PropTypes.func, - downloadSelectedBackingImages: PropTypes.func, - backupSelectedBackingImages: PropTypes.func, - backupProps: PropTypes.object, +BulkActions.propTypes = { + selectedRows: PropTypes.array.isRequired, + deleteBackingImages: PropTypes.func.isRequired, + downloadSelectedBackingImages: PropTypes.func.isRequired, + backupSelectedBackingImages: PropTypes.func.isRequired, + backupProps: PropTypes.object.isRequired, + backupTargets: PropTypes.array.isRequired, + form: PropTypes.object.isRequired, } -export default bulkActions +export default Form.create()(BulkActions) diff --git a/src/routes/backingImage/BackingImageBulkActions.less b/src/routes/backingImage/BackingImageBulkActions.less new file mode 100644 index 000000000..ef48dacd8 --- /dev/null +++ b/src/routes/backingImage/BackingImageBulkActions.less @@ -0,0 +1,41 @@ +.bulkActionBtns { + margin-right: 10px; +} + +.backupModal { + display: flex; + + .questionCircleIcon { + margin-right: 16px; + font-size: 22px; + color: #faad14; + } + + .backupForm { + color: #000000d9; + font-weight: 500; + font-size: 17px; + line-height: 1.4; + } + + :global { + .ant-modal-body { + display: flex; + padding: 32px 32px 0; + + .ant-form-item { + display: flex; + margin-bottom: 0; + } + + .ant-col.ant-form-item-control-wrapper { + flex: 1; + } + } + + .ant-modal-footer { + padding: 24px 32px; + border-top: 0; + } + } +} diff --git a/src/routes/backingImage/index.js b/src/routes/backingImage/index.js index 2afdf85a0..684665e9e 100644 --- a/src/routes/backingImage/index.js +++ b/src/routes/backingImage/index.js @@ -125,6 +125,7 @@ class BackingImage extends React.Component { const backingImages = filterBackingImage(data, biSearchField, biSearchValue) const volumeNameOptions = volumeData.map((volume) => volume.name) + const backupTargets = getBackupTargets(backupTarget) const backingImageListProps = { dataSource: backingImages, @@ -179,7 +180,7 @@ class BackingImage extends React.Component { const createBackupBackingImageModalProps = { backingImage: selectedBackingImage, - backupTargets: getBackupTargets(backupTarget), + backupTargets, visible: backupBackingImageModalVisible, onOk(url, payload) { dispatch({ @@ -327,6 +328,7 @@ class BackingImage extends React.Component { const backingImageBulkActionsProps = { selectedRows, backupProps: this.props.backup, + backupTargets, deleteBackingImages(record) { dispatch({ type: 'backingImage/bulkDelete', @@ -344,7 +346,7 @@ class BackingImage extends React.Component { type: 'backingImage/bulkBackup', payload: record, }) - }, + } } const minCopiesCountProps = {