Skip to content

Commit

Permalink
add more projection features
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidBadura committed Mar 5, 2024
1 parent 1a70b43 commit e57f368
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 21 deletions.
14 changes: 13 additions & 1 deletion config/routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,21 @@ patchlevel_event_sourcing_admin_projection_show:
controller: Patchlevel\EventSourcingAdminBundle\Controller\ProjectionController::showAction

patchlevel_eventsourcingadmin_projection_rebuild:
path: /projection/rebuild
path: /projection/{id}/rebuild
controller: Patchlevel\EventSourcingAdminBundle\Controller\ProjectionController::rebuildAction

patchlevel_eventsourcingadmin_projection_pause:
path: /projection/{id}/pause
controller: Patchlevel\EventSourcingAdminBundle\Controller\ProjectionController::pauseAction

patchlevel_eventsourcingadmin_projection_reactivate:
path: /projection/{id}/reactivate
controller: Patchlevel\EventSourcingAdminBundle\Controller\ProjectionController::reactivateAction

patchlevel_eventsourcingadmin_projection_remove:
path: /projection/{id}/remove
controller: Patchlevel\EventSourcingAdminBundle\Controller\ProjectionController::removeAction

patchlevel_event_sourcing_admin_inspection_index:
path: /inspection
controller: Patchlevel\EventSourcingAdminBundle\Controller\InspectionController::indexAction
Expand Down
26 changes: 25 additions & 1 deletion public/build/app.css

Large diffs are not rendered by default.

89 changes: 86 additions & 3 deletions src/Controller/ProjectionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

namespace Patchlevel\EventSourcingAdminBundle\Controller;

use Patchlevel\EventSourcing\Projection\Projection\ProjectionStatus;
use Patchlevel\EventSourcing\Projection\Projection\RunMode;
use Patchlevel\EventSourcing\Projection\Projectionist\Projectionist;
use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria;
use Patchlevel\EventSourcing\Store\Store;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;
Expand All @@ -22,15 +25,51 @@ public function __construct(
) {
}

public function showAction(): Response
public function showAction(Request $request): Response
{
$projections = $this->projectionist->projections();
$messageCount = $this->store->count();

$groups = [];

foreach ($projections as $projection) {
$groups[$projection->group()] = true;
}

$filteredProjections = [];
$search = $request->get('search');
$group = $request->get('group');
$mode = $request->get('mode');
$status = $request->get('status');


foreach ($projections as $projection) {
if ($search && !str_contains($projection->id(), $search)) {
continue;
}

if ($group && $projection->group() !== $group) {
continue;
}

if ($mode && $projection->runMode()->value !== $mode) {
continue;
}

if ($status && $projection->status()->value !== $status) {
continue;
}

$filteredProjections[] = $projection;
}

return new Response(
$this->twig->render('@PatchlevelEventSourcingAdmin/projection/show.html.twig', [
'projections' => $projections,
'projections' => $filteredProjections,
'messageCount' => $messageCount,
'statuses' => array_map(fn (ProjectionStatus $status) => $status->value, ProjectionStatus::cases()),
'modes' => array_map(fn (RunMode $mode) => $mode->value, RunMode::cases()),
'groups' => array_keys($groups),
]),
);
}
Expand All @@ -43,7 +82,51 @@ public function rebuildAction(string $id): Response
$this->projectionist->boot($criteria);

return new RedirectResponse(
$this->router->generate('patchlevel_eventsourcing_admin_projection_show'),
$this->router->generate('patchlevel_event_sourcing_admin_projection_show'),
);
}

public function pauseAction(string $id): Response
{
$criteria = new ProjectionistCriteria([$id]);

$this->projectionist->pause($criteria);

return new RedirectResponse(
$this->router->generate('patchlevel_event_sourcing_admin_projection_show'),
);
}

public function bootAction(string $id): Response
{
$criteria = new ProjectionistCriteria([$id]);

$this->projectionist->boot($criteria);

return new RedirectResponse(
$this->router->generate('patchlevel_event_sourcing_admin_projection_show'),
);
}

public function reactivateAction(string $id): Response
{
$criteria = new ProjectionistCriteria([$id]);

$this->projectionist->reactivate($criteria);

return new RedirectResponse(
$this->router->generate('patchlevel_event_sourcing_admin_projection_show'),
);
}

public function removeAction(string $id): Response
{
$criteria = new ProjectionistCriteria([$id]);

$this->projectionist->remove($criteria);

return new RedirectResponse(
$this->router->generate('patchlevel_event_sourcing_admin_projection_show'),
);
}
}
38 changes: 24 additions & 14 deletions templates/projection/detail.html.twig
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
<dl class="divide-y divide-gray-100">
{{ _self.text('ID', projection.id) }}
{{ _self.text('Group', projection.group) }}
{{ _self.text('Run mode', projection.runMode.value) }}
{{ _self.text('Run mode', projection.runMode.value|replace({'_': ' '})) }}
{# { _self.text('Class', 'unknown') } #}
{{ _self.text('Position', projection.position) }}
{{ _self.text('Status', projection.status.value) }}

{{ dump(projection) }}
{{ _self.text('Retry attempt', projection.retryAttempt) }}

{{ _self.text('Updated at', projection.lastSavedAt|date('Y-m-d H:i:s')) }}

{% if projection.projectionError %}
{% if projection.projectionError.errorContext %}
{% for context in projection.projectionError.errorContext %}
<div class="rounded-md bg-gray-50 p-4">
<div class="flex">
<div class="rounded-md bg-gray-50 overflow-hidden">
<div class="flex bg-red-600 p-4">
<div class="flex-shrink-0">
{{ heroicon('exclamation-circle', class='h-5 w-5 text-red-800') }}
{{ heroicon('exclamation-circle', class='h-5 w-5 text-white') }}
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">{{ context.message }}</h3>
<h3 class="text-sm font-medium text-white">{{ context.message }}</h3>
</div>
</div>

{% for step in context.trace %}
<div><span title="{{ step.class }}">{{~ _self.shortClass(step.class) ~}}</span>{{ step.type }}{{ step.function }}({{ _self.args(step.args) }})</div>
<div>in {{ step.file }} (line {{ step.line }})</div>
<div class="rounded-md bg-gray-50 p-4">
{% for step in context.trace %}
<div class="py-2">
{% if step.class is defined %}
<div class="text-gray-900" title="{{ step.class }}">
{{~ step.class|split('\\')|last ~}}{{~ step.type ~}}{{~ step.function ~}}(...)
</div>
{% else %}
<div class="text-gray-900">
<span title="{{ step.function }}">{{ step.function }}(...)</span>
</div>
{% endif %}
<div class="text-xs text-gray-500">in {{ step.file }} (line {{ step.line }})</div>
</div>
{% endfor %}

{{ dump(step) }}
{% endfor %}
</div>
</div>


Expand Down Expand Up @@ -69,6 +82,3 @@
{{ args|join(', ') }}
{% endmacro %}

{% macro shortClass(class) ~%}
{{~ class|split('\\')|last ~}}
{%~ endmacro %}
99 changes: 97 additions & 2 deletions templates/projection/show.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,76 @@
Projections
</div>

<div class="">
<form class="flex gap-2 items-end" action="{{ path('patchlevel_event_sourcing_admin_projection_show') }}">
<div class="w-48">
<label for="search" class="block text-xs font-medium leading-6 text-gray-900">Search</label>
<div class="mt-1">
<input type="text" name="search" id="search"
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder="Search..."
value="{{ app.request.get('search') }}">
</div>
</div>

<div class="w-48">
<label for="group" class="block text-xs font-medium leading-6 text-gray-900">Group</label>
<select id="group" name="group"
class="mt-1 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6">
<option {% if app.request.get('group') == "" %}selected{% endif %}
value="">-- choose --
</option>
{% for group in groups|default([]) %}
<option {% if app.request.get('group') == group %}selected{% endif %}
value="{{ group }}">{{ group }}</option>
{% endfor %}
</select>
</div>


<div class="w-48">
<label for="mode" class="block text-xs font-medium leading-6 text-gray-900">Run mode</label>
<select id="mode" name="mode"
class="mt-1 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6">
<option {% if app.request.get('mode') == "" %}selected{% endif %}
value="">-- choose --
</option>
{% for mode in modes|default([]) %}
<option {% if app.request.get('mode') == mode %}selected{% endif %}
value="{{ mode }}">{{ mode|replace({'_': ' '}) }}</option>
{% endfor %}
</select>
</div>


<div class="w-48">
<label for="status" class="block text-xs font-medium leading-6 text-gray-900">Status</label>
<select id="status" name="status"
class="mt-1 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6">
<option {% if app.request.get('status') == "" %}selected{% endif %}
value="">-- choose --
</option>
{% for status in statuses|default([]) %}
<option {% if app.request.get('status') == status %}selected{% endif %}
value="{{ status }}">{{ status }}</option>
{% endfor %}
</select>
</div>

<button type="submit"
class="rounded-md bg-indigo-600 px-2.5 py-1.5 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
{{ heroicon('funnel', 'h-6 w-6') }}
</button>

{% if app.request.get('search') or app.request.get('group') or app.request.get('mode') or app.request.get('status') %}
<a class="rounded-md bg-red-600 px-2.5 py-1.5 font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
href="{{ path('patchlevel_event_sourcing_admin_projection_show') }}">
{{ heroicon('backspace', 'h-6 w-6') }}
</a>
{% endif %}
</form>
</div>

<div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
Expand All @@ -32,6 +102,9 @@
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Status
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Updated At
</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6"></th>
</tr>
</thead>
Expand All @@ -45,9 +118,9 @@
{{ projection.group }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{{ projection.runMode.value }}
{{ projection.runMode.value|replace({'_': ' '}) }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 text-right">
<td class="whitespace-nowrap px-3 py-4 text-sm {{ projection.position < messageCount ? 'text-yellow-500' : 'text-gray-500'}} text-right">
{{ projection.position }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
Expand All @@ -57,12 +130,19 @@
{{ _self.badge('Booting', 'fill-yellow-500') }}
{% elseif projection.status.value == 'active' %}
{{ _self.badge('Active', 'fill-green-500') }}
{% elseif projection.status.value == 'finished' %}
{{ _self.badge('Finished', 'fill-green-500') }}
{% elseif projection.status.value == 'paused' %}
{{ _self.badge('Paused', 'fill-gray-500') }}
{% elseif projection.status.value == 'outdated' %}
{{ _self.badge('Outdated', 'fill-gray-500') }}
{% elseif projection.status.value == 'error' %}
{{ _self.badge('Error', 'fill-red-500') }}
{% endif %}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{{ projection.lastSavedAt|date('Y-m-d H:i:s') }}
</td>
<td class="relative flex gap-2 whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm sm:pr-6">
<a href="#" class="text-gray-500 hover:text-gray-900"
data-modal="modal-{{ loop.index }}">
Expand All @@ -80,6 +160,21 @@
projection: projection,
}) }}
</dialog>
{% if projection.status.value in ['active', 'error'] %}
<a href="{{ path('patchlevel_eventsourcingadmin_projection_pause', {id: projection.id}) }}" class="text-gray-500 hover:text-gray-900">
{{ heroicon('pause-circle', 'h-5 w-5') }}
</a>
{% endif %}
{% if projection.status.value in ['paused', 'error', 'finished', 'outdated'] %}
<a href="{{ path('patchlevel_eventsourcingadmin_projection_reactivate', {id: projection.id}) }}" class="text-gray-500 hover:text-gray-900">
{{ heroicon('play-circle', 'h-5 w-5') }}
</a>
{% endif %}
{% if projection.status.value in ['paused', 'error', 'outdated'] %}
<a href="{{ path('patchlevel_eventsourcingadmin_projection_remove', {id: projection.id}) }}" class="text-gray-500 hover:text-gray-900">
{{ heroicon('trash', 'h-5 w-5') }}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
Expand Down
11 changes: 11 additions & 0 deletions templates/store/show.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@
class="text-gray-500 hover:text-gray-900 invisible group-hover/parent:visible">
{{ heroicon('identification', 'h-5 w-5 inline') }}
</a>
<a href="#" data-clipboard="{{ message.aggregateId }}"
class="text-gray-500 hover:text-gray-900 invisible group-hover/parent:visible">
{{ heroicon('clipboard', 'h-5 w-5 inline') }}
</a>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 text-right">{{ message.playhead }}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 group/parent">
Expand Down Expand Up @@ -155,6 +159,13 @@
});
});
});
document.querySelectorAll("[data-clipboard]").forEach(function(button) {
button.addEventListener("click", function(event) {
event.preventDefault();
navigator.clipboard.writeText(button.dataset.clipboard);
});
});
</script>

{% endblock %}

0 comments on commit e57f368

Please sign in to comment.