diff --git a/library/Notifications/Common/Icons.php b/library/Notifications/Common/Icons.php index 9019906e..d63fecad 100644 --- a/library/Notifications/Common/Icons.php +++ b/library/Notifications/Common/Icons.php @@ -22,7 +22,7 @@ private function __construct() public const USER_MANAGER = 'user-tie'; - public const CLOSED = 'circle-check'; + public const CLOSED = 'check'; public const OPENED = 'sun'; @@ -38,5 +38,7 @@ private function __construct() public const NOTIFIED = 'paper-plane'; - public const RULE_MATCHED = 'filter-check-circle'; + public const RULE_MATCHED = 'filter'; + + public const UNDEFINED = 'notdef'; } diff --git a/library/Notifications/Model/Event.php b/library/Notifications/Model/Event.php index 777dff50..11cc5ab1 100644 --- a/library/Notifications/Model/Event.php +++ b/library/Notifications/Model/Event.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Notifications\Model; +use DateTime; use Icinga\Module\Notifications\Common\Database; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; @@ -17,7 +18,17 @@ /** * Event model * - * @property Query|Incident $incident + * @property int $id + * @property DateTime $time + * @property string $object_id + * @property string $type + * @property ?string $severity + * @property ?string $message + * @property ?string $username + * + * @property Query | Objects $object + * @property Query | IncidentHistory $incident_history + * @property Query | Incident $incident */ class Event extends Model { diff --git a/library/Notifications/Model/Incident.php b/library/Notifications/Model/Incident.php index 996843d4..408786b2 100644 --- a/library/Notifications/Model/Incident.php +++ b/library/Notifications/Model/Incident.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Notifications\Model; +use DateTime; use Icinga\Module\Notifications\Common\Database; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; @@ -18,6 +19,18 @@ * Incident Model * * @property int $id + * @property string $object_id + * @property DateTime $started_at + * @property ?DateTime $recovered_at + * @property string $severity + * + * @property Query | Objects $object + * @property Query | Event $event + * @property Query | Contact $contact + * @property Query | IncidentContact $incident_contact + * @property Query | IncidentHistory $incident_history + * @property Query | Rule $rule + * @property Query | RuleEscalation $rule_escalation */ class Incident extends Model { @@ -44,10 +57,10 @@ public function getColumns() public function getColumnDefinitions() { return [ - 'object_id' => t('Object Id'), - 'started_at' => t('Started At'), - 'recovered_at' => t('Recovered At'), - 'severity' => t('Severity') + 'object_id' => t('Object Id'), + 'started_at' => t('Started At'), + 'recovered_at' => t('Recovered At'), + 'severity' => t('Severity') ]; } diff --git a/library/Notifications/Model/IncidentHistory.php b/library/Notifications/Model/IncidentHistory.php index 803b4318..b38ff248 100644 --- a/library/Notifications/Model/IncidentHistory.php +++ b/library/Notifications/Model/IncidentHistory.php @@ -4,11 +4,40 @@ namespace Icinga\Module\Notifications\Model; +use DateTime; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use ipl\Orm\Model; +use ipl\Orm\Query; use ipl\Orm\Relations; +/** + * IncidentHistory + * + * @property int $id + * @property int $incident_id + * @property ?int $event_id + * @property ?int $rule_id + * @property ?int $rule_escalation_id + * @property DateTime $time + * @property string $type + * @property ?int $contact_id + * @property ?int $channel_id + * @property ?string $new_severity + * @property ?string $old_severity + * @property ?string $new_recipient_role + * @property ?string $old_recipient_role + * @property ?string $message + * + * @property Query | Incident $incident + * @property Query | Event $event + * @property Query | Contact $contact + * @property Query | Contactgroup $contactgroup + * @property Query | Schedule $schedule + * @property Query | Rule $rule + * @property Query | RuleEscalation $rule_escalation + * @property Query | Channel $channel + */ class IncidentHistory extends Model { public function getTableName() @@ -69,7 +98,7 @@ public function createBehaviors(Behaviors $behaviors) public function getDefaultSort() { - return ['incident_history.time desc']; + return ['incident_history.time desc, incident_history.type desc']; } public function createRelations(Relations $relations) diff --git a/library/Notifications/Model/Objects.php b/library/Notifications/Model/Objects.php index 61ff187e..8a2b08a4 100644 --- a/library/Notifications/Model/Objects.php +++ b/library/Notifications/Model/Objects.php @@ -10,9 +10,25 @@ use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use ipl\Orm\Model; +use ipl\Orm\Query; use ipl\Orm\Relations; /** + * Object + * + * @property string $id + * @property int $source_id + * @property string $name + * @property string $host + * @property ?string $service + * @property ?string $url + * + * @property Query | Event $event + * @property Query | Incident $incident + * @property Query | Tag $tag + * @property Query | ObjectExtraTag $object_extra_tag + * @property Query | ExtraTag $extra_tag + * @property Query | Source $source * @property array $id_tags */ class Objects extends Model diff --git a/library/Notifications/Widget/Detail/EventDetail.php b/library/Notifications/Widget/Detail/EventDetail.php index 0a67d2b6..9f16a697 100644 --- a/library/Notifications/Widget/Detail/EventDetail.php +++ b/library/Notifications/Widget/Detail/EventDetail.php @@ -130,10 +130,17 @@ protected function createIncident(): ?array /** @return ValidHtml[] */ protected function createSource(): array { - return [ - Html::tag('h2', t('Source')), - new EventSourceBadge($this->event->object->source) - ]; + $elements = []; + if ($this->event->type === 'internal') { + // return no source elements for internal events + return $elements; + } + + $elements[] = Html::tag('h2', t('Source')); + + $elements[] = new EventSourceBadge($this->event->object->source); + + return $elements; } protected function assemble() diff --git a/library/Notifications/Widget/Detail/IncidentDetail.php b/library/Notifications/Widget/Detail/IncidentDetail.php index 949cefae..e3fff80b 100644 --- a/library/Notifications/Widget/Detail/IncidentDetail.php +++ b/library/Notifications/Widget/Detail/IncidentDetail.php @@ -4,7 +4,6 @@ namespace Icinga\Module\Notifications\Widget\Detail; -use Icinga\Module\Notifications\Common\Database; use Icinga\Module\Notifications\Model\Incident; use Icinga\Module\Notifications\Model\Objects; use Icinga\Module\Notifications\Widget\EventSourceBadge; @@ -15,8 +14,6 @@ use ipl\Html\Html; use ipl\Html\HtmlElement; use ipl\Html\Table; -use ipl\Orm\Query; -use ipl\Sql\Select; use ipl\Web\Widget\Link; use ipl\Web\Widget\StateBall; @@ -93,28 +90,20 @@ protected function createRelatedObject() protected function createHistory() { - $history = $this->incident->incident_history - ->with([ - 'event', - 'incident.object', - 'incident.object.source', - 'contact', - 'rule', - 'rule_escalation', - 'contactgroup', - 'schedule', - 'channel' - ]); - - $history - ->withColumns('incident.object.id_tags') - ->on(Query::ON_SELECT_ASSEMBLED, function (Select $select) use ($history) { - Database::registerGroupBy($history, $select); - }); - return [ Html::tag('h2', t('Incident History')), - new IncidentHistoryList($history) + new IncidentHistoryList( + $this->incident->incident_history + ->with([ + 'incident.object.source', + 'contact', + 'rule', + 'rule_escalation', + 'contactgroup', + 'schedule', + 'channel' + ]) + ) ]; } diff --git a/library/Notifications/Widget/Detail/IncidentQuickActions.php b/library/Notifications/Widget/Detail/IncidentQuickActions.php index 356ab39e..86565ea4 100644 --- a/library/Notifications/Widget/Detail/IncidentQuickActions.php +++ b/library/Notifications/Widget/Detail/IncidentQuickActions.php @@ -92,7 +92,7 @@ protected function assembleUnsubscribeButton(): void 'unsubscribe', [ 'class' => ['control-button', 'spinner'], - 'label' => [new Icon(Icons::UNSUBSCRIBED), t('Unubscribe')], + 'label' => [new Icon(Icons::UNSUBSCRIBED), t('Unsubscribe')], 'title' => t('Unsubscribe from this incident') ] ); diff --git a/library/Notifications/Widget/IconBall.php b/library/Notifications/Widget/IconBall.php new file mode 100644 index 00000000..41cfa15d --- /dev/null +++ b/library/Notifications/Widget/IconBall.php @@ -0,0 +1,25 @@ + ['icon-ball']]; + + public function __construct(string $name, ?string $style = 'fa-solid') + { + $icon = new Icon($name); + if ($style !== null) { + $icon->setStyle($style); + } + + $this->addHtml($icon); + } +} diff --git a/library/Notifications/Widget/ItemList/EventListItem.php b/library/Notifications/Widget/ItemList/EventListItem.php index 5e6f691e..a305f155 100644 --- a/library/Notifications/Widget/ItemList/EventListItem.php +++ b/library/Notifications/Widget/ItemList/EventListItem.php @@ -8,10 +8,13 @@ use Icinga\Module\Notifications\Model\Event; use Icinga\Module\Notifications\Model\Incident; use Icinga\Module\Notifications\Model\Objects; +use Icinga\Module\Notifications\Model\Source; +use Icinga\Module\Notifications\Widget\IconBall; use Icinga\Module\Notifications\Widget\SourceIcon; use InvalidArgumentException; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; +use ipl\Stdlib\Str; use ipl\Web\Common\BaseListItem; use ipl\Web\Widget\Icon; use ipl\Web\Widget\Link; @@ -50,34 +53,43 @@ protected function assembleVisual(BaseHtmlElement $visual): void $content = null; $severity = $this->item->severity; $class = 'severity-' . $severity; - switch ($severity) { - case 'ok': - $content = (new Icon('heart', ['class' => $class]))->setStyle('fa-regular'); - break; - case 'crit': - $content = new Icon('circle-exclamation', ['class' => $class]); - break; - case 'warning': - $content = new Icon('exclamation-triangle', ['class' => $class]); - break; - case 'err': - $content = (new Icon('circle-xmark', ['class' => $class]))->setStyle('fa-regular'); - break; - case 'debug': - $content = new Icon('bug-slash'); - break; - case 'info': - $content = new Icon('info'); - break; - case 'alert': - $content = new Icon('bell'); - break; - case 'emerg': - $content = new Icon('tower-broadcast'); - break; - case 'notice': - $content = new Icon('envelope'); - break; + + if ($this->item->type === 'internal') { + /* + * TODO(nc): Add proper handling of internal events once + * https://github.com/Icinga/icinga-notifications/issues/162 gets sorted out + */ + $content = new IconBall('square-up-right', 'fa-solid'); + } else { + switch ($severity) { + case 'ok': + $content = (new Icon('heart', ['class' => $class]))->setStyle('fa-regular'); + break; + case 'crit': + $content = new Icon('circle-exclamation', ['class' => $class]); + break; + case 'warning': + $content = new Icon('exclamation-triangle', ['class' => $class]); + break; + case 'err': + $content = (new Icon('circle-xmark', ['class' => $class]))->setStyle('fa-regular'); + break; + case 'debug': + $content = new Icon('bug-slash'); + break; + case 'info': + $content = new Icon('info'); + break; + case 'alert': + $content = new Icon('bell'); + break; + case 'emerg': + $content = new Icon('tower-broadcast'); + break; + case 'notice': + $content = new Icon('envelope'); + break; + } } if ($content) { @@ -102,7 +114,14 @@ protected function assembleTitle(BaseHtmlElement $title): void $msg = null; if ($this->item->severity === null) { - $msg = t('acknowledged'); + $description = strtolower(trim($this->item->message ?? '')); + if (Str::startsWith($description, 'incident reached age')) { + $msg = t('exceeded time constraint'); + } elseif (Str::startsWith($description, 'incident reevaluation')) { + $msg = t('was reevaluated at daemon startup'); + } else { + $msg = t('was acknowledged'); + } } elseif ($this->item->severity === 'ok') { $msg = t('recovered'); } else { @@ -120,16 +139,25 @@ protected function assembleCaption(BaseHtmlElement $caption): void protected function assembleHeader(BaseHtmlElement $header): void { + $content = []; + if ($this->item->type !== 'internal') { + /** @var Objects $object */ + $object = $this->item->object; + /** @var Source $source */ + $source = $object->source; + $content[] = (new SourceIcon(SourceIcon::SIZE_BIG))->addHtml($source->getIcon()); + } + + $content[] = new TimeAgo($this->item->time->getTimestamp()); + $header->add($this->createTitle()); - $header->add(Html::tag( - 'span', - ['class' => 'meta'], - [ - (new SourceIcon(SourceIcon::SIZE_BIG)) - ->addHtml($this->item->object->source->getIcon()), - new TimeAgo($this->item->time->getTimestamp()) - ] - )); + $header->add( + Html::tag( + 'span', + ['class' => 'meta'], + $content + ) + ); } protected function assembleMain(BaseHtmlElement $main): void diff --git a/library/Notifications/Widget/ItemList/IncidentHistoryListItem.php b/library/Notifications/Widget/ItemList/IncidentHistoryListItem.php index cc1876b7..21f70579 100644 --- a/library/Notifications/Widget/ItemList/IncidentHistoryListItem.php +++ b/library/Notifications/Widget/ItemList/IncidentHistoryListItem.php @@ -5,16 +5,13 @@ namespace Icinga\Module\Notifications\Widget\ItemList; use Icinga\Module\Notifications\Common\Icons; -use Icinga\Module\Notifications\Common\Links; use Icinga\Module\Notifications\Model\Event; use Icinga\Module\Notifications\Model\IncidentHistory; -use Icinga\Module\Notifications\Model\Objects; +use Icinga\Module\Notifications\Widget\IconBall; use Icinga\Module\Notifications\Widget\SourceIcon; use ipl\Html\BaseHtmlElement; -use ipl\Web\Widget\IcingaIcon; use ipl\Web\Common\BaseListItem; use ipl\Web\Widget\Icon; -use ipl\Web\Widget\Link; use ipl\Web\Widget\TimeAgo; /** @@ -31,38 +28,15 @@ class IncidentHistoryListItem extends BaseListItem protected function assembleVisual(BaseHtmlElement $visual): void { $incidentIcon = $this->getIncidentEventIcon(); - if ($this->item->type === 'incident_severity_changed' || $this->item->type === 'opened') { + if ($this->item->type === 'incident_severity_changed') { $content = new Icon($incidentIcon, ['class' => 'severity-' . $this->item->new_severity]); - } elseif ($this->item->type === 'rule_matched') { - $content = new IcingaIcon($incidentIcon, ['class' => 'type-' . $this->item->type]); } else { - $content = new Icon($incidentIcon, ['class' => 'type-' . $this->item->type]); - - switch ($this->item->type) { - case 'closed': - case 'recipient_role_changed': - case 'notified': - $content->setStyle('fa-regular'); - } + $content = new IconBall($incidentIcon); } $visual->addHtml($content); } - protected function assembleTitle(BaseHtmlElement $title): void - { - if ($this->item->type === 'opened' || $this->item->type == 'incident_severity_changed') { - $this->getAttributes() - ->set('data-action-item', true); - - /** @var Objects $obj */ - $obj = $this->item->incident->object; - $content = new Link($obj->getName(), Links::event($this->item->event_id), ['class' => 'subject']); - - $title->addHtml($content); - } - } - protected function assembleCaption(BaseHtmlElement $caption): void { $caption->add($this->buildMessage()); @@ -89,6 +63,7 @@ protected function getIncidentEventIcon(): string { switch ($this->item->type) { case 'opened': + return Icons::OPENED; case 'incident_severity_changed': return $this->getSeverityIcon(); case 'recipient_role_changed': @@ -99,8 +74,10 @@ protected function getIncidentEventIcon(): string return Icons::RULE_MATCHED; case 'escalation_triggered': return Icons::TRIGGERED; - default: + case 'notified': return Icons::NOTIFIED; + default: + return Icons::UNDEFINED; } } @@ -109,16 +86,18 @@ protected function getSeverityIcon(): string switch ($this->item->new_severity) { case 'ok': return Icons::OK; + case 'warning': + return Icons::WARNING; case 'err': return Icons::ERROR; case 'crit': return Icons::CRITICAL; default: - return Icons::WARNING; + return Icons::UNDEFINED; } } - protected function getRoleIcon(): ?string + protected function getRoleIcon(): string { switch ($this->item->new_recipient_role) { case 'manager': @@ -134,7 +113,7 @@ protected function getRoleIcon(): ?string } } - return ''; + return Icons::UNDEFINED; } } @@ -151,9 +130,11 @@ protected function buildMessage(): string t('Incident opened at severity %s'), Event::mapSeverity($this->item->new_severity) ); + break; case 'closed': - $message = t('All sources recovered, incident closed'); + $message = t('Incident closed'); + break; case "notified": if ($this->item->contactgroup_id) { @@ -177,6 +158,7 @@ protected function buildMessage(): string $this->item->channel->type ); } + break; case 'incident_severity_changed': $message = sprintf( @@ -184,6 +166,7 @@ protected function buildMessage(): string Event::mapSeverity($this->item->old_severity), Event::mapSeverity($this->item->new_severity) ); + break; case 'recipient_role_changed': $newRole = $this->item->new_recipient_role; @@ -238,6 +221,7 @@ protected function buildMessage(): string break; case 'rule_matched': $message = sprintf(t('Rule %s matched on this incident'), $this->item->rule->name); + break; case 'escalation_triggered': $message = sprintf( @@ -245,6 +229,7 @@ protected function buildMessage(): string $this->item->rule->name, $this->item->rule_escalation->name ); + break; default: $message = ''; diff --git a/public/css/common.less b/public/css/common.less index b9fa4e10..9f704c66 100644 --- a/public/css/common.less +++ b/public/css/common.less @@ -33,6 +33,7 @@ color: @text-color; margin-right: 0.2em; + i.icon { vertical-align: text-top; } @@ -42,6 +43,26 @@ } } +.icon-ball { + .ball(); + .ball-size-l(); + .ball-solid(@gray-light); + + color: @text-color; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + + i.icon::before { + font-size: 0.6em; + } + + i.icon.fa-paper-plane::before { + margin-right: .2em; + } +} + .contact-ball { .ball(); .ball-size-l();