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
4 changes: 2 additions & 2 deletions config/routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ patchlevel_event_sourcing_dashboard_default_index:
path: /
controller: Patchlevel\EventSourcingDashboardBundle\Controller\DefaultController::indexAction

patchlevel_event_sourcing_dashboard_default_dashboard:
patchlevel_event_sourcing_dashboard_dashboard_index:
path: /dashboard
controller: Patchlevel\EventSourcingDashboardBundle\Controller\DefaultController::dashboardAction
controller: Patchlevel\EventSourcingDashboardBundle\Controller\DashboardController::indexAction

patchlevel_event_sourcing_dashboard_store_show:
path: /store
Expand Down
43 changes: 43 additions & 0 deletions docs/dashboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Dashboard

The dashboard view is the landing page of the bundle. It gives you an overview of your event
sourcing setup at a glance, before you drill down into the store, a single aggregate or your
subscriptions. The index route of the bundle redirects here.

## Overview

Four cards summarize the current state of your system:

* **Events in store**: how many messages your event store holds in total
* **Aggregates**: how many aggregate roots are registered
* **Registered events**: how many event classes are registered
* **Subscriptions**: how many subscriptions the subscription engine knows about

Each card links to the view where you can explore that number in detail.

## Subscription health

Below the cards you see how your subscriptions are distributed across the statuses of the
subscription engine, for example `active`, `booting`, `paused` or `error`. Statuses without any
subscription are hidden, so in a healthy system you only see `active`.

Every status is a link into the [subscriptions](subscriptions.md) view, pre-filtered to that
status. This makes the dashboard a good starting point when something goes wrong: a red `error`
or `failed` pill takes you straight to the subscriptions that need attention.

:::tip
If you only ever want to look at one thing, look at this row. Everything else on the page is
informational, but a non-empty `error` or `failed` count means events are not being processed.
:::

## Latest events

The last recorded events are listed at the bottom, newest first, with the same columns as the
[store](store.md) view. From here you can jump into the inspection of the aggregate or stream
that recorded an event, or open the full store listing.

## Learn more

* [How to browse and filter the event store](store.md)
* [How to inspect a single aggregate](inspection.md)
* [How to manage subscriptions](subscriptions.md)
1 change: 1 addition & 0 deletions docs/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
{
"title": "Dashboard",
"subEntries": [
{ "title": "Dashboard", "file": "dashboard.md" },
{ "title": "Store", "file": "store.md" },
{ "title": "Inspection", "file": "inspection.md" },
{ "title": "Events", "file": "events.md" },
Expand Down
6 changes: 3 additions & 3 deletions docs/store.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Store

The store view is the landing page of the dashboard. It lists the messages in your event store in
reverse order, newest first, so you can see what your application recorded most recently. The index
route of the bundle redirects here.
The store view lists the messages in your event store in reverse order, newest first, so you can
see what your application recorded most recently. It is reachable from the sidebar and from the
[dashboard](dashboard.md) overview.

![The store view](screenshot1.png)

Expand Down
81 changes: 81 additions & 0 deletions public/build/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,9 @@ input:where([type='file']):focus {
.mt-3 {
margin-top: 0.75rem;
}
.mt-4 {
margin-top: 1rem;
}
.mt-5 {
margin-top: 1.25rem;
}
Expand Down Expand Up @@ -873,6 +876,9 @@ input:where([type='file']):focus {
.flow-root {
display: flow-root;
}
.grid {
display: grid;
}
.hidden {
display: none;
}
Expand Down Expand Up @@ -993,9 +999,15 @@ input:where([type='file']):focus {
.cursor-help {
cursor: help;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.place-content-between {
place-content: space-between;
}
Expand Down Expand Up @@ -1023,13 +1035,20 @@ input:where([type='file']):focus {
.gap-2 {
gap: 0.5rem;
}
.gap-3 {
gap: 0.75rem;
}
.gap-4 {
gap: 1rem;
}
.gap-x-1\.5 {
-moz-column-gap: 0.375rem;
column-gap: 0.375rem;
}
.gap-x-2 {
-moz-column-gap: 0.5rem;
column-gap: 0.5rem;
}
.gap-x-3 {
-moz-column-gap: 0.75rem;
column-gap: 0.75rem;
Expand Down Expand Up @@ -1163,6 +1182,10 @@ input:where([type='file']):focus {
--tw-bg-opacity: 1;
background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1));
}
.bg-green-50 {
--tw-bg-opacity: 1;
background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1));
}
.bg-indigo-600 {
--tw-bg-opacity: 1;
background-color: rgb(79 70 229 / var(--tw-bg-opacity, 1));
Expand All @@ -1186,6 +1209,10 @@ input:where([type='file']):focus {
.bg-white\/10 {
background-color: rgb(255 255 255 / 0.1);
}
.bg-yellow-50 {
--tw-bg-opacity: 1;
background-color: rgb(254 252 232 / var(--tw-bg-opacity, 1));
}
.bg-opacity-75 {
--tw-bg-opacity: 0.75;
}
Expand All @@ -1210,6 +1237,9 @@ input:where([type='file']):focus {
.p-2\.5 {
padding: 0.625rem;
}
.p-3 {
padding: 0.75rem;
}
.p-4 {
padding: 1rem;
}
Expand Down Expand Up @@ -1326,6 +1356,10 @@ input:where([type='file']):focus {
font-size: 1.5rem;
line-height: 2rem;
}
.text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
Expand Down Expand Up @@ -1355,6 +1389,13 @@ input:where([type='file']):focus {
.font-semibold {
font-weight: 600;
}
.capitalize {
text-transform: capitalize;
}
.tabular-nums {
--tw-numeric-spacing: tabular-nums;
font-variant-numeric: var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction);
}
.leading-6 {
line-height: 1.5rem;
}
Expand All @@ -1369,6 +1410,10 @@ input:where([type='file']):focus {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity, 1));
}
.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
}
.text-gray-700 {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity, 1));
Expand All @@ -1381,6 +1426,10 @@ input:where([type='file']):focus {
--tw-text-opacity: 1;
color: rgb(22 163 74 / var(--tw-text-opacity, 1));
}
.text-green-700 {
--tw-text-opacity: 1;
color: rgb(21 128 61 / var(--tw-text-opacity, 1));
}
.text-indigo-200 {
--tw-text-opacity: 1;
color: rgb(199 210 254 / var(--tw-text-opacity, 1));
Expand All @@ -1393,6 +1442,10 @@ input:where([type='file']):focus {
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity, 1));
}
.text-red-700 {
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity, 1));
}
.text-red-800 {
--tw-text-opacity: 1;
color: rgb(153 27 27 / var(--tw-text-opacity, 1));
Expand All @@ -1405,6 +1458,10 @@ input:where([type='file']):focus {
--tw-text-opacity: 1;
color: rgb(234 179 8 / var(--tw-text-opacity, 1));
}
.text-yellow-800 {
--tw-text-opacity: 1;
color: rgb(133 77 14 / var(--tw-text-opacity, 1));
}
.opacity-0 {
opacity: 0;
}
Expand Down Expand Up @@ -1451,13 +1508,25 @@ input:where([type='file']):focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity, 1));
}
.ring-gray-500\/10 {
--tw-ring-color: rgb(107 114 128 / 0.1);
}
.ring-gray-900\/5 {
--tw-ring-color: rgb(17 24 39 / 0.05);
}
.ring-green-600\/20 {
--tw-ring-color: rgb(22 163 74 / 0.2);
}
.ring-red-600\/20 {
--tw-ring-color: rgb(220 38 38 / 0.2);
}
.ring-white {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity, 1));
}
.ring-yellow-600\/20 {
--tw-ring-color: rgb(202 138 4 / 0.2);
}
.ring-opacity-5 {
--tw-ring-opacity: 0.05;
}
Expand Down Expand Up @@ -1526,6 +1595,10 @@ input:where([type='file']):focus {
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
}
.hover\:text-indigo-500:hover {
--tw-text-opacity: 1;
color: rgb(99 102 241 / var(--tw-text-opacity, 1));
}
.hover\:text-white:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
Expand Down Expand Up @@ -1633,6 +1706,10 @@ input:where([type='file']):focus {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}

.sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}

.sm\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
Expand Down Expand Up @@ -1735,6 +1812,10 @@ input:where([type='file']):focus {
width: 18rem;
}

.lg\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}

.lg\:flex-col {
flex-direction: column;
}
Expand Down
66 changes: 66 additions & 0 deletions src/Controller/DashboardController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcingDashboardBundle\Controller;

use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry;
use Patchlevel\EventSourcing\Metadata\Event\EventRegistry;
use Patchlevel\EventSourcing\Store\Store;
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
use Patchlevel\EventSourcing\Subscription\Status;
use Patchlevel\EventSourcing\Subscription\Subscription;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;

use function count;

final readonly class DashboardController
{
private const LATEST_MESSAGE_LIMIT = 10;

public function __construct(
private Environment $twig,
private Store $store,
private AggregateRootRegistry $aggregateRootRegistry,
private EventRegistry $eventRegistry,
private SubscriptionEngine $engine,
) {
}

public function indexAction(): Response
{
$subscriptions = $this->engine->subscriptions();

return new Response(
$this->twig->render('@PatchlevelEventSourcingDashboard/dashboard/index.html.twig', [
'messageCount' => $this->store->count(),
'aggregateCount' => count($this->aggregateRootRegistry->aggregateNames()),
'eventCount' => count($this->eventRegistry->eventNames()),
'subscriptionCount' => count($subscriptions),
'subscriptionStatuses' => $this->countByStatus($subscriptions),
'messages' => $this->store->load(null, self::LATEST_MESSAGE_LIMIT, 0, true),
]),
);
}

/**
* @param list<Subscription> $subscriptions
*
* @return array<string, int>
*/
private function countByStatus(array $subscriptions): array
{
$counts = [];

foreach (Status::cases() as $status) {
$counts[$status->value] = 0;
}

foreach ($subscriptions as $subscription) {
$counts[$subscription->status()->value]++;
}

return $counts;
}
}
2 changes: 1 addition & 1 deletion src/Controller/DefaultController.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public function __construct(
public function indexAction(): Response
{
return new RedirectResponse(
$this->router->generate('patchlevel_event_sourcing_dashboard_store_show'),
$this->router->generate('patchlevel_event_sourcing_dashboard_dashboard_index'),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Patchlevel\EventSourcing\Snapshot\SnapshotStore;
use Patchlevel\EventSourcing\Store\Store;
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
use Patchlevel\EventSourcingDashboardBundle\Controller\DashboardController;
use Patchlevel\EventSourcingDashboardBundle\Controller\DefaultController;
use Patchlevel\EventSourcingDashboardBundle\Controller\EventController;
use Patchlevel\EventSourcingDashboardBundle\Controller\InspectionController;
Expand Down Expand Up @@ -56,13 +57,23 @@
}

$container->register(DefaultController::class)
->setArguments([

Check warning on line 60 in src/DependencyInjection/PatchlevelEventSourcingDashboardExtension.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ } $container->register(DefaultController::class) - ->setArguments([ - new Reference(RouterInterface::class), - ]) + ->setArguments([]) ->addTag('controller.service_arguments'); $container->register(DashboardController::class)
new Reference(RouterInterface::class),
])
->addTag('controller.service_arguments');

$container->register(DashboardController::class)
->setArguments([

Check warning on line 66 in src/DependencyInjection/PatchlevelEventSourcingDashboardExtension.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ $container->register(DashboardController::class) ->setArguments([ - new Reference('twig'), new Reference(Store::class), new Reference(AggregateRootRegistry::class), new Reference(EventRegistry::class),

Check warning on line 66 in src/DependencyInjection/PatchlevelEventSourcingDashboardExtension.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ $container->register(DashboardController::class) ->setArguments([ - new Reference('twig'), new Reference(Store::class), new Reference(AggregateRootRegistry::class), new Reference(EventRegistry::class),
new Reference('twig'),
new Reference(Store::class),
new Reference(AggregateRootRegistry::class),
new Reference(EventRegistry::class),
new Reference(SubscriptionEngine::class),
])
->addTag('controller.service_arguments');

$container->register(StoreController::class)
->setArguments([

Check warning on line 76 in src/DependencyInjection/PatchlevelEventSourcingDashboardExtension.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ $container->register(StoreController::class) ->setArguments([ - new Reference('twig'), new Reference(Store::class), new Reference(AggregateRootRegistry::class), new Reference(EventRegistry::class),
new Reference('twig'),
new Reference(Store::class),
new Reference(AggregateRootRegistry::class),
Expand All @@ -71,7 +82,7 @@
->addTag('controller.service_arguments');

$container->register(InspectionController::class)
->setArguments([

Check warning on line 85 in src/DependencyInjection/PatchlevelEventSourcingDashboardExtension.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ $container->register(InspectionController::class) ->setArguments([ - new Reference('twig'), new Reference(RouterInterface::class), new Reference(Store::class), new Reference(AggregateRootRegistry::class),
new Reference('twig'),
new Reference(RouterInterface::class),
new Reference(Store::class),
Expand All @@ -83,7 +94,7 @@
->addTag('controller.service_arguments');

$container->register(SubscriptionController::class)
->setArguments([

Check warning on line 97 in src/DependencyInjection/PatchlevelEventSourcingDashboardExtension.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ $container->register(SubscriptionController::class) ->setArguments([ - new Reference('twig'), new Reference(SubscriptionEngine::class), new Reference(Store::class), new Reference(RouterInterface::class),
new Reference('twig'),
new Reference(SubscriptionEngine::class),
new Reference(Store::class),
Expand All @@ -92,7 +103,7 @@
->addTag('controller.service_arguments');

$container->register(EventController::class)
->setArguments([

Check warning on line 106 in src/DependencyInjection/PatchlevelEventSourcingDashboardExtension.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ $container->register(EventController::class) ->setArguments([ - new Reference('twig'), new Reference(EventRegistry::class), new Reference(ListenerProvider::class, ContainerInterface::NULL_ON_INVALID_REFERENCE), new TaggedIteratorArgument('event_sourcing.subscriber'),
new Reference('twig'),
new Reference(EventRegistry::class),
new Reference(ListenerProvider::class, ContainerInterface::NULL_ON_INVALID_REFERENCE),
Expand All @@ -101,8 +112,8 @@
])
->addTag('controller.service_arguments');

$container->register(EventSourcingDashboardExtension::class)

Check warning on line 115 in src/DependencyInjection/PatchlevelEventSourcingDashboardExtension.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ ]) ->addTag('controller.service_arguments'); - $container->register(EventSourcingDashboardExtension::class) - ->setArguments([ - new Reference(AggregateRootRegistry::class), - new Reference(EventRegistry::class), - new Reference(EventSerializer::class), - new Reference(TokenMapper::class), - new Reference(Store::class), - ]) - ->addTag('twig.extension'); + $container->register(HeroiconsExtension::class) ->addTag('twig.extension');
->setArguments([

Check warning on line 116 in src/DependencyInjection/PatchlevelEventSourcingDashboardExtension.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ $container->register(EventSourcingDashboardExtension::class) ->setArguments([ - new Reference(AggregateRootRegistry::class), new Reference(EventRegistry::class), new Reference(EventSerializer::class), new Reference(TokenMapper::class),
new Reference(AggregateRootRegistry::class),
new Reference(EventRegistry::class),
new Reference(EventSerializer::class),
Expand All @@ -111,7 +122,7 @@
])
->addTag('twig.extension');

$container->register(HeroiconsExtension::class)

Check warning on line 125 in src/DependencyInjection/PatchlevelEventSourcingDashboardExtension.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ ]) ->addTag('twig.extension'); - $container->register(HeroiconsExtension::class) - ->addTag('twig.extension'); + $container->register('event_sourcing_dashboard.expression_language', ExpressionLanguage::class);
->addTag('twig.extension');

$container->register('event_sourcing_dashboard.expression_language', ExpressionLanguage::class);
Expand Down
Loading
Loading