@blocklet/ui-react 2.12.70 → 2.12.72

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.
@@ -13,6 +13,7 @@ import { UserSessions } from "../../UserSessions/index.js";
13
13
  import ThirdPartyLogin from "./third-party-login/index.js";
14
14
  import ConfigProfile from "./config-profile.js";
15
15
  import DangerZone from "./danger-zone.js";
16
+ import { client } from "../../libs/client.js";
16
17
  export default function Settings({
17
18
  user,
18
19
  settings,
@@ -59,7 +60,16 @@ export default function Settings({
59
60
  {
60
61
  label: t("sessionManagement"),
61
62
  value: "session",
62
- content: /* @__PURE__ */ jsx(UserSessions, { user, showUser: false })
63
+ content: /* @__PURE__ */ jsx(
64
+ UserSessions,
65
+ {
66
+ user,
67
+ showUser: false,
68
+ getUserSessions: (params) => {
69
+ return client.userSession.getMyLoginSessions({}, params);
70
+ }
71
+ }
72
+ )
63
73
  },
64
74
  {
65
75
  label: t("dangerZone.title"),
@@ -1,8 +1,19 @@
1
+ import { UserSession } from '@blocklet/js-sdk';
1
2
  import { User } from '../../@types';
2
- export default function UserSessions({ user, showAction, showUser, }: {
3
+ export default function UserSessions({ user, showAction, showUser, getUserSessions, }: {
3
4
  readonly user: User & {
4
5
  userSessions?: any[];
5
6
  };
6
7
  readonly showAction?: boolean;
7
8
  readonly showUser?: boolean;
9
+ readonly getUserSessions: (params: {
10
+ page: number;
11
+ pageSize: number;
12
+ status: string;
13
+ }) => Promise<{
14
+ paging: {
15
+ total: number;
16
+ };
17
+ list: UserSession[];
18
+ }>;
8
19
  }): import("react").JSX.Element;
@@ -4,16 +4,23 @@ import { useCreation, useMemoizedFn, useReactive, useRequest } from "ahooks";
4
4
  import { translate } from "@arcblock/ux/lib/Locale/util";
5
5
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
6
6
  import RelativeTime from "@arcblock/ux/lib/RelativeTime";
7
- import sortBy from "lodash/sortBy";
8
7
  import UAParser from "ua-parser-js";
9
8
  import { getVisitorId } from "@arcblock/ux/lib/Util";
10
9
  import { useConfirm } from "@arcblock/ux/lib/Dialog";
11
10
  import { temp as colors } from "@arcblock/ux/lib/Colors";
12
- import pAll from "p-all";
13
11
  import PQueue from "p-queue";
14
- import { Box, Button, CircularProgress, Tooltip, Typography, useMediaQuery } from "@mui/material";
15
- import { memo, useContext, useEffect } from "react";
16
- import { SessionContext } from "@arcblock/did-connect/lib/Session";
12
+ import {
13
+ Box,
14
+ Button,
15
+ CircularProgress,
16
+ FormControlLabel,
17
+ Radio,
18
+ RadioGroup,
19
+ Tooltip,
20
+ Typography,
21
+ useMediaQuery
22
+ } from "@mui/material";
23
+ import { memo, useEffect } from "react";
17
24
  import useMobile from "../../hooks/use-mobile.js";
18
25
  import UserSessionInfo from "./user-session-info.js";
19
26
  import { client } from "../../libs/client.js";
@@ -54,9 +61,18 @@ const UserSessionIp = memo(({ userSession, isMobile = false }) => {
54
61
  export default function UserSessions({
55
62
  user,
56
63
  showAction = true,
57
- showUser = true
64
+ showUser = true,
65
+ getUserSessions
58
66
  }) {
59
- const { session } = useContext(SessionContext);
67
+ const filterParams = useReactive({
68
+ status: "online",
69
+ page: 1,
70
+ pageSize: 10
71
+ });
72
+ const userSessionsCountMap = useReactive({
73
+ online: 0,
74
+ expired: 0
75
+ });
60
76
  const currentVisitorId = getVisitorId();
61
77
  const { locale } = useLocaleContext();
62
78
  const isMobile = useMobile({ key: "md" });
@@ -66,27 +82,40 @@ export default function UserSessions({
66
82
  return translate(translations, key, locale, "en", data);
67
83
  });
68
84
  const getData = useMemoizedFn(async () => {
69
- let data = user?.userSessions || [];
70
- try {
71
- if (!user?.userSessions && session.user) {
72
- data = await client.userSession.getMyLoginSessions();
73
- }
74
- } catch (e) {
75
- console.warn("Failed to convert ip to region");
76
- console.error(e);
77
- }
78
- const now = (/* @__PURE__ */ new Date()).getTime();
79
- return sortBy(data, (x) => {
80
- if (x.visitorId === currentVisitorId) {
81
- return -1;
82
- }
83
- return now - new Date(x.updatedAt).getTime();
85
+ const result = await getUserSessions({
86
+ page: filterParams.page,
87
+ pageSize: filterParams.pageSize,
88
+ status: filterParams.status
84
89
  });
90
+ const total = result?.paging?.total || 0;
91
+ userSessionsCountMap[filterParams.status] = total;
92
+ return {
93
+ total,
94
+ list: result?.list || []
95
+ };
96
+ });
97
+ useRequest(async () => {
98
+ const result = await getUserSessions({
99
+ page: 1,
100
+ pageSize: 1,
101
+ status: "expired"
102
+ });
103
+ userSessionsCountMap.expired = result?.paging?.total || 0;
104
+ return result?.paging?.total || 0;
105
+ });
106
+ const pageState = useRequest(getData, {
107
+ refreshDeps: [filterParams.status, filterParams.page, filterParams.pageSize]
85
108
  });
86
- const pageState = useRequest(getData);
87
109
  const safeData = useCreation(() => {
88
- return pageState.data || [];
89
- }, [pageState.data]);
110
+ return pageState.data?.list || [];
111
+ }, [pageState.data?.list]);
112
+ const disableLogout = useCreation(() => {
113
+ const status = filterParams.status;
114
+ if (status === "online") {
115
+ return userSessionsCountMap[status] <= 1;
116
+ }
117
+ return userSessionsCountMap[status] === 0;
118
+ }, [safeData, currentVisitorId]);
90
119
  const logout = useMemoizedFn(({ visitorId }) => {
91
120
  confirmApi.open({
92
121
  title: t("logoutThisSession"),
@@ -103,26 +132,24 @@ export default function UserSessions({
103
132
  }
104
133
  });
105
134
  });
106
- const otherUserSessions = useCreation(() => {
107
- const list = safeData.filter((x) => x.visitorId !== currentVisitorId);
108
- return list;
109
- }, [safeData]);
110
135
  const logoutAll = useMemoizedFn(() => {
111
136
  confirmApi.open({
112
- title: t("logoutAllSession"),
113
- content: t("logoutAllSessionConfirm"),
137
+ title: t("logoutAllSession", {
138
+ type: t(filterParams.status)
139
+ }),
140
+ content: t("logoutAllSessionConfirm", {
141
+ type: t(filterParams.status)
142
+ }),
114
143
  confirmButtonText: t("confirm"),
115
144
  confirmButtonProps: {
116
145
  color: "error"
117
146
  },
118
147
  cancelButtonText: t("cancel"),
119
148
  onConfirm: async () => {
120
- const list = otherUserSessions.map((x) => {
121
- return () => client.user.logout({ visitorId: x.visitorId });
122
- });
123
- await pAll(list, {
124
- concurrency: 3,
125
- stopOnError: false
149
+ await client.user.logout({
150
+ // @ts-expect-error js-sdk 发了新版后,会有这个类型定义
151
+ status: filterParams.status,
152
+ visitorId: currentVisitorId
126
153
  });
127
154
  pageState.refresh();
128
155
  confirmApi.close();
@@ -132,18 +159,29 @@ export default function UserSessions({
132
159
  const customButtons = [];
133
160
  if (showAction) {
134
161
  customButtons.push(
135
- /* @__PURE__ */ jsx(Tooltip, { title: t("logoutAllTips"), children: /* @__PURE__ */ jsx(
136
- Button,
162
+ /* @__PURE__ */ jsx(
163
+ Tooltip,
137
164
  {
138
- sx: { ml: 0.5 },
139
- size: "small",
140
- variant: "contained",
141
- color: "error",
142
- onClick: logoutAll,
143
- disabled: otherUserSessions.length === 0,
144
- children: t("logoutAll")
145
- }
146
- ) }, "logoutAll")
165
+ title: t("logoutAllTips", {
166
+ type: t(filterParams.status)
167
+ }),
168
+ children: /* @__PURE__ */ jsx(
169
+ Button,
170
+ {
171
+ sx: { ml: 0.5 },
172
+ size: "small",
173
+ variant: "contained",
174
+ color: "error",
175
+ onClick: logoutAll,
176
+ disabled: disableLogout,
177
+ children: t("logoutAll", {
178
+ type: t(filterParams.status)
179
+ })
180
+ }
181
+ )
182
+ },
183
+ "logoutAll"
184
+ )
147
185
  );
148
186
  }
149
187
  const tableOptions = useCreation(() => {
@@ -155,9 +193,12 @@ export default function UserSessions({
155
193
  filter: false,
156
194
  print: false,
157
195
  expandableRowsOnClick: false,
158
- searchDebounceTime: 600
196
+ searchDebounceTime: 600,
197
+ page: filterParams.page - 1,
198
+ rowsPerPage: filterParams.pageSize,
199
+ count: pageState.data?.total || 0
159
200
  };
160
- }, []);
201
+ }, [pageState.data?.total, filterParams.page, filterParams.pageSize]);
161
202
  const columns = [
162
203
  {
163
204
  label: t("platform"),
@@ -307,13 +348,60 @@ export default function UserSessions({
307
348
  /* @__PURE__ */ jsx(
308
349
  Datatable,
309
350
  {
351
+ count: pageState.data?.total || 0,
310
352
  locale,
311
353
  data: safeData,
312
354
  columns,
313
355
  customButtons,
314
356
  options: tableOptions,
315
357
  loading: pageState.loading,
316
- className: "pc-user-sessions-table"
358
+ className: "pc-user-sessions-table",
359
+ title: /* @__PURE__ */ jsxs(
360
+ RadioGroup,
361
+ {
362
+ row: true,
363
+ sx: { lineHeight: 1, ml: 1 },
364
+ onChange: (e) => {
365
+ filterParams.status = e.target.value;
366
+ },
367
+ children: [
368
+ /* @__PURE__ */ jsx(
369
+ FormControlLabel,
370
+ {
371
+ value: "online",
372
+ control: /* @__PURE__ */ jsx(Radio, { size: "small", sx: { lineHeight: 1, fontSize: 0 }, checked: filterParams.status === "online" }),
373
+ label: /* @__PURE__ */ jsxs(Typography, { sx: { display: "flex", alignItems: "center" }, children: [
374
+ t("online"),
375
+ /* @__PURE__ */ jsxs(Typography, { component: "span", sx: { ml: 0.5, color: "text.secondary" }, children: [
376
+ "(",
377
+ userSessionsCountMap.online,
378
+ ")"
379
+ ] })
380
+ ] })
381
+ }
382
+ ),
383
+ /* @__PURE__ */ jsx(
384
+ FormControlLabel,
385
+ {
386
+ value: "expired",
387
+ control: /* @__PURE__ */ jsx(Radio, { size: "small", sx: { lineHeight: 1, fontSize: 0 }, checked: filterParams.status === "expired" }),
388
+ label: /* @__PURE__ */ jsxs(Typography, { sx: { display: "flex", alignItems: "center" }, children: [
389
+ t("expired"),
390
+ /* @__PURE__ */ jsxs(Typography, { component: "span", sx: { ml: 0.5, color: "text.secondary" }, children: [
391
+ "(",
392
+ userSessionsCountMap.expired,
393
+ ")"
394
+ ] })
395
+ ] })
396
+ }
397
+ )
398
+ ]
399
+ }
400
+ ),
401
+ onChange: (state) => {
402
+ filterParams.page = state.page + 1;
403
+ filterParams.pageSize = state.rowsPerPage;
404
+ }
317
405
  }
318
406
  )
319
407
  ]
@@ -26,6 +26,7 @@ export declare const translations: {
26
26
  logoutThisSessionConfirm: string;
27
27
  logoutAllSession: string;
28
28
  logoutAllSessionConfirm: string;
29
+ online: string;
29
30
  };
30
31
  en: {
31
32
  confirm: string;
@@ -54,5 +55,6 @@ export declare const translations: {
54
55
  logoutThisSessionConfirm: string;
55
56
  logoutAllSession: string;
56
57
  logoutAllSessionConfirm: string;
58
+ online: string;
57
59
  };
58
60
  };
@@ -15,7 +15,7 @@ export const translations = {
15
15
  updatedAt: "\u6700\u8FD1\u6D3B\u52A8\u65F6\u95F4",
16
16
  lastLoginIp: "\u6700\u8FD1\u6D3B\u52A8\u5730\u5740",
17
17
  actions: "\u64CD\u4F5C",
18
- logoutAll: "\u6CE8\u9500\u6240\u6709\u4F1A\u8BDD",
18
+ logoutAll: "\u6CE8\u9500\u6240\u6709{type}\u4F1A\u8BDD",
19
19
  logoutAllTips: "\u4E0D\u4F1A\u6CE8\u9500\u5F53\u524D\u4F1A\u8BDD",
20
20
  logout: "\u6CE8\u9500",
21
21
  currentSession: "\u5F53\u524D\u4F1A\u8BDD",
@@ -24,8 +24,9 @@ export const translations = {
24
24
  remove: "\u5220\u9664",
25
25
  logoutThisSession: "\u6CE8\u9500\u6307\u5B9A\u4F1A\u8BDD",
26
26
  logoutThisSessionConfirm: "\u786E\u5B9A\u8981\u6CE8\u9500\u6B64\u4F1A\u8BDD\u5417?",
27
- logoutAllSession: "\u6CE8\u9500\u6240\u6709\u4F1A\u8BDD",
28
- logoutAllSessionConfirm: "\u786E\u5B9A\u8981\u6CE8\u9500\u6240\u6709\u4F1A\u8BDD\u5417?"
27
+ logoutAllSession: "\u6CE8\u9500\u6240\u6709{type}\u4F1A\u8BDD",
28
+ logoutAllSessionConfirm: "\u786E\u5B9A\u8981\u6CE8\u9500\u6240\u6709{type}\u4F1A\u8BDD\u5417?",
29
+ online: "\u6D3B\u8DC3"
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/ui-react",
3
- "version": "2.12.70",
3
+ "version": "2.12.72",
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.70",
38
- "@arcblock/react-hooks": "^2.12.70",
39
- "@arcblock/ws": "^1.19.19",
37
+ "@arcblock/bridge": "^2.12.72",
38
+ "@arcblock/react-hooks": "^2.12.72",
39
+ "@arcblock/ws": "^1.19.20",
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": "89e31e2599d8ab7462307771239849a6edd19d7f"
97
+ "gitHead": "9050a7670edd33bb1edd1014770a00ddcbc2bd03"
98
98
  }
@@ -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'),
@@ -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
  };