This is an automated email from the ASF dual-hosted git repository.

lzwang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/inlong.git


The following commit(s) were added to refs/heads/master by this push:
     new 357a3b99aa [INLONG-8431][Dashboard] Support tenant management and 
tenant role management (#8432)
357a3b99aa is described below

commit 357a3b99aa6368f594455551f3c52b46657721f5
Author: Lizhen <[email protected]>
AuthorDate: Fri Jul 7 10:22:25 2023 +0800

    [INLONG-8431][Dashboard] Support tenant management and tenant role 
management (#8432)
---
 inlong-dashboard/src/configs/menus/conf.tsx        |   6 +
 inlong-dashboard/src/configs/routes/conf.ts        |   4 +
 inlong-dashboard/src/core/stores/index.ts          |  12 ++
 inlong-dashboard/src/core/utils/localStorage.ts    |  38 ++++
 inlong-dashboard/src/core/utils/request.ts         |   7 +-
 inlong-dashboard/src/i18n.ts                       |   2 +
 .../src/ui/components/Layout/NavWidget/index.tsx   |   3 +
 .../src/ui/components/Layout/Tenant/index.tsx      | 139 +++++++++++++
 .../src/ui/components/Layout/index.tsx             |  21 +-
 .../src/ui/components/Provider/index.tsx           |   4 +
 inlong-dashboard/src/ui/locales/cn.json            |  12 +-
 inlong-dashboard/src/ui/locales/en.json            |  12 +-
 .../src/ui/pages/TenantManagement/DetailModal.tsx  | 214 +++++++++++++++++++++
 .../src/ui/pages/TenantManagement/TenantModal.tsx  |  83 ++++++++
 .../src/ui/pages/TenantManagement/config.tsx       |  71 +++++++
 .../src/ui/pages/TenantManagement/index.tsx        | 130 +++++++++++++
 16 files changed, 754 insertions(+), 4 deletions(-)

diff --git a/inlong-dashboard/src/configs/menus/conf.tsx 
b/inlong-dashboard/src/configs/menus/conf.tsx
index 2aa2f1f6ac..0552624d1e 100644
--- a/inlong-dashboard/src/configs/menus/conf.tsx
+++ b/inlong-dashboard/src/configs/menus/conf.tsx
@@ -79,8 +79,14 @@ const conf: MenuItemType[] = [
         path: '/user',
         name: i18n.t('configs.menus.UserManagement'),
       },
+      {
+        path: '/tenant',
+        isAdmin: true,
+        name: i18n.t('configs.menus.TenantManagement'),
+      },
       {
         path: '/approval',
+        isAdmin: true,
         name: i18n.t('configs.menus.ProcessManagement'),
       },
     ],
diff --git a/inlong-dashboard/src/configs/routes/conf.ts 
b/inlong-dashboard/src/configs/routes/conf.ts
index 703e3f0a69..667f0c3f91 100644
--- a/inlong-dashboard/src/configs/routes/conf.ts
+++ b/inlong-dashboard/src/configs/routes/conf.ts
@@ -111,6 +111,10 @@ const conf: RouteProps[] = [
       },
     ],
   },
+  {
+    path: '/tenant',
+    component: () => import('@/ui/pages/TenantManagement'),
+  },
   {
     component: () => import('@/ui/pages/Error/404'),
   },
diff --git a/inlong-dashboard/src/core/stores/index.ts 
b/inlong-dashboard/src/core/stores/index.ts
index 09eab7b583..1ea9effc01 100644
--- a/inlong-dashboard/src/core/stores/index.ts
+++ b/inlong-dashboard/src/core/stores/index.ts
@@ -28,6 +28,8 @@ export interface State {
   userName: string;
   userId: number;
   roles: string[];
+  tenant: string;
+  tenantList: string[];
   currentMenu: null | Omit<MenuItemType, 'children'>;
 }
 
@@ -36,6 +38,8 @@ const state: State = {
   userName: '',
   userId: 0,
   roles: [],
+  tenant: '',
+  tenantList: [],
   currentMenu: null,
 };
 
@@ -46,6 +50,7 @@ const reducers = {
       userName: payload.userName,
       userId: payload.userId,
       roles: payload.roles,
+      tenant: payload.tenant,
     };
   },
 
@@ -56,6 +61,13 @@ const reducers = {
     };
   },
 
+  setTenantInfo(state, payload) {
+    return {
+      ...state,
+      tenantList: payload.tenantList,
+    };
+  },
+
   setCurrentMenu(state, payload) {
     const pathname = payload && payload.pathname;
     if (!pathname) return state;
diff --git a/inlong-dashboard/src/core/utils/localStorage.ts 
b/inlong-dashboard/src/core/utils/localStorage.ts
new file mode 100644
index 0000000000..9b6054a0c8
--- /dev/null
+++ b/inlong-dashboard/src/core/utils/localStorage.ts
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+export const useLocalStorage = name => {
+  const getLocalStorage = () => {
+    const local = localStorage.getItem(name);
+
+    if (local !== null) {
+      return JSON.parse(local);
+    }
+
+    return null;
+  };
+
+  const setLocalStorage = item => {
+    localStorage.setItem(name, JSON.stringify(item));
+  };
+
+  const removeLocalStorage = () => {
+    return localStorage.removeItem(name);
+  };
+
+  return [getLocalStorage, setLocalStorage, removeLocalStorage];
+};
diff --git a/inlong-dashboard/src/core/utils/request.ts 
b/inlong-dashboard/src/core/utils/request.ts
index 17c8d7c488..07297911f8 100644
--- a/inlong-dashboard/src/core/utils/request.ts
+++ b/inlong-dashboard/src/core/utils/request.ts
@@ -23,6 +23,7 @@ import nprogress from 'nprogress';
 import { config } from '@/configs/default';
 import requestConcurrentMiddleware from './requestConcurrentMiddleware';
 import 'nprogress/nprogress.css';
+import { useLocalStorage } from './localStorage';
 
 export interface FetchOptions extends RequestOptionsInit {
   url: string;
@@ -46,6 +47,9 @@ const extendRequest = extend({
   timeout: 60 * 1000,
 });
 
+// eslint-disable-next-line react-hooks/rules-of-hooks
+const [getLocalStorage, setLocalStorage, removeLocalStorage] = 
useLocalStorage('tenant');
+
 extendRequest.use(requestConcurrentMiddleware);
 
 const fetch = (options: FetchOptions) => {
@@ -63,10 +67,11 @@ const fetch = (options: FetchOptions) => {
     //     });
     // });
   }
-
+  const tenantName = getLocalStorage('tenant')?.['name'];
   const config = { ...options };
   delete config.url;
   delete config.fetchType;
+  config.headers = tenantName === undefined ? {} : { tenant: tenantName };
 
   return extendRequest(url, {
     method,
diff --git a/inlong-dashboard/src/i18n.ts b/inlong-dashboard/src/i18n.ts
index 61d06fe5fa..36457c022d 100644
--- a/inlong-dashboard/src/i18n.ts
+++ b/inlong-dashboard/src/i18n.ts
@@ -35,6 +35,7 @@ const resources = {
       'configs.menus.ProcessManagement': 'Process Management',
       'configs.menus.Nodes': 'DataNodes',
       'configs.menus.DataSynchronize': 'Synchronization',
+      'configs.menus.TenantManagement': 'Tenant Management',
     },
   },
   cn: {
@@ -49,6 +50,7 @@ const resources = {
       'configs.menus.ProcessManagement': '流程管理',
       'configs.menus.Nodes': '数据节点',
       'configs.menus.DataSynchronize': '数据同步',
+      'configs.menus.TenantManagement': '租户管理',
     },
   },
 };
diff --git a/inlong-dashboard/src/ui/components/Layout/NavWidget/index.tsx 
b/inlong-dashboard/src/ui/components/Layout/NavWidget/index.tsx
index 44568cff59..7fa611e131 100644
--- a/inlong-dashboard/src/ui/components/Layout/NavWidget/index.tsx
+++ b/inlong-dashboard/src/ui/components/Layout/NavWidget/index.tsx
@@ -25,10 +25,12 @@ import { useTranslation } from 'react-i18next';
 import request from '@/core/utils/request';
 import PasswordModal from './PasswordModal';
 import KeyModal from './KeyModal';
+import { useLocalStorage } from '@/core/utils/localStorage';
 
 const Comp: React.FC = () => {
   const { t } = useTranslation();
   const userName = useSelector<State, State['userName']>(state => 
state.userName);
+  const [getLocalStorage, setLocalStorage, removeLocalStorage] = 
useLocalStorage('tenant');
 
   const [createModal, setCreateModal] = useState<Record<string, unknown>>({
     open: false,
@@ -40,6 +42,7 @@ const Comp: React.FC = () => {
 
   const runLogout = async () => {
     await request('/anno/logout');
+    removeLocalStorage('tenant');
     window.location.href = '/';
   };
 
diff --git a/inlong-dashboard/src/ui/components/Layout/Tenant/index.tsx 
b/inlong-dashboard/src/ui/components/Layout/Tenant/index.tsx
new file mode 100644
index 0000000000..b2da26d48a
--- /dev/null
+++ b/inlong-dashboard/src/ui/components/Layout/Tenant/index.tsx
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 { Divider, Dropdown, Input, MenuProps, message, Space, theme } from 
'antd';
+import { useDispatch, useRequest, useSelector } from '@/ui/hooks';
+import { State } from '@/core/stores';
+import { useTranslation } from 'react-i18next';
+import { useLocalStorage } from '@/core/utils/localStorage';
+
+const { useToken } = theme;
+
+const Comp: React.FC = () => {
+  const { t } = useTranslation();
+  const tenant = useSelector<State, State['tenant']>(state => state.tenant);
+  const userName = useSelector<State, State['userName']>(state => 
state.userName);
+  const roles = useSelector<State, State['roles']>(state => state.roles);
+  const [getLocalStorage, setLocalStorage, removeLocalStorage] = 
useLocalStorage('tenant');
+
+  const { token } = useToken();
+  const contentStyle = {
+    backgroundColor: token.colorBgElevated,
+    borderRadius: token.borderRadiusLG,
+    boxShadow: token.boxShadowSecondary,
+  };
+  const [filter, setFilter] = useState(true);
+  const [filterData, setFilterData] = useState([]);
+  const [data, setData] = useState([]);
+
+  const defaultOptions = {
+    keyword: '',
+    pageSize: 10,
+    pageNum: 1,
+  };
+  const dispatch = useDispatch();
+
+  const [options, setOptions] = useState(defaultOptions);
+
+  const { run: getStreamData } = useRequest(
+    {
+      url: '/tenant/listByUser',
+      method: 'POST',
+      data: {
+        ...options,
+      },
+    },
+    {
+      refreshDeps: [options],
+      manual: userName !== undefined ? false : true,
+      onSuccess: result => {
+        const tenant = [];
+        const tenantList = [];
+        result.list.map(item => {
+          tenantList.push(item.name);
+          tenant.push({
+            label: item.name,
+            key: item.name,
+          });
+        });
+        dispatch({
+          type: 'setTenantInfo',
+          payload: {
+            tenantList: tenantList,
+          },
+        });
+        setData(tenant);
+      },
+    },
+  );
+
+  const { run: getData } = useRequest(
+    name => ({
+      url: `/user/currentUser`,
+      method: 'post',
+      headers: {
+        tenant: name,
+      },
+    }),
+    {
+      manual: true,
+    },
+  );
+
+  const onClick: MenuProps['onClick'] = ({ key }) => {
+    getData(key);
+    setLocalStorage({ name: key });
+    message.success(t('components.Layout.Tenant.Success'));
+    window.location.reload();
+  };
+
+  const onFilter = allValues => {
+    setFilterData(data.filter(item => item.key === allValues));
+    setFilter(false);
+  };
+
+  useEffect(() => {
+    if (userName !== undefined) {
+      getStreamData();
+    }
+  }, [userName]);
+
+  return (
+    <>
+      <Dropdown
+        menu={{ items: filter ? data : filterData, onClick }}
+        placement="bottomLeft"
+        dropdownRender={menu => (
+          <div style={contentStyle}>
+            {React.cloneElement(menu as React.ReactElement, { style: { 
boxShadow: 'none' } })}
+            <Divider style={{ margin: 0 }} />
+            <Space style={{ padding: 8 }}>
+              <Input.Search allowClear onSearch={onFilter} style={{ width: 130 
}} />
+            </Space>
+          </div>
+        )}
+      >
+        <span style={{ fontSize: 14 }}>{tenant}</span>
+      </Dropdown>
+    </>
+  );
+};
+
+export default Comp;
diff --git a/inlong-dashboard/src/ui/components/Layout/index.tsx 
b/inlong-dashboard/src/ui/components/Layout/index.tsx
index 63c2382296..f9c5ab7bc9 100644
--- a/inlong-dashboard/src/ui/components/Layout/index.tsx
+++ b/inlong-dashboard/src/ui/components/Layout/index.tsx
@@ -36,6 +36,7 @@ import type { MenuProps } from 'antd/es/menu';
 import { State } from '@/core/stores';
 import NavWidget from './NavWidget';
 import LocaleSelect from './NavWidget/LocaleSelect';
+import Tenant from './Tenant';
 
 const BasicLayout: React.FC = props => {
   const location = useLocation();
@@ -46,8 +47,25 @@ const BasicLayout: React.FC = props => {
   const { pathname } = location;
   const roles = useSelector<State, State['roles']>(state => state.roles);
   const { breadcrumbMap, menuData } = useMemo(() => {
+    if (roles?.includes('INLONG_ADMIN') || roles?.includes('INLONG_OPERATOR')) 
{
+      const _menus = menusTree.filter(
+        item => (item.isAdmin && roles?.includes('INLONG_ADMIN')) || 
!item.isAdmin,
+      );
+      return getMenuData(_menus);
+    }
+    if (roles?.includes('TENANT_ADMIN')) {
+      const _menus = menusTree.filter(item => {
+        if (item.isAdmin) {
+          item.children = item.children?.filter(
+            i => (i.isAdmin && roles.includes('TENANT_ADMIN')) || i.isAdmin,
+          );
+        }
+        return item;
+      });
+      return getMenuData(_menus);
+    }
     const _menus = menusTree.filter(
-      item => (item.isAdmin && roles?.includes('ADMIN')) || !item.isAdmin,
+      item => (!item.isAdmin && roles?.includes('TENANT_OPERATOR')) || 
!item.isAdmin,
     );
     return getMenuData(_menus);
   }, [roles]);
@@ -113,6 +131,7 @@ const BasicLayout: React.FC = props => {
           ),
           <LocaleSelect />,
           <NavWidget />,
+          <Tenant />,
         ]}
       >
         {props.children}
diff --git a/inlong-dashboard/src/ui/components/Provider/index.tsx 
b/inlong-dashboard/src/ui/components/Provider/index.tsx
index a07edcce43..2721fda9e4 100644
--- a/inlong-dashboard/src/ui/components/Provider/index.tsx
+++ b/inlong-dashboard/src/ui/components/Provider/index.tsx
@@ -25,11 +25,13 @@ import { State } from '@/core/stores';
 import { localesConfig } from '@/configs/locales';
 import i18n from '@/i18n';
 import './antd.cover.less';
+import { useLocalStorage } from '@/core/utils/localStorage';
 
 const Provider = ({ children }) => {
   const dispatch = useDispatch();
 
   const locale = useSelector<State, State['locale']>(state => state.locale);
+  const [getLocalStorage, setLocalStorage, removeLocalStorage] = 
useLocalStorage('tenant');
 
   const [antdMessages, setAntdMessages] = useState();
 
@@ -40,12 +42,14 @@ const Provider = ({ children }) => {
     },
     {
       onSuccess: result => {
+        setLocalStorage({ name: result.tenant });
         dispatch({
           type: 'setUserInfo',
           payload: {
             userName: result.name,
             userId: result.userId,
             roles: result.roles,
+            tenant: result.tenant,
           },
         });
       },
diff --git a/inlong-dashboard/src/ui/locales/cn.json 
b/inlong-dashboard/src/ui/locales/cn.json
index 4356466a22..0952b3f868 100644
--- a/inlong-dashboard/src/ui/locales/cn.json
+++ b/inlong-dashboard/src/ui/locales/cn.json
@@ -536,6 +536,7 @@
   "components.Layout.NavWidget.NewPassword": "新密码",
   "components.Layout.NavWidget.ConfirmPassword": "确认密码",
   "components.Layout.NavWidget.Remind": "密码不一致,请重新输入",
+  "components.Layout.Tenant.Success": "切换成功",
   "components.HighSelect.Customize": "自定义",
   "components.HighSelect.SearchPlaceholder": "请输入关键字搜索",
   "components.NodeSelect.Create": "新建节点",
@@ -796,5 +797,14 @@
   "components.FieldList.Sink": "目标字段",
   "components.FieldList.CreateSource": "新建源字段",
   "components.FieldList.CreateSink": "新建目标字段",
-  "components.FieldList.AddField": "添加字段"
+  "components.FieldList.AddField": "添加字段",
+  "pages.Tenant.config.Description": "描述",
+  "pages.Tenant.config.Name": "租户名称",
+  "pages.Tenant.New": "新建角色",
+  "pages.Tenant.config.Username": "用户名称",
+  "pages.Tenant.config.TenantRole": "租户角色",
+  "pages.Tenant.config.Admin": "租户管理员",
+  "pages.Tenant.config.GeneralUser": "普通用户",
+  "pages.Tenant.config.Creator": "创建人",
+  "pages.Tenant.config.CreateTime": "创建时间"
 }
diff --git a/inlong-dashboard/src/ui/locales/en.json 
b/inlong-dashboard/src/ui/locales/en.json
index 7864b5e8c4..5593a6cfdf 100644
--- a/inlong-dashboard/src/ui/locales/en.json
+++ b/inlong-dashboard/src/ui/locales/en.json
@@ -536,6 +536,7 @@
   "components.Layout.NavWidget.NewPassword": "New password",
   "components.Layout.NavWidget.ConfirmPassword": "Confirm password",
   "components.Layout.NavWidget.Remind": "Password does not match, please 
re-enter",
+  "components.Layout.Tenant.Success": "Success",
   "components.HighSelect.Customize": "Customize",
   "components.HighSelect.SearchPlaceholder": "Please enter keyword...",
   "components.NodeSelect.Create": "Create Node",
@@ -796,5 +797,14 @@
   "components.FieldList.Sink": "SinkField",
   "components.FieldList.CreateSource": "Create SourceField",
   "components.FieldList.CreateSink": "Create SinkField",
-  "components.FieldList.AddField": "Add Field"
+  "components.FieldList.AddField": "Add Field",
+  "pages.Tenant.config.Description": "Description",
+  "pages.Tenant.config.Name": "Tenant Name",
+  "pages.Tenant.New": "Create",
+  "pages.Tenant.config.Username": "User Name",
+  "pages.Tenant.config.TenantRole": "Tenant Role",
+  "pages.Tenant.config.Admin": "Tenant Admin",
+  "pages.Tenant.config.GeneralUser": "General User",
+  "pages.Tenant.config.Creator": "Creator",
+  "pages.Tenant.config.CreateTime": "Create Time"
 }
diff --git a/inlong-dashboard/src/ui/pages/TenantManagement/DetailModal.tsx 
b/inlong-dashboard/src/ui/pages/TenantManagement/DetailModal.tsx
new file mode 100644
index 0000000000..64e8cfef41
--- /dev/null
+++ b/inlong-dashboard/src/ui/pages/TenantManagement/DetailModal.tsx
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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, { useMemo, useState } from 'react';
+import { Modal, message, Button } from 'antd';
+import { ModalProps } from 'antd/es/modal';
+import FormGenerator, { useForm } from '@/ui/components/FormGenerator';
+import { useRequest, useSelector, useUpdateEffect } from '@/ui/hooks';
+import i18n from '@/i18n';
+import request from '@/core/utils/request';
+import { State } from '@/core/stores';
+import TenantModal from './TenantModal';
+
+export interface Props extends ModalProps {
+  id?: number;
+  record?: Record<string, any>;
+}
+
+const Comp: React.FC<Props> = ({ id, ...modalProps }) => {
+  const [form] = useForm();
+  const tenant = useSelector<State, State['tenant']>(state => state.tenant);
+  const roles = useSelector<State, State['roles']>(state => state.roles);
+  const [createModal, setCreateModal] = useState<Record<string, unknown>>({
+    open: false,
+  });
+
+  const onClick = () => {
+    setCreateModal({ open: true });
+  };
+
+  const formContent = useMemo(() => {
+    return [
+      {
+        type: 'select',
+        label: i18n.t('pages.Tenant.config.Name'),
+        name: 'tenant',
+        rules: [{ required: true }],
+        props: {
+          showSearch: true,
+          allowClear: true,
+          filterOption: false,
+          options: {
+            requestTrigger: ['onOpen', 'onSearch'],
+            requestService: keyword => ({
+              url: '/tenant/listByUser',
+              method: 'POST',
+              data: {
+                keyword,
+                pageNum: 1,
+                pageSize: 10,
+              },
+            }),
+            requestParams: {
+              formatResult: result =>
+                result?.list?.map(item => ({
+                  ...item,
+                  label: item.name,
+                  value: item.name,
+                })),
+            },
+          },
+          dropdownRender: menu => (
+            <>
+              {roles.includes('INLONG_ADMIN') || 
roles.includes('INLONG_OPERATOR') ? (
+                <Button type="link" onClick={onClick} style={{ marginLeft: 0 
}}>
+                  {i18n.t('pages.Tenant.New')}
+                </Button>
+              ) : (
+                ''
+              )}
+              {menu}
+            </>
+          ),
+        },
+      },
+      {
+        type: 'select',
+        label: i18n.t('pages.Tenant.config.Username'),
+        name: 'username',
+        rules: [{ required: true }],
+        props: values => ({
+          showSearch: true,
+          allowClear: true,
+          filterOption: false,
+          options: {
+            requestTrigger: ['onOpen', 'onSearch'],
+            requestService: keyword => ({
+              url: '/user/listAll',
+              method: 'POST',
+              data: {
+                keyword,
+                pageNum: 1,
+                pageSize: 10,
+              },
+            }),
+            requestParams: {
+              formatResult: result =>
+                result?.list?.map(item => ({
+                  ...item,
+                  label: item.name,
+                  value: item.name,
+                })),
+            },
+          },
+        }),
+      },
+      {
+        type: 'select',
+        label: i18n.t('pages.Tenant.config.TenantRole'),
+        name: 'roleCode',
+        rules: [{ required: true }],
+        props: {
+          options: [
+            {
+              label: i18n.t('pages.Tenant.config.Admin'),
+              value: 'TENANT_ADMIN',
+            },
+            {
+              label: i18n.t('pages.Tenant.config.GeneralUser'),
+              value: 'TENANT_OPERATOR',
+            },
+          ],
+        },
+      },
+    ];
+  }, []);
+
+  const { data, run: getData } = useRequest(
+    id => ({
+      url: `/role/tenant/get/${id}`,
+    }),
+    {
+      manual: true,
+      onSuccess: result => {
+        form.setFieldsValue(result);
+      },
+    },
+  );
+
+  const onOk = async () => {
+    const values = await form.validateFields();
+    const submitData = {
+      tenant,
+      ...values,
+    };
+    const isUpdate = Boolean(id);
+    if (isUpdate) {
+      submitData.id = id;
+      submitData.version = data?.version;
+    }
+    await request({
+      url: isUpdate ? '/role/tenant/update' : '/role/tenant/save',
+      method: 'POST',
+      data: { ...submitData },
+    });
+    await modalProps?.onOk(submitData);
+    message.success(i18n.t('basic.OperatingSuccess'));
+  };
+
+  useUpdateEffect(() => {
+    if (modalProps.open) {
+      if (id) {
+        getData(id);
+      }
+    } else {
+      form.resetFields();
+    }
+  }, [modalProps.open]);
+
+  return (
+    <>
+      <Modal
+        {...modalProps}
+        title={id ? i18n.t('basic.Edit') : i18n.t('pages.Tenant.New')}
+        width={600}
+        onOk={onOk}
+      >
+        <FormGenerator
+          labelCol={{ span: 5 }}
+          wrapperCol={{ span: 20 }}
+          content={formContent}
+          form={form}
+          initialValues={id ? data : ''}
+          useMaxWidth
+        />
+      </Modal>
+      <TenantModal
+        {...createModal}
+        onOk={async () => {
+          setCreateModal(prev => ({ ...prev, open: false }));
+        }}
+        onCancel={() => setCreateModal(prev => ({ ...prev, open: false }))}
+      />
+    </>
+  );
+};
+
+export default Comp;
diff --git a/inlong-dashboard/src/ui/pages/TenantManagement/TenantModal.tsx 
b/inlong-dashboard/src/ui/pages/TenantManagement/TenantModal.tsx
new file mode 100644
index 0000000000..f42e576811
--- /dev/null
+++ b/inlong-dashboard/src/ui/pages/TenantManagement/TenantModal.tsx
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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, { useMemo } from 'react';
+import { Modal, message } from 'antd';
+import { ModalProps } from 'antd/es/modal';
+import FormGenerator, { useForm } from '@/ui/components/FormGenerator';
+import { useUpdateEffect } from '@/ui/hooks';
+import i18n from '@/i18n';
+import request from '@/core/utils/request';
+
+export interface Props extends ModalProps {
+  name?: string;
+  record?: Record<string, any>;
+}
+
+const Comp: React.FC<Props> = ({ name, ...modalProps }) => {
+  const [form] = useForm();
+
+  const formContent = useMemo(() => {
+    return [
+      {
+        type: 'input',
+        label: i18n.t('pages.Tenant.config.Name'),
+        name: 'name',
+        rules: [{ required: true }],
+      },
+      {
+        type: 'input',
+        label: i18n.t('pages.Tenant.config.Description'),
+        name: 'description',
+        rules: [{ required: true }],
+      },
+    ];
+  }, []);
+
+  const onOk = async () => {
+    const values = await form.validateFields();
+    await request({
+      url: '/tenant/save',
+      method: 'POST',
+      data: { ...values },
+    });
+    await modalProps?.onOk(values);
+    message.success(i18n.t('basic.OperatingSuccess'));
+  };
+
+  useUpdateEffect(() => {
+    if (modalProps.open) {
+      form.resetFields();
+    }
+  }, [modalProps.open]);
+
+  return (
+    <Modal {...modalProps} title={i18n.t('pages.Tenant.New')} width={600} 
onOk={onOk}>
+      <FormGenerator
+        labelCol={{ span: 5 }}
+        wrapperCol={{ span: 20 }}
+        content={formContent}
+        form={form}
+        useMaxWidth
+      />
+    </Modal>
+  );
+};
+
+export default Comp;
diff --git a/inlong-dashboard/src/ui/pages/TenantManagement/config.tsx 
b/inlong-dashboard/src/ui/pages/TenantManagement/config.tsx
new file mode 100644
index 0000000000..6d65ba294c
--- /dev/null
+++ b/inlong-dashboard/src/ui/pages/TenantManagement/config.tsx
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 { Button } from 'antd';
+import i18n from '@/i18n';
+import { timestampFormat } from '@/core/utils';
+
+export const getFilterFormContent = () => [
+  {
+    type: 'inputsearch',
+    name: 'keyword',
+  },
+];
+
+export const getColumns = ({ onEdit }) => {
+  return [
+    {
+      title: i18n.t('pages.Tenant.config.Name'),
+      dataIndex: 'tenant',
+    },
+    {
+      title: i18n.t('pages.Tenant.config.Username'),
+      dataIndex: 'username',
+    },
+    {
+      title: i18n.t('pages.Tenant.config.TenantRole'),
+      dataIndex: 'roleCode',
+      render: text =>
+        text === 'TENANT_ADMIN'
+          ? i18n.t('pages.Tenant.config.Admin')
+          : i18n.t('pages.Tenant.config.GeneralUser'),
+    },
+    {
+      title: i18n.t('pages.Tenant.config.Creator'),
+      dataIndex: 'creator',
+    },
+    {
+      title: i18n.t('pages.Tenant.config.CreateTime'),
+      dataIndex: 'createTime',
+      render: text => text && timestampFormat(text),
+    },
+    {
+      title: i18n.t('basic.Operating'),
+      dataIndex: 'action',
+      render: (text, record) => (
+        <>
+          <Button type="link" onClick={() => onEdit(record)}>
+            {i18n.t('basic.Edit')}
+          </Button>
+        </>
+      ),
+    },
+  ];
+};
diff --git a/inlong-dashboard/src/ui/pages/TenantManagement/index.tsx 
b/inlong-dashboard/src/ui/pages/TenantManagement/index.tsx
new file mode 100644
index 0000000000..85de5780f1
--- /dev/null
+++ b/inlong-dashboard/src/ui/pages/TenantManagement/index.tsx
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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, { useState } from 'react';
+import { Button, Card } from 'antd';
+import { PageContainer, Container } from '@/ui/components/PageContainer';
+import HighTable from '@/ui/components/HighTable';
+import { useRequest, useSelector } from '@/ui/hooks';
+import { useTranslation } from 'react-i18next';
+import { defaultSize } from '@/configs/pagination';
+import DetailModal from './DetailModal';
+import { getFilterFormContent, getColumns } from './config';
+import { State } from '@/core/stores';
+
+const Comp: React.FC = () => {
+  const { t } = useTranslation();
+
+  const tenantList = useSelector<State, State['tenantList']>(state => 
state.tenantList);
+
+  const [options, setOptions] = useState({
+    keyword: '',
+    pageSize: defaultSize,
+    pageNum: 1,
+    tenantList: tenantList,
+  });
+
+  const [createModal, setCreateModal] = useState<Record<string, unknown>>({
+    open: false,
+  });
+
+  const {
+    data,
+    loading,
+    run: getList,
+  } = useRequest(
+    {
+      url: '/role/tenant/list',
+      method: 'POST',
+      data: options,
+    },
+    {
+      refreshDeps: [options],
+    },
+  );
+
+  const onEdit = ({ id }) => {
+    setCreateModal({
+      open: true,
+      id,
+    });
+  };
+
+  const onChange = ({ current: pageNum, pageSize }) => {
+    setOptions(prev => ({
+      ...prev,
+      pageNum,
+      pageSize,
+    }));
+  };
+
+  const onFilter = allValues => {
+    setOptions(prev => ({
+      ...prev,
+      ...allValues,
+      pageNum: 1,
+    }));
+  };
+
+  const pagination = {
+    pageSize: options.pageSize,
+    current: options.pageNum,
+    total: data?.total,
+  };
+
+  return (
+    <PageContainer useDefaultBreadcrumb={false} useDefaultContainer={false}>
+      <Container>
+        <Card>
+          <HighTable
+            suffix={
+              <Button type="primary" onClick={() => setCreateModal({ open: 
true })}>
+                {t('pages.Tenant.New')}
+              </Button>
+            }
+            filterForm={{
+              content: getFilterFormContent(),
+              onFilter,
+            }}
+            table={{
+              columns: getColumns({ onEdit }),
+              rowKey: 'id',
+              dataSource: data?.list,
+              pagination,
+              loading,
+              onChange,
+            }}
+          />
+        </Card>
+      </Container>
+
+      <DetailModal
+        {...createModal}
+        open={createModal.open as boolean}
+        onOk={async () => {
+          await getList();
+          setCreateModal({ open: false });
+        }}
+        onCancel={() => setCreateModal({ open: false })}
+      />
+    </PageContainer>
+  );
+};
+
+export default Comp;

Reply via email to