Add web server
This commit is contained in:
parent
655a9e34df
commit
1241a9b44d
43
server.go
43
server.go
|
@ -2,13 +2,30 @@ package secondly
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/GeertJohan/go.rice"
|
||||||
)
|
)
|
||||||
|
|
||||||
func startServer(addr string) {
|
func startServer(addr string) {
|
||||||
|
staticHandler := http.FileServer(rice.MustFindBox("static").HTTPBox())
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/fields.json", fieldsHandler)
|
mux.HandleFunc("/fields.json", fieldsHandler)
|
||||||
|
mux.HandleFunc("/save", saveHandler)
|
||||||
|
|
||||||
|
// Static
|
||||||
|
mux.Handle("/app.js", staticHandler)
|
||||||
|
mux.Handle("/app.css", staticHandler)
|
||||||
|
mux.Handle("/config.html", staticHandler)
|
||||||
|
// Redirect from root to a static file. Ugly yet effective.
|
||||||
|
mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.RequestURI == "/" {
|
||||||
|
http.Redirect(rw, req, "/config.html", http.StatusMovedPermanently)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
log.Println("Starting configuration server on", addr)
|
log.Println("Starting configuration server on", addr)
|
||||||
go http.ListenAndServe(addr, mux)
|
go http.ListenAndServe(addr, mux)
|
||||||
|
@ -16,6 +33,30 @@ func startServer(addr string) {
|
||||||
|
|
||||||
func fieldsHandler(rw http.ResponseWriter, req *http.Request) {
|
func fieldsHandler(rw http.ResponseWriter, req *http.Request) {
|
||||||
fields := extractFields(config, "")
|
fields := extractFields(config, "")
|
||||||
body, _ := json.Marshal(fields)
|
body, err := json.Marshal(fields)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.Write(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveHandler(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
cbody, err := ioutil.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfig(cbody)
|
||||||
|
writeConfig()
|
||||||
|
|
||||||
|
resp := struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}{
|
||||||
|
Success: true,
|
||||||
|
Msg: "Config successfully updated",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(resp)
|
||||||
rw.Write(body)
|
rw.Write(body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
line-height: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
margin: 50px 0 0 50px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 32px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
line-height: 40px;
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 30px;
|
||||||
|
margin: 0 0 30px;
|
||||||
|
padding: 0;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0 5px;
|
||||||
|
width: 250px;
|
||||||
|
line-height: 26px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
float: left;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 0 15px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
.padding {
|
||||||
|
float: left;
|
||||||
|
width: 5px;
|
||||||
|
height: 40px;
|
||||||
|
margin: 0 15px 0 0;
|
||||||
|
background-color: #ade;
|
||||||
|
}
|
||||||
|
#notice {
|
||||||
|
margin: 15px 0 0 100px;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
color: #0a0;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: #a00;
|
||||||
|
}
|
|
@ -0,0 +1,293 @@
|
||||||
|
/*
|
||||||
|
* Secondly: Configuration manager for Go language apps
|
||||||
|
* Copyright (c) 2015 Gregory Eremin
|
||||||
|
*
|
||||||
|
* Source: https://github.com/localhots/secondly
|
||||||
|
* Licence: https://github.com/localhots/secondly/blob/master/LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
function loadFields(callback) {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("GET", "/fields.json", true);
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
var fields = JSON.parse(xhr.responseText);
|
||||||
|
callback(fields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFields(payload, callback) {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", "/save", true);
|
||||||
|
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
callback(JSON.parse(xhr.responseText));
|
||||||
|
} else {
|
||||||
|
callback({"success": false, "msg": "Failed to save config"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawForm(fields) {
|
||||||
|
var elems = [];
|
||||||
|
|
||||||
|
var curLevel = 0;
|
||||||
|
var titlesPrinted = {};
|
||||||
|
for (var i = 0; i < fields.length; i++) {
|
||||||
|
var field = fields[i];
|
||||||
|
|
||||||
|
var tokens = field.path.split(".");
|
||||||
|
var section = tokens.slice(0, -1).join(".");
|
||||||
|
|
||||||
|
if (section != "" && !titlesPrinted[section]) {
|
||||||
|
titlesPrinted[section] = 1;
|
||||||
|
elems.push({
|
||||||
|
level: tokens.length - 1,
|
||||||
|
nodes: [makeSectionNode("/"+ section)],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
elems.push({
|
||||||
|
level: tokens.length - 1,
|
||||||
|
nodes: makeFieldNode(field),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render(elems);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(elems) {
|
||||||
|
var fields = document.getElementById("fields");
|
||||||
|
|
||||||
|
for (var i = 0; i < elems.length; i++) {
|
||||||
|
var row = elems[i];
|
||||||
|
var nodes = row.nodes;
|
||||||
|
|
||||||
|
for (var j = 0; j < row.level; j++) {
|
||||||
|
nodes.unshift(makePaddingNode())
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.appendChild(makeRow(nodes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePaddingNode() {
|
||||||
|
var div = document.createElement("div");
|
||||||
|
div.setAttribute("class", "padding");
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRow(nodes) {
|
||||||
|
var div = document.createElement("div");
|
||||||
|
div.setAttribute("class", "row");
|
||||||
|
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
div.appendChild(nodes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeFieldNode(field) {
|
||||||
|
var formGroup = [],
|
||||||
|
label = makeLabelNode(field.path, field.name),
|
||||||
|
input = document.createElement("input");
|
||||||
|
|
||||||
|
|
||||||
|
input.setAttribute("id", field.path);
|
||||||
|
|
||||||
|
if (field.kind !== "bool") {
|
||||||
|
input.value = field.value;
|
||||||
|
} else {
|
||||||
|
if (field.value) {
|
||||||
|
input.setAttribute("checked", "checked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input.setAttribute("data-type", field.kind);
|
||||||
|
|
||||||
|
switch (field.kind) {
|
||||||
|
case "string":
|
||||||
|
input.setAttribute("type", "text");
|
||||||
|
formGroup.push(label);
|
||||||
|
formGroup.push(input);
|
||||||
|
break;
|
||||||
|
case "bool":
|
||||||
|
input.setAttribute("type", "checkbox");
|
||||||
|
label.innerHTML = "";
|
||||||
|
label.appendChild(document.createTextNode(field.name));
|
||||||
|
formGroup.push(label);
|
||||||
|
formGroup.push(input);
|
||||||
|
break;
|
||||||
|
case "int":
|
||||||
|
case "int8":
|
||||||
|
case "int16":
|
||||||
|
case "int32":
|
||||||
|
case "int64":
|
||||||
|
case "uint":
|
||||||
|
case "uint8":
|
||||||
|
case "uint16":
|
||||||
|
case "uint32":
|
||||||
|
case "uint64":
|
||||||
|
case "float32":
|
||||||
|
case "float64":
|
||||||
|
input.setAttribute("type", "number");
|
||||||
|
switch (field.kind) {
|
||||||
|
case "int8":
|
||||||
|
input.setAttribute("min", "-128");
|
||||||
|
input.setAttribute("max", "127");
|
||||||
|
break;
|
||||||
|
case "int16":
|
||||||
|
input.setAttribute("min", "-32768");
|
||||||
|
input.setAttribute("max", "32767");
|
||||||
|
break;
|
||||||
|
case "int32":
|
||||||
|
input.setAttribute("min", "-2147483648");
|
||||||
|
input.setAttribute("max", "2147483647");
|
||||||
|
break;
|
||||||
|
case "int": // Assuming x86-64 architecture
|
||||||
|
case "int64":
|
||||||
|
input.setAttribute("min", "-9223372036854775808");
|
||||||
|
input.setAttribute("max", "9223372036854775807");
|
||||||
|
break;
|
||||||
|
case "uint8":
|
||||||
|
input.setAttribute("min", "0");
|
||||||
|
input.setAttribute("max", "255");
|
||||||
|
break;
|
||||||
|
case "uint16":
|
||||||
|
input.setAttribute("min", "0");
|
||||||
|
input.setAttribute("max", "65535");
|
||||||
|
break;
|
||||||
|
case "uint32":
|
||||||
|
input.setAttribute("min", "0");
|
||||||
|
input.setAttribute("max", "4294967295");
|
||||||
|
break;
|
||||||
|
case "uint": // Assuming x86-64 architecture
|
||||||
|
case "uint64":
|
||||||
|
input.setAttribute("min", "0");
|
||||||
|
input.setAttribute("max", "18446744073709551615");
|
||||||
|
break;
|
||||||
|
case "float32":
|
||||||
|
case "float64":
|
||||||
|
input.setAttribute("step", "any");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
formGroup.push(label);
|
||||||
|
formGroup.push(input);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Invalid field type: "+ field.kind, field.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSectionNode(section) {
|
||||||
|
var h2 = document.createElement("h2"),
|
||||||
|
contents = document.createTextNode(section);
|
||||||
|
h2.appendChild(contents);
|
||||||
|
return h2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDivNode(classes) {
|
||||||
|
var div = document.createElement("div");
|
||||||
|
div.setAttribute("class", classes);
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeLabelNode(forId, text) {
|
||||||
|
var label = document.createElement("label"),
|
||||||
|
contents = document.createTextNode(text);
|
||||||
|
label.setAttribute("for", forId);
|
||||||
|
label.appendChild(contents);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePayload(elems) {
|
||||||
|
var payload = {};
|
||||||
|
for (path in elems) {
|
||||||
|
var value = elems[path],
|
||||||
|
tokens = path.split('.'),
|
||||||
|
parents = tokens.slice(0, -1),
|
||||||
|
key = tokens.slice(-1)[0],
|
||||||
|
parent = payload;
|
||||||
|
|
||||||
|
for (var i = 0; i < parents.length; i++) {
|
||||||
|
var pkey = parents[i];
|
||||||
|
|
||||||
|
if (!parent[pkey]) {
|
||||||
|
parent[pkey] = {}
|
||||||
|
}
|
||||||
|
parent = parent[pkey];
|
||||||
|
}
|
||||||
|
|
||||||
|
parent[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("config").addEventListener("submit", function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var elems = {},
|
||||||
|
inputs = document.getElementsByTagName("input");
|
||||||
|
|
||||||
|
for (var i = 0; i < inputs.length; i++) {
|
||||||
|
var input = inputs[i],
|
||||||
|
type = input.getAttribute("data-type"),
|
||||||
|
path = input.getAttribute("id"),
|
||||||
|
value = input.value;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "string":
|
||||||
|
elems[path] = value;
|
||||||
|
break;
|
||||||
|
case "bool":
|
||||||
|
elems[path] = input.checked;
|
||||||
|
break;
|
||||||
|
case "int":
|
||||||
|
case "int8":
|
||||||
|
case "int16":
|
||||||
|
case "int32":
|
||||||
|
case "int64":
|
||||||
|
case "uint":
|
||||||
|
case "uint8":
|
||||||
|
case "uint16":
|
||||||
|
case "uint32":
|
||||||
|
case "uint64":
|
||||||
|
elems[path] = parseInt(value, 10);
|
||||||
|
break;
|
||||||
|
case "float32":
|
||||||
|
case "float64":
|
||||||
|
elems[path] = parseFloat(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveFields(makePayload(elems), function(resp){
|
||||||
|
var notice = document.getElementById("notice");
|
||||||
|
notice.innerHTML = resp.msg;
|
||||||
|
if (resp.success) {
|
||||||
|
notice.setAttribute("class", "success");
|
||||||
|
} else {
|
||||||
|
notice.setAttribute("class", "error");
|
||||||
|
}
|
||||||
|
notice.style.display = "block";
|
||||||
|
window.setTimeout(function() {
|
||||||
|
notice.style.display = "none";
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
loadFields(drawForm);
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Application Configuration</title>
|
||||||
|
<link rel="stylesheet" href="/app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>Secondly<h1>
|
||||||
|
<h3>Application Configuration</h3>
|
||||||
|
<form id="config">
|
||||||
|
<div id="fields"></div>
|
||||||
|
<button type="submit" class="btn btn-default">Save</button>
|
||||||
|
<div id="notice"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/app.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue