@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
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { TrustLevel } from "@peers-app/peers-sdk";
|
|
1
|
+
import { type INetworkDeviceDetails, TrustLevel } from "@peers-app/peers-sdk";
|
|
2
2
|
import { useCallback, useEffect, useState } from "react";
|
|
3
3
|
import { LoadingIndicator } from "../../components/loading-indicator";
|
|
4
4
|
import { TrustLevelBadge } from "../../components/trust-level-badge";
|
|
5
|
-
import {
|
|
5
|
+
import { getNetworkControlConsumer, getNetworkDiagnosticsConsumer } from "./network-viewer-ipc";
|
|
6
6
|
import { RemoteLogsPanel } from "./remote-logs-panel";
|
|
7
7
|
|
|
8
8
|
/** Format bytes to human-readable string (KB, MB, GB) */
|
|
@@ -24,41 +24,6 @@ function formatRate(mbps: number | undefined): string {
|
|
|
24
24
|
return `${mbps.toFixed(2)} MB/s`;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// TypeScript type definitions (matching backend)
|
|
28
|
-
interface IDeviceDetails {
|
|
29
|
-
deviceId: string;
|
|
30
|
-
userId: string;
|
|
31
|
-
userName?: string;
|
|
32
|
-
deviceName?: string;
|
|
33
|
-
trustLevel: number;
|
|
34
|
-
connectionState: "connected" | "syncing" | "idle";
|
|
35
|
-
canReadRemoteLogs: boolean;
|
|
36
|
-
sharedGroups: string[];
|
|
37
|
-
roles: Record<string, unknown>;
|
|
38
|
-
pathType: "direct" | "indirect" | "unknown";
|
|
39
|
-
throughDeviceIds?: string[];
|
|
40
|
-
networkInfo?: {
|
|
41
|
-
deviceId: string;
|
|
42
|
-
connections: Array<{ deviceId: string; userId: string }>;
|
|
43
|
-
preferredDeviceIds: string[];
|
|
44
|
-
connectionSlotsAvailable: number;
|
|
45
|
-
cpuPercent: number;
|
|
46
|
-
memPercent: number;
|
|
47
|
-
timestampLastApplied: number;
|
|
48
|
-
};
|
|
49
|
-
syncStatsByGroup: {
|
|
50
|
-
[groupId: string]: {
|
|
51
|
-
lastSyncTime: number;
|
|
52
|
-
isPreferred: boolean;
|
|
53
|
-
};
|
|
54
|
-
};
|
|
55
|
-
// Throughput stats
|
|
56
|
-
bytesSent?: number;
|
|
57
|
-
bytesReceived?: number;
|
|
58
|
-
sendRateMBps?: number;
|
|
59
|
-
receiveRateMBps?: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
27
|
interface IDeviceDetailsModalProps {
|
|
63
28
|
deviceId: string;
|
|
64
29
|
onClose: () => void;
|
|
@@ -67,22 +32,14 @@ interface IDeviceDetailsModalProps {
|
|
|
67
32
|
|
|
68
33
|
/** Show connection, sync, topology, and provider-authorized remote-log diagnostics for one device. */
|
|
69
34
|
export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceDetailsModalProps) {
|
|
70
|
-
const [device, setDevice] = useState<
|
|
35
|
+
const [device, setDevice] = useState<INetworkDeviceDetails | null>(null);
|
|
71
36
|
const [loading, setLoading] = useState(true);
|
|
72
37
|
const [notFound, setNotFound] = useState(false);
|
|
73
38
|
|
|
74
39
|
const loadData = useCallback(async () => {
|
|
75
40
|
try {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
console.warn("Network Viewer API not available");
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const deviceData = (await api.getDeviceDetails(deviceId)) as
|
|
83
|
-
| IDeviceDetails
|
|
84
|
-
| null
|
|
85
|
-
| undefined;
|
|
41
|
+
const diagnostics = await getNetworkDiagnosticsConsumer();
|
|
42
|
+
const { device: deviceData } = await diagnostics.tools.getDeviceDetails({ deviceId });
|
|
86
43
|
|
|
87
44
|
if (!deviceData) {
|
|
88
45
|
setNotFound(true);
|
|
@@ -104,14 +61,14 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
|
|
|
104
61
|
}
|
|
105
62
|
|
|
106
63
|
try {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
onClose();
|
|
64
|
+
const control = await getNetworkControlConsumer();
|
|
65
|
+
const result = await control.tools.disconnectDevice({ deviceId });
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
alert(result.message);
|
|
68
|
+
return;
|
|
114
69
|
}
|
|
70
|
+
onDisconnect?.();
|
|
71
|
+
onClose();
|
|
115
72
|
} catch (error) {
|
|
116
73
|
console.error("Error disconnecting device:", error);
|
|
117
74
|
}
|
|
@@ -197,9 +154,14 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
|
|
|
197
154
|
<span className="badge bg-success">{device.connectionState}</span>
|
|
198
155
|
</div>
|
|
199
156
|
<div className="col-md-6 mb-3">
|
|
200
|
-
<strong>Trust
|
|
157
|
+
<strong>User Trust:</strong>
|
|
201
158
|
<br />
|
|
202
|
-
<TrustLevelBadge level={device.
|
|
159
|
+
<TrustLevelBadge level={device.userTrustLevel || TrustLevel.Unknown} />
|
|
160
|
+
</div>
|
|
161
|
+
<div className="col-md-6 mb-3">
|
|
162
|
+
<strong>Device Trust:</strong>
|
|
163
|
+
<br />
|
|
164
|
+
<TrustLevelBadge level={device.deviceTrustLevel || TrustLevel.Unknown} />
|
|
203
165
|
</div>
|
|
204
166
|
<div className="col-md-6 mb-3">
|
|
205
167
|
<strong>Path Type:</strong>
|
|
@@ -345,7 +307,6 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
|
|
|
345
307
|
<thead>
|
|
346
308
|
<tr>
|
|
347
309
|
<th>Device ID</th>
|
|
348
|
-
<th>User ID</th>
|
|
349
310
|
</tr>
|
|
350
311
|
</thead>
|
|
351
312
|
<tbody>
|
|
@@ -354,9 +315,6 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
|
|
|
354
315
|
<td>
|
|
355
316
|
<code className="text-muted small">{conn.deviceId}</code>
|
|
356
317
|
</td>
|
|
357
|
-
<td>
|
|
358
|
-
<code className="text-muted small">{conn.userId}</code>
|
|
359
|
-
</td>
|
|
360
318
|
</tr>
|
|
361
319
|
))}
|
|
362
320
|
</tbody>
|
|
@@ -1,38 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type INetworkGroupDetails,
|
|
3
|
+
type INetworkGroupSyncInfo,
|
|
4
|
+
TrustLevel,
|
|
5
|
+
} from "@peers-app/peers-sdk";
|
|
2
6
|
import { useCallback, useEffect, useState } from "react";
|
|
3
7
|
import { LoadingIndicator } from "../../components/loading-indicator";
|
|
4
8
|
import { TrustLevelBadge } from "../../components/trust-level-badge";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
groupName: string;
|
|
11
|
-
founderUserId: string;
|
|
12
|
-
founderUserName?: string;
|
|
13
|
-
members: {
|
|
14
|
-
userId: string;
|
|
15
|
-
userName?: string;
|
|
16
|
-
role: string;
|
|
17
|
-
trustLevel: TrustLevel;
|
|
18
|
-
}[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface IConnectedDevice {
|
|
22
|
-
deviceId: string;
|
|
23
|
-
latencyMs: number;
|
|
24
|
-
errorRate: number;
|
|
25
|
-
timestampLastApplied: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface IGroupSyncInfo {
|
|
29
|
-
groupId: string;
|
|
30
|
-
groupName: string;
|
|
31
|
-
connectedDevices: IConnectedDevice[];
|
|
32
|
-
preferredDeviceIds: string[];
|
|
33
|
-
lastSyncTimestamp: number;
|
|
34
|
-
totalMembers: number;
|
|
35
|
-
}
|
|
9
|
+
import {
|
|
10
|
+
getNetworkControlConsumer,
|
|
11
|
+
getNetworkDiagnosticsConsumer,
|
|
12
|
+
getNetworkViewerPlatformApi,
|
|
13
|
+
} from "./network-viewer-ipc";
|
|
36
14
|
|
|
37
15
|
interface IGroupDetailsModalProps {
|
|
38
16
|
groupId: string;
|
|
@@ -40,8 +18,8 @@ interface IGroupDetailsModalProps {
|
|
|
40
18
|
}
|
|
41
19
|
|
|
42
20
|
export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps) {
|
|
43
|
-
const [group, setGroup] = useState<
|
|
44
|
-
const [syncInfo, setSyncInfo] = useState<
|
|
21
|
+
const [group, setGroup] = useState<INetworkGroupDetails | null>(null);
|
|
22
|
+
const [syncInfo, setSyncInfo] = useState<INetworkGroupSyncInfo | null>(null);
|
|
45
23
|
const [loading, setLoading] = useState(true);
|
|
46
24
|
const [notFound, setNotFound] = useState(false);
|
|
47
25
|
const [downloading, setDownloading] = useState(false);
|
|
@@ -52,21 +30,26 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
52
30
|
filePath?: string;
|
|
53
31
|
size?: number;
|
|
54
32
|
} | null>(null);
|
|
33
|
+
const [fullSyncing, setFullSyncing] = useState(false);
|
|
34
|
+
const [fullSyncResult, setFullSyncResult] = useState<{
|
|
35
|
+
success: boolean;
|
|
36
|
+
message: string;
|
|
37
|
+
} | null>(null);
|
|
38
|
+
const [syncingNow, setSyncingNow] = useState(false);
|
|
39
|
+
const [syncNowResult, setSyncNowResult] = useState<{
|
|
40
|
+
success: boolean;
|
|
41
|
+
message: string;
|
|
42
|
+
} | null>(null);
|
|
43
|
+
const platformApi = getNetworkViewerPlatformApi();
|
|
55
44
|
|
|
56
45
|
const loadData = useCallback(async () => {
|
|
57
46
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const [groupData, syncDataUnknown] = await Promise.all([
|
|
65
|
-
api.getGroupDetails(groupId),
|
|
66
|
-
api.getGroupSyncStatus(groupId),
|
|
47
|
+
const diagnostics = await getNetworkDiagnosticsConsumer();
|
|
48
|
+
const [{ group: groupDetails }, { groups }] = await Promise.all([
|
|
49
|
+
diagnostics.tools.getGroupDetails({ groupId }),
|
|
50
|
+
diagnostics.tools.getGroupSyncStatus({ groupId }),
|
|
67
51
|
]);
|
|
68
52
|
|
|
69
|
-
const groupDetails = groupData as IGroupDetails | null | undefined;
|
|
70
53
|
if (!groupDetails) {
|
|
71
54
|
setNotFound(true);
|
|
72
55
|
} else {
|
|
@@ -74,9 +57,8 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
74
57
|
setNotFound(false);
|
|
75
58
|
}
|
|
76
59
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
setSyncInfo(syncData[0]);
|
|
60
|
+
if (groups.length > 0) {
|
|
61
|
+
setSyncInfo(groups[0]);
|
|
80
62
|
}
|
|
81
63
|
} catch (error) {
|
|
82
64
|
console.error("Error loading group details:", error);
|
|
@@ -92,8 +74,7 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
92
74
|
setDownloadProgress("Preparing download...");
|
|
93
75
|
setDownloadResult(null);
|
|
94
76
|
|
|
95
|
-
|
|
96
|
-
if (!api?.downloadRemoteDatabase) {
|
|
77
|
+
if (!platformApi?.downloadRemoteDatabase) {
|
|
97
78
|
setDownloadResult({
|
|
98
79
|
success: false,
|
|
99
80
|
message: "Download API not available",
|
|
@@ -102,7 +83,7 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
102
83
|
}
|
|
103
84
|
|
|
104
85
|
setDownloadProgress("Downloading database...");
|
|
105
|
-
const result = (await
|
|
86
|
+
const result = (await platformApi.downloadRemoteDatabase(groupId, deviceId)) as {
|
|
106
87
|
success: boolean;
|
|
107
88
|
message: string;
|
|
108
89
|
filePath?: string;
|
|
@@ -125,6 +106,53 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
125
106
|
}
|
|
126
107
|
};
|
|
127
108
|
|
|
109
|
+
const handleSyncNow = async () => {
|
|
110
|
+
setSyncingNow(true);
|
|
111
|
+
setSyncNowResult(null);
|
|
112
|
+
try {
|
|
113
|
+
const control = await getNetworkControlConsumer();
|
|
114
|
+
const result = await control.tools.syncGroupNow({ groupId });
|
|
115
|
+
setSyncNowResult(result);
|
|
116
|
+
if (result.success) {
|
|
117
|
+
await loadData();
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
setSyncNowResult({
|
|
121
|
+
success: false,
|
|
122
|
+
message: error instanceof Error ? error.message : String(error),
|
|
123
|
+
});
|
|
124
|
+
} finally {
|
|
125
|
+
setSyncingNow(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const handleFullSync = async () => {
|
|
130
|
+
if (
|
|
131
|
+
!confirm(
|
|
132
|
+
`Do a full sync for "${group?.groupName || groupId}"? This resets local change tracking for only this context and may transfer substantial data.`,
|
|
133
|
+
)
|
|
134
|
+
) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
setFullSyncing(true);
|
|
138
|
+
setFullSyncResult(null);
|
|
139
|
+
try {
|
|
140
|
+
const control = await getNetworkControlConsumer();
|
|
141
|
+
const result = await control.tools.resetChangeTracking({ dataContextId: groupId });
|
|
142
|
+
setFullSyncResult(result);
|
|
143
|
+
if (result.success) {
|
|
144
|
+
await loadData();
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
setFullSyncResult({
|
|
148
|
+
success: false,
|
|
149
|
+
message: error instanceof Error ? error.message : String(error),
|
|
150
|
+
});
|
|
151
|
+
} finally {
|
|
152
|
+
setFullSyncing(false);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
128
156
|
useEffect(() => {
|
|
129
157
|
loadData();
|
|
130
158
|
}, [loadData]);
|
|
@@ -239,98 +267,151 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
|
|
|
239
267
|
</div>
|
|
240
268
|
)}
|
|
241
269
|
|
|
242
|
-
{/* Database Download Section */}
|
|
243
270
|
<div className="mb-4">
|
|
244
271
|
<h6 className="border-bottom pb-2 mb-3">
|
|
245
|
-
<i className="bi bi-
|
|
246
|
-
|
|
272
|
+
<i className="bi bi-arrow-repeat me-2"></i>
|
|
273
|
+
Synchronization
|
|
247
274
|
</h6>
|
|
248
275
|
<p className="text-muted small mb-3">
|
|
249
|
-
|
|
250
|
-
|
|
276
|
+
<strong>Sync now</strong> notifies connected peers that this context may have
|
|
277
|
+
changes. <strong>Do full sync</strong> resets change tracking for this context and
|
|
278
|
+
rebuilds synchronization state; that may transfer substantial data and should be
|
|
279
|
+
used only to repair suspected drift.
|
|
251
280
|
</p>
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
{device.deviceId.substring(0, 8)}...
|
|
277
|
-
<span className="badge bg-secondary ms-1">
|
|
278
|
-
{Math.round(device.latencyMs)}ms
|
|
279
|
-
</span>
|
|
280
|
-
</>
|
|
281
|
-
)}
|
|
282
|
-
</button>
|
|
283
|
-
))}
|
|
284
|
-
</div>
|
|
285
|
-
</div>
|
|
286
|
-
) : (
|
|
287
|
-
<div className="alert alert-info mb-0">
|
|
288
|
-
<i className="bi bi-info-circle me-2"></i>
|
|
289
|
-
No devices are currently connected to this group. Connect to a device first to
|
|
290
|
-
download its database.
|
|
291
|
-
</div>
|
|
292
|
-
)}
|
|
293
|
-
|
|
294
|
-
{downloadProgress && (
|
|
295
|
-
<div className="mt-3">
|
|
296
|
-
<div className="d-flex align-items-center">
|
|
297
|
-
<span
|
|
298
|
-
className="spinner-border spinner-border-sm me-2"
|
|
299
|
-
role="status"
|
|
300
|
-
aria-hidden="true"
|
|
301
|
-
></span>
|
|
302
|
-
<span>{downloadProgress}</span>
|
|
303
|
-
</div>
|
|
281
|
+
<div className="d-flex flex-wrap gap-2">
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
className="btn btn-sm btn-outline-primary"
|
|
285
|
+
onClick={() => void handleSyncNow()}
|
|
286
|
+
disabled={syncingNow || fullSyncing}
|
|
287
|
+
>
|
|
288
|
+
{syncingNow ? "Sending sync notification..." : "Sync now"}
|
|
289
|
+
</button>
|
|
290
|
+
<button
|
|
291
|
+
type="button"
|
|
292
|
+
className="btn btn-sm btn-outline-warning"
|
|
293
|
+
onClick={() => void handleFullSync()}
|
|
294
|
+
disabled={fullSyncing || syncingNow}
|
|
295
|
+
>
|
|
296
|
+
{fullSyncing ? "Starting full sync..." : "Do full sync"}
|
|
297
|
+
</button>
|
|
298
|
+
</div>
|
|
299
|
+
{syncNowResult && (
|
|
300
|
+
<div
|
|
301
|
+
className={`alert mt-3 mb-0 ${syncNowResult.success ? "alert-success" : "alert-danger"}`}
|
|
302
|
+
role="status"
|
|
303
|
+
>
|
|
304
|
+
{syncNowResult.message}
|
|
304
305
|
</div>
|
|
305
306
|
)}
|
|
306
|
-
|
|
307
|
-
{downloadResult && (
|
|
307
|
+
{fullSyncResult && (
|
|
308
308
|
<div
|
|
309
|
-
className={`alert mt-3 mb-0 ${
|
|
309
|
+
className={`alert mt-3 mb-0 ${fullSyncResult.success ? "alert-success" : "alert-danger"}`}
|
|
310
|
+
role="status"
|
|
310
311
|
>
|
|
311
|
-
{
|
|
312
|
-
<>
|
|
313
|
-
<i className="bi bi-check-circle me-2"></i>
|
|
314
|
-
<strong>Download complete!</strong>
|
|
315
|
-
<br />
|
|
316
|
-
<small>
|
|
317
|
-
File: <code>{downloadResult.filePath}</code>
|
|
318
|
-
<br />
|
|
319
|
-
Size:{" "}
|
|
320
|
-
{downloadResult.size
|
|
321
|
-
? `${(downloadResult.size / (1024 * 1024)).toFixed(2)} MB`
|
|
322
|
-
: "Unknown"}
|
|
323
|
-
</small>
|
|
324
|
-
</>
|
|
325
|
-
) : (
|
|
326
|
-
<>
|
|
327
|
-
<i className="bi bi-exclamation-circle me-2"></i>
|
|
328
|
-
<strong>Download failed:</strong> {downloadResult.message}
|
|
329
|
-
</>
|
|
330
|
-
)}
|
|
312
|
+
{fullSyncResult.message}
|
|
331
313
|
</div>
|
|
332
314
|
)}
|
|
333
315
|
</div>
|
|
316
|
+
|
|
317
|
+
{/* Database Download Section */}
|
|
318
|
+
{platformApi?.downloadRemoteDatabase ? (
|
|
319
|
+
<div className="mb-4">
|
|
320
|
+
<h6 className="border-bottom pb-2 mb-3">
|
|
321
|
+
<i className="bi bi-download me-2"></i>
|
|
322
|
+
Download Database (Fast Sync)
|
|
323
|
+
</h6>
|
|
324
|
+
<p className="text-muted small mb-3">
|
|
325
|
+
Download the entire database from a connected device. This is useful for fast
|
|
326
|
+
initial sync on a new device. The database will be saved as a backup file.
|
|
327
|
+
</p>
|
|
328
|
+
|
|
329
|
+
{syncInfo && syncInfo.connectedDevices.length > 0 ? (
|
|
330
|
+
<div>
|
|
331
|
+
<label className="form-label">Select a connected device:</label>
|
|
332
|
+
<div className="d-flex flex-wrap gap-2">
|
|
333
|
+
{syncInfo.connectedDevices.map((device) => (
|
|
334
|
+
<button
|
|
335
|
+
key={device.deviceId}
|
|
336
|
+
className="btn btn-outline-primary btn-sm"
|
|
337
|
+
onClick={() => handleDownloadDatabase(device.deviceId)}
|
|
338
|
+
disabled={downloading}
|
|
339
|
+
>
|
|
340
|
+
{downloading ? (
|
|
341
|
+
<>
|
|
342
|
+
<span
|
|
343
|
+
className="spinner-border spinner-border-sm me-1"
|
|
344
|
+
role="status"
|
|
345
|
+
aria-hidden="true"
|
|
346
|
+
></span>
|
|
347
|
+
Downloading...
|
|
348
|
+
</>
|
|
349
|
+
) : (
|
|
350
|
+
<>
|
|
351
|
+
<i className="bi bi-cloud-download me-1"></i>
|
|
352
|
+
{device.deviceId.substring(0, 8)}...
|
|
353
|
+
<span className="badge bg-secondary ms-1">
|
|
354
|
+
{Math.round(device.latencyMs)}ms
|
|
355
|
+
</span>
|
|
356
|
+
</>
|
|
357
|
+
)}
|
|
358
|
+
</button>
|
|
359
|
+
))}
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
) : (
|
|
363
|
+
<div className="alert alert-info mb-0">
|
|
364
|
+
<i className="bi bi-info-circle me-2"></i>
|
|
365
|
+
No devices are currently connected to this group. Connect to a device first to
|
|
366
|
+
download its database.
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
|
|
370
|
+
{downloadProgress && (
|
|
371
|
+
<div className="mt-3">
|
|
372
|
+
<div className="d-flex align-items-center">
|
|
373
|
+
<span
|
|
374
|
+
className="spinner-border spinner-border-sm me-2"
|
|
375
|
+
role="status"
|
|
376
|
+
aria-hidden="true"
|
|
377
|
+
></span>
|
|
378
|
+
<span>{downloadProgress}</span>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{downloadResult && (
|
|
384
|
+
<div
|
|
385
|
+
className={`alert mt-3 mb-0 ${downloadResult.success ? "alert-success" : "alert-danger"}`}
|
|
386
|
+
>
|
|
387
|
+
{downloadResult.success ? (
|
|
388
|
+
<>
|
|
389
|
+
<i className="bi bi-check-circle me-2"></i>
|
|
390
|
+
<strong>Download complete!</strong>
|
|
391
|
+
<br />
|
|
392
|
+
<small>
|
|
393
|
+
File: <code>{downloadResult.filePath}</code>
|
|
394
|
+
<br />
|
|
395
|
+
Size:{" "}
|
|
396
|
+
{downloadResult.size
|
|
397
|
+
? `${(downloadResult.size / (1024 * 1024)).toFixed(2)} MB`
|
|
398
|
+
: "Unknown"}
|
|
399
|
+
</small>
|
|
400
|
+
</>
|
|
401
|
+
) : (
|
|
402
|
+
<>
|
|
403
|
+
<i className="bi bi-exclamation-circle me-2"></i>
|
|
404
|
+
<strong>Download failed:</strong> {downloadResult.message}
|
|
405
|
+
</>
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
410
|
+
) : (
|
|
411
|
+
<div className="alert alert-info mb-4">
|
|
412
|
+
Raw database download is available only in the Electron desktop app.
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
334
415
|
</>
|
|
335
416
|
)}
|
|
336
417
|
</div>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONTRACT_REQUEST_CHANNEL,
|
|
3
|
+
inProcessTransportPair,
|
|
4
|
+
newid,
|
|
5
|
+
registerHostContractTransport,
|
|
6
|
+
} from "@peers-app/peers-sdk";
|
|
7
|
+
import { getNetworkControlConsumer, getNetworkDiagnosticsConsumer } from "./network-viewer-ipc";
|
|
8
|
+
|
|
9
|
+
function setupHost(label: string) {
|
|
10
|
+
const [consumerTransport, providerTransport] = inProcessTransportPair();
|
|
11
|
+
providerTransport.on(CONTRACT_REQUEST_CHANNEL, (call) => {
|
|
12
|
+
if (call.member === "observable" && call.name === "status" && call.operation === "get") {
|
|
13
|
+
return {
|
|
14
|
+
state: "offline",
|
|
15
|
+
connectionCount: 0,
|
|
16
|
+
syncingContextCount: 0,
|
|
17
|
+
errorCount: 0,
|
|
18
|
+
errors: [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (call.member === "tool" && call.name === "getOverview") {
|
|
22
|
+
return { overview: { localDeviceId: label } };
|
|
23
|
+
}
|
|
24
|
+
if (call.member === "tool" && call.name === "refreshNetworkInfo") {
|
|
25
|
+
return { success: true, message: label };
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
dispose: registerHostContractTransport(consumerTransport),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("Network Viewer host consumers", () => {
|
|
35
|
+
it("caches consumers for one transport and recreates them after replacement", async () => {
|
|
36
|
+
const firstHost = setupHost(newid());
|
|
37
|
+
const firstDiagnostics = await getNetworkDiagnosticsConsumer();
|
|
38
|
+
expect(await getNetworkDiagnosticsConsumer()).toBe(firstDiagnostics);
|
|
39
|
+
|
|
40
|
+
const secondLabel = newid();
|
|
41
|
+
const secondHost = setupHost(secondLabel);
|
|
42
|
+
const secondDiagnostics = await getNetworkDiagnosticsConsumer();
|
|
43
|
+
const control = await getNetworkControlConsumer();
|
|
44
|
+
|
|
45
|
+
expect(secondDiagnostics).not.toBe(firstDiagnostics);
|
|
46
|
+
await expect(firstDiagnostics.tools.getOverview()).rejects.toThrow("disposed");
|
|
47
|
+
await expect(control.tools.refreshNetworkInfo()).resolves.toEqual({
|
|
48
|
+
success: true,
|
|
49
|
+
message: secondLabel,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
secondHost.dispose();
|
|
53
|
+
firstHost.dispose();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("shares one consumer pair across concurrent callers", async () => {
|
|
57
|
+
const host = setupHost(newid());
|
|
58
|
+
const [first, second, third] = await Promise.all([
|
|
59
|
+
getNetworkDiagnosticsConsumer(),
|
|
60
|
+
getNetworkDiagnosticsConsumer(),
|
|
61
|
+
getNetworkControlConsumer(),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
expect(second).toBe(first);
|
|
65
|
+
const cachedControl = await getNetworkControlConsumer();
|
|
66
|
+
expect(third).toBe(cachedControl);
|
|
67
|
+
|
|
68
|
+
host.dispose();
|
|
69
|
+
});
|
|
70
|
+
});
|