(control) GUI for ranking sets

Still missing is some polish, forms don't have proper labels, validation is inconsistent, no error messages, etc.
This commit is contained in:
Viktor Lofgren 2024-01-16 17:10:09 +01:00
parent e968365858
commit 2fe5705542
8 changed files with 313 additions and 1 deletions

View File

@ -63,6 +63,9 @@ public class DomainRankingSetsService {
stmt.setInt(4, domainRankingSet.depth()); stmt.setInt(4, domainRankingSet.depth());
stmt.setString(5, domainRankingSet.definition()); stmt.setString(5, domainRankingSet.definition());
stmt.executeUpdate(); stmt.executeUpdate();
if (!conn.getAutoCommit())
conn.commit();
} }
catch (SQLException ex) { catch (SQLException ex) {
logger.error("Failed to update domain set", ex); logger.error("Failed to update domain set", ex);
@ -78,6 +81,9 @@ public class DomainRankingSetsService {
{ {
stmt.setString(1, domainRankingSet.name()); stmt.setString(1, domainRankingSet.name());
stmt.executeUpdate(); stmt.executeUpdate();
if (!conn.getAutoCommit())
conn.commit();
} }
catch (SQLException ex) { catch (SQLException ex) {
logger.error("Failed to delete domain set", ex); logger.error("Failed to delete domain set", ex);
@ -152,5 +158,9 @@ public class DomainRankingSetsService {
.toArray(String[]::new); .toArray(String[]::new);
} }
public boolean isSpecial() {
return algorithm() == DomainSetAlgorithm.SPECIAL;
}
} }
} }

View File

@ -53,6 +53,7 @@ public class ControlService extends Service {
RandomExplorationService randomExplorationService, RandomExplorationService randomExplorationService,
DataSetsService dataSetsService, DataSetsService dataSetsService,
ControlNodeService controlNodeService, ControlNodeService controlNodeService,
ControlDomainRankingSetsService controlDomainRankingSetsService,
ControlActorService controlActorService ControlActorService controlActorService
) throws IOException { ) throws IOException {
@ -66,6 +67,7 @@ public class ControlService extends Service {
messageQueueService.register(); messageQueueService.register();
sysActionsService.register(); sysActionsService.register();
dataSetsService.register(); dataSetsService.register();
controlDomainRankingSetsService.register();
// node // node
controlFileStorageService.register(); controlFileStorageService.register();

View File

@ -8,6 +8,7 @@ public class Redirects {
public static final HtmlRedirect redirectToOverview = new HtmlRedirect("/"); public static final HtmlRedirect redirectToOverview = new HtmlRedirect("/");
public static final HtmlRedirect redirectToBlacklist = new HtmlRedirect("/blacklist"); public static final HtmlRedirect redirectToBlacklist = new HtmlRedirect("/blacklist");
public static final HtmlRedirect redirectToComplaints = new HtmlRedirect("/complaints"); public static final HtmlRedirect redirectToComplaints = new HtmlRedirect("/complaints");
public static final HtmlRedirect redirectToRankingDataSets = new HtmlRedirect("/domain-ranking-sets");
public static final HtmlRedirect redirectToMessageQueue = new HtmlRedirect("/message-queue"); public static final HtmlRedirect redirectToMessageQueue = new HtmlRedirect("/message-queue");
public static class HtmlRedirect implements ResponseTransformer { public static class HtmlRedirect implements ResponseTransformer {

View File

@ -0,0 +1,98 @@
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.Redirects;
import nu.marginalia.db.DomainRankingSetsService;
import spark.Request;
import spark.Response;
import spark.Spark;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Map;
public class ControlDomainRankingSetsService {
private final HikariDataSource dataSource;
private final ControlRendererFactory rendererFactory;
private final DomainRankingSetsService domainRankingSetsService;
@Inject
public ControlDomainRankingSetsService(HikariDataSource dataSource,
ControlRendererFactory rendererFactory,
DomainRankingSetsService domainRankingSetsService) {
this.dataSource = dataSource;
this.rendererFactory = rendererFactory;
this.domainRankingSetsService = domainRankingSetsService;
}
public void register() throws IOException {
var datasetsRenderer = rendererFactory.renderer("control/sys/domain-ranking-sets");
var updateDatasetRenderer = rendererFactory.renderer("control/sys/update-domain-ranking-set");
var newDatasetRenderer = rendererFactory.renderer("control/sys/new-domain-ranking-set");
Spark.get("/public/domain-ranking-sets", this::rankingSetsModel, datasetsRenderer::render);
Spark.get("/public/domain-ranking-sets/new", (rq,rs) -> new Object(), newDatasetRenderer::render);
Spark.get("/public/domain-ranking-sets/:id", this::rankingSetModel, updateDatasetRenderer::render);
Spark.post("/public/domain-ranking-sets/:id", this::alterSetModel, Redirects.redirectToRankingDataSets);
}
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,
request.queryParams("description"),
DomainRankingSetsService.DomainSetAlgorithm.valueOf(request.queryParams("algorithm")),
Integer.parseInt(request.queryParams("depth")),
request.queryParams("definition")
));
return "";
}
else if ("delete".equals(act)) {
var model = domainRankingSetsService.get(id).orElseThrow();
if (model.isSpecial()) {
throw new IllegalArgumentException("Cannot delete special ranking set");
}
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");
}
domainRankingSetsService.upsert(new DomainRankingSetsService.DomainRankingSet(
request.queryParams("name"),
request.queryParams("description"),
DomainRankingSetsService.DomainSetAlgorithm.valueOf(request.queryParams("algorithm")),
Integer.parseInt(request.queryParams("depth")),
request.queryParams("definition")
));
return "";
}
throw new UnsupportedOperationException();
}
private Object rankingSetsModel(Request request, Response response) {
return Map.of("rankingSets", domainRankingSetsService.getAll());
}
private Object rankingSetModel(Request request, Response response) throws SQLException {
var model = domainRankingSetsService.get(request.params("id")).orElseThrow();
return Map.of("rankingSet", model,
"selectedAlgo", Map.of(
"special", model.algorithm() == DomainRankingSetsService.DomainSetAlgorithm.SPECIAL,
"adjacency_cheirank", model.algorithm() == DomainRankingSetsService.DomainSetAlgorithm.ADJACENCY_CHEIRANK,
"adjacency_pagerank", model.algorithm() == DomainRankingSetsService.DomainSetAlgorithm.ADJACENCY_PAGERANK,
"links_cheirank", model.algorithm() == DomainRankingSetsService.DomainSetAlgorithm.LINKS_CHEIRANK,
"links_pagerank", model.algorithm() == DomainRankingSetsService.DomainSetAlgorithm.LINKS_PAGERANK)
);
}
}

View File

@ -34,7 +34,8 @@
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false">System</a> <a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false">System</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="/actions" title="System actions">Actions</a></li> <li><a class="dropdown-item" href="/actions" title="System actions">Actions</a></li>
<li><a class="dropdown-item" href="/datasets" title="View and update the data sets">Datasets</a></li> <li><a class="dropdown-item" href="/datasets" title="View and update the data sets">Data Sets</a></li>
<li><a class="dropdown-item" href="/domain-ranking-sets" title="View and update domain rankings ">Domain Ranking Sets</a></li>
<li><a class="dropdown-item" href="/events" title="View the event log">Events</a></li> <li><a class="dropdown-item" href="/events" title="View the event log">Events</a></li>
<li><a class="dropdown-item" href="/message-queue" title="View or manipulate the system message queue">Message Queue</a></li> <li><a class="dropdown-item" href="/message-queue" title="View or manipulate the system message queue">Message Queue</a></li>
</ul> </ul>

View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<title>Control Service</title>
{{> control/partials/head-includes }}
</head>
<body>
{{> control/partials/nav}}
<div class="container">
<h1 class="my-3">Domain Ranking Sets</h1>
<div class="border my-3 p-3 bg-light">
Domain ranking sets configure the ranking algorithms used to determine the importance of a domain.
</div>
<table class="table">
<tr>
<th>Name</th>
<th>Description</th>
<th>Algorithm</th>
<th>Depth</th>
</tr>
{{#each rankingSets}}
<tr>
<td><a href="/domain-ranking-sets/{{name}}">{{name}}</td></td>
<td>{{description}}</td>
<td>{{algorithm}}</td>
<td>{{depth}}</td>
</tr>
{{/each}}
</table>
<div class="my-3">
<a href="/domain-ranking-sets/new" class="btn btn-primary">New Domain Ranking Set</a>
</div>
</div>
</body>
{{> control/partials/foot-includes }}
</html>

View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<head>
<title>Control Service</title>
{{> control/partials/head-includes }}
</head>
<body>
{{> control/partials/nav}}
<div class="container">
<h1 class="my-3">Create Domain Ranking Set</h1>
<form method="post" action="?act=create">
<table class="table">
<tr>
<th>Name</th>
<td>
<input pattern="\w+" type="text" value="{{name}}" id="name" name="name" />
<div>
<small class="text-muted">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>
<td>
<select id="algorithm" name="algorithm">
<option value="LINKS_PAGERANK">LINKS_PAGERANK</option>
<option value="LINKS_CHEIRANK">LINKS_CHEIRANK</option>
<option value="ADJACENCY_PAGERANK">ADJACENCY_PAGERANK</option>
<option value="ADJACENCY_CHEIRANK">ADJACENCY_CHEIRANK</option>
</select>
<div>
<small class="text-muted">
The algorithm used to rank the domains. The LINKS algorithms use the link graph, and the ADJACENCY
algorithms use the adjacency graph. CheiRank is a variant of PageRank that uses the reversed graph.
</small>
</div>
</td>
</tr>
<tr>
<th>Description</th>
<td>
<input type="text" value="{{description}}" id="description" name="description" {{#if special}}disabled{{/if}} />
<div>
<small class="text-muted">This is purely to help keep track of what this ranking set does.</small>
</div>
</td>
</tr>
<tr>
<th>Depth</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>
</div>
</td>
</tr>
<tr><th colspan="2">Definition</th></tr>
<tr><td colspan="2">
<textarea name="definition" id="definition" rows="10" style="width: 100%">{{definition}}</textarea>
<div>
<small class="text-muted">A list of domain names, one per line, possibly globbed with SQL-style '%' wildcards.
These are used as the origin point for the Personalized PageRank algorithm, and will be considered
the central points of the link or adjacency graph. If no domains are specified, the entire domain space is used, as per the PageRank paper.
</small>
</div>
</td></tr>
</table>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</div>
</body>
{{> control/partials/foot-includes }}
</html>

View File

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html>
<head>
<title>Control Service</title>
{{> control/partials/head-includes }}
</head>
<body>
{{> control/partials/nav}}
<div class="container">
{{#with rankingSet}}
<h1 class="my-3">Domain Ranking Set: {{name}}</h1>
<form method="post" action="?act=update">
<table class="table" id="update-form">
<tr>
<th>Name</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}} />
<div>
<small class="text-muted">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>
<td>
{{#if special}}<input type="hidden" name="algorithm" value="{{algorithm}}" />{{/if}}
<select id="algorithm" name="algorithm" {{#if special}}disabled{{/if}}>
{{#with algorithm}}
<option value="SPECIAL" disabled {{#if selectedAlgo.special}}selected{{/if}}>SPECIAL</option>
<option value="LINKS_PAGERANK" {{#if selectedAlgo.links_pagerank}}selected{{/if}}>LINKS_PAGERANK</option>
<option value="LINKS_CHEIRANK" {{#if selectedAlgo.links_cheirank}}selected{{/if}}>LINKS_CHEIRANK</option>
<option value="ADJACENCY_PAGERANK" {{#if selectedAlgo.adjacency_pagerank}}selected{{/if}}>ADJACENCY_PAGERANK</option>
<option value="ADJACENCY_CHEIRANK" {{#if selectedAlgo.adjacency_cheirank}}selected{{/if}}>ADJACENCY_CHEIRANK</option>
{{/with}}
</select>
<div>
<small class="text-muted">
The algorithm used to rank the domains. The LINKS algorithms use the link graph, and the ADJACENCY
algorithms use the adjacency graph. CheiRank is a variant of PageRank that uses the reversed graph.
</small>
</div>
</td>
</tr>
<tr>
<th>Description</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}} />
<div>
<small class="text-muted">This is purely to help keep track of what this ranking set does.</small>
</div>
</td>
</tr>
<tr>
<th>Depth</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>
</div>
</td>
</tr>
<tr><th colspan="2">Definition</th></tr>
<tr><td colspan="2">
<textarea name="definition" id="definition" rows="10" style="width: 100%">{{definition}}</textarea>
<div>
<small class="text-muted">A list of domain names, one per line, possibly globbed with SQL-style '%' wildcards.
These are used as the origin point for the Personalized PageRank algorithm, and will be considered
the central points of the link or adjacency graph. If no domains are specified, the entire domain space is used, as per the PageRank paper.
</small>
</div>
</td></tr>
</table>
</form>
<form method="post" action="?act=delete" id="delete-form"></form>
<button type="submit" class="btn btn-danger" form="delete-form" style="float:right" {{#if special}}disabled title="Cannot delete special sets!"{{/if}} onclick="return confirm('Confirm deletion of ranking set')">Delete</button>
<button type="submit" class="btn btn-primary" form="update-form">Update</button>
{{/with}}
</div>
</body>
{{> control/partials/foot-includes }}
</html>