From c371cb4f0263febfc33da9eab642d34795a14950 Mon Sep 17 00:00:00 2001 From: Gareth George Date: Sat, 12 Jul 2025 22:54:15 -0700 Subject: [PATCH] improve peer state handling --- webui/src/state/logstate.ts | 36 ++++++++++++++++------------ webui/src/state/oplog.ts | 2 +- webui/src/state/peerstates.ts | 26 ++++++++++++++++++-- webui/src/views/App.tsx | 31 +++--------------------- webui/src/views/SettingsModal.tsx | 17 ++----------- webui/src/views/SummaryDashboard.tsx | 34 ++++++++------------------ 6 files changed, 61 insertions(+), 85 deletions(-) diff --git a/webui/src/state/logstate.ts b/webui/src/state/logstate.ts index 46c02797..5595c6f7 100644 --- a/webui/src/state/logstate.ts +++ b/webui/src/state/logstate.ts @@ -1,10 +1,6 @@ import { Operation, OperationEvent, OperationEventType, OperationStatus } from "../../gen/ts/v1/operations_pb"; import { GetOperationsRequest, GetOperationsRequestSchema, OpSelector } from "../../gen/ts/v1/service_pb"; import { getOperations, subscribeToOperations, unsubscribeFromOperations } from "./oplog"; -import { - STATS_OPERATION_HISTORY, - STATUS_OPERATION_HISTORY, -} from "../constants"; import { create } from "@bufbuild/protobuf"; type Subscriber = (ids: bigint[], flowIDs: bigint[], event: OperationEventType) => void; @@ -90,7 +86,6 @@ export const getStatusForSelector = async (sel: OpSelector) => { return await getStatus(req); }; - export class OplogState { private byID: Map = new Map(); private byFlowID: Map = new Map(); @@ -220,15 +215,26 @@ export class OplogState { } -export const matchSelector = (selector: OpSelector, op: Operation) => { - if (selector.planId && selector.planId !== op.planId) { - return false; - } - if (selector.repoGuid && selector.repoGuid !== op.repoGuid) { - return false; - } - if (selector.flowId && selector.flowId !== op.flowId) { - return false; + +// Defining matchers for each field in OpSelector to determine if an operation matches the selector. +// Type system asserts that a check must exist for each field in OpSelector. +const selectorFieldMatchers: { [K in keyof OpSelector]: (op: Operation, sel: OpSelector) => boolean } = { + planId: (op, sel) => op.planId === sel.planId, + repoGuid: (op, sel) => op.repoGuid === sel.repoGuid, + flowId: (op, sel) => op.flowId === sel.flowId, + instanceId: (op, sel) => op.instanceId === sel.instanceId, + snapshotId: (op, sel) => op.snapshotId === sel.snapshotId, + originalInstanceKeyid: (op, sel) => op.originalInstanceKeyid === sel.originalInstanceKeyid, + ids: (op: Operation, sel: OpSelector) => sel.ids.includes(op.id), + ["$typeName"]: (op: Operation, sel: OpSelector): boolean => true, // $typeName is a proto property that isn't used for matching +}; + +export const matchSelector = (selector: OpSelector, op: Operation): boolean => { + for (const key in selector) { + const matcher = selectorFieldMatchers[key as keyof OpSelector]; + if (matcher && !matcher(op, selector)) { + return false; + } } return true; -} +}; diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts index 1ae921df..87bfa13e 100644 --- a/webui/src/state/oplog.ts +++ b/webui/src/state/oplog.ts @@ -10,6 +10,7 @@ import { EmptySchema } from "../../gen/ts/types/value_pb"; import { create } from "@bufbuild/protobuf"; import _ from "lodash"; import { backrestService } from "../api"; +import { useEffect, useState } from "react"; const subscribers: ((event?: OperationEvent, err?: Error) => void)[] = []; @@ -57,7 +58,6 @@ export const unsubscribeFromOperations = ( console.log("unsubscribed from operations, subscriber count: ", subscribers.length); }; - export const shouldHideOperation = (operation: Operation) => { return ( operation.op.case === "operationStats" || diff --git a/webui/src/state/peerstates.ts b/webui/src/state/peerstates.ts index f51c6ffc..cdcefa56 100644 --- a/webui/src/state/peerstates.ts +++ b/webui/src/state/peerstates.ts @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react"; import { PeerState } from "../../gen/ts/v1/syncservice_pb"; import { syncStateService } from "../api"; @@ -43,19 +44,20 @@ const subscribeToSyncStates = async ( let peerStates: Map = new Map(); const subscribers: Set<(peerStates: PeerState[]) => void> = new Set(); -export const subscribeToPeerStates = ( +const subscribeToPeerStates = ( callback: (peerStates: PeerState[]) => void, ): void => { subscribers.add(callback); callback(Array.from(peerStates.values())); }; -export const unsubscribeFromPeerStates = ( +const unsubscribeFromPeerStates = ( callback: (peerStates: PeerState[]) => void, ): void => { subscribers.delete(callback); }; + (async () => { const abortController = new AbortController(); // never aborts at the moment. subscribeToSyncStates(() => { @@ -73,3 +75,23 @@ export const unsubscribeFromPeerStates = ( } }, abortController); })(); + +export const useSyncStates = (): PeerState[] => { + const [syncStates, setSyncStates] = useState(() => + Array.from(peerStates.values()) + ); + + useEffect(() => { + const handleStateUpdate = (states: PeerState[]) => { + setSyncStates(states); + }; + + subscribeToPeerStates(handleStateUpdate); + + return () => { + unsubscribeFromPeerStates(handleStateUpdate); + }; + }, []); + + return syncStates; +}; diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index 2de5b576..dd29a203 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -35,10 +35,7 @@ import { Route, Routes, useNavigate, useParams } from "react-router-dom"; import { MainContentAreaTemplate } from "./MainContentArea"; import { create } from "@bufbuild/protobuf"; import { PeerState } from "../../gen/ts/v1/syncservice_pb"; -import { - subscribeToPeerStates, - unsubscribeFromPeerStates, -} from "../state/peerstates"; +import { useSyncStates } from "../state/peerstates"; const { Header, Sider } = Layout; const SummaryDashboard = React.lazy(() => @@ -97,18 +94,7 @@ const RepoViewContainer = () => { const RemoteRepoViewContainer = () => { const { peerInstanceId, repoId } = useParams(); - const [peerStates, setPeerStates] = useState([]); - - // subscribe to peer states - useEffect(() => { - const cb = (states: PeerState[]) => { - setPeerStates(states); - }; - subscribeToPeerStates(cb); - return () => { - unsubscribeFromPeerStates(cb); - }; - }, []); + const peerStates = useSyncStates(); // Peer state is used to find the right repo const peerState = peerStates.find( @@ -171,18 +157,7 @@ export const App: React.FC = () => { const navigate = useNavigate(); const [config, setConfig] = useConfig(); - const [peerStates, setPeerStates] = useState([]); - - useEffect(() => { - if (!config || !config.multihost) return; - const cb = (states: PeerState[]) => { - setPeerStates(states); - }; - subscribeToPeerStates(cb); - return () => { - unsubscribeFromPeerStates(cb); - }; - }, [config]); + const peerStates = useSyncStates(); const items = getSidenavItems(config, peerStates); diff --git a/webui/src/views/SettingsModal.tsx b/webui/src/views/SettingsModal.tsx index 8ba9c860..409506f1 100644 --- a/webui/src/views/SettingsModal.tsx +++ b/webui/src/views/SettingsModal.tsx @@ -30,10 +30,7 @@ import { Multihost_Permission_Type, } from "../../gen/ts/v1/config_pb"; import { PeerState } from "../../gen/ts/v1/syncservice_pb"; -import { - subscribeToPeerStates, - unsubscribeFromPeerStates, -} from "../state/peerstates"; +import { useSyncStates } from "../state/peerstates"; import { PeerStateConnectionStatusIcon } from "../components/SyncStateIcon"; interface FormData { @@ -76,7 +73,7 @@ export const SettingsModal = () => { const showModal = useShowModal(); const alertsApi = useAlertApi()!; const [form] = Form.useForm(); - const [peerStates, setPeerStates] = useState([]); + const peerStates = useSyncStates(); const [reloadOnCancel, setReloadOnCancel] = useState(false); const [formEdited, setFormEdited] = useState(false); @@ -84,16 +81,6 @@ export const SettingsModal = () => { return null; } - useEffect(() => { - const cb = (syncStates: PeerState[]) => { - setPeerStates(syncStates); - }; - subscribeToPeerStates(cb); - return () => { - unsubscribeFromPeerStates(cb); - }; - }, []); - const handleOk = async () => { try { // Validate form diff --git a/webui/src/views/SummaryDashboard.tsx b/webui/src/views/SummaryDashboard.tsx index 0c9d4765..0af7c2ce 100644 --- a/webui/src/views/SummaryDashboard.tsx +++ b/webui/src/views/SummaryDashboard.tsx @@ -11,7 +11,7 @@ import { Spin, Typography, } from "antd"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useMemo } from "react"; import { useConfig } from "../components/ConfigProvider"; import { SummaryDashboardResponse, @@ -42,10 +42,7 @@ import { isMobile } from "../lib/browserutil"; import { useNavigate } from "react-router"; import { toJsonString } from "@bufbuild/protobuf"; import { ConfigSchema, Multihost } from "../../gen/ts/v1/config_pb"; -import { - subscribeToPeerStates, - unsubscribeFromPeerStates, -} from "../state/peerstates"; +import { useSyncStates } from "../state/peerstates"; import { PeerState } from "../../gen/ts/v1/syncservice_pb"; import { PeerStateConnectionStatusIcon } from "../components/SyncStateIcon"; import { last } from "lodash"; @@ -322,25 +319,14 @@ const MultihostSummary = ({ }: { multihostConfig: Multihost | null; }) => { - const [peerStates, setPeerStates] = useState>( - new Map() - ); - - useEffect(() => { - const cb = (syncStates: PeerState[]) => { - setPeerStates((prev) => { - const updated = new Map(prev); - for (const state of syncStates) { - updated.set(state.peerKeyid, state); - } - return updated; - }); - }; - subscribeToPeerStates(cb); - return () => { - unsubscribeFromPeerStates(cb); - }; - }, []); + const allPeerStates = useSyncStates(); + const peerStates = useMemo(() => { + const map = new Map(); + for (const state of allPeerStates) { + map.set(state.peerKeyid, state); + } + return map; + }, [allPeerStates]); const knownHostTiles: JSX.Element[] = []; for (const cfgPeer of multihostConfig?.knownHosts || []) {