Added changes for group properties editing

This commit is contained in:
vladfreeze
2021-12-02 11:29:00 +01:00
committed by Min RK
parent 85e37e7f8c
commit 471e492c11
7 changed files with 453 additions and 13 deletions

View File

@@ -0,0 +1,256 @@
import React, { useState } from "react";
import "./table-select.css";
function hasDuplicates(array) {
var valuesSoFar = Object.create(null);
for (var i = 0; i < array.length; ++i) {
var value = array[i];
if (value in valuesSoFar) {
return true;
}
valuesSoFar[value] = true;
}
return false;
}
export default class DynamicTable extends React.Component {
constructor(props) {
super(props);
this.current_propobject = props.current_propobject;
this.setProp = props.setProp;
this.setPropKeys = props.setPropKeys;
this.setPropValues = props.setPropValues;
this.setChanged = props.setChanged;
let current_keys = [];
let current_values = [];
for (var property in this.current_propobject) {
current_keys.push(property);
current_values.push(this.current_propobject[property]);
}
//current_keys = this.current_propobject.propkeys
//current_values = this.current_propobject.propvalues
this.state = {
message: "",
message2: "",
propkeys: current_keys,
propvalues: current_values,
propobject: "",
};
}
updateMessageKey(event) {
this.setState({
message: event.target.value,
});
}
updateMessageValue(event) {
this.setState({
message2: event.target.value,
});
}
handleRefresh(i) {
var propkeys = this.state.propkeys;
var propvalues = this.state.propvalues;
var propobject = {};
propkeys.forEach((key, i) => (propobject[key] = propvalues[i]));
console.log(propobject);
this.setProp(propobject);
this.setPropKeys(propkeys);
this.setPropValues(propvalues);
this.setChanged(true);
this.setState({
propkeys: propkeys,
propvalues: propvalues,
message: "",
message2: "",
propobject: propobject,
});
}
handleClick() {
var propkeys = this.state.propkeys;
var propvalues = this.state.propvalues;
if (this.state.message != "") {
if (this.state.message2 != "") {
propkeys.push(this.state.message);
propvalues.push(this.state.message2);
} else {
console.log("Value not valid");
}
} else {
console.log("Value not valid");
}
var propobject = {};
propkeys.forEach((key, i) => (propobject[key] = propvalues[i]));
console.log(propobject);
this.setProp(propobject);
this.setPropKeys(propkeys);
this.setPropValues(propvalues);
this.setChanged(true);
this.setState({
propkeys: propkeys,
propvalues: propvalues,
message: "",
message2: "",
propobject: propobject,
});
}
handleValueChanged(i, event) {
var propvalues = this.state.propvalues;
var propkeys = this.state.propkeys;
propvalues[i] = event.target.value;
this.handleRefresh();
this.setPropKeys(propkeys);
this.setPropValues(propvalues);
this.setState({
propvalues: propvalues,
});
}
handleKeyChanged(i, event) {
var propkeys = this.state.propkeys;
if (event.target.value != "") {
propkeys[i] = event.target.value;
}
console.log(event.target.value);
if (event.target.value == "") {
this.handleItemDeleted(i);
}
this.handleRefresh(i);
this.setPropKeys(propkeys);
this.setState({
propkeys: propkeys,
});
}
handleItemDeleted(i) {
var propvalues = this.state.propvalues;
var propkeys = this.state.propkeys;
propvalues.splice(i, 1);
propkeys.splice(i, 1);
this.setPropKeys(propkeys);
this.setPropValues(propvalues);
this.handleRefresh(i);
this.setState({
propvalues: propvalues,
propkeys: propkeys,
});
}
renderKeyRows() {
var context = this;
return this.state.propkeys.map(function (o, i) {
return (
<tr key={"item-" + i}>
<td>
<input
className="properties-table-keyvalues"
type="text"
value={o}
id={o + i}
onChange={context.handleKeyChanged.bind(context, i)}
/>
</td>
</tr>
);
});
}
renderValueRows() {
var context = this;
return this.state.propvalues.map(function (o, i) {
return (
<tr key={"item-" + i}>
<td>
<input
className="properties-table-keyvalues"
type="text"
value={o}
onChange={context.handleValueChanged.bind(context, i)}
/>
</td>
</tr>
);
});
}
renderDelete() {
var context = this;
return this.state.propvalues.map(function (o, i) {
return (
<tr key={"item-" + i}>
<td>
<button
className="btn btn-default"
onClick={context.handleItemDeleted.bind(context, i)}
>
Delete
</button>
</td>
</tr>
);
});
}
render() {
return (
<div>
<table className="">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>{this.renderKeyRows()}</td>
<td>{this.renderValueRows()}</td>
<td>{this.renderDelete()}</td>
</tr>
</tbody>
</table>
<form>
<tr>
<td>
<input
className="properties-table"
type="text"
value={this.state.message}
onChange={this.updateMessageKey.bind(this)}
/>
</td>
<td>
<input
className="properties-table"
type="text"
value={this.state.message2}
onChange={this.updateMessageValue.bind(this)}
/>
</td>
<td>
<button
className="btn btn-default"
onClick={this.handleClick.bind(this)}
>
Add Item
</button>
</td>
</tr>
</form>
<hr />
</div>
);
}
}

View File

@@ -0,0 +1,64 @@
@import url(../../style/root.css);
.properties-table {
width: 95%;
position: relative;
padding: 5px;
overflow-x: scroll;
}
.properties-table-keyvalues {
width: 95%;
position: relative;
padding: 5px;
overflow-x: scroll;
}
.properties-table div {
display: inline-block;
}
.properties-table-keyvalues .item {
padding: 3px;
padding-left: 2px;
padding-right: 2px;
border-radius: 1px;
font-size: 14px;
margin-left: 1px;
margin-right: 1px;
transition: 30ms ease-in all;
cursor: pointer;
user-select: none;
border: solid 1px #dfdfdf;
}
.properties-table .item {
padding: 3px;
padding-left: 2px;
padding-right: 2px;
border-radius: 1px;
font-size: 14px;
margin-left: 1px;
margin-right: 1px;
transition: 30ms ease-in all;
cursor: pointer;
user-select: none;
border: solid 1px #dfdfdf;
}
.properties-table .item.unselected {
background-color: #f7f7f7;
color: #777;
}
.properties-table .item.selected {
background-color: orange;
color: white;
}
.properties-table .item:hover {
opacity: 0.7;
}
.boxed {
border: 1px solid red;
}

View File

@@ -3,6 +3,19 @@ import { useSelector, useDispatch } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import GroupSelect from "../GroupSelect/GroupSelect"; import GroupSelect from "../GroupSelect/GroupSelect";
import DynamicTable from "../DynamicTable/DynamicTable";
function hasDuplicates(array) {
var valuesSoFar = Object.create(null);
for (var i = 0; i < array.length; ++i) {
var value = array[i];
if (value in valuesSoFar) {
return true;
}
valuesSoFar[value] = true;
}
return false;
}
const GroupEdit = (props) => { const GroupEdit = (props) => {
var [selected, setSelected] = useState([]), var [selected, setSelected] = useState([]),
@@ -23,6 +36,7 @@ const GroupEdit = (props) => {
var { var {
addToGroup, addToGroup,
updateProp,
removeFromGroup, removeFromGroup,
deleteGroup, deleteGroup,
updateGroups, updateGroups,
@@ -37,6 +51,9 @@ const GroupEdit = (props) => {
} }
var { group_data } = location.state; var { group_data } = location.state;
var [propobject, setProp] = useState(group_data.properties);
var [propkeys, setPropKeys] = useState([]);
var [propvalues, setPropValues] = useState([]);
if (!group_data) return <div></div>; if (!group_data) return <div></div>;
@@ -57,6 +74,19 @@ const GroupEdit = (props) => {
setChanged(true); setChanged(true);
}} }}
/> />
<div className="container">
<div className="row">
<div className="alert alert-info">Manage group properties</div>
</div>
</div>
<DynamicTable
current_propobject={group_data.properties}
setProp={setProp}
setPropKeys={setPropKeys}
setPropValues={setPropValues}
setChanged={setChanged}
//Add keys
/>
<div className="row"> <div className="row">
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2"> <div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
<button id="return" className="btn btn-light"> <button id="return" className="btn btn-light">
@@ -68,10 +98,10 @@ const GroupEdit = (props) => {
className="btn btn-primary" className="btn btn-primary"
onClick={() => { onClick={() => {
// check for changes // check for changes
if (!changed) { //if (!changed) {
history.push("/groups"); // history.push("/groups");
return; // return;
} //}
let new_users = selected.filter( let new_users = selected.filter(
(e) => !group_data.users.includes(e) (e) => !group_data.users.includes(e)
@@ -79,7 +109,6 @@ const GroupEdit = (props) => {
let removed_users = group_data.users.filter( let removed_users = group_data.users.filter(
(e) => !selected.includes(e) (e) => !selected.includes(e)
); );
let promiseQueue = []; let promiseQueue = [];
if (new_users.length > 0) if (new_users.length > 0)
promiseQueue.push(addToGroup(new_users, group_data.name)); promiseQueue.push(addToGroup(new_users, group_data.name));
@@ -87,18 +116,33 @@ const GroupEdit = (props) => {
promiseQueue.push( promiseQueue.push(
removeFromGroup(removed_users, group_data.name) removeFromGroup(removed_users, group_data.name)
); );
if (hasDuplicates(propkeys) == true) {
error.textContent = "Duplicate key found!";
error.style.color = "red";
} else {
error.textContent = "";
propkeys.forEach((key, i) => (propobject[key] = propvalues[i]));
}
if (
propobject != group_data.properties &&
hasDuplicates(propkeys) == false
) {
promiseQueue.push(updateProp(propobject, group_data.name));
}
Promise.all(promiseQueue) Promise.all(promiseQueue)
.then(() => { .then(() => {
updateGroups(0, limit) updateGroups(0, limit).then((data) =>
.then((data) => dispatchPageUpdate(data, 0)) dispatchPageUpdate(data, 0)
.then(() => history.push("/groups")); );
}) })
.catch((err) => console.log(err)); .catch((err) => console.log(err));
}} }}
> >
Apply Apply
</button> </button>
<div>
<span id="error"></span>
</div>
<button <button
id="delete-group" id="delete-group"
className="btn btn-danger" className="btn btn-danger"

View File

@@ -19,6 +19,8 @@ const withAPI = withProps(() => ({
names.map((e) => jhapiRequest("/users/" + e + "/server", "DELETE")), names.map((e) => jhapiRequest("/users/" + e + "/server", "DELETE")),
addToGroup: (users, groupname) => addToGroup: (users, groupname) =>
jhapiRequest("/groups/" + groupname + "/users", "POST", { users }), jhapiRequest("/groups/" + groupname + "/users", "POST", { users }),
updateProp: (propobject, groupname) =>
jhapiRequest("/groups/" + groupname + "/properties", "PUT", propobject),
removeFromGroup: (users, groupname) => removeFromGroup: (users, groupname) =>
jhapiRequest("/groups/" + groupname + "/users", "DELETE", { users }), jhapiRequest("/groups/" + groupname + "/users", "DELETE", { users }),
createGroup: (groupName) => jhapiRequest("/groups/" + groupName, "POST"), createGroup: (groupName) => jhapiRequest("/groups/" + groupName, "POST"),

View File

@@ -181,8 +181,25 @@ class GroupUsersAPIHandler(_GroupAPIHandler):
self.write(json.dumps(self.group_model(group))) self.write(json.dumps(self.group_model(group)))
class GroupPropertiesAPIHandler(_GroupAPIHandler):
"""Modify a group's properties"""
@needs_scope('groups')
def put(self, group_name):
group = self.find_group(group_name)
data = self.get_json_body()
# self._check_group_model(data)
if not isinstance(data, dict):
raise web.HTTPError(400, "Must specify properties")
self.log.info("Updating properties of group %s", group_name)
group.properties = data
self.db.commit()
self.write(json.dumps(self.group_model(group)))
default_handlers = [ default_handlers = [
(r"/api/groups", GroupListAPIHandler), (r"/api/groups", GroupListAPIHandler),
(r"/api/groups/([^/]+)", GroupAPIHandler), (r"/api/groups/([^/]+)", GroupAPIHandler),
(r"/api/groups/([^/]+)/users", GroupUsersAPIHandler), (r"/api/groups/([^/]+)/users", GroupUsersAPIHandler),
(r"/api/groups/([^/]+)/properties", GroupPropertiesAPIHandler),
] ]

View File

@@ -222,6 +222,8 @@ class Group(Base):
__tablename__ = 'groups' __tablename__ = 'groups'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Unicode(255), unique=True) name = Column(Unicode(255), unique=True)
# The properties column contains key:value pairs that represent group settings and their values. Example: {"ram": 8, "cpu": 4}
properties = Column(JSONDict, default={})
users = relationship('User', secondary='user_group_map', backref='groups') users = relationship('User', secondary='user_group_map', backref='groups')
def __repr__(self): def __repr__(self):

View File

@@ -1613,8 +1613,20 @@ async def test_groups_list(app):
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
assert reply == [ assert reply == [
{'kind': 'group', 'name': 'alphaflight', 'users': [], 'roles': []}, {
{'kind': 'group', 'name': 'betaflight', 'users': [], 'roles': []}, 'kind': 'group',
'name': 'alphaflight',
'users': [],
'roles': [],
'properties': {},
},
{
'kind': 'group',
'name': 'betaflight',
'users': [],
'roles': [],
'properties': {},
},
] ]
# Test offset for pagination # Test offset for pagination
@@ -1622,7 +1634,15 @@ async def test_groups_list(app):
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
assert r.status_code == 200 assert r.status_code == 200
assert reply == [{'kind': 'group', 'name': 'betaflight', 'users': [], 'roles': []}] assert reply == [
{
'kind': 'group',
'name': 'betaflight',
'users': [],
'roles': [],
'properties': {},
}
]
r = await api_request(app, "groups?offset=10") r = await api_request(app, "groups?offset=10")
r.raise_for_status() r.raise_for_status()
@@ -1634,7 +1654,15 @@ async def test_groups_list(app):
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
assert r.status_code == 200 assert r.status_code == 200
assert reply == [{'kind': 'group', 'name': 'alphaflight', 'users': [], 'roles': []}] assert reply == [
{
'kind': 'group',
'name': 'alphaflight',
'users': [],
'roles': [],
'properties': {},
}
]
# 0 is rounded up to 1 # 0 is rounded up to 1
r = await api_request(app, "groups?limit=0") r = await api_request(app, "groups?limit=0")
@@ -1683,6 +1711,7 @@ async def test_group_get(app):
'name': 'alphaflight', 'name': 'alphaflight',
'users': ['sasquatch'], 'users': ['sasquatch'],
'roles': [], 'roles': [],
'properties': {},
} }
@@ -1770,6 +1799,32 @@ async def test_group_add_delete_users(app):
assert sorted(u.name for u in group.users) == sorted(names[2:]) assert sorted(u.name for u in group.users) == sorted(names[2:])
@mark.group
async def test_group_add_properties(app):
db = app.db
# must specify users
r = await api_request(app, 'groups/alphaflight/properties', method='put', data='{}')
assert r.status_code == 200
properties_object = {'cpu': "8", 'ram': "4", 'image': "testimage"}
r = await api_request(
app,
'groups/alphaflight/properties',
method='put',
data=json.dumps(properties_object),
)
r.raise_for_status()
group = orm.Group.find(db, name='alphaflight')
assert sorted([k for k in group.properties]) == sorted(
[k for k in properties_object]
)
assert sorted([group.properties[k] for k in group.properties]) == sorted(
[properties_object[k] for k in properties_object]
)
# ----------------- # -----------------
# Service API tests # Service API tests
# ----------------- # -----------------