(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:
parent
e968365858
commit
2fe5705542
@ -63,6 +63,9 @@ public class DomainRankingSetsService {
|
||||
stmt.setInt(4, domainRankingSet.depth());
|
||||
stmt.setString(5, domainRankingSet.definition());
|
||||
stmt.executeUpdate();
|
||||
|
||||
if (!conn.getAutoCommit())
|
||||
conn.commit();
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
logger.error("Failed to update domain set", ex);
|
||||
@ -78,6 +81,9 @@ public class DomainRankingSetsService {
|
||||
{
|
||||
stmt.setString(1, domainRankingSet.name());
|
||||
stmt.executeUpdate();
|
||||
|
||||
if (!conn.getAutoCommit())
|
||||
conn.commit();
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
logger.error("Failed to delete domain set", ex);
|
||||
@ -152,5 +158,9 @@ public class DomainRankingSetsService {
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
public boolean isSpecial() {
|
||||
return algorithm() == DomainSetAlgorithm.SPECIAL;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ public class ControlService extends Service {
|
||||
RandomExplorationService randomExplorationService,
|
||||
DataSetsService dataSetsService,
|
||||
ControlNodeService controlNodeService,
|
||||
ControlDomainRankingSetsService controlDomainRankingSetsService,
|
||||
ControlActorService controlActorService
|
||||
) throws IOException {
|
||||
|
||||
@ -66,6 +67,7 @@ public class ControlService extends Service {
|
||||
messageQueueService.register();
|
||||
sysActionsService.register();
|
||||
dataSetsService.register();
|
||||
controlDomainRankingSetsService.register();
|
||||
|
||||
// node
|
||||
controlFileStorageService.register();
|
||||
|
@ -8,6 +8,7 @@ public class Redirects {
|
||||
public static final HtmlRedirect redirectToOverview = new HtmlRedirect("/");
|
||||
public static final HtmlRedirect redirectToBlacklist = new HtmlRedirect("/blacklist");
|
||||
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 class HtmlRedirect implements ResponseTransformer {
|
||||
|
@ -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)
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -34,7 +34,8 @@
|
||||
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false">System</a>
|
||||
<ul class="dropdown-menu">
|
||||
<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="/message-queue" title="View or manipulate the system message queue">Message Queue</a></li>
|
||||
</ul>
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
Loading…
Reference in New Issue
Block a user