@strapi-community/plugin-io 5.1.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.
@@ -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--2NeIKGR.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 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-CzvX8YTe.mjs";
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-DMbMGU6J.mjs").then((mod) => ({ default: mod.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-Bn9XJSlg.mjs").then((mod) => ({ default: mod.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
- console.log(`[${PLUGIN_ID}] [SUCCESS] Socket.IO Stats Widget registered`);
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-BeNq_EnQ.mjs");
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-4OkXJAjU.js")).then((mod) => ({ default: mod.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-K5Y3hhKF.js")).then((mod) => ({ default: mod.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
- console.log(`[${PLUGIN_ID}] [SUCCESS] Socket.IO Stats Widget registered`);
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-CNaEK-Gk.js"));
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]);
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index--2NeIKGR.js");
2
+ const index = require("../_chunks/index-DVNfszio.js");
3
3
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-CzvX8YTe.mjs";
1
+ import { i } from "../_chunks/index-Bw7WjN5H.mjs";
2
2
  export {
3
3
  i as default
4
4
  };