From b356ca021220d4fb028b21e8046ce8c435eec509 Mon Sep 17 00:00:00 2001 From: "andy.lee" Date: Wed, 12 Jun 2024 11:01:11 +0800 Subject: [PATCH 1/4] Add backup target page Signed-off-by: andy.lee --- src/components/Filter/Filter.js | 87 +++++-- src/components/Layout/Footer.js | 6 +- src/index.js | 2 + src/models/backingImage.js | 1 + src/models/backup.js | 25 +- src/models/backupTarget.js | 160 ++++++++++++ src/models/snapshot.js | 3 +- src/router.js | 9 +- src/routes/backingImage/BackingImageList.js | 2 +- src/routes/backup/BackupDetail.js | 10 +- src/routes/backup/BackupList.js | 8 +- src/routes/backup/BackupVolumeList.js | 7 +- src/routes/backupTarget/BackupTarget.less | 49 ++++ .../backupTarget/BackupTargetActions.js | 46 ++++ .../backupTarget/BackupTargetBulkActions.js | 50 ++++ src/routes/backupTarget/BackupTargetList.js | 132 ++++++++++ .../backupTarget/CreateBackupTargetModal.js | 128 ++++++++++ .../backupTarget/EditBackupTargetModal.js | 118 +++++++++ src/routes/backupTarget/index.js | 235 ++++++++++++++++++ src/routes/host/HostList.js | 1 + src/routes/recurringJob/index.js | 1 - src/services/backup.js | 8 - src/services/backupTarget.js | 40 +++ src/services/recurringJob.js | 1 + src/utils/dataDependency.js | 22 +- src/utils/formatter.js | 22 ++ src/utils/menu.js | 6 + src/utils/websocket.js | 66 +++-- 28 files changed, 1154 insertions(+), 91 deletions(-) create mode 100644 src/models/backupTarget.js create mode 100644 src/routes/backupTarget/BackupTarget.less create mode 100644 src/routes/backupTarget/BackupTargetActions.js create mode 100644 src/routes/backupTarget/BackupTargetBulkActions.js create mode 100644 src/routes/backupTarget/BackupTargetList.js create mode 100644 src/routes/backupTarget/CreateBackupTargetModal.js create mode 100644 src/routes/backupTarget/EditBackupTargetModal.js create mode 100644 src/routes/backupTarget/index.js create mode 100755 src/services/backupTarget.js diff --git a/src/components/Filter/Filter.js b/src/components/Filter/Filter.js index 5fa34614..b0a8c6d3 100644 --- a/src/components/Filter/Filter.js +++ b/src/components/Filter/Filter.js @@ -6,10 +6,24 @@ import styles from './Filter.less' const Option = Select.Option +const BOOLEAN_OPTIONS = { True: true, False: false } + class Filter extends React.Component { constructor(props) { super(props) - const { field = props.defaultField || 'name', value = '', stateValue = '', nodeRedundancyValue = '', engineImageUpgradableValue = '', scheduleValue = '', pvStatusValue = '', revisionCounterValue = '', isGroupValue = '', createdFromValue = '' } = queryString.parse(props.location.search) + const { + field = props.defaultField || 'name', + value = '', stateValue = '', + nodeRedundancyValue = '', + engineImageUpgradableValue = '', + scheduleValue = '', + pvStatusValue = '', + revisionCounterValue = '', + isGroupValue = '', + createdFromValue = '', + booleanValue = BOOLEAN_OPTIONS.True, + } = queryString.parse(props.location.search) + this.state = { field, stateValue, @@ -22,6 +36,7 @@ class Filter extends React.Component { createdFromValue, isGroupValue, keyword: value, + booleanValue, } } @@ -64,6 +79,10 @@ class Filter extends React.Component { this.setState({ ...this.state, isGroupValue }) } + handleBooleanValueChange = (booleanValue) => { + this.setState({ ...this.state, booleanValue }) + } + handleCreatedFromValueChange = (createdFromValue) => { this.setState({ ...this.state, createdFromValue }) } @@ -73,8 +92,19 @@ class Filter extends React.Component { } render() { - const { field = this.props.defaultField || 'name', value = '', stateValue = '', nodeRedundancyValue = '', engineImageUpgradableValue = '', scheduleValue = '', pvStatusValue = '', revisionCounterValue = '', isGroup = '', createdFromValue = '' } = queryString.parse(this.props.location.search) - + const { booleanFields = [] } = this.props + const { + field = this.props.defaultField || 'name', + value = '', + stateValue = '', + nodeRedundancyValue = '', + engineImageUpgradableValue = '', + scheduleValue = '', + pvStatusValue = '', + revisionCounterValue = '', + isGroup = '', + createdFromValue = '', + } = queryString.parse(this.props.location.search) let defaultContent = { field, stateValue, @@ -160,15 +190,29 @@ class Filter extends React.Component { {this.props.revisionCounterOption.map(item => ())} ) } else if (this.state.field === 'sourceType' && this.props.createdFromOption) { - valueForm = () + valueForm = ( + + ) + } else if (booleanFields.includes(this.state.field)) { + valueForm = ( + + ) } let content = '' @@ -183,15 +227,15 @@ class Filter extends React.Component { return (
- - - - - { valueForm } - - + + + + + { valueForm } + +
) } @@ -204,6 +248,7 @@ Filter.propTypes = { stateOption: PropTypes.array, fieldOption: PropTypes.array, defaultField: PropTypes.string, + booleanFields: PropTypes.array, scheduleOption: PropTypes.array, replicaNodeRedundancyOption: PropTypes.array, engineImageUpgradableOption: PropTypes.array, diff --git a/src/components/Layout/Footer.js b/src/components/Layout/Footer.js index 35238e60..5f7243cf 100644 --- a/src/components/Layout/Footer.js +++ b/src/components/Layout/Footer.js @@ -10,7 +10,7 @@ import semver from 'semver' import BundlesModel from './BundlesModel' import StableLonghornVersions from './StableLonghornVersions' -function Footer({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, backup, systemBackups, dispatch }) { +function Footer({ app, host, volume, setting, engineimage, eventlog, backingImage, backupTarget, recurringJob, backup, systemBackups, dispatch }) { const { bundlesropsVisible, bundlesropsKey, stableLonghornVersionslVisible, stableLonghornVersionsKey, okText, modalButtonDisabled, progressPercentage } = app const currentVersion = config.version === '${VERSION}' ? 'dev' : config.version // eslint-disable-line no-template-curly-in-string const issueHref = 'https://github.com/longhorn/longhorn/issues/new/choose' @@ -127,6 +127,7 @@ function Footer({ app, host, volume, setting, engineimage, eventlog, backingImag {getStatusIcon(engineimage)} {getStatusIcon(eventlog)} {getStatusIcon(backingImage)} + {getStatusIcon(backupTarget)} {getStatusIcon(recurringJob)} {getBackupStatusIcon(backup, 'backupVolumes')} {getBackupStatusIcon(backup, 'backups')} @@ -145,6 +146,7 @@ Footer.propTypes = { volume: PropTypes.object, setting: PropTypes.object, engineimage: PropTypes.object, + backupTarget: PropTypes.object, eventlog: PropTypes.object, backingImage: PropTypes.object, recurringJob: PropTypes.object, @@ -154,4 +156,4 @@ Footer.propTypes = { dispatch: PropTypes.func, } -export default connect(({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, backup, systemBackups }) => ({ app, host, volume, setting, engineimage, eventlog, backingImage, recurringJob, backup, systemBackups }))(Footer) +export default connect(({ app, host, volume, setting, engineimage, eventlog, backupTarget, backingImage, recurringJob, backup, systemBackups }) => ({ app, host, volume, setting, engineimage, eventlog, backingImage, backupTarget, recurringJob, backup, systemBackups }))(Footer) diff --git a/src/index.js b/src/index.js index bfd614fa..ad4535e2 100755 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ import setting from './models/setting' import eventlog from './models/eventlog' import engineimage from './models/engineimage' import backingImage from './models/backingImage' +import backupTarget from './models/backupTarget' import backup from './models/backup' import snapshot from './models/snapshot' import recurringJob from './models/recurringJob' @@ -34,6 +35,7 @@ app.model(setting) app.model(eventlog) app.model(engineimage) app.model(backingImage) +app.model(backupTarget) app.model(backup) app.model(volume) app.model(recurringJob) diff --git a/src/models/backingImage.js b/src/models/backingImage.js index 26087c7d..ec592d47 100644 --- a/src/models/backingImage.js +++ b/src/models/backingImage.js @@ -155,6 +155,7 @@ 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 a33f24ff..7588c1fe 100644 --- a/src/models/backup.js +++ b/src/models/backup.js @@ -1,4 +1,5 @@ -import { query, queryBackupList, execAction, restore, deleteBackup, syncBackupVolume, syncAllBackupVolumes, createVolume, deleteAllBackups, getNodeTags, getDiskTags, queryTarget } from '../services/backup' +import { query, queryBackupList, execAction, restore, deleteBackup, syncBackupVolume, syncAllBackupVolumes, createVolume, deleteAllBackups, getNodeTags, getDiskTags } from '../services/backup' +import { queryBackupTarget } from '../services/backupTarget' import { message } from 'antd' import { wsChanges } from '../utils/websocket' import queryString from 'query-string' @@ -99,18 +100,14 @@ export default { } }, *queryBackupTarget({ + // eslint-disable-next-line no-unused-vars payload, }, { call, put }) { - let resp = yield call(queryTarget) + const resp = yield call(queryBackupTarget) if (resp && resp.data && resp.data[0]) { - let isbackupVolumePage = true - let path = ['/node', '/dashboard', '/volume', '/engineimage', '/setting', '/backingImage', '/recurringJob'] - - isbackupVolumePage = payload.history && payload.history.location && payload.history.location.pathname && payload.history.location.pathname !== '/' && path.every(ele => !payload.history.location.pathname.startsWith(ele)) - if (isbackupVolumePage) { - !resp.data[0].available ? message.error(resp.data[0].message) : message.destroy() - } - yield put({ type: 'setBackupTargetAvailable', payload: { backupTargetAvailable: resp.data[0].available, backupTargetMessage: resp.data[0].message } }) + const backupTargetAvailable = resp.data.some(d => d.available === true) + const backupTargetMessage = backupTargetAvailable ? '' : 'No backup target available' + yield put({ type: 'setBackupTargetAvailable', payload: { backupTargetAvailable, backupTargetMessage } }) } }, *queryBackupStatus({ @@ -311,7 +308,6 @@ export default { wsChanges(payload.dispatch, payload.type, '1s', payload.ns) } } - if (payload.type === 'backups' && getBackupVolumeName(payload.search)) { let wsBackup = yield select(state => state.backup.wsBackup) if (wsBackup) { @@ -353,7 +349,12 @@ export default { let volumeName = getBackupVolumeName(state.search) if (volumeName && action.payload && action.payload.data) { let backupData = action.payload.data.filter((item) => { - return item.volumeName === volumeName + if (item.backupTargetName) { + // after support multiple backup targets feature volumeName is composed by ${volumeName}-${backupTargetName} + return volumeName === `${item.volumeName}-${item.backupTargetName}` + } else { + return item.volumeName === volumeName + } }) return { ...state, diff --git a/src/models/backupTarget.js b/src/models/backupTarget.js new file mode 100644 index 00000000..438ad98c --- /dev/null +++ b/src/models/backupTarget.js @@ -0,0 +1,160 @@ +import { queryBackupTarget, createBackupTarget, deleteBackupTarget, updateBackupTarget } from '../services/backupTarget' +import { message } from 'antd' +// import { delay } from 'dva/saga' +import { wsChanges, updateState } from '../utils/websocket' +import queryString from 'query-string' +import { enableQueryData } from '../utils/dataDependency' + +export default { + ws: null, + namespace: 'backupTarget', + state: { + data: [], + resourceType: 'backupTarget', + selectedRows: [], + // createBackingImageModalVisible: false, + // createBackingImageModalKey: Math.random(), + // diskStateMapDetailModalVisible: false, + // diskStateMapDetailModalKey: Math.random(), + // diskStateMapDeleteDisabled: true, + // diskStateMapDeleteLoading: false, + // selectedDiskStateMapRows: [], + // selectedDiskStateMapRowKeys: [], + socketStatus: 'closed', + }, + subscriptions: { + setup({ dispatch, history }) { + history.listen(location => { + if (enableQueryData(location.pathname, 'backupTarget')) { + dispatch({ + type: 'query', + payload: location.pathname.startsWith('/backupTarget') ? queryString.parse(location.search) : {}, + }) + } + }) + }, + }, + effects: { + *query({ + payload, + }, { call, put }) { + const data = yield call(queryBackupTarget, payload) + console.log('🚀 ~ data:', data) + console.log('🚀 ~ backupTarget query data:', data) + if (payload && payload.field && payload.keyword && data.data) { + data.data = data.data.filter(item => item[payload.field] && item[payload.field].indexOf(payload.keyword.trim()) > -1) + } + if (data.data) { + data.data.sort((a, b) => a.name.localeCompare(b.name)) + } + yield put({ type: 'queryBackupTarget', payload: { ...data } }) + yield put({ type: 'clearSelection' }) + }, + *create({ + payload, + }, { call, put }) { + const resp = yield call(createBackupTarget, payload) + // console.log('🚀 ~ create resp:', resp) + if (resp && resp.status === 200) { + message.success(`Successfully create backup target ${payload.name}.`) + } + yield put({ type: 'query' }) + }, + *delete({ + payload, + }, { call, put }) { + yield call(deleteBackupTarget, payload) + yield put({ type: 'query' }) + }, + *edit({ + payload, + }, { call, put }) { + // console.log('🚀 ~edit payload:', payload) + yield call(updateBackupTarget, payload) + yield put({ type: 'query' }) + }, + *bulkDelete({ + payload, + }, { call, put }) { + if (payload && payload.length > 0) { + yield payload.map(item => call(deleteBackupTarget, item)) + } + yield put({ type: 'query' }) + }, + *startWS({ + payload, + }, { select }) { + let ws = yield select(state => state.backupTarget.ws) + console.log('🚀 ~ backupTarget startWS ws:', ws) + if (ws) { + ws.open() + } else { + wsChanges(payload.dispatch, payload.type, '1s', payload.ns) + } + }, + *stopWS({ + // eslint-disable-next-line no-unused-vars + payload, + }, { select }) { + let ws = yield select(state => state.backupTarget.ws) + if (ws) { + ws.close(1000) + } + }, + }, + reducers: { + queryBackupTarget(state, action) { + return { + ...state, + ...action.payload, + } + }, + changeSelection(state, action) { + return { ...state, ...action.payload } + }, + clearSelection(state) { + return { ...state, selectedRows: [] } + }, + updateBackground(state, action) { + return updateState(state, action) + }, + // showCreateBackingImageModal(state, action) { + // return { ...state, ...action.payload, createBackingImageModalVisible: true, createBackingImageModalKey: Math.random() } + // }, + // hideCreateBackingImageModal(state) { + // return { ...state, createBackingImageModalVisible: false } + // }, + // showDiskStateMapDetailModal(state, action) { + // return { + // ...state, + // selected: action.payload.record, + // diskStateMapDetailModalVisible: true, + // diskStateMapDetailModalKey: Math.random(), + // } + // }, + // hideDiskStateMapDetailModal(state) { + // return { ...state, diskStateMapDetailModalVisible: false, diskStateMapDetailModalKey: Math.random() } + // }, + // disableDiskStateMapDelete(state) { + // return { ...state, diskStateMapDeleteDisabled: true } + // }, + // enableDiskStateMapDelete(state) { + // return { ...state, diskStateMapDeleteDisabled: false } + // }, + // disableDiskStateMapDeleteLoading(state) { + // return { ...state, diskStateMapDeleteLoading: false } + // }, + // enableDiskStateMapDeleteLoading(state) { + // return { ...state, diskStateMapDeleteLoading: true } + // }, + changeDiskStateMapSelection(state, action) { + return { ...state, ...action.payload } + }, + updateSocketStatus(state, action) { + return { ...state, socketStatus: action.payload } + }, + updateWs(state, action) { + return { ...state, ws: action.payload } + }, + }, +} diff --git a/src/models/snapshot.js b/src/models/snapshot.js index e30aeaeb..a1d74c10 100644 --- a/src/models/snapshot.js +++ b/src/models/snapshot.js @@ -1,4 +1,5 @@ /* eslint no-unused-vars: "off" */ +import backupTarget from '../routes/backupTarget' import { execAction } from '../services/volume' import { delay } from 'dva/saga' @@ -205,7 +206,7 @@ export default (namespace) => { if (Object.getOwnPropertyNames(payload.labels).length === 0) { yield call(execAction, payload.snapshotBackupUrl, { name: snapshot.name }) } else { - yield call(execAction, payload.snapshotBackupUrl, { name: snapshot.name, labels: payload.labels }) + yield call(execAction, payload.snapshotBackupUrl, { name: snapshot.name, labels: payload.labels, backupTargetName: 'andy-minio-backupstore' }) } yield put({ type: 'querySnapShot', payload: { url: payload.querySnapShotUrl } }) yield put({ type: 'setLoading', payload: false }) diff --git a/src/router.js b/src/router.js index 7b48bdfd..234d8a20 100755 --- a/src/router.js +++ b/src/router.js @@ -14,6 +14,7 @@ import settingComponent from './routes/setting/' import engineimageComponent from './routes/engineimage/' import instanceManagerComponent from './routes/instanceManager/' import backingImageComponent from './routes/backingImage/' +import backupTargetComponent from './routes/backupTarget/' import recurringJobComponent from './routes/recurringJob/' import orphanedDataComponent from './routes/orphanedData/' import engineimageDetailComponent from './routes/engineimage/detail' @@ -70,6 +71,11 @@ const Routers = function ({ history, app }) { component: () => backingImageComponent, }) + const backupTarget = dynamic({ + app, + component: () => backupTargetComponent, + }) + const recurringJob = dynamic({ app, component: () => recurringJobComponent, @@ -113,7 +119,8 @@ const Routers = function ({ history, app }) { - + + {/* */} diff --git a/src/routes/backingImage/BackingImageList.js b/src/routes/backingImage/BackingImageList.js index 06116c4a..e2801307 100644 --- a/src/routes/backingImage/BackingImageList.js +++ b/src/routes/backingImage/BackingImageList.js @@ -94,7 +94,7 @@ function list({ loading, dataSource, deleteBackingImage, showDiskStateMapDetail, dataSource={dataSource} loading={loading} simple - pagination={pagination} + pagination={pagination('backingImagePageSize')} rowKey={record => record.id} scroll={{ x: 970, y: dataSource.length > 0 ? height : 1 }} /> diff --git a/src/routes/backup/BackupDetail.js b/src/routes/backup/BackupDetail.js index fa62df06..72e7f723 100644 --- a/src/routes/backup/BackupDetail.js +++ b/src/routes/backup/BackupDetail.js @@ -20,15 +20,21 @@ function Backup({ backup, volume, setting, backingImage, loading, location, disp const backingImages = backingImage.data const defaultReplicaCountSetting = settings.find(s => s.id === 'default-replica-count') const defaultNumberOfReplicas = defaultReplicaCountSetting !== undefined ? parseInt(defaultReplicaCountSetting.value, 10) : 3 + + const backupTargetName = queryString.parse(location.search).backupTargetName || '' const volumeName = queryString.parse(location.search).keyword + const currentBackUp = backupVolumes.find((item) => { return item.id === volumeName }) const v1DataEngineEnabledSetting = settings.find(s => s.id === 'v1-data-engine') const v2DataEngineEnabledSetting = settings.find(s => s.id === 'v2-data-engine') const v1DataEngineEnabled = v1DataEngineEnabledSetting?.value === 'true' const v2DataEngineEnabled = v2DataEngineEnabledSetting?.value === 'true' - sortBackups(backupData) + + const backups = backupTargetName ? backupData.filter((item) => item.backupTargetName === backupTargetName) : backupData + sortBackups(backups) + const backupProps = { - backup: backupData, + backup: backups, volumeList, loading, dispatch, diff --git a/src/routes/backup/BackupList.js b/src/routes/backup/BackupList.js index e7b5f6c9..d802e439 100755 --- a/src/routes/backup/BackupList.js +++ b/src/routes/backup/BackupList.js @@ -62,7 +62,7 @@ class List extends React.Component { }) } - fomartData = (data, key) => { + formatData = (data, key) => { if (this.isJson(data)) { let obj = JSON.parse(data) @@ -85,7 +85,7 @@ class List extends React.Component { } } - onCopy = (text, copySuccess) => { // eslint-disable-line no-unused-vars + onCopy = (_text, copySuccess) => { if (copySuccess) { message.success('Copied', 1.5) } else { @@ -213,7 +213,7 @@ class List extends React.Component { let storageObj = {} if (record) { - storageObj = this.fomartData(record.KubernetesStatus) + storageObj = this.formatData(record.KubernetesStatus) } let title = (
PV Name: {storageObj.pvName}
@@ -253,7 +253,7 @@ class List extends React.Component { let storageObj = {} if (record) { - storageObj = this.fomartData(record.KubernetesStatus) + storageObj = this.formatData(record.KubernetesStatus) storageObj.snapshotCreated = record.snapshotCreated ? record.snapshotCreated : '' } diff --git a/src/routes/backup/BackupVolumeList.js b/src/routes/backup/BackupVolumeList.js index 4dd8feca..edd4b24a 100644 --- a/src/routes/backup/BackupVolumeList.js +++ b/src/routes/backup/BackupVolumeList.js @@ -41,7 +41,7 @@ class List extends React.Component { }) } - fomartData = (data, key) => { + formatData = (data, key) => { if (this.isJson(data)) { let obj = JSON.parse(data) @@ -117,6 +117,7 @@ class List extends React.Component { search: queryString.stringify({ field: 'volumeName', keyword: id, + backupTargetName: record.backupTargetName, }), }}> {id} @@ -177,7 +178,7 @@ class List extends React.Component { let storageObj = {} if (record) { - storageObj = this.fomartData(record.KubernetesStatus) + storageObj = this.formatData(record.KubernetesStatus) } let title = (
PV Name: {storageObj.pvName}
@@ -218,7 +219,7 @@ class List extends React.Component { let storageObj = {} if (record) { - storageObj = this.fomartData(record.KubernetesStatus) + storageObj = this.formatData(record.KubernetesStatus) storageObj.snapshotCreated = record.snapshotCreated ? record.snapshotCreated : '' } diff --git a/src/routes/backupTarget/BackupTarget.less b/src/routes/backupTarget/BackupTarget.less new file mode 100644 index 00000000..5065c68f --- /dev/null +++ b/src/routes/backupTarget/BackupTarget.less @@ -0,0 +1,49 @@ +// .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 new file mode 100644 index 00000000..a3aed9f9 --- /dev/null +++ b/src/routes/backupTarget/BackupTargetActions.js @@ -0,0 +1,46 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Modal } from 'antd' +import { DropOption } from '../../components' +const confirm = Modal.confirm + +function actions({ selected, deleteBackupTarget, editBackupTarget }) { + const handleMenuClick = (event, record) => { + event.domEvent?.stopPropagation?.() + switch (event.key) { + case 'edit': + editBackupTarget(record) + break + case 'delete': + confirm({ + width: 'fit-content', + title:

Are you sure you want to delete {record.name} backup target ?

, + onOk() { + deleteBackupTarget(record) + }, + }) + break + default: + } + } + + const availableActions = [ + { key: 'edit', name: 'Edit' }, + { key: 'delete', name: 'Delete' }, + ] + + return ( + handleMenuClick(e, selected)} + /> + ) +} + +actions.propTypes = { + selected: PropTypes.object, + deleteBackupTarget: PropTypes.func, + editBackupTarget: PropTypes.func, +} + +export default actions diff --git a/src/routes/backupTarget/BackupTargetBulkActions.js b/src/routes/backupTarget/BackupTargetBulkActions.js new file mode 100644 index 00000000..67c14e5a --- /dev/null +++ b/src/routes/backupTarget/BackupTargetBulkActions.js @@ -0,0 +1,50 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Button, Modal } from 'antd' +const confirm = Modal.confirm + +function bulkActions({ selectedRows, bulkDeleteBackupTargets }) { + const handleClick = (action) => { + const count = selectedRows.length + switch (action) { + case 'delete': + confirm({ + width: 'fit-content', + title: (<> +

Are you sure to you want to delete below {count} Backup {count === 1 ? 'Target' : 'Targets' } ?

+
    + {selectedRows.map(item =>
  • {item.name}
  • )} +
+ ), + onOk() { + bulkDeleteBackupTargets(selectedRows) + }, + }) + break + default: + } + } + + const allActions = [ + { key: 'delete', name: 'Delete', disabled() { return selectedRows.length === 0 } }, + ] + + return ( +
+ { allActions.map(item => { + return ( +
+ +
+ ) + }) } +
+ ) +} + +bulkActions.propTypes = { + selectedRows: PropTypes.array, + bulkDeleteBackupTargets: PropTypes.func, +} + +export default bulkActions diff --git a/src/routes/backupTarget/BackupTargetList.js b/src/routes/backupTarget/BackupTargetList.js new file mode 100644 index 00000000..81f8a3d8 --- /dev/null +++ b/src/routes/backupTarget/BackupTargetList.js @@ -0,0 +1,132 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Table, Icon, Tooltip } from 'antd' +import BackupTargetActions from './BackupTargetActions' +import { pagination } from '../../utils/page' + +function list({ loading, dataSource, deleteBackupTarget, editBackupTarget, rowSelection, height }) { + const columns = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + width: 150, + sorter: (a, b) => a.name.localeCompare(b.name), + render: (text) => { + return ( +
{text}
+ ) + }, + }, { + title: 'URL', + dataIndex: 'backupTargetURL', + key: 'backupTargetURL', + width: 250, + sorter: (a, b) => a.backupTargetURL.localeCompare(b.backupTargetURL), + render: (text) => { + return ( +
{text}
+ ) + }, + }, { + title: 'Credential Secret', + dataIndex: 'credentialSecret', + key: 'credentialSecret', + width: 150, + sorter: (a, b) => a.credentialSecret.localeCompare(b.credentialSecret), + render: (text) => { + return ( +
{text}
+ ) + }, + }, { + title: 'Poll Interval', + dataIndex: 'pollInterval', + key: 'pollInterval', + width: 100, + sorter: (a, b) => a.pollInterval.localeCompare(b.pollInterval), + render: (text) => { + return ( +
{text}
+ ) + }, + }, { + title: 'Read Only', + dataIndex: 'readOnly', + key: 'readOnly', + width: 80, + sorter: (a, b) => a.readOnly - b.readOnly, + render: (text) => { + return ( +
{text.toString().firstUpperCase()}
+ ) + }, + }, { + title: 'Default', + dataIndex: 'default', + key: 'default', + width: 80, + sorter: (a, b) => a.default - b.default, + render: (text) => { + return ( +
{text.toString().firstUpperCase()}
+ ) + }, + }, { + title: 'Available', + dataIndex: 'available', + key: 'available', + width: 80, + sorter: (a, b) => a.available - b.available, + render: (text) => { + return ( +
+
{text.toString().firstUpperCase()}
+ {text === false && ( + + + + )} +
+ ) + }, + }, { + title: 'Operation', + key: 'operation', + width: 100, + render: (text, record) => { + return ( + + ) + }, + }, + ] + + return ( +
+ record.id} + scroll={{ x: 970, y: dataSource.length > 0 ? height : 1 }} + /> + + ) +} + +list.propTypes = { + dataSource: PropTypes.array, + height: PropTypes.number, + loading: PropTypes.bool, + rowSelection: PropTypes.object, + deleteBackupTarget: PropTypes.func, + editBackupTarget: PropTypes.func, +} + +export default list diff --git a/src/routes/backupTarget/CreateBackupTargetModal.js b/src/routes/backupTarget/CreateBackupTargetModal.js new file mode 100644 index 00000000..67033a18 --- /dev/null +++ b/src/routes/backupTarget/CreateBackupTargetModal.js @@ -0,0 +1,128 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Form, Input, Checkbox, Icon, InputNumber, Tooltip } from 'antd' +import { ModalBlur } from '../../components' +const FormItem = Form.Item + +const formItemLayout = { + labelCol: { + span: 6, + }, + wrapperCol: { + span: 15, + }, +} + +const modal = ({ + item, + allBackupTargetsName, + visible, + onCancel, + onOk, + form: { + getFieldDecorator, + validateFields, + getFieldsValue, + getFieldValue, + }, +}) => { + function handleOk() { + validateFields((errors) => { + if (errors) { + return + } + const data = { + ...getFieldsValue(), + credentialSecret: getFieldValue('credentialSecret')?.trim() || '', + backupTargetURL: getFieldValue('backupTargetURL')?.trim() || '', + pollInterval: getFieldValue('pollInterval')?.toString() || item.pollInterval.toString(), // pollInterval is a string type + } + onOk(data) + }) + } + + const modalOpts = { + title: 'Create Backup Target', + visible, + onCancel, + width: 700, + onOk: handleOk, + } + + return ( + +
+ + {getFieldDecorator('name', { + initialValue: item.name, + rules: [ + { + required: true, + message: 'Backup target name is required', + }, + { + validator: (_rule, value, callback) => { + if (allBackupTargetsName.includes(value)) { + callback(`"${value}" is duplicated to existing backup target name`) + } else { + callback() + } + }, + }, + ], + })()} + + + {getFieldDecorator('backupTargetURL', { + initialValue: item.backupTargetURL, + })()} + + + {getFieldDecorator('credentialSecret', { + initialValue: item.credentialSecret, + })()} + + +
+ {getFieldDecorator('pollInterval', { + initialValue: item.pollInterval || 0, + })()} + seconds + + + +
+
+ + + {getFieldDecorator('readOnly', { + valuePropName: 'checked', + initialValue: item.readOnly, + })()} + + + {getFieldDecorator('default', { + valuePropName: 'checked', + initialValue: item.default, + })()} + + +
+ ) +} + +modal.propTypes = { + item: PropTypes.object, + visible: PropTypes.bool, + onCancel: PropTypes.func, + onOk: PropTypes.func, + form: PropTypes.object.isRequired, +} + +export default Form.create()(modal) diff --git a/src/routes/backupTarget/EditBackupTargetModal.js b/src/routes/backupTarget/EditBackupTargetModal.js new file mode 100644 index 00000000..8bdbbb68 --- /dev/null +++ b/src/routes/backupTarget/EditBackupTargetModal.js @@ -0,0 +1,118 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Form, Input, Checkbox, Icon, InputNumber, Tooltip } from 'antd' +import { ModalBlur } from '../../components' +const FormItem = Form.Item + +const formItemLayout = { + labelCol: { + span: 6, + }, + wrapperCol: { + span: 15, + }, +} + +const modal = ({ + item, + visible, + onCancel, + onOk, + form: { + getFieldDecorator, + validateFields, + getFieldsValue, + getFieldValue, + }, +}) => { + function handleOk() { + validateFields((errors) => { + if (errors) { + return + } + const data = { + ...getFieldsValue(), + credentialSecret: getFieldValue('credentialSecret')?.trim() || '', + backupTargetURL: getFieldValue('backupTargetURL')?.trim() || '', + pollInterval: getFieldValue('pollInterval')?.toString() || item.pollInterval.toString(), // pollInterval is a string type + } + onOk(data) + }) + } + + const modalOpts = { + title: `Edit Backup Target ${item.name}`, + visible, + onCancel, + width: 700, + onOk: handleOk, + } + + return ( + +
+ + {getFieldDecorator('name', { + initialValue: item.name, + rules: [ + { + required: true, + message: 'Backup target name is required', + }, + ], + })()} + + + {getFieldDecorator('backupTargetURL', { + initialValue: item.backupTargetURL, + })()} + + + {getFieldDecorator('credentialSecret', { + initialValue: item.credentialSecret, + })()} + + +
+ {getFieldDecorator('pollInterval', { + initialValue: item.pollInterval, + })()} + seconds + + + +
+
+ + + {getFieldDecorator('readOnly', { + valuePropName: 'checked', + initialValue: item.readOnly, + })()} + + + {getFieldDecorator('default', { + valuePropName: 'checked', + initialValue: item.default, + })()} + + +
+ ) +} + +modal.propTypes = { + item: PropTypes.object, + visible: PropTypes.bool, + onCancel: PropTypes.func, + onOk: PropTypes.func, + form: PropTypes.object.isRequired, +} + +export default Form.create()(modal) diff --git a/src/routes/backupTarget/index.js b/src/routes/backupTarget/index.js new file mode 100644 index 00000000..9ba1b2e8 --- /dev/null +++ b/src/routes/backupTarget/index.js @@ -0,0 +1,235 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { routerRedux } from 'dva/router' +import { connect } from 'dva' +import { Filter } from '../../components/index' +import { Row, Col, Button } from 'antd' +import CreateBackupTargetModal from './CreateBackupTargetModal' +import EditBackupTargetModal from './EditBackupTargetModal' +import BackupTargetList from './BackupTargetList' +import BackupTargetBulkActions from './BackupTargetBulkActions' +import { timeDurationStrToInt } from '../../utils/formatter' +import queryString from 'query-string' +import C from '../../utils/constants' + +const filterDataByField = (backupTargetData, field, value, booleanValue) => { + if (!field) return backupTargetData + let result = backupTargetData + switch (field) { + case 'name': + case 'backupTargetURL': + case 'credentialSecret': + case 'pollInterval': + result = backupTargetData.filter((d) => (value ? d[`${field}`].includes(value?.trim()) : true)) + break + case 'default': + case 'readOnly': + case 'available': { + result = backupTargetData.filter((d) => (booleanValue ? d[`${field}`].toString() === booleanValue?.toLowerCase() : true)) + break + } + default: + break + } + return result +} + +class BackupTarget extends React.Component { + constructor(props) { + super(props) + this.state = { + height: 300, + createBackupTargetModalVisible: false, + editBackupTargetModalVisible: false, + selectedEditRow: {}, + } + } + + componentDidMount() { + this.onResize() + window.addEventListener('resize', this.onResize) + } + + componentWillUnmount() { + window.removeEventListener('resize', this.onResize) + } + + onResize = () => { + const table = document.getElementById('backupTargetTable') + if (table) { + const height = table.offsetHeight - C.ContainerMarginHeight + this.setState({ + height, + }) + } + } + + handleEditClick = (record) => { + this.setState({ + ...this.state, + selectedEditRow: record, + editBackupTargetModalVisible: true, + }) + this.handleEditModalOpen() + } + + handleEditModalClose = () => { + this.setState({ ...this.state, editBackupTargetModalVisible: false }) + } + + + handleCreateModalClose = () => { + this.setState({ ...this.state, createBackupTargetModalVisible: false }) + } + + handleCreateModalOpen = () => { + this.setState({ ...this.state, createBackupTargetModalVisible: true }) + } + + render() { + const { dispatch, loading, location } = this.props + const { createBackupTargetModalVisible, editBackupTargetModalVisible, selectedEditRow } = this.state + const { data, selectedRows } = this.props.backupTarget + const { field, value, booleanValue } = queryString.parse(this.props.location.search) + + const backupTargetData = filterDataByField(data, field, value, booleanValue) + + const backupTargetListProps = { + dataSource: backupTargetData, + height: this.state.height, + loading, + rowSelection: { + selectedRowKeys: selectedRows.map(item => item.id), + onChange(_, records) { + dispatch({ + type: 'backupTarget/changeSelection', + payload: { + selectedRows: records, + }, + }) + }, + }, + deleteBackupTarget: (record) => { + dispatch({ + type: 'backupTarget/delete', + payload: record, + }) + }, + editBackupTarget: (record) => { + this.handleEditClick(record) + }, + } + + const createBackupTargetModalProps = { + item: { + name: '', + backupTargetURL: '', + credentialSecret: '', + pollInterval: 300, + readOnly: false, + default: false, + }, + allBackupTargetsName: backupTargetData.map(item => item.name), + visible: createBackupTargetModalVisible, + onOk: (newBackupTarget) => { + dispatch({ + type: 'backupTarget/create', + payload: newBackupTarget, + }) + this.handleCreateModalClose() + }, + onCancel: () => this.handleCreateModalClose(), + } + + const editBackupTargetModalProps = { + item: { + ...selectedEditRow, + pollInterval: timeDurationStrToInt(selectedEditRow.pollInterval), + }, + visible: editBackupTargetModalVisible, + onOk: (updatedBackupTarget) => { + dispatch({ + type: 'backupTarget/edit', + payload: updatedBackupTarget, + }) + this.handleEditModalClose() + }, + onCancel: () => this.handleEditModalClose(), + } + + const backupTargetBulkActionsProps = { + selectedRows, + bulkDeleteBackupTargets(records) { + dispatch({ + type: 'backupTarget/bulkDelete', + payload: records, + }) + }, + } + + const backupTargetFilterProps = { + location, + defaultField: 'name', + fieldOption: [ + { value: 'name', name: 'Name' }, + { value: 'backupTargetURL', name: 'URL' }, + { value: 'credentialSecret', name: 'Credential Secret' }, + { value: 'pollInterval', name: 'Poll Interval' }, + { value: 'readOnly', name: 'Read Only' }, + { value: 'default', name: 'Default' }, + { value: 'available', name: 'Available' }, + ], + booleanFields: ['readOnly', 'default', 'available'], + onSearch(filter) { + const { field: filterField, value: filterValue, booleanValue: filterBooleanValue } = filter + if (filterField && (filterValue || typeof filterBooleanValue === 'boolean')) { + const queryStringObj = { field: filterField } + if (['readOnly', 'default', 'available'].includes(filterField) && typeof filterBooleanValue === 'boolean') { + queryStringObj.booleanValue = filterBooleanValue + } + if (['name', 'backupTargetURL', 'credentialSecret', 'pollInterval'].includes(filterField) && filterValue) { + queryStringObj.value = filterValue + } + dispatch(routerRedux.push({ + pathname: '/backupTarget', + search: queryString.stringify(queryStringObj), + })) + } else { + dispatch(routerRedux.push({ + pathname: '/backupTarget', + search: queryString.stringify({}), + })) + } + }, + } + + return ( +
+ +
+ + + + + + + + + {createBackupTargetModalVisible && } + {editBackupTargetModalVisible && } + + ) + } +} + +BackupTarget.propTypes = { + app: PropTypes.object, + backupTarget: PropTypes.object, + loading: PropTypes.bool, + location: PropTypes.object, + dispatch: PropTypes.func, +} + +export default connect(({ app, backupTarget, loading }) => ({ app, backupTarget, loading: loading.models.backupTarget }))(BackupTarget) diff --git a/src/routes/host/HostList.js b/src/routes/host/HostList.js index 8251a905..1adc6492 100755 --- a/src/routes/host/HostList.js +++ b/src/routes/host/HostList.js @@ -375,6 +375,7 @@ class List extends React.Component { title: 'Operation', key: 'operation', width: 120, + fixed: 'right', render: (text, record) => { return ( diff --git a/src/routes/recurringJob/index.js b/src/routes/recurringJob/index.js index 341038ec..28bb3a20 100644 --- a/src/routes/recurringJob/index.js +++ b/src/routes/recurringJob/index.js @@ -121,7 +121,6 @@ class RecurringJob extends React.Component { if (recurringJobs && recurringJobs.length > 0) { recurringJobs.sort((a, b) => a.name.localeCompare(b.name)) } - const createRecurringJobModalProps = { item: this.state.selected, visible: this.state.createRecurringJobModalVisible, diff --git a/src/services/backup.js b/src/services/backup.js index 50150d4b..4e492d5a 100755 --- a/src/services/backup.js +++ b/src/services/backup.js @@ -15,14 +15,6 @@ export async function queryBackupList(name) { }) } -export async function queryTarget(params) { - return request({ - url: '/v1/backuptargets', - method: 'get', - data: params, - }) -} - export async function createVolume(params) { return request({ url: '/v1/volumes', diff --git a/src/services/backupTarget.js b/src/services/backupTarget.js new file mode 100755 index 00000000..7cacc507 --- /dev/null +++ b/src/services/backupTarget.js @@ -0,0 +1,40 @@ +import { request } from '../utils' + +export async function queryBackupTarget(params) { + return request({ + url: '/v1/backuptargets', + method: 'get', + data: params, + }) +} + +export async function createBackupTarget(params) { + return request({ + url: '/v1/backuptargets', + method: 'post', + data: params, + }) +} + +export async function updateBackupTarget(params) { + return request({ + url: `/v1/backuptargets/${params.name}?action=backupTargetUpdate`, + method: 'post', + data: { ...params }, + }) +} + +export async function deleteBackupTarget(params) { + return request({ + url: `/v1/backuptargets/${params.name}`, + method: 'delete', + }) +} + +export async function execAction(url, params) { + return request({ + url, + method: 'post', + data: params, + }) +} diff --git a/src/services/recurringJob.js b/src/services/recurringJob.js index b93801f6..51dd3fe6 100644 --- a/src/services/recurringJob.js +++ b/src/services/recurringJob.js @@ -14,6 +14,7 @@ export async function create(params) { method: 'post', data: { ...params, + backupTargetName: 'default', }, }) } diff --git a/src/utils/dataDependency.js b/src/utils/dataDependency.js index 261ea523..eecefdec 100644 --- a/src/utils/dataDependency.js +++ b/src/utils/dataDependency.js @@ -61,6 +61,13 @@ const dependency = { key: 'recurringjobs', }], }, + backupTarget: { + path: '/backupTarget', + runWs: [{ + ns: 'backupTarget', + key: 'backuptargets', + }], + }, backingImage: { path: '/backingImage', runWs: [{ @@ -134,6 +141,9 @@ const list = [{ }, { ns: 'engineimage', key: 'engineimages', +}, { + ns: 'backupTarget', + key: 'backuptargets', }, { ns: 'recurringJob', key: 'recurringjobs', @@ -158,6 +168,7 @@ const httpDataDependency = { '/engineimage': ['engineimage'], '/recurringJob': ['recurringJob'], '/backingImage': ['volume', 'backingImage'], + '/backupTarget': ['backupTarget'], '/setting': ['setting'], '/backup': ['host', 'setting', 'backingImage', 'backup'], '/instanceManager': ['volume', 'instanceManager'], @@ -168,23 +179,18 @@ const httpDataDependency = { export function getDataDependency(pathName) { let keys = Object.keys(dependency).filter((key) => { if (pathName && dependency[key].path) { - let max = dependency[key].path.length - return dependency[key].path === pathName.substring(0, max) + // let max = dependency[key].path.length + return dependency[key].path === pathName } return false }) - if (keys && keys.length === 1) { let modal = dependency[keys[0]] modal.stopWs = list.filter((item) => { - return modal.runWs.every((ele) => { - return ele.ns !== item.ns - }) + return modal.runWs.every((ele) => ele.ns !== item.ns) }) - return dependency[keys[0]] } - return null } diff --git a/src/utils/formatter.js b/src/utils/formatter.js index 5eb2d776..44caa23d 100644 --- a/src/utils/formatter.js +++ b/src/utils/formatter.js @@ -19,6 +19,28 @@ function formatSi(val, increment = 1024) { return `${out} ${units[exp]}` } + +export function timeDurationStrToInt(time) { + if (time === undefined || time === null || typeof time !== 'string') { + return + } + let timeInSeconds = 0 + const re = /((\d+)h)?((\d+)m)?((\d+)s)?/m + let match = re.exec(time) + if (match != null) { + if (typeof (match[2]) !== 'undefined') { + timeInSeconds += parseInt(match[2], 10) * 60 * 60 + } + if (typeof (match[4]) !== 'undefined') { + timeInSeconds += parseInt(match[4], 10) * 60 + } + if (typeof (match[6]) !== 'undefined') { + timeInSeconds += parseInt(match[6], 10) + } + } + return timeInSeconds +} + export function formatMib(...args) { return formatSi(...args) } diff --git a/src/utils/menu.js b/src/utils/menu.js index d8eae50e..25ac4445 100755 --- a/src/utils/menu.js +++ b/src/utils/menu.js @@ -65,6 +65,12 @@ module.exports = [ name: 'Backing Image', icon: 'file-image', }, + { + show: true, + key: 'backupTarget', + name: 'Backup Target', + icon: 'diff', + }, { show: true, key: 'instanceManager', diff --git a/src/utils/websocket.js b/src/utils/websocket.js index 0e7f8b47..1f9377d3 100644 --- a/src/utils/websocket.js +++ b/src/utils/websocket.js @@ -23,14 +23,19 @@ export function wsChanges(dispatch, type, period, ns, search) { const url = constructWebsocketURL(type, period) const options = { timeout: 4000, - shouldReconnect: function(event, ws) { + shouldReconnect(event, ws) { if (event.code === 1008 || event.code === 1011) return return [0, 3000, 10000][ws.attempts] }, automaticOpen: true, } // To do. Because two ws connections will be maintained under backup ns. - const backupType = type ? type : '' + 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') { @@ -39,7 +44,7 @@ export function wsChanges(dispatch, type, period, ns, search) { } if (backupType === 'backups') { dispatch({ type: `${ns}/updateSocketStatusBackups`, payload: 'connecting' }) - dispatch({ type: `${ns}/updateWsBackups`, payload: { rws, search }}) + dispatch({ type: `${ns}/updateWsBackups`, payload: { rws, search } }) } } else if (ns === 'systemBackups') { if (backupType === 'systembackups') { @@ -69,24 +74,33 @@ export function wsChanges(dispatch, type, period, ns, search) { rws.addEventListener('message', (msg) => { recentWrite = true if (ns === 'backup') { - if (backupType === 'backupvolumes') dispatch({ - type: `${ns}/updateBackgroundBackupVolumes`, - payload: JSON.parse(msg.data), - }) - if (backupType === 'backups') dispatch({ - type: `${ns}/updateBackgroundBackups`, - payload: JSON.parse(msg.data), - }) + if (backupType === 'backupvolumes') { + dispatch({ + type: `${ns}/updateBackgroundBackupVolumes`, + payload: JSON.parse(msg.data), + }) + } + if (backupType === 'backups') { + dispatch({ + type: `${ns}/updateBackgroundBackups`, + payload: JSON.parse(msg.data), + }) + } } else if (ns === 'systemBackups') { - if (backupType === 'systembackups') dispatch({ - type: `${ns}/updateBackgroundSystemBackups`, - payload: JSON.parse(msg.data), - }) - if (backupType === 'systemrestores') dispatch({ - type: `${ns}/updateBackgroundSystemrestores`, - payload: JSON.parse(msg.data), - }) + if (backupType === 'systembackups') { + dispatch({ + type: `${ns}/updateBackgroundSystemBackups`, + payload: JSON.parse(msg.data), + }) + } + if (backupType === 'systemrestores') { + dispatch({ + type: `${ns}/updateBackgroundSystemrestores`, + payload: JSON.parse(msg.data), + }) + } } else { + console.log('dispatch ns/updateBackground') dispatch({ type: `${ns}/updateBackground`, payload: JSON.parse(msg.data), @@ -118,13 +132,11 @@ export function wsChanges(dispatch, type, period, ns, search) { rws.addEventListener('error', () => { if (expectError) { expectError = false + } else if (ns === 'backup') { + if (backupType === 'backupvolumes') dispatch({ type: `${ns}/updateSocketStatusBackupVolumes`, payload: 'error' }) + if (backupType === 'backups') dispatch({ type: `${ns}/updateSocketStatusBackups`, payload: 'error' }) } else { - if (ns === 'backup') { - if (backupType === 'backupvolumes') dispatch({ type: `${ns}/updateSocketStatusBackupVolumes`, payload: 'error' }) - if (backupType === 'backups') dispatch({ type: `${ns}/updateSocketStatusBackups`, payload: 'error' }) - } else { - dispatch({ type: `${ns}/updateSocketStatus`, payload: 'error' }) - } + dispatch({ type: `${ns}/updateSocketStatus`, payload: 'error' }) } }) } @@ -162,7 +174,7 @@ export function getStatusIcon(resource) { } // Backup model has two websocket status. -export function getBackupStatusIcon (resource, type) { +export function getBackupStatusIcon(resource, type) { if (resource === undefined) { return } @@ -194,7 +206,7 @@ export function getBackupStatusIcon (resource, type) { } // System Backup model has two websocket status. -export function getSystemBackupStatusIcon (resource, type) { +export function getSystemBackupStatusIcon(resource, type) { if (resource === undefined) { return } From f78fbff82c6e3f981b59f88cc4040e851127a4a2 Mon Sep 17 00:00:00 2001 From: "andy.lee" Date: Mon, 17 Jun 2024 13:49:59 +0800 Subject: [PATCH 2/4] Allow to choose backup target when create volume backup and recurring job Signed-off-by: andy.lee --- .../BackupLabelInput/BackupLabelInput.js | 92 +++++++++---------- src/models/backupTarget.js | 46 ---------- src/models/snapshot.js | 8 +- src/routes/backupTarget/BackupTargetList.js | 4 +- src/routes/recurringJob/CreateRecurringJob.js | 21 ++++- src/routes/recurringJob/index.js | 7 +- src/routes/volume/detail/CreateBackupModal.js | 54 ++++++++--- src/routes/volume/detail/Snapshots.js | 36 +++++--- src/routes/volume/detail/index.js | 7 +- src/services/recurringJob.js | 1 - src/utils/dataDependency.js | 10 +- 11 files changed, 153 insertions(+), 133 deletions(-) diff --git a/src/components/BackupLabelInput/BackupLabelInput.js b/src/components/BackupLabelInput/BackupLabelInput.js index c7cbe894..2ebdfb1c 100644 --- a/src/components/BackupLabelInput/BackupLabelInput.js +++ b/src/components/BackupLabelInput/BackupLabelInput.js @@ -37,64 +37,62 @@ class BackupLabelInput extends React.Component { render() { const { getFieldDecorator, getFieldValue } = this.props.form - const formItemLayoutWithOutLabel = { - wrapperCol: { - xs: { span: 24, offset: 0 }, - sm: { span: 16, offset: 4 }, - }, + const formItemLayout = { + labelCol: { span: 6 }, + wrapperCol: { span: 18 }, } getFieldDecorator('keys', { initialValue: [] }) const keys = getFieldValue('keys') const formItems = keys.map((k, index) => ( -
- - {getFieldDecorator(`key[${k}]`, { - validateTrigger: ['onChange', 'onBlur'], - rules: [ - { - required: true, - whitespace: true, - message: 'key is required', - }, - ], - })()} - - - {getFieldDecorator(`value[${k}]`, { - validateTrigger: ['onChange', 'onBlur'], - rules: [ - { - required: true, - whitespace: true, - message: 'value is required', - }, - ], - })()} - {keys.length > 0 ? ( - this.remove(k)} - />) : null} +
+ + {getFieldDecorator(`key[${k}]`, { + validateTrigger: ['onChange', 'onBlur'], + rules: [ + { + required: true, + whitespace: true, + message: 'key is required', + }, + ], + })()} + + + {getFieldDecorator(`value[${k}]`, { + validateTrigger: ['onChange', 'onBlur'], + rules: [ + { + required: true, + whitespace: true, + message: 'value is required', + }, + ], + })()} + {keys.length > 0 ? ( + this.remove(k)} + />) : null}
)) return (
- {formItems} - + + {formItems} ) } diff --git a/src/models/backupTarget.js b/src/models/backupTarget.js index 438ad98c..2a8e2c7b 100644 --- a/src/models/backupTarget.js +++ b/src/models/backupTarget.js @@ -1,6 +1,5 @@ import { queryBackupTarget, createBackupTarget, deleteBackupTarget, updateBackupTarget } from '../services/backupTarget' import { message } from 'antd' -// import { delay } from 'dva/saga' import { wsChanges, updateState } from '../utils/websocket' import queryString from 'query-string' import { enableQueryData } from '../utils/dataDependency' @@ -12,14 +11,6 @@ export default { data: [], resourceType: 'backupTarget', selectedRows: [], - // createBackingImageModalVisible: false, - // createBackingImageModalKey: Math.random(), - // diskStateMapDetailModalVisible: false, - // diskStateMapDetailModalKey: Math.random(), - // diskStateMapDeleteDisabled: true, - // diskStateMapDeleteLoading: false, - // selectedDiskStateMapRows: [], - // selectedDiskStateMapRowKeys: [], socketStatus: 'closed', }, subscriptions: { @@ -39,8 +30,6 @@ export default { payload, }, { call, put }) { const data = yield call(queryBackupTarget, payload) - console.log('🚀 ~ data:', data) - console.log('🚀 ~ backupTarget query data:', data) if (payload && payload.field && payload.keyword && data.data) { data.data = data.data.filter(item => item[payload.field] && item[payload.field].indexOf(payload.keyword.trim()) > -1) } @@ -54,7 +43,6 @@ export default { payload, }, { call, put }) { const resp = yield call(createBackupTarget, payload) - // console.log('🚀 ~ create resp:', resp) if (resp && resp.status === 200) { message.success(`Successfully create backup target ${payload.name}.`) } @@ -69,7 +57,6 @@ export default { *edit({ payload, }, { call, put }) { - // console.log('🚀 ~edit payload:', payload) yield call(updateBackupTarget, payload) yield put({ type: 'query' }) }, @@ -85,7 +72,6 @@ export default { payload, }, { select }) { let ws = yield select(state => state.backupTarget.ws) - console.log('🚀 ~ backupTarget startWS ws:', ws) if (ws) { ws.open() } else { @@ -118,38 +104,6 @@ export default { updateBackground(state, action) { return updateState(state, action) }, - // showCreateBackingImageModal(state, action) { - // return { ...state, ...action.payload, createBackingImageModalVisible: true, createBackingImageModalKey: Math.random() } - // }, - // hideCreateBackingImageModal(state) { - // return { ...state, createBackingImageModalVisible: false } - // }, - // showDiskStateMapDetailModal(state, action) { - // return { - // ...state, - // selected: action.payload.record, - // diskStateMapDetailModalVisible: true, - // diskStateMapDetailModalKey: Math.random(), - // } - // }, - // hideDiskStateMapDetailModal(state) { - // return { ...state, diskStateMapDetailModalVisible: false, diskStateMapDetailModalKey: Math.random() } - // }, - // disableDiskStateMapDelete(state) { - // return { ...state, diskStateMapDeleteDisabled: true } - // }, - // enableDiskStateMapDelete(state) { - // return { ...state, diskStateMapDeleteDisabled: false } - // }, - // disableDiskStateMapDeleteLoading(state) { - // return { ...state, diskStateMapDeleteLoading: false } - // }, - // enableDiskStateMapDeleteLoading(state) { - // return { ...state, diskStateMapDeleteLoading: true } - // }, - changeDiskStateMapSelection(state, action) { - return { ...state, ...action.payload } - }, updateSocketStatus(state, action) { return { ...state, socketStatus: action.payload } }, diff --git a/src/models/snapshot.js b/src/models/snapshot.js index a1d74c10..c26c6495 100644 --- a/src/models/snapshot.js +++ b/src/models/snapshot.js @@ -204,9 +204,9 @@ export default (namespace) => { yield put({ type: 'setLoading', payload: true }) const snapshot = yield call(execAction, payload.snapshotCreateUrl, {}) if (Object.getOwnPropertyNames(payload.labels).length === 0) { - yield call(execAction, payload.snapshotBackupUrl, { name: snapshot.name }) + yield call(execAction, payload.snapshotBackupUrl, { name: snapshot.name, backupTargetName: payload.backupTargetName }) } else { - yield call(execAction, payload.snapshotBackupUrl, { name: snapshot.name, labels: payload.labels, backupTargetName: 'andy-minio-backupstore' }) + yield call(execAction, payload.snapshotBackupUrl, { name: snapshot.name, labels: payload.labels, backupTargetName: payload.backupTargetName }) } yield put({ type: 'querySnapShot', payload: { url: payload.querySnapShotUrl } }) yield put({ type: 'setLoading', payload: false }) @@ -216,9 +216,9 @@ export default (namespace) => { }, { call, put }) { yield put({ type: 'setLoading', payload: true }) if (Object.getOwnPropertyNames(payload.labels).length === 0) { - yield call(execAction, payload.snapshotBackupUrl, { name: payload.snapshotName }) + yield call(execAction, payload.snapshotBackupUrl, { name: payload.snapshotName, backupTargetName: payload.backupTargetName }) } else { - yield call(execAction, payload.snapshotBackupUrl, { name: payload.snapshotName, labels: payload.labels }) + yield call(execAction, payload.snapshotBackupUrl, { name: payload.snapshotName, labels: payload.labels, backupTargetName: payload.backupTargetName }) } yield put({ type: 'querySnapShot', payload: { url: payload.querySnapShotUrl } }) yield put({ type: 'setLoading', payload: false }) diff --git a/src/routes/backupTarget/BackupTargetList.js b/src/routes/backupTarget/BackupTargetList.js index 81f8a3d8..2463a7aa 100644 --- a/src/routes/backupTarget/BackupTargetList.js +++ b/src/routes/backupTarget/BackupTargetList.js @@ -69,7 +69,7 @@ function list({ loading, dataSource, deleteBackupTarget, editBackupTarget, rowSe sorter: (a, b) => a.default - b.default, render: (text) => { return ( -
{text.toString().firstUpperCase()}
+
{text.toString().firstUpperCase()}
) }, }, { @@ -81,7 +81,7 @@ function list({ loading, dataSource, deleteBackupTarget, editBackupTarget, rowSe render: (text) => { return (
-
{text.toString().firstUpperCase()}
+
{text.toString().firstUpperCase()}
{text === false && ( diff --git a/src/routes/recurringJob/CreateRecurringJob.js b/src/routes/recurringJob/CreateRecurringJob.js index 424a2067..a9e16e17 100644 --- a/src/routes/recurringJob/CreateRecurringJob.js +++ b/src/routes/recurringJob/CreateRecurringJob.js @@ -46,6 +46,7 @@ const noRetain = (val) => { const modal = ({ item, + availBackupTargets, visible, isEdit, onCancel, @@ -107,7 +108,6 @@ const modal = ({ delete data.keysForlabels } delete data.defaultGroup - onOk(data) }) } @@ -174,6 +174,10 @@ const modal = ({ return getFieldValue('task') === 'backup' || getFieldValue('task') === 'snapshot' } + const showBackupTargetDropdown = () => { + return getFieldValue('task') === 'backup' + } + // init params getFieldDecorator('keys', { initialValue: isEdit && item.groups && item.groups.length > 0 ? item.groups.map((group, index) => { return { initialValue: group, index } }) : [{ index: 0, initialValue: '' }] }) getFieldDecorator('keysForlabels', { initialValue: isEdit && item.labels ? Object.keys(item.labels).map((_, index) => index) : [0] }) @@ -310,6 +314,20 @@ const modal = ({ }
+
+ {showBackupTargetDropdown() + && + {getFieldDecorator('backupTargetName', { + // eslint-disable-next-line no-nested-ternary + initialValue: isEdit ? item.backupTarget : availBackupTargets.length > 0 ? availBackupTargets[0].name : '', + })( + + )} + + } +
{getFieldDecorator('retain', { initialValue: isEdit ? item.retain : 1, @@ -373,6 +391,7 @@ const modal = ({ modal.propTypes = { form: PropTypes.object.isRequired, + availBackupTargets: PropTypes.array, visible: PropTypes.bool, onCancel: PropTypes.func, item: PropTypes.object, diff --git a/src/routes/recurringJob/index.js b/src/routes/recurringJob/index.js index 28bb3a20..d179b18c 100644 --- a/src/routes/recurringJob/index.js +++ b/src/routes/recurringJob/index.js @@ -91,7 +91,8 @@ class RecurringJob extends React.Component { render() { const me = this - const { dispatch, loading, location } = this.props + const { dispatch, loading, location, backupTarget } = this.props + const availAndWritableBackupTarget = backupTarget.data.filter((item) => item.available && !item.readOnly) const { data } = this.props.recurringJob const { field, value } = queryString.parse(this.props.location.search) // Front-end filtering @@ -125,6 +126,7 @@ class RecurringJob extends React.Component { item: this.state.selected, visible: this.state.createRecurringJobModalVisible, isEdit: this.state.isEdit, + availBackupTargets: availAndWritableBackupTarget, onOk(newRecurringJob) { me.setState({ ...me.state, @@ -265,9 +267,10 @@ class RecurringJob extends React.Component { RecurringJob.propTypes = { recurringJob: PropTypes.object, + backupTarget: PropTypes.object, loading: PropTypes.bool, location: PropTypes.object, dispatch: PropTypes.func, } -export default connect(({ recurringJob, loading }) => ({ recurringJob, loading: loading.models.recurringJob }))(RecurringJob) +export default connect(({ recurringJob, backupTarget, loading }) => ({ recurringJob, backupTarget, loading: loading.models.recurringJob }))(RecurringJob) diff --git a/src/routes/volume/detail/CreateBackupModal.js b/src/routes/volume/detail/CreateBackupModal.js index 34f4b27b..fcbbf92b 100644 --- a/src/routes/volume/detail/CreateBackupModal.js +++ b/src/routes/volume/detail/CreateBackupModal.js @@ -1,13 +1,31 @@ import React from 'react' import PropTypes from 'prop-types' -import { Form, Icon } from 'antd' +import { Form, Select, Alert } from 'antd' import { ModalBlur } from '../../../components' import { BackupLabelInput } from '../../../components' +const FormItem = Form.Item +const Option = Select.Option + +const formItemLayout = { + labelCol: { span: 6 }, + wrapperCol: { span: 18 }, +} +const getLabels = (getFieldsValue) => { + const labels = {} + if (getFieldsValue().keys && getFieldsValue().key && getFieldsValue().value) { + getFieldsValue().keys.forEach((item) => { + labels[getFieldsValue().key[item]] = getFieldsValue().value[item] + }) + } + return labels +} + const modal = ({ visible, onCancel, onOk, + availBackupTargets, form: { getFieldDecorator, validateFields, @@ -18,14 +36,12 @@ const modal = ({ }) => { function handleOk() { validateFields((errors) => { - if (errors) { - return - } - let data = {} - if (getFieldsValue().keys && getFieldsValue().key && getFieldsValue().value) { - getFieldsValue().keys.forEach((item) => { - data[getFieldsValue().key[item]] = getFieldsValue().value[item] - }) + if (errors) return + + const labels = getLabels(getFieldsValue) + const data = { + labels, + backupTargetName: getFieldValue('backupTargetName'), } onOk(data) }) @@ -46,15 +62,31 @@ const modal = ({ setFieldsValue, } + const onChangeTask = () => { + + } + return ( -

This could take a while depending on the actual size of the volume and network bandwidth.

- + +
+ + {getFieldDecorator('backupTargetName', { + initialValue: availBackupTargets.length > 0 ? availBackupTargets[0].name : '', + })( + + )} + + +
) } modal.propTypes = { + availBackupTargets: PropTypes.array, form: PropTypes.object.isRequired, visible: PropTypes.bool, onCancel: PropTypes.func, diff --git a/src/routes/volume/detail/Snapshots.js b/src/routes/volume/detail/Snapshots.js index fb39d370..bed6066e 100644 --- a/src/routes/volume/detail/Snapshots.js +++ b/src/routes/volume/detail/Snapshots.js @@ -11,13 +11,14 @@ class Snapshots extends React.Component { super(props) this.state = { createBackModalKey: Math.random(), - createBackBySnapsotModalKey: Math.random(), - createBackModalVisible: false, - createBackBySnapsotModalVisible: false, + createBackBySnapshotModalKey: Math.random(), + createBackModalVisible: false, // click create backup button + createBackBySnapshotModalVisible: false, // click backup button in snapshot dropdown currentSnapshotName: '', snapshotBackupUrl: '', snapshotListUrl: '', } + this.onAction = (action) => { if (action.type === 'backup') { this.setState({ @@ -37,7 +38,7 @@ class Snapshots extends React.Component { if (action.type === 'snapshotBackup') { this.setState({ ...this.state, - createBackBySnapsotModalVisible: true, + createBackBySnapshotModalVisible: true, currentSnapshotName: action.payload && action.payload.snapshot && action.payload.snapshot.name ? action.payload.snapshot.name : '', snapshotBackupUrl: action.payload && action.payload.volume && action.payload.volume.actions && action.payload.volume.actions.snapshotBackup ? action.payload.volume.actions.snapshotBackup : '', snapshotListUrl: action.payload && action.payload.volume && action.payload.volume.actions && action.payload.volume.actions.snapshotList ? action.payload.volume.actions.snapshotList : '', @@ -139,15 +140,16 @@ class Snapshots extends React.Component { item: { frontend: 'iscsi', }, + availBackupTargets: me.props.availBackupTargets, visible: me.state.createBackModalVisible, - onOk(data) { + onOk(params) { me.props.dispatch({ type: 'snapshotModal/backup', payload: { snapshotCreateUrl: me.props.volume.actions.snapshotCreate, snapshotBackupUrl: me.props.volume.actions.snapshotBackup, querySnapShotUrl: me.props.volume.actions.snapshotList, - labels: data, + ...params, }, }) me.setState({ @@ -166,14 +168,15 @@ class Snapshots extends React.Component { } } - createBackupBySnapsotModal = () => { + createBackupBySnapshotModal = () => { let me = this return { item: { frontend: 'iscsi', }, - visible: me.state.createBackBySnapsotModalVisible, - onOk(data) { + availBackupTargets: me.props.availBackupTargets, + visible: me.state.createBackBySnapshotModalVisible, + onOk(params) { if (me.state.snapshotBackupUrl && me.state.currentSnapshotName && me.state.snapshotListUrl) { me.props.dispatch({ type: 'snapshotModal/createBackupBySnapshot', @@ -181,13 +184,13 @@ class Snapshots extends React.Component { snapshotBackupUrl: me.state.snapshotBackupUrl, snapshotName: me.state.currentSnapshotName, querySnapShotUrl: me.state.snapshotListUrl, - labels: data, + ...params, }, }) me.setState({ ...me.state, - createBackBySnapsotModalKey: Math.random(), - createBackBySnapsotModalVisible: false, + createBackBySnapshotModalKey: Math.random(), + createBackBySnapshotModalVisible: false, currentSnapshotName: '', snapshotBackupUrl: '', }) @@ -196,8 +199,8 @@ class Snapshots extends React.Component { onCancel() { me.setState({ ...me.state, - createBackBySnapsotModalKey: Math.random(), - createBackBySnapsotModalVisible: false, + createBackBySnapshotModalKey: Math.random(), + createBackBySnapshotModalVisible: false, currentSnapshotName: '', snapshotBackupUrl: '', }) @@ -210,6 +213,8 @@ 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) => { @@ -308,7 +313,7 @@ class Snapshots extends React.Component { Show System Hidden:   { this.onAction({ type: 'toggleShowRemoved' }) }} checked={this.props.showRemoved} />
{this.state.createBackModalVisible ? : ''} - {this.state.createBackBySnapsotModalVisible ? : ''} + {this.state.createBackBySnapshotModalVisible ? : ''} ) } @@ -326,6 +331,7 @@ Snapshots.propTypes = { backupTargetAvailable: PropTypes.bool, backupTargetMessage: PropTypes.string, volumeHead: PropTypes.object, + availBackupTargets: PropTypes.array, } export default Snapshots diff --git a/src/routes/volume/detail/index.js b/src/routes/volume/detail/index.js index af443e2b..8aa8641a 100644 --- a/src/routes/volume/detail/index.js +++ b/src/routes/volume/detail/index.js @@ -49,7 +49,7 @@ import { const confirm = Modal.confirm -function VolumeDetail({ snapshotModal, dispatch, backup, engineimage, eventlog, host, volume, volumeId, setting, loading, backingImage, recurringJob }) { +function VolumeDetail({ snapshotModal, dispatch, backup, engineimage, backupTarget, eventlog, host, volume, volumeId, setting, loading, backingImage, recurringJob }) { const { data, attachHostModalVisible, @@ -100,6 +100,7 @@ function VolumeDetail({ snapshotModal, dispatch, backup, engineimage, eventlog, const engineImages = engineimage.data const selectedVolume = data.find(item => item.id === volumeId) const currentBackingImage = selectedVolume && selectedVolume.backingImage && backingImage.data ? backingImage.data.find(item => item.name === selectedVolume.backingImage) : null + const availBackupTargets = backupTarget.data.filter(item => item.available && !item.readOnly) const settings = setting.data const defaultDataLocalitySetting = settings.find(s => s.id === 'default-data-locality') const defaultSnapshotDataIntegritySetting = settings.find(s => s.id === 'snapshot-data-integrity') @@ -506,6 +507,7 @@ function VolumeDetail({ snapshotModal, dispatch, backup, engineimage, eventlog, volume: selectedVolume, volumeId, dispatch, + availBackupTargets, backupTargetAvailable, backupTargetMessage, volumeHead: snapshotData.find(d => d.name === 'volume-head'), @@ -698,6 +700,7 @@ VolumeDetail.propTypes = { host: PropTypes.object, engineimage: PropTypes.object, volumeId: PropTypes.string, + backupTarget: PropTypes.object, loading: PropTypes.bool, snapshotModal: PropTypes.object, eventlog: PropTypes.object, @@ -706,4 +709,4 @@ VolumeDetail.propTypes = { recurringJob: PropTypes.object, } -export default connect(({ snapshotModal, backup, host, engineimage, volume, loading, eventlog, setting, backingImage, recurringJob }, { match }) => ({ snapshotModal, backup, host, volume, engineimage, loading: loading.models.volume, volumeId: match.params.id, eventlog, setting, backingImage, recurringJob }))(VolumeDetail) +export default connect(({ snapshotModal, backup, host, backupTarget, engineimage, volume, loading, eventlog, setting, backingImage, recurringJob }, { match }) => ({ snapshotModal, backup, host, volume, backupTarget, engineimage, loading: loading.models.volume, volumeId: match.params.id, eventlog, setting, backingImage, recurringJob }))(VolumeDetail) diff --git a/src/services/recurringJob.js b/src/services/recurringJob.js index 51dd3fe6..b93801f6 100644 --- a/src/services/recurringJob.js +++ b/src/services/recurringJob.js @@ -14,7 +14,6 @@ export async function create(params) { method: 'post', data: { ...params, - backupTargetName: 'default', }, }) } diff --git a/src/utils/dataDependency.js b/src/utils/dataDependency.js index eecefdec..5ca5875b 100644 --- a/src/utils/dataDependency.js +++ b/src/utils/dataDependency.js @@ -45,6 +45,9 @@ const dependency = { }, { ns: 'recurringJob', key: 'recurringjobs', + }, { + ns: 'backupTarget', + key: 'backuptargets', }], }, engineimage: { @@ -59,6 +62,9 @@ const dependency = { runWs: [{ ns: 'recurringJob', key: 'recurringjobs', + }, { + ns: 'backupTarget', + key: 'backuptargets', }], }, backupTarget: { @@ -164,9 +170,9 @@ const list = [{ const httpDataDependency = { '/dashboard': ['volume', 'host', 'eventlog'], '/node': ['volume', 'host', 'setting'], - '/volume': ['volume', 'host', 'setting', 'backingImage', 'engineimage', 'recurringJob', 'backup'], + '/volume': ['volume', 'host', 'setting', 'backupTarget', 'backingImage', 'engineimage', 'recurringJob', 'backup'], '/engineimage': ['engineimage'], - '/recurringJob': ['recurringJob'], + '/recurringJob': ['recurringJob', 'backupTarget'], '/backingImage': ['volume', 'backingImage'], '/backupTarget': ['backupTarget'], '/setting': ['setting'], From 44b63962e1a835e23e5aaf2d2dfa23dc64b1b93b Mon Sep 17 00:00:00 2001 From: "andy.lee" Date: Mon, 17 Jun 2024 17:49:00 +0800 Subject: [PATCH 3/4] Allow user to choose backup target when backup backing image Signed-off-by: andy.lee --- src/assets/images/read-only.svg | 1 + src/models/backingImage.js | 13 ++- src/models/backup.js | 2 +- src/router.js | 1 - .../backingImage/BackingImageActions.js | 11 ++- src/routes/backingImage/BackingImageList.js | 4 +- .../CreateBackupBackingImageModal.js | 80 +++++++++++++++++++ src/routes/backingImage/index.js | 49 +++++++++++- src/routes/backupTarget/BackupTarget.less | 49 ------------ .../backupTarget/BackupTargetActions.js | 4 +- .../backupTarget/BackupTargetBulkActions.js | 4 +- src/routes/backupTarget/BackupTargetList.js | 24 ++++-- .../backupTarget/EditBackupTargetModal.js | 2 +- src/routes/backupTarget/index.js | 2 +- src/routes/host/HostList.js | 1 - src/routes/recurringJob/CreateRecurringJob.js | 21 +++-- .../recurringJob/RecurringJobActions.js | 2 +- src/routes/recurringJob/RecurringJobList.js | 6 +- src/routes/volume/detail/CreateBackupModal.js | 6 +- src/routes/volume/detail/Snapshots.js | 2 - src/services/backingImage.js | 8 ++ src/utils/backupTarget.js | 6 ++ src/utils/dataDependency.js | 5 +- src/utils/formatter.js | 1 - src/utils/menu.js | 2 +- src/utils/websocket.js | 6 -- 26 files changed, 215 insertions(+), 97 deletions(-) create mode 100644 src/assets/images/read-only.svg create mode 100644 src/routes/backingImage/CreateBackupBackingImageModal.js delete mode 100644 src/routes/backupTarget/BackupTarget.less create mode 100644 src/utils/backupTarget.js diff --git a/src/assets/images/read-only.svg b/src/assets/images/read-only.svg new file mode 100644 index 00000000..bd2b8206 --- /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 ec592d47..69f94b50 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 7588c1fe..298b813d 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 234d8a20..5a4fbd52 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 ef519420..38fa6cbf 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 e2801307..355f1767 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 00000000..f83327e5 --- /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 d26fcd4e..50544e6e 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 5065c68f..00000000 --- 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 a3aed9f9..f25680f4 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 67c14e5a..50af2d08 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 2463a7aa..cec17842 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 8bdbbb68..20bee095 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 9ba1b2e8..dc72b233 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 1adc6492..8251a905 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 a9e16e17..dd2e40dc 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 bed6066e..70f8b610 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 057a74ea..b664cc60 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 00000000..0a40837d --- /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 5ca5875b..95bb068f 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 44caa23d..337039d2 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 25ac4445..95edb412 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 1f9377d3..c7843b05 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), From 744ca72284b1e789ab38a87bc47080ba34bee0f1 Mon Sep 17 00:00:00 2001 From: "andy.lee" Date: Wed, 19 Jun 2024 12:14:14 +0800 Subject: [PATCH 4/4] Disable create backup option if no available and writable backup target Signed-off-by: andy.lee --- src/models/backup.js | 13 +++++++++---- src/models/backupTarget.js | 2 ++ src/routes/backingImage/BackingImageActions.js | 11 +++++++---- src/routes/backingImage/BackingImageList.js | 14 +++++++++++++- src/routes/backingImage/index.js | 3 ++- src/routes/backupTarget/index.js | 3 +-- src/routes/recurringJob/CreateRecurringJob.js | 11 ++++++----- src/routes/systemBackups/index.js | 4 +++- .../systemBackups/systemBackupsBulkActions.js | 7 +++++-- src/routes/volume/detail/CreateBackupModal.js | 4 ++-- src/routes/volume/detail/Snapshots.js | 12 ++++++------ src/routes/volume/detail/index.js | 7 +++---- src/utils/backupTarget.js | 6 ++++++ src/utils/dataDependency.js | 5 ++++- 14 files changed, 69 insertions(+), 33 deletions(-) diff --git a/src/models/backup.js b/src/models/backup.js index 298b813d..9f334808 100644 --- a/src/models/backup.js +++ b/src/models/backup.js @@ -23,6 +23,7 @@ export default { lastBackupUrl: '', volumeName: '', backupTargetMessage: '', + backupTargetAvailable: false, previousChecked: false, tagsLoading: true, size: '', @@ -32,7 +33,6 @@ export default { backupVolumesForBulkCreate: [], search: {}, restoreBackupModalVisible: false, - backupTargetAvailable: false, workloadDetailModalVisible: false, createVolumeStandModalVisible: false, bulkCreateVolumeStandModalVisible: false, @@ -104,9 +104,14 @@ export default { payload, }, { call, put }) { const resp = yield call(queryBackupTarget) - if (resp && resp.data && resp.data[0]) { - const backupTargetAvailable = resp.data.some(d => d.available === true) - const backupTargetMessage = backupTargetAvailable ? '' : 'No backup target available' + if (resp && resp.status === 200) { + const backupTargetAvailable = resp?.data?.some(d => d.available === true) || false + const backupTargetMessage = backupTargetAvailable ? '' : 'No backup target is available, please go to Setting -> Backup Target page to create one' + if (payload.history.location.pathname === '/backup' && !backupTargetAvailable) { + message.error(backupTargetMessage, 5) + } else { + message.destroy() + } yield put({ type: 'setBackupTargetAvailable', payload: { backupTargetAvailable, backupTargetMessage } }) } }, diff --git a/src/models/backupTarget.js b/src/models/backupTarget.js index 2a8e2c7b..efc97e3e 100644 --- a/src/models/backupTarget.js +++ b/src/models/backupTarget.js @@ -3,6 +3,7 @@ import { message } from 'antd' import { wsChanges, updateState } from '../utils/websocket' import queryString from 'query-string' import { enableQueryData } from '../utils/dataDependency' +import { delay } from 'dva/saga' export default { ws: null, @@ -58,6 +59,7 @@ export default { payload, }, { call, put }) { yield call(updateBackupTarget, payload) + yield delay(1000) yield put({ type: 'query' }) }, *bulkDelete({ diff --git a/src/routes/backingImage/BackingImageActions.js b/src/routes/backingImage/BackingImageActions.js index 38fa6cbf..13195566 100644 --- a/src/routes/backingImage/BackingImageActions.js +++ b/src/routes/backingImage/BackingImageActions.js @@ -6,7 +6,7 @@ import { hasReadyBackingDisk } from '../../utils/status' const confirm = Modal.confirm -function actions({ selected, deleteBackingImage, downloadBackingImage, openBackupBackingImageModal }) { +function actions({ selected, deleteBackingImage, downloadBackingImage, openBackupBackingImageModal, hasWritableBackupTargets }) { const handleMenuClick = (event, record) => { event.domEvent?.stopPropagation?.() switch (event.key) { @@ -29,11 +29,14 @@ function actions({ selected, deleteBackingImage, downloadBackingImage, openBacku } } - const disableDownloadAction = !hasReadyBackingDisk(selected) + const disableAction = !hasReadyBackingDisk(selected) + + const disabledBackupAction = !hasWritableBackupTargets || !hasReadyBackingDisk(selected) + const backupTargetMessageTooltip = !hasWritableBackupTargets ? 'No backup target is available and writable' : 'Missing disk with ready state' const availableActions = [ - { key: 'download', name: 'Download', disabled: disableDownloadAction, tooltip: disableDownloadAction ? 'Missing disk with ready state' : '' }, - { key: 'backup', name: 'Backup' }, + { key: 'download', name: 'Download', disabled: disableAction, tooltip: disableAction ? 'Missing disk with ready state' : '' }, + { key: 'backup', name: 'Backup', disabled: disabledBackupAction, tooltip: disabledBackupAction ? backupTargetMessageTooltip : '' }, { key: 'delete', name: 'Delete' }, ] diff --git a/src/routes/backingImage/BackingImageList.js b/src/routes/backingImage/BackingImageList.js index 355f1767..55f21c7a 100644 --- a/src/routes/backingImage/BackingImageList.js +++ b/src/routes/backingImage/BackingImageList.js @@ -5,11 +5,22 @@ import BackingImageActions from './BackingImageActions' import { pagination } from '../../utils/page' import { formatMib } from '../../utils/formatter' -function list({ loading, dataSource, openBackupBackingImageModal, deleteBackingImage, showDiskStateMapDetail, rowSelection, downloadBackingImage, height }) { +function list({ + loading, + dataSource, + openBackupBackingImageModal, + deleteBackingImage, + showDiskStateMapDetail, + rowSelection, + downloadBackingImage, + height, + hasWritableBackupTargets, +}) { const backingImageActionsProps = { deleteBackingImage, downloadBackingImage, openBackupBackingImageModal, + hasWritableBackupTargets, } const state = (record) => { if (record.deletionTimestamp) { @@ -111,6 +122,7 @@ list.propTypes = { openBackupBackingImageModal: PropTypes.func, rowSelection: PropTypes.object, height: PropTypes.number, + hasWritableBackupTargets: PropTypes.bool, } export default list diff --git a/src/routes/backingImage/index.js b/src/routes/backingImage/index.js index 50544e6e..2886dcfe 100644 --- a/src/routes/backingImage/index.js +++ b/src/routes/backingImage/index.js @@ -10,7 +10,7 @@ import CreateBackupBackingImageModal from './CreateBackupBackingImageModal' import { Filter } from '../../components/index' import BackingImageBulkActions from './BackingImageBulkActions' import queryString from 'query-string' -import { getAvailBackupTargets } from '../../utils/backupTarget' +import { getAvailBackupTargets, hasWritableBackupTargets } from '../../utils/backupTarget' import style from './BackingImage.less' import C from '../../utils/constants' @@ -117,6 +117,7 @@ class BackingImage extends React.Component { const backingImageListProps = { dataSource: backingImages, height: this.state.height, + hasWritableBackupTargets: hasWritableBackupTargets(backupTarget), loading, deleteBackingImage(record) { dispatch({ diff --git a/src/routes/backupTarget/index.js b/src/routes/backupTarget/index.js index dc72b233..660c97f8 100644 --- a/src/routes/backupTarget/index.js +++ b/src/routes/backupTarget/index.js @@ -70,7 +70,6 @@ class BackupTarget extends React.Component { selectedEditRow: record, editBackupTargetModalVisible: true, }) - this.handleEditModalOpen() } handleEditModalClose = () => { @@ -216,7 +215,7 @@ class BackupTarget extends React.Component { - + {createBackupTargetModalVisible && } {editBackupTargetModalVisible && } diff --git a/src/routes/recurringJob/CreateRecurringJob.js b/src/routes/recurringJob/CreateRecurringJob.js index dd2e40dc..6b602e7e 100644 --- a/src/routes/recurringJob/CreateRecurringJob.js +++ b/src/routes/recurringJob/CreateRecurringJob.js @@ -61,9 +61,11 @@ const modal = ({ setFieldsValue, }, }) => { + const isBackupTask = () => getFieldValue('task') === 'backup' || getFieldValue('task') === 'backup-force-create' + function handleOk() { validateFields((errors) => { - if (errors) { + if (errors || (isBackupTask() && getFieldValue('backupTargetName') === '')) { return } const data = { @@ -108,6 +110,7 @@ const modal = ({ delete data.keysForlabels } delete data.defaultGroup + onOk(data) }) } @@ -174,9 +177,6 @@ const modal = ({ return getFieldValue('task') === 'backup' || getFieldValue('task') === 'snapshot' } - const showBackupTargetDropdown = () => { - return getFieldValue('task') === 'backup' || getFieldValue('task') === 'backup-force-create' - } // init params getFieldDecorator('keys', { initialValue: isEdit && item.groups && item.groups.length > 0 ? item.groups.map((group, index) => { return { initialValue: group, index } }) : [{ index: 0, initialValue: '' }] }) @@ -315,7 +315,7 @@ const modal = ({ }
- {showBackupTargetDropdown() + {isBackupTask() && {getFieldDecorator('backupTargetName', { // eslint-disable-next-line no-nested-ternary @@ -323,6 +323,7 @@ const modal = ({ rules: [ { required: true, + message: 'Please select a backup target', }, ], })( diff --git a/src/routes/systemBackups/index.js b/src/routes/systemBackups/index.js index a4fb028e..16f59177 100644 --- a/src/routes/systemBackups/index.js +++ b/src/routes/systemBackups/index.js @@ -104,6 +104,7 @@ class SystemBackups extends React.Component { const SystemBackupsBulkActionProps = { selectedRows: this.state.selectedSystemBackupsRows, + backupTarget: this.props.backupTarget, deleteSystemBackups() { dispatch({ type: 'systemBackups/bulkDeleteSystemBackup', @@ -264,6 +265,7 @@ SystemBackups.propTypes = { dispatch: PropTypes.func, systemBackups: PropTypes.object, location: PropTypes.object, + backupTarget: PropTypes.object, } -export default connect(({ systemBackups, loading }) => ({ systemBackups, loading: loading.models.systemBackups }))(SystemBackups) +export default connect(({ systemBackups, loading, backupTarget }) => ({ systemBackups, backupTarget, loading: loading.models.systemBackups }))(SystemBackups) diff --git a/src/routes/systemBackups/systemBackupsBulkActions.js b/src/routes/systemBackups/systemBackupsBulkActions.js index e322a0e3..72f96345 100644 --- a/src/routes/systemBackups/systemBackupsBulkActions.js +++ b/src/routes/systemBackups/systemBackupsBulkActions.js @@ -2,10 +2,12 @@ import React from 'react' import PropTypes from 'prop-types' import { Button, Modal } from 'antd' import style from './systemBackupsBulkActions.less' +import { hasWritableBackupTargets } from '../../utils/backupTarget' const confirm = Modal.confirm -function bulkActions({ selectedRows, deleteSystemBackups, createSystemBackup }) { +function bulkActions({ selectedRows, deleteSystemBackups, createSystemBackup, backupTarget }) { + const createBtnDisable = !hasWritableBackupTargets(backupTarget) const handleClick = (action) => { switch (action) { case 'create': @@ -23,7 +25,7 @@ function bulkActions({ selectedRows, deleteSystemBackups, createSystemBackup }) } } const allActions = [ - { key: 'create', name: 'Create' }, + { key: 'create', name: 'Create', disabled: createBtnDisable }, { key: 'delete', name: 'Delete', disabled: selectedRows.length === 0 }, ] @@ -45,6 +47,7 @@ bulkActions.propTypes = { selectedRows: PropTypes.array, deleteSystemBackups: PropTypes.func, createSystemBackup: PropTypes.func, + backupTarget: PropTypes.object, } export default bulkActions diff --git a/src/routes/volume/detail/CreateBackupModal.js b/src/routes/volume/detail/CreateBackupModal.js index 5c429958..37124e6c 100644 --- a/src/routes/volume/detail/CreateBackupModal.js +++ b/src/routes/volume/detail/CreateBackupModal.js @@ -37,11 +37,11 @@ const modal = ({ function handleOk() { validateFields((errors) => { if (errors) return - const labels = getLabels(getFieldsValue) + const backupTargetName = getFieldValue('backupTargetName') const data = { labels, - backupTargetName: getFieldValue('backupTargetName'), + backupTargetName, } onOk(data) }) diff --git a/src/routes/volume/detail/Snapshots.js b/src/routes/volume/detail/Snapshots.js index 70f8b610..e80531e4 100644 --- a/src/routes/volume/detail/Snapshots.js +++ b/src/routes/volume/detail/Snapshots.js @@ -225,11 +225,13 @@ class Snapshots extends React.Component { } const upgradingEngine = () => this.props.volume.currentImage !== this.props.volume.image - const disableBackup = !this.props.volume.actions || !this.props.volume.actions.snapshotCreate || !this.props.state || this.props.volume.standby || isRestoring() || upgradingEngine() || !this.props.backupTargetAvailable + const disableBackup = !this.props.volume.actions || !this.props.volume.actions.snapshotCreate || !this.props.state || this.props.volume.standby || isRestoring() || upgradingEngine() || this.props.availBackupTargets.length === 0 + + const createSnapshotDisabled = disabledSnapshotAction(this.props.volume, this.props.state) || this.props.volume.standby || isRestoring() || upgradingEngine() const createBackupTooltipMessage = () => { - if (!this.props.backupTargetAvailable) { - return this.props.backupTargetMessage + if (this.props.availBackupTargets.length === 0) { + return 'No backup target is available and writable.' } if (this.props.volume.standby) { return 'Unable to create backup for DR volume.' @@ -285,7 +287,7 @@ class Snapshots extends React.Component {
Snapshots and Backups
-