diff --git a/application/controllers/HostController.php b/application/controllers/HostController.php index 5e00e9493..4d12e3176 100644 --- a/application/controllers/HostController.php +++ b/application/controllers/HostController.php @@ -20,6 +20,7 @@ use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\ServicestateSummary; +use Icinga\Module\Icingadb\Notifications\SubscribeIncidents; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Icinga\Module\Icingadb\Util\OptimizerHints; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; @@ -45,6 +46,7 @@ class HostController extends Controller { use CommandActions; use HookActions; + use SubscribeIncidents; /** @var Host The host object */ protected $host; @@ -590,6 +592,16 @@ public function servicesSearchEditorAction(): void $this->setTitle($this->translate('Adjust Filter')); } + public function subscribeAction(): void + { + $this->handleSubscription(true, $this->host, $this->getCommandTargetsUrl()); + } + + public function unsubscribeAction(): void + { + $this->handleSubscription(false, $this->host, $this->getCommandTargetsUrl()); + } + /** * Fetch the dependency nodes of the current host * diff --git a/application/controllers/ServiceController.php b/application/controllers/ServiceController.php index 775268aa3..1f7380293 100644 --- a/application/controllers/ServiceController.php +++ b/application/controllers/ServiceController.php @@ -18,6 +18,7 @@ use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Model\Service; +use Icinga\Module\Icingadb\Notifications\SubscribeIncidents; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Icinga\Module\Icingadb\Util\OptimizerHints; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; @@ -43,6 +44,7 @@ class ServiceController extends Controller { use CommandActions; use HookActions; + use SubscribeIncidents; /** @var Service The service object */ protected $service; @@ -600,6 +602,16 @@ protected function getCommandTargetsUrl(): Url return Links::service($this->service, $this->service->host); } + public function subscribeAction(): void + { + $this->handleSubscription(true, $this->service, $this->getCommandTargetsUrl()); + } + + public function unsubscribeAction(): void + { + $this->handleSubscription(false, $this->service, $this->getCommandTargetsUrl()); + } + protected function getDefaultTabControls(): array { return [new ObjectHeader($this->service)]; diff --git a/application/forms/Command/Object/AcknowledgeProblemForm.php b/application/forms/Command/Object/AcknowledgeProblemForm.php index 6a61973c3..a18e3257d 100644 --- a/application/forms/Command/Object/AcknowledgeProblemForm.php +++ b/application/forms/Command/Object/AcknowledgeProblemForm.php @@ -12,6 +12,7 @@ use Icinga\Module\Icingadb\Command\Object\AcknowledgeProblemCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Notifications\ManagesIncidents; use Icinga\Web\Notification; use ipl\Html\Attributes; use ipl\Html\HtmlElement; @@ -27,6 +28,8 @@ class AcknowledgeProblemForm extends CommandForm { + use ManagesIncidents; + public function __construct() { $this->on(self::ON_SUCCESS, function () { @@ -34,6 +37,7 @@ public function __construct() return; } + $this->manageIncidents(true); $countObjects = count($this->getObjects()); if (iterable_value_first($this->getObjects()) instanceof Host) { $message = sprintf(tp( diff --git a/application/forms/Command/Object/RemoveAcknowledgementForm.php b/application/forms/Command/Object/RemoveAcknowledgementForm.php index b94bf21dc..48351752f 100644 --- a/application/forms/Command/Object/RemoveAcknowledgementForm.php +++ b/application/forms/Command/Object/RemoveAcknowledgementForm.php @@ -9,6 +9,7 @@ use Icinga\Module\Icingadb\Command\Object\RemoveAcknowledgementCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Notifications\ManagesIncidents; use Icinga\Web\Notification; use ipl\Orm\Model; use ipl\Web\Widget\Icon; @@ -19,6 +20,8 @@ class RemoveAcknowledgementForm extends CommandForm { + use ManagesIncidents; + public function __construct() { $this->on(self::ON_SUCCESS, function () { @@ -26,6 +29,7 @@ public function __construct() return; } + $this->manageIncidents(false); $countObjects = count($this->getObjects()); if (iterable_value_first($this->getObjects()) instanceof Host) { $message = sprintf(tp( diff --git a/configuration.php b/configuration.php index e133d797c..fe33bbcde 100644 --- a/configuration.php +++ b/configuration.php @@ -100,6 +100,10 @@ 'icingadb/command/send-custom-notification', $this->translate('Allow to send custom notifications for hosts and services') ); + $this->providePermission( + 'icingadb/notifications/subscribe', + $this->translate('Allow to subscribe and manage incidents related to hosts and services') + ); $this->providePermission( 'icingadb/object/show-source', diff --git a/library/Icingadb/Common/HostLinks.php b/library/Icingadb/Common/HostLinks.php index 9cac18d87..5926e67be 100644 --- a/library/Icingadb/Common/HostLinks.php +++ b/library/Icingadb/Common/HostLinks.php @@ -74,4 +74,14 @@ public static function services(Host $host): Url { return Url::fromPath('icingadb/host/services', ['name' => $host->name]); } + + public static function subscribe(Host $host): Url + { + return Url::fromPath('icingadb/host/subscribe', ['name' => $host->name]); + } + + public static function unsubscribe(Host $host): Url + { + return Url::fromPath('icingadb/host/unsubscribe', ['name' => $host->name]); + } } diff --git a/library/Icingadb/Common/ServiceLinks.php b/library/Icingadb/Common/ServiceLinks.php index ac8d148db..fabb56f19 100644 --- a/library/Icingadb/Common/ServiceLinks.php +++ b/library/Icingadb/Common/ServiceLinks.php @@ -106,4 +106,14 @@ public static function toggleFeatures(Service $service, Host $host): Url ['name' => $service->name, 'host.name' => $host->name] ); } + + public static function subscribe(Service $service, Host $host): Url + { + return Url::fromPath('icingadb/service/subscribe', ['name' => $service->name, 'host.name' => $host->name]); + } + + public static function unsubscribe(Service $service, Host $host): Url + { + return Url::fromPath('icingadb/service/unsubscribe', ['name' => $service->name, 'host.name' => $host->name]); + } } diff --git a/library/Icingadb/Notifications/IncidentFinder.php b/library/Icingadb/Notifications/IncidentFinder.php new file mode 100644 index 000000000..3c8d51aa0 --- /dev/null +++ b/library/Icingadb/Notifications/IncidentFinder.php @@ -0,0 +1,49 @@ + +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace Icinga\Module\Icingadb\Notifications; + +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\Service; +use Icinga\Module\Notifications\Integrations\Incidents; + +class IncidentFinder +{ + /** + * Find the open incidents related to the given object + * + * @param Host|Service $object + * + * @return Incidents + */ + public static function forObject(Host|Service $object): Incidents + { + return Incidents::find(self::buildTags($object)); + } + + /** + * Build the tags to match the given host/service + * + * @param Host|Service $object + * + * @return array + */ + private static function buildTags(Host|Service $object): array + { + if ($object instanceof Host) { + return [ + 'host' => $object->name, + 'service' => null, + 'environment' => bin2hex($object->environment_id) + ]; + } + + return [ + 'host' => $object->host->name, + 'service' => $object->name, + 'environment' => bin2hex($object->environment_id) + ]; + } +} diff --git a/library/Icingadb/Notifications/ManagesIncidents.php b/library/Icingadb/Notifications/ManagesIncidents.php new file mode 100644 index 000000000..a692cb022 --- /dev/null +++ b/library/Icingadb/Notifications/ManagesIncidents.php @@ -0,0 +1,66 @@ + +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace Icinga\Module\Icingadb\Notifications; + +use Icinga\Application\Icinga; +use Icinga\Module\Icingadb\Forms\Command\CommandForm; +use Icinga\Web\Notification; +use InvalidArgumentException; + +/** + * @phpstan-require-extends CommandForm + */ +trait ManagesIncidents +{ + /** + * Add or remove the current user as manager of all incidents related to the form's objects + * + * @param bool $manage + * + * @return void + */ + protected function manageIncidents(bool $manage): void + { + if (! Icinga::app()->getModuleManager()->hasEnabled('notifications')) { + return; + } + + $username = $this->getAuth()->getUser()->getUsername(); + $count = 0; + try { + foreach ($this->getObjects() as $object) { + foreach (IncidentFinder::forObject($object) as $incident) { + if ($manage) { + $incident->addManager($username); + } else { + $incident->removeManager($username); + } + + $count++; + } + } + + if ($count > 0) { + Notification::success(sprintf( + $manage + ? tp('Now managing the matching incident', 'Now managing %d matching incidents', $count) + : tp( + 'No longer managing the matching incident', + 'No longer managing %d matching incidents', + $count + ), + $count + )); + } + } catch (InvalidArgumentException) { + if ($manage) { + Notification::warning( + t('Cannot manage matching incident, no notification contact configured for your account.') + ); + } + } + } +} diff --git a/library/Icingadb/Notifications/SubscribeIncidents.php b/library/Icingadb/Notifications/SubscribeIncidents.php new file mode 100644 index 000000000..bd6bfed73 --- /dev/null +++ b/library/Icingadb/Notifications/SubscribeIncidents.php @@ -0,0 +1,45 @@ + +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace Icinga\Module\Icingadb\Notifications; + +use Icinga\Application\Icinga; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\Service; +use Icinga\Module\Icingadb\Web\Controller; +use ipl\Html\Form; +use ipl\Web\Url; + +/** + * @phpstan-require-extends Controller + */ +trait SubscribeIncidents +{ + /** + * Subscribe or unsubscribe all incidents matching the given tags + * + * @param bool $subscribe + * @param Host|Service $object + * @param Url $redirectUrl The URL to redirect to after the form has been handled + * @return void + */ + protected function handleSubscription(bool $subscribe, Host|Service $object, Url $redirectUrl): void + { + if ( + Icinga::app()->getModuleManager()->hasEnabled('notifications') + && $this->isGrantedOn('icingadb/notifications/subscribe', $object) + ) { + $form = (new SubscriptionForm($subscribe, $object)) + ->setAction((string) Url::fromRequest()) + ->on(Form::ON_SUBMIT, function () use ($redirectUrl) { + $this->redirectNow($redirectUrl); + }); + + $form->handleRequest($this->getServerRequest()); + + $this->addContent($form); + } + } +} diff --git a/library/Icingadb/Notifications/SubscriptionForm.php b/library/Icingadb/Notifications/SubscriptionForm.php new file mode 100644 index 000000000..152004629 --- /dev/null +++ b/library/Icingadb/Notifications/SubscriptionForm.php @@ -0,0 +1,88 @@ + +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace Icinga\Module\Icingadb\Notifications; + +use Icinga\Authentication\Auth; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\Service; +use Icinga\Web\Notification; +use Icinga\Web\Session; +use InvalidArgumentException; +use ipl\Html\Form; +use ipl\I18n\Translation; +use ipl\Web\Common\CsrfCounterMeasure; +use ipl\Web\Widget\Icon; + +class SubscriptionForm extends Form +{ + use CsrfCounterMeasure; + use Translation; + + protected $defaultAttributes = ['class' => 'inline']; + + /** @var bool Whether to subscribe (true) or unsubscribe (false) the current user */ + protected bool $subscribe; + + /** @var Host|Service The object whose incidents should be subscribed/unsubscribed */ + protected Host|Service $object; + + /** + * Create a new SubscriptionForm + * + * @param bool $subscribe Whether the form should subscribe or unsubscribe on submit + * @param Host|Service $object The object whose incidents should be subscribed/unsubscribed + */ + public function __construct(bool $subscribe, Host|Service $object) + { + $this->subscribe = $subscribe; + $this->object = $object; + $this->setAttribute( + 'title', + $subscribe + ? $this->translate('Subscribe to the related incident') + : $this->translate('Unsubscribe from the related incident') + ); + + $this->on(self::ON_SUBMIT, function () { + $username = Auth::getInstance()->getUser()->getUsername(); + + try { + foreach (IncidentFinder::forObject($this->object) as $incident) { + if ($this->subscribe) { + $incident->addSubscriber($username); + } else { + $incident->removeSubscriber($username); + } + } + + Notification::success( + $this->subscribe + ? $this->translate('Subscribed to the incident successfully') + : $this->translate('Unsubscribed from the incident successfully') + ); + } catch (InvalidArgumentException) { + Notification::error( + $this->translate('Cannot subscribe, there is no notification contact configured for your account.') + ); + } + }); + } + + protected function assemble(): void + { + $this->addCsrfCounterMeasure(Session::getSession()->getId()); + $this->addElement( + 'submitButton', + 'btn_submit', + [ + 'class' => ['link-button', 'spinner'], + 'label' => $this->subscribe + ? [new Icon('share'), $this->translate('Subscribe')] + : [new Icon('bell-slash'), $this->translate('Unsubscribe')] + ] + ); + } +} diff --git a/library/Icingadb/View/CommentRenderer.php b/library/Icingadb/View/CommentRenderer.php index 4db821826..c99ee6f82 100644 --- a/library/Icingadb/View/CommentRenderer.php +++ b/library/Icingadb/View/CommentRenderer.php @@ -101,14 +101,16 @@ public function assembleTitle($item, HtmlDocument $title, string $layout): void ]; if ($isAck) { - $label = [Text::create('ack')]; + $label = [new Icon('check-circle')]; if ($item->is_persistent) { - array_unshift($label, new Icon(Icons::IS_PERSISTENT)); + $label[] = new Icon(Icons::IS_PERSISTENT); } + $label[] = Text::create('ack'); + $headerParts[] = Text::create(' '); - $headerParts[] = new HtmlElement('span', Attributes::create(['class' => 'ack-badge badge']), ...$label); + $headerParts[] = new HtmlElement('span', Attributes::create(['class' => 'ack-badge']), ...$label); } if ($expires !== null) { diff --git a/library/Icingadb/Widget/Detail/HostDetail.php b/library/Icingadb/Widget/Detail/HostDetail.php index fe8b56bae..cc4beb704 100644 --- a/library/Icingadb/Widget/Detail/HostDetail.php +++ b/library/Icingadb/Widget/Detail/HostDetail.php @@ -51,7 +51,8 @@ protected function assemble() 400 => $this->createComments(), 401 => $this->createDowntimes(), 500 => $this->createGroups(), - 501 => $this->createNotifications(), + 501 => $this->createNotificationRecipients(), + 502 => $this->createConfiguredContacts(), 510 => $this->createAffectedObjects(), 600 => $this->createCheckStatistics(), 601 => $this->createPerformanceData(), diff --git a/library/Icingadb/Widget/Detail/MultiselectQuickActions.php b/library/Icingadb/Widget/Detail/MultiselectQuickActions.php index e5cb433e2..1a0a0e87b 100644 --- a/library/Icingadb/Widget/Detail/MultiselectQuickActions.php +++ b/library/Icingadb/Widget/Detail/MultiselectQuickActions.php @@ -5,6 +5,7 @@ namespace Icinga\Module\Icingadb\Widget\Detail; +use Icinga\Application\Icinga; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Forms\Command\Object\CheckNowForm; use Icinga\Module\Icingadb\Forms\Command\Object\RemoveAcknowledgementForm; @@ -136,7 +137,11 @@ protected function assemble() 'acknowledge', t('Acknowledge'), 'check-circle', - t('Acknowledge this problem, suppress all future notifications for it and tag it as being handled') + Icinga::app()->getModuleManager()->hasEnabled('notifications') + ? t('Acknowledge this problem, tag it as being handled and become the manager of the ' + . 'related incident') + : t('Acknowledge this problem, suppress all future notifications for it and tag it as ' + . 'being handled') ); } diff --git a/library/Icingadb/Widget/Detail/ObjectDetail.php b/library/Icingadb/Widget/Detail/ObjectDetail.php index c295ddeaa..36c9340fa 100644 --- a/library/Icingadb/Widget/Detail/ObjectDetail.php +++ b/library/Icingadb/Widget/Detail/ObjectDetail.php @@ -27,6 +27,7 @@ use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\UnreachableParent; +use Icinga\Module\Icingadb\Notifications\IncidentFinder; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Icinga\Module\Icingadb\Web\Navigation\Action; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; @@ -381,7 +382,7 @@ protected function createNotes() return $content; } - protected function createNotifications(): array + protected function createConfiguredContacts(): array { [$users, $usergroups] = $this->getUsersAndUsergroups(); @@ -399,8 +400,16 @@ protected function createNotifications(): array ); } + if ( + Icinga::app()->getModuleManager()->hasEnabled('notifications') + && ! $userList->hasContent() + && ! $usergroupList->hasContent() + ) { + return []; + } + return [ - Html::tag('h2', t('Notifications')), + Html::tag('h2', t('Configured Contacts')), new HorizontalKeyValue( t('Contacts'), $userList->hasContent() ? $userList : new EmptyState(t('No contacts configured.')) @@ -414,6 +423,94 @@ protected function createNotifications(): array ]; } + protected function createNotificationRecipients(): array + { + if (! Icinga::app()->getModuleManager()->hasEnabled('notifications')) { + return []; + } + + $incidents = IncidentFinder::forObject($this->object); + if (! $incidents->hasIncident()) { + return []; + } + + $recipientIcons = [ + 'contact' => Icons::USER, + 'contactgroup' => Icons::USERGROUP, + 'schedule' => 'calendar', + ]; + + $translatedTypes = [ + 'contact' => t('Contact'), + 'contactgroup' => t('Contact Group'), + 'schedule' => t('Schedule'), + ]; + + $subscribers = []; + $recipients = []; + foreach ($incidents as $incident) { + foreach ($incident->getSubscribers() as $subscriber) { + $isManager = $subscriber['role'] === 'manager'; + $changedAt = DateFormatter::formatDateTime($subscriber['roleChangedAt']->getTimestamp()); + + $subscriberItem = Html::tag('li', [ + 'title' => $isManager + ? sprintf(t('%s acknowledged on %s'), $subscriber['full_name'], $changedAt) + : sprintf(t('%s subscribed on %s'), $subscriber['full_name'], $changedAt) + ], Text::create($subscriber['full_name'])); + + if ($isManager) { + $subscriberItem->addHtml( + Html::tag( + 'span', + ['class' => ['ack-badge']], + [ + new Icon('check-circle'), + Text::create('ack') + ] + ) + ); + } + + $subscribers[] = $subscriberItem; + } + + foreach ($incident->getRecipients() as $recipient) { + $icon = new Icon($recipientIcons[$recipient['type']]); + $name = $recipient['full_name'] ?? $recipient['name']; + $recipients[] = Html::tag( + 'li', + [ + 'title' => sprintf( + '[%s] %s', + $translatedTypes[$recipient['type']], + $name + ) + ], + Html::tag( + 'span', + ['class' => 'recipient'], + [$icon, Text::create($name)] + ) + ); + } + } + + $subscribersValue = empty($subscribers) + ? new EmptyState(t('No subscribers.')) + : Html::tag('div', ['class' => 'subscriber-list'], $subscribers); + + $recipientsValue = empty($recipients) + ? new EmptyState(t('No recipients.')) + : Html::tag('div', ['class' => 'recipient-list'], $recipients); + + return [ + Html::tag('h2', t('Notification Recipients')), + new HorizontalKeyValue(t('Subscribers'), $subscribersValue), + new HorizontalKeyValue(t('Recipients'), $recipientsValue), + ]; + } + protected function createPerformanceData(): array { $content[] = Html::tag('h2', t('Performance Data')); diff --git a/library/Icingadb/Widget/Detail/QuickActions.php b/library/Icingadb/Widget/Detail/QuickActions.php index f961b229b..fcc7c0793 100644 --- a/library/Icingadb/Widget/Detail/QuickActions.php +++ b/library/Icingadb/Widget/Detail/QuickActions.php @@ -5,6 +5,7 @@ namespace Icinga\Module\Icingadb\Widget\Detail; +use Icinga\Application\Icinga; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\HostLinks; use Icinga\Module\Icingadb\Common\ServiceLinks; @@ -12,6 +13,8 @@ use Icinga\Module\Icingadb\Forms\Command\Object\RemoveAcknowledgementForm; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; +use Icinga\Module\Icingadb\Notifications\IncidentFinder; +use Icinga\Module\Icingadb\Notifications\SubscriptionForm; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Web\Widget\Icon; @@ -48,11 +51,51 @@ protected function assemble() 'acknowledge', t('Acknowledge'), 'check-circle', - t('Acknowledge this problem, suppress all future notifications for it and tag it as being handled') + Icinga::app()->getModuleManager()->hasEnabled('notifications') + ? t('Acknowledge this problem, tag it as being handled and become the manager of the ' + . 'related incident') + : t('Acknowledge this problem, suppress all future notifications for it and tag it as ' + . 'being handled') ); } } + if ( + Icinga::app()->getModuleManager()->hasEnabled('notifications') + && $this->isGrantedOn('icingadb/notifications/subscribe', $this->object) + ) { + $incidents = IncidentFinder::forObject($this->object); + + if ($incidents->hasIncident()) { + $username = $this->getAuth()->getUser()->getUsername(); + $role = null; + foreach ($incidents as $incident) { + foreach ($incident->getSubscribers() as $subscriber) { + if ($subscriber['name'] === $username) { + $role = $subscriber['role']; + break; + } + } + + if ($role === 'manager') { + break; + } + } + + if ($role !== 'manager') { + $action = $role === 'subscriber' ? 'unsubscribe' : 'subscribe'; + + $this->addHtml( + Html::tag( + 'li', + (new SubscriptionForm($action === 'subscribe', $this->object)) + ->setAction($this->getLink($action)) + ) + ); + } + } + } + if ( $this->isGrantedOn('icingadb/command/schedule-check', $this->object) || ( diff --git a/library/Icingadb/Widget/Detail/ServiceDetail.php b/library/Icingadb/Widget/Detail/ServiceDetail.php index 945568a3b..2a5fbace8 100644 --- a/library/Icingadb/Widget/Detail/ServiceDetail.php +++ b/library/Icingadb/Widget/Detail/ServiceDetail.php @@ -29,7 +29,8 @@ protected function assemble() 400 => $this->createComments(), 401 => $this->createDowntimes(), 500 => $this->createGroups(), - 501 => $this->createNotifications(), + 501 => $this->createNotificationRecipients(), + 502 => $this->createConfiguredContacts(), 510 => $this->createAffectedObjects(), 600 => $this->createCheckStatistics(), 601 => $this->createPerformanceData(), diff --git a/public/css/common.less b/public/css/common.less index 6279e2727..8e7087c44 100644 --- a/public/css/common.less +++ b/public/css/common.less @@ -59,6 +59,11 @@ div.show-more { } } +.ack-badge:not(.badge) { + color: @color-ok; + margin-top: 0.25em; +} + .controls { .box-shadow(0, 0, 0, 1px, @gray-lighter); flex-shrink: 0; diff --git a/public/css/widget/notification-recipients.less b/public/css/widget/notification-recipients.less new file mode 100644 index 000000000..8d4c30a09 --- /dev/null +++ b/public/css/widget/notification-recipients.less @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2026 Icinga GmbH +// SPDX-License-Identifier: GPL-3.0-or-later + +.recipient-list li, .subscriber-list li { + display: inline-block; + line-height: 2; + + .ack-badge { + margin-left: .25em; + } + + &:not(:last-child)::after { + content: ','; + margin-right: .25em; + } +} + +.recipient-list .recipient { + .rounded-corners(1em); + padding: .25em .5em; + background-color: @gray-lighter; + white-space: nowrap; + + i { + color: @text-color-light; + } +}