From 56f11431d8721a01e11baf74721796ca6afdf500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Wed, 2 Mar 2022 21:12:40 +0100 Subject: Minimum sort-of working frontend. --- package.json | 4 +- pyenc/__init__.py | 38 +++++- pyenc/model.py | 4 + pyenc/static/css/style.css | 8 ++ pyenc/static/js-source/script.js | 284 ++++++++++++++++++++++++++++++++++----- pyenc/templates/start_page.html | 1 + 6 files changed, 299 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index f7440c4..9918b34 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "plugins": [] }, "scripts": { - "build": "browserify --debug -t babelify static/js-source/script.js -o static/js/script.js", - "watch": "watchify --debug -t babelify static/js-source/script.js -o static/js/script.js" + "build": "browserify --debug -t babelify pyenc/static/js-source/script.js -o pyenc/static/js/script.js", + "watch": "watchify --debug -t babelify pyenc/static/js-source/script.js -o pyenc/static/js/script.js" }, "devDependencies": { "@babel/core": "^7.16.7", diff --git a/pyenc/__init__.py b/pyenc/__init__.py index 6323fbe..851488c 100644 --- a/pyenc/__init__.py +++ b/pyenc/__init__.py @@ -48,7 +48,7 @@ def create_app(): return redirect(url_for('root_page')) # API - @app.route('/list-classes') + @app.route('/api/list-classes') def list_classes(): q = request.args.get('q', '') qq = '%{}%'.format('%'.join(q.split(' '))) @@ -58,6 +58,40 @@ def create_app(): return Response(json.dumps([x.class_name for x in results]), mimetype='application/json') + @app.route('/api/classes-for') + def classes_for(): + fqdn = request.args.get('fqdn') + classes = [cls.class_name for cls in + model.Host.query.where(model.Host.fqdn==fqdn).first().classes] + return Response(json.dumps(classes), + mimetype='application/json') + + @app.route('/api/change-classes', methods=['POST']) + def change_classes(): + j = request.json + host = model.Host.query.where(model.Host.fqdn==j['fqdn']).first() + remove_set = set(j['removed']) + + new_cls = [] + for cls in host.classes: + if cls.class_name in remove_set: + continue + new_cls.append(cls) + host.classes = new_cls + + cls = model.PuppetClass.query \ + .where(model.PuppetClass.class_name.in_(j['added'])) \ + .all() + host.classes.extend(cls) + print(remove_set, db.db.session.dirty) + return flask.redirect(url_for('classes_for', fqdn=j['fqdn'])) + + + @app.route('/api/hosts') + def list_hosts(): + return Response(flask.json.dumps([x.serialize() for x in model.Host.query.all()]), + mimetype='application/json') + @app.route('/enc') def enc(): fqdn = request.args.get('fqdn', 'default') @@ -75,6 +109,4 @@ def create_app(): mimetype='application/x-yaml') - - return app diff --git a/pyenc/model.py b/pyenc/model.py index dc0db05..1999d9d 100644 --- a/pyenc/model.py +++ b/pyenc/model.py @@ -28,6 +28,10 @@ class Host(db.Model): back_populates='hosts', secondary=host_classes) + def serialize(self): + return { column.name: self.__getattribute__(column.name) + for column in self.__table__.columns } + class PuppetClass(db.Model): __tablename__ = 'puppet_class' id = db.Column(db.Integer, primary_key=True) diff --git a/pyenc/static/css/style.css b/pyenc/static/css/style.css index 6510ceb..4dcef2a 100644 --- a/pyenc/static/css/style.css +++ b/pyenc/static/css/style.css @@ -33,3 +33,11 @@ dd ul { background-color: yellow; border: 2px solid orange; } + +.added { + color: green; +} + +.removed { + color: red; +} diff --git a/pyenc/static/js-source/script.js b/pyenc/static/js-source/script.js index 1f5c6d6..fd1251f 100644 --- a/pyenc/static/js-source/script.js +++ b/pyenc/static/js-source/script.js @@ -1,62 +1,276 @@ let React = require('react') let ReactDOM = require('react-dom') +/* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set + * 2022-03-02 + * Under CC0 + */ +function isSuperset(set, subset) { + for (let elem of subset) { + if (!set.has(elem)) { + return false + } + } + return true +} + +function union(setA, setB) { + let _union = new Set(setA) + for (let elem of setB) { + _union.add(elem) + } + return _union +} + +function intersection(setA, setB) { + let _intersection = new Set() + for (let elem of setB) { + if (setA.has(elem)) { + _intersection.add(elem) + } + } + return _intersection +} + +function symmetricDifference(setA, setB) { + let _difference = new Set(setA) + for (let elem of setB) { + if (_difference.has(elem)) { + _difference.delete(elem) + } else { + _difference.add(elem) + } + } + return _difference +} + +function difference(setA, setB) { + let _difference = new Set(setA) + for (let elem of setB) { + _difference.delete(elem) + } + return _difference +} +/* End borrowed code */ + // npx babel --watch src --out-dir . --presets react-app/prod +function iterator_to_list(iterator) { + let object = iterator.next() + let lst = []; + while (! object.done) { + lst.push(object.value); + object = iterator.next(); + } + return lst; +} + + +/* + * properties: + * classList: Array of String + * addedClasses: Set of String + * removedClasses: Set of String + * onAddClass: string => () + * onRemoveClass: string => () + * abandonChange: string => () + * submit: () => () + */ class ClassListForm extends React.Component { - constructor(props) { + constructor (props) { super(props); + this.state = { - selected: [], - classList: [], + shownClasses: [], } } + /* Callback for search box */ onChange = (e) => { - fetch('/list-classes?q=' + e.target.value) - .then(resp => resp.json()) - .then((data) => { - this.setState({classList: data}) - }) + if (e.target.value == '') { + this.setState({shownClasses: []}) + } else { + fetch('/api/list-classes?q=' + e.target.value) + .then(resp => resp.json()) + .then(data => this.setState({shownClasses: data})) + } } render() { - return (
+ let added = iterator_to_list(this.props.addedClasses.values()) + let removed = iterator_to_list(this.props.removedClasses.values()) + return (
+

Current classes

+
    + {this.props.classList.map(i => +
  • )} +
+ +
    + {added.map(i =>
  • )} + {removed.map(i =>
  • )} +
+ + +
+ - -
- - - ) - } - - onClassChange = (e) => { - if (e.target.checked) { - this.setState((state, props) => ({ - selected: state.selected.concat(e.target.value) - })) - } else { - } + placeholder="class name" + onInput={this.onChange}/> +
    + {this.state.shownClasses.map(i => +
  • + +
  • )} +
+
) + } + + removeClass = (e) => { + this.props.onRemoveClass(e.target.value); + } + + addClass = (e) => { + this.props.onAddClass(e.target.value); + } + + abandonChange = (e) => { + this.props.abandonChange(e.target.value); } } -class PuppetClassList extends React.Component { + +/* + * Information box about a single node, containing its name, + * environment, and current classes. Also includes options for adding + * and removing classes. + * + * Properties: + * environment: String + * fqdn: String + * + */ +class PuppetNode extends React.Component { + + constructor(props) { + super(props); + + this.state = { + /* Nodes puppet environment */ + environment: props.environment, + /* List of puppet classes */ + classes: [], + /* Classes to be added on commit */ + addedClasses: new Set(), + /* Classes to be removed on commit */ + removedClasses: new Set(), + } + + /* Fetch initial set of classes */ + fetch(`/api/classes-for?fqdn=${this.props.fqdn}`) + .then(r => r.json()) + .then(data => { + this.setState({ classes: data }) + }) + } render() { - return () + // console.log(this.state.addedClasses) + // console.log(this.state.addedClasses.values()) + // console.log(iterator_to_list(this.state.addedClasses.values())) + return ( +
+

{this.props.fqdn}

+
+
Environment
+
{this.state.environment}
+
+ +
+ ) + } + + submitChanges = () => { + let changes = { + fqdn: this.props.fqdn, + added: iterator_to_list(this.state.addedClasses.values()), + removed: iterator_to_list(this.state.removedClasses.values()), + } + console.log("Submitting", changes) + fetch('/api/change-classes', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + redirect: 'follow', + body: JSON.stringify(changes), + }).then(response => { + if (response.ok) { + response.json().then(data => { + console.log('Submit succeeded, updating classes', data) + this.setState((state, props) => ({ + addedClasses: new Set(), + removedClasses: new Set(), + classes: data, + })) + }) + } else { + response.text().then(data => { + console.log("Submit failed:") + console.log(response.status) + console.log(data) + }) + } + }) + } + + abandonChange = (name) => { + this.setState((state, props) => ({ + removedClasses: difference(state.removedClasses, new Set([name])), + addedClasses: difference(state.addedClasses, new Set([name])), + })) + } + + removeClass = (name) => { + this.setState((state, props) => ({ + removedClasses: union(state.removedClasses, new Set([name])), + addedClasses: difference(state.addedClasses, new Set([name])), + })) + } + + addClass = (name) => { + /* Only add if not alreaddy there */ + if (this.state.classes.indexOf(name) !== -1) return; + this.setState((state, props) => ({ + addedClasses: union(state.addedClasses, new Set([name])), + removedClasses: difference(state.removedClasses, new Set([name])), + })) } } window.onload = function() { - for (let el of document.getElementsByClassName('class-search')) { - ReactDOM.render((), el) - } + + fetch('/api/hosts') + .then(resp => resp.json()) + .then(data => { + ReactDOM.render( + (
+ {data.map(d => )} +
), + document.getElementById('react-base')) + + }) + + // for (let el of document.getElementsByClassName('class-search')) { + // ReactDOM.render((), el) + // } } diff --git a/pyenc/templates/start_page.html b/pyenc/templates/start_page.html index dccb1c6..8df08a3 100644 --- a/pyenc/templates/start_page.html +++ b/pyenc/templates/start_page.html @@ -1,6 +1,7 @@ {% extends "base.html" %} {% block content %}

This certainly is a page

+
{% for host in hosts %}
-- cgit v1.2.3