skyline/src/pages/storage/containers/Volume/actions/Create/index.jsx
Jingwei.Zhang 74f2f03e88 refactor: optimize the rendering of data with map values
Update Table/SimpleTable/DetailCard/TabDetail component to optimize the rendering of data with map values, such as status value, which has a value map to deal with the i18n display

Change-Id: I9696c194e15db14197334f31d09a49de8170fe20
2022-08-25 17:11:39 +08:00

645 lines
16 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,
getQuotaInfo,
getAdd,
fetchQuota,
getQuota,
onVolumeSizeChange,
setCreateVolumeCount,
setCreateVolumeSize,
setCreateVolumeType,
} from 'resources/cinder/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 globalProjectStore from 'stores/keystone/project';
import globalBackupStore from 'stores/cinder/backup';
import { InputNumber, message as $message } from 'antd';
import { toJS } from 'mobx';
import { FormAction } from 'containers/Action';
import classnames from 'classnames';
import {
getImageSystemTabs,
getImageOS,
getImageColumns,
canImageCreateIronicInstance,
canImageCreateInstance,
} from 'resources/glance/image';
import { volumeTypeSelectProps } from 'resources/cinder/volume-type';
import { allSettled } from 'utils';
import styles from './index.less';
export class Create extends FormAction {
init() {
this.state = {
...this.state,
quotaLoading: true,
count: 1,
sharedDisabled: false,
confirmCount: 0,
};
this.message = '';
this.snapshotStore = globalSnapshotStore;
this.imageStore = globalImageStore;
this.volumeStore = globalVolumeStore;
this.volumeTypeStore = globalVolumeTypeStore;
this.backupStore = globalBackupStore;
this.getQuota();
this.getAvailZones();
this.getImages();
this.getVolumeTypes();
}
static id = 'volume-create';
static title = t('Create Volume');
static path = '/storage/volume/create';
get listUrl() {
return this.getRoutePath('volume');
}
get name() {
return t('create volume');
}
static policy = 'volume:create';
static allowed() {
return Promise.resolve(true);
}
get disableSubmit() {
const { quotaLoading } = this.state;
if (quotaLoading) {
return true;
}
const { cinderQuota = {} } = globalProjectStore;
const { add, error } = getAdd(cinderQuota);
const disable = add === 0;
if (!disable) {
this.message = '';
} else if (error !== this.message) {
$message.error(error);
this.message = error;
}
return disable;
}
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() {
if (this.message) {
return t(
'Unable to create volume: insufficient quota to create resources.'
);
}
return super.errorText;
}
get showQuota() {
return true;
}
getVolumeQuota() {
const { quota = {} } = this.state;
return getQuota(quota);
}
get quotaInfo() {
return getQuotaInfo(this);
}
get defaultSize() {
return this.quotaIsLimit && this.maxSize < 10 ? this.maxSize : 10;
}
get defaultValue() {
const { initVolumeType } = this.state;
const values = {
source: this.sourceTypes[0],
size: this.defaultSize,
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('Volume Snapshot'), value: 'snapshot' },
// { label: t('Backup'), value: 'backup' },
];
}
get quotaIsLimit() {
const { gigabytes: { limit } = {} } = this.getVolumeQuota();
return limit !== -1;
}
get maxSize() {
const { gigabytes: { left = 0 } = {} } = this.getVolumeQuota();
return left === -1 ? 1000 : left;
}
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 defaultType = types[0];
const { id, name } = defaultType;
const initVolumeType = {
selectedRowKeys: [id],
selectedRows: [defaultType],
};
setCreateVolumeType(name);
this.setState(
{
initVolumeType,
volume_type: defaultType,
},
() => {
this.updateFormValue('volume_type', initVolumeType);
this.updateDefaultValue();
}
);
}
}
async getQuota() {
await fetchQuota(this, 0);
setCreateVolumeSize(this.defaultSize);
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) {
setCreateVolumeType('');
this.setState({
multiattach: false,
});
return;
}
const {
id,
extra_specs: { multiattach = 'False' } = {},
name,
} = selectedRows[0];
setCreateVolumeType(name);
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 sizeGiB = Math.ceil(size / 1024 / 1024 / 1024);
imageSize = Math.max(min_disk, sizeGiB, 1);
} else if (this.sourceTypeIsSnapshot) {
const { size = 0 } = this.state.snapshot || {};
imageSize = size;
}
return Math.max(imageSize, 1);
}
onSnapshotChange = async (value) => {
const { selectedRows = [] } = value || {};
let volumeTypeId = '';
let volumeType = null;
if (selectedRows.length) {
const { origin_data: { volume_type_id } = {}, id } =
selectedRows[0] || {};
if (!volume_type_id) {
try {
const detail = await this.snapshotStore.fetchDetail({ id });
const {
volume: { volume_type },
} = detail || {};
volumeType = this.volumeTypes.find((it) => it.name === volume_type);
volumeTypeId = volumeType.id;
} catch (e) {
console.log('volume already not exist', e);
}
} else {
volumeTypeId = volume_type_id;
volumeType = this.volumeTypes.find((it) => it.id === volumeTypeId);
}
if (volumeType) {
const newValue = {
selectedRowKeys: [volumeTypeId],
selectedRows: [volumeType],
snapshotId: id,
};
this.setState({
initVolumeType: newValue,
});
}
}
};
get nameForStateUpdate() {
return ['source', 'image', 'snapshot', 'size', 'volume_type'];
}
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',
data: this.images,
isLoading: this.imageStore.list.isLoading,
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('Volume 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}GiB`,
sorter: false,
},
{
title: t('Status'),
dataIndex: 'status',
valueMap: volumeStatus,
},
{
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,
data: this.volumeTypes,
isLoading: this.volumeTypeStore.list.isLoading,
required: true,
extra: this.getVolumeTypeExtra(),
onChange: this.onVolumeTypeChange,
initValue: initVolumeType,
},
{
name: 'size',
label: t('Capacity (GiB)'),
type: 'slider-input',
max: this.maxSize,
min: minSize,
description: `${minSize}GiB-${this.maxSize}GiB`,
required: this.quotaIsLimit,
hidden: !this.quotaIsLimit,
onChange: onVolumeSizeChange,
},
{
name: 'size',
label: t('Capacity (GiB)'),
type: 'input-int',
min: minSize,
hidden: this.quotaIsLimit,
required: !this.quotaIsLimit,
onChange: onVolumeSizeChange,
},
{
type: 'divider',
},
{
name: 'name',
label: t('Name'),
type: 'input-name',
placeholder: t('Please input name'),
required: true,
},
{
title: t('Description'),
dataIndex: 'description',
},
];
}
onCountChangeCallback() {}
onCountChange = (value) => {
setCreateVolumeCount(value);
this.setState(
{
count: value,
},
() => {
if (this.onCountChangeCallback) {
this.onCountChangeCallback();
}
}
);
};
getCountMax = () => {
const { volumes: { left = 0 } = {} } = this.getVolumeQuota();
if (left === -1) {
return Infinity;
}
return left;
};
renderFooterLeft() {
const { count = 1 } = this.state;
const max = this.getCountMax();
const configs = {
min: 1,
max,
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')}
/>
</div>
);
}
onSubmit = (data) => {
const { count } = this.state;
if (this.message) {
return Promise.reject(this.message);
}
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 allSettled(
new Array(count).fill(count).map((_, index) => {
const body = {
...volume,
name: `${volume.name}-${index + 1}`,
};
return this.volumeStore.create(body);
})
);
};
}
export default inject('rootStore')(observer(Create));