(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, DataSetsService dataSetsService,
ControlNodeService controlNodeService, ControlNodeService controlNodeService,
ControlDomainRankingSetsService controlDomainRankingSetsService, ControlDomainRankingSetsService controlDomainRankingSetsService,
ControlActorService controlActorService ControlActorService controlActorService,
ControlErrorHandler errorHandler
) throws IOException { ) throws IOException {
super(params); super(params);
@ -81,6 +82,8 @@ public class ControlService extends Service {
domainComplaintService.register(); domainComplaintService.register();
randomExplorationService.register(); randomExplorationService.register();
errorHandler.register();
var indexRenderer = rendererFactory.renderer("control/index"); var indexRenderer = rendererFactory.renderer("control/index");
var eventsRenderer = rendererFactory.renderer("control/sys/events"); var eventsRenderer = rendererFactory.renderer("control/sys/events");
var serviceByIdRenderer = rendererFactory.renderer("control/sys/service-by-id"); var serviceByIdRenderer = rendererFactory.renderer("control/sys/service-by-id");
@ -106,6 +109,7 @@ public class ControlService extends Service {
Spark.get("/public/:resource", this::serveStatic); Spark.get("/public/:resource", this::serveStatic);
monitors.subscribe(this::logMonitorStateChange); monitors.subscribe(this::logMonitorStateChange);
controlActorService.startDefaultActors(); 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.google.inject.Inject;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import nu.marginalia.control.ControlRendererFactory; import nu.marginalia.control.ControlRendererFactory;
import nu.marginalia.control.ControlValidationError;
import nu.marginalia.control.Redirects; import nu.marginalia.control.Redirects;
import nu.marginalia.db.DomainRankingSetsService; import nu.marginalia.db.DomainRankingSetsService;
import spark.Request; import spark.Request;
@ -41,6 +42,7 @@ public class ControlDomainRankingSetsService {
private Object alterSetModel(Request request, Response response) throws SQLException { private Object alterSetModel(Request request, Response response) throws SQLException {
final String act = request.queryParams("act"); final String act = request.queryParams("act");
final String id = request.params("id"); final String id = request.params("id");
if ("update".equals(act)) { if ("update".equals(act)) {
domainRankingSetsService.upsert(new DomainRankingSetsService.DomainRankingSet( domainRankingSetsService.upsert(new DomainRankingSetsService.DomainRankingSet(
id, id,
@ -54,18 +56,26 @@ public class ControlDomainRankingSetsService {
else if ("delete".equals(act)) { else if ("delete".equals(act)) {
var model = domainRankingSetsService.get(id).orElseThrow(); var model = domainRankingSetsService.get(id).orElseThrow();
if (model.isSpecial()) { 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); domainRankingSetsService.delete(model);
return ""; return "";
} }
else if ("create".equals(act)) { else if ("create".equals(act)) {
if (domainRankingSetsService.get(request.queryParams("name")).isPresent()) { 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( domainRankingSetsService.upsert(new DomainRankingSetsService.DomainRankingSet(
request.queryParams("name"), request.queryParams("name").toUpperCase(),
request.queryParams("description"), request.queryParams("description"),
DomainRankingSetsService.DomainSetAlgorithm.valueOf(request.queryParams("algorithm")), DomainRankingSetsService.DomainSetAlgorithm.valueOf(request.queryParams("algorithm")),
Integer.parseInt(request.queryParams("depth")), Integer.parseInt(request.queryParams("depth")),
@ -74,7 +84,10 @@ public class ControlDomainRankingSetsService {
return ""; 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) { 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"> <div class="my-3">
<a href="/domain-ranking-sets/new" class="btn btn-primary">New Domain Ranking Set</a> <a href="/domain-ranking-sets/new" class="btn btn-primary">New Domain Ranking Set</a>
</div> </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> </div>
</body> </body>
{{> control/partials/foot-includes }} {{> control/partials/foot-includes }}

View File

@ -11,17 +11,18 @@
<form method="post" action="?act=create"> <form method="post" action="?act=create">
<table class="table"> <table class="table">
<tr> <tr>
<th>Name</th> <th><label for="name">Name</label></th>
<td> <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> <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> and also decides the file name of the persisted ranking set definition. Keep it simple.</small>
</div> </div>
</td> </td>
</tr> </tr>
<tr> <tr>
<th>Algorithm</th> <th><label for="algorithm">Algorithm</label></th>
<td> <td>
<select id="algorithm" name="algorithm"> <select id="algorithm" name="algorithm">
<option value="LINKS_PAGERANK">LINKS_PAGERANK</option> <option value="LINKS_PAGERANK">LINKS_PAGERANK</option>
@ -38,7 +39,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th>Description</th> <th><label for="description">Description</label></th>
<td> <td>
<input type="text" value="{{description}}" id="description" name="description" {{#if special}}disabled{{/if}} /> <input type="text" value="{{description}}" id="description" name="description" {{#if special}}disabled{{/if}} />
<div> <div>
@ -47,15 +48,15 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th>Depth</th> <th><label for="depth">Depth</label></th>
<td> <td>
<input pattern="\d+" type="text" value="{{depth}}" id="depth" name="depth" /> <input pattern="\d+" type="text" value="{{depth}}" id="depth" name="depth" />
<div> <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> </div>
</td> </td>
</tr> </tr>
<tr><th colspan="2">Definition</th></tr> <tr><th colspan="2"><label for="definition">Definition</label></th></tr>
<tr><td colspan="2"> <tr><td colspan="2">
<textarea name="definition" id="definition" rows="10" style="width: 100%">{{definition}}</textarea> <textarea name="definition" id="definition" rows="10" style="width: 100%">{{definition}}</textarea>
<div> <div>

View File

@ -12,7 +12,7 @@
<form method="post" action="?act=update"> <form method="post" action="?act=update">
<table class="table" id="update-form"> <table class="table" id="update-form">
<tr> <tr>
<th>Name</th> <th><label for="name">Name</label></th>
<td> <td>
{{#if special}}<input type="hidden" name="name" value="{{name}}" />{{/if}} {{#if special}}<input type="hidden" name="name" value="{{name}}" />{{/if}}
<input type="text" value="{{name}}" id="name" name="name" {{#if special}}disabled{{/if}} /> <input type="text" value="{{name}}" id="name" name="name" {{#if special}}disabled{{/if}} />
@ -23,7 +23,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th>Algorithm</th> <th><label for="algorithm">Algorithm</label></th>
<td> <td>
{{#if special}}<input type="hidden" name="algorithm" value="{{algorithm}}" />{{/if}} {{#if special}}<input type="hidden" name="algorithm" value="{{algorithm}}" />{{/if}}
<select id="algorithm" name="algorithm" {{#if special}}disabled{{/if}}> <select id="algorithm" name="algorithm" {{#if special}}disabled{{/if}}>
@ -44,7 +44,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th>Description</th> <th><label for="description">Description</label></th>
<td> <td>
{{#if special}}<input type="hidden" name="description" value="{{description}}" />{{/if}} {{#if special}}<input type="hidden" name="description" value="{{description}}" />{{/if}}
<input type="text" value="{{description}}" id="description" name="description" {{#if special}}disabled{{/if}} /> <input type="text" value="{{description}}" id="description" name="description" {{#if special}}disabled{{/if}} />
@ -54,15 +54,15 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<th>Depth</th> <th><label for="depth">Depth</label></th>
<td> <td>
<input type="text" value="{{depth}}" id="depth" name="depth" /> <input type="text" value="{{depth}}" id="depth" name="depth" />
<div> <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> </div>
</td> </td>
</tr> </tr>
<tr><th colspan="2">Definition</th></tr> <tr><th colspan="2"><label for="definition">Definition</label></th></tr>
<tr><td colspan="2"> <tr><td colspan="2">
<textarea name="definition" id="definition" rows="10" style="width: 100%">{{definition}}</textarea> <textarea name="definition" id="definition" rows="10" style="width: 100%">{{definition}}</textarea>
<div> <div>