From 5eaa2d8ff47876a01c54c9ceb745e22de54bdcac Mon Sep 17 00:00:00 2001 From: "Jingwei.Zhang" Date: Thu, 7 Jul 2022 17:11:53 +0800 Subject: [PATCH] feat: detach instance snapshot list page 1. Update instance create image params: add image_type && instance_id 2. Support instance snapshot list page 3. Support instance snapshot detail page 4. Support instance snapshot tab in instance detail page 5. Support edit instance snapshot 6. Support delete instance snapshot 7. Support instance snapshot create volume 8. Update image list page: remove snapshot from origin data 9. Update BaseDetail commponent support path 10. Update TabDetail component support path Change-Id: I577c046e8d80ebf26be04db881aa0f6f3d9bc01e --- src/containers/BaseDetail/index.jsx | 5 + src/containers/TabDetail/index.jsx | 5 + src/layouts/admin-menu.jsx | 15 ++ src/layouts/menu.jsx | 15 ++ src/locales/en.json | 11 +- src/locales/zh.json | 11 +- .../containers/Image/Detail/BaseDetail.jsx | 3 +- .../compute/containers/Image/Detail/index.jsx | 21 ++- src/pages/compute/containers/Image/Image.jsx | 4 +- .../containers/Instance/Detail/index.jsx | 9 +- .../InstanceSnapshot/actions/CreateVolume.jsx | 169 ++++++++++++++++++ .../InstanceSnapshot/actions/Delete.jsx | 42 +++++ .../InstanceSnapshot/actions/Edit.jsx | 89 +++++++++ .../InstanceSnapshot/actions/index.jsx | 46 +++++ .../containers/InstanceSnapshot/index.jsx | 109 +++++++++++ src/pages/compute/routes/index.js | 21 +++ src/resources/glance/image.jsx | 6 +- src/resources/glance/instance-snapshot.js | 53 ++++++ src/stores/glance/image.js | 12 +- src/stores/glance/instance-snapshot.js | 158 ++++++++++++++++ src/stores/nova/instance.js | 2 + 21 files changed, 785 insertions(+), 21 deletions(-) create mode 100644 src/pages/compute/containers/InstanceSnapshot/actions/CreateVolume.jsx create mode 100644 src/pages/compute/containers/InstanceSnapshot/actions/Delete.jsx create mode 100644 src/pages/compute/containers/InstanceSnapshot/actions/Edit.jsx create mode 100644 src/pages/compute/containers/InstanceSnapshot/actions/index.jsx create mode 100644 src/pages/compute/containers/InstanceSnapshot/index.jsx create mode 100644 src/resources/glance/instance-snapshot.js create mode 100644 src/stores/glance/instance-snapshot.js diff --git a/src/containers/BaseDetail/index.jsx b/src/containers/BaseDetail/index.jsx index 23541f63..343f7b86 100644 --- a/src/containers/BaseDetail/index.jsx +++ b/src/containers/BaseDetail/index.jsx @@ -66,6 +66,11 @@ export default class BaseDetail extends React.Component { return this.props.rootStore.routing; } + get path() { + const { location: { pathname = '' } = {} } = this.props; + return pathname || ''; + } + get leftCards() { return []; } diff --git a/src/containers/TabDetail/index.jsx b/src/containers/TabDetail/index.jsx index 0e515314..9a8b984d 100644 --- a/src/containers/TabDetail/index.jsx +++ b/src/containers/TabDetail/index.jsx @@ -78,6 +78,11 @@ export default class DetailBase extends React.Component { return this.props.rootStore.routing; } + get path() { + const { location: { pathname = '' } = {} } = this.props; + return pathname || ''; + } + get isAdminPage() { const { pathname } = this.props.location; return isAdminPage(pathname); diff --git a/src/layouts/admin-menu.jsx b/src/layouts/admin-menu.jsx index 66a25a26..7dc8f3cc 100644 --- a/src/layouts/admin-menu.jsx +++ b/src/layouts/admin-menu.jsx @@ -61,6 +61,21 @@ const renderMenu = (t) => { }, ], }, + { + path: '/compute/instance-snapshot-admin', + name: t('Instance Snapshot'), + key: 'instanceSnapshotAdmin', + level: 1, + children: [ + { + path: /^\/compute\/instance-snapshot-admin\/detail\/[^/]+$/, + name: t('Instance Snapshot Detail'), + key: 'instanceSnapshotDetailAdmin', + level: 2, + routePath: '/compute/instance-snapshot-admin/detail/:id', + }, + ], + }, { path: '/compute/flavor-admin', name: t('Flavor'), diff --git a/src/layouts/menu.jsx b/src/layouts/menu.jsx index db761e3b..8e0a519a 100644 --- a/src/layouts/menu.jsx +++ b/src/layouts/menu.jsx @@ -72,6 +72,21 @@ const renderMenu = (t) => { }, ], }, + { + path: '/compute/instance-snapshot', + name: t('Instance Snapshot'), + key: 'instanceSnapshot', + level: 1, + children: [ + { + path: /^\/compute\/instance-snapshot\/detail\/[^/]+$/, + name: t('Instance Snapshot Detail'), + key: 'instanceSnapshotDetail', + level: 2, + routePath: '/compute/instance-snapshot/detail/:id', + }, + ], + }, { path: '/compute/flavor', name: t('Flavor'), diff --git a/src/locales/en.json b/src/locales/en.json index 92fafd89..c7f62806 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -323,10 +323,7 @@ "Checksum": "Checksum", "Chile": "Chile", "China": "China", - "Choose a External Network": "Choose a External Network", "Choose a Network Driver": "Choose a Network Driver", - "Choose a Private Network": "Choose a Private Network", - "Choose a Private Network at first": "Choose a Private Network at first", "Choose a host to live migrate instance to. If not selected, the scheduler will auto select target host.": "Choose a host to live migrate instance to. If not selected, the scheduler will auto select target host.", "Choose a host to migrate instance to. If not selected, the scheduler will auto select target host.": "Choose a host to migrate instance to. If not selected, the scheduler will auto select target host.", "Choosing a QoS policy can limit bandwidth and DSCP": "Choosing a QoS policy can limit bandwidth and DSCP", @@ -629,6 +626,7 @@ "Delete Image": "Delete Image", "Delete In Progress": "Delete In Progress", "Delete Instance": "Delete Instance", + "Delete Instance Snapshot": "Delete Instance Snapshot", "Delete Keypair": "Delete Keypair", "Delete Listener": "Delete Listener", "Delete Load Balancer": "Delete Load Balancer", @@ -778,6 +776,7 @@ "Edit IPsec Site Connection": "Edit IPsec Site Connection", "Edit Image": "Edit Image", "Edit Instance": "Edit Instance", + "Edit Instance Snapshot": "Edit Instance Snapshot", "Edit Listener": "Edit Listener", "Edit Load Balancer": "Edit Load Balancer", "Edit Member": "Edit Member", @@ -1181,6 +1180,8 @@ "Instance IP": "Instance IP", "Instance Info": "Instance Info", "Instance Name": "Instance Name", + "Instance Snapshot": "Instance Snapshot", + "Instance Snapshot Detail": "Instance Snapshot Detail", "Instance Status": "Instance Status", "Instances": "Instances", "Instances \"{ name }\" are locked, can not delete them.": "Instances \"{ name }\" are locked, can not delete them.", @@ -2067,6 +2068,7 @@ "Snapshot Instance": "Snapshot Instance", "Snapshot Name": "Snapshot Name", "Snapshots": "Snapshots", + "Snapshots can be converted into volume and used to create an instance from the volume.": "Snapshots can be converted into volume and used to create an instance from the volume.", "Snapshotting": "Snapshotting", "Soft Delete Instance": "Soft Delete Instance", "Soft Deleted": "Soft Deleted", @@ -2615,6 +2617,7 @@ "delete group": "delete group", "delete image": "delete image", "delete instance": "delete instance", + "delete instance snapshot": "delete instance snapshot", "delete ipsec site connection": "delete ipsec site connection", "delete ironic instance": "delete ironic instance", "delete keypair": "delete keypair", @@ -2652,6 +2655,7 @@ "edit default pool": "edit default pool", "edit health monitor": "edit health monitor", "edit image": "edit image", + "edit instance snapshot": "edit instance snapshot", "edit member": "edit member", "edit system permission": "edit system permission", "egress": "egress", @@ -2674,6 +2678,7 @@ "insert": "insert", "instance": "instance", "instance snapshot": "instance snapshot", + "instance snapshots": "instance snapshots", "instance: {name}.": "instance: {name}.", "instances": "instances", "ipsec site connection": "ipsec site connection", diff --git a/src/locales/zh.json b/src/locales/zh.json index dc8ce959..10f7c6ec 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -323,10 +323,7 @@ "Checksum": "校验和", "Chile": "智利", "China": "中国大陆", - "Choose a External Network": "选择外部网络", "Choose a Network Driver": "选择网络驱动程序", - "Choose a Private Network": "选择专用网络", - "Choose a Private Network at first": "首先选择一个专用网络", "Choose a host to live migrate instance to. If not selected, the scheduler will auto select target host.": "选择计算节点来热迁移云主机,如果没有选择,调度器会自动选择目标计算节点。", "Choose a host to migrate instance to. If not selected, the scheduler will auto select target host.": "选择计算节点来迁移云主机,如果没有选择,调度器会自动选择目标计算节点。", "Choosing a QoS policy can limit bandwidth and DSCP": "选择QoS策略可以限制带宽和DSCP", @@ -629,6 +626,7 @@ "Delete Image": "删除镜像", "Delete In Progress": "正在删除", "Delete Instance": "删除云主机", + "Delete Instance Snapshot": "删除云主机快照", "Delete Keypair": "删除密钥", "Delete Listener": "删除监听器", "Delete Load Balancer": "删除负载均衡", @@ -778,6 +776,7 @@ "Edit IPsec Site Connection": "编辑IPsec站点连接", "Edit Image": "编辑镜像", "Edit Instance": "编辑云主机", + "Edit Instance Snapshot": "编辑云主机快照", "Edit Listener": "编辑监听器", "Edit Load Balancer": "编辑负载均衡", "Edit Member": "编辑成员", @@ -1181,6 +1180,8 @@ "Instance IP": "云主机IP", "Instance Info": "云主机信息", "Instance Name": "云主机名称", + "Instance Snapshot": "云主机快照", + "Instance Snapshot Detail": "云主机快照详情", "Instance Status": "云主机状态", "Instances": "云主机", "Instances \"{ name }\" are locked, can not delete them.": "云主机\"{ name }\"被锁定,无法删除。", @@ -2067,6 +2068,7 @@ "Snapshot Instance": "创建云主机快照", "Snapshot Name": "快照名称", "Snapshots": "快照", + "Snapshots can be converted into volume and used to create an instance from the volume.": "快照可以转换成云硬盘,用于从云硬盘启动云主机。", "Snapshotting": "创建快照中", "Soft Delete Instance": "软删除云主机", "Soft Deleted": "软删除", @@ -2615,6 +2617,7 @@ "delete group": "删除组", "delete image": "删除镜像", "delete instance": "删除云主机", + "delete instance snapshot": "删除云主机快照", "delete ipsec site connection": "删除IPsec站点连接", "delete ironic instance": "删除裸机", "delete keypair": "删除密钥", @@ -2652,6 +2655,7 @@ "edit default pool": "编辑资源池", "edit health monitor": "编辑健康检查器", "edit image": "编辑镜像", + "edit instance snapshot": "编辑云主机快照", "edit member": "编辑成员", "edit system permission": "编辑系统角色", "egress": "出方向", @@ -2674,6 +2678,7 @@ "insert": "插入", "instance": "云主机", "instance snapshot": "云主机快照", + "instance snapshots": "云主机快照", "instance: {name}.": "实例名称:{name}。", "instances": "云主机", "ipsec site connection": "IPsec站点连接", diff --git a/src/pages/compute/containers/Image/Detail/BaseDetail.jsx b/src/pages/compute/containers/Image/Detail/BaseDetail.jsx index e5c564f2..fc6062e4 100644 --- a/src/pages/compute/containers/Image/Detail/BaseDetail.jsx +++ b/src/pages/compute/containers/Image/Detail/BaseDetail.jsx @@ -24,8 +24,7 @@ import { isObject, isArray } from 'lodash'; export class BaseDetail extends Base { get isImageDetail() { - const { pathname } = this.props.location; - return pathname.indexOf('image') >= 0; + return this.path.includes('image'); } get leftCards() { diff --git a/src/pages/compute/containers/Image/Detail/index.jsx b/src/pages/compute/containers/Image/Detail/index.jsx index d0b33193..66e7418f 100644 --- a/src/pages/compute/containers/Image/Detail/index.jsx +++ b/src/pages/compute/containers/Image/Detail/index.jsx @@ -15,6 +15,8 @@ import { inject, observer } from 'mobx-react'; import { imageStatus } from 'resources/glance/image'; import { ImageStore } from 'stores/glance/image'; +import { InstanceSnapshotStore } from 'stores/glance/instance-snapshot'; +import actionConfigsSnapshot from 'pages/compute/containers/InstanceSnapshot/actions'; import Base from 'containers/TabDetail'; import BaseDetail from './BaseDetail'; import actionConfigs from '../actions'; @@ -29,18 +31,25 @@ export class ImageDetail extends Base { } get isImageDetail() { - const { pathname } = this.props.location; - return pathname.indexOf('image') >= 0; + return this.path.includes('image'); } get listUrl() { + if (!this.isImageDetail) { + return this.getRoutePath('instanceSnapshot'); + } return this.getRoutePath('image'); } get actionConfigs() { + if (this.isImageDetail) { + return this.isAdminPage + ? actionConfigs.actionConfigsAdmin + : actionConfigs.actionConfigs; + } return this.isAdminPage - ? actionConfigs.actionConfigsAdmin - : actionConfigs.actionConfigs; + ? actionConfigsSnapshot.adminConfigs + : actionConfigsSnapshot.actionConfigs; } get detailInfos() { @@ -87,7 +96,9 @@ export class ImageDetail extends Base { } init() { - this.store = new ImageStore(); + this.store = this.isImageDetail + ? new ImageStore() + : new InstanceSnapshotStore(); } } diff --git a/src/pages/compute/containers/Image/Image.jsx b/src/pages/compute/containers/Image/Image.jsx index 2168903b..25d49e26 100644 --- a/src/pages/compute/containers/Image/Image.jsx +++ b/src/pages/compute/containers/Image/Image.jsx @@ -52,7 +52,7 @@ export class Image extends Base { } get isFilterByBackend() { - return true; + return false; } get isSortByBackend() { @@ -67,7 +67,7 @@ export class Image extends Base { return !this.isAdminPage; } - updateFetchParamsByPage = (params) => { + updateFetchParams = (params) => { if (this.isAdminPage) { return { ...params, diff --git a/src/pages/compute/containers/Instance/Detail/index.jsx b/src/pages/compute/containers/Instance/Detail/index.jsx index 96313cb6..aa0e3a53 100644 --- a/src/pages/compute/containers/Instance/Detail/index.jsx +++ b/src/pages/compute/containers/Instance/Detail/index.jsx @@ -28,6 +28,7 @@ import { toJS } from 'mobx'; import BaseDetail from './BaseDetail'; import SecurityGroup from './SecurityGroup'; import ActionLog from './ActionLog'; +import Snapshots from '../../InstanceSnapshot'; import actionConfigs from '../actions'; export class InstanceDetail extends Base { @@ -44,8 +45,7 @@ export class InstanceDetail extends Base { } get isRecycleBinDetail() { - const { pathname } = this.props.location; - return pathname.indexOf('recycle-bin') >= 0; + return this.path.includes('recycle-bin'); } get listUrl() { @@ -123,6 +123,11 @@ export class InstanceDetail extends Base { key: 'BaseDetail', component: BaseDetail, }, + { + title: t('Instance Snapshot'), + key: 'snapshots', + component: Snapshots, + }, { title: t('Interface'), key: 'interface', diff --git a/src/pages/compute/containers/InstanceSnapshot/actions/CreateVolume.jsx b/src/pages/compute/containers/InstanceSnapshot/actions/CreateVolume.jsx new file mode 100644 index 00000000..d84eb7b0 --- /dev/null +++ b/src/pages/compute/containers/InstanceSnapshot/actions/CreateVolume.jsx @@ -0,0 +1,169 @@ +// Copyright 2022 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 { inject, observer } from 'mobx-react'; +import { toJS } from 'mobx'; +import { ModalAction } from 'containers/Action'; +import globalVolumeStore from 'stores/cinder/volume'; +import { InstanceSnapshotStore } from 'stores/glance/instance-snapshot'; + +export class CreateVolume extends ModalAction { + static id = 'create'; + + static title = t('Create Volume'); + + init() { + this.volumeStore = globalVolumeStore; + this.snapshotStore = new InstanceSnapshotStore(); + this.getVolumeTypes(); + this.getMinSize(); + } + + get name() { + return t('Create Volume'); + } + + get instanceName() { + return this.values.name; + } + + static policy = 'volume:create_from_image'; + + static allowed = () => Promise.resolve(true); + + async getVolumeTypes() { + const { id } = this.item; + // eslint-disable-next-line no-unused-vars + const [_, snapshot] = await Promise.all([ + this.volumeStore.fetchVolumeTypes(), + this.snapshotStore.fetchDetail({ id }), + ]); + const { volumeDetail: { volume_type: volumeType } = {} } = snapshot; + const typeItem = this.volumeTypes.find((it) => it.label === volumeType); + if (typeItem) { + this.volumeType = typeItem.value; + } + this.updateFormValue('volume_type', this.volumeType); + } + + async getMinSize() { + const { id } = this.item; + if (this.snapshot && this.snapshot.volume_size) { + return; + } + await this.snapshotStore.fetchDetail({ id }); + this.updateDefaultValue(); + } + + get volumeTypes() { + return this.volumeStore.volumeTypes; + } + + get tips() { + return t( + 'Snapshots can be converted into volume and used to create an instance from the volume.' + ); + } + + get defaultValue() { + const { name } = this.item; + const value = { + snapshot: name, + size: this.minSize, + volume_type: this.volumeType, + }; + return value; + } + + get bdmData() { + const { block_device_mapping: bdm = '[]' } = this.item; + return JSON.parse(bdm); + } + + get snapshot() { + return this.bdmData.find((it) => it.boot_index === 0); + } + + get minSize() { + const { min_disk, size } = this.item; + const biggerSize = Math.max( + min_disk, + Math.ceil(size / 1024 / 1024 / 1024), + 1, + (this.snapshot || {}).volume_size || 1 + ); + if (biggerSize) { + return biggerSize; + } + const { snapshotDetail: { size: snapshotSize = 0 } = {} } = + toJS(this.snapshotStore.detail) || {}; + return Math.max(snapshotSize, 1); + } + + get formItems() { + const { more } = this.state; + return [ + { + name: 'snapshot', + label: t('Snapshot'), + type: 'label', + iconType: 'snapshot', + }, + { + name: 'name', + label: t('Name'), + type: 'input-name', + placeholder: t('Please input name'), + required: true, + }, + { + name: 'size', + label: t('Capacity (GiB)'), + type: 'input-int', + min: this.minSize, + extra: `${t('Min size')}: ${this.minSize}GiB`, + required: true, + }, + { + name: 'more', + type: 'more', + label: t('Advanced Options'), + }, + { + name: 'volume_type', + label: t('Volume Type'), + type: 'select', + options: this.volumeTypes, + placeholder: t('Please select volume type'), + hidden: !more, + }, + ]; + } + + onSubmit = ({ name, size, volume_type }) => { + const body = { + imageRef: this.item.id, + name, + size, + }; + if (volume_type) { + body.volume_type = volume_type; + } else { + body.volume_type = this.volumeType; + } + return globalVolumeStore.create(body); + }; +} + +export default inject('rootStore')(observer(CreateVolume)); diff --git a/src/pages/compute/containers/InstanceSnapshot/actions/Delete.jsx b/src/pages/compute/containers/InstanceSnapshot/actions/Delete.jsx new file mode 100644 index 00000000..a4bebd8a --- /dev/null +++ b/src/pages/compute/containers/InstanceSnapshot/actions/Delete.jsx @@ -0,0 +1,42 @@ +// Copyright 2022 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 { ConfirmAction } from 'containers/Action'; +import globalImageStore from 'stores/glance/image'; + +export default class Delete extends ConfirmAction { + get id() { + return 'delete'; + } + + get title() { + return t('Delete Instance Snapshot'); + } + + get isDanger() { + return true; + } + + get buttonText() { + return t('Delete'); + } + + get actionName() { + return t('delete instance snapshot'); + } + + policy = 'delete_image'; + + onSubmit = (data) => globalImageStore.delete({ id: data.id }); +} diff --git a/src/pages/compute/containers/InstanceSnapshot/actions/Edit.jsx b/src/pages/compute/containers/InstanceSnapshot/actions/Edit.jsx new file mode 100644 index 00000000..a09300e0 --- /dev/null +++ b/src/pages/compute/containers/InstanceSnapshot/actions/Edit.jsx @@ -0,0 +1,89 @@ +// Copyright 2022 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 { inject, observer } from 'mobx-react'; +import { ModalAction } from 'containers/Action'; +import globalImageStore from 'stores/glance/image'; +import { get, has } from 'lodash'; + +export class EditAction extends ModalAction { + static id = 'edit'; + + static title = t('Edit Instance Snapshot'); + + static buttonText = t('Edit'); + + get name() { + return t('edit instance snapshot'); + } + + get defaultValue() { + const { name, description } = this.item; + const value = { + name, + description, + }; + return value; + } + + static policy = 'modify_image'; + + static allowed = () => Promise.resolve(true); + + get formItems() { + return [ + { + name: 'name', + label: t('Name'), + type: 'input-name', + placeholder: t('Please input name'), + isImage: true, + required: true, + }, + { + name: 'description', + label: t('Description'), + type: 'textarea', + }, + ]; + } + + onSubmit = (values) => { + const { id } = this.item; + const changeValues = []; + Object.keys(values).forEach((key) => { + if (has(this.item, key) && get(this.item, key) !== values[key]) { + const item = { + op: 'replace', + path: `/${key}`, + value: values[key], + }; + changeValues.push(item); + } else if (!has(this.item, key) && values[key]) { + const item = { + op: 'add', + path: `/${key}`, + value: values[key], + }; + changeValues.push(item); + } + }); + if (changeValues.length === 0) { + return Promise.resolve(); + } + return globalImageStore.update({ id }, changeValues); + }; +} + +export default inject('rootStore')(observer(EditAction)); diff --git a/src/pages/compute/containers/InstanceSnapshot/actions/index.jsx b/src/pages/compute/containers/InstanceSnapshot/actions/index.jsx new file mode 100644 index 00000000..43b39b10 --- /dev/null +++ b/src/pages/compute/containers/InstanceSnapshot/actions/index.jsx @@ -0,0 +1,46 @@ +// Copyright 2022 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 CreateVolume from './CreateVolume'; +import Edit from './Edit'; +import Delete from './Delete'; + +const actionConfigs = { + rowActions: { + firstAction: Edit, + moreActions: [ + { + action: CreateVolume, + }, + { + action: Delete, + }, + ], + }, + batchActions: [Delete], +}; + +const adminConfigs = { + rowActions: { + firstAction: Edit, + moreActions: [ + { + action: Delete, + }, + ], + }, + batchActions: [Delete], +}; + +export default { actionConfigs, adminConfigs }; diff --git a/src/pages/compute/containers/InstanceSnapshot/index.jsx b/src/pages/compute/containers/InstanceSnapshot/index.jsx new file mode 100644 index 00000000..0d51e878 --- /dev/null +++ b/src/pages/compute/containers/InstanceSnapshot/index.jsx @@ -0,0 +1,109 @@ +// Copyright 2022 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 { observer, inject } from 'mobx-react'; +import Base from 'containers/List'; +import { transitionStatusList } from 'resources/glance/image'; +import globalInstanceSnapshotStore, { + InstanceSnapshotStore, +} from 'stores/glance/instance-snapshot'; +import { emptyActionConfig } from 'utils/constants'; +import { getBaseSnapshotColumns } from 'resources/glance/instance-snapshot'; +import actionConfigs from './actions'; + +export class Snapshots extends Base { + init() { + this.store = this.inDetailPage + ? new InstanceSnapshotStore() + : globalInstanceSnapshotStore; + this.downloadStore = this.inDetailPage + ? this.store + : new InstanceSnapshotStore(); + } + + get policy() { + return 'get_images'; + } + + get name() { + return t('instance snapshots'); + } + + get isRecycleBinDetail() { + return this.path.includes('recycle-bin'); + } + + get actionConfigs() { + if (this.isRecycleBinDetail) { + return emptyActionConfig; + } + return this.isAdminPage + ? actionConfigs.adminConfigs + : actionConfigs.actionConfigs; + } + + get transitionStatusList() { + return transitionStatusList; + } + + get isFilterByBackend() { + return false; + } + + get isSortByBackend() { + return true; + } + + get defaultSortKey() { + return 'created_at'; + } + + get adminPageHasProjectFilter() { + return true; + } + + get projectFilterKey() { + return 'owner'; + } + + updateFetchParams = (params) => ({ + ...params, + owner: this.inDetailPage ? this.props.detail.tenant_id : null, + }); + + get currentProjectId() { + return this.props.detail.tenant_id; + } + + getColumns = () => getBaseSnapshotColumns(this); + + get searchFilters() { + return [ + { + label: t('Name'), + name: 'name', + }, + { + label: t('Status'), + name: 'status', + options: [ + { label: t('Active'), key: 'active' }, + { label: t('Saving'), key: 'saving' }, + ], + }, + ]; + } +} + +export default inject('rootStore')(observer(Snapshots)); diff --git a/src/pages/compute/routes/index.js b/src/pages/compute/routes/index.js index e4c233ce..788785cd 100644 --- a/src/pages/compute/routes/index.js +++ b/src/pages/compute/routes/index.js @@ -24,6 +24,7 @@ import CreateIronic from '../containers/Instance/actions/CreateIronic'; import TabImage from '../containers/Image'; import ImageAdmin from '../containers/Image/Image'; import ImageCreate from '../containers/Image/actions/Create'; +import InstanceSnapshot from '../containers/InstanceSnapshot'; import Keypair from '../containers/Keypair'; import KeypairDetail from '../containers/Keypair/Detail'; import ServerGroup from '../containers/ServerGroup'; @@ -60,6 +61,26 @@ export default [ component: CreateIronic, exact: true, }, + { + path: `${PATH}/instance-snapshot`, + component: InstanceSnapshot, + exact: true, + }, + { + path: `${PATH}/instance-snapshot-admin`, + component: InstanceSnapshot, + exact: true, + }, + { + path: `${PATH}/instance-snapshot/detail/:id`, + component: ImageDetail, + exact: true, + }, + { + path: `${PATH}/instance-snapshot-admin/detail/:id`, + component: ImageDetail, + exact: true, + }, { path: `${PATH}/flavor`, component: Flavor, exact: true }, { path: `${PATH}/flavor-admin`, component: Flavor, exact: true }, { diff --git a/src/resources/glance/image.jsx b/src/resources/glance/image.jsx index 118ad246..8a65d0ac 100644 --- a/src/resources/glance/image.jsx +++ b/src/resources/glance/image.jsx @@ -123,10 +123,12 @@ export const isOwner = (item) => { }; export const isSnapshot = (item) => { - const { block_device_mapping: bdm = '[]', image_type } = item; + // bfv vm has bdm; non-bfv has instance_uuid, image_type is added by frontend + const { block_device_mapping: bdm = '[]', image_type, instance_uuid } = item; return ( image_type === 'snapshot' || - get(JSON.parse(bdm)[0] || {}, 'source_type') === 'snapshot' + get(JSON.parse(bdm)[0] || {}, 'source_type') === 'snapshot' || + instance_uuid ); }; diff --git a/src/resources/glance/instance-snapshot.js b/src/resources/glance/instance-snapshot.js new file mode 100644 index 00000000..3b0c677d --- /dev/null +++ b/src/resources/glance/instance-snapshot.js @@ -0,0 +1,53 @@ +// Copyright 2022 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 { imageStatus, imageFormats } from 'resources/glance/image'; + +export const getBaseSnapshotColumns = (self) => [ + { + title: t('ID/Name'), + dataIndex: 'name', + routeName: self.getRouteName('instanceSnapshotDetail'), + }, + { + title: t('Project ID/Name'), + dataIndex: 'project_name', + isHideable: true, + hidden: !self.isAdminPage, + sorter: false, + }, + { + title: t('Description'), + dataIndex: 'description', + isHideable: true, + sorter: false, + }, + { + title: t('Disk Format'), + dataIndex: 'disk_format', + isHideable: true, + render: (value) => imageFormats[value] || '-', + }, + { + title: t('Status'), + dataIndex: 'status', + render: (value) => imageStatus[value] || '-', + }, + { + title: t('Created At'), + dataIndex: 'created_at', + isHideable: true, + valueRender: 'sinceTime', + }, +]; diff --git a/src/stores/glance/image.js b/src/stores/glance/image.js index d87eac11..6104d43a 100644 --- a/src/stores/glance/image.js +++ b/src/stores/glance/image.js @@ -15,7 +15,7 @@ import { action, observable } from 'mobx'; import client from 'client'; import Base from 'stores/base'; -import { imageOS } from 'resources/glance/image'; +import { imageOS, isSnapshot } from 'resources/glance/image'; import { isString } from 'lodash'; export class ImageStore extends Base { @@ -41,12 +41,13 @@ export class ImageStore extends Base { } }; + updateParamsSort = this.updateParamsSortPage; + get paramsFuncPage() { return (params) => { const { current, all_projects, ...rest } = params; return { ...rest, - // image_type: 'image', }; }; } @@ -80,6 +81,13 @@ export class ImageStore extends Base { }; } + listDidFetch(items) { + if (items.length === 0) { + return items; + } + return items.filter((it) => !isSnapshot(it)); + } + @action async uploadImage(imageId, file, conf) { return this.client.uploadFile(imageId, file, conf); diff --git a/src/stores/glance/instance-snapshot.js b/src/stores/glance/instance-snapshot.js new file mode 100644 index 00000000..75f08d87 --- /dev/null +++ b/src/stores/glance/instance-snapshot.js @@ -0,0 +1,158 @@ +// Copyright 2022 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 client from 'client'; +import { isSnapshot } from 'src/resources/glance/image'; +import Base from '../base'; + +export class InstanceSnapshotStore extends Base { + get client() { + return client.glance.images; + } + + get listFilterByProject() { + return true; + } + + get fetchListByLimit() { + return true; + } + + updateParamsSortPage = (params, sortKey, sortOrder) => { + if (sortKey && sortOrder) { + params.sort_key = sortKey; + params.sort_dir = sortOrder === 'descend' ? 'desc' : 'asc'; + } + }; + + updateParamsSort = this.updateParamsSortPage; + + get paramsFunc() { + return this.paramsFuncPage; + } + + get paramsFuncPage() { + return (params, all_projects) => { + const { id, current, owner, ...rest } = params; + const newParams = { + ...rest, + }; + if (owner) { + newParams.owner = owner; + } else if (!all_projects) { + newParams.owner = this.currentProjectId; + } + return newParams; + }; + } + + get mapperBeforeFetchProject() { + return (data) => ({ + ...data, + project_name: data.owner_project_name || data.project_name, + project_id: data.owner || data.project_id, + }); + } + + async listDidFetch(items, allProjects, filters) { + if (items.length === 0) { + return items; + } + const newItems = items.filter(isSnapshot); + const { id } = filters; + if (!id) { + return newItems; + } + const volumeParams = {}; + const snapshotParams = { all_tenants: allProjects }; + const results = await Promise.all([ + client.cinder.snapshots.list(snapshotParams), + client.nova.servers.volumeAttachments.list(id, volumeParams), + ]); + const snapshotsAll = results[0].snapshots; + const volumesAll = results[1].volumeAttachments; + const data = []; + newItems.forEach((item) => { + const { block_device_mapping: bdm = '[]', instance_id } = item; + if (instance_id === id) { + data.push(item); + } else { + const snapshot = JSON.parse(bdm).find((it) => it.boot_index === 0); + if (snapshot) { + item.snapshotId = snapshot.snapshot_id; + const snapshotDetail = snapshotsAll.find( + (it) => it.id === snapshot.snapshot_id + ); + if (snapshotDetail) { + const volumeId = snapshotDetail.volume_id; + const volume = volumesAll.find((it) => it.volumeId === volumeId); + if (volume) { + data.push(item); + } + } + } else { + const { instance_uuid: instanceId } = item; + if (id === instanceId) { + data.push(item); + } + } + } + }); + return data; + } + + async detailDidFetch(item) { + item.originData = { ...item }; + const { block_device_mapping: bdm = '[]' } = item; + const snapshot = JSON.parse(bdm).find((it) => it.boot_index === 0); + let instanceId = null; + let instanceName = ''; + if (snapshot) { + const { snapshot_id: snapshotId } = snapshot; + item.snapshotId = snapshotId; + const snapshotResult = await client.cinder.snapshots.show(snapshotId); + const snapshotDetail = snapshotResult.snapshot; + item.snapshotDetail = snapshotDetail; + const { volume_id: volumeId } = snapshotDetail; + const volumeResult = await client.cinder.volumes.show(volumeId); + const volumeDetail = volumeResult.volume; + item.volumeDetail = volumeDetail; + instanceId = + volumeDetail.attachments.length > 0 + ? volumeDetail.attachments[0].server_id + : ''; + } else { + // fix for not bfv instance + const { instance_uuid } = item; + instanceId = instance_uuid; + } + let instanceResult = {}; + try { + if (instanceId) { + instanceResult = await client.nova.servers.show(instanceId); + const { server: { name } = {} } = instanceResult; + instanceName = name; + } + } catch (e) {} + item.instance = { + server_id: instanceId, + server_name: instanceName, + }; + item.instanceDetail = instanceResult.server || {}; + return item; + } +} + +const globalInstanceSnapshotStore = new InstanceSnapshotStore(); +export default globalInstanceSnapshotStore; diff --git a/src/stores/nova/instance.js b/src/stores/nova/instance.js index bda47b33..3d804e40 100644 --- a/src/stores/nova/instance.js +++ b/src/stores/nova/instance.js @@ -373,6 +373,8 @@ export class ServerStore extends Base { name: image, metadata: { usage_type: 'common', + image_type: 'snapshot', + instance_id: id, }, }, };