Skip to content

Commit 47f316f

Browse files
authored
feat: refactor WebSocket (#208)
1 parent a5379e2 commit 47f316f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+1370
-967
lines changed

package-lock.json

Lines changed: 0 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@
6969
"react-i18next": "^15.7.3",
7070
"react-image": "^4.1.0",
7171
"react-router": "^7.8.2",
72-
"react-use-websocket": "^4.13.0",
7372
"react-virtuoso": "^4.14.0",
7473
"reagraph": "^4.30.3",
7574
"store2": "^2.14.4",
@@ -98,4 +97,4 @@
9897
"zigbee",
9998
"zigbee2mqtt"
10099
]
101-
}
100+
}

src/Main.tsx

Lines changed: 53 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import NiceModal from "@ebay/nice-modal-react";
2-
import React, { lazy, Suspense } from "react";
3-
import { I18nextProvider, useTranslation } from "react-i18next";
2+
import React, { lazy, Suspense, useEffect } from "react";
3+
import { I18nextProvider } from "react-i18next";
44
import { HashRouter, Route, Routes } from "react-router";
5-
import { AuthForm } from "./components/modal/components/AuthModal.js";
5+
import { useShallow } from "zustand/react/shallow";
66
import NavBarWithNotifications from "./components/navbar/NavBar.js";
77
import ScrollToTop from "./components/ScrollToTop.js";
88
import Toasts from "./components/Toasts.js";
99
import { ErrorBoundary } from "./ErrorBoundary.js";
1010
import i18n from "./i18n/index.js";
11-
import { WebSocketApiRouter } from "./WebSocketApiRouter.js";
11+
import { LoginPage } from "./pages/LoginPage.js";
12+
import { useAppStore } from "./store.js";
13+
import { startWebSocketManager } from "./websocket/WebSocketManager.js";
1214

1315
const HomePage = lazy(async () => await import("./pages/HomePage.js"));
1416
const DevicesPage = lazy(async () => await import("./pages/DevicesPage.js"));
@@ -23,50 +25,59 @@ const LogsPage = lazy(async () => await import("./pages/LogsPage.js"));
2325
const SettingsPage = lazy(async () => await import("./pages/SettingsPage.js"));
2426
const FrontendSettingsPage = lazy(async () => await import("./pages/FrontendSettingsPage.js"));
2527

26-
export function Main() {
27-
const { t } = useTranslation("common");
28+
function App() {
29+
const authRequired = useAppStore(useShallow((s) => s.authRequired.some((v) => v === true)));
30+
31+
useEffect(() => {
32+
// do the initial startup, will determine if LoginPage needs to be shown or not
33+
startWebSocketManager();
34+
}, []);
35+
36+
if (authRequired) {
37+
return <LoginPage />;
38+
}
39+
40+
return (
41+
<HashRouter>
42+
<ScrollToTop />
43+
<NavBarWithNotifications />
44+
<main className="pt-3 px-2">
45+
<Suspense
46+
fallback={
47+
<div className="flex flex-row justify-center items-center gap-2">
48+
<span className="loading loading-infinity loading-xl" />
49+
</div>
50+
}
51+
>
52+
<Routes>
53+
<Route path="/dashboard" element={<DashboardPage />} />
54+
<Route path="/devices" element={<DevicesPage />} />
55+
<Route path="/device/:sourceIdx/:deviceId/:tab?" element={<DevicePage />} />
56+
<Route path="/groups" element={<GroupsPage />} />
57+
<Route path="/group/:sourceIdx/:groupId/:tab?" element={<GroupPage />} />
58+
<Route path="/touchlink" element={<TouchlinkPage />} />
59+
<Route path="/ota" element={<OtaPage />} />
60+
<Route path="/network/:sourceIdx?" element={<NetworkPage />} />
61+
<Route path="/logs/:sourceIdx?" element={<LogsPage />} />
62+
<Route path="/settings/:sourceIdx?/:tab?/:subTab?" element={<SettingsPage />} />
63+
<Route path="/frontend-settings" element={<FrontendSettingsPage />} />
64+
<Route path="/" element={<HomePage />} />
65+
<Route path="*" element={<HomePage />} />
66+
</Routes>
67+
</Suspense>
68+
</main>
69+
<Toasts />
70+
</HashRouter>
71+
);
72+
}
2873

74+
export function Main() {
2975
return (
3076
<React.StrictMode>
3177
<I18nextProvider i18n={i18n}>
3278
<NiceModal.Provider>
33-
<AuthForm id="auth-form" onAuth={async () => {}} />
3479
<ErrorBoundary>
35-
<HashRouter>
36-
<ScrollToTop />
37-
<WebSocketApiRouter>
38-
<NavBarWithNotifications />
39-
<main className="pt-3 px-2">
40-
<Suspense
41-
fallback={
42-
<>
43-
<div className="flex flex-row justify-center items-center gap-2">
44-
<span className="loading loading-infinity loading-xl" />
45-
</div>
46-
<div className="flex flex-row justify-center items-center gap-2">{t("loading")}</div>
47-
</>
48-
}
49-
>
50-
<Routes>
51-
<Route path="/dashboard" element={<DashboardPage />} />
52-
<Route path="/devices" element={<DevicesPage />} />
53-
<Route path="/device/:sourceIdx/:deviceId/:tab?" element={<DevicePage />} />
54-
<Route path="/groups" element={<GroupsPage />} />
55-
<Route path="/group/:sourceIdx/:groupId/:tab?" element={<GroupPage />} />
56-
<Route path="/touchlink" element={<TouchlinkPage />} />
57-
<Route path="/ota" element={<OtaPage />} />
58-
<Route path="/network/:sourceIdx?" element={<NetworkPage />} />
59-
<Route path="/logs/:sourceIdx?" element={<LogsPage />} />
60-
<Route path="/settings/:sourceIdx?/:tab?/:subTab?" element={<SettingsPage />} />
61-
<Route path="/frontend-settings" element={<FrontendSettingsPage />} />
62-
<Route path="/" element={<HomePage />} />
63-
<Route path="*" element={<HomePage />} />
64-
</Routes>
65-
</Suspense>
66-
</main>
67-
<Toasts />
68-
</WebSocketApiRouter>
69-
</HashRouter>
80+
<App />
7081
</ErrorBoundary>
7182
</NiceModal.Provider>
7283
</I18nextProvider>

src/WebSocketApiRouter.tsx

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/WebSocketApiRouterContext.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/components/Notifications.tsx

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { faClose, faEllipsisH, faInbox, faPowerOff, faServer, faTrashCan } from "@fortawesome/free-solid-svg-icons";
22
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3-
import { memo, type RefObject, useCallback, useContext, useRef, type useState } from "react";
3+
import { memo, type RefObject, useCallback, useRef, type useState } from "react";
44
import { useTranslation } from "react-i18next";
55
import { Link } from "react-router";
6-
import { ReadyState } from "react-use-websocket";
76
import { useShallow } from "zustand/react/shallow";
8-
import { LOG_LEVELS_CMAP } from "../consts.js";
7+
import { CONNECTION_STATUS, LOG_LEVELS_CMAP } from "../consts.js";
98
import { API_URLS, MULTI_INSTANCE, useAppStore } from "../store.js";
109
import type { LogMessage } from "../types.js";
11-
import { WebSocketApiRouterContext } from "../WebSocketApiRouterContext.js";
10+
import { getTransactionPrefix, sendMessage } from "../websocket/WebSocketManager.js";
1211
import Button from "./Button.js";
1312
import ConfirmButton from "./ConfirmButton.js";
1413
import SourceDot from "./SourceDot.js";
@@ -17,21 +16,13 @@ type NotificationsProps = {
1716
setShowNotifications: ReturnType<typeof useState<boolean>>[1];
1817
};
1918

20-
type SourceNotificationsProps = { sourceIdx: number; readyState: ReadyState };
19+
type SourceNotificationsProps = { sourceIdx: number; readyState: number };
2120

2221
type NotificationProps = {
2322
log: LogMessage;
2423
onClick: (ref: RefObject<HTMLDivElement | null>) => void;
2524
};
2625

27-
const CONNECTION_STATUS = {
28-
[ReadyState.CONNECTING]: "text-info",
29-
[ReadyState.OPEN]: "text-success",
30-
[ReadyState.CLOSING]: "text-warning",
31-
[ReadyState.CLOSED]: "text-error",
32-
[ReadyState.UNINSTANTIATED]: "text-error",
33-
};
34-
3526
const Notification = memo(({ log, onClick }: NotificationProps) => {
3627
const alertRef = useRef<HTMLDivElement | null>(null);
3728

@@ -48,13 +39,13 @@ const Notification = memo(({ log, onClick }: NotificationProps) => {
4839
});
4940

5041
const SourceNotifications = memo(({ sourceIdx, readyState }: SourceNotificationsProps) => {
42+
const status = CONNECTION_STATUS[readyState];
5143
const { t } = useTranslation(["navbar", "common"]);
52-
const { sendMessage, transactionPrefixes } = useContext(WebSocketApiRouterContext);
5344
const notifications = useAppStore(useShallow((state) => state.notifications[sourceIdx]));
5445
const restartRequired = useAppStore(useShallow((state) => state.bridgeInfo[sourceIdx].restart_required));
5546
const clearNotifications = useAppStore((state) => state.clearNotifications);
5647

57-
const restart = useCallback(async () => await sendMessage(sourceIdx, "bridge/request/restart", ""), [sourceIdx, sendMessage]);
48+
const restart = useCallback(async () => await sendMessage(sourceIdx, "bridge/request/restart", ""), [sourceIdx]);
5849
const onNotificationClick = useCallback((ref: RefObject<HTMLDivElement | null>) => {
5950
if (ref?.current) {
6051
ref.current.className += " hidden";
@@ -66,7 +57,7 @@ const SourceNotifications = memo(({ sourceIdx, readyState }: SourceNotifications
6657
<li>
6758
<details open={sourceIdx === 0}>
6859
<summary>
69-
<span title={`${sourceIdx} | ${t("transaction_prefix")}: ${transactionPrefixes[sourceIdx]}`}>
60+
<span title={`${sourceIdx} | ${t("transaction_prefix")}: ${getTransactionPrefix(sourceIdx)}`}>
7061
{MULTI_INSTANCE ? <SourceDot idx={sourceIdx} alwaysShowName /> : "Zigbee2MQTT"}
7162
</span>
7263
<span className="ml-auto">
@@ -81,8 +72,8 @@ const SourceNotifications = memo(({ sourceIdx, readyState }: SourceNotifications
8172
<FontAwesomeIcon icon={faPowerOff} />
8273
</ConfirmButton>
8374
)}
84-
<span title={`${t("websocket_status")}: ${ReadyState[readyState]}`}>
85-
<FontAwesomeIcon icon={faServer} className={CONNECTION_STATUS[readyState]} />
75+
<span title={`${t("websocket_status")}: ${status?.[0]}`}>
76+
<FontAwesomeIcon icon={faServer} className={status?.[1]} />
8677
</span>
8778
</span>
8879
</summary>
@@ -108,7 +99,7 @@ const SourceNotifications = memo(({ sourceIdx, readyState }: SourceNotifications
10899

109100
const Notifications = memo(({ setShowNotifications }: NotificationsProps) => {
110101
const { t } = useTranslation("common");
111-
const { readyStates } = useContext(WebSocketApiRouterContext);
102+
const readyStates = useAppStore((state) => state.readyStates);
112103
const clearAllNotifications = useAppStore((state) => state.clearAllNotifications);
113104

114105
return (

src/components/SourceDot.tsx

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { faDotCircle, faExclamationCircle, faQuestionCircle, faXmarkCircle } from "@fortawesome/free-solid-svg-icons";
22
import { FontAwesomeIcon, type FontAwesomeIconProps } from "@fortawesome/react-fontawesome";
3-
import { memo, useContext } from "react";
4-
import { ReadyState } from "react-use-websocket";
3+
import { memo } from "react";
54
import store2 from "store2";
5+
import { useShallow } from "zustand/react/shallow";
66
import { MULTI_INSTANCE_SHOW_SOURCE_NAME_KEY } from "../localStoreConsts.js";
7-
import { API_NAMES, MULTI_INSTANCE } from "../store.js";
8-
import { WebSocketApiRouterContext } from "../WebSocketApiRouterContext.js";
7+
import { API_NAMES, MULTI_INSTANCE, useAppStore } from "../store.js";
98

109
type SourceDotProps = Omit<FontAwesomeIconProps, "icon" | "style" | "title"> & {
1110
idx: number;
@@ -19,11 +18,10 @@ type SourceDotProps = Omit<FontAwesomeIconProps, "icon" | "style" | "title"> & {
1918
};
2019

2120
const CONNECTION_STATUS = {
22-
[ReadyState.CONNECTING]: faQuestionCircle,
23-
[ReadyState.OPEN]: faDotCircle,
24-
[ReadyState.CLOSING]: faExclamationCircle,
25-
[ReadyState.CLOSED]: faXmarkCircle,
26-
[ReadyState.UNINSTANTIATED]: faXmarkCircle,
21+
[WebSocket.CONNECTING]: faQuestionCircle,
22+
[WebSocket.OPEN]: faDotCircle,
23+
[WebSocket.CLOSING]: faExclamationCircle,
24+
[WebSocket.CLOSED]: faXmarkCircle,
2725
};
2826

2927
const DOT_COLORS = [
@@ -45,7 +43,7 @@ const DOT_COLORS = [
4543
];
4644

4745
const SourceDot = memo(({ idx, autoHide, alwaysShowName, alwaysHideName, nameClassName, namePostfix, ...rest }: SourceDotProps) => {
48-
const { readyStates } = useContext(WebSocketApiRouterContext);
46+
const readyState = useAppStore(useShallow((state) => state.readyStates[idx]));
4947
const showName = !alwaysHideName && (alwaysShowName || store2.get(MULTI_INSTANCE_SHOW_SOURCE_NAME_KEY, true));
5048

5149
if (autoHide && !MULTI_INSTANCE) {
@@ -54,7 +52,7 @@ const SourceDot = memo(({ idx, autoHide, alwaysShowName, alwaysHideName, nameCla
5452

5553
return (
5654
<span title={`${idx} | ${API_NAMES[idx]}`}>
57-
<FontAwesomeIcon icon={CONNECTION_STATUS[readyStates[idx]]} style={{ color: DOT_COLORS[idx] }} {...rest} />
55+
<FontAwesomeIcon icon={CONNECTION_STATUS[readyState]} style={{ color: DOT_COLORS[idx] }} {...rest} />
5856
{showName && <span className={`ms-1 ${nameClassName ?? ""}`}>{API_NAMES[idx]}</span>}
5957
{showName && namePostfix}
6058
</span>

src/components/dashboard-page/DashboardItem.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@ import NiceModal from "@ebay/nice-modal-react";
22
import { faTrash } from "@fortawesome/free-solid-svg-icons";
33
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
44
import type { Row } from "@tanstack/react-table";
5-
import { useCallback, useContext } from "react";
5+
import { useCallback } from "react";
66
import { useTranslation } from "react-i18next";
77
import type { DashboardTableData } from "../../pages/Dashboard.js";
8-
import { WebSocketApiRouterContext } from "../../WebSocketApiRouterContext.js";
8+
import { sendMessage } from "../../websocket/WebSocketManager.js";
99
import Button from "../Button.js";
1010
import DeviceCard from "../device/DeviceCard.js";
1111
import { RemoveDeviceModal } from "../modal/components/RemoveDeviceModal.js";
1212
import DashboardFeatureWrapper from "./DashboardFeatureWrapper.js";
1313

1414
const DashboardItem = ({ original: { sourceIdx, device, deviceState, features, lastSeenConfig, removeDevice } }: Row<DashboardTableData>) => {
1515
const { t } = useTranslation("zigbee");
16-
const { sendMessage } = useContext(WebSocketApiRouterContext);
1716

1817
const onCardChange = useCallback(
1918
async (value: unknown) => {
@@ -24,7 +23,7 @@ const DashboardItem = ({ original: { sourceIdx, device, deviceState, features, l
2423
value,
2524
);
2625
},
27-
[sourceIdx, device.ieee_address, sendMessage],
26+
[sourceIdx, device.ieee_address],
2827
);
2928

3029
return (

src/components/device-page/AddScene.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { memo, useCallback, useContext, useMemo, useState } from "react";
1+
import { memo, useCallback, useMemo, useState } from "react";
22
import { useTranslation } from "react-i18next";
33
import type { Zigbee2MQTTAPI } from "zigbee2mqtt";
44
import { useShallow } from "zustand/react/shallow";
55
import { useAppStore } from "../../store.js";
66
import type { Device, DeviceState, Group } from "../../types.js";
77
import { isDevice } from "../../utils.js";
8-
import { WebSocketApiRouterContext } from "../../WebSocketApiRouterContext.js";
8+
import { sendMessage } from "../../websocket/WebSocketManager.js";
99
import Button from "../Button.js";
1010
import DashboardFeatureWrapper from "../dashboard-page/DashboardFeatureWrapper.js";
1111
import Feature from "../features/Feature.js";
@@ -21,7 +21,6 @@ type AddSceneProps = {
2121

2222
const AddScene = memo(({ sourceIdx, target, deviceState }: AddSceneProps) => {
2323
const { t } = useTranslation("scene");
24-
const { sendMessage } = useContext(WebSocketApiRouterContext);
2524
const [sceneId, setSceneId] = useState<number>(0);
2625
const [sceneName, setSceneName] = useState<string>("");
2726
const scenes = useMemo(() => getScenes(target), [target]);
@@ -36,7 +35,7 @@ const AddScene = memo(({ sourceIdx, target, deviceState }: AddSceneProps) => {
3635
value,
3736
);
3837
},
39-
[sourceIdx, target, sendMessage],
38+
[sourceIdx, target],
4039
);
4140

4241
const onStoreClick = useCallback(async () => {
@@ -48,7 +47,7 @@ const AddScene = memo(({ sourceIdx, target, deviceState }: AddSceneProps) => {
4847
`${target.friendly_name}/set`, // TODO: swap to ID/ieee_address
4948
{ scene_store: payload },
5049
);
51-
}, [sourceIdx, target, sceneId, sceneName, sendMessage]);
50+
}, [sourceIdx, target, sceneId, sceneName]);
5251

5352
const isValidSceneId = useMemo(() => {
5453
return sceneId >= 0 && sceneId <= 255 && !scenes.find((s) => s.id === sceneId);

0 commit comments

Comments
 (0)