Add Gateway device inventory panel
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user