From c677eeaae4b375c2a820e6c4891ef35fc34ee1d5 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 8 Dec 2022 17:24:34 +0100 Subject: [PATCH] Add distribution groups to Network routes (#118) Users can add distribution groups to network routes Groups can be added to individual network routes or to all routes in the group Adding a new group in the modal is restricted to individual network route operations --- src/components/PeerUpdate.tsx | 2 +- src/components/RouteUpdate.tsx | 97 ++++++++++++++++++++++----- src/store/route/actions.ts | 4 +- src/store/route/sagas.ts | 32 ++++++++- src/store/route/types.ts | 7 ++ src/utils/routes.ts | 6 ++ src/views/DNS.tsx | 2 +- src/views/Routes.tsx | 117 ++++++++++++++++++++++++++++----- 8 files changed, 229 insertions(+), 38 deletions(-) diff --git a/src/components/PeerUpdate.tsx b/src/components/PeerUpdate.tsx index 137ce60..ec76e90 100644 --- a/src/components/PeerUpdate.tsx +++ b/src/components/PeerUpdate.tsx @@ -353,7 +353,7 @@ const PeerUpdate = () => { max={59}/> diff --git a/src/components/RouteUpdate.tsx b/src/components/RouteUpdate.tsx index 7429ffc..1709472 100644 --- a/src/components/RouteUpdate.tsx +++ b/src/components/RouteUpdate.tsx @@ -19,7 +19,7 @@ import { Typography } from "antd"; import {CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons"; -import {Route} from "../store/route/types"; +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'; @@ -31,6 +31,7 @@ import { transformGroupedDataTable } from '../utils/routes' import {useGetAccessTokenSilently} from "../utils/token"; +import {useGetGroupTagHelpers} from "../utils/groups"; const {Paragraph} = Typography; @@ -38,6 +39,17 @@ interface FormRoute extends Route { } const RouteUpdate = () => { + const { + tagRender, + handleChangeTags, + dropDownRender, + optionRender, + tagGroups, + getExistingAndToCreateGroupsLists, + getGroupNamesFromIDs, + selectValidator + } = useGetGroupTagHelpers() + const {Option} = Select; const {getAccessTokenSilently} = useGetAccessTokenSilently() const dispatch = useDispatch() const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible) @@ -46,7 +58,6 @@ const RouteUpdate = () => { 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 [groupedDataTable, setGroupedDataTable] = useState([] as GroupedDataTable[]); const [previousRouteKey, setPreviousRouteKey] = useState("") const [editName, setEditName] = useState(false) const [editDescription, setEditDescription] = useState(false) @@ -63,11 +74,12 @@ const RouteUpdate = () => { const defaultStatusMSG = "Status" const [statusMSG, setStatusMSG] = useState(defaultStatusMSG) const [peerNameToIP, peerIPToName] = initPeerMaps(peers); + const [newRoute, setNewRoute] = useState(false) const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}] useEffect(() => { - if (setupNewRouteHA) { + if (!newRoute ) { setRoutingPeerMSG("Add additional routing peer") setMasqueradeMSG("Update Masquerade") setStatusMSG("Update Status") @@ -77,7 +89,7 @@ const RouteUpdate = () => { setStatusMSG(defaultStatusMSG) setPreviousRouteKey("") } - }, [setupNewRouteHA]) + }, [newRoute]) useEffect(() => { if (editName) inputNameRef.current!.focus({ @@ -96,9 +108,15 @@ const RouteUpdate = () => { 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]) @@ -114,7 +132,7 @@ const RouteUpdate = () => { } }) - const createRouteToSave = (inputRoute: FormRoute): Route => { + const createRouteToSave = (inputRoute: FormRoute): RouteToSave => { let peerIDList = inputRoute.peer.split(routePeerSeparator) let peerID: string if (peerIDList[1]) { @@ -123,6 +141,8 @@ const RouteUpdate = () => { peerID = peerNameToIP[inputRoute.peer] } + let [ existingGroups, groupsToCreate ] = getExistingAndToCreateGroupsLists(inputRoute.groups) + return { id: inputRoute.id, network: inputRoute.network, @@ -131,8 +151,10 @@ const RouteUpdate = () => { peer: peerID, enabled: inputRoute.enabled, masquerade: inputRoute.masquerade, - metric: inputRoute.metric - } as Route + metric: inputRoute.metric, + groups: existingGroups, + groupsToCreate: groupsToCreate, + } as RouteToSave } const handleFormSubmit = () => { @@ -195,13 +217,14 @@ const RouteUpdate = () => { setVisibleNewRoute(false) setSetupNewRouteHA(false) setPreviousRouteKey("") + setNewRoute(false) } const onChange = (data: any) => { setFormRoute({...formRoute, ...data}) } - const dropDownRender = (menu: React.ReactElement) => ( + const peerDropDownRender = (menu: React.ReactElement) => ( <> {menu} @@ -227,13 +250,32 @@ const RouteUpdate = () => { 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 && { + onClick={handleFormSubmit}>{`${newRoute ? 'Create' : 'Save'}`} } > -
+
@@ -278,7 +320,7 @@ const RouteUpdate = () => { }]} > toggleEditName(false)} onBlur={() => toggleEditName(false)} autoComplete="off" maxLength={40}/> @@ -294,7 +336,7 @@ const RouteUpdate = () => { style={{marginTop: 24}} > toggleEditDescription(false)} onBlur={() => toggleEditDescription(false)} autoComplete="off" maxLength={200}/> @@ -321,7 +363,7 @@ const RouteUpdate = () => { tooltip="Use CIDR notation. e.g. 192.168.10.0/24 or 172.16.0.0/16" rules={[{validator: networkRangeValidator}]} > - @@ -343,12 +385,13 @@ const RouteUpdate = () => { name="peer" label={routingPeerMSG} tooltip="Assign a peer as a routing peer for the Network CIDR" + rules={[{validator:peerValidator}]} > + { + tagGroups.map(m => + + ) + } + + + diff --git a/src/store/route/actions.ts b/src/store/route/actions.ts index 02c468c..82c987e 100644 --- a/src/store/route/actions.ts +++ b/src/store/route/actions.ts @@ -1,5 +1,5 @@ import { ActionType, createAction, createAsyncAction } from 'typesafe-actions'; -import {Route} from './types'; +import {Route, RouteToSave} from './types'; import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types'; const actions = { @@ -13,7 +13,7 @@ const actions = { 'SAVE_ROUTE_REQUEST', 'SAVE_ROUTE_SUCCESS', 'SAVE_ROUTE_FAILURE', - ), CreateResponse, CreateResponse>(), + ), CreateResponse, CreateResponse>(), setSavedRoute: createAction('SET_CREATE_ROUTE')>(), resetSavedRoute: createAction('RESET_CREATE_ROUTE')(), diff --git a/src/store/route/sagas.ts b/src/store/route/sagas.ts index 9afd3d8..3715c9a 100644 --- a/src/store/route/sagas.ts +++ b/src/store/route/sagas.ts @@ -3,6 +3,9 @@ import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../servi import {Route} from './types' import service from './service'; import actions from './actions'; +import serviceGroup from "../group/service"; +import {Group} from "../group/types"; +import {actions as groupActions} from "../group"; export function* getRoutes(action: ReturnType): Generator { try { @@ -40,6 +43,21 @@ export function* saveRoute(action: ReturnType) const routeToSave = action.payload.payload + let groupsToCreate = routeToSave.groupsToCreate + if (!groupsToCreate) { + groupsToCreate = [] + } + + // first, create groups that were newly added by user + const responsesGroup = yield all(groupsToCreate.map(g => call(serviceGroup.createGroup, { + getAccessTokenSilently: action.payload.getAccessTokenSilently, + payload: {name: g} + }) + )) + + const resGroups = (responsesGroup as ApiResponse[]).filter(r => r.statusCode === 200).map(g => (g.body as Group)).map(g => g.id) + const newGroups = [...routeToSave.groups, ...resGroups] + const payloadToSave = { getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: { @@ -50,7 +68,8 @@ export function* saveRoute(action: ReturnType) metric: routeToSave.metric, network: routeToSave.network, network_id: routeToSave.network_id, - peer: routeToSave.peer + peer: routeToSave.peer, + groups: newGroups } as Route } @@ -72,8 +91,19 @@ export function* saveRoute(action: ReturnType) data: response.body } as CreateResponse)); + yield put(groupActions.getGroups.request({ + getAccessTokenSilently: action.payload.getAccessTokenSilently, + payload: null + })); + yield put(actions.getRoutes.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null })); + } catch (err) { + yield put(groupActions.getGroups.request({ + getAccessTokenSilently: action.payload.getAccessTokenSilently, + payload: null + })); + yield put(actions.saveRoute.failure({ loading: false, success: false, diff --git a/src/store/route/types.ts b/src/store/route/types.ts index c7ac3a8..cf7e2ea 100644 --- a/src/store/route/types.ts +++ b/src/store/route/types.ts @@ -1,3 +1,4 @@ + export interface Route { id?: string description: string @@ -8,4 +9,10 @@ export interface Route { network_type?: string metric?: number masquerade: boolean + groups: string[] +} + +export interface RouteToSave extends Route +{ + groupsToCreate: string[] } \ No newline at end of file diff --git a/src/utils/routes.ts b/src/utils/routes.ts index 6355b15..7f0fc4f 100644 --- a/src/utils/routes.ts +++ b/src/utils/routes.ts @@ -34,6 +34,7 @@ export interface GroupedDataTable { description: string routesCount: number groupedRoutes: RouteDataTable[] + routesGroups: string[] } export const transformDataTable = (d:Route[],peerIPToName:PeerIPToName):RouteDataTable[] => { @@ -52,10 +53,12 @@ export const transformGroupedDataTable = (routes:Route[],peerIPToName:PeerIPToNa })) let groupedRoutes:GroupedDataTable[] = [] + keySet.forEach((p) => { let hasEnabled = false let lastRoute:Route let listedRoutes:Route[] = [] + let groupList:string[] = [] routes.forEach((r) => { if ( p === r.network_id + r.network ) { lastRoute = r @@ -63,8 +66,10 @@ export const transformGroupedDataTable = (routes:Route[],peerIPToName:PeerIPToNa hasEnabled = true } listedRoutes.push(r) + groupList = groupList.concat(r.groups) } }) + groupList = groupList.filter((value,index,arrary) => arrary.indexOf(value) === index) let groupDataTableRoutes = transformDataTable(listedRoutes,peerIPToName) groupedRoutes.push({ key: p.toString(), @@ -75,6 +80,7 @@ export const transformGroupedDataTable = (routes:Route[],peerIPToName:PeerIPToNa enabled: hasEnabled, routesCount: groupDataTableRoutes.length, groupedRoutes: groupDataTableRoutes, + routesGroups: groupList, }) }) return groupedRoutes diff --git a/src/views/DNS.tsx b/src/views/DNS.tsx index 92a32d8..c49b370 100644 --- a/src/views/DNS.tsx +++ b/src/views/DNS.tsx @@ -301,7 +301,7 @@ export const DNS = () => { let errorMsg = "Failed to update nameserver group" switch (savedNSGroup.error.statusCode) { case 403: - errorMsg = "Failed to update user. You might not have enough permissions." + errorMsg = "Failed to update nameserver group. You might not have enough permissions." break default: errorMsg = savedNSGroup.error.data.message ? savedNSGroup.error.data.message : errorMsg diff --git a/src/views/Routes.tsx b/src/views/Routes.tsx index 67ec27c..82e97d9 100644 --- a/src/views/Routes.tsx +++ b/src/views/Routes.tsx @@ -9,7 +9,7 @@ import { Input, Menu, message, - Modal, + Modal, Popover, Radio, RadioChangeEvent, Row, @@ -24,7 +24,7 @@ import { import {Container} from "../components/Container"; import {useDispatch, useSelector} from "react-redux"; import {RootState} from "typesafe-actions"; -import {Route} from "../store/route/types"; +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"; @@ -42,6 +42,10 @@ import { transformGroupedDataTable } from '../utils/routes' import {useGetAccessTokenSilently} 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"; const {Title, Paragraph, Text} = Typography; const {Column} = Table; @@ -50,7 +54,11 @@ const {confirm} = Modal; export const Routes = () => { const {getAccessTokenSilently} = useGetAccessTokenSilently() 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); @@ -58,6 +66,7 @@ export const Routes = () => { 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'); @@ -67,6 +76,7 @@ export const Routes = () => { const [routeToAction, setRouteToAction] = useState(null as RouteDataTable | null); const [groupedDataTable, setGroupedDataTable] = useState([] as GroupedDataTable[]); const [expandRowsOnClick, setExpandRowsOnClick] = useState(true) + const [groupPopupVisible, setGroupPopupVisible] = useState(false as boolean | undefined) const [peerNameToIP, peerIPToName] = initPeerMaps(peers); @@ -83,10 +93,6 @@ export const Routes = () => { key: "view", label: () }, - // { - // key: "delete", - // label: () - // }, { key: "delete", label: () @@ -104,12 +110,15 @@ export const Routes = () => { useEffect(() => { dispatch(peerActions.getPeers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null})); + dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, 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 === "") + (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) @@ -151,10 +160,19 @@ export const Routes = () => { 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: savedRoute.error.data ? savedRoute.error.data : savedRoute.error.message, + content: errorMsg, key: saveKey, - duration: 2, + duration: 5, style: styleNotification }); dispatch(routeActions.setSavedRoute({...savedRoute, error: null})) @@ -224,7 +242,6 @@ export const Routes = () => { const onClickAddNewRoute = () => { - dispatch(routeActions.setSetupNewRouteHA(true)); dispatch(routeActions.setSetupNewRouteVisible(true)); dispatch(routeActions.setRoute({ network: '', @@ -264,7 +281,8 @@ export const Routes = () => { peer: route.peer ? peerToPeerIP(route.peer, peerNameToIP[route.peer]) : '', metric: route.metric ? route.metric : 9999, masquerade: route.masquerade, - enabled: route.enabled + enabled: route.enabled, + groups: route.groups } as Route)) dispatch(routeActions.setSetupNewRouteVisible(true)); } @@ -293,17 +311,77 @@ export const Routes = () => { }); } + const onPopoverVisibleChange = () => { + if (setupNewRouteVisible) { + setGroupPopupVisible(false) + } else { + setGroupPopupVisible(undefined) + } + } + function handleSwitchMasquerade(routeGroup: GroupedDataTable, checked: boolean) { routeGroup.groupedRoutes.forEach((record) => { const route = { ...record, peer: peerNameToIP[record.peer], masquerade: checked, - } as Route + groupsToCreate: [] + } as RouteToSave dispatch(routeActions.saveRoute.request({getAccessTokenSilently: getAccessTokenSilently, 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 ( + + {btn} + + ) + } + const expandedRowRender = (record: GroupedDataTable) => { return { onFilter={(value: string | number | boolean, record) => (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 }} @@ -336,7 +419,7 @@ export const Routes = () => { render={(text, record) => { if (deletedRoute.loading || savedRoute.loading) return <> return { + onOpenChange={visible => { if (visible) setRouteToAction(record as RouteDataTable) }}> }} @@ -438,7 +521,7 @@ export const Routes = () => { sorter={(a, b) => ((a as any).network.localeCompare((b as any).network))} // defaultSortOrder='ascend' /> - { return text ? enabled : disabled @@ -463,8 +546,8 @@ export const Routes = () => { if (count > 1) { tag = on } - return
{tag} + return
{tag} +
}} />