Skip to content

Commit 999965a

Browse files
Introduce new NotificationHistoryList and detail-view
1 parent 4eb392a commit 999965a

16 files changed

Lines changed: 949 additions & 5 deletions
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 Icinga GmbH <https://icinga.com>
4+
// SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
namespace Icinga\Module\Notifications\Controllers;
7+
8+
use Icinga\Module\Notifications\Common\Auth;
9+
use Icinga\Module\Notifications\Common\Database;
10+
use Icinga\Module\Notifications\Model\NotificationHistory;
11+
use Icinga\Module\Notifications\Widget\Detail\ObjectHeader;
12+
use Icinga\Module\Notifications\Widget\Detail\NotificationHistoryDetail;
13+
use ipl\Html\Attributes;
14+
use ipl\Stdlib\Filter;
15+
use ipl\Web\Compat\CompatController;
16+
17+
class NotificationHistoryController extends CompatController
18+
{
19+
use Auth;
20+
21+
public function indexAction(): void
22+
{
23+
$this->addTitleTab(t('Notification History'));
24+
25+
$id = $this->params->getRequired('id');
26+
27+
$query = NotificationHistory::on(Database::get())
28+
->with([
29+
'contact',
30+
'channel',
31+
'incident',
32+
'rule',
33+
'rule_escalation',
34+
'incident.object',
35+
'incident.object.source'
36+
])
37+
->withColumns('incident.object.id_tags')
38+
->filter(Filter::equal('id', $id));
39+
40+
$this->applyRestrictions($query);
41+
42+
/** @var NotificationHistory $notificationHistory */
43+
$notificationHistory = $query->first();
44+
if ($notificationHistory === null) {
45+
$this->httpNotFound(t('Notification History not found'));
46+
}
47+
$this->controls->addAttributes(Attributes::create(['class' => 'notification-history-detail']));
48+
$this->addControl(new ObjectHeader($notificationHistory));
49+
$this->addContent(new NotificationHistoryDetail($notificationHistory));
50+
}
51+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 Icinga GmbH <https://icinga.com>
4+
// SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
namespace Icinga\Module\Notifications\Controllers;
7+
8+
use Icinga\Module\Notifications\Common\Auth;
9+
use Icinga\Module\Notifications\Common\Database;
10+
use Icinga\Module\Notifications\Model\NotificationHistory;
11+
use Icinga\Module\Notifications\View\NotificationHistoryRenderer;
12+
use Icinga\Module\Notifications\Web\Control\SearchBar\ObjectSuggestions;
13+
use Icinga\Module\Notifications\Widget\ItemList\ObjectList;
14+
use ipl\Web\Common\Controls;
15+
use ipl\Web\Compat\CompatController;
16+
use ipl\Web\Compat\SearchControls;
17+
use ipl\Web\Control\LimitControl;
18+
use ipl\Web\Control\SortControl;
19+
use ipl\Web\Filter\QueryString;
20+
use ipl\Web\Layout\DetailedItemLayout;
21+
use ipl\Web\Layout\ItemLayout;
22+
use ipl\Web\Layout\MinimalItemLayout;
23+
24+
class NotificationHistoryListController extends CompatController
25+
{
26+
use Auth;
27+
use SearchControls;
28+
use Controls;
29+
30+
public function indexAction(): void
31+
{
32+
$this->addTitleTab(t('Sent Notifications'));
33+
34+
$notificationHistory = NotificationHistory::on(Database::get())
35+
->with(['channel', 'contact', 'contactgroup', 'schedule']);
36+
37+
$limitControl = $this->createLimitControl();
38+
$sortControl = $this->createSortControl(
39+
$notificationHistory,
40+
[
41+
'notification_history.triggered_at desc' => t('Triggered At'),
42+
'notification_history.state, notification_history.triggered_at desc' => t('State')
43+
]
44+
);
45+
46+
$paginationControl = $this->createPaginationControl($notificationHistory);
47+
$viewModeSwitcher = $this->createViewModeSwitcher($this->params);
48+
$searchBar = $this->createSearchBar($notificationHistory, [
49+
$limitControl->getLimitParam(),
50+
$sortControl->getSortParam(),
51+
$viewModeSwitcher->getViewModeParam()
52+
]);
53+
54+
$this->applyViewModeLimit($limitControl, $paginationControl);
55+
$this->handleControls($this->getServerRequest());
56+
57+
if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
58+
if ($searchBar->hasBeenSubmitted()) {
59+
$filter = QueryString::parse((string) $this->params);
60+
} else {
61+
$this->addControl($searchBar);
62+
$this->sendMultipartUpdate();
63+
return;
64+
}
65+
} else {
66+
$filter = $searchBar->getFilter();
67+
}
68+
69+
$this->applyRestrictions($notificationHistory);
70+
$notificationHistory->filter($filter);
71+
72+
$this->addControl($paginationControl);
73+
$this->addControl($sortControl);
74+
$this->addControl($limitControl);
75+
$this->addControl($viewModeSwitcher);
76+
$this->addControl($searchBar);
77+
78+
$incidentList = (new ObjectList($notificationHistory, new NotificationHistoryRenderer()))
79+
->setItemLayoutClass(match ($viewModeSwitcher->getViewMode()) {
80+
'minimal' => MinimalItemLayout::class,
81+
'detailed' => DetailedItemLayout::class,
82+
'common' => ItemLayout::class
83+
});
84+
85+
$this->addContent($incidentList);
86+
87+
if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
88+
$this->sendMultipartUpdate();
89+
}
90+
91+
$this->setAutorefreshInterval(10);
92+
}
93+
94+
public function completeAction(): void
95+
{
96+
$suggestions = new ObjectSuggestions();
97+
$suggestions->setModel(NotificationHistory::class);
98+
$suggestions->forRequest($this->getServerRequest());
99+
$this->getDocument()->add($suggestions);
100+
}
101+
102+
public function searchEditorAction(): void
103+
{
104+
$editor = $this->createSearchEditor(NotificationHistory::on(Database::get()), [
105+
LimitControl::DEFAULT_LIMIT_PARAM,
106+
SortControl::DEFAULT_SORT_PARAM,
107+
]);
108+
109+
$this->getDocument()->add($editor);
110+
$this->setTitle(t('Adjust Filter'));
111+
}
112+
}

configuration.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@
5252
);
5353
}
5454

55+
$section->add(
56+
N_('Notification History'),
57+
[
58+
'icon' => 'paper-plane',
59+
'description' => $this->translate('Notification History'),
60+
'url' => 'notifications/notification-history-list',
61+
'priority' => 40
62+
]
63+
);
64+
5565
$this->providePermission(
5666
'notifications/config/schedules',
5767
$this->translate('Allow to configure schedules')
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 Icinga GmbH <https://icinga.com>
4+
// SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
namespace Icinga\Module\Notifications\Common;
7+
8+
/**
9+
* Notification transmission reason
10+
*
11+
* Each case maps to the backing string stored as the reason a notification was triggered.
12+
* Register {@see \ipl\Orm\Behavior\EnumCast} on a model to have those columns hydrated automatically as enum instances.
13+
*/
14+
enum NotificationTransmissionReason: string
15+
{
16+
case INCIDENT_SEVERITY_CHANGED = 'incident_severity_changed';
17+
case ESCALATION_TRIGGERED = 'escalation_triggered';
18+
case OPENED = 'opened';
19+
case CLOSED = 'closed';
20+
case MUTED = 'muted';
21+
case UNMUTED = 'unmuted';
22+
23+
/**
24+
* Get the backing string value
25+
*
26+
* @return string
27+
*/
28+
public function getValue(): string
29+
{
30+
return $this->value;
31+
}
32+
33+
/**
34+
* Get the translated label
35+
*
36+
* @return string
37+
*/
38+
public function getLabel(): string
39+
{
40+
return match ($this) {
41+
self::INCIDENT_SEVERITY_CHANGED => t('Incident severity changed'),
42+
self::ESCALATION_TRIGGERED => t('Escalation triggered'),
43+
self::OPENED => t('Incident opened'),
44+
self::CLOSED => t('Incident closed'),
45+
self::MUTED => t('Incident muted'),
46+
self::UNMUTED => t('Incident unmuted')
47+
};
48+
}
49+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 Icinga GmbH <https://icinga.com>
4+
// SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
namespace Icinga\Module\Notifications\Common;
7+
8+
use Icinga\Module\Notifications\Widget\IconBall;
9+
use ipl\Html\Attributes;
10+
11+
/**
12+
* Notification transmission state
13+
*
14+
* Each case maps to the backing string stored as notification transmission state.
15+
* Register {@see \ipl\Orm\Behavior\EnumCast} on a model to have those columns hydrated automatically as enum instances.
16+
*/
17+
enum NotificationTransmissionState: string
18+
{
19+
case SENT = 'sent';
20+
case FAILED = 'failed';
21+
case PENDING = 'pending';
22+
case SUPPRESSED = 'suppressed';
23+
24+
/**
25+
* Get the backing string value
26+
*
27+
* @return string
28+
*/
29+
public function getValue(): string
30+
{
31+
return $this->value;
32+
}
33+
34+
/**
35+
* Get the translated label
36+
*
37+
* @return string
38+
*/
39+
public function getLabel(): string
40+
{
41+
return match ($this) {
42+
self::SENT => t('Successfully sent notification'),
43+
self::FAILED => t('Failed to send notification'),
44+
self::PENDING => t('Pending notification'),
45+
self::SUPPRESSED => t('Suppressed notification')
46+
};
47+
}
48+
49+
/**
50+
* Get the icon
51+
*
52+
* @return IconBall
53+
*/
54+
public function getIcon(): IconBall
55+
{
56+
return (new IconBall('paper-plane'))
57+
->addAttributes(Attributes::create([
58+
'title' => $this->getLabel(),
59+
'class' => match ($this) {
60+
self::SENT => 'sent',
61+
self::FAILED => 'failed',
62+
self::PENDING => 'pending',
63+
self::SUPPRESSED => 'suppressed'
64+
}
65+
]));
66+
}
67+
}

0 commit comments

Comments
 (0)