1import "./styles.sass";
2import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
3import {
4 faArrowDown,
5 faArrowUp,
6 faCircleNotch,
7 faClock,
8 faExclamationTriangle,
9 faKey,
10 faPen,
11 faServer,
12 faTableTennisPaddleBall,
13 faTrash
14} from "@fortawesome/free-solid-svg-icons";
15import React, {useContext, useEffect, useState} from "react";
16import {NodeContext} from "@/common/contexts/Node";
17import {useAlert} from "@/common/contexts/Alert";
18import {ToastNotificationContext} from "@/common/contexts/ToastNotification";
19import {baseRequest, patchRequest} from "@/common/utils/RequestUtil";
20import {t} from "i18next";
21import {Trans} from "react-i18next";
22import {getIconBySpeed} from "@/common/utils/TestUtil";
23import {ConfigContext} from "@/common/contexts/Config";
24import {PreferencesContext} from "@/common/contexts/Preferences";
25import {convertSpeed, getSpeedUnit} from "@/common/utils/FormatUtil";
26import {useNavigate} from "react-router-dom";
27import ContextMenu from "@/common/components/ContextMenu";
28
29export const NodeContainer = (node) => {
30 const updateNodes = useContext(NodeContext)[1];
31 const updateCurrentNode = useContext(NodeContext)[3];
32 const reloadConfig = useContext(ConfigContext)[1];
33 const [preferences] = useContext(PreferencesContext);
34 const speedUnit = getSpeedUnit(preferences);
35 const updateToast = useContext(ToastNotificationContext);
36 const alert = useAlert();
37 const [nodeData, setNodeData] = useState(null);
38 const [nodeError, setNodeError] = useState(undefined);
39 const [contextMenu, setContextMenu] = useState(null);
40
41 const navigate = useNavigate();
42
43 const prefix = node.currentNode ? "" : "/nodes/" + node.id;
44
45 const updateData = async () => {
46 const testRequest = await baseRequest(prefix + "/speedtests?limit=1");
47
48 if (testRequest.status === 401) return setNodeError("PASSWORD_CHANGED");
49 if (!testRequest.ok) return setNodeError("SERVER_NOT_REACHABLE");
50 const tests = await testRequest.json();
51
52 if (tests.length < 0) return setNodeError("SERVER_NOT_REACHABLE");
53
54 const configRequest = await baseRequest(prefix + "/config");
55
56 if (configRequest.status === 401) return setNodeError("PASSWORD_CHANGED");
57 if (!configRequest.ok) return setNodeError("SERVER_NOT_REACHABLE");
58 const config = await configRequest.json();
59
60 if (config.viewMode) return setNodeError("PASSWORD_CHANGED");
61
62 if (tests[0] === undefined) return setNodeData({pending: true});
63
64 setNodeError(undefined);
65 setNodeData({
66 ping: tests[0]?.ping,
67 download: Math.round(tests[0]?.download),
68 upload: Math.round(tests[0]?.upload),
69 pingIcon: getIconBySpeed(tests[0]?.ping, config.ping, false),
70 downloadIcon: getIconBySpeed(tests[0]?.download, config.download, true),
71 uploadIcon: getIconBySpeed(tests[0]?.upload, config.upload, true)
72 });
73 }
74
75 const updatePassword = async (wrong = false) => {
76 const result = await alert.openInput(t("nodes.password_outdated"), {
77 inputType: "password",
78 description: wrong ?
79 <span className="icon-red">{t("dialog.password.wrong")}</span> : t("nodes.update_password"),
80 placeholder: t("dialog.password.placeholder"),
81 buttonText: t("dialog.update")
82 });
83
84 if (result) {
85 const res = await (await baseRequest(`/nodes/${node.id}/password`, "PATCH", {password: result})).json();
86
87 if (res.type === "PASSWORD_UPDATED") {
88 updateData().catch(() => setNodeError("SERVER_NOT_REACHABLE"));
89 updateToast(t("nodes.password_updated"), "green", faKey);
90 } else {
91 updatePassword(true);
92 }
93 }
94 };
95
96 useEffect(() => {
97 updateData().catch(() => setNodeError("SERVER_NOT_REACHABLE"));
98 const interval = setInterval(() => updateData().catch(() => setNodeError("SERVER_NOT_REACHABLE")), 10000);
99 return () => clearInterval(interval);
100 }, []);
101
102 const switchNode = () => {
103 if (nodeError || !nodeData) {
104 if (nodeError === "PASSWORD_CHANGED") updatePassword();
105 return;
106 }
107
108 navigate("/");
109 updateCurrentNode(node.id);
110 reloadConfig();
111 }
112
113 const onContext = async (event) => {
114 event.preventDefault();
115 event.stopPropagation();
116
117 if (node.currentNode) return;
118
119 setContextMenu({x: event.clientX, y: event.clientY});
120 };
121
122 const closeContextMenu = () => setContextMenu(null);
123
124 const handleRename = async () => {
125 const newName = await alert.openInput(t("nodes.rename.title"), {
126 placeholder: t("nodes.rename.placeholder"),
127 buttonText: t("dialog.save"),
128 value: node.name
129 });
130
131 if (newName && newName !== node.name) {
132 await patchRequest(`/nodes/${node.id}/name`, {name: newName});
133 updateToast(t("nodes.rename.success"), "green", faPen);
134 updateNodes();
135 }
136 };
137
138 const handleDelete = async () => {
139 const confirmed = await alert.openConfirm(
140 t("nodes.delete.title"),
141 <Trans components={{Bold: <span className="dialog-value"/>}}
142 values={node}>nodes.delete.description</Trans>,
143 {
144 buttonText: t("nodes.delete.yes"),
145 danger: true
146 }
147 );
148
149 if (confirmed) {
150 await baseRequest("/nodes/" + node.id, "DELETE");
151 updateToast(t("nodes.delete.success"), "green", faServer);
152 updateNodes();
153 }
154 };
155
156 const contextMenuItems = [
157 {
158 icon: faPen,
159 label: t("nodes.rename.title"),
160 onClick: handleRename
161 },
162 {divider: true},
163 {
164 icon: faTrash,
165 label: t("nodes.delete.title"),
166 onClick: handleDelete,
167 danger: true
168 }
169 ];
170
171 return (
172 <>
173 {contextMenu && (
174 <ContextMenu
175 items={contextMenuItems}
176 position={contextMenu}
177 onClose={closeContextMenu}
178 />
179 )}
180 <div className={"node-item hover-" + (nodeError ? "red" : (nodeData ? "green" : "orange"))} key={node.id}
181 onClick={switchNode} onContextMenu={onContext}>
182 <div className="node-info-area">
183 <FontAwesomeIcon icon={faServer}
184 className={"icon-" + (nodeError ? "red" : (nodeData ? "green" : "orange"))}/>
185 <div className="node-info">
186 <h1>{node.name}</h1>
187 <p>{node.url.replace(/(^\w+:|^)\/\//, '')}</p>
188 </div>
189 </div>
190 <div className="speed-area">
191
192 {nodeError === "SERVER_NOT_REACHABLE" && (<div className="icon-text">
193 <h2>{t("nodes.messages.not_reachable")}</h2>
194 <FontAwesomeIcon icon={faExclamationTriangle} className="speed-icon icon-red"/>
195 </div>)}
196
197 {nodeError === "PASSWORD_CHANGED" && (<div className="icon-text">
198 <h2>{t("nodes.messages.password_changed")}</h2>
199 <FontAwesomeIcon icon={faKey} className="speed-icon icon-red"/>
200 </div>)}
201
202 {!nodeError && !nodeData && (
203 <FontAwesomeIcon icon={faCircleNotch} className="speed-icon" spin={true}/>)}
204
205 {nodeData && nodeData.pending && !nodeError && (<div className="icon-text">
206 <h2>{t("nodes.messages.tests_pending")}</h2>
207 <FontAwesomeIcon icon={faClock} className="speed-icon icon-blue"/>
208 </div>)}
209
210 {nodeData && !nodeData.pending && !nodeError && (
211 <>
212 <div className="speed-item">
213 <FontAwesomeIcon icon={faTableTennisPaddleBall}
214 className={"icon-" + nodeData.pingIcon}/>
215 <h1>{nodeData.ping} {t("latest.ping_unit")}</h1>
216 </div>
217
218 <div className="speed-item">
219 <FontAwesomeIcon icon={faArrowDown}
220 className={"icon-" + nodeData.downloadIcon}/>
221 <h1>{convertSpeed(nodeData.download, preferences)} {speedUnit}</h1>
222 </div>
223
224 <div className="speed-item">
225 <FontAwesomeIcon icon={faArrowUp}
226 className={"icon-" + nodeData.uploadIcon}/>
227 <h1>{convertSpeed(nodeData.upload, preferences)} {speedUnit}</h1>
228 </div>
229 </>
230 )}
231 </div>
232
233 </div>
234 </>
235 );
236}