@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/ui-react",
3
- "version": "2.12.64",
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.64",
38
- "@arcblock/react-hooks": "^2.12.64",
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.43",
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": "535a7ce33f7325f00aab13b8812123cafc5788c7"
97
+ "gitHead": "12e8ad0b88c0d3e9ee9cbd84634e0778044cf494"
98
98
  }
@@ -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, theme, copyright } = formattedBlocklet;
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
- <OverridableThemeProvider theme={themeOverrides}>
66
- <StyledInternalFooter
67
- {...props}
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), {
@@ -167,7 +167,7 @@ Links.defaultProps = {
167
167
 
168
168
  const Root = styled('div')`
169
169
  overflow: hidden;
170
- color: #9397a1;
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: #4672ea;
202
- background-color: #e1e8fb;
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: #25292f;
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: #25292f;
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[400]};
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: #25292f;
59
+ color: ${({ theme }) => theme.palette.text.primary};
60
60
  }
61
61
  }
62
62
  ${(props) => props.theme.breakpoints.down('md')} {
@@ -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, theme } = formattedBlocklet;
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
- <OverridableThemeProvider theme={themeOverrides}>
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={theme?.background?.header}
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="#777"
189
+ textColor={mergeTheme.palette.grey[500]}
190
+ activeTextColor={mergeTheme.palette.text.primary}
189
191
  />
190
192
  )}
191
193
  </StyledUxHeader>
192
- </OverridableThemeProvider>
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: <UserSessions user={user} showUser={false} />,
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
- <a
458
- href={`mailto:${value}`}
459
- style={{
460
- color: 'inherit',
461
- textDecoration: 'none',
462
- }}>
463
- {value}
464
- </a>
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 { Box, Button, CircularProgress, Tooltip, Typography, useMediaQuery } from '@mui/material';
16
- import { memo, ReactElement, useContext, useEffect } from 'react';
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, SessionContext as TSessionContext } from '../../@types';
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 { session } = useContext<TSessionContext>(SessionContext);
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: () => Promise<(UserSession & { ipRegion?: string })[]> = useMemoizedFn(async () => {
100
- let data = (user?.userSessions || []) as (UserSession & { ipRegion?: string })[];
101
- try {
102
- if (!user?.userSessions && session.user) {
103
- data = await client.userSession.getMyLoginSessions();
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 now = new Date().getTime();
111
- return sortBy(data, (x) => {
112
- if (x.visitorId === currentVisitorId) {
113
- return -1;
114
- }
115
- return now - new Date(x.updatedAt).getTime();
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
- content: t('logoutAllSessionConfirm'),
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
- const list = otherUserSessions.map((x) => {
156
- return () => client.user.logout({ visitorId: x.visitorId });
157
- });
158
- await pAll(list, {
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 key="logoutAll" title={t('logoutAllTips')}>
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={otherUserSessions.length === 0}>
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
  };