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: { additionalConfig: {
description: "", description: "",
rulesSource: constants.defaultRulesSource, rulesSource: constants.defaultRulesSource,
tagsByName: {},
capabilitiesByName: {},
}, },
members: [], members: [],
}; };
@ -79,6 +81,12 @@ async function updateNetworkAdditionalData(nwid, data) {
if (data.hasOwnProperty("rulesSource")) { if (data.hasOwnProperty("rulesSource")) {
additionalData.rulesSource = data.rulesSource; additionalData.rulesSource = data.rulesSource;
} }
if (data.hasOwnProperty("tagsByName")) {
additionalData.tagsByName = data.tagsByName;
}
if (data.hasOwnProperty("capabilitiesByName")) {
additionalData.capabilitiesByName = data.capabilitiesByName;
}
if (additionalData) { if (additionalData) {
db.get("networks") db.get("networks")

View file

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

View file

@ -1,16 +1,19 @@
import { useState } from "react";
import { import {
Checkbox, Checkbox,
Dialog, Dialog,
DialogTitle,
DialogContent, DialogContent,
DialogTitle,
FormControlLabel,
Grid, Grid,
IconButton, IconButton,
Paper,
Typography,
} from "@material-ui/core"; } from "@material-ui/core";
import BuildIcon from "@material-ui/icons/Build"; 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 [open, setOpen] = useState(false);
const handleClickOpen = () => { const handleClickOpen = () => {
@ -55,6 +58,66 @@ function MemberSettings({ member, handleChange }) {
/> />
<span>Do Not Auto-Assign IPs</span> <span>Do Not Auto-Assign IPs</span>
</Grid> </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> </DialogContent>
</Dialog> </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 { import {
Accordion, Accordion,
AccordionSummary,
AccordionDetails, AccordionDetails,
AccordionSummary,
Button, Button,
Divider, Divider,
Snackbar,
Hidden,
Grid, Grid,
Hidden,
Snackbar,
Typography, Typography,
} from "@material-ui/core"; } from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import CodeMirror from "@uiw/react-codemirror"; import CodeMirror from "@uiw/react-codemirror";
import "codemirror/theme/3024-day.css"; import "codemirror/theme/3024-day.css";
import { compile } from "external/RuleCompiler"; import { compile } from "external/RuleCompiler";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import { useState } from "react";
import API from "utils/API"; import API from "utils/API";
function NetworkRules({ network }) { function NetworkRules({ network, callback }) {
const [editor, setEditor] = useState(null); const [editor, setEditor] = useState(null);
const [flowData, setFlowData] = useState({ const [flowData, setFlowData] = useState({
rules: [...network.config.rules], rules: [...network.config.rules],
capabilities: [...network.config.capabilities], capabilities: [...network.config.capabilities],
tags: [...network.config.tags], tags: [...network.config.tags],
}); });
const [tagCapByNameData, setTagCapByNameData] = useState({
tagsByName: network.tagsByName || {},
capabilitiesByName: network.capabilitiesByName || {},
});
const [errors, setErrors] = useState([]); const [errors, setErrors] = useState([]);
const [snackbarOpen, setSnackbarOpen] = useState(false); const [snackbarOpen, setSnackbarOpen] = useState(false);
@ -37,12 +36,16 @@ function NetworkRules({ network }) {
const req = await API.post("/network/" + network["config"]["id"], { const req = await API.post("/network/" + network["config"]["id"], {
config: { ...flowData }, config: { ...flowData },
rulesSource: editor.getValue(), rulesSource: editor.getValue(),
...tagCapByNameData,
}); });
console.log("Action", req); console.log("Action", req);
setSnackbarOpen(true); setSnackbarOpen(true);
const timer = setTimeout(() => { const timer = setTimeout(() => {
setSnackbarOpen(false); setSnackbarOpen(false);
}, 1500); }, 1500);
callback();
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}; };
@ -51,14 +54,29 @@ function NetworkRules({ network }) {
const src = event.getValue(); const src = event.getValue();
setEditor(event); setEditor(event);
let rules = [], let rules = [],
caps = [], caps = {},
tags = []; tags = {};
const res = compile(src, rules, caps, tags); const res = compile(src, rules, caps, tags);
if (!res) { 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({ setFlowData({
rules: [...rules], rules: [...rules],
capabilities: [...caps], capabilities: [...Object.values(caps)],
tags: [...tags], tags: [...Object.values(tags)],
});
setTagCapByNameData({
tagsByName: tagsByName,
capabilitiesByName: capabilitiesByName,
}); });
setErrors([]); setErrors([]);
} else { } else {

View file

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

View file

@ -1,3 +1,5 @@
import { pull } from "lodash";
export function parseValue( export function parseValue(
event, event,
mode = "text", mode = "text",
@ -23,6 +25,25 @@ export function parseValue(
} }
} else if (mode === "custom") { } else if (mode === "custom") {
value = data; 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 { } else {
value = event.target.value; value = event.target.value;
} }