1. Auto select correct volume type when change snapshot 2. Show confirm modal when first change volume type used by snapshot Change-Id: I37a70414e835c8c187ce5edd858b83c2273d5ca8
582 lines
15 KiB
JavaScript
582 lines
15 KiB
JavaScript
// Copyright 2021 99cloud
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import React from 'react';
|
|
import { inject, observer } from 'mobx-react';
|
|
import Confirm from 'components/Confirm';
|
|
import { getSinceTime } from 'utils/time';
|
|
import { volumeStatus, multiTip, snapshotTypeTip } from 'resources/volume';
|
|
import globalSnapshotStore from 'stores/cinder/snapshot';
|
|
import globalImageStore from 'stores/glance/image';
|
|
import globalVolumeStore from 'stores/cinder/volume';
|
|
import globalVolumeTypeStore from 'stores/cinder/volume-type';
|
|
import globalBackupStore from 'stores/cinder/backup';
|
|
import { InputNumber, Badge } from 'antd';
|
|
import { toJS } from 'mobx';
|
|
import { FormAction } from 'containers/Action';
|
|
import classnames from 'classnames';
|
|
import { isFinite } from 'lodash';
|
|
import {
|
|
getImageSystemTabs,
|
|
getImageOS,
|
|
getImageColumns,
|
|
canImageCreateIronicInstance,
|
|
canImageCreateInstance,
|
|
} from 'resources/image';
|
|
import { volumeTypeSelectProps } from 'resources/volume-type';
|
|
import styles from './index.less';
|
|
|
|
@inject('rootStore')
|
|
@observer
|
|
export default class Create extends FormAction {
|
|
init() {
|
|
this.snapshotStore = globalSnapshotStore;
|
|
this.imageStore = globalImageStore;
|
|
this.volumeStore = globalVolumeStore;
|
|
this.volumeTypeStore = globalVolumeTypeStore;
|
|
this.backupstore = globalBackupStore;
|
|
this.getQuota();
|
|
this.getAvailZones();
|
|
this.getImages();
|
|
this.getVolumeTypes();
|
|
this.state = {
|
|
...this.state,
|
|
count: 1,
|
|
sharedDisabled: false,
|
|
confirmCount: 0,
|
|
};
|
|
}
|
|
|
|
static id = 'volume-create';
|
|
|
|
static title = t('Create Volume');
|
|
|
|
static path = '/storage/volume/create';
|
|
|
|
get listUrl() {
|
|
return '/storage/volume';
|
|
}
|
|
|
|
get name() {
|
|
return t('create volume');
|
|
}
|
|
|
|
static policy = 'volume:create';
|
|
|
|
static allowed() {
|
|
return Promise.resolve(true);
|
|
}
|
|
|
|
get instanceName() {
|
|
const { name } = this.values || {};
|
|
const { count = 1 } = this.state;
|
|
if (count === 1) {
|
|
return name;
|
|
}
|
|
return new Array(count)
|
|
.fill(count)
|
|
.map((_, index) => `${name}-${index + 1}`)
|
|
.join(', ');
|
|
}
|
|
|
|
get errorText() {
|
|
const { status } = this.state;
|
|
if (status === 'error') {
|
|
return t(
|
|
'Unable to create volume: insufficient quota to create resources.'
|
|
);
|
|
}
|
|
return super.errorText;
|
|
}
|
|
|
|
get defaultValue() {
|
|
const size = this.quotaIsLimit && this.maxSize < 10 ? this.maxSize : 10;
|
|
const { initVolumeType } = this.state;
|
|
const values = {
|
|
source: this.sourceTypes[0],
|
|
size,
|
|
project: this.currentProjectName,
|
|
availableZone: (this.availableZones[0] || []).value,
|
|
volume_type: initVolumeType,
|
|
};
|
|
return values;
|
|
}
|
|
|
|
get availableZones() {
|
|
const availableZonesList = [{ label: t('Not select'), value: 'noSelect' }];
|
|
(this.volumeStore.availabilityZones || [])
|
|
.filter((it) => it.zoneState.available)
|
|
.forEach((it) => {
|
|
availableZonesList.push({
|
|
value: it.zoneName,
|
|
label: it.zoneName,
|
|
});
|
|
});
|
|
return availableZonesList;
|
|
}
|
|
|
|
get images() {
|
|
const { imageTab } = this.state;
|
|
const images = (this.imageStore.list.data || []).filter((it) => {
|
|
if (!canImageCreateInstance(it) && !canImageCreateIronicInstance(it)) {
|
|
return false;
|
|
}
|
|
if (imageTab) {
|
|
return getImageOS(it) === imageTab && it.status === 'active';
|
|
}
|
|
return it;
|
|
});
|
|
return images.map((it) => ({
|
|
...it,
|
|
key: it.id,
|
|
}));
|
|
}
|
|
|
|
get volumeTypes() {
|
|
return toJS(this.volumeTypeStore.list.data || []);
|
|
}
|
|
|
|
get backups() {
|
|
return (this.backupstore.list.data || []).map((it) => ({
|
|
...it,
|
|
key: it.id,
|
|
}));
|
|
}
|
|
|
|
get sourceTypes() {
|
|
return [
|
|
{ label: t('Blank Volume'), value: 'blank-volume' },
|
|
{ label: t('Image'), value: 'image' },
|
|
{ label: t('Snapshot'), value: 'snapshot' },
|
|
// { label: t('Backup'), value: 'backup' },
|
|
];
|
|
}
|
|
|
|
get quota() {
|
|
const { volumes: { limit = 10, in_use = 0 } = {} } =
|
|
toJS(this.volumeStore.quotaSet) || {};
|
|
if (limit === -1) {
|
|
return Infinity;
|
|
}
|
|
return limit - in_use;
|
|
}
|
|
|
|
get quotaIsLimit() {
|
|
const { gigabytes: { limit } = {} } = toJS(this.volumeStore.quotaSet) || {};
|
|
return limit !== -1;
|
|
}
|
|
|
|
get maxSize() {
|
|
const { gigabytes: { limit = 10, in_use = 0 } = {} } =
|
|
toJS(this.volumeStore.quotaSet) || {};
|
|
return limit - in_use;
|
|
}
|
|
|
|
getAvailZones() {
|
|
this.volumeStore.fetchAvailabilityZoneList();
|
|
}
|
|
|
|
getImages() {
|
|
this.imageStore.fetchList({ all_projects: this.hasAdminRole });
|
|
}
|
|
|
|
async getVolumeTypes() {
|
|
const types = await this.volumeTypeStore.fetchList();
|
|
if (types.length > 0) {
|
|
const initVolumeType = {
|
|
selectedRowKeys: [types[0].id],
|
|
selectedRows: [types[0]],
|
|
};
|
|
this.setState(
|
|
{
|
|
initVolumeType,
|
|
},
|
|
() => {
|
|
this.updateFormValue('volume_type', initVolumeType);
|
|
this.updateDefaultValue();
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
async getQuota() {
|
|
await this.volumeStore.fetchQuota();
|
|
this.onCountChange(1);
|
|
this.updateDefaultValue();
|
|
}
|
|
|
|
onImageTabChange = (value) => {
|
|
this.setState({
|
|
imageTab: value,
|
|
});
|
|
};
|
|
|
|
get systemTabs() {
|
|
return getImageSystemTabs();
|
|
}
|
|
|
|
getVolumeTypeExtra() {
|
|
if (this.sourceTypeIsSnapshot) {
|
|
return snapshotTypeTip;
|
|
}
|
|
const { multiattach = false } = this.state;
|
|
return multiattach ? multiTip : undefined;
|
|
}
|
|
|
|
onConfirmCancel = () => {
|
|
const { initVolumeType } = this.state;
|
|
const { selectedRows, selectedRowKeys, snapshotId } = initVolumeType;
|
|
const newValue = {
|
|
selectedRows,
|
|
selectedRowKeys,
|
|
snapshotId: `${snapshotId}-1`,
|
|
};
|
|
this.setState({
|
|
initVolumeType: newValue,
|
|
});
|
|
};
|
|
|
|
onVolumeTypeChange = (value) => {
|
|
const { selectedRows = [] } = value;
|
|
if (selectedRows.length === 0) {
|
|
this.setState({
|
|
multiattach: false,
|
|
});
|
|
return;
|
|
}
|
|
const { id, extra_specs: { multiattach = 'False' } = {} } = selectedRows[0];
|
|
if (this.sourceTypeIsSnapshot) {
|
|
const {
|
|
initVolumeType: { selectedRowKeys = [] },
|
|
confirmCount = 0,
|
|
} = this.state;
|
|
if (id !== selectedRowKeys[0] && confirmCount < 1) {
|
|
Confirm.warn({
|
|
title: t('Note: Are you sure you need to modify the volume type?'),
|
|
content: snapshotTypeTip,
|
|
onCancel: this.onConfirmCancel,
|
|
});
|
|
this.setState({
|
|
confirmCount: 1,
|
|
});
|
|
}
|
|
}
|
|
this.setState({
|
|
multiattach: multiattach === '<is> True',
|
|
});
|
|
};
|
|
|
|
get sourceTypeIsImage() {
|
|
const { source } = this.state;
|
|
return source === this.sourceTypes[1].value;
|
|
}
|
|
|
|
get sourceTypeIsSnapshot() {
|
|
const { source } = this.state;
|
|
return source === this.sourceTypes[2].value;
|
|
}
|
|
|
|
getDiskMinSize() {
|
|
let imageSize = 0;
|
|
if (this.sourceTypeIsImage) {
|
|
const { min_disk = 0, size = 0 } = this.state.image || {};
|
|
const sizeGB = Math.ceil(size / 1024 / 1024 / 1024);
|
|
imageSize = Math.max(min_disk, sizeGB, 1);
|
|
} else if (this.sourceTypeIsSnapshot) {
|
|
const { size = 0 } = this.state.snapshot || {};
|
|
imageSize = size;
|
|
}
|
|
return Math.max(imageSize, 1);
|
|
}
|
|
|
|
onSnapshotChange = (value) => {
|
|
const { selectedRows = [] } = value || {};
|
|
if (selectedRows.length) {
|
|
const { origin_data: { volume_type_id } = {}, id } =
|
|
selectedRows[0] || {};
|
|
const volumeType = this.volumeTypes.find(
|
|
(it) => it.id === volume_type_id
|
|
);
|
|
if (volumeType) {
|
|
const newValue = {
|
|
selectedRowKeys: [volume_type_id],
|
|
selectedRows: [volumeType],
|
|
snapshotId: id,
|
|
};
|
|
this.setState({
|
|
initVolumeType: newValue,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
get nameForStateUpdate() {
|
|
return ['source', 'image', 'snapshot'];
|
|
}
|
|
|
|
get formItems() {
|
|
const { initVolumeType } = this.state;
|
|
const minSize = this.getDiskMinSize();
|
|
return [
|
|
{
|
|
name: 'project',
|
|
label: t('Project'),
|
|
type: 'label',
|
|
},
|
|
{
|
|
name: 'availableZone',
|
|
label: t('Available Zone'),
|
|
type: 'select',
|
|
placeholder: t('Please select'),
|
|
options: this.availableZones,
|
|
tip: t(
|
|
'Unless you know clearly which AZ to create the volume in, you don not need to fill in here.'
|
|
),
|
|
},
|
|
{
|
|
type: 'divider',
|
|
},
|
|
{
|
|
name: 'source',
|
|
label: t('Data Source Type'),
|
|
type: 'radio',
|
|
options: this.sourceTypes,
|
|
required: true,
|
|
isWrappedValue: true,
|
|
},
|
|
{
|
|
name: 'image',
|
|
label: t('Operating System'),
|
|
type: 'select-table',
|
|
datas: this.images,
|
|
required: this.sourceTypeIsImage,
|
|
isMulti: false,
|
|
hidden: !this.sourceTypeIsImage,
|
|
filterParams: [
|
|
{
|
|
label: t('Name'),
|
|
name: 'name',
|
|
},
|
|
],
|
|
columns: getImageColumns(this),
|
|
tabs: this.systemTabs,
|
|
defaultTabValue: this.systemTabs[0].value,
|
|
selectedLabel: t('Image'),
|
|
onTabChange: this.onImageTabChange,
|
|
},
|
|
{
|
|
name: 'snapshot',
|
|
label: t('Snapshot'),
|
|
type: 'select-table',
|
|
backendPageStore: this.snapshotStore,
|
|
required: this.sourceTypeIsSnapshot,
|
|
isMulti: false,
|
|
hidden: !this.sourceTypeIsSnapshot,
|
|
isSortByBack: true,
|
|
defaultSortKey: 'created_at',
|
|
defaultSortOrder: 'descend',
|
|
onChange: this.onSnapshotChange,
|
|
filterParams: [
|
|
{
|
|
label: t('Name'),
|
|
name: 'name',
|
|
},
|
|
],
|
|
columns: [
|
|
{
|
|
title: t('Name'),
|
|
dataIndex: 'name',
|
|
},
|
|
{
|
|
title: t('Size'),
|
|
dataIndex: 'size',
|
|
render: (value) => `${value}GB`,
|
|
sorter: false,
|
|
},
|
|
{
|
|
title: t('Status'),
|
|
dataIndex: 'status',
|
|
render: (value) => volumeStatus[value] || '-',
|
|
},
|
|
{
|
|
title: t('Description'),
|
|
dataIndex: 'description',
|
|
sorter: false,
|
|
},
|
|
{
|
|
title: t('Created At'),
|
|
dataIndex: 'created_at',
|
|
render: (time) => getSinceTime(time),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: 'divider',
|
|
},
|
|
{
|
|
name: 'volume_type',
|
|
label: t('Volume Type'),
|
|
type: 'select-table',
|
|
tip: t(
|
|
'The volume type needs to set "multiattach" in the metadata to support shared volume attributes.'
|
|
),
|
|
...volumeTypeSelectProps,
|
|
datas: this.volumeTypes,
|
|
required: true,
|
|
extra: this.getVolumeTypeExtra(),
|
|
onChange: this.onVolumeTypeChange,
|
|
initValue: initVolumeType,
|
|
},
|
|
{
|
|
name: 'size',
|
|
label: t('Capacity (GB)'),
|
|
type: 'slider-input',
|
|
max: this.maxSize,
|
|
min: minSize,
|
|
description: `${minSize}GB-${this.maxSize}GB`,
|
|
required: this.quotaIsLimit,
|
|
hidden: !this.quotaIsLimit,
|
|
onChange: this.onChangeSize,
|
|
},
|
|
{
|
|
name: 'size',
|
|
label: t('Capacity (GB)'),
|
|
type: 'input-int',
|
|
min: minSize,
|
|
hidden: this.quotaIsLimit,
|
|
required: !this.quotaIsLimit,
|
|
onChange: this.onChangeSize,
|
|
},
|
|
{
|
|
type: 'divider',
|
|
},
|
|
{
|
|
name: 'name',
|
|
label: t('Name'),
|
|
type: 'input-name',
|
|
placeholder: t('Please input name'),
|
|
required: true,
|
|
},
|
|
{
|
|
title: t('Description'),
|
|
dataIndex: 'description',
|
|
},
|
|
];
|
|
}
|
|
|
|
onCountChange = (value) => {
|
|
let msg = t('Quota: Project quotas sufficient resources can be created');
|
|
let status = 'success';
|
|
if (isFinite(this.quota) && value > this.quota) {
|
|
msg = t(
|
|
'Quota: Insufficient quota to create resources, please adjust resource quantity or quota(left { quota }, input { input }).',
|
|
{ quota: this.quota, input: value }
|
|
);
|
|
status = 'error';
|
|
}
|
|
this.msg = msg;
|
|
this.setState({
|
|
count: value,
|
|
status,
|
|
});
|
|
};
|
|
|
|
renderBadge() {
|
|
const { status } = this.state;
|
|
if (status === 'success') {
|
|
return null;
|
|
}
|
|
return <Badge status={status} text={this.msg} />;
|
|
}
|
|
|
|
renderFooterLeft() {
|
|
const { count = 1 } = this.state;
|
|
const configs = {
|
|
min: 1,
|
|
max: 100,
|
|
precision: 0,
|
|
onChange: this.onCountChange,
|
|
formatter: (value) => `$ ${value}`.replace(/\D/g, ''),
|
|
};
|
|
return (
|
|
<div>
|
|
<span>{t('Count')}</span>
|
|
<InputNumber
|
|
{...configs}
|
|
value={count}
|
|
className={classnames(styles.input, 'volume-count')}
|
|
/>
|
|
{this.renderBadge()}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
onSubmit = (data) => {
|
|
const { count, status } = this.state;
|
|
if (status === 'error') {
|
|
return Promise.reject();
|
|
}
|
|
const {
|
|
backup,
|
|
image,
|
|
snapshot,
|
|
size,
|
|
availableZone,
|
|
shared,
|
|
name,
|
|
volume_type,
|
|
} = data;
|
|
const volume = {
|
|
name,
|
|
size,
|
|
availability_zone: availableZone !== 'noSelect' ? availableZone : null,
|
|
multiattach: shared,
|
|
volume_type: volume_type.selectedRowKeys[0],
|
|
};
|
|
if (
|
|
backup &&
|
|
Array.isArray(backup.selectedRowKeys) &&
|
|
backup.selectedRowKeys.length
|
|
) {
|
|
volume.backup_id = backup.selectedRowKeys[0];
|
|
}
|
|
if (
|
|
image &&
|
|
Array.isArray(image.selectedRowKeys) &&
|
|
image.selectedRowKeys.length
|
|
) {
|
|
volume.imageRef = image.selectedRowKeys[0];
|
|
}
|
|
if (
|
|
snapshot &&
|
|
Array.isArray(snapshot.selectedRowKeys) &&
|
|
snapshot.selectedRowKeys.length
|
|
) {
|
|
volume.snapshot_id = snapshot.selectedRowKeys[0];
|
|
}
|
|
if (count === 1) {
|
|
return this.volumeStore.create(volume);
|
|
}
|
|
return Promise.all(
|
|
new Array(count).fill(count).map((_, index) => {
|
|
const body = {
|
|
...volume,
|
|
name: `${volume.name}-${index + 1}`,
|
|
};
|
|
return this.volumeStore.create(body);
|
|
})
|
|
);
|
|
};
|
|
}
|