@peers-app/peers-ui 0.21.2 → 0.21.3

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.
Files changed (31) hide show
  1. package/dist/screens/network-viewer/connection-troubleshooter.js +1 -1
  2. package/dist/screens/network-viewer/device-details-modal.js +11 -15
  3. package/dist/screens/network-viewer/group-details-modal.js +60 -17
  4. package/dist/screens/network-viewer/network-viewer-ipc.d.ts +9 -39
  5. package/dist/screens/network-viewer/network-viewer-ipc.js +71 -3
  6. package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +1 -0
  7. package/dist/screens/network-viewer/network-viewer-ipc.test.js +59 -0
  8. package/dist/screens/network-viewer/network-viewer.js +46 -60
  9. package/dist/screens/network-viewer/remote-logs-panel.js +11 -12
  10. package/dist/tabs-layout/network-status-indicator.d.ts +19 -0
  11. package/dist/tabs-layout/network-status-indicator.js +105 -0
  12. package/dist/tabs-layout/network-status-indicator.test.d.ts +1 -0
  13. package/dist/tabs-layout/network-status-indicator.test.js +81 -0
  14. package/dist/tabs-layout/tabs-layout.js +25 -24
  15. package/dist/tabs-layout/use-network-status.d.ts +8 -0
  16. package/dist/tabs-layout/use-network-status.js +57 -0
  17. package/dist/tabs-layout/use-network-status.test.d.ts +1 -0
  18. package/dist/tabs-layout/use-network-status.test.js +42 -0
  19. package/package.json +3 -3
  20. package/src/screens/network-viewer/connection-troubleshooter.tsx +2 -2
  21. package/src/screens/network-viewer/device-details-modal.tsx +19 -61
  22. package/src/screens/network-viewer/group-details-modal.tsx +211 -130
  23. package/src/screens/network-viewer/network-viewer-ipc.test.ts +70 -0
  24. package/src/screens/network-viewer/network-viewer-ipc.ts +102 -35
  25. package/src/screens/network-viewer/network-viewer.tsx +76 -148
  26. package/src/screens/network-viewer/remote-logs-panel.tsx +14 -16
  27. package/src/tabs-layout/network-status-indicator.test.tsx +94 -0
  28. package/src/tabs-layout/network-status-indicator.tsx +209 -0
  29. package/src/tabs-layout/tabs-layout.tsx +4 -1
  30. package/src/tabs-layout/use-network-status.test.tsx +55 -0
  31. 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
- if (!(0, globals_1.isDesktop)())
51
- return;
52
- const api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
53
- if (!api) {
54
- console.warn("Network Viewer API not available");
55
- return;
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 api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
79
- if (api) {
80
- await api.refreshNetworkInfo();
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 api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
97
- if (api) {
98
- await api.disconnectDevice(deviceId);
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 api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
112
- if (api) {
113
- setConnecting((prev) => new Set(prev).add(deviceId));
114
- const result = await api.connectToDevice(deviceId);
115
- if (result.success) {
116
- // Refresh data to see if connection was established
117
- await loadData();
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 api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
139
- if (api) {
140
- const result = await api.deleteDevice(deviceId);
141
- if (result.success) {
142
- // Refresh data to remove the device from the list
143
- await loadData();
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 api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
175
- if (!api)
176
- return 0;
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 api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
185
- if (!api)
186
- return 0;
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 api = (0, network_viewer_ipc_1.getNetworkViewerApi)();
195
- if (!api)
196
- return 0;
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: ", new Date(device.timestampLastApplied).toLocaleString()] })] }), 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
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 response = await api.getRemoteLogs(deviceId, {
33
- sinceTimestamp,
34
- level: level ? level : undefined,
35
- process: processName || undefined,
36
- search: search || undefined,
37
- pageSize: 100,
38
- timeoutMs: 10_000,
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,19 @@
1
+ import { type INetworkStatusViewState } from "./use-network-status";
2
+ interface INetworkStatusPresentation {
3
+ label: string;
4
+ shortLabel: string;
5
+ iconClass: string;
6
+ colorClass: string;
7
+ spinning?: boolean;
8
+ }
9
+ /** Convert host availability and network state into accessible indicator text and styling. */
10
+ export declare function getNetworkStatusPresentation(viewState: INetworkStatusViewState): INetworkStatusPresentation;
11
+ /** Accessible reactive network status button and details popover. */
12
+ export declare function NetworkStatusIndicator({ compact }: {
13
+ compact?: boolean;
14
+ }): import("react/jsx-runtime").JSX.Element;
15
+ /** Fixed tabs-strip area for built-in shell indicators. */
16
+ export declare function TabsStripIndicators({ compact }: {
17
+ compact?: boolean;
18
+ }): import("react/jsx-runtime").JSX.Element;
19
+ export {};
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getNetworkStatusPresentation = getNetworkStatusPresentation;
4
+ exports.NetworkStatusIndicator = NetworkStatusIndicator;
5
+ exports.TabsStripIndicators = TabsStripIndicators;
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
7
+ const react_1 = require("react");
8
+ const tabs_state_1 = require("./tabs-state");
9
+ const use_network_status_1 = require("./use-network-status");
10
+ /** Convert host availability and network state into accessible indicator text and styling. */
11
+ function getNetworkStatusPresentation(viewState) {
12
+ if (viewState.availability === "loading") {
13
+ return {
14
+ label: "Network status loading",
15
+ shortLabel: "Loading",
16
+ iconClass: "bi-arrow-repeat",
17
+ colorClass: "text-secondary",
18
+ spinning: true,
19
+ };
20
+ }
21
+ if (viewState.availability === "unavailable" || !viewState.status) {
22
+ return {
23
+ label: "Network status unavailable",
24
+ shortLabel: "Unavailable",
25
+ iconClass: "bi-question-circle",
26
+ colorClass: "text-secondary",
27
+ };
28
+ }
29
+ const { connectionCount, errorCount, state, syncingContextCount } = viewState.status;
30
+ if (state === "error") {
31
+ return {
32
+ label: `Network error; ${errorCount} sync ${errorCount === 1 ? "error" : "errors"}; ${connectionCount} active ${connectionCount === 1 ? "connection" : "connections"}`,
33
+ shortLabel: "Error",
34
+ iconClass: "bi-exclamation-triangle-fill",
35
+ colorClass: "text-danger",
36
+ };
37
+ }
38
+ if (state === "syncing") {
39
+ return {
40
+ label: `Network syncing ${syncingContextCount} ${syncingContextCount === 1 ? "context" : "contexts"}; ${connectionCount} active ${connectionCount === 1 ? "connection" : "connections"}`,
41
+ shortLabel: "Syncing",
42
+ iconClass: "bi-arrow-repeat",
43
+ colorClass: "text-primary",
44
+ spinning: true,
45
+ };
46
+ }
47
+ if (state === "offline") {
48
+ return {
49
+ label: "Network offline; no active connections",
50
+ shortLabel: "Offline",
51
+ iconClass: "bi-cloud-slash-fill",
52
+ colorClass: "text-secondary",
53
+ };
54
+ }
55
+ return {
56
+ label: `Network synced; ${connectionCount} active ${connectionCount === 1 ? "connection" : "connections"}`,
57
+ shortLabel: "Synced",
58
+ iconClass: "bi-check-circle-fill",
59
+ colorClass: "text-success",
60
+ };
61
+ }
62
+ function NetworkStatusDetails({ detailsId, status, unavailable, onOpenViewer, }) {
63
+ 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: "280px", 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 && ((0, jsx_runtime_1.jsxs)("p", { className: "small text-muted mb-2", children: ["Last successful sync: ", new Date(status.lastSuccessfulSyncAt).toLocaleString()] })), 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.jsx)("button", { type: "button", className: "btn btn-sm btn-primary w-100", onClick: onOpenViewer, children: "Open Network Viewer" })] }) }));
64
+ }
65
+ /** Accessible reactive network status button and details popover. */
66
+ function NetworkStatusIndicator({ compact = false }) {
67
+ const viewState = (0, use_network_status_1.useNetworkStatus)();
68
+ const presentation = getNetworkStatusPresentation(viewState);
69
+ const [open, setOpen] = (0, react_1.useState)(false);
70
+ const containerRef = (0, react_1.useRef)(null);
71
+ const detailsId = (0, react_1.useId)();
72
+ (0, react_1.useEffect)(() => {
73
+ if (!open) {
74
+ return;
75
+ }
76
+ const closeOutside = (event) => {
77
+ if (!containerRef.current?.contains(event.target)) {
78
+ setOpen(false);
79
+ }
80
+ };
81
+ const closeOnEscape = (event) => {
82
+ if (event.key === "Escape") {
83
+ setOpen(false);
84
+ }
85
+ };
86
+ document.addEventListener("mousedown", closeOutside);
87
+ document.addEventListener("keydown", closeOnEscape);
88
+ return () => {
89
+ document.removeEventListener("mousedown", closeOutside);
90
+ document.removeEventListener("keydown", closeOnEscape);
91
+ };
92
+ }, [open]);
93
+ 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: {
94
+ border: "none",
95
+ background: "transparent",
96
+ minWidth: compact ? "30px" : undefined,
97
+ }, "aria-label": presentation.label, "aria-expanded": open, "aria-controls": detailsId, title: presentation.label, onClick: () => setOpen((current) => !current), children: [(0, jsx_runtime_1.jsx)("i", { className: `bi ${presentation.iconClass} ${presentation.spinning ? "spin" : ""}`, "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", onOpenViewer: () => {
98
+ setOpen(false);
99
+ (0, tabs_state_1.goToTabPath)("network-viewer");
100
+ } }))] }));
101
+ }
102
+ /** Fixed tabs-strip area for built-in shell indicators. */
103
+ function TabsStripIndicators({ compact = false }) {
104
+ 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 }) }));
105
+ }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const jsx_runtime_1 = require("react/jsx-runtime");
4
+ const react_1 = require("@testing-library/react");
5
+ const network_status_indicator_1 = require("./network-status-indicator");
6
+ const goToTabPath = jest.fn();
7
+ let mockViewState = { availability: "loading" };
8
+ jest.mock("./tabs-state", () => ({
9
+ goToTabPath: (...args) => goToTabPath(...args),
10
+ }));
11
+ jest.mock("./use-network-status", () => ({
12
+ useNetworkStatus: () => mockViewState,
13
+ }));
14
+ function status(overrides) {
15
+ return {
16
+ state: "synced",
17
+ connectionCount: 2,
18
+ syncingContextCount: 0,
19
+ errorCount: 0,
20
+ errors: [],
21
+ ...overrides,
22
+ };
23
+ }
24
+ describe("NetworkStatusIndicator", () => {
25
+ beforeEach(() => {
26
+ mockViewState = { availability: "loading" };
27
+ jest.clearAllMocks();
28
+ });
29
+ it.each([
30
+ [{ availability: "loading" }, /Network status loading/],
31
+ [{ availability: "unavailable" }, /Network status unavailable/],
32
+ [
33
+ { availability: "ready", status: status({ state: "offline", connectionCount: 0 }) },
34
+ /Network offline/,
35
+ ],
36
+ [
37
+ {
38
+ availability: "ready",
39
+ status: status({ state: "syncing", syncingContextCount: 1 }),
40
+ },
41
+ /Network syncing/,
42
+ ],
43
+ [{ availability: "ready", status: status({ state: "synced" }) }, /Network synced/],
44
+ [
45
+ {
46
+ availability: "ready",
47
+ status: status({
48
+ state: "error",
49
+ errorCount: 1,
50
+ errors: [{ dataContextId: "context", message: "failed", occurredAt: 1 }],
51
+ }),
52
+ },
53
+ /Network error/,
54
+ ],
55
+ ])("renders an accessible state for %#", (viewState, accessibleName) => {
56
+ mockViewState = viewState;
57
+ const view = (0, react_1.render)((0, jsx_runtime_1.jsx)(network_status_indicator_1.NetworkStatusIndicator, {}));
58
+ expect(view.getByRole("button", { name: accessibleName })).toBeTruthy();
59
+ });
60
+ it("updates on rerender and opens details before navigating to Network Viewer", () => {
61
+ const view = (0, react_1.render)((0, jsx_runtime_1.jsx)(network_status_indicator_1.NetworkStatusIndicator, {}));
62
+ expect(view.getByRole("button", { name: /loading/ })).toBeTruthy();
63
+ mockViewState = {
64
+ availability: "ready",
65
+ status: status({
66
+ state: "error",
67
+ connectionCount: 3,
68
+ errorCount: 1,
69
+ errors: [{ dataContextId: "context", message: "sync failed", occurredAt: 1 }],
70
+ }),
71
+ };
72
+ view.rerender((0, jsx_runtime_1.jsx)(network_status_indicator_1.NetworkStatusIndicator, {}));
73
+ react_1.fireEvent.click(view.getByRole("button", { name: /Network error/ }));
74
+ expect(view.getByRole("dialog", { name: "Network status details" })).toHaveTextContent("sync failed");
75
+ react_1.fireEvent.keyDown(document, { key: "Escape" });
76
+ expect(view.queryByRole("dialog", { name: "Network status details" })).toBeNull();
77
+ react_1.fireEvent.click(view.getByRole("button", { name: /Network error/ }));
78
+ react_1.fireEvent.click(view.getByRole("button", { name: "Open Network Viewer" }));
79
+ expect(goToTabPath).toHaveBeenCalledWith("network-viewer");
80
+ });
81
+ });
@@ -48,6 +48,7 @@ const color_mode_dropdown_1 = require("../screens/settings/color-mode-dropdown")
48
48
  const system_apps_1 = require("../system-apps");
49
49
  const routes_loader_1 = require("../ui-router/routes-loader");
50
50
  const ui_loader_1 = require("../ui-router/ui-loader");
51
+ const network_status_indicator_1 = require("./network-status-indicator");
51
52
  const tabs_state_1 = require("./tabs-state");
52
53
  /**
53
54
  * Wraps a single tab's content so a crash in one tab doesn't take down the whole app.
@@ -167,7 +168,7 @@ function TabsLayoutInternal({ fillContainer = false } = {}) {
167
168
  }, children: (0, jsx_runtime_1.jsx)("i", { className: "bi-search", style: { fontSize: "14px" } }) }), (0, jsx_runtime_1.jsx)("div", { className: "d-flex flex-grow-1 overflow-auto", style: {
168
169
  scrollbarWidth: "none",
169
170
  msOverflowStyle: "none",
170
- }, children: tabs.map((tab) => ((0, jsx_runtime_1.jsx)(TabHeader, { tab: tab, isActive: activeTab === tab.tabId, onSwitch: switchTab, onClose: closeTab, colorMode: _colorMode }, tab.tabId))) })] })) }), (0, jsx_runtime_1.jsx)("div", { className: "flex-grow-1 overflow-hidden", children: tabs.map((tab) => ((0, jsx_runtime_1.jsx)("div", { className: `h-100 ${activeTab === tab.tabId ? "d-block" : "d-none"}`, style: { display: activeTab === tab.tabId ? "block" : "none" }, children: (0, jsx_runtime_1.jsx)(TabErrorBoundary, { tabTitle: tab.title, tabId: tab.tabId, children: (0, jsx_runtime_1.jsx)(TabContent, { tab: tab, isMobile: isMobile, isActive: activeTab === tab.tabId }) }) }, tab.tabId))) })] }, currentlyActiveGroupId));
171
+ }, children: tabs.map((tab) => ((0, jsx_runtime_1.jsx)(TabHeader, { tab: tab, isActive: activeTab === tab.tabId, onSwitch: switchTab, onClose: closeTab, colorMode: _colorMode }, tab.tabId))) }), (0, jsx_runtime_1.jsx)(network_status_indicator_1.TabsStripIndicators, {})] })) }), (0, jsx_runtime_1.jsx)("div", { className: "flex-grow-1 overflow-hidden", children: tabs.map((tab) => ((0, jsx_runtime_1.jsx)("div", { className: `h-100 ${activeTab === tab.tabId ? "d-block" : "d-none"}`, style: { display: activeTab === tab.tabId ? "block" : "none" }, children: (0, jsx_runtime_1.jsx)(TabErrorBoundary, { tabTitle: tab.title, tabId: tab.tabId, children: (0, jsx_runtime_1.jsx)(TabContent, { tab: tab, isMobile: isMobile, isActive: activeTab === tab.tabId }) }) }, tab.tabId))) })] }, currentlyActiveGroupId));
171
172
  }
172
173
  function MobileTabsHeader({ tabs, activeTab, onSwitch, onClose, colorMode, }) {
173
174
  const [showMenuDropdown, setShowMenuDropdown] = react_1.default.useState(false);
@@ -178,29 +179,29 @@ function MobileTabsHeader({ tabs, activeTab, onSwitch, onClose, colorMode, }) {
178
179
  transform: "translateX(-50%)",
179
180
  pointerEvents: "none",
180
181
  maxWidth: "calc(100% - 140px)",
181
- }, children: [currentTab?.iconClassName && currentTab.packageId !== "launcher" && ((0, jsx_runtime_1.jsx)("i", { className: `${currentTab.iconClassName} me-2` })), (0, jsx_runtime_1.jsx)("span", { className: "fw-medium text-truncate small", children: currentTab?.title || "Apps" })] }), (0, jsx_runtime_1.jsx)("div", { className: "d-flex align-items-center", children: (0, jsx_runtime_1.jsxs)("div", { className: "dropdown", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `btn btn-sm ${colorMode === "light" ? "btn-outline-dark" : "btn-outline-light"}`, onClick: () => setShowMenuDropdown(!showMenuDropdown), style: { minWidth: "36px" }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-list" }), nonLauncherTabs.length > 0 && (0, jsx_runtime_1.jsx)("span", { className: "ms-1", children: nonLauncherTabs.length })] }), showMenuDropdown && ((0, jsx_runtime_1.jsxs)("div", { className: `dropdown-menu show position-absolute ${colorMode === "light" ? "" : "dropdown-menu-dark"}`, style: {
182
- right: 0,
183
- top: "100%",
184
- zIndex: 1000,
185
- minWidth: "280px",
186
- maxHeight: "calc(100vh - 60px)",
187
- overflowY: "auto",
188
- }, children: [(0, jsx_runtime_1.jsxs)("div", { className: `dropdown-item d-flex align-items-center ${activeTab === "launcher" ? "active" : ""}`, style: { cursor: "pointer", padding: "14px 18px", fontSize: "17px" }, onClick: () => {
189
- onSwitch("launcher");
190
- setShowMenuDropdown(false);
191
- }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-grid-3x3-gap me-3", style: { fontSize: "20px" } }), (0, jsx_runtime_1.jsx)("span", { children: "Apps" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dropdown-item d-flex align-items-center", style: { cursor: "pointer", padding: "14px 18px", fontSize: "17px" }, onClick: () => {
192
- (0, command_palette_1.openCommandPalette)();
193
- setShowMenuDropdown(false);
194
- }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-search me-3", style: { fontSize: "20px" } }), (0, jsx_runtime_1.jsx)("span", { children: "Search" })] }), nonLauncherTabs.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "dropdown-divider", style: { margin: "8px 0" } })), nonLauncherTabs
195
- .slice()
196
- .reverse()
197
- .map((tab) => ((0, jsx_runtime_1.jsxs)("div", { className: `dropdown-item d-flex align-items-center justify-content-between ${activeTab === tab.tabId ? "active" : ""}`, style: { cursor: "pointer", padding: "14px 18px", fontSize: "17px" }, onClick: () => {
198
- onSwitch(tab.tabId);
199
- setShowMenuDropdown(false);
200
- }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [tab.iconClassName && ((0, jsx_runtime_1.jsx)("i", { className: `${tab.iconClassName} me-3`, style: { fontSize: "20px" } })), (0, jsx_runtime_1.jsx)("span", { children: tab.title })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm p-0 ms-3", style: { width: "32px", height: "32px", fontSize: "20px" }, onClick: (e) => {
201
- e.stopPropagation();
202
- onClose(tab.tabId);
203
- }, children: (0, jsx_runtime_1.jsx)("i", { className: "bi-x" }) })] }, tab.tabId)))] }))] }) }), showMenuDropdown && ((0, jsx_runtime_1.jsx)("div", { className: "position-fixed w-100 h-100", style: { top: 0, left: 0, zIndex: 999 }, onClick: () => setShowMenuDropdown(false) }))] }));
182
+ }, children: [currentTab?.iconClassName && currentTab.packageId !== "launcher" && ((0, jsx_runtime_1.jsx)("i", { className: `${currentTab.iconClassName} me-2` })), (0, jsx_runtime_1.jsx)("span", { className: "fw-medium text-truncate small", children: currentTab?.title || "Apps" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-1", children: [(0, jsx_runtime_1.jsx)(network_status_indicator_1.TabsStripIndicators, { compact: true }), (0, jsx_runtime_1.jsxs)("div", { className: "dropdown", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `btn btn-sm ${colorMode === "light" ? "btn-outline-dark" : "btn-outline-light"}`, onClick: () => setShowMenuDropdown(!showMenuDropdown), style: { minWidth: "36px" }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-list" }), nonLauncherTabs.length > 0 && (0, jsx_runtime_1.jsx)("span", { className: "ms-1", children: nonLauncherTabs.length })] }), showMenuDropdown && ((0, jsx_runtime_1.jsxs)("div", { className: `dropdown-menu show position-absolute ${colorMode === "light" ? "" : "dropdown-menu-dark"}`, style: {
183
+ right: 0,
184
+ top: "100%",
185
+ zIndex: 1000,
186
+ minWidth: "280px",
187
+ maxHeight: "calc(100vh - 60px)",
188
+ overflowY: "auto",
189
+ }, children: [(0, jsx_runtime_1.jsxs)("div", { className: `dropdown-item d-flex align-items-center ${activeTab === "launcher" ? "active" : ""}`, style: { cursor: "pointer", padding: "14px 18px", fontSize: "17px" }, onClick: () => {
190
+ onSwitch("launcher");
191
+ setShowMenuDropdown(false);
192
+ }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-grid-3x3-gap me-3", style: { fontSize: "20px" } }), (0, jsx_runtime_1.jsx)("span", { children: "Apps" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dropdown-item d-flex align-items-center", style: { cursor: "pointer", padding: "14px 18px", fontSize: "17px" }, onClick: () => {
193
+ (0, command_palette_1.openCommandPalette)();
194
+ setShowMenuDropdown(false);
195
+ }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-search me-3", style: { fontSize: "20px" } }), (0, jsx_runtime_1.jsx)("span", { children: "Search" })] }), nonLauncherTabs.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "dropdown-divider", style: { margin: "8px 0" } })), nonLauncherTabs
196
+ .slice()
197
+ .reverse()
198
+ .map((tab) => ((0, jsx_runtime_1.jsxs)("div", { className: `dropdown-item d-flex align-items-center justify-content-between ${activeTab === tab.tabId ? "active" : ""}`, style: { cursor: "pointer", padding: "14px 18px", fontSize: "17px" }, onClick: () => {
199
+ onSwitch(tab.tabId);
200
+ setShowMenuDropdown(false);
201
+ }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [tab.iconClassName && ((0, jsx_runtime_1.jsx)("i", { className: `${tab.iconClassName} me-3`, style: { fontSize: "20px" } })), (0, jsx_runtime_1.jsx)("span", { children: tab.title })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm p-0 ms-3", style: { width: "32px", height: "32px", fontSize: "20px" }, onClick: (e) => {
202
+ e.stopPropagation();
203
+ onClose(tab.tabId);
204
+ }, children: (0, jsx_runtime_1.jsx)("i", { className: "bi-x" }) })] }, tab.tabId)))] }))] })] }), showMenuDropdown && ((0, jsx_runtime_1.jsx)("div", { className: "position-fixed w-100 h-100", style: { top: 0, left: 0, zIndex: 999 }, onClick: () => setShowMenuDropdown(false) }))] }));
204
205
  }
205
206
  function TabHeader({ tab, isActive, onSwitch, onClose, colorMode }) {
206
207
  const activeClass = isActive
@@ -0,0 +1,8 @@
1
+ import { type INetworkStatusSummary } from "@peers-app/peers-sdk";
2
+ /** Host availability plus the latest reactive network status snapshot. */
3
+ export interface INetworkStatusViewState {
4
+ availability: "loading" | "ready" | "unavailable";
5
+ status?: INetworkStatusSummary;
6
+ }
7
+ /** Subscribe to the current host's System Network Diagnostics status observable. */
8
+ export declare function useNetworkStatus(): INetworkStatusViewState;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useNetworkStatus = useNetworkStatus;
4
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
5
+ const react_1 = require("react");
6
+ const network_viewer_ipc_1 = require("../screens/network-viewer/network-viewer-ipc");
7
+ /** Subscribe to the current host's System Network Diagnostics status observable. */
8
+ function useNetworkStatus() {
9
+ const [viewState, setViewState] = (0, react_1.useState)({
10
+ availability: "loading",
11
+ });
12
+ (0, react_1.useEffect)(() => {
13
+ let active = true;
14
+ let generation = 0;
15
+ let statusSubscription;
16
+ let hadTransport = false;
17
+ const unsubscribeTransport = (0, peers_sdk_1.subscribeHostContractTransport)((transport) => {
18
+ generation += 1;
19
+ const currentGeneration = generation;
20
+ statusSubscription?.dispose();
21
+ statusSubscription = undefined;
22
+ if (!transport) {
23
+ setViewState({ availability: hadTransport ? "unavailable" : "loading" });
24
+ return;
25
+ }
26
+ hadTransport = true;
27
+ setViewState({ availability: "loading" });
28
+ void (0, network_viewer_ipc_1.getNetworkDiagnosticsConsumer)()
29
+ .then((diagnostics) => {
30
+ if (!active || generation !== currentGeneration) {
31
+ return;
32
+ }
33
+ setViewState({
34
+ availability: "ready",
35
+ status: diagnostics.observables.status(),
36
+ });
37
+ statusSubscription = diagnostics.observables.status.subscribe((status) => {
38
+ if (active && generation === currentGeneration) {
39
+ setViewState({ availability: "ready", status });
40
+ }
41
+ });
42
+ })
43
+ .catch(() => {
44
+ if (active && generation === currentGeneration) {
45
+ setViewState({ availability: "unavailable" });
46
+ }
47
+ });
48
+ });
49
+ return () => {
50
+ active = false;
51
+ generation += 1;
52
+ statusSubscription?.dispose();
53
+ unsubscribeTransport();
54
+ };
55
+ }, []);
56
+ return viewState;
57
+ }
@@ -0,0 +1 @@
1
+ export {};