@peers-app/peers-ui 0.7.25 → 0.7.27

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.
@@ -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 [searchQuery, setSearchQuery] = (0, hooks_1.useObservable)(command_palette_1.commandSearchQuery);
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
- setSearchQuery(e.target.value);
127
- (0, command_palette_1.commandSearchQuery)(e.target.value);
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
- const [, forceUpdate] = (0, react_1.useState)({});
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
- return () => {
68
- subscription1.dispose();
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
- const setQuery = (newQuery) => {
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,7 @@
1
+ import React from 'react';
2
+ interface IGroupDetailsModalProps {
3
+ groupId: string;
4
+ onClose: () => void;
5
+ }
6
+ export declare function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps): React.JSX.Element;
7
+ export {};
@@ -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 ID"),
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("code", { className: "text-muted small" }, conn.deviceId)),
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("span", { className: "badge bg-secondary" }, group.totalMembers))))))))))),
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.25",
3
+ "version": "0.7.27",
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.25",
30
+ "@peers-app/peers-sdk": "^0.7.27",
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.25",
60
+ "@peers-app/peers-sdk": "0.7.27",
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 [searchQuery, setSearchQuery] = useObservable(commandSearchQuery);
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
- setSearchQuery(e.target.value);
124
- commandSearchQuery(e.target.value);
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
- const [, forceUpdate] = useState({});
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
- // Subscribe to tab changes
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 subscription1 = queryTabs.subscribe(() => forceUpdate({}));
43
- const subscription2 = activeQueryTabId.subscribe(() => forceUpdate({}));
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
- return () => {
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
- const setQuery = (newQuery: string) => {
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 ID</th>
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
- <code className="text-muted small">{conn.deviceId}</code>
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
- <span className="badge bg-secondary">{group.totalMembers}</span>
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
  }