From 7c556f66736bddcebb322d179deec4e1e8bd7cd8 Mon Sep 17 00:00:00 2001 From: chenwenchang <479999519@qq.com> Date: Mon, 2 Dec 2024 18:09:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20redis=E5=AE=89=E8=A3=85module?= =?UTF-8?q?=20#8020=20#=20Reviewed,=20transaction=20id:=2025565?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/src/common/const/clusterTypes.ts | 2 +- .../frontend/src/common/const/ticketTypes.ts | 1 + .../render-table/columns/select/index.vue | 2 + .../generateCloneData/index.ts | 2 + .../generateCloneData/redis/index.ts | 1 + .../generateCloneData/redis/installModule.ts | 38 +++ dbm-ui/frontend/src/locales/zh-cn.json | 6 + .../function-controller/functionController.ts | 2 + .../src/services/model/redis/redis.ts | 2 + .../services/model/ticket/details/redis.ts | 11 + .../src/services/source/redisToolbox.ts | 9 + dbm-ui/frontend/src/types/auto-imports.d.ts | 2 +- .../db-manage/redis/install-module/Index.vue | 49 ++++ .../install-module/pages/page1/Index.vue | 275 ++++++++++++++++++ .../pages/page1/components/Index.vue | 90 ++++++ .../pages/page1/components/RenderModule.vue | 166 +++++++++++ .../pages/page1/components/Row.vue | 162 +++++++++++ .../install-module/pages/page2/Index.vue | 87 ++++++ .../redis/list-ha/components/list/Index.vue | 7 + .../redis/list/components/list/Index.vue | 7 + .../src/views/db-manage/redis/routes.ts | 10 + .../src/views/db-manage/redis/toolbox-menu.ts | 18 +- .../components/demand-factory/Index.vue | 2 + .../demand-factory/redis/InstallModule.vue | 48 +++ 24 files changed, 991 insertions(+), 8 deletions(-) create mode 100644 dbm-ui/frontend/src/hooks/useTicketCloneInfo/generateCloneData/redis/installModule.ts create mode 100644 dbm-ui/frontend/src/views/db-manage/redis/install-module/Index.vue create mode 100644 dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/Index.vue create mode 100644 dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/components/Index.vue create mode 100644 dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/components/RenderModule.vue create mode 100644 dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/components/Row.vue create mode 100644 dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page2/Index.vue create mode 100644 dbm-ui/frontend/src/views/tickets/common/components/demand-factory/redis/InstallModule.vue diff --git a/dbm-ui/frontend/src/common/const/clusterTypes.ts b/dbm-ui/frontend/src/common/const/clusterTypes.ts index dab6801cea..95d71c603d 100644 --- a/dbm-ui/frontend/src/common/const/clusterTypes.ts +++ b/dbm-ui/frontend/src/common/const/clusterTypes.ts @@ -13,7 +13,7 @@ export enum ClusterTypes { // redis export enum ClusterTypes { REDIS = 'redis', // Redis - PREDIXY_REDIS_CLUSTER = 'PredixyRedisCluster', // Redis集群 + PREDIXY_REDIS_CLUSTER = 'PredixyRedisCluster', // RedisCluster集群 PREDIXY_TENDISPLUS_CLUSTER = 'PredixyTendisplusCluster', // Tendisplus存储版集群 TWEMPROXY_REDIS_INSTANCE = 'TwemproxyRedisInstance', // TendisCache集群 TWEMPROXY_TENDIS_SSD_INSTANCE = 'TwemproxyTendisSSDInstance', // TendisSSD集群 diff --git a/dbm-ui/frontend/src/common/const/ticketTypes.ts b/dbm-ui/frontend/src/common/const/ticketTypes.ts index bb9c81c0e5..3485a6a2e3 100644 --- a/dbm-ui/frontend/src/common/const/ticketTypes.ts +++ b/dbm-ui/frontend/src/common/const/ticketTypes.ts @@ -79,6 +79,7 @@ export enum TicketTypes { REDIS_INSTANCE_OPEN = 'REDIS_INSTANCE_OPEN', // redis 主从集群启用 REDIS_INSTANCE_CLOSE = 'REDIS_INSTANCE_CLOSE', // redis 主从集群禁用 REDIS_INSTANCE_DESTROY = 'REDIS_INSTANCE_DESTROY', // redis 主从集群删除 + REDIS_CLUSTER_LOAD_MODULES = 'REDIS_CLUSTER_LOAD_MODULES', // redis 安装Module } export enum TicketTypes { TENDBCLUSTER_APPLY = 'TENDBCLUSTER_APPLY', diff --git a/dbm-ui/frontend/src/components/render-table/columns/select/index.vue b/dbm-ui/frontend/src/components/render-table/columns/select/index.vue index aa26025734..a65d5326ee 100644 --- a/dbm-ui/frontend/src/components/render-table/columns/select/index.vue +++ b/dbm-ui/frontend/src/components/render-table/columns/select/index.vue @@ -46,6 +46,7 @@ @@ -64,6 +65,7 @@ export interface IListItem { value: IKey; label: string; + disabled?: boolean; [x: string]: any; } diff --git a/dbm-ui/frontend/src/hooks/useTicketCloneInfo/generateCloneData/index.ts b/dbm-ui/frontend/src/hooks/useTicketCloneInfo/generateCloneData/index.ts index f17e1bc98e..b20ebf88f9 100644 --- a/dbm-ui/frontend/src/hooks/useTicketCloneInfo/generateCloneData/index.ts +++ b/dbm-ui/frontend/src/hooks/useTicketCloneInfo/generateCloneData/index.ts @@ -39,6 +39,7 @@ import { generateRedisDataCopyCheckRepairCloneData, generateRedisDataCopyCloneData, generateRedisDataStructureCloneData, + generateRedisInstallModule, generateRedisMasterSlaveSwitchCloneData, generateRedisOperationCloneData, generateRedisProxyScaleDownCloneData, @@ -91,6 +92,7 @@ export const generateCloneDataHandlerMap = { [TicketTypes.REDIS_KEYS_DELETE]: generateRedisOperationCloneData, // Redis 删除Key [TicketTypes.REDIS_BACKUP]: generateRedisOperationCloneData, // Redis 集群备份 [TicketTypes.REDIS_PURGE]: generateRedisOperationCloneData, + [TicketTypes.REDIS_CLUSTER_LOAD_MODULES]: generateRedisInstallModule, // Redis 安装Module [TicketTypes.MYSQL_AUTHORIZE_RULES]: generateMysqlAuthorizeRuleCloneData, // Mysql 集群授权 [TicketTypes.MYSQL_IMPORT_SQLFILE]: generateMysqlImportSqlFileCloneData, // Mysql SQL变更执行 [TicketTypes.MYSQL_CHECKSUM]: generateMysqlChecksumCloneData, // Mysql 数据校验 diff --git a/dbm-ui/frontend/src/hooks/useTicketCloneInfo/generateCloneData/redis/index.ts b/dbm-ui/frontend/src/hooks/useTicketCloneInfo/generateCloneData/redis/index.ts index 08d2110cba..06fe5a9d17 100644 --- a/dbm-ui/frontend/src/hooks/useTicketCloneInfo/generateCloneData/redis/index.ts +++ b/dbm-ui/frontend/src/hooks/useTicketCloneInfo/generateCloneData/redis/index.ts @@ -6,6 +6,7 @@ export * from './clusterTypeUpdate'; export * from './dataCopy'; export * from './dataCopyCheckRepair'; export * from './dataStructure'; +export * from './installModule'; export * from './masterSlaveSwitch'; export * from './operation'; export * from './proxyScaleDown'; diff --git a/dbm-ui/frontend/src/hooks/useTicketCloneInfo/generateCloneData/redis/installModule.ts b/dbm-ui/frontend/src/hooks/useTicketCloneInfo/generateCloneData/redis/installModule.ts new file mode 100644 index 0000000000..ac029404f9 --- /dev/null +++ b/dbm-ui/frontend/src/hooks/useTicketCloneInfo/generateCloneData/redis/installModule.ts @@ -0,0 +1,38 @@ +/* + * TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available. + * + * Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at https://opensource.org/licenses/MIT + * + * 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 type { RedisInstallModuleDetails } from '@services/model/ticket/details/redis'; +import TicketModel from '@services/model/ticket/ticket'; + +import { random } from '@utils'; + +// Redis 安装Module +export function generateRedisInstallModule(ticketData: TicketModel) { + const { clusters, infos } = ticketData.details; + return Promise.resolve({ + tableDataList: infos.map((info) => { + const cluster = clusters[info.cluster_id]; + return { + rowKey: random(), + isLoading: false, + srcCluster: cluster.immute_domain, + clusterId: info.cluster_id, + bkCloudId: cluster.bk_cloud_id, + clusterType: cluster.cluster_type, + clusterTypeName: cluster.cluster_type_name, + dbVersion: info.db_version, + loadModules: info.load_modules, + }; + }), + remark: ticketData.remark, + }); +} diff --git a/dbm-ui/frontend/src/locales/zh-cn.json b/dbm-ui/frontend/src/locales/zh-cn.json index 20050ec280..50eb02bd2e 100644 --- a/dbm-ui/frontend/src/locales/zh-cn.json +++ b/dbm-ui/frontend/src/locales/zh-cn.json @@ -3662,5 +3662,11 @@ "源集群不能为空": "源集群不能为空", "源集群不存在": "源集群不存在", "源集群重复": "源集群重复", + "为集群安装扩展 Module,仅 RedisCluster、Redis 主从 支持安装 Module。": "为集群安装扩展 Module,仅 RedisCluster、Redis 主从 支持安装 Module。", + "安装 Module": "安装 Module", + "已安装": "已安装", + "请选择Module": "请选择Module", + "安装 Module任务提交成功": "安装 Module任务提交成功", + "仅允许同一管控区域的集群一起安装module": "仅允许同一管控区域的集群一起安装module", "这行勿动!新增翻译请在上一行添加!": "" } diff --git a/dbm-ui/frontend/src/services/model/function-controller/functionController.ts b/dbm-ui/frontend/src/services/model/function-controller/functionController.ts index cd1e8a7c7b..a05108fe6f 100644 --- a/dbm-ui/frontend/src/services/model/function-controller/functionController.ts +++ b/dbm-ui/frontend/src/services/model/function-controller/functionController.ts @@ -144,6 +144,7 @@ interface ControllerData { 'redis.instanceManage': ControllerItem; 'redis.haClusterManage': ControllerItem; 'redis.haInstanceManage': ControllerItem; + 'redis.toolbox.installModule': ControllerItem; 'redis.toolbox.capacityChange': ControllerItem; 'redis.toolbox.proxyScaleUp': ControllerItem; 'redis.toolbox.proxyScaleDown': ControllerItem; @@ -284,6 +285,7 @@ export default class FunctionController { 'redis.instanceManage': ControllerItem; 'redis.haClusterManage': ControllerItem; 'redis.haInstanceManage': ControllerItem; + 'redis.toolbox.installModule': ControllerItem; 'redis.toolbox.capacityChange': ControllerItem; 'redis.toolbox.proxyScaleUp': ControllerItem; 'redis.toolbox.proxyScaleDown': ControllerItem; diff --git a/dbm-ui/frontend/src/services/model/redis/redis.ts b/dbm-ui/frontend/src/services/model/redis/redis.ts index 460619137a..eaef5dc9a0 100644 --- a/dbm-ui/frontend/src/services/model/redis/redis.ts +++ b/dbm-ui/frontend/src/services/model/redis/redis.ts @@ -69,6 +69,7 @@ export default class Redis { machine_pair_cnt: number; major_version: string; master_domain: string; + module_names: string[]; operations: ClusterListOperation[]; permission: { access_entry_edit: boolean; @@ -122,6 +123,7 @@ export default class Redis { this.machine_pair_cnt = payload.machine_pair_cnt; this.major_version = payload.major_version; this.master_domain = payload.master_domain; + this.module_names = payload.module_names || []; this.operations = payload.operations || []; this.permission = payload.permission || {}; this.phase = payload.phase; diff --git a/dbm-ui/frontend/src/services/model/ticket/details/redis.ts b/dbm-ui/frontend/src/services/model/ticket/details/redis.ts index 422c0d17db..1554098ce2 100644 --- a/dbm-ui/frontend/src/services/model/ticket/details/redis.ts +++ b/dbm-ui/frontend/src/services/model/ticket/details/redis.ts @@ -418,3 +418,14 @@ export interface RedisVersionUpgrade extends DetailBase { target_version: string; }[]; } + +// redis 安装Module +export interface RedisInstallModuleDetails extends DetailBase { + bk_cloud_id: number; + clusters: DetailClusters; + infos: { + cluster_id: number; + db_version: string; + load_modules: string[]; + }[]; +} diff --git a/dbm-ui/frontend/src/services/source/redisToolbox.ts b/dbm-ui/frontend/src/services/source/redisToolbox.ts index 0f4fa07881..8f281a8aa4 100644 --- a/dbm-ui/frontend/src/services/source/redisToolbox.ts +++ b/dbm-ui/frontend/src/services/source/redisToolbox.ts @@ -124,3 +124,12 @@ export function getRedisClusterCapacityUpdateInfo(params: { err_msg: string; }>(`${getRootPath()}/get_cluster_capacity_update_info/`, params); } + +/** + * 查询集群模块信息 + */ +export function getRedisClusterModuleInfo(params: { cluster_id: number; version: string }) { + return http.get<{ + results: Record; + }>(`${getRootPath()}/get_cluster_module_info/`, params); +} diff --git a/dbm-ui/frontend/src/types/auto-imports.d.ts b/dbm-ui/frontend/src/types/auto-imports.d.ts index d0c217f6f1..7021ac5101 100644 --- a/dbm-ui/frontend/src/types/auto-imports.d.ts +++ b/dbm-ui/frontend/src/types/auto-imports.d.ts @@ -71,6 +71,6 @@ declare global { // for type re-export declare global { // @ts-ignore - export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' + export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' import('vue') } diff --git a/dbm-ui/frontend/src/views/db-manage/redis/install-module/Index.vue b/dbm-ui/frontend/src/views/db-manage/redis/install-module/Index.vue new file mode 100644 index 0000000000..e87e447e78 --- /dev/null +++ b/dbm-ui/frontend/src/views/db-manage/redis/install-module/Index.vue @@ -0,0 +1,49 @@ + + + + diff --git a/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/Index.vue b/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/Index.vue new file mode 100644 index 0000000000..c356053055 --- /dev/null +++ b/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/Index.vue @@ -0,0 +1,275 @@ + + + + + + + diff --git a/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/components/Index.vue b/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/components/Index.vue new file mode 100644 index 0000000000..d6283ccda9 --- /dev/null +++ b/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/components/Index.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/components/RenderModule.vue b/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/components/RenderModule.vue new file mode 100644 index 0000000000..98310b9c74 --- /dev/null +++ b/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/components/RenderModule.vue @@ -0,0 +1,166 @@ + + + + + + + diff --git a/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/components/Row.vue b/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/components/Row.vue new file mode 100644 index 0000000000..ebf8feaf5c --- /dev/null +++ b/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page1/components/Row.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page2/Index.vue b/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page2/Index.vue new file mode 100644 index 0000000000..49c3a83dca --- /dev/null +++ b/dbm-ui/frontend/src/views/db-manage/redis/install-module/pages/page2/Index.vue @@ -0,0 +1,87 @@ + + + + diff --git a/dbm-ui/frontend/src/views/db-manage/redis/list-ha/components/list/Index.vue b/dbm-ui/frontend/src/views/db-manage/redis/list-ha/components/list/Index.vue index e2eac396ec..e10da02abb 100644 --- a/dbm-ui/frontend/src/views/db-manage/redis/list-ha/components/list/Index.vue +++ b/dbm-ui/frontend/src/views/db-manage/redis/list-ha/components/list/Index.vue @@ -682,6 +682,12 @@ }, render: ({ data }: { data: RedisModel }) => {data.major_version || '--'}, }, + { + label: 'Modules', + field: 'module_names', + minWidth: 100, + render: ({ data }: { data: RedisModel }) => data.module_names.length ? data.module_names.map(item=>

{item}

) : '--', + }, { label: t('地域'), field: 'region', @@ -947,6 +953,7 @@ ClusterNodeKeys.REDIS_SLAVE, 'cluster_type_name', 'major_version', + 'module_names', 'region', ], showLineHeight: false, diff --git a/dbm-ui/frontend/src/views/db-manage/redis/list/components/list/Index.vue b/dbm-ui/frontend/src/views/db-manage/redis/list/components/list/Index.vue index c18be84ec9..723f7d41bc 100644 --- a/dbm-ui/frontend/src/views/db-manage/redis/list/components/list/Index.vue +++ b/dbm-ui/frontend/src/views/db-manage/redis/list/components/list/Index.vue @@ -708,6 +708,12 @@ }, render: ({ data }: ColumnRenderData) => data.major_version || '--', }, + { + label: 'Modules', + field: 'module_names', + minWidth: 100, + render: ({ data }: ColumnRenderData) => data.module_names.length ? data.module_names.map(item=>

{item}

) : '--', + }, { label: t('地域'), field: 'region', @@ -1024,6 +1030,7 @@ ClusterNodeKeys.REDIS_SLAVE, 'cluster_type_name', 'major_version', + 'module_names', 'region', ], showLineHeight: false, diff --git a/dbm-ui/frontend/src/views/db-manage/redis/routes.ts b/dbm-ui/frontend/src/views/db-manage/redis/routes.ts index 6695dc4fa3..51fc092d65 100644 --- a/dbm-ui/frontend/src/views/db-manage/redis/routes.ts +++ b/dbm-ui/frontend/src/views/db-manage/redis/routes.ts @@ -21,6 +21,15 @@ import { checkDbConsole } from '@utils'; import { t } from '@locales/index'; +const redisInstallModuleRoute = { + name: 'RedisInstallModule', + path: 'install-module/:page?', + meta: { + navName: t('安装 Module'), + }, + component: () => import('@views/db-manage/redis/install-module/Index.vue'), +}; + const redisCapacityChangeRoute = { name: 'RedisCapacityChange', path: 'capacity-change/:page?', @@ -157,6 +166,7 @@ const redisWebconsoleRoute = { }; const toolboxDbConsoleRouteMap = { + 'redis.toolbox.installModule': redisInstallModuleRoute, 'redis.toolbox.capacityChange': redisCapacityChangeRoute, 'redis.toolbox.proxyScaleUp': redisProxyScaleUpRoute, 'redis.toolbox.proxyScaleDown': redisProxyScaleDownRoute, diff --git a/dbm-ui/frontend/src/views/db-manage/redis/toolbox-menu.ts b/dbm-ui/frontend/src/views/db-manage/redis/toolbox-menu.ts index eace25a4d4..70909211bb 100644 --- a/dbm-ui/frontend/src/views/db-manage/redis/toolbox-menu.ts +++ b/dbm-ui/frontend/src/views/db-manage/redis/toolbox-menu.ts @@ -62,6 +62,12 @@ export default [ parentId: 'common-manage', dbConsoleValue: 'redis.toolbox.versionUpgrade', }, + { + name: t('安装 Module'), + id: 'RedisInstallModule', + parentId: 'cluster-manage', + dbConsoleValue: 'redis.toolbox.installModule', + }, ], }, { @@ -133,12 +139,6 @@ export default [ id: 'cluster-manage', icon: 'db-icon-cluster', children: [ - { - name: t('集群容量变更'), - id: 'RedisCapacityChange', - parentId: 'cluster-manage', - dbConsoleValue: 'redis.toolbox.capacityChange', - }, { name: t('扩容接入层'), id: 'RedisProxyScaleUp', @@ -151,6 +151,12 @@ export default [ parentId: 'cluster-manage', dbConsoleValue: 'redis.toolbox.proxyScaleDown', }, + { + name: t('集群容量变更'), + id: 'RedisCapacityChange', + parentId: 'cluster-manage', + dbConsoleValue: 'redis.toolbox.capacityChange', + }, { name: t('集群分片变更'), id: 'RedisClusterShardUpdate', diff --git a/dbm-ui/frontend/src/views/tickets/common/components/demand-factory/Index.vue b/dbm-ui/frontend/src/views/tickets/common/components/demand-factory/Index.vue index a3f8b85a6f..a4be879f43 100644 --- a/dbm-ui/frontend/src/views/tickets/common/components/demand-factory/Index.vue +++ b/dbm-ui/frontend/src/views/tickets/common/components/demand-factory/Index.vue @@ -101,6 +101,7 @@ import DetailsRedis from './redis/Details.vue'; import DetailsRedisHa from './redis/DetailsHa.vue'; import RedisHaClusterOperation from './redis/HaClusterOperation.vue'; + import RedisInstallModule from './redis/InstallModule.vue'; import RedisMasterFailover from './redis/MasterFailover.vue'; import RedisOperation from './redis/Operation.vue'; import RedisProxyScaleDown from './redis/ProxyScaleDown.vue'; @@ -328,6 +329,7 @@ [TicketTypes.REDIS_DATACOPY_CHECK_REPAIR]: RedisDataCheckAndRepair, [TicketTypes.REDIS_CLUSTER_ROLLBACK_DATA_COPY]: RedisRollbackDataCopy, [TicketTypes.REDIS_VERSION_UPDATE_ONLINE]: RedisVersionUpgrade, + [TicketTypes.REDIS_CLUSTER_LOAD_MODULES]: RedisInstallModule, [TicketTypes.TENDBCLUSTER_APPLY]: DetailsSpider, [TicketTypes.TENDBCLUSTER_SPIDER_ADD_NODES]: SpiderAddNodes, [TicketTypes.TENDBCLUSTER_CHECKSUM]: SpiderCheckSum, diff --git a/dbm-ui/frontend/src/views/tickets/common/components/demand-factory/redis/InstallModule.vue b/dbm-ui/frontend/src/views/tickets/common/components/demand-factory/redis/InstallModule.vue new file mode 100644 index 0000000000..4afa160649 --- /dev/null +++ b/dbm-ui/frontend/src/views/tickets/common/components/demand-factory/redis/InstallModule.vue @@ -0,0 +1,48 @@ + + + + +