Merge pull request #41 from snachx/main

feat: add capabilities and tags support. Thanks @snachx
This commit is contained in:
dec0dOS 2021-12-14 01:25:21 +03:00 committed by GitHub
commit 3827fe81a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 347 additions and 75 deletions

View file

@ -62,6 +62,8 @@ async function createNetworkAdditionalData(nwid) {
additionalConfig: {
description: "",
rulesSource: constants.defaultRulesSource,
tagsByName: {},
capabilitiesByName: {},
},
members: [],
};
@ -79,6 +81,12 @@ async function updateNetworkAdditionalData(nwid, data) {
if (data.hasOwnProperty("rulesSource")) {
additionalData.rulesSource = data.rulesSource;
}
if (data.hasOwnProperty("tagsByName")) {
additionalData.tagsByName = data.tagsByName;
}
if (data.hasOwnProperty("capabilitiesByName")) {
additionalData.capabilitiesByName = data.capabilitiesByName;
}
if (additionalData) {
db.get("networks")

View file

@ -1,30 +1,26 @@
import { useState, useEffect, useCallback } from "react";
import { useParams } from "react-router-dom";
import {
Accordion,
AccordionSummary,
AccordionDetails,
AccordionSummary,
Checkbox,
Grid,
Typography,
IconButton,
Typography,
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import RefreshIcon from "@material-ui/icons/Refresh";
import { useCallback, useEffect, useState } from "react";
import DataTable from "react-data-table-component";
import MemberName from "./components/MemberName";
import ManagedIP from "./components/ManagedIP";
import DeleteMember from "./components/DeleteMember";
import MemberSettings from "./components/MemberSettings";
import AddMember from "./components/AddMember";
import { useParams } from "react-router-dom";
import API from "utils/API";
import { parseValue, replaceValue, setValue } from "utils/ChangeHelper";
import AddMember from "./components/AddMember";
import DeleteMember from "./components/DeleteMember";
import ManagedIP from "./components/ManagedIP";
import MemberName from "./components/MemberName";
import MemberSettings from "./components/MemberSettings";
function NetworkMembers() {
function NetworkMembers({ network }) {
const { nwid } = useParams();
const [members, setMembers] = useState([]);
@ -49,27 +45,23 @@ function NetworkMembers() {
console.log("Action:", req);
};
const handleChange = (
member,
key1,
key2 = null,
mode = "text",
id = null
) => (event) => {
const value = parseValue(event, mode, member, key1, key2, id);
const handleChange =
(member, key1, key2 = null, mode = "text", id = null) =>
(event) => {
const value = parseValue(event, mode, member, key1, key2, id);
const updatedMember = replaceValue({ ...member }, key1, key2, value);
const updatedMember = replaceValue({ ...member }, key1, key2, value);
const index = members.findIndex((item) => {
return item["config"]["id"] === member["config"]["id"];
});
let mutableMembers = [...members];
mutableMembers[index] = updatedMember;
setMembers(mutableMembers);
const index = members.findIndex((item) => {
return item["config"]["id"] === member["config"]["id"];
});
let mutableMembers = [...members];
mutableMembers[index] = updatedMember;
setMembers(mutableMembers);
const data = setValue({}, key1, key2, value);
sendReq(member["config"]["id"], data);
};
const data = setValue({}, key1, key2, value);
sendReq(member["config"]["id"], data);
};
const columns = [
{
@ -130,7 +122,11 @@ function NetworkMembers() {
right: true,
cell: (row) => (
<>
<MemberSettings member={row} handleChange={handleChange} />
<MemberSettings
member={row}
network={network}
handleChange={handleChange}
/>
<DeleteMember nwid={nwid} mid={row.config.id} callback={fetchData} />
</>
),

View file

@ -1,16 +1,19 @@
import { useState } from "react";
import {
Checkbox,
Dialog,
DialogTitle,
DialogContent,
DialogTitle,
FormControlLabel,
Grid,
IconButton,
Paper,
Typography,
} from "@material-ui/core";
import BuildIcon from "@material-ui/icons/Build";
import { useState } from "react";
import Tag from "./components/Tag";
function MemberSettings({ member, handleChange }) {
function MemberSettings({ member, network, handleChange }) {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
@ -55,6 +58,66 @@ function MemberSettings({ member, handleChange }) {
/>
<span>Do Not Auto-Assign IPs</span>
</Grid>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h5">Capabilities</Typography>
</Grid>
<Grid item xs={12}>
<Paper style={{ padding: 20 }}>
{Object.entries(network["capabilitiesByName"] || []).length ===
0
? "No capabilities defined"
: ""}
{Object.entries(network["capabilitiesByName"] || []).map(
([capName, capId]) => (
<FormControlLabel
control={
<Checkbox
checked={member["config"]["capabilities"].includes(
capId
)}
color="primary"
onChange={handleChange(
member,
"config",
"capabilities",
"capChange",
capId
)}
/>
}
key={"cap-" + capId}
label={capName}
/>
)
)}
</Paper>
</Grid>
</Grid>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h5">Tags</Typography>
</Grid>
{Object.entries(network["tagsByName"] || []).length === 0 ? (
<Grid item xs={12}>
<Paper style={{ padding: 20 }}>No tags defined</Paper>
</Grid>
) : (
""
)}
{Object.entries(network["tagsByName"] || []).map(
([tagName, tagDetail]) => (
<Grid item xs={12} sm={6} key={"tag-" + tagName}>
<Tag
member={member}
tagName={tagName}
tagDetail={tagDetail}
handleChange={handleChange}
/>
</Grid>
)
)}
</Grid>
</DialogContent>
</Dialog>
</>

View file

@ -0,0 +1,165 @@
import {
Checkbox,
FormControlLabel,
Grid,
IconButton,
Input,
Paper,
Select,
Typography,
} from "@material-ui/core";
import DeleteIcon from "@material-ui/icons/Delete";
import { useEffect, useState } from "react";
import { useDebounce } from "react-use";
function Tag({ member, tagName, tagDetail, handleChange }) {
const [tagValue, setTagValue] = useState("");
const [tagChangedByUser, setTagChangedByUser] = useState(false);
useEffect(() => {
let tagIndex = member["config"]["tags"].findIndex((item) => {
return item[0] === tagDetail["id"];
});
let value = "";
if (tagIndex !== -1) {
value = member["config"]["tags"][tagIndex][1];
}
value = value !== false ? value : "";
setTagValue(value);
}, [member, tagDetail]);
useDebounce(
async () => {
if (tagChangedByUser) {
let value = tagValue === "" ? "" : parseInt(tagValue);
let event = { target: { value: value } };
handleChange(
member,
"config",
"tags",
"tagChange",
tagDetail["id"]
)(event);
}
setTagChangedByUser(false);
},
500,
[tagValue]
);
const handleSelectChange = (event) => {
let newValue = event.target.value;
setTagChangedByUser(true);
setTagValue(newValue);
};
const handleFlagChange = (value) => (event) => {
let newValue;
let oldValue;
if (tagValue === "") {
oldValue = 0;
} else {
oldValue = tagValue;
}
if (event.target.checked) {
newValue = oldValue + value;
} else {
newValue = oldValue - value;
}
setTagChangedByUser(true);
setTagValue(newValue);
};
const handleInputChange = (event) => {
let value = event.target.value;
if (/^(|0|[1-9]\d*)$/.test(value)) {
value = value === "" ? value : parseInt(value);
} else {
value = 0;
}
setTagChangedByUser(true);
setTagValue(value);
};
const clearTag = (event) => {
setTagChangedByUser(true);
setTagValue("");
};
return (
<Paper style={{ padding: 20 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography
variant="h5"
color={tagValue === "" ? "error" : "primary"}
>
{tagName}
{tagValue === "" ? (
""
) : (
<IconButton aria-label="delete" onClick={clearTag}>
<DeleteIcon />
</IconButton>
)}
</Typography>
</Grid>
<Grid container>
<Grid item xs={6}>
<Select
native
value={tagValue}
onChange={handleSelectChange}
displayEmpty
style={{ minWidth: 100 }}
>
<option value="">--</option>
{Object.entries(tagDetail["enums"]).map(
([enumKey, enumValue]) => (
<option key={enumKey} value={enumValue}>
{enumKey}
</option>
)
)}
{Object.values(tagDetail["enums"]).length === 0 &&
tagValue !== "" ? (
<option value={tagValue}>(no enums)</option>
) : (
""
)}
{Object.values(tagDetail["enums"]).length !== 0 &&
!Object.values(tagDetail["enums"]).includes(tagValue) &&
tagValue !== "" ? (
<option value={tagValue}>(custom)</option>
) : (
""
)}
</Select>
</Grid>
<Grid item xs={6}>
<Input value={tagValue} onChange={handleInputChange} />
</Grid>
</Grid>
<Grid item xs={12}>
{Object.entries(tagDetail["flags"]).map(([flagKey, flagValue]) => (
<FormControlLabel
control={
<Checkbox
checked={(tagValue & flagValue) === flagValue}
onChange={handleFlagChange(flagValue)}
color="primary"
/>
}
key={"flag-" + flagKey}
label={flagKey}
/>
))}
</Grid>
</Grid>
</Paper>
);
}
export default Tag;

View file

@ -0,0 +1 @@
export { default } from "./Tag";

View file

@ -1,34 +1,33 @@
import { useState } from "react";
import {
Accordion,
AccordionSummary,
AccordionDetails,
AccordionSummary,
Button,
Divider,
Snackbar,
Hidden,
Grid,
Hidden,
Snackbar,
Typography,
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import CodeMirror from "@uiw/react-codemirror";
import "codemirror/theme/3024-day.css";
import { compile } from "external/RuleCompiler";
import debounce from "lodash/debounce";
import { useState } from "react";
import API from "utils/API";
function NetworkRules({ network }) {
function NetworkRules({ network, callback }) {
const [editor, setEditor] = useState(null);
const [flowData, setFlowData] = useState({
rules: [...network.config.rules],
capabilities: [...network.config.capabilities],
tags: [...network.config.tags],
});
const [tagCapByNameData, setTagCapByNameData] = useState({
tagsByName: network.tagsByName || {},
capabilitiesByName: network.capabilitiesByName || {},
});
const [errors, setErrors] = useState([]);
const [snackbarOpen, setSnackbarOpen] = useState(false);
@ -37,12 +36,16 @@ function NetworkRules({ network }) {
const req = await API.post("/network/" + network["config"]["id"], {
config: { ...flowData },
rulesSource: editor.getValue(),
...tagCapByNameData,
});
console.log("Action", req);
setSnackbarOpen(true);
const timer = setTimeout(() => {
setSnackbarOpen(false);
}, 1500);
callback();
return () => clearTimeout(timer);
}
};
@ -51,14 +54,29 @@ function NetworkRules({ network }) {
const src = event.getValue();
setEditor(event);
let rules = [],
caps = [],
tags = [];
caps = {},
tags = {};
const res = compile(src, rules, caps, tags);
if (!res) {
let capabilitiesByName = {};
for (var key in caps) {
capabilitiesByName[key] = caps[key].id;
}
let tagsByName = { ...tags };
for (let key in tags) {
tags[key] = { id: tags[key].id, default: tags[key].default };
}
setFlowData({
rules: [...rules],
capabilities: [...caps],
tags: [...tags],
capabilities: [...Object.values(caps)],
tags: [...Object.values(tags)],
});
setTagCapByNameData({
tagsByName: tagsByName,
capabilitiesByName: capabilitiesByName,
});
setErrors([]);
} else {

View file

@ -1,18 +1,15 @@
import { useState, useEffect } from "react";
import { Link as RouterLink, useParams, useHistory } from "react-router-dom";
import { useLocalStorage } from "react-use";
import { Link, Grid, Typography } from "@material-ui/core";
import { Grid, Link, Typography } from "@material-ui/core";
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
import useStyles from "./Network.styles";
import NetworkHeader from "components/NetworkHeader";
import NetworkSettings from "components/NetworkSettings";
import NetworkManagement from "components/NetworkManagement";
import NetworkMembers from "components/NetworkMembers";
import NetworkRules from "components/NetworkRules";
import NetworkManagement from "components/NetworkManagement";
import NetworkSettings from "components/NetworkSettings";
import { useCallback, useEffect, useState } from "react";
import { Link as RouterLink, useHistory, useParams } from "react-router-dom";
import { useLocalStorage } from "react-use";
import API from "utils/API";
import useStyles from "./Network.styles";
function Network() {
const { nwid } = useParams();
@ -22,22 +19,23 @@ function Network() {
const classes = useStyles();
const history = useHistory();
useEffect(() => {
async function fetchData() {
try {
const network = await API.get("network/" + nwid);
setNetwork(network.data);
console.log("Current network:", network.data);
} catch (err) {
if (err.response.status === 404) {
history.push("/404");
}
console.error(err);
const fetchData = useCallback(async () => {
try {
const network = await API.get("network/" + nwid);
setNetwork(network.data);
console.log("Current network:", network.data);
} catch (err) {
if (err.response.status === 404) {
history.push("/404");
}
console.error(err);
}
fetchData();
}, [nwid, history]);
useEffect(() => {
fetchData();
}, [nwid, fetchData]);
if (loggedIn) {
return (
<>
@ -52,8 +50,10 @@ function Network() {
<NetworkSettings network={network} setNetwork={setNetwork} />
</>
)}
<NetworkMembers />
{network["config"] && <NetworkRules network={network} />}
<NetworkMembers network={network} />
{network["config"] && (
<NetworkRules network={network} callback={fetchData} />
)}
<NetworkManagement />
</div>
</>

View file

@ -1,3 +1,5 @@
import { pull } from "lodash";
export function parseValue(
event,
mode = "text",
@ -23,6 +25,25 @@ export function parseValue(
}
} else if (mode === "custom") {
value = data;
} else if (mode === "capChange") {
value = data[key1][key2];
if (event.target.checked) {
value.push(id);
} else {
pull(value, id);
}
} else if (mode === "tagChange") {
value = data[key1][key2];
let tagValue = event.target.value;
let tagIndex = value.findIndex((item) => {
return item[0] === id;
});
if (tagIndex !== -1) {
value.splice(tagIndex, 1);
}
if (tagValue !== "") {
value.push([id, tagValue]);
}
} else {
value = event.target.value;
}