@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.
- package/lib/UserCenter/components/settings.js +11 -1
- 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/package.json +6 -6
- package/src/UserCenter/components/settings.tsx +14 -1
- package/src/UserSessions/components/user-sessions.tsx +130 -43
- package/src/UserSessions/libs/locales.ts +8 -6
|
@@ -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(
|
|
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 {
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
136
|
-
|
|
162
|
+
/* @__PURE__ */ jsx(
|
|
163
|
+
Tooltip,
|
|
137
164
|
{
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
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.
|
|
38
|
-
"@arcblock/react-hooks": "^2.12.
|
|
39
|
-
"@arcblock/ws": "^1.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.
|
|
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": "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:
|
|
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 {
|
|
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
|
};
|