Add Gateway device inventory panel
Build and Deploy Gateway / build-and-push (push) Successful in 15s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-06 10:17:33 +03:00
parent fdc6f687f3
commit 158aaadd23
18 changed files with 811 additions and 5 deletions
+56
View File
@@ -15,6 +15,7 @@ import { formatBytes } from '../utils/format.js';
import { instructionBlocks } from '../instructions.js';
import { operationBlocked } from '../state/operations.js';
import { ConfirmationPopup } from './ConfirmationPopup.jsx';
import { DevicesPanel } from './DevicesPanel.jsx';
import { ServerPicker } from './ServerPicker.jsx';
import { ERROR_DEFINITIONS } from '../../shared/errors.js';
import { canAppendRouteRule } from '../../shared/routingRules.js';
@@ -640,6 +641,7 @@ export function ClientOverviewPage({
const [serversLeaving, setServersLeaving] = useState(false);
const [instructionsOpen, setInstructionsOpen] = useState(false);
const [localRulesOpen, setLocalRulesOpen] = useState(false);
const [devicesOpen, setDevicesOpen] = useState(false);
const [localRulesDraft, setLocalRulesDraft] = useState([]);
const [localRulesRevision, setLocalRulesRevision] = useState(state?.route?.localRulesRevision || 0);
const [confirmingLocalRulesClose, setConfirmingLocalRulesClose] = useState(false);
@@ -655,6 +657,9 @@ export function ClientOverviewPage({
const localRulesPanelRef = useRef(null);
const localRulesToggleRef = useRef(null);
const localRulesCloseRef = useRef(null);
const devicesPanelRef = useRef(null);
const devicesToggleRef = useRef(null);
const devicesCloseRef = useRef(null);
const localRulesBaselineRef = useRef('[]');
const previousHasSubscriptionRef = useRef(hasSubscription);
const gatewayAddress = isGateway ? window.location.hostname : '127.0.0.1';
@@ -849,6 +854,28 @@ export function ClientOverviewPage({
};
}, [instructionsOpen]);
useEffect(() => {
if (!devicesOpen) return undefined;
const frame = requestAnimationFrame(() => devicesCloseRef.current?.focus());
const closeDevices = (event) => {
if (event.type === 'keydown' && event.key !== 'Escape') return;
if (event.type !== 'keydown' && (
devicesPanelRef.current?.contains(event.target) || devicesToggleRef.current?.contains(event.target)
)) return;
setDevicesOpen(false);
};
document.addEventListener('pointerdown', closeDevices);
document.addEventListener('keydown', closeDevices);
return () => {
cancelAnimationFrame(frame);
document.removeEventListener('pointerdown', closeDevices);
document.removeEventListener('keydown', closeDevices);
requestAnimationFrame(() => {
if (devicesPanelRef.current?.contains(document.activeElement)) devicesToggleRef.current?.focus();
});
};
}, [devicesOpen]);
useEffect(() => {
if (!localRulesOpen) return undefined;
const frame = requestAnimationFrame(() => localRulesCloseRef.current?.focus());
@@ -993,6 +1020,7 @@ export function ClientOverviewPage({
function openLocalRules() {
const rules = state?.route?.localRules || [];
setInstructionsOpen(false);
setDevicesOpen(false);
localRulesBaselineRef.current = localRulesSignature(rules);
setLocalRulesDraft(rules.map(createLocalRuleDraft));
setLocalRulesRevision(state?.route?.localRulesRevision || 0);
@@ -1078,6 +1106,7 @@ export function ClientOverviewPage({
aria-label={instructionsOpen ? 'Закрыть инструкции' : 'Как использовать'}
onClick={() => {
if (localRulesOpen && !requestCloseLocalRules()) return;
setDevicesOpen(false);
setInstructionsOpen((open) => !open);
}}
>
@@ -1087,6 +1116,26 @@ export function ClientOverviewPage({
</svg>
<span>Как использовать</span>
</button>
{isGateway && <button
ref={devicesToggleRef}
className={`client-instructions-toggle client-devices-toggle${devicesOpen ? ' is-open' : ''}`}
type="button"
aria-expanded={devicesOpen}
aria-controls="client-devices"
aria-label={devicesOpen ? 'Закрыть устройства' : 'Устройства Gateway'}
onClick={() => {
if (localRulesOpen && !requestCloseLocalRules()) return;
setInstructionsOpen(false);
setDevicesOpen((open) => !open);
}}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="3.5" y="5" width="7" height="10" rx="1.5" />
<rect x="13.5" y="8" width="7" height="7" rx="1.5" />
<path d="M6 19h12M7 15v4M17 15v4" />
</svg>
<span>Устройства</span>
</button>}
<button
ref={localRulesToggleRef}
className={`client-local-rules-toggle${localRulesOpen ? ' is-open' : ''}${localRulesPendingRestart ? ' has-pending' : ''}`}
@@ -1429,6 +1478,13 @@ export function ClientOverviewPage({
</div>
</aside>}
{hasSubscription && subscriptionContentReady && isGateway && <DevicesPanel
open={devicesOpen}
panelRef={devicesPanelRef}
closeRef={devicesCloseRef}
onClose={() => setDevicesOpen(false)}
/>}
{hasSubscription && subscriptionContentReady && <LocalRulesPanel
open={localRulesOpen}
rules={localRulesDraft}