@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 CHANGED
@@ -898,13 +898,17 @@ Authorization: Bearer <admin-jwt>
898
898
 
899
899
  The plugin provides a full admin interface for configuration and monitoring.
900
900
 
901
- ### Dashboard Widget
901
+ ### Dashboard Widgets
902
902
 
903
903
  > **Requires Strapi v5.13+**
904
904
 
905
- After installation, a live statistics widget appears on your Strapi admin homepage:
905
+ After installation, live statistics widgets appear on your Strapi admin homepage:
906
906
 
907
- **Widget Shows:**
907
+ #### Socket.IO Statistics Widget
908
+
909
+ ![Socket.IO Statistics Widget](./pics/widget.png)
910
+
911
+ **Shows:**
908
912
  - Live connection status (pulsing indicator when active)
909
913
  - Active connections count
910
914
  - Active rooms count
@@ -913,12 +917,24 @@ After installation, a live statistics widget appears on your Strapi admin homepa
913
917
 
914
918
  Updates automatically every 5 seconds.
915
919
 
920
+ #### Who's Online Widget
921
+
922
+ ![Who's Online Widget](./pics/whoisonlinewidget.png)
923
+
924
+ **Shows:**
925
+ - List of currently online admin users
926
+ - User avatars with role badges
927
+ - Online status and last activity
928
+ - Quick access to view all activity
929
+
916
930
  ### Settings Page
917
931
 
918
932
  Navigate to **Settings > Socket.IO > Settings** for visual configuration:
919
933
 
920
934
  **Path:** `/admin/settings/io/settings`
921
935
 
936
+ ![Socket.IO Settings](./pics/settings.png)
937
+
922
938
  #### General Settings
923
939
  - Enable/disable the plugin
924
940
  - Configure CORS origins
@@ -956,6 +972,8 @@ Navigate to **Settings > Socket.IO > Monitoring** for live statistics:
956
972
 
957
973
  **Path:** `/admin/settings/io/monitoring`
958
974
 
975
+ ![Monitoring & Logging](./pics/monitoringSettings.png)
976
+
959
977
  - View active connections with user details
960
978
  - See event logs in real-time
961
979
  - Monitor performance metrics (events/second)
@@ -971,6 +989,8 @@ When editing content in the Content Manager, a **Live Presence** panel appears i
971
989
  - **Active Editors** - List of other users editing the same content
972
990
  - **Typing Indicator** - Shows when someone is typing and in which field
973
991
 
992
+ ![Live Presence Panel](./pics/livepresenceindi.png)
993
+
974
994
  **How It Works:**
975
995
 
976
996
  1. When you open a content entry, the panel connects via Socket.IO
@@ -978,24 +998,6 @@ When editing content in the Content Manager, a **Live Presence** panel appears i
978
998
  3. Typing in any field broadcasts a typing indicator to others
979
999
  4. When you leave, others are notified
980
1000
 
981
- **Example Display:**
982
-
983
- ```
984
- +-----------------------------+
985
- | Live Presence |
986
- +-----------------------------+
987
- | [*] Live |
988
- | Real-time sync active |
989
- +-----------------------------+
990
- | ALSO EDITING (1) |
991
- | +-------------------------+ |
992
- | | SA Sarah Admin | |
993
- | | Typing in: title | |
994
- | | [Typing...] | |
995
- | +-------------------------+ |
996
- +-----------------------------+
997
- ```
998
-
999
1001
  ---
1000
1002
 
1001
1003
  ## Monitoring Service
@@ -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-BEZDDgvZ.js");
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-Dof_eA3e.mjs";
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-BEZDDgvZ.js");
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-Dof_eA3e.mjs";
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;