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..69f94b50b 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 }) {
@@ -155,7 +165,6 @@ export default {
*startWS({
payload,
}, { select }) {
- // console.log('🚀 ~ backing images payload:', payload)
let ws = yield select(state => state.backingImage.ws)
if (ws) {
ws.open()
diff --git a/src/models/backup.js b/src/models/backup.js
index 7588c1fee..298b813df 100644
--- a/src/models/backup.js
+++ b/src/models/backup.js
@@ -350,7 +350,7 @@ export default {
if (volumeName && action.payload && action.payload.data) {
let backupData = action.payload.data.filter((item) => {
if (item.backupTargetName) {
- // after support multiple backup targets feature volumeName is composed by ${volumeName}-${backupTargetName}
+ // after implement multiple backup targets feature, backup volume name in backup page is composed by ${volumeName}-${backupTargetName}
return volumeName === `${item.volumeName}-${item.backupTargetName}`
} else {
return item.volumeName === volumeName
diff --git a/src/router.js b/src/router.js
index 234d8a206..5a4fbd527 100755
--- a/src/router.js
+++ b/src/router.js
@@ -120,7 +120,6 @@ const Routers = function ({ history, app }) {
- {/* */}
diff --git a/src/routes/backingImage/BackingImageActions.js b/src/routes/backingImage/BackingImageActions.js
index ef5194205..38fa6cbfd 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,10 @@ function actions({ selected, deleteBackingImage, downloadBackingImage }) {
case 'download':
downloadBackingImage(record)
break
+ case 'backup': {
+ openBackupBackingImageModal(record)
+ break
+ }
default:
}
}
@@ -27,8 +32,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 +48,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/BackupTarget.less b/src/routes/backupTarget/BackupTarget.less
deleted file mode 100644
index 5065c68f0..000000000
--- a/src/routes/backupTarget/BackupTarget.less
+++ /dev/null
@@ -1,49 +0,0 @@
-// .backupTargetModalContainer {
-// margin-bottom: 10px;
-// &>div {
-// margin-bottom: 10px;
-// }
-// div {
-// word-break: break-all;
-// }
-// .parametersContainer {
-// margin-bottom: 10px;
-// display: grid;
-// grid-template-columns: 36% 63%;
-// grid-row-gap: 15px;
-// div {
-// font-weight: 700;
-// }
-// span {
-// display: block;
-// }
-// .currentChecksum {
-// position: relative;
-// text-align: left;
-// summary {
-// position: absolute;
-// top: 24px;
-// left: 0px;
-// border: 1px solid #e1e4e8;
-// border-radius: 2em;
-// display: inline-block;
-// font-size: 12px;
-// font-weight: 500;
-// line-height: 22px;
-// padding: 0 7px;
-// }
-// }
-// }
-// }
-
-// .backupTargetUploadingContainer {
-// position: absolute;
-// top: 45%;
-// left: 0;
-// right: 0;
-// bottom: 0;
-// margin: auto;
-// width: 25%;
-// height: 50;
-// z-index: 9999;
-// }
diff --git a/src/routes/backupTarget/BackupTargetActions.js b/src/routes/backupTarget/BackupTargetActions.js
index a3aed9f9b..f25680f40 100644
--- a/src/routes/backupTarget/BackupTargetActions.js
+++ b/src/routes/backupTarget/BackupTargetActions.js
@@ -14,6 +14,8 @@ function actions({ selected, deleteBackupTarget, editBackupTarget }) {
case 'delete':
confirm({
width: 'fit-content',
+ okText: 'Delete',
+ okType: 'danger',
title:
Are you sure you want to delete {record.name} backup target ?
,
onOk() {
deleteBackupTarget(record)
@@ -26,7 +28,7 @@ function actions({ selected, deleteBackupTarget, editBackupTarget }) {
const availableActions = [
{ key: 'edit', name: 'Edit' },
- { key: 'delete', name: 'Delete' },
+ { key: 'delete', name: 'Delete', disabled: selected.default === true, tooltip: selected.default === true ? 'Default backup target can not be deleted' : '' },
]
return (
diff --git a/src/routes/backupTarget/BackupTargetBulkActions.js b/src/routes/backupTarget/BackupTargetBulkActions.js
index 67c14e5af..50af2d08d 100644
--- a/src/routes/backupTarget/BackupTargetBulkActions.js
+++ b/src/routes/backupTarget/BackupTargetBulkActions.js
@@ -10,8 +10,10 @@ function bulkActions({ selectedRows, bulkDeleteBackupTargets }) {
case 'delete':
confirm({
width: 'fit-content',
+ okText: 'Delete',
+ okType: 'danger',
title: (<>
- Are you sure to you want to delete below {count} Backup {count === 1 ? 'Target' : 'Targets' } ?
+ Are you sure to you want to delete below {count} backup {count === 1 ? 'target' : 'targets' } ?
{selectedRows.map(item => - {item.name}
)}
diff --git a/src/routes/backupTarget/BackupTargetList.js b/src/routes/backupTarget/BackupTargetList.js
index 2463a7aa0..cec178426 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 ? (
+
+
+
+ ) : (
+
+
+ )
+ }
+ >
)
},
}, {
@@ -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/EditBackupTargetModal.js b/src/routes/backupTarget/EditBackupTargetModal.js
index 8bdbbb68e..20bee095e 100644
--- a/src/routes/backupTarget/EditBackupTargetModal.js
+++ b/src/routes/backupTarget/EditBackupTargetModal.js
@@ -34,7 +34,7 @@ const modal = ({
...getFieldsValue(),
credentialSecret: getFieldValue('credentialSecret')?.trim() || '',
backupTargetURL: getFieldValue('backupTargetURL')?.trim() || '',
- pollInterval: getFieldValue('pollInterval')?.toString() || item.pollInterval.toString(), // pollInterval is a string type
+ pollInterval: getFieldValue('pollInterval')?.toString() || item.pollInterval.toString(), // pollInterval should be second number and in string type
}
onOk(data)
})
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/host/HostList.js b/src/routes/host/HostList.js
index 1adc64928..8251a9050 100755
--- a/src/routes/host/HostList.js
+++ b/src/routes/host/HostList.js
@@ -375,7 +375,6 @@ class List extends React.Component {
title: 'Operation',
key: 'operation',
width: 120,
- fixed: 'right',
render: (text, record) => {
return (
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,
+ },
+ ],
})(