diff --git a/ui/src/components/TopoComponent/helper.ts b/ui/src/components/TopoComponent/helper.ts index 85d0517d7..8372bc407 100644 --- a/ui/src/components/TopoComponent/helper.ts +++ b/ui/src/components/TopoComponent/helper.ts @@ -3,6 +3,7 @@ import { CLUSTER_IMG_MAP, SERVER_IMG_MAP, ZONE_IMG_MAP, + TOPO_INFO_CONFIG } from '@/constants'; import { intl } from '@/utils/intl'; import { Graph, IG6GraphEvent, INode, IShape } from '@antv/g6'; @@ -199,12 +200,18 @@ export const formatTopoData = ( }; topoData.children = getChildren(responseData.topology, tenantReplicas); - let basicInfo: BasicInfoType = { - name: responseData.name, - namespace: responseData.namespace, - status: responseData.status, - image: responseData.image, - }; + // let basicInfo: BasicInfoType = { + // name: responseData.name, + // namespace: responseData.namespace, + // status: responseData.status, + // image: responseData.image, + // }; + let basicInfo:API.ClusterInfo = {}; + for(let key of Object.keys(responseData)){ + if(TOPO_INFO_CONFIG.includes(key)){ + basicInfo[key] = responseData[key]; + } + } return { topoData, diff --git a/ui/src/components/TopoComponent/index.tsx b/ui/src/components/TopoComponent/index.tsx index 78239a7dc..4ecee9abf 100644 --- a/ui/src/components/TopoComponent/index.tsx +++ b/ui/src/components/TopoComponent/index.tsx @@ -49,11 +49,13 @@ export default function TopoComponent({ const [inNode, setInNode] = useState(false); const [inModal, setInModal] = useState(false); const [operateDisable, setOperateDisable] = useState(false); + const [[ns, name]] = useState( namespace && clusterNameOfKubectl ? [namespace, clusterNameOfKubectl] : getNSName(), ); + //Control the visibility of operation and maintenance modal const [operateModalVisible, setOperateModalVisible] = useState(false); @@ -114,7 +116,7 @@ export default function TopoComponent({ zoneName: chooseZoneName.current, }); if (res.successful) { - message.success(res.message); + message.success(res.message || '删除成功'); getTopoData({ ns, name, useFor: 'topo', tenantReplicas }); } }; @@ -286,6 +288,7 @@ export default function TopoComponent({ ? header : originTopoData && ( diff --git a/ui/src/constants/index.ts b/ui/src/constants/index.ts index a7279802c..f1fe6b96e 100644 --- a/ui/src/constants/index.ts +++ b/ui/src/constants/index.ts @@ -60,7 +60,30 @@ const MINIMAL_CONFIG = { const RESULT_STATUS = ['running','failed']; -const BACKUP_RESULT_STATUS = ['RUNNING','FAILED','PAUSED'] +const BACKUP_RESULT_STATUS = ['RUNNING','FAILED','PAUSED']; + +const CLUSTER_INFO_CONFIG = [ + 'name', + 'namespace', + 'status', + 'image', + 'resource', + 'storage', + 'backupVolume', + 'monitor', + 'rootPasswordSecret', + 'mode', + 'parameters' +] + +const TOPO_INFO_CONFIG = [ + 'name', + 'namespace', + 'status', + 'image', + 'mode', + 'rootPasswordSecret', +] const RESOURCE_NAME_REG = /^[a-z\-]+$/; // use for tenant name or zone name @@ -82,5 +105,7 @@ export { RESULT_STATUS, BACKUP_RESULT_STATUS, RESOURCE_NAME_REG, - TZ_NAME_REG + TZ_NAME_REG, + CLUSTER_INFO_CONFIG, + TOPO_INFO_CONFIG }; diff --git a/ui/src/i18n/strings/zh-CN.json b/ui/src/i18n/strings/zh-CN.json index ee4144555..8783de191 100644 --- a/ui/src/i18n/strings/zh-CN.json +++ b/ui/src/i18n/strings/zh-CN.json @@ -628,6 +628,6 @@ "Dashboard.Detail.Overview.Replicas.MinimumAvailableCpu": "最小可用 CPU", "Dashboard.Detail.Overview.Replicas.ClogDiskSize": "Clog 盘大小", "Dashboard.Detail.Overview.Replicas.ResourcePool": "资源池", - "Dashboard.Detail.Overview.Replicas.ResourcePoolReplicazone": "资源池 - {{replicaZone}}", + "Dashboard.Detail.Overview.Replicas.ResourcePoolReplicazone": "资源池 - {replicaZone}", "Dashboard.Detail.Overview.TenantOverview": "租户概览" } diff --git a/ui/src/pages/Cluster/Detail/Overview/BasicInfo.tsx b/ui/src/pages/Cluster/Detail/Overview/BasicInfo.tsx index c6d9b208b..f6782d35b 100644 --- a/ui/src/pages/Cluster/Detail/Overview/BasicInfo.tsx +++ b/ui/src/pages/Cluster/Detail/Overview/BasicInfo.tsx @@ -1,6 +1,6 @@ import { COLOR_MAP } from '@/constants'; import { intl } from '@/utils/intl'; -import { Card, Col, Descriptions, Switch, Tag } from 'antd'; +import { Card,Col,Descriptions,Switch,Tag } from 'antd'; import { useState } from 'react'; import styles from './index.less'; @@ -10,69 +10,72 @@ export default function BasicInfo({ namespace, status, image, + mode, + rootPasswordSecret, resource, storage, backupVolume, monitor, - rootPasswordSecret, - mode, parameters, + extra = true, style, -}: API.ClusterInfo & { style: React.CSSProperties }) { +}: API.ClusterInfo & { style?: React.CSSProperties,extra?: boolean }) { const [checked, setChecked] = useState(false); - const OBServerConfig = [ - { - label: 'CPU', - value: resource.cpu, - }, - { - label: 'Memory', - value: resource.memory, - }, - { - label: intl.formatMessage({ - id: 'Dashboard.Detail.Overview.BasicInfo.DatafileStorageClass', - defaultMessage: 'Datafile 存储类', - }), - value: storage.dataStorage.storageClass, - }, - { - label: intl.formatMessage({ - id: 'Dashboard.Detail.Overview.BasicInfo.DatafileStorageSize', - defaultMessage: 'Datafile 存储大小', - }), - value: storage.dataStorage.size, - }, - { - label: intl.formatMessage({ - id: 'Dashboard.Detail.Overview.BasicInfo.RedologStorageClass', - defaultMessage: 'RedoLog 存储类', - }), - value: storage.redoLogStorage.storageClass, - }, - { - label: intl.formatMessage({ - id: 'Dashboard.Detail.Overview.BasicInfo.RedologSize', - defaultMessage: 'RedoLog 大小', - }), - value: storage.redoLogStorage.size, - }, - { - label: intl.formatMessage({ - id: 'Dashboard.Detail.Overview.BasicInfo.SystemLogStorageClass', - defaultMessage: '系统日志存储类', - }), - value: storage.sysLogStorage.storageClass, - }, - { - label: intl.formatMessage({ - id: 'Dashboard.Detail.Overview.BasicInfo.SystemLogStorageSize', - defaultMessage: '系统日志存储大小', - }), - value: storage.sysLogStorage.size, - }, - ]; - + const OBServerConfig = extra + ? [ + { + label: 'CPU', + value: resource.cpu, + }, + { + label: 'Memory', + value: resource.memory, + }, + { + label: intl.formatMessage({ + id: 'Dashboard.Detail.Overview.BasicInfo.DatafileStorageClass', + defaultMessage: 'Datafile 存储类', + }), + value: storage.dataStorage.storageClass, + }, + { + label: intl.formatMessage({ + id: 'Dashboard.Detail.Overview.BasicInfo.DatafileStorageSize', + defaultMessage: 'Datafile 存储大小', + }), + value: storage.dataStorage.size, + }, + { + label: intl.formatMessage({ + id: 'Dashboard.Detail.Overview.BasicInfo.RedologStorageClass', + defaultMessage: 'RedoLog 存储类', + }), + value: storage.redoLogStorage.storageClass, + }, + { + label: intl.formatMessage({ + id: 'Dashboard.Detail.Overview.BasicInfo.RedologSize', + defaultMessage: 'RedoLog 大小', + }), + value: storage.redoLogStorage.size, + }, + { + label: intl.formatMessage({ + id: 'Dashboard.Detail.Overview.BasicInfo.SystemLogStorageClass', + defaultMessage: '系统日志存储类', + }), + value: storage.sysLogStorage.storageClass, + }, + { + label: intl.formatMessage({ + id: 'Dashboard.Detail.Overview.BasicInfo.SystemLogStorageSize', + defaultMessage: '系统日志存储大小', + }), + value: storage.sysLogStorage.size, + }, + ] + : []; + return ( @@ -135,26 +138,28 @@ export default function BasicInfo({ {rootPasswordSecret || '-'} -
- - {intl.formatMessage({ - id: 'Dashboard.Detail.Overview.BasicInfo.DetailedClusterConfiguration', - defaultMessage: '集群详细配置', - })} - - setChecked(checked)} - /> -
- {checked && ( + {extra && ( +
+ + {intl.formatMessage({ + id: 'Dashboard.Detail.Overview.BasicInfo.DetailedClusterConfiguration', + defaultMessage: '集群详细配置', + })} + + setChecked(checked)} + /> +
+ )} + {checked && extra && (
{ showDeleteConfirm({ onOk: () => remove(record.zone), diff --git a/ui/src/pages/Cluster/Detail/Overview/helper.ts b/ui/src/pages/Cluster/Detail/Overview/helper.ts index cd3b07d16..ce74ddba1 100644 --- a/ui/src/pages/Cluster/Detail/Overview/helper.ts +++ b/ui/src/pages/Cluster/Detail/Overview/helper.ts @@ -1,4 +1,5 @@ // Functions without UI +import { CLUSTER_INFO_CONFIG } from "@/constants"; /** * Get the namespace, name and cluster name or tenant name through the path of the url @@ -32,20 +33,6 @@ const getNSName = () => { return res; }; -const clusterInfoConfig = [ - 'name', - 'namespace', - 'status', - 'image', - 'resource', - 'storage', - 'backupVolume', - 'monitor', - 'rootPasswordSecret', - 'mode', - 'parameters' -] - // if there is cluster|zone|server whose status isn't running,the return status is operating. const formatClusterData = (responseData: any): API.ClusterDetail => { const res: any = { @@ -59,7 +46,7 @@ const formatClusterData = (responseData: any): API.ClusterDetail => { if (key === 'status' && responseData[key] !== 'running') { status = 'operating'; } - if(clusterInfoConfig.includes(key)){ + if(CLUSTER_INFO_CONFIG.includes(key)){ res['info'][key] = responseData[key]; } if (key === 'metrics') { diff --git a/ui/src/pages/Cluster/New/Topo.tsx b/ui/src/pages/Cluster/New/Topo.tsx index 6ce747668..b3a40d751 100644 --- a/ui/src/pages/Cluster/New/Topo.tsx +++ b/ui/src/pages/Cluster/New/Topo.tsx @@ -1,18 +1,19 @@ import { intl } from '@/utils/intl'; -import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; +import { DeleteOutlined,PlusOutlined } from '@ant-design/icons'; import type { FormInstance } from 'antd'; import { - Button, - Card, - Col, - Form, - Input, - InputNumber, - Popconfirm, - Row, +Button, +Card, +Col, +Form, +Input, +InputNumber, +Popconfirm, +Row, } from 'antd'; import NodeSelector from '@/components/NodeSelector'; +import { TZ_NAME_REG } from '@/constants'; import { resourceNameRule } from './helper'; export default function Topo({ form }: { form: FormInstance }) { @@ -48,6 +49,7 @@ export default function Topo({ form }: { form: FormInstance }) { defaultMessage: 'Zone名称', }) } + validateFirst name={[name, 'zone']} rules={[ { @@ -57,6 +59,10 @@ export default function Topo({ form }: { form: FormInstance }) { defaultMessage: '请输入zone名称', }), }, + { + pattern: TZ_NAME_REG, + message: '首字符必须是字母或者下划线,不能包含 -', + }, resourceNameRule, ]} > @@ -142,8 +148,7 @@ export default function Topo({ form }: { form: FormInstance }) { onClick={() => add({ zone: `zone${fields.length + 1}`, - nodeSelector: [ - ], + nodeSelector: [], replicas: 1, }) } diff --git a/ui/src/pages/Tenant/Detail/Overview/BasicInfo.tsx b/ui/src/pages/Tenant/Detail/Overview/BasicInfo.tsx index 31b0d82b6..da1077b09 100644 --- a/ui/src/pages/Tenant/Detail/Overview/BasicInfo.tsx +++ b/ui/src/pages/Tenant/Detail/Overview/BasicInfo.tsx @@ -20,7 +20,7 @@ export default function BasicInfo({ id: 'Dashboard.Detail.Overview.BasicInfo.TenantName', defaultMessage: '租户名', }), - clusterName: intl.formatMessage({ + clusterResourceName: intl.formatMessage({ id: 'Dashboard.Detail.Overview.BasicInfo.ClusterName', defaultMessage: '集群名', }), diff --git a/ui/src/pages/Tenant/Detail/Overview/Replicas.tsx b/ui/src/pages/Tenant/Detail/Overview/Replicas.tsx index 9948f9d02..33752de3b 100644 --- a/ui/src/pages/Tenant/Detail/Overview/Replicas.tsx +++ b/ui/src/pages/Tenant/Detail/Overview/Replicas.tsx @@ -1,11 +1,18 @@ import CollapsibleCard from '@/components/CollapsibleCard'; +import showDeleteConfirm from '@/components/customModal/DeleteModal'; +import { getNSName } from '@/pages/Cluster/Detail/Overview/helper'; +import { deleteObtenantPool } from '@/services/tenant'; import { intl } from '@/utils/intl'; -import { Col,Descriptions } from 'antd'; +import { Button,Col,Descriptions,message } from 'antd'; +import type { ClusterNSName } from '.'; +import styles from './index.less'; export default function Replicas({ replicaList, + refreshTenant }: { replicaList: API.ReplicaDetailType[]; + refreshTenant:()=>void; }) { const LABEL_TEXT_MAP = { priority: intl.formatMessage({ @@ -41,6 +48,16 @@ export default function Replicas({ keys[memorySizeIdx] = temp; return keys; }; + + const deleteZone = async (zoneName: string) => { + const [ns, name] = getNSName(); + const res = await deleteObtenantPool({ ns, name, zoneName }); + if (res.successful) { + refreshTenant(); + message.success(res.message || '删除成功'); + } + }; + return ( {replicaList.map((replica, index) => ( + + {intl.formatMessage( + { + id: 'Dashboard.Detail.Overview.Replicas.ResourcePoolReplicazone', + defaultMessage: '资源池 - {{replicaZone}}', + }, + { replicaZone: replica.zone }, + )} + +
+ + +
+
+ } > {sortKeys(Object.keys(replica)).map((key, idx) => ( diff --git a/ui/src/pages/Tenant/Detail/Overview/index.less b/ui/src/pages/Tenant/Detail/Overview/index.less index 283071ff4..dea8cf946 100644 --- a/ui/src/pages/Tenant/Detail/Overview/index.less +++ b/ui/src/pages/Tenant/Detail/Overview/index.less @@ -1,37 +1,40 @@ -.tenantContainer{ - :global{ - .ant-tooltip-arrow::before{ - background-color: #ffffff; - } - .ant-tooltip-inner{ - background-color: #ffffff; - color: #132039; - } - .ant-pro-card-collapsible-icon{ - position: absolute; - right: 12px; - top: 28px; - } - .ant-pro-card-collapsible-icon svg { - height: 24px; - width: 24px; - } +.tenantContainer { + :global { + .ant-tooltip-arrow::before { + background-color: #ffffff; } + .ant-tooltip-inner { + background-color: #ffffff; + color: #132039; + } + .ant-pro-card-collapsible-icon { + position: absolute; + right: 12px; + top: 28px; + } + .ant-pro-card-collapsible-icon svg { + height: 24px; + width: 24px; + } + } } ul { - list-style-type: none; - margin: 0; - padding: 0; - } - - li { - display: block; - margin: 0; - padding: 5px 20px; - cursor: pointer; - } - - li:hover { - background-color: rgb(248, 250, 254); - } - \ No newline at end of file + list-style-type: none; + margin: 0; + padding: 0; +} + +li { + display: block; + margin: 0; + padding: 5px 20px; + cursor: pointer; +} + +li:hover { + background-color: rgb(248, 250, 254); +} +.titleContainer { + display: flex; + justify-content: space-between; +} diff --git a/ui/src/pages/Tenant/Detail/Overview/index.tsx b/ui/src/pages/Tenant/Detail/Overview/index.tsx index 52f323395..f39556f53 100644 --- a/ui/src/pages/Tenant/Detail/Overview/index.tsx +++ b/ui/src/pages/Tenant/Detail/Overview/index.tsx @@ -1,25 +1,25 @@ import EventsTable from '@/components/EventsTable'; import showDeleteConfirm from '@/components/customModal/DeleteModal'; import OperateModal from '@/components/customModal/OperateModal'; -import { REFRESH_TENANT_TIME, RESULT_STATUS } from '@/constants'; +import { REFRESH_TENANT_TIME,RESULT_STATUS } from '@/constants'; import { getNSName } from '@/pages/Cluster/Detail/Overview/helper'; import { - getEssentialParameters as getEssentialParametersReq, - getSimpleClusterList, +getEssentialParameters as getEssentialParametersReq, +getSimpleClusterList, } from '@/services'; import { - deleteTenent, - getBackupJobs, - getBackupPolicy, - getTenant, +deleteTenent, +getBackupJobs, +getBackupPolicy, +getTenant, } from '@/services/tenant'; import { intl } from '@/utils/intl'; import { EllipsisOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; import { history } from '@umijs/max'; import { useRequest } from 'ahooks'; -import { Button, Row, Tooltip, message } from 'antd'; -import { useEffect, useRef, useState } from 'react'; +import { Button,Row,Tooltip,message } from 'antd'; +import { useEffect,useRef,useState } from 'react'; import Backups from './Backups'; import BasicInfo from './BasicInfo'; import Replicas from './Replicas'; @@ -33,6 +33,8 @@ type OperateItemConfigType = { danger?: boolean; }; +export type ClusterNSName = { ns?: string; name?: string } + export default function TenantOverview() { const [operateModalVisible, setOperateModalVisible] = useState(false); @@ -112,15 +114,15 @@ export default function TenantOverview() { const backupJobs = backupJobsResponse?.data; const essentialParameter = essentialParameterRes?.data; const operateListConfig: OperateItemConfigType[] = [ - { - text: intl.formatMessage({ - id: 'Dashboard.Detail.Overview.UnitSpecificationManagement', - defaultMessage: 'Unit规格管理', - }), - onClick: () => openOperateModal('modifyUnitSpecification'), - show: tenantDetail?.info.tenantRole === 'PRIMARY', - isMore: false, - }, + // { + // text: intl.formatMessage({ + // id: 'Dashboard.Detail.Overview.UnitSpecificationManagement', + // defaultMessage: 'Unit规格管理', + // }), + // onClick: () => openOperateModal('modifyUnitSpecification'), + // show: tenantDetail?.info.tenantRole === 'PRIMARY', + // isMore: false, + // }, { text: intl.formatMessage({ id: 'Dashboard.Detail.Overview.ChangePassword', @@ -213,7 +215,12 @@ export default function TenantOverview() { ...operateListConfig .filter((item) => item.show && !item.isMore) .map((item, index) => ( - )), @@ -282,7 +289,10 @@ export default function TenantOverview() { )} {tenantDetail && tenantDetail.replicas && ( - + )} diff --git a/ui/src/pages/Tenant/Detail/Topo/index.tsx b/ui/src/pages/Tenant/Detail/Topo/index.tsx index afe7264ad..7c39b1701 100644 --- a/ui/src/pages/Tenant/Detail/Topo/index.tsx +++ b/ui/src/pages/Tenant/Detail/Topo/index.tsx @@ -10,7 +10,6 @@ export default function Topo() { defaultParams: [{ ns, name }], }); const tenantTopoData = tenantResponse?.data; - return (
{tenantTopoData && ( diff --git a/ui/src/pages/Tenant/TenantsList.tsx b/ui/src/pages/Tenant/TenantsList.tsx index 9188e96f0..c8301b9c5 100644 --- a/ui/src/pages/Tenant/TenantsList.tsx +++ b/ui/src/pages/Tenant/TenantsList.tsx @@ -1,8 +1,8 @@ +import { COLOR_MAP } from '@/constants'; import { intl } from '@/utils/intl'; import { Link } from '@umijs/max'; -import { Button, Col, Table, Tag, Card } from 'antd'; +import { Button,Card,Col,Table,Tag } from 'antd'; import type { ColumnsType } from 'antd/es/table'; -import { COLOR_MAP } from '@/constants'; import styles from './index.less'; @@ -20,7 +20,12 @@ const columns: ColumnsType = [ dataIndex: 'name', key: 'name', render: (value, record) => ( - {value} + + {value} + ), }, { @@ -36,8 +41,8 @@ const columns: ColumnsType = [ id: 'Dashboard.pages.Tenant.TenantsList.Cluster', defaultMessage: '所属集群', }), - dataIndex: 'clusterName', - key: 'clusterName', + dataIndex: 'clusterResourceName', + key: 'clusterResourceName', }, { title: intl.formatMessage({ @@ -75,7 +80,7 @@ const columns: ColumnsType = [ }), dataIndex: 'status', key: 'status', - render:(value)=>{value} , + render: (value) => {value} , }, { title: intl.formatMessage({ diff --git a/ui/src/services/tenant.ts b/ui/src/services/tenant.ts index c21552f97..3ffc2f474 100644 --- a/ui/src/services/tenant.ts +++ b/ui/src/services/tenant.ts @@ -132,7 +132,7 @@ export async function getBackupPolicy({ 'bakEncryptionSecret', 'jobKeepDays', 'pieceIntervalDays', - 'recoveryDays' + 'recoveryDays', ]; if (r.successful && r.data) { @@ -148,7 +148,7 @@ export async function getBackupJobs({ ns, name, type, - limit + limit, }: API.NamespaceAndName & { type: API.JobType; limit?: number; @@ -177,7 +177,7 @@ export async function getBackupJobs({ return r; } -// 备租户回放日志 +// tenant replay log export async function replayLogOfTenant({ ns, name, @@ -219,7 +219,7 @@ export async function modifyUnitNumber({ }); } -// 升级特定租户的租户兼容版本以匹配集群版本 +// Upgrade the tenant-compatible version of a specific tenant to match the cluster version export async function upgradeTenantCompatibilityVersion({ ns, name, @@ -254,3 +254,42 @@ export async function patchTenantConfiguration({ data: body, }); } + +// Create obtenant pool +export async function createObtenantPool({ + ns, + name, + zoneName, + ...body +}: API.PoolConfig & + API.NamespaceAndName & { zoneName: string }): Promise { + return request(`${tenantPrefix}/${ns}/${name}/pools/${zoneName}`, { + method: 'PUT', + data: body, + }); +} + +// Delete obtenant pool +export async function deleteObtenantPool({ + ns, + name, + zoneName, +}: API.NamespaceAndName & { zoneName: string }): Promise { + return request(`${tenantPrefix}/${ns}/${name}/pools/${zoneName}`, { + method: 'DELETE', + }); +} + +// Patch obtenant pool +export async function patchObtenantPool({ + ns, + name, + zoneName, + ...body +}: API.PoolConfig & + API.NamespaceAndName & { zoneName: string }): Promise { + return request(`${tenantPrefix}/${ns}/${name}/pools/${zoneName}`, { + method: 'PATCH', + data: body, + }); +} diff --git a/ui/src/services/typings.d.ts b/ui/src/services/typings.d.ts index b5bf2c247..36b4033d2 100644 --- a/ui/src/services/typings.d.ts +++ b/ui/src/services/typings.d.ts @@ -28,29 +28,29 @@ declare namespace API { namespace: string; status: string; image: string; - resource:{ + rootPasswordSecret: string; + mode: ClusterMode; + resource?:{ cpu: number; memory: number; }; - storage: { + storage?: { dataStorage: Storage; redoLogStorage: Storage; sysLogStorage: Storage; }; - backupVolume: { + backupVolume?: { address: string; path: string; }; - monitor: { + monitor?: { image: string; resource: { cpu: number; memory: number; }; }; - rootPasswordSecret: string; - mode: ClusterMode; - parameters: { + parameters?: { key: string; value: string; }[]; @@ -239,6 +239,18 @@ declare namespace API { minIops?: number; } + type PoolConfig = { + priority: number; + unitConfig: { + iopsWeight: number; + logDiskSize: string; + cpuCount: string; + maxIops: number; + memorySize: string; + minIops: number; + }; + }; + type TenantBody = { connectWhiteList?: string; name: string;