diff --git a/ui/src/features/configure/externals/networks/utils/editNetwork.jsx b/ui/src/features/configure/externals/networks/utils/editNetwork.jsx
new file mode 100644
index 0000000..e53ca9c
--- /dev/null
+++ b/ui/src/features/configure/externals/networks/utils/editNetwork.jsx
@@ -0,0 +1,363 @@
+import * as React from "react";
+import { useSelector, useDispatch } from "react-redux";
+
+import { useSnackbar } from "notistack";
+
+import Draggable from "react-draggable";
+
+import {
+ Box,
+ Button,
+ Tooltip,
+ TextField,
+ Dialog,
+ DialogTitle,
+ DialogActions,
+ DialogContent,
+ Paper
+} from "@mui/material";
+
+import LoadingButton from "@mui/lab/LoadingButton";
+
+import {
+ selectNetworks,
+ updateBlockExternalAsync
+} from "../../../../ipam/ipamSlice";
+
+import {
+ isSubnetOf,
+ isSubnetOverlap
+} from "../../../../tools/planner/utils/iputils";
+
+import {
+ EXTERNAL_NAME_REGEX,
+ EXTERNAL_DESC_REGEX,
+ CIDR_REGEX
+} from "../../../../../global/globals";
+
+function DraggablePaper(props) {
+ const nodeRef = React.useRef(null);
+
+ return (
+
+
+
+ );
+}
+
+export default function EditExtNetwork(props) {
+ const { open, handleClose, space, block, externals, selectedExternal } = props;
+
+ const { enqueueSnackbar } = useSnackbar();
+
+ const [extName, setExtName] = React.useState({ value: "", error: false });
+ const [extDesc, setExtDesc] = React.useState({ value: "", error: false });
+ const [extCidr, setExtCidr] = React.useState({ value: "", error: false });
+
+ const [sending, setSending] = React.useState(false);
+
+ const dispatch = useDispatch();
+
+ const networks = useSelector(selectNetworks);
+
+ function onCancel() {
+ handleClose();
+
+ if(selectedExternal) {
+ setExtName({ value: selectedExternal.name, error: false });
+ setExtDesc({ value: selectedExternal.desc, error: false });
+ setExtCidr({ value: selectedExternal.cidr, error: false });
+ } else {
+ setExtName({ value: "", error: false });
+ setExtDesc({ value: "", error: false });
+ setExtCidr({ value: "", error: false });
+ }
+ }
+
+ function onSubmit() {
+ var body = [
+ {
+ op: "replace",
+ path: "/name",
+ value: extName.value
+ },
+ {
+ op: "replace",
+ path: "/desc",
+ value: extDesc.value,
+ },
+ {
+ op: "replace",
+ path: "/cidr",
+ value: extCidr.value
+ }
+ ];
+
+ (async () => {
+ try {
+ setSending(true);
+ await dispatch(updateBlockExternalAsync({ space: space, block: block.name, external: selectedExternal.name, body: body }));
+ enqueueSnackbar("Successfully updated External Network", { variant: "success" });
+ onCancel();
+ } catch (e) {
+ console.log("ERROR");
+ console.log("------------------");
+ console.log(e);
+ console.log("------------------");
+ enqueueSnackbar(e.message, { variant: "error" });
+ } finally {
+ setSending(false);
+ }
+ })();
+ }
+
+ function onNameChange(event) {
+ const newName = event.target.value;
+
+ if(externals) {
+ const regex = new RegExp(
+ EXTERNAL_NAME_REGEX
+ );
+
+ const nameError = newName ? !regex.test(newName) : false;
+ const nameExists = externals?.reduce((acc, curr) => {
+ curr['name'] !== selectedExternal['name'] && acc.push(curr['name'].toLowerCase());
+
+ return acc;
+ }, []).includes(newName.toLowerCase());
+
+ setExtName({
+ value: newName,
+ error: (nameError || nameExists)
+ });
+ }
+ }
+
+ function onDescChange(event) {
+ const newDesc = event.target.value;
+
+ const regex = new RegExp(
+ EXTERNAL_DESC_REGEX
+ );
+
+ setExtDesc({
+ value: newDesc,
+ error: (newDesc ? !regex.test(newDesc) : false)
+ });
+ }
+
+ function onCidrChange(event) {
+ const newCidr = event.target.value;
+
+ const regex = new RegExp(
+ CIDR_REGEX
+ );
+
+ const cidrError = newCidr ? !regex.test(newCidr) : false;
+
+ var blockNetworks= [];
+ var extNetworks = [];
+
+ var cidrInBlock = false;
+ var resvOverlap = true;
+ var vnetOverlap = true;
+ var extOverlap = true;
+
+ if(!cidrError && newCidr.length > 0) {
+ cidrInBlock = isSubnetOf(newCidr, block.cidr);
+
+ const openResv = block?.resv.reduce((acc, curr) => {
+ if(!curr['settledOn']) {
+ acc.push(curr['cidr']);
+ }
+
+ return acc;
+ }, []);
+
+ if(space && block && networks) {
+ blockNetworks = networks?.reduce((acc, curr) => {
+ if(curr['parent_space'] && curr['parent_block']) {
+ if(curr['parent_space'] === space && curr['parent_block'].includes(block.name)) {
+ acc = acc.concat(curr['prefixes']);
+ }
+ }
+
+ return acc;
+ }, []);
+ }
+
+ if(externals) {
+ extNetworks = externals?.reduce((acc, curr) => {
+ curr['name'] !== selectedExternal['name'] && acc.push(curr['cidr']);
+
+ return acc;
+ }, []);
+ }
+
+ resvOverlap = isSubnetOverlap(newCidr, openResv);
+ vnetOverlap = isSubnetOverlap(newCidr, blockNetworks);
+ extOverlap = isSubnetOverlap(newCidr, extNetworks);
+ }
+
+ setExtCidr({
+ value: newCidr,
+ error: (cidrError || !cidrInBlock || resvOverlap || vnetOverlap || extOverlap)
+ });
+ }
+
+ const unchanged = React.useMemo(() => {
+ if(selectedExternal) {
+ return (
+ extName.value === selectedExternal.name &&
+ extDesc.value === selectedExternal.desc &&
+ extCidr.value === selectedExternal.cidr
+ );
+ } else {
+ return true;
+ }
+ }, [selectedExternal, extName, extDesc, extCidr]);
+
+ const hasError = React.useMemo(() => {
+ var emptyCheck = false;
+ var errorCheck = false;
+
+ errorCheck = (extName.error || extDesc.error || extCidr.error);
+ emptyCheck = (extName.value.length === 0 || extDesc.value.length === 0 || extCidr.value.length === 0);
+
+ return (errorCheck || emptyCheck);
+ }, [extName, extDesc, extCidr]);
+
+ React.useEffect(() => {
+ if (selectedExternal) {
+ setExtName({ value: selectedExternal.name, error: false });
+ setExtDesc({ value: selectedExternal.desc, error: false });
+ setExtCidr({ value: selectedExternal.cidr, error: false });
+ } else {
+ handleClose();
+
+ setExtName({ value: "", error: false });
+ setExtDesc({ value: "", error: false });
+ setExtCidr({ value: "", error: false });
+ }
+ }, [selectedExternal, handleClose]);
+
+ return (
+
+
+
+ );
+}
diff --git a/ui/src/features/configure/externals/subnets/subnets.jsx b/ui/src/features/configure/externals/subnets/subnets.jsx
index 10a2545..a647a36 100644
--- a/ui/src/features/configure/externals/subnets/subnets.jsx
+++ b/ui/src/features/configure/externals/subnets/subnets.jsx
@@ -29,17 +29,19 @@ import {
TaskAltOutlined,
CancelOutlined,
AddOutlined,
- // EditOutlined,
+ EditOutlined,
DeleteOutline,
EditNoteOutlined
} from "@mui/icons-material";
import {
selectViewSetting,
- updateMeAsync
+ updateMeAsync,
+ getAdminStatus
} from "../../../ipam/ipamSlice";
import AddExtSubnet from "./utils/addSubnet";
+import EditExtSubnet from "./utils/editSubnet";
import DeleteExtSubnet from "./utils/deleteSubnet";
import ManageExtEndpoints from "./utils/manageEndpoints";
@@ -59,6 +61,7 @@ function HeaderMenu(props) {
selectedExternal,
selectedSubnet,
setAddExtSubOpen,
+ setEditExtSubOpen,
setDelExtSubOpen,
setManExtEndOpen,
saving,
@@ -72,6 +75,7 @@ function HeaderMenu(props) {
const menuRef = React.useRef(null);
+ const isAdmin = useSelector(getAdminStatus);
const viewSetting = useSelector(state => selectViewSetting(state, setting));
const onClick = () => {
@@ -83,6 +87,11 @@ function HeaderMenu(props) {
setMenuOpen(false);
}
+ const onEditExtSub = () => {
+ setEditExtSubOpen(true);
+ setMenuOpen(false);
+ }
+
const onDelExtSub = () => {
setDelExtSubOpen(true);
setMenuOpen(false);
@@ -183,25 +192,25 @@ function HeaderMenu(props) {
>
- {/*
@@ -270,9 +279,11 @@ const Subnets = (props) => {
const [columnSortState, setColumnSortState] = React.useState({});
const [addExtSubOpen, setAddExtSubOpen] = React.useState(false);
+ const [editExtSubOpen, setEditExtSubOpen] = React.useState(false);
const [delExtSubOpen, setDelExtSubOpen] = React.useState(false);
const [manExtEndOpen, setManExtEndOpen] = React.useState(false);
+ const isAdmin = useSelector(getAdminStatus);
const viewSetting = useSelector(state => selectViewSetting(state, 'extsubnets'));
const saveTimer = React.useRef();
@@ -497,22 +508,35 @@ const Subnets = (props) => {
return (
- setAddExtSubOpen(false)}
- space={selectedSpace ? selectedSpace.name : null}
- block={selectedBlock ? selectedBlock.name : null}
- external={selectedExternal ? selectedExternal : null}
- subnets={subnets}
- />
- setDelExtSubOpen(false)}
- space={selectedSpace ? selectedSpace.name : null}
- block={selectedBlock ? selectedBlock.name : null}
- external={selectedExternal ? selectedExternal.name : null}
- subnet={selectedSubnet ? selectedSubnet.name : null}
- />
+ { !isAdmin &&
+
+ setAddExtSubOpen(false)}
+ space={selectedSpace ? selectedSpace.name : null}
+ block={selectedBlock ? selectedBlock.name : null}
+ external={selectedExternal ? selectedExternal : null}
+ subnets={subnets}
+ />
+ setEditExtSubOpen(false)}
+ space={selectedSpace ? selectedSpace.name : null}
+ block={selectedBlock ? selectedBlock.name : null}
+ external={selectedExternal ? selectedExternal : null}
+ subnets={subnets}
+ selectedSubnet={selectedSubnet}
+ />
+ setDelExtSubOpen(false)}
+ space={selectedSpace ? selectedSpace.name : null}
+ block={selectedBlock ? selectedBlock.name : null}
+ external={selectedExternal ? selectedExternal.name : null}
+ subnet={selectedSubnet ? selectedSubnet.name : null}
+ />
+
+ }
setManExtEndOpen(false)}
@@ -521,7 +545,7 @@ const Subnets = (props) => {
external={selectedExternal ? selectedExternal.name : null}
subnet={selectedSubnet ? selectedSubnet : null}
/>
-
+
diff --git a/ui/src/features/configure/externals/subnets/utils/addSubnet.jsx b/ui/src/features/configure/externals/subnets/utils/addSubnet.jsx
index 3a4d4a8..88fd142 100644
--- a/ui/src/features/configure/externals/subnets/utils/addSubnet.jsx
+++ b/ui/src/features/configure/externals/subnets/utils/addSubnet.jsx
@@ -36,7 +36,6 @@ import {
CIDR_REGEX,
cidrMasks
} from "../../../../../global/globals";
-import { Spellcheck } from "@mui/icons-material";
function DraggablePaper(props) {
const nodeRef = React.useRef(null);
diff --git a/ui/src/features/configure/externals/subnets/utils/editSubnet.jsx b/ui/src/features/configure/externals/subnets/utils/editSubnet.jsx
new file mode 100644
index 0000000..952b399
--- /dev/null
+++ b/ui/src/features/configure/externals/subnets/utils/editSubnet.jsx
@@ -0,0 +1,335 @@
+import * as React from "react";
+import { useDispatch } from "react-redux";
+
+import { useSnackbar } from "notistack";
+
+import Draggable from "react-draggable";
+
+import {
+ Box,
+ Button,
+ Tooltip,
+ TextField,
+ Dialog,
+ DialogTitle,
+ DialogActions,
+ DialogContent,
+ Paper
+} from "@mui/material";
+
+import LoadingButton from "@mui/lab/LoadingButton";
+
+import {
+ updateBlockExtSubnetAsync
+} from "../../../../ipam/ipamSlice";
+
+import {
+ isSubnetOf,
+ isSubnetOverlap
+} from "../../../../tools/planner/utils/iputils";
+
+import {
+ EXTSUBNET_NAME_REGEX,
+ EXTSUBNET_DESC_REGEX,
+ CIDR_REGEX
+} from "../../../../../global/globals";
+
+function DraggablePaper(props) {
+ const nodeRef = React.useRef(null);
+
+ return (
+
+
+
+ );
+}
+
+export default function EditExtSubnet(props) {
+ const { open, handleClose, space, block, external, subnets, selectedSubnet } = props;
+
+ const { enqueueSnackbar } = useSnackbar();
+
+ const [subName, setSubName] = React.useState({ value: "", error: false });
+ const [subDesc, setSubDesc] = React.useState({ value: "", error: false });
+ const [subCidr, setSubCidr] = React.useState({ value: "", error: false });
+
+ const [sending, setSending] = React.useState(false);
+
+ const dispatch = useDispatch();
+
+ function onCancel() {
+ handleClose();
+
+ if(selectedSubnet) {
+ setSubName({ value: selectedSubnet.name, error: false });
+ setSubDesc({ value: selectedSubnet.desc, error: false });
+ setSubCidr({ value: selectedSubnet.cidr, error: false });
+ } else {
+ setSubName({ value: "", error: false });
+ setSubDesc({ value: "", error: false });
+ setSubCidr({ value: "", error: false });
+ }
+ }
+
+ function onSubmit() {
+ var body = [
+ {
+ op: "replace",
+ path: "/name",
+ value: subName.value
+ },
+ {
+ op: "replace",
+ path: "/desc",
+ value: subDesc.value
+ },
+ {
+ op: "replace",
+ path: "/cidr",
+ value: subCidr.value
+ }
+ ];
+
+ (async () => {
+ try {
+ setSending(true);
+ await dispatch(updateBlockExtSubnetAsync({ space: space, block: block, external: external.name, subnet: selectedSubnet.name, body: body }));
+ enqueueSnackbar("Successfully updated External Subnet", { variant: "success" });
+ onCancel();
+ } catch (e) {
+ console.log("ERROR");
+ console.log("------------------");
+ console.log(e);
+ console.log("------------------");
+ enqueueSnackbar(e.message, { variant: "error" });
+ } finally {
+ setSending(false);
+ }
+ })();
+ }
+
+ function onNameChange(event) {
+ const newName = event.target.value;
+
+ if(subnets) {
+ const regex = new RegExp(
+ EXTSUBNET_NAME_REGEX
+ );
+
+ const nameError = newName ? !regex.test(newName) : false;
+ const nameExists = subnets?.reduce((acc, curr) => {
+ curr['name'] !== selectedSubnet['name'] && acc.push(curr['name'].toLowerCase());
+
+ return acc;
+ }, []).includes(newName.toLowerCase());
+
+ setSubName({
+ value: newName,
+ error: (nameError || nameExists)
+ });
+ }
+ }
+
+ function onDescChange(event) {
+ const newDesc = event.target.value;
+
+ const regex = new RegExp(
+ EXTSUBNET_DESC_REGEX
+ );
+
+ setSubDesc({
+ value: newDesc,
+ error: (newDesc ? !regex.test(newDesc) : false)
+ });
+ }
+
+ function onCidrChange(event) {
+ const newCidr = event.target.value;
+
+ const regex = new RegExp(
+ CIDR_REGEX
+ );
+
+ const cidrError = newCidr ? !regex.test(newCidr) : false;
+
+ var extSubnets = [];
+
+ var cidrInBlock = false;
+ var subOverlap = true;
+
+ if(!cidrError && newCidr.length > 0) {
+ cidrInBlock = isSubnetOf(newCidr, external.cidr);
+
+ if(subnets) {
+ extSubnets = subnets?.reduce((acc, curr) => {
+ curr['name'] !== selectedSubnet['name'] && acc.push(curr['cidr']);
+
+ return acc;
+ }, []);
+ }
+
+ subOverlap = isSubnetOverlap(newCidr, extSubnets);
+ }
+
+ setSubCidr({
+ value: newCidr,
+ error: (cidrError || !cidrInBlock || subOverlap)
+ });
+ }
+
+ const unchanged = React.useMemo(() => {
+ if(selectedSubnet) {
+ return (
+ subName.value === selectedSubnet.name &&
+ subDesc.value === selectedSubnet.desc &&
+ subCidr.value === selectedSubnet.cidr
+ );
+ } else {
+ return true;
+ }
+ }, [selectedSubnet, subName, subDesc, subCidr]);
+
+ const hasError = React.useMemo(() => {
+ var emptyCheck = false;
+ var errorCheck = false;
+
+ errorCheck = (subName.error || subDesc.error || subCidr.error);
+ emptyCheck = (subName.value.length === 0 || subDesc.value.length === 0 || subCidr.value.length === 0);
+
+ return (errorCheck || emptyCheck);
+ }, [subName, subDesc, subCidr]);
+
+ React.useEffect(() => {
+ if (selectedSubnet) {
+ setSubName({ value: selectedSubnet.name, error: false });
+ setSubDesc({ value: selectedSubnet.desc, error: false });
+ setSubCidr({ value: selectedSubnet.cidr, error: false });
+ } else {
+ handleClose();
+
+ setSubName({ value: "", error: false });
+ setSubDesc({ value: "", error: false });
+ setSubCidr({ value: "", error: false });
+ }
+ }, [selectedSubnet, handleClose]);
+
+ return (
+
+
+
+ );
+}
diff --git a/ui/src/features/configure/externals/subnets/utils/manageEndpoints.jsx b/ui/src/features/configure/externals/subnets/utils/manageEndpoints.jsx
index d8a4575..816991f 100644
--- a/ui/src/features/configure/externals/subnets/utils/manageEndpoints.jsx
+++ b/ui/src/features/configure/externals/subnets/utils/manageEndpoints.jsx
@@ -2,7 +2,7 @@ import * as React from "react";
import { useSelector, useDispatch } from "react-redux";
import { styled } from "@mui/material/styles";
-import { isEmpty, isEqual, pickBy, orderBy, cloneDeep } from "lodash";
+import { omit, isEmpty, isEqual, pickBy, orderBy, cloneDeep } from "lodash";
import { useSnackbar } from "notistack";
@@ -14,6 +14,8 @@ import Draggable from "react-draggable";
import { useTheme } from "@mui/material/styles";
+import md5 from "md5";
+
import {
Box,
Button,
@@ -43,7 +45,9 @@ import {
TaskAltOutlined,
CancelOutlined,
PlaylistAddOutlined,
- HighlightOff,
+ PlaylistAddCheckOutlined,
+ PlaylistRemoveOutlined,
+ // HighlightOff,
InfoOutlined
} from "@mui/icons-material";
@@ -52,11 +56,13 @@ import LoadingButton from "@mui/lab/LoadingButton";
import {
replaceBlockExtSubnetEndpointsAsync,
selectViewSetting,
- updateMeAsync
+ updateMeAsync,
+ getAdminStatus
} from "../../../../ipam/ipamSlice";
import {
- expandCIDR
+ expandCIDR,
+ getSubnetSize
} from "../../../../tools/planner/utils/iputils";
import {
@@ -87,7 +93,7 @@ const gridStyle = {
function RenderDelete(props) {
const { value } = props;
- const { endpoints, setAdded, deleted, setDeleted, selectionModel } = React.useContext(EndpointContext);
+ const { setChanges, selectionModel } = React.useContext(EndpointContext);
const flexCenter = {
display: "flex",
@@ -108,14 +114,18 @@ function RenderDelete(props) {
disableTouchRipple
disableRipple
onClick={() => {
- if(endpoints.find(e => e.name === value.name) && !deleted.includes(value.name)) {
- setDeleted(prev => [...prev, value.name]);
- } else {
- setAdded(prev => prev.filter(e => e.name !== value.name));
- }
+ var endpointDetails = cloneDeep(value);
+
+ endpointDetails['op'] = "delete";
+
+ setChanges(prev => [
+ ...prev,
+ endpointDetails
+ ]);
}}
>
-
+ {/* */}
+
@@ -284,8 +294,7 @@ export default function ManageExtEndpoints(props) {
const [sendResults, setSendResults] = React.useState(null);
const [endpoints, setEndpoints] = React.useState(null);
const [addressOptions, setAddressOptions] = React.useState([]);
- const [added, setAdded] = React.useState([]);
- const [deleted, setDeleted] = React.useState([]);
+ const [changes, setChanges] = React.useState([]);
const [gridData, setGridData] = React.useState(null);
const [sending, setSending] = React.useState(false);
const [selectionModel, setSelectionModel] = React.useState({});
@@ -300,6 +309,7 @@ export default function ManageExtEndpoints(props) {
const [endAddrInput, setEndAddrInput] = React.useState("");
const [endAddr, setEndAddr] = React.useState(null);
+ const isAdmin = useSelector(getAdminStatus);
const viewSetting = useSelector(state => selectViewSetting(state, 'extendpoints'));
const dispatch = useDispatch();
@@ -331,6 +341,31 @@ export default function ManageExtEndpoints(props) {
setSelectionModel(prevState => {
if(!prevState.hasOwnProperty(id)) {
newSelectionModel[id] = data;
+
+ setEndName({ value: data.name, error: false });
+ setEndDesc({ value: data.desc, error: false });
+ setEndAddrInput(data.ip);
+ setEndAddr(data.ip);
+
+ const endpointAddresses = endpoints.map(e => {
+ if (data.ip !== e.ip) {
+ return e.ip;
+ }
+ });
+
+ const newAddressOptions = expandCIDR(subnet.cidr).slice(1,-1).filter(addr => !endpointAddresses.includes(addr));
+
+ setAddressOptions(newAddressOptions);
+ } else {
+ setEndName({ value: "", error: true });
+ setEndDesc({ value: "", error: true });
+ setEndAddrInput("");
+ setEndAddr(null);
+
+ const endpointAddresses = endpoints.map(e => e.ip);
+ const newAddressOptions = expandCIDR(subnet.cidr).slice(1,-1).filter(addr => !endpointAddresses.includes(addr));
+
+ setAddressOptions(["", ...newAddressOptions]);
}
return newSelectionModel;
@@ -482,14 +517,43 @@ export default function ManageExtEndpoints(props) {
function onAddExternal() {
if(!hasError) {
- setAdded(prev => [
- ...prev,
- {
- name: endName.value,
- desc: endDesc.value,
- ip: endAddr
+ var endpointDetails = {
+ name: endName.value,
+ desc: endDesc.value,
+ ip: endAddr
+ };
+
+ endpointDetails['id'] = md5(JSON.stringify(endpointDetails));
+
+ if (Object.keys(selectionModel).length !== 0) {
+ const updates = {
+ op: "update",
+ old: Object.values(selectionModel)[0],
+ new: endpointDetails
+ }
+
+ setChanges(prev => [
+ ...prev,
+ updates
+ ]);
+ } else {
+ const numEndpoints = endpoints.length;
+ const numAdditions = changes.filter(change => change.op === "add").length;
+ const numDeletions = changes.filter(change => change.op === "delete").length;
+ const subnetSize = getSubnetSize(subnet.cidr) - 2;
+
+ if (((numEndpoints + numAdditions) - numDeletions) >= subnetSize) {
+ enqueueSnackbar(`Number of endpoints cannot exceed subnet size of ${subnetSize}`, { variant: "error" });
+ return;
}
- ]);
+
+ endpointDetails['op'] = "add";
+
+ setChanges(prev => [
+ ...prev,
+ endpointDetails
+ ]);
+ }
setEndName({ value: "", error: true });
setEndDesc({ value: "", error: true });
@@ -533,15 +597,15 @@ export default function ManageExtEndpoints(props) {
const onCancel = React.useCallback(() => {
if (open) {
- setAdded([]);
- setDeleted([]);
+ handleClose();
+
+ setSelectionModel({});
+ setChanges([]);
setEndName({ value: "", error: true });
setEndDesc({ value: "", error: true });
setEndAddrInput("");
setEndAddr(null);
-
- handleClose();
}
}, [open, handleClose]);
@@ -561,7 +625,17 @@ export default function ManageExtEndpoints(props) {
);
const nameError = newName ? !regex.test(newName) : false;
- const nameExists = endpoints.map(e => e.name.toLowerCase()).includes(newName.toLowerCase());
+ const nameExists = endpoints?.reduce((acc, curr) => {
+ if(Object.keys(selectionModel).length !== 0) {
+ if (curr['name'].toLowerCase() !== Object.values(selectionModel)[0].name.toLowerCase()) {
+ acc.push(curr['name'].toLowerCase());
+ }
+ } else {
+ acc.push(curr['name'].toLowerCase());
+ }
+
+ return acc;
+ }, []).includes(newName.toLowerCase());
setEndName({
value: newName,
@@ -594,17 +668,41 @@ export default function ManageExtEndpoints(props) {
if(subnet) {
var newEndpoints = cloneDeep(subnet['endpoints']);
- newEndpoints = newEndpoints.filter(e => !deleted.includes(e.name));
- newEndpoints = newEndpoints.concat(added);
-
- const newData = newEndpoints.reduce((acc, curr) => {
- curr['id'] = `${subnet}@${curr.name}}`
+ var newData = newEndpoints.reduce((acc, curr) => {
+ // curr['id'] = `${subnet}@${curr.name}}`;
+ curr['id'] = md5(JSON.stringify(curr));
acc.push(curr);
return acc;
}, []);
+ changes.forEach(change => {
+ switch(change.op) {
+ case "add": {
+ newData.push(omit(change, 'op'));
+
+ break;
+ }
+ case "update": {
+ const index = newData.findIndex(e => e.name === change.old.name);
+
+ if (index !== -1) {
+ newData[index] = change.new;
+ }
+
+ break;
+ }
+ case "delete": {
+ newData = newData.filter(e => e.name !== change.name);
+
+ break;
+ }
+ default:
+ break;
+ }
+ });
+
const endpointAddresses = newData.map(e => e.ip);
const newAddressOptions = expandCIDR(subnet.cidr).slice(1,-1).filter(addr => !endpointAddresses.includes(addr));
@@ -613,10 +711,10 @@ export default function ManageExtEndpoints(props) {
} else {
onCancel();
}
- }, [subnet, added, deleted, onCancel]);
+ }, [subnet, changes, onCancel]);
return (
-
+
+
+ }
onClick(rowData.data)}
+ onRowClick={(rowData) => { isAdmin && onClick(rowData.data)}}
onCellDoubleClick={onCellDoubleClick}
selected={selectionModel}
style={gridStyle}
diff --git a/ui/src/features/ipam/ipamAPI.jsx b/ui/src/features/ipam/ipamAPI.jsx
index 4b6a825..84498d8 100644
--- a/ui/src/features/ipam/ipamAPI.jsx
+++ b/ui/src/features/ipam/ipamAPI.jsx
@@ -149,6 +149,12 @@ export function createBlockExternal(space, block, body) {
return api.post(url, body);
}
+export function updateBlockExternal(space, block, external, body) {
+ const url = new URL(`${ENGINE_URL}/api/spaces/${space}/blocks/${block}/externals/${external}`);
+
+ return api.patch(url, body);
+}
+
export function deleteBlockExternal(space, block, external, force) {
const url = new URL(`${ENGINE_URL}/api/spaces/${space}/blocks/${block}/externals/${external}`);
var urlParams = url.searchParams;
@@ -164,6 +170,12 @@ export function createBlockExtSubnet(space, block, external, body) {
return api.post(url, body);
}
+export function updateBlockExtSubnet(space, block, external, subnet, body) {
+ const url = new URL(`${ENGINE_URL}/api/spaces/${space}/blocks/${block}/externals/${external}/subnets/${subnet}`);
+
+ return api.patch(url, body);
+}
+
export function deleteBlockExtSubnet(space, block, external, subnet, force) {
const url = new URL(`${ENGINE_URL}/api/spaces/${space}/blocks/${block}/externals/${external}/subnets/${subnet}`);
var urlParams = url.searchParams;
diff --git a/ui/src/features/ipam/ipamSlice.jsx b/ui/src/features/ipam/ipamSlice.jsx
index 1c6c17a..c47d43c 100644
--- a/ui/src/features/ipam/ipamSlice.jsx
+++ b/ui/src/features/ipam/ipamSlice.jsx
@@ -13,8 +13,10 @@ import {
updateBlock,
deleteBlock,
createBlockExternal,
+ updateBlockExternal,
deleteBlockExternal,
createBlockExtSubnet,
+ updateBlockExtSubnet,
deleteBlockExtSubnet,
replaceBlockExtSubnetEndpoints,
createBlockResv,
@@ -178,6 +180,19 @@ export const createBlockExternalAsync = createAsyncThunk(
}
);
+export const updateBlockExternalAsync = createAsyncThunk(
+ 'ipam/updateBlockExternal',
+ async (args, { rejectWithValue }) => {
+ try {
+ const response = await updateBlockExternal(args.space, args.block, args.external, args.body);
+
+ return response;
+ } catch (err) {
+ return rejectWithValue(err);
+ }
+ }
+);
+
export const deleteBlockExternalAsync = createAsyncThunk(
'ipam/deleteBlockExternal',
async (args, { rejectWithValue }) => {
@@ -204,6 +219,19 @@ export const createBlockExtSubnetAsync = createAsyncThunk(
}
);
+export const updateBlockExtSubnetAsync = createAsyncThunk(
+ 'ipam/updateBlockExtSubnet',
+ async (args, { rejectWithValue }) => {
+ try {
+ const response = await updateBlockExtSubnet(args.space, args.block, args.external, args.subnet, args.body);
+
+ return response;
+ } catch (err) {
+ return rejectWithValue(err);
+ }
+ }
+);
+
export const deleteBlockExtSubnetAsync = createAsyncThunk(
'ipam/deleteBlockExtSubnet',
async (args, { rejectWithValue }) => {
@@ -565,6 +593,31 @@ export const ipamSlice = createSlice({
// SnackbarUtils.error(`Error fetching user settings (${action.error.message})`);
throw action.payload;
})
+ .addCase(updateBlockExternalAsync.fulfilled, (state, action) => {
+ const spaceName = action.meta.arg.space;
+ const blockName = action.meta.arg.block;
+ const externalName = action.meta.arg.external;
+ const updatedExternal = action.payload;
+ const spaceIndex = state.spaces.findIndex((x) => x.name === spaceName);
+
+ if (spaceIndex > -1) {
+ const blockIndex = state.spaces[spaceIndex].blocks.findIndex((x) => x.name === blockName);
+
+ if(blockIndex > -1) {
+ const externalIndex = state.spaces[spaceIndex].blocks[blockIndex].externals.findIndex((x) => x.name === externalName);
+
+ if(externalIndex > -1) {
+ state.spaces[spaceIndex].blocks[blockIndex].externals[externalIndex] = merge(state.spaces[spaceIndex].blocks[blockIndex].externals[externalIndex], updatedExternal);
+ }
+ }
+ }
+ })
+ .addCase(updateBlockExternalAsync.rejected, (state, action) => {
+ console.log("updateBlockExternalAsync Rejected");
+ console.log(action);
+ // SnackbarUtils.error(`Error fetching user settings (${action.error.message})`);
+ throw action.payload;
+ })
.addCase(deleteBlockExternalAsync.fulfilled, (state, action) => {
const spaceName = action.meta.arg.space;
const blockName = action.meta.arg.block;
@@ -604,6 +657,36 @@ export const ipamSlice = createSlice({
// SnackbarUtils.error(`Error fetching user settings (${action.error.message})`);
throw action.payload;
})
+ .addCase(updateBlockExtSubnetAsync.fulfilled, (state, action) => {
+ const spaceName = action.meta.arg.space;
+ const blockName = action.meta.arg.block;
+ const externalName = action.meta.arg.external;
+ const subnetName = action.meta.arg.subnet;
+ const updatedSubnet = action.payload;
+ const spaceIndex = state.spaces.findIndex((x) => x.name === spaceName);
+
+ if (spaceIndex > -1) {
+ const blockIndex = state.spaces[spaceIndex].blocks.findIndex((x) => x.name === blockName);
+
+ if(blockIndex > -1) {
+ const externalIndex = state.spaces[spaceIndex].blocks[blockIndex].externals.findIndex((x) => x.name === externalName);
+
+ if(externalIndex > -1) {
+ const subnetIndex = state.spaces[spaceIndex].blocks[blockIndex].externals[externalIndex].subnets.findIndex((x) => x.name === subnetName);
+
+ if(subnetIndex > -1) {
+ state.spaces[spaceIndex].blocks[blockIndex].externals[externalIndex].subnets[subnetIndex] = merge(state.spaces[spaceIndex].blocks[blockIndex].externals[externalIndex].subnets[subnetIndex], updatedSubnet);
+ }
+ }
+ }
+ }
+ })
+ .addCase(updateBlockExtSubnetAsync.rejected, (state, action) => {
+ console.log("updateBlockExtSubnetAsync Rejected");
+ console.log(action);
+ // SnackbarUtils.error(`Error fetching user settings (${action.error.message})`);
+ throw action.payload;
+ })
.addCase(deleteBlockExtSubnetAsync.fulfilled, (state, action) => {
const spaceName = action.meta.arg.space;
const blockName = action.meta.arg.block;
diff --git a/ui/src/features/tools/planner/utils/iputils.jsx b/ui/src/features/tools/planner/utils/iputils.jsx
index c1c6b81..a80ceea 100644
--- a/ui/src/features/tools/planner/utils/iputils.jsx
+++ b/ui/src/features/tools/planner/utils/iputils.jsx
@@ -23,6 +23,13 @@ function getIpRangeForSubnet(cidr) {
return results;
}
+export function getSubnetSize(cidr) {
+ var mask = parseInt(cidr.split('/')[1], 10);
+ var size = Math.pow(2, (32 - mask));
+
+ return size;
+}
+
export function isSubnetOverlap(subnetCIDR, existingSubnetCIDR) {
var ipRangeforCurrent = getIpRangeForSubnet(subnetCIDR);