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 ? ( + + + + ) : ( + + 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/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, + }, + ], })( + )} diff --git a/src/routes/volume/detail/Snapshots.js b/src/routes/volume/detail/Snapshots.js index bed6066ee..70f8b6102 100644 --- a/src/routes/volume/detail/Snapshots.js +++ b/src/routes/volume/detail/Snapshots.js @@ -213,8 +213,6 @@ class Snapshots extends React.Component { return null } - // console.log('🚀 ~ Snapshots ~ render ~ createBackModalVisible:', this.state.createBackModalVisible) - // console.log('🚀 ~ Snapshots ~ render ~ createBackBySnapshotModalVisible:', this.state.createBackBySnapshotModalVisible) const isRestoring = () => { if (this.props.volume.restoreStatus && this.props.volume.restoreStatus.length > 0) { let flag = this.props.volume.restoreStatus.every((item) => { diff --git a/src/services/backingImage.js b/src/services/backingImage.js index 057a74ea0..b664cc605 100644 --- a/src/services/backingImage.js +++ b/src/services/backingImage.js @@ -19,6 +19,14 @@ export async function create(params) { }) } +export async function execAction(url, params) { + return request({ + url, + method: 'post', + data: params, + }) +} + export async function deleteBackingImage(params) { if (params.actions && params.actions.backingImageCleanup) { return request({ diff --git a/src/utils/backupTarget.js b/src/utils/backupTarget.js new file mode 100644 index 000000000..0a40837d8 --- /dev/null +++ b/src/utils/backupTarget.js @@ -0,0 +1,6 @@ +export function getAvailBackupTargets(backupTarget) { + if (!backupTarget || !backupTarget.data || !backupTarget.data.length) { + return [] + } + return backupTarget.data.filter((item) => item.available && !item.readOnly) +} diff --git a/src/utils/dataDependency.js b/src/utils/dataDependency.js index 5ca5875bb..95bb068ff 100644 --- a/src/utils/dataDependency.js +++ b/src/utils/dataDependency.js @@ -82,6 +82,9 @@ const dependency = { }, { ns: 'backingImage', key: 'backingimages', + }, { + ns: 'backupTarget', + key: 'backuptargets', }], }, settings: { @@ -173,7 +176,7 @@ const httpDataDependency = { '/volume': ['volume', 'host', 'setting', 'backupTarget', 'backingImage', 'engineimage', 'recurringJob', 'backup'], '/engineimage': ['engineimage'], '/recurringJob': ['recurringJob', 'backupTarget'], - '/backingImage': ['volume', 'backingImage'], + '/backingImage': ['volume', 'backingImage', 'backupTarget'], '/backupTarget': ['backupTarget'], '/setting': ['setting'], '/backup': ['host', 'setting', 'backingImage', 'backup'], diff --git a/src/utils/formatter.js b/src/utils/formatter.js index 44caa23dd..337039d26 100644 --- a/src/utils/formatter.js +++ b/src/utils/formatter.js @@ -19,7 +19,6 @@ function formatSi(val, increment = 1024) { return `${out} ${units[exp]}` } - export function timeDurationStrToInt(time) { if (time === undefined || time === null || typeof time !== 'string') { return diff --git a/src/utils/menu.js b/src/utils/menu.js index 25ac44453..95edb4123 100755 --- a/src/utils/menu.js +++ b/src/utils/menu.js @@ -69,7 +69,7 @@ module.exports = [ show: true, key: 'backupTarget', name: 'Backup Target', - icon: 'diff', + icon: 'cloud-server', }, { show: true, diff --git a/src/utils/websocket.js b/src/utils/websocket.js index 1f9377d3f..c7843b053 100644 --- a/src/utils/websocket.js +++ b/src/utils/websocket.js @@ -31,11 +31,6 @@ export function wsChanges(dispatch, type, period, ns, search) { } // To do. Because two ws connections will be maintained under backup ns. const backupType = type || '' - // console.log('🚀 ~ wsChanges ~ search:', search) - // console.log('🚀 ~ wsChanges ~ type:', type) - // console.log('🚀 ~ wsChanges ~ url:', url) - // console.log('🚀 ~ wsChanges ~ backupType:', backupType) - // console.log('🚀 ~ wsChanges ~ ns:', ns) const rws = new RobustWebSocket(url, [], options) if (ns === 'backup') { if (backupType === 'backupvolumes') { @@ -100,7 +95,6 @@ export function wsChanges(dispatch, type, period, ns, search) { }) } } else { - console.log('dispatch ns/updateBackground') dispatch({ type: `${ns}/updateBackground`, payload: JSON.parse(msg.data),