mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
support to routes with peer group updated the view logic to support the new type updated peer view as well for now, we are supporting a single group, but that can be extended
740 lines
23 KiB
TypeScript
740 lines
23 KiB
TypeScript
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,
|
|
Collapse,
|
|
Form,
|
|
Input,
|
|
InputNumber,
|
|
message,
|
|
Row,
|
|
Select,
|
|
SelectProps,
|
|
Breadcrumb,
|
|
Switch,
|
|
Card,
|
|
Typography,
|
|
Tabs,
|
|
} from "antd";
|
|
import type { TabsProps } from "antd";
|
|
import { Route, RouteToSave } from "../store/route/types";
|
|
import { Header } from "antd/es/layout/layout";
|
|
import { RuleObject } from "antd/lib/form";
|
|
import { isEmpty } from "lodash";
|
|
import {
|
|
initPeerMaps,
|
|
peerToPeerIP,
|
|
routePeerSeparator,
|
|
} from "../utils/routes";
|
|
import { useGetTokenSilently } from "../utils/token";
|
|
import { useGetGroupTagHelpers } from "../utils/groups";
|
|
import { Container } from "./Container";
|
|
|
|
const { Paragraph, Text } = Typography;
|
|
const { Panel } = Collapse;
|
|
|
|
interface FormRoute extends Route {}
|
|
|
|
const RoutePeerUpdate = () => {
|
|
const {
|
|
blueTagRender,
|
|
handleChangeTags,
|
|
dropDownRender,
|
|
optionRender,
|
|
tagGroups,
|
|
getExistingAndToCreateGroupsLists,
|
|
getGroupNamesFromIDs,
|
|
selectValidator,
|
|
} = useGetGroupTagHelpers();
|
|
|
|
const { Option } = Select;
|
|
const { getTokenSilently } = useGetTokenSilently();
|
|
const dispatch = useDispatch();
|
|
|
|
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 [editDescription, setEditDescription] = useState(false);
|
|
const options: SelectProps["options"] = [];
|
|
const [formRoute, setFormRoute] = useState({} as FormRoute);
|
|
const [form] = Form.useForm();
|
|
const inputDescriptionRef = useRef<any>(null);
|
|
const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers);
|
|
const [activeTab, setActiveTab] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!isEmpty(formRoute))
|
|
if (formRoute.peer_groups) {
|
|
setActiveTab("groupOfPeers");
|
|
} else {
|
|
setActiveTab("routingPeer");
|
|
}
|
|
}, [formRoute]);
|
|
|
|
useEffect(() => {
|
|
if (editDescription)
|
|
inputDescriptionRef.current!.focus({
|
|
cursor: "end",
|
|
});
|
|
}, [editDescription]);
|
|
|
|
useEffect(() => {
|
|
if (!route) return;
|
|
const fRoute = {
|
|
...route,
|
|
groups: route.groups,
|
|
} as FormRoute;
|
|
setFormRoute(fRoute);
|
|
form.setFieldsValue(fRoute);
|
|
// let options = [];
|
|
}, [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,
|
|
});
|
|
}
|
|
});
|
|
|
|
const handleSingleChangeTags = (values: any) => {
|
|
const lastValue = values[values.length - 1];
|
|
if (values.length > 0) {
|
|
form.setFieldsValue({
|
|
peer_groups: [lastValue],
|
|
});
|
|
}
|
|
};
|
|
|
|
const createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
|
|
if (inputRoute.peer_groups) {
|
|
inputRoute = {
|
|
...inputRoute,
|
|
peer_groups: [
|
|
inputRoute.peer_groups[inputRoute.peer_groups.length - 1],
|
|
],
|
|
};
|
|
}
|
|
|
|
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
|
|
);
|
|
|
|
const payload = {
|
|
id: inputRoute.id,
|
|
network: inputRoute.network,
|
|
network_id: inputRoute.network_id,
|
|
description: inputRoute.description,
|
|
enabled: inputRoute.enabled,
|
|
masquerade: inputRoute.masquerade,
|
|
metric: inputRoute.metric,
|
|
groups: existingGroups,
|
|
groupsToCreate: groupsToCreate,
|
|
} as RouteToSave;
|
|
|
|
if (activeTab === "routingPeer") {
|
|
let pay = { ...payload, peer: peerID };
|
|
return pay;
|
|
}
|
|
|
|
if (activeTab === "groupOfPeers") {
|
|
if (inputRoute.peer_groups) {
|
|
let [currentPeersGroup, peerGroupsToCreate] =
|
|
getExistingAndToCreateGroupsLists(inputRoute.peer_groups);
|
|
|
|
let pay = {
|
|
...payload,
|
|
peer_groups: currentPeersGroup,
|
|
peerGroupsToCreate: peerGroupsToCreate,
|
|
};
|
|
return pay;
|
|
}
|
|
}
|
|
|
|
return payload;
|
|
};
|
|
|
|
const handleFormSubmit = () => {
|
|
form
|
|
.validateFields()
|
|
.then(() => {
|
|
if (formRoute.peer !== "") {
|
|
const routeToSave = createRouteToSave(formRoute);
|
|
dispatch(
|
|
routeActions.saveRoute.request({
|
|
getAccessTokenSilently: getTokenSilently,
|
|
payload: routeToSave,
|
|
})
|
|
);
|
|
}
|
|
})
|
|
.catch((errorInfo) => {
|
|
console.log("errorInfo", errorInfo);
|
|
});
|
|
};
|
|
|
|
const setVisibleNewRoute = (status: boolean) => {
|
|
dispatch(routeActions.setSetupEditRoutePeerVisible(status));
|
|
};
|
|
|
|
const onCancel = () => {
|
|
if (savedRoute.loading) return;
|
|
dispatch(
|
|
routeActions.setRoute({
|
|
network: "",
|
|
network_id: "",
|
|
description: "",
|
|
peer: "",
|
|
metric: 9999,
|
|
masquerade: false,
|
|
enabled: true,
|
|
groups: [],
|
|
peer_groups: [],
|
|
} as Route)
|
|
);
|
|
setVisibleNewRoute(false);
|
|
};
|
|
|
|
const onChange = (data: any) => {
|
|
setFormRoute({ ...formRoute, ...data });
|
|
};
|
|
|
|
const peerDropDownRender = (menu: React.ReactElement) => <>{menu}</>;
|
|
|
|
const toggleEditDescription = (status: boolean) => {
|
|
setEditDescription(status);
|
|
};
|
|
|
|
const peerValidator = (_: RuleObject, value: string) => {
|
|
if (value == "") {
|
|
return Promise.reject(new Error("Please select routing one peer"));
|
|
}
|
|
|
|
return Promise.resolve();
|
|
};
|
|
|
|
const selectPreValidator = (obj: RuleObject, value: string[]) => {
|
|
if (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,
|
|
});
|
|
};
|
|
|
|
const onBreadcrumbUsersClick = () => {
|
|
onCancel();
|
|
};
|
|
|
|
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.setSetupEditRoutePeerVisible(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 onTabChange = (key: string) => {
|
|
console.log(key);
|
|
// setActiveTab(key);
|
|
};
|
|
|
|
const items: TabsProps["items"] = [
|
|
{
|
|
key: "routingPeer",
|
|
label: "Routing Peer",
|
|
disabled: true,
|
|
children: (
|
|
<>
|
|
<Paragraph
|
|
type={"secondary"}
|
|
style={{
|
|
marginTop: "-2",
|
|
fontWeight: "400",
|
|
marginBottom: "5px",
|
|
}}
|
|
>
|
|
Assign a peer as a routing peer for the Network CIDR
|
|
</Paragraph>
|
|
<Form.Item
|
|
name="peer"
|
|
rules={[
|
|
{
|
|
required: true,
|
|
message: "Please select routing one peer",
|
|
},
|
|
]}
|
|
style={{ maxWidth: "400px" }}
|
|
>
|
|
<Select
|
|
showSearch
|
|
style={{ width: "100%" }}
|
|
placeholder="Select Peer"
|
|
dropdownRender={peerDropDownRender}
|
|
options={options}
|
|
allowClear={true}
|
|
/>
|
|
</Form.Item>
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
key: "groupOfPeers",
|
|
label: "Peer group",
|
|
disabled: true,
|
|
children: (
|
|
<>
|
|
<Paragraph
|
|
type={"secondary"}
|
|
style={{
|
|
marginTop: "-2",
|
|
fontWeight: "400",
|
|
marginBottom: "5px",
|
|
}}
|
|
>
|
|
Assign peer group with Linux machines to be used as routing peers
|
|
</Paragraph>
|
|
<Form.Item
|
|
name="peer_groups"
|
|
rules={[
|
|
{
|
|
required: true,
|
|
message: "Please select peer group",
|
|
},
|
|
]}
|
|
>
|
|
<Select
|
|
mode="tags"
|
|
style={{ maxWidth: "400px" }}
|
|
tagRender={blueTagRender}
|
|
onChange={handleSingleChangeTags}
|
|
dropdownRender={dropDownRender}
|
|
optionFilterProp="serchValue"
|
|
>
|
|
{tagGroups.map((m, index) => (
|
|
<Option key={index} value={m.id} serchValue={m.name}>
|
|
{optionRender(m.name, m.id)}
|
|
</Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
</>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<Container style={{ paddingTop: "40px", paddingBottom: "50px" }}>
|
|
<Breadcrumb
|
|
style={{ marginBottom: "25px" }}
|
|
items={[
|
|
{
|
|
title: <a onClick={onBreadcrumbUsersClick}>Network Routes</a>,
|
|
},
|
|
{
|
|
title: formRoute.network_id,
|
|
},
|
|
]}
|
|
/>
|
|
{route && (
|
|
<Card>
|
|
<Form
|
|
layout="vertical"
|
|
form={form}
|
|
requiredMark={false}
|
|
onValuesChange={onChange}
|
|
style={{ width: "100%", maxWidth: "600px" }}
|
|
className="route-form edit-form-wrapper"
|
|
>
|
|
<Row gutter={16}>
|
|
<Col span={24}>
|
|
<Header
|
|
style={{
|
|
border: "none",
|
|
}}
|
|
>
|
|
<Row align="top">
|
|
<Col span={24} style={{ lineHeight: "20px" }}>
|
|
<div
|
|
style={{
|
|
color: "rgba(0, 0, 0, 0.88)",
|
|
fontWeight: "500",
|
|
fontSize: "22px",
|
|
}}
|
|
>
|
|
{formRoute.network_id}
|
|
|
|
<Paragraph
|
|
type={"secondary"}
|
|
style={{
|
|
textAlign: "left",
|
|
whiteSpace: "pre-line",
|
|
fontWeight: "400",
|
|
marginBottom: "0",
|
|
}}
|
|
>
|
|
<div style={{ margin: "5px 0" }}>
|
|
{" "}
|
|
{formRoute.network}
|
|
</div>
|
|
<div></div>
|
|
</Paragraph>
|
|
</div>
|
|
|
|
{!editDescription ? (
|
|
<div
|
|
onClick={() => toggleEditDescription(true)}
|
|
style={{
|
|
margin: "0 0 30px",
|
|
lineHeight: "22px",
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
{formRoute.description &&
|
|
formRoute.description.trim() !== "" ? (
|
|
formRoute.description
|
|
) : (
|
|
<span style={{ textDecoration: "underline" }}>
|
|
Add description
|
|
</span>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<Form.Item
|
|
name="description"
|
|
label="Description"
|
|
style={{ marginTop: 24, fontWeight: 500 }}
|
|
>
|
|
<Input
|
|
placeholder="Add description..."
|
|
ref={inputDescriptionRef}
|
|
onPressEnter={() => toggleEditDescription(false)}
|
|
onBlur={() => toggleEditDescription(false)}
|
|
autoComplete="off"
|
|
style={{ maxWidth: "400px" }}
|
|
/>
|
|
</Form.Item>
|
|
)}
|
|
</Col>
|
|
</Row>
|
|
<Row align="top">
|
|
<Col flex="auto"></Col>
|
|
</Row>
|
|
</Header>
|
|
</Col>
|
|
|
|
<Col span={24}>
|
|
<Form.Item name="enabled" label="">
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
gap: "15px",
|
|
}}
|
|
>
|
|
<Switch
|
|
size={"small"}
|
|
checked={formRoute.enabled}
|
|
onChange={handleEnableChange}
|
|
/>
|
|
<div>
|
|
<label
|
|
style={{
|
|
color: "rgba(0, 0, 0, 0.88)",
|
|
fontSize: "14px",
|
|
fontWeight: "500",
|
|
}}
|
|
>
|
|
Enabled
|
|
</label>
|
|
<Paragraph
|
|
type={"secondary"}
|
|
style={{
|
|
marginTop: "-2",
|
|
fontWeight: "400",
|
|
marginBottom: "0",
|
|
}}
|
|
>
|
|
You can enable or disable the route
|
|
</Paragraph>
|
|
</div>
|
|
</div>
|
|
</Form.Item>
|
|
</Col>
|
|
|
|
<Col span={24}>
|
|
{activeTab && (
|
|
<Tabs
|
|
defaultActiveKey={activeTab}
|
|
items={items}
|
|
onChange={onTabChange}
|
|
/>
|
|
)}
|
|
</Col>
|
|
<Col span={24}>
|
|
<label
|
|
style={{
|
|
color: "rgba(0, 0, 0, 0.88)",
|
|
fontSize: "14px",
|
|
fontWeight: "500",
|
|
}}
|
|
>
|
|
Distribution groups
|
|
</label>
|
|
<Paragraph
|
|
type={"secondary"}
|
|
style={{
|
|
marginTop: "-2",
|
|
fontWeight: "400",
|
|
marginBottom: "5px",
|
|
}}
|
|
>
|
|
Advertise this route to peers that belong to the following
|
|
groups
|
|
</Paragraph>
|
|
<Form.Item
|
|
name="groups"
|
|
label=""
|
|
rules={[{ validator: selectPreValidator }]}
|
|
style={{ maxWidth: "400px" }}
|
|
>
|
|
<Select
|
|
mode="tags"
|
|
style={{ width: "100%" }}
|
|
placeholder="Associate groups with the network route"
|
|
tagRender={blueTagRender}
|
|
onChange={handleChangeTags}
|
|
dropdownRender={dropDownRender}
|
|
optionFilterProp="serchValue"
|
|
>
|
|
{tagGroups.map((m, index) => (
|
|
<Option key={index} value={m.id} serchValue={m.name}>
|
|
{optionRender(m.name, m.id)}
|
|
</Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
|
|
<Col span={24}>
|
|
<Collapse
|
|
onChange={onChange}
|
|
bordered={false}
|
|
ghost={true}
|
|
style={{ padding: "0" }}
|
|
className="remove-bg"
|
|
>
|
|
<Panel
|
|
key="0"
|
|
header={
|
|
<Paragraph
|
|
style={{
|
|
textAlign: "left",
|
|
whiteSpace: "pre-line",
|
|
fontSize: "14px",
|
|
fontWeight: "400",
|
|
margin: "0",
|
|
textDecoration: "underline",
|
|
}}
|
|
>
|
|
More settings
|
|
</Paragraph>
|
|
}
|
|
className="system-info-panel"
|
|
>
|
|
<Row gutter={16} style={{ padding: "15px 0 0" }}>
|
|
<Col span={12}>
|
|
<Form.Item name="masquerade" label="">
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
gap: "15px",
|
|
}}
|
|
>
|
|
<Switch
|
|
size={"small"}
|
|
checked={formRoute.masquerade}
|
|
onChange={handleMasqueradeChange}
|
|
/>
|
|
<div>
|
|
<label
|
|
style={{
|
|
color: "rgba(0, 0, 0, 0.88)",
|
|
fontSize: "14px",
|
|
fontWeight: "500",
|
|
}}
|
|
>
|
|
Masquerade
|
|
</label>
|
|
<Paragraph
|
|
type={"secondary"}
|
|
style={{
|
|
marginTop: "-2",
|
|
fontWeight: "400",
|
|
marginBottom: "0",
|
|
}}
|
|
>
|
|
Allow access to your private networks without
|
|
configuring routes on your local routers or
|
|
other devices.
|
|
</Paragraph>
|
|
</div>
|
|
</div>
|
|
</Form.Item>
|
|
</Col>
|
|
|
|
<Col span={12}>
|
|
<label
|
|
style={{
|
|
color: "rgba(0, 0, 0, 0.88)",
|
|
fontSize: "14px",
|
|
fontWeight: "500",
|
|
}}
|
|
>
|
|
Metric
|
|
</label>
|
|
<Paragraph
|
|
type={"secondary"}
|
|
style={{
|
|
marginTop: "-2",
|
|
fontWeight: "400",
|
|
marginBottom: "5px",
|
|
}}
|
|
>
|
|
Lower metrics indicating higher priority routes
|
|
</Paragraph>
|
|
<Row>
|
|
<Col span={12}>
|
|
<Form.Item name="metric" label="">
|
|
<InputNumber
|
|
min={1}
|
|
max={9999}
|
|
autoComplete="off"
|
|
className="w-100"
|
|
/>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
</Col>
|
|
</Row>
|
|
</Panel>
|
|
</Collapse>
|
|
</Col>
|
|
</Row>
|
|
<Col
|
|
span={24}
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "start",
|
|
gap: "10px",
|
|
marginTop: "20px",
|
|
}}
|
|
>
|
|
<Button onClick={onCancel} disabled={savedRoute.loading}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
disabled={savedRoute.loading}
|
|
onClick={handleFormSubmit}
|
|
>
|
|
Save
|
|
</Button>
|
|
</Col>
|
|
</Form>
|
|
</Card>
|
|
)}
|
|
</Container>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default RoutePeerUpdate;
|