@blocklet/ui-react 2.12.64 → 2.12.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Footer/index.js +6 -14
- package/lib/Footer/links.js +5 -5
- package/lib/Footer/social-media.js +2 -2
- package/lib/Header/index.js +8 -8
- package/lib/UserCenter/components/editable-field.d.ts +2 -1
- package/lib/UserCenter/components/editable-field.js +3 -2
- package/lib/UserCenter/components/settings.js +11 -1
- package/lib/UserCenter/components/user-info/metadata.js +6 -4
- package/lib/UserSessions/components/user-sessions.d.ts +12 -1
- package/lib/UserSessions/components/user-sessions.js +139 -51
- package/lib/UserSessions/libs/locales.d.ts +2 -0
- package/lib/UserSessions/libs/locales.js +8 -6
- package/lib/common/notification-addon.js +0 -2
- package/package.json +5 -5
- package/src/Footer/index.jsx +8 -13
- package/src/Footer/links.jsx +5 -5
- package/src/Footer/social-media.jsx +2 -2
- package/src/Header/index.tsx +9 -9
- package/src/UserCenter/components/editable-field.tsx +3 -1
- package/src/UserCenter/components/settings.tsx +14 -1
- package/src/UserCenter/components/user-info/metadata.tsx +16 -12
- package/src/UserSessions/components/user-sessions.tsx +130 -43
- package/src/UserSessions/libs/locales.ts +8 -6
- package/src/common/notification-addon.jsx +0 -2
- package/lib/UserCenter/components/user-info/clock.d.ts +0 -3
- package/lib/UserCenter/components/user-info/clock.js +0 -50
- package/lib/hooks/use-clock.d.ts +0 -9
- package/lib/hooks/use-clock.js +0 -49
- package/src/UserCenter/components/user-info/clock.tsx +0 -43
- package/src/hooks/use-clock.tsx +0 -61
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/ui-react",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.71",
|
|
4
4
|
"description": "Some useful front-end web components that can be used in Blocklets.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@abtnode/constant": "^1.16.41",
|
|
36
36
|
"@abtnode/util": "^1.16.41",
|
|
37
|
-
"@arcblock/bridge": "^2.12.
|
|
38
|
-
"@arcblock/react-hooks": "^2.12.
|
|
37
|
+
"@arcblock/bridge": "^2.12.71",
|
|
38
|
+
"@arcblock/react-hooks": "^2.12.71",
|
|
39
39
|
"@arcblock/ws": "^1.19.19",
|
|
40
40
|
"@blocklet/constant": "^1.16.42-beta-20250408-072924-4b6a877a",
|
|
41
|
-
"@blocklet/did-space-react": "^1.0.
|
|
41
|
+
"@blocklet/did-space-react": "^1.0.45",
|
|
42
42
|
"@iconify-icons/logos": "^1.2.36",
|
|
43
43
|
"@iconify-icons/material-symbols": "^1.2.58",
|
|
44
44
|
"@iconify-icons/tabler": "^1.2.95",
|
|
@@ -94,5 +94,5 @@
|
|
|
94
94
|
"jest": "^29.7.0",
|
|
95
95
|
"unbuild": "^2.0.0"
|
|
96
96
|
},
|
|
97
|
-
"gitHead": "
|
|
97
|
+
"gitHead": "12e8ad0b88c0d3e9ee9cbd84634e0778044cf494"
|
|
98
98
|
}
|
package/src/Footer/index.jsx
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import { styled } from '@arcblock/ux/lib/Theme';
|
|
3
|
+
import { styled, useTheme, deepmerge, ThemeProvider } from '@arcblock/ux/lib/Theme';
|
|
4
4
|
import { withErrorBoundary } from 'react-error-boundary';
|
|
5
5
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
6
6
|
import { ErrorFallback } from '@arcblock/ux/lib/ErrorBoundary';
|
|
7
7
|
import { temp as colors } from '@arcblock/ux/lib/Colors';
|
|
8
8
|
import omit from 'lodash/omit';
|
|
9
9
|
|
|
10
|
-
import OverridableThemeProvider from '../common/overridable-theme-provider';
|
|
11
10
|
import InternalFooter from './internal-footer';
|
|
12
11
|
import { mapRecursive } from '../utils';
|
|
13
12
|
import { formatBlockletInfo, getLocalizedNavigation } from '../blocklets';
|
|
@@ -19,6 +18,7 @@ import withHideWhenEmbed from '../libs/with-hide-when-embed';
|
|
|
19
18
|
*/
|
|
20
19
|
function Footer({ meta, theme: themeOverrides, ...rest }) {
|
|
21
20
|
const { locale } = useLocaleContext() || {};
|
|
21
|
+
const parentTheme = useTheme();
|
|
22
22
|
const formattedBlocklet = useMemo(() => {
|
|
23
23
|
const blocklet = Object.assign({}, window.blocklet, meta);
|
|
24
24
|
try {
|
|
@@ -28,12 +28,14 @@ function Footer({ meta, theme: themeOverrides, ...rest }) {
|
|
|
28
28
|
return blocklet;
|
|
29
29
|
}
|
|
30
30
|
}, [meta]);
|
|
31
|
+
const mergeTheme = useMemo(() => deepmerge(parentTheme, themeOverrides), [parentTheme, themeOverrides]);
|
|
31
32
|
|
|
32
33
|
if (!formattedBlocklet.appName) {
|
|
33
34
|
return null;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
const { appLogo, appLogoRect, appName, appDescription, description,
|
|
37
|
+
const { appLogo, appLogoRect, appName, appDescription, description, copyright } = formattedBlocklet;
|
|
38
|
+
const $bgColor = mergeTheme.palette.background.default;
|
|
37
39
|
|
|
38
40
|
const localized = {
|
|
39
41
|
footerNav: getLocalizedNavigation(formattedBlocklet?.navigation?.footer, locale) || [],
|
|
@@ -62,14 +64,9 @@ function Footer({ meta, theme: themeOverrides, ...rest }) {
|
|
|
62
64
|
};
|
|
63
65
|
|
|
64
66
|
return (
|
|
65
|
-
<
|
|
66
|
-
<StyledInternalFooter
|
|
67
|
-
|
|
68
|
-
{...omit(rest, ['bordered'])}
|
|
69
|
-
$bordered={rest?.bordered}
|
|
70
|
-
$bgcolor={theme?.background?.footer}
|
|
71
|
-
/>
|
|
72
|
-
</OverridableThemeProvider>
|
|
67
|
+
<ThemeProvider theme={mergeTheme}>
|
|
68
|
+
<StyledInternalFooter {...props} {...omit(rest, ['bordered'])} $bordered={rest?.bordered} $bgcolor={$bgColor} />
|
|
69
|
+
</ThemeProvider>
|
|
73
70
|
);
|
|
74
71
|
}
|
|
75
72
|
|
|
@@ -88,8 +85,6 @@ const StyledInternalFooter = styled(InternalFooter)`
|
|
|
88
85
|
${({ $bordered }) => `border-top: 1px solid ${$bordered ? colors.strokeSep : '#eee'};`}
|
|
89
86
|
color: ${(props) => props.theme.palette.grey[600]};
|
|
90
87
|
${({ $bgcolor }) => $bgcolor && `background-color: ${$bgcolor};`}
|
|
91
|
-
font-family: Inter, Avenir, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif,
|
|
92
|
-
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
|
93
88
|
`;
|
|
94
89
|
|
|
95
90
|
export default withErrorBoundary(withHideWhenEmbed(Footer), {
|
package/src/Footer/links.jsx
CHANGED
|
@@ -167,7 +167,7 @@ Links.defaultProps = {
|
|
|
167
167
|
|
|
168
168
|
const Root = styled('div')`
|
|
169
169
|
overflow: hidden;
|
|
170
|
-
color:
|
|
170
|
+
color: ${({ theme }) => theme.palette.grey[500]};
|
|
171
171
|
.footer-links-inner {
|
|
172
172
|
display: flex;
|
|
173
173
|
justify-content: space-between;
|
|
@@ -198,8 +198,8 @@ const Root = styled('div')`
|
|
|
198
198
|
font-size: 14px;
|
|
199
199
|
&--new::after {
|
|
200
200
|
content: 'New';
|
|
201
|
-
color:
|
|
202
|
-
background-color:
|
|
201
|
+
color: ${({ theme }) => theme.palette.info.main};
|
|
202
|
+
background-color: ${({ theme }) => theme.palette.info.light};
|
|
203
203
|
padding: 1px 8px;
|
|
204
204
|
border-radius: 10px/50%;
|
|
205
205
|
margin-left: 8px;
|
|
@@ -209,7 +209,7 @@ const Root = styled('div')`
|
|
|
209
209
|
.footer-links-group {
|
|
210
210
|
> .footer-links-item {
|
|
211
211
|
font-weight: 600;
|
|
212
|
-
color:
|
|
212
|
+
color: ${({ theme }) => theme.palette.text.primary};
|
|
213
213
|
}
|
|
214
214
|
.footer-links-sub {
|
|
215
215
|
margin-top: 8px;
|
|
@@ -223,7 +223,7 @@ const Root = styled('div')`
|
|
|
223
223
|
text-decoration: none;
|
|
224
224
|
transition: color 0.2s ease-in-out;
|
|
225
225
|
&:hover {
|
|
226
|
-
color:
|
|
226
|
+
color: ${({ theme }) => theme.palette.text.primary};
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
/* columns 布局 */
|
|
@@ -52,11 +52,11 @@ const Root = styled('div')`
|
|
|
52
52
|
justify-content: center;
|
|
53
53
|
gap: 20px;
|
|
54
54
|
a {
|
|
55
|
-
color: ${(props) => props.theme.palette.grey[
|
|
55
|
+
color: ${(props) => props.theme.palette.grey[500]};
|
|
56
56
|
text-decoration: none;
|
|
57
57
|
transition: color 0.2s ease-in-out;
|
|
58
58
|
&:hover {
|
|
59
|
-
color:
|
|
59
|
+
color: ${({ theme }) => theme.palette.text.primary};
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
${(props) => props.theme.breakpoints.down('md')} {
|
package/src/Header/index.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
|
|
2
2
|
import { useMemoizedFn } from 'ahooks';
|
|
3
3
|
import { withErrorBoundary } from 'react-error-boundary';
|
|
4
4
|
import { ErrorFallback } from '@arcblock/ux/lib/ErrorBoundary';
|
|
5
|
-
import { styled } from '@arcblock/ux/lib/Theme';
|
|
5
|
+
import { styled, useTheme, deepmerge, ThemeProvider } from '@arcblock/ux/lib/Theme';
|
|
6
6
|
import { ResponsiveHeader } from '@arcblock/ux/lib/Header';
|
|
7
7
|
import NavMenu, { Products } from '@arcblock/ux/lib/NavMenu';
|
|
8
8
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
@@ -14,7 +14,6 @@ import type { BoxProps, Breakpoint } from '@mui/material';
|
|
|
14
14
|
import clsx from 'clsx';
|
|
15
15
|
|
|
16
16
|
import Icon from '../Icon';
|
|
17
|
-
import OverridableThemeProvider from '../common/overridable-theme-provider';
|
|
18
17
|
import { mapRecursive, flatRecursive, matchPaths } from '../utils';
|
|
19
18
|
import { publicPath, formatBlockletInfo, getLocalizedNavigation } from '../blocklets';
|
|
20
19
|
import HeaderAddons from '../common/header-addons';
|
|
@@ -115,6 +114,7 @@ function Header({
|
|
|
115
114
|
...rest
|
|
116
115
|
}: HeaderProps & Omit<BoxProps, keyof HeaderProps>) {
|
|
117
116
|
useWalletHiddenTopbar();
|
|
117
|
+
const parentTheme = useTheme();
|
|
118
118
|
const { locale } = useLocaleContext() || {};
|
|
119
119
|
const t = useMemoizedFn((key, data = {}) => {
|
|
120
120
|
return translate(translations, key, locale, 'en', data);
|
|
@@ -129,11 +129,12 @@ function Header({
|
|
|
129
129
|
}
|
|
130
130
|
}, [meta]);
|
|
131
131
|
const isMobileDevice = useMobile();
|
|
132
|
+
const mergeTheme = useMemo(() => deepmerge(parentTheme, themeOverrides), [parentTheme, themeOverrides]);
|
|
132
133
|
|
|
133
134
|
if (!formattedBlocklet.appName) {
|
|
134
135
|
return null;
|
|
135
136
|
}
|
|
136
|
-
const { appLogo, appLogoRect
|
|
137
|
+
const { appLogo, appLogoRect } = formattedBlocklet;
|
|
137
138
|
const navigation = getLocalizedNavigation(formattedBlocklet?.navigation?.header, locale);
|
|
138
139
|
const parsedNavigation = parseNavigation(navigation);
|
|
139
140
|
const { navItems, activeId } = parsedNavigation;
|
|
@@ -164,7 +165,7 @@ function Header({
|
|
|
164
165
|
);
|
|
165
166
|
|
|
166
167
|
return (
|
|
167
|
-
<
|
|
168
|
+
<ThemeProvider theme={mergeTheme}>
|
|
168
169
|
<StyledUxHeader
|
|
169
170
|
// @ts-ignore
|
|
170
171
|
homeLink={homeLink}
|
|
@@ -172,7 +173,7 @@ function Header({
|
|
|
172
173
|
addons={headerAddons}
|
|
173
174
|
{...omit(rest, ['bordered'])}
|
|
174
175
|
$bordered={rest?.bordered}
|
|
175
|
-
$bgcolor={
|
|
176
|
+
$bgcolor={mergeTheme.palette.background.default}
|
|
176
177
|
className={clsx('blocklet__header', rest.className)}>
|
|
177
178
|
{/* blocklet.yml 没有配置 navigation 时, 则为 children 传入 null, 此时 ResponsiveHeader 会渲染普通的不带 menu 的 Header */}
|
|
178
179
|
{hideNavMenu || !navItems?.length
|
|
@@ -185,11 +186,12 @@ function Header({
|
|
|
185
186
|
items={navItems}
|
|
186
187
|
className="header-nav"
|
|
187
188
|
bgColor="transparent"
|
|
188
|
-
textColor=
|
|
189
|
+
textColor={mergeTheme.palette.grey[500]}
|
|
190
|
+
activeTextColor={mergeTheme.palette.text.primary}
|
|
189
191
|
/>
|
|
190
192
|
)}
|
|
191
193
|
</StyledUxHeader>
|
|
192
|
-
</
|
|
194
|
+
</ThemeProvider>
|
|
193
195
|
);
|
|
194
196
|
}
|
|
195
197
|
|
|
@@ -200,8 +202,6 @@ type StyledUxHeaderProps = {
|
|
|
200
202
|
|
|
201
203
|
const StyledUxHeader = styled(ResponsiveHeader)<StyledUxHeaderProps>`
|
|
202
204
|
${({ $bgcolor }) => `background-color: ${$bgcolor || '#fff'};`}
|
|
203
|
-
font-family: Inter, Avenir, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif,
|
|
204
|
-
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
|
205
205
|
.header-logo {
|
|
206
206
|
min-width: 44px;
|
|
207
207
|
}
|
|
@@ -27,6 +27,7 @@ interface EditableFieldProps {
|
|
|
27
27
|
verified?: boolean;
|
|
28
28
|
errorMsg?: string;
|
|
29
29
|
canEdit?: boolean;
|
|
30
|
+
hidePreview?: boolean;
|
|
30
31
|
renderValue?: (value: string) => React.ReactNode;
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -78,6 +79,7 @@ function EditableField({
|
|
|
78
79
|
canEdit = true,
|
|
79
80
|
renderValue,
|
|
80
81
|
disabled = false,
|
|
82
|
+
hidePreview = false,
|
|
81
83
|
}: EditableFieldProps) {
|
|
82
84
|
const { locale } = useLocaleContext();
|
|
83
85
|
const t = useMemoizedFn((key, data = {}) => {
|
|
@@ -174,7 +176,7 @@ function EditableField({
|
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
if (!editable) {
|
|
177
|
-
return value ? (
|
|
179
|
+
return value && !hidePreview ? (
|
|
178
180
|
<Tooltip
|
|
179
181
|
open={Boolean(mousePosition)}
|
|
180
182
|
title={tooltip}
|
|
@@ -15,6 +15,7 @@ import { UserSessions } from '../../UserSessions';
|
|
|
15
15
|
import ThirdPartyLogin from './third-party-login';
|
|
16
16
|
import ConfigProfile from './config-profile';
|
|
17
17
|
import DangerZone from './danger-zone';
|
|
18
|
+
import { client } from '../../libs/client';
|
|
18
19
|
|
|
19
20
|
export default function Settings({
|
|
20
21
|
user,
|
|
@@ -68,7 +69,19 @@ export default function Settings({
|
|
|
68
69
|
{
|
|
69
70
|
label: t('sessionManagement'),
|
|
70
71
|
value: 'session',
|
|
71
|
-
content:
|
|
72
|
+
content: (
|
|
73
|
+
<UserSessions
|
|
74
|
+
user={user}
|
|
75
|
+
showUser={false}
|
|
76
|
+
// FIXME: @zhanghan 暂时忽略类型不对的问题 (js-sdk 新版发布后,此处的类型就正确了)
|
|
77
|
+
// @ts-ignore
|
|
78
|
+
getUserSessions={(params) => {
|
|
79
|
+
// FIXME: @zhanghan 暂时忽略类型不对的问题 (js-sdk 新版发布后,此处的类型就正确了)
|
|
80
|
+
// @ts-ignore
|
|
81
|
+
return client.userSession.getMyLoginSessions({}, params);
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
),
|
|
72
85
|
},
|
|
73
86
|
{
|
|
74
87
|
label: t('dangerZone.title'),
|
|
@@ -25,6 +25,7 @@ import isEmail from 'validator/lib/isEmail';
|
|
|
25
25
|
import isPostalCode from 'validator/lib/isPostalCode';
|
|
26
26
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
27
27
|
import { useBrowser } from '@arcblock/react-hooks';
|
|
28
|
+
import Clock from '@arcblock/ux/lib/UserCard/Content/clock';
|
|
28
29
|
|
|
29
30
|
import { translations } from '../../libs/locales';
|
|
30
31
|
import type { User, UserAddress, UserMetadata, UserPhoneProps } from '../../../@types';
|
|
@@ -32,7 +33,6 @@ import EditableField, { commonInputStyle, inputFieldStyle } from '../editable-fi
|
|
|
32
33
|
import { LinkPreviewInput } from './link-preview-input';
|
|
33
34
|
import { currentTimezone, defaultButtonStyle, primaryButtonStyle } from './utils';
|
|
34
35
|
import { TimezoneSelect } from './timezone-select';
|
|
35
|
-
import Clock from './clock';
|
|
36
36
|
import AddressEditor from './address';
|
|
37
37
|
|
|
38
38
|
const LocationIcon = lazy(() => import('@arcblock/icons/lib/Location'));
|
|
@@ -429,6 +429,7 @@ export default function UserMetadataComponent({
|
|
|
429
429
|
<EditableField
|
|
430
430
|
value={metadata.email ?? user?.email ?? ''}
|
|
431
431
|
editable={editing}
|
|
432
|
+
hidePreview={!isMyself}
|
|
432
433
|
disabled={user?.sourceProvider === LOGIN_PROVIDER.EMAIL}
|
|
433
434
|
canEdit={!emailVerified}
|
|
434
435
|
verified={emailVerified}
|
|
@@ -453,16 +454,18 @@ export default function UserMetadataComponent({
|
|
|
453
454
|
}
|
|
454
455
|
onChange={(value) => onChange(value, 'email')}
|
|
455
456
|
errorMsg={validateMsg.email}
|
|
456
|
-
renderValue={(value) =>
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
457
|
+
renderValue={(value) =>
|
|
458
|
+
isMyself ? (
|
|
459
|
+
<a
|
|
460
|
+
href={`mailto:${value}`}
|
|
461
|
+
style={{
|
|
462
|
+
color: 'inherit',
|
|
463
|
+
textDecoration: 'none',
|
|
464
|
+
}}>
|
|
465
|
+
{value}
|
|
466
|
+
</a>
|
|
467
|
+
) : null
|
|
468
|
+
}
|
|
466
469
|
onValueValidate={(value) => {
|
|
467
470
|
let msg = '';
|
|
468
471
|
if (!!value && !isEmail(value)) {
|
|
@@ -475,6 +478,7 @@ export default function UserMetadataComponent({
|
|
|
475
478
|
<EditableField
|
|
476
479
|
value={phoneValue.phone}
|
|
477
480
|
editable={editing}
|
|
481
|
+
hidePreview={!isMyself}
|
|
478
482
|
canEdit={!phoneVerified}
|
|
479
483
|
verified={phoneVerified}
|
|
480
484
|
placeholder="Phone"
|
|
@@ -482,7 +486,7 @@ export default function UserMetadataComponent({
|
|
|
482
486
|
onChange={(value) => onChange(value, 'phone')}
|
|
483
487
|
label={t('profile.phone')}
|
|
484
488
|
renderValue={() => {
|
|
485
|
-
return <PhoneInput value={phoneValue} preview
|
|
489
|
+
return isMyself ? <PhoneInput value={phoneValue} preview /> : null;
|
|
486
490
|
}}>
|
|
487
491
|
<PhoneInput
|
|
488
492
|
variant="outlined"
|
|
@@ -5,21 +5,28 @@ import { useCreation, useMemoizedFn, useReactive, useRequest } from 'ahooks';
|
|
|
5
5
|
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
6
6
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
7
7
|
import RelativeTime from '@arcblock/ux/lib/RelativeTime';
|
|
8
|
-
import sortBy from 'lodash/sortBy';
|
|
9
8
|
import UAParser from 'ua-parser-js';
|
|
10
9
|
import { getVisitorId } from '@arcblock/ux/lib/Util';
|
|
11
10
|
import { useConfirm } from '@arcblock/ux/lib/Dialog';
|
|
12
11
|
import { temp as colors } from '@arcblock/ux/lib/Colors';
|
|
13
|
-
import pAll from 'p-all';
|
|
14
12
|
import PQueue from 'p-queue';
|
|
15
|
-
import {
|
|
16
|
-
|
|
13
|
+
import {
|
|
14
|
+
Box,
|
|
15
|
+
Button,
|
|
16
|
+
CircularProgress,
|
|
17
|
+
FormControlLabel,
|
|
18
|
+
Radio,
|
|
19
|
+
RadioGroup,
|
|
20
|
+
Tooltip,
|
|
21
|
+
Typography,
|
|
22
|
+
useMediaQuery,
|
|
23
|
+
} from '@mui/material';
|
|
24
|
+
import { memo, ReactElement, useEffect } from 'react';
|
|
17
25
|
import { UserSession } from '@blocklet/js-sdk';
|
|
18
|
-
import { SessionContext } from '@arcblock/did-connect/lib/Session';
|
|
19
26
|
|
|
20
27
|
import useMobile from '../../hooks/use-mobile';
|
|
21
28
|
import UserSessionInfo from './user-session-info';
|
|
22
|
-
import { User
|
|
29
|
+
import { User } from '../../@types';
|
|
23
30
|
import { client } from '../../libs/client';
|
|
24
31
|
import { translations } from '../libs/locales';
|
|
25
32
|
import { ip2Region } from '../libs/utils';
|
|
@@ -80,14 +87,29 @@ export default function UserSessions({
|
|
|
80
87
|
user,
|
|
81
88
|
showAction = true,
|
|
82
89
|
showUser = true,
|
|
90
|
+
getUserSessions,
|
|
83
91
|
}: {
|
|
84
92
|
readonly user: User & {
|
|
85
93
|
userSessions?: any[];
|
|
86
94
|
};
|
|
87
95
|
readonly showAction?: boolean;
|
|
88
96
|
readonly showUser?: boolean;
|
|
97
|
+
readonly getUserSessions: (params: { page: number; pageSize: number; status: string }) => Promise<{
|
|
98
|
+
paging: {
|
|
99
|
+
total: number;
|
|
100
|
+
};
|
|
101
|
+
list: UserSession[];
|
|
102
|
+
}>;
|
|
89
103
|
}) {
|
|
90
|
-
const
|
|
104
|
+
const filterParams = useReactive({
|
|
105
|
+
status: 'online',
|
|
106
|
+
page: 1,
|
|
107
|
+
pageSize: 10,
|
|
108
|
+
});
|
|
109
|
+
const userSessionsCountMap = useReactive({
|
|
110
|
+
online: 0,
|
|
111
|
+
expired: 0,
|
|
112
|
+
});
|
|
91
113
|
const currentVisitorId = getVisitorId();
|
|
92
114
|
const { locale } = useLocaleContext();
|
|
93
115
|
const isMobile = useMobile({ key: 'md' });
|
|
@@ -96,31 +118,48 @@ export default function UserSessions({
|
|
|
96
118
|
const t = useMemoizedFn((key, data = {}) => {
|
|
97
119
|
return translate(translations, key, locale, 'en', data);
|
|
98
120
|
});
|
|
99
|
-
const getData
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
} catch (e) {
|
|
106
|
-
console.warn('Failed to convert ip to region');
|
|
107
|
-
console.error(e);
|
|
108
|
-
}
|
|
121
|
+
const getData = useMemoizedFn(async () => {
|
|
122
|
+
const result = await getUserSessions({
|
|
123
|
+
page: filterParams.page,
|
|
124
|
+
pageSize: filterParams.pageSize,
|
|
125
|
+
status: filterParams.status,
|
|
126
|
+
});
|
|
109
127
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
const total = result?.paging?.total || 0;
|
|
129
|
+
userSessionsCountMap[filterParams.status as keyof typeof userSessionsCountMap] = total;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
total,
|
|
133
|
+
list: result?.list || [],
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
useRequest(async () => {
|
|
138
|
+
const result = await getUserSessions({
|
|
139
|
+
page: 1,
|
|
140
|
+
pageSize: 1,
|
|
141
|
+
status: 'expired',
|
|
116
142
|
});
|
|
143
|
+
userSessionsCountMap.expired = result?.paging?.total || 0;
|
|
144
|
+
return result?.paging?.total || 0;
|
|
117
145
|
});
|
|
118
146
|
|
|
119
|
-
const pageState = useRequest(getData
|
|
147
|
+
const pageState = useRequest(getData, {
|
|
148
|
+
refreshDeps: [filterParams.status, filterParams.page, filterParams.pageSize],
|
|
149
|
+
});
|
|
120
150
|
|
|
121
151
|
const safeData = useCreation(() => {
|
|
122
|
-
return pageState.data || [];
|
|
123
|
-
}, [pageState.data]);
|
|
152
|
+
return pageState.data?.list || [];
|
|
153
|
+
}, [pageState.data?.list]);
|
|
154
|
+
|
|
155
|
+
const disableLogout = useCreation(() => {
|
|
156
|
+
const status = filterParams.status as keyof typeof userSessionsCountMap;
|
|
157
|
+
if (status === 'online') {
|
|
158
|
+
// HACK: 这里只能假设会话列表包含了当前登录的会话,所以只有大于 1 的时候才能够去注销其他会话
|
|
159
|
+
return userSessionsCountMap[status] <= 1;
|
|
160
|
+
}
|
|
161
|
+
return userSessionsCountMap[status] === 0;
|
|
162
|
+
}, [safeData, currentVisitorId]);
|
|
124
163
|
|
|
125
164
|
const logout = useMemoizedFn(({ visitorId }) => {
|
|
126
165
|
confirmApi.open({
|
|
@@ -138,26 +177,24 @@ export default function UserSessions({
|
|
|
138
177
|
},
|
|
139
178
|
});
|
|
140
179
|
});
|
|
141
|
-
const otherUserSessions = useCreation(() => {
|
|
142
|
-
const list = safeData.filter((x) => x.visitorId !== currentVisitorId);
|
|
143
|
-
return list;
|
|
144
|
-
}, [safeData]);
|
|
145
180
|
const logoutAll = useMemoizedFn(() => {
|
|
146
181
|
confirmApi.open({
|
|
147
|
-
title: t('logoutAllSession'
|
|
148
|
-
|
|
182
|
+
title: t('logoutAllSession', {
|
|
183
|
+
type: t(filterParams.status),
|
|
184
|
+
}),
|
|
185
|
+
content: t('logoutAllSessionConfirm', {
|
|
186
|
+
type: t(filterParams.status),
|
|
187
|
+
}),
|
|
149
188
|
confirmButtonText: t('confirm'),
|
|
150
189
|
confirmButtonProps: {
|
|
151
190
|
color: 'error',
|
|
152
191
|
},
|
|
153
192
|
cancelButtonText: t('cancel'),
|
|
154
193
|
onConfirm: async () => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
concurrency: 3,
|
|
160
|
-
stopOnError: false,
|
|
194
|
+
await client.user.logout({
|
|
195
|
+
// @ts-expect-error js-sdk 发了新版后,会有这个类型定义
|
|
196
|
+
status: filterParams.status,
|
|
197
|
+
visitorId: currentVisitorId as string,
|
|
161
198
|
});
|
|
162
199
|
pageState.refresh();
|
|
163
200
|
confirmApi.close();
|
|
@@ -167,15 +204,21 @@ export default function UserSessions({
|
|
|
167
204
|
const customButtons: ReactElement[] = [];
|
|
168
205
|
if (showAction) {
|
|
169
206
|
customButtons.push(
|
|
170
|
-
<Tooltip
|
|
207
|
+
<Tooltip
|
|
208
|
+
key="logoutAll"
|
|
209
|
+
title={t('logoutAllTips', {
|
|
210
|
+
type: t(filterParams.status),
|
|
211
|
+
})}>
|
|
171
212
|
<Button
|
|
172
213
|
sx={{ ml: 0.5 }}
|
|
173
214
|
size="small"
|
|
174
215
|
variant="contained"
|
|
175
216
|
color="error"
|
|
176
217
|
onClick={logoutAll}
|
|
177
|
-
disabled={
|
|
178
|
-
{t('logoutAll'
|
|
218
|
+
disabled={disableLogout}>
|
|
219
|
+
{t('logoutAll', {
|
|
220
|
+
type: t(filterParams.status),
|
|
221
|
+
})}
|
|
179
222
|
</Button>
|
|
180
223
|
</Tooltip>
|
|
181
224
|
);
|
|
@@ -190,8 +233,11 @@ export default function UserSessions({
|
|
|
190
233
|
print: false,
|
|
191
234
|
expandableRowsOnClick: false,
|
|
192
235
|
searchDebounceTime: 600,
|
|
236
|
+
page: filterParams.page - 1,
|
|
237
|
+
rowsPerPage: filterParams.pageSize,
|
|
238
|
+
count: pageState.data?.total || 0,
|
|
193
239
|
};
|
|
194
|
-
}, []);
|
|
240
|
+
}, [pageState.data?.total, filterParams.page, filterParams.pageSize]);
|
|
195
241
|
const columns = [
|
|
196
242
|
{
|
|
197
243
|
label: t('platform'),
|
|
@@ -313,7 +359,6 @@ export default function UserSessions({
|
|
|
313
359
|
size="small"
|
|
314
360
|
color="error"
|
|
315
361
|
onClick={() => logout({ visitorId: x.visitorId })}>
|
|
316
|
-
{/* @ts-ignore FIXME: @zhanghan 新版 js-sdk 会提供这个属性的提示 */}
|
|
317
362
|
{x.status === 'expired'
|
|
318
363
|
? t('remove')
|
|
319
364
|
: currentVisitorId === x.visitorId
|
|
@@ -371,6 +416,7 @@ export default function UserSessions({
|
|
|
371
416
|
}}>
|
|
372
417
|
{confirmHolder}
|
|
373
418
|
<Datatable
|
|
419
|
+
count={pageState.data?.total || 0}
|
|
374
420
|
locale={locale}
|
|
375
421
|
data={safeData}
|
|
376
422
|
// @ts-expect-error
|
|
@@ -380,6 +426,47 @@ export default function UserSessions({
|
|
|
380
426
|
options={tableOptions}
|
|
381
427
|
loading={pageState.loading}
|
|
382
428
|
className="pc-user-sessions-table"
|
|
429
|
+
title={
|
|
430
|
+
<RadioGroup
|
|
431
|
+
row
|
|
432
|
+
sx={{ lineHeight: 1, ml: 1 }}
|
|
433
|
+
onChange={(e) => {
|
|
434
|
+
filterParams.status = e.target.value;
|
|
435
|
+
}}>
|
|
436
|
+
<FormControlLabel
|
|
437
|
+
value="online"
|
|
438
|
+
control={
|
|
439
|
+
<Radio size="small" sx={{ lineHeight: 1, fontSize: 0 }} checked={filterParams.status === 'online'} />
|
|
440
|
+
}
|
|
441
|
+
label={
|
|
442
|
+
<Typography sx={{ display: 'flex', alignItems: 'center' }}>
|
|
443
|
+
{t('online')}
|
|
444
|
+
<Typography component="span" sx={{ ml: 0.5, color: 'text.secondary' }}>
|
|
445
|
+
({userSessionsCountMap.online})
|
|
446
|
+
</Typography>
|
|
447
|
+
</Typography>
|
|
448
|
+
}
|
|
449
|
+
/>
|
|
450
|
+
<FormControlLabel
|
|
451
|
+
value="expired"
|
|
452
|
+
control={
|
|
453
|
+
<Radio size="small" sx={{ lineHeight: 1, fontSize: 0 }} checked={filterParams.status === 'expired'} />
|
|
454
|
+
}
|
|
455
|
+
label={
|
|
456
|
+
<Typography sx={{ display: 'flex', alignItems: 'center' }}>
|
|
457
|
+
{t('expired')}
|
|
458
|
+
<Typography component="span" sx={{ ml: 0.5, color: 'text.secondary' }}>
|
|
459
|
+
({userSessionsCountMap.expired})
|
|
460
|
+
</Typography>
|
|
461
|
+
</Typography>
|
|
462
|
+
}
|
|
463
|
+
/>
|
|
464
|
+
</RadioGroup>
|
|
465
|
+
}
|
|
466
|
+
onChange={(state) => {
|
|
467
|
+
filterParams.page = state.page + 1;
|
|
468
|
+
filterParams.pageSize = state.rowsPerPage;
|
|
469
|
+
}}
|
|
383
470
|
/>
|
|
384
471
|
</Box>
|
|
385
472
|
);
|
|
@@ -15,7 +15,7 @@ export const translations = {
|
|
|
15
15
|
updatedAt: '最近活动时间',
|
|
16
16
|
lastLoginIp: '最近活动地址',
|
|
17
17
|
actions: '操作',
|
|
18
|
-
logoutAll: '
|
|
18
|
+
logoutAll: '注销所有{type}会话',
|
|
19
19
|
logoutAllTips: '不会注销当前会话',
|
|
20
20
|
logout: '注销',
|
|
21
21
|
currentSession: '当前会话',
|
|
@@ -24,8 +24,9 @@ export const translations = {
|
|
|
24
24
|
remove: '删除',
|
|
25
25
|
logoutThisSession: '注销指定会话',
|
|
26
26
|
logoutThisSessionConfirm: '确定要注销此会话吗?',
|
|
27
|
-
logoutAllSession: '
|
|
28
|
-
logoutAllSessionConfirm: '
|
|
27
|
+
logoutAllSession: '注销所有{type}会话',
|
|
28
|
+
logoutAllSessionConfirm: '确定要注销所有{type}会话吗?',
|
|
29
|
+
online: '活跃',
|
|
29
30
|
},
|
|
30
31
|
en: {
|
|
31
32
|
confirm: 'Confirm',
|
|
@@ -43,7 +44,7 @@ export const translations = {
|
|
|
43
44
|
updatedAt: 'Last Active Time',
|
|
44
45
|
actions: 'Actions',
|
|
45
46
|
lastLoginIp: 'Last Login IP',
|
|
46
|
-
logoutAll: 'Logout all',
|
|
47
|
+
logoutAll: 'Logout all {type} sessions',
|
|
47
48
|
logoutAllTips: 'Will not logout current session',
|
|
48
49
|
logout: 'Logout',
|
|
49
50
|
currentSession: 'Current Session',
|
|
@@ -52,7 +53,8 @@ export const translations = {
|
|
|
52
53
|
remove: 'Remove',
|
|
53
54
|
logoutThisSession: 'Logout this session',
|
|
54
55
|
logoutThisSessionConfirm: 'Are you sure to logout this session?',
|
|
55
|
-
logoutAllSession: 'Logout all sessions',
|
|
56
|
-
logoutAllSessionConfirm: 'Are you sure to logout all sessions?',
|
|
56
|
+
logoutAllSession: 'Logout all {type} sessions',
|
|
57
|
+
logoutAllSessionConfirm: 'Are you sure to logout all {type} sessions?',
|
|
58
|
+
online: 'Active',
|
|
57
59
|
},
|
|
58
60
|
};
|