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
645 lines
16 KiB
JavaScript
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));
|