Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ Install with Composer:
composer require nails/module-find-replace
```

Admin searches are queued. `FIND_REPLACE_QUEUE` chooses the queue; when it is unset the search uses `default`. The Find & Replace index warns when that queue has no live worker, using the same check as the queue dashboard alert.

See [the documentation](https://docs.nailsapp.co.uk/modules/find-replace).
21 changes: 16 additions & 5 deletions admin/views/FindReplace/index.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
<?php

/** @var string|null $queueWarning */

$apiUrl = siteUrl('api/find-replace/search');

?>
<div id="find-replace" data-api="<?=htmlspecialchars($apiUrl)?>">
<?php

if (is_string($queueWarning) && $queueWarning !== '') {
?>
<div class="alert alert-warning"><?=htmlspecialchars($queueWarning)?></div>
<?php
}

?>
<p>Search saved content, then replace matches from the results screen. A search and its rows are kept or deleted together.</p>
<div id="find-replace-alert" class="alert" hidden></div>
<?=form_open(null, 'id="find-replace-form"')?>
Expand Down Expand Up @@ -51,11 +62,11 @@
<thead class="table-dark">
<tr>
<th>Term</th>
<th style="width: 200px;">Status</th>
<th style="width: 200px;">Progress</th>
<th style="width: 200px;">Results</th>
<th style="width: 200px;">Last run</th>
<th class="actions" style="width: 200px;">Actions</th>
<th class="find-replace-status">Status</th>
<th class="find-replace-progress">Progress</th>
<th class="find-replace-results">Results</th>
<th class="find-replace-last-run">Last run</th>
<th class="actions">Actions</th>
</tr>
</thead>
<tbody id="find-replace-searches">
Expand Down
2 changes: 1 addition & 1 deletion assets/css/admin.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions assets/sass/admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@

.find-replace-searches {
margin-top: 2.5rem;

.find-replace-status,
.find-replace-progress,
.find-replace-results,
.find-replace-last-run,
.actions {
width: 250px;
}
}

.find-replace-detail__term {
Expand Down
8 changes: 8 additions & 0 deletions services/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Nails\FindReplace\Resource;
use Nails\FindReplace\Service\Access;
use Nails\FindReplace\Service\Engine;
use Nails\FindReplace\Service\Queue;
use Nails\FindReplace\Service\Registry;
use Nails\FindReplace\Service\Schema;

Expand Down Expand Up @@ -56,6 +57,13 @@

return new Access($searches, $results, $changes, $database);
},
'Queue' => function (): Queue {
if (class_exists('\App\FindReplace\Service\Queue')) {
return new \App\FindReplace\Service\Queue();
}

return new Queue();
},
],
'models' => [
'Search' => function (): Model\Search {
Expand Down
32 changes: 22 additions & 10 deletions src/Admin/Controller/FindReplace.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@
use Nails\FindReplace\Model\Result;
use Nails\FindReplace\Model\Search;
use Nails\FindReplace\Output\Csv;
use Nails\FindReplace\Queue\Task\Search as SearchTask;
use Nails\FindReplace\Resource;
use Nails\FindReplace\Service\Access;
use Nails\FindReplace\Service\Engine;
use Nails\FindReplace\Service\Queue;
use Nails\FindReplace\Service\Registry;
use Nails\Queue\Interface\Data;

class FindReplace extends Base
{
Expand All @@ -52,6 +51,7 @@ public function index(): void
$this
->addBreadcrumb('Utilities')
->addBreadcrumb('Find & Replace')
->setData('queueWarning', $this->searchQueue()->warning())
->loadView('index');
}

Expand All @@ -67,6 +67,8 @@ public function create(): void
try {
$include = $this->tableEntries($input, 'include_tables');
$exclude = $this->tableEntries($input, 'exclude_tables');
$queue = $this->searchQueue();
$queue->assertAvailable();
$id = $this->searches()->open(
$this->userId(),
$this->requiredTerm($input),
Expand All @@ -75,7 +77,7 @@ public function create(): void
$include,
$exclude,
);
$this->queue($id);
$queue->push($id);
} catch (\InvalidArgumentException $exception) {
$this->oUserFeedback->error($exception->getMessage());
redirect(self::url());
Expand Down Expand Up @@ -257,9 +259,20 @@ public function rerun(): void
{
$this->requireSearchPermission();
$search = $this->requireOwned();
$queue = $this->searchQueue();

try {
$queue->assertAvailable();
} catch (\InvalidArgumentException $exception) {
$this->oUserFeedback->error($exception->getMessage());
redirect(self::url('results') . '?id=' . (int) $search->id);

return;
}

$this->searches()->markPending((int) $search->id);
$this->resultModel()->clear((int) $search->id);
$this->queue((int) $search->id);
$queue->push((int) $search->id);
$this->oUserFeedback->success('Search queued again.');
redirect(self::url('results') . '?id=' . (int) $search->id);
}
Expand Down Expand Up @@ -410,16 +423,15 @@ private function userId(): int
return (int) $id;
}

private function queue(int $id): void
private function searchQueue(): Queue
{
$manager = Factory::service('Manager', \Nails\Queue\Constants::MODULE_SLUG);
$data = Factory::factory('Data', \Nails\Queue\Constants::MODULE_SLUG, $id);
$queue = Factory::service('Queue', Constants::MODULE_SLUG);

if (!method_exists($manager, 'push') || !$data instanceof Data) {
throw new \RuntimeException('Queue is not available');
if (!$queue instanceof Queue) {
throw new \RuntimeException('Find-replace queue is not available');
}

$manager->push(new SearchTask(), $data);
return $queue;
}

/**
Expand Down
28 changes: 18 additions & 10 deletions src/Api/Controller/Search.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@
use Nails\FindReplace\Exclusion\Pattern;
use Nails\FindReplace\Model\Result;
use Nails\FindReplace\Model\Search as SearchModel;
use Nails\FindReplace\Queue\Task\Search as SearchTask;
use Nails\FindReplace\Resource;
use Nails\Queue\Interface\Data;
use Nails\FindReplace\Service\Queue;

class Search extends \Nails\Api\Controller\Base
{
Expand Down Expand Up @@ -51,6 +50,8 @@ public function postIndex(): ApiResponse
$input = $this->input();

try {
$queue = $this->searchQueue();
$queue->assertAvailable();
$id = $this->searches()->open(
$this->userId(),
$this->requiredTerm($input),
Expand All @@ -59,7 +60,7 @@ public function postIndex(): ApiResponse
$this->tableEntries($input['include_tables'] ?? null),
$this->tableEntries($input['exclude_tables'] ?? null),
);
$this->queue($id);
$queue->push($id);
} catch (\InvalidArgumentException $exception) {
throw new ValidationException($exception->getMessage());
}
Expand All @@ -70,9 +71,17 @@ public function postIndex(): ApiResponse
public function postRerun(): ApiResponse
{
$id = (int) $this->owned($this->requestId($this->input()))->id;
$queue = $this->searchQueue();

try {
$queue->assertAvailable();
} catch (\InvalidArgumentException $exception) {
throw new ValidationException($exception->getMessage());
}

$this->searches()->markPending($id);
$this->results()->clear($id);
$this->queue($id);
$queue->push($id);

return $this->respond($this->payload($this->owned($id)));
}
Expand Down Expand Up @@ -249,16 +258,15 @@ private function userId(): int
return (int) $id;
}

private function queue(int $id): void
private function searchQueue(): Queue
{
$manager = Factory::service('Manager', \Nails\Queue\Constants::MODULE_SLUG);
$data = Factory::factory('Data', \Nails\Queue\Constants::MODULE_SLUG, $id);
$queue = Factory::service('Queue', Constants::MODULE_SLUG);

if (!method_exists($manager, 'push') || !$data instanceof Data) {
throw new \RuntimeException('Queue is not available');
if (!$queue instanceof Queue) {
throw new \RuntimeException('Find-replace queue is not available');
}

$manager->push(new SearchTask(), $data);
return $queue;
}

/**
Expand Down
110 changes: 110 additions & 0 deletions src/Service/Queue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

namespace Nails\FindReplace\Service;

use Nails\Config;
use Nails\Factory;
use Nails\FindReplace\Queue\Task\Search as SearchTask;
use Nails\Queue\Interface\Data;
use Nails\Queue\Service\Manager;

class Queue
{
public const CONFIG_KEY = 'FIND_REPLACE_QUEUE';

public function __construct(private ?Manager $manager = null)
{
}

/**
* Alias or queue class from FIND_REPLACE_QUEUE. Blank and non-strings use `default`.
*
* @phpstan-impure
*/
public function name(): string
{
$configured = Config::get(self::CONFIG_KEY, 'default');

if (!is_string($configured)) {
return 'default';
}

$configured = trim($configured);

return $configured === '' ? 'default' : $configured;
}

/**
* @throws \InvalidArgumentException when FIND_REPLACE_QUEUE does not name a queue
*/
public function assertAvailable(): void
{
try {
$this->manager()->resolveQueue($this->name());
} catch (\InvalidArgumentException | \TypeError $exception) {
throw new \InvalidArgumentException(
sprintf('Find & Replace is set to use the queue "%s", which is not available.', $this->name()),
0,
$exception,
);
}
}

public function push(int $id): void
{
$this->assertAvailable();
$data = Factory::factory('Data', \Nails\Queue\Constants::MODULE_SLUG, $id);

if (!$data instanceof Data) {
throw new \RuntimeException('Queue is not available');
}

$this->manager()->push(new SearchTask(), $data, $this->name());
}

/**
* Warning for the index when this queue has no live worker, or is not a queue.
* Liveness is Manager::isProcessed(), the same check as the queue dashboard alert.
*/
public function warning(): ?string
{
$manager = $this->manager();

try {
$queue = $manager->resolveQueue($this->name());
} catch (\InvalidArgumentException | \TypeError) {
return sprintf(
'Find & Replace is set to use the queue "%s", which is not available. Searches cannot be queued.',
$this->name(),
);
}

if ($manager->isProcessed($queue)) {

Check failure on line 82 in src/Service/Queue.php

View workflow job for this annotation

GitHub Actions / Static Analysis

Call to an undefined method Nails\Queue\Service\Manager::isProcessed().
return null;
}

$name = $this->name();
$class = $queue::class;
$label = $name === $class ? $class : $name . ' (' . $class . ')';

return sprintf(
'There are no queue workers running for %s. Searches will stay pending until a worker is processing that queue.',
$label,
);
}

private function manager(): Manager
{
if ($this->manager instanceof Manager) {
return $this->manager;
}

$manager = Factory::service('Manager', \Nails\Queue\Constants::MODULE_SLUG);

if (!$manager instanceof Manager) {
throw new \RuntimeException('Queue is not available');
}

return $this->manager = $manager;
}
}
Loading
Loading