Add support for provisioning and rebuilding via interface

This commit is contained in:
Tom 2023-07-09 11:18:14 +02:00
parent f5c5999f44
commit ddb4bb3546
76 changed files with 1306 additions and 625 deletions

View file

@ -75,6 +75,7 @@ var (
menuInstances = component.MenuItem{Title: "Instances", Path: "/admin/instances/"}
menuInstance = component.DummyMenuItem()
menuRebuild = component.DummyMenuItem()
menuGrants = component.DummyMenuItem()
menuIngredients = component.DummyMenuItem()
)
@ -146,6 +147,11 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http
router.Handler(http.MethodGet, route+"instance/:slug", instance)
}
{
rebuild := admin.instanceRebuild(ctx)
router.Handler(http.MethodGet, route+"rebuild/:slug", rebuild)
}
{
grants := admin.grants(ctx)
router.Handler(http.MethodGet, route+"grants/:slug", grants)

View file

@ -139,7 +139,7 @@ func (gc *grantsContext) use(r *http.Request, slug string, admin *Admin) (funcs
// replace the functions
funcs = []templating.FlagFunc{
templating.ReplaceCrumb(menuInstance, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + slug)}),
templating.ReplaceCrumb(menuGrants, component.MenuItem{Title: "Grants", Path: template.URL("/admin/instance/" + slug + "/grants/")}),
templating.ReplaceCrumb(menuGrants, component.MenuItem{Title: "Grants", Path: template.URL("/admin/grants/" + slug)}),
templating.Title(gc.Instance.Slug + " - Grants"),
}
return funcs, nil

View file

@ -84,6 +84,48 @@
</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">
System
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
PHP Version
</td>
<td>
<code>{{ .Instance.System.PHP }}</code>
</td>
</tr>
<tr>
<td>
Docker Base Image
</td>
<td>
<code>{{ .Instance.System.GetDockerBaseImage }}</code>
</td>
</tr>
<tr>
<td>
OPCache Development Config
</td>
<td>
<code>{{ .Instance.System.OpCacheDevelopment }}</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-2-5">
<div class="padding">
<div class="overflow">
@ -182,7 +224,7 @@
<tr>
<td>
Last Rebuild <br>
<button class="remote-action pure-button pure-button-action" data-action="rebuild" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Rebuild</button>
<a class="pure-button" href="/admin/rebuild/{{ .Info.Slug }}">Rebuild</button>
</td>
<td>
<code class="date">{{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }}</code>

View file

@ -1,11 +1,38 @@
<div class="pure-u-1-1">
<form class="pure-form pure-form-aligned" id="provision">
<fieldset disabled="disabled">
<fieldset>
<legend>Main Parameters</legend>
<div class="pure-control-group">
<label for="slug">Slug</label>
<input name="slug" id="slug" placeholder="" autocomplete="slug">
</div>
<input type="submit" value="Provision" class="pure-button">
</fieldset>
<fieldset>
<legend>System Parameters</legend>
<div class="pure-control-group">
<label for="php">PHP Version</label>
<select id="php">
<option value="" selected>Default ({{ .DefaultPHPVersion }})</option>
{{ range .PHPVersions }}
<option value="{{ . }}">{{ . }}</option>
{{ end }}
</select>
</div>
<label for="opcacheDevelopment" class="pure-checkbox">
<input type="checkbox" id="opcacheDevelopment" />
Opache Development Configuration
</label>
</fieldset>
<fieldset>
<legend>Profile</legend>
<div class="pure-control-group">
Not yet available
</div>
</fieldset>
<input type="submit" value="Provision" class="pure-button">
</form>
</div>

View file

@ -0,0 +1,39 @@
<div class="pure-u-1-1">
<form class="pure-form pure-form-aligned" id="provision">
<fieldset>
<legend>Main Parameters</legend>
<div class="pure-control-group">
<label for="slug">Slug</label>
<input name="slug" id="slug" placeholder="" autocomplete="slug" readonly="readonly" value="{{ .Slug }}">
</div>
</fieldset>
<fieldset>
<legend>System Parameters</legend>
<div class="pure-control-group">
<label for="php">PHP Version</label>
<select id="php">
{{ $PHP := .System.PHP }}
<option {{ if eq $PHP "" }}selected{{ end }}>Default ({{ .DefaultPHPVersion }})</option>
{{ range .PHPVersions }}
<option {{ if eq $PHP . }}selected{{ end }} value="{{ . }}">{{ . }}</option>
{{ end }}
</select>
</div>
<label for="opcacheDevelopment" class="pure-checkbox">
<input {{ if .System.OpCacheDevelopment }}checked{{end}} type="checkbox" id="opcacheDevelopment" check/>
Opache Development Configuration
</label>
</fieldset>
<fieldset>
<legend>Profile</legend>
<div class="pure-control-group">
Not yet available
</div>
</fieldset>
<input type="submit" value="Rebuild" class="pure-button">
</form>
</div>

View file

@ -6,6 +6,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/models"
_ "embed"
)
@ -22,7 +23,18 @@ var instanceProvisionTemplate = templating.Parse[instanceProvisionContext](
type instanceProvisionContext struct {
templating.RuntimeFlags
// nothing for the moment
systemParams
}
type systemParams struct {
PHPVersions []string
DefaultPHPVersion string
}
func newSystemParams() (sp systemParams) {
sp.PHPVersions = models.KnownPHPVersions()
sp.DefaultPHPVersion = models.DefaultPHPVersion
return sp
}
func (admin *Admin) instanceProvision(ctx context.Context) http.Handler {
@ -37,6 +49,7 @@ func (admin *Admin) instanceProvision(ctx context.Context) http.Handler {
)
return tpl.HTMLHandler(func(r *http.Request) (ipc instanceProvisionContext, err error) {
ipc.systemParams = newSystemParams()
return ipc, nil
})
}

View file

@ -0,0 +1,74 @@
package admin
import (
"context"
"html/template"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/julienschmidt/httprouter"
"github.com/tkw1536/pkglib/httpx"
_ "embed"
)
//go:embed "html/instance_rebuild.html"
var instanceRebuildHTML []byte
var instanceRebuildTemplate = templating.Parse[instanceRebuildContext](
"instance_rebuild.html", instanceRebuildHTML, nil,
templating.Title("Rebuild Instance"),
templating.Assets(assets.AssetsAdminRebuild),
)
type instanceRebuildContext struct {
templating.RuntimeFlags
Slug string
System models.System
systemParams
}
func (admin *Admin) instanceRebuild(ctx context.Context) http.Handler {
tpl := instanceRebuildTemplate.Prepare(
admin.Dependencies.Templating,
templating.Crumbs(
menuAdmin,
menuInstances,
menuInstance,
menuRebuild,
),
)
return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ib instanceRebuildContext, funcs []templating.FlagFunc, err error) {
slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
var instance *wisski.WissKI
instance, err = admin.Dependencies.Instances.WissKI(r.Context(), slug)
if err == instances.ErrWissKINotFound {
return ib, nil, httpx.ErrNotFound
}
if err != nil {
return ib, nil, err
}
ib.Slug = instance.Slug
ib.System = instance.System
// replace the menu item
funcs = []templating.FlagFunc{
templating.ReplaceCrumb(menuInstance, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + instance.Slug)}),
templating.ReplaceCrumb(menuRebuild, component.MenuItem{Title: "Rebuild", Path: template.URL("/admin/rebuild/" + instance.Slug)}),
templating.Title(instance.Slug + " - Rebuild"),
}
ib.systemParams = newSystemParams()
return
})
}

View file

@ -8,6 +8,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/provision"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
)
@ -28,7 +29,7 @@ func (sockets *Sockets) Actions() ActionMap {
}),
"provision": sockets.Generic(1, func(ctx context.Context, sockets *Sockets, in io.Reader, out io.Writer, params ...string) error {
// read the flags of the instance to be provisioned
var flags provision.ProvisionFlags
var flags provision.Flags
if err := json.Unmarshal([]byte(params[0]), &flags); err != nil {
return err
}
@ -63,8 +64,13 @@ func (sockets *Sockets) Actions() ActionMap {
},
)
}),
"rebuild": sockets.Instance(0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error {
return instance.Barrel().Build(ctx, out, true)
"rebuild": sockets.Instance(1, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error {
// read the flags of the instance to be provisioned
var system models.System
if err := json.Unmarshal([]byte(params[0]), &system); err != nil {
return err
}
return instance.SystemManager().Apply(ctx, out, system, true)
}),
"update": sockets.Instance(0, func(ctx context.Context, _ *Sockets, instance *wisski.WissKI, in io.Reader, out io.Writer, params ...string) error {
return instance.Drush().Update(ctx, out)

View file

@ -21,4 +21,4 @@ type Assets struct {
Styles template.HTML // <link> tags inserted by the asset
}
//go:generate node build.mjs Default User Admin AdminProvision
//go:generate node build.mjs Default User Admin AdminProvision AdminRebuild

View file

@ -24,12 +24,18 @@ var AssetsUser = Assets{
// AssetsAdmin contains assets for the 'Admin' entrypoint.
var AssetsAdmin = Assets{
Scripts: `<script nomodule="" defer src="/⛰/User.924f7900.js"></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/Admin.205f0180.js"></script><script src="/⛰/Admin.59fb2e50.js" nomodule="" defer></script>`,
Scripts: `<script nomodule="" defer src="/⛰/User.924f7900.js"></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/Admin.ad1b495b.js"></script><script src="/⛰/Admin.6daf9fdd.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css">`,
}
// AssetsAdminProvision contains assets for the 'AdminProvision' entrypoint.
var AssetsAdminProvision = Assets{
Scripts: `<script nomodule="" defer src="/⛰/User.924f7900.js"></script><script nomodule="" defer src="/⛰/Admin.59fb2e50.js"></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script type="module" src="/⛰/Admin.205f0180.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/AdminProvision.3cf9e19e.js"></script><script src="/⛰/AdminProvision.d195fd59.js" nomodule="" defer></script>`,
Scripts: `<script nomodule="" defer src="/⛰/User.924f7900.js"></script><script nomodule="" defer src="/⛰/Admin.6daf9fdd.js"></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script type="module" src="/⛰/Admin.ad1b495b.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/AdminProvision.b7679968.js"></script><script src="/⛰/AdminProvision.12a47f22.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css"><link rel="stylesheet" href="/⛰/AdminProvision.38d394c2.css">`,
}
// AssetsAdminRebuild contains assets for the 'AdminRebuild' entrypoint.
var AssetsAdminRebuild = Assets{
Scripts: `<script nomodule="" defer src="/⛰/User.924f7900.js"></script><script nomodule="" defer src="/⛰/Admin.6daf9fdd.js"></script><script type="module" src="/⛰/User.47a3b7e3.js"></script><script type="module" src="/⛰/Admin.ad1b495b.js"></script><script type="module" src="/⛰/Default.38d394c2.js"></script><script src="/⛰/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/⛰/AdminRebuild.330247d9.js"></script><script src="/⛰/AdminRebuild.527a9616.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/⛰/Default.938b4407.css"><link rel="stylesheet" href="/⛰/Admin.a1e05c23.css"><link rel="stylesheet" href="/⛰/User.840de3b4.css"><link rel="stylesheet" href="/⛰/User.68febbf8.css"><link rel="stylesheet" href="/⛰/Admin.d79c7b30.css"><link rel="stylesheet" href="/⛰/AdminRebuild.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

@ -0,0 +1 @@
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},o={},t=e.parcelRequireafa4;null==t&&((t=function(e){if(e in n)return n[e].exports;if(e in o){var t=o[e];delete o[e];var r={id:e,exports:{}};return n[e]=r,t.call(r.exports,r,r.exports),r.exports}var l=new Error("Cannot find module '"+e+"'");throw l.code="MODULE_NOT_FOUND",l}).register=function(e,n){o[e]=n},e.parcelRequireafa4=t),t("dK5Bi");var r,l=t("8vh0V");async function i(e){return new Promise(((n,o)=>{(0,l.createModal)("provision",[JSON.stringify(e)],{bufferSize:0,onClose:(t,r)=>{t?n(e.Slug):o(new Error(null!=r?r:"unspecified error"))}})}))}const d=document.getElementById("provision"),a=document.getElementById("slug"),u=document.getElementById("php"),c=document.getElementById("opcacheDevelopment");d.addEventListener("submit",(e=>{e.preventDefault(),i({Slug:a.value,System:{PHP:u.value,OpCacheDevelopment:c.checked}}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),null===(r=d.querySelector("fieldset"))||void 0===r||r.removeAttribute("disabled")}();

View file

@ -1 +0,0 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},o={},t=e.parcelRequireafa4;null==t&&((t=function(e){if(e in n)return n[e].exports;if(e in o){var t=o[e];delete o[e];var r={id:e,exports:{}};return n[e]=r,t.call(r.exports,r,r.exports),r.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,n){o[e]=n},e.parcelRequireafa4=t),t("8xGhL");var r=t("12vpF");const i=document.getElementById("provision"),l=document.getElementById("slug");i.addEventListener("submit",(e=>{e.preventDefault();const n={Slug:l.value};(0,r.createModal)("provision",[JSON.stringify(n)],{bufferSize:0,onClose:e=>{e?location.href="/admin/instance/"+n.Slug:location.reload()}})})),i.querySelector("fieldset")?.removeAttribute("disabled");

View file

@ -0,0 +1 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},t={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in t){var o=t[e];delete t[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,n){t[e]=n},e.parcelRequireafa4=o),o("8xGhL");var r=o("12vpF");async function i(e){return new Promise(((n,t)=>{(0,r.createModal)("provision",[JSON.stringify(e)],{bufferSize:0,onClose:(o,r)=>{o?n(e.Slug):t(new Error(r??"unspecified error"))}})}))}const l=document.getElementById("provision"),a=document.getElementById("slug"),d=document.getElementById("php"),u=document.getElementById("opcacheDevelopment");l.addEventListener("submit",(e=>{e.preventDefault(),i({Slug:a.value,System:{PHP:d.value,OpCacheDevelopment:u.checked}}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),l.querySelector("fieldset")?.removeAttribute("disabled");

View file

@ -1 +0,0 @@
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},o={},t=e.parcelRequireafa4;null==t&&((t=function(e){if(e in n)return n[e].exports;if(e in o){var t=o[e];delete o[e];var i={id:e,exports:{}};return n[e]=i,t.call(i.exports,i,i.exports),i.exports}var r=new Error("Cannot find module '"+e+"'");throw r.code="MODULE_NOT_FOUND",r}).register=function(e,n){o[e]=n},e.parcelRequireafa4=t),t("dK5Bi");var i,r=t("8vh0V");const l=document.getElementById("provision"),d=document.getElementById("slug");l.addEventListener("submit",(e=>{e.preventDefault();const n={Slug:d.value};(0,r.createModal)("provision",[JSON.stringify(n)],{bufferSize:0,onClose:e=>{e?location.href="/admin/instance/"+n.Slug:location.reload()}})})),null===(i=l.querySelector("fieldset"))||void 0===i||i.removeAttribute("disabled")}();

View file

@ -0,0 +1 @@
var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},t={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in t){var o=t[e];delete t[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}).register=function(e,n){t[e]=n},e.parcelRequireafa4=o),o("8xGhL");var r=o("12vpF");async function i(e,n){return new Promise(((t,o)=>{(0,r.createModal)("rebuild",[e,JSON.stringify(n)],{bufferSize:0,onClose:(n,r)=>{n?t(e):o(new Error(r??"unspecified error"))}})}))}const l=document.getElementById("slug"),d=document.getElementById("provision"),a=document.getElementById("php"),u=document.getElementById("opcacheDevelopment");d.addEventListener("submit",(e=>{e.preventDefault(),i(l.value,{PHP:a.value,OpCacheDevelopment:u.checked}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),d.querySelector("fieldset")?.removeAttribute("disabled");

View file

@ -0,0 +1 @@
!function(){var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},t={},o=e.parcelRequireafa4;null==o&&((o=function(e){if(e in n)return n[e].exports;if(e in t){var o=t[e];delete t[e];var r={id:e,exports:{}};return n[e]=r,o.call(r.exports,r,r.exports),r.exports}var l=new Error("Cannot find module '"+e+"'");throw l.code="MODULE_NOT_FOUND",l}).register=function(e,n){t[e]=n},e.parcelRequireafa4=o),o("dK5Bi");var r,l=o("8vh0V");async function i(e,n){return new Promise(((t,o)=>{(0,l.createModal)("rebuild",[e,JSON.stringify(n)],{bufferSize:0,onClose:(n,r)=>{n?t(e):o(new Error(null!=r?r:"unspecified error"))}})}))}const d=document.getElementById("slug"),a=document.getElementById("provision"),u=document.getElementById("php"),c=document.getElementById("opcacheDevelopment");a.addEventListener("submit",(e=>{e.preventDefault(),i(d.value,{PHP:u.value,OpCacheDevelopment:c.checked}).then((e=>{location.href="/admin/instance/"+e})).catch((e=>{console.error(e),location.reload()}))})),null===(r=a.querySelector("fieldset"))||void 0===r||r.removeAttribute("disabled")}();

View file

@ -1,30 +1,22 @@
import "../Admin/index.ts"
import "../Admin/index.css"
import { createModal } from "~/src/lib/remote"
import { Provision } from "~/src/lib/remote/api"
const provision = document.getElementById("provision") as HTMLFormElement;
const slug = document.getElementById("slug") as HTMLInputElement;
const php = document.getElementById("php") as HTMLSelectElement;
const opcacheDevelopment = document.getElementById("opcacheDevelopment") as HTMLInputElement;
// add an event handler to open the modal form!
provision.addEventListener('submit', (evt) => {
evt.preventDefault();
// flags used to create the server
const flags = { Slug: slug.value };
// open a modal to provision a new instance
createModal("provision", [JSON.stringify(flags)], {
bufferSize: 0,
onClose: (success: boolean) => {
if (success) {
location.href = "/admin/instance/" + flags.Slug
} else {
location.reload();
}
},
})
Provision({ Slug: slug.value, System: { PHP: php.value, OpCacheDevelopment: opcacheDevelopment.checked } })
.then(slug => {
location.href = "/admin/instance/" + slug;
})
.catch((e) => {console.error(e); location.reload()});
})
// enable the form!

View file

@ -0,0 +1,24 @@
import "../Admin/index.ts"
import "../Admin/index.css"
import { Rebuild } from "~/src/lib/remote/api"
const slug = document.getElementById("slug") as HTMLInputElement
const provision = document.getElementById("provision") as HTMLFormElement;
const php = document.getElementById("php") as HTMLSelectElement;
const opcacheDevelopment = document.getElementById("opcacheDevelopment") as HTMLInputElement;
// add an event handler to open the modal form!
provision.addEventListener('submit', (evt) => {
evt.preventDefault();
Rebuild(slug.value, { PHP: php.value, OpCacheDevelopment: opcacheDevelopment.checked })
.then(slug => {
location.href = "/admin/instance/" + slug;
})
.catch((e) => {console.error(e); location.reload()});
})
// enable the form!
provision.querySelector('fieldset')?.removeAttribute('disabled');

View file

@ -0,0 +1,50 @@
import { createModal } from "~/src/lib/remote"
/**
* Flags to provision a new system.
* Should mirror "provision".Flags.
*/
interface ProvisionFlags {
Slug: string
System: System
}
interface System {
PHP: string;
OpCacheDevelopment: boolean
}
/** Rebuild the specified instance */
export async function Rebuild(slug: string, system: System): Promise<string> {
return new Promise((rs, rj) => {
createModal("rebuild", [slug, JSON.stringify(system)], {
bufferSize: 0,
onClose: (success: boolean, message?: string) => {
if (!success) {
rj(new Error(message ?? "unspecified error"))
return;
}
rs(slug);
},
})
});
}
/** Provision provisions a new instance */
export async function Provision(flags: ProvisionFlags): Promise<string> {
// open a modal to provision a new instance
return new Promise((rs, rj) => {
createModal("provision", [JSON.stringify(flags)], {
bufferSize: 0,
onClose: (success: boolean, message?: string) => {
if (!success) {
rj(new Error(message ?? "unspecified error"))
return;
}
rs(flags.Slug);
},
})
});
}

View file

@ -105,7 +105,7 @@ export default function setup() {
type ModalOptions = {
bufferSize: number;
onClose: (success: boolean) => void
onClose: (success: boolean, message?: string) => void
}
export function createModal(action: string, params: string[], opts: Partial<ModalOptions>) {
// create a modal dialog and append it to the body
@ -123,14 +123,14 @@ export function createModal(action: string, params: string[], opts: Partial<Moda
finishButton.className = "pure-button pure-button-success"
finishButton.append(typeof opts?.onClose === 'function' ? "Close & Finish" : "Close")
let result = {success: false, error: "unknown error"};
let result: ResultMessage = {success: false};
finishButton.addEventListener('click', (event) => {
event.preventDefault();
if (typeof opts?.onClose === 'function') {
finishButton.setAttribute('disabled', 'disabled')
target.innerHTML = 'Finishing up ...'
opts.onClose(result.success)
opts.onClose(result.success, result.message)
return;
}
@ -147,7 +147,9 @@ export function createModal(action: string, params: string[], opts: Partial<Moda
window.onbeforeunload = () => "A remote session is in progress. Are you sure you want to leave?";
// when closing, add a button to the modal!
const close = (result: ResultMessage) => {
const close = (message: ResultMessage) => {
result = message
if (result.success) {
println('Process completed successfully. ', true);
} else {
@ -168,6 +170,7 @@ export function createModal(action: string, params: string[], opts: Partial<Moda
// connect to the socket and send the action
callServerAction(
location.href.replace('http', 'ws'),
{
'name': action,
'params': params,

View file

@ -11,12 +11,14 @@ function isResultMessage(value: any): value is ResultMessage {
/**
* Opens a WebSocket connection and calls a server action
* @param endpoint Endpoint to call
* @param call Function to call
* @param onOpen callback for once the connection is opened. The send function can be used to send additional text to the server.
* @param onText called when the connection receives some text
* @returns a promise that is resolved once the conneciton is closed. Rejected if the connection errors.
*/
export default async function callServerAction(
endpoint: string,
call: CallMessage,
onOpen: (send: (text: string) => void, cancel: () => void) => void,
onText: (text: string) => void,
@ -24,7 +26,7 @@ export default async function callServerAction(
return new Promise((rs, rj) => {
const mutex = new Mutex();
const socket = new WebSocket(location.href.replace('http', 'ws'));
const socket = new WebSocket(endpoint);
let result: ResultMessage;
socket.onmessage = (msg) => {