feat(users): add user permissions management and enhance theme switcher
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: require.resolve('@umijs/max/eslint'),
|
extends: require.resolve('@umijs/max/eslint'),
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ export const commonManagerRoutes = [
|
|||||||
path: '/manager/users',
|
path: '/manager/users',
|
||||||
component: './Manager/User',
|
component: './Manager/User',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/manager/users/:userId/permissions',
|
||||||
|
component: './Manager/User/Permission/Assign',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { ROUTE_LOGIN, ROUTE_PROFILE } from '@/constants/routes';
|
import { ROUTE_LOGIN, ROUTE_PROFILE } from '@/constants/routes';
|
||||||
import { clearAllData, clearSessionData, removeToken } from '@/utils/storage';
|
import { clearAllData, clearSessionData, removeToken } from '@/utils/storage';
|
||||||
import { LogoutOutlined, ProfileOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
LogoutOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import { history, useIntl } from '@umijs/max';
|
import { history, useIntl } from '@umijs/max';
|
||||||
|
|
||||||
import { Dropdown } from 'antd';
|
import { Dropdown } from 'antd';
|
||||||
@@ -18,7 +22,7 @@ export const AvatarDropdown = ({
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
key: ROUTE_PROFILE,
|
key: ROUTE_PROFILE,
|
||||||
icon: <ProfileOutlined />,
|
icon: <SettingOutlined />,
|
||||||
label: intl.formatMessage({ id: 'menu.profile' }),
|
label: intl.formatMessage({ id: 'menu.profile' }),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
history.push(ROUTE_PROFILE);
|
history.push(ROUTE_PROFILE);
|
||||||
@@ -42,17 +46,8 @@ export const AvatarDropdown = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<img
|
<UserOutlined />
|
||||||
src="/avatar.svg"
|
<span className="font-sans">
|
||||||
alt="avatar"
|
|
||||||
style={{
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
borderRadius: '50%',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="font-bold">
|
|
||||||
{currentUserProfile?.metadata?.full_name || ''}
|
{currentUserProfile?.metadata?.full_name || ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { THEME_KEY } from '@/constants';
|
import { THEME_KEY } from '@/constants';
|
||||||
import { MoonOutlined, SunOutlined } from '@ant-design/icons';
|
import { MoonOutlined, SunOutlined } from '@ant-design/icons';
|
||||||
import { useIntl, useModel } from '@umijs/max';
|
import { useModel } from '@umijs/max';
|
||||||
import { Button, Dropdown } from 'antd';
|
import { Segmented } from 'antd';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export interface ThemeSwitcherProps {
|
export interface ThemeSwitcherProps {
|
||||||
@@ -10,7 +10,6 @@ export interface ThemeSwitcherProps {
|
|||||||
|
|
||||||
const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({ className }) => {
|
const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({ className }) => {
|
||||||
const { initialState, setInitialState } = useModel('@@initialState');
|
const { initialState, setInitialState } = useModel('@@initialState');
|
||||||
const intl = useIntl();
|
|
||||||
const [isDark, setIsDark] = useState(
|
const [isDark, setIsDark] = useState(
|
||||||
(initialState?.theme as 'light' | 'dark') === 'dark',
|
(initialState?.theme as 'light' | 'dark') === 'dark',
|
||||||
);
|
);
|
||||||
@@ -30,46 +29,80 @@ const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({ className }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleThemeChange = (newTheme: 'light' | 'dark') => {
|
const handleThemeChange = (newTheme: 'light' | 'dark') => {
|
||||||
|
// Check if browser supports View Transitions API
|
||||||
|
const supportsViewTransition = 'startViewTransition' in document;
|
||||||
|
|
||||||
|
if (!supportsViewTransition) {
|
||||||
|
// Fallback: just change theme without animation
|
||||||
localStorage.setItem(THEME_KEY, newTheme);
|
localStorage.setItem(THEME_KEY, newTheme);
|
||||||
// Update global state để trigger layout re-render
|
setIsDark(newTheme === 'dark');
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('theme-change', { detail: { theme: newTheme } }),
|
||||||
|
);
|
||||||
setInitialState({
|
setInitialState({
|
||||||
...initialState,
|
...initialState,
|
||||||
theme: newTheme,
|
theme: newTheme,
|
||||||
} as any).then(() => {
|
} as any);
|
||||||
// Dispatch event để notify ThemeProvider
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set origin to top-right corner
|
||||||
|
const x = window.innerWidth;
|
||||||
|
const y = 0;
|
||||||
|
|
||||||
|
// Calculate end radius to cover the entire screen
|
||||||
|
const endRadius = Math.hypot(
|
||||||
|
Math.max(x, window.innerWidth - x),
|
||||||
|
Math.max(y, window.innerHeight - y),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start the view transition
|
||||||
|
const transition = (document as any).startViewTransition(() => {
|
||||||
|
localStorage.setItem(THEME_KEY, newTheme);
|
||||||
|
setIsDark(newTheme === 'dark');
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('theme-change', { detail: { theme: newTheme } }),
|
new CustomEvent('theme-change', { detail: { theme: newTheme } }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Animate the ripple effect
|
||||||
|
transition.ready.then(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
document.documentElement.animate(
|
||||||
|
{
|
||||||
|
clipPath: [
|
||||||
|
`circle(0px at ${x}px ${y}px)`,
|
||||||
|
`circle(${endRadius}px at ${x}px ${y}px)`,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duration: 800,
|
||||||
|
easing: 'ease-in-out',
|
||||||
|
pseudoElement: '::view-transition-new(root)',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update initialState after transition
|
||||||
|
setInitialState({
|
||||||
|
...initialState,
|
||||||
|
theme: newTheme,
|
||||||
|
} as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
const items = [
|
|
||||||
{
|
|
||||||
key: 'light',
|
|
||||||
label: intl.formatMessage({
|
|
||||||
id: 'common.theme.light',
|
|
||||||
defaultMessage: 'Light Theme',
|
|
||||||
}),
|
|
||||||
icon: <SunOutlined />,
|
|
||||||
onClick: () => handleThemeChange('light'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'dark',
|
|
||||||
label: intl.formatMessage({
|
|
||||||
id: 'common.theme.dark',
|
|
||||||
defaultMessage: 'Dark Theme',
|
|
||||||
}),
|
|
||||||
icon: <MoonOutlined />,
|
|
||||||
onClick: () => handleThemeChange('dark'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={{ items }} placement="bottomRight" trigger={['click']}>
|
<Segmented
|
||||||
<Button type="text" className={className}>
|
className={className}
|
||||||
{isDark ? <MoonOutlined /> : <SunOutlined />}
|
size="small"
|
||||||
</Button>
|
shape="round"
|
||||||
</Dropdown>
|
value={isDark ? 'dark' : 'light'}
|
||||||
|
onChange={(value) => handleThemeChange(value as 'light' | 'dark')}
|
||||||
|
options={[
|
||||||
|
{ value: 'light', icon: <SunOutlined /> },
|
||||||
|
{ value: 'dark', icon: <MoonOutlined /> },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/* View Transitions API - Disable default animation for custom ripple effect */
|
||||||
|
/* stylelint-disable selector-pseudo-element-no-unknown */
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: none;
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
}
|
||||||
|
/* stylelint-enable selector-pseudo-element-no-unknown */
|
||||||
|
|
||||||
/* From Uiverse.io by Galahhad */
|
/* From Uiverse.io by Galahhad */
|
||||||
.theme-switch {
|
.theme-switch {
|
||||||
--toggle-size: 20px;
|
--toggle-size: 20px;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const API_ALARMS_CONFIRM = '/api/alarms/confirm';
|
|||||||
|
|
||||||
// Thing API Constants
|
// Thing API Constants
|
||||||
export const API_THINGS_SEARCH = '/api/things/search';
|
export const API_THINGS_SEARCH = '/api/things/search';
|
||||||
|
export const API_THINGS_POLICY = '/api/things/policy';
|
||||||
|
|
||||||
// Group API Constants
|
// Group API Constants
|
||||||
export const API_GROUPS = '/api/groups';
|
export const API_GROUPS = '/api/groups';
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export const ROUTE_LOGIN = '/login';
|
export const ROUTE_LOGIN = '/login';
|
||||||
export const ROUTER_HOME = '/';
|
export const ROUTER_HOME = '/';
|
||||||
export const ROUTE_PROFILE = '/profile';
|
export const ROUTE_PROFILE = '/profile';
|
||||||
|
export const ROUTE_MANAGER_USERS = '/manager/users';
|
||||||
|
export const ROUTE_MANAGER_USERS_PERMISSIONS = 'permissions';
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import masterMenuProfileEn from './master-profile-en';
|
|||||||
import masterThingEn from './master-thing-en';
|
import masterThingEn from './master-thing-en';
|
||||||
import masterUserEn from './master-user-en';
|
import masterUserEn from './master-user-en';
|
||||||
export default {
|
export default {
|
||||||
|
'master.footer.chosen': 'Chosen',
|
||||||
...masterAuthEn,
|
...masterAuthEn,
|
||||||
...masterMenuEn,
|
...masterMenuEn,
|
||||||
...masterMenuProfileEn,
|
...masterMenuProfileEn,
|
||||||
|
|||||||
@@ -32,4 +32,23 @@ export default {
|
|||||||
'master.users.role.sgw.end_user': 'Ship Owner',
|
'master.users.role.sgw.end_user': 'Ship Owner',
|
||||||
'master.users.create.error': 'User creation failed',
|
'master.users.create.error': 'User creation failed',
|
||||||
'master.users.create.success': 'User created successfully',
|
'master.users.create.success': 'User created successfully',
|
||||||
|
'master.users.change_role.confirm.title': 'Confirm role change',
|
||||||
|
'master.users.change_role.admin.content':
|
||||||
|
'Are you sure you want to change the role to Unit Manager?',
|
||||||
|
'master.users.change_role.user.content':
|
||||||
|
'Are you sure you want to change the role to Unit Supervisor?',
|
||||||
|
'master.users.change_role.user.success': 'Role change successful',
|
||||||
|
'master.users.change_role.user.fail': 'Role change failed',
|
||||||
|
'master.users.group_assign.title': 'Assigned Groups',
|
||||||
|
'master.users.group_assign.button.title': 'Assign Groups',
|
||||||
|
'master.users.group_assign.select.title': 'Select Group',
|
||||||
|
'master.users.unassign.content':
|
||||||
|
'Are you sure you want to unassign this group?',
|
||||||
|
'master.users.unassign.success': 'Unassign successful',
|
||||||
|
'master.users.unassign.fail': 'Unassign failed',
|
||||||
|
'master.users.assign.success': 'Assign group successful',
|
||||||
|
'master.users.assign.fail': 'Assign group failed',
|
||||||
|
'master.users.deletion.title': 'Are you sure to delete this selected items?',
|
||||||
|
'master.users.delete.success': 'User deleted successfully',
|
||||||
|
'master.users.delete.fail': 'User deletion failed',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default {
|
|||||||
'Không thể tạo đơn vị con khi gốc đã có thiết bị',
|
'Không thể tạo đơn vị con khi gốc đã có thiết bị',
|
||||||
'master.groups.add': 'Tạo đơn vị cấp dưới',
|
'master.groups.add': 'Tạo đơn vị cấp dưới',
|
||||||
'master.groups.delete.confirm': 'Bạn có chắc muốn xóa nhóm này không?',
|
'master.groups.delete.confirm': 'Bạn có chắc muốn xóa nhóm này không?',
|
||||||
'master.groups.code': 'Mã',
|
'master.groups.code': 'Mã đỡ vị',
|
||||||
'master.groups.code.exists': 'Mã đã tồn tại',
|
'master.groups.code.exists': 'Mã đã tồn tại',
|
||||||
'master.groups.short_name': 'Tên viết tắt',
|
'master.groups.short_name': 'Tên viết tắt',
|
||||||
'master.groups.short_name.exists': 'Tên viết tắt đã tồn tại',
|
'master.groups.short_name.exists': 'Tên viết tắt đã tồn tại',
|
||||||
|
|||||||
@@ -32,4 +32,22 @@ export default {
|
|||||||
'master.users.role.sgw.end_user': 'Chủ tàu',
|
'master.users.role.sgw.end_user': 'Chủ tàu',
|
||||||
'master.users.create.error': 'Tạo người dùng lỗi',
|
'master.users.create.error': 'Tạo người dùng lỗi',
|
||||||
'master.users.create.success': 'Tạo người dùng thành công',
|
'master.users.create.success': 'Tạo người dùng thành công',
|
||||||
|
'master.users.change_role.confirm.title': 'Xác nhận thay đổi vai trò',
|
||||||
|
'master.users.change_role.admin.content':
|
||||||
|
'Bạn có chắc muốn thay đổi vai trò thành quản lý đơn vị không?',
|
||||||
|
'master.users.change_role.user.content':
|
||||||
|
'Bạn có chắc muốn thay đổi vai trò thành giám sát đơn vị không?',
|
||||||
|
'master.users.change_role.user.success': 'Thay đổi vai trò thành công',
|
||||||
|
'master.users.change_role.user.fail': 'Thay đổi vai trò thất bại',
|
||||||
|
'master.users.group_assign.title': 'Đơn vị được phân quyền',
|
||||||
|
'master.users.group_assign.button.title': 'Phân quyền đơn vị',
|
||||||
|
'master.users.group_assign.select.title': 'Chọn đơn vị',
|
||||||
|
'master.users.unassign.content': 'Bạn chắc chắn ngừng phân quyền khỏi đơn vị',
|
||||||
|
'master.users.unassign.success': 'Ngừng phân quyền thành công',
|
||||||
|
'master.users.unassign.fail': 'Ngừng phân quyền thất bại',
|
||||||
|
'master.users.assign.success': 'Phân quyền đơn vị thành công',
|
||||||
|
'master.users.assign.fail': 'Phân quyền đơn vị thất bại',
|
||||||
|
'master.users.deletion.title': 'Chắc chắn xoá các tài khoản đã chọn?',
|
||||||
|
'master.users.delete.success': 'Xoá người dùng thành công',
|
||||||
|
'master.users.delete.fail': 'Xoá người dùng thất bại',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import masterProfileVi from './master-profile-vi';
|
|||||||
import masterThingVi from './master-thing-vi';
|
import masterThingVi from './master-thing-vi';
|
||||||
import masterUserVi from './master-user-vi';
|
import masterUserVi from './master-user-vi';
|
||||||
export default {
|
export default {
|
||||||
|
'master.footer.chosen': 'Đã chọn',
|
||||||
...masterAuthVi,
|
...masterAuthVi,
|
||||||
...masterMenuVi,
|
...masterMenuVi,
|
||||||
...masterProfileVi,
|
...masterProfileVi,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const AlarmFormConfirm = ({
|
|||||||
formRef={formRef}
|
formRef={formRef}
|
||||||
title={
|
title={
|
||||||
<Flex align="center" justify="center">
|
<Flex align="center" justify="center">
|
||||||
<FormattedMessage id="alarms.confirm.title" />
|
<FormattedMessage id="master.alarms.confirm.title" />
|
||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
width="40%"
|
width="40%"
|
||||||
@@ -66,7 +66,7 @@ const AlarmFormConfirm = ({
|
|||||||
if (resp.status === HTTPSTATUS.HTTP_ACCEPTED) {
|
if (resp.status === HTTPSTATUS.HTTP_ACCEPTED) {
|
||||||
message.success({
|
message.success({
|
||||||
content: intl.formatMessage({
|
content: intl.formatMessage({
|
||||||
id: 'alarms.confirm.success',
|
id: 'master.alarms.confirm.success',
|
||||||
defaultMessage: 'Confirm alarm successfully',
|
defaultMessage: 'Confirm alarm successfully',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -74,7 +74,7 @@ const AlarmFormConfirm = ({
|
|||||||
} else if (resp.status === HTTPSTATUS.HTTP_NOTFOUND) {
|
} else if (resp.status === HTTPSTATUS.HTTP_NOTFOUND) {
|
||||||
message.warning({
|
message.warning({
|
||||||
content: intl.formatMessage({
|
content: intl.formatMessage({
|
||||||
id: 'alarms.not_found',
|
id: 'master.alarms.not_found',
|
||||||
defaultMessage: 'Alarm has expired or does not exist',
|
defaultMessage: 'Alarm has expired or does not exist',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -86,7 +86,7 @@ const AlarmFormConfirm = ({
|
|||||||
console.error('Error when confirm alarm: ', error);
|
console.error('Error when confirm alarm: ', error);
|
||||||
message.error({
|
message.error({
|
||||||
content: intl.formatMessage({
|
content: intl.formatMessage({
|
||||||
id: 'alarms.confirm.fail',
|
id: 'master.alarms.confirm.fail',
|
||||||
defaultMessage: 'Confirm alarm failed',
|
defaultMessage: 'Confirm alarm failed',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -120,7 +120,7 @@ const AlarmFormConfirm = ({
|
|||||||
<ProFormText
|
<ProFormText
|
||||||
name="time"
|
name="time"
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'alarms.occurred_at',
|
id: 'master.alarms.occurred_at',
|
||||||
defaultMessage: 'When',
|
defaultMessage: 'When',
|
||||||
})}
|
})}
|
||||||
readonly={true}
|
readonly={true}
|
||||||
@@ -139,7 +139,7 @@ const AlarmFormConfirm = ({
|
|||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: intl.formatMessage({
|
message: intl.formatMessage({
|
||||||
id: 'alarms.confirm.description.required',
|
id: 'master.alarms.confirm.description.required',
|
||||||
defaultMessage: 'The description is required',
|
defaultMessage: 'The description is required',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { ROUTE_MANAGER_USERS } from '@/constants/routes';
|
||||||
|
import { apiQueryUserById } from '@/services/master/UserController';
|
||||||
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
|
import { history, useParams } from '@umijs/max';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import AssignGroup from './components/AssignGroup';
|
||||||
|
import ShareThing from './components/ShareThing';
|
||||||
|
|
||||||
|
enum AssignTabsKey {
|
||||||
|
group = 'group',
|
||||||
|
device = 'device',
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssignUserPage = () => {
|
||||||
|
const { userId } = useParams<{ userId: string }>();
|
||||||
|
const [userProfile, setUserProfile] =
|
||||||
|
useState<MasterModel.ProfileResponse | null>(null);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [tabSelected, setTabSelected] = useState<AssignTabsKey>(
|
||||||
|
AssignTabsKey.group,
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUserProfile = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const profile = await apiQueryUserById(userId || '');
|
||||||
|
setUserProfile(profile);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch user profile:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchUserProfile();
|
||||||
|
}, [userId]);
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
title={userProfile?.email}
|
||||||
|
header={{
|
||||||
|
onBack: () => history.push(ROUTE_MANAGER_USERS),
|
||||||
|
}}
|
||||||
|
loading={loading}
|
||||||
|
tabList={[
|
||||||
|
{
|
||||||
|
tab: 'Phân quyền đơn vị',
|
||||||
|
key: AssignTabsKey.group,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: 'Chia sẻ thiết bị',
|
||||||
|
key: AssignTabsKey.device,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onTabChange={(key) => {
|
||||||
|
setTabSelected(key as AssignTabsKey);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabSelected === AssignTabsKey.group && (
|
||||||
|
<AssignGroup user={userProfile} />
|
||||||
|
)}
|
||||||
|
{tabSelected === AssignTabsKey.device && (
|
||||||
|
<ShareThing user={userProfile} />
|
||||||
|
)}
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AssignUserPage;
|
||||||
|
|||||||
318
src/pages/Manager/User/Permission/components/AssignGroup.tsx
Normal file
318
src/pages/Manager/User/Permission/components/AssignGroup.tsx
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
import TreeSelectedGroup from '@/components/shared/TreeSelectedGroup';
|
||||||
|
import { HTTPSTATUS } from '@/constants';
|
||||||
|
import {
|
||||||
|
apiAssignToGroup,
|
||||||
|
apiQueryMembers,
|
||||||
|
apiUnassignToGroup,
|
||||||
|
} from '@/services/master/GroupController';
|
||||||
|
import { apiChangeRoleUser } from '@/services/master/UserController';
|
||||||
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
|
import { ActionType, ProList, ProListMetas } from '@ant-design/pro-components';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Popconfirm,
|
||||||
|
Segmented,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
const { Text } = Typography;
|
||||||
|
type AssignGroupProps = {
|
||||||
|
user: MasterModel.ProfileResponse | null;
|
||||||
|
};
|
||||||
|
const AssignGroup = ({ user }: AssignGroupProps) => {
|
||||||
|
const groupActionRef = useRef<ActionType>();
|
||||||
|
const intl = useIntl();
|
||||||
|
const [assignedGroups, setAssignedGroups] = useState<MasterModel.GroupNode[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
const [userRole, setUserRole] = useState<string>(
|
||||||
|
user?.metadata?.user_type || 'users',
|
||||||
|
);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||||
|
const [groupSelected, setGroupSelected] = useState<string | string[] | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const columns: ProListMetas<MasterModel.GroupNode> = {
|
||||||
|
title: {
|
||||||
|
dataIndex: 'name',
|
||||||
|
|
||||||
|
render: (_, item: MasterModel.GroupNode) => (
|
||||||
|
<Typography.Paragraph
|
||||||
|
style={{
|
||||||
|
marginBottom: 0,
|
||||||
|
marginLeft: 8,
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
display: 'inline-block',
|
||||||
|
}}
|
||||||
|
copyable
|
||||||
|
>
|
||||||
|
{item?.name}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
subTitle: {
|
||||||
|
render: (_, entity) => {
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
<Tooltip
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'master.groups.code',
|
||||||
|
defaultMessage: 'Mã đơn vị',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Tag color="blue">{entity?.metadata?.code || '-'}</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'master.groups.short_name',
|
||||||
|
defaultMessage: 'Tên viết tắt',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Tag color="green">{entity?.metadata?.short_name || '-'}</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
render: (_, item: MasterModel.GroupNode) => {
|
||||||
|
return (
|
||||||
|
<Popconfirm
|
||||||
|
title={`${intl.formatMessage({
|
||||||
|
id: 'master.users.unassign.content',
|
||||||
|
defaultMessage: 'Are you sure unassign this group',
|
||||||
|
})} ${item?.name}?`}
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
const body: MasterModel.AssignMemberRequest = {
|
||||||
|
group_id: item.id,
|
||||||
|
type: 'users',
|
||||||
|
members: [user?.id || ''],
|
||||||
|
};
|
||||||
|
const success = await apiUnassignToGroup(body);
|
||||||
|
if (success.status === HTTPSTATUS.HTTP_NOCONTENT) {
|
||||||
|
messageApi.success(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.users.unassign.success',
|
||||||
|
defaultMessage: 'Unassign group successfully',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
groupActionRef.current?.reload();
|
||||||
|
} else {
|
||||||
|
throw new Error('Unassign group failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error when unassign group: ', error);
|
||||||
|
messageApi.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.users.unassign.fail',
|
||||||
|
defaultMessage: 'Unassign group failed',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" danger icon={<DeleteOutlined />} />
|
||||||
|
</Popconfirm>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const showModal = () => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssignUserToGroup = async () => {
|
||||||
|
setConfirmLoading(true);
|
||||||
|
try {
|
||||||
|
const body: MasterModel.AssignMemberRequest = {
|
||||||
|
group_id: groupSelected as string,
|
||||||
|
type: userRole === 'admin' ? 'admin' : 'users',
|
||||||
|
members: [user?.id || ''],
|
||||||
|
};
|
||||||
|
const resp = await apiAssignToGroup(body);
|
||||||
|
if (resp.status === HTTPSTATUS.HTTP_SUCCESS) {
|
||||||
|
messageApi.success(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.users.assign.success',
|
||||||
|
defaultMessage: 'Assign group successfully',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
groupActionRef.current?.reload();
|
||||||
|
setOpen(false);
|
||||||
|
} else {
|
||||||
|
throw new Error('Assign group failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error when assign group: ', error);
|
||||||
|
messageApi.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.users.assign.fail',
|
||||||
|
defaultMessage: 'Assign group failed',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setConfirmLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{contextHolder}
|
||||||
|
<Modal
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'master.users.group_assign.button.title',
|
||||||
|
defaultMessage: 'Assign Groups',
|
||||||
|
})}
|
||||||
|
open={open}
|
||||||
|
onOk={handleAssignUserToGroup}
|
||||||
|
confirmLoading={confirmLoading}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
>
|
||||||
|
<Flex vertical gap={5}>
|
||||||
|
<Text>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'master.users.group_assign.select.title',
|
||||||
|
defaultMessage: 'Select Group',
|
||||||
|
})}
|
||||||
|
:
|
||||||
|
</Text>
|
||||||
|
<TreeSelectedGroup
|
||||||
|
disabled
|
||||||
|
groupIds={assignedGroups.map((group) => group.id)}
|
||||||
|
onSelected={(value) => {
|
||||||
|
setGroupSelected(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Modal>
|
||||||
|
<ProList<MasterModel.GroupNode>
|
||||||
|
headerTitle={intl.formatMessage({
|
||||||
|
id: 'master.users.group_assign.title',
|
||||||
|
defaultMessage: 'Assigned list',
|
||||||
|
})}
|
||||||
|
actionRef={groupActionRef}
|
||||||
|
metas={columns}
|
||||||
|
toolBarRender={() => [
|
||||||
|
user?.metadata?.user_type !== 'enduser' && (
|
||||||
|
<Segmented
|
||||||
|
key="role"
|
||||||
|
value={userRole}
|
||||||
|
onChange={(value) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.users.change_role.confirm.title',
|
||||||
|
defaultMessage: 'Xác nhận thay đổi vai trò',
|
||||||
|
}),
|
||||||
|
content:
|
||||||
|
userRole === 'admin'
|
||||||
|
? intl.formatMessage({
|
||||||
|
id: 'master.users.change_role.user.content',
|
||||||
|
defaultMessage:
|
||||||
|
'Bạn có chắc muốn thay đổi vai trò thành giám sát đơn vị không?',
|
||||||
|
})
|
||||||
|
: intl.formatMessage({
|
||||||
|
id: 'master.users.change_role.admin.content',
|
||||||
|
defaultMessage:
|
||||||
|
'Bạn có chắc muốn thay đổi vai trò thành quản lý đơn vị không?',
|
||||||
|
}),
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const resp = await apiChangeRoleUser(
|
||||||
|
user?.id || '',
|
||||||
|
value as 'users' | 'admin',
|
||||||
|
);
|
||||||
|
if (resp.status === HTTPSTATUS.HTTP_SUCCESS) {
|
||||||
|
messageApi.success(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.users.change_role.user.success',
|
||||||
|
defaultMessage: 'Role change successful',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
groupActionRef.current?.reload();
|
||||||
|
setUserRole(value as string);
|
||||||
|
} else {
|
||||||
|
throw new Error('Change role failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error when change role user: ', error);
|
||||||
|
messageApi.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.users.change_role.user.fail',
|
||||||
|
defaultMessage: 'Role change failed',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'master.users.role.user',
|
||||||
|
defaultMessage: 'Giám sát',
|
||||||
|
}),
|
||||||
|
value: 'users',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'master.users.role.admin',
|
||||||
|
defaultMessage: 'Quản lý',
|
||||||
|
}),
|
||||||
|
value: 'admin',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
<Button
|
||||||
|
key="assign"
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
showModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'master.users.group_assign.button.title',
|
||||||
|
defaultMessage: 'Assign',
|
||||||
|
})}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
request={async () => {
|
||||||
|
if (user?.id) {
|
||||||
|
const resp = await apiQueryMembers(user.id);
|
||||||
|
if (resp?.groups) {
|
||||||
|
setAssignedGroups(resp.groups);
|
||||||
|
return Promise.resolve({
|
||||||
|
success: true,
|
||||||
|
data: resp?.groups,
|
||||||
|
total: resp?.groups?.length || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
success: false,
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
rowKey="id"
|
||||||
|
search={false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AssignGroup;
|
||||||
100
src/pages/Manager/User/Permission/components/ShareThing.tsx
Normal file
100
src/pages/Manager/User/Permission/components/ShareThing.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { ActionType, ProList } from '@ant-design/pro-components';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
type ShareThingProps = {
|
||||||
|
user: MasterModel.ProfileResponse | null;
|
||||||
|
};
|
||||||
|
const ShareThing = ({ user }: ShareThingProps) => {
|
||||||
|
const listActionRef = useRef<ActionType>();
|
||||||
|
const intl = useIntl();
|
||||||
|
const [messageAPI, contextHolder] = message.useMessage();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{contextHolder}
|
||||||
|
<ProList
|
||||||
|
headerTitle={intl.formatMessage({
|
||||||
|
id: 'pages.users.things.list',
|
||||||
|
defaultMessage: 'List things',
|
||||||
|
})}
|
||||||
|
actionRef={listActionRef}
|
||||||
|
toolBarRender={() => [
|
||||||
|
// <Button
|
||||||
|
// type="primary"
|
||||||
|
// key="primary"
|
||||||
|
// onClick={() => {
|
||||||
|
// handleShareModalVisible(true);
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <PlusOutlined />{" "}
|
||||||
|
// <FormattedMessage
|
||||||
|
// id="pages.things.share.text"
|
||||||
|
// defaultMessage="Share"
|
||||||
|
// />
|
||||||
|
// </Button>,
|
||||||
|
]}
|
||||||
|
metas={columns}
|
||||||
|
request={async () => {
|
||||||
|
const query = {
|
||||||
|
type: 'sub',
|
||||||
|
id: user?.id || '',
|
||||||
|
};
|
||||||
|
if (user?.id) {
|
||||||
|
const resp = (await apiQueryThingsByPolicy(
|
||||||
|
query,
|
||||||
|
)) as PolicyResponse;
|
||||||
|
const { relations } = resp;
|
||||||
|
if (relations) {
|
||||||
|
const queries = relations.map(async (rel: PolicyRelation) => {
|
||||||
|
const thg = await apiQueryThing(rel.id);
|
||||||
|
return {
|
||||||
|
...thg,
|
||||||
|
relations: rel?.actions,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const policies = await Promise.all(queries);
|
||||||
|
return Promise.resolve({
|
||||||
|
success: true,
|
||||||
|
data: policies,
|
||||||
|
total: policies.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
success: false,
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
rowKey="id"
|
||||||
|
search={false}
|
||||||
|
// rowSelection={{
|
||||||
|
// selectedRowKeys: selectedRowsState.map((row) => row.id).filter((id): id is string => id !== undefined),
|
||||||
|
// onChange: (_: React.Key[], selectedRows: API.Thing[]) => {
|
||||||
|
// setSelectedRows(selectedRows);
|
||||||
|
// },
|
||||||
|
// }}
|
||||||
|
/>
|
||||||
|
{/* <FormShareVms
|
||||||
|
visible={shareModalVisibale}
|
||||||
|
onVisibleChange={handleShareModalVisible}
|
||||||
|
user={user}
|
||||||
|
onSubmit={async (values: ShareFormValues) => {
|
||||||
|
console.log(values);
|
||||||
|
const success = await handleShare(values);
|
||||||
|
if (success) {
|
||||||
|
handleShareModalVisible(false);
|
||||||
|
onReload();
|
||||||
|
if (actionRef.current) {
|
||||||
|
//await delay(1000);
|
||||||
|
actionRef.current.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShareThing;
|
||||||
@@ -232,6 +232,7 @@ const CreateUser = ({ message, onSuccess }: CreateUserProps) => {
|
|||||||
{
|
{
|
||||||
validator: async (rule, value) => {
|
validator: async (rule, value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
|
try {
|
||||||
const query: MasterModel.SearchUserPaginationBody = {
|
const query: MasterModel.SearchUserPaginationBody = {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
@@ -251,6 +252,9 @@ const CreateUser = ({ message, onSuccess }: CreateUserProps) => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,17 +2,24 @@ import IconFont from '@/components/IconFont';
|
|||||||
import TreeGroup from '@/components/shared/TreeGroup';
|
import TreeGroup from '@/components/shared/TreeGroup';
|
||||||
import { DEFAULT_PAGE_SIZE } from '@/constants';
|
import { DEFAULT_PAGE_SIZE } from '@/constants';
|
||||||
import {
|
import {
|
||||||
|
ROUTE_MANAGER_USERS,
|
||||||
|
ROUTE_MANAGER_USERS_PERMISSIONS,
|
||||||
|
} from '@/constants/routes';
|
||||||
|
import {
|
||||||
|
apiDeleteUser,
|
||||||
apiQueryUsers,
|
apiQueryUsers,
|
||||||
apiQueryUsersByGroup,
|
apiQueryUsersByGroup,
|
||||||
} from '@/services/master/UserController';
|
} from '@/services/master/UserController';
|
||||||
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
|
FooterToolbar,
|
||||||
ProCard,
|
ProCard,
|
||||||
ProColumns,
|
ProColumns,
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { FormattedMessage, useIntl } from '@umijs/max';
|
import { FormattedMessage, history, useIntl } from '@umijs/max';
|
||||||
import { Button, Grid } from 'antd';
|
import { Button, Grid, Popconfirm, theme } from 'antd';
|
||||||
import message from 'antd/es/message';
|
import message from 'antd/es/message';
|
||||||
import Paragraph from 'antd/lib/typography/Paragraph';
|
import Paragraph from 'antd/lib/typography/Paragraph';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
@@ -22,18 +29,21 @@ const ManagerUserPage = () => {
|
|||||||
const { useBreakpoint } = Grid;
|
const { useBreakpoint } = Grid;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const screens = useBreakpoint();
|
const screens = useBreakpoint();
|
||||||
|
const { token } = theme.useToken();
|
||||||
const actionRef = useRef<ActionType | null>(null);
|
const actionRef = useRef<ActionType | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [messageApi, contextHolder] = message.useMessage();
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
const [selectedRowsState, setSelectedRowsState] = useState<
|
const [selectedRowsState, setSelectedRowsState] = useState<
|
||||||
MasterModel.ProfileResponse[]
|
MasterModel.ProfileResponse[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const [groupCheckedKeys, setGroupCheckedKeys] = useState<
|
const [groupCheckedKeys, setGroupCheckedKeys] = useState<
|
||||||
string | string[] | null
|
string | string[] | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
const handleClickAssign = (user: MasterModel.ProfileResponse) => {
|
const handleClickAssign = (user: MasterModel.ProfileResponse) => {
|
||||||
console.log('User ', user);
|
const path = `${ROUTE_MANAGER_USERS}/${user.id}/${ROUTE_MANAGER_USERS_PERMISSIONS}`;
|
||||||
|
history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ProColumns<MasterModel.ProfileResponse>[] = [
|
const columns: ProColumns<MasterModel.ProfileResponse>[] = [
|
||||||
@@ -53,6 +63,7 @@ const ManagerUserPage = () => {
|
|||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
|
color: token.colorText,
|
||||||
}}
|
}}
|
||||||
copyable
|
copyable
|
||||||
>
|
>
|
||||||
@@ -82,6 +93,7 @@ const ManagerUserPage = () => {
|
|||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
|
color: token.colorText,
|
||||||
}}
|
}}
|
||||||
copyable
|
copyable
|
||||||
>
|
>
|
||||||
@@ -124,6 +136,48 @@ const ManagerUserPage = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleRemove = async (selectedRows: MasterModel.ProfileResponse[]) => {
|
||||||
|
const key = 'remove_user';
|
||||||
|
if (!selectedRows) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
messageApi.open({
|
||||||
|
type: 'loading',
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: 'common.deleting',
|
||||||
|
defaultMessage: 'deleting...',
|
||||||
|
}),
|
||||||
|
duration: 0,
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
const allDelete = selectedRows.map(
|
||||||
|
async (row: MasterModel.ProfileResponse) => {
|
||||||
|
await apiDeleteUser(row?.id || '');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await Promise.all(allDelete);
|
||||||
|
messageApi.open({
|
||||||
|
type: 'success',
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: 'master.users.delete.success',
|
||||||
|
defaultMessage: 'Deleted successfully and will refresh soon',
|
||||||
|
}),
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
messageApi.open({
|
||||||
|
type: 'error',
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: 'master.users.delete.fail',
|
||||||
|
defaultMessage: 'Delete failed, please try again!',
|
||||||
|
}),
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
@@ -182,6 +236,7 @@ const ManagerUserPage = () => {
|
|||||||
const offset = current === 1 ? 0 : (current - 1) * size;
|
const offset = current === 1 ? 0 : (current - 1) * size;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
// If groups are checked, use queryUsersByGroup
|
// If groups are checked, use queryUsersByGroup
|
||||||
|
try {
|
||||||
if (groupCheckedKeys && groupCheckedKeys.length > 0) {
|
if (groupCheckedKeys && groupCheckedKeys.length > 0) {
|
||||||
// Ensure groupCheckedKeys is an array
|
// Ensure groupCheckedKeys is an array
|
||||||
const groupIdsArray = Array.isArray(groupCheckedKeys)
|
const groupIdsArray = Array.isArray(groupCheckedKeys)
|
||||||
@@ -224,7 +279,8 @@ const ManagerUserPage = () => {
|
|||||||
dir: 'asc',
|
dir: 'asc',
|
||||||
};
|
};
|
||||||
if (email) query.email = email;
|
if (email) query.email = email;
|
||||||
if (Object.keys(metadata).length > 0) query.metadata = metadata;
|
if (Object.keys(metadata).length > 0)
|
||||||
|
query.metadata = metadata;
|
||||||
|
|
||||||
const response = await apiQueryUsers(query);
|
const response = await apiQueryUsers(query);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -234,6 +290,14 @@ const ManagerUserPage = () => {
|
|||||||
total: response.total,
|
total: response.total,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
success: false,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
options={{
|
options={{
|
||||||
search: false,
|
search: false,
|
||||||
@@ -255,6 +319,53 @@ const ManagerUserPage = () => {
|
|||||||
/>
|
/>
|
||||||
</ProCard>
|
</ProCard>
|
||||||
</ProCard>
|
</ProCard>
|
||||||
|
{selectedRowsState?.length > 0 && (
|
||||||
|
<FooterToolbar
|
||||||
|
extra={
|
||||||
|
<div>
|
||||||
|
<FormattedMessage
|
||||||
|
id="master.footer.chosen"
|
||||||
|
defaultMessage="Chosen"
|
||||||
|
/>{' '}
|
||||||
|
<a
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedRowsState.length}
|
||||||
|
</a>{' '}
|
||||||
|
<FormattedMessage
|
||||||
|
id="master.users.table.pagination"
|
||||||
|
defaultMessage="users"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Popconfirm
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'master.users.deletion.title',
|
||||||
|
defaultMessage: 'Are you sure to delete this selected items',
|
||||||
|
})}
|
||||||
|
okText={intl.formatMessage({
|
||||||
|
id: 'common.sure',
|
||||||
|
defaultMessage: 'Sure',
|
||||||
|
})}
|
||||||
|
onConfirm={async () => {
|
||||||
|
const success = await handleRemove(selectedRowsState);
|
||||||
|
if (success) {
|
||||||
|
setSelectedRowsState([]);
|
||||||
|
if (actionRef.current) {
|
||||||
|
actionRef.current.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button icon={<DeleteOutlined />} type="primary" danger>
|
||||||
|
<FormattedMessage id="common.delete" defaultMessage="Delete" />
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</FooterToolbar>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,3 +55,20 @@ export async function apiDeleteGroup(group_id: string) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function apiUnassignToGroup(
|
||||||
|
body: MasterModel.AssignMemberRequest,
|
||||||
|
) {
|
||||||
|
return request(`${API_GROUPS}/${body.group_id}/members`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
data: body,
|
||||||
|
getResponse: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function apiAssignToGroup(body: MasterModel.AssignMemberRequest) {
|
||||||
|
return request(`${API_GROUPS}/${body.group_id}/members`, {
|
||||||
|
method: 'POST',
|
||||||
|
data: body,
|
||||||
|
getResponse: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { API_USERS, API_USERS_BY_GROUP } from '@/constants/api';
|
import { API_GROUPS, API_USERS, API_USERS_BY_GROUP } from '@/constants/api';
|
||||||
import { request } from '@umijs/max';
|
import { request } from '@umijs/max';
|
||||||
|
|
||||||
export async function apiQueryUsers(
|
export async function apiQueryUsers(
|
||||||
@@ -9,6 +9,10 @@ export async function apiQueryUsers(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function apiQueryUserById(userId: string) {
|
||||||
|
return request<MasterModel.ProfileResponse>(`${API_USERS}/${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function apiQueryUsersByGroup(
|
export async function apiQueryUsersByGroup(
|
||||||
group_id: string,
|
group_id: string,
|
||||||
): Promise<MasterModel.UserResponse> {
|
): Promise<MasterModel.UserResponse> {
|
||||||
@@ -22,3 +26,19 @@ export async function apiCreateUsers(body: MasterModel.CreateUserBodyRequest) {
|
|||||||
getResponse: true,
|
getResponse: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
export async function apiChangeRoleUser(
|
||||||
|
userId: string,
|
||||||
|
role: 'users' | 'admin',
|
||||||
|
) {
|
||||||
|
return request(`${API_GROUPS}/${userId}/${role}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
getResponse: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apiDeleteUser(userId: string) {
|
||||||
|
return request(`${API_USERS}/${userId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
getResponse: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
35
src/services/master/typings.d.ts
vendored
35
src/services/master/typings.d.ts
vendored
@@ -36,6 +36,7 @@ declare namespace MasterModel {
|
|||||||
subtopic?: string;
|
subtopic?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auth
|
||||||
interface LoginRequestBody {
|
interface LoginRequestBody {
|
||||||
guid: string;
|
guid: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -66,6 +67,7 @@ declare namespace MasterModel {
|
|||||||
user_type?: 'admin' | 'enduser' | 'sysadmin' | 'users';
|
user_type?: 'admin' | 'enduser' | 'sysadmin' | 'users';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User
|
||||||
interface CreateUserMetadata extends ProfileMetadata {
|
interface CreateUserMetadata extends ProfileMetadata {
|
||||||
group_id?: string;
|
group_id?: string;
|
||||||
}
|
}
|
||||||
@@ -96,7 +98,7 @@ declare namespace MasterModel {
|
|||||||
thing_id?: string;
|
thing_id?: string;
|
||||||
time?: number;
|
time?: number;
|
||||||
}
|
}
|
||||||
|
// Alarm
|
||||||
interface Alarm {
|
interface Alarm {
|
||||||
name?: string;
|
name?: string;
|
||||||
time?: number;
|
time?: number;
|
||||||
@@ -110,6 +112,8 @@ declare namespace MasterModel {
|
|||||||
thing_name?: string;
|
thing_name?: string;
|
||||||
thing_type?: string;
|
thing_type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thing
|
||||||
interface ThingMetadata {
|
interface ThingMetadata {
|
||||||
address?: string;
|
address?: string;
|
||||||
alarm_list?: string;
|
alarm_list?: string;
|
||||||
@@ -155,6 +159,25 @@ declare namespace MasterModel {
|
|||||||
metadata?: T;
|
metadata?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thing Policy
|
||||||
|
interface ThingPolicy {
|
||||||
|
next_page_token?: string;
|
||||||
|
relations?: Relation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Relation {
|
||||||
|
id?: string;
|
||||||
|
actions?: Action[];
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Action {
|
||||||
|
Delete = 'delete',
|
||||||
|
Read = 'read',
|
||||||
|
Write = 'write',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group
|
||||||
|
|
||||||
interface GroupBodyRequest {
|
interface GroupBodyRequest {
|
||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -200,6 +223,8 @@ declare namespace MasterModel {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log
|
||||||
|
|
||||||
type LogTypeRequest = 'user_logs' | undefined;
|
type LogTypeRequest = 'user_logs' | undefined;
|
||||||
|
|
||||||
interface LogResponse {
|
interface LogResponse {
|
||||||
@@ -222,4 +247,12 @@ declare namespace MasterModel {
|
|||||||
time?: number;
|
time?: number;
|
||||||
string_value?: string;
|
string_value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User
|
||||||
|
|
||||||
|
interface AssignMemberRequest {
|
||||||
|
group_id: string;
|
||||||
|
type: 'users' | 'admin' | 'things' | undefined;
|
||||||
|
members: string[];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user