@strapi-community/plugin-io 5.2.0 → 5.3.1
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/README.md +23 -21
- 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 +137 -7
- package/dist/server/index.mjs +137 -7
- package/package.json +1 -1
|
@@ -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
|
};
|
|
@@ -11346,6 +11383,7 @@ const getNonVisibleAttributes = (model) => {
|
|
|
11346
11383
|
return ___default.uniq([
|
|
11347
11384
|
ID_ATTRIBUTE$4,
|
|
11348
11385
|
DOC_ID_ATTRIBUTE$4,
|
|
11386
|
+
PUBLISHED_AT_ATTRIBUTE$1,
|
|
11349
11387
|
...getTimestamps(model),
|
|
11350
11388
|
...nonVisibleAttributes
|
|
11351
11389
|
]);
|
|
@@ -30550,7 +30588,10 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30550
30588
|
*/
|
|
30551
30589
|
registerConnection(socketId, user = null) {
|
|
30552
30590
|
const settings2 = getPresenceSettings();
|
|
30553
|
-
if (!settings2.enabled)
|
|
30591
|
+
if (!settings2.enabled) {
|
|
30592
|
+
strapi2.log.warn(`socket.io: Presence disabled, skipping registration for ${socketId}`);
|
|
30593
|
+
return;
|
|
30594
|
+
}
|
|
30554
30595
|
activeConnections.set(socketId, {
|
|
30555
30596
|
user,
|
|
30556
30597
|
entities: /* @__PURE__ */ new Map(),
|
|
@@ -30558,7 +30599,8 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30558
30599
|
lastSeen: Date.now(),
|
|
30559
30600
|
connectedAt: Date.now()
|
|
30560
30601
|
});
|
|
30561
|
-
|
|
30602
|
+
const username = user?.username || user?.firstname || "anonymous";
|
|
30603
|
+
strapi2.log.info(`socket.io: Presence registered for ${username} (socket: ${socketId}, total: ${activeConnections.size})`);
|
|
30562
30604
|
},
|
|
30563
30605
|
/**
|
|
30564
30606
|
* Unregisters a socket connection and cleans up all entity presence
|
|
@@ -30569,7 +30611,9 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30569
30611
|
if (!connection) return;
|
|
30570
30612
|
if (connection.entities) {
|
|
30571
30613
|
for (const entityKey of connection.entities.keys()) {
|
|
30572
|
-
const
|
|
30614
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
30615
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
30616
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
30573
30617
|
await this.leaveEntity(socketId, uid, documentId, false);
|
|
30574
30618
|
}
|
|
30575
30619
|
}
|
|
@@ -30804,6 +30848,90 @@ var presence$1 = ({ strapi: strapi2 }) => {
|
|
|
30804
30848
|
timestamp: Date.now()
|
|
30805
30849
|
});
|
|
30806
30850
|
}
|
|
30851
|
+
},
|
|
30852
|
+
/**
|
|
30853
|
+
* Gets all online users with their currently editing entities
|
|
30854
|
+
* Used for the "Who's Online" dashboard widget
|
|
30855
|
+
* @returns {Array} List of online users with their editing info
|
|
30856
|
+
*/
|
|
30857
|
+
getOnlineUsers() {
|
|
30858
|
+
const users = [];
|
|
30859
|
+
const now = Date.now();
|
|
30860
|
+
for (const [socketId, connection] of activeConnections) {
|
|
30861
|
+
if (!connection.user) continue;
|
|
30862
|
+
const editingEntities = [];
|
|
30863
|
+
if (connection.entities) {
|
|
30864
|
+
for (const [entityKey, joinedAt] of connection.entities) {
|
|
30865
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
30866
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
30867
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
30868
|
+
let contentTypeName = uid;
|
|
30869
|
+
try {
|
|
30870
|
+
const contentType = strapi2.contentTypes[uid];
|
|
30871
|
+
if (contentType?.info?.displayName) {
|
|
30872
|
+
contentTypeName = contentType.info.displayName;
|
|
30873
|
+
} else if (contentType?.info?.singularName) {
|
|
30874
|
+
contentTypeName = contentType.info.singularName;
|
|
30875
|
+
}
|
|
30876
|
+
} catch (e) {
|
|
30877
|
+
}
|
|
30878
|
+
editingEntities.push({
|
|
30879
|
+
uid,
|
|
30880
|
+
documentId,
|
|
30881
|
+
contentTypeName,
|
|
30882
|
+
joinedAt,
|
|
30883
|
+
editingFor: Math.floor((now - joinedAt) / 1e3)
|
|
30884
|
+
// seconds
|
|
30885
|
+
});
|
|
30886
|
+
}
|
|
30887
|
+
}
|
|
30888
|
+
users.push({
|
|
30889
|
+
socketId,
|
|
30890
|
+
user: {
|
|
30891
|
+
id: connection.user.id,
|
|
30892
|
+
username: connection.user.username,
|
|
30893
|
+
email: connection.user.email,
|
|
30894
|
+
firstname: connection.user.firstname,
|
|
30895
|
+
lastname: connection.user.lastname,
|
|
30896
|
+
isAdmin: connection.user.isAdmin || false
|
|
30897
|
+
},
|
|
30898
|
+
connectedAt: connection.connectedAt,
|
|
30899
|
+
lastSeen: connection.lastSeen,
|
|
30900
|
+
onlineFor: Math.floor((now - connection.connectedAt) / 1e3),
|
|
30901
|
+
// seconds
|
|
30902
|
+
editingEntities,
|
|
30903
|
+
isEditing: editingEntities.length > 0
|
|
30904
|
+
});
|
|
30905
|
+
}
|
|
30906
|
+
users.sort((a, b) => {
|
|
30907
|
+
if (a.isEditing && !b.isEditing) return -1;
|
|
30908
|
+
if (!a.isEditing && b.isEditing) return 1;
|
|
30909
|
+
return b.connectedAt - a.connectedAt;
|
|
30910
|
+
});
|
|
30911
|
+
return users;
|
|
30912
|
+
},
|
|
30913
|
+
/**
|
|
30914
|
+
* Gets count of online users
|
|
30915
|
+
* @returns {object} Online user counts
|
|
30916
|
+
*/
|
|
30917
|
+
getOnlineCounts() {
|
|
30918
|
+
let total = 0;
|
|
30919
|
+
let admins = 0;
|
|
30920
|
+
let users = 0;
|
|
30921
|
+
let editing = 0;
|
|
30922
|
+
for (const connection of activeConnections.values()) {
|
|
30923
|
+
if (!connection.user) continue;
|
|
30924
|
+
total++;
|
|
30925
|
+
if (connection.user.isAdmin) {
|
|
30926
|
+
admins++;
|
|
30927
|
+
} else {
|
|
30928
|
+
users++;
|
|
30929
|
+
}
|
|
30930
|
+
if (connection.entities?.size > 0) {
|
|
30931
|
+
editing++;
|
|
30932
|
+
}
|
|
30933
|
+
}
|
|
30934
|
+
return { total, admins, users, editing };
|
|
30807
30935
|
}
|
|
30808
30936
|
};
|
|
30809
30937
|
};
|
|
@@ -31020,7 +31148,9 @@ var preview$1 = ({ strapi: strapi2 }) => {
|
|
|
31020
31148
|
getActivePreviewEntities() {
|
|
31021
31149
|
const entities = [];
|
|
31022
31150
|
for (const [entityKey, subscribers] of previewSubscribers) {
|
|
31023
|
-
const
|
|
31151
|
+
const lastColonIndex = entityKey.lastIndexOf(":");
|
|
31152
|
+
const uid = entityKey.substring(0, lastColonIndex);
|
|
31153
|
+
const documentId = entityKey.substring(lastColonIndex + 1);
|
|
31024
31154
|
entities.push({
|
|
31025
31155
|
uid,
|
|
31026
31156
|
documentId,
|