app: JSON configuration with API call; Home implementation with buttons support

This commit is contained in:
Silvan Calarco 2022-10-16 20:51:06 +02:00
parent b576c12bc4
commit 4fe4e36381
8 changed files with 124 additions and 40 deletions

View File

@ -1,11 +1,13 @@
""" """
Webdist Flask backend Webdist Flask backend
""" """
import json
import logging import logging
import subprocess import subprocess
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime
from flask import Flask, render_template from flask import Flask, render_template, request
# Enable logging # Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
@ -16,11 +18,12 @@ logger = logging.getLogger(__name__)
def create_app(config): def create_app(config):
""" creates and returns the Flask app """ """ creates and returns the Flask app """
_app = Flask(__name__) _app = Flask(__name__)
_app.config.from_pyfile(config) _app.config.from_file(config, load=json.load)
# initialize moment on the app within create_app() # initialize moment on the app within create_app()
return _app return _app
app = create_app("/etc/webdist/main.cfg") app = create_app("/etc/webdist/main.json")
logger.info(app.config)
def cursor_row_to_dict(cursor, row): def cursor_row_to_dict(cursor, row):
""" Converts a sqlite3 cursor row into a dict """ """ Converts a sqlite3 cursor row into a dict """
@ -35,12 +38,11 @@ def cursor_row_to_dict(cursor, row):
def latest(): def latest():
""" Returns a dict of latest packages """ """ Returns a dict of latest packages """
response = {} response = {}
for repo in app.config['REPOS']: for repo in app.config['WEBDIST_CONFIG']['repos']:
response[repo] = [] response[repo] = []
REPO_SOURCES_DB = "file:" + app.config['REPOS'][repo]['path'] + \ repo_sources_db = "file:" + app.config['WEBDIST_CONFIG']['repos'][repo]['path'] + \
f"{app.config['REPOS'][repo]['dbname']}-sources.db?mode=ro" f"{app.config['WEBDIST_CONFIG']['repos'][repo]['dbname']}-sources.db?mode=ro"
logger.info(REPO_SOURCES_DB) conn = sqlite3.connect(repo_sources_db, uri=True)
conn = sqlite3.connect(REPO_SOURCES_DB, uri=True)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
'SELECT * FROM sources ORDER BY buildtime DESC limit 100') 'SELECT * FROM sources ORDER BY buildtime DESC limit 100')
@ -55,11 +57,11 @@ def query(querystring):
logger.info("Query of %s", querystring) logger.info("Query of %s", querystring)
response = {} response = {}
for repo in app.config['REPOS']: for repo in app.config['WEBDIST_CONFIG']['repos']:
response[repo] = [] response[repo] = []
REPO_SOURCES_DB = "file:" + app.config['REPOS'][repo]['path'] + \ repo_sources_db = "file:" + app.config['WEBDIST_CONFIG']['repos'][repo]['path'] + \
f"{app.config['REPOS'][repo]['dbname']}-sources.db?mode=ro" f"{app.config['WEBDIST_CONFIG']['repos'][repo]['dbname']}-sources.db?mode=ro"
conn = sqlite3.connect(REPO_SOURCES_DB, uri=True) conn = sqlite3.connect(repo_sources_db, uri=True)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
f'SELECT * FROM sources where name like "%%{querystring}%%" ' f'SELECT * FROM sources where name like "%%{querystring}%%" '
@ -85,7 +87,7 @@ def _latest():
@app.route("/button/<name>") @app.route("/button/<name>")
def _button(name): def _button(name):
script = app.config['BUTTONS'][name]['script'] script = app.config['WEBDIST_CONFIG']['buttons'][name]['script']
output = subprocess.check_output([script]) output = subprocess.check_output([script])
return render_template("button.html",output=output.decode()) return render_template("button.html",output=output.decode())
@ -95,7 +97,35 @@ def _query(querystring):
data = query(querystring) data = query(querystring)
return render_template("query.html", data=data) return render_template("query.html", data=data)
@app.route("/api/v1/config")
def api_config():
""" GET config API call """
logger.info("Origin: %s", request.headers['Origin'])
return {'result': app.config['WEBDIST_CONFIG'] }
@app.route("/api/v1/latest") @app.route("/api/v1/latest")
def _hello(): def api_latest():
""" Get latest packages API call """
data = latest() data = latest()
return {'result': data } return {'result': data }
@app.route("/api/v1/button/<name>")
def api_button(name):
""" Call a button associated script """
script = app.config['WEBDIST_CONFIG']['buttons'][name]['script']
output = subprocess.check_output([script])
return {'result': output.decode() }
@app.after_request
def after_request(response):
""" Allow API request origins specified in configuration file """
white_origin = app.config['WEBDIST_CONFIG']['white_origins']
if request.headers['Origin'] in white_origin:
response.headers['Access-Control-Allow-Origin'] = request.headers['Origin']
response.headers['Access-Control-Allow-Methods'] = 'PUT,GET,POST,DELETE'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
return response

View File

@ -1,10 +0,0 @@
#
# WebDist main configuration file
#
REPOS = {
'openmamba-rolling': { 'dbname': 'devel', 'path': '/var/webbuild/db/' },
'openmamba-testing': { 'dbname': 'devel-makedist', 'path': '/var/webbuild/db/' },
}
BUTTONS = {
'sample_button': { 'text':"This is a sample button", 'script': '/bin/true' }
}

13
main.json.example Normal file
View File

@ -0,0 +1,13 @@
{
"WEBDIST_CONFIG": {
"white_origins": ["http://buildvm01:5000", "http://localhost:3000"],
"repos": {
"devel": { "dbname": "devel", "path": "/var/webbuild/db/" },
"devel-makedist": { "dbname": "devel-makedist", "path": "/var/webbuild/db/" },
"devel-autodist": { "dbname": "devel-autodist", "path": "/var/webbuild/db/" }
},
"buttons": {
"sample_button": { "text":"This is a sample button", "script": "/usr/bin/ls" }
}
}
}

View File

@ -1,7 +1,6 @@
{ {
"name": "react-webdist-app", "name": "react-webdist-app",
"version": "0.1.0", "version": "0.1.0",
"proxy": "http://buildvm01:5000",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@emotion/hash": "^0.9.0", "@emotion/hash": "^0.9.0",

View File

@ -5,19 +5,22 @@ import TypoGraphy from '@mui/material/Typography';
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from "@mui/icons-material/Menu";
import configData from "./config.json";
import './App.css'; import './App.css';
import Home from "./Home"
import Latest from "./Latest" import Latest from "./Latest"
import NavBar from "./NavBar" import NavBar from "./NavBar"
class App extends Component { class App extends Component {
constructor(props){ constructor(props){
super(props) super(props)
// Set initial state // Set initial state
this.state = { menuState: 0 } this.state = { menuState: 0 }
// Binding this keyword // Binding this keyword
this.showLatest = this.showLatest.bind(this) this.showLatest = this.showLatest.bind(this)
this.showHome = this.showHome.bind(this) this.showHome = this.showHome.bind(this)
} }
showLatest() { showLatest() {
@ -49,13 +52,8 @@ class App extends Component {
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<br/> <br/>
<div> {this.state.menuState === 0 && <Home />}
<Button color="primary" variant="contained" onClick={this.showLatest}>
Click Me
</Button>
</div>
<div className="container-fluid"> <div className="container-fluid">
<hr/>
{this.state.menuState === 1 && <Latest />} {this.state.menuState === 1 && <Latest />}
</div> </div>
</div> </div>

50
react-webdist-app/src/Home.js vendored Normal file
View File

@ -0,0 +1,50 @@
import React, { useState, useEffect, Component } from "react";
import Button from "@mui/material/Button";
import configData from "./config.json";
import "./App.css";
class Home extends Component {
constructor(props){
super(props)
// Set initial state
this.state = { buttonOutput: null, config: null }
// Binding this keyword
this.callButton = this.callButton.bind(this)
}
componentDidMount() {
fetch(configData.SERVER_URL + 'config').then(res => res.json()).then(data => {
this.setState({ config: data.result });
});
}
callButton(button) {
fetch(configData.SERVER_URL + 'button/' + button).then(res => res.json()).then(data => {
this.setState({ buttonOutput: data.result })
});
}
render() {
return (
<div className="row align-items-start">
{this.state.config !== null && Object.entries(this.state.config.buttons).map(([button,data])=>(
<div className="row" key={button}>
<div className="col align-items-start" key={button} onClick={() => this.callButton(button)}>
<Button color="primary" variant="contained">
{data.text}
</Button>
</div>
</div>
))}
{this.state.buttonOutput &&
<div className="row">
<pre><br/>{this.state.buttonOutput}</pre>
</div>
}
</div>
);
}
}
export default Home;

View File

@ -1,20 +1,21 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { format } from 'date-fns' import { format } from 'date-fns'
import configData from "./config.json";
import "./App.css"; import "./App.css";
function Latest() { function Latest() {
const [placeholder, setPlaceholder] = useState({}); const [latest, setLatest] = useState({});
useEffect(() => { useEffect(() => {
fetch('/api/v1/latest').then(res => res.json()).then(data => { fetch(configData.SERVER_URL + 'latest').then(res => res.json()).then(data => {
setPlaceholder(data.result); setLatest(data.result);
}); });
}, []); }, []);
return ( return (
<div className="row align-items-start"> <div className="row align-items-start">
<h1>Latest packages</h1> <h1>Latest packages</h1>
{Object.entries(placeholder).map(([repo,pkgs])=>( {Object.entries(latest).map(([repo,pkgs])=>(
<div className="col align-items-start" key={repo}> <div className="col align-items-start" key={repo}>
<h2 key={repo}>{repo}</h2> <h2 key={repo}>{repo}</h2>
{pkgs.map(pkg =>( {pkgs.map(pkg =>(

View File

@ -0,0 +1,3 @@
{
"SERVER_URL": "http://buildvm01:5000/api/v1/"
}