mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-23 21: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.util.*;
|
||||
|
||||
import static nu.marginalia.mq.MqMessageState.NEW;
|
||||
|
||||
@Singleton
|
||||
public class MqPersistence {
|
||||
private final HikariDataSource dataSource;
|
||||
@ -100,12 +102,18 @@ public class MqPersistence {
|
||||
|
||||
/** Modifies the state of a message by id */
|
||||
public void updateMessageState(long id, MqMessageState mqMessageState) throws SQLException {
|
||||
if (NEW == mqMessageState) {
|
||||
reinitializeMessage(id);
|
||||
return;
|
||||
}
|
||||
|
||||
try (var conn = dataSource.getConnection();
|
||||
var stmt = conn.prepareStatement("""
|
||||
UPDATE MESSAGE_QUEUE
|
||||
SET STATE=?, UPDATED_TIME=CURRENT_TIMESTAMP(6)
|
||||
WHERE ID=?
|
||||
""")) {
|
||||
|
||||
stmt.setString(1, mqMessageState.name());
|
||||
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
|
||||
* 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=?
|
||||
LIMIT ?
|
||||
""")
|
||||
) {
|
||||
)
|
||||
{
|
||||
queryStmt.setString(1, inboxName);
|
||||
queryStmt.setInt(2, n);
|
||||
var rs = queryStmt.executeQuery();
|
||||
@ -230,9 +259,44 @@ public class MqPersistence {
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
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,
|
||||
* 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.FileStorageType;
|
||||
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.service.server.*;
|
||||
import org.slf4j.Logger;
|
||||
@ -43,7 +45,8 @@ public class ControlService extends Service {
|
||||
ControlActorService controlActorService,
|
||||
StaticResources staticResources,
|
||||
MessageQueueViewService messageQueueViewService,
|
||||
ControlFileStorageService controlFileStorageService
|
||||
ControlFileStorageService controlFileStorageService,
|
||||
MqPersistence persistence
|
||||
) throws IOException {
|
||||
|
||||
super(params);
|
||||
@ -60,7 +63,9 @@ public class ControlService extends Service {
|
||||
var storageSpecsRenderer = rendererFactory.renderer("control/storage-specs");
|
||||
var storageCrawlsRenderer = rendererFactory.renderer("control/storage-crawls");
|
||||
var storageProcessedRenderer = rendererFactory.renderer("control/storage-processed");
|
||||
|
||||
var storageDetailsRenderer = rendererFactory.renderer("control/storage-details");
|
||||
var updateMessageStateRenderer = rendererFactory.renderer("control/dialog-update-message-state");
|
||||
|
||||
this.controlActorService = controlActorService;
|
||||
|
||||
@ -102,6 +107,14 @@ public class ControlService extends Service {
|
||||
Spark.post("/public/storage/specs", controlActorService::createCrawlSpecification, 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);
|
||||
|
||||
monitors.subscribe(this::logMonitorStateChange);
|
||||
|
@ -1,15 +1,7 @@
|
||||
package nu.marginalia.control.model;
|
||||
|
||||
import nu.marginalia.db.storage.model.FileStorage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record FileStorageFileModel(String filename,
|
||||
String type,
|
||||
String size
|
||||
) {
|
||||
|
||||
public boolean isDownloadable() {
|
||||
return type.equals("file");
|
||||
}
|
||||
String mTime,
|
||||
String size)
|
||||
{
|
||||
}
|
||||
|
@ -116,11 +116,9 @@ public class ControlFileStorageService {
|
||||
|
||||
try (var filesStream = Files.list(storage.asPath())) {
|
||||
filesStream
|
||||
.filter(Files::isRegularFile)
|
||||
.map(this::createFileModel)
|
||||
.sorted(Comparator
|
||||
.comparing(FileStorageFileModel::type)
|
||||
.thenComparing(FileStorageFileModel::filename)
|
||||
)
|
||||
.sorted(Comparator.comparing(FileStorageFileModel::filename))
|
||||
.forEach(files::add);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
@ -132,7 +130,7 @@ public class ControlFileStorageService {
|
||||
|
||||
private FileStorageFileModel createFileModel(Path p) {
|
||||
try {
|
||||
String type = Files.isRegularFile(p) ? "file" : "directory";
|
||||
String mTime = Files.getLastModifiedTime(p).toInstant().toString();
|
||||
String size;
|
||||
if (Files.isDirectory(p)) {
|
||||
size = "-";
|
||||
@ -146,7 +144,7 @@ public class ControlFileStorageService {
|
||||
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) {
|
||||
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>
|
||||
{{#each messages}}
|
||||
<tr>
|
||||
<td>{{stateCode}} {{state}}</td>
|
||||
<td><a onClick="editMessage({{id}})" href="#">{{id}}</a></td>
|
||||
<td>{{stateCode}} <a onClick="updateMsgState({{id}})" href="/message/{{id}}/state">{{state}}</a></td>
|
||||
<td>{{id}}</td>
|
||||
<td>{{recipientInbox}}</td>
|
||||
<td>{{function}}</td>
|
||||
<td title="{{ownerInstanceFull}}">
|
||||
@ -30,61 +30,3 @@
|
||||
</tr>
|
||||
{{/each}}
|
||||
</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>
|
||||
<tr>
|
||||
<th>File Name</th>
|
||||
<th>Type</th>
|
||||
<th>Last Mod</th>
|
||||
<th>Size</th>
|
||||
</tr>
|
||||
{{#each storage.files}}
|
||||
<tr>
|
||||
<td>
|
||||
{{#if downloadable}}<a href="/storage/{{storage.self.storage.id}}/file?name={{filename}}">{{filename}}</a></td>
|
||||
{{else}} {{filename}} {{/if}}
|
||||
<td>{{type}}</td>
|
||||
<a href="/storage/{{storage.self.storage.id}}/file?name={{filename}}">{{filename}}</a>
|
||||
<td>{{mTime}}</td>
|
||||
<td>{{size}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
Loading…
Reference in New Issue
Block a user