(control) Clean up UX and accessibility for new domain ranking sets.

The change also adds basic support for error messages in the GUI.
This commit is contained in:
Viktor Lofgren 2024-01-17 10:47:14 +01:00
parent 2fe5705542
commit 7fd4c092e3
8 changed files with 127 additions and 19 deletions

View File

@ -54,7 +54,8 @@ public class ControlService extends Service {
DataSetsService dataSetsService,
ControlNodeService controlNodeService,
ControlDomainRankingSetsService controlDomainRankingSetsService,
ControlActorService controlActorService
ControlActorService controlActorService,
ControlErrorHandler errorHandler
) throws IOException {
super(params);
@ -81,6 +82,8 @@ public class ControlService extends Service {
domainComplaintService.register();
randomExplorationService.register();
errorHandler.register();
var indexRenderer = rendererFactory.renderer("control/index");
var eventsRenderer = rendererFactory.renderer("control/sys/events");
var serviceByIdRenderer = rendererFactory.renderer("control/sys/service-by-id");
@ -106,6 +109,7 @@ public class ControlService extends Service {
Spark.get("/public/:resource", this::serveStatic);
monitors.subscribe(this::logMonitorStateChange);
controlActorService.startDefaultActors();

View File

@ -0,0 +1,15 @@
package nu.marginalia.control;
public class ControlValidationError extends RuntimeException {
public final String title;
public final String messageLong;
public final String redirect;
public ControlValidationError(String title, String messageLong, String redirect) {
super(title);
this.title = title;
this.messageLong = messageLong;
this.redirect = redirect;
}
}

View File

@ -3,6 +3,7 @@ package nu.marginalia.control.sys.svc;
import com.google.inject.Inject;
import com.zaxxer.hikari.HikariDataSource;
import nu.marginalia.control.ControlRendererFactory;
import nu.marginalia.control.ControlValidationError;
import nu.marginalia.control.Redirects;
import nu.marginalia.db.DomainRankingSetsService;
import spark.Request;
@ -41,6 +42,7 @@ public class ControlDomainRankingSetsService {
private Object alterSetModel(Request request, Response response) throws SQLException {
final String act = request.queryParams("act");
final String id = request.params("id");
if ("update".equals(act)) {
domainRankingSetsService.upsert(new DomainRankingSetsService.DomainRankingSet(
id,
@ -54,18 +56,26 @@ public class ControlDomainRankingSetsService {
else if ("delete".equals(act)) {
var model = domainRankingSetsService.get(id).orElseThrow();
if (model.isSpecial()) {
throw new IllegalArgumentException("Cannot delete special ranking set");
throw new ControlValidationError("Cannot delete special ranking set",
"""
SPECIAL data sets are reserved by the system and can not be deleted.
""",
"/domain-ranking-sets");
}
domainRankingSetsService.delete(model);
return "";
}
else if ("create".equals(act)) {
if (domainRankingSetsService.get(request.queryParams("name")).isPresent()) {
throw new IllegalArgumentException("Ranking set with that name already exists");
throw new ControlValidationError("Ranking set with that name already exists",
"""
Ensure the new data set has a unique name and try again.
""",
"/domain-ranking-sets");
}
domainRankingSetsService.upsert(new DomainRankingSetsService.DomainRankingSet(
request.queryParams("name"),
request.queryParams("name").toUpperCase(),
request.queryParams("description"),
DomainRankingSetsService.DomainSetAlgorithm.valueOf(request.queryParams("algorithm")),
Integer.parseInt(request.queryParams("depth")),
@ -74,7 +84,10 @@ public class ControlDomainRankingSetsService {
return "";
}
throw new UnsupportedOperationException();
throw new ControlValidationError("Unknown action", """
An unknown action was requested and the system does not understand how to act on it.
""",
"/domain-ranking-sets");
}
private Object rankingSetsModel(Request request, Response response) {

View File

@ -0,0 +1,35 @@
package nu.marginalia.control.sys.svc;
import com.google.inject.Inject;
import nu.marginalia.control.ControlRendererFactory;
import nu.marginalia.control.ControlValidationError;
import spark.Request;
import spark.Response;
import spark.Spark;
import java.util.Map;
public class ControlErrorHandler {
private final ControlRendererFactory.Renderer renderer;
@Inject
public ControlErrorHandler(ControlRendererFactory rendererFactory) {
this.renderer = rendererFactory.renderer("control/error");
}
public void render(ControlValidationError error, Request request, Response response) {
String text = renderer.render(
Map.of(
"title", error.title,
"messageLong", error.messageLong,
"redirect", error.redirect
)
);
response.body(text);
}
public void register() {
Spark.exception(ControlValidationError.class, this::render);
}
}

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>Control Service: Error</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/style.css" />
{{> control/partials/head-includes }}
</head>
<body>
{{> control/partials/nav}}
<div class="container">
<h1 class="my-3">Error: {{title}}</h1>
<div class="my-3 p-3 border bg-light">
<p>{{messageLong}}</p>
<a href="{{redirect}}">Go back</a>
</div>
</div>
</body>
{{> control/partials/foot-includes }}
<script>
window.setInterval(() => {
refresh(["processes", "services", "jobs", "events"]);
}, 2000);
</script>
</html>

View File

@ -32,6 +32,21 @@
<div class="my-3">
<a href="/domain-ranking-sets/new" class="btn btn-primary">New Domain Ranking Set</a>
</div>
<div class="border my-3 p-3 bg-light">
<p>Several reserved ranking sets are available for use in the query parameters.</p>
<dl>
<dt>NONE</dt><dd>Placeholder for no restriction on the domains returned.
Does nothing, and exists only to prevent a new ranking
set from being created with this name.</dd>
<dt>RANK</dt><dd>Used to calculate the domain ranking for a given domain.
This affects the order they are stored in the index, and increases the odds they'll
even be considered within the time restrictions of the query.</dd>
<dt>BLOGS</dt><dd>Returns a fixed list of domains, configurable in <a href="/datasets">Datasets</a>.
Changes to this list will not be reflected in the index until the next time the index is rebuilt.</dd>
</dl>
</div>
</div>
</body>
{{> control/partials/foot-includes }}

View File

@ -11,17 +11,18 @@
<form method="post" action="?act=create">
<table class="table">
<tr>
<th>Name</th>
<th><label for="name">Name</label></th>
<td>
<input pattern="\w+" type="text" value="{{name}}" id="name" name="name" />
<input pattern="\w+" type="text" value="{{name}}" id="name" name="name" style="text-transform: uppercase" />
<div>
<small class="text-muted">The name is how the ranking set is identified in the query parameters,
<small class="text-muted">Must be all letters.
The name is how the ranking set is identified in the query parameters,
and also decides the file name of the persisted ranking set definition. Keep it simple.</small>
</div>
</td>
</tr>
<tr>
<th>Algorithm</th>
<th><label for="algorithm">Algorithm</label></th>
<td>
<select id="algorithm" name="algorithm">
<option value="LINKS_PAGERANK">LINKS_PAGERANK</option>
@ -38,7 +39,7 @@
</td>
</tr>
<tr>
<th>Description</th>
<th><label for="description">Description</label></th>
<td>
<input type="text" value="{{description}}" id="description" name="description" {{#if special}}disabled{{/if}} />
<div>
@ -47,15 +48,15 @@
</td>
</tr>
<tr>
<th>Depth</th>
<th><label for="depth">Depth</label></th>
<td>
<input pattern="\d+" type="text" value="{{depth}}" id="depth" name="depth" />
<div>
<small class="text-muted">Up to this number of domains are ranked, the rest are excluded.</small>
<small class="text-muted">Number. Up to this number of domains are ranked, the rest are excluded.</small>
</div>
</td>
</tr>
<tr><th colspan="2">Definition</th></tr>
<tr><th colspan="2"><label for="definition">Definition</label></th></tr>
<tr><td colspan="2">
<textarea name="definition" id="definition" rows="10" style="width: 100%">{{definition}}</textarea>
<div>

View File

@ -12,7 +12,7 @@
<form method="post" action="?act=update">
<table class="table" id="update-form">
<tr>
<th>Name</th>
<th><label for="name">Name</label></th>
<td>
{{#if special}}<input type="hidden" name="name" value="{{name}}" />{{/if}}
<input type="text" value="{{name}}" id="name" name="name" {{#if special}}disabled{{/if}} />
@ -23,7 +23,7 @@
</td>
</tr>
<tr>
<th>Algorithm</th>
<th><label for="algorithm">Algorithm</label></th>
<td>
{{#if special}}<input type="hidden" name="algorithm" value="{{algorithm}}" />{{/if}}
<select id="algorithm" name="algorithm" {{#if special}}disabled{{/if}}>
@ -44,7 +44,7 @@
</td>
</tr>
<tr>
<th>Description</th>
<th><label for="description">Description</label></th>
<td>
{{#if special}}<input type="hidden" name="description" value="{{description}}" />{{/if}}
<input type="text" value="{{description}}" id="description" name="description" {{#if special}}disabled{{/if}} />
@ -54,15 +54,15 @@
</td>
</tr>
<tr>
<th>Depth</th>
<th><label for="depth">Depth</label></th>
<td>
<input type="text" value="{{depth}}" id="depth" name="depth" />
<div>
<small class="text-muted">Up to this number of domains are ranked, the rest are excluded.</small>
<small class="text-muted">Number. Up to this number of domains are ranked, the rest are excluded.</small>
</div>
</td>
</tr>
<tr><th colspan="2">Definition</th></tr>
<tr><th colspan="2"><label for="definition">Definition</label></th></tr>
<tr><td colspan="2">
<textarea name="definition" id="definition" rows="10" style="width: 100%">{{definition}}</textarea>
<div>