diff options
Diffstat (limited to 'pyenc/static/js-source/script.js')
-rw-r--r-- | pyenc/static/js-source/script.js | 284 |
1 files changed, 249 insertions, 35 deletions
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) + // } } |