aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHugo Hörnquist <hugo@lysator.liu.se>2022-03-02 21:12:40 +0100
committerHugo Hörnquist <hugo@lysator.liu.se>2022-03-02 21:12:40 +0100
commit56f11431d8721a01e11baf74721796ca6afdf500 (patch)
treea6822d305e78133d4eda3966dd1b50bdb02fb293
parentInitial code add. (diff)
downloadpuppet-classifier-56f11431d8721a01e11baf74721796ca6afdf500.tar.gz
puppet-classifier-56f11431d8721a01e11baf74721796ca6afdf500.tar.xz
Minimum sort-of working frontend.
-rw-r--r--package.json4
-rw-r--r--pyenc/__init__.py38
-rw-r--r--pyenc/model.py4
-rw-r--r--pyenc/static/css/style.css8
-rw-r--r--pyenc/static/js-source/script.js284
-rw-r--r--pyenc/templates/start_page.html1
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">