skyline/src/pages/storage/containers/Volume/actions/Create/index.jsx
Jingwei.Zhang d422bd59aa fix: Update volume type hint when create volume by snapshot
1. Auto select correct volume type when change snapshot
2. Show confirm modal when first change volume type used by snapshot

Change-Id: I37a70414e835c8c187ce5edd858b83c2273d5ca8
2021-08-25 15:38:36 +08:00

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);
})
);
};
}