diff options
-rw-r--r-- | package.json | 4 | ||||
-rw-r--r-- | pyenc/__init__.py | 38 | ||||
-rw-r--r-- | pyenc/model.py | 4 | ||||
-rw-r--r-- | pyenc/static/css/style.css | 8 | ||||
-rw-r--r-- | pyenc/static/js-source/script.js | 284 | ||||
-rw-r--r-- | 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 (<form> + let added = iterator_to_list(this.props.addedClasses.values()) + let removed = iterator_to_list(this.props.removedClasses.values()) + return (<div> + <h2>Current classes</h2> + <ul> + {this.props.classList.map(i => + <li key={i}> <button value={i} onClick={this.removeClass}>{i}</button> </li>)} + </ul> + + <ul> + {added.map(i => <li className="added" key={i}><button value={i} onClick={this.abandonChange} >+ {i}</button></li>)} + {removed.map(i => <li className="removed" key={i}><button value={i} onClick={this.abandonChange} >- {i}</button></li>)} + </ul> + + <button onClick={this.props.submit}>Submit changes</button> + <br/> + <input type="text" - placeholder="class name" - onInput={this.onChange}/> - <PuppetClassList onChange={this.onClassChange} nodes={this.state.selected}/> - <hr/> - <PuppetClassList onChange={this.onClassChange} nodes={this.state.classList}/> - <input type="submit" value="Add Selected"/> - </form>) - } - - onClassChange = (e) => { - if (e.target.checked) { - this.setState((state, props) => ({ - selected: state.selected.concat(e.target.value) - })) - } else { - } + placeholder="class name" + onInput={this.onChange}/> + <ul> + {this.state.shownClasses.map(i => + <li key={i}> + <button value={i} onClick={this.addClass}>{i}</button> + </li>)} + </ul> + </div>) + } + + 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 (<ul> - {this.props.nodes.map(node => <li key={node}> - <input checked value={node} onChange={this.props.onChange} type="checkbox"/> - <label>{node}</label> - </li>)} - </ul>) + // console.log(this.state.addedClasses) + // console.log(this.state.addedClasses.values()) + // console.log(iterator_to_list(this.state.addedClasses.values())) + return ( + <div className="host"> + <h2>{this.props.fqdn}</h2> + <dl> + <dt>Environment</dt> + <dd>{this.state.environment}</dd> + </dl> + <ClassListForm + submit={this.submitChanges} + classList={this.state.classes} + addedClasses={this.state.addedClasses} + removedClasses={this.state.removedClasses} + onRemoveClass={this.removeClass} + onAddClass={this.addClass} + abandonChange={this.abandonChange} + /> + </div> + ) + } + + 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((<ClassListForm/>), el) - } + + fetch('/api/hosts') + .then(resp => resp.json()) + .then(data => { + ReactDOM.render( + (<div className="hosts"> + {data.map(d => <PuppetNode key={d.fqdn} {...d}/>)} + </div>), + document.getElementById('react-base')) + + }) + + // for (let el of document.getElementsByClassName('class-search')) { + // ReactDOM.render((<ClassListForm/>), 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 %} <h1>This certainly is a page</h1> + <div id="react-base"></div> <div class="hosts"> {% for host in hosts %} <div class="host"> |