@strapi-community/plugin-io 5.2.0 → 5.3.0
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/dist/_chunks/{LivePresencePanel-2U3I3yL0.js → LivePresencePanel-BkeWL4kq.js} +1 -1
- package/dist/_chunks/{LivePresencePanel-CIFG_05s.mjs → LivePresencePanel-D_vzQr4B.mjs} +1 -1
- package/dist/_chunks/{MonitoringPage-9f4Gzd2X.js → MonitoringPage-CYGqkzva.js} +1 -1
- package/dist/_chunks/{MonitoringPage-Bbkoh6ih.mjs → MonitoringPage-DKfhYUgU.mjs} +1 -1
- package/dist/_chunks/OnlineEditorsWidget-Bf8hfVha.js +341 -0
- package/dist/_chunks/OnlineEditorsWidget-RcYLxQke.mjs +339 -0
- package/dist/_chunks/{SettingsPage-CsRazf0j.js → SettingsPage-0k9qPAJZ.js} +1 -1
- package/dist/_chunks/{SettingsPage-Btz_5MuC.mjs → SettingsPage-Qi0iMaWc.mjs} +1 -1
- package/dist/_chunks/{index-Dof_eA3e.mjs → index-Bw7WjN5H.mjs} +17 -4
- package/dist/_chunks/{index-BEZDDgvZ.js → index-DVNfszio.js} +17 -4
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +136 -7
- package/dist/server/index.mjs +136 -7
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ const designSystem = require("@strapi/design-system");
|
|
|
7
7
|
const admin = require("@strapi/strapi/admin");
|
|
8
8
|
const styled = require("styled-components");
|
|
9
9
|
const socket_ioClient = require("socket.io-client");
|
|
10
|
-
const index$1 = require("./index-
|
|
10
|
+
const index$1 = require("./index-DVNfszio.js");
|
|
11
11
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
12
12
|
const styled__default = /* @__PURE__ */ _interopDefault(styled);
|
|
13
13
|
const pulse = styled.keyframes`
|
|
@@ -5,7 +5,7 @@ import { Flex } from "@strapi/design-system";
|
|
|
5
5
|
import { useFetchClient } from "@strapi/strapi/admin";
|
|
6
6
|
import styled, { css, keyframes } from "styled-components";
|
|
7
7
|
import { io } from "socket.io-client";
|
|
8
|
-
import { P as PLUGIN_ID } from "./index-
|
|
8
|
+
import { P as PLUGIN_ID } from "./index-Bw7WjN5H.mjs";
|
|
9
9
|
const pulse = keyframes`
|
|
10
10
|
0%, 100% {
|
|
11
11
|
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2);
|
|
@@ -6,7 +6,7 @@ const admin = require("@strapi/strapi/admin");
|
|
|
6
6
|
const styled = require("styled-components");
|
|
7
7
|
const designSystem = require("@strapi/design-system");
|
|
8
8
|
const index = require("./index-DkTxsEqL.js");
|
|
9
|
-
const index$1 = require("./index-
|
|
9
|
+
const index$1 = require("./index-DVNfszio.js");
|
|
10
10
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
11
11
|
const styled__default = /* @__PURE__ */ _interopDefault(styled);
|
|
12
12
|
const UsersIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" }) });
|
|
@@ -4,7 +4,7 @@ import { useFetchClient, useNotification } from "@strapi/strapi/admin";
|
|
|
4
4
|
import styled, { css, keyframes } from "styled-components";
|
|
5
5
|
import { Box, Loader, Flex, Field, TextInput, Badge, Typography } from "@strapi/design-system";
|
|
6
6
|
import { u as useIntl } from "./index-CEh8vkxY.mjs";
|
|
7
|
-
import { P as PLUGIN_ID } from "./index-
|
|
7
|
+
import { P as PLUGIN_ID } from "./index-Bw7WjN5H.mjs";
|
|
8
8
|
const UsersIcon = () => /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" }) });
|
|
9
9
|
const BoltIcon = () => /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" }) });
|
|
10
10
|
const ChartBarIcon = () => /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", strokeWidth: 1.5, stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" }) });
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
+
const React = require("react");
|
|
5
|
+
const designSystem = require("@strapi/design-system");
|
|
6
|
+
const icons = require("@strapi/icons");
|
|
7
|
+
const admin = require("@strapi/strapi/admin");
|
|
8
|
+
const styled = require("styled-components");
|
|
9
|
+
const socket_ioClient = require("socket.io-client");
|
|
10
|
+
const index = require("./index-DVNfszio.js");
|
|
11
|
+
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
12
|
+
const styled__default = /* @__PURE__ */ _interopDefault(styled);
|
|
13
|
+
const pulse = styled.keyframes`
|
|
14
|
+
0%, 100% { opacity: 1; }
|
|
15
|
+
50% { opacity: 0.5; }
|
|
16
|
+
`;
|
|
17
|
+
const WidgetContainer = styled__default.default(designSystem.Box)`
|
|
18
|
+
padding: 0;
|
|
19
|
+
position: relative;
|
|
20
|
+
`;
|
|
21
|
+
const HeaderContainer = styled__default.default(designSystem.Flex)`
|
|
22
|
+
justify-content: space-between;
|
|
23
|
+
align-items: center;
|
|
24
|
+
margin-bottom: ${({ theme }) => theme.spaces[3]};
|
|
25
|
+
padding-bottom: ${({ theme }) => theme.spaces[2]};
|
|
26
|
+
border-bottom: 1px solid ${({ theme }) => theme.colors.neutral150};
|
|
27
|
+
`;
|
|
28
|
+
const LiveDot = styled__default.default.span`
|
|
29
|
+
display: inline-block;
|
|
30
|
+
width: 8px;
|
|
31
|
+
height: 8px;
|
|
32
|
+
border-radius: 50%;
|
|
33
|
+
background: ${({ theme, $connected }) => $connected ? theme.colors.success500 : theme.colors.neutral400};
|
|
34
|
+
margin-right: ${({ theme }) => theme.spaces[2]};
|
|
35
|
+
animation: ${({ $connected }) => $connected ? pulse : "none"} 2s ease-in-out infinite;
|
|
36
|
+
`;
|
|
37
|
+
const CountBadge = styled__default.default.span`
|
|
38
|
+
display: inline-flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
min-width: 24px;
|
|
42
|
+
height: 24px;
|
|
43
|
+
padding: 0 8px;
|
|
44
|
+
background: ${({ theme, $active }) => $active ? theme.colors.primary100 : theme.colors.neutral100};
|
|
45
|
+
color: ${({ theme, $active }) => $active ? theme.colors.primary700 : theme.colors.neutral600};
|
|
46
|
+
border-radius: 12px;
|
|
47
|
+
font-size: 12px;
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
`;
|
|
50
|
+
const UserList = styled__default.default.div`
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
gap: ${({ theme }) => theme.spaces[2]};
|
|
54
|
+
max-height: 280px;
|
|
55
|
+
overflow-y: auto;
|
|
56
|
+
`;
|
|
57
|
+
const UserCard = styled__default.default.div`
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: flex-start;
|
|
60
|
+
gap: ${({ theme }) => theme.spaces[3]};
|
|
61
|
+
padding: ${({ theme }) => theme.spaces[3]};
|
|
62
|
+
background: ${({ theme }) => theme.colors.neutral0};
|
|
63
|
+
border: 1px solid ${({ theme }) => theme.colors.neutral150};
|
|
64
|
+
border-radius: ${({ theme }) => theme.borderRadius};
|
|
65
|
+
transition: all 0.2s ease;
|
|
66
|
+
|
|
67
|
+
&:hover {
|
|
68
|
+
border-color: ${({ theme }) => theme.colors.primary200};
|
|
69
|
+
box-shadow: 0 2px 8px rgba(73, 69, 255, 0.08);
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
const AVATAR_COLORS = [
|
|
73
|
+
"linear-gradient(135deg, #4945ff 0%, #7b79ff 100%)",
|
|
74
|
+
"linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)",
|
|
75
|
+
"linear-gradient(135deg, #10b981 0%, #34d399 100%)",
|
|
76
|
+
"linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)",
|
|
77
|
+
"linear-gradient(135deg, #ef4444 0%, #f87171 100%)",
|
|
78
|
+
"linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%)"
|
|
79
|
+
];
|
|
80
|
+
const UserAvatar = styled__default.default.div`
|
|
81
|
+
width: 40px;
|
|
82
|
+
height: 40px;
|
|
83
|
+
border-radius: 50%;
|
|
84
|
+
background: ${({ $colorIndex }) => AVATAR_COLORS[$colorIndex % AVATAR_COLORS.length]};
|
|
85
|
+
color: white;
|
|
86
|
+
font-size: 14px;
|
|
87
|
+
font-weight: 700;
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
justify-content: center;
|
|
91
|
+
flex-shrink: 0;
|
|
92
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
93
|
+
`;
|
|
94
|
+
const UserInfo = styled__default.default.div`
|
|
95
|
+
flex: 1;
|
|
96
|
+
min-width: 0;
|
|
97
|
+
`;
|
|
98
|
+
const UserName = styled__default.default.div`
|
|
99
|
+
font-size: 14px;
|
|
100
|
+
font-weight: 600;
|
|
101
|
+
color: ${({ theme }) => theme.colors.neutral800};
|
|
102
|
+
white-space: nowrap;
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
text-overflow: ellipsis;
|
|
105
|
+
`;
|
|
106
|
+
const UserMeta = styled__default.default.div`
|
|
107
|
+
font-size: 12px;
|
|
108
|
+
color: ${({ theme }) => theme.colors.neutral500};
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
gap: ${({ theme }) => theme.spaces[2]};
|
|
112
|
+
margin-top: 2px;
|
|
113
|
+
`;
|
|
114
|
+
const EditingBadge = styled__default.default.a`
|
|
115
|
+
display: inline-flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
gap: 4px;
|
|
118
|
+
font-size: 11px;
|
|
119
|
+
font-weight: 500;
|
|
120
|
+
color: ${({ theme }) => theme.colors.success700};
|
|
121
|
+
background: ${({ theme }) => theme.colors.success100};
|
|
122
|
+
padding: 4px 10px;
|
|
123
|
+
border-radius: 10px;
|
|
124
|
+
margin-top: ${({ theme }) => theme.spaces[1]};
|
|
125
|
+
word-break: break-all;
|
|
126
|
+
max-width: 100%;
|
|
127
|
+
text-decoration: none;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
transition: all 0.15s ease;
|
|
130
|
+
|
|
131
|
+
&:hover {
|
|
132
|
+
background: ${({ theme }) => theme.colors.success200};
|
|
133
|
+
color: ${({ theme }) => theme.colors.success800};
|
|
134
|
+
transform: translateY(-1px);
|
|
135
|
+
}
|
|
136
|
+
`;
|
|
137
|
+
const IdleBadge = styled__default.default.span`
|
|
138
|
+
display: inline-flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
gap: 4px;
|
|
141
|
+
font-size: 11px;
|
|
142
|
+
font-weight: 500;
|
|
143
|
+
color: ${({ theme }) => theme.colors.neutral600};
|
|
144
|
+
background: ${({ theme }) => theme.colors.neutral100};
|
|
145
|
+
padding: 2px 8px;
|
|
146
|
+
border-radius: 10px;
|
|
147
|
+
margin-top: ${({ theme }) => theme.spaces[1]};
|
|
148
|
+
`;
|
|
149
|
+
const EmptyState = styled__default.default.div`
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: center;
|
|
154
|
+
text-align: center;
|
|
155
|
+
padding: ${({ theme }) => theme.spaces[8]} ${({ theme }) => theme.spaces[4]};
|
|
156
|
+
color: ${({ theme }) => theme.colors.neutral500};
|
|
157
|
+
min-height: 180px;
|
|
158
|
+
`;
|
|
159
|
+
const EmptyIcon = styled__default.default.div`
|
|
160
|
+
width: 64px;
|
|
161
|
+
height: 64px;
|
|
162
|
+
border-radius: 50%;
|
|
163
|
+
background: ${({ theme }) => theme.colors.neutral100};
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
justify-content: center;
|
|
167
|
+
margin-bottom: ${({ theme }) => theme.spaces[3]};
|
|
168
|
+
`;
|
|
169
|
+
const LoadingContainer = styled__default.default.div`
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
justify-content: center;
|
|
173
|
+
padding: ${({ theme }) => theme.spaces[6]};
|
|
174
|
+
`;
|
|
175
|
+
const FooterLink = styled__default.default.a`
|
|
176
|
+
font-size: 12px;
|
|
177
|
+
color: ${({ theme }) => theme.colors.primary600};
|
|
178
|
+
text-decoration: none;
|
|
179
|
+
|
|
180
|
+
&:hover {
|
|
181
|
+
text-decoration: underline;
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
184
|
+
const getInitials = (user) => {
|
|
185
|
+
const first = (user.firstname?.[0] || user.username?.[0] || user.email?.[0] || "?").toUpperCase();
|
|
186
|
+
const last = (user.lastname?.[0] || "").toUpperCase();
|
|
187
|
+
return `${first}${last}`.trim() || "?";
|
|
188
|
+
};
|
|
189
|
+
const getDisplayName = (user) => {
|
|
190
|
+
if (user.firstname) {
|
|
191
|
+
return `${user.firstname} ${user.lastname || ""}`.trim();
|
|
192
|
+
}
|
|
193
|
+
return user.username || user.email || "Unknown";
|
|
194
|
+
};
|
|
195
|
+
const formatDuration = (seconds) => {
|
|
196
|
+
if (seconds < 60) return "just now";
|
|
197
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
198
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
|
199
|
+
return `${Math.floor(seconds / 86400)}d`;
|
|
200
|
+
};
|
|
201
|
+
const OnlineEditorsWidget = () => {
|
|
202
|
+
const { get, post } = admin.useFetchClient();
|
|
203
|
+
const [data, setData] = React.useState(null);
|
|
204
|
+
const [loading, setLoading] = React.useState(true);
|
|
205
|
+
const [error, setError] = React.useState(null);
|
|
206
|
+
const [connected, setConnected] = React.useState(false);
|
|
207
|
+
const socketRef = React.useRef(null);
|
|
208
|
+
const fetchOnlineUsers = React.useCallback(async () => {
|
|
209
|
+
try {
|
|
210
|
+
const response = await get(`/${index.PLUGIN_ID}/online-users`);
|
|
211
|
+
setData(response.data?.data || response.data);
|
|
212
|
+
setError(null);
|
|
213
|
+
setLoading(false);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error("[plugin-io] Failed to fetch online users:", err);
|
|
216
|
+
setError(err.message);
|
|
217
|
+
setLoading(false);
|
|
218
|
+
}
|
|
219
|
+
}, [get]);
|
|
220
|
+
React.useEffect(() => {
|
|
221
|
+
let cancelled = false;
|
|
222
|
+
let socket = null;
|
|
223
|
+
const connectSocket = async () => {
|
|
224
|
+
try {
|
|
225
|
+
const { data: sessionData } = await post(`/${index.PLUGIN_ID}/presence/session`, {});
|
|
226
|
+
if (cancelled || !sessionData?.token) return;
|
|
227
|
+
const socketUrl = sessionData.wsUrl || `${window.location.protocol}//${window.location.host}`;
|
|
228
|
+
socket = socket_ioClient.io(socketUrl, {
|
|
229
|
+
path: sessionData.wsPath || "/socket.io",
|
|
230
|
+
transports: ["websocket", "polling"],
|
|
231
|
+
auth: {
|
|
232
|
+
token: sessionData.token,
|
|
233
|
+
strategy: "admin-jwt",
|
|
234
|
+
isAdmin: true
|
|
235
|
+
},
|
|
236
|
+
reconnection: true,
|
|
237
|
+
reconnectionAttempts: 3
|
|
238
|
+
});
|
|
239
|
+
socketRef.current = socket;
|
|
240
|
+
socket.on("connect", () => {
|
|
241
|
+
if (!cancelled) {
|
|
242
|
+
setConnected(true);
|
|
243
|
+
console.log(`[${index.PLUGIN_ID}] Dashboard presence connected`);
|
|
244
|
+
fetchOnlineUsers();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
socket.on("disconnect", () => {
|
|
248
|
+
if (!cancelled) {
|
|
249
|
+
setConnected(false);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
socket.on("connect_error", (err) => {
|
|
253
|
+
console.warn(`[${index.PLUGIN_ID}] Dashboard socket error:`, err.message);
|
|
254
|
+
});
|
|
255
|
+
socket.on("presence:update", () => {
|
|
256
|
+
fetchOnlineUsers();
|
|
257
|
+
});
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.error("[plugin-io] Failed to connect dashboard socket:", err);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
connectSocket();
|
|
263
|
+
return () => {
|
|
264
|
+
cancelled = true;
|
|
265
|
+
if (socket) {
|
|
266
|
+
socket.disconnect();
|
|
267
|
+
socketRef.current = null;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}, [post, fetchOnlineUsers]);
|
|
271
|
+
React.useEffect(() => {
|
|
272
|
+
const interval = setInterval(fetchOnlineUsers, 15e3);
|
|
273
|
+
return () => clearInterval(interval);
|
|
274
|
+
}, [fetchOnlineUsers]);
|
|
275
|
+
if (loading) {
|
|
276
|
+
return /* @__PURE__ */ jsxRuntime.jsx(LoadingContainer, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: "Loading..." }) });
|
|
277
|
+
}
|
|
278
|
+
if (error) {
|
|
279
|
+
return /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
|
|
280
|
+
"Failed to load: ",
|
|
281
|
+
error
|
|
282
|
+
] }) });
|
|
283
|
+
}
|
|
284
|
+
const users = data?.users || [];
|
|
285
|
+
const counts = data?.counts || { total: 0, editing: 0 };
|
|
286
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(WidgetContainer, { children: [
|
|
287
|
+
/* @__PURE__ */ jsxRuntime.jsxs(HeaderContainer, { children: [
|
|
288
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
|
|
289
|
+
/* @__PURE__ */ jsxRuntime.jsx(LiveDot, { $connected: connected }),
|
|
290
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "bold", textColor: "neutral800", children: "Who's Online" })
|
|
291
|
+
] }),
|
|
292
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
|
|
293
|
+
/* @__PURE__ */ jsxRuntime.jsxs(CountBadge, { $active: counts.editing > 0, title: "Users editing", children: [
|
|
294
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.Pencil, { width: "12", height: "12", style: { marginRight: 4 } }),
|
|
295
|
+
counts.editing
|
|
296
|
+
] }),
|
|
297
|
+
/* @__PURE__ */ jsxRuntime.jsxs(CountBadge, { title: "Total online", children: [
|
|
298
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.User, { width: "12", height: "12", style: { marginRight: 4 } }),
|
|
299
|
+
counts.total
|
|
300
|
+
] })
|
|
301
|
+
] })
|
|
302
|
+
] }),
|
|
303
|
+
users.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(EmptyState, { children: [
|
|
304
|
+
/* @__PURE__ */ jsxRuntime.jsx(EmptyIcon, { children: /* @__PURE__ */ jsxRuntime.jsx(icons.User, { width: "28", height: "28", fill: "#a5a5ba" }) }),
|
|
305
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", textColor: "neutral600", children: "No one else is online" }),
|
|
306
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", style: { marginTop: 4 }, children: "You're the only one here right now" })
|
|
307
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(UserList, { children: users.map((userData, index2) => /* @__PURE__ */ jsxRuntime.jsxs(UserCard, { children: [
|
|
308
|
+
/* @__PURE__ */ jsxRuntime.jsx(UserAvatar, { $colorIndex: index2, children: getInitials(userData.user) }),
|
|
309
|
+
/* @__PURE__ */ jsxRuntime.jsxs(UserInfo, { children: [
|
|
310
|
+
/* @__PURE__ */ jsxRuntime.jsxs(UserName, { children: [
|
|
311
|
+
getDisplayName(userData.user),
|
|
312
|
+
userData.user.isAdmin && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { size: "S", style: { marginLeft: 8 }, children: "Admin" })
|
|
313
|
+
] }),
|
|
314
|
+
/* @__PURE__ */ jsxRuntime.jsxs(UserMeta, { children: [
|
|
315
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.Clock, { width: "12", height: "12" }),
|
|
316
|
+
"Online ",
|
|
317
|
+
formatDuration(userData.onlineFor)
|
|
318
|
+
] }),
|
|
319
|
+
userData.isEditing ? userData.editingEntities.map((entity, idx) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
320
|
+
EditingBadge,
|
|
321
|
+
{
|
|
322
|
+
href: `/admin/content-manager/collection-types/${entity.uid}/${entity.documentId}`,
|
|
323
|
+
target: "_blank",
|
|
324
|
+
rel: "noopener noreferrer",
|
|
325
|
+
title: "Open in new tab",
|
|
326
|
+
children: [
|
|
327
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.Pencil, { width: "10", height: "10" }),
|
|
328
|
+
entity.contentTypeName,
|
|
329
|
+
" - ",
|
|
330
|
+
entity.documentId
|
|
331
|
+
]
|
|
332
|
+
},
|
|
333
|
+
idx
|
|
334
|
+
)) : /* @__PURE__ */ jsxRuntime.jsx(IdleBadge, { children: "Idle" })
|
|
335
|
+
] })
|
|
336
|
+
] }, userData.socketId)) }),
|
|
337
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", marginTop: 3, children: /* @__PURE__ */ jsxRuntime.jsx(FooterLink, { href: "/admin/settings/io/monitoring", children: "View All Activity" }) })
|
|
338
|
+
] });
|
|
339
|
+
};
|
|
340
|
+
exports.OnlineEditorsWidget = OnlineEditorsWidget;
|
|
341
|
+
exports.default = OnlineEditorsWidget;
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useCallback, useEffect } from "react";
|
|
3
|
+
import { Typography, Flex, Badge, Box } from "@strapi/design-system";
|
|
4
|
+
import { Pencil, User, Clock } from "@strapi/icons";
|
|
5
|
+
import { useFetchClient } from "@strapi/strapi/admin";
|
|
6
|
+
import styled, { keyframes } from "styled-components";
|
|
7
|
+
import { io } from "socket.io-client";
|
|
8
|
+
import { P as PLUGIN_ID } from "./index-Bw7WjN5H.mjs";
|
|
9
|
+
const pulse = keyframes`
|
|
10
|
+
0%, 100% { opacity: 1; }
|
|
11
|
+
50% { opacity: 0.5; }
|
|
12
|
+
`;
|
|
13
|
+
const WidgetContainer = styled(Box)`
|
|
14
|
+
padding: 0;
|
|
15
|
+
position: relative;
|
|
16
|
+
`;
|
|
17
|
+
const HeaderContainer = styled(Flex)`
|
|
18
|
+
justify-content: space-between;
|
|
19
|
+
align-items: center;
|
|
20
|
+
margin-bottom: ${({ theme }) => theme.spaces[3]};
|
|
21
|
+
padding-bottom: ${({ theme }) => theme.spaces[2]};
|
|
22
|
+
border-bottom: 1px solid ${({ theme }) => theme.colors.neutral150};
|
|
23
|
+
`;
|
|
24
|
+
const LiveDot = styled.span`
|
|
25
|
+
display: inline-block;
|
|
26
|
+
width: 8px;
|
|
27
|
+
height: 8px;
|
|
28
|
+
border-radius: 50%;
|
|
29
|
+
background: ${({ theme, $connected }) => $connected ? theme.colors.success500 : theme.colors.neutral400};
|
|
30
|
+
margin-right: ${({ theme }) => theme.spaces[2]};
|
|
31
|
+
animation: ${({ $connected }) => $connected ? pulse : "none"} 2s ease-in-out infinite;
|
|
32
|
+
`;
|
|
33
|
+
const CountBadge = styled.span`
|
|
34
|
+
display: inline-flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
min-width: 24px;
|
|
38
|
+
height: 24px;
|
|
39
|
+
padding: 0 8px;
|
|
40
|
+
background: ${({ theme, $active }) => $active ? theme.colors.primary100 : theme.colors.neutral100};
|
|
41
|
+
color: ${({ theme, $active }) => $active ? theme.colors.primary700 : theme.colors.neutral600};
|
|
42
|
+
border-radius: 12px;
|
|
43
|
+
font-size: 12px;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
`;
|
|
46
|
+
const UserList = styled.div`
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
gap: ${({ theme }) => theme.spaces[2]};
|
|
50
|
+
max-height: 280px;
|
|
51
|
+
overflow-y: auto;
|
|
52
|
+
`;
|
|
53
|
+
const UserCard = styled.div`
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: flex-start;
|
|
56
|
+
gap: ${({ theme }) => theme.spaces[3]};
|
|
57
|
+
padding: ${({ theme }) => theme.spaces[3]};
|
|
58
|
+
background: ${({ theme }) => theme.colors.neutral0};
|
|
59
|
+
border: 1px solid ${({ theme }) => theme.colors.neutral150};
|
|
60
|
+
border-radius: ${({ theme }) => theme.borderRadius};
|
|
61
|
+
transition: all 0.2s ease;
|
|
62
|
+
|
|
63
|
+
&:hover {
|
|
64
|
+
border-color: ${({ theme }) => theme.colors.primary200};
|
|
65
|
+
box-shadow: 0 2px 8px rgba(73, 69, 255, 0.08);
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
const AVATAR_COLORS = [
|
|
69
|
+
"linear-gradient(135deg, #4945ff 0%, #7b79ff 100%)",
|
|
70
|
+
"linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)",
|
|
71
|
+
"linear-gradient(135deg, #10b981 0%, #34d399 100%)",
|
|
72
|
+
"linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)",
|
|
73
|
+
"linear-gradient(135deg, #ef4444 0%, #f87171 100%)",
|
|
74
|
+
"linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%)"
|
|
75
|
+
];
|
|
76
|
+
const UserAvatar = styled.div`
|
|
77
|
+
width: 40px;
|
|
78
|
+
height: 40px;
|
|
79
|
+
border-radius: 50%;
|
|
80
|
+
background: ${({ $colorIndex }) => AVATAR_COLORS[$colorIndex % AVATAR_COLORS.length]};
|
|
81
|
+
color: white;
|
|
82
|
+
font-size: 14px;
|
|
83
|
+
font-weight: 700;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
flex-shrink: 0;
|
|
88
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
89
|
+
`;
|
|
90
|
+
const UserInfo = styled.div`
|
|
91
|
+
flex: 1;
|
|
92
|
+
min-width: 0;
|
|
93
|
+
`;
|
|
94
|
+
const UserName = styled.div`
|
|
95
|
+
font-size: 14px;
|
|
96
|
+
font-weight: 600;
|
|
97
|
+
color: ${({ theme }) => theme.colors.neutral800};
|
|
98
|
+
white-space: nowrap;
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
text-overflow: ellipsis;
|
|
101
|
+
`;
|
|
102
|
+
const UserMeta = styled.div`
|
|
103
|
+
font-size: 12px;
|
|
104
|
+
color: ${({ theme }) => theme.colors.neutral500};
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
gap: ${({ theme }) => theme.spaces[2]};
|
|
108
|
+
margin-top: 2px;
|
|
109
|
+
`;
|
|
110
|
+
const EditingBadge = styled.a`
|
|
111
|
+
display: inline-flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 4px;
|
|
114
|
+
font-size: 11px;
|
|
115
|
+
font-weight: 500;
|
|
116
|
+
color: ${({ theme }) => theme.colors.success700};
|
|
117
|
+
background: ${({ theme }) => theme.colors.success100};
|
|
118
|
+
padding: 4px 10px;
|
|
119
|
+
border-radius: 10px;
|
|
120
|
+
margin-top: ${({ theme }) => theme.spaces[1]};
|
|
121
|
+
word-break: break-all;
|
|
122
|
+
max-width: 100%;
|
|
123
|
+
text-decoration: none;
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
transition: all 0.15s ease;
|
|
126
|
+
|
|
127
|
+
&:hover {
|
|
128
|
+
background: ${({ theme }) => theme.colors.success200};
|
|
129
|
+
color: ${({ theme }) => theme.colors.success800};
|
|
130
|
+
transform: translateY(-1px);
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
const IdleBadge = styled.span`
|
|
134
|
+
display: inline-flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 4px;
|
|
137
|
+
font-size: 11px;
|
|
138
|
+
font-weight: 500;
|
|
139
|
+
color: ${({ theme }) => theme.colors.neutral600};
|
|
140
|
+
background: ${({ theme }) => theme.colors.neutral100};
|
|
141
|
+
padding: 2px 8px;
|
|
142
|
+
border-radius: 10px;
|
|
143
|
+
margin-top: ${({ theme }) => theme.spaces[1]};
|
|
144
|
+
`;
|
|
145
|
+
const EmptyState = styled.div`
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
align-items: center;
|
|
149
|
+
justify-content: center;
|
|
150
|
+
text-align: center;
|
|
151
|
+
padding: ${({ theme }) => theme.spaces[8]} ${({ theme }) => theme.spaces[4]};
|
|
152
|
+
color: ${({ theme }) => theme.colors.neutral500};
|
|
153
|
+
min-height: 180px;
|
|
154
|
+
`;
|
|
155
|
+
const EmptyIcon = styled.div`
|
|
156
|
+
width: 64px;
|
|
157
|
+
height: 64px;
|
|
158
|
+
border-radius: 50%;
|
|
159
|
+
background: ${({ theme }) => theme.colors.neutral100};
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: center;
|
|
163
|
+
margin-bottom: ${({ theme }) => theme.spaces[3]};
|
|
164
|
+
`;
|
|
165
|
+
const LoadingContainer = styled.div`
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
padding: ${({ theme }) => theme.spaces[6]};
|
|
170
|
+
`;
|
|
171
|
+
const FooterLink = styled.a`
|
|
172
|
+
font-size: 12px;
|
|
173
|
+
color: ${({ theme }) => theme.colors.primary600};
|
|
174
|
+
text-decoration: none;
|
|
175
|
+
|
|
176
|
+
&:hover {
|
|
177
|
+
text-decoration: underline;
|
|
178
|
+
}
|
|
179
|
+
`;
|
|
180
|
+
const getInitials = (user) => {
|
|
181
|
+
const first = (user.firstname?.[0] || user.username?.[0] || user.email?.[0] || "?").toUpperCase();
|
|
182
|
+
const last = (user.lastname?.[0] || "").toUpperCase();
|
|
183
|
+
return `${first}${last}`.trim() || "?";
|
|
184
|
+
};
|
|
185
|
+
const getDisplayName = (user) => {
|
|
186
|
+
if (user.firstname) {
|
|
187
|
+
return `${user.firstname} ${user.lastname || ""}`.trim();
|
|
188
|
+
}
|
|
189
|
+
return user.username || user.email || "Unknown";
|
|
190
|
+
};
|
|
191
|
+
const formatDuration = (seconds) => {
|
|
192
|
+
if (seconds < 60) return "just now";
|
|
193
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
194
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
|
195
|
+
return `${Math.floor(seconds / 86400)}d`;
|
|
196
|
+
};
|
|
197
|
+
const OnlineEditorsWidget = () => {
|
|
198
|
+
const { get, post } = useFetchClient();
|
|
199
|
+
const [data, setData] = useState(null);
|
|
200
|
+
const [loading, setLoading] = useState(true);
|
|
201
|
+
const [error, setError] = useState(null);
|
|
202
|
+
const [connected, setConnected] = useState(false);
|
|
203
|
+
const socketRef = useRef(null);
|
|
204
|
+
const fetchOnlineUsers = useCallback(async () => {
|
|
205
|
+
try {
|
|
206
|
+
const response = await get(`/${PLUGIN_ID}/online-users`);
|
|
207
|
+
setData(response.data?.data || response.data);
|
|
208
|
+
setError(null);
|
|
209
|
+
setLoading(false);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error("[plugin-io] Failed to fetch online users:", err);
|
|
212
|
+
setError(err.message);
|
|
213
|
+
setLoading(false);
|
|
214
|
+
}
|
|
215
|
+
}, [get]);
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
let cancelled = false;
|
|
218
|
+
let socket = null;
|
|
219
|
+
const connectSocket = async () => {
|
|
220
|
+
try {
|
|
221
|
+
const { data: sessionData } = await post(`/${PLUGIN_ID}/presence/session`, {});
|
|
222
|
+
if (cancelled || !sessionData?.token) return;
|
|
223
|
+
const socketUrl = sessionData.wsUrl || `${window.location.protocol}//${window.location.host}`;
|
|
224
|
+
socket = io(socketUrl, {
|
|
225
|
+
path: sessionData.wsPath || "/socket.io",
|
|
226
|
+
transports: ["websocket", "polling"],
|
|
227
|
+
auth: {
|
|
228
|
+
token: sessionData.token,
|
|
229
|
+
strategy: "admin-jwt",
|
|
230
|
+
isAdmin: true
|
|
231
|
+
},
|
|
232
|
+
reconnection: true,
|
|
233
|
+
reconnectionAttempts: 3
|
|
234
|
+
});
|
|
235
|
+
socketRef.current = socket;
|
|
236
|
+
socket.on("connect", () => {
|
|
237
|
+
if (!cancelled) {
|
|
238
|
+
setConnected(true);
|
|
239
|
+
console.log(`[${PLUGIN_ID}] Dashboard presence connected`);
|
|
240
|
+
fetchOnlineUsers();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
socket.on("disconnect", () => {
|
|
244
|
+
if (!cancelled) {
|
|
245
|
+
setConnected(false);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
socket.on("connect_error", (err) => {
|
|
249
|
+
console.warn(`[${PLUGIN_ID}] Dashboard socket error:`, err.message);
|
|
250
|
+
});
|
|
251
|
+
socket.on("presence:update", () => {
|
|
252
|
+
fetchOnlineUsers();
|
|
253
|
+
});
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error("[plugin-io] Failed to connect dashboard socket:", err);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
connectSocket();
|
|
259
|
+
return () => {
|
|
260
|
+
cancelled = true;
|
|
261
|
+
if (socket) {
|
|
262
|
+
socket.disconnect();
|
|
263
|
+
socketRef.current = null;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}, [post, fetchOnlineUsers]);
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
const interval = setInterval(fetchOnlineUsers, 15e3);
|
|
269
|
+
return () => clearInterval(interval);
|
|
270
|
+
}, [fetchOnlineUsers]);
|
|
271
|
+
if (loading) {
|
|
272
|
+
return /* @__PURE__ */ jsx(LoadingContainer, { children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Loading..." }) });
|
|
273
|
+
}
|
|
274
|
+
if (error) {
|
|
275
|
+
return /* @__PURE__ */ jsx(EmptyState, { children: /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", children: [
|
|
276
|
+
"Failed to load: ",
|
|
277
|
+
error
|
|
278
|
+
] }) });
|
|
279
|
+
}
|
|
280
|
+
const users = data?.users || [];
|
|
281
|
+
const counts = data?.counts || { total: 0, editing: 0 };
|
|
282
|
+
return /* @__PURE__ */ jsxs(WidgetContainer, { children: [
|
|
283
|
+
/* @__PURE__ */ jsxs(HeaderContainer, { children: [
|
|
284
|
+
/* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
|
|
285
|
+
/* @__PURE__ */ jsx(LiveDot, { $connected: connected }),
|
|
286
|
+
/* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "bold", textColor: "neutral800", children: "Who's Online" })
|
|
287
|
+
] }),
|
|
288
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
289
|
+
/* @__PURE__ */ jsxs(CountBadge, { $active: counts.editing > 0, title: "Users editing", children: [
|
|
290
|
+
/* @__PURE__ */ jsx(Pencil, { width: "12", height: "12", style: { marginRight: 4 } }),
|
|
291
|
+
counts.editing
|
|
292
|
+
] }),
|
|
293
|
+
/* @__PURE__ */ jsxs(CountBadge, { title: "Total online", children: [
|
|
294
|
+
/* @__PURE__ */ jsx(User, { width: "12", height: "12", style: { marginRight: 4 } }),
|
|
295
|
+
counts.total
|
|
296
|
+
] })
|
|
297
|
+
] })
|
|
298
|
+
] }),
|
|
299
|
+
users.length === 0 ? /* @__PURE__ */ jsxs(EmptyState, { children: [
|
|
300
|
+
/* @__PURE__ */ jsx(EmptyIcon, { children: /* @__PURE__ */ jsx(User, { width: "28", height: "28", fill: "#a5a5ba" }) }),
|
|
301
|
+
/* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", textColor: "neutral600", children: "No one else is online" }),
|
|
302
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", style: { marginTop: 4 }, children: "You're the only one here right now" })
|
|
303
|
+
] }) : /* @__PURE__ */ jsx(UserList, { children: users.map((userData, index) => /* @__PURE__ */ jsxs(UserCard, { children: [
|
|
304
|
+
/* @__PURE__ */ jsx(UserAvatar, { $colorIndex: index, children: getInitials(userData.user) }),
|
|
305
|
+
/* @__PURE__ */ jsxs(UserInfo, { children: [
|
|
306
|
+
/* @__PURE__ */ jsxs(UserName, { children: [
|
|
307
|
+
getDisplayName(userData.user),
|
|
308
|
+
userData.user.isAdmin && /* @__PURE__ */ jsx(Badge, { size: "S", style: { marginLeft: 8 }, children: "Admin" })
|
|
309
|
+
] }),
|
|
310
|
+
/* @__PURE__ */ jsxs(UserMeta, { children: [
|
|
311
|
+
/* @__PURE__ */ jsx(Clock, { width: "12", height: "12" }),
|
|
312
|
+
"Online ",
|
|
313
|
+
formatDuration(userData.onlineFor)
|
|
314
|
+
] }),
|
|
315
|
+
userData.isEditing ? userData.editingEntities.map((entity, idx) => /* @__PURE__ */ jsxs(
|
|
316
|
+
EditingBadge,
|
|
317
|
+
{
|
|
318
|
+
href: `/admin/content-manager/collection-types/${entity.uid}/${entity.documentId}`,
|
|
319
|
+
target: "_blank",
|
|
320
|
+
rel: "noopener noreferrer",
|
|
321
|
+
title: "Open in new tab",
|
|
322
|
+
children: [
|
|
323
|
+
/* @__PURE__ */ jsx(Pencil, { width: "10", height: "10" }),
|
|
324
|
+
entity.contentTypeName,
|
|
325
|
+
" - ",
|
|
326
|
+
entity.documentId
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
idx
|
|
330
|
+
)) : /* @__PURE__ */ jsx(IdleBadge, { children: "Idle" })
|
|
331
|
+
] })
|
|
332
|
+
] }, userData.socketId)) }),
|
|
333
|
+
/* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", marginTop: 3, children: /* @__PURE__ */ jsx(FooterLink, { href: "/admin/settings/io/monitoring", children: "View All Activity" }) })
|
|
334
|
+
] });
|
|
335
|
+
};
|
|
336
|
+
export {
|
|
337
|
+
OnlineEditorsWidget,
|
|
338
|
+
OnlineEditorsWidget as default
|
|
339
|
+
};
|
|
@@ -7,7 +7,7 @@ const icons = require("@strapi/icons");
|
|
|
7
7
|
const admin = require("@strapi/strapi/admin");
|
|
8
8
|
const index = require("./index-DkTxsEqL.js");
|
|
9
9
|
const styled = require("styled-components");
|
|
10
|
-
const index$1 = require("./index-
|
|
10
|
+
const index$1 = require("./index-DVNfszio.js");
|
|
11
11
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
12
12
|
const styled__default = /* @__PURE__ */ _interopDefault(styled);
|
|
13
13
|
const ResponsiveMain = styled__default.default(designSystem.Main)`
|
|
@@ -5,7 +5,7 @@ import { Download, Upload, Check } from "@strapi/icons";
|
|
|
5
5
|
import { useFetchClient, useNotification } from "@strapi/strapi/admin";
|
|
6
6
|
import { u as useIntl } from "./index-CEh8vkxY.mjs";
|
|
7
7
|
import styled from "styled-components";
|
|
8
|
-
import { P as PLUGIN_ID } from "./index-
|
|
8
|
+
import { P as PLUGIN_ID } from "./index-Bw7WjN5H.mjs";
|
|
9
9
|
const ResponsiveMain = styled(Main)`
|
|
10
10
|
& > div {
|
|
11
11
|
padding: 16px !important;
|
|
@@ -50,7 +50,7 @@ const index = {
|
|
|
50
50
|
},
|
|
51
51
|
id: `${PLUGIN_ID}-settings`,
|
|
52
52
|
to: `${PLUGIN_ID}/settings`,
|
|
53
|
-
Component: () => import("./SettingsPage-
|
|
53
|
+
Component: () => import("./SettingsPage-Qi0iMaWc.mjs").then((mod) => ({ default: mod.SettingsPage }))
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
intlLabel: {
|
|
@@ -59,7 +59,7 @@ const index = {
|
|
|
59
59
|
},
|
|
60
60
|
id: `${PLUGIN_ID}-monitoring`,
|
|
61
61
|
to: `${PLUGIN_ID}/monitoring`,
|
|
62
|
-
Component: () => import("./MonitoringPage-
|
|
62
|
+
Component: () => import("./MonitoringPage-DKfhYUgU.mjs").then((mod) => ({ default: mod.MonitoringPage }))
|
|
63
63
|
}
|
|
64
64
|
]
|
|
65
65
|
);
|
|
@@ -77,13 +77,26 @@ const index = {
|
|
|
77
77
|
id: "socket-io-stats-widget",
|
|
78
78
|
pluginId: PLUGIN_ID
|
|
79
79
|
});
|
|
80
|
-
|
|
80
|
+
app.widgets.register({
|
|
81
|
+
icon: PluginIcon,
|
|
82
|
+
title: {
|
|
83
|
+
id: `${PLUGIN_ID}.widget.online-editors.title`,
|
|
84
|
+
defaultMessage: "Who's Online"
|
|
85
|
+
},
|
|
86
|
+
component: async () => {
|
|
87
|
+
const component = await import("./OnlineEditorsWidget-RcYLxQke.mjs");
|
|
88
|
+
return component.OnlineEditorsWidget;
|
|
89
|
+
},
|
|
90
|
+
id: "socket-io-online-editors-widget",
|
|
91
|
+
pluginId: PLUGIN_ID
|
|
92
|
+
});
|
|
93
|
+
console.log(`[${PLUGIN_ID}] [SUCCESS] Dashboard widgets registered`);
|
|
81
94
|
}
|
|
82
95
|
},
|
|
83
96
|
async bootstrap(app) {
|
|
84
97
|
console.log(`[${PLUGIN_ID}] [INFO] Bootstrapping plugin...`);
|
|
85
98
|
try {
|
|
86
|
-
const { default: LivePresencePanel } = await import("./LivePresencePanel-
|
|
99
|
+
const { default: LivePresencePanel } = await import("./LivePresencePanel-D_vzQr4B.mjs");
|
|
87
100
|
const contentManagerPlugin = app.getPlugin("content-manager");
|
|
88
101
|
if (contentManagerPlugin && contentManagerPlugin.apis) {
|
|
89
102
|
contentManagerPlugin.apis.addEditViewSidePanel([LivePresencePanel]);
|
|
@@ -51,7 +51,7 @@ const index = {
|
|
|
51
51
|
},
|
|
52
52
|
id: `${PLUGIN_ID}-settings`,
|
|
53
53
|
to: `${PLUGIN_ID}/settings`,
|
|
54
|
-
Component: () => Promise.resolve().then(() => require("./SettingsPage-
|
|
54
|
+
Component: () => Promise.resolve().then(() => require("./SettingsPage-0k9qPAJZ.js")).then((mod) => ({ default: mod.SettingsPage }))
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
intlLabel: {
|
|
@@ -60,7 +60,7 @@ const index = {
|
|
|
60
60
|
},
|
|
61
61
|
id: `${PLUGIN_ID}-monitoring`,
|
|
62
62
|
to: `${PLUGIN_ID}/monitoring`,
|
|
63
|
-
Component: () => Promise.resolve().then(() => require("./MonitoringPage-
|
|
63
|
+
Component: () => Promise.resolve().then(() => require("./MonitoringPage-CYGqkzva.js")).then((mod) => ({ default: mod.MonitoringPage }))
|
|
64
64
|
}
|
|
65
65
|
]
|
|
66
66
|
);
|
|
@@ -78,13 +78,26 @@ const index = {
|
|
|
78
78
|
id: "socket-io-stats-widget",
|
|
79
79
|
pluginId: PLUGIN_ID
|
|
80
80
|
});
|
|
81
|
-
|
|
81
|
+
app.widgets.register({
|
|
82
|
+
icon: PluginIcon,
|
|
83
|
+
title: {
|
|
84
|
+
id: `${PLUGIN_ID}.widget.online-editors.title`,
|
|
85
|
+
defaultMessage: "Who's Online"
|
|
86
|
+
},
|
|
87
|
+
component: async () => {
|
|
88
|
+
const component = await Promise.resolve().then(() => require("./OnlineEditorsWidget-Bf8hfVha.js"));
|
|
89
|
+
return component.OnlineEditorsWidget;
|
|
90
|
+
},
|
|
91
|
+
id: "socket-io-online-editors-widget",
|
|
92
|
+
pluginId: PLUGIN_ID
|
|
93
|
+
});
|
|
94
|
+
console.log(`[${PLUGIN_ID}] [SUCCESS] Dashboard widgets registered`);
|
|
82
95
|
}
|
|
83
96
|
},
|
|
84
97
|
async bootstrap(app) {
|
|
85
98
|
console.log(`[${PLUGIN_ID}] [INFO] Bootstrapping plugin...`);
|
|
86
99
|
try {
|
|
87
|
-
const { default: LivePresencePanel } = await Promise.resolve().then(() => require("./LivePresencePanel-
|
|
100
|
+
const { default: LivePresencePanel } = await Promise.resolve().then(() => require("./LivePresencePanel-BkeWL4kq.js"));
|
|
88
101
|
const contentManagerPlugin = app.getPlugin("content-manager");
|
|
89
102
|
if (contentManagerPlugin && contentManagerPlugin.apis) {
|
|
90
103
|
contentManagerPlugin.apis.addEditViewSidePanel([LivePresencePanel]);
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -666,7 +666,9 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
666
666
|
});
|
|
667
667
|
socket.on("get-entity-subscriptions", (callback) => {
|
|
668
668
|
const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id && r.includes(":")).map((room) => {
|
|
669
|
-
const
|
|
669
|
+
const lastColonIndex = room.lastIndexOf(":");
|
|
670
|
+
const uid = room.substring(0, lastColonIndex);
|
|
671
|
+
const id = room.substring(lastColonIndex + 1);
|
|
670
672
|
return { uid, id, room };
|
|
671
673
|
});
|
|
672
674
|
if (callback) callback({ success: true, subscriptions: rooms });
|
|
@@ -1348,7 +1350,7 @@ const sessionTokens = /* @__PURE__ */ new Map();
|
|
|
1348
1350
|
const activeSockets = /* @__PURE__ */ new Map();
|
|
1349
1351
|
const refreshThrottle = /* @__PURE__ */ new Map();
|
|
1350
1352
|
const SESSION_TTL = 10 * 60 * 1e3;
|
|
1351
|
-
const REFRESH_COOLDOWN =
|
|
1353
|
+
const REFRESH_COOLDOWN = 3 * 1e3;
|
|
1352
1354
|
const CLEANUP_INTERVAL = 2 * 60 * 1e3;
|
|
1353
1355
|
const hashToken = (token) => {
|
|
1354
1356
|
return createHash("sha256").update(token).digest("hex");
|
|
@@ -1399,7 +1401,7 @@ var presence$3 = ({ strapi: strapi2 }) => ({
|
|
|
1399
1401
|
userId: adminUser.id,
|
|
1400
1402
|
user: {
|
|
1401
1403
|
id: adminUser.id,
|
|
1402
|
-
|
|
1404
|
+
email: adminUser.email,
|
|
1403
1405
|
firstname: adminUser.firstname,
|
|
1404
1406
|
lastname: adminUser.lastname
|
|
1405
1407
|
},
|
|
@@ -1562,6 +1564,32 @@ var presence$3 = ({ strapi: strapi2 }) => ({
|
|
|
1562
1564
|
strapi2.log.error("[plugin-io] Failed to invalidate user sessions:", error2);
|
|
1563
1565
|
return ctx.internalServerError("Failed to invalidate sessions");
|
|
1564
1566
|
}
|
|
1567
|
+
},
|
|
1568
|
+
/**
|
|
1569
|
+
* HTTP Handler: Gets all online users with their editing info
|
|
1570
|
+
* Used for the "Who's Online" dashboard widget
|
|
1571
|
+
* @param {object} ctx - Koa context
|
|
1572
|
+
*/
|
|
1573
|
+
async getOnlineUsers(ctx) {
|
|
1574
|
+
const adminUser = ctx.state.user;
|
|
1575
|
+
if (!adminUser) {
|
|
1576
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1577
|
+
}
|
|
1578
|
+
try {
|
|
1579
|
+
const presenceService = strapi2.plugin("io").service("presence");
|
|
1580
|
+
const onlineUsers = presenceService.getOnlineUsers();
|
|
1581
|
+
const counts = presenceService.getOnlineCounts();
|
|
1582
|
+
ctx.body = {
|
|
1583
|
+
data: {
|
|
1584
|
+
users: onlineUsers,
|
|
1585
|
+
counts,
|
|
1586
|
+
timestamp: Date.now()
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
} catch (error2) {
|
|
1590
|
+
strapi2.log.error("[plugin-io] Failed to get online users:", error2);
|
|
1591
|
+
return ctx.internalServerError("Failed to get online users");
|
|
1592
|
+
}
|
|
1565
1593
|
}
|
|
1566
1594
|
});
|
|
1567
1595
|
const settings$2 = settings$3;
|
|
@@ -1671,6 +1699,15 @@ var admin$1 = {
|
|
|
1671
1699
|
config: {
|
|
1672
1700
|
policies: ["admin::isAuthenticatedAdmin"]
|
|
1673
1701
|
}
|
|
1702
|
+
},
|
|
1703
|
+
// Who's Online: Get all online users with editing info
|
|
1704
|
+
{
|
|
1705
|
+
method: "GET",
|
|
1706
|
+
path: "/online-users",
|
|
1707
|
+
handler: "presence.getOnlineUsers",
|
|
1708
|
+
config: {
|
|
1709
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1710
|
+
}
|
|
1674
1711
|
}
|
|
1675
1712
|
]
|
|
1676
1713
|
};
|
|
@@ -30550,7 +30587,10 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30550
30587
|
*/
|
|
30551
30588
|
registerConnection(socketId, user = null) {
|
|
30552
30589
|
const settings2 = getPresenceSettings();
|
|
30553
|
-
if (!settings2.enabled)
|
|
30590
|
+
if (!settings2.enabled) {
|
|
30591
|
+
strapi2.log.warn(`socket.io: Presence disabled, skipping registration for ${socketId}`);
|
|
30592
|
+
return;
|
|
30593
|
+
}
|
|
30554
30594
|
activeConnections.set(socketId, {
|
|
30555
30595
|
user,
|
|
30556
30596
|
entities: /* @__PURE__ */ new Map(),
|
|
@@ -30558,7 +30598,8 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30558
30598
|
lastSeen: Date.now(),
|
|
30559
30599
|
connectedAt: Date.now()
|
|
30560
30600
|
});
|
|
30561
|
-
|
|
30601
|
+
const username = user?.username || user?.firstname || "anonymous";
|
|
30602
|
+
strapi2.log.info(`socket.io: Presence registered for ${username} (socket: ${socketId}, total: ${activeConnections.size})`);
|
|
30562
30603
|
},
|
|
30563
30604
|
/**
|
|
30564
30605
|
* Unregisters a socket connection and cleans up all entity presence
|
|
@@ -30569,7 +30610,9 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30569
30610
|
if (!connection) return;
|
|
30570
30611
|
if (connection.entities) {
|
|
30571
30612
|
for (const entityKey of connection.entities.keys()) {
|
|
30572
|
-
const
|
|
30613
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
30614
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
30615
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
30573
30616
|
await this.leaveEntity(socketId, uid, documentId, false);
|
|
30574
30617
|
}
|
|
30575
30618
|
}
|
|
@@ -30804,6 +30847,90 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30804
30847
|
timestamp: Date.now()
|
|
30805
30848
|
});
|
|
30806
30849
|
}
|
|
30850
|
+
},
|
|
30851
|
+
/**
|
|
30852
|
+
* Gets all online users with their currently editing entities
|
|
30853
|
+
* Used for the "Who's Online" dashboard widget
|
|
30854
|
+
* @returns {Array} List of online users with their editing info
|
|
30855
|
+
*/
|
|
30856
|
+
getOnlineUsers() {
|
|
30857
|
+
const users = [];
|
|
30858
|
+
const now = Date.now();
|
|
30859
|
+
for (const [socketId, connection] of activeConnections) {
|
|
30860
|
+
if (!connection.user) continue;
|
|
30861
|
+
const editingEntities = [];
|
|
30862
|
+
if (connection.entities) {
|
|
30863
|
+
for (const [entityKey, joinedAt] of connection.entities) {
|
|
30864
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
30865
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
30866
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
30867
|
+
let contentTypeName = uid;
|
|
30868
|
+
try {
|
|
30869
|
+
const contentType = strapi2.contentTypes[uid];
|
|
30870
|
+
if (contentType?.info?.displayName) {
|
|
30871
|
+
contentTypeName = contentType.info.displayName;
|
|
30872
|
+
} else if (contentType?.info?.singularName) {
|
|
30873
|
+
contentTypeName = contentType.info.singularName;
|
|
30874
|
+
}
|
|
30875
|
+
} catch (e) {
|
|
30876
|
+
}
|
|
30877
|
+
editingEntities.push({
|
|
30878
|
+
uid,
|
|
30879
|
+
documentId,
|
|
30880
|
+
contentTypeName,
|
|
30881
|
+
joinedAt,
|
|
30882
|
+
editingFor: Math.floor((now - joinedAt) / 1e3)
|
|
30883
|
+
// seconds
|
|
30884
|
+
});
|
|
30885
|
+
}
|
|
30886
|
+
}
|
|
30887
|
+
users.push({
|
|
30888
|
+
socketId,
|
|
30889
|
+
user: {
|
|
30890
|
+
id: connection.user.id,
|
|
30891
|
+
username: connection.user.username,
|
|
30892
|
+
email: connection.user.email,
|
|
30893
|
+
firstname: connection.user.firstname,
|
|
30894
|
+
lastname: connection.user.lastname,
|
|
30895
|
+
isAdmin: connection.user.isAdmin || false
|
|
30896
|
+
},
|
|
30897
|
+
connectedAt: connection.connectedAt,
|
|
30898
|
+
lastSeen: connection.lastSeen,
|
|
30899
|
+
onlineFor: Math.floor((now - connection.connectedAt) / 1e3),
|
|
30900
|
+
// seconds
|
|
30901
|
+
editingEntities,
|
|
30902
|
+
isEditing: editingEntities.length > 0
|
|
30903
|
+
});
|
|
30904
|
+
}
|
|
30905
|
+
users.sort((a, b) => {
|
|
30906
|
+
if (a.isEditing && !b.isEditing) return -1;
|
|
30907
|
+
if (!a.isEditing && b.isEditing) return 1;
|
|
30908
|
+
return b.connectedAt - a.connectedAt;
|
|
30909
|
+
});
|
|
30910
|
+
return users;
|
|
30911
|
+
},
|
|
30912
|
+
/**
|
|
30913
|
+
* Gets count of online users
|
|
30914
|
+
* @returns {object} Online user counts
|
|
30915
|
+
*/
|
|
30916
|
+
getOnlineCounts() {
|
|
30917
|
+
let total = 0;
|
|
30918
|
+
let admins = 0;
|
|
30919
|
+
let users = 0;
|
|
30920
|
+
let editing = 0;
|
|
30921
|
+
for (const connection of activeConnections.values()) {
|
|
30922
|
+
if (!connection.user) continue;
|
|
30923
|
+
total++;
|
|
30924
|
+
if (connection.user.isAdmin) {
|
|
30925
|
+
admins++;
|
|
30926
|
+
} else {
|
|
30927
|
+
users++;
|
|
30928
|
+
}
|
|
30929
|
+
if (connection.entities?.size > 0) {
|
|
30930
|
+
editing++;
|
|
30931
|
+
}
|
|
30932
|
+
}
|
|
30933
|
+
return { total, admins, users, editing };
|
|
30807
30934
|
}
|
|
30808
30935
|
};
|
|
30809
30936
|
};
|
|
@@ -31020,7 +31147,9 @@ var preview$1 = ({ strapi: strapi2 }) => {
|
|
|
31020
31147
|
getActivePreviewEntities() {
|
|
31021
31148
|
const entities = [];
|
|
31022
31149
|
for (const [entityKey, subscribers] of previewSubscribers) {
|
|
31023
|
-
const
|
|
31150
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
31151
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
31152
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
31024
31153
|
entities.push({
|
|
31025
31154
|
uid,
|
|
31026
31155
|
documentId,
|
package/dist/server/index.mjs
CHANGED
|
@@ -634,7 +634,9 @@ async function bootstrapIO$1({ strapi: strapi2 }) {
|
|
|
634
634
|
});
|
|
635
635
|
socket.on("get-entity-subscriptions", (callback) => {
|
|
636
636
|
const rooms = Array.from(socket.rooms).filter((r) => r !== socket.id && r.includes(":")).map((room) => {
|
|
637
|
-
const
|
|
637
|
+
const lastColonIndex = room.lastIndexOf(":");
|
|
638
|
+
const uid = room.substring(0, lastColonIndex);
|
|
639
|
+
const id = room.substring(lastColonIndex + 1);
|
|
638
640
|
return { uid, id, room };
|
|
639
641
|
});
|
|
640
642
|
if (callback) callback({ success: true, subscriptions: rooms });
|
|
@@ -1316,7 +1318,7 @@ const sessionTokens = /* @__PURE__ */ new Map();
|
|
|
1316
1318
|
const activeSockets = /* @__PURE__ */ new Map();
|
|
1317
1319
|
const refreshThrottle = /* @__PURE__ */ new Map();
|
|
1318
1320
|
const SESSION_TTL = 10 * 60 * 1e3;
|
|
1319
|
-
const REFRESH_COOLDOWN =
|
|
1321
|
+
const REFRESH_COOLDOWN = 3 * 1e3;
|
|
1320
1322
|
const CLEANUP_INTERVAL = 2 * 60 * 1e3;
|
|
1321
1323
|
const hashToken = (token) => {
|
|
1322
1324
|
return createHash("sha256").update(token).digest("hex");
|
|
@@ -1367,7 +1369,7 @@ var presence$3 = ({ strapi: strapi2 }) => ({
|
|
|
1367
1369
|
userId: adminUser.id,
|
|
1368
1370
|
user: {
|
|
1369
1371
|
id: adminUser.id,
|
|
1370
|
-
|
|
1372
|
+
email: adminUser.email,
|
|
1371
1373
|
firstname: adminUser.firstname,
|
|
1372
1374
|
lastname: adminUser.lastname
|
|
1373
1375
|
},
|
|
@@ -1530,6 +1532,32 @@ var presence$3 = ({ strapi: strapi2 }) => ({
|
|
|
1530
1532
|
strapi2.log.error("[plugin-io] Failed to invalidate user sessions:", error2);
|
|
1531
1533
|
return ctx.internalServerError("Failed to invalidate sessions");
|
|
1532
1534
|
}
|
|
1535
|
+
},
|
|
1536
|
+
/**
|
|
1537
|
+
* HTTP Handler: Gets all online users with their editing info
|
|
1538
|
+
* Used for the "Who's Online" dashboard widget
|
|
1539
|
+
* @param {object} ctx - Koa context
|
|
1540
|
+
*/
|
|
1541
|
+
async getOnlineUsers(ctx) {
|
|
1542
|
+
const adminUser = ctx.state.user;
|
|
1543
|
+
if (!adminUser) {
|
|
1544
|
+
return ctx.unauthorized("Admin authentication required");
|
|
1545
|
+
}
|
|
1546
|
+
try {
|
|
1547
|
+
const presenceService = strapi2.plugin("io").service("presence");
|
|
1548
|
+
const onlineUsers = presenceService.getOnlineUsers();
|
|
1549
|
+
const counts = presenceService.getOnlineCounts();
|
|
1550
|
+
ctx.body = {
|
|
1551
|
+
data: {
|
|
1552
|
+
users: onlineUsers,
|
|
1553
|
+
counts,
|
|
1554
|
+
timestamp: Date.now()
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
} catch (error2) {
|
|
1558
|
+
strapi2.log.error("[plugin-io] Failed to get online users:", error2);
|
|
1559
|
+
return ctx.internalServerError("Failed to get online users");
|
|
1560
|
+
}
|
|
1533
1561
|
}
|
|
1534
1562
|
});
|
|
1535
1563
|
const settings$2 = settings$3;
|
|
@@ -1639,6 +1667,15 @@ var admin$1 = {
|
|
|
1639
1667
|
config: {
|
|
1640
1668
|
policies: ["admin::isAuthenticatedAdmin"]
|
|
1641
1669
|
}
|
|
1670
|
+
},
|
|
1671
|
+
// Who's Online: Get all online users with editing info
|
|
1672
|
+
{
|
|
1673
|
+
method: "GET",
|
|
1674
|
+
path: "/online-users",
|
|
1675
|
+
handler: "presence.getOnlineUsers",
|
|
1676
|
+
config: {
|
|
1677
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1678
|
+
}
|
|
1642
1679
|
}
|
|
1643
1680
|
]
|
|
1644
1681
|
};
|
|
@@ -30518,7 +30555,10 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30518
30555
|
*/
|
|
30519
30556
|
registerConnection(socketId, user = null) {
|
|
30520
30557
|
const settings2 = getPresenceSettings();
|
|
30521
|
-
if (!settings2.enabled)
|
|
30558
|
+
if (!settings2.enabled) {
|
|
30559
|
+
strapi2.log.warn(`socket.io: Presence disabled, skipping registration for ${socketId}`);
|
|
30560
|
+
return;
|
|
30561
|
+
}
|
|
30522
30562
|
activeConnections.set(socketId, {
|
|
30523
30563
|
user,
|
|
30524
30564
|
entities: /* @__PURE__ */ new Map(),
|
|
@@ -30526,7 +30566,8 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30526
30566
|
lastSeen: Date.now(),
|
|
30527
30567
|
connectedAt: Date.now()
|
|
30528
30568
|
});
|
|
30529
|
-
|
|
30569
|
+
const username = user?.username || user?.firstname || "anonymous";
|
|
30570
|
+
strapi2.log.info(`socket.io: Presence registered for ${username} (socket: ${socketId}, total: ${activeConnections.size})`);
|
|
30530
30571
|
},
|
|
30531
30572
|
/**
|
|
30532
30573
|
* Unregisters a socket connection and cleans up all entity presence
|
|
@@ -30537,7 +30578,9 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30537
30578
|
if (!connection) return;
|
|
30538
30579
|
if (connection.entities) {
|
|
30539
30580
|
for (const entityKey of connection.entities.keys()) {
|
|
30540
|
-
const
|
|
30581
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
30582
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
30583
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
30541
30584
|
await this.leaveEntity(socketId, uid, documentId, false);
|
|
30542
30585
|
}
|
|
30543
30586
|
}
|
|
@@ -30772,6 +30815,90 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30772
30815
|
timestamp: Date.now()
|
|
30773
30816
|
});
|
|
30774
30817
|
}
|
|
30818
|
+
},
|
|
30819
|
+
/**
|
|
30820
|
+
* Gets all online users with their currently editing entities
|
|
30821
|
+
* Used for the "Who's Online" dashboard widget
|
|
30822
|
+
* @returns {Array} List of online users with their editing info
|
|
30823
|
+
*/
|
|
30824
|
+
getOnlineUsers() {
|
|
30825
|
+
const users = [];
|
|
30826
|
+
const now = Date.now();
|
|
30827
|
+
for (const [socketId, connection] of activeConnections) {
|
|
30828
|
+
if (!connection.user) continue;
|
|
30829
|
+
const editingEntities = [];
|
|
30830
|
+
if (connection.entities) {
|
|
30831
|
+
for (const [entityKey, joinedAt] of connection.entities) {
|
|
30832
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
30833
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
30834
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
30835
|
+
let contentTypeName = uid;
|
|
30836
|
+
try {
|
|
30837
|
+
const contentType = strapi2.contentTypes[uid];
|
|
30838
|
+
if (contentType?.info?.displayName) {
|
|
30839
|
+
contentTypeName = contentType.info.displayName;
|
|
30840
|
+
} else if (contentType?.info?.singularName) {
|
|
30841
|
+
contentTypeName = contentType.info.singularName;
|
|
30842
|
+
}
|
|
30843
|
+
} catch (e) {
|
|
30844
|
+
}
|
|
30845
|
+
editingEntities.push({
|
|
30846
|
+
uid,
|
|
30847
|
+
documentId,
|
|
30848
|
+
contentTypeName,
|
|
30849
|
+
joinedAt,
|
|
30850
|
+
editingFor: Math.floor((now - joinedAt) / 1e3)
|
|
30851
|
+
// seconds
|
|
30852
|
+
});
|
|
30853
|
+
}
|
|
30854
|
+
}
|
|
30855
|
+
users.push({
|
|
30856
|
+
socketId,
|
|
30857
|
+
user: {
|
|
30858
|
+
id: connection.user.id,
|
|
30859
|
+
username: connection.user.username,
|
|
30860
|
+
email: connection.user.email,
|
|
30861
|
+
firstname: connection.user.firstname,
|
|
30862
|
+
lastname: connection.user.lastname,
|
|
30863
|
+
isAdmin: connection.user.isAdmin || false
|
|
30864
|
+
},
|
|
30865
|
+
connectedAt: connection.connectedAt,
|
|
30866
|
+
lastSeen: connection.lastSeen,
|
|
30867
|
+
onlineFor: Math.floor((now - connection.connectedAt) / 1e3),
|
|
30868
|
+
// seconds
|
|
30869
|
+
editingEntities,
|
|
30870
|
+
isEditing: editingEntities.length > 0
|
|
30871
|
+
});
|
|
30872
|
+
}
|
|
30873
|
+
users.sort((a, b) => {
|
|
30874
|
+
if (a.isEditing && !b.isEditing) return -1;
|
|
30875
|
+
if (!a.isEditing && b.isEditing) return 1;
|
|
30876
|
+
return b.connectedAt - a.connectedAt;
|
|
30877
|
+
});
|
|
30878
|
+
return users;
|
|
30879
|
+
},
|
|
30880
|
+
/**
|
|
30881
|
+
* Gets count of online users
|
|
30882
|
+
* @returns {object} Online user counts
|
|
30883
|
+
*/
|
|
30884
|
+
getOnlineCounts() {
|
|
30885
|
+
let total = 0;
|
|
30886
|
+
let admins = 0;
|
|
30887
|
+
let users = 0;
|
|
30888
|
+
let editing = 0;
|
|
30889
|
+
for (const connection of activeConnections.values()) {
|
|
30890
|
+
if (!connection.user) continue;
|
|
30891
|
+
total++;
|
|
30892
|
+
if (connection.user.isAdmin) {
|
|
30893
|
+
admins++;
|
|
30894
|
+
} else {
|
|
30895
|
+
users++;
|
|
30896
|
+
}
|
|
30897
|
+
if (connection.entities?.size > 0) {
|
|
30898
|
+
editing++;
|
|
30899
|
+
}
|
|
30900
|
+
}
|
|
30901
|
+
return { total, admins, users, editing };
|
|
30775
30902
|
}
|
|
30776
30903
|
};
|
|
30777
30904
|
};
|
|
@@ -30988,7 +31115,9 @@ var preview$1 = ({ strapi: strapi2 }) => {
|
|
|
30988
31115
|
getActivePreviewEntities() {
|
|
30989
31116
|
const entities = [];
|
|
30990
31117
|
for (const [entityKey, subscribers] of previewSubscribers) {
|
|
30991
|
-
const
|
|
31118
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
31119
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
31120
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
30992
31121
|
entities.push({
|
|
30993
31122
|
uid,
|
|
30994
31123
|
documentId,
|
package/package.json
CHANGED