diff --git a/src/components/PeerUpdate.tsx b/src/components/PeerUpdate.tsx index cffe0b5..3ea1b65 100644 --- a/src/components/PeerUpdate.tsx +++ b/src/components/PeerUpdate.tsx @@ -7,7 +7,7 @@ import { Col, Collapse, Divider, - Drawer, + message, Form, Input, Radio, @@ -36,6 +36,8 @@ import { RuleObject } from "antd/lib/form"; import { useGetTokenSilently } from "../utils/token"; import { timeAgo } from "../utils/common"; import { actions as routeActions } from "../store/route"; +import RouteAddNew from "./RouteAddNew"; +import { Route } from "../store/route/types"; import {useGetGroupTagHelpers} from "../utils/groups"; const { Paragraph } = Typography; @@ -65,7 +67,13 @@ const PeerUpdate = () => { const updatedPeers = useSelector( (state: RootState) => state.peer.updatedPeer ); - + const savedRoute = useSelector((state: RootState) => state.route.savedRoute); + const deletedRoute = useSelector( + (state: RootState) => state.route.deletedRoute + ); + const setupNewRouteVisible = useSelector( + (state: RootState) => state.route.setupNewRouteVisible + ); const [tagGroups, setTagGroups] = useState([] as string[]); const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[]); const [peerGroups, setPeerGroups] = useState([] as GroupPeer[]); @@ -85,6 +93,8 @@ const PeerUpdate = () => { } as PeerGroupsToSave); const routes = useSelector((state: RootState) => state.route.data); const [form] = Form.useForm(); + const styleNotification = { marginTop: 85 }; + useEffect(() => { //Unmounting component clean @@ -274,6 +284,20 @@ const PeerUpdate = () => { setCallingPeerAPI(false); setSubmitRunning(false); setEstimatedName(""); + + dispatch(routeActions.setSetupNewRouteVisible(false)); + dispatch( + routeActions.setRoute({ + network: "", + network_id: "", + description: "", + peer: "", + masquerade: true, + metric: 9999, + enabled: true, + groups: [], + } as Route) + ); }; const noUpdateToGroups = (): Boolean => { @@ -432,6 +456,90 @@ const PeerUpdate = () => { setFormPeer({ ...formPeer, login_expiration_enabled: checked }); }; + const onClickAddNewRoute = () => { + dispatch(routeActions.setSetupNewRouteVisible(true)); + dispatch( + routeActions.setRoute({ + network: "", + network_id: "", + description: "", + peer: "", + masquerade: true, + metric: 9999, + enabled: true, + groups: [], + } as Route) + ); + }; + + const saveKey = "saving"; + useEffect(() => { + if (savedRoute.loading) { + message.loading({ + content: "Saving...", + key: saveKey, + duration: 0, + style: styleNotification, + }); + } else if (savedRoute.success) { + message.success({ + content: "Route has been successfully updated.", + key: saveKey, + duration: 2, + style: styleNotification, + }); + dispatch(routeActions.setSetupNewRouteVisible(false)); + dispatch(routeActions.setSavedRoute({ ...savedRoute, success: false })); + dispatch(routeActions.resetSavedRoute(null)); + } else if (savedRoute.error) { + let errorMsg = "Failed to update network route"; + switch (savedRoute.error.statusCode) { + case 403: + errorMsg = + "Failed to update network route. You might not have enough permissions."; + break; + default: + errorMsg = savedRoute.error.data.message + ? savedRoute.error.data.message + : errorMsg; + break; + } + message.error({ + content: errorMsg, + key: saveKey, + duration: 5, + style: styleNotification, + }); + dispatch(routeActions.setSavedRoute({ ...savedRoute, error: null })); + dispatch(routeActions.resetSavedRoute(null)); + } + }, [savedRoute]); + + const deleteKey = "deleting"; + useEffect(() => { + const style = { marginTop: 85 }; + if (deletedRoute.loading) { + message.loading({ content: "Deleting...", key: deleteKey, style }); + } else if (deletedRoute.success) { + message.success({ + content: "Route has been successfully deleted.", + key: deleteKey, + duration: 2, + style, + }); + dispatch(routeActions.resetDeletedRoute(null)); + } else if (deletedRoute.error) { + message.error({ + content: + "Failed to remove route. You might not have enough permissions.", + key: deleteKey, + duration: 2, + style, + }); + dispatch(routeActions.resetDeletedRoute(null)); + } + }, [deletedRoute]); + return ( <> {peer && ( @@ -460,71 +568,71 @@ const PeerUpdate = () => { > - - - {!editName && peer.id && formPeer.name ? ( -
toggleEditName(true, peer.name)} - > - {formPeer.name ? formPeer.name : peer.name} - + + + {!editName && peer.id && formPeer.name ? ( +
toggleEditName(true, peer.name)} + > + {formPeer.name ? formPeer.name : peer.name} + - + {formPeer.userEmail}{" "} + +
+ ) : ( + + + - {formPeer.userEmail}{" "} - -
- ) : ( - - - - toggleEditName(false)} - onBlur={() => toggleEditName(false)} - autoComplete="off" - max={59} - /> - - - - {estimatedName} - - - - - )} - -
+ toggleEditName(false)} + onBlur={() => toggleEditName(false)} + autoComplete="off" + max={59} + /> + + + + {estimatedName} + + + +
+ )} + + @@ -558,16 +666,16 @@ const PeerUpdate = () => { } + disabled={true} + value={formPeer.userEmail} + style={{ color: "#8c8c8c" }} + autoComplete="off" + suffix={} /> @@ -714,7 +822,9 @@ const PeerUpdate = () => { style={{ marginTop: "-16px" }} > {peerRoutes && peerRoutes.length > 0 && ( - + )} @@ -783,7 +893,9 @@ const PeerUpdate = () => { > You don't have any routes yet - + )} @@ -799,16 +911,19 @@ const PeerUpdate = () => { > - System info - } + > + System info + + } className="system-info-panel" > @@ -827,9 +942,7 @@ const PeerUpdate = () => { > Hostname: - - {formPeer.hostname} - + {formPeer.hostname} { }} > Agent version: - - {formPeer.version} - + {formPeer.version} {formPeer.ui_version && ( { > UI version: - - {formPeer.ui_version} - + {formPeer.ui_version} )} @@ -895,6 +1004,7 @@ const PeerUpdate = () => { )} + {setupNewRouteVisible && } ); }; diff --git a/src/components/RouteAddNew.tsx b/src/components/RouteAddNew.tsx new file mode 100644 index 0000000..32a15f7 --- /dev/null +++ b/src/components/RouteAddNew.tsx @@ -0,0 +1,840 @@ +import React, { useEffect, useRef, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { RootState } from "typesafe-actions"; +import { actions as routeActions } from "../store/route"; +import { + Button, + Col, + Divider, + Collapse, + Form, + Input, + InputNumber, + Radio, + Row, + Select, + SelectProps, + Space, + Switch, + Modal, + Typography, +} from "antd"; +import { + CloseOutlined, + FlagFilled, + QuestionCircleFilled, +} from "@ant-design/icons"; +import { Route, RouteToSave } from "../store/route/types"; +import { Header } from "antd/es/layout/layout"; +import { RuleObject } from "antd/lib/form"; +import cidrRegex from "cidr-regex"; +import { + initPeerMaps, + masqueradeDisabledMSG, + peerToPeerIP, + routePeerSeparator, + transformGroupedDataTable, +} from "../utils/routes"; +import { useGetTokenSilently } from "../utils/token"; +import { useGetGroupTagHelpers } from "../utils/groups"; + +const { Paragraph, Text } = Typography; +const { Panel } = Collapse; + +interface FormRoute extends Route {} + +const RouteAddNew = (selectedPeer: any) => { + const { + blueTagRender, + handleChangeTags, + dropDownRender, + optionRender, + tagGroups, + getExistingAndToCreateGroupsLists, + getGroupNamesFromIDs, + selectValidator, + } = useGetGroupTagHelpers(); + // const { optionRender, blueTagRender, grayTagRender } = + // useGetGroupTagHelpers(); + + const { Option } = Select; + const { getTokenSilently } = useGetTokenSilently(); + const dispatch = useDispatch(); + const setupNewRouteVisible = useSelector( + (state: RootState) => state.route.setupNewRouteVisible + ); + const setupNewRouteHA = useSelector( + (state: RootState) => state.route.setupNewRouteHA + ); + const peers = useSelector((state: RootState) => state.peer.data); + const route = useSelector((state: RootState) => state.route.route); + const routes = useSelector((state: RootState) => state.route.data); + const savedRoute = useSelector((state: RootState) => state.route.savedRoute); + const [previousRouteKey, setPreviousRouteKey] = useState(""); + const [editName, setEditName] = useState(false); + const [editDescription, setEditDescription] = useState(false); + const options: SelectProps["options"] = []; + const [formRoute, setFormRoute] = useState({} as FormRoute); + const [form] = Form.useForm(); + const inputNameRef = useRef(null); + const inputDescriptionRef = useRef(null); + const defaultRoutingPeerMSG = "Routing Peer"; + const [routingPeerMSG, setRoutingPeerMSG] = useState(defaultRoutingPeerMSG); + const defaultMasqueradeMSG = "Masquerade"; + const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG); + const defaultStatusMSG = "Status"; + const [statusMSG, setStatusMSG] = useState(defaultStatusMSG); + const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers); + const [newRoute, setNewRoute] = useState(false); + + useEffect(() => { + if (!newRoute) { + setRoutingPeerMSG(defaultRoutingPeerMSG); + setMasqueradeMSG("Update Masquerade"); + setStatusMSG("Update Status"); + } else { + setRoutingPeerMSG(defaultRoutingPeerMSG); + setMasqueradeMSG(defaultMasqueradeMSG); + setStatusMSG(defaultStatusMSG); + setPreviousRouteKey(""); + } + }, [newRoute]); + + useEffect(() => { + if (editName) + inputNameRef.current!.focus({ + cursor: "end", + }); + }, [editName]); + + useEffect(() => { + if (editDescription) + inputDescriptionRef.current!.focus({ + cursor: "end", + }); + }, [editDescription]); + + useEffect(() => { + if (!route) return; + + if (selectedPeer && selectedPeer.peer) { + options?.push({ + label: peerToPeerIP(selectedPeer.peer.name, selectedPeer.peer.ip), + value: peerToPeerIP(selectedPeer.peer.name, selectedPeer.peer.ip), + disabled: false, + }); + const udpateRoute = { ...route, peer: options[0].value } as FormRoute; + setFormRoute(udpateRoute); + form.setFieldsValue(udpateRoute); + setPreviousRouteKey(udpateRoute.network_id + udpateRoute.network); + } else { + const fRoute = { + ...route, + groups: getGroupNamesFromIDs(route.groups), + } as FormRoute; + setFormRoute(fRoute); + setPreviousRouteKey(fRoute.network_id + fRoute.network); + form.setFieldsValue(fRoute); + } + + if (!route.network_id) { + setNewRoute(true); + } else { + setNewRoute(false); + } + }, [route]); + + if (!selectedPeer.peer) { + peers.forEach((p) => { + let os: string; + os = p.os; + if ( + !os.toLowerCase().startsWith("darwin") && + !os.toLowerCase().startsWith("windows") && + !os.toLowerCase().startsWith("android") && + route && + !routes + .filter((r) => r.network_id === route.network_id) + .find((r) => r.peer === p.id) + ) { + options?.push({ + label: peerToPeerIP(p.name, p.ip), + value: peerToPeerIP(p.name, p.ip), + disabled: false, + }); + } + }); + } + + const createRouteToSave = (inputRoute: FormRoute): RouteToSave => { + let peerIDList = inputRoute.peer.split(routePeerSeparator); + let peerID: string; + if (peerIDList.length === 1) { + peerID = inputRoute.peer; + } else { + if (peerIDList[1]) { + peerID = peerIPToID[peerIDList[1]]; + } else { + peerID = peerIPToID[peerNameToIP[inputRoute.peer]]; + } + } + + let [existingGroups, groupsToCreate] = getExistingAndToCreateGroupsLists( + inputRoute.groups + ); + + return { + id: inputRoute.id, + network: inputRoute.network, + network_id: inputRoute.network_id, + description: inputRoute.description, + peer: peerID, + enabled: inputRoute.enabled, + masquerade: inputRoute.masquerade, + metric: inputRoute.metric, + groups: existingGroups, + groupsToCreate: groupsToCreate, + } as RouteToSave; + }; + + const handleFormSubmit = () => { + form + .validateFields() + .then(() => { + if (!setupNewRouteHA || formRoute.peer != "") { + const routeToSave = createRouteToSave(formRoute); + dispatch( + routeActions.saveRoute.request({ + getAccessTokenSilently: getTokenSilently, + payload: routeToSave, + }) + ); + } else { + let groupedDataTable = transformGroupedDataTable(routes, peers); + groupedDataTable.forEach((group) => { + if (group.key == previousRouteKey) { + group.groupedRoutes.forEach((route) => { + let updateRoute: FormRoute = { + ...formRoute, + id: route.id, + peer: route.peer, + metric: route.metric, + enabled: + formRoute.enabled != group.enabled + ? formRoute.enabled + : route.enabled, + }; + const routeToSave = createRouteToSave(updateRoute); + dispatch( + routeActions.saveRoute.request({ + getAccessTokenSilently: getTokenSilently, + payload: routeToSave, + }) + ); + }); + } + }); + } + }) + .catch((errorInfo) => { + console.log("errorInfo", errorInfo); + }); + }; + + const setVisibleNewRoute = (status: boolean) => { + dispatch(routeActions.setSetupNewRouteVisible(status)); + }; + + const setSetupNewRouteHA = (status: boolean) => { + dispatch(routeActions.setSetupNewRouteHA(status)); + }; + + const onCancel = () => { + if (savedRoute.loading) return; + setEditName(false); + dispatch( + routeActions.setRoute({ + network: "", + network_id: "", + description: "", + peer: "", + metric: 9999, + masquerade: false, + enabled: true, + groups: [], + } as Route) + ); + setVisibleNewRoute(false); + setSetupNewRouteHA(false); + setPreviousRouteKey(""); + setNewRoute(false); + }; + + const onChange = (data: any) => { + setFormRoute({ ...formRoute, ...data }); + }; + + const peerDropDownRender = (menu: React.ReactElement) => <>{menu}; + + const toggleEditName = (status: boolean) => { + setEditName(status); + }; + + const toggleEditDescription = (status: boolean) => { + setEditDescription(status); + }; + + const networkRangeValidator = (_: RuleObject, value: string) => { + if (!cidrRegex().test(value)) { + return Promise.reject( + new Error("Please enter a valid CIDR, e.g. 192.168.1.0/24") + ); + } + + if (Number(value.split("/")[1]) < 7) { + return Promise.reject( + new Error("Please enter a network mask larger than /7") + ); + } + + return Promise.resolve(); + }; + + const peerValidator = (_: RuleObject, value: string) => { + if (value == "" && newRoute) { + return Promise.reject(new Error("Please select routing one peer")); + } + + return Promise.resolve(); + }; + + const selectPreValidator = (obj: RuleObject, value: string[]) => { + if (setupNewRouteHA && formRoute.peer == "") { + let [, newGroups] = getExistingAndToCreateGroupsLists(value); + if (newGroups.length > 0) { + return Promise.reject( + new Error( + "You can't add new Groups from the group update view, please remove:\"" + + newGroups + + '"' + ) + ); + } + } + return selectValidator(obj, value); + }; + + const handleMasqueradeChange = (checked: boolean) => { + setFormRoute({ + ...formRoute, + masquerade: checked, + }); + }; + + const handleEnableChange = (checked: boolean) => { + setFormRoute({ + ...formRoute, + enabled: checked, + }); + }; + + return ( + <> + {route && ( + + + + + } + > +
+ + +
+ + Add Route + + + {!!selectedPeer.peer && ( +
+ + + Assign a peer as a routing peer for the Network CIDR + + + toggleEditName(false)} + onBlur={() => toggleEditName(false)} + autoComplete="off" + maxLength={40} + /> + + + )} + {!editDescription ? ( +
toggleEditDescription(true)} + style={{ + margin: "0 0 30px", + lineHeight: "22px", + cursor: "pointer", + }} + > + {formRoute.description && + formRoute.description.trim() !== "" ? ( + formRoute.description + ) : ( + + Add description + + )} +
+ ) : ( + + toggleEditDescription(false)} + onBlur={() => toggleEditDescription(false)} + autoComplete="off" + maxLength={200} + /> + + )} + + + + + +
+ + {/* {!!!selectedPeer.peer && ( + + +
+ +
+ + + You can enable or disable the route + +
+
+
+ + )} */} + + + + Add a private IP address range + + + + + + {!!!selectedPeer.peer && ( + + + + Assign a peer as a routing peer for the Network CIDR + + + + {tagGroups.map((m) => ( + + ))} + + + + + + +
+ +
+ + + You can enable or disable the route + +
+
+
+ + + + + + More settings + + } + className="system-info-panel" + > + + + +
+ +
+ + + Allow access to your private networks without + configuring routes on your local routers or + other devices. + +
+
+
+ + + + + + Lower metrics indicating higher priority routes + + + + + + + + + +
+
+
+ + + + Learn more about + + {" "} + Network Routes + + + +
+
+
+ )} + + ); +}; + +export default RouteAddNew; diff --git a/src/components/RouteUpdate.tsx b/src/components/RouteUpdate.tsx index 1be7e4d..a9a1857 100644 --- a/src/components/RouteUpdate.tsx +++ b/src/components/RouteUpdate.tsx @@ -1,472 +1,560 @@ -import React, {useEffect, useRef, useState} from 'react'; -import {useDispatch, useSelector} from "react-redux"; -import {RootState} from "typesafe-actions"; -import {actions as routeActions} from '../store/route'; +import React, { useEffect, useRef, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { RootState } from "typesafe-actions"; +import { actions as routeActions } from "../store/route"; import { - Button, - Col, - Divider, - Drawer, - Form, - Input, - InputNumber, - Radio, - Row, - Select, - SelectProps, - Space, - Switch, - Typography + Button, + Col, + Divider, + Drawer, + Form, + Input, + InputNumber, + Radio, + Row, + Select, + SelectProps, + Space, + Switch, + Typography, } from "antd"; -import {CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons"; -import {Route, RouteToSave} from "../store/route/types"; -import {Header} from "antd/es/layout/layout"; -import {RuleObject} from "antd/lib/form"; -import cidrRegex from 'cidr-regex'; import { - initPeerMaps, - masqueradeDisabledMSG, - peerToPeerIP, - routePeerSeparator, - transformGroupedDataTable -} from '../utils/routes' -import {useGetTokenSilently} from "../utils/token"; -import {useGetGroupTagHelpers} from "../utils/groups"; + CloseOutlined, + FlagFilled, + QuestionCircleFilled, +} from "@ant-design/icons"; +import { Route, RouteToSave } from "../store/route/types"; +import { Header } from "antd/es/layout/layout"; +import { RuleObject } from "antd/lib/form"; +import cidrRegex from "cidr-regex"; +import { + initPeerMaps, + masqueradeDisabledMSG, + peerToPeerIP, + routePeerSeparator, + transformGroupedDataTable, +} from "../utils/routes"; +import { useGetTokenSilently } from "../utils/token"; +import { useGetGroupTagHelpers } from "../utils/groups"; -const {Paragraph} = Typography; +const { Paragraph } = Typography; -interface FormRoute extends Route { -} +interface FormRoute extends Route {} const RouteUpdate = () => { - const { - blueTagRender, - handleChangeTags, - dropDownRender, - optionRender, - tagGroups, - getExistingAndToCreateGroupsLists, - getGroupNamesFromIDs, - selectValidator - } = useGetGroupTagHelpers() - const {Option} = Select; - const {getTokenSilently} = useGetTokenSilently() - const dispatch = useDispatch() - const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible) - const setupNewRouteHA = useSelector((state: RootState) => state.route.setupNewRouteHA) - const peers = useSelector((state: RootState) => state.peer.data) - const route = useSelector((state: RootState) => state.route.route) - const routes = useSelector((state: RootState) => state.route.data) - const savedRoute = useSelector((state: RootState) => state.route.savedRoute) - const [previousRouteKey, setPreviousRouteKey] = useState("") - const [editName, setEditName] = useState(false) - const [editDescription, setEditDescription] = useState(false) - const options: SelectProps['options'] = []; - const [formRoute, setFormRoute] = useState({} as FormRoute) - const [form] = Form.useForm() - const inputNameRef = useRef(null) - const inputDescriptionRef = useRef(null) + const { + blueTagRender, + handleChangeTags, + dropDownRender, + optionRender, + tagGroups, + getExistingAndToCreateGroupsLists, + getGroupNamesFromIDs, + selectValidator, + } = useGetGroupTagHelpers(); + const { Option } = Select; + const { getTokenSilently } = useGetTokenSilently(); + const dispatch = useDispatch(); + const setupEditRouteVisible = useSelector( + (state: RootState) => state.route.setupEditRouteVisible + ); + const setupNewRouteHA = useSelector( + (state: RootState) => state.route.setupNewRouteHA + ); + const peers = useSelector((state: RootState) => state.peer.data); + const route = useSelector((state: RootState) => state.route.route); + const routes = useSelector((state: RootState) => state.route.data); + const savedRoute = useSelector((state: RootState) => state.route.savedRoute); + const [previousRouteKey, setPreviousRouteKey] = useState(""); + const [editName, setEditName] = useState(false); + const [editDescription, setEditDescription] = useState(false); + const options: SelectProps["options"] = []; + const [formRoute, setFormRoute] = useState({} as FormRoute); + const [form] = Form.useForm(); + const inputNameRef = useRef(null); + const inputDescriptionRef = useRef(null); - const defaultRoutingPeerMSG = "Routing Peer" - const [routingPeerMSG, setRoutingPeerMSG] = useState(defaultRoutingPeerMSG) - const defaultMasqueradeMSG = "Masquerade" - const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG) - const defaultStatusMSG = "Status" - const [statusMSG, setStatusMSG] = useState(defaultStatusMSG) - const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers); - const [newRoute, setNewRoute] = useState(false) + const defaultRoutingPeerMSG = "Routing Peer"; + const [routingPeerMSG, setRoutingPeerMSG] = useState(defaultRoutingPeerMSG); + const defaultMasqueradeMSG = "Masquerade"; + const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG); + const defaultStatusMSG = "Status"; + const [statusMSG, setStatusMSG] = useState(defaultStatusMSG); + const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers); + const [newRoute, setNewRoute] = useState(false); - const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}] + const optionsDisabledEnabled = [ + { label: "Enabled", value: true }, + { label: "Disabled", value: false }, + ]; - useEffect(() => { - if (!newRoute ) { - setRoutingPeerMSG(defaultRoutingPeerMSG) - setMasqueradeMSG("Update Masquerade") - setStatusMSG("Update Status") - } else { - setRoutingPeerMSG(defaultRoutingPeerMSG) - setMasqueradeMSG(defaultMasqueradeMSG) - setStatusMSG(defaultStatusMSG) - setPreviousRouteKey("") - } - }, [newRoute]) + useEffect(() => { + if (!newRoute) { + setRoutingPeerMSG(defaultRoutingPeerMSG); + setMasqueradeMSG("Update Masquerade"); + setStatusMSG("Update Status"); + } else { + setRoutingPeerMSG(defaultRoutingPeerMSG); + setMasqueradeMSG(defaultMasqueradeMSG); + setStatusMSG(defaultStatusMSG); + setPreviousRouteKey(""); + } + }, [newRoute]); - useEffect(() => { - if (editName) inputNameRef.current!.focus({ - cursor: 'end', - }); - }, [editName]); + useEffect(() => { + if (editName) + inputNameRef.current!.focus({ + cursor: "end", + }); + }, [editName]); - useEffect(() => { - if (editDescription) inputDescriptionRef.current!.focus({ - cursor: 'end', - }); - }, [editDescription]); + useEffect(() => { + if (editDescription) + inputDescriptionRef.current!.focus({ + cursor: "end", + }); + }, [editDescription]); - useEffect(() => { - if (!route) return + useEffect(() => { + if (!route) return; - const fRoute = { - ...route, - groups: getGroupNamesFromIDs(route.groups) - } as FormRoute - setFormRoute(fRoute) - setPreviousRouteKey(fRoute.network_id + fRoute.network) - if (!route.network_id) { - setNewRoute(true) - } else { - setNewRoute(false) - } - form.setFieldsValue(fRoute) - }, [route]) + const fRoute = { + ...route, + groups: getGroupNamesFromIDs(route.groups), + } as FormRoute; + setFormRoute(fRoute); + setPreviousRouteKey(fRoute.network_id + fRoute.network); + if (!route.network_id) { + setNewRoute(true); + } else { + setNewRoute(false); + } + form.setFieldsValue(fRoute); + }, [route]); - peers.forEach((p) => { - let os: string - os = p.os - if (!os.toLowerCase().startsWith("darwin") && !os.toLowerCase().startsWith("windows") && !os.toLowerCase().startsWith("android") - && route && !routes.filter(r => r.network_id === route.network_id).find(r => r.peer === p.id)) { - options?.push({ - label: peerToPeerIP(p.name, p.ip), - value: peerToPeerIP(p.name, p.ip), - disabled: false + peers.forEach((p) => { + let os: string; + os = p.os; + if ( + !os.toLowerCase().startsWith("darwin") && + !os.toLowerCase().startsWith("windows") && + !os.toLowerCase().startsWith("android") && + route && + !routes + .filter((r) => r.network_id === route.network_id) + .find((r) => r.peer === p.id) + ) { + options?.push({ + label: peerToPeerIP(p.name, p.ip), + value: peerToPeerIP(p.name, p.ip), + disabled: false, + }); + } + }); + + const createRouteToSave = (inputRoute: FormRoute): RouteToSave => { + let peerIDList = inputRoute.peer.split(routePeerSeparator); + let peerID: string; + if (peerIDList.length === 1) { + peerID = inputRoute.peer; + } else { + if (peerIDList[1]) { + peerID = peerIPToID[peerIDList[1]]; + } else { + peerID = peerIPToID[peerNameToIP[inputRoute.peer]]; + } + } + + let [existingGroups, groupsToCreate] = getExistingAndToCreateGroupsLists( + inputRoute.groups + ); + + return { + id: inputRoute.id, + network: inputRoute.network, + network_id: inputRoute.network_id, + description: inputRoute.description, + peer: peerID, + enabled: inputRoute.enabled, + masquerade: inputRoute.masquerade, + metric: inputRoute.metric, + groups: existingGroups, + groupsToCreate: groupsToCreate, + } as RouteToSave; + }; + + const handleFormSubmit = () => { + form + .validateFields() + .then(() => { + if (!setupNewRouteHA || formRoute.peer != "") { + const routeToSave = createRouteToSave(formRoute); + dispatch( + routeActions.saveRoute.request({ + getAccessTokenSilently: getTokenSilently, + payload: routeToSave, }) - } - }) - - const createRouteToSave = (inputRoute: FormRoute): RouteToSave => { - let peerIDList = inputRoute.peer.split(routePeerSeparator) - let peerID: string - if (peerIDList.length === 1) { - peerID = inputRoute.peer + ); } else { - if (peerIDList[1]) { - peerID = peerIPToID[peerIDList[1]] - } else { - peerID = peerIPToID[peerNameToIP[inputRoute.peer]] + let groupedDataTable = transformGroupedDataTable(routes, peers); + groupedDataTable.forEach((group) => { + if (group.key == previousRouteKey) { + group.groupedRoutes.forEach((route) => { + let updateRoute: FormRoute = { + ...formRoute, + id: route.id, + peer: route.peer, + metric: route.metric, + enabled: + formRoute.enabled != group.enabled + ? formRoute.enabled + : route.enabled, + }; + const routeToSave = createRouteToSave(updateRoute); + dispatch( + routeActions.saveRoute.request({ + getAccessTokenSilently: getTokenSilently, + payload: routeToSave, + }) + ); + }); } + }); } + }) + .catch((errorInfo) => { + console.log("errorInfo", errorInfo); + }); + }; - let [ existingGroups, groupsToCreate ] = getExistingAndToCreateGroupsLists(inputRoute.groups) + const setVisibleNewRoute = (status: boolean) => { + dispatch(routeActions.setSetupEditRouteVisible(status)); + }; - return { - id: inputRoute.id, - network: inputRoute.network, - network_id: inputRoute.network_id, - description: inputRoute.description, - peer: peerID, - enabled: inputRoute.enabled, - masquerade: inputRoute.masquerade, - metric: inputRoute.metric, - groups: existingGroups, - groupsToCreate: groupsToCreate, - } as RouteToSave + const setSetupNewRouteHA = (status: boolean) => { + dispatch(routeActions.setSetupNewRouteHA(status)); + }; + + const onCancel = () => { + if (savedRoute.loading) return; + setEditName(false); + dispatch( + routeActions.setRoute({ + network: "", + network_id: "", + description: "", + peer: "", + metric: 9999, + masquerade: false, + enabled: true, + groups: [], + } as Route) + ); + setVisibleNewRoute(false); + setSetupNewRouteHA(false); + setPreviousRouteKey(""); + setNewRoute(false); + }; + + const onChange = (data: any) => { + setFormRoute({ ...formRoute, ...data }); + }; + + const peerDropDownRender = (menu: React.ReactElement) => <>{menu}; + + const toggleEditName = (status: boolean) => { + setEditName(status); + }; + + const toggleEditDescription = (status: boolean) => { + setEditDescription(status); + }; + + const networkRangeValidator = (_: RuleObject, value: string) => { + if (!cidrRegex().test(value)) { + return Promise.reject( + new Error("Please enter a valid CIDR, e.g. 192.168.1.0/24") + ); } - const handleFormSubmit = () => { - form.validateFields() - .then(() => { - if (!setupNewRouteHA || formRoute.peer != '') { - const routeToSave = createRouteToSave(formRoute) - dispatch(routeActions.saveRoute.request({ - getAccessTokenSilently: getTokenSilently, - payload: routeToSave - })) - } else { - let groupedDataTable = transformGroupedDataTable(routes, peers) - groupedDataTable.forEach((group) => { - if (group.key == previousRouteKey) { - group.groupedRoutes.forEach((route) => { - let updateRoute: FormRoute = { - ...formRoute, - id: route.id, - peer: route.peer, - metric: route.metric, - enabled: (formRoute.enabled != group.enabled) ? formRoute.enabled : route.enabled - } - const routeToSave = createRouteToSave(updateRoute) - dispatch(routeActions.saveRoute.request({ - getAccessTokenSilently: getTokenSilently, - payload: routeToSave - })) - }) - } - }) - } - - }) - .catch((errorInfo) => { - console.log('errorInfo', errorInfo) - }); - }; - - const setVisibleNewRoute = (status: boolean) => { - dispatch(routeActions.setSetupNewRouteVisible(status)); + if (Number(value.split("/")[1]) < 7) { + return Promise.reject( + new Error("Please enter a network mask larger than /7") + ); } - const setSetupNewRouteHA = (status: boolean) => { - dispatch(routeActions.setSetupNewRouteHA(status)); + return Promise.resolve(); + }; + + const peerValidator = (_: RuleObject, value: string) => { + if (value == "" && newRoute) { + return Promise.reject(new Error("Please select routing one peer")); } - const onCancel = () => { - if (savedRoute.loading) return - setEditName(false) - dispatch(routeActions.setRoute({ - network: '', - network_id: '', - description: '', - peer: "", - metric: 9999, - masquerade: false, - enabled: true - } as Route)) - setVisibleNewRoute(false) - setSetupNewRouteHA(false) - setPreviousRouteKey("") - setNewRoute(false) + return Promise.resolve(); + }; + + const selectPreValidator = (obj: RuleObject, value: string[]) => { + if (setupNewRouteHA && formRoute.peer == "") { + let [, newGroups] = getExistingAndToCreateGroupsLists(value); + if (newGroups.length > 0) { + return Promise.reject( + new Error( + "You can't add new Groups from the group update view, please remove:\"" + + newGroups + + '"' + ) + ); + } } + return selectValidator(obj, value); + }; - const onChange = (data: any) => { - setFormRoute({...formRoute, ...data}) - } - - const peerDropDownRender = (menu: React.ReactElement) => ( - <> - {menu} - - ) - - const toggleEditName = (status: boolean) => { - setEditName(status); - } - - const toggleEditDescription = (status: boolean) => { - setEditDescription(status); - } - - const networkRangeValidator = (_: RuleObject, value: string) => { - if (!cidrRegex().test(value)) { - return Promise.reject(new Error("Please enter a valid CIDR, e.g. 192.168.1.0/24")) - } - - if (Number(value.split("/")[1]) < 7) { - return Promise.reject(new Error("Please enter a network mask larger than /7")) - } - - return Promise.resolve() - } - - const peerValidator = (_: RuleObject, value: string) => { - - if (value == "" && newRoute) { - return Promise.reject(new Error("Please select routing one peer")) - } - - return Promise.resolve() - } - - const selectPreValidator = (obj: RuleObject, value: string[]) => { - if (setupNewRouteHA && formRoute.peer == '') { - let [, newGroups ] = getExistingAndToCreateGroupsLists(value) - if (newGroups.length > 0) { - return Promise.reject(new Error("You can't add new Groups from the group update view, please remove:\"" + newGroups +"\"")) - } - } - return selectValidator(obj, value) - } - - return ( - <> - {route && - - - - - } + return ( + <> + {route && ( + + + + + } + > +
+ + +
- - - -
- - - {!editName && !editDescription && formRoute.id && - - } - - - {!editName && formRoute.id ? ( -
toggleEditName(true)}>{formRoute.id ? formRoute.network_id : 'New Route'}
- ) : ( - - toggleEditName(false)} - onBlur={() => toggleEditName(false)} autoComplete="off" - maxLength={40}/> - - )} - {!editDescription ? ( -
toggleEditDescription(true)}>{formRoute.description && formRoute.description.trim() !== "" ? formRoute.description : 'Add description...'}
- ) : ( - - toggleEditDescription(false)} - onBlur={() => toggleEditDescription(false)} - autoComplete="off" maxLength={200}/> - - )} + + + {!editName && !editDescription && formRoute.id && ( + + )} + + + {!editName && formRoute.id ? ( +
toggleEditName(true)} + > + {formRoute.id ? formRoute.network_id : "New Route"} +
+ ) : ( + + toggleEditName(false)} + onBlur={() => toggleEditName(false)} + autoComplete="off" + maxLength={40} + /> + + )} + {!editDescription ? ( +
toggleEditDescription(true)} + > + {formRoute.description && + formRoute.description.trim() !== "" + ? formRoute.description + : "Add description..."} +
+ ) : ( + + toggleEditDescription(false)} + onBlur={() => toggleEditDescription(false)} + autoComplete="off" + maxLength={200} + /> + + )} + +
+ + + +
+ + + + + + + + + + + + - -
- - + + + + {tagGroups.map((m) => ( + + ))} + + + + + + + + + + + You can enable high-availability by assigning the same + network identifier and network CIDR to multiple routes. + + + + + + + + + + + + )} + + ); +}; - - - -
- - - - - - - - - - - - - - - - - - - { - tagGroups.map(m => - - ) - } - - - - - - - - - - - You can enable high-availability by assigning the same network identifier - and network CIDR to multiple routes. - - - - - - - - -
- - -
- } - - ) -} - -export default RouteUpdate \ No newline at end of file +export default RouteUpdate; diff --git a/src/index.css b/src/index.css index 843a585..de20f14 100644 --- a/src/index.css +++ b/src/index.css @@ -197,7 +197,7 @@ td.non-highlighted-table-column { } .tag-box .ant-select-selector { - padding: 0 5px!important; + padding: 0 5px !important; } .tag-box .ant-select-selection-item { @@ -219,4 +219,8 @@ td.non-highlighted-table-column { align-items: center; margin-top: 3px; text-align: center; +} + +.w-100 { + width: 100%; } \ No newline at end of file diff --git a/src/store/route/actions.ts b/src/store/route/actions.ts index 82c987e..e65dc3b 100644 --- a/src/store/route/actions.ts +++ b/src/store/route/actions.ts @@ -28,6 +28,7 @@ const actions = { setRoute: createAction('SET_ROUTE')(), setSetupNewRouteVisible: createAction('SET_SETUP_NEW_ROUTE_VISIBLE')(), + setSetupEditRouteVisible: createAction('SET_SETUP_EDIT_ROUTE_VISIBLE')(), setSetupNewRouteHA: createAction('SET_SETUP_NEW_ROUTE_HA')() }; diff --git a/src/store/route/reducer.ts b/src/store/route/reducer.ts index 0f41178..895cecc 100644 --- a/src/store/route/reducer.ts +++ b/src/store/route/reducer.ts @@ -13,7 +13,8 @@ type StateType = Readonly<{ deleteRoute: DeleteResponse; savedRoute: CreateResponse; setupNewRouteVisible: boolean; - setupNewRouteHA: boolean + setupNewRouteHA: boolean; + setupEditRouteVisible: boolean; }>; const initialState: StateType = { @@ -27,17 +28,18 @@ const initialState: StateType = { success: false, failure: false, error: null, - data : null + data: null, }, savedRoute: >{ loading: false, success: false, failure: false, error: null, - data : null + data: null, }, setupNewRouteVisible: false, - setupNewRouteHA: false + setupNewRouteHA: false, + setupEditRouteVisible: false, }; const data = createReducer(initialState.data as Route[]) @@ -79,6 +81,13 @@ const savedRoute = createReducer, ActionTypes>(init const setupNewRouteVisible = createReducer(initialState.setupNewRouteVisible) .handleAction(actions.setSetupNewRouteVisible, (store, action) => action.payload) +const setupEditRouteVisible = createReducer( + initialState.setupEditRouteVisible +).handleAction( + actions.setSetupEditRouteVisible, + (store, action) => action.payload +); + const setupNewRouteHA = createReducer(initialState.setupNewRouteHA) .handleAction(actions.setSetupNewRouteHA, (store, action) => action.payload) @@ -91,5 +100,6 @@ export default combineReducers({ deletedRoute, savedRoute, setupNewRouteVisible, - setupNewRouteHA + setupNewRouteHA, + setupEditRouteVisible, }); diff --git a/src/views/Routes.tsx b/src/views/Routes.tsx index e006278..4283e12 100644 --- a/src/views/Routes.tsx +++ b/src/views/Routes.tsx @@ -1,570 +1,792 @@ -import React, {useEffect, useState} from 'react'; +import React, { useEffect, useState } from "react"; import { - Alert, - Button, - Card, - Col, - Divider, - Dropdown, - Input, - Menu, - message, - Modal, Popover, - Radio, - RadioChangeEvent, - Row, - Select, - Space, - Switch, - Table, - Tag, - Tooltip, - Typography + Alert, + Button, + Card, + Col, + Divider, + Dropdown, + Input, + Menu, + message, + Modal, + Popover, + Radio, + RadioChangeEvent, + Row, + Select, + Space, + Switch, + Table, + Tag, + Tooltip, + Typography, } from "antd"; -import {Container} from "../components/Container"; -import {useDispatch, useSelector} from "react-redux"; -import {RootState} from "typesafe-actions"; -import {Route, RouteToSave} from "../store/route/types"; -import {actions as routeActions} from "../store/route"; -import {actions as peerActions} from "../store/peer"; -import {filter, sortBy} from "lodash"; -import {EllipsisOutlined, ExclamationCircleOutlined, QuestionCircleOutlined} from "@ant-design/icons"; -import RouteUpdate from "../components/RouteUpdate"; +import { Container } from "../components/Container"; +import { useDispatch, useSelector } from "react-redux"; +import { RootState } from "typesafe-actions"; +import { Route, RouteToSave } from "../store/route/types"; +import { actions as routeActions } from "../store/route"; +import { actions as peerActions } from "../store/peer"; +import { filter, sortBy } from "lodash"; +import { + EllipsisOutlined, + ExclamationCircleOutlined, + QuestionCircleOutlined, +} from "@ant-design/icons"; +import RouteAddNew from "../components/RouteAddNew"; import tableSpin from "../components/Spin"; import { - GroupedDataTable, - initPeerMaps, - masqueradeDisabledMSG, - masqueradeEnabledMSG, - peerToPeerIP, - RouteDataTable, - transformDataTable, - transformGroupedDataTable -} from '../utils/routes' -import {useGetTokenSilently} from "../utils/token"; -import {Group} from "../store/group/types"; -import {TooltipPlacement} from "antd/es/tooltip"; -import {actions as groupActions} from "../store/group"; -import {useGetGroupTagHelpers} from "../utils/groups"; -import {usePageSizeHelpers} from "../utils/pageSize"; + GroupedDataTable, + initPeerMaps, + masqueradeDisabledMSG, + masqueradeEnabledMSG, + peerToPeerIP, + RouteDataTable, + transformDataTable, + transformGroupedDataTable, +} from "../utils/routes"; +import { useGetTokenSilently } from "../utils/token"; +import { Group } from "../store/group/types"; +import { TooltipPlacement } from "antd/es/tooltip"; +import { actions as groupActions } from "../store/group"; +import { useGetGroupTagHelpers } from "../utils/groups"; +import { usePageSizeHelpers } from "../utils/pageSize"; +import RouteUpdate from "../components/RouteUpdate"; -const {Title, Paragraph, Text} = Typography; -const {Column} = Table; -const {confirm} = Modal; +const { Title, Paragraph, Text } = Typography; +const { Column } = Table; +const { confirm } = Modal; export const Routes = () => { - const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers() - const {getTokenSilently} = useGetTokenSilently() - const dispatch = useDispatch() - const { - getGroupNamesFromIDs, - } = useGetGroupTagHelpers() + const { onChangePageSize, pageSizeOptions, pageSize } = usePageSizeHelpers(); + const { getTokenSilently } = useGetTokenSilently(); + const dispatch = useDispatch(); + const { getGroupNamesFromIDs } = useGetGroupTagHelpers(); - const groups = useSelector((state: RootState) => state.group.data) - const routes = useSelector((state: RootState) => state.route.data); - const failed = useSelector((state: RootState) => state.route.failed); - const loading = useSelector((state: RootState) => state.route.loading); - const deletedRoute = useSelector((state: RootState) => state.route.deletedRoute); - const savedRoute = useSelector((state: RootState) => state.route.savedRoute); - const peers = useSelector((state: RootState) => state.peer.data) - const loadingPeer = useSelector((state: RootState) => state.peer.loading); - const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible) - const [showTutorial, setShowTutorial] = useState(true) - const [textToSearch, setTextToSearch] = useState(''); - const [optionAllEnable, setOptionAllEnable] = useState('enabled'); - const [currentPage, setCurrentPage] = useState(1); - const [dataTable, setDataTable] = useState([] as RouteDataTable[]); - const [routeToAction, setRouteToAction] = useState(null as RouteDataTable | null); - const [groupedDataTable, setGroupedDataTable] = useState([] as GroupedDataTable[]); - const [expandRowsOnClick, setExpandRowsOnClick] = useState(true) - const [groupPopupVisible, setGroupPopupVisible] = useState("") + const groups = useSelector((state: RootState) => state.group.data); + const routes = useSelector((state: RootState) => state.route.data); + const failed = useSelector((state: RootState) => state.route.failed); + const loading = useSelector((state: RootState) => state.route.loading); + const deletedRoute = useSelector( + (state: RootState) => state.route.deletedRoute + ); + const savedRoute = useSelector((state: RootState) => state.route.savedRoute); + const peers = useSelector((state: RootState) => state.peer.data); + const loadingPeer = useSelector((state: RootState) => state.peer.loading); + const setupNewRouteVisible = useSelector( + (state: RootState) => state.route.setupNewRouteVisible + ); + const [showTutorial, setShowTutorial] = useState(true); + const [textToSearch, setTextToSearch] = useState(""); + const [optionAllEnable, setOptionAllEnable] = useState("enabled"); + const [currentPage, setCurrentPage] = useState(1); + const [dataTable, setDataTable] = useState([] as RouteDataTable[]); + const [routeToAction, setRouteToAction] = useState( + null as RouteDataTable | null + ); + const [groupedDataTable, setGroupedDataTable] = useState( + [] as GroupedDataTable[] + ); + const [expandRowsOnClick, setExpandRowsOnClick] = useState(true); + const [groupPopupVisible, setGroupPopupVisible] = useState(""); - const [peerNameToIP, peerIPToName] = initPeerMaps(peers); - const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'}, {label: 'All', value: 'all'}] + const [peerNameToIP, peerIPToName] = initPeerMaps(peers); + const optionsAllEnabled = [ + { label: "Enabled", value: "enabled" }, + { label: "All", value: "all" }, + ]; - const itemsMenuAction = [ - { - key: "view", - label: () - }, - { - key: "delete", - label: () - } - ] - const actionsMenu = () + const itemsMenuAction = [ + { + key: "view", + label: ( + + ), + }, + { + key: "delete", + label: ( + + ), + }, + ]; + const actionsMenu = ; - const isShowTutorial = (routes: Route[]): boolean => { - return (!routes.length || (routes.length === 1 && routes[0].network === "Default")) - } + const isShowTutorial = (routes: Route[]): boolean => { + return ( + !routes.length || (routes.length === 1 && routes[0].network === "Default") + ); + }; - useEffect(() => { - dispatch(routeActions.getRoutes.request({getAccessTokenSilently: getTokenSilently, payload: null})); - }, [peers]) + useEffect(() => { + dispatch( + routeActions.getRoutes.request({ + getAccessTokenSilently: getTokenSilently, + payload: null, + }) + ); + }, [peers]); - useEffect(() => { - dispatch(peerActions.getPeers.request({getAccessTokenSilently: getTokenSilently, payload: null})); - dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null})); - }, []) + useEffect(() => { + dispatch( + peerActions.getPeers.request({ + getAccessTokenSilently: getTokenSilently, + payload: null, + }) + ); + dispatch( + groupActions.getGroups.request({ + getAccessTokenSilently: getTokenSilently, + payload: null, + }) + ); + }, []); - const filterGroupedDataTable = (routes: GroupedDataTable[]): GroupedDataTable[] => { - const t = textToSearch.toLowerCase().trim() - let f: GroupedDataTable[] = filter(routes, (f) => - (f.network_id.toLowerCase().includes(t) || f.network.toLowerCase().includes(t) || - f.description.toLowerCase().includes(t) || t === "" || - getGroupNamesFromIDs(f.routesGroups).find(u => u.toLowerCase().trim().includes(t)) ) - ) as GroupedDataTable[] - if (optionAllEnable !== "all") { - f = filter(f, (f) => f.enabled) - } - return f - } - - useEffect(() => { - setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers))) - }, [dataTable]) - - useEffect(() => { - if (failed) { - setShowTutorial(false) - } else { - setShowTutorial(isShowTutorial(routes)) - setDataTable(sortBy(transformDataTable(routes, peers), "network_id")) - } - }, [routes]) - - useEffect(() => { - setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers))) - }, [textToSearch, optionAllEnable]) - - const styleNotification = {marginTop: 85} - - const saveKey = 'saving'; - useEffect(() => { - if (savedRoute.loading) { - message.loading({content: 'Saving...', key: saveKey, duration: 0, style: styleNotification}) - } else if (savedRoute.success) { - message.success({ - content: 'Route has been successfully updated.', - key: saveKey, - duration: 2, - style: styleNotification - }); - dispatch(routeActions.setSetupNewRouteVisible(false)) - dispatch(routeActions.setSavedRoute({...savedRoute, success: false})) - dispatch(routeActions.resetSavedRoute(null)) - } else if (savedRoute.error) { - let errorMsg = "Failed to update network route" - switch (savedRoute.error.statusCode) { - case 403: - errorMsg = "Failed to update network route. You might not have enough permissions." - break - default: - errorMsg = savedRoute.error.data.message ? savedRoute.error.data.message : errorMsg - break - } - message.error({ - content: errorMsg, - key: saveKey, - duration: 5, - style: styleNotification - }); - dispatch(routeActions.setSavedRoute({...savedRoute, error: null})) - dispatch(routeActions.resetSavedRoute(null)) - } - }, [savedRoute]) - - const deleteKey = 'deleting'; - useEffect(() => { - const style = {marginTop: 85} - if (deletedRoute.loading) { - message.loading({content: 'Deleting...', key: deleteKey, style}) - } else if (deletedRoute.success) { - message.success({content: 'Route has been successfully deleted.', key: deleteKey, duration: 2, style}) - dispatch(routeActions.resetDeletedRoute(null)) - } else if (deletedRoute.error) { - message.error({ - content: 'Failed to remove route. You might not have enough permissions.', - key: deleteKey, - duration: 2, - style - }) - dispatch(routeActions.resetDeletedRoute(null)) - } - }, [deletedRoute]) - - const onChangeTextToSearch = (e: React.ChangeEvent) => { - setTextToSearch(e.target.value) - }; - - const searchDataTable = () => { - setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers))) - } - - const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => { - setOptionAllEnable(value) - } - - const showConfirmDelete = () => { - let name = routeToAction ? routeToAction.network_id : ''; - confirm({ - icon: , - title: "Delete network route \"" + name + "\"", - width: 600, - content: - Are you sure you want to delete this route from your account? - , - okType: 'danger', - onOk() { - dispatch(routeActions.deleteRoute.request({ - getAccessTokenSilently: getTokenSilently, - payload: routeToAction?.id || '' - })); - }, - onCancel() { - setRouteToAction(null); - }, - }); - } - - - const onClickAddNewRoute = () => { - dispatch(routeActions.setSetupNewRouteVisible(true)); - dispatch(routeActions.setRoute({ - network: '', - network_id: '', - description: '', - peer: '', - masquerade: true, - metric: 9999, - enabled: true - } as Route)) - } - - const onClickViewRoute = () => { - dispatch(routeActions.setSetupNewRouteHA(false)); - dispatch(routeActions.setRoute({ - id: routeToAction?.id || null, - network: routeToAction?.network, - network_id: routeToAction?.network_id, - description: routeToAction?.description, - peer: peerToPeerIP(routeToAction!.peer_name, routeToAction!.peer_ip), - metric: routeToAction?.metric, - masquerade: routeToAction?.masquerade, - enabled: routeToAction?.enabled, - groups: routeToAction?.groups - } as Route)) - dispatch(routeActions.setSetupNewRouteVisible(true)); - } - - const setRouteAndView = (route: RouteDataTable) => { - if (!route.id) { - dispatch(routeActions.setSetupNewRouteHA(true)); - } - dispatch(routeActions.setRoute({ - id: route.id || null, - network: route.network, - network_id: route.network_id, - description: route.description, - peer: route.peer ? peerToPeerIP(route.peer_name, route.peer_ip) : '', - metric: route.metric ? route.metric : 9999, - masquerade: route.masquerade, - enabled: route.enabled, - groups: route.groups - } as Route)) - dispatch(routeActions.setSetupNewRouteVisible(true)); - } - - const showConfirmEnableMasquerade = (record: GroupedDataTable, checked: boolean) => { - let label = record.network_id ? record.network_id : record.network - let tittle = "Enable Masquerade for \"" + label + "\"?" - let content = masqueradeDisabledMSG - - if (!checked) { - tittle = "Disable Masquerade for \"" + label + "\"?" - content = masqueradeEnabledMSG - } - - confirm({ - icon: , - title: tittle, - width: 600, - content: content, - okType: 'danger', - onOk() { - handleSwitchMasquerade(record, checked) - }, - onCancel() { - }, - }); - } - - const onPopoverVisibleChange = (b:boolean, key: string) => { - if (setupNewRouteVisible) { - setGroupPopupVisible("") - } else { - if(b) { - setGroupPopupVisible(key) - } else { - setGroupPopupVisible("") - } - } - } - - function handleSwitchMasquerade(routeGroup: GroupedDataTable, checked: boolean) { - routeGroup.groupedRoutes.forEach((record) => { - const route = { - ...record, - peer: record.peer, - masquerade: checked, - groupsToCreate: [] - } as RouteToSave - dispatch(routeActions.saveRoute.request({getAccessTokenSilently: getTokenSilently, payload: route})); - }) - } - - const renderPopoverGroups = (label: string, rowGroups: string[] | null, userToAction: RouteDataTable) => { - - let groupsMap = new Map(); - groups.forEach(g => { - groupsMap.set(g.id!, g) - }) - - let displayGroups: Group[] = [] - if (rowGroups) { - displayGroups = rowGroups.filter(g => groupsMap.get(g)).map(g => groupsMap.get(g)!) - } - - let btn = - - if (!displayGroups || displayGroups!.length < 1) { - return btn - } - - const content = displayGroups?.map((g, i) => { - const _g = g as Group - const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} ` - return ( -
- - {_g.name} - - {peersCount} -
- ) - }) - const mainContent = ({content}) - let popoverPlacement = "top" - if (content && content.length > 5) { - popoverPlacement = "rightTop" - } - - return ( - onPopoverVisibleChange(b, userToAction.key)} - open={groupPopupVisible === userToAction.key} - content={mainContent} - title={null}> - {btn} - + const filterGroupedDataTable = ( + routes: GroupedDataTable[] + ): GroupedDataTable[] => { + const t = textToSearch.toLowerCase().trim(); + let f: GroupedDataTable[] = filter( + routes, + (f) => + f.network_id.toLowerCase().includes(t) || + f.network.toLowerCase().includes(t) || + f.description.toLowerCase().includes(t) || + t === "" || + getGroupNamesFromIDs(f.routesGroups).find((u) => + u.toLowerCase().trim().includes(t) ) + ) as GroupedDataTable[]; + if (optionAllEnable !== "all") { + f = filter(f, (f) => f.enabled); + } + return f; + }; + + useEffect(() => { + setGroupedDataTable( + filterGroupedDataTable(transformGroupedDataTable(routes, peers)) + ); + }, [dataTable]); + + useEffect(() => { + if (failed) { + setShowTutorial(false); + } else { + setShowTutorial(isShowTutorial(routes)); + setDataTable(sortBy(transformDataTable(routes, peers), "network_id")); + } + }, [routes]); + + useEffect(() => { + setGroupedDataTable( + filterGroupedDataTable(transformGroupedDataTable(routes, peers)) + ); + }, [textToSearch, optionAllEnable]); + + const styleNotification = { marginTop: 85 }; + + const saveKey = "saving"; + useEffect(() => { + if (savedRoute.loading) { + message.loading({ + content: "Saving...", + key: saveKey, + duration: 0, + style: styleNotification, + }); + } else if (savedRoute.success) { + message.success({ + content: "Route has been successfully updated.", + key: saveKey, + duration: 2, + style: styleNotification, + }); + dispatch(routeActions.setSetupNewRouteVisible(false)); + dispatch(routeActions.setSetupEditRouteVisible(false)); + dispatch(routeActions.setSavedRoute({ ...savedRoute, success: false })); + dispatch(routeActions.resetSavedRoute(null)); + } else if (savedRoute.error) { + let errorMsg = "Failed to update network route"; + switch (savedRoute.error.statusCode) { + case 403: + errorMsg = + "Failed to update network route. You might not have enough permissions."; + break; + default: + errorMsg = savedRoute.error.data.message + ? savedRoute.error.data.message + : errorMsg; + break; + } + message.error({ + content: errorMsg, + key: saveKey, + duration: 5, + style: styleNotification, + }); + dispatch(routeActions.setSavedRoute({ ...savedRoute, error: null })); + dispatch(routeActions.resetSavedRoute(null)); + } + }, [savedRoute]); + + const deleteKey = "deleting"; + useEffect(() => { + const style = { marginTop: 85 }; + if (deletedRoute.loading) { + message.loading({ content: "Deleting...", key: deleteKey, style }); + } else if (deletedRoute.success) { + message.success({ + content: "Route has been successfully deleted.", + key: deleteKey, + duration: 2, + style, + }); + dispatch(routeActions.resetDeletedRoute(null)); + } else if (deletedRoute.error) { + message.error({ + content: + "Failed to remove route. You might not have enough permissions.", + key: deleteKey, + duration: 2, + style, + }); + dispatch(routeActions.resetDeletedRoute(null)); + } + }, [deletedRoute]); + + const onChangeTextToSearch = ( + e: React.ChangeEvent + ) => { + setTextToSearch(e.target.value); + }; + + const searchDataTable = () => { + setGroupedDataTable( + filterGroupedDataTable(transformGroupedDataTable(routes, peers)) + ); + }; + + const onChangeAllEnabled = ({ target: { value } }: RadioChangeEvent) => { + setOptionAllEnable(value); + }; + + const showConfirmDelete = () => { + let name = routeToAction ? routeToAction.network_id : ""; + confirm({ + icon: , + title: 'Delete network route "' + name + '"', + width: 600, + content: ( + + + Are you sure you want to delete this route from your account? + + + ), + okType: "danger", + onOk() { + dispatch( + routeActions.deleteRoute.request({ + getAccessTokenSilently: getTokenSilently, + payload: routeToAction?.id || "", + }) + ); + }, + onCancel() { + setRouteToAction(null); + }, + }); + }; + + const onClickAddNewRoute = () => { + dispatch(routeActions.setSetupNewRouteVisible(true)); + dispatch( + routeActions.setRoute({ + network: "", + network_id: "", + description: "", + peer: "", + masquerade: true, + metric: 9999, + enabled: true, + groups: [], + } as Route) + ); + }; + + + + const onClickViewRoute = () => { + dispatch(routeActions.setSetupNewRouteHA(false)); + dispatch( + routeActions.setRoute({ + id: routeToAction?.id || null, + network: routeToAction?.network, + network_id: routeToAction?.network_id, + description: routeToAction?.description, + peer: peerToPeerIP(routeToAction!.peer_name, routeToAction!.peer_ip), + metric: routeToAction?.metric, + masquerade: routeToAction?.masquerade, + enabled: routeToAction?.enabled, + groups: routeToAction?.groups, + } as Route) + ); + dispatch(routeActions.setSetupNewRouteVisible(true)); + }; + + const setRouteAndView = (route: RouteDataTable) => { + if (!route.id) { + dispatch(routeActions.setSetupNewRouteHA(true)); + } + dispatch( + routeActions.setRoute({ + id: route.id || null, + network: route.network, + network_id: route.network_id, + description: route.description, + peer: route.peer ? peerToPeerIP(route.peer_name, route.peer_ip) : "", + metric: route.metric ? route.metric : 9999, + masquerade: route.masquerade, + enabled: route.enabled, + groups: route.groups, + } as Route) + ); + dispatch(routeActions.setSetupEditRouteVisible(true)); + }; + + const showConfirmEnableMasquerade = ( + record: GroupedDataTable, + checked: boolean + ) => { + let label = record.network_id ? record.network_id : record.network; + let tittle = 'Enable Masquerade for "' + label + '"?'; + let content = masqueradeDisabledMSG; + + if (!checked) { + tittle = 'Disable Masquerade for "' + label + '"?'; + content = masqueradeEnabledMSG; } - const expandedRowRender = (record: GroupedDataTable) => { + confirm({ + icon: , + title: tittle, + width: 600, + content: content, + okType: "danger", + onOk() { + handleSwitchMasquerade(record, checked); + }, + onCancel() {}, + }); + }; - return - (record as any).peer.includes(value)} - sorter={(a, b) => ((a as any).peer.localeCompare((b as any).peer))} - render={(text, record) => { - return setRouteAndView(record as RouteDataTable)} - className="tooltip-label">{text} - }} - /> - (record as any).metric.includes(value)} - sorter={(a, b) => ((a as any).metric - ((b as any).metric))} - /> - { - return renderPopoverGroups(text, record.groups, record) - }} - /> - { - return text ? enabled : disabled - }} - /> - { - if (deletedRoute.loading || savedRoute.loading) return <> - return ( - { - if (visible) setRouteToAction(record as RouteDataTable) - }}> - - - ) - }} - /> -
- }; + const onPopoverVisibleChange = (b: boolean, key: string) => { + if (setupNewRouteVisible) { + setGroupPopupVisible(""); + } else { + if (b) { + setGroupPopupVisible(key); + } else { + setGroupPopupVisible(""); + } + } + }; + + function handleSwitchMasquerade( + routeGroup: GroupedDataTable, + checked: boolean + ) { + routeGroup.groupedRoutes.forEach((record) => { + const route = { + ...record, + peer: record.peer, + masquerade: checked, + groupsToCreate: [], + } as RouteToSave; + dispatch( + routeActions.saveRoute.request({ + getAccessTokenSilently: getTokenSilently, + payload: route, + }) + ); + }); + } + + const renderPopoverGroups = ( + label: string, + rowGroups: string[] | null, + userToAction: RouteDataTable + ) => { + let groupsMap = new Map(); + groups.forEach((g) => { + groupsMap.set(g.id!, g); + }); + + let displayGroups: Group[] = []; + if (rowGroups) { + displayGroups = rowGroups + .filter((g) => groupsMap.get(g)) + .map((g) => groupsMap.get(g)!); + } + + let btn = ( + + ); + + if (!displayGroups || displayGroups!.length < 1) { + return btn; + } + + const content = displayGroups?.map((g, i) => { + const _g = g as Group; + const peersCount = ` - ${_g.peers_count || 0} ${ + !_g.peers_count || parseInt(_g.peers_count) !== 1 ? "peers" : "peer" + } `; + return ( +
+ + {_g.name} + + {peersCount} +
+ ); + }); + const mainContent = {content}; + let popoverPlacement = "top"; + if (content && content.length > 5) { + popoverPlacement = "rightTop"; + } return ( - <> - - - - Network Routes - Network routes allow you to create routes to access other networks without installing - NetBird on every resource. - - - - - - - - - + + + + +