@peers-app/peers-ui 0.7.26 → 0.7.28
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/command-palette/command-palette-ui.js +41 -3
- package/dist/screens/data-explorer/query-executor.js +58 -14
- package/dist/screens/network-viewer/device-details-modal.js +4 -0
- package/dist/screens/network-viewer/group-details-modal.d.ts +7 -0
- package/dist/screens/network-viewer/group-details-modal.js +127 -0
- package/dist/screens/network-viewer/network-viewer.js +10 -5
- package/package.json +3 -3
- package/src/command-palette/command-palette-ui.tsx +46 -3
- package/src/screens/data-explorer/query-executor.tsx +68 -16
- package/src/screens/network-viewer/device-details-modal.tsx +8 -0
- package/src/screens/network-viewer/group-details-modal.tsx +178 -0
- package/src/screens/network-viewer/network-viewer.tsx +28 -3
|
@@ -40,10 +40,40 @@ const color_mode_dropdown_1 = require("../screens/settings/color-mode-dropdown")
|
|
|
40
40
|
const command_palette_1 = require("./command-palette");
|
|
41
41
|
function CommandPaletteOverlay() {
|
|
42
42
|
const [isOpen] = (0, hooks_1.useObservable)(command_palette_1.isCommandPaletteOpen);
|
|
43
|
-
const [
|
|
43
|
+
const [_persistedQuery] = (0, hooks_1.useObservable)(command_palette_1.commandSearchQuery);
|
|
44
44
|
const [_colorMode] = (0, hooks_1.useObservable)(color_mode_dropdown_1.colorMode);
|
|
45
45
|
const [selectedIndex, setSelectedIndex] = (0, react_1.useState)(0);
|
|
46
46
|
const inputRef = (0, react_1.useRef)(null);
|
|
47
|
+
// Local state for query input to avoid locking up on fast typing
|
|
48
|
+
const [localQuery, setLocalQuery] = (0, react_1.useState)('');
|
|
49
|
+
const debounceTimeoutRef = (0, react_1.useRef)();
|
|
50
|
+
// Sync local query with persisted query when palette opens
|
|
51
|
+
(0, react_1.useEffect)(() => {
|
|
52
|
+
if (isOpen) {
|
|
53
|
+
setLocalQuery(_persistedQuery || '');
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
setLocalQuery('');
|
|
57
|
+
}
|
|
58
|
+
}, [isOpen, _persistedQuery]);
|
|
59
|
+
// Debounced function to update the persisted query
|
|
60
|
+
const updatePersistedQuery = (newQuery) => {
|
|
61
|
+
if (debounceTimeoutRef.current) {
|
|
62
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
63
|
+
}
|
|
64
|
+
debounceTimeoutRef.current = setTimeout(() => {
|
|
65
|
+
(0, command_palette_1.commandSearchQuery)(newQuery);
|
|
66
|
+
}, 150); // Shorter debounce for command palette (150ms vs 300ms)
|
|
67
|
+
};
|
|
68
|
+
// Cleanup debounce timeout on unmount
|
|
69
|
+
(0, react_1.useEffect)(() => {
|
|
70
|
+
return () => {
|
|
71
|
+
if (debounceTimeoutRef.current) {
|
|
72
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
76
|
+
const searchQuery = localQuery;
|
|
47
77
|
const filteredCommands = (0, command_palette_1.searchCommands)(searchQuery);
|
|
48
78
|
// Create a flattened list that matches the visual rendering order
|
|
49
79
|
const visualOrderCommands = Object.entries(filteredCommands.reduce((acc, cmd) => {
|
|
@@ -123,8 +153,16 @@ function CommandPaletteOverlay() {
|
|
|
123
153
|
zIndex: 1
|
|
124
154
|
} }),
|
|
125
155
|
react_1.default.createElement("input", { ref: inputRef, type: "text", className: `form-control ${isDark ? 'bg-dark text-light border-secondary' : ''}`, placeholder: "Type a command or search...", value: searchQuery, onChange: (e) => {
|
|
126
|
-
|
|
127
|
-
(
|
|
156
|
+
const newQuery = e.target.value;
|
|
157
|
+
setLocalQuery(newQuery);
|
|
158
|
+
updatePersistedQuery(newQuery);
|
|
159
|
+
}, onBlur: () => {
|
|
160
|
+
// Save immediately on blur
|
|
161
|
+
if (debounceTimeoutRef.current) {
|
|
162
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
163
|
+
debounceTimeoutRef.current = undefined;
|
|
164
|
+
}
|
|
165
|
+
(0, command_palette_1.commandSearchQuery)(localQuery);
|
|
128
166
|
}, style: {
|
|
129
167
|
paddingLeft: '40px',
|
|
130
168
|
fontSize: '16px',
|
|
@@ -39,6 +39,7 @@ const loading_indicator_1 = require("../../components/loading-indicator");
|
|
|
39
39
|
const ui_loader_1 = require("../../ui-router/ui-loader");
|
|
40
40
|
const globals_1 = require("../../globals");
|
|
41
41
|
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
42
|
+
const hooks_1 = require("../../hooks");
|
|
42
43
|
const defaultQuery = 'SELECT * FROM sqlite_master WHERE type=\'table\' LIMIT 10';
|
|
43
44
|
// Persistent vars at module level
|
|
44
45
|
const queryTabs = (0, peers_sdk_1.groupDeviceVar)('dataExplorerQueryTabs', {
|
|
@@ -52,35 +53,72 @@ const activeQueryTabId = (0, peers_sdk_1.groupDeviceVar)('dataExplorerActiveQuer
|
|
|
52
53
|
defaultValue: ''
|
|
53
54
|
});
|
|
54
55
|
function QueryExecutor() {
|
|
55
|
-
|
|
56
|
+
(0, hooks_1.useObservable)(queryTabs);
|
|
57
|
+
(0, hooks_1.useObservable)(activeQueryTabId);
|
|
56
58
|
const [results, setResults] = (0, react_1.useState)({});
|
|
57
59
|
const [errors, setErrors] = (0, react_1.useState)({});
|
|
58
60
|
const [loading, setLoading] = (0, react_1.useState)({});
|
|
61
|
+
// Local state for query input to avoid locking up on fast typing
|
|
62
|
+
const [localQuery, setLocalQuery] = (0, react_1.useState)('');
|
|
63
|
+
const debounceTimeoutRef = (0, react_1.useRef)();
|
|
64
|
+
const lastSavedQueryRef = (0, react_1.useRef)('');
|
|
65
|
+
const tabs = queryTabs();
|
|
66
|
+
const activeTabId = activeQueryTabId();
|
|
67
|
+
const activeTab = tabs.find(t => t.id === activeTabId);
|
|
68
|
+
// Sync local query with persisted query when tab changes
|
|
69
|
+
(0, react_1.useEffect)(() => {
|
|
70
|
+
const persistedQuery = activeTab?.query || '';
|
|
71
|
+
setLocalQuery(persistedQuery);
|
|
72
|
+
lastSavedQueryRef.current = persistedQuery;
|
|
73
|
+
}, [activeTabId, activeTab?.id]);
|
|
59
74
|
// Subscribe to tab changes
|
|
60
75
|
(0, react_1.useEffect)(() => {
|
|
61
|
-
const subscription1 = queryTabs.subscribe(() => forceUpdate({}));
|
|
62
|
-
const subscription2 = activeQueryTabId.subscribe(() => forceUpdate({}));
|
|
63
76
|
// Initialize activeTabId if not set
|
|
64
77
|
if (!activeQueryTabId() && queryTabs().length > 0) {
|
|
65
78
|
activeQueryTabId(queryTabs()[0].id);
|
|
66
79
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
subscription2.dispose();
|
|
70
|
-
};
|
|
71
|
-
}, []);
|
|
72
|
-
const tabs = queryTabs();
|
|
73
|
-
const activeTabId = activeQueryTabId();
|
|
74
|
-
const activeTab = tabs.find(t => t.id === activeTabId);
|
|
75
|
-
const query = activeTab?.query || '';
|
|
80
|
+
}, [queryTabs()]);
|
|
81
|
+
const query = localQuery;
|
|
76
82
|
const result = activeTab ? results[activeTab.id] : null;
|
|
77
83
|
const error = activeTab ? errors[activeTab.id] : null;
|
|
78
84
|
const isLoading = activeTab ? loading[activeTab.id] : false;
|
|
79
|
-
|
|
85
|
+
// Debounced function to update the persisted query
|
|
86
|
+
const updatePersistedQuery = (newQuery) => {
|
|
80
87
|
if (!activeTab)
|
|
81
88
|
return;
|
|
89
|
+
if (newQuery === lastSavedQueryRef.current)
|
|
90
|
+
return; // Skip if unchanged
|
|
82
91
|
const currentTabs = queryTabs();
|
|
83
92
|
queryTabs(currentTabs.map(t => t.id === activeTab.id ? { ...t, query: newQuery } : t));
|
|
93
|
+
lastSavedQueryRef.current = newQuery;
|
|
94
|
+
};
|
|
95
|
+
const setQuery = (newQuery) => {
|
|
96
|
+
// Update local state immediately for responsive UI
|
|
97
|
+
setLocalQuery(newQuery);
|
|
98
|
+
// Clear existing debounce timeout
|
|
99
|
+
if (debounceTimeoutRef.current) {
|
|
100
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
101
|
+
}
|
|
102
|
+
// Debounce the persisted update (300ms delay)
|
|
103
|
+
debounceTimeoutRef.current = setTimeout(() => {
|
|
104
|
+
updatePersistedQuery(newQuery);
|
|
105
|
+
}, 300);
|
|
106
|
+
};
|
|
107
|
+
// Cleanup debounce timeout on unmount
|
|
108
|
+
(0, react_1.useEffect)(() => {
|
|
109
|
+
return () => {
|
|
110
|
+
if (debounceTimeoutRef.current) {
|
|
111
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}, []);
|
|
115
|
+
// Save immediately on blur
|
|
116
|
+
const handleBlur = () => {
|
|
117
|
+
if (debounceTimeoutRef.current) {
|
|
118
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
119
|
+
debounceTimeoutRef.current = undefined;
|
|
120
|
+
}
|
|
121
|
+
updatePersistedQuery(localQuery);
|
|
84
122
|
};
|
|
85
123
|
const setResult = (result) => {
|
|
86
124
|
if (!activeTab)
|
|
@@ -120,6 +158,12 @@ function QueryExecutor() {
|
|
|
120
158
|
setError('Please enter a query');
|
|
121
159
|
return;
|
|
122
160
|
}
|
|
161
|
+
// Save the current query immediately before execution (in case debounce hasn't fired yet)
|
|
162
|
+
if (debounceTimeoutRef.current) {
|
|
163
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
164
|
+
debounceTimeoutRef.current = undefined;
|
|
165
|
+
}
|
|
166
|
+
updatePersistedQuery(query);
|
|
123
167
|
try {
|
|
124
168
|
setLoadingState(true);
|
|
125
169
|
setError(null);
|
|
@@ -222,7 +266,7 @@ function QueryExecutor() {
|
|
|
222
266
|
react_1.default.createElement("label", { htmlFor: "query-input", className: "form-label" },
|
|
223
267
|
react_1.default.createElement("strong", null, "SQL Query"),
|
|
224
268
|
react_1.default.createElement("span", { className: "text-muted small ms-2" }, "(Read-only: SELECT and PRAGMA only)")),
|
|
225
|
-
react_1.default.createElement("textarea", { id: "query-input", className: "form-control font-monospace", rows: 6, value: query, onChange: (e) => setQuery(e.target.value), onKeyDown: handleKeyDown, placeholder: "Enter your SQL query here..." }),
|
|
269
|
+
react_1.default.createElement("textarea", { id: "query-input", className: "form-control font-monospace", rows: 6, value: query, onChange: (e) => setQuery(e.target.value), onBlur: handleBlur, onKeyDown: handleKeyDown, placeholder: "Enter your SQL query here..." }),
|
|
226
270
|
react_1.default.createElement("div", { className: "mt-3 d-flex justify-content-between align-items-center" },
|
|
227
271
|
react_1.default.createElement("button", { className: "btn btn-primary", onClick: executeQuery, disabled: isLoading || !query.trim() }, isLoading ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
228
272
|
react_1.default.createElement("span", { className: "spinner-border spinner-border-sm me-2", role: "status", "aria-hidden": "true" }),
|
|
@@ -109,6 +109,10 @@ function DeviceDetailsModal({ deviceId, onClose, onDisconnect }) {
|
|
|
109
109
|
react_1.default.createElement("div", { className: "mb-4" },
|
|
110
110
|
react_1.default.createElement("h6", { className: "border-bottom pb-2 mb-3" }, "Connection Information"),
|
|
111
111
|
react_1.default.createElement("div", { className: "row" },
|
|
112
|
+
device.deviceName && (react_1.default.createElement("div", { className: "col-md-6 mb-3" },
|
|
113
|
+
react_1.default.createElement("strong", null, "Device Name:"),
|
|
114
|
+
react_1.default.createElement("br", null),
|
|
115
|
+
react_1.default.createElement("span", null, device.deviceName))),
|
|
112
116
|
react_1.default.createElement("div", { className: "col-md-6 mb-3" },
|
|
113
117
|
react_1.default.createElement("strong", null, "Device ID:"),
|
|
114
118
|
react_1.default.createElement("br", null),
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.GroupDetailsModal = GroupDetailsModal;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
39
|
+
const loading_indicator_1 = require("../../components/loading-indicator");
|
|
40
|
+
const trust_level_badge_1 = require("../../components/trust-level-badge");
|
|
41
|
+
function GroupDetailsModal({ groupId, onClose }) {
|
|
42
|
+
const [group, setGroup] = (0, react_1.useState)(null);
|
|
43
|
+
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
44
|
+
const [notFound, setNotFound] = (0, react_1.useState)(false);
|
|
45
|
+
const loadData = async () => {
|
|
46
|
+
try {
|
|
47
|
+
const api = window.electronAPI?.networkViewer;
|
|
48
|
+
if (!api) {
|
|
49
|
+
console.warn('Network Viewer API not available');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const groupData = await api.getGroupDetails(groupId);
|
|
53
|
+
if (!groupData) {
|
|
54
|
+
setNotFound(true);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
setGroup(groupData);
|
|
58
|
+
setNotFound(false);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error('Error loading group details:', error);
|
|
63
|
+
setNotFound(true);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
setLoading(false);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
(0, react_1.useEffect)(() => {
|
|
70
|
+
loadData();
|
|
71
|
+
}, [groupId]);
|
|
72
|
+
return (react_1.default.createElement("div", { className: "position-fixed top-0 start-0 w-100 h-100 d-flex align-items-start justify-content-center", style: { backgroundColor: 'rgba(0,0,0,0.5)', zIndex: 1050, overflowY: 'auto', paddingTop: '2rem', paddingBottom: '2rem' }, onClick: onClose },
|
|
73
|
+
react_1.default.createElement("div", { className: "card", style: { maxWidth: '900px', width: '90%', maxHeight: '90vh' }, onClick: (e) => e.stopPropagation() },
|
|
74
|
+
react_1.default.createElement("div", { className: "card-header d-flex justify-content-between align-items-center" },
|
|
75
|
+
react_1.default.createElement("div", null,
|
|
76
|
+
react_1.default.createElement("h5", { className: "mb-0" }, loading ? 'Loading...' : group?.groupName || 'Group Details'),
|
|
77
|
+
group && (react_1.default.createElement("small", { className: "text-muted" },
|
|
78
|
+
react_1.default.createElement("code", null, group.groupId)))),
|
|
79
|
+
react_1.default.createElement("button", { className: "btn-close", onClick: onClose, "aria-label": "Close" })),
|
|
80
|
+
react_1.default.createElement("div", { className: "card-body", style: { overflowY: 'auto', maxHeight: 'calc(90vh - 120px)' } },
|
|
81
|
+
loading && react_1.default.createElement(loading_indicator_1.LoadingIndicator, null),
|
|
82
|
+
!loading && (notFound || !group) && (react_1.default.createElement("div", { className: "alert alert-warning" }, "Group not found.")),
|
|
83
|
+
!loading && group && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
84
|
+
react_1.default.createElement("div", { className: "mb-4" },
|
|
85
|
+
react_1.default.createElement("h6", { className: "border-bottom pb-2 mb-3" }, "Group Information"),
|
|
86
|
+
react_1.default.createElement("div", { className: "row" },
|
|
87
|
+
react_1.default.createElement("div", { className: "col-md-6 mb-3" },
|
|
88
|
+
react_1.default.createElement("strong", null, "Group ID:"),
|
|
89
|
+
react_1.default.createElement("br", null),
|
|
90
|
+
react_1.default.createElement("code", { className: "text-muted small" }, group.groupId)),
|
|
91
|
+
react_1.default.createElement("div", { className: "col-md-6 mb-3" },
|
|
92
|
+
react_1.default.createElement("strong", null, "Group Name:"),
|
|
93
|
+
react_1.default.createElement("br", null),
|
|
94
|
+
react_1.default.createElement("span", null, group.groupName)),
|
|
95
|
+
react_1.default.createElement("div", { className: "col-md-6 mb-3" },
|
|
96
|
+
react_1.default.createElement("strong", null, "Founder:"),
|
|
97
|
+
react_1.default.createElement("br", null),
|
|
98
|
+
group.founderUserName ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
99
|
+
react_1.default.createElement("span", null, group.founderUserName),
|
|
100
|
+
react_1.default.createElement("br", null),
|
|
101
|
+
react_1.default.createElement("code", { className: "text-muted small" }, group.founderUserId))) : (react_1.default.createElement("code", { className: "text-muted small" }, group.founderUserId))),
|
|
102
|
+
react_1.default.createElement("div", { className: "col-md-6 mb-3" },
|
|
103
|
+
react_1.default.createElement("strong", null, "Total Members:"),
|
|
104
|
+
react_1.default.createElement("br", null),
|
|
105
|
+
react_1.default.createElement("span", { className: "h5" }, group.members.length)))),
|
|
106
|
+
group.members.length > 0 && (react_1.default.createElement("div", { className: "mb-4" },
|
|
107
|
+
react_1.default.createElement("h6", { className: "border-bottom pb-2 mb-3" },
|
|
108
|
+
"Members (",
|
|
109
|
+
group.members.length,
|
|
110
|
+
")"),
|
|
111
|
+
react_1.default.createElement("div", { className: "table-responsive" },
|
|
112
|
+
react_1.default.createElement("table", { className: "table table-sm table-hover" },
|
|
113
|
+
react_1.default.createElement("thead", null,
|
|
114
|
+
react_1.default.createElement("tr", null,
|
|
115
|
+
react_1.default.createElement("th", null, "User"),
|
|
116
|
+
react_1.default.createElement("th", null, "User ID"),
|
|
117
|
+
react_1.default.createElement("th", null, "Role"),
|
|
118
|
+
react_1.default.createElement("th", null, "Trust Level"))),
|
|
119
|
+
react_1.default.createElement("tbody", null, group.members.map((member) => (react_1.default.createElement("tr", { key: member.userId },
|
|
120
|
+
react_1.default.createElement("td", null, member.userName || (react_1.default.createElement("span", { className: "text-muted" }, "-"))),
|
|
121
|
+
react_1.default.createElement("td", null,
|
|
122
|
+
react_1.default.createElement("code", { className: "text-muted small" }, member.userId)),
|
|
123
|
+
react_1.default.createElement("td", null,
|
|
124
|
+
react_1.default.createElement("span", { className: "badge bg-primary" }, member.role)),
|
|
125
|
+
react_1.default.createElement("td", null,
|
|
126
|
+
react_1.default.createElement(trust_level_badge_1.TrustLevelBadge, { level: member.trustLevel || peers_sdk_1.TrustLevel.Unknown }))))))))))))))));
|
|
127
|
+
}
|
|
@@ -41,6 +41,7 @@ const trust_level_badge_1 = require("../../components/trust-level-badge");
|
|
|
41
41
|
const ui_loader_1 = require("../../ui-router/ui-loader");
|
|
42
42
|
const globals_1 = require("../../globals");
|
|
43
43
|
const device_details_modal_1 = require("./device-details-modal");
|
|
44
|
+
const group_details_modal_1 = require("./group-details-modal");
|
|
44
45
|
const usage_graph_1 = require("./usage-graph");
|
|
45
46
|
function NetworkViewerList() {
|
|
46
47
|
const [overview, setOverview] = (0, react_1.useState)(null);
|
|
@@ -50,6 +51,7 @@ function NetworkViewerList() {
|
|
|
50
51
|
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
51
52
|
const [refreshing, setRefreshing] = (0, react_1.useState)(false);
|
|
52
53
|
const [selectedDeviceId, setSelectedDeviceId] = (0, react_1.useState)(null);
|
|
54
|
+
const [selectedGroupId, setSelectedGroupId] = (0, react_1.useState)(null);
|
|
53
55
|
const [connecting, setConnecting] = (0, react_1.useState)(new Set());
|
|
54
56
|
const loadData = async () => {
|
|
55
57
|
try {
|
|
@@ -262,7 +264,7 @@ function NetworkViewerList() {
|
|
|
262
264
|
react_1.default.createElement("th", null, "User"),
|
|
263
265
|
react_1.default.createElement("th", null, "User Trust"),
|
|
264
266
|
react_1.default.createElement("th", null, "Device Trust"),
|
|
265
|
-
react_1.default.createElement("th", null, "Device
|
|
267
|
+
react_1.default.createElement("th", null, "Device"),
|
|
266
268
|
react_1.default.createElement("th", null, "Handshake"),
|
|
267
269
|
react_1.default.createElement("th", null, "Connection Address"),
|
|
268
270
|
react_1.default.createElement("th", null, "Shared Groups"),
|
|
@@ -280,8 +282,10 @@ function NetworkViewerList() {
|
|
|
280
282
|
react_1.default.createElement(trust_level_badge_1.TrustLevelBadge, { level: conn.userTrustLevel || peers_sdk_1.TrustLevel.Unknown })),
|
|
281
283
|
react_1.default.createElement("td", null,
|
|
282
284
|
react_1.default.createElement(trust_level_badge_1.TrustLevelBadge, { level: conn.deviceTrustLevel || peers_sdk_1.TrustLevel.Unknown })),
|
|
283
|
-
react_1.default.createElement("td", null,
|
|
284
|
-
react_1.default.createElement("
|
|
285
|
+
react_1.default.createElement("td", null, conn.deviceName ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
286
|
+
react_1.default.createElement("span", { className: "small" }, conn.deviceName),
|
|
287
|
+
react_1.default.createElement("br", null),
|
|
288
|
+
react_1.default.createElement("code", { className: "text-muted small", style: { fontSize: '.7em' } }, conn.deviceId))) : (react_1.default.createElement("code", { className: "text-muted small" }, conn.deviceId))),
|
|
285
289
|
react_1.default.createElement("td", null,
|
|
286
290
|
react_1.default.createElement("span", { className: `badge bg-${conn.isServer ? 'secondary' : 'primary'}` }, conn.isServer ? 'Receiver' : 'Initiator')),
|
|
287
291
|
react_1.default.createElement("td", null, conn.connectionAddress ? (react_1.default.createElement("small", { className: "text-muted" }, conn.connectionAddress)) : (react_1.default.createElement("span", { className: "text-muted" }, "-"))),
|
|
@@ -353,7 +357,7 @@ function NetworkViewerList() {
|
|
|
353
357
|
? new Date(group.lastSyncTimestamp).toLocaleString()
|
|
354
358
|
: 'Never')),
|
|
355
359
|
react_1.default.createElement("td", null,
|
|
356
|
-
react_1.default.createElement("
|
|
360
|
+
react_1.default.createElement("button", { className: "btn btn-sm btn-outline-primary", onClick: () => setSelectedGroupId(group.groupId), title: "View Group Details" }, group.totalMembers))))))))))),
|
|
357
361
|
react_1.default.createElement("div", { className: "card" },
|
|
358
362
|
react_1.default.createElement("div", { className: "card-body" },
|
|
359
363
|
react_1.default.createElement("h5", { className: "card-title" }, "Disconnected Devices"),
|
|
@@ -393,7 +397,8 @@ function NetworkViewerList() {
|
|
|
393
397
|
" Connect"))),
|
|
394
398
|
react_1.default.createElement("button", { className: "btn btn-sm btn-outline-danger", onClick: () => handleDeleteDevice(device.deviceId), title: "Forget this device" },
|
|
395
399
|
react_1.default.createElement("i", { className: "bi bi-trash" })))))))))))),
|
|
396
|
-
selectedDeviceId && (react_1.default.createElement(device_details_modal_1.DeviceDetailsModal, { deviceId: selectedDeviceId, onClose: () => setSelectedDeviceId(null), onDisconnect: loadData }))
|
|
400
|
+
selectedDeviceId && (react_1.default.createElement(device_details_modal_1.DeviceDetailsModal, { deviceId: selectedDeviceId, onClose: () => setSelectedDeviceId(null), onDisconnect: loadData })),
|
|
401
|
+
selectedGroupId && (react_1.default.createElement(group_details_modal_1.GroupDetailsModal, { groupId: selectedGroupId, onClose: () => setSelectedGroupId(null) }))));
|
|
397
402
|
}
|
|
398
403
|
(0, ui_loader_1.registerInternalPeersUI)({
|
|
399
404
|
peersUIId: 'network-viewer-list-ui',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peers-app/peers-ui",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.28",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/peers-app/peers-ui.git"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"bootstrap": "^5.3.3",
|
|
30
|
-
"@peers-app/peers-sdk": "^0.7.
|
|
30
|
+
"@peers-app/peers-sdk": "^0.7.28",
|
|
31
31
|
"react": "^18.0.0",
|
|
32
32
|
"react-dom": "^18.0.0"
|
|
33
33
|
},
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"jest": "^29.7.0",
|
|
58
58
|
"jest-environment-jsdom": "^30.0.5",
|
|
59
59
|
"path-browserify": "^1.0.1",
|
|
60
|
-
"@peers-app/peers-sdk": "0.7.
|
|
60
|
+
"@peers-app/peers-sdk": "0.7.28",
|
|
61
61
|
"react": "^18.0.0",
|
|
62
62
|
"react-dom": "^18.0.0",
|
|
63
63
|
"string-width": "^7.1.0",
|
|
@@ -12,11 +12,45 @@ import {
|
|
|
12
12
|
|
|
13
13
|
export function CommandPaletteOverlay() {
|
|
14
14
|
const [isOpen] = useObservable(isCommandPaletteOpen);
|
|
15
|
-
const [
|
|
15
|
+
const [_persistedQuery] = useObservable(commandSearchQuery);
|
|
16
16
|
const [_colorMode] = useObservable(colorMode);
|
|
17
17
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
18
18
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
19
19
|
|
|
20
|
+
// Local state for query input to avoid locking up on fast typing
|
|
21
|
+
const [localQuery, setLocalQuery] = useState<string>('');
|
|
22
|
+
const debounceTimeoutRef = useRef<NodeJS.Timeout>();
|
|
23
|
+
|
|
24
|
+
// Sync local query with persisted query when palette opens
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (isOpen) {
|
|
27
|
+
setLocalQuery(_persistedQuery || '');
|
|
28
|
+
} else {
|
|
29
|
+
setLocalQuery('');
|
|
30
|
+
}
|
|
31
|
+
}, [isOpen, _persistedQuery]);
|
|
32
|
+
|
|
33
|
+
// Debounced function to update the persisted query
|
|
34
|
+
const updatePersistedQuery = (newQuery: string) => {
|
|
35
|
+
if (debounceTimeoutRef.current) {
|
|
36
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
debounceTimeoutRef.current = setTimeout(() => {
|
|
40
|
+
commandSearchQuery(newQuery);
|
|
41
|
+
}, 150); // Shorter debounce for command palette (150ms vs 300ms)
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Cleanup debounce timeout on unmount
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
return () => {
|
|
47
|
+
if (debounceTimeoutRef.current) {
|
|
48
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const searchQuery = localQuery;
|
|
20
54
|
const filteredCommands = searchCommands(searchQuery);
|
|
21
55
|
|
|
22
56
|
// Create a flattened list that matches the visual rendering order
|
|
@@ -120,8 +154,17 @@ export function CommandPaletteOverlay() {
|
|
|
120
154
|
placeholder="Type a command or search..."
|
|
121
155
|
value={searchQuery}
|
|
122
156
|
onChange={(e) => {
|
|
123
|
-
|
|
124
|
-
|
|
157
|
+
const newQuery = e.target.value;
|
|
158
|
+
setLocalQuery(newQuery);
|
|
159
|
+
updatePersistedQuery(newQuery);
|
|
160
|
+
}}
|
|
161
|
+
onBlur={() => {
|
|
162
|
+
// Save immediately on blur
|
|
163
|
+
if (debounceTimeoutRef.current) {
|
|
164
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
165
|
+
debounceTimeoutRef.current = undefined;
|
|
166
|
+
}
|
|
167
|
+
commandSearchQuery(localQuery);
|
|
125
168
|
}}
|
|
126
169
|
style={{
|
|
127
170
|
paddingLeft: '40px',
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { LoadingIndicator } from '../../components/loading-indicator';
|
|
3
3
|
import { registerInternalPeersUI } from '../../ui-router/ui-loader';
|
|
4
4
|
import { isDesktop } from '../../globals';
|
|
5
5
|
import { groupDeviceVar, newid } from '@peers-app/peers-sdk';
|
|
6
|
+
import { useObservable } from '../../hooks';
|
|
6
7
|
|
|
7
8
|
interface ISqlQueryResult {
|
|
8
9
|
columns: string[];
|
|
@@ -32,41 +33,84 @@ const activeQueryTabId = groupDeviceVar<string>('dataExplorerActiveQueryTab', {
|
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
export function QueryExecutor() {
|
|
35
|
-
|
|
36
|
+
useObservable(queryTabs);
|
|
37
|
+
useObservable(activeQueryTabId);
|
|
36
38
|
const [results, setResults] = useState<{ [tabId: string]: ISqlQueryResult }>({});
|
|
37
39
|
const [errors, setErrors] = useState<{ [tabId: string]: string }>({});
|
|
38
40
|
const [loading, setLoading] = useState<{ [tabId: string]: boolean }>({});
|
|
41
|
+
|
|
42
|
+
// Local state for query input to avoid locking up on fast typing
|
|
43
|
+
const [localQuery, setLocalQuery] = useState<string>('');
|
|
44
|
+
const debounceTimeoutRef = useRef<NodeJS.Timeout>();
|
|
45
|
+
const lastSavedQueryRef = useRef<string>('');
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
const tabs = queryTabs();
|
|
48
|
+
const activeTabId = activeQueryTabId();
|
|
49
|
+
const activeTab = tabs.find(t => t.id === activeTabId);
|
|
50
|
+
|
|
51
|
+
// Sync local query with persisted query when tab changes
|
|
41
52
|
useEffect(() => {
|
|
42
|
-
const
|
|
43
|
-
|
|
53
|
+
const persistedQuery = activeTab?.query || '';
|
|
54
|
+
setLocalQuery(persistedQuery);
|
|
55
|
+
lastSavedQueryRef.current = persistedQuery;
|
|
56
|
+
}, [activeTabId, activeTab?.id]);
|
|
44
57
|
|
|
58
|
+
// Subscribe to tab changes
|
|
59
|
+
useEffect(() => {
|
|
45
60
|
// Initialize activeTabId if not set
|
|
46
61
|
if (!activeQueryTabId() && queryTabs().length > 0) {
|
|
47
62
|
activeQueryTabId(queryTabs()[0].id);
|
|
48
63
|
}
|
|
64
|
+
}, [queryTabs()]);
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
subscription1.dispose();
|
|
52
|
-
subscription2.dispose();
|
|
53
|
-
};
|
|
54
|
-
}, []);
|
|
55
|
-
|
|
56
|
-
const tabs = queryTabs();
|
|
57
|
-
const activeTabId = activeQueryTabId();
|
|
58
|
-
const activeTab = tabs.find(t => t.id === activeTabId);
|
|
59
|
-
const query = activeTab?.query || '';
|
|
66
|
+
const query = localQuery;
|
|
60
67
|
const result = activeTab ? results[activeTab.id] : null;
|
|
61
68
|
const error = activeTab ? errors[activeTab.id] : null;
|
|
62
69
|
const isLoading = activeTab ? loading[activeTab.id] : false;
|
|
63
70
|
|
|
64
|
-
|
|
71
|
+
// Debounced function to update the persisted query
|
|
72
|
+
const updatePersistedQuery = (newQuery: string) => {
|
|
65
73
|
if (!activeTab) return;
|
|
74
|
+
if (newQuery === lastSavedQueryRef.current) return; // Skip if unchanged
|
|
75
|
+
|
|
66
76
|
const currentTabs = queryTabs();
|
|
67
77
|
queryTabs(currentTabs.map(t =>
|
|
68
78
|
t.id === activeTab.id ? { ...t, query: newQuery } : t
|
|
69
79
|
));
|
|
80
|
+
lastSavedQueryRef.current = newQuery;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const setQuery = (newQuery: string) => {
|
|
84
|
+
// Update local state immediately for responsive UI
|
|
85
|
+
setLocalQuery(newQuery);
|
|
86
|
+
|
|
87
|
+
// Clear existing debounce timeout
|
|
88
|
+
if (debounceTimeoutRef.current) {
|
|
89
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Debounce the persisted update (300ms delay)
|
|
93
|
+
debounceTimeoutRef.current = setTimeout(() => {
|
|
94
|
+
updatePersistedQuery(newQuery);
|
|
95
|
+
}, 300);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Cleanup debounce timeout on unmount
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
return () => {
|
|
101
|
+
if (debounceTimeoutRef.current) {
|
|
102
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
// Save immediately on blur
|
|
108
|
+
const handleBlur = () => {
|
|
109
|
+
if (debounceTimeoutRef.current) {
|
|
110
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
111
|
+
debounceTimeoutRef.current = undefined;
|
|
112
|
+
}
|
|
113
|
+
updatePersistedQuery(localQuery);
|
|
70
114
|
};
|
|
71
115
|
|
|
72
116
|
const setResult = (result: ISqlQueryResult | null) => {
|
|
@@ -106,6 +150,13 @@ export function QueryExecutor() {
|
|
|
106
150
|
return;
|
|
107
151
|
}
|
|
108
152
|
|
|
153
|
+
// Save the current query immediately before execution (in case debounce hasn't fired yet)
|
|
154
|
+
if (debounceTimeoutRef.current) {
|
|
155
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
156
|
+
debounceTimeoutRef.current = undefined;
|
|
157
|
+
}
|
|
158
|
+
updatePersistedQuery(query);
|
|
159
|
+
|
|
109
160
|
try {
|
|
110
161
|
setLoadingState(true);
|
|
111
162
|
setError(null);
|
|
@@ -271,6 +322,7 @@ export function QueryExecutor() {
|
|
|
271
322
|
rows={6}
|
|
272
323
|
value={query}
|
|
273
324
|
onChange={(e) => setQuery(e.target.value)}
|
|
325
|
+
onBlur={handleBlur}
|
|
274
326
|
onKeyDown={handleKeyDown}
|
|
275
327
|
placeholder="Enter your SQL query here..."
|
|
276
328
|
/>
|
|
@@ -8,6 +8,7 @@ interface IDeviceDetails {
|
|
|
8
8
|
deviceId: string;
|
|
9
9
|
userId: string;
|
|
10
10
|
userName?: string;
|
|
11
|
+
deviceName?: string;
|
|
11
12
|
trustLevel: number;
|
|
12
13
|
connectionState: 'connected' | 'syncing' | 'idle';
|
|
13
14
|
sharedGroups: string[];
|
|
@@ -144,6 +145,13 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
|
|
|
144
145
|
<div className="mb-4">
|
|
145
146
|
<h6 className="border-bottom pb-2 mb-3">Connection Information</h6>
|
|
146
147
|
<div className="row">
|
|
148
|
+
{device.deviceName && (
|
|
149
|
+
<div className="col-md-6 mb-3">
|
|
150
|
+
<strong>Device Name:</strong>
|
|
151
|
+
<br />
|
|
152
|
+
<span>{device.deviceName}</span>
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
147
155
|
<div className="col-md-6 mb-3">
|
|
148
156
|
<strong>Device ID:</strong>
|
|
149
157
|
<br />
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { TrustLevel } from '@peers-app/peers-sdk';
|
|
3
|
+
import { LoadingIndicator } from '../../components/loading-indicator';
|
|
4
|
+
import { TrustLevelBadge } from '../../components/trust-level-badge';
|
|
5
|
+
|
|
6
|
+
// TypeScript type definitions (matching backend)
|
|
7
|
+
interface IGroupDetails {
|
|
8
|
+
groupId: string;
|
|
9
|
+
groupName: string;
|
|
10
|
+
founderUserId: string;
|
|
11
|
+
founderUserName?: string;
|
|
12
|
+
members: {
|
|
13
|
+
userId: string;
|
|
14
|
+
userName?: string;
|
|
15
|
+
role: string;
|
|
16
|
+
trustLevel: TrustLevel;
|
|
17
|
+
}[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface IGroupDetailsModalProps {
|
|
21
|
+
groupId: string;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps) {
|
|
26
|
+
const [group, setGroup] = useState<IGroupDetails | null>(null);
|
|
27
|
+
const [loading, setLoading] = useState(true);
|
|
28
|
+
const [notFound, setNotFound] = useState(false);
|
|
29
|
+
|
|
30
|
+
const loadData = async () => {
|
|
31
|
+
try {
|
|
32
|
+
const api = (window as any).electronAPI?.networkViewer;
|
|
33
|
+
if (!api) {
|
|
34
|
+
console.warn('Network Viewer API not available');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const groupData = await api.getGroupDetails(groupId);
|
|
39
|
+
|
|
40
|
+
if (!groupData) {
|
|
41
|
+
setNotFound(true);
|
|
42
|
+
} else {
|
|
43
|
+
setGroup(groupData);
|
|
44
|
+
setNotFound(false);
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Error loading group details:', error);
|
|
48
|
+
setNotFound(true);
|
|
49
|
+
} finally {
|
|
50
|
+
setLoading(false);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
loadData();
|
|
56
|
+
}, [groupId]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
className="position-fixed top-0 start-0 w-100 h-100 d-flex align-items-start justify-content-center"
|
|
61
|
+
style={{ backgroundColor: 'rgba(0,0,0,0.5)', zIndex: 1050, overflowY: 'auto', paddingTop: '2rem', paddingBottom: '2rem' }}
|
|
62
|
+
onClick={onClose}
|
|
63
|
+
>
|
|
64
|
+
<div
|
|
65
|
+
className="card"
|
|
66
|
+
style={{ maxWidth: '900px', width: '90%', maxHeight: '90vh' }}
|
|
67
|
+
onClick={(e) => e.stopPropagation()}
|
|
68
|
+
>
|
|
69
|
+
{/* Header */}
|
|
70
|
+
<div className="card-header d-flex justify-content-between align-items-center">
|
|
71
|
+
<div>
|
|
72
|
+
<h5 className="mb-0">
|
|
73
|
+
{loading ? 'Loading...' : group?.groupName || 'Group Details'}
|
|
74
|
+
</h5>
|
|
75
|
+
{group && (
|
|
76
|
+
<small className="text-muted">
|
|
77
|
+
<code>{group.groupId}</code>
|
|
78
|
+
</small>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
<button className="btn-close" onClick={onClose} aria-label="Close"></button>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Body */}
|
|
85
|
+
<div className="card-body" style={{ overflowY: 'auto', maxHeight: 'calc(90vh - 120px)' }}>
|
|
86
|
+
{loading && <LoadingIndicator />}
|
|
87
|
+
|
|
88
|
+
{!loading && (notFound || !group) && (
|
|
89
|
+
<div className="alert alert-warning">
|
|
90
|
+
Group not found.
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{!loading && group && (
|
|
95
|
+
<>
|
|
96
|
+
{/* Group Info */}
|
|
97
|
+
<div className="mb-4">
|
|
98
|
+
<h6 className="border-bottom pb-2 mb-3">Group Information</h6>
|
|
99
|
+
<div className="row">
|
|
100
|
+
<div className="col-md-6 mb-3">
|
|
101
|
+
<strong>Group ID:</strong>
|
|
102
|
+
<br />
|
|
103
|
+
<code className="text-muted small">{group.groupId}</code>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="col-md-6 mb-3">
|
|
106
|
+
<strong>Group Name:</strong>
|
|
107
|
+
<br />
|
|
108
|
+
<span>{group.groupName}</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="col-md-6 mb-3">
|
|
111
|
+
<strong>Founder:</strong>
|
|
112
|
+
<br />
|
|
113
|
+
{group.founderUserName ? (
|
|
114
|
+
<>
|
|
115
|
+
<span>{group.founderUserName}</span>
|
|
116
|
+
<br />
|
|
117
|
+
<code className="text-muted small">{group.founderUserId}</code>
|
|
118
|
+
</>
|
|
119
|
+
) : (
|
|
120
|
+
<code className="text-muted small">{group.founderUserId}</code>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
<div className="col-md-6 mb-3">
|
|
124
|
+
<strong>Total Members:</strong>
|
|
125
|
+
<br />
|
|
126
|
+
<span className="h5">{group.members.length}</span>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Members List */}
|
|
132
|
+
{group.members.length > 0 && (
|
|
133
|
+
<div className="mb-4">
|
|
134
|
+
<h6 className="border-bottom pb-2 mb-3">
|
|
135
|
+
Members ({group.members.length})
|
|
136
|
+
</h6>
|
|
137
|
+
<div className="table-responsive">
|
|
138
|
+
<table className="table table-sm table-hover">
|
|
139
|
+
<thead>
|
|
140
|
+
<tr>
|
|
141
|
+
<th>User</th>
|
|
142
|
+
<th>User ID</th>
|
|
143
|
+
<th>Role</th>
|
|
144
|
+
<th>Trust Level</th>
|
|
145
|
+
</tr>
|
|
146
|
+
</thead>
|
|
147
|
+
<tbody>
|
|
148
|
+
{group.members.map((member) => (
|
|
149
|
+
<tr key={member.userId}>
|
|
150
|
+
<td>
|
|
151
|
+
{member.userName || (
|
|
152
|
+
<span className="text-muted">-</span>
|
|
153
|
+
)}
|
|
154
|
+
</td>
|
|
155
|
+
<td>
|
|
156
|
+
<code className="text-muted small">{member.userId}</code>
|
|
157
|
+
</td>
|
|
158
|
+
<td>
|
|
159
|
+
<span className="badge bg-primary">{member.role}</span>
|
|
160
|
+
</td>
|
|
161
|
+
<td>
|
|
162
|
+
<TrustLevelBadge level={member.trustLevel || TrustLevel.Unknown} />
|
|
163
|
+
</td>
|
|
164
|
+
</tr>
|
|
165
|
+
))}
|
|
166
|
+
</tbody>
|
|
167
|
+
</table>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
@@ -5,6 +5,7 @@ import { TrustLevelBadge } from '../../components/trust-level-badge';
|
|
|
5
5
|
import { registerInternalPeersUI } from '../../ui-router/ui-loader';
|
|
6
6
|
import { isDesktop } from '../../globals';
|
|
7
7
|
import { DeviceDetailsModal } from './device-details-modal';
|
|
8
|
+
import { GroupDetailsModal } from './group-details-modal';
|
|
8
9
|
import { UsageGraph } from './usage-graph';
|
|
9
10
|
|
|
10
11
|
// TypeScript type definitions (matching backend)
|
|
@@ -30,6 +31,7 @@ interface IConnectionInfo {
|
|
|
30
31
|
deviceId: string;
|
|
31
32
|
userId: string;
|
|
32
33
|
userName?: string;
|
|
34
|
+
deviceName?: string;
|
|
33
35
|
userTrustLevel: number;
|
|
34
36
|
deviceTrustLevel: number;
|
|
35
37
|
connectionState: 'connected' | 'syncing' | 'idle';
|
|
@@ -79,6 +81,7 @@ export function NetworkViewerList() {
|
|
|
79
81
|
const [loading, setLoading] = useState(true);
|
|
80
82
|
const [refreshing, setRefreshing] = useState(false);
|
|
81
83
|
const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
|
|
84
|
+
const [selectedGroupId, setSelectedGroupId] = useState<string | null>(null);
|
|
82
85
|
const [connecting, setConnecting] = useState<Set<string>>(new Set());
|
|
83
86
|
|
|
84
87
|
const loadData = async () => {
|
|
@@ -369,7 +372,7 @@ export function NetworkViewerList() {
|
|
|
369
372
|
<th>User</th>
|
|
370
373
|
<th>User Trust</th>
|
|
371
374
|
<th>Device Trust</th>
|
|
372
|
-
<th>Device
|
|
375
|
+
<th>Device</th>
|
|
373
376
|
{/* <th>Path Type</th> */}
|
|
374
377
|
<th>Handshake</th>
|
|
375
378
|
<th>Connection Address</th>
|
|
@@ -400,7 +403,15 @@ export function NetworkViewerList() {
|
|
|
400
403
|
<TrustLevelBadge level={conn.deviceTrustLevel || TrustLevel.Unknown} />
|
|
401
404
|
</td>
|
|
402
405
|
<td>
|
|
403
|
-
|
|
406
|
+
{conn.deviceName ? (
|
|
407
|
+
<>
|
|
408
|
+
<span className="small">{conn.deviceName}</span>
|
|
409
|
+
<br />
|
|
410
|
+
<code className="text-muted small" style={{ fontSize: '.7em'}}>{conn.deviceId}</code>
|
|
411
|
+
</>
|
|
412
|
+
) : (
|
|
413
|
+
<code className="text-muted small">{conn.deviceId}</code>
|
|
414
|
+
)}
|
|
404
415
|
</td>
|
|
405
416
|
{/* <td>
|
|
406
417
|
<span
|
|
@@ -560,7 +571,13 @@ export function NetworkViewerList() {
|
|
|
560
571
|
</small>
|
|
561
572
|
</td>
|
|
562
573
|
<td>
|
|
563
|
-
<
|
|
574
|
+
<button
|
|
575
|
+
className="btn btn-sm btn-outline-primary"
|
|
576
|
+
onClick={() => setSelectedGroupId(group.groupId)}
|
|
577
|
+
title="View Group Details"
|
|
578
|
+
>
|
|
579
|
+
{group.totalMembers}
|
|
580
|
+
</button>
|
|
564
581
|
</td>
|
|
565
582
|
</tr>
|
|
566
583
|
))}
|
|
@@ -663,6 +680,14 @@ export function NetworkViewerList() {
|
|
|
663
680
|
onDisconnect={loadData}
|
|
664
681
|
/>
|
|
665
682
|
)}
|
|
683
|
+
|
|
684
|
+
{/* Group Details Modal */}
|
|
685
|
+
{selectedGroupId && (
|
|
686
|
+
<GroupDetailsModal
|
|
687
|
+
groupId={selectedGroupId}
|
|
688
|
+
onClose={() => setSelectedGroupId(null)}
|
|
689
|
+
/>
|
|
690
|
+
)}
|
|
666
691
|
</div>
|
|
667
692
|
);
|
|
668
693
|
}
|