mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-24 05:18:58 +00:00
(control) Dialog for updating message state; clean up file view.
This commit is contained in:
parent
01476577b8
commit
866db6c63f
@ -12,6 +12,8 @@ import java.sql.SQLException;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import static nu.marginalia.mq.MqMessageState.NEW;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class MqPersistence {
|
public class MqPersistence {
|
||||||
private final HikariDataSource dataSource;
|
private final HikariDataSource dataSource;
|
||||||
@ -100,12 +102,18 @@ public class MqPersistence {
|
|||||||
|
|
||||||
/** Modifies the state of a message by id */
|
/** Modifies the state of a message by id */
|
||||||
public void updateMessageState(long id, MqMessageState mqMessageState) throws SQLException {
|
public void updateMessageState(long id, MqMessageState mqMessageState) throws SQLException {
|
||||||
|
if (NEW == mqMessageState) {
|
||||||
|
reinitializeMessage(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try (var conn = dataSource.getConnection();
|
try (var conn = dataSource.getConnection();
|
||||||
var stmt = conn.prepareStatement("""
|
var stmt = conn.prepareStatement("""
|
||||||
UPDATE MESSAGE_QUEUE
|
UPDATE MESSAGE_QUEUE
|
||||||
SET STATE=?, UPDATED_TIME=CURRENT_TIMESTAMP(6)
|
SET STATE=?, UPDATED_TIME=CURRENT_TIMESTAMP(6)
|
||||||
WHERE ID=?
|
WHERE ID=?
|
||||||
""")) {
|
""")) {
|
||||||
|
|
||||||
stmt.setString(1, mqMessageState.name());
|
stmt.setString(1, mqMessageState.name());
|
||||||
stmt.setLong(2, id);
|
stmt.setLong(2, id);
|
||||||
|
|
||||||
@ -115,6 +123,26 @@ public class MqPersistence {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sets the message to 'NEW' state and removes any owner */
|
||||||
|
public void reinitializeMessage(long id) throws SQLException {
|
||||||
|
try (var conn = dataSource.getConnection();
|
||||||
|
var stmt = conn.prepareStatement("""
|
||||||
|
UPDATE MESSAGE_QUEUE
|
||||||
|
SET STATE='NEW',
|
||||||
|
OWNER_INSTANCE=NULL,
|
||||||
|
OWNER_TICK=NULL,
|
||||||
|
UPDATED_TIME=CURRENT_TIMESTAMP(6)
|
||||||
|
WHERE ID=?
|
||||||
|
""")) {
|
||||||
|
|
||||||
|
stmt.setLong(1, id);
|
||||||
|
|
||||||
|
if (stmt.executeUpdate() != 1) {
|
||||||
|
throw new IllegalArgumentException("No rows updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Creates a new message in the queue referencing as a reply to an existing message
|
/** Creates a new message in the queue referencing as a reply to an existing message
|
||||||
* This message will have it's RELATED_ID set to the original message's ID.
|
* This message will have it's RELATED_ID set to the original message's ID.
|
||||||
*/
|
*/
|
||||||
@ -207,7 +235,8 @@ public class MqPersistence {
|
|||||||
AND RECIPIENT_INBOX=?
|
AND RECIPIENT_INBOX=?
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""")
|
""")
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
queryStmt.setString(1, inboxName);
|
queryStmt.setString(1, inboxName);
|
||||||
queryStmt.setInt(2, n);
|
queryStmt.setInt(2, n);
|
||||||
var rs = queryStmt.executeQuery();
|
var rs = queryStmt.executeQuery();
|
||||||
@ -233,6 +262,41 @@ public class MqPersistence {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MqMessage getMessage(long id) throws SQLException {
|
||||||
|
try (var conn = dataSource.getConnection();
|
||||||
|
var queryStmt = conn.prepareStatement("""
|
||||||
|
SELECT
|
||||||
|
ID,
|
||||||
|
RELATED_ID,
|
||||||
|
FUNCTION,
|
||||||
|
PAYLOAD,
|
||||||
|
STATE,
|
||||||
|
SENDER_INBOX IS NOT NULL AS EXPECTS_RESPONSE
|
||||||
|
FROM MESSAGE_QUEUE
|
||||||
|
WHERE ID=?
|
||||||
|
""")
|
||||||
|
)
|
||||||
|
{
|
||||||
|
queryStmt.setLong(1, id);
|
||||||
|
var rs = queryStmt.executeQuery();
|
||||||
|
|
||||||
|
if (rs.next()) {
|
||||||
|
long msgId = rs.getLong("ID");
|
||||||
|
long relatedId = rs.getLong("RELATED_ID");
|
||||||
|
|
||||||
|
String function = rs.getString("FUNCTION");
|
||||||
|
String payload = rs.getString("PAYLOAD");
|
||||||
|
|
||||||
|
MqMessageState state = MqMessageState.valueOf(rs.getString("STATE"));
|
||||||
|
boolean expectsResponse = rs.getBoolean("EXPECTS_RESPONSE");
|
||||||
|
|
||||||
|
return new MqMessage(msgId, relatedId, function, payload, state, expectsResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("No message with id " + id);
|
||||||
|
}
|
||||||
/** Marks unclaimed messages addressed to this inbox with instanceUUID and tick,
|
/** Marks unclaimed messages addressed to this inbox with instanceUUID and tick,
|
||||||
* then returns these messages.
|
* then returns these messages.
|
||||||
*/
|
*/
|
||||||
@ -378,4 +442,5 @@ public class MqPersistence {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ import nu.marginalia.control.svc.*;
|
|||||||
import nu.marginalia.db.storage.model.FileStorageId;
|
import nu.marginalia.db.storage.model.FileStorageId;
|
||||||
import nu.marginalia.db.storage.model.FileStorageType;
|
import nu.marginalia.db.storage.model.FileStorageType;
|
||||||
import nu.marginalia.model.gson.GsonFactory;
|
import nu.marginalia.model.gson.GsonFactory;
|
||||||
|
import nu.marginalia.mq.MqMessageState;
|
||||||
|
import nu.marginalia.mq.persistence.MqPersistence;
|
||||||
import nu.marginalia.renderer.RendererFactory;
|
import nu.marginalia.renderer.RendererFactory;
|
||||||
import nu.marginalia.service.server.*;
|
import nu.marginalia.service.server.*;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -43,7 +45,8 @@ public class ControlService extends Service {
|
|||||||
ControlActorService controlActorService,
|
ControlActorService controlActorService,
|
||||||
StaticResources staticResources,
|
StaticResources staticResources,
|
||||||
MessageQueueViewService messageQueueViewService,
|
MessageQueueViewService messageQueueViewService,
|
||||||
ControlFileStorageService controlFileStorageService
|
ControlFileStorageService controlFileStorageService,
|
||||||
|
MqPersistence persistence
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
|
|
||||||
super(params);
|
super(params);
|
||||||
@ -60,7 +63,9 @@ public class ControlService extends Service {
|
|||||||
var storageSpecsRenderer = rendererFactory.renderer("control/storage-specs");
|
var storageSpecsRenderer = rendererFactory.renderer("control/storage-specs");
|
||||||
var storageCrawlsRenderer = rendererFactory.renderer("control/storage-crawls");
|
var storageCrawlsRenderer = rendererFactory.renderer("control/storage-crawls");
|
||||||
var storageProcessedRenderer = rendererFactory.renderer("control/storage-processed");
|
var storageProcessedRenderer = rendererFactory.renderer("control/storage-processed");
|
||||||
|
|
||||||
var storageDetailsRenderer = rendererFactory.renderer("control/storage-details");
|
var storageDetailsRenderer = rendererFactory.renderer("control/storage-details");
|
||||||
|
var updateMessageStateRenderer = rendererFactory.renderer("control/dialog-update-message-state");
|
||||||
|
|
||||||
this.controlActorService = controlActorService;
|
this.controlActorService = controlActorService;
|
||||||
|
|
||||||
@ -102,6 +107,14 @@ public class ControlService extends Service {
|
|||||||
Spark.post("/public/storage/specs", controlActorService::createCrawlSpecification, redirectToStorage);
|
Spark.post("/public/storage/specs", controlActorService::createCrawlSpecification, redirectToStorage);
|
||||||
Spark.post("/public/storage/:fid/delete", controlFileStorageService::flagFileForDeletionRequest, redirectToStorage);
|
Spark.post("/public/storage/:fid/delete", controlFileStorageService::flagFileForDeletionRequest, redirectToStorage);
|
||||||
|
|
||||||
|
Spark.get("/public/message/:id/state", (rq, rsp) -> persistence.getMessage(Long.parseLong(rq.params("id"))), updateMessageStateRenderer::render);
|
||||||
|
Spark.post("/public/message/:id/state", (rq, rsp) -> {
|
||||||
|
MqMessageState state = MqMessageState.valueOf(rq.queryParams("state"));
|
||||||
|
long id = Long.parseLong(rq.params("id"));
|
||||||
|
persistence.updateMessageState(id, state);
|
||||||
|
return "";
|
||||||
|
}, redirectToProcesses);
|
||||||
|
|
||||||
Spark.get("/public/:resource", this::serveStatic);
|
Spark.get("/public/:resource", this::serveStatic);
|
||||||
|
|
||||||
monitors.subscribe(this::logMonitorStateChange);
|
monitors.subscribe(this::logMonitorStateChange);
|
||||||
|
@ -1,15 +1,7 @@
|
|||||||
package nu.marginalia.control.model;
|
package nu.marginalia.control.model;
|
||||||
|
|
||||||
import nu.marginalia.db.storage.model.FileStorage;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public record FileStorageFileModel(String filename,
|
public record FileStorageFileModel(String filename,
|
||||||
String type,
|
String mTime,
|
||||||
String size
|
String size)
|
||||||
) {
|
{
|
||||||
|
|
||||||
public boolean isDownloadable() {
|
|
||||||
return type.equals("file");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -116,11 +116,9 @@ public class ControlFileStorageService {
|
|||||||
|
|
||||||
try (var filesStream = Files.list(storage.asPath())) {
|
try (var filesStream = Files.list(storage.asPath())) {
|
||||||
filesStream
|
filesStream
|
||||||
|
.filter(Files::isRegularFile)
|
||||||
.map(this::createFileModel)
|
.map(this::createFileModel)
|
||||||
.sorted(Comparator
|
.sorted(Comparator.comparing(FileStorageFileModel::filename))
|
||||||
.comparing(FileStorageFileModel::type)
|
|
||||||
.thenComparing(FileStorageFileModel::filename)
|
|
||||||
)
|
|
||||||
.forEach(files::add);
|
.forEach(files::add);
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
@ -132,7 +130,7 @@ public class ControlFileStorageService {
|
|||||||
|
|
||||||
private FileStorageFileModel createFileModel(Path p) {
|
private FileStorageFileModel createFileModel(Path p) {
|
||||||
try {
|
try {
|
||||||
String type = Files.isRegularFile(p) ? "file" : "directory";
|
String mTime = Files.getLastModifiedTime(p).toInstant().toString();
|
||||||
String size;
|
String size;
|
||||||
if (Files.isDirectory(p)) {
|
if (Files.isDirectory(p)) {
|
||||||
size = "-";
|
size = "-";
|
||||||
@ -146,7 +144,7 @@ public class ControlFileStorageService {
|
|||||||
else size = sizeBytes / (1024 * 1024 * 1024) + " GB";
|
else size = sizeBytes / (1024 * 1024 * 1024) + " GB";
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FileStorageFileModel(p.toFile().getName(), type, size);
|
return new FileStorageFileModel(p.toFile().getName(), mTime, size);
|
||||||
}
|
}
|
||||||
catch (IOException ex) {
|
catch (IOException ex) {
|
||||||
throw new RuntimeException(ex);
|
throw new RuntimeException(ex);
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<link rel="stylesheet" href="/style.css" />
|
||||||
|
<head><title>Update ID</title></head>
|
||||||
|
<body>
|
||||||
|
{{> control/partials/nav}}
|
||||||
|
<section>
|
||||||
|
<h1>Update Message State</h1>
|
||||||
|
<p>Update the of a message in the message queue. This may be useful to prevent an actor
|
||||||
|
from resuming an action when this is not desirable. Setting an old message to 'NEW' will
|
||||||
|
erase information about its owner, and inboxes will consider the message new again.</p>
|
||||||
|
<form method="post" action="/message/{{msgId}}/state">
|
||||||
|
<label for="msgId">msgId</label><br>
|
||||||
|
<input type="text" disabled id="msgId" name="msgId" value="{{msgId}}">
|
||||||
|
<br><br>
|
||||||
|
<label for="relatedId">relatedId</label><br>
|
||||||
|
<input type="text" disabled id="relatedId" name="relatedId" value="{{relatedId}}">
|
||||||
|
<br><br>
|
||||||
|
<label for="function">function</label><br>
|
||||||
|
<input type="text" disabled id="function" name="function" value="{{function}}">
|
||||||
|
<br><br>
|
||||||
|
<label for="payload">payload</label><br>
|
||||||
|
<input type="text" disabled id="payload" name="payload" value="{{payload}}">
|
||||||
|
<br><br>
|
||||||
|
<label for="oldState">current state</label><br>
|
||||||
|
<input type="text" disabled id="oldState" name="oldState" value="{{state}}">
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<label for="state">new state</label><br>
|
||||||
|
<select id="state" name="state">
|
||||||
|
<option value="NEW">NEW</option>
|
||||||
|
<option value="ACK">ACK</option>
|
||||||
|
<option value="OK">OK</option>
|
||||||
|
<option value="ERR">ERR</option>
|
||||||
|
<option value="DEAD">DEAD</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input type="submit" value="Update">
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</html>
|
@ -11,8 +11,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{{#each messages}}
|
{{#each messages}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{stateCode}} {{state}}</td>
|
<td>{{stateCode}} <a onClick="updateMsgState({{id}})" href="/message/{{id}}/state">{{state}}</a></td>
|
||||||
<td><a onClick="editMessage({{id}})" href="#">{{id}}</a></td>
|
<td>{{id}}</td>
|
||||||
<td>{{recipientInbox}}</td>
|
<td>{{recipientInbox}}</td>
|
||||||
<td>{{function}}</td>
|
<td>{{function}}</td>
|
||||||
<td title="{{ownerInstanceFull}}">
|
<td title="{{ownerInstanceFull}}">
|
||||||
@ -30,61 +30,3 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<dialog id="edit-message">
|
|
||||||
<h1>Edit Message</h1>
|
|
||||||
<form method="post" action="/message/:id/edit">
|
|
||||||
<div class="form-grid">
|
|
||||||
<label for="id">ID</label> <input readonly type="text" name="id" id="id" pattern="\d+" value="12" />
|
|
||||||
<label for="relatedId">Related ID</label> <input type="text" name="relatedId" id="relatedId" pattern="\d+" />
|
|
||||||
|
|
||||||
<label for="state">State</label>
|
|
||||||
<select name="state" id="state">
|
|
||||||
<option value="NEW">NEW</option>
|
|
||||||
<option value="ACK">ACK</option>
|
|
||||||
<option value="OK">OK</option>
|
|
||||||
<option value="ERR">ERR</option>
|
|
||||||
<option value="DEAD">DEAD</option>
|
|
||||||
</select>
|
|
||||||
<label for="sender">Sender</label> <input type="text" name="sender" id="sender" />
|
|
||||||
<label for="recipient">Recipient</label> <input type="text" name="recipient" id="recipient" />
|
|
||||||
<label for="function">Function</label> <input type="text" name="function" id="function" />
|
|
||||||
<label for="payload">Payload</label> <input type="text" name="payload" id="payload" />
|
|
||||||
<label for="ttl">TTL</label> <input type="text" name="ttl" id="ttl" />
|
|
||||||
<label for="ownerInstance">Owner Instance</label> <input type="text" name="ownerInstance" id="ownerInstance" />
|
|
||||||
<label for="ownerTick" pattern="\d+">Owner Tick</label> <input type="text" name="ownerTick" id="ownerTick" />
|
|
||||||
<div>
|
|
||||||
<input type="submit" value="Save" style="float:left" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input type="button" value="Cancel" style="float:right" onClick="document.getElementById('edit-message').close()"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function editMessage(id) {
|
|
||||||
var message = document.getElementById('edit-message');
|
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('GET', '/messages/' + id, true);
|
|
||||||
xhr.onload = function () {
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
var data = JSON.parse(xhr.responseText);
|
|
||||||
message.querySelector('#edit-message #id').value = data.id;
|
|
||||||
message.querySelector('#edit-message #relatedId').value = data.relatedId;
|
|
||||||
message.querySelector('#edit-message #state').value = data.state;
|
|
||||||
message.querySelector('#edit-message #sender').value = data.sender;
|
|
||||||
message.querySelector('#edit-message #recipient').value = data.recipient;
|
|
||||||
message.querySelector('#edit-message #function').value = data.function;
|
|
||||||
message.querySelector('#edit-message #payload').value = data.payload;
|
|
||||||
message.querySelector('#edit-message #ttl').value = data.ttl;
|
|
||||||
message.querySelector('#edit-message #ownerInstance').value = data.ownerInstance;
|
|
||||||
message.querySelector('#edit-message #ownerTick').value = data.ownerTick;
|
|
||||||
message.showModal();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.send();
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -30,15 +30,14 @@
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>File Name</th>
|
<th>File Name</th>
|
||||||
<th>Type</th>
|
<th>Last Mod</th>
|
||||||
<th>Size</th>
|
<th>Size</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{#each storage.files}}
|
{{#each storage.files}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{{#if downloadable}}<a href="/storage/{{storage.self.storage.id}}/file?name={{filename}}">{{filename}}</a></td>
|
<a href="/storage/{{storage.self.storage.id}}/file?name={{filename}}">{{filename}}</a>
|
||||||
{{else}} {{filename}} {{/if}}
|
<td>{{mTime}}</td>
|
||||||
<td>{{type}}</td>
|
|
||||||
<td>{{size}}</td>
|
<td>{{size}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
Loading…
Reference in New Issue
Block a user