Add Status Report to Info page
This commit is contained in:
parent
cda68d3454
commit
f5c5999f44
11 changed files with 361 additions and 20 deletions
|
|
@ -114,5 +114,10 @@ func (i info) Run(context wisski_distillery.Context) (err error) {
|
|||
context.Printf("- %v\n", grant)
|
||||
}
|
||||
|
||||
context.Printf("Requirements: (count %d)\n", len(info.Requirements))
|
||||
for _, req := range info.Requirements {
|
||||
context.Printf("- %v\n", req)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +1,20 @@
|
|||
<div class="pure-u-1-1">
|
||||
<p>
|
||||
Sections
|
||||
</p>
|
||||
|
||||
<div class="pure-button-group" role="group" aria-label="Table Of Contents">
|
||||
<a class="pure-button pure-button-small" href="#overview">Info & Status</a>
|
||||
<a class="pure-button pure-button-small" href="#requirements">Drupal Status Report</a>
|
||||
<a class="pure-button pure-button-small" href="#users">Users (Drupal)</a>
|
||||
<a class="pure-button pure-button-small" href="#wisski">WissKI Data</a>
|
||||
<a class="pure-button pure-button-small" href="#stats">Statistics</a>
|
||||
<a class="pure-button pure-button-small" href="#ssh">SSH Keys</a>
|
||||
<a class="pure-button pure-button-small" href="#snapshots">Snapshots</a>
|
||||
<a class="pure-button pure-button-small" href="#danger">Dangerous Actions</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="overview">Info & Status</h2>
|
||||
</div>
|
||||
|
|
@ -196,9 +213,62 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="requirements">Drupal Status Report</h2>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1">
|
||||
<div class="padding">
|
||||
<div class="overflow">
|
||||
<table class="pure-table pure-table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
ID
|
||||
</th>
|
||||
<th>
|
||||
Severity
|
||||
</th>
|
||||
<th>
|
||||
Title
|
||||
</th>
|
||||
<th>
|
||||
Value
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{ range $name, $req := .Info.Requirements }}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{{ $req.ID }}</code>
|
||||
</td>
|
||||
<td>
|
||||
{{ $req.Level }}
|
||||
</td>
|
||||
<td>
|
||||
{{ $req.Title }}
|
||||
</td>
|
||||
<td>
|
||||
{{ $req.Value }}
|
||||
</td>
|
||||
<td>
|
||||
{{ $req.Description }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="wisski">Users (Drupal)</h2>
|
||||
<h2 id="users">Users (Drupal)</h2>
|
||||
<a class="pure-button" href="/admin/grants/{{ .Info.Slug }}">Manage Grants</a>
|
||||
</div>
|
||||
|
||||
|
|
@ -239,7 +309,7 @@
|
|||
{{ $slug := .Instance.Slug }}
|
||||
{{ $csrf := .CSRF }}
|
||||
{{ range $index, $user := .Info.Users }}
|
||||
<tr {{ if not $user.Status }}style="color:gray"{{ end }}>
|
||||
<tr {{ if not $user.Status }}style="color:gray" aria-disabled="true"{{ end }}>
|
||||
<td>
|
||||
<code>{{ $user.UID }}</code>
|
||||
</td>
|
||||
|
|
@ -490,7 +560,7 @@
|
|||
</div>
|
||||
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="overview">Dangerous Actions</h2>
|
||||
<h2 id="danger">Dangerous Actions</h2>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 pure-u-xl-2-5">
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ 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>`,
|
||||
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.6d2ae968.css">`,
|
||||
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>`,
|
||||
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.6d2ae968.css"><link rel="stylesheet" href="/⛰/AdminProvision.38d394c2.css">`,
|
||||
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">`,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
.wisski{padding:1em}.wisski h3{padding:0}.wisski a.pure-button{float:right;position:relative;bottom:1em}.wisski.running{background-color:#9ada07}.wisski.stopped{background-color:#ff7a7a}
|
||||
1
internal/dis/component/server/assets/dist/Admin.d79c7b30.css
vendored
Normal file
1
internal/dis/component/server/assets/dist/Admin.d79c7b30.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.wisski{padding:1em}.wisski h3{padding:0}.wisski a.pure-button{float:right;position:relative;bottom:1em}.wisski.running{background-color:#9ada07}.wisski.stopped{background-color:#ff7a7a}.info-chip{min-width:75px;height:15px;text-align:center;border-radius:5px;padding:5px;display:block}
|
||||
|
|
@ -18,4 +18,13 @@
|
|||
|
||||
.wisski.stopped {
|
||||
background-color: #ff7a7a;
|
||||
}
|
||||
|
||||
.info-chip {
|
||||
min-width: 75px;
|
||||
height: 15px;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package status
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/models"
|
||||
|
|
@ -15,6 +17,10 @@ type WissKI struct {
|
|||
Slug string // slug
|
||||
URL string // complete URL, including http(s)
|
||||
|
||||
// golang html requirements.
|
||||
// Note that the html in templates may contain dirty html.
|
||||
Requirements []Requirement
|
||||
|
||||
Locked bool // Is this instance currently locked?
|
||||
|
||||
// Information about the running instance
|
||||
|
|
@ -40,6 +46,30 @@ type WissKI struct {
|
|||
Grants []models.Grant
|
||||
}
|
||||
|
||||
// Requirement represents a drupal requirement or status check.
|
||||
type Requirement struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Weight int `json:"weight"`
|
||||
Severity int `json:"severity"`
|
||||
Value template.HTML `json:"value"`
|
||||
Description template.HTML `json:"description"`
|
||||
}
|
||||
|
||||
func (req Requirement) Level() template.HTML {
|
||||
switch req.Severity {
|
||||
case -1:
|
||||
return "<span style='background-color: gray; color: #fff;' class='info-chip'>Note</span>"
|
||||
case 0:
|
||||
return "<span style='background-color: green; color: #fff;' class='info-chip'>Info</span>"
|
||||
case 1:
|
||||
return "<span style='background-color: yellow; color: #000;' class='info-chip'>Warning</span>"
|
||||
case 2:
|
||||
return "<span style='background-color: red; color: #000;' class='info-chip'>Error</span>"
|
||||
}
|
||||
return template.HTML(strconv.Itoa(req.Severity))
|
||||
}
|
||||
|
||||
// Statistics holds statistics generated by the WissKI module
|
||||
type Statistics struct {
|
||||
Activity struct {
|
||||
|
|
|
|||
66
internal/wisski/ingredient/php/extras/requirements.go
Normal file
66
internal/wisski/ingredient/php/extras/requirements.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package extras
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/phpx"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/status"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
type Requirements struct {
|
||||
ingredient.Base
|
||||
Dependencies struct {
|
||||
PHP *php.PHP
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ ingredient.WissKIFetcher = (*Requirements)(nil)
|
||||
)
|
||||
|
||||
//go:embed requirements.php
|
||||
var requirementsPHP string
|
||||
|
||||
// Create creates a new block with the given title and html content
|
||||
func (requirements *Requirements) Get(ctx context.Context, server *phpx.Server) (data []status.Requirement, err error) {
|
||||
err = requirements.Dependencies.PHP.ExecScript(ctx, server, &data, requirementsPHP, "get_requirements", requirements.URL().String())
|
||||
if err == nil {
|
||||
// sort first by weight, then by id!
|
||||
slices.SortFunc(data, func(a, b status.Requirement) bool {
|
||||
// compare first by weight
|
||||
if a.Weight < b.Weight {
|
||||
return true
|
||||
}
|
||||
if a.Weight > b.Weight {
|
||||
return false
|
||||
}
|
||||
|
||||
// then by severity
|
||||
if a.Severity < b.Severity {
|
||||
return true
|
||||
}
|
||||
if a.Severity > b.Severity {
|
||||
return false
|
||||
}
|
||||
|
||||
// and finally by id
|
||||
return a.ID < b.ID
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch fetches information
|
||||
func (requirements *Requirements) Fetch(flags ingredient.FetcherFlags, target *status.WissKI) error {
|
||||
if flags.Quick {
|
||||
return nil
|
||||
}
|
||||
|
||||
target.Requirements, _ = requirements.Get(flags.Context, flags.Server)
|
||||
return nil
|
||||
}
|
||||
125
internal/wisski/ingredient/php/extras/requirements.php
Normal file
125
internal/wisski/ingredient/php/extras/requirements.php
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Returns a well-typed array of all requirements.
|
||||
* Relative URLs will be placed in the given domain.
|
||||
*/
|
||||
function get_requirements(string $public = ""): array {
|
||||
$results = [];
|
||||
|
||||
$managerService = \Drupal::service('system.manager');
|
||||
$rendererService = \Drupal::service('renderer');
|
||||
|
||||
$requirements = $managerService->listRequirements();
|
||||
foreach($requirements as $id => $req) {
|
||||
$title = $req['title'] ?? NULL;
|
||||
$weight = $req['weight'] ?? NULL;
|
||||
$severity = $req['severity'] ?? NULL;
|
||||
$value = $req['value'] ?? NULL;
|
||||
$description = $req['description'] ?? NULL;
|
||||
|
||||
$results[] = array(
|
||||
"id" => $id,
|
||||
"title" => strip_tags(ensure_html($rendererService, $title)),
|
||||
"weight" => is_numeric($weight) ? $weight : 0,
|
||||
"severity" => is_numeric($severity) ? $severity : 0,
|
||||
"value" => clean_html(ensure_html($rendererService, $value), $public),
|
||||
"description" => clean_html(ensure_html($rendererService, $description), $public),
|
||||
);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensures that the passed data is drupal html code.
|
||||
*
|
||||
* @param mixed $data
|
||||
* @return string
|
||||
*/
|
||||
function ensure_html(mixed $renderer, mixed $data): string {
|
||||
// already a string => return it!
|
||||
if (is_string($data)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// it is null => return the empty string
|
||||
if ($data === NULL) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// create a render array and render it!
|
||||
$rary = is_array($data) ? $data : ["#markup" => $data];
|
||||
return $renderer->renderPlain($rary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses source as an html fragment.
|
||||
* It then iterates over all '<a>' elements and performs the following modifications:
|
||||
* - Setup the distillery redirector (/next/?next=...) on relative links according to $public
|
||||
* - Make all links target="_blank" rel="noopener"
|
||||
*/
|
||||
function clean_html(string $source, string $public): string|bool {
|
||||
// fast path: we don't have a source
|
||||
if ($source === "" ) {
|
||||
return $source;
|
||||
}
|
||||
|
||||
// trim the trailing '/' from the public URL.
|
||||
// this technically trims the beginning as well, but that should not be a problem
|
||||
$public = trim($public, '/');
|
||||
|
||||
// attempt to parse a new document
|
||||
$doc = new DOMDocument();
|
||||
$ok = $doc->loadHTML('<?xml encoding="UTF-8">' . $source);
|
||||
if ($ok === FALSE) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// replace all the <a href="..."> which are relative
|
||||
$as = $doc->getElementsByTagName('a');
|
||||
$modified = $as->count() > 0;
|
||||
foreach($as as $a) {
|
||||
// setup a link to open in a new tab
|
||||
$a->setAttribute('rel', 'noopener noreferer');
|
||||
$a->setAttribute('target', '_blank');
|
||||
|
||||
// if we don't have a domain don't even bother replacing hrefs.
|
||||
if ($domain === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// take only href="/" relative to the current domain.
|
||||
$href = $a->getAttribute('href');
|
||||
if (!is_string($href) || !str_starts_with($href, '/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$a->setAttribute('href', $public . $href);
|
||||
}
|
||||
|
||||
// we didn't modify the document => return as is
|
||||
// (no need to re-serialize)
|
||||
if (!$modified) {
|
||||
return $source;
|
||||
}
|
||||
|
||||
// find the body
|
||||
$body = $doc->getElementsByTagName('body')->item(0);
|
||||
if ($body === NULL) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// re-build the source document
|
||||
$source = "";
|
||||
|
||||
// get the inner html§
|
||||
$nodes = $body->childNodes;
|
||||
foreach($nodes as $node) {
|
||||
$source .= $doc->saveHTML($node);
|
||||
}
|
||||
|
||||
// and turn it back into html
|
||||
return $source;
|
||||
}
|
||||
|
|
@ -110,6 +110,7 @@ func (wisski *WissKI) allIngredients() []initFunc {
|
|||
auto[*extras.Pathbuilder],
|
||||
auto[*extras.Stats],
|
||||
auto[*extras.Blocks],
|
||||
auto[*extras.Requirements],
|
||||
auto[*users.Users],
|
||||
auto[*users.UserPolicy],
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue