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;