Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support bulk backup backing images #836

Merged
merged 1 commit into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/models/backingImage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
276 changes: 165 additions & 111 deletions src/routes/backingImage/BackingImageBulkActions.js
Original file line number Diff line number Diff line change
@@ -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 = (
<>
<p>
Are you sure you want to delete the following {selectedRows.length} backing {getImageLabel}?
</p>
<ul>
{selectedRows.map((item) => (
<li key={item.name}>{item.name}</li>
))}
</ul>
</>
)
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 = (
<>
<p>
The following backing {getImageLabel} with <strong style={readyTextStyle}>Ready</strong> status disk will be downloaded.
</p>
<ul>
{downloadableImages.map((item) => (
<li key={item.name}>{item.name}</li>
))}
</ul>
<p>
Note: You need to allow <strong>Automatic Downloads</strong> in browser settings to download multiple files at once.
</p>
</>
)
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)
}

a110605 marked this conversation as resolved.
Show resolved Hide resolved
const handleClick = (action) => {
const count = selectedRows.length
switch (action) {
case 'delete':
confirm({
width: 'fit-content',
okText: 'Delete',
okType: 'danger',
title: (<>
<p>Are you sure to delete below {count} backing {count === 1 ? 'image' : 'images' } ?</p>
<ul>
{selectedRows.map(item => <li>{item.name}</li>)},
</ul>
</>),
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: (<>
<p>Below backing {count === 1 ? 'image' : 'images' } with <strong style={readyTextStyle}>Ready</strong> status disk will be downloaded</p>
<ul>
{downloadableImages.map(item => <li>{item.name}</li>)}
</ul>
<p>Note. You need allow <strong>Automatic Downloads</strong> permission<br />in browser settings to download multiple files at once.</p>
</>),
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: (<>
<p>Are you sure to backup below <strong style={readyTextStyle}>Ready</strong> status backing {count === 1 ? 'image' : 'images'} ?</p>
<ul>
{backupImages.map(item => <li key={item.name}>{item.name}</li>)}
</ul>
</>),
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 (
<div style={{ display: 'flex' }}>
{ allActions.map(item => {
return (
<div key={item.key} style={{ marginRight: '10px' }}>
<Button size="large" type="primary" disabled={item.disabled()} onClick={() => handleClick(item.key)}>{ item.name }</Button>
</div>
)
}) }
</div>
<>
<div style={{ display: 'flex' }}>
{allActions.map(({ key, name, disabled, onClick }) => (
<Button
key={key}
size="large"
type="primary"
disabled={disabled()}
onClick={onClick}
className={styles.bulkActionBtns}
>
{name}
</Button>
))}
</div>
<Modal
className={styles.backupModal}
a110605 marked this conversation as resolved.
Show resolved Hide resolved
closable={false}
width="fit-content"
okText="Backup"
visible={isBackupModalVisible}
onOk={handleBackupOk}
onCancel={handleBackupCancel}
>
<Icon className={styles.questionCircleIcon} type="question-circle" />
<Form className={styles.backupForm}>
<p>
Are you sure you want to backup the following <strong style={readyTextStyle}>Ready</strong> status backing {readyImages.length === 1 ? 'image' : 'images'}?
</p>
<ul>{readyImages.map((item) => <li key={item.name}>{item.name}</li>)}</ul>
<Form.Item label="Backup Target">
{getFieldDecorator('backupTargetName', {
initialValue: backupTargets.find((bk) => bk.name === 'default')?.name || '',
})(
<Select>
{backupTargets.map((bkTarget) => (
<Select.Option
key={bkTarget.name}
disabled={!bkTarget.available}
value={bkTarget.name}
>
{bkTarget.name}
</Select.Option>
))}
</Select>
)}
</Form.Item>
</Form>
</Modal>
</>
)
}

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)
41 changes: 41 additions & 0 deletions src/routes/backingImage/BackingImageBulkActions.less
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
6 changes: 4 additions & 2 deletions src/routes/backingImage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -179,7 +180,7 @@ class BackingImage extends React.Component {

const createBackupBackingImageModalProps = {
backingImage: selectedBackingImage,
backupTargets: getBackupTargets(backupTarget),
backupTargets,
visible: backupBackingImageModalVisible,
onOk(url, payload) {
dispatch({
Expand Down Expand Up @@ -327,6 +328,7 @@ class BackingImage extends React.Component {
const backingImageBulkActionsProps = {
selectedRows,
backupProps: this.props.backup,
backupTargets,
deleteBackingImages(record) {
dispatch({
type: 'backingImage/bulkDelete',
Expand All @@ -344,7 +346,7 @@ class BackingImage extends React.Component {
type: 'backingImage/bulkBackup',
payload: record,
})
},
}
}

const minCopiesCountProps = {
Expand Down
Loading