diff --git a/src/assets/images/read-only.svg b/src/assets/images/read-only.svg new file mode 100644 index 000000000..bd2b8206a --- /dev/null +++ b/src/assets/images/read-only.svg @@ -0,0 +1 @@ + diff --git a/src/models/backingImage.js b/src/models/backingImage.js index ec592d474..0bc64fdb4 100644 --- a/src/models/backingImage.js +++ b/src/models/backingImage.js @@ -1,4 +1,4 @@ -import { create, deleteBackingImage, query, deleteDisksOnBackingImage, uploadChunk, download, bulkDownload } from '../services/backingImage' +import { create, deleteBackingImage, query, execAction, deleteDisksOnBackingImage, uploadChunk, download, bulkDownload } from '../services/backingImage' import { message, notification } from 'antd' import { delay } from 'dva/saga' import { wsChanges, updateState } from '../utils/websocket' @@ -89,6 +89,16 @@ export default { payload.sourceType === 'upload' && notification.destroy() } }, + *createBackingImageBackup({ + url, + payload, + }, { call, put }) { + const resp = yield call(execAction, url, payload) + if (resp && resp.status === 200) { + message.success(`Successfully backup backing image ${payload.backingImageName}`, 5) + } + yield put({ type: 'query' }) + }, *delete({ payload, }, { call, put }) { diff --git a/src/routes/backingImage/BackingImageActions.js b/src/routes/backingImage/BackingImageActions.js index ef5194205..73a99c8d4 100644 --- a/src/routes/backingImage/BackingImageActions.js +++ b/src/routes/backingImage/BackingImageActions.js @@ -3,9 +3,10 @@ import PropTypes from 'prop-types' import { Modal } from 'antd' import { DropOption } from '../../components' import { hasReadyBackingDisk } from '../../utils/status' + const confirm = Modal.confirm -function actions({ selected, deleteBackingImage, downloadBackingImage }) { +function actions({ selected, deleteBackingImage, downloadBackingImage, openBackupBackingImageModal }) { const handleMenuClick = (event, record) => { event.domEvent?.stopPropagation?.() switch (event.key) { @@ -20,6 +21,17 @@ function actions({ selected, deleteBackingImage, downloadBackingImage }) { case 'download': downloadBackingImage(record) break + case 'backup': { + openBackupBackingImageModal(record) + // const data = { + // ...record, + // // backupTargetName: 'default', + // // backupTargetURL: 'nfs://longhorn-test-nfs-svc.default:/opt/backupstore', + // // backingImageName: record.name, + // } + // backupBackingImage(data) + break + } default: } } @@ -27,8 +39,9 @@ function actions({ selected, deleteBackingImage, downloadBackingImage }) { const disableDownloadAction = !hasReadyBackingDisk(selected) const availableActions = [ - { key: 'delete', name: 'Delete' }, { key: 'download', name: 'Download', disabled: disableDownloadAction, tooltip: disableDownloadAction ? 'Missing disk with ready state' : '' }, + { key: 'backup', name: 'Backup' }, + { key: 'delete', name: 'Delete' }, ] return ( @@ -42,6 +55,7 @@ actions.propTypes = { selected: PropTypes.object, deleteBackingImage: PropTypes.func, downloadBackingImage: PropTypes.func, + openBackupBackingImageModal: PropTypes.func, } export default actions diff --git a/src/routes/backingImage/BackingImageList.js b/src/routes/backingImage/BackingImageList.js index e2801307e..355f17672 100644 --- a/src/routes/backingImage/BackingImageList.js +++ b/src/routes/backingImage/BackingImageList.js @@ -5,10 +5,11 @@ import BackingImageActions from './BackingImageActions' import { pagination } from '../../utils/page' import { formatMib } from '../../utils/formatter' -function list({ loading, dataSource, deleteBackingImage, showDiskStateMapDetail, rowSelection, downloadBackingImage, height }) { +function list({ loading, dataSource, openBackupBackingImageModal, deleteBackingImage, showDiskStateMapDetail, rowSelection, downloadBackingImage, height }) { const backingImageActionsProps = { deleteBackingImage, downloadBackingImage, + openBackupBackingImageModal, } const state = (record) => { if (record.deletionTimestamp) { @@ -107,6 +108,7 @@ list.propTypes = { dataSource: PropTypes.array, deleteBackingImage: PropTypes.func, showDiskStateMapDetail: PropTypes.func, + openBackupBackingImageModal: PropTypes.func, rowSelection: PropTypes.object, height: PropTypes.number, } diff --git a/src/routes/backingImage/CreateBackupBackingImageModal.js b/src/routes/backingImage/CreateBackupBackingImageModal.js new file mode 100644 index 000000000..f83327e57 --- /dev/null +++ b/src/routes/backingImage/CreateBackupBackingImageModal.js @@ -0,0 +1,80 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Form, Select, Icon } from 'antd' +import { ModalBlur } from '../../components' + +const FormItem = Form.Item +const Option = Select.Option + +const formItemLayout = { + labelCol: { + span: 6, + }, + wrapperCol: { + span: 15, + }, +} + +const modal = ({ + backingImage, + availBackupTargets, + visible, + onCancel, + onOk, + form: { + getFieldDecorator, + getFieldValue, + }, +}) => { + function handleOk() { + const backupTarget = availBackupTargets.find(bkTarget => bkTarget.name === getFieldValue('backupTargetName')) + if (backupTarget) { + const url = backingImage.actions?.backupBackingImageCreate + const payload = { + ...backingImage, + backingImageName: backingImage.name, + backupTargetName: backupTarget.name, + backupTargetURL: backupTarget.backupTargetURL, + } + onOk(url, payload) + } + } + + const modalOpts = { + title: 'Create Backup Backing Image', + visible, + onCancel, + onOk: handleOk, + } + + return ( + +

+ Choose a backup target to backup {backingImage.name} backing image +

+
+ + {getFieldDecorator('backupTargetName', { + initialValue: availBackupTargets.length > 0 ? availBackupTargets[0].name : '', + })( + + )} + +
+
+ ) +} + +modal.propTypes = { + backingImage: PropTypes.object, + availBackupTargets: PropTypes.array, + form: PropTypes.object.isRequired, + visible: PropTypes.bool, + onCancel: PropTypes.func, + item: PropTypes.object, + onOk: PropTypes.func, +} + +export default Form.create()(modal) diff --git a/src/routes/backingImage/index.js b/src/routes/backingImage/index.js index d26fcd4e4..50544e6e6 100644 --- a/src/routes/backingImage/index.js +++ b/src/routes/backingImage/index.js @@ -6,9 +6,11 @@ import { Row, Col, Button, Progress, notification } from 'antd' import CreateBackingImage from './CreateBackingImage' import BackingImageList from './BackingImageList' import DiskStateMapDetail from './DiskStateMapDetail' +import CreateBackupBackingImageModal from './CreateBackupBackingImageModal' import { Filter } from '../../components/index' import BackingImageBulkActions from './BackingImageBulkActions' import queryString from 'query-string' +import { getAvailBackupTargets } from '../../utils/backupTarget' import style from './BackingImage.less' import C from '../../utils/constants' @@ -18,6 +20,8 @@ class BackingImage extends React.Component { this.state = { height: 300, message: null, + backupBackingImageModalVisible: false, + selectedBackingImage: {}, } } @@ -37,6 +41,22 @@ class BackingImage extends React.Component { }) } + handleBackupBackingImageModalOpen = (record) => { + this.setState({ + ...this.state, + backupBackingImageModalVisible: true, + selectedBackingImage: record, + }) + } + + handleBackupBackingImageModalClose = () => { + this.setState({ + ...this.state, + backupBackingImageModalVisible: false, + selectedBackingImage: {}, + }) + } + uploadFile = (file, record) => { let totalSize = file.size this.props.dispatch({ @@ -65,8 +85,9 @@ class BackingImage extends React.Component { } render() { - const { dispatch, loading, location } = this.props - const { uploadFile } = this + const { dispatch, loading, location, backupTarget } = this.props + const { uploadFile, handleBackupBackingImageModalOpen, handleBackupBackingImageModalClose } = this + const { backupBackingImageModalVisible, selectedBackingImage } = this.state const { data: volumeData } = this.props.volume const { data, selected, createBackingImageModalVisible, createBackingImageModalKey, diskStateMapDetailModalVisible, diskStateMapDetailModalKey, diskStateMapDeleteDisabled, diskStateMapDeleteLoading, selectedDiskStateMapRows, selectedDiskStateMapRowKeys, selectedRows } = this.props.backingImage const { backingImageUploadPercent, backingImageUploadStarted } = this.props.app @@ -103,6 +124,9 @@ class BackingImage extends React.Component { payload: record, }) }, + openBackupBackingImageModal: (record) => { + handleBackupBackingImageModalOpen(record) + }, downloadBackingImage(record) { dispatch({ type: 'backingImage/downloadBackingImage', @@ -128,6 +152,23 @@ class BackingImage extends React.Component { }, } + const createBackupBackingImageModalProps = { + backingImage: selectedBackingImage, + availBackupTargets: getAvailBackupTargets(backupTarget), + visible: backupBackingImageModalVisible, + onOk(url, payload) { + dispatch({ + type: 'backingImage/createBackingImageBackup', + url, + payload, + }) + handleBackupBackingImageModalClose() + }, + onCancel() { + handleBackupBackingImageModalClose() + }, + } + const addBackingImage = () => { dispatch({ type: 'backingImage/showCreateBackingImageModal', @@ -315,6 +356,7 @@ class BackingImage extends React.Component { { createBackingImageModalVisible ? : ''} { diskStateMapDetailModalVisible ? : ''} + ) } @@ -323,10 +365,11 @@ class BackingImage extends React.Component { BackingImage.propTypes = { app: PropTypes.object, backingImage: PropTypes.object, + backupTarget: PropTypes.object, loading: PropTypes.bool, location: PropTypes.object, volume: PropTypes.object, dispatch: PropTypes.func, } -export default connect(({ app, volume, backingImage, loading }) => ({ app, volume, backingImage, loading: loading.models.backingImage }))(BackingImage) +export default connect(({ app, volume, backupTarget, backingImage, loading }) => ({ app, volume, backupTarget, backingImage, loading: loading.models.backingImage }))(BackingImage) diff --git a/src/routes/backupTarget/BackupTargetList.js b/src/routes/backupTarget/BackupTargetList.js index 2463a7aa0..da16ca540 100644 --- a/src/routes/backupTarget/BackupTargetList.js +++ b/src/routes/backupTarget/BackupTargetList.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import { Table, Icon, Tooltip } from 'antd' import BackupTargetActions from './BackupTargetActions' import { pagination } from '../../utils/page' +import readOnly from '../../assets/images/read-only.svg' function list({ loading, dataSource, deleteBackupTarget, editBackupTarget, rowSelection, height }) { const columns = [ @@ -58,7 +59,17 @@ function list({ loading, dataSource, deleteBackupTarget, editBackupTarget, rowSe sorter: (a, b) => a.readOnly - b.readOnly, render: (text) => { return ( -
{text.toString().firstUpperCase()}
+ <> + {text === false ? ( + + + + ) : ( + + readOnlyIcon + ) + } + ) }, }, { @@ -69,7 +80,7 @@ function list({ loading, dataSource, deleteBackupTarget, editBackupTarget, rowSe sorter: (a, b) => a.default - b.default, render: (text) => { return ( -
{text.toString().firstUpperCase()}
+ <>{text === true ? () : ''} ) }, }, { @@ -81,10 +92,13 @@ function list({ loading, dataSource, deleteBackupTarget, editBackupTarget, rowSe render: (text) => { return (
-
{text.toString().firstUpperCase()}
- {text === false && ( + {text === true ? ( + + + + ) : ( - + )}
diff --git a/src/routes/backupTarget/index.js b/src/routes/backupTarget/index.js index 9ba1b2e80..dc72b2331 100644 --- a/src/routes/backupTarget/index.js +++ b/src/routes/backupTarget/index.js @@ -216,7 +216,7 @@ class BackupTarget extends React.Component { - + {createBackupTargetModalVisible && } {editBackupTargetModalVisible && } diff --git a/src/routes/recurringJob/CreateRecurringJob.js b/src/routes/recurringJob/CreateRecurringJob.js index a9e16e172..dd2e40dc4 100644 --- a/src/routes/recurringJob/CreateRecurringJob.js +++ b/src/routes/recurringJob/CreateRecurringJob.js @@ -131,9 +131,9 @@ const modal = ({ } const add = () => { const currentKeys = getFieldValue('keys') - const nextkeys = currentKeys.concat({ index: id++, initialValue: '' }) + const nextKeys = currentKeys.concat({ index: id++, initialValue: '' }) setFieldsValue({ - keys: nextkeys, + keys: nextKeys, }) } const addDefaultGroup = () => { @@ -141,9 +141,9 @@ const modal = ({ let currentId = groups ? groups.length - 1 : 0 if (getFieldValue('groups')[currentId]) { const currentKeys = getFieldValue('keys') - const nextkeys = currentKeys.concat({ index: id++, initialValue: 'default' }) + const nextKeys = currentKeys.concat({ index: id++, initialValue: 'default' }) setFieldsValue({ - keys: nextkeys, + keys: nextKeys, }) } else { groups[currentId] = 'default' @@ -175,7 +175,7 @@ const modal = ({ } const showBackupTargetDropdown = () => { - return getFieldValue('task') === 'backup' + return getFieldValue('task') === 'backup' || getFieldValue('task') === 'backup-force-create' } // init params @@ -220,9 +220,9 @@ const modal = ({ } const addLabel = () => { const currentKeys = getFieldValue('keysForlabels') - const nextkeys = currentKeys.concat(id++) + const nextKeys = currentKeys.concat(id++) setFieldsValue({ - keysForlabels: nextkeys, + keysForlabels: nextKeys, }) } const keysForlabels = getFieldValue('keysForlabels') @@ -319,7 +319,12 @@ const modal = ({ && {getFieldDecorator('backupTargetName', { // eslint-disable-next-line no-nested-ternary - initialValue: isEdit ? item.backupTarget : availBackupTargets.length > 0 ? availBackupTargets[0].name : '', + initialValue: isEdit ? item.backupTargetName : availBackupTargets.length > 0 ? availBackupTargets[0].name : '', + rules: [ + { + required: true, + }, + ], })(