@peers-app/peers-ui 0.21.2 → 0.21.4
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/screens/network-viewer/connection-troubleshooter.js +1 -1
- package/dist/screens/network-viewer/device-details-modal.js +11 -15
- package/dist/screens/network-viewer/group-details-modal.js +60 -17
- package/dist/screens/network-viewer/network-viewer-ipc.d.ts +9 -39
- package/dist/screens/network-viewer/network-viewer-ipc.js +71 -3
- package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +1 -0
- package/dist/screens/network-viewer/network-viewer-ipc.test.js +59 -0
- package/dist/screens/network-viewer/network-viewer.js +46 -60
- package/dist/screens/network-viewer/remote-logs-panel.js +11 -12
- package/dist/tabs-layout/network-status-indicator.d.ts +41 -0
- package/dist/tabs-layout/network-status-indicator.js +261 -0
- package/dist/tabs-layout/network-status-indicator.test.d.ts +1 -0
- package/dist/tabs-layout/network-status-indicator.test.js +333 -0
- package/dist/tabs-layout/tabs-layout.js +25 -24
- package/dist/tabs-layout/use-network-status.d.ts +8 -0
- package/dist/tabs-layout/use-network-status.js +57 -0
- package/dist/tabs-layout/use-network-status.test.d.ts +1 -0
- package/dist/tabs-layout/use-network-status.test.js +42 -0
- package/package.json +3 -3
- package/src/screens/network-viewer/connection-troubleshooter.tsx +2 -2
- package/src/screens/network-viewer/device-details-modal.tsx +19 -61
- package/src/screens/network-viewer/group-details-modal.tsx +211 -130
- package/src/screens/network-viewer/network-viewer-ipc.test.ts +70 -0
- package/src/screens/network-viewer/network-viewer-ipc.ts +102 -35
- package/src/screens/network-viewer/network-viewer.tsx +76 -148
- package/src/screens/network-viewer/remote-logs-panel.tsx +14 -16
- package/src/tabs-layout/network-status-indicator.test.tsx +391 -0
- package/src/tabs-layout/network-status-indicator.tsx +475 -0
- package/src/tabs-layout/tabs-layout.tsx +4 -1
- package/src/tabs-layout/use-network-status.test.tsx +55 -0
- package/src/tabs-layout/use-network-status.ts +68 -0
|
@@ -6,7 +6,6 @@ const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
|
6
6
|
const react_1 = require("react");
|
|
7
7
|
const loading_indicator_1 = require("../../components/loading-indicator");
|
|
8
8
|
const trust_level_badge_1 = require("../../components/trust-level-badge");
|
|
9
|
-
const globals_1 = require("../../globals");
|
|
10
9
|
const ui_loader_1 = require("../../ui-router/ui-loader");
|
|
11
10
|
const connection_troubleshooter_1 = require("./connection-troubleshooter");
|
|
12
11
|
const device_details_modal_1 = require("./device-details-modal");
|
|
@@ -47,18 +46,12 @@ function NetworkViewerList() {
|
|
|
47
46
|
const [showTroubleshooter, setShowTroubleshooter] = (0, react_1.useState)(false);
|
|
48
47
|
const loadData = (0, react_1.useCallback)(async () => {
|
|
49
48
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
const [overviewData, connectionsData, groupSyncData, disconnectedData] = await Promise.all([
|
|
58
|
-
api.getOverview(),
|
|
59
|
-
api.getConnections(),
|
|
60
|
-
api.getGroupSyncStatus(),
|
|
61
|
-
api.getDisconnectedDevices(),
|
|
49
|
+
const diagnostics = await (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)();
|
|
50
|
+
const [{ overview: overviewData }, { connections: connectionsData }, { groups: groupSyncData }, { devices: disconnectedData },] = await Promise.all([
|
|
51
|
+
diagnostics.tools.getOverview(),
|
|
52
|
+
diagnostics.tools.getConnections(),
|
|
53
|
+
diagnostics.tools.getGroupSyncStatus(),
|
|
54
|
+
diagnostics.tools.getDisconnectedDevices(),
|
|
62
55
|
]);
|
|
63
56
|
setOverview(overviewData);
|
|
64
57
|
setConnections(connectionsData);
|
|
@@ -75,11 +68,14 @@ function NetworkViewerList() {
|
|
|
75
68
|
const handleRefresh = async () => {
|
|
76
69
|
setRefreshing(true);
|
|
77
70
|
try {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
const control = await (0, network_viewer_ipc_1.getNetworkControlConsumer)();
|
|
72
|
+
const result = await control.tools.refreshNetworkInfo();
|
|
73
|
+
if (result.success) {
|
|
81
74
|
await loadData();
|
|
82
75
|
}
|
|
76
|
+
else {
|
|
77
|
+
alert(result.message);
|
|
78
|
+
}
|
|
83
79
|
}
|
|
84
80
|
catch (error) {
|
|
85
81
|
console.error("Error refreshing network info:", error);
|
|
@@ -93,11 +89,14 @@ function NetworkViewerList() {
|
|
|
93
89
|
return;
|
|
94
90
|
}
|
|
95
91
|
try {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
const control = await (0, network_viewer_ipc_1.getNetworkControlConsumer)();
|
|
93
|
+
const result = await control.tools.disconnectDevice({ deviceId });
|
|
94
|
+
if (result.success) {
|
|
99
95
|
await loadData();
|
|
100
96
|
}
|
|
97
|
+
else {
|
|
98
|
+
alert(result.message);
|
|
99
|
+
}
|
|
101
100
|
}
|
|
102
101
|
catch (error) {
|
|
103
102
|
console.error("Error disconnecting device:", error);
|
|
@@ -108,17 +107,14 @@ function NetworkViewerList() {
|
|
|
108
107
|
};
|
|
109
108
|
const handleConnectToDevice = async (deviceId) => {
|
|
110
109
|
try {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
alert(`Failed to connect: ${result.message}`);
|
|
121
|
-
}
|
|
110
|
+
const control = await (0, network_viewer_ipc_1.getNetworkControlConsumer)();
|
|
111
|
+
setConnecting((prev) => new Set(prev).add(deviceId));
|
|
112
|
+
const result = await control.tools.connectToDevice({ deviceId });
|
|
113
|
+
if (result.success) {
|
|
114
|
+
await loadData();
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
alert(`Failed to connect: ${result.message}`);
|
|
122
118
|
}
|
|
123
119
|
}
|
|
124
120
|
catch (error) {
|
|
@@ -135,16 +131,13 @@ function NetworkViewerList() {
|
|
|
135
131
|
};
|
|
136
132
|
const handleDeleteDevice = async (deviceId) => {
|
|
137
133
|
try {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
alert(`Failed to delete device: ${result.message}`);
|
|
147
|
-
}
|
|
134
|
+
const control = await (0, network_viewer_ipc_1.getNetworkControlConsumer)();
|
|
135
|
+
const result = await control.tools.deleteDevice({ deviceId });
|
|
136
|
+
if (result.success) {
|
|
137
|
+
await loadData();
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
alert(`Failed to delete device: ${result.message}`);
|
|
148
141
|
}
|
|
149
142
|
}
|
|
150
143
|
catch (error) {
|
|
@@ -161,41 +154,32 @@ function NetworkViewerList() {
|
|
|
161
154
|
if (loading) {
|
|
162
155
|
return (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {});
|
|
163
156
|
}
|
|
164
|
-
if (!(0, globals_1.isDesktop)()) {
|
|
165
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: "container-fluid p-4", children: (0, jsx_runtime_1.jsx)("div", { className: "alert alert-info", children: "Network Viewer is only available in the desktop application." }) }));
|
|
166
|
-
}
|
|
167
157
|
if (!overview) {
|
|
168
158
|
return ((0, jsx_runtime_1.jsx)("div", { className: "container-fluid p-4", children: (0, jsx_runtime_1.jsx)("div", { className: "alert alert-warning", children: "Unable to load network information." }) }));
|
|
169
159
|
}
|
|
170
160
|
if (showTroubleshooter) {
|
|
171
161
|
return (0, jsx_runtime_1.jsx)(connection_troubleshooter_1.ConnectionTroubleshooter, { onBack: () => setShowTroubleshooter(false) });
|
|
172
162
|
}
|
|
173
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-4", children: [(0, jsx_runtime_1.jsx)("h2", { children: "Network Viewer" }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2", children: [(0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-warning", onClick: () => setShowTroubleshooter(true), title: "Run connection diagnostics", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-stethoscope me-1" }), "Troubleshoot"] }), (0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-primary", onClick: handleRefresh, disabled: refreshing, children: [(0, jsx_runtime_1.jsx)("i", { className: `bi bi-arrow-clockwise ${refreshing ? "spin" : ""}` }), refreshing ? " Refreshing..." : " Refresh"] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "card mb-2", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsx)("h5", { className: "card-title", children: "Local Device" }), (0, jsx_runtime_1.jsxs)("div", { className: "row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-6", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Device ID:" }), " ", (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: overview.localDeviceId })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6", children: [(0, jsx_runtime_1.jsx)("strong", { children: "User ID:" }), " ", (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: overview.localUserId })] })] })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "row mb-4", children: [(0, jsx_runtime_1.jsx)("div", { className: "col-md-4", children: (0, jsx_runtime_1.jsx)("div", { className: "card", style: { height: "100%" }, children: (0, jsx_runtime_1.jsx)("div", { className: "card-body p-2", children: (0, jsx_runtime_1.jsx)(usage_graph_1.UsageGraph, { label: "Connections", fetchUsage: async () => {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const data = (await api.getOverview());
|
|
178
|
-
return data.totalConnections;
|
|
163
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-4", children: [(0, jsx_runtime_1.jsx)("h2", { children: "Network Viewer" }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2", children: [(0, network_viewer_ipc_1.getNetworkViewerPlatformApi)()?.runDiagnostics && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-warning", onClick: () => setShowTroubleshooter(true), title: "Run connection diagnostics", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-stethoscope me-1" }), "Troubleshoot"] })), (0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-primary", onClick: handleRefresh, disabled: refreshing, children: [(0, jsx_runtime_1.jsx)("i", { className: `bi bi-arrow-clockwise ${refreshing ? "spin" : ""}` }), refreshing ? " Refreshing..." : " Refresh"] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "card mb-2", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsx)("h5", { className: "card-title", children: "Local Device" }), (0, jsx_runtime_1.jsxs)("div", { className: "row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-6", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Device ID:" }), " ", (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: overview.localDeviceId })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6", children: [(0, jsx_runtime_1.jsx)("strong", { children: "User ID:" }), " ", (0, jsx_runtime_1.jsx)("code", { className: "text-muted small", children: overview.localUserId })] })] })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "row mb-4", children: [(0, jsx_runtime_1.jsx)("div", { className: "col-md-4", children: (0, jsx_runtime_1.jsx)("div", { className: "card", style: { height: "100%" }, children: (0, jsx_runtime_1.jsx)("div", { className: "card-body p-2", children: (0, jsx_runtime_1.jsx)(usage_graph_1.UsageGraph, { label: "Connections", fetchUsage: async () => {
|
|
164
|
+
const diagnostics = await (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)();
|
|
165
|
+
const { overview: currentOverview } = await diagnostics.tools.getOverview();
|
|
166
|
+
return currentOverview.totalConnections;
|
|
179
167
|
}, durationMinutes: 5, updateInterval: 5000, color: "rgba(111, 66, 193, 1)", colorZones: [
|
|
180
168
|
{ maxValue: 1, color: "rgba(220, 53, 69, 1)" },
|
|
181
169
|
{ maxValue: 2, color: "rgba(255, 193, 7, 1)" },
|
|
182
170
|
{ maxValue: overview?.maxConnections || 30, color: "rgba(111, 66, 193, 1)" },
|
|
183
171
|
], minValue: 0, maxValue: overview?.maxConnections || 30, formatOverlayText: (value) => `Connections: ${Math.round(value)}` }) }) }) }), (0, jsx_runtime_1.jsx)("div", { className: "col-md-4", children: (0, jsx_runtime_1.jsx)("div", { className: "card", style: { height: "100%" }, children: (0, jsx_runtime_1.jsx)("div", { className: "card-body p-2", children: (0, jsx_runtime_1.jsx)(usage_graph_1.UsageGraph, { label: "CPU", fetchUsage: async () => {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const data = (await api.getOverview());
|
|
188
|
-
return data.cpuPercent;
|
|
172
|
+
const diagnostics = await (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)();
|
|
173
|
+
const { overview: currentOverview } = await diagnostics.tools.getOverview();
|
|
174
|
+
return currentOverview.cpuPercent;
|
|
189
175
|
}, durationMinutes: 5, updateInterval: 5000, color: "rgba(13, 110, 253, 1)", colorZones: [
|
|
190
176
|
{ maxValue: 60, color: "rgba(13, 110, 253, 1)" },
|
|
191
177
|
{ maxValue: 80, color: "rgba(255, 193, 7, 1)" },
|
|
192
178
|
{ maxValue: Infinity, color: "rgba(220, 53, 69, 1)" },
|
|
193
179
|
] }) }) }) }), (0, jsx_runtime_1.jsx)("div", { className: "col-md-4", children: (0, jsx_runtime_1.jsx)("div", { className: "card", style: { height: "100%" }, children: (0, jsx_runtime_1.jsx)("div", { className: "card-body p-2", children: (0, jsx_runtime_1.jsx)(usage_graph_1.UsageGraph, { label: "Memory", fetchUsage: async () => {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const data = (await api.getOverview());
|
|
198
|
-
return data.memPercent;
|
|
180
|
+
const diagnostics = await (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)();
|
|
181
|
+
const { overview: currentOverview } = await diagnostics.tools.getOverview();
|
|
182
|
+
return currentOverview.memPercent;
|
|
199
183
|
}, durationMinutes: 5, updateInterval: 5000, color: "rgba(25, 135, 84, 1)", colorZones: [
|
|
200
184
|
{ maxValue: 60, color: "rgba(25, 135, 84, 1)" },
|
|
201
185
|
{ maxValue: 80, color: "rgba(255, 193, 7, 1)" },
|
|
@@ -218,7 +202,9 @@ function NetworkViewerList() {
|
|
|
218
202
|
? "success"
|
|
219
203
|
: device.errorRate < 0.2
|
|
220
204
|
? "warning"
|
|
221
|
-
: "danger"}`, children: [(device.errorRate * 100).toFixed(1), "% errors"] }), (0, jsx_runtime_1.jsxs)("span", { className: "badge bg-info", children: ["Last:
|
|
205
|
+
: "danger"}`, children: [(device.errorRate * 100).toFixed(1), "% errors"] }), (0, jsx_runtime_1.jsxs)("span", { className: "badge bg-info", children: ["Last:", " ", device.timestampLastApplied
|
|
206
|
+
? new Date(device.timestampLastApplied).toLocaleString()
|
|
207
|
+
: "Never"] })] }), idx < group.connectedDevices.length - 1 && ((0, jsx_runtime_1.jsx)("hr", { className: "my-2" }))] }, device.deviceId))) })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "text-muted", children: "No connected devices" })) }), (0, jsx_runtime_1.jsx)("td", { children: group.preferredDeviceIds.length > 0 ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("span", { className: "badge bg-primary mb-2", children: [group.preferredDeviceIds.length, " preferred"] }), (0, jsx_runtime_1.jsx)("div", { children: group.preferredDeviceIds.map((deviceId) => ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("small", { children: (0, jsx_runtime_1.jsx)("code", { className: "text-muted", children: deviceId }) }) }, deviceId))) })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "text-muted", children: "None" })) }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("small", { children: group.lastSyncTimestamp > 0
|
|
222
208
|
? new Date(group.lastSyncTimestamp).toLocaleString()
|
|
223
209
|
: "Never" }) }), (0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-primary", onClick: () => setSelectedGroupId(group.groupId), title: "View Group Details", children: group.totalMembers }) })] }, group.groupId))) })] }) }))] }) }), (0, jsx_runtime_1.jsx)("div", { className: "card", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsx)("h5", { className: "card-title", children: "Disconnected Devices" }), disconnectedDevices.length === 0 ? ((0, jsx_runtime_1.jsx)("p", { className: "text-muted", children: "No known disconnected devices" })) : ((0, jsx_runtime_1.jsx)("div", { className: "table-responsive", children: (0, jsx_runtime_1.jsxs)("table", { className: "table table-hover", children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { children: "User" }), (0, jsx_runtime_1.jsx)("th", { children: "Trust Level" }), (0, jsx_runtime_1.jsx)("th", { children: "Device ID" }), (0, jsx_runtime_1.jsx)("th", { children: "Device Name" }), (0, jsx_runtime_1.jsx)("th", { children: "First Seen" }), (0, jsx_runtime_1.jsx)("th", { children: "Last Seen" }), (0, jsx_runtime_1.jsx)("th", { children: "Actions" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: disconnectedDevices
|
|
224
210
|
.sort((a, b) => new Date(b.lastSeen).getTime() - new Date(a.lastSeen).getTime())
|
|
@@ -17,11 +17,6 @@ function RemoteLogsPanel({ deviceId }) {
|
|
|
17
17
|
const [search, setSearch] = (0, react_1.useState)("");
|
|
18
18
|
const [minutes, setMinutes] = (0, react_1.useState)("60");
|
|
19
19
|
const loadLogs = (0, react_1.useCallback)(async () => {
|
|
20
|
-
const api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
|
|
21
|
-
if (!api) {
|
|
22
|
-
setError("Network Viewer API is unavailable");
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
20
|
setLoading(true);
|
|
26
21
|
setError(undefined);
|
|
27
22
|
try {
|
|
@@ -29,13 +24,17 @@ function RemoteLogsPanel({ deviceId }) {
|
|
|
29
24
|
const sinceTimestamp = Number.isFinite(minutesNumber) && minutesNumber > 0
|
|
30
25
|
? Date.now() - minutesNumber * 60_000
|
|
31
26
|
: undefined;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
const diagnostics = await (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)();
|
|
28
|
+
const response = await diagnostics.tools.queryRemoteLogs({
|
|
29
|
+
deviceId,
|
|
30
|
+
query: {
|
|
31
|
+
sinceTimestamp,
|
|
32
|
+
level: level ? level : undefined,
|
|
33
|
+
process: processName || undefined,
|
|
34
|
+
search: search || undefined,
|
|
35
|
+
pageSize: 100,
|
|
36
|
+
timeoutMs: 10_000,
|
|
37
|
+
},
|
|
39
38
|
});
|
|
40
39
|
setResult(response);
|
|
41
40
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type INetworkStatusViewState } from "./use-network-status";
|
|
2
|
+
/** How long continuous raw syncing stays a green pulse before the indeterminate circle. */
|
|
3
|
+
export declare const SYNCING_GREEN_MS = 2000;
|
|
4
|
+
/** How long status must stay fully synced before leaving the syncing visual. */
|
|
5
|
+
export declare const SYNCED_STABLE_MS = 1000;
|
|
6
|
+
type NetworkStatusMark = "solid" | "pulse" | "indeterminate";
|
|
7
|
+
interface INetworkStatusPresentation {
|
|
8
|
+
label: string;
|
|
9
|
+
shortLabel: string;
|
|
10
|
+
colorClass: string;
|
|
11
|
+
/** Bootstrap Icons class when not using a status mark. */
|
|
12
|
+
iconClass?: string;
|
|
13
|
+
spinning?: boolean;
|
|
14
|
+
/** Size the Bootstrap icon to match the status marks. */
|
|
15
|
+
compactIcon?: boolean;
|
|
16
|
+
/** Solid / pulsing / indeterminate mark (syncing / error). */
|
|
17
|
+
mark?: NetworkStatusMark;
|
|
18
|
+
}
|
|
19
|
+
/** Convert host availability and network state into accessible indicator text and styling. */
|
|
20
|
+
export declare function getNetworkStatusPresentation(viewState: INetworkStatusViewState, opts?: {
|
|
21
|
+
syncingLongerThanGreenMs?: boolean;
|
|
22
|
+
/** Keep the calm green syncing pulse while waiting for stable synced. */
|
|
23
|
+
holdSyncingVisual?: boolean;
|
|
24
|
+
}): INetworkStatusPresentation;
|
|
25
|
+
/** Compact relative time for the status popover (always short enough for one line). */
|
|
26
|
+
export declare function formatRelativeSyncTime(timestampMs: number, nowMs?: number): string;
|
|
27
|
+
/** Prefer a friendly name; otherwise a compact middle-ellipsis of the id. */
|
|
28
|
+
export declare function connectionIdentityLabel(name: string | undefined, id: string): {
|
|
29
|
+
text: string;
|
|
30
|
+
isId: boolean;
|
|
31
|
+
title: string;
|
|
32
|
+
};
|
|
33
|
+
/** Accessible reactive network status button and details popover. */
|
|
34
|
+
export declare function NetworkStatusIndicator({ compact }: {
|
|
35
|
+
compact?: boolean;
|
|
36
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
37
|
+
/** Fixed tabs-strip area for built-in shell indicators. */
|
|
38
|
+
export declare function TabsStripIndicators({ compact }: {
|
|
39
|
+
compact?: boolean;
|
|
40
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SYNCED_STABLE_MS = exports.SYNCING_GREEN_MS = void 0;
|
|
7
|
+
exports.getNetworkStatusPresentation = getNetworkStatusPresentation;
|
|
8
|
+
exports.formatRelativeSyncTime = formatRelativeSyncTime;
|
|
9
|
+
exports.connectionIdentityLabel = connectionIdentityLabel;
|
|
10
|
+
exports.NetworkStatusIndicator = NetworkStatusIndicator;
|
|
11
|
+
exports.TabsStripIndicators = TabsStripIndicators;
|
|
12
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
13
|
+
const moment_1 = __importDefault(require("moment"));
|
|
14
|
+
const react_1 = require("react");
|
|
15
|
+
const network_viewer_ipc_1 = require("../screens/network-viewer/network-viewer-ipc");
|
|
16
|
+
const tabs_state_1 = require("./tabs-state");
|
|
17
|
+
const use_network_status_1 = require("./use-network-status");
|
|
18
|
+
/** How long continuous raw syncing stays a green pulse before the indeterminate circle. */
|
|
19
|
+
exports.SYNCING_GREEN_MS = 2_000;
|
|
20
|
+
/** How long status must stay fully synced before leaving the syncing visual. */
|
|
21
|
+
exports.SYNCED_STABLE_MS = 1_000;
|
|
22
|
+
/** Slightly slower than Bootstrap's default spinner-grow (~0.75s). */
|
|
23
|
+
const PULSE_ANIMATION_DURATION = "1.0s";
|
|
24
|
+
const DOT_SIZE = "0.7rem";
|
|
25
|
+
/** Convert host availability and network state into accessible indicator text and styling. */
|
|
26
|
+
function getNetworkStatusPresentation(viewState, opts) {
|
|
27
|
+
if (viewState.availability === "loading") {
|
|
28
|
+
return {
|
|
29
|
+
label: "Network status loading",
|
|
30
|
+
shortLabel: "Loading",
|
|
31
|
+
iconClass: "bi-arrow-repeat",
|
|
32
|
+
colorClass: "text-secondary",
|
|
33
|
+
spinning: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (viewState.availability === "unavailable" || !viewState.status) {
|
|
37
|
+
return {
|
|
38
|
+
label: "Network status unavailable",
|
|
39
|
+
shortLabel: "Unavailable",
|
|
40
|
+
iconClass: "bi-question-circle",
|
|
41
|
+
colorClass: "text-secondary",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const { connectionCount, errorCount, state, syncingContextCount } = viewState.status;
|
|
45
|
+
if (state === "error") {
|
|
46
|
+
return {
|
|
47
|
+
label: `Network error; ${errorCount} sync ${errorCount === 1 ? "error" : "errors"}; ${connectionCount} active ${connectionCount === 1 ? "connection" : "connections"}`,
|
|
48
|
+
shortLabel: "Error",
|
|
49
|
+
colorClass: "text-danger",
|
|
50
|
+
mark: "solid",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (state === "syncing" || opts?.holdSyncingVisual) {
|
|
54
|
+
const longSync = state === "syncing" && opts?.syncingLongerThanGreenMs === true;
|
|
55
|
+
const contextsLabel = state === "syncing"
|
|
56
|
+
? `${syncingContextCount} ${syncingContextCount === 1 ? "context" : "contexts"}`
|
|
57
|
+
: "recent activity";
|
|
58
|
+
return {
|
|
59
|
+
label: `Network syncing ${contextsLabel}; ${connectionCount} active ${connectionCount === 1 ? "connection" : "connections"}`,
|
|
60
|
+
shortLabel: "Syncing",
|
|
61
|
+
colorClass: longSync ? "text-primary" : "text-success",
|
|
62
|
+
mark: longSync ? "indeterminate" : "pulse",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (state === "offline") {
|
|
66
|
+
return {
|
|
67
|
+
label: "Network offline; no active connections",
|
|
68
|
+
shortLabel: "Offline",
|
|
69
|
+
iconClass: "bi-cloud-slash-fill",
|
|
70
|
+
colorClass: "text-secondary",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
label: `Network synced; ${connectionCount} active ${connectionCount === 1 ? "connection" : "connections"}`,
|
|
75
|
+
shortLabel: "Synced",
|
|
76
|
+
iconClass: "bi-check-circle-fill",
|
|
77
|
+
colorClass: "text-success",
|
|
78
|
+
compactIcon: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function StatusMark({ mode, colorClass }) {
|
|
82
|
+
if (mode === "pulse") {
|
|
83
|
+
return ((0, jsx_runtime_1.jsx)("span", { className: `spinner-grow spinner-grow-sm ${colorClass}`, style: {
|
|
84
|
+
width: DOT_SIZE,
|
|
85
|
+
height: DOT_SIZE,
|
|
86
|
+
animationDuration: PULSE_ANIMATION_DURATION,
|
|
87
|
+
}, "aria-hidden": "true" }));
|
|
88
|
+
}
|
|
89
|
+
if (mode === "indeterminate") {
|
|
90
|
+
return ((0, jsx_runtime_1.jsx)("span", { className: `spinner-border spinner-border-sm ${colorClass}`, style: { width: DOT_SIZE, height: DOT_SIZE, borderWidth: "0.12em" }, "aria-hidden": "true" }));
|
|
91
|
+
}
|
|
92
|
+
const bgClass = colorClass === "text-danger"
|
|
93
|
+
? "bg-danger"
|
|
94
|
+
: colorClass === "text-primary"
|
|
95
|
+
? "bg-primary"
|
|
96
|
+
: "bg-success";
|
|
97
|
+
return ((0, jsx_runtime_1.jsx)("span", { className: `rounded-circle ${bgClass}`, style: { width: DOT_SIZE, height: DOT_SIZE, display: "inline-block" }, "aria-hidden": "true" }));
|
|
98
|
+
}
|
|
99
|
+
/** Compact relative time for the status popover (always short enough for one line). */
|
|
100
|
+
function formatRelativeSyncTime(timestampMs, nowMs = Date.now()) {
|
|
101
|
+
const then = (0, moment_1.default)(timestampMs);
|
|
102
|
+
const now = (0, moment_1.default)(nowMs);
|
|
103
|
+
if (now.diff(then, "seconds") < 45) {
|
|
104
|
+
return "now";
|
|
105
|
+
}
|
|
106
|
+
if (now.clone().subtract(1, "day").isSame(then, "day")) {
|
|
107
|
+
return "yesterday";
|
|
108
|
+
}
|
|
109
|
+
return then.from(now);
|
|
110
|
+
}
|
|
111
|
+
function middleEllipsis(value, head = 4, tail = 4) {
|
|
112
|
+
if (value.length <= head + tail + 1) {
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
return `${value.slice(0, head)}…${value.slice(-tail)}`;
|
|
116
|
+
}
|
|
117
|
+
/** Prefer a friendly name; otherwise a compact middle-ellipsis of the id. */
|
|
118
|
+
function connectionIdentityLabel(name, id) {
|
|
119
|
+
const trimmed = name?.trim();
|
|
120
|
+
if (trimmed) {
|
|
121
|
+
return { text: trimmed, isId: false, title: trimmed };
|
|
122
|
+
}
|
|
123
|
+
return { text: middleEllipsis(id), isId: true, title: id };
|
|
124
|
+
}
|
|
125
|
+
function IdentityPart({ label }) {
|
|
126
|
+
return ((0, jsx_runtime_1.jsx)("span", { className: `text-truncate ${label.isId ? "text-muted" : ""}`, style: {
|
|
127
|
+
minWidth: 0,
|
|
128
|
+
...(label.isId ? { fontSize: "0.7rem" } : undefined),
|
|
129
|
+
}, title: label.title, children: label.text }));
|
|
130
|
+
}
|
|
131
|
+
function ConnectedDeviceRow({ connection }) {
|
|
132
|
+
const user = connectionIdentityLabel(connection.userName, connection.userId);
|
|
133
|
+
const device = connectionIdentityLabel(connection.deviceName, connection.deviceId);
|
|
134
|
+
const groupCount = connection.sharedGroups.length;
|
|
135
|
+
return ((0, jsx_runtime_1.jsxs)("li", { className: "d-flex align-items-baseline gap-1 py-1", style: { listStyle: "none", minWidth: 0 }, children: [(0, jsx_runtime_1.jsx)(IdentityPart, { label: user }), (0, jsx_runtime_1.jsx)("span", { className: "text-muted flex-shrink-0", "aria-hidden": "true", children: "\u00B7" }), (0, jsx_runtime_1.jsx)(IdentityPart, { label: device }), groupCount > 0 && ((0, jsx_runtime_1.jsxs)("span", { className: "text-muted flex-shrink-0 ms-auto", style: { fontSize: "0.7rem" }, title: `${groupCount} ${groupCount === 1 ? "group" : "groups"}`, children: [groupCount, "g"] }))] }));
|
|
136
|
+
}
|
|
137
|
+
function ConnectedDevicesList({ loadState }) {
|
|
138
|
+
if (loadState.status === "idle" || loadState.status === "loading") {
|
|
139
|
+
return (0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-2", children: "Loading\u2026" });
|
|
140
|
+
}
|
|
141
|
+
if (loadState.status === "error") {
|
|
142
|
+
return (0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-2", children: "Couldn't load devices" });
|
|
143
|
+
}
|
|
144
|
+
if (loadState.connections.length === 0) {
|
|
145
|
+
return (0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-2", children: "No connected devices" });
|
|
146
|
+
}
|
|
147
|
+
return ((0, jsx_runtime_1.jsx)("ul", { className: "small mb-0 ps-0", style: { maxHeight: "180px", overflowY: "auto" }, children: loadState.connections.map((connection) => ((0, jsx_runtime_1.jsx)(ConnectedDeviceRow, { connection: connection }, connection.deviceId))) }));
|
|
148
|
+
}
|
|
149
|
+
function NetworkStatusDetails({ detailsId, status, unavailable, connectionsLoadState, onOpenViewer, }) {
|
|
150
|
+
return ((0, jsx_runtime_1.jsx)("div", { id: detailsId, role: "dialog", "aria-label": "Network status details", className: "card shadow position-absolute end-0 mt-1", style: { top: "100%", width: "320px", zIndex: 1100 }, children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body p-3", children: [(0, jsx_runtime_1.jsx)("h6", { className: "card-title mb-2", children: "Network status" }), unavailable || !status ? ((0, jsx_runtime_1.jsx)("p", { className: "small text-muted mb-3", children: "Network diagnostics are temporarily unavailable. The local host may still be starting or reconnecting." })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("dl", { className: "row small mb-2", children: [(0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Connections" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.connectionCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Syncing contexts" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.syncingContextCount }), (0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Sync errors" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end", children: status.errorCount }), status.lastSuccessfulSyncAt != null && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("dt", { className: "col-7", children: "Last sync" }), (0, jsx_runtime_1.jsx)("dd", { className: "col-5 text-end text-truncate", title: new Date(status.lastSuccessfulSyncAt).toLocaleString(), children: formatRelativeSyncTime(status.lastSuccessfulSyncAt) })] }))] }), status.errors.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger py-2 px-2 small mb-2", children: [status.errors[0].message, status.errors.length > 1 && ` (+${status.errors.length - 1} more)`] })), (0, jsx_runtime_1.jsxs)("div", { className: "border-top pt-2 mt-1 mb-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "small text-muted mb-1", children: "Devices" }), (0, jsx_runtime_1.jsx)(ConnectedDevicesList, { loadState: connectionsLoadState })] })] })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-primary w-100", onClick: onOpenViewer, children: "Open Network Viewer" })] }) }));
|
|
151
|
+
}
|
|
152
|
+
/** Accessible reactive network status button and details popover. */
|
|
153
|
+
function NetworkStatusIndicator({ compact = false }) {
|
|
154
|
+
const viewState = (0, use_network_status_1.useNetworkStatus)();
|
|
155
|
+
const [displaySyncing, setDisplaySyncing] = (0, react_1.useState)(false);
|
|
156
|
+
const [syncingLongerThanGreenMs, setSyncingLongerThanGreenMs] = (0, react_1.useState)(false);
|
|
157
|
+
const [open, setOpen] = (0, react_1.useState)(false);
|
|
158
|
+
const [connectionsLoadState, setConnectionsLoadState] = (0, react_1.useState)({
|
|
159
|
+
status: "idle",
|
|
160
|
+
});
|
|
161
|
+
const containerRef = (0, react_1.useRef)(null);
|
|
162
|
+
const detailsId = (0, react_1.useId)();
|
|
163
|
+
const rawState = viewState.availability === "ready" ? viewState.status?.state : undefined;
|
|
164
|
+
const rawIsSyncing = rawState === "syncing";
|
|
165
|
+
const rawIsSynced = rawState === "synced";
|
|
166
|
+
const connectionCount = viewState.status?.connectionCount;
|
|
167
|
+
const diagnosticsReady = viewState.availability === "ready";
|
|
168
|
+
(0, react_1.useEffect)(() => {
|
|
169
|
+
if (rawIsSyncing) {
|
|
170
|
+
setDisplaySyncing(true);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (rawIsSynced) {
|
|
174
|
+
if (!displaySyncing) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const timer = window.setTimeout(() => {
|
|
178
|
+
setDisplaySyncing(false);
|
|
179
|
+
}, exports.SYNCED_STABLE_MS);
|
|
180
|
+
return () => {
|
|
181
|
+
window.clearTimeout(timer);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
setDisplaySyncing(false);
|
|
185
|
+
}, [rawIsSyncing, rawIsSynced, displaySyncing]);
|
|
186
|
+
(0, react_1.useEffect)(() => {
|
|
187
|
+
if (!rawIsSyncing) {
|
|
188
|
+
setSyncingLongerThanGreenMs(false);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const timer = window.setTimeout(() => {
|
|
192
|
+
setSyncingLongerThanGreenMs(true);
|
|
193
|
+
}, exports.SYNCING_GREEN_MS);
|
|
194
|
+
return () => {
|
|
195
|
+
window.clearTimeout(timer);
|
|
196
|
+
};
|
|
197
|
+
}, [rawIsSyncing]);
|
|
198
|
+
(0, react_1.useEffect)(() => {
|
|
199
|
+
if (!open || !diagnosticsReady) {
|
|
200
|
+
setConnectionsLoadState({ status: "idle" });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
let active = true;
|
|
204
|
+
setConnectionsLoadState({ status: "loading" });
|
|
205
|
+
void (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)()
|
|
206
|
+
.then((diagnostics) => diagnostics.tools.getConnections())
|
|
207
|
+
.then((result) => {
|
|
208
|
+
if (active) {
|
|
209
|
+
setConnectionsLoadState({ status: "ready", connections: result.connections });
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
.catch(() => {
|
|
213
|
+
if (active) {
|
|
214
|
+
setConnectionsLoadState({ status: "error" });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
return () => {
|
|
218
|
+
active = false;
|
|
219
|
+
};
|
|
220
|
+
}, [open, diagnosticsReady, connectionCount]);
|
|
221
|
+
const holdSyncingVisual = displaySyncing && !rawIsSyncing;
|
|
222
|
+
const presentation = getNetworkStatusPresentation(viewState, {
|
|
223
|
+
syncingLongerThanGreenMs,
|
|
224
|
+
holdSyncingVisual,
|
|
225
|
+
});
|
|
226
|
+
(0, react_1.useEffect)(() => {
|
|
227
|
+
if (!open) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const closeOutside = (event) => {
|
|
231
|
+
if (!containerRef.current?.contains(event.target)) {
|
|
232
|
+
setOpen(false);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const closeOnEscape = (event) => {
|
|
236
|
+
if (event.key === "Escape") {
|
|
237
|
+
setOpen(false);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
document.addEventListener("mousedown", closeOutside);
|
|
241
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
242
|
+
return () => {
|
|
243
|
+
document.removeEventListener("mousedown", closeOutside);
|
|
244
|
+
document.removeEventListener("keydown", closeOnEscape);
|
|
245
|
+
};
|
|
246
|
+
}, [open]);
|
|
247
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, className: "position-relative d-flex align-items-center", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `btn btn-sm d-flex align-items-center gap-1 px-2 ${presentation.colorClass}`, style: {
|
|
248
|
+
border: "none",
|
|
249
|
+
background: "transparent",
|
|
250
|
+
minWidth: compact ? "30px" : undefined,
|
|
251
|
+
}, "aria-label": presentation.label, "aria-expanded": open, "aria-controls": detailsId, title: presentation.label, onClick: () => setOpen((current) => !current), children: [presentation.mark ? ((0, jsx_runtime_1.jsx)(StatusMark, { mode: presentation.mark, colorClass: presentation.colorClass })) : ((0, jsx_runtime_1.jsx)("i", { className: `bi ${presentation.iconClass} ${presentation.spinning ? "spin" : ""}`, style: presentation.compactIcon
|
|
252
|
+
? { fontSize: DOT_SIZE, lineHeight: 1, display: "inline-block" }
|
|
253
|
+
: undefined, "aria-hidden": "true" })), !compact && ((0, jsx_runtime_1.jsxs)("span", { className: "small", children: [viewState.status?.connectionCount ?? 0, (0, jsx_runtime_1.jsx)("span", { className: "visually-hidden", children: " connections" })] }))] }), open && ((0, jsx_runtime_1.jsx)(NetworkStatusDetails, { detailsId: detailsId, status: viewState.status, unavailable: viewState.availability !== "ready", connectionsLoadState: connectionsLoadState, onOpenViewer: () => {
|
|
254
|
+
setOpen(false);
|
|
255
|
+
(0, tabs_state_1.goToTabPath)("network-viewer");
|
|
256
|
+
} }))] }));
|
|
257
|
+
}
|
|
258
|
+
/** Fixed tabs-strip area for built-in shell indicators. */
|
|
259
|
+
function TabsStripIndicators({ compact = false }) {
|
|
260
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "d-flex align-items-center flex-shrink-0", role: "group", "aria-label": "Application status", children: (0, jsx_runtime_1.jsx)(NetworkStatusIndicator, { compact: compact }) }));
|
|
261
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|