frontend: Make Control Server nicer

This commit is contained in:
Tom Wiesing 2022-10-16 15:27:35 +02:00
parent c258b46443
commit 36891d7e7c
No known key found for this signature in database
16 changed files with 231 additions and 170 deletions

View file

@ -6,7 +6,6 @@ import (
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/core"
"github.com/tkw1536/goprogram/exit"
"github.com/tkw1536/goprogram/status"
"github.com/tkw1536/goprogram/stream"
)
@ -32,11 +31,6 @@ func (cron) Description() wisski_distillery.Description {
}
}
var errCronFailed = exit.Error{
Message: "Failed to run cron script for instance %q: exited with code %s",
ExitCode: exit.ExitGeneric,
}
func (cr cron) Run(context wisski_distillery.Context) error {
// find all the instances!
wissKIs, err := context.Environment.Instances().Load(cr.Positionals.Slug...)
@ -46,17 +40,7 @@ func (cr cron) Run(context wisski_distillery.Context) error {
// and do the actual blind_update!
return status.StreamGroup(context.IOStream, cr.Parallel, func(instance instances.WissKI, io stream.IOStream) error {
code, err := instance.Shell(io, "/runtime/cron.sh")
if err != nil {
io.EPrintln(err)
}
if code != 0 {
// keep going, because we want to run as many crons as possible
err = errBlindUpdateFailed.WithMessageF(instance.Slug, code)
io.EPrintln(err)
}
return nil
return instance.Cron(io)
}, wissKIs, status.SmartMessage(func(item instances.WissKI) string {
return fmt.Sprintf("cron %q", item.Slug)
}))

View file

@ -61,6 +61,7 @@ func (i info) Run(context wisski_distillery.Context) error {
context.Printf("Locked: %v\n", info.Locked)
context.Printf("Last Rebuild: %v\n", info.LastRebuild.String())
context.Printf("Last Update: %v\n", info.LastUpdate.String())
context.Printf("Last Cron: %v\n", info.LastCron.String())
context.Printf("Skip Prefixes: %v\n", info.NoPrefixes)
context.Printf("Prefixes: (count %d)\n", len(info.Prefixes))

View file

@ -19,9 +19,9 @@
<main>
<div class="pure-g">
<div class="pure-u-1-1">
<h2 id="overview">Instance Overview</h2>
<h2 id="overview">Info &amp; Status</h2>
</div>
<div class="pure-u-1 pure-u-xl-1-3">
<div class="pure-u-1 pure-u-xl-2-5">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
@ -70,14 +70,72 @@
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-1-3">
<div class="pure-u-1 pure-u-xl-2-5">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Build
Component Settings
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Directory
</td>
<td>
<code style="overflow: auto;">{{ .Instance.FilesystemBase }}</code>
</td>
</tr>
<tr>
<td>
SQL DB
</td>
<td>
<code>{{ .Instance.SqlDatabase }}</code>
</td>
</tr>
<tr>
<td>
SQL User
</td>
<td>
<code>{{ .Instance.SqlUsername }}</code>
</td>
</tr>
<tr>
<td>
TS Repo
</td>
<td>
<code>{{ .Instance.GraphDBRepository }}</code>
</td>
</tr>
<tr>
<td>
TS User
</td>
<td>
<code>{{ .Instance.GraphDBUsername }}</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-2-5">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Build Status
</th>
</tr>
</thead>
@ -85,7 +143,7 @@
<tr>
<td>
Created
</td>
</td>
<td>
<code class="date">{{ .Instance.Created.Format "2006-01-02T15:04:05Z07:00" }}</code>
</td>
@ -101,10 +159,11 @@
</tr>
<tr>
<td>
Automatic Updates
Last Cron<br>
<button class="remote-action pure-button pure-button-action" data-action="cron" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload="true">Cron</button>
</td>
<td>
<code>{{ .Instance.AutoBlindUpdateEnabled }}</code>
<code class="date">{{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }}</code>
</td>
</tr>
<tr>
@ -113,7 +172,8 @@
<button class="remote-action pure-button pure-button-action" data-action="update" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload="true">Update</button>
</td>
<td>
<code class="date">{{ .Info.LastUpdate.Format "2006-01-02T15:04:05Z07:00" }}</code>
<code class="date">{{ .Info.LastUpdate.Format "2006-01-02T15:04:05Z07:00" }}</code><br>
(Automatic: <code>{{ .Instance.AutoBlindUpdateEnabled }}</code>)
</td>
</tr>
</tbody>
@ -122,138 +182,89 @@
</div>
</div>
<div class="pure-u-1 pure-u-xl-1-3">
<div class="pure-u-1 pure-u-xl-2-5">
<!--
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Resolver
Composer Status
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Excluded
???
</td>
<td>
<code>{{ .Info.NoPrefixes }}</code>
</td>
</tr>
<tr>
<td>
URI Prefixes
</td>
<td>
{{ range .Info.Prefixes }}
<code>{{ . }}</code><br />
{{ end}}
???
</td>
</tr>
</tbody>
</table>
</div>
</div>
-->
</div>
<div class="pure-u-1 pure-u-xl-1-3">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Database Settings
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
SQL Database
</td>
<td>
<code>{{ .Instance.SqlDatabase }}</code>
</td>
</tr>
<tr>
<td>
SQL Username
</td>
<td>
<code>{{ .Instance.SqlUsername }}</code>
</td>
</tr>
<tr>
<td>
GraphDB Repository
</td>
<td>
<code>{{ .Instance.GraphDBRepository }}</code>
</td>
</tr>
<tr>
<td>
GraphDB Username
</td>
<td>
<code>{{ .Instance.GraphDBUsername }}</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-1-3">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Whitebox Data
</th>
</tr>
</thead>
<tbody>
<div class="pure-u-1-1">
<h2 id="wisski">WissKI Data</h2>
</div>
<div class="pure-u-1 pure-u-xl-1-2">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<td>
<th colspan="2">
Pathbuilders
</td>
<td>
<code class="pathbuilders">{{ .Info.Pathbuilders }}</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-1-3">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Misc Settings
</th>
</tr>
</thead>
<tbody>
{{ range $name, $xml := .Info.Pathbuilders }}
<tr>
<td>
Filesystem Base
<code>{{ $name }}</code>
</td>
<td>
<code>{{ .Instance.FilesystemBase }}</code>
<script>window.pathbuilders = {{ .Info.Pathbuilders }};</script>
<code class="pathbuilder" data-name="{{ $name }}">{{ $xml }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-1-2">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th>
URI Prefixes
{{ if .Info.NoPrefixes }}
(excluded from resolver)
{{ end }}
</th>
</tr>
</thead>
<tbody>
{{ range $index, $prefix := .Info.Prefixes }}
<tr>
<td>
<code>{{ $prefix }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>

View file

@ -28,6 +28,9 @@ var socketInstanceActions = map[string]instanceActionFunc{
"update": func(_ *Info, instance instances.WissKI, str stream.IOStream) error {
return instance.BlindUpdate(str)
},
"cron": func(_ *Info, instance instances.WissKI, str stream.IOStream) error {
return instance.Cron(str)
},
}
func (info *Info) serveSocket(conn httpx.WebSocketConnection) {

View file

@ -0,0 +1,36 @@
package instances
import (
"time"
"github.com/tkw1536/goprogram/exit"
"github.com/tkw1536/goprogram/stream"
)
var errCronFailed = exit.Error{
Message: "Failed to run cron script for instance %q: exited with code %s",
ExitCode: exit.ExitGeneric,
}
func (wisski *WissKI) Cron(io stream.IOStream) error {
code, err := wisski.Shell(io, "/runtime/cron.sh")
if err != nil {
io.EPrintln(err)
}
if code != 0 {
// keep going, because we want to run as many crons as possible
err = errBlindUpdateFailed.WithMessageF(wisski.Slug, code)
io.EPrintln(err)
}
return nil
}
func (wisski *WissKI) LastCron(server *PHPServer) (t time.Time, err error) {
var timestamp int64
err = wisski.EvalPHPCode(server, &timestamp, `$val = \Drupal::state()->get('system.cron_last'); return $val; `)
if err != nil {
return
}
return time.Unix(timestamp, 0), nil
}

View file

@ -1,6 +1,7 @@
package instances
import (
"log"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/models"
@ -22,6 +23,7 @@ type WissKIInfo struct {
Running bool
LastRebuild time.Time
LastUpdate time.Time
LastCron time.Time
// List of backups made
Snapshots []models.Export
@ -40,9 +42,9 @@ func (wisski *WissKI) Info(quick bool) (info WissKIInfo, err error) {
if !quick {
server, err := wisski.NewPHPServer()
if err == nil {
wisski.infoSlow(&info, server, &group)
defer server.Close()
}
wisski.infoSlow(&info, server, &group)
}
err = group.Wait()
@ -87,19 +89,28 @@ func (wisski *WissKI) infoQuick(info *WissKIInfo, group *errgroup.Group) {
func (wisski *WissKI) infoSlow(info *WissKIInfo, server *PHPServer, group *errgroup.Group) {
group.Go(func() (err error) {
info.Prefixes, _ = wisski.Prefixes(server)
info.Prefixes, err = wisski.Prefixes(server)
log.Println("error prefixes: ", err)
return nil
})
group.Go(func() error {
info.Snapshots, _ = wisski.Snapshots()
group.Go(func() (err error) {
info.Snapshots, err = wisski.Snapshots()
log.Println("error snapshots: ", err)
return nil
})
group.Go(func() error {
info.Pathbuilders, _ = wisski.AllPathbuilders(server)
group.Go(func() (err error) {
info.Pathbuilders, err = wisski.AllPathbuilders(server)
log.Println("error pathbuilders: ", err)
return nil
})
group.Go(func() (err error) {
info.LastCron, err = wisski.LastCron(server)
log.Println("error cron: ", err)
return
})
}
// Running checks if this WissKI is currently running.

View file

@ -174,9 +174,13 @@ func (server *PHPServer) MarshalCall(value any, function string, args ...any) er
if err != nil {
return PHPServerError{Message: errPHPMarshal, Err: err}
}
userFunctionArgs, err := marshalPHP(args)
if err != nil {
return PHPServerError{Message: errPHPMarshal, Err: err}
userFunctionArgs := "[]"
if len(args) > 0 {
userFunctionArgs, err = marshalPHP(args)
if err != nil {
return PHPServerError{Message: errPHPMarshal, Err: err}
}
}
code := "return call_user_func_array(" + userFunction + "," + userFunctionArgs + ");"
@ -246,6 +250,7 @@ func (server *PHPServer) Close() error {
// ExecPHPScript executes the PHP code as a script on the given server.
// When server is nil, creates a new server and automatically closes it after execution.
// Calling this function repeatedly with server = nil is inefficient.
//
// The script should define a function called entrypoint, and may define additional functions.
//
@ -257,8 +262,6 @@ func (server *PHPServer) Close() error {
// It's arguments are encoded as json using [json.Marshal] and decoded within php.
//
// The return value of the function is again marshaled with json and returned to the caller.
//
// Calling this function is inefficient, and a [NewPHPServer] call should be prefered instead.
func (wisski *WissKI) ExecPHPScript(server *PHPServer, value any, code string, entrypoint string, args ...any) (err error) {
if server == nil {
server, err = wisski.NewPHPServer()
@ -268,13 +271,27 @@ func (wisski *WissKI) ExecPHPScript(server *PHPServer, value any, code string, e
defer server.Close()
}
if err := server.MarshalEval(nil, strings.TrimPrefix(code, "<?php")); err != nil {
return err
if code != "" {
if err := server.MarshalEval(nil, strings.TrimPrefix(code, "<?php")); err != nil {
return err
}
}
return server.MarshalCall(value, entrypoint, args...)
}
func (wisski *WissKI) EvalPHPCode(server *PHPServer, value any, code string) (err error) {
if server == nil {
server, err = wisski.NewPHPServer()
if err != nil {
return
}
defer server.Close()
}
return server.MarshalEval(value, code)
}
//go:embed php/server.php
var serverPHP string

View file

@ -1,6 +1,7 @@
package static
import (
"encoding/json"
"html/template"
)
@ -30,11 +31,17 @@ func (assets *Assets) MustParse(value string) *template.Template {
return template.Must(assets.RegisterFuncs(template.New("")).Parse(value))
}
// RegisterFuncs registers two new template functions called "JS" and "CSS".
// Both take no arguments, and return a html-safe version of the Scripts and Style tags to be included.
// RegisterFuncs registers three new template functions called "JS", "CSS" and "json".
//
// "JS" and "CSS" take no arguments, and return appropriate tags to be inserted into html.
// json takes a single argument of any type, and returns it's encoding as a string to be inserted into the page.
func (assets *Assets) RegisterFuncs(t *template.Template) *template.Template {
return t.Funcs(template.FuncMap{
"JS": func() template.HTML { return template.HTML(assets.Scripts) },
"CSS": func() template.HTML { return template.HTML(assets.Styles) },
"json": func(data any) (string, error) {
bytes, err := json.Marshal(data)
return string(bytes), err
},
})
}

View file

@ -5,17 +5,17 @@ package static
// AssetsHomeHome contains assets for the 'HomeHome' entrypoint.
var AssetsHomeHome = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.518b2dbe.css"><link rel="stylesheet" href="/static/HomeHome.38d394c2.css">`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/HomeHome.38d394c2.css">`,
}
// AssetsControlIndex contains assets for the 'ControlIndex' entrypoint.
var AssetsControlIndex = Assets{
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlIndex.43f953d2.js"></script><script src="/static/ControlIndex.c70a89e1.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.518b2dbe.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css">`,
Scripts: `<script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlIndex.cfbf936d.js"></script><script src="/static/ControlIndex.613b02c2.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css">`,
}
// AssetsControlInstance contains assets for the 'ControlInstance' entrypoint.
var AssetsControlInstance = Assets{
Scripts: `<script nomodule="" defer src="/static/ControlIndex.c70a89e1.js"></script><script type="module" src="/static/ControlIndex.43f953d2.js"></script><script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlInstance.66b95713.js"></script><script src="/static/ControlInstance.9cc7166d.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.518b2dbe.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css"><link rel="stylesheet" href="/static/ControlInstance.38d394c2.css">`,
Scripts: `<script nomodule="" defer src="/static/ControlIndex.613b02c2.js"></script><script type="module" src="/static/ControlIndex.cfbf936d.js"></script><script type="module" src="/static/HomeHome.38d394c2.js"></script><script src="/static/HomeHome.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/ControlInstance.66b95713.js"></script><script src="/static/ControlInstance.9cc7166d.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/HomeHome.a75f04fa.css"><link rel="stylesheet" href="/static/ControlIndex.6d59e220.css"><link rel="stylesheet" href="/static/ControlIndex.6d2ae968.css"><link rel="stylesheet" href="/static/ControlInstance.38d394c2.css">`,
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -20,6 +20,11 @@ footer {
width: 100%;
}
.overflow table td,
.overflow table th{
padding: .5em .5em;
}
.overflow table td:not(:last-child),
.overflow table th:not(:last-child) {
width: 1px;
@ -34,7 +39,7 @@ footer {
.hspace {
display: block;
height: 2em;
height: 1em;
}
.pure-button-action {

View file

@ -7,32 +7,19 @@ const types: Record<string, (element: HTMLElement) => HTMLElement | string> = {
const text = element.innerText.split("/");
return text[text.length - 1];
},
"pathbuilders": () => {
const pathbuilders: {[name: string]: string} = (window as any).pathbuilders; // must be declared globally on page!
const wrapper = document.createElement("span");
"pathbuilder": (element) => {
// create a link and get the blob
const filename = (element.getAttribute('data-name') ?? 'pathbuilder') + ".xml"
const [link, blob] = make_download_link(filename, element.innerText, "application/xml")
let found_one = false
Object.keys(pathbuilders).forEach(name => {
found_one = true
const filename = name + ".xml"
const data = pathbuilders[name]
const mime = "application/xml"
wrapper.append(make_download_link(filename, name, data, mime))
wrapper.append(document.createTextNode(" "))
})
if (!found_one) return '(none)';
const small = document.createElement('small')
small.append(document.createTextNode("(click to download)"))
wrapper.append(small)
return wrapper
link.className = "pure-button"
const title = filename + ' (' + blob.size + ' Bytes)';
link.append(title)
return link
}
}
const make_download_link = (filename: string, title: string, content: string, type: string) => {
const make_download_link = (filename: string, content: string, type: string): [HTMLAnchorElement, Blob] => {
const blob = new Blob(
[content],
{
@ -44,9 +31,8 @@ const make_download_link = (filename: string, title: string, content: string, ty
link.target = "_blank"
link.download = filename
link.href = URL.createObjectURL(blob)
link.append(document.createTextNode(title))
return link
return [link, blob]
}
Object.keys(types).forEach(key => {