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 ? (
+
+
+
+ ) : (
+
+
+ )
+ }
+ >
)
},
}, {
@@ -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,
+ },
+ ],
})(