admin: Add purge interface

This commit adds a new option to the admin interface to purge an
instance. This requires the user to manually confirm the name of the
instance.
This commit is contained in:
Tom Wiesing 2023-01-16 18:22:13 +01:00
parent 3321b5d0ba
commit 2384ee0841
No known key found for this signature in database
16 changed files with 175 additions and 84 deletions

View file

@ -10,6 +10,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/purger"
"github.com/julienschmidt/httprouter"
"github.com/rs/zerolog"
@ -32,6 +33,8 @@ type Admin struct {
Policy *policy.Policy
Custom *custom.Custom
Purger *purger.Purger
}
Analytics *lazy.PoolAnalytics

View file

@ -40,10 +40,10 @@
<td>
<code>{{ .Info.Running }}</code>
<div class="pure-button-group" role="group">
<button class="remote-action pure-button pure-button-action" data-action="start" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload="true">
<button class="remote-action pure-button pure-button-action" data-action="start" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>
(Re)Start
</button>
<button class="remote-action pure-button pure-button-danger" data-action="stop" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload="true">
<button class="remote-action pure-button pure-button-danger" data-action="stop" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>
Stop
</button>
</div>
@ -144,7 +144,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="true">Rebuild</button>
<button class="remote-action pure-button pure-button-action" data-action="rebuild" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Rebuild</button>
</td>
<td>
<code class="date">{{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }}</code>
@ -153,7 +153,7 @@
<tr>
<td>
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>
<button class="remote-action pure-button pure-button-action" data-action="cron" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Cron</button>
</td>
<td>
<code class="date">{{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }}</code>
@ -162,7 +162,7 @@
<tr>
<td>
Last Update <br>
<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>
<button class="remote-action pure-button pure-button-action" data-action="update" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Update</button>
</td>
<td>
<code class="date">{{ .Info.LastUpdate.Format "2006-01-02T15:04:05Z07:00" }}</code><br>
@ -437,7 +437,7 @@
<div class="pure-u-1-1">
<h2 id="snapshots">Snapshots</h2>
<p>
<button class="remote-action pure-button pure-button-action" data-action="snapshot" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload="true">Take a snapshot</button>
<button class="remote-action pure-button pure-button-action" data-action="snapshot" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Take a snapshot</button>
</p>
</div>
@ -467,4 +467,22 @@
</tbody>
</table>
</div>
<div class="pure-u-1-1">
<h2 id="overview">Dangerous Actions</h2>
</div>
<div class="pure-u-1 pure-u-xl-2-5">
<p>
Purging this instance completely removes it from the distillery.
Backups containing the instance will remain, but it will not be possible to restore it directly.
You must enter the slug <code>{{ .Instance.Slug }}</code> to confirm purging.
</p>
<form class="pure-form">
<fieldset>
<input type="text" id="purge-confirm-slug" placeholder="{{ .Instance.Slug }}" />
<button class="remote-action pure-button pure-button-danger" data-action="purge" data-param="{{ .Instance.Slug }}" data-confirm-param="#purge-confirm-slug" data-buffer="1000" data-force-reload="/admin/">Purge Instance</button>
</fieldset>
</form>
</div>
{{ end }}

View file

@ -22,8 +22,8 @@ type InstanceAction struct {
var socketInstanceActions = map[string]InstanceAction{
"snapshot": {
HandleInteractive: func(ctx context.Context, info *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
return info.Dependencies.Exporter.MakeExport(
HandleInteractive: func(ctx context.Context, admin *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
return admin.Dependencies.Exporter.MakeExport(
ctx,
out,
exporter.ExportTask{
@ -60,6 +60,11 @@ var socketInstanceActions = map[string]InstanceAction{
return instance.Barrel().Stack().Down(ctx, out)
},
},
"purge": {
HandleInteractive: func(ctx context.Context, admin *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
return admin.Dependencies.Purger.Purge(ctx, out, instance.Slug)
},
},
}
func (admin *Admin) serveSocket(conn httpx.WebSocketConnection) {

View file

@ -0,0 +1,6 @@
---
title: Removing instances from admin interface
date: 2023-01-16
---
- added an option to purge and remove instances from the admin page

View file

@ -21,6 +21,6 @@ var AssetsUser = Assets{
// AssetsAdmin contains assets for the 'Admin' entrypoint.
var AssetsAdmin = Assets{
Scripts: `<script nomodule="" defer src="/static/User.b2f9a57c.js"></script><script type="module" src="/static/User.e0367d79.js"></script><script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Admin.4ca3cb6f.js"></script><script src="/static/Admin.9750ba9c.js" nomodule="" defer></script>`,
Scripts: `<script nomodule="" defer src="/static/User.b2f9a57c.js"></script><script type="module" src="/static/User.e0367d79.js"></script><script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Admin.1a380f6f.js"></script><script src="/static/Admin.cb58d290.js" nomodule="" defer></script>`,
Styles: `<link rel="stylesheet" href="/static/Default.db26a303.css"><link rel="stylesheet" href="/static/Admin.6d59e220.css"><link rel="stylesheet" href="/static/User.840de3b4.css"><link rel="stylesheet" href="/static/User.68febbf8.css"><link rel="stylesheet" href="/static/Admin.6d2ae968.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

@ -49,16 +49,40 @@ function makeTextBuffer(target: HTMLElement, scrollContainer: HTMLElement, size:
const remote_action = document.getElementsByClassName('remote-action')
Array.from(remote_action).forEach((element) => {
const action = element.getAttribute('data-action') as string;
const reload = element.hasAttribute('data-force-reload');
const reload = element.getAttribute('data-force-reload');
const param = element.getAttribute('data-param') as string | undefined;
const confirmElementName = element.getAttribute('data-confirm-param');
const confirmElement = (confirmElementName ? document.querySelector(confirmElementName) : null) as HTMLInputElement | null;
const bufferSize = (function () {
const number = parseInt(element.getAttribute('data-buffer') ?? "", 10) ?? 0;
return (isFinite(number) && number > 0) ? number : 0;
})()
const validate = function() {
if (!confirmElement) return true
return confirmElement.value === param;
}
if (confirmElement) {
const runValidation = () => {
if (validate()) {
element.removeAttribute('disabled')
} else {
element.setAttribute('disabled', 'disabled')
}
}
confirmElement.addEventListener('change', runValidation)
runValidation()
}
element.addEventListener('click', function (ev) {
ev.preventDefault();
// do nothing if the validation fails
if (!validate()) return;
// create a modal dialog and append it to the body
const modal = document.createElement("div")
modal.className = "modal-terminal"
@ -73,14 +97,18 @@ Array.from(remote_action).forEach((element) => {
// create a button to eventually close everything
const button = document.createElement("button")
button.className = "pure-button pure-button-success"
button.append(reload ? "Close & Reload" : "Close")
button.append(typeof reload === 'string' ? "Close & Reload" : "Close")
button.addEventListener('click', function (event) {
event.preventDefault();
if (reload) {
button.setAttribute('disabled', 'disabled');
if (typeof reload === 'string') {
button.setAttribute('disabled', 'disabled')
target.innerHTML = 'Reloading page ...'
location.reload()
if (reload === '') {
location.reload()
} else {
location.href = reload
}
return;
}