From 81c037de4135b7b5193ad42247d1933167309472 Mon Sep 17 00:00:00 2001 From: xusongfu Date: Fri, 11 Mar 2022 13:51:19 +0800 Subject: [PATCH] feat: Add tags to nova service and so on 1.Lists tags, creates, replaces tags for a server 2.Fix input autocomplete in Chrome browser 3.Fix some e2e test bugs Change-Id: I6236da8670d36c88978317d34a50cde3974b83d9 --- .zuul.yaml | 4 + cypress.json | 2 +- src/client/nova/index.js | 10 + src/components/Form/index.jsx | 1 + .../Tables/Base/ActionButton/index.jsx | 31 ++-- src/components/Tags/index.jsx | 173 ++++++++++++++++++ src/locales/en.json | 6 + src/locales/zh.json | 6 + .../Instance/Detail/BaseDetail/index.jsx | 24 ++- .../Instance/actions/ModifyTags.jsx | 113 ++++++++++++ .../containers/Instance/actions/index.jsx | 4 + .../compute/containers/Instance/index.jsx | 11 ++ .../containers/Project/actions/ModifyTags.jsx | 142 +------------- .../Project/actions/UserManager.jsx | 4 +- .../identity/containers/Project/index.jsx | 35 +--- .../containers/User/actions/SystemRole.jsx | 8 +- src/resources/instance.jsx | 27 ++- src/stores/nova/instance.js | 5 +- src/stores/nova/tag.js | 35 ++++ src/stores/skyline/server-group-instance.js | 10 + .../pages/configuration/system.spec.js | 2 +- .../pages/identity/project.spec.js | 17 +- .../integration/pages/identity/user.spec.js | 21 ++- test/e2e/support/index.js | 2 +- test/e2e/support/table-commands.js | 23 ++- 25 files changed, 505 insertions(+), 211 deletions(-) create mode 100644 src/components/Tags/index.jsx create mode 100644 src/pages/compute/containers/Instance/actions/ModifyTags.jsx create mode 100644 src/stores/nova/tag.js diff --git a/.zuul.yaml b/.zuul.yaml index 4909edee..e85a198a 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -127,6 +127,7 @@ /opt/stack/skyline-console/test/e2e/report: logs /opt/stack/skyline-console/test/e2e/screenshots: logs /opt/stack/skyline-console/test/e2e/config: logs + /opt/stack/skyline-console/test/e2e/videos: logs group-vars: subnode: devstack_services: @@ -290,6 +291,7 @@ /opt/stack/skyline-console/test/e2e/report: logs /opt/stack/skyline-console/test/e2e/screenshots: logs /opt/stack/skyline-console/test/e2e/config: logs + /opt/stack/skyline-console/test/e2e/videos: logs # octavia /var/log/dib-build/: logs /var/log/octavia-tenant-traffic.log: logs @@ -391,6 +393,7 @@ /opt/stack/skyline-console/test/e2e/report: logs /opt/stack/skyline-console/test/e2e/screenshots: logs /opt/stack/skyline-console/test/e2e/config: logs + /opt/stack/skyline-console/test/e2e/videos: logs - job: name: skyline-console-devstack-e2etests-storage @@ -494,6 +497,7 @@ /opt/stack/skyline-console/test/e2e/report: logs /opt/stack/skyline-console/test/e2e/screenshots: logs /opt/stack/skyline-console/test/e2e/config: logs + /opt/stack/skyline-console/test/e2e/videos: logs - job: name: skyline-nodejs14-run-lint-src diff --git a/cypress.json b/cypress.json index 526fbdb4..4d5dcc87 100644 --- a/cypress.json +++ b/cypress.json @@ -2,7 +2,7 @@ "baseUrl": "http://localhost:8081", "viewportWidth": 1600, "viewportHeight": 900, - "video": false, + "video": true, "retries": 5, "env": { "username": "administrator", diff --git a/src/client/nova/index.js b/src/client/nova/index.js index f05ef111..6f6af7a9 100644 --- a/src/client/nova/index.js +++ b/src/client/nova/index.js @@ -40,6 +40,11 @@ class NovaClient extends Base { key: 'os-instance-actions', responseKey: 'instanceAction', }, + { + name: 'tags', + key: 'tags', + responseKey: 'tag', + }, ], extendOperations: [ { @@ -51,6 +56,11 @@ class NovaClient extends Base { key: 'action', method: 'post', }, + { + name: 'updateTags', + key: 'tags', + method: 'put', + }, ], }, { diff --git a/src/components/Form/index.jsx b/src/components/Form/index.jsx index 87aa1aa8..e54290f6 100644 --- a/src/components/Form/index.jsx +++ b/src/components/Form/index.jsx @@ -602,6 +602,7 @@ export default class BaseForm extends React.Component { onValuesChange={this.onValuesChangeForm} scrollToFirstError > + {this.renderFormItems()} ); diff --git a/src/components/Tables/Base/ActionButton/index.jsx b/src/components/Tables/Base/ActionButton/index.jsx index 19e6485c..cb0d4869 100644 --- a/src/components/Tables/Base/ActionButton/index.jsx +++ b/src/components/Tables/Base/ActionButton/index.jsx @@ -406,18 +406,27 @@ class ActionButton extends Component { }; onClickModalActionCancel = (finish) => { - if (!isBoolean(finish)) { - this.formRef.current.wrappedInstance.onClickCancel(); - } - const { onCancelAction } = this.props; - this.setState( - { - visible: false, - }, - () => { - onCancelAction && onCancelAction(); + const callback = () => { + if (!isBoolean(finish)) { + this.formRef.current.wrappedInstance.onClickCancel(); } - ); + const { onCancelAction } = this.props; + this.setState( + { + visible: false, + }, + () => { + onCancelAction && onCancelAction(); + } + ); + }; + const { + action: { beforeCancel }, + } = this.props; + if (beforeCancel) { + return beforeCancel(callback); + } + callback(); }; getModalWidth = (size) => { diff --git a/src/components/Tags/index.jsx b/src/components/Tags/index.jsx new file mode 100644 index 00000000..5c9b1b02 --- /dev/null +++ b/src/components/Tags/index.jsx @@ -0,0 +1,173 @@ +// 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, { useEffect, useState } from 'react'; +import { Col, Input, Row, Tag, Tooltip } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import { projectTagsColors } from 'src/utils/constants'; +import PropTypes from 'prop-types'; + +const Tags = ({ tags: source, onChange, maxLength, maxCount }) => { + const [tags, setTags] = useState(source); + const [inputVisible, setInputVisible] = useState(false); + const [inputValue, setInputValue] = useState(''); + const [editInputIdx, setEditInputIdx] = useState(-1); + const [editInputValue, setEditInputValue] = useState(''); + const tagLength = maxLength && maxLength > 0 ? { maxLength } : {}; + const tagCount = (maxCount && maxCount > 0) || -1; + + function handleClose(removedTag) { + setTags(tags.filter((tag) => tag !== removedTag)); + } + + let editInput = null; + let saveInput = null; + const saveEditInputRef = (input) => { + editInput = input; + }; + + const saveInputRef = (input) => { + saveInput = input; + }; + + function handleEditInputChange(e) { + setEditInputValue(e.target.value); + } + + function handleEditInputConfirm() { + const newTags = [...tags]; + newTags[editInputIdx] = editInputValue; + setTags(newTags); + setEditInputValue(''); + setEditInputIdx(-1); + } + + function handleInputChange(e) { + setInputValue(e.target.value); + } + + function handleInputConfirm() { + const retVal = inputValue.toLocaleLowerCase(); + if (inputValue && !tags.some((tag) => tag.toLowerCase() === retVal)) { + if (tagCount !== -1 && tags.length < maxCount) { + setTags([...tags, inputValue]); + } else if (tagCount === -1) { + setTags([...tags, inputValue]); + } + } + setInputVisible(false); + setInputValue(''); + } + + function showInput() { + setInputVisible(true); + } + + useEffect(() => { + saveInput && saveInput.focus(); + }, [inputVisible]); + + useEffect(() => { + editInput && editInput.focus(); + }, [editInputIdx]); + + useEffect(() => { + onChange(tags); + }, [tags]); + + return ( + + {tags.map((tag, index) => { + if (editInputIdx === index) { + return ( + + ); + } + const isLongTag = tag.length > 20; + const tagText = isLongTag ? `${tag.slice(0, 20)}...` : tag; + const tagEl = ( + handleClose(tag)} + color={projectTagsColors[index % 10]} + > + { + setEditInputIdx(index); + setEditInputValue(tag); + e.preventDefault(); + }} + > + {tagText} + + + ); + return ( + + {isLongTag ? ( + {tag}} + > + {tagEl} + + ) : ( + tagEl + )} + + ); + })} + + {inputVisible && ( + + )} + {!inputVisible && ( + + New Tag + + )} + + + ); +}; + +Tags.propTypes = { + tags: PropTypes.array, + onChange: PropTypes.func, + maxLength: PropTypes.number, + maxCount: PropTypes.number, +}; + +export default Tags; diff --git a/src/locales/en.json b/src/locales/en.json index 63fdb564..f4cf337b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -624,6 +624,7 @@ "ESP": "ESP", "Each instance belongs to at least one security group, which needs to be specified when it is created. Instances in the same security group can communicate with each other on the network, and instances in different security groups are disconnected from the internal network by default.": "Each instance belongs to at least one security group, which needs to be specified when it is created. Instances in the same security group can communicate with each other on the network, and instances in different security groups are disconnected from the internal network by default.", "Each new connection request is assigned to the next server in order, and all requests are finally divided equally among all servers. Commonly used for short connection services, such as HTTP services.": "Each new connection request is assigned to the next server in order, and all requests are finally divided equally among all servers. Commonly used for short connection services, such as HTTP services.", + "Each server can have up to 50 tags": "Each server can have up to 50 tags", "East Timor": "East Timor", "Ecuador": "Ecuador", "Edit": "Edit", @@ -1203,6 +1204,7 @@ "Missing Port": "Missing Port", "Missing Subnet": "Missing Subnet", "Missing Weight": "Missing Weight", + "Modify Instance Tags": "Modify Instance Tags", "Modify Project Tags": "Modify Project Tags", "Modify QoS": "Modify QoS", "Moldova": "Moldova", @@ -1849,7 +1851,10 @@ "System is error, please try again later.": "System is error, please try again later.", "TCP": "TCP", "TCP Connections": "TCP Connections", + "Tag Name is too long: {tag}": "Tag Name is too long: {tag}", + "Tag is no longer than 60 characters": "Tag is no longer than 60 characters", "Tags": "Tags", + "Tags Info": "Tags Info", "Tags are not case sensitive": "Tags are not case sensitive", "Taiwan": "Taiwan", "Tajikistan": "Tajikistan", @@ -2298,6 +2303,7 @@ "message.reason": "message.reason", "metadata": "metadata", "migrate": "migrate", + "modify instance tags": "modify instance tags", "modify project tags": "modify project tags", "network": "network", "networks": "networks", diff --git a/src/locales/zh.json b/src/locales/zh.json index d570bf97..d9102cb2 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -624,6 +624,7 @@ "ESP": "", "Each instance belongs to at least one security group, which needs to be specified when it is created. Instances in the same security group can communicate with each other on the network, and instances in different security groups are disconnected from the internal network by default.": "每个云主机至少属于一个安全组,在创建的时候就需要指定。同一安全组内的云主机之间网络互通,不同安全组的云主机之间默认内网不通。", "Each new connection request is assigned to the next server in order, and all requests are finally divided equally among all servers. Commonly used for short connection services, such as HTTP services.": "按顺序把每个新的连接请求分配给下一个服务器,最终把所有请求平分给所有的服务器。常用于短连接服务,例如HTTP等服务。", + "Each server can have up to 50 tags": "每台云主机最多绑定50个标签", "East Timor": "东帝汶", "Ecuador": "厄瓜多尔", "Edit": "编辑", @@ -1203,6 +1204,7 @@ "Missing Port": "未填写端口号", "Missing Subnet": "未填写子网", "Missing Weight": "未填写权重", + "Modify Instance Tags": "修改云主机标签", "Modify Project Tags": "修改项目标签", "Modify QoS": "修改QoS", "Moldova": "摩尔多瓦", @@ -1849,7 +1851,10 @@ "System is error, please try again later.": "系统出错,请稍后再试。", "TCP": "", "TCP Connections": "TCP连接数", + "Tag Name is too long: {tag}": "标签名称太长: {tag}", + "Tag is no longer than 60 characters": "标签名长度不超过60个字符", "Tags": "标签", + "Tags Info": "标签信息", "Tags are not case sensitive": "标签不区分大小写", "Taiwan": "台湾", "Tajikistan": "塔吉克", @@ -2298,6 +2303,7 @@ "message.reason": "", "metadata": "元数据", "migrate": "迁移", + "modify instance tags": "修改云主机标签", "modify project tags": "修改项目标签", "network": "网络", "networks": "网络", diff --git a/src/pages/compute/containers/Instance/Detail/BaseDetail/index.jsx b/src/pages/compute/containers/Instance/Detail/BaseDetail/index.jsx index b7b8d94c..47665d7a 100644 --- a/src/pages/compute/containers/Instance/Detail/BaseDetail/index.jsx +++ b/src/pages/compute/containers/Instance/Detail/BaseDetail/index.jsx @@ -31,7 +31,11 @@ import instanceIcon from 'asset/image/instance.svg'; import interfaceIcon from 'asset/image/interface.svg'; import classnames from 'classnames'; import ImageType from 'components/ImageType'; -import { instanceStatus, isIronicInstance } from 'resources/instance'; +import { + instanceStatus, + isIronicInstance, + SimpleTag, +} from 'resources/instance'; import { generateId } from 'utils/index'; import { getSinceTime, getLocalTimeStr } from 'utils/time'; import AttachVolume from 'pages/compute/containers/Instance/actions/AttachVolume'; @@ -60,6 +64,7 @@ export class BaseDetail extends Base { this.flavorCard, this.imageCard, this.securityGroupCard, + this.tagsCard, ]; if (!isIronicInstance(this.detailData)) { cards.push(this.serverGroupCard); @@ -78,6 +83,23 @@ export class BaseDetail extends Base { return ret; } + get tagsCard() { + const tags = toJS(this.detailData.tags) || []; + const content = !tags.length + ? '-' + : tags.map((tag, index) => SimpleTag({ tag, index })); + const options = [ + { + label: t('Tags'), + content, + }, + ]; + return { + title: t('Tags Info'), + options, + }; + } + get networkCard() { const addresses = toJS(this.detailData.addresses) || []; const networks = []; diff --git a/src/pages/compute/containers/Instance/actions/ModifyTags.jsx b/src/pages/compute/containers/Instance/actions/ModifyTags.jsx new file mode 100644 index 00000000..ac035cff --- /dev/null +++ b/src/pages/compute/containers/Instance/actions/ModifyTags.jsx @@ -0,0 +1,113 @@ +// 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 { ModalAction } from 'containers/Action'; +import Tags from 'components/Tags'; +import globalTagStore from 'stores/nova/tag'; +import { isEqual } from 'lodash'; + +@inject('rootStore') +@observer +export default class ModifyTags extends ModalAction { + static id = 'modify-instance-tags'; + + static title = t('Modify Instance Tags'); + + static buttonText = t('Modify Instance Tags'); + + static policy = 'os_compute_api:os-server-tags:update_all'; + + static allowed = () => Promise.resolve(true); + + get name() { + return t('modify instance tags'); + } + + init() { + this.state.tags = this.props.item.tags || []; + } + + onSubmit = (values) => { + return globalTagStore.update({ serverId: this.props.item.id }, values); + }; + + get formItems() { + const { tags } = this.state; + return [ + { + name: 'tags', + label: t('Tags'), + component: , + validator: (rule, val) => { + const initialTags = this.props.item.tags || []; + + // for init modal + if (isEqual(val, initialTags)) { + return Promise.resolve(true); + } + + let errorTag = ''; + // 检测是否包含 / 和 , + if ( + val.some((tag) => { + const ret = tag.includes('/') || tag.includes(','); + ret && (errorTag = tag); + return ret; + }) + ) { + return Promise.reject( + new Error(t('Invalid Tag Value: {tag}', { tag: errorTag })) + ); + } + // 检测大小写 + if (initialTags.some(checkEqual)) { + return Promise.reject( + new Error(t('Duplicate tag name: {tag}', { tag: errorTag })) + ); + } + return Promise.resolve(true); + + function checkEqual(tag) { + return val.some((v) => { + // 不是原始值,并且新值大小写不敏感 + const flag = tag !== v && v.toLowerCase() === tag.toLowerCase(); + if (flag) { + errorTag = v; + } + return flag; + }); + } + }, + extra: ( +
+
1. {t('Each server can have up to 50 tags')}
+
2. {t('Tags are not case sensitive')}
+
3. {t('Tag is no longer than 60 characters')}
+
+ 4. {t('Forward Slash ‘/’ is not allowed to be in a tag name')} +
+
+ 5.{' '} + {t( + 'Commas ‘,’ are not allowed to be in a tag name in order to simplify requests that specify lists of tags' + )} +
+
+ ), + }, + ]; + } +} diff --git a/src/pages/compute/containers/Instance/actions/index.jsx b/src/pages/compute/containers/Instance/actions/index.jsx index 28d899fa..8747a96c 100644 --- a/src/pages/compute/containers/Instance/actions/index.jsx +++ b/src/pages/compute/containers/Instance/actions/index.jsx @@ -48,6 +48,7 @@ import ManageSecurityGroup from './ManageSecurityGroup'; import DeleteIronic from './DeleteIronic'; import ConfirmResize from './ConfirmResize'; import RevertResize from './RevertResize'; +import ModifyTags from './ModifyTags'; const statusActions = [ StartAction, @@ -130,6 +131,9 @@ const actionConfigs = { { action: DeleteIronic, }, + { + action: ModifyTags, + }, ], }, batchActions, diff --git a/src/pages/compute/containers/Instance/index.jsx b/src/pages/compute/containers/Instance/index.jsx index 2b637f83..93dd8f49 100644 --- a/src/pages/compute/containers/Instance/index.jsx +++ b/src/pages/compute/containers/Instance/index.jsx @@ -22,6 +22,7 @@ import { lockRender, instanceStatusFilter, isIronicInstance, + SimpleTag, } from 'resources/instance'; import globalServerStore, { ServerStore } from 'stores/nova/instance'; import { ServerGroupInstanceStore } from 'stores/skyline/server-group-instance'; @@ -205,6 +206,12 @@ export class Instance extends Base { sorter: false, render: (value) => instanceStatus[value && value.toLowerCase()] || '-', }, + { + title: t('Tags'), + dataIndex: 'tags', + render: (tags) => tags.map((tag, index) => SimpleTag({ tag, index })), + isHideable: true, + }, { title: t('Locked'), dataIndex: 'locked', @@ -273,6 +280,10 @@ export class Instance extends Base { ] : []), instanceStatusFilter, + { + label: t('Tags'), + name: 'tags', + }, ]; } diff --git a/src/pages/identity/containers/Project/actions/ModifyTags.jsx b/src/pages/identity/containers/Project/actions/ModifyTags.jsx index f3442ae8..1be8d782 100644 --- a/src/pages/identity/containers/Project/actions/ModifyTags.jsx +++ b/src/pages/identity/containers/Project/actions/ModifyTags.jsx @@ -12,13 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { inject, observer } from 'mobx-react'; import { ModalAction } from 'containers/Action'; -import { Col, Input, Row, Tag, Tooltip } from 'antd'; +import Tags from 'components/Tags'; import globalTagStore from 'stores/keystone/tag'; -import { PlusOutlined } from '@ant-design/icons'; -import { projectTagsColors } from 'src/utils/constants'; import { isEqual } from 'lodash'; @inject('rootStore') @@ -113,139 +111,3 @@ export default class ModifyTags extends ModalAction { ]; } } - -const Tags = ({ tags: source, onChange }) => { - const [tags, setTags] = useState(source); - const [inputVisible, setInputVisible] = useState(false); - const [inputValue, setInputValue] = useState(''); - const [editInputIdx, setEditInputIdx] = useState(-1); - const [editInputValue, setEditInputValue] = useState(''); - - function handleClose(removedTag) { - setTags(tags.filter((tag) => tag !== removedTag)); - } - - let editInput = null; - let saveInput = null; - const saveEditInputRef = (input) => { - editInput = input; - }; - - const saveInputRef = (input) => { - saveInput = input; - }; - - function handleEditInputChange(e) { - setEditInputValue(e.target.value); - } - - function handleEditInputConfirm() { - const newTags = [...tags]; - newTags[editInputIdx] = editInputValue; - setTags(newTags); - setEditInputValue(''); - setEditInputIdx(-1); - } - - function handleInputChange(e) { - setInputValue(e.target.value); - } - - function handleInputConfirm() { - const retVal = inputValue.toLocaleLowerCase(); - if (inputValue && !tags.some((tag) => tag.toLowerCase() === retVal)) { - setTags([...tags, inputValue]); - } - setInputVisible(false); - setInputValue(''); - } - - function showInput() { - setInputVisible(true); - } - - useEffect(() => { - saveInput && saveInput.focus(); - }, [inputVisible]); - - useEffect(() => { - editInput && editInput.focus(); - }, [editInputIdx]); - - useEffect(() => { - onChange(tags); - }, [tags]); - - return ( - - {tags.map((tag, index) => { - if (editInputIdx === index) { - return ( - - ); - } - const isLongTag = tag.length > 20; - const tagEl = ( - handleClose(tag)} - color={projectTagsColors[index % 10]} - > - { - setEditInputIdx(index); - setEditInputValue(tag); - e.preventDefault(); - }} - > - {isLongTag ? `${tag.slice(0, 20)}...` : tag} - - - ); - return ( - - {isLongTag ? ( - {tag}} - > - {tagEl} - - ) : ( - tagEl - )} - - ); - })} - - {inputVisible && ( - - )} - {!inputVisible && ( - - New Tag - - )} - - - ); -}; diff --git a/src/pages/identity/containers/Project/actions/UserManager.jsx b/src/pages/identity/containers/Project/actions/UserManager.jsx index ee8b0593..3785e342 100644 --- a/src/pages/identity/containers/Project/actions/UserManager.jsx +++ b/src/pages/identity/containers/Project/actions/UserManager.jsx @@ -17,7 +17,7 @@ import { inject, observer } from 'mobx-react'; import { Select } from 'antd'; import globalProjectStore from 'stores/keystone/project'; import { UserStore } from 'stores/keystone/user'; -import globalRoleStore from 'stores/keystone/role'; +import { RoleStore } from 'stores/keystone/role'; import { ModalAction } from 'containers/Action'; import globalDomainStore from 'stores/keystone/domain'; @@ -34,7 +34,7 @@ export class UserManager extends ModalAction { const projectRole = JSON.stringify(this.item.userMapProjectRoles); this.state.domainDefault = this.item.domain_id; this.state.userRoles = JSON.parse(projectRole); - this.store = globalRoleStore; + this.store = new RoleStore(); this.domainStore = globalDomainStore; this.userStore = new UserStore(); this.getRoleList(); diff --git a/src/pages/identity/containers/Project/index.jsx b/src/pages/identity/containers/Project/index.jsx index 0b34fd65..13567de6 100644 --- a/src/pages/identity/containers/Project/index.jsx +++ b/src/pages/identity/containers/Project/index.jsx @@ -14,14 +14,11 @@ import React from 'react'; import { observer, inject } from 'mobx-react'; -import { Divider, Badge, Tag, Tooltip } from 'antd'; +import { Divider, Badge } from 'antd'; import Base from 'containers/List'; import globalProjectStore, { ProjectStore } from 'stores/keystone/project'; -import { - yesNoOptions, - projectTagsColors, - emptyActionConfig, -} from 'utils/constants'; +import { yesNoOptions, emptyActionConfig } from 'utils/constants'; +import { SimpleTag } from 'resources/instance'; import actionConfigs from './actions'; import styles from './index.less'; @@ -124,31 +121,7 @@ export class Projects extends Base { { title: t('Tags'), dataIndex: 'tags', - render: (tags) => - tags.map((tag, index) => { - const isLongTag = tag.length > 20; - const tagEl = ( - - - {isLongTag ? `${tag.slice(0, 20)}...` : tag} - - - ); - return isLongTag ? ( - {tag}} - > - {tagEl} - - ) : ( - tagEl - ); - }), + render: (tags) => tags.map((tag, index) => SimpleTag({ tag, index })), isHideable: true, }, { diff --git a/src/pages/identity/containers/User/actions/SystemRole.jsx b/src/pages/identity/containers/User/actions/SystemRole.jsx index 952c6199..6fefb841 100644 --- a/src/pages/identity/containers/User/actions/SystemRole.jsx +++ b/src/pages/identity/containers/User/actions/SystemRole.jsx @@ -17,7 +17,7 @@ import { inject, observer } from 'mobx-react'; import { Select } from 'antd'; import globalProjectStore from 'stores/keystone/project'; import globalUserStore from 'stores/keystone/user'; -import globalRoleStore from 'stores/keystone/role'; +import { RoleStore } from 'stores/keystone/role'; import { ModalAction } from 'containers/Action'; import globalDomainStore from 'stores/keystone/domain'; @@ -34,7 +34,7 @@ export class SystemRole extends ModalAction { const systemRole = JSON.stringify(this.item.projectMapSystemRole); this.state.domainDefault = this.item.domain_id; this.state.projectRoles = JSON.parse(systemRole); - this.store = globalRoleStore; + this.store = new RoleStore(); this.domainStore = globalDomainStore; this.userStore = globalUserStore; this.getRoleList(); @@ -199,6 +199,10 @@ export class SystemRole extends ModalAction { (it) => it === this.adminRoleId )[0]; } + // for test e2e, will delete by next patch + localStorage.setItem('test-project-role', this.projectRolesList(id)); + localStorage.setItem('test-total-role', this.systemRoleList); + localStorage.setItem('test-actual', 'can get localstorage'); return (