diff --git a/src/components/Snapshot/Snapshot.js b/src/components/Snapshot/Snapshot.js index 5482c162..ac340cf9 100644 --- a/src/components/Snapshot/Snapshot.js +++ b/src/components/Snapshot/Snapshot.js @@ -31,10 +31,11 @@ function VolumeHead(props) { Volume Head - ) : (
+ ) : ( +
+
) ) } @@ -49,7 +50,6 @@ VolumeHead.propTypes = { // render each snapshot action dropdown function SnapshotIcon(props, snapshotProps) { function doAction(key) { - console.log('🚀 ~ doAction ~ key:', key) snapshotProps.onAction({ type: key, payload: { @@ -260,7 +260,6 @@ class Snapshot extends React.Component { render() { let props = this.props let children = null - console.log('props.snapshotTree', this.props.snapshotTree) if (props.snapshotTree) { children = props.snapshotTree.length > 0 ? loop(props.snapshotTree, props) : if (props.loading || this.state.loadingState !== props.loading) { diff --git a/src/models/snapshot.js b/src/models/snapshot.js index 68ab4760..e30aeaeb 100644 --- a/src/models/snapshot.js +++ b/src/models/snapshot.js @@ -163,7 +163,6 @@ export default (namespace) => { *queryVolume({ payload, }, { put }) { - console.log('🚀 ~queryVolume payload:', payload) const data = payload.volume if (data && data.actions) { yield put({ type: 'setVolume', payload: data }) @@ -173,7 +172,6 @@ export default (namespace) => { *querySnapShot({ payload, }, { call, put }) { - console.log('🚀 ~querySnapShot payload:', payload) if (!payload.url) { yield put({ type: 'setSnapshotData', payload: [] }) yield put({ type: 'setSnapshot', payload: [] }) diff --git a/src/models/volume.js b/src/models/volume.js index 6ebcb018..b8d5732b 100755 --- a/src/models/volume.js +++ b/src/models/volume.js @@ -1,11 +1,13 @@ import { create, deleteVolume, query, execAction, createVolumePV, createVolumePVC, createVolumeAllPVC, volumeActivate, getNodeTags, getDiskTags, expandVolume, cancelExpansion, createRecurringJob, recurringJobAdd, getVolumeRecurringJobList, removeVolumeRecurringJob, updateRecurringJob } from '../services/volume' import { query as getRecurringJob } from '../services/recurringJob' import { wsChanges, updateState } from '../utils/websocket' +import { message } from 'antd' import { sortVolume } from '../utils/sort' import { routerRedux } from 'dva/router' import { getSorter, saveSorter } from '../utils/store' import queryString from 'query-string' import { enableQueryData } from '../utils/dataDependency' +import { sortSnapshots } from '../utils/sort' export default { namespace: 'volume', @@ -13,7 +15,11 @@ export default { ws: null, data: [], resourceType: 'volume', + cloneVolumeType: 'volume', // volume or snapshot + snapshotsOptions: {}, + snapshotLoading: true, selected: null, + selectSnapshot: null, selectedRows: [], WorkloadDetailModalItem: {}, volumeRecurringJobs: [], @@ -205,14 +211,18 @@ export default { *createClonedVolume({ payload, }, { call, put }) { - yield put({ type: 'hideVolumeCloneModal' }) - yield call(create, payload) - yield put({ type: 'query' }) + yield put({ type: 'hideCloneVolumeModal' }) + const resp = yield call(create, payload) + if (resp && resp.status === 200) { + message.success(`New volume (${payload.name}) created successfully`, 5) + yield put({ type: 'query' }) + } }, - *showCreateVolumeModalBefore({ + *showCloneVolumeModalBefore({ payload, }, { call, put }) { - yield put({ type: 'showCreateVolumeModal' }) + yield put({ type: 'showCloneVolumeModal', payload }) + const nodeTags = yield call(getNodeTags, payload) const diskTags = yield call(getDiskTags, payload) if (nodeTags.status === 200 && diskTags.status === 200) { @@ -221,6 +231,34 @@ export default { yield put({ type: 'changeTagsLoading', payload: { tagsLoading: false } }) } }, + *showCreateVolumeModalBefore({ + payload, + }, { call, put, all }) { + yield put({ type: 'showCreateVolumeModal' }) + // TODO: longhorn manager should have an API to get all volume's snapshots or add snapshotList array in GET /v1/volumes + const snapshotListRequests = payload.filter(item => item.actions.snapshotList).map(item => call(execAction, item.actions.snapshotList)) + const snapshotResp = yield all(snapshotListRequests) + if (snapshotResp && snapshotResp.length > 0 && snapshotResp.every(resp => resp.status === 200)) { + // construct snapshots data by volume name + const snapshotsOptions = {} + for (const resp of snapshotResp) { + if (resp?.links?.self) { + const vol = resp?.links.self.split('/').pop() + const snapshots = resp.data.filter(d => d.name !== 'volume-head') // no include volume-head + sortSnapshots(snapshots) + snapshotsOptions[vol] = snapshots + } + } + yield put({ type: 'setSnapshotsData', payload: { snapshotsOptions, snapshotLoading: false } }) + } + const nodeTags = yield call(getNodeTags) + const diskTags = yield call(getDiskTags) + if (nodeTags.status === 200 && diskTags.status === 200) { + yield put({ type: 'changeTagsLoading', payload: { nodeTags: nodeTags.data, diskTags: diskTags.data, tagsLoading: false } }) + } else { + yield put({ type: 'changeTagsLoading', payload: { tagsLoading: false } }) + } + }, *delete({ payload, }, { call, put }) { @@ -682,6 +720,9 @@ export default { updateBackground(state, action) { return updateState(state, action) }, + setSnapshotsData(state, action) { + return { ...state, ...action.payload } + }, showChangeVolumeModal(state, action) { return { ...state, changeVolumeActivate: action.payload, changeVolumeModalVisible: true, changeVolumeModalKey: Math.random() } }, @@ -755,7 +796,7 @@ export default { hideRecurringJobModal(state) { return { ...state, recurringJobModalVisible: false, recurringJobModalKey: Math.random() } }, - showVolumeCloneModal(state, action) { + showCloneVolumeModal(state, action) { return { ...state, ...action.payload, volumeCloneModalVisible: true, volumeCloneModalKey: Math.random() } }, showAttachHostModal(state, action) { @@ -782,7 +823,7 @@ export default { showBulkEngineUpgradeModal(state, action) { return { ...state, ...action.payload, bulkEngineUpgradeModalVisible: true, bulkEngineUpgradeModalKey: Math.random() } }, - hideVolumeCloneModal(state) { + hideCloneVolumeModal(state) { return { ...state, volumeCloneModalVisible: false } }, hideEngineUpgradeModal(state) { diff --git a/src/router.js b/src/router.js index 6637123f..2ce5a001 100755 --- a/src/router.js +++ b/src/router.js @@ -114,7 +114,6 @@ const Routers = function ({ history, app }) { - {/* */} diff --git a/src/routes/volume/CloneVolume.js b/src/routes/volume/CloneVolume.js new file mode 100644 index 00000000..2180e3e6 --- /dev/null +++ b/src/routes/volume/CloneVolume.js @@ -0,0 +1,274 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { + Form, + Input, + InputNumber, + Select, + Checkbox, + Spin, + Alert, +} from 'antd' +import { ModalBlur } from '../../components' +import { frontends } from './helper/index' +import { formatSize } from '../../utils/formatter' + +const FormItem = Form.Item +const { Option } = Select + +const formItemLayout = { + labelCol: { span: 6 }, + wrapperCol: { span: 13 }, +} + +const genOkData = (getFieldsValue, getFieldValue, volume, cloneType) => { + const dataSourceResult = getFieldValue('dataSource') + const dataSource = cloneType === 'volume' ? `vol://${dataSourceResult}` : `snap://${volume.name}/${dataSourceResult}` + const data = { + ...volume, + ...getFieldsValue(), + size: volume.size, + dataSource, + } + if (data.unit) { + delete data.unit + } + return data +} + + +const modal = ({ + cloneType = 'volume', // 'volume' or 'snapshot' + volume = {}, + snapshot = {}, + visible, + onCancel, + onOk, + nodeTags, + diskTags, + defaultDataLocalityOption, + defaultDataLocalityValue, + backingImageOptions, + tagsLoading, + v1DataEngineEnabled, + v2DataEngineEnabled, + form: { + getFieldDecorator, + validateFields, + getFieldsValue, + getFieldValue, + }, +}) => { + function handleOk() { + validateFields((errors) => { + if (errors) { + return + } + const data = genOkData(getFieldsValue, getFieldValue, volume, cloneType) + onOk(data) + }) + } + + const modalOpts = { + title: cloneType === 'volume' ? `Clone Volume from ${volume.name}` : `Clone Volume from ${volume.name} snapshot ${snapshot.name}`, + visible, + onCancel, + width: 880, + onOk: handleOk, + style: { top: 0 }, + } + + return ( + + +
+ + {getFieldDecorator('name', { + initialValue: `cloned-${volume.name}`, + rules: [ + { + required: true, + message: 'Please input volume name', + }, + ], + })()} + +
+ + {getFieldDecorator('size', { + initialValue: formatSize(volume), + rules: [ + { + required: true, + message: 'Please input volume size', + }, { + validator: (_rule, value, callback) => { + if (value === '' || typeof value !== 'number') { + callback() + return + } + if (value < 0 || value > 65536) { + callback('The value should be between 0 and 65535') + } else if (!/^\d+([.]\d{1,2})?$/.test(value)) { + callback('This value should have at most two decimal places') + } else if (value < 10 && getFieldsValue().unit === 'Mi') { + callback('The volume size must be greater than 10 Mi') + } else if (value % 1 !== 0 && getFieldsValue().unit === 'Mi') { + callback('Decimals are not allowed') + } else { + callback() + } + }, + }, + ], + })()} + + + {getFieldDecorator('unit', { + initialValue: volume.unit || 'Gi', + rules: [{ required: true, message: 'Please select your unit!' }], + })( + , + )} + +
+ + {getFieldDecorator('numberOfReplicas', { + initialValue: volume.numberOfReplicas, + rules: [ + { + required: true, + message: 'Please input the number of replicas', + }, + { + validator: (rule, value, callback) => { + if (value === '' || typeof value !== 'number') { + callback() + return + } + if (value < 1 || value > 10) { + callback('The value should be between 1 and 10') + } else if (!/^\d+$/.test(value)) { + callback('The value must be a positive integer') + } else { + callback() + } + }, + }, + ], + })()} + + + {getFieldDecorator('frontend', { + initialValue: volume.frontend || frontends[0].value, + rules: [ + { + required: true, + message: 'Please select a frontend', + }, + ], + })()} + + + {getFieldDecorator('dataLocality', { + initialValue: volume.dataLocality || defaultDataLocalityValue, + })()} + + + {getFieldDecorator('accessMode', { + initialValue: volume.accessMode || 'rwo', + })()} + + + {getFieldDecorator('backingImage', { + initialValue: volume.backingImage || '', + })()} + + + {getFieldDecorator('dataSource', { + initialValue: cloneType === 'volume' ? volume?.name : snapshot?.name, + })( + + + )} + + + {getFieldDecorator('encrypted', { + valuePropName: 'checked', + initialValue: volume.encrypted || false, + })()} + + + + {getFieldDecorator('nodeSelector', { + initialValue: volume.nodeSelector || [], + })()} + + + + + {getFieldDecorator('diskSelector', { + initialValue: volume.diskSelector || [], + })()} + + +
+
+ ) +} + +modal.propTypes = { + cloneType: PropTypes.string, + volume: PropTypes.object, + snapshot: PropTypes.object, + form: PropTypes.object.isRequired, + visible: PropTypes.bool, + onCancel: PropTypes.func, + onOk: PropTypes.func, + nodeTags: PropTypes.array, + diskTags: PropTypes.array, + defaultDataLocalityOption: PropTypes.array, + defaultDataLocalityValue: PropTypes.string, + tagsLoading: PropTypes.bool, + v1DataEngineEnabled: PropTypes.bool, + v2DataEngineEnabled: PropTypes.bool, + backingImageOptions: PropTypes.array, +} + +export default Form.create()(modal) diff --git a/src/routes/volume/CreateVolume.js b/src/routes/volume/CreateVolume.js index ded55989..649aec32 100644 --- a/src/routes/volume/CreateVolume.js +++ b/src/routes/volume/CreateVolume.js @@ -10,9 +10,13 @@ import { Collapse, Tooltip, Icon, + Alert, + Popover, } from 'antd' import { ModalBlur } from '../../components' import { frontends } from './helper/index' +import { formatSize } from '../../utils/formatter' +import { formatDate } from '../../utils/formatDate' const FormItem = Form.Item const { Panel } = Collapse @@ -38,9 +42,64 @@ const formItemLayoutForAdvanced = { const dataSourceOptions = ['Volume', 'Volume Snapshot'] +const getDataSource = (getFieldValue) => { + let dataSource = '' + const dataSourceType = getFieldValue('dataSourceType') || '' + const dataSourceVol = getFieldValue('dataSourceVolume') || '' + const dataSourceSnapshot = getFieldValue('dataSourceSnapshot') || '' + if (dataSourceType && dataSourceVol) { + switch (dataSourceType) { + case 'Volume': + dataSource = `vol://${dataSourceVol}` + break + case 'Volume Snapshot': + dataSource = dataSourceSnapshot ? `snap://${dataSourceVol}/${dataSourceSnapshot}` : '' + break + default: + } + } + return dataSource +} + +const getSize = (getFieldValue, volumeOptions) => { + const dataSourceType = getFieldValue('dataSourceType') || '' + const dataSourceVol = getFieldValue('dataSourceVolume') || '' + const dataSourceSnapshot = getFieldValue('dataSourceSnapshot') || '' + + if (dataSourceType && dataSourceType === dataSourceOptions[0] && dataSourceVol) { + const sourceVolSize = volumeOptions.find(vol => vol.name === dataSourceVol)?.size || 0 + return sourceVolSize + } + + if (dataSourceType && dataSourceType === dataSourceOptions[1] && dataSourceVol && dataSourceSnapshot) { + const sourceVolSize = volumeOptions.find(vol => vol.name === dataSourceVol)?.size || 0 + return sourceVolSize + } + + return `${getFieldValue('size')}${getFieldValue('unit')}` +} + +const genOkData = (getFieldsValue, getFieldValue, volumeOptions) => { + const data = { + ...getFieldsValue(), + dataSource: getDataSource(getFieldValue), + size: getSize(getFieldValue, volumeOptions), + snapshotMaxSize: `${getFieldsValue().snapshotMaxSize}${getFieldsValue().snapshotSizeUnit}`, + } + if (data.dataSourceType) { + delete data.dataSourceType + } + if (data.unit) { + delete data.unit + } + return data +} + + const modal = ({ item, - volumes, + volumeOptions = [], + snapshotsOptions = {}, visible, onCancel, onOk, @@ -50,8 +109,9 @@ const modal = ({ defaultRevisionCounterValue, defaultSnapshotDataIntegrityOption, diskTags, - backingImages, + backingImageOptions, tagsLoading, + snapshotLoading, v1DataEngineEnabled, v2DataEngineEnabled, form: { @@ -62,38 +122,12 @@ const modal = ({ setFieldsValue, }, }) => { - console.log('🚀 ~ createVolumes volumes:', volumes) function handleOk() { validateFields((errors) => { if (errors) { return } - let dataSourceValue = '' - if (getFieldValue('dataSource')) { - switch (getFieldValue('dataSourceType')) { - case 'Volume': - dataSourceValue = `vol://${getFieldValue('dataSource')}` - break - case 'Volume Snapshot': - dataSourceValue = `snap://${123}/${getFieldValue('dataSource')}` - break - default: - } - } - const data = { - ...getFieldsValue(), - dataSource: dataSourceValue, - size: `${getFieldsValue().size}${getFieldsValue().unit}`, - snapshotMaxSize: `${getFieldsValue().snapshotMaxSize}${getFieldsValue().snapshotSizeUnit}`, - } - - if (data.dataSourceType) { - delete data.dataSourceType - } - if (data.unit) { - delete data.unit - } - console.log('🚀 ~ validateFields ~ data:', data) + const data = genOkData(getFieldsValue, getFieldValue, volumeOptions) onOk(data) }) } @@ -121,13 +155,34 @@ const modal = ({ size: currentSize, }) } + const displayDataSourceAlert = () => { + const selectedVol = getFieldValue('dataSourceVolume') + if (selectedVol === '' || selectedVol === undefined) { + return false + } else { + return true + } + } + + const handleDataSourceVolumeChange = (value) => { + const dataSourceVol = volumeOptions.find(vol => vol.name === value) + if (dataSourceVol) { + // set size field according to the selected data source + setFieldsValue({ + ...getFieldsValue(), + size: formatSize(dataSourceVol), + }) + } + } + const targetVolumeSnaps = snapshotsOptions[getFieldValue('dataSourceVolume')] || [] + const dataSourceAlertMsg = 'The volume size is set to the selected volume size. Mismatched size will cause create volume failed.' return (
{getFieldDecorator('name', { - initialValue: item.name, + initialValue: item.name || '', rules: [ { required: true, @@ -137,51 +192,50 @@ const modal = ({ })()}
- - {getFieldDecorator('size', { - initialValue: item.size, - rules: [ - { - required: true, - message: 'Please input volume size', - }, { - validator: (rule, value, callback) => { - if (value === '' || typeof value !== 'number') { - callback() - return - } - if (value < 0 || value > 65536) { - callback('The value should be between 0 and 65535') - } else if (!/^\d+([.]\d{1,2})?$/.test(value)) { - callback('This value should have at most two decimal places') - } else if (value < 10 && getFieldsValue().unit === 'Mi') { - callback('The volume size must be greater than 10 Mi') - } else if (value % 1 !== 0 && getFieldsValue().unit === 'Mi') { - callback('Decimals are not allowed') - } else { - callback() - } - }, + + {getFieldDecorator('size', { + initialValue: item.size, + rules: [ + { + required: true, + message: 'Please input volume size', + }, { + validator: (rule, value, callback) => { + if (value === '' || typeof value !== 'number') { + callback() + return + } + if (value < 0 || value > 65536) { + callback('The value should be between 0 and 65535') + } else if (!/^\d+([.]\d{1,2})?$/.test(value)) { + callback('This value should have at most two decimal places') + } else if (value < 10 && getFieldsValue().unit === 'Mi') { + callback('The volume size must be greater than 10 Mi') + } else if (value % 1 !== 0 && getFieldsValue().unit === 'Mi') { + callback('Decimals are not allowed') + } else { + callback() + } }, - ], - })()} - - - {getFieldDecorator('unit', { - initialValue: item.unit, - rules: [{ required: true, message: 'Please select your unit!' }], - })( - , - )} - + }, + ], + })()} + + + {getFieldDecorator('unit', { + initialValue: item.unit || 'Gi', + rules: [{ required: true, message: 'Please select your unit!' }], + })( + , + )} +
- {getFieldDecorator('numberOfReplicas', { initialValue: item.numberOfReplicas, @@ -239,32 +293,68 @@ const modal = ({ {getFieldDecorator('backingImage', { initialValue: '', - })( + { backingImageOptions.map(backingImage => ) } )} - - {getFieldDecorator('dataSourceType', { - initialValue: dataSourceOptions[0], - })( - - )} - {getFieldDecorator('dataSource', { - initialValue: '', - })( - - )} - + +
+ + Data Source + + + + + + } + hasFeedback + {...formItemLayout}> + {getFieldDecorator('dataSourceType', { initialValue: '' })( + + )} + + {getFieldValue('dataSourceType') && ( + +
+ } + > + + {getFieldDecorator('dataSourceVolume', { initialValue: '' })( + + )} + + + )} +
+ {getFieldValue('dataSourceType') === dataSourceOptions[1] && + {getFieldDecorator('dataSourceSnapshot', { initialValue: '' })( + + )} + + } + {getFieldDecorator('dataEngine', { - initialValue: 'v1', + initialValue: item.dataEngine || 'v1', rules: [ { - validator: (rule, value, callback) => { + validator: (_rule, value, callback) => { if (value === 'v1' && !v1DataEngineEnabled) { callback('v1 data engine is not enabled') } else if (value === 'v2' && !v2DataEngineEnabled) { @@ -281,7 +371,7 @@ const modal = ({ {getFieldDecorator('encrypted', { - valuePropName: 'encrypted', + valuePropName: 'checked', initialValue: false, })()} @@ -303,21 +393,22 @@ const modal = ({ )} + {/* Advanced Configurations */} {getFieldDecorator('snapshotDataIntegrity', { initialValue: 'ignored', })()} Snapshot Max Count @@ -338,8 +429,8 @@ const modal = ({ Snapshot Max Size @@ -357,7 +448,7 @@ const modal = ({ {getFieldDecorator('snapshotSizeUnit', { - initialValue: item.unit, + initialValue: item.unit || 'Gi', rules: [{ required: true, message: 'Please select your unit!' }], })( )} - - - - ) -} - -modal.propTypes = { - selectedVolume: PropTypes.object, - form: PropTypes.object.isRequired, - visible: PropTypes.bool, - option: PropTypes.array, - onCancel: PropTypes.func, - onOk: PropTypes.func, -} - -export default Form.create()(modal) diff --git a/src/routes/volume/detail/Snapshots.js b/src/routes/volume/detail/Snapshots.js index 7541bd2f..e5406f68 100644 --- a/src/routes/volume/detail/Snapshots.js +++ b/src/routes/volume/detail/Snapshots.js @@ -21,16 +21,12 @@ class Snapshots extends React.Component { this.onAction = (action) => { if (action.type === 'cloneVolumeFromSnapshot') { const { volume, snapshot } = action.payload - if (!volume || !snapshot) { return } - console.log('🚀 ~ Snapshots ~ this.onAction ~ cloneVolumeFromSnapshot:', { - ...volume, - dataSource: `snap://${volume.name}/${snapshot.name}`, - }) this.props.dispatch({ - type: 'volume/createClonedVolume', + type: 'volume/showCloneVolumeModalBefore', payload: { - ...volume, - dataSource: `snap://${volume.name}/${snapshot.name}`, + selectedSnapshot: snapshot, + selected: volume, + cloneVolumeType: 'snapshot', }, }) return @@ -48,14 +44,6 @@ class Snapshots extends React.Component { }) this.refs.Snapshot.showReomve() } - if (action.type === 'createVolumeFromSnapshot') { - this.props.dispatch({ - type: 'volume/createClonedVolume', - payload: { - - }, - }) - } if (action.type === 'snapshotBackup') { this.setState({ ...this.state, diff --git a/src/routes/volume/detail/index.js b/src/routes/volume/detail/index.js index f911ab30..8acaef19 100644 --- a/src/routes/volume/detail/index.js +++ b/src/routes/volume/detail/index.js @@ -23,6 +23,7 @@ import Snapshots from './Snapshots' import RecurringJob from './RecurringJob' import EventList from './EventList' import SnapshotList from './SnapshotList' +import CloneVolume from '../CloneVolume' import CreatePVAndPVCSingle from '../CreatePVAndPVCSingle' import ChangeVolumeModal from '../ChangeVolumeModal' import ExpansionVolumeSizeModal from '../ExpansionVolumeSizeModal' @@ -30,6 +31,7 @@ import UpdateReplicaAutoBalanceModal from '../UpdateReplicaAutoBalanceModal' import CommonModal from '../components/CommonModal' import Salvage from '../Salvage' import { ReplicaList, ExpansionErrorDetail } from '../../../components' +import { hasReadyBackingDisk } from '../../../utils/status' import { getAttachHostModalProps, getEngineUpgradeModalProps, @@ -49,9 +51,72 @@ import { const confirm = Modal.confirm +const hasReplica = (selected, name) => { + if (selected && selected.replicas && selected.replicas.length > 0) { + return selected.replicas.some((item) => item.name === name) + } + return false +} +const hasEngine = (selected, name) => { + if (selected && selected.replicas && selected.replicas.length > 0) { + return selected.controllers.some((item) => item.name === name) + } + return false +} + +function filterEventLogByName(eventlog, selectedVolume) { + if (!eventlog || !eventlog.data) { + return [] + } + return eventlog.data.filter((ele) => { + if (ele?.event?.involvedObject.name && ele.event.involvedObject.kind && selectedVolume) { + switch (ele.event.involvedObject.kind) { + case 'Engine': + return hasEngine(selectedVolume, ele.event.involvedObject.name) + case 'Replica': + return hasReplica(selectedVolume, ele.event.involvedObject.name) + case 'Volume': + return selectedVolume.id === ele.event.involvedObject.name + default: + return false + } + } + return false + }) +} + +const mapEventLogs = (eventLogs) => { + return eventLogs?.map((ele, index) => ({ + count: ele?.event?.count || '', + firstTimestamp: ele?.event?.firstTimestamp || '', + lastTimestamp: ele?.event?.lastTimestamp || '', + timestamp: ele?.event?.lastTimestamp ? Date.parse(ele.event.lastTimestamp) : 0, + type: ele?.event?.type || '', + reason: ele?.event?.reason || '', + message: ele?.event?.message || '', + source: ele?.event?.source?.component || '', + kind: ele?.event?.involvedObject?.kind || '', + name: ele?.event?.involvedObject?.name || '', + id: index, + })) || [] +} + +const getEventData = (eventlog, selectedVolume) => { + const eventLogs = filterEventLogByName(eventlog, selectedVolume) + const mappedEventLogs = mapEventLogs(eventLogs) + mappedEventLogs.sort((a, b) => b.timestamp - a.timestamp) + return mappedEventLogs +} + function VolumeDetail({ snapshotModal, dispatch, backup, engineimage, eventlog, host, volume, volumeId, setting, loading, backingImage, recurringJob }) { const { data, + cloneVolumeType, + selectedSnapshot, + selected, + nodeTags, + diskTags, + tagsLoading, attachHostModalVisible, engineUpgradeModalVisible, salvageModalVisible, @@ -67,6 +132,7 @@ function VolumeDetail({ snapshotModal, dispatch, backup, engineimage, eventlog, changeVolumeActivate, changeVolumeModalVisible, previousChecked, + volumeCloneModalVisible, expansionVolumeSizeModalVisible, expansionVolumeSizeModalKey, updateDataLocalityModalVisible, @@ -95,81 +161,44 @@ function VolumeDetail({ snapshotModal, dispatch, backup, engineimage, eventlog, } = volume const { backupStatus, backupTargetAvailable, backupTargetMessage } = backup const { data: snapshotData, state: snapshotModalState } = snapshotModal - console.log('🚀 ~ VolumeDetail ~ snapshotModalState:', snapshotModalState) - console.log('🚀 ~ VolumeDetail ~ snapshotData:', snapshotData) const { data: recurringJobData } = recurringJob const hosts = host.data 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 backingImageOptions = backingImage?.data?.filter(image => hasReadyBackingDisk(image)) || [] const settings = setting.data const defaultDataLocalitySetting = settings.find(s => s.id === 'default-data-locality') const defaultSnapshotDataIntegritySetting = settings.find(s => s.id === 'snapshot-data-integrity') const engineUpgradePerNodeLimit = settings.find(s => s.id === 'concurrent-automatic-engine-upgrade-per-node-limit') - const defaultDataLocalityOption = defaultDataLocalitySetting && defaultDataLocalitySetting.definition && defaultDataLocalitySetting.definition.options ? defaultDataLocalitySetting.definition.options : [] - const defaultSnapshotDataIntegrityOption = defaultSnapshotDataIntegritySetting?.definition?.options ? defaultSnapshotDataIntegritySetting.definition.options.map((item) => { return { key: item.firstUpperCase(), value: item } }) : [] + + const defaultDataLocalityOption = defaultDataLocalitySetting?.definition?.options || [] + const defaultDataLocalityValue = defaultDataLocalitySetting?.value || 'disabled' + + const defaultSnapshotDataIntegrityOption = defaultSnapshotDataIntegritySetting?.definition?.options.map((item) => ({ key: item.firstUpperCase(), value: item })) || [] + if (defaultSnapshotDataIntegrityOption.length > 0) { - defaultSnapshotDataIntegrityOption.push({ key: 'Ignored (Follow the global setting)', value: 'ignored' }) + defaultSnapshotDataIntegrityOption.unshift({ key: 'Ignored (Follow the global setting)', value: 'ignored' }) } - const hasReplica = (selected, name) => { - if (selected && selected.replicas && selected.replicas.length > 0) { - return selected.replicas.some((item) => { - return item.name === name - }) - } - return false - } - const hasEngine = (selected, name) => { - if (selected && selected.replicas && selected.replicas.length > 0) { - return selected.controllers.some((item) => { - return item.name === name - }) - } + 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' - return false - } - const eventData = eventlog && eventlog.data ? eventlog.data.filter((ele) => { - if (ele.event && ele.event.involvedObject && ele.event.involvedObject.name && ele.event.involvedObject.kind && selectedVolume) { - switch (ele.event.involvedObject.kind) { - case 'Engine': - return hasEngine(selectedVolume, ele.event.involvedObject.name) - case 'Replica': - return hasReplica(selectedVolume, ele.event.involvedObject.name) - case 'Volume': - return selectedVolume.id === ele.event.involvedObject.name - default: - return false - } - } + const eventData = getEventData(eventlog, selectedVolume) - return false - }).map((ele, index) => { - return { - count: ele.event ? ele.event.count : '', - firstTimestamp: ele.event && ele.event.firstTimestamp ? ele.event.firstTimestamp : '', - lastTimestamp: ele.event && ele.event.lastTimestamp ? ele.event.lastTimestamp : '', - timestamp: ele.event && ele.event.lastTimestamp ? Date.parse(ele.event.lastTimestamp) : 0, - type: ele.event ? ele.event.type : '', - reason: ele.event ? ele.event.reason : '', - message: ele.event ? ele.event.message : '', - source: ele.event && ele.event.source && ele.event.source.component ? ele.event.source.component : '', - kind: ele.event && ele.event.involvedObject && ele.event.involvedObject.kind ? ele.event.involvedObject.kind : '', - name: ele.event && ele.event.involvedObject && ele.event.involvedObject.name ? ele.event.involvedObject.name : '', - id: index, - } - }) : [] - eventData.sort((a, b) => { - return b.timestamp - a.timestamp - }) if (!selectedVolume) { return (
) } + const found = hosts.find(h => selectedVolume.controller && h.id === selectedVolume.controller.hostId) if (found) { selectedVolume.host = found.name } selectedVolume.replicas.forEach(replica => { replica.volState = selectedVolume.state }) + const replicaListProps = { dataSource: selectedVolume.replicas || [], purgeStatus: selectedVolume.purgeStatus || [], @@ -229,14 +258,14 @@ function VolumeDetail({ snapshotModal, dispatch, backup, engineimage, eventlog, }, }) }, - // showVolumeCloneModal(record) { - // dispatch({ - // type: 'volume/showVolumeCloneModal', - // payload: { - // selected: record, - // }, - // }) - // }, + showVolumeCloneModal(record) { + dispatch({ + type: 'volume/showCloneVolumeModalBefore', + payload: { + selected: record, + }, + }) + }, showAttachHost(record) { dispatch({ type: 'volume/showAttachHostModal', @@ -645,6 +674,34 @@ function VolumeDetail({ snapshotModal, dispatch, backup, engineimage, eventlog, }, } + const volumeCloneModalBySnapshotProps = { + cloneType: cloneVolumeType, + volume: selected, + snapshot: selectedSnapshot, + visible: volumeCloneModalVisible, + diskTags, + nodeTags, + tagsLoading, + v1DataEngineEnabled, + v2DataEngineEnabled, + defaultDataLocalityOption, + defaultDataLocalityValue, + backingImageOptions, + onOk(vol) { + if (vol) { + dispatch({ + type: 'volume/createClonedVolume', + payload: vol, + }) + } + }, + onCancel() { + dispatch({ + type: 'volume/hideCloneVolumeModal', + }) + }, + } + return (
@@ -677,6 +734,7 @@ function VolumeDetail({ snapshotModal, dispatch, backup, engineimage, eventlog, + {volumeCloneModalVisible && } {attachHostModalVisible && } {detachHostModalVisible && } {engineUpgradeModalVisible && } diff --git a/src/routes/volume/index.js b/src/routes/volume/index.js index b00f834b..e3954a93 100644 --- a/src/routes/volume/index.js +++ b/src/routes/volume/index.js @@ -6,13 +6,16 @@ import moment from 'moment' import { Row, Col, Button, Modal, Alert } from 'antd' import queryString from 'query-string' import VolumeList from './VolumeList' -import CreateVolume from './CreateVolume' -import CustomColumn from './CustomColumn' import ExpansionVolumeSizeModal from './ExpansionVolumeSizeModal' import ChangeVolumeModal from './ChangeVolumeModal' import BulkChangeVolumeModal from './BulkChangeVolumeModal' +import CreateVolume from './CreateVolume' +import CloneVolume from './CloneVolume' +import CustomColumn from './CustomColumn' import CreatePVAndPVC from './CreatePVAndPVC' import CreatePVAndPVCSingle from './CreatePVAndPVCSingle' +import CreateBackupModal from './detail/CreateBackupModal' +import CommonModal from './components/CommonModal' import WorkloadDetailModal from './WorkloadDetailModal' import RecurringJobModal from './RecurringJobModal' import AttachHost from './AttachHost' @@ -36,9 +39,6 @@ import UpdateBulkDataLocality from './UpdateBulkDataLocality' import Salvage from './Salvage' import { Filter, ExpansionErrorDetail } from '../../components/index' import VolumeBulkActions from './VolumeBulkActions' -import CreateBackupModal from './detail/CreateBackupModal' -import CommonModal from './components/CommonModal' -import VolumeCloneModal from './VolumeCloneModal' import { getAttachHostModalProps, getEngineUpgradeModalProps, @@ -119,11 +119,11 @@ class Volume extends React.Component { render() { const me = this const { dispatch, loading, location } = this.props - const { data: snapshotData, state: snapshotModalState } = this.props.snapshotModal - console.log('🚀 ~ Volume ~ snapshotModalState:', snapshotModalState) - console.log('🚀 ~ Volume ~ snapshotData:', snapshotData) const { selected, + snapshotsOptions, + snapshotLoading, + cloneVolumeType, selectedRows, data, createPVAndPVCVisible, @@ -230,16 +230,18 @@ class Volume extends React.Component { if (replicaSoftAntiAffinitySetting) { replicaSoftAntiAffinitySettingValue = replicaSoftAntiAffinitySetting?.value.toLowerCase() === 'true' } - const defaultDataLocalityOption = defaultDataLocalitySetting?.definition?.options ? defaultDataLocalitySetting.definition.options : [] - const defaultDataLocalityValue = defaultDataLocalitySetting?.value ? defaultDataLocalitySetting.value : 'disabled' + const defaultDataLocalityOption = defaultDataLocalitySetting?.definition?.options || [] + const defaultDataLocalityValue = defaultDataLocalitySetting?.value || 'disabled' const defaultRevisionCounterValue = defaultRevisionCounterSetting?.value === 'true' - const defaultSnapshotDataIntegrityOption = defaultSnapshotDataIntegritySetting?.definition?.options ? defaultSnapshotDataIntegritySetting.definition.options.map((item) => { return { key: item.firstUpperCase(), value: item } }) : [] - const v1DataEngineEnabled = v1DataEngineEnabledSetting?.value === 'true' - const v2DataEngineEnabled = v2DataEngineEnabledSetting?.value === 'true' + const defaultSnapshotDataIntegrityOption = defaultSnapshotDataIntegritySetting?.definition?.options.map((item) => ({ key: item.firstUpperCase(), value: item })) || [] if (defaultSnapshotDataIntegrityOption.length > 0) { defaultSnapshotDataIntegrityOption.push({ key: 'Ignored (Follow the global setting)', value: 'ignored' }) } + const backingImageOptions = backingImages?.filter(image => hasReadyBackingDisk(image)) || [] + const v1DataEngineEnabled = v1DataEngineEnabledSetting?.value === 'true' + const v2DataEngineEnabled = v2DataEngineEnabledSetting?.value === 'true' + const volumeFilterMap = { healthy: healthyVolume, inProgress: inProgressVolume, @@ -248,6 +250,7 @@ class Volume extends React.Component { faulted: faultedVolume, } let volumes = data + // TODO: extract these filter functions to a separate file if (field && field === 'status' && volumeFilterMap[stateValue]) { volumes = volumeFilterMap[stateValue](volumes) } else if (field && field === 'engineImageUpgradable') { @@ -287,8 +290,6 @@ class Volume extends React.Component { }) } - console.log('🚀 ~ Volume ~ render ~ all volumes:', volumes) - const volumeListProps = { dataSource: volumes, loading, @@ -319,9 +320,10 @@ class Volume extends React.Component { }, showVolumeCloneModal(record) { dispatch({ - type: 'volume/showVolumeCloneModal', + type: 'volume/showCloneVolumeModalBefore', payload: { selected: record, + cloneVolumeType: 'volume', }, }) }, @@ -626,7 +628,6 @@ class Volume extends React.Component { }, } - const volumeFilterProps = { location, stateOption: [ @@ -788,7 +789,8 @@ class Volume extends React.Component { diskSelector: [], nodeSelector: [], }, - volumes, + volumeOptions: volumes, + snapshotsOptions, nodeTags, defaultDataLocalityOption, defaultDataLocalityValue, @@ -797,7 +799,8 @@ class Volume extends React.Component { v1DataEngineEnabled, v2DataEngineEnabled, diskTags, - backingImages: backingImages?.filter(image => hasReadyBackingDisk(image)) || [], + backingImageOptions, + snapshotLoading, tagsLoading, hosts, visible: createVolumeModalVisible, @@ -1169,23 +1172,28 @@ class Volume extends React.Component { } const volumeCloneModalProps = { - selectedVolume: selected, + cloneType: cloneVolumeType, + volume: selected, visible: volumeCloneModalVisible, + diskTags, + nodeTags, + tagsLoading, + v1DataEngineEnabled, + v2DataEngineEnabled, + defaultDataLocalityOption, + defaultDataLocalityValue, + backingImageOptions, onOk(volume) { - if (selected.name && volume) { + if (volume) { dispatch({ type: 'volume/createClonedVolume', - payload: { - ...volume, - name: volume.name, - dataSource: `vol://${selected.name}`, - }, + payload: volume, }) } }, onCancel() { dispatch({ - type: 'volume/hideVolumeCloneModal', + type: 'volume/hideCloneVolumeModal', }) }, } @@ -1193,11 +1201,7 @@ class Volume extends React.Component { const addVolume = () => { dispatch({ type: 'volume/showCreateVolumeModalBefore', - }) - this.setState({ - CreateVolumeGen() { - return - }, + payload: volumes, }) } @@ -1274,7 +1278,7 @@ class Volume extends React.Component { {detachHostModalVisible ? : ''} {engineUpgradeModalVisible ? : ''} {bulkEngineUpgradeModalVisible ? : ''} - {volumeCloneModalVisible ? : ''} + {volumeCloneModalVisible ? : ''} {salvageModalVisible ? : ''} {updateReplicaCountModalVisible ? : ''} {updateBulkReplicaCountModalVisible ? : ''} diff --git a/src/services/volume.js b/src/services/volume.js index 25d69a91..e9af3c4b 100755 --- a/src/services/volume.js +++ b/src/services/volume.js @@ -18,7 +18,6 @@ export async function execAction(url, params, silence = false) { } export async function create(params) { - console.log('🚀 ~ create params:', params) return request({ url: '/v1/volumes', method: 'post', diff --git a/src/utils/formatDate.js b/src/utils/formatDate.js index 2124d1fc..9add64cc 100644 --- a/src/utils/formatDate.js +++ b/src/utils/formatDate.js @@ -15,7 +15,7 @@ function utcStrToDate(utcStr) { export function formatDate(date, hasTooltip = true) { // Initial date return null - if (date === '0001-01-01 00:00:00 +0000 UTC') { + if (date === '0001-01-01 00:00:00 +0000 UTC' || !date) { return '' } let gmt = utcStrToDate(date) diff --git a/src/utils/formatter.js b/src/utils/formatter.js index 5eb2d776..bdb35791 100644 --- a/src/utils/formatter.js +++ b/src/utils/formatter.js @@ -23,6 +23,15 @@ export function formatMib(...args) { return formatSi(...args) } +// Convert selected size Bi to Gi, return size in Gi +export function formatSize(selected, unit = 'Gi') { + if (selected && selected.size) { + const sizeMi = parseInt(selected.size, 10) / (1024 * 1024) + return unit === 'Gi' ? Number((sizeMi / 1024).toFixed(2)) : parseInt(sizeMi, 10) + } + return 0 +} + export function utcStrToDate(utcStr) { const reg = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) \+\d{4} UTC$/ const results = utcStr.match(reg) diff --git a/src/utils/sort.js b/src/utils/sort.js index 89af0861..020a54bc 100644 --- a/src/utils/sort.js +++ b/src/utils/sort.js @@ -46,6 +46,12 @@ export function sortVolume(dataSource) { }) } +export function sortSnapshots(dataSource) { + dataSource && dataSource.sort((a, b) => { + return new Date(b.created).getTime() - new Date(a.created).getTime() + }) +} + export function sortBackups(dataSource) { dataSource && dataSource.sort((a, b) => { return new Date(b.created).getTime() - new Date(a.created).getTime()