summaryrefslogtreecommitdiffstats
path: root/library/Director/Web/Table
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
commitf66ab8dae2f3d0418759f81a3a64dc9517a62449 (patch)
treefbff2135e7013f196b891bbde54618eb050e4aaf /library/Director/Web/Table
parentInitial commit. (diff)
downloadicingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.tar.xz
icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.zip
Adding upstream version 1.10.2.upstream/1.10.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Director/Web/Table')
-rw-r--r--library/Director/Web/Table/ActivityLogTable.php294
-rw-r--r--library/Director/Web/Table/ApplyRulesTable.php240
-rw-r--r--library/Director/Web/Table/BasketSnapshotTable.php125
-rw-r--r--library/Director/Web/Table/BasketTable.php50
-rw-r--r--library/Director/Web/Table/BranchActivityTable.php116
-rw-r--r--library/Director/Web/Table/BranchedIcingaCommandArgumentTable.php78
-rw-r--r--library/Director/Web/Table/ChoicesTable.php65
-rw-r--r--library/Director/Web/Table/ConfigFileDiffTable.php140
-rw-r--r--library/Director/Web/Table/CoreApiFieldsTable.php106
-rw-r--r--library/Director/Web/Table/CoreApiObjectsTable.php60
-rw-r--r--library/Director/Web/Table/CoreApiPrototypesTable.php43
-rw-r--r--library/Director/Web/Table/CustomvarTable.php102
-rw-r--r--library/Director/Web/Table/CustomvarVariantsTable.php125
-rw-r--r--library/Director/Web/Table/DatafieldCategoryTable.php64
-rw-r--r--library/Director/Web/Table/DatafieldTable.php118
-rw-r--r--library/Director/Web/Table/DatalistEntryTable.php73
-rw-r--r--library/Director/Web/Table/DatalistTable.php41
-rw-r--r--library/Director/Web/Table/DbHelper.php67
-rw-r--r--library/Director/Web/Table/Dependency/DependencyInfoTable.php101
-rw-r--r--library/Director/Web/Table/Dependency/Html.php74
-rw-r--r--library/Director/Web/Table/DependencyTemplateUsageTable.php22
-rw-r--r--library/Director/Web/Table/DeploymentLogTable.php90
-rw-r--r--library/Director/Web/Table/FilterableByUsage.php10
-rw-r--r--library/Director/Web/Table/GeneratedConfigFileTable.php120
-rw-r--r--library/Director/Web/Table/GroupMemberTable.php201
-rw-r--r--library/Director/Web/Table/HostTemplateUsageTable.php22
-rw-r--r--library/Director/Web/Table/IcingaAppliedServiceTable.php49
-rw-r--r--library/Director/Web/Table/IcingaCommandArgumentTable.php89
-rw-r--r--library/Director/Web/Table/IcingaHostAppliedForServiceTable.php117
-rw-r--r--library/Director/Web/Table/IcingaHostAppliedServicesTable.php207
-rw-r--r--library/Director/Web/Table/IcingaHostsMatchingFilterTable.php71
-rw-r--r--library/Director/Web/Table/IcingaObjectDatafieldTable.php87
-rw-r--r--library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php67
-rw-r--r--library/Director/Web/Table/IcingaServiceSetHostTable.php64
-rw-r--r--library/Director/Web/Table/IcingaServiceSetServiceTable.php259
-rw-r--r--library/Director/Web/Table/IcingaTimePeriodRangeTable.php61
-rw-r--r--library/Director/Web/Table/ImportedrowsTable.php103
-rw-r--r--library/Director/Web/Table/ImportrunTable.php90
-rw-r--r--library/Director/Web/Table/ImportsourceHookTable.php107
-rw-r--r--library/Director/Web/Table/ImportsourceTable.php63
-rw-r--r--library/Director/Web/Table/JobTable.php82
-rw-r--r--library/Director/Web/Table/NotificationTemplateUsageTable.php22
-rw-r--r--library/Director/Web/Table/ObjectSetTable.php211
-rw-r--r--library/Director/Web/Table/ObjectsTable.php315
-rw-r--r--library/Director/Web/Table/ObjectsTableApiUser.php13
-rw-r--r--library/Director/Web/Table/ObjectsTableCommand.php67
-rw-r--r--library/Director/Web/Table/ObjectsTableEndpoint.php86
-rw-r--r--library/Director/Web/Table/ObjectsTableHost.php40
-rw-r--r--library/Director/Web/Table/ObjectsTableHostTemplateChoice.php27
-rw-r--r--library/Director/Web/Table/ObjectsTableService.php219
-rw-r--r--library/Director/Web/Table/ObjectsTableZone.php13
-rw-r--r--library/Director/Web/Table/PropertymodifierTable.php145
-rw-r--r--library/Director/Web/Table/QuickTable.php547
-rw-r--r--library/Director/Web/Table/ReadOnlyFormAvpTable.php113
-rw-r--r--library/Director/Web/Table/ServiceTemplateUsageTable.php27
-rw-r--r--library/Director/Web/Table/SyncRunTable.php90
-rw-r--r--library/Director/Web/Table/SyncpropertyTable.php97
-rw-r--r--library/Director/Web/Table/SyncruleTable.php67
-rw-r--r--library/Director/Web/Table/TableLoader.php34
-rw-r--r--library/Director/Web/Table/TableWithBranchSupport.php69
-rw-r--r--library/Director/Web/Table/TemplateUsageTable.php157
-rw-r--r--library/Director/Web/Table/TemplatesTable.php156
62 files changed, 6578 insertions, 0 deletions
diff --git a/library/Director/Web/Table/ActivityLogTable.php b/library/Director/Web/Table/ActivityLogTable.php
new file mode 100644
index 0000000..5460bc2
--- /dev/null
+++ b/library/Director/Web/Table/ActivityLogTable.php
@@ -0,0 +1,294 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\Format\LocalTimeFormat;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Icinga\Module\Director\Util;
+use ipl\Html\Html;
+use ipl\Html\HtmlElement;
+
+class ActivityLogTable extends ZfQueryBasedTable
+{
+ protected $filters = [];
+
+ protected $lastDeployedId;
+
+ protected $extraParams = [];
+
+ protected $columnCount;
+
+ protected $hasObjectFilter = false;
+
+ protected $searchColumns = [
+ 'author',
+ 'object_name',
+ 'object_type',
+ ];
+
+ /** @var LocalTimeFormat */
+ protected $timeFormat;
+
+ protected $ranges = [];
+
+ /** @var ?object */
+ protected $currentRange = null;
+ /** @var ?HtmlElement */
+ protected $currentRangeCell = null;
+ /** @var int */
+ protected $rangeRows = 0;
+ protected $continueRange = false;
+ protected $currentRow;
+
+ public function __construct($db)
+ {
+ parent::__construct($db);
+ $this->timeFormat = new LocalTimeFormat();
+ }
+
+ public function assemble()
+ {
+ $this->getAttributes()->add('class', 'activity-log');
+ }
+
+ public function setLastDeployedId($id)
+ {
+ $this->lastDeployedId = $id;
+ return $this;
+ }
+
+ protected function fetchQueryRows()
+ {
+ $rows = parent::fetchQueryRows();
+ // Hint -> DESC, that's why they are inverted
+ if (empty($rows)) {
+ return $rows;
+ }
+ $last = $rows[0]->id;
+ $first = $rows[count($rows) - 1]->id;
+ $db = $this->db();
+ $this->ranges = $db->fetchAll(
+ $db->select()
+ ->from('director_activity_log_remark')
+ ->where('first_related_activity <= ?', $last)
+ ->where('last_related_activity >= ?', $first)
+ );
+
+ return $rows;
+ }
+
+
+ public function renderRow($row)
+ {
+ $this->currentRow = $row;
+ $this->splitByDay($row->ts_change_time);
+ $action = 'action-' . $row->action. ' ';
+ if ($row->id > $this->lastDeployedId) {
+ $action .= 'undeployed';
+ } else {
+ $action .= 'deployed';
+ }
+
+ $columns = [
+ $this::td($this->makeLink($row))->setSeparator(' '),
+ ];
+ if (! $this->hasObjectFilter) {
+ $columns[] = $this->makeRangeInfo($row->id);
+ }
+ $columns[] = $this::td($this->timeFormat->getTime($row->ts_change_time));
+
+ return $this::tr($columns)->addAttributes(['class' => $action]);
+ }
+
+ /**
+ * Hint: cloned from parent class and modified
+ * @param int $timestamp
+ */
+ protected function renderDayIfNew($timestamp)
+ {
+ $day = $this->getDateFormatter()->getFullDay($timestamp);
+
+ if ($this->lastDay !== $day) {
+ $this->nextHeader()->add(
+ $this::th($day, [
+ 'colspan' => $this->hasObjectFilter ? 2 : 3,
+ 'class' => 'table-header-day'
+ ])
+ );
+
+ $this->lastDay = $day;
+ if ($this->currentRangeCell) {
+ if ($this->currentRange->first_related_activity <= $this->currentRow->id) {
+ $this->currentRangeCell->addAttributes(['class' => 'continuing']);
+ $this->continueRange = true;
+ } else {
+ $this->continueRange = false;
+ }
+ }
+ $this->currentRangeCell = null;
+ $this->currentRange = null;
+ $this->rangeRows = 0;
+ $this->nextBody();
+ }
+ }
+
+ protected function makeRangeInfo($id)
+ {
+ $range = $this->getRangeForId($id);
+ if ($range === null) {
+ if ($this->currentRangeCell) {
+ $this->currentRangeCell->getAttributes()->remove('class', 'continuing');
+ }
+ $this->currentRange = null;
+ $this->currentRangeCell = null;
+ $this->rangeRows = 0;
+ return $this::td();
+ }
+
+ if ($range === $this->currentRange) {
+ $this->growCurrentRange();
+ return null;
+ }
+ $this->startRange($range);
+
+ return $this->currentRangeCell;
+ }
+
+ protected function startRange($range)
+ {
+ $this->currentRangeCell = $this::td($this->renderRangeComment($range), [
+ 'colspan' => $this->rangeRows = 1,
+ 'class' => 'comment-cell'
+ ]);
+ if ($this->continueRange) {
+ $this->currentRangeCell->addAttributes(['class' => 'continued']);
+ $this->continueRange = false;
+ }
+ $this->currentRange = $range;
+ }
+
+ protected function renderRangeComment($range)
+ {
+ // The only purpose of this container is to avoid hovered rows from influencing
+ // the comments background color, as we're using the alpha channel to lighten it
+ // This can be replaced once we get theme-safe colors for such messages
+ return Html::tag('div', [
+ 'class' => 'range-comment-container',
+ ], Link::create($this->continueRange ? '' : $range->remark, '#', null, [
+ 'title' => $range->remark,
+ 'class' => 'range-comment'
+ ]));
+ }
+
+ protected function growCurrentRange()
+ {
+ $this->rangeRows++;
+ $this->currentRangeCell->setAttribute('rowspan', $this->rangeRows);
+ }
+
+ protected function getRangeForId($id)
+ {
+ foreach ($this->ranges as $range) {
+ if ($id >= $range->first_related_activity && $id <= $range->last_related_activity) {
+ return $range;
+ }
+ }
+
+ return null;
+ }
+
+ protected function makeLink($row)
+ {
+ $type = $row->object_type;
+ $name = $row->object_name;
+ if (substr($type, 0, 7) === 'icinga_') {
+ $type = substr($type, 7);
+ }
+
+ if (Util::hasPermission('director/showconfig')) {
+ // Later on replacing, service_set -> serviceset
+
+ // multi column key :(
+ if ($type === 'service' || $this->hasObjectFilter) {
+ $object = "\"$name\"";
+ } else {
+ $object = Link::create(
+ "\"$name\"",
+ 'director/' . str_replace('_', '', $type),
+ ['name' => $name],
+ ['title' => $this->translate('Jump to this object')]
+ );
+ }
+
+ return [
+ '[' . $row->author . ']',
+ Link::create(
+ $row->action,
+ 'director/config/activity',
+ array_merge(['id' => $row->id], $this->extraParams),
+ ['title' => $this->translate('Show details related to this change')]
+ ),
+ str_replace('_', ' ', $type),
+ $object
+ ];
+ } else {
+ return sprintf(
+ '[%s] %s %s "%s"',
+ $row->author,
+ $row->action,
+ $type,
+ $name
+ );
+ }
+ }
+
+ public function filterObject($type, $name)
+ {
+ $this->hasObjectFilter = true;
+ $this->filters[] = ['l.object_type = ?', $type];
+ $this->filters[] = ['l.object_name = ?', $name];
+
+ return $this;
+ }
+
+ public function filterHost($name)
+ {
+ $db = $this->db();
+ $filter = '%"host":' . json_encode($name) . '%';
+ $this->filters[] = ['('
+ . $db->quoteInto('l.old_properties LIKE ?', $filter)
+ . ' OR '
+ . $db->quoteInto('l.new_properties LIKE ?', $filter)
+ . ')', null];
+
+ return $this;
+ }
+
+ public function getColumns()
+ {
+ return [
+ 'author' => 'l.author',
+ 'action' => 'l.action_name',
+ 'object_name' => 'l.object_name',
+ 'object_type' => 'l.object_type',
+ 'id' => 'l.id',
+ 'change_time' => 'l.change_time',
+ 'ts_change_time' => 'UNIX_TIMESTAMP(l.change_time)',
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ $query = $this->db()->select()->from(
+ ['l' => 'director_activity_log'],
+ $this->getColumns()
+ )->order('change_time DESC')->order('id DESC')->limit(100);
+
+ foreach ($this->filters as $filter) {
+ $query->where($filter[0], $filter[1]);
+ }
+
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/ApplyRulesTable.php b/library/Director/Web/Table/ApplyRulesTable.php
new file mode 100644
index 0000000..a861bac
--- /dev/null
+++ b/library/Director/Web/Table/ApplyRulesTable.php
@@ -0,0 +1,240 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\DbUtil;
+use Icinga\Module\Director\Db\IcingaObjectFilterHelper;
+use Icinga\Module\Director\IcingaConfig\AssignRenderer;
+use Icinga\Module\Director\Objects\IcingaObject;
+use gipfl\IcingaWeb2\Icon;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+use gipfl\IcingaWeb2\Zf1\Db\FilterRenderer;
+use Ramsey\Uuid\Uuid;
+use Zend_Db_Select as ZfSelect;
+
+class ApplyRulesTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'o.object_name',
+ 'o.assign_filter',
+ ];
+
+ private $type;
+
+ /** @var IcingaObject */
+ protected $dummyObject;
+
+ protected $baseObjectUrl;
+
+ protected $linkWithName = false;
+
+ public static function create($type, Db $db)
+ {
+ $table = new static($db);
+ $table->setType($type);
+ return $table;
+ }
+
+ public function setType($type)
+ {
+ $this->type = $type;
+
+ return $this;
+ }
+
+ public function setBaseObjectUrl($url)
+ {
+ $this->baseObjectUrl = $url;
+
+ return $this;
+ }
+
+ public function createLinksWithNames($linksWithName = true)
+ {
+ $this->linkWithName = (bool) $linksWithName;
+
+ return $this;
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return ['Name', 'assign where'/*, 'Actions'*/];
+ }
+
+ public function renderRow($row)
+ {
+ $row->uuid = DbUtil::binaryResult($row->uuid);
+ if ($this->linkWithName) {
+ $params = ['name' => $row->object_name];
+ } else {
+ $params = ['uuid' => Uuid::fromBytes($row->uuid)->toString()];
+ }
+ $url = Url::fromPath("director/{$this->baseObjectUrl}/edit", $params);
+
+ $assignWhere = $this->renderApplyFilter($row->assign_filter);
+
+ if (! empty($row->apply_for)) {
+ $assignWhere = sprintf('apply for %s / %s', $row->apply_for, $assignWhere);
+ }
+
+ $tr = static::tr([
+ static::td(Link::create($row->object_name, $url)),
+ static::td($assignWhere),
+ // NOT (YET) static::td($this->createActionLinks($row))->setSeparator(' ')
+ ]);
+
+ if ($row->disabled === 'y') {
+ $tr->getAttributes()->add('class', 'disabled');
+ }
+
+ return $tr;
+ }
+
+ /**
+ * Should be triggered from renderRow, still unused.
+ *
+ * @param IcingaObject $template
+ * @param string $inheritance
+ * @return $this
+ * @throws \Icinga\Exception\ProgrammingError
+ */
+ public function filterTemplate(
+ IcingaObject $template,
+ $inheritance = IcingaObjectFilterHelper::INHERIT_DIRECT
+ ) {
+ IcingaObjectFilterHelper::filterByTemplate(
+ $this->getQuery(),
+ $template,
+ 'o',
+ $inheritance
+ );
+
+ return $this;
+ }
+
+ protected function renderApplyFilter($assignFilter)
+ {
+ try {
+ $string = AssignRenderer::forFilter(
+ Filter::fromQueryString($assignFilter)
+ )->renderAssign();
+ // Do not prefix it
+ $string = preg_replace('/^assign where /', '', $string);
+ } catch (IcingaException $e) {
+ // ignore errors in filter rendering
+ $string = 'Error in Filter rendering: ' . $e->getMessage();
+ }
+
+ return $string;
+ }
+
+ public function createActionLinks($row)
+ {
+ $params = ['uuid' => Uuid::fromBytes($row->uuid)->toString()];
+ $baseUrl = 'director/' . $this->baseObjectUrl;
+ $links = [];
+ $links[] = Link::create(
+ Icon::create('sitemap'),
+ "${baseUrl}template/applytargets",
+ ['id' => $row->id],
+ ['title' => $this->translate('Show affected Objects')]
+ );
+
+ $links[] = Link::create(
+ Icon::create('edit'),
+ "$baseUrl/edit",
+ $params,
+ ['title' => $this->translate('Modify this Apply Rule')]
+ );
+
+ $links[] = Link::create(
+ Icon::create('doc-text'),
+ "$baseUrl/render",
+ $params,
+ ['title' => $this->translate('Apply Rule rendering preview')]
+ );
+
+ $links[] = Link::create(
+ Icon::create('history'),
+ "$baseUrl/history",
+ $params,
+ ['title' => $this->translate('Apply rule history')]
+ );
+
+ return $links;
+ }
+
+ protected function applyRestrictions(ZfSelect $query)
+ {
+ $auth = Auth::getInstance();
+ $type = $this->type;
+ // TODO: Centralize this logic
+ if ($type === 'scheduledDowntime') {
+ $type = 'scheduled-downtime';
+ }
+ $restrictions = $auth->getRestrictions("director/$type/apply/filter-by-name");
+ if (empty($restrictions)) {
+ return $query;
+ }
+
+ $filter = Filter::matchAny();
+ foreach ($restrictions as $restriction) {
+ $filter->addFilter(Filter::where('o.object_name', $restriction));
+ }
+
+ return FilterRenderer::applyToQuery($filter, $query);
+ }
+
+
+ /**
+ * @return IcingaObject
+ */
+ protected function getDummyObject()
+ {
+ if ($this->dummyObject === null) {
+ $type = $this->type;
+ $this->dummyObject = IcingaObject::createByType($type);
+ }
+ return $this->dummyObject;
+ }
+
+ public function prepareQuery()
+ {
+ $table = $this->getDummyObject()->getTableName();
+ $columns = [
+ 'id' => 'o.id',
+ 'uuid' => 'o.uuid',
+ 'object_name' => 'o.object_name',
+ 'disabled' => 'o.disabled',
+ 'assign_filter' => 'o.assign_filter',
+ 'apply_for' => '(NULL)',
+ ];
+
+ if ($table === 'icinga_service') {
+ $columns['apply_for'] = 'o.apply_for';
+ }
+ $query = $this->db()->select()->from(
+ ['o' => $table],
+ $columns
+ )->where(
+ "object_type = 'apply'"
+ )->order('o.object_name');
+
+ if ($this->type === 'service') {
+ $query->where('service_set_id IS NULL');
+ }
+
+ return $this->applyRestrictions($query);
+ }
+}
diff --git a/library/Director/Web/Table/BasketSnapshotTable.php b/library/Director/Web/Table/BasketSnapshotTable.php
new file mode 100644
index 0000000..08f808a
--- /dev/null
+++ b/library/Director/Web/Table/BasketSnapshotTable.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Director\Core\Json;
+use Icinga\Module\Director\DirectorObject\Automation\Basket;
+use RuntimeException;
+
+class BasketSnapshotTable extends ZfQueryBasedTable
+{
+ use DbHelper;
+
+ protected $searchColumns = [
+ 'basket_name',
+ 'summary'
+ ];
+
+ /** @var Basket */
+ protected $basket;
+
+ public function setBasket(Basket $basket)
+ {
+ $this->basket = $basket;
+ $this->searchColumns = [];
+
+ return $this;
+ }
+
+ public function renderRow($row)
+ {
+ $this->splitByDay($row->ts_create_seconds);
+ $link = $this->linkToSnapshot($this->renderSummary($row->summary), $row);
+
+ if ($this->basket === null) {
+ $columns = [
+ [
+ new Link(
+ Html::tag('strong', $row->basket_name),
+ 'director/basket',
+ ['name' => $row->basket_name]
+ ),
+ Html::tag('br'),
+ $link,
+ ],
+ DateFormatter::formatTime($row->ts_create / 1000),
+ ];
+ } else {
+ $columns = [
+ $link,
+ DateFormatter::formatTime($row->ts_create / 1000),
+ ];
+ }
+ return $this::row($columns);
+ }
+
+ protected function renderSummary($summary)
+ {
+ $summary = Json::decode($summary);
+ if ($summary === null) {
+ return '-';
+ }
+ $result = [];
+ if (! is_object($summary) && ! is_array($summary)) {
+ throw new RuntimeException(sprintf(
+ 'Got invalid basket summary: %s ',
+ var_export($summary, 1)
+ ));
+ }
+
+ foreach ($summary as $type => $count) {
+ $result[] = sprintf(
+ '%dx %s',
+ $count,
+ $type
+ );
+ }
+
+ if (empty($result)) {
+ return '-';
+ }
+
+ return implode(', ', $result);
+ }
+
+ protected function linkToSnapshot($caption, $row)
+ {
+ return new Link($caption, 'director/basket/snapshot', [
+ 'checksum' => bin2hex($this->wantBinaryValue($row->content_checksum)),
+ 'ts' => $row->ts_create,
+ 'name' => $row->basket_name,
+ ]);
+ }
+
+ public function prepareQuery()
+ {
+ $query = $this->db()->select()->from([
+ 'b' => 'director_basket'
+ ], [
+ 'b.uuid',
+ 'b.basket_name',
+ 'bs.ts_create',
+ 'ts_create_seconds' => '(bs.ts_create / 1000)',
+ 'bs.content_checksum',
+ 'bc.summary',
+ ])->join(
+ ['bs' => 'director_basket_snapshot'],
+ 'bs.basket_uuid = b.uuid',
+ []
+ )->join(
+ ['bc' => 'director_basket_content'],
+ 'bc.checksum = bs.content_checksum',
+ []
+ )->order('bs.ts_create DESC');
+
+ if ($this->basket !== null) {
+ $query->where('b.uuid = ?', $this->quoteBinary($this->basket->get('uuid')));
+ }
+
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/BasketTable.php b/library/Director/Web/Table/BasketTable.php
new file mode 100644
index 0000000..25e37e0
--- /dev/null
+++ b/library/Director/Web/Table/BasketTable.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class BasketTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'basket_name',
+ ];
+
+ public function renderRow($row)
+ {
+ $tr = $this::row([
+ new Link(
+ $row->basket_name,
+ 'director/basket',
+ ['name' => $row->basket_name]
+ ),
+ $row->cnt_snapshots
+ ]);
+
+ return $tr;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Basket'),
+ $this->translate('Snapshots'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from([
+ 'b' => 'director_basket'
+ ], [
+ 'b.uuid',
+ 'b.basket_name',
+ 'cnt_snapshots' => 'COUNT(bs.basket_uuid)',
+ ])->joinLeft(
+ ['bs' => 'director_basket_snapshot'],
+ 'bs.basket_uuid = b.uuid',
+ []
+ )->group('b.uuid')->order('b.basket_name');
+ }
+}
diff --git a/library/Director/Web/Table/BranchActivityTable.php b/library/Director/Web/Table/BranchActivityTable.php
new file mode 100644
index 0000000..e7131ef
--- /dev/null
+++ b/library/Director/Web/Table/BranchActivityTable.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\Format\LocalTimeFormat;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Branch\BranchActivity;
+use Icinga\Module\Director\Util;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Ramsey\Uuid\UuidInterface;
+
+class BranchActivityTable extends ZfQueryBasedTable
+{
+ protected $extraParams = [];
+
+ /** @var UuidInterface */
+ protected $branchUuid;
+
+ /** @var ?UuidInterface */
+ protected $objectUuid;
+
+ /** @var LocalTimeFormat */
+ protected $timeFormat;
+
+ protected $linkToObject = true;
+
+ public function __construct(UuidInterface $branchUuid, $db, UuidInterface $objectUuid = null)
+ {
+ $this->branchUuid = $branchUuid;
+ $this->objectUuid = $objectUuid;
+ $this->timeFormat = new LocalTimeFormat();
+ parent::__construct($db);
+ }
+
+ public function assemble()
+ {
+ $this->getAttributes()->add('class', 'activity-log');
+ }
+
+ public function renderRow($row)
+ {
+ $ts = (int) floor(BranchActivity::fixFakeTimestamp($row->timestamp_ns) / 1000000);
+ $this->splitByDay($ts);
+ $activity = BranchActivity::fromDbRow($row);
+ return $this::tr([
+ $this::td($this->makeBranchLink($activity))->setSeparator(' '),
+ $this::td($this->timeFormat->getTime($ts))
+ ])->addAttributes(['class' => ['action-' . $activity->getAction(), 'branched']]);
+ }
+
+ public function disableObjectLink()
+ {
+ $this->linkToObject = false;
+ return $this;
+ }
+
+ protected function linkObject(BranchActivity $activity)
+ {
+ if (! $this->linkToObject) {
+ return $activity->getObjectName();
+ }
+ // $type, UuidInterface $uuid
+ // Later on replacing, service_set -> serviceset
+ $type = preg_replace('/^icinga_/', '', $activity->getObjectTable());
+ return Link::create(
+ $activity->getObjectName(),
+ 'director/' . str_replace('_', '', $type),
+ ['uuid' => $activity->getObjectUuid()->toString()],
+ ['title' => $this->translate('Jump to this object')]
+ );
+ }
+
+ protected function makeBranchLink(BranchActivity $activity)
+ {
+ $type = preg_replace('/^icinga_/', '', $activity->getObjectTable());
+
+ if (Util::hasPermission('director/showconfig')) {
+ // Later on replacing, service_set -> serviceset
+ return [
+ '[' . $activity->getAuthor() . ']',
+ Link::create(
+ $activity->getAction(),
+ 'director/branch/activity',
+ array_merge(['ts' => $activity->getTimestampNs()], $this->extraParams),
+ ['title' => $this->translate('Show details related to this change')]
+ ),
+ str_replace('_', ' ', $type),
+ $this->linkObject($activity)
+ ];
+ } else {
+ return sprintf(
+ '[%s] %s %s "%s"',
+ $activity->getAuthor(),
+ $activity->getAction(),
+ $type,
+ $activity->getObjectName()
+ );
+ }
+ }
+
+ public function prepareQuery()
+ {
+ /** @var Db $connection */
+ $connection = $this->connection();
+ $query = $this->db()->select()->from(['ba' => 'director_branch_activity'], 'ba.*')
+ ->join(['b' => 'director_branch'], 'b.uuid = ba.branch_uuid', ['b.owner'])
+ ->where('branch_uuid = ?', $connection->quoteBinary($this->branchUuid->getBytes()))
+ ->order('timestamp_ns DESC');
+ if ($this->objectUuid) {
+ $query->where('ba.object_uuid = ?', $connection->quoteBinary($this->objectUuid->getBytes()));
+ }
+
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/BranchedIcingaCommandArgumentTable.php b/library/Director/Web/Table/BranchedIcingaCommandArgumentTable.php
new file mode 100644
index 0000000..3d5dbcb
--- /dev/null
+++ b/library/Director/Web/Table/BranchedIcingaCommandArgumentTable.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Data\SimpleQueryPaginationAdapter;
+use gipfl\IcingaWeb2\Table\QueryBasedTable;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use gipfl\IcingaWeb2\Link;
+
+class BranchedIcingaCommandArgumentTable extends QueryBasedTable
+{
+ /** @var IcingaCommand */
+ protected $command;
+
+ /** @var Branch */
+ protected $branch;
+
+ protected $searchColumns = [
+ 'ca.argument_name',
+ 'ca.argument_value',
+ ];
+
+ public function __construct(IcingaCommand $command, Branch $branch)
+ {
+ $this->command = $command;
+ $this->branch = $branch;
+ $this->getAttributes()->set('data-base-target', '_self');
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create($row->argument_name, 'director/command/arguments', [
+ 'argument' => $row->argument_name,
+ 'uuid' => $this->command->getUniqueId()->toString(),
+ ]),
+ $row->argument_value
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Argument'),
+ $this->translate('Value'),
+ ];
+ }
+
+ protected function getPaginationAdapter()
+ {
+ return new SimpleQueryPaginationAdapter($this->getQuery());
+ }
+
+ public function getQuery()
+ {
+ return $this->prepareQuery();
+ }
+
+ protected function fetchQueryRows()
+ {
+ return $this->getQuery()->fetchAll();
+ }
+
+ protected function prepareQuery()
+ {
+ $list = [];
+ foreach ($this->command->arguments()->toPlainObject() as $name => $argument) {
+ $new = (object) [];
+ $new->argument_name = $name;
+ $new->argument_value = isset($argument->value) ? $argument->value : null;
+ $list[] = $new;
+ }
+
+ return (new ArrayDatasource($list))->select();
+ }
+}
diff --git a/library/Director/Web/Table/ChoicesTable.php b/library/Director/Web/Table/ChoicesTable.php
new file mode 100644
index 0000000..4ba2460
--- /dev/null
+++ b/library/Director/Web/Table/ChoicesTable.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Db;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+
+class ChoicesTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = ['o.object_name'];
+
+ protected $type;
+
+ /**
+ * @param $type
+ * @param Db $db
+ * @return static
+ */
+ public static function create($type, Db $db)
+ {
+ $class = __NAMESPACE__ . '\\ChoicesTable' . ucfirst($type);
+ if (! class_exists($class)) {
+ $class = __CLASS__;
+ }
+
+ /** @var static $table */
+ $table = new $class($db);
+ $table->type = $type;
+ return $table;
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [$this->translate('Name')];
+ }
+
+ public function renderRow($row)
+ {
+ $type = $this->getType();
+ $url = Url::fromPath("director/templatechoice/${type}", [
+ 'name' => $row->object_name
+ ]);
+
+ return $this::row([
+ Link::create($row->object_name, $url)
+ ]);
+ }
+
+ protected function prepareQuery()
+ {
+ $type = $this->getType();
+ $table = "icinga_${type}_template_choice";
+ return $this->db()
+ ->select()
+ ->from(['o' => $table], 'object_name')
+ ->order('o.object_name');
+ }
+}
diff --git a/library/Director/Web/Table/ConfigFileDiffTable.php b/library/Director/Web/Table/ConfigFileDiffTable.php
new file mode 100644
index 0000000..1d14d5e
--- /dev/null
+++ b/library/Director/Web/Table/ConfigFileDiffTable.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Util;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class ConfigFileDiffTable extends ZfQueryBasedTable
+{
+ use DbHelper;
+
+ protected $leftChecksum;
+
+ protected $rightChecksum;
+
+ /**
+ * @param $leftSum
+ * @param $rightSum
+ * @param Db $connection
+ * @return static
+ */
+ public static function load($leftSum, $rightSum, Db $connection)
+ {
+ $table = new static($connection);
+ $table->getAttributes()->add('class', 'config-diff');
+ return $table->setLeftChecksum($leftSum)
+ ->setRightChecksum($rightSum);
+ }
+
+ public function renderRow($row)
+ {
+ $tr = $this::row([
+ $this->getFileFiffLink($row),
+ $row->file_path,
+ ]);
+
+ $tr->getAttributes()->add('class', 'file-' . $row->file_action);
+ return $tr;
+ }
+
+ protected function getFileFiffLink($row)
+ {
+ $params = array('file_path' => $row->file_path);
+
+ if ($row->file_checksum_left === $row->file_checksum_right) {
+ $params['config_checksum'] = $row->config_checksum_right;
+ } elseif ($row->file_checksum_left === null) {
+ $params['config_checksum'] = $row->config_checksum_right;
+ } elseif ($row->file_checksum_right === null) {
+ $params['config_checksum'] = $row->config_checksum_left;
+ } else {
+ $params['left'] = $row->config_checksum_left;
+ $params['right'] = $row->config_checksum_right;
+ return Link::create(
+ $row->file_action,
+ 'director/config/filediff',
+ $params
+ );
+ }
+
+ return Link::create($row->file_action, 'director/config/file', $params);
+ }
+
+ public function setLeftChecksum($checksum)
+ {
+ $this->leftChecksum = $checksum;
+ return $this;
+ }
+
+ public function setRightChecksum($checksum)
+ {
+ $this->rightChecksum = $checksum;
+ return $this;
+ }
+
+ public function getTitles()
+ {
+ return array(
+ $this->translate('Action'),
+ $this->translate('File'),
+ );
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+
+ $left = $db->select()
+ ->from(
+ array('cfl' => 'director_generated_config_file'),
+ array(
+ 'file_path' => 'COALESCE(cfl.file_path, cfr.file_path)',
+ 'config_checksum_left' => $this->dbHexFunc('cfl.config_checksum'),
+ 'config_checksum_right' => $this->dbHexFunc('cfr.config_checksum'),
+ 'file_checksum_left' => $this->dbHexFunc('cfl.file_checksum'),
+ 'file_checksum_right' => $this->dbHexFunc('cfr.file_checksum'),
+ 'file_action' => '(CASE WHEN cfr.config_checksum IS NULL'
+ . " THEN 'removed' WHEN cfl.file_checksum = cfr.file_checksum"
+ . " THEN 'unmodified' ELSE 'modified' END)",
+ )
+ )->joinLeft(
+ array('cfr' => 'director_generated_config_file'),
+ $db->quoteInto(
+ 'cfl.file_path = cfr.file_path AND cfr.config_checksum = ?',
+ $this->quoteBinary(hex2bin($this->rightChecksum))
+ ),
+ array()
+ )->where(
+ 'cfl.config_checksum = ?',
+ $this->quoteBinary(hex2bin($this->leftChecksum))
+ );
+
+ $right = $db->select()
+ ->from(
+ array('cfl' => 'director_generated_config_file'),
+ array(
+ 'file_path' => 'COALESCE(cfr.file_path, cfl.file_path)',
+ 'config_checksum_left' => $this->dbHexFunc('cfl.config_checksum'),
+ 'config_checksum_right' => $this->dbHexFunc('cfr.config_checksum'),
+ 'file_checksum_left' => $this->dbHexFunc('cfl.file_checksum'),
+ 'file_checksum_right' => $this->dbHexFunc('cfr.file_checksum'),
+ 'file_action' => "('created')",
+ )
+ )->joinRight(
+ array('cfr' => 'director_generated_config_file'),
+ $db->quoteInto(
+ 'cfl.file_path = cfr.file_path AND cfl.config_checksum = ?',
+ $this->quoteBinary(hex2bin($this->leftChecksum))
+ ),
+ array()
+ )->where(
+ 'cfr.config_checksum = ?',
+ $this->quoteBinary(hex2bin($this->rightChecksum))
+ )->where('cfl.file_checksum IS NULL');
+
+ return $db->select()->union(array($left, $right))->order('file_path');
+ }
+}
diff --git a/library/Director/Web/Table/CoreApiFieldsTable.php b/library/Director/Web/Table/CoreApiFieldsTable.php
new file mode 100644
index 0000000..24a6521
--- /dev/null
+++ b/library/Director/Web/Table/CoreApiFieldsTable.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use ipl\Html\Table;
+use gipfl\Translation\TranslationHelper;
+use gipfl\IcingaWeb2\Url;
+
+class CoreApiFieldsTable extends Table
+{
+ use TranslationHelper;
+
+ protected $defaultAttributes = [
+ 'class' => ['common-table'/*, 'table-row-selectable'*/],
+ //'data-base-target' => '_next',
+ ];
+
+ protected $fields;
+
+ /** @var Url */
+ protected $url;
+
+ public function __construct($fields, Url $url)
+ {
+ $this->url = $url;
+ $this->fields = $fields;
+ }
+
+ public function assemble()
+ {
+ if (empty($this->fields)) {
+ return;
+ }
+ $this->add(Html::tag('thead', Html::tag('tr', Html::wrapEach($this->getColumnsToBeRendered(), 'th'))));
+ foreach ($this->fields as $name => $field) {
+ $tr = $this::tr([
+ $this::td($name),
+ $this::td(Link::create(
+ $field->type,
+ $this->url->with('type', $field->type)
+ )),
+ $this::td($field->id)
+ // $this::td($field->array_rank),
+ // $this::td($this->renderKeyValue($field->attributes))
+ ]);
+ $this->addAttributeColumns($tr, $field->attributes);
+ $this->add($tr);
+ }
+ }
+
+ protected function addAttributeColumns(BaseHtmlElement $tr, $attrs)
+ {
+ $tr->add([
+ $this->makeBooleanColumn($attrs->state),
+ $this->makeBooleanColumn($attrs->config),
+ $this->makeBooleanColumn($attrs->required),
+ $this->makeBooleanColumn(isset($attrs->deprecated) ? $attrs->deprecated : null),
+ $this->makeBooleanColumn($attrs->no_user_modify),
+ $this->makeBooleanColumn($attrs->no_user_view),
+ $this->makeBooleanColumn($attrs->navigation),
+ ]);
+ }
+
+ protected function makeBooleanColumn($value)
+ {
+ if ($value === null) {
+ return $this::td('-');
+ }
+
+ return $this::td($value ? Html::tag('strong', 'true') : 'false');
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Name'),
+ $this->translate('Type'),
+ $this->translate('Id'),
+ // $this->translate('Array Rank'),
+ // $this->translate('Attributes')
+ $this->translate('State'),
+ $this->translate('Config'),
+ $this->translate('Required'),
+ $this->translate('Deprecated'),
+ $this->translate('Protected'),
+ $this->translate('Hidden'),
+ $this->translate('Nav'),
+ ];
+ }
+
+ protected function renderKeyValue($values)
+ {
+ $parts = [];
+ foreach ((array) $values as $key => $value) {
+ if (is_bool($value)) {
+ $value = $value ? 'true' : 'false';
+ }
+ $parts[] = "$key: $value";
+ }
+
+ return implode(', ', $parts);
+ }
+}
diff --git a/library/Director/Web/Table/CoreApiObjectsTable.php b/library/Director/Web/Table/CoreApiObjectsTable.php
new file mode 100644
index 0000000..c2cefea
--- /dev/null
+++ b/library/Director/Web/Table/CoreApiObjectsTable.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use gipfl\IcingaWeb2\Link;
+use ipl\Html\Html;
+use ipl\Html\Table;
+use gipfl\Translation\TranslationHelper;
+
+class CoreApiObjectsTable extends Table
+{
+ use TranslationHelper;
+
+ protected $defaultAttributes = [
+ 'class' => ['common-table', 'table-row-selectable'],
+ 'data-base-target' => '_next',
+ ];
+
+ /** @var IcingaEndpoint */
+ protected $endpoint;
+
+ protected $objects;
+
+ protected $type;
+
+ public function __construct($objects, IcingaEndpoint $endpoint, $type)
+ {
+ $this->objects = $objects;
+ $this->endpoint = $endpoint;
+ $this->type = $type;
+ }
+
+ public function assemble()
+ {
+ if (empty($this->objects)) {
+ return;
+ }
+ $this->add(Html::tag('thead', Html::tag('tr', Html::wrapEach($this->getColumnsToBeRendered(), 'th'))));
+ foreach ($this->objects as $name) {
+ $this->add($this::tr($this::td(Link::create(
+ str_replace('!', ': ', $name),
+ 'director/inspect/object',
+ [
+ 'name' => $name,
+ 'type' => $this->type->name,
+ 'plural' => $this->type->plural_name,
+ 'endpoint' => $this->endpoint->getObjectName()
+ ]
+ ))));
+ }
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Name'),
+ ];
+ }
+}
diff --git a/library/Director/Web/Table/CoreApiPrototypesTable.php b/library/Director/Web/Table/CoreApiPrototypesTable.php
new file mode 100644
index 0000000..78fd964
--- /dev/null
+++ b/library/Director/Web/Table/CoreApiPrototypesTable.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\Html;
+use ipl\Html\Table;
+use gipfl\Translation\TranslationHelper;
+
+class CoreApiPrototypesTable extends Table
+{
+ use TranslationHelper;
+
+ protected $defaultAttributes = ['class' => ['common-table']];
+
+ protected $prototypes;
+
+ protected $typeName;
+
+ public function __construct($prototypes, $typeName)
+ {
+ $this->prototypes = $prototypes;
+ $this->typeName = $typeName;
+ }
+
+ public function assemble()
+ {
+ if (empty($this->prototypes)) {
+ return;
+ }
+ $this->add(Html::tag('thead', Html::tag('tr', Html::wrapEach($this->getColumnsToBeRendered(), 'th'))));
+ $type = $this->typeName;
+ foreach ($this->prototypes as $name) {
+ $this->add($this::tr($this::td("$type.$name()")));
+ }
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Name'),
+ ];
+ }
+}
diff --git a/library/Director/Web/Table/CustomvarTable.php b/library/Director/Web/Table/CustomvarTable.php
new file mode 100644
index 0000000..f9a3844
--- /dev/null
+++ b/library/Director/Web/Table/CustomvarTable.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Zend_Db_Adapter_Abstract as ZfDbAdapter;
+use Zend_Db_Select as ZfDbSelect;
+
+class CustomvarTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = array(
+ 'varname',
+ );
+
+ public function renderRow($row)
+ {
+ $tr = $this::row([
+ new Link(
+ $row->varname,
+ 'director/customvar/variants',
+ ['name' => $row->varname]
+ )
+ ]);
+
+ foreach ($this->getObjectTypes() as $type) {
+ $tr->add($this::td(Html::tag('nobr', null, sprintf(
+ $this->translate('%d / %d'),
+ $row->{"cnt_$type"},
+ $row->{"distinct_$type"}
+ ))));
+ }
+
+ return $tr;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return array(
+ $this->translate('Variable name'),
+ $this->translate('Distinct Commands'),
+ $this->translate('Hosts'),
+ $this->translate('Services'),
+ $this->translate('Service Sets'),
+ $this->translate('Notifications'),
+ $this->translate('Users'),
+ );
+ }
+
+ protected function getObjectTypes()
+ {
+ return ['command', 'host', 'service', 'service_set', 'notification', 'user'];
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+ $varsColumns = ['varname' => 'v.varname'];
+ $varsTypes = $this->getObjectTypes();
+ foreach ($varsTypes as $type) {
+ $varsColumns["cnt_$type"] = '(0)';
+ $varsColumns["distinct_$type"] = '(0)';
+ }
+ $varsQueries = [];
+ foreach ($varsTypes as $type) {
+ $varsQueries[] = $this->makeVarSub($type, $varsColumns, $db);
+ }
+
+ $union = $db->select()->union($varsQueries, ZfDbSelect::SQL_UNION_ALL);
+
+ $columns = ['varname' => 'u.varname'];
+ foreach ($varsTypes as $column) {
+ $columns["cnt_$column"] = "SUM(u.cnt_$column)";
+ $columns["distinct_$column"] = "SUM(u.distinct_$column)";
+ }
+ return $db->select()->from(
+ array('u' => $union),
+ $columns
+ )->group('u.varname')->order('u.varname ASC')->limit(100);
+ }
+
+ /**
+ * @param string $type
+ * @param array $columns
+ * @param ZfDbAdapter $db
+ * @return ZfDbSelect
+ */
+ protected function makeVarSub($type, array $columns, ZfDbAdapter $db)
+ {
+ $columns["cnt_$type"] = 'COUNT(*)';
+ $columns["distinct_$type"] = 'COUNT(DISTINCT varvalue)';
+ return $db->select()->from(
+ ['v' => "icinga_${type}_var"],
+ $columns
+ )->join(
+ ['o' => "icinga_${type}"],
+ "o.id = v.${type}_id",
+ []
+ )->where('o.object_type != ?', 'external_object')->group('varname');
+ }
+}
diff --git a/library/Director/Web/Table/CustomvarVariantsTable.php b/library/Director/Web/Table/CustomvarVariantsTable.php
new file mode 100644
index 0000000..80fca70
--- /dev/null
+++ b/library/Director/Web/Table/CustomvarVariantsTable.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\PlainObjectRenderer;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Zend_Db_Adapter_Abstract as ZfDbAdapter;
+use Zend_Db_Select as ZfDbSelect;
+
+class CustomvarVariantsTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = ['varvalue'];
+
+ protected $varName;
+
+ public static function create(Db $db, $varName)
+ {
+ $table = new static($db);
+ $table->varName = $varName;
+ $table->getAttributes()->set('class', 'common-table');
+ return $table;
+ }
+
+ public function renderRow($row)
+ {
+ if ($row->format === 'json') {
+ $value = PlainObjectRenderer::render(json_decode($row->varvalue));
+ } else {
+ $value = $row->varvalue;
+ }
+ $tr = $this::row([
+ /* new Link(
+ $value,
+ 'director/customvar/value',
+ ['name' => $row->varvalue]
+ )*/
+ $value
+ ]);
+
+ foreach ($this->getObjectTypes() as $type) {
+ $cnt = (int) $row->{"cnt_$type"};
+ if ($cnt === 0) {
+ $cnt = '-';
+ }
+ $tr->add($this::td($cnt));
+ }
+
+ return $tr;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return array(
+ $this->translate('Variable Value'),
+ $this->translate('Commands'),
+ $this->translate('Hosts'),
+ $this->translate('Services'),
+ $this->translate('Service Sets'),
+ $this->translate('Notifications'),
+ $this->translate('Users'),
+ );
+ }
+
+ protected function getObjectTypes()
+ {
+ return ['command', 'host', 'service', 'service_set', 'notification', 'user'];
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+ $varsColumns = ['varvalue' => 'v.varvalue'];
+ $varsTypes = $this->getObjectTypes();
+ foreach ($varsTypes as $type) {
+ $varsColumns["cnt_$type"] = '(0)';
+ }
+ $varsQueries = [];
+ foreach ($varsTypes as $type) {
+ $varsQueries[] = $this->makeVarSub($type, $varsColumns, $db);
+ }
+
+ $union = $db->select()->union($varsQueries, ZfDbSelect::SQL_UNION_ALL);
+
+ $columns = [
+ 'varvalue' => 'u.varvalue',
+ 'format' => 'u.format',
+ ];
+ foreach ($varsTypes as $column) {
+ $columns["cnt_$column"] = "SUM(u.cnt_$column)";
+ }
+ return $db->select()->from(['u' => $union], $columns)
+ ->group('u.varvalue')->group('u.format')
+ ->order('u.varvalue ASC')
+ ->order('u.format ASC')
+ ->limit(100);
+ }
+
+ /**
+ * @param string $type
+ * @param array $columns
+ * @param ZfDbAdapter $db
+ * @return ZfDbSelect
+ */
+ protected function makeVarSub($type, array $columns, ZfDbAdapter $db)
+ {
+ $columns["cnt_$type"] = 'COUNT(*)';
+ $columns['format'] = 'v.format';
+ return $db->select()->from(
+ ['v' => "icinga_${type}_var"],
+ $columns
+ )->join(
+ ['o' => "icinga_${type}"],
+ "o.id = v.${type}_id",
+ []
+ )->where(
+ 'v.varname = ?',
+ $this->varName
+ )->where(
+ 'o.object_type != ?',
+ 'external_object'
+ )->group('varvalue')->group('v.format');
+ }
+}
diff --git a/library/Director/Web/Table/DatafieldCategoryTable.php b/library/Director/Web/Table/DatafieldCategoryTable.php
new file mode 100644
index 0000000..6f07939
--- /dev/null
+++ b/library/Director/Web/Table/DatafieldCategoryTable.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use ipl\Html\Html;
+
+class DatafieldCategoryTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'dfc.category_name',
+ 'dfc.description',
+ ];
+
+ public function getColumns()
+ {
+ return array(
+ 'id' => 'dfc.id',
+ 'category_name' => 'dfc.category_name',
+ 'description' => 'dfc.description',
+ 'assigned_fields' => 'COUNT(df.id)',
+ );
+ }
+
+ public function renderRow($row)
+ {
+ $main = [Link::create(
+ $row->category_name,
+ 'director/datafieldcategory/edit',
+ ['name' => $row->category_name]
+ )];
+
+ if ($row->description !== null && strlen($row->description)) {
+ $main[] = Html::tag('br');
+ $main[] = Html::tag('small', $row->description);
+ }
+ return $this::tr([
+ $this::td($main),
+ $this::td($row->assigned_fields)
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Category Name'),
+ $this->translate('# Used'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+ return $db->select()->from(
+ ['dfc' => 'director_datafield_category'],
+ $this->getColumns()
+ )->joinLeft(
+ ['df' => 'director_datafield'],
+ 'df.category_id = dfc.id',
+ []
+ )->group('dfc.id')->group('dfc.category_name')->order('category_name ASC');
+ }
+}
diff --git a/library/Director/Web/Table/DatafieldTable.php b/library/Director/Web/Table/DatafieldTable.php
new file mode 100644
index 0000000..4b321d7
--- /dev/null
+++ b/library/Director/Web/Table/DatafieldTable.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Zend_Db_Adapter_Abstract as ZfDbAdapter;
+use Zend_Db_Select as ZfDbSelect;
+
+class DatafieldTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'df.varname',
+ 'df.caption',
+ ];
+
+ public function getColumns()
+ {
+ return [
+ 'id' => 'df.id',
+ 'varname' => 'df.varname',
+ 'caption' => 'df.caption',
+ 'description' => 'df.description',
+ 'datatype' => 'df.datatype',
+ 'category' => 'dfc.category_name',
+ 'assigned_fields' => 'SUM(used_fields.cnt)',
+ 'assigned_vars' => 'SUM(used_vars.cnt)',
+ ];
+ }
+
+ public function renderRow($row)
+ {
+ return $this::tr([
+ $this::td(Link::create(
+ $row->caption,
+ 'director/datafield/edit',
+ ['id' => $row->id]
+ )),
+ $this::td($row->varname),
+ $this::td($row->category),
+ $this::td($row->assigned_fields),
+ $this::td($row->assigned_vars)
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Label'),
+ $this->translate('Field name'),
+ $this->translate('Category'),
+ $this->translate('# Used'),
+ $this->translate('# Vars'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+ $fieldTypes = ['command', 'host', 'notification', 'service', 'user'];
+ $varsTypes = ['command', 'host', 'notification', 'service', 'service_set', 'user'];
+
+ $fieldsQueries = [];
+ foreach ($fieldTypes as $type) {
+ $fieldsQueries[] = $this->makeDatafieldSub($type, $db);
+ }
+
+ $varsQueries = [];
+ foreach ($varsTypes as $type) {
+ $varsQueries[] = $this->makeVarSub($type, $db);
+ }
+
+ return $db->select()->from(
+ ['df' => 'director_datafield'],
+ $this->getColumns()
+ )->joinLeft(
+ ['dfc' => 'director_datafield_category'],
+ 'df.category_id = dfc.id',
+ []
+ )->joinLeft(
+ ['used_fields' => $db->select()->union($fieldsQueries, ZfDbSelect::SQL_UNION_ALL)],
+ 'used_fields.datafield_id = df.id',
+ []
+ )->joinLeft(
+ ['used_vars' => $db->select()->union($varsQueries, ZfDbSelect::SQL_UNION_ALL)],
+ 'used_vars.varname = df.varname',
+ []
+ )->group('df.id')->group('df.varname')->group('dfc.category_name')->order('caption ASC');
+ }
+
+ /**
+ * @param $type
+ * @param ZfDbAdapter $db
+ *
+ * @return ZfDbSelect
+ */
+ protected function makeDatafieldSub($type, ZfDbAdapter $db)
+ {
+ return $db->select()->from("icinga_${type}_field", [
+ 'cnt' => 'COUNT(*)',
+ 'datafield_id'
+ ])->group('datafield_id');
+ }
+
+ /**
+ * @param $type
+ * @param ZfDbAdapter $db
+ *
+ * @return ZfDbSelect
+ */
+ protected function makeVarSub($type, ZfDbAdapter $db)
+ {
+ return $db->select()->from("icinga_${type}_var", [
+ 'cnt' => 'COUNT(*)',
+ 'varname'
+ ])->group('varname');
+ }
+}
diff --git a/library/Director/Web/Table/DatalistEntryTable.php b/library/Director/Web/Table/DatalistEntryTable.php
new file mode 100644
index 0000000..70167c7
--- /dev/null
+++ b/library/Director/Web/Table/DatalistEntryTable.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\DirectorDatalist;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class DatalistEntryTable extends ZfQueryBasedTable
+{
+ protected $datalist;
+
+ protected $searchColumns = [
+ 'entry_name',
+ 'entry_value'
+ ];
+
+ public function setList(DirectorDatalist $list)
+ {
+ $this->datalist = $list;
+
+ return $this;
+ }
+
+ public function getList()
+ {
+ return $this->datalist;
+ }
+
+ public function getColumns()
+ {
+ return [
+ 'list_name' => 'l.list_name',
+ 'list_id' => 'le.list_id',
+ 'entry_name' => 'le.entry_name',
+ 'entry_value' => 'le.entry_value',
+ ];
+ }
+
+ public function renderRow($row)
+ {
+ return $this::tr([
+ $this::td(Link::create($row->entry_name, 'director/data/listentry/edit', [
+ 'list' => $row->list_name,
+ 'entry_name' => $row->entry_name,
+ ])),
+ $this::td($row->entry_value)
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ 'entry_name' => $this->translate('Key'),
+ 'entry_value' => $this->translate('Label'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['le' => 'director_datalist_entry'],
+ $this->getColumns()
+ )->join(
+ ['l' => 'director_datalist'],
+ 'l.id = le.list_id',
+ []
+ )->where(
+ 'le.list_id = ?',
+ $this->getList()->id
+ )->order('le.entry_name ASC');
+ }
+}
diff --git a/library/Director/Web/Table/DatalistTable.php b/library/Director/Web/Table/DatalistTable.php
new file mode 100644
index 0000000..7b35fe0
--- /dev/null
+++ b/library/Director/Web/Table/DatalistTable.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class DatalistTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = ['list_name'];
+
+ public function getColumns()
+ {
+ return [
+ 'id' => 'l.id',
+ 'list_name' => 'l.list_name',
+ ];
+ }
+
+ public function renderRow($row)
+ {
+ return $this::tr($this::td(Link::create(
+ $row->list_name,
+ 'director/data/listentry',
+ array('list' => $row->list_name)
+ )));
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [$this->translate('List name')];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['l' => 'director_datalist'],
+ $this->getColumns()
+ )->order('list_name ASC');
+ }
+}
diff --git a/library/Director/Web/Table/DbHelper.php b/library/Director/Web/Table/DbHelper.php
new file mode 100644
index 0000000..573f946
--- /dev/null
+++ b/library/Director/Web/Table/DbHelper.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Zend_Db_Expr as Expr;
+
+trait DbHelper
+{
+ public function dbHexFunc($column)
+ {
+ if ($this->isPgsql()) {
+ return sprintf("LOWER(ENCODE(%s, 'hex'))", $column);
+ } else {
+ return sprintf("LOWER(HEX(%s))", $column);
+ }
+ }
+
+ public function quoteBinary($binary)
+ {
+ if ($binary === '') {
+ return '';
+ }
+
+ if (is_array($binary)) {
+ return array_map([$this, 'quoteBinary'], $binary);
+ }
+
+ if ($this->isPgsql()) {
+ return new Expr("'\\x" . bin2hex($binary) . "'");
+ }
+
+ return new Expr('0x' . bin2hex($binary));
+ }
+
+ public function isPgsql()
+ {
+ return $this->db() instanceof \Zend_Db_Adapter_Pdo_Pgsql;
+ }
+
+ public function isMysql()
+ {
+ return $this->db() instanceof \Zend_Db_Adapter_Pdo_Mysql;
+ }
+
+ public function wantBinaryValue($value)
+ {
+ if (is_resource($value)) {
+ return stream_get_contents($value);
+ }
+
+ return $value;
+ }
+
+ public function getChecksum($checksum)
+ {
+ return bin2hex($this->wantBinaryValue($checksum));
+ }
+
+ public function getShortChecksum($checksum)
+ {
+ if ($checksum === null) {
+ return null;
+ }
+
+ return substr($this->getChecksum($checksum), 0, 7);
+ }
+}
diff --git a/library/Director/Web/Table/Dependency/DependencyInfoTable.php b/library/Director/Web/Table/Dependency/DependencyInfoTable.php
new file mode 100644
index 0000000..28aa856
--- /dev/null
+++ b/library/Director/Web/Table/Dependency/DependencyInfoTable.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table\Dependency;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Director\Application\DependencyChecker;
+use Icinga\Web\Url;
+
+class DependencyInfoTable
+{
+ protected $module;
+
+ protected $checker;
+
+ public function __construct(DependencyChecker $checker, Module $module)
+ {
+ $this->module = $module;
+ $this->checker = $checker;
+ }
+
+ protected function linkToModule($name, $icon)
+ {
+ return Html::link(
+ Html::escape($name),
+ Html::webUrl('config/module', ['name' => $name]),
+ [
+ 'class' => "icon-$icon"
+ ]
+ );
+ }
+
+ public function render()
+ {
+ $html = '<table class="common-table table-row-selectable">
+<thead>
+<tr>
+ <th>' . Html::escape($this->translate('Module name')) . '</th>
+ <th>' . Html::escape($this->translate('Required')) . '</th>
+ <th>' . Html::escape($this->translate('Installed')) . '</th>
+</tr>
+</thead>
+<tbody data-base-target="_next">
+';
+ foreach ($this->checker->getDependencies($this->module) as $dependency) {
+ $name = $dependency->getName();
+ $isLibrary = substr($name, 0, 11) === 'icinga-php-';
+ $rowAttributes = $isLibrary ? ['data-base-target' => '_self'] : null;
+ if ($dependency->isSatisfied()) {
+ if ($dependency->isSatisfied()) {
+ $icon = 'ok';
+ } else {
+ $icon = 'cancel';
+ }
+ $link = $isLibrary ? $this->noLink($name, $icon) : $this->linkToModule($name, $icon);
+ $installed = $dependency->getInstalledVersion();
+ } elseif ($dependency->isInstalled()) {
+ $installed = sprintf('%s (%s)', $dependency->getInstalledVersion(), $this->translate('disabled'));
+ $link = $this->linkToModule($name, 'cancel');
+ } else {
+ $installed = $this->translate('missing');
+ $repository = $isLibrary ? $name : "icingaweb2-module-$name";
+ $link = sprintf(
+ '%s (%s)',
+ $this->noLink($name, 'cancel'),
+ Html::linkToGitHub(Html::escape($this->translate('more')), 'Icinga', $repository)
+ );
+ }
+
+ $html .= $this->htmlRow([
+ $link,
+ Html::escape($dependency->getRequirement()),
+ Html::escape($installed)
+ ], $rowAttributes);
+ }
+
+ return $html . '</tbody>
+</table>
+';
+ }
+
+ protected function noLink($label, $icon)
+ {
+ return Html::link(Html::escape($label), Url::fromRequest()->with('rnd', rand(1, 100000)), [
+ 'class' => "icon-$icon"
+ ]);
+ }
+
+ protected function translate($string)
+ {
+ return \mt('director', $string);
+ }
+
+ protected function htmlRow(array $cols, $rowAttributes)
+ {
+ $content = '';
+ foreach ($cols as $escapedContent) {
+ $content .= Html::tag('td', null, $escapedContent);
+ }
+ return Html::tag('tr', $rowAttributes, $content);
+ }
+}
diff --git a/library/Director/Web/Table/Dependency/Html.php b/library/Director/Web/Table/Dependency/Html.php
new file mode 100644
index 0000000..092f799
--- /dev/null
+++ b/library/Director/Web/Table/Dependency/Html.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table\Dependency;
+
+use Icinga\Web\Url;
+use InvalidArgumentException;
+
+/**
+ * Minimal HTML helper, as we might be forced to run without ipl
+ */
+class Html
+{
+ public static function tag($tag, $attributes = [], $escapedContent = null)
+ {
+ $result = "<$tag";
+ if (! empty($attributes)) {
+ foreach ($attributes as $name => $value) {
+ if (! preg_match('/^[a-z][a-z0-9:-]*$/i', $name)) {
+ throw new InvalidArgumentException("Invalid attribute name: '$name'");
+ }
+
+ $result .= " $name=\"" . self::escapeAttributeValue($value) . '"';
+ }
+ }
+
+ return "$result>$escapedContent</$tag>";
+ }
+
+ public static function webUrl($path, $params)
+ {
+ return Url::fromPath($path, $params);
+ }
+
+ public static function link($escapedLabel, $url, $attributes = [])
+ {
+ return static::tag('a', [
+ 'href' => $url,
+ ] + $attributes, $escapedLabel);
+ }
+
+ public static function linkToGitHub($escapedLabel, $namespace, $repository)
+ {
+ return static::link(
+ $escapedLabel,
+ 'https://github.com/' . urlencode($namespace) . '/' . urlencode($repository),
+ [
+ 'target' => '_blank',
+ 'rel' => 'noreferrer',
+ 'class' => 'icon-forward'
+ ]
+ );
+ }
+
+ protected static function escapeAttributeValue($value)
+ {
+ $value = str_replace('"', '&quot;', $value);
+ // Escape ambiguous ampersands
+ return preg_replace_callback('/&[0-9A-Z]+;/i', function ($match) {
+ $subject = $match[0];
+
+ if (htmlspecialchars_decode($subject, ENT_COMPAT | ENT_HTML5) === $subject) {
+ // Ambiguous ampersand
+ return str_replace('&', '&amp;', $subject);
+ }
+
+ return $subject;
+ }, $value);
+ }
+
+ public static function escape($any)
+ {
+ return htmlspecialchars($any);
+ }
+}
diff --git a/library/Director/Web/Table/DependencyTemplateUsageTable.php b/library/Director/Web/Table/DependencyTemplateUsageTable.php
new file mode 100644
index 0000000..d7537c5
--- /dev/null
+++ b/library/Director/Web/Table/DependencyTemplateUsageTable.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+class DependencyTemplateUsageTable extends TemplateUsageTable
+{
+ public function getTypes()
+ {
+ return [
+ 'templates' => $this->translate('Templates'),
+ 'applyrules' => $this->translate('Apply Rules'),
+ ];
+ }
+
+ protected function getTypeSummaryDefinitions()
+ {
+ return [
+ 'templates' => $this->getSummaryLine('template'),
+ 'applyrules' => $this->getSummaryLine('apply'),
+ ];
+ }
+}
diff --git a/library/Director/Web/Table/DeploymentLogTable.php b/library/Director/Web/Table/DeploymentLogTable.php
new file mode 100644
index 0000000..2d5cb94
--- /dev/null
+++ b/library/Director/Web/Table/DeploymentLogTable.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use Icinga\Date\DateFormatter;
+
+class DeploymentLogTable extends ZfQueryBasedTable
+{
+ use DbHelper;
+
+ protected $activeStageName;
+
+ public function setActiveStageName($name)
+ {
+ $this->activeStageName = $name;
+ return $this;
+ }
+
+ public function assemble()
+ {
+ $this->getAttributes()->add('class', 'deployment-log');
+ }
+
+ public function renderRow($row)
+ {
+ $this->splitByDay($row->start_time);
+
+ $shortSum = $this->getShortChecksum($row->config_checksum);
+ $tr = $this::tr([
+ $this::td(Link::create(
+ $shortSum === null ? $row->peer_identity : [$row->peer_identity, " ($shortSum)"],
+ 'director/deployment',
+ ['id' => $row->id]
+ )),
+ $this::td(DateFormatter::formatTime($row->start_time))
+ ])->addAttributes(['class' => $this->getMyRowClasses($row)]);
+
+ return $tr;
+ }
+
+ protected function getMyRowClasses($row)
+ {
+ if ($row->startup_succeeded === 'y') {
+ $classes = ['succeeded'];
+ } elseif ($row->startup_succeeded === 'n') {
+ $classes = ['failed'];
+ } elseif ($row->stage_collected === null) {
+ $classes = ['pending'];
+ } elseif ($row->dump_succeeded === 'y') {
+ $classes = ['sent'];
+ } else {
+ // TODO: does this ever be stored?
+ $classes = ['notsent'];
+ }
+
+ if ($this->activeStageName !== null
+ && $row->stage_name === $this->activeStageName
+ ) {
+ $classes[] = 'running';
+ }
+
+ return $classes;
+ }
+
+ public function getColumns()
+ {
+ $columns = [
+ 'id' => 'l.id',
+ 'peer_identity' => 'l.peer_identity',
+ 'start_time' => 'UNIX_TIMESTAMP(l.start_time)',
+ 'stage_collected' => 'l.stage_collected',
+ 'dump_succeeded' => 'l.dump_succeeded',
+ 'stage_name' => 'l.stage_name',
+ 'startup_succeeded' => 'l.startup_succeeded',
+ 'config_checksum' => 'l.config_checksum',
+ ];
+
+ return $columns;
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ array('l' => 'director_deployment_log'),
+ $this->getColumns()
+ )->order('l.start_time DESC')->limit(100);
+ }
+}
diff --git a/library/Director/Web/Table/FilterableByUsage.php b/library/Director/Web/Table/FilterableByUsage.php
new file mode 100644
index 0000000..5e8695f
--- /dev/null
+++ b/library/Director/Web/Table/FilterableByUsage.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+interface FilterableByUsage
+{
+ public function showOnlyUsed();
+
+ public function showOnlyUnUsed();
+}
diff --git a/library/Director/Web/Table/GeneratedConfigFileTable.php b/library/Director/Web/Table/GeneratedConfigFileTable.php
new file mode 100644
index 0000000..97f7091
--- /dev/null
+++ b/library/Director/Web/Table/GeneratedConfigFileTable.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class GeneratedConfigFileTable extends ZfQueryBasedTable
+{
+ use DbHelper;
+
+ protected $searchColumns = ['file_path'];
+
+ protected $deploymentId;
+
+ protected $activeFile;
+
+ /** @var IcingaConfig */
+ protected $config;
+
+ public static function load(IcingaConfig $config, Db $db)
+ {
+ $table = new static($db);
+ $table->config = $config;
+ $table->getAttributes()->set('data-base-target', '_self');
+ return $table;
+ }
+
+ public function renderRow($row)
+ {
+ $counts = implode(' / ', [
+ $row->cnt_object,
+ $row->cnt_template,
+ $row->cnt_apply
+ ]);
+
+ $tr = $this::row([
+ $this->getFileLink($row),
+ $counts,
+ $row->size
+ ]);
+
+ if ($row->file_path === $this->activeFile) {
+ $tr->getAttributes()->add('class', 'active');
+ }
+
+ return $tr;
+ }
+
+ public function setActiveFilename($filename)
+ {
+ $this->activeFile = $filename;
+ return $this;
+ }
+
+ protected function getFileLink($row)
+ {
+ $params = [
+ 'config_checksum' => $row->config_checksum,
+ 'file_path' => $row->file_path
+ ];
+
+ if ($this->deploymentId) {
+ $params['deployment_id'] = $this->deploymentId;
+ }
+
+ return Link::create($row->file_path, 'director/config/file', $params);
+ }
+
+ public function setDeploymentId($id)
+ {
+ if ($id) {
+ $this->deploymentId = (int) $id;
+ }
+
+ return $this;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('File'),
+ $this->translate('Object/Tpl/Apply'),
+ $this->translate('Size'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ $columns = [
+ 'file_path' => 'cf.file_path',
+ 'size' => 'LENGTH(f.content)',
+ 'cnt_object' => 'f.cnt_object',
+ 'cnt_template' => 'f.cnt_template',
+ 'cnt_apply' => 'f.cnt_apply',
+ 'cnt_all' => "f.cnt_object || ' / ' || f.cnt_template || ' / ' || f.cnt_apply",
+ 'checksum' => 'LOWER(HEX(f.checksum))',
+ 'config_checksum' => 'LOWER(HEX(cf.config_checksum))',
+ ];
+
+ if ($this->isPgsql()) {
+ $columns['checksum'] = "LOWER(ENCODE(f.checksum, 'hex'))";
+ $columns['config_checksum'] = "LOWER(ENCODE(cf.config_checksum, 'hex'))";
+ }
+
+ return $this->db()->select()->from(
+ ['cf' => 'director_generated_config_file'],
+ $columns
+ )->join(
+ ['f' => 'director_generated_file'],
+ 'cf.file_checksum = f.checksum',
+ []
+ )->where(
+ 'config_checksum = ?',
+ $this->quoteBinary($this->config->getChecksum())
+ )->order('cf.file_path ASC');
+ }
+}
diff --git a/library/Director/Web/Table/GroupMemberTable.php b/library/Director/Web/Table/GroupMemberTable.php
new file mode 100644
index 0000000..b0814ad
--- /dev/null
+++ b/library/Director/Web/Table/GroupMemberTable.php
@@ -0,0 +1,201 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Table\Extension\MultiSelect;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\IcingaConfig\AssignRenderer;
+use Icinga\Module\Director\Objects\IcingaObjectGroup;
+use Exception;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+
+class GroupMemberTable extends ZfQueryBasedTable
+{
+ use MultiSelect;
+
+ protected $searchColumns = [
+ 'o.object_name',
+ // membership_type
+ ];
+
+ protected $type;
+
+ /** @var IcingaObjectGroup */
+ protected $group;
+
+ /**
+ * @param $type
+ * @param Db $db
+ * @return static
+ */
+ public static function create($type, Db $db)
+ {
+ $class = __NAMESPACE__ . '\\GroupMemberTable' . ucfirst($type);
+ if (! class_exists($class)) {
+ $class = __CLASS__;
+ }
+
+ /** @var static $table */
+ $table = new $class($db);
+ $table->type = $type;
+ return $table;
+ }
+ public function assemble()
+ {
+ if ($this->type === 'host') {
+ $this->enableMultiSelect(
+ 'director/hosts/edit',
+ 'director/hosts',
+ ['name']
+ );
+ }
+ }
+
+ public function setGroup(IcingaObjectGroup $group)
+ {
+ $this->group = $group;
+ return $this;
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ if ($this->group === null) {
+ return [
+ $this->translate('Group'),
+ $this->translate('Member'),
+ $this->translate('via')
+ ];
+ } else {
+ return [
+ $this->translate('Member'),
+ $this->translate('via')
+ ];
+ }
+ }
+
+ public function renderRow($row)
+ {
+ $type = $this->getType();
+ if ($row->object_type === 'apply') {
+ $params = [
+ 'id' => $row->id
+ ];
+ } elseif (isset($row->host_id)) {
+ // I would prefer to see host=<name> and set=<name>, but joining
+ // them here is pointless. We should use DeferredHtml for these,
+ // remember hosts/sets we need and fetch them in a single query at
+ // rendering time. For now, this works fine - just... the URLs are
+ // not so nice
+ $params = [
+ 'name' => $row->object_name,
+ 'host_id' => $row->host_id
+ ];
+ } elseif (isset($row->service_set_id)) {
+ $params = [
+ 'name' => $row->object_name,
+ 'set_id' => $row->service_set_id
+ ];
+ } else {
+ $params = [
+ 'name' => $row->object_name
+ ];
+ }
+
+ $url = Url::fromPath("director/${type}", $params);
+
+ $tr = $this::tr();
+
+ if ($this->group === null) {
+ $tr->add($this::td($row->group_name));
+ }
+ $link = Link::create($row->object_name, $url);
+ if ($row->object_type === 'apply') {
+ $link = [
+ $link,
+ ' (where ',
+ $this->renderApplyFilter($row->assign_filter),
+ ')'
+ ];
+ }
+
+ $tr->add([
+ $this::td($link),
+ $this::td($row->membership_type)
+ ]);
+
+ return $tr;
+ }
+
+ protected function renderApplyFilter($assignFilter)
+ {
+ try {
+ $string = AssignRenderer::forFilter(
+ Filter::fromQueryString($assignFilter)
+ )->renderAssign();
+ // Do not prefix it
+ $string = preg_replace('/^assign where /', '', $string);
+ } catch (Exception $e) {
+ // ignore errors in filter rendering
+ $string = 'Error in Filter rendering: ' . $e->getMessage();
+ }
+
+ return $string;
+ }
+
+ protected function prepareQuery()
+ {
+ // select h.object_name, hg.object_name,
+ // CASE WHEN hgh.host_id IS NULL THEN 'apply' ELSE 'direct' END AS assi
+ // from icinga_hostgroup_host_resolved hgr join icinga_host h on h.id = hgr.host_id
+ // join icinga_hostgroup hg on hgr.hostgroup_id = hg.id
+ // left join icinga_hostgroup_host hgh on hgh.host_id = h.id and hgh.hostgroup_id = hg.id;
+
+ $type = $this->getType();
+ $columns = [
+ 'o.id',
+ 'o.object_type',
+ 'o.object_name',
+ 'membership_type' => "CASE WHEN go.${type}_id IS NULL THEN 'apply' ELSE 'direct' END"
+ ];
+
+ if ($this->group === null) {
+ $columns = ['group_name' => 'g.object_name'] + $columns;
+ }
+ if ($type === 'service') {
+ $columns[] = 'o.assign_filter';
+ $columns[] = 'o.host_id';
+ $columns[] = 'o.service_set_id';
+ }
+
+ $query = $this->db()->select()->from(
+ ['gro' => "icinga_${type}group_${type}_resolved"],
+ $columns
+ )->join(
+ ['o' => "icinga_${type}"],
+ "o.id = gro.${type}_id",
+ []
+ )->join(
+ ['g' => "icinga_${type}group"],
+ "gro.${type}group_id = g.id",
+ []
+ )->joinLeft(
+ ['go' => "icinga_${type}group_${type}"],
+ "go.${type}_id = o.id AND go.${type}group_id = g.id",
+ []
+ )->order('o.object_name');
+
+ if ($this->group !== null) {
+ $query->where('g.id = ?', $this->group->get('id'));
+ }
+
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/HostTemplateUsageTable.php b/library/Director/Web/Table/HostTemplateUsageTable.php
new file mode 100644
index 0000000..2d1ee2f
--- /dev/null
+++ b/library/Director/Web/Table/HostTemplateUsageTable.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+class HostTemplateUsageTable extends TemplateUsageTable
+{
+ public function getTypes()
+ {
+ return [
+ 'templates' => $this->translate('Templates'),
+ 'objects' => $this->translate('Objects'),
+ ];
+ }
+
+ protected function getTypeSummaryDefinitions()
+ {
+ return [
+ 'templates' => $this->getSummaryLine('template'),
+ 'objects' => $this->getSummaryLine('object'),
+ ];
+ }
+}
diff --git a/library/Director/Web/Table/IcingaAppliedServiceTable.php b/library/Director/Web/Table/IcingaAppliedServiceTable.php
new file mode 100644
index 0000000..b669296
--- /dev/null
+++ b/library/Director/Web/Table/IcingaAppliedServiceTable.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\IcingaService;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class IcingaAppliedServiceTable extends ZfQueryBasedTable
+{
+ protected $service;
+
+ protected $searchColumns = array(
+ 'service',
+ );
+
+ public function setService(IcingaService $service)
+ {
+ $this->service = $service;
+ return $this;
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ new Link($row->service, 'director/service', ['id' => $row->id])
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [$this->translate('Servicename')];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ array('s' => 'icinga_service'),
+ array()
+ )->joinLeft(
+ array('si' => 'icinga_service_inheritance'),
+ 's.id = si.service_id',
+ array()
+ )->where(
+ 'si.parent_service_id = ?',
+ $this->service->id
+ )->where('s.object_type = ?', 'apply');
+ }
+}
diff --git a/library/Director/Web/Table/IcingaCommandArgumentTable.php b/library/Director/Web/Table/IcingaCommandArgumentTable.php
new file mode 100644
index 0000000..37cbc78
--- /dev/null
+++ b/library/Director/Web/Table/IcingaCommandArgumentTable.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Module\Director\Data\Json;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Branch\Branch;
+use Icinga\Module\Director\Db\Branch\BranchModificationStore;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class IcingaCommandArgumentTable extends ZfQueryBasedTable
+{
+ /** @var IcingaCommand */
+ protected $command;
+
+ /** @var Branch */
+ protected $branch;
+
+ protected $searchColumns = [
+ 'ca.argument_name',
+ 'ca.argument_value',
+ ];
+
+ public function __construct(IcingaCommand $command, Branch $branch)
+ {
+ $this->command = $command;
+ $this->branch = $branch;
+ parent::__construct($command->getConnection());
+ $this->getAttributes()->set('data-base-target', '_self');
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create($row->argument_name, 'director/command/arguments', [
+ 'argument' => $row->argument_name,
+ 'name' => $this->command->getObjectName()
+ ]),
+ $row->argument_value
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Argument'),
+ $this->translate('Value'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+ if ($this->branch->isBranch()) {
+ return (new ArrayDatasource((array) $this->command->arguments()->toPlainObject()))->select();
+ /** @var Db $connection */
+ $connection = $this->connection();
+ $store = new BranchModificationStore($connection, 'command');
+ $modification = $store->loadOptionalModificationByName(
+ $this->command->getObjectName(),
+ $this->branch->getUuid()
+ );
+ if ($modification) {
+ $props = $modification->getProperties()->jsonSerialize();
+ if (isset($props->arguments)) {
+ return new ArrayDatasource((array) $this->command->arguments()->toPlainObject());
+ }
+ }
+ }
+ $id = $this->command->get('id');
+ if ($id === null) {
+ return new ArrayDatasource([]);
+ }
+ return $this->db()->select()->from(
+ ['ca' => 'icinga_command_argument'],
+ [
+ 'id' => 'ca.id',
+ 'argument_name' => "COALESCE(ca.argument_name, '(none)')",
+ 'argument_value' => 'ca.argument_value',
+ ]
+ )->where(
+ 'ca.command_id = ?',
+ $id
+ )->order('ca.sort_order')->order('ca.argument_name')->limit(100);
+ }
+}
diff --git a/library/Director/Web/Table/IcingaHostAppliedForServiceTable.php b/library/Director/Web/Table/IcingaHostAppliedForServiceTable.php
new file mode 100644
index 0000000..0d2f8e8
--- /dev/null
+++ b/library/Director/Web/Table/IcingaHostAppliedForServiceTable.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\Html;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Module\Director\CustomVariable\CustomVariableDictionary;
+use Icinga\Module\Director\Objects\IcingaHost;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable;
+
+class IcingaHostAppliedForServiceTable extends SimpleQueryBasedTable
+{
+ protected $title;
+
+ protected $host;
+
+ /** @var CustomVariableDictionary */
+ protected $cv;
+
+ protected $searchColumns = [
+ 'service',
+ ];
+
+ /** @var bool */
+ protected $readonly = false;
+
+ /** @var string|null */
+ protected $highlightedService;
+
+ /**
+ * @param IcingaHost $host
+ * @param CustomVariableDictionary $dict
+ * @return static
+ */
+ public static function load(IcingaHost $host, CustomVariableDictionary $dict)
+ {
+ $table = (new static())->setHost($host)->setDictionary($dict);
+ $table->getAttributes()->set('data-base-target', '_self');
+ return $table;
+ }
+
+ public function setDictionary(CustomVariableDictionary $dict)
+ {
+ $this->cv = $dict;
+ return $this;
+ }
+
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ /**
+ * Show no related links
+ *
+ * @param bool $readonly
+ * @return $this
+ */
+ public function setReadonly($readonly = true)
+ {
+ $this->readonly = (bool) $readonly;
+
+ return $this;
+ }
+
+ public function highlightService($service)
+ {
+ $this->highlightedService = $service;
+
+ return $this;
+ }
+
+ public function renderRow($row)
+ {
+ if ($this->readonly) {
+ if ($this->highlightedService === $row->service) {
+ $link = Html::tag('span', ['class' => 'icon-right-big'], $row->service);
+ } else {
+ $link = $row->service;
+ }
+ } else {
+ $link = Link::create($row->service, 'director/host/appliedservice', [
+ 'name' => $this->host->object_name,
+ 'service' => $row->service,
+ ]);
+ }
+
+ return $this::row([$link]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->title ?: $this->translate('Service name'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ $data = [];
+ foreach ($this->cv->getValue() as $key => $var) {
+ $data[] = (object) array(
+ 'service' => $key,
+ );
+ }
+
+ return (new ArrayDatasource($data))->select();
+ }
+}
diff --git a/library/Director/Web/Table/IcingaHostAppliedServicesTable.php b/library/Director/Web/Table/IcingaHostAppliedServicesTable.php
new file mode 100644
index 0000000..415903b
--- /dev/null
+++ b/library/Director/Web/Table/IcingaHostAppliedServicesTable.php
@@ -0,0 +1,207 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\Html;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\IcingaConfig\AssignRenderer;
+use Icinga\Module\Director\Objects\HostApplyMatches;
+use Icinga\Module\Director\Objects\IcingaHost;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable;
+
+class IcingaHostAppliedServicesTable extends SimpleQueryBasedTable
+{
+ protected $title;
+
+ /** @var IcingaHost */
+ protected $host;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ /** @var bool */
+ protected $readonly = false;
+
+ /** @var string|null */
+ protected $highlightedService;
+
+ private $allApplyRules;
+
+ /**
+ * @param IcingaHost $host
+ * @return static
+ */
+ public static function load(IcingaHost $host)
+ {
+ $table = (new static())->setHost($host);
+ $table->getAttributes()->set('data-base-target', '_self');
+ return $table;
+ }
+
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [$this->title];
+ }
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ $this->db = $host->getDb();
+ return $this;
+ }
+
+ /**
+ * Show no related links
+ *
+ * @param bool $readonly
+ * @return $this
+ */
+ public function setReadonly($readonly = true)
+ {
+ $this->readonly = (bool) $readonly;
+
+ return $this;
+ }
+
+ public function highlightService($service)
+ {
+ $this->highlightedService = $service;
+
+ return $this;
+ }
+
+ public function renderRow($row)
+ {
+ $classes = [];
+ if ($row->blacklisted === 'y') {
+ $classes[] = 'strike-links';
+ }
+ if ($row->disabled === 'y') {
+ $classes[] = 'disabled';
+ }
+
+ $attributes = empty($classes) ? null : ['class' => $classes];
+
+ if ($this->readonly) {
+ if ($this->highlightedService === $row->name) {
+ $link = Html::tag('a', ['class' => 'icon-right-big'], $row->name);
+ } else {
+ $link = Html::tag('a', $row->name);
+ }
+ } else {
+ $applyFor = '';
+ if (! empty($row->apply_for)) {
+ $applyFor = sprintf('(apply for %s) ', $row->apply_for);
+ }
+
+ $link = Link::create(sprintf(
+ $this->translate('%s %s(%s)'),
+ $row->name,
+ $applyFor,
+ $this->renderApplyFilter($row->filter)
+ ), 'director/host/appliedservice', [
+ 'name' => $this->host->getObjectName(),
+ 'service_id' => $row->id,
+ ]);
+ }
+
+ return $this::row([$link], $attributes);
+ }
+
+ /**
+ * @param Filter $assignFilter
+ *
+ * @return string
+ */
+ protected function renderApplyFilter(Filter $assignFilter)
+ {
+ try {
+ $string = AssignRenderer::forFilter($assignFilter)->renderAssign();
+ } catch (IcingaException $e) {
+ $string = 'Error in Filter rendering: ' . $e->getMessage();
+ }
+
+ return $string;
+ }
+
+ /**
+ * @return \Icinga\Data\SimpleQuery
+ */
+ public function prepareQuery()
+ {
+ $services = [];
+ $matcher = HostApplyMatches::prepare($this->host);
+ foreach ($this->getAllApplyRules() as $rule) {
+ if ($matcher->matchesFilter($rule->filter)) {
+ $services[] = $rule;
+ }
+ }
+
+ $ds = new ArrayDatasource($services);
+ return $ds->select()->columns([
+ 'id' => 'id',
+ 'uuid' => 'uuid',
+ 'name' => 'name',
+ 'filter' => 'filter',
+ 'disabled' => 'disabled',
+ 'blacklisted' => 'blacklisted',
+ 'assign_filter' => 'assign_filter',
+ 'apply_for' => 'apply_for',
+ ]);
+ }
+
+ /***
+ * @return array
+ */
+ protected function getAllApplyRules()
+ {
+ if ($this->allApplyRules === null) {
+ $this->allApplyRules = $this->fetchAllApplyRules();
+ foreach ($this->allApplyRules as $rule) {
+ $rule->filter = Filter::fromQueryString($rule->assign_filter);
+ }
+ }
+
+ return $this->allApplyRules;
+ }
+
+ /**
+ * @return array
+ */
+ protected function fetchAllApplyRules()
+ {
+ $db = $this->db;
+ $hostId = $this->host->get('id');
+ $query = $db->select()->from(
+ ['s' => 'icinga_service'],
+ [
+ 'id' => 's.id',
+ 'uuid' => 's.uuid',
+ 'name' => 's.object_name',
+ 'assign_filter' => 's.assign_filter',
+ 'apply_for' => 's.apply_for',
+ 'disabled' => 's.disabled',
+ 'blacklisted' => $hostId ? "CASE WHEN hsb.service_id IS NULL THEN 'n' ELSE 'y' END" : "('n')",
+ ]
+ )->where('object_type = ? AND assign_filter IS NOT NULL', 'apply')
+ ->order('s.object_name');
+ if ($hostId) {
+ $query->joinLeft(
+ ['hsb' => 'icinga_host_service_blacklist'],
+ $db->quoteInto('s.id = hsb.service_id AND hsb.host_id = ?', $hostId),
+ []
+ );
+ }
+
+ return $db->fetchAll($query);
+ }
+}
diff --git a/library/Director/Web/Table/IcingaHostsMatchingFilterTable.php b/library/Director/Web/Table/IcingaHostsMatchingFilterTable.php
new file mode 100644
index 0000000..8d225bf
--- /dev/null
+++ b/library/Director/Web/Table/IcingaHostsMatchingFilterTable.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Data\SimpleQueryPaginationAdapter;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\QueryBasedTable;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\SimpleQuery;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Resolver\IcingaHostObjectResolver;
+
+class IcingaHostsMatchingFilterTable extends QueryBasedTable
+{
+ protected $searchColumns = [
+ 'object_name',
+ ];
+
+ /** @var ArrayDatasource */
+ protected $dataSource;
+
+ public static function load(Filter $filter, Db $db)
+ {
+ $table = new static();
+ $table->dataSource = new ArrayDatasource(
+ (new IcingaHostObjectResolver($db->getDbAdapter()))
+ ->fetchObjectsMatchingFilter($filter)
+ );
+
+ return $table;
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create(
+ $row->object_name,
+ 'director/host',
+ ['name' => $row->object_name]
+ )
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Hostname'),
+ ];
+ }
+
+ protected function getPaginationAdapter()
+ {
+ return new SimpleQueryPaginationAdapter($this->getQuery());
+ }
+
+ public function getQuery()
+ {
+ return $this->prepareQuery();
+ }
+
+ protected function fetchQueryRows()
+ {
+ return $this->dataSource->fetchAll($this->getQuery());
+ }
+
+ protected function prepareQuery()
+ {
+ return new SimpleQuery($this->dataSource, ['object_name']);
+ }
+}
diff --git a/library/Director/Web/Table/IcingaObjectDatafieldTable.php b/library/Director/Web/Table/IcingaObjectDatafieldTable.php
new file mode 100644
index 0000000..f97692e
--- /dev/null
+++ b/library/Director/Web/Table/IcingaObjectDatafieldTable.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Web\Form\IcingaObjectFieldLoader;
+use Icinga\Web\Url;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable;
+
+class IcingaObjectDatafieldTable extends SimpleQueryBasedTable
+{
+ protected $object;
+
+ /** @var int */
+ protected $objectId;
+
+ public function __construct(IcingaObject $object)
+ {
+ $this->object = $object;
+ $this->objectId = (int) $object->id;
+ return $this;
+ }
+
+ protected $searchColumns = array(
+ 'varname',
+ 'caption'
+ );
+
+ public function getColumns()
+ {
+ return array(
+ 'object_id',
+ 'var_filter',
+ 'is_required',
+ 'id',
+ 'varname',
+ 'caption',
+ 'description',
+ 'datatype',
+ 'format',
+ );
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return array(
+ 'caption' => $this->translate('Label'),
+ 'varname' => $this->translate('Field name'),
+ 'is_required' => $this->translate('Mandatory'),
+ );
+ }
+
+ public function renderRow($row)
+ {
+ $definedOnThis = (int) $row->object_id === $this->objectId;
+ if ($definedOnThis) {
+ $caption = new Link(
+ $row->caption,
+ Url::fromRequest()->with('field_id', $row->id)
+ );
+ } else {
+ $caption = $row->caption;
+ }
+
+ $row = $this::row([
+ $caption,
+ $row->varname,
+ $row->is_required
+ ]);
+
+ if (! $definedOnThis) {
+ $row->getAttributes()->add('class', 'disabled');
+ }
+
+ return $row;
+ }
+
+ public function prepareQuery()
+ {
+ $loader = new IcingaObjectFieldLoader($this->object);
+ $fields = $loader->fetchFieldDetailsForObject($this->object);
+ $ds = new ArrayDatasource($fields);
+ return $ds->select();
+ }
+}
diff --git a/library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php b/library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php
new file mode 100644
index 0000000..cd8f8b1
--- /dev/null
+++ b/library/Director/Web/Table/IcingaScheduledDowntimeRangeTable.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\IcingaScheduledDowntime;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class IcingaScheduledDowntimeRangeTable extends ZfQueryBasedTable
+{
+ /** @var IcingaScheduledDowntime */
+ protected $downtime;
+
+ protected $searchColumns = [
+ 'range_key',
+ 'range_value',
+ ];
+
+ /**
+ * @param IcingaScheduledDowntime $downtime
+ * @return static
+ */
+ public static function load(IcingaScheduledDowntime $downtime)
+ {
+ $table = new static($downtime->getConnection());
+ $table->downtime = $downtime;
+ $table->getAttributes()->set('data-base-target', '_self');
+
+ return $table;
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create(
+ $row->range_key,
+ 'director/scheduled-downtime/ranges',
+ [
+ 'name' => $this->downtime->getObjectName(),
+ 'range' => $row->range_key,
+ 'range_type' => 'include'
+ ]
+ ),
+ $row->range_value
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Day(s)'),
+ $this->translate('Timeperiods'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['r' => 'icinga_scheduled_downtime_range'],
+ [
+ 'scheduled_downtime_id' => 'r.scheduled_downtime_id',
+ 'range_key' => 'r.range_key',
+ 'range_value' => 'r.range_value',
+ ]
+ )->where('r.scheduled_downtime_id = ?', $this->downtime->id);
+ }
+}
diff --git a/library/Director/Web/Table/IcingaServiceSetHostTable.php b/library/Director/Web/Table/IcingaServiceSetHostTable.php
new file mode 100644
index 0000000..9fc3c61
--- /dev/null
+++ b/library/Director/Web/Table/IcingaServiceSetHostTable.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class IcingaServiceSetHostTable extends ZfQueryBasedTable
+{
+ protected $set;
+
+ protected $searchColumns = array(
+ 'host',
+ );
+
+ public static function load(IcingaServiceSet $set)
+ {
+ $table = new static($set->getConnection());
+ $table->set = $set;
+ return $table;
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create(
+ $row->host,
+ 'director/host',
+ ['name' => $row->host]
+ )
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Hostname'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['h' => 'icinga_host'],
+ [
+ 'id' => 'h.id',
+ 'host' => 'h.object_name',
+ 'object_type' => 'h.object_type',
+ ]
+ )->joinLeft(
+ ['ssh' => 'icinga_service_set'],
+ 'ssh.host_id = h.id',
+ []
+ )->joinLeft(
+ ['ssih' => 'icinga_service_set_inheritance'],
+ 'ssih.service_set_id = ssh.id',
+ []
+ )->where(
+ 'ssih.parent_service_set_id = ?',
+ $this->set->id
+ )->order('h.object_name');
+ }
+}
diff --git a/library/Director/Web/Table/IcingaServiceSetServiceTable.php b/library/Director/Web/Table/IcingaServiceSetServiceTable.php
new file mode 100644
index 0000000..c205e66
--- /dev/null
+++ b/library/Director/Web/Table/IcingaServiceSetServiceTable.php
@@ -0,0 +1,259 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Data\Db\ServiceSetQueryBuilder;
+use Icinga\Module\Director\Db;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use Icinga\Module\Director\Forms\RemoveLinkForm;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+
+class IcingaServiceSetServiceTable extends ZfQueryBasedTable
+{
+ use TableWithBranchSupport;
+
+ /** @var IcingaServiceSet */
+ protected $set;
+
+ protected $title;
+
+ /** @var IcingaHost */
+ protected $host;
+
+ /** @var IcingaHost */
+ protected $affectedHost;
+
+ protected $searchColumns = [
+ 'service',
+ ];
+
+ /** @var bool */
+ protected $readonly = false;
+
+ /** @var string|null */
+ protected $highlightedService;
+
+ /**
+ * @param IcingaServiceSet $set
+ * @return static
+ */
+ public static function load(IcingaServiceSet $set)
+ {
+ $table = new static($set->getConnection());
+ $table->set = $set;
+ $table->getAttributes()->set('data-base-target', '_self');
+ return $table;
+ }
+
+ /**
+ * @param string $title
+ * @return $this
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ /**
+ * @param IcingaHost $host
+ * @return $this
+ */
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ /**
+ * @param IcingaHost $host
+ * @return $this
+ */
+ public function setAffectedHost(IcingaHost $host)
+ {
+ $this->affectedHost = $host;
+ return $this;
+ }
+
+ /**
+ * Show no related links
+ *
+ * @param bool $readonly
+ * @return $this
+ */
+ public function setReadonly($readonly = true)
+ {
+ $this->readonly = (bool) $readonly;
+
+ return $this;
+ }
+
+ public function highlightService($service)
+ {
+ $this->highlightedService = $service;
+
+ return $this;
+ }
+
+ /**
+ * @param $row
+ * @return BaseHtmlElement
+ */
+ protected function getServiceLink($row)
+ {
+ if ($this->readonly) {
+ if ($this->highlightedService === $row->service) {
+ return Html::tag('span', ['class' => 'ro-service icon-right-big'], $row->service);
+ }
+
+ return Html::tag('span', ['class' => 'ro-service'], $row->service);
+ }
+
+ if ($this->affectedHost) {
+ $params = [
+ 'uuid' => $this->affectedHost->getUniqueId()->toString(),
+ 'service' => $row->service,
+ 'set' => $row->service_set
+ ];
+ $url = 'director/host/servicesetservice';
+ } else {
+ $params = [
+ 'name' => $row->service,
+ 'set' => $row->service_set
+ ];
+ $url = 'director/service';
+ }
+
+ return Link::create(
+ $row->service,
+ $url,
+ $params
+ );
+ }
+
+ public function renderRow($row)
+ {
+ $tr = $this::row([
+ $this->getServiceLink($row)
+ ]);
+ $classes = $this->getRowClasses($row);
+ if ($row->disabled === 'y') {
+ $classes[] = 'disabled';
+ }
+ if ($row->blacklisted === 'y') {
+ $classes[] = 'strike-links';
+ }
+ if (! empty($classes)) {
+ $tr->getAttributes()->add('class', $classes);
+ }
+
+ return $tr;
+ }
+
+ protected function getRowClasses($row)
+ {
+ if ($row->branch_uuid !== null) {
+ return ['branch_modified'];
+ }
+ return [];
+ }
+
+ protected function getTitle()
+ {
+ return $this->title ?: $this->translate('Servicename');
+ }
+
+ protected function renderTitleColumns()
+ {
+ if (! $this->host || ! $this->affectedHost) {
+ return Html::tag('th', $this->getTitle());
+ }
+
+ if ($this->readonly) {
+ $link = $this->createFakeRemoveLinkForReadonlyView();
+ } elseif ($this->affectedHost->get('id') !== $this->host->get('id')) {
+ $link = $this->linkToHost($this->host);
+ } else {
+ $link = $this->createRemoveLinkForm();
+ }
+
+ return $this::th([$this->getTitle(), $link]);
+ }
+
+ /**
+ * @return \Zend_Db_Select
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function prepareQuery()
+ {
+ $connection = $this->connection();
+ assert($connection instanceof Db);
+ $builder = new ServiceSetQueryBuilder($connection, $this->branchUuid);
+ return $builder->selectServicesForSet($this->set)->limit(100);
+ }
+
+ protected function createFakeRemoveLinkForReadonlyView()
+ {
+ return Html::tag('span', [
+ 'class' => 'icon-paste',
+ 'style' => 'float: right; font-weight: normal',
+ ], $this->host->getObjectName());
+ }
+
+ protected function linkToHost(IcingaHost $host)
+ {
+ $hostname = $host->getObjectName();
+ return Link::create($hostname, 'director/host/services', ['name' => $hostname], [
+ 'class' => 'icon-paste',
+ 'style' => 'float: right; font-weight: normal',
+ 'data-base-target' => '_next',
+ 'title' => sprintf(
+ $this->translate('This set has been inherited from %s'),
+ $hostname
+ )
+ ]);
+ }
+
+ protected function createRemoveLinkForm()
+ {
+ $deleteLink = new RemoveLinkForm(
+ $this->translate('Remove'),
+ sprintf(
+ $this->translate('Remove "%s" from this host'),
+ $this->getTitle()
+ ),
+ Url::fromPath('director/host/services', [
+ 'name' => $this->host->getObjectName()
+ ]),
+ ['title' => $this->getTitle()]
+ );
+ $deleteLink->runOnSuccess(function () {
+ $conn = $this->set->getConnection();
+ $db = $conn->getDbAdapter();
+ $query = $db->select()->from(['ss' => 'icinga_service_set'], 'ss.id')
+ ->join(['ssih' => 'icinga_service_set_inheritance'], 'ssih.service_set_id = ss.id', [])
+ ->where('ssih.parent_service_set_id = ?', $this->set->get('id'))
+ ->where('ss.host_id = ?', $this->host->get('id'));
+ IcingaServiceSet::loadWithAutoIncId(
+ $db->fetchOne($query),
+ $conn
+ )->delete();
+ });
+ $deleteLink->handleRequest();
+ return $deleteLink;
+ }
+
+ public function removeQueryLimit()
+ {
+ $query = $this->getQuery();
+ $query->reset($query::LIMIT_OFFSET);
+ $query->reset($query::LIMIT_COUNT);
+
+ return $this;
+ }
+}
diff --git a/library/Director/Web/Table/IcingaTimePeriodRangeTable.php b/library/Director/Web/Table/IcingaTimePeriodRangeTable.php
new file mode 100644
index 0000000..5870e67
--- /dev/null
+++ b/library/Director/Web/Table/IcingaTimePeriodRangeTable.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\IcingaTimePeriod;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class IcingaTimePeriodRangeTable extends ZfQueryBasedTable
+{
+ protected $period;
+
+ protected $searchColumns = array(
+ 'range_key',
+ 'range_value',
+ );
+
+ public static function load(IcingaTimePeriod $period)
+ {
+ $table = new static($period->getConnection());
+ $table->period = $period;
+ $table->getAttributes()->set('data-base-target', '_self');
+ return $table;
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create(
+ $row->range_key,
+ 'director/timeperiod/ranges',
+ array(
+ 'name' => $this->period->object_name,
+ 'range' => $row->range_key,
+ 'range_type' => 'include'
+ )
+ ),
+ $row->range_value
+ ]);
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Day(s)'),
+ $this->translate('Timeperiods'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['r' => 'icinga_timeperiod_range'],
+ [
+ 'timeperiod_id' => 'r.timeperiod_id',
+ 'range_key' => 'r.range_key',
+ 'range_value' => 'r.range_value',
+ ]
+ )->where('r.timeperiod_id = ?', $this->period->id);
+ }
+}
diff --git a/library/Director/Web/Table/ImportedrowsTable.php b/library/Director/Web/Table/ImportedrowsTable.php
new file mode 100644
index 0000000..d5c9811
--- /dev/null
+++ b/library/Director/Web/Table/ImportedrowsTable.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\Html;
+use ipl\Html\ValidHtml;
+use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Module\Director\Objects\ImportRun;
+use Icinga\Module\Director\PlainObjectRenderer;
+
+class ImportedrowsTable extends SimpleQueryBasedTable
+{
+ protected $columns;
+
+ /** @var ImportRun */
+ protected $importRun;
+
+ protected $keyColumn;
+
+ public static function load(ImportRun $run)
+ {
+ $table = new static();
+ $table->setImportRun($run);
+ return $table;
+ }
+
+ public function setImportRun(ImportRun $run)
+ {
+ $this->importRun = $run;
+ return $this;
+ }
+
+ public function setColumns($columns)
+ {
+ $this->columns = $columns;
+ return $this;
+ }
+
+ protected function getKeyColumn()
+ {
+ if ($this->keyColumn === null) {
+ $this->keyColumn = $this->importRun->importSource()->get('key_column');
+ }
+
+ return $this->keyColumn;
+ }
+
+ public function getColumns()
+ {
+ if ($this->columns === null) {
+ $cols = $this->importRun->listColumnNames();
+
+ $keyColumn = $this->getKeyColumn();
+ if ($keyColumn !== null && ($pos = array_search($keyColumn, $cols)) !== false) {
+ unset($cols[$pos]);
+ array_unshift($cols, $keyColumn);
+ }
+ } else {
+ $cols = $this->columns;
+ }
+
+ return array_combine($cols, $cols);
+ }
+
+ public function renderRow($row)
+ {
+ // Find a better place!
+ if ($row === null) {
+ return null;
+ }
+ $tr = $this::tr();
+
+ foreach ($this->getColumnsToBeRendered() as $column) {
+ $td = $this::td();
+ if (property_exists($row, $column)) {
+ if (is_string($row->$column) || $row->$column instanceof ValidHtml) {
+ $td->setContent($row->$column);
+ } else {
+ $html = Html::tag('pre', null, PlainObjectRenderer::render($row->$column));
+ $td->setContent($html);
+ }
+ }
+ $tr->add($td);
+ }
+
+ return $tr;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return $this->getColumns();
+ }
+
+ public function prepareQuery()
+ {
+ $ds = new ArrayDatasource(
+ $this->importRun->fetchRows($this->columns)
+ );
+
+ return $ds->select()->order($this->getKeyColumn());
+ }
+}
diff --git a/library/Director/Web/Table/ImportrunTable.php b/library/Director/Web/Table/ImportrunTable.php
new file mode 100644
index 0000000..e6c8a38
--- /dev/null
+++ b/library/Director/Web/Table/ImportrunTable.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\ImportSource;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class ImportrunTable extends ZfQueryBasedTable
+{
+ use DbHelper;
+
+ /** @var ImportSource */
+ protected $source;
+
+ protected $searchColumns = [
+ 'source_name',
+ ];
+
+ public static function load(ImportSource $source)
+ {
+ $table = new static($source->getConnection());
+ $table->source = $source;
+ return $table;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Source name'),
+ $this->translate('Timestamp'),
+ $this->translate('Imported rows'),
+ ];
+ }
+
+ public function renderRow($row)
+ {
+ return $this::row([
+ Link::create(
+ $row->source_name,
+ 'director/importrun',
+ ['id' => $row->id]
+ ),
+ $row->start_time,
+ $row->cnt_rows
+ ]);
+ }
+
+ public function prepareQuery()
+ {
+ $db = $this->db();
+ $columns = array(
+ 'id' => 'r.id',
+ 'source_id' => 's.id',
+ 'source_name' => 's.source_name',
+ 'start_time' => 'r.start_time',
+ 'rowset' => 'LOWER(HEX(rs.checksum))',
+ 'cnt_rows' => 'COUNT(rsr.row_checksum)',
+ );
+
+ if ($this->isPgsql()) {
+ $columns['rowset'] = "LOWER(ENCODE(rs.checksum, 'hex'))";
+ }
+
+ // TODO: Store row count to rowset
+ $query = $db->select()->from(
+ ['s' => 'import_source'],
+ $columns
+ )->join(
+ ['r' => 'import_run'],
+ 'r.source_id = s.id',
+ []
+ )->joinLeft(
+ ['rs' => 'imported_rowset'],
+ 'rs.checksum = r.rowset_checksum',
+ []
+ )->joinLeft(
+ ['rsr' => 'imported_rowset_row'],
+ 'rs.checksum = rsr.rowset_checksum',
+ []
+ )->group('r.id')->group('s.id')->group('rs.checksum')
+ ->order('r.start_time DESC');
+
+ if ($this->source) {
+ $query->where('r.source_id = ?', $this->source->get('id'));
+ }
+
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/ImportsourceHookTable.php b/library/Director/Web/Table/ImportsourceHookTable.php
new file mode 100644
index 0000000..5ddb6f3
--- /dev/null
+++ b/library/Director/Web/Table/ImportsourceHookTable.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use ipl\Html\ValidHtml;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Import\SyncUtils;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\PlainObjectRenderer;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Table\SimpleQueryBasedTable;
+
+class ImportsourceHookTable extends SimpleQueryBasedTable
+{
+ /** @var ImportSource */
+ protected $source;
+
+ protected $columnCache;
+
+ /** @var ImportSourceHook */
+ protected $sourceHook;
+
+ protected function assemble()
+ {
+ $this->getAttributes()->add('class', 'raw-data-table collapsed');
+ }
+
+ public function getColumns()
+ {
+ if ($this->columnCache === null) {
+ $this->columnCache = SyncUtils::getRootVariables(array_merge(
+ $this->sourceHook()->listColumns(),
+ $this->source->listModifierTargetProperties()
+ ));
+
+ sort($this->columnCache);
+
+ // prioritize key column
+ $keyColumn = $this->source->get('key_column');
+ if ($keyColumn !== null && ($pos = array_search($keyColumn, $this->columnCache)) !== false) {
+ unset($this->columnCache[$pos]);
+ array_unshift($this->columnCache, $keyColumn);
+ }
+ }
+
+ return $this->columnCache;
+ }
+
+ public function setImportSource(ImportSource $source)
+ {
+ $this->source = $source;
+ return $this;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return $this->getColumns();
+ }
+
+ public function renderRow($row)
+ {
+ // Find a better place!
+ if ($row === null) {
+ return null;
+ }
+ if (\is_array($row)) {
+ $row = (object) $row;
+ }
+ $tr = $this::tr();
+
+ foreach ($this->getColumnsToBeRendered() as $column) {
+ $td = $this::td();
+ if (\property_exists($row, $column)) {
+ if (\is_string($row->$column) || $row->$column instanceof ValidHtml) {
+ $td->setContent($row->$column);
+ } else {
+ $html = Html::tag('pre', null, PlainObjectRenderer::render($row->$column));
+ $td->setContent($html);
+ }
+ }
+ $tr->add($td);
+ }
+
+ return $tr;
+ }
+
+ protected function sourceHook()
+ {
+ if ($this->sourceHook === null) {
+ $this->sourceHook = ImportSourceHook::forImportSource(
+ $this->source
+ );
+ }
+
+ return $this->sourceHook;
+ }
+
+ public function prepareQuery()
+ {
+ $data = $this->sourceHook()->fetchData();
+ $this->source->applyModifiers($data);
+
+ $ds = new ArrayDatasource($data);
+ return $ds->select();
+ }
+}
diff --git a/library/Director/Web/Table/ImportsourceTable.php b/library/Director/Web/Table/ImportsourceTable.php
new file mode 100644
index 0000000..1a93ef5
--- /dev/null
+++ b/library/Director/Web/Table/ImportsourceTable.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class ImportsourceTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'source_name',
+ 'description',
+ ];
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Source name'),
+ ];
+ }
+
+ protected function assemble()
+ {
+ $this->getAttributes()->add('class', 'syncstate');
+ parent::assemble();
+ }
+
+ public function renderRow($row)
+ {
+ $caption = [Link::create(
+ $row->source_name,
+ 'director/importsource',
+ ['id' => $row->id]
+ )];
+ if ($row->description !== null) {
+ $caption[] = ': ' . $row->description;
+ }
+
+ if ($row->import_state === 'failing' && $row->last_error_message) {
+ $caption[] = ' (' . $row->last_error_message . ')';
+ }
+
+ $tr = $this::row([$caption]);
+ $tr->getAttributes()->add('class', $row->import_state);
+
+ return $tr;
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['s' => 'import_source'],
+ [
+ 'id' => 's.id',
+ 'source_name' => 's.source_name',
+ 'provider_class' => 's.provider_class',
+ 'import_state' => 's.import_state',
+ 'last_error_message' => 's.last_error_message',
+ 'description' => 's.description',
+ ]
+ )->order('source_name ASC');
+ }
+}
diff --git a/library/Director/Web/Table/JobTable.php b/library/Director/Web/Table/JobTable.php
new file mode 100644
index 0000000..81ba07b
--- /dev/null
+++ b/library/Director/Web/Table/JobTable.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class JobTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'job_name',
+ ];
+
+ protected function assemble()
+ {
+ $this->getAttributes()->add('class', 'jobs');
+ parent::assemble();
+ }
+
+ public function renderRow($row)
+ {
+ $caption = [Link::create(
+ $row->job_name,
+ 'director/job',
+ ['id' => $row->id]
+ )];
+
+ if ($row->last_attempt_succeeded === 'n' && $row->last_error_message) {
+ $caption[] = ' (' . $row->last_error_message . ')';
+ }
+
+ $tr = $this::row([$caption]);
+ $tr->getAttributes()->add('class', $this->getJobClasses($row));
+
+ return $tr;
+ }
+
+ protected function getJobClasses($row)
+ {
+ if ($row->unixts_last_attempt === null) {
+ return 'pending';
+ }
+
+ if ($row->unixts_last_attempt + $row->run_interval < time()) {
+ return 'pending';
+ }
+
+ if ($row->last_attempt_succeeded === 'y') {
+ return 'ok';
+ } elseif ($row->last_attempt_succeeded === 'n') {
+ return 'critical';
+ } else {
+ return 'unknown';
+ }
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Job name'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['j' => 'director_job'],
+ [
+ 'id' => 'j.id',
+ 'job_name' => 'j.job_name',
+ 'job_class' => 'j.job_class',
+ 'disabled' => 'j.disabled',
+ 'run_interval' => 'j.run_interval',
+ 'last_attempt_succeeded' => 'j.last_attempt_succeeded',
+ 'ts_last_attempt' => 'j.ts_last_attempt',
+ 'unixts_last_attempt' => 'UNIX_TIMESTAMP(j.ts_last_attempt)',
+ 'ts_last_error' => 'j.ts_last_error',
+ 'last_error_message' => 'j.last_error_message',
+ ]
+ )->order('job_name');
+ }
+}
diff --git a/library/Director/Web/Table/NotificationTemplateUsageTable.php b/library/Director/Web/Table/NotificationTemplateUsageTable.php
new file mode 100644
index 0000000..da411a3
--- /dev/null
+++ b/library/Director/Web/Table/NotificationTemplateUsageTable.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+class NotificationTemplateUsageTable extends TemplateUsageTable
+{
+ public function getTypes()
+ {
+ return [
+ 'templates' => $this->translate('Templates'),
+ 'applyrules' => $this->translate('Apply Rules'),
+ ];
+ }
+
+ protected function getTypeSummaryDefinitions()
+ {
+ return [
+ 'templates' => $this->getSummaryLine('template'),
+ 'applyrules' => $this->getSummaryLine('apply', 'o.host_id IS NULL'),
+ ];
+ }
+}
diff --git a/library/Director/Web/Table/ObjectSetTable.php b/library/Director/Web/Table/ObjectSetTable.php
new file mode 100644
index 0000000..2773841
--- /dev/null
+++ b/library/Director/Web/Table/ObjectSetTable.php
@@ -0,0 +1,211 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+use Icinga\Module\Director\Db\DbSelectParenthesis;
+use Icinga\Module\Director\Restriction\FilterByNameRestriction;
+use ipl\Html\Html;
+use Ramsey\Uuid\Uuid;
+
+class ObjectSetTable extends ZfQueryBasedTable
+{
+ use TableWithBranchSupport;
+
+ protected $searchColumns = [
+ 'os.object_name',
+ 'os.description',
+ 'os.assign_filter',
+ 'o.object_name',
+ ];
+
+ private $type;
+
+ /** @var Auth */
+ private $auth;
+
+ public static function create($type, Db $db, Auth $auth)
+ {
+ $table = new static($db);
+ $table->type = $type;
+ $table->auth = $auth;
+ return $table;
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [$this->translate('Name')];
+ }
+
+ public function renderRow($row)
+ {
+ $type = $this->getType();
+ $params = [
+ 'uuid' => Uuid::fromBytes(Db\DbUtil::binaryResult($row->uuid))->toString(),
+ ];
+
+ $url = Url::fromPath("director/${type}set", $params);
+
+ $classes = $this->getRowClasses($row);
+ $tr = static::tr([
+ static::td([
+ Link::create(sprintf(
+ $this->translate('%s (%d members)'),
+ $row->object_name,
+ $row->count_services
+ ), $url),
+ $row->description ? [Html::tag('br'), Html::tag('i', $row->description)] : null
+ ])
+ ]);
+ if (! empty($classes)) {
+ $tr->getAttributes()->add('class', $classes);
+ }
+
+ return $tr;
+ }
+
+ protected function getRowClasses($row)
+ {
+ if ($row->branch_uuid !== null) {
+ return ['branch_modified'];
+ }
+ return [];
+ }
+
+ protected function prepareQuery()
+ {
+ $type = $this->getType();
+
+ $table = "icinga_${type}_set";
+ $columns = [
+ 'id' => 'os.id',
+ 'uuid' => 'os.uuid',
+ 'branch_uuid' => '(NULL)',
+ 'object_name' => 'os.object_name',
+ 'object_type' => 'os.object_type',
+ 'assign_filter' => 'os.assign_filter',
+ 'description' => 'os.description',
+ 'count_services' => 'COUNT(DISTINCT o.uuid)',
+ ];
+ if ($this->branchUuid) {
+ $columns['branch_uuid'] = 'bos.branch_uuid';
+ $columns = $this->branchifyColumns($columns);
+ $this->stripSearchColumnAliases();
+ }
+
+ $query = $this->db()->select()->from(
+ ['os' => $table],
+ $columns
+ )->joinLeft(
+ ['o' => "icinga_${type}"],
+ "o.${type}_set_id = os.id",
+ []
+ );
+
+ $nameFilter = new FilterByNameRestriction(
+ $this->connection(),
+ $this->auth,
+ "${type}_set"
+ );
+ $nameFilter->applyToQuery($query, 'os');
+ /** @var Db $conn */
+ $conn = $this->connection();
+ if ($this->branchUuid) {
+ $right = clone($query);
+
+ $query->joinLeft(
+ ['bos' => "branched_$table"],
+ // TODO: PgHexFunc
+ $this->db()->quoteInto(
+ 'bos.uuid = os.uuid AND bos.branch_uuid = ?',
+ $conn->quoteBinary($this->branchUuid->getBytes())
+ ),
+ []
+ )->where("(bos.branch_deleted IS NULL OR bos.branch_deleted = 'n')");
+ $right->joinRight(
+ ['bos' => "branched_$table"],
+ 'bos.uuid = os.uuid',
+ []
+ )
+ ->where('os.uuid IS NULL')
+ ->where('bos.branch_uuid = ?', $conn->quoteBinary($this->branchUuid->getBytes()));
+ $query->group('COALESCE(os.uuid, bos.uuid)');
+ $right->group('COALESCE(os.uuid, bos.uuid)');
+ if ($conn->isPgsql()) {
+ // This is ugly, might want to modify the query - even a subselect looks better
+ $query->group('bos.uuid')->group('os.uuid')->group('os.id')->group('bos.branch_uuid');
+ $right->group('bos.uuid')->group('os.uuid')->group('os.id')->group('bos.branch_uuid');
+ }
+
+ $query = $this->db()->select()->union([
+ 'l' => new DbSelectParenthesis($query),
+ 'r' => new DbSelectParenthesis($right),
+ ]);
+ $query = $this->db()->select()->from(['u' => $query]);
+ $query->order('object_name')->limit(100);
+
+ $query
+ ->group('uuid')
+ ->where('object_type = ?', 'template')
+ ->order('object_name');
+ if ($conn->isPgsql()) {
+ // BS. Drop count? Sub-select? Better query?
+ $query
+ ->group('uuid')
+ ->group('id')
+ ->group('branch_uuid')
+ ->group('object_name')
+ ->group('object_type')
+ ->group('assign_filter')
+ ->group('description')
+ ->group('count_services');
+ };
+ } else {
+ // Disabled for now, check for correctness:
+ // $query->joinLeft(
+ // ['osi' => "icinga_${type}_set_inheritance"],
+ // "osi.parent_${type}_set_id = os.id",
+ // []
+ // )->joinLeft(
+ // ['oso' => "icinga_${type}_set"],
+ // "oso.id = oso.${type}_set_id",
+ // []
+ // );
+ // 'count_hosts' => 'COUNT(DISTINCT oso.id)',
+
+ $query
+ ->group('os.uuid')
+ ->where('os.object_type = ?', 'template')
+ ->order('os.object_name');
+ if ($conn->isPgsql()) {
+ // BS. Drop count? Sub-select? Better query?
+ $query
+ ->group('os.uuid')
+ ->group('os.id')
+ ->group('os.object_name')
+ ->group('os.object_type')
+ ->group('os.assign_filter')
+ ->group('os.description');
+ };
+ }
+
+ return $query;
+ }
+
+ /**
+ * @return Db
+ */
+ public function connection()
+ {
+ return parent::connection();
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTable.php b/library/Director/Web/Table/ObjectsTable.php
new file mode 100644
index 0000000..792cb6d
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTable.php
@@ -0,0 +1,315 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\DbSelectParenthesis;
+use Icinga\Module\Director\Db\IcingaObjectFilterHelper;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Restriction\FilterByNameRestriction;
+use Icinga\Module\Director\Restriction\HostgroupRestriction;
+use Icinga\Module\Director\Restriction\ObjectRestriction;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+use Ramsey\Uuid\Uuid;
+use Zend_Db_Select as ZfSelect;
+
+class ObjectsTable extends ZfQueryBasedTable
+{
+ use TableWithBranchSupport;
+
+ /** @var ObjectRestriction[] */
+ protected $objectRestrictions;
+
+ protected $columns = [
+ 'object_name' => 'o.object_name',
+ 'object_type' => 'o.object_type',
+ 'disabled' => 'o.disabled',
+ 'uuid' => 'o.uuid',
+ ];
+
+ protected $searchColumns = ['o.object_name'];
+
+ protected $showColumns = ['object_name' => 'Name'];
+
+ protected $filterObjectType = 'object';
+
+ protected $type;
+
+ protected $baseObjectUrl;
+
+ /** @var IcingaObject */
+ protected $dummyObject;
+
+ protected $leftSubQuery;
+
+ protected $rightSubQuery;
+
+ /** @var Auth */
+ private $auth;
+
+ /**
+ * @param $type
+ * @param Db $db
+ * @return static
+ */
+ public static function create($type, Db $db)
+ {
+ $class = __NAMESPACE__ . '\\ObjectsTable' . ucfirst($type);
+ if (! class_exists($class)) {
+ $class = __CLASS__;
+ }
+
+ /** @var static $table */
+ $table = new $class($db);
+ $table->type = $type;
+ return $table;
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * @param string $url
+ * @return $this
+ */
+ public function setBaseObjectUrl($url)
+ {
+ $this->baseObjectUrl = $url;
+
+ return $this;
+ }
+
+ /**
+ * @return Auth
+ */
+ public function getAuth()
+ {
+ return $this->auth;
+ }
+
+ public function setAuth(Auth $auth)
+ {
+ $this->auth = $auth;
+ return $this;
+ }
+
+ public function filterObjectType($type)
+ {
+ $this->filterObjectType = $type;
+ return $this;
+ }
+
+ public function addObjectRestriction(ObjectRestriction $restriction)
+ {
+ $this->objectRestrictions[$restriction->getName()] = $restriction;
+ return $this;
+ }
+
+ public function getColumns()
+ {
+ return $this->columns;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return $this->showColumns;
+ }
+
+ public function filterTemplate(
+ IcingaObject $template,
+ $inheritance = Db\IcingaObjectFilterHelper::INHERIT_DIRECT
+ ) {
+ IcingaObjectFilterHelper::filterByTemplate(
+ $this->getQuery(),
+ $template,
+ 'o',
+ $inheritance
+ );
+
+ return $this;
+ }
+
+ protected function getMainLinkLabel($row)
+ {
+ return $row->object_name;
+ }
+
+ protected function renderObjectNameColumn($row)
+ {
+ $type = $this->baseObjectUrl;
+ $url = Url::fromPath("director/${type}", [
+ 'uuid' => Uuid::fromBytes($row->uuid)->toString()
+ ]);
+
+ return static::td(Link::create($this->getMainLinkLabel($row), $url));
+ }
+
+ protected function renderExtraColumns($row)
+ {
+ $columns = $this->getColumnsToBeRendered();
+ unset($columns['object_name']);
+ $cols = [];
+ foreach ($columns as $key => & $label) {
+ $cols[] = static::td($row->$key);
+ }
+
+ return $cols;
+ }
+
+ public function renderRow($row)
+ {
+ if (isset($row->uuid) && is_resource($row->uuid)) {
+ $row->uuid = stream_get_contents($row->uuid);
+ }
+ $tr = static::tr([
+ $this->renderObjectNameColumn($row),
+ $this->renderExtraColumns($row)
+ ]);
+
+ $classes = $this->getRowClasses($row);
+ if ($row->disabled === 'y') {
+ $classes[] = 'disabled';
+ }
+ if (! empty($classes)) {
+ $tr->getAttributes()->add('class', $classes);
+ }
+
+ return $tr;
+ }
+
+ protected function getRowClasses($row)
+ {
+ // TODO: remove isset, to figure out where it is missing
+ if (isset($row->branch_uuid) && $row->branch_uuid !== null) {
+ return ['branch_modified'];
+ }
+ return [];
+ }
+
+ protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null)
+ {
+ if ($right) {
+ $right->where(
+ 'bo.object_type = ?',
+ $this->filterObjectType
+ );
+ }
+ return $query->where(
+ 'o.object_type = ?',
+ $this->filterObjectType
+ );
+ }
+
+ protected function applyRestrictions(ZfSelect $query)
+ {
+ foreach ($this->getRestrictions() as $restriction) {
+ $restriction->applyToQuery($query);
+ }
+
+ return $query;
+ }
+
+ protected function getRestrictions()
+ {
+ if ($this->objectRestrictions === null) {
+ $this->objectRestrictions = $this->loadRestrictions();
+ }
+
+ return $this->objectRestrictions;
+ }
+
+ protected function loadRestrictions()
+ {
+ /** @var Db $db */
+ $db = $this->connection();
+ $auth = $this->getAuth();
+
+ return [
+ new HostgroupRestriction($db, $auth),
+ new FilterByNameRestriction($db, $auth, $this->getDummyObject()->getShortTableName())
+ ];
+ }
+
+ /**
+ * @return IcingaObject
+ */
+ protected function getDummyObject()
+ {
+ if ($this->dummyObject === null) {
+ $type = $this->getType();
+ $this->dummyObject = IcingaObject::createByType($type);
+ }
+ return $this->dummyObject;
+ }
+
+ protected function prepareQuery()
+ {
+ $table = $this->getDummyObject()->getTableName();
+ if ($this->branchUuid) {
+ $this->columns['branch_uuid'] = 'bo.branch_uuid';
+ }
+
+ $columns = $this->getColumns();
+ if ($this->branchUuid) {
+ $columns = $this->branchifyColumns($columns);
+ $this->stripSearchColumnAliases();
+ }
+ $query = $this->db()->select()->from(['o' => $table], $columns);
+
+ if ($this->branchUuid) {
+ $right = clone($query);
+ // Hint: Right part has only those with object = null
+ // This means that restrictions on $right would hide all
+ // new rows. Dedicated restriction logic for the branch-only
+ // part of thw union are not required, we assume that restrictions
+ // for new objects have been checked once they have been created
+ $query = $this->applyRestrictions($query);
+ /** @var Db $conn */
+ $conn = $this->connection();
+ $query->joinLeft(
+ ['bo' => "branched_$table"],
+ // TODO: PgHexFunc
+ $this->db()->quoteInto(
+ 'bo.uuid = o.uuid AND bo.branch_uuid = ?',
+ $conn->quoteBinary($this->branchUuid->getBytes())
+ ),
+ []
+ )->where("(bo.branch_deleted IS NULL OR bo.branch_deleted = 'n')");
+ $this->applyObjectTypeFilter($query, $right);
+ $right->joinRight(
+ ['bo' => "branched_$table"],
+ 'bo.uuid = o.uuid',
+ []
+ )
+ ->where('o.uuid IS NULL')
+ ->where('bo.branch_uuid = ?', $conn->quoteBinary($this->branchUuid->getBytes()));
+ $this->leftSubQuery = $query;
+ $this->rightSubQuery = $right;
+ $query = $this->db()->select()->union([
+ 'l' => new DbSelectParenthesis($query),
+ 'r' => new DbSelectParenthesis($right),
+ ]);
+ $query = $this->db()->select()->from(['u' => $query]);
+ $query->order('object_name')->limit(100);
+ } else {
+ $this->applyObjectTypeFilter($query);
+ $query->order('o.object_name')->limit(100);
+ }
+
+ return $query;
+ }
+
+ public function removeQueryLimit()
+ {
+ $query = $this->getQuery();
+ $query->reset($query::LIMIT_OFFSET);
+ $query->reset($query::LIMIT_COUNT);
+
+ return $this;
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableApiUser.php b/library/Director/Web/Table/ObjectsTableApiUser.php
new file mode 100644
index 0000000..2287c2f
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableApiUser.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Zend_Db_Select as ZfSelect;
+
+class ObjectsTableApiUser extends ObjectsTable
+{
+ protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null)
+ {
+ return $query->where("o.object_type IN ('object', 'external_object')");
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableCommand.php b/library/Director/Web/Table/ObjectsTableCommand.php
new file mode 100644
index 0000000..ebd89da
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableCommand.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Zend_Db_Select as ZfSelect;
+
+class ObjectsTableCommand extends ObjectsTable implements FilterableByUsage
+{
+ // TODO: Notifications separately?
+ protected $searchColumns = [
+ 'o.object_name',
+ 'o.command',
+ ];
+
+ protected $columns = [
+ 'uuid' => 'o.uuid',
+ 'object_name' => 'o.object_name',
+ 'object_type' => 'o.object_type',
+ 'disabled' => 'o.disabled',
+ 'command' => 'o.command',
+ ];
+
+ protected $showColumns = [
+ 'object_name' => 'Command',
+ 'command' => 'Command line'
+ ];
+
+ private $objectType;
+
+ public function setType($type)
+ {
+ $this->getQuery()->where('object_type = ?', $type);
+
+ return $this;
+ }
+
+ public function showOnlyUsed()
+ {
+ $this->getQuery()->where(
+ '('
+ . 'EXISTS (SELECT check_command_id FROM icinga_host WHERE check_command_id = o.id)'
+ . ' OR EXISTS (SELECT check_command_id FROM icinga_service WHERE check_command_id = o.id)'
+ . ' OR EXISTS (SELECT event_command_id FROM icinga_host WHERE event_command_id = o.id)'
+ . ' OR EXISTS (SELECT event_command_id FROM icinga_service WHERE event_command_id = o.id)'
+ . ' OR EXISTS (SELECT command_id FROM icinga_notification WHERE command_id = o.id)'
+ . ')'
+ );
+ }
+
+ public function showOnlyUnUsed()
+ {
+ $this->getQuery()->where(
+ '('
+ . 'NOT EXISTS (SELECT check_command_id FROM icinga_host WHERE check_command_id = o.id)'
+ . ' AND NOT EXISTS (SELECT check_command_id FROM icinga_service WHERE check_command_id = o.id)'
+ . ' AND NOT EXISTS (SELECT event_command_id FROM icinga_host WHERE event_command_id = o.id)'
+ . ' AND NOT EXISTS (SELECT event_command_id FROM icinga_service WHERE event_command_id = o.id)'
+ . ' AND NOT EXISTS (SELECT command_id FROM icinga_notification WHERE command_id = o.id)'
+ . ')'
+ );
+ }
+
+ protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null)
+ {
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableEndpoint.php b/library/Director/Web/Table/ObjectsTableEndpoint.php
new file mode 100644
index 0000000..f73b38b
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableEndpoint.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Icon;
+use Zend_Db_Select as ZfSelect;
+
+class ObjectsTableEndpoint extends ObjectsTable
+{
+ protected $searchColumns = [
+ 'o.object_name',
+ ];
+
+ protected $deploymentEndpoint;
+
+ public function getColumnsToBeRendered()
+ {
+ return array(
+ 'object_name' => $this->translate('Endpoint'),
+ 'host' => $this->translate('Host'),
+ 'zone' => $this->translate('Zone'),
+ 'object_type' => $this->translate('Type'),
+ );
+ }
+
+ public function getColumns()
+ {
+ return [
+ 'uuid' => 'o.uuid',
+ 'object_name' => 'o.object_name',
+ 'object_type' => 'o.object_type',
+ 'disabled' => 'o.disabled',
+ 'host' => "(CASE WHEN o.host IS NULL THEN NULL ELSE"
+ . " CONCAT(o.host || ':' || COALESCE(o.port, 5665)) END)",
+ 'zone' => 'z.object_name',
+ ];
+ }
+
+ protected function getMainLinkLabel($row)
+ {
+ if ($row->object_name === $this->deploymentEndpoint) {
+ return [
+ $row->object_name,
+ ' ',
+ Icon::create('upload', [
+ 'title' => $this->translate(
+ 'This is your Config master and will receive our Deployments'
+ )
+ ])
+ ];
+ } else {
+ return $row->object_name;
+ }
+ }
+
+ public function getRowClasses($row)
+ {
+ if ($row->object_name === $this->deploymentEndpoint) {
+ return array_merge(array('deployment-endpoint'), parent::getRowClasses($row));
+ } else {
+ return null;
+ }
+ }
+
+ protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null)
+ {
+ return $query->where("o.object_type IN ('object', 'external_object')");
+ }
+
+ public function prepareQuery()
+ {
+ if ($this->deploymentEndpoint === null) {
+ /** @var \Icinga\Module\Director\Db $c */
+ $c = $this->connection();
+ if ($c->hasDeploymentEndpoint()) {
+ $this->deploymentEndpoint = $c->getDeploymentEndpointName();
+ }
+ }
+
+ return parent::prepareQuery()->joinLeft(
+ ['z' => 'icinga_zone'],
+ 'o.zone_id = z.id',
+ []
+ );
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableHost.php b/library/Director/Web/Table/ObjectsTableHost.php
new file mode 100644
index 0000000..5128e04
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableHost.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Table\Extension\MultiSelect;
+
+class ObjectsTableHost extends ObjectsTable
+{
+ use MultiSelect;
+
+ protected $type = 'host';
+
+ protected $searchColumns = [
+ 'o.object_name',
+ 'o.display_name',
+ 'o.address',
+ ];
+
+ protected $columns = [
+ 'object_name' => 'o.object_name',
+ 'display_name' => 'o.display_name',
+ 'address' => 'o.address',
+ 'disabled' => 'o.disabled',
+ 'uuid' => 'o.uuid',
+ ];
+
+ protected $showColumns = [
+ 'object_name' => 'Hostname',
+ 'address' => 'Address'
+ ];
+
+ public function assemble()
+ {
+ $this->enableMultiSelect(
+ 'director/hosts/edit',
+ 'director/hosts',
+ ['uuid']
+ );
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableHostTemplateChoice.php b/library/Director/Web/Table/ObjectsTableHostTemplateChoice.php
new file mode 100644
index 0000000..929e050
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableHostTemplateChoice.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Zend_Db_Select as ZfSelect;
+
+class ObjectsTableHostTemplateChoice extends ObjectsTable
+{
+ protected $columns = [
+ 'object_name' => 'o.object_name',
+ 'templates' => 'GROUP_CONCAT(t.object_name)'
+ ];
+
+ protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null)
+ {
+ return $query;
+ }
+
+ protected function prepareQuery()
+ {
+ return parent::prepareQuery()->joinLeft(
+ ['t' => 'icinga_host'],
+ 't.template_choice_id = o.id',
+ []
+ )->group('o.id');
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableService.php b/library/Director/Web/Table/ObjectsTableService.php
new file mode 100644
index 0000000..2d4ad41
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableService.php
@@ -0,0 +1,219 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Db\DbUtil;
+use Icinga\Module\Director\Objects\IcingaHost;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Table\Extension\MultiSelect;
+use gipfl\IcingaWeb2\Link;
+use Ramsey\Uuid\Uuid;
+
+class ObjectsTableService extends ObjectsTable
+{
+ use MultiSelect;
+
+ /** @var IcingaHost */
+ protected $host;
+
+ protected $type = 'service';
+
+ protected $title;
+
+ /** @var IcingaHost */
+ protected $inheritedBy;
+
+ /** @var bool */
+ protected $readonly = false;
+
+ /** @var string|null */
+ protected $highlightedService;
+
+ protected $columns = [
+ 'object_name' => 'o.object_name',
+ 'disabled' => 'o.disabled',
+ 'host' => 'h.object_name',
+ 'host_id' => 'h.id',
+ 'host_object_type' => 'h.object_type',
+ 'host_disabled' => 'h.disabled',
+ 'id' => 'o.id',
+ 'uuid' => 'o.uuid',
+ 'blacklisted' => "CASE WHEN hsb.service_id IS NULL THEN 'n' ELSE 'y' END",
+ ];
+
+ protected $searchColumns = [
+ 'o.object_name',
+ 'h.object_name'
+ ];
+
+ public function assemble()
+ {
+ $this->enableMultiSelect(
+ 'director/services/edit',
+ 'director/services',
+ ['uuid']
+ );
+ }
+
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function setHost(IcingaHost $host)
+ {
+ $this->host = $host;
+ $this->getAttributes()->set('data-base-target', '_self');
+ return $this;
+ }
+
+ public function setInheritedBy(IcingaHost $host)
+ {
+ $this->inheritedBy = $host;
+ return $this;
+ }
+
+ /**
+ * Show no related links
+ *
+ * @param bool $readonly
+ * @return $this
+ */
+ public function setReadonly($readonly = true)
+ {
+ $this->readonly = (bool) $readonly;
+
+ return $this;
+ }
+
+ public function highlightService($service)
+ {
+ $this->highlightedService = $service;
+
+ return $this;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ if ($this->title) {
+ return [$this->title];
+ }
+ if ($this->host) {
+ return [$this->translate('Servicename')];
+ }
+ return [
+ 'host' => $this->translate('Host'),
+ 'object_name' => $this->translate('Service Name'),
+ ];
+ }
+
+ public function renderRow($row)
+ {
+ $caption = $row->host === null
+ ? Html::tag('span', ['class' => 'error'], '- none -')
+ : $row->host;
+
+ $hostField = static::td($caption);
+ if ($row->host === null) {
+ $hostField->getAttributes()->add('class', 'error');
+ }
+ if ($this->host) {
+ $tr = static::tr([
+ static::td($this->getServiceLink($row))
+ ]);
+ } else {
+ $tr = static::tr([
+ $hostField,
+ static::td($this->getServiceLink($row))
+ ]);
+ }
+
+ $attributes = $tr->getAttributes();
+ $classes = $this->getRowClasses($row);
+ if ($row->host_disabled === 'y' || $row->disabled === 'y') {
+ $classes[] = 'disabled';
+ }
+ if ($row->blacklisted === 'y') {
+ $classes[] = 'strike-links';
+ }
+ $attributes->add('class', $classes);
+
+ return $tr;
+ }
+
+ protected function getInheritedServiceLink($row, $target)
+ {
+ $params = [
+ 'name' => $target->object_name,
+ 'service' => $row->object_name,
+ 'inheritedFrom' => $row->host,
+ ];
+
+ return Link::create(
+ $row->object_name,
+ 'director/host/inheritedservice',
+ $params
+ );
+ }
+
+ protected function getServiceLink($row)
+ {
+ if ($this->readonly) {
+ if ($this->highlightedService === $row->object_name) {
+ return Html::tag('span', ['class' => 'icon-right-big'], $row->object_name);
+ } else {
+ return $row->object_name;
+ }
+ }
+
+ $params = [
+ 'uuid' => Uuid::fromBytes(DbUtil::binaryResult($row->uuid))->toString(),
+ ];
+ if ($row->host !== null) {
+ $params['host'] = $row->host;
+ }
+ if ($target = $this->inheritedBy) {
+ return $this->getInheritedServiceLink($row, $target);
+ }
+
+ return Link::create(
+ $row->object_name,
+ 'director/service/edit',
+ $params
+ );
+ }
+
+ public function prepareQuery()
+ {
+ $query = parent::prepareQuery();
+ if ($this->branchUuid) {
+ $queries = [$this->leftSubQuery, $this->rightSubQuery];
+ } else {
+ $queries = [$query];
+ }
+
+ foreach ($queries as $subQuery) {
+ $subQuery->joinLeft(
+ ['h' => 'icinga_host'],
+ 'o.host_id = h.id',
+ []
+ )->joinLeft(
+ ['hsb' => 'icinga_host_service_blacklist'],
+ 'hsb.service_id = o.id AND hsb.host_id = o.host_id',
+ []
+ )->where('o.service_set_id IS NULL')
+ ->order('o.object_name')->order('h.object_name');
+
+ if ($this->host) {
+ if ($this->branchUuid) {
+ $subQuery->where('COALESCE(h.object_name, bo.host) = ?', $this->host->getObjectName());
+ } else {
+ $subQuery->where('h.id = ?', $this->host->get('id'));
+ }
+ }
+ }
+
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/ObjectsTableZone.php b/library/Director/Web/Table/ObjectsTableZone.php
new file mode 100644
index 0000000..602cf0a
--- /dev/null
+++ b/library/Director/Web/Table/ObjectsTableZone.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Zend_Db_Select as ZfSelect;
+
+class ObjectsTableZone extends ObjectsTable
+{
+ protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null)
+ {
+ return $query;
+ }
+}
diff --git a/library/Director/Web/Table/PropertymodifierTable.php b/library/Director/Web/Table/PropertymodifierTable.php
new file mode 100644
index 0000000..bf9e4a3
--- /dev/null
+++ b/library/Director/Web/Table/PropertymodifierTable.php
@@ -0,0 +1,145 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Error;
+use Exception;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Objects\ImportSource;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\Extension\ZfSortablePriority;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+
+class PropertymodifierTable extends ZfQueryBasedTable
+{
+ use ZfSortablePriority;
+
+ protected $searchColumns = [
+ 'property_name',
+ 'target_property',
+ ];
+
+ /** @var ImportSource */
+ protected $source;
+
+ /** @var Url */
+ protected $url;
+
+ protected $keyColumn = 'id';
+
+ protected $priorityColumn = 'priority';
+
+ protected $readOnly = false;
+
+ public static function load(ImportSource $source, Url $url)
+ {
+ $table = new static($source->getConnection());
+ $table->source = $source;
+ $table->url = $url;
+ return $table;
+ }
+
+ public function setReadOnly($readOnly = true)
+ {
+ $this->readOnly = $readOnly;
+ return $this;
+ }
+
+ public function render()
+ {
+ if ($this->readOnly) {
+ return parent::render();
+ }
+ return $this->renderWithSortableForm();
+ }
+
+ protected function assemble()
+ {
+ $this->getAttributes()->set('data-base-target', '_self');
+ }
+
+ public function getColumns()
+ {
+ return array(
+ 'id' => 'm.id',
+ 'source_id' => 'm.source_id',
+ 'property_name' => 'm.property_name',
+ 'target_property' => 'm.target_property',
+ 'description' => 'm.description',
+ 'provider_class' => 'm.provider_class',
+ 'priority' => 'm.priority',
+ );
+ }
+
+ public function renderRow($row)
+ {
+ $caption = $row->property_name;
+ if ($row->target_property !== null) {
+ $caption .= ' -> ' . $row->target_property;
+ }
+ if ($row->description === null) {
+ $class = $row->provider_class;
+ try {
+ /** @var ImportSourceHook $hook */
+ $hook = new $class;
+ $caption .= ': ' . $hook->getName();
+ } catch (Exception $e) {
+ $caption = $this->createErrorCaption($caption, $e);
+ } catch (Error $e) {
+ $caption = $this->createErrorCaption($caption, $e);
+ }
+ } else {
+ $caption .= ': ' . $row->description;
+ }
+
+ $renderedRow = $this::row([
+ Link::create($caption, 'director/importsource/editmodifier', [
+ 'id' => $row->id,
+ 'source_id' => $row->source_id,
+ ]),
+ ]);
+ if ($this->readOnly) {
+ return $renderedRow;
+ }
+
+ return $this->addSortPriorityButtons(
+ $renderedRow,
+ $row
+ );
+ }
+
+ /**
+ * @param $caption
+ * @param Exception|Error $e
+ * @return array
+ */
+ protected function createErrorCaption($caption, $e)
+ {
+ return [
+ $caption,
+ ': ',
+ $this::tag('span', ['class' => 'error'], $e->getMessage())
+ ];
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ if ($this->readOnly) {
+ return [$this->translate('Property')];
+ }
+ return [
+ $this->translate('Property'),
+ $this->getSortPriorityTitle()
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['m' => 'import_row_modifier'],
+ $this->getColumns()
+ )->where('m.source_id = ?', $this->source->get('id'))
+ ->order('priority');
+ }
+}
diff --git a/library/Director/Web/Table/QuickTable.php b/library/Director/Web/Table/QuickTable.php
new file mode 100644
index 0000000..ff3edcc
--- /dev/null
+++ b/library/Director/Web/Table/QuickTable.php
@@ -0,0 +1,547 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\Filter\FilterNot;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Selectable;
+use Icinga\Data\Paginatable;
+use Icinga\Exception\QueryException;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\PlainObjectRenderer;
+use Icinga\Web\Request;
+use gipfl\IcingaWeb2\Url;
+use Icinga\Web\View;
+use Icinga\Web\Widget;
+use Icinga\Web\Widget\Paginator;
+use ipl\Html\ValidHtml;
+use stdClass;
+use Zend_Db_Select as ZfDbSelect;
+
+abstract class QuickTable implements Paginatable, ValidHtml
+{
+ protected $view;
+
+ /** @var Db */
+ protected $connection;
+
+ protected $limit;
+
+ protected $offset;
+
+ /** @var Filter */
+ protected $filter;
+
+ protected $enforcedFilters = array();
+
+ protected $searchColumns = array();
+
+ protected function getRowClasses($row)
+ {
+ return array();
+ }
+
+ protected function getRowClassesString($row)
+ {
+ return $this->createClassAttribute($this->getRowClasses($row));
+ }
+
+ protected function createClassAttribute($classes)
+ {
+ $str = $this->createClassesString($classes);
+ if (strlen($str) > 0) {
+ return ' class="' . $str . '"';
+ } else {
+ return '';
+ }
+ }
+
+ private function createClassesString($classes)
+ {
+ if (is_string($classes)) {
+ $classes = array($classes);
+ }
+
+ if (empty($classes)) {
+ return '';
+ } else {
+ return implode(' ', $classes);
+ }
+ }
+
+ protected function getMultiselectProperties()
+ {
+ /* array(
+ * 'url' => 'director/hosts/edit',
+ * 'sourceUrl' => 'director/hosts',
+ * 'keys' => 'name'
+ * ) */
+
+ return array();
+ }
+
+ protected function renderMultiselectAttributes()
+ {
+ $props = $this->getMultiselectProperties();
+
+ if (empty($props)) {
+ return '';
+ }
+
+ $prefix = 'data-icinga-multiselect-';
+ $view = $this->view();
+ $parts = array();
+ $multi = array(
+ 'url' => $view->href($props['url']),
+ 'controllers' => $view->href($props['sourceUrl']),
+ 'data' => implode(',', $props['keys']),
+ );
+
+ foreach ($multi as $k => $v) {
+ $parts[] = $prefix . $k . '="' . $v . '"';
+ }
+
+ return ' ' . implode(' ', $parts);
+ }
+
+ protected function renderRow($row)
+ {
+ $htm = " <tr" . $this->getRowClassesString($row) . ">\n";
+ $firstCol = true;
+
+ foreach ($this->getTitles() as $key => $title) {
+ // Support missing columns
+ if (property_exists($row, $key)) {
+ $val = $row->$key;
+ } else {
+ $val = null;
+ }
+
+ $value = null;
+
+ if ($firstCol) {
+ if ($val !== null && $url = $this->getActionUrl($row)) {
+ $value = $this->view()->qlink($val, $this->getActionUrl($row));
+ }
+ $firstCol = false;
+ }
+
+ if ($value === null) {
+ if ($val === null) {
+ $value = '-';
+ } elseif (is_array($val) || $val instanceof stdClass || is_bool($val)) {
+ $value = '<pre>'
+ . $this->view()->escape(PlainObjectRenderer::render($val))
+ . '</pre>';
+ } else {
+ $value = $this->view()->escape($val);
+ }
+ }
+
+ $htm .= ' <td>' . $value . "</td>\n";
+ }
+
+ if ($this->hasAdditionalActions()) {
+ $htm .= ' <td class="actions">' . $this->renderAdditionalActions($row) . "</td>\n";
+ }
+
+ return $htm . " </tr>\n";
+ }
+
+ abstract protected function getTitles();
+
+ protected function getActionUrl($row)
+ {
+ return false;
+ }
+
+ public function setConnection(Selectable $connection)
+ {
+ $this->connection = $connection;
+ return $this;
+ }
+
+ /**
+ * @return ZfDbSelect
+ */
+ abstract protected function getBaseQuery();
+
+ public function fetchData()
+ {
+ $db = $this->db();
+ $query = $this->getBaseQuery()->columns($this->getColumns());
+
+ if ($this->hasLimit() || $this->hasOffset()) {
+ $query->limit($this->getLimit(), $this->getOffset());
+ }
+
+ $this->applyFiltersToQuery($query);
+
+ return $db->fetchAll($query);
+ }
+
+ protected function applyFiltersToQuery(ZfDbSelect $query)
+ {
+ $filter = null;
+ $enforced = $this->enforcedFilters;
+ if ($this->filter && ! $this->filter->isEmpty()) {
+ $filter = $this->filter;
+ } elseif (! empty($enforced)) {
+ $filter = array_shift($enforced);
+ }
+ if ($filter) {
+ foreach ($enforced as $f) {
+ $filter = $filter->andFilter($f);
+ }
+ $query->where($this->renderFilter($filter));
+ }
+
+ return $query;
+ }
+
+ public function getPaginator()
+ {
+ $paginator = new Paginator();
+ $paginator->setQuery($this);
+
+ return $paginator;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ $db = $this->db();
+ $query = clone($this->getBaseQuery());
+ $query->reset('order')->columns(array('COUNT(*)'));
+ $this->applyFiltersToQuery($query);
+
+ return $db->fetchOne($query);
+ }
+
+ public function limit($count = null, $offset = null)
+ {
+ $this->limit = $count;
+ $this->offset = $offset;
+
+ return $this;
+ }
+
+ public function hasLimit()
+ {
+ return $this->limit !== null;
+ }
+
+ public function getLimit()
+ {
+ return $this->limit;
+ }
+
+ public function hasOffset()
+ {
+ return $this->offset !== null;
+ }
+
+ public function getOffset()
+ {
+ return $this->offset;
+ }
+
+ public function hasAdditionalActions()
+ {
+ return method_exists($this, 'renderAdditionalActions');
+ }
+
+ /** @return Db */
+ protected function connection()
+ {
+ // TODO: Fail if missing? Require connection in constructor?
+ return $this->connection;
+ }
+
+ protected function db()
+ {
+ return $this->connection()->getDbAdapter();
+ }
+
+ protected function renderTitles($row)
+ {
+ $view = $this->view();
+ $htm = "<thead>\n <tr>\n";
+
+ foreach ($row as $title) {
+ $htm .= ' <th>' . $view->escape($title) . "</th>\n";
+ }
+
+ if ($this->hasAdditionalActions()) {
+ $htm .= ' <th class="actions">' . $view->translate('Actions') . "</th>\n";
+ }
+
+ return $htm . " </tr>\n</thead>\n";
+ }
+
+ protected function url($url, $params)
+ {
+ return Url::fromPath($url, $params);
+ }
+
+ protected function listTableClasses()
+ {
+ $classes = array('simple', 'common-table', 'table-row-selectable');
+ $multi = $this->getMultiselectProperties();
+ if (! empty($multi)) {
+ $classes[] = 'multiselect';
+ }
+
+ return $classes;
+ }
+
+ public function render()
+ {
+ $data = $this->fetchData();
+
+ $htm = '<table'
+ . $this->createClassAttribute($this->listTableClasses())
+ . $this->renderMultiselectAttributes()
+ . '>' . "\n"
+ . $this->renderTitles($this->getTitles())
+ . $this->beginTableBody();
+ foreach ($data as $row) {
+ $htm .= $this->renderRow($row);
+ }
+ return $htm . $this->endTableBody() . $this->endTable();
+ }
+
+ protected function beginTableBody()
+ {
+ return "<tbody>\n";
+ }
+
+ protected function endTableBody()
+ {
+ return "</tbody>\n";
+ }
+
+ protected function endTable()
+ {
+ return "</table>\n";
+ }
+
+ /**
+ * @return View
+ */
+ protected function view()
+ {
+ if ($this->view === null) {
+ $this->view = Icinga::app()->getViewRenderer()->view;
+ }
+ return $this->view;
+ }
+
+
+ public function setView($view)
+ {
+ $this->view = $view;
+ }
+
+ public function __toString()
+ {
+ return $this->render();
+ }
+
+ protected function getSearchColumns()
+ {
+ return $this->searchColumns;
+ }
+
+ abstract public function getColumns();
+
+ public function getFilterColumns()
+ {
+ $keys = array_keys($this->getColumns());
+ return array_combine($keys, $keys);
+ }
+
+ public function setFilter($filter)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ public function enforceFilter($filter, $expression = null)
+ {
+ if (! $filter instanceof Filter) {
+ $filter = Filter::where($filter, $expression);
+ }
+ $this->enforcedFilters[] = $filter;
+ return $this;
+ }
+
+ public function getFilterEditor(Request $request)
+ {
+ $filterEditor = Widget::create('filterEditor')
+ ->setColumns(array_keys($this->getColumns()))
+ ->setSearchColumns($this->getSearchColumns())
+ ->preserveParams('limit', 'sort', 'dir', 'view', 'backend', '_dev')
+ ->ignoreParams('page')
+ ->handleRequest($request);
+
+ $filter = $filterEditor->getFilter();
+ $this->setFilter($filter);
+
+ return $filterEditor;
+ }
+
+ protected function mapFilterColumn($col)
+ {
+ $cols = $this->getColumns();
+ return $cols[$col];
+ }
+
+ protected function renderFilter(Filter $filter, $level = 0)
+ {
+ $str = '';
+ if ($filter instanceof FilterChain) {
+ if ($filter instanceof FilterAnd) {
+ $op = ' AND ';
+ } elseif ($filter instanceof FilterOr) {
+ $op = ' OR ';
+ } elseif ($filter instanceof FilterNot) {
+ $op = ' AND ';
+ $str .= ' NOT ';
+ } else {
+ throw new QueryException(
+ 'Cannot render filter: %s',
+ $filter
+ );
+ }
+ $parts = array();
+ if (! $filter->isEmpty()) {
+ foreach ($filter->filters() as $f) {
+ $filterPart = $this->renderFilter($f, $level + 1);
+ if ($filterPart !== '') {
+ $parts[] = $filterPart;
+ }
+ }
+ if (! empty($parts)) {
+ if ($level > 0) {
+ $str .= ' (' . implode($op, $parts) . ') ';
+ } else {
+ $str .= implode($op, $parts);
+ }
+ }
+ }
+ } else {
+ /** @var FilterExpression $filter */
+ $str .= $this->whereToSql(
+ $this->mapFilterColumn($filter->getColumn()),
+ $filter->getSign(),
+ $filter->getExpression()
+ );
+ }
+
+ return $str;
+ }
+
+ protected function escapeForSql($value)
+ {
+ // bindParam? bindValue?
+ if (is_array($value)) {
+ $ret = array();
+ foreach ($value as $val) {
+ $ret[] = $this->escapeForSql($val);
+ }
+ return implode(', ', $ret);
+ } else {
+ //if (preg_match('/^\d+$/', $value)) {
+ // return $value;
+ //} else {
+ return $this->db()->quote($value);
+ //}
+ }
+ }
+
+ protected function escapeWildcards($value)
+ {
+ return preg_replace('/\*/', '%', $value);
+ }
+
+ protected function valueToTimestamp($value)
+ {
+ // We consider integers as valid timestamps. Does not work for URL params
+ if (! is_string($value) || ctype_digit($value)) {
+ return $value;
+ }
+ $value = strtotime($value);
+ if (! $value) {
+ /*
+ NOTE: It's too late to throw exceptions, we might finish in __toString
+ throw new QueryException(sprintf(
+ '"%s" is not a valid time expression',
+ $value
+ ));
+ */
+ }
+ return $value;
+ }
+
+ protected function timestampForSql($value)
+ {
+ // TODO: do this db-aware
+ return $this->escapeForSql(date('Y-m-d H:i:s', $value));
+ }
+
+ /**
+ * Check for timestamp fields
+ *
+ * TODO: This is not here to do automagic timestamp stuff. One may
+ * override this function for custom voodoo, IdoQuery right now
+ * does. IMO we need to split whereToSql functionality, however
+ * I'd prefer to wait with this unless we understood how other
+ * backends will work. We probably should also rename this
+ * function to isTimestampColumn().
+ *
+ * @param string $field Field Field name to checked
+ * @return bool Whether this field expects timestamps
+ */
+ public function isTimestamp($field)
+ {
+ return false;
+ }
+
+ public function whereToSql($col, $sign, $expression)
+ {
+ if ($this->isTimestamp($col)) {
+ $expression = $this->valueToTimestamp($expression);
+ }
+
+ if (is_array($expression) && $sign === '=') {
+ // TODO: Should we support this? Doesn't work for blub*
+ return $col . ' IN (' . $this->escapeForSql($expression) . ')';
+ } elseif ($sign === '=' && strpos($expression, '*') !== false) {
+ if ($expression === '*') {
+ // We'll ignore such filters as it prevents index usage and because "*" means anything, anything means
+ // all whereas all means that whether we use a filter to match anything or no filter at all makes no
+ // difference, except for performance reasons...
+ return '';
+ }
+
+ return $col . ' LIKE ' . $this->escapeForSql($this->escapeWildcards($expression));
+ } elseif ($sign === '!=' && strpos($expression, '*') !== false) {
+ if ($expression === '*') {
+ // We'll ignore such filters as it prevents index usage and because "*" means nothing, so whether we're
+ // using a real column with a valid comparison here or just an expression which cannot be evaluated to
+ // true makes no difference, except for performance reasons...
+ return $this->escapeForSql(0);
+ }
+
+ return $col . ' NOT LIKE ' . $this->escapeForSql($this->escapeWildcards($expression));
+ } else {
+ return $col . ' ' . $sign . ' ' . $this->escapeForSql($expression);
+ }
+ }
+}
diff --git a/library/Director/Web/Table/ReadOnlyFormAvpTable.php b/library/Director/Web/Table/ReadOnlyFormAvpTable.php
new file mode 100644
index 0000000..c3b44f3
--- /dev/null
+++ b/library/Director/Web/Table/ReadOnlyFormAvpTable.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\PlainObjectRenderer;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Zend_Form_Element as ZfElement;
+use Zend_Form_DisplayGroup as ZfDisplayGroup;
+
+class ReadOnlyFormAvpTable
+{
+ protected $form;
+
+ public function __construct(QuickForm $form)
+ {
+ $this->form = $form;
+ }
+
+ protected function renderDisplayGroups(QuickForm $form)
+ {
+ $html = '';
+
+ foreach ($form->getDisplayGroups() as $group) {
+ $elements = $this->filterGroupElements($group);
+
+ if (empty($elements)) {
+ continue;
+ }
+
+ $html .= '<tr><th colspan="2" style="text-align: right">' . $group->getLegend() . '</th></tr>';
+ $html .= $this->renderElements($elements);
+ }
+
+ return $html;
+ }
+
+ /**
+ * @param ZfDisplayGroup $group
+ * @return ZfElement[]
+ */
+ protected function filterGroupElements(ZfDisplayGroup $group)
+ {
+ $blacklist = array('disabled', 'assign_filter');
+ $elements = array();
+ /** @var ZfElement $element */
+ foreach ($group->getElements() as $element) {
+ if ($element->getValue() === null) {
+ continue;
+ }
+
+ if ($element->getType() === 'Zend_Form_Element_Hidden') {
+ continue;
+ }
+
+ if (in_array($element->getName(), $blacklist)) {
+ continue;
+ }
+
+
+ $elements[] = $element;
+ }
+
+ return $elements;
+ }
+
+ protected function renderElements($elements)
+ {
+ $html = '';
+ foreach ($elements as $element) {
+ $html .= $this->renderElement($element);
+ }
+
+ return $html;
+ }
+
+ /**
+ * @param ZfElement $element
+ *
+ * @return string
+ */
+ protected function renderElement(ZfElement $element)
+ {
+ $value = $element->getValue();
+ return '<tr><th>'
+ . $this->escape($element->getLabel())
+ . '</th><td>'
+ . $this->renderValue($value)
+ . '</td></tr>';
+ }
+
+ protected function renderValue($value)
+ {
+ if (is_string($value)) {
+ return $this->escape($value);
+ } elseif (is_array($value)) {
+ return $this->escape(implode(', ', $value));
+ }
+ return $this->escape(PlainObjectRenderer::render($value));
+ }
+
+ protected function escape($string)
+ {
+ return htmlspecialchars($string);
+ }
+
+ public function render()
+ {
+ $this->form->initializeForObject();
+ return '<table class="name-value-table">' . "\n"
+ . $this->renderDisplayGroups($this->form)
+ . '</table>';
+ }
+}
diff --git a/library/Director/Web/Table/ServiceTemplateUsageTable.php b/library/Director/Web/Table/ServiceTemplateUsageTable.php
new file mode 100644
index 0000000..82f9643
--- /dev/null
+++ b/library/Director/Web/Table/ServiceTemplateUsageTable.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+class ServiceTemplateUsageTable extends TemplateUsageTable
+{
+ public function getTypes()
+ {
+ return [
+ 'templates' => $this->translate('Templates'),
+ 'objects' => $this->translate('Objects'),
+ 'applyrules' => $this->translate('Apply Rules'),
+ // 'setmembers' => $this->translate('Set Members'),
+ ];
+ }
+
+ protected function getTypeSummaryDefinitions()
+ {
+ return [
+ 'templates' => $this->getSummaryLine('template'),
+ 'objects' => $this->getSummaryLine('object'),
+ 'applyrules' => $this->getSummaryLine('apply', 'o.service_set_id IS NULL'),
+ // TODO: re-enable
+ // 'setmembers' => $this->getSummaryLine('apply', 'o.service_set_id IS NOT NULL'),
+ ];
+ }
+}
diff --git a/library/Director/Web/Table/SyncRunTable.php b/library/Director/Web/Table/SyncRunTable.php
new file mode 100644
index 0000000..e08aad7
--- /dev/null
+++ b/library/Director/Web/Table/SyncRunTable.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\Format\LocalTimeFormat;
+use Icinga\Module\Director\Objects\SyncRule;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class SyncRunTable extends ZfQueryBasedTable
+{
+ /** @var SyncRule */
+ protected $rule;
+
+ protected $timeFormat;
+
+ public function __construct(SyncRule $rule)
+ {
+ parent::__construct($rule->getConnection());
+ $this->timeFormat = new LocalTimeFormat();
+ $this->getAttributes()
+ ->set('data-base-target', '_self')
+ ->add('class', 'history');
+ $this->rule = $rule;
+ }
+
+ public function renderRow($row)
+ {
+ $time = strtotime($row->start_time);
+ $this->renderDayIfNew($time);
+ return $this::tr([
+ $this::td($this->makeSummary($row)),
+ $this::td(new Link(
+ $this->timeFormat->getTime($time),
+ 'director/syncrule/history',
+ [
+ 'id' => $row->rule_id,
+ 'run_id' => $row->id,
+ ]
+ ))
+ ]);
+ }
+
+ protected function makeSummary($row)
+ {
+ $parts = [];
+ if ($row->objects_created > 0) {
+ $parts[] = sprintf(
+ $this->translate('%d created'),
+ $row->objects_created
+ );
+ }
+ if ($row->objects_modified > 0) {
+ $parts[] = sprintf(
+ $this->translate('%d modified'),
+ $row->objects_modified
+ );
+ }
+ if ($row->objects_deleted > 0) {
+ $parts[] = sprintf(
+ $this->translate('%d deleted'),
+ $row->objects_deleted
+ );
+ }
+
+ return implode(', ', $parts);
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ array('sr' => 'sync_run'),
+ [
+ 'id' => 'sr.id',
+ 'rule_id' => 'sr.rule_id',
+ 'rule_name' => 'sr.rule_name',
+ 'start_time' => 'sr.start_time',
+ 'duration_ms' => 'sr.duration_ms',
+ 'objects_deleted' => 'sr.objects_deleted',
+ 'objects_created' => 'sr.objects_created',
+ 'objects_modified' => 'sr.objects_modified',
+ 'last_former_activity' => 'sr.last_former_activity',
+ 'last_related_activity' => 'sr.last_related_activity',
+ ]
+ )->where(
+ 'sr.rule_id = ?',
+ $this->rule->get('id')
+ )->order('start_time DESC');
+ }
+}
diff --git a/library/Director/Web/Table/SyncpropertyTable.php b/library/Director/Web/Table/SyncpropertyTable.php
new file mode 100644
index 0000000..79461ce
--- /dev/null
+++ b/library/Director/Web/Table/SyncpropertyTable.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Objects\SyncRule;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\Extension\ZfSortablePriority;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class SyncpropertyTable extends ZfQueryBasedTable
+{
+ use ZfSortablePriority;
+
+ /** @var SyncRule */
+ protected $rule;
+
+ protected $searchColumns = [
+ 'source_expression',
+ 'destination_field',
+ ];
+
+ protected $keyColumn = 'id';
+
+ protected $priorityColumn = 'priority';
+
+ public static function create(SyncRule $rule)
+ {
+ $table = new static($rule->getConnection());
+ $table->getAttributes()->set('data-base-target', '_self');
+ $table->rule = $rule;
+ return $table;
+ }
+
+ public function render()
+ {
+ return $this->renderWithSortableForm();
+ }
+
+ public function renderRow($row)
+ {
+ return $this->addSortPriorityButtons(
+ $this::row([
+ $row->source_name,
+ $row->source_expression,
+ new Link(
+ $row->destination_field,
+ 'director/syncrule/editproperty',
+ [
+ 'id' => $row->id,
+ 'rule_id' => $row->rule_id,
+ ]
+ ),
+ ]),
+ $row
+ );
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Source name'),
+ $this->translate('Source field'),
+ $this->translate('Destination'),
+ $this->getSortPriorityTitle()
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['p' => 'sync_property'],
+ [
+ 'id' => 'p.id',
+ 'rule_id' => 'p.rule_id',
+ 'rule_name' => 'r.rule_name',
+ 'source_id' => 'p.source_id',
+ 'source_name' => 's.source_name',
+ 'source_expression' => 'p.source_expression',
+ 'destination_field' => 'p.destination_field',
+ 'priority' => 'p.priority',
+ 'filter_expression' => 'p.filter_expression',
+ 'merge_policy' => 'p.merge_policy'
+ ]
+ )->join(
+ ['r' => 'sync_rule'],
+ 'r.id = p.rule_id',
+ []
+ )->join(
+ ['s' => 'import_source'],
+ 's.id = p.source_id',
+ []
+ )->where(
+ 'p.rule_id = ?',
+ $this->rule->get('id')
+ )->order('p.priority');
+ }
+}
diff --git a/library/Director/Web/Table/SyncruleTable.php b/library/Director/Web/Table/SyncruleTable.php
new file mode 100644
index 0000000..4a8e4e5
--- /dev/null
+++ b/library/Director/Web/Table/SyncruleTable.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+
+class SyncruleTable extends ZfQueryBasedTable
+{
+ protected $searchColumns = [
+ 'rule_name',
+ 'description',
+ ];
+
+ protected function assemble()
+ {
+ $this->getAttributes()->add('class', 'syncstate');
+ parent::assemble();
+ }
+
+ public function renderRow($row)
+ {
+ $caption = [Link::create(
+ $row->rule_name,
+ 'director/syncrule',
+ ['id' => $row->id]
+ )];
+ if ($row->description !== null) {
+ $caption[] = ': ' . $row->description;
+ }
+
+ if ($row->sync_state === 'failing' && $row->last_error_message) {
+ $caption[] = ' (' . $row->last_error_message . ')';
+ }
+
+ $tr = $this::row([$caption, $row->object_type]);
+ $tr->getAttributes()->add('class', $row->sync_state);
+
+ return $tr;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ $this->translate('Rule name'),
+ $this->translate('Object type'),
+ ];
+ }
+
+ public function prepareQuery()
+ {
+ return $this->db()->select()->from(
+ ['s' => 'sync_rule'],
+ [
+ 'id' => 's.id',
+ 'rule_name' => 's.rule_name',
+ 'sync_state' => 's.sync_state',
+ 'object_type' => 's.object_type',
+ 'update_policy' => 's.update_policy',
+ 'purge_existing' => 's.purge_existing',
+ 'filter_expression' => 's.filter_expression',
+ 'last_error_message' => 's.last_error_message',
+ 'description' => 's.description',
+ ]
+ )->order('rule_name');
+ }
+}
diff --git a/library/Director/Web/Table/TableLoader.php b/library/Director/Web/Table/TableLoader.php
new file mode 100644
index 0000000..f7e378b
--- /dev/null
+++ b/library/Director/Web/Table/TableLoader.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Exception\ProgrammingError;
+
+class TableLoader
+{
+ /** @return QuickTable */
+ public static function load($name, Module $module = null)
+ {
+ if ($module === null) {
+ $basedir = Icinga::app()->getApplicationDir('tables');
+ $ns = '\\Icinga\\Web\\Tables\\';
+ } else {
+ $basedir = $module->getBaseDir() . '/application/tables';
+ $ns = '\\Icinga\\Module\\' . ucfirst($module->getName()) . '\\Tables\\';
+ }
+ if (preg_match('~^[a-z0-9/]+$~i', $name)) {
+ $parts = preg_split('~/~', $name);
+ $class = ucfirst(array_pop($parts)) . 'Table';
+ $file = sprintf('%s/%s/%s.php', rtrim($basedir, '/'), implode('/', $parts), $class);
+ if (file_exists($file)) {
+ require_once($file);
+ /** @var QuickTable $class */
+ $class = $ns . $class;
+ return new $class();
+ }
+ }
+ throw new ProgrammingError(sprintf('Cannot load %s (%s), no such table', $name, $file));
+ }
+}
diff --git a/library/Director/Web/Table/TableWithBranchSupport.php b/library/Director/Web/Table/TableWithBranchSupport.php
new file mode 100644
index 0000000..7c5b15c
--- /dev/null
+++ b/library/Director/Web/Table/TableWithBranchSupport.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Module\Director\Db\Branch\Branch;
+use Ramsey\Uuid\UuidInterface;
+
+trait TableWithBranchSupport
+{
+
+ /** @var UuidInterface|null */
+ protected $branchUuid;
+
+ /**
+ * Convenience method, only UUID is required
+ *
+ * @param Branch|null $branch
+ * @return $this
+ */
+ public function setBranch(Branch $branch = null)
+ {
+ if ($branch && $branch->isBranch()) {
+ $this->setBranchUuid($branch->getUuid());
+ }
+
+ return $this;
+ }
+
+ public function setBranchUuid(UuidInterface $uuid = null)
+ {
+ $this->branchUuid = $uuid;
+
+ return $this;
+ }
+
+ protected function branchifyColumns($columns)
+ {
+ $result = [
+ 'uuid' => 'COALESCE(o.uuid, bo.uuid)'
+ ];
+ $ignore = ['o.id', 'os.id', 'o.service_set_id', 'os.host_id'];
+ foreach ($columns as $alias => $column) {
+ if (substr($column, 0, 2) === 'o.' && ! in_array($column, $ignore)) {
+ // bo.column, o.column
+ $column = "COALESCE(b$column, $column)";
+ }
+ if (substr($column, 0, 3) === 'os.' && ! in_array($column, $ignore)) {
+ // bo.column, o.column
+ $column = "COALESCE(b$column, $column)";
+ }
+
+ // Used in Service Tables:
+ if ($column === 'h.object_name' && $alias = 'host') {
+ $column = "COALESCE(bo.host, $column)";
+ }
+
+ $result[$alias] = $column;
+ }
+
+ return $result;
+ }
+
+ protected function stripSearchColumnAliases()
+ {
+ foreach ($this->searchColumns as &$column) {
+ $column = preg_replace('/^[a-z]+\./', '', $column);
+ }
+ }
+}
diff --git a/library/Director/Web/Table/TemplateUsageTable.php b/library/Director/Web/Table/TemplateUsageTable.php
new file mode 100644
index 0000000..66e56ea
--- /dev/null
+++ b/library/Director/Web/Table/TemplateUsageTable.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Resolver\TemplateTree;
+use gipfl\IcingaWeb2\Link;
+use ipl\Html\Table;
+use gipfl\Translation\TranslationHelper;
+
+class TemplateUsageTable extends Table
+{
+ use TranslationHelper;
+
+ protected $defaultAttributes = ['class' => 'pivot'];
+
+ protected $objectType;
+
+ public function getTypes()
+ {
+ return [
+ 'templates' => $this->translate('Templates'),
+ 'objects' => $this->translate('Objects'),
+ ];
+ }
+
+ protected function getTypeSummaryDefinitions()
+ {
+ return [
+ 'templates' => $this->getSummaryLine('template'),
+ 'objects' => $this->getSummaryLine('object'),
+ ];
+ }
+
+ /**
+ * @param IcingaObject $template
+ * @return TemplateUsageTable
+ */
+ public static function forTemplate(IcingaObject $template)
+ {
+ $type = ucfirst($template->getShortTableName());
+ $class = __NAMESPACE__ . "\\${type}TemplateUsageTable";
+ if (class_exists($class)) {
+ return new $class($template);
+ } else {
+ return new static($template);
+ }
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [
+ '',
+ $this->translate('Direct'),
+ $this->translate('Indirect'),
+ $this->translate('Total')
+ ];
+ }
+
+ protected function __construct(IcingaObject $template)
+ {
+
+ if ($template->get('object_type') !== 'template') {
+ throw new ProgrammingError(
+ 'TemplateUsageTable expects a template, got %s',
+ $template->get('object_type')
+ );
+ }
+
+ $this->objectType = $objectType = $template->getShortTableName();
+ $types = $this->getTypes();
+ $usage = $this->getUsageSummary($template);
+
+ $used = false;
+ $rows = [];
+ foreach ($types as $type => $typeTitle) {
+ $tr = Table::tr(Table::th($typeTitle));
+ foreach (['direct', 'indirect', 'total'] as $inheritance) {
+ $count = $usage->$inheritance->$type;
+ if (! $used && $count > 0) {
+ $used = true;
+ }
+ $tr->add(
+ Table::td(
+ Link::create(
+ $count,
+ "director/${objectType}template/$type",
+ [
+ 'name' => $template->getObjectName(),
+ 'inheritance' => $inheritance
+ ]
+ )
+ )
+ );
+ }
+ $rows[] = $tr;
+ }
+
+ if ($used) {
+ $this->add($rows);
+ } else {
+ $this->add($this->translate('This template is not in use'));
+ }
+ }
+
+ protected function getUsageSummary(IcingaObject $template)
+ {
+ $id = $template->getAutoincId();
+ $connection = $template->getConnection();
+ $db = $connection->getDbAdapter();
+ $oType = $this->objectType;
+ $tree = new TemplateTree($oType, $connection);
+ $ids = $tree->listDescendantIdsFor($template);
+ if (empty($ids)) {
+ $ids = [0];
+ }
+
+ $baseQuery = $db->select()->from(
+ ['o' => 'icinga_' . $oType],
+ $this->getTypeSummaryDefinitions()
+ )->joinLeft(
+ ['oi' => "icinga_${oType}_inheritance"],
+ "oi.${oType}_id = o.id",
+ []
+ );
+
+ $query = clone($baseQuery);
+ $direct = $db->fetchRow(
+ $query->where("oi.parent_${oType}_id = ?", $id)
+ );
+ $query = clone($baseQuery);
+ $indirect = $db->fetchRow(
+ $query->where("oi.parent_${oType}_id IN (?)", $ids)
+ );
+ //$indirect->templates = count($ids) - 1;
+ $total = [];
+ $types = array_keys($this->getTypes());
+ foreach ($types as $type) {
+ $total[$type] = $direct->$type + $indirect->$type;
+ }
+
+ return (object) [
+ 'direct' => $direct,
+ 'indirect' => $indirect,
+ 'total' => (object) $total
+ ];
+ }
+
+ protected function getSummaryLine($type, $extra = null)
+ {
+ if ($extra !== null) {
+ $extra = " AND $extra";
+ }
+ return "COALESCE(SUM(CASE WHEN o.object_type = '${type}'${extra} THEN 1 ELSE 0 END), 0)";
+ }
+}
diff --git a/library/Director/Web/Table/TemplatesTable.php b/library/Director/Web/Table/TemplatesTable.php
new file mode 100644
index 0000000..be195b2
--- /dev/null
+++ b/library/Director/Web/Table/TemplatesTable.php
@@ -0,0 +1,156 @@
+<?php
+
+namespace Icinga\Module\Director\Web\Table;
+
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\IcingaObjectFilterHelper;
+use Icinga\Module\Director\Objects\IcingaObject;
+use ipl\Html\Html;
+use gipfl\IcingaWeb2\Icon;
+use gipfl\IcingaWeb2\Link;
+use gipfl\IcingaWeb2\Table\Extension\MultiSelect;
+use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
+use gipfl\IcingaWeb2\Url;
+use gipfl\IcingaWeb2\Zf1\Db\FilterRenderer;
+use Ramsey\Uuid\Uuid;
+use Zend_Db_Select as ZfSelect;
+
+class TemplatesTable extends ZfQueryBasedTable implements FilterableByUsage
+{
+ use MultiSelect;
+
+ protected $searchColumns = ['o.object_name'];
+
+ private $type;
+
+ public static function create($type, Db $db)
+ {
+ $table = new static($db);
+ $table->type = strtolower($type);
+ return $table;
+ }
+
+ protected function assemble()
+ {
+ $type = $this->type;
+ $this->enableMultiSelect(
+ "director/${type}s/edittemplates",
+ "director/${type}template",
+ ['name']
+ );
+ }
+
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ public function getColumnsToBeRendered()
+ {
+ return [$this->translate('Template Name')];
+ }
+
+ public function renderRow($row)
+ {
+ $name = $row->object_name;
+ $type = str_replace('_', '-', $this->getType());
+ $caption = $row->is_used === 'y' ? $name : [
+ $name,
+ Html::tag(
+ 'span',
+ ['style' => 'font-style: italic'],
+ $this->translate(' - not in use -')
+ )
+ ];
+
+ $url = Url::fromPath("director/${type}template/usage", [
+ 'name' => $name
+ ]);
+
+ return $this::row([
+ new Link($caption, $url),
+ [
+ new Link(new Icon('plus'), "director/$type/add", [
+ 'type' => 'object',
+ 'imports' => $name
+ ]),
+ new Link(new Icon('history'), "director/$type/history", [
+ 'uuid' => Uuid::fromBytes(Db\DbUtil::binaryResult($row->uuid))->toString(),
+ ])
+ ]
+ ]);
+ }
+
+ public function filterTemplate(
+ IcingaObject $template,
+ $inheritance = IcingaObjectFilterHelper::INHERIT_DIRECT
+ ) {
+ IcingaObjectFilterHelper::filterByTemplate(
+ $this->getQuery(),
+ $template,
+ 'o',
+ $inheritance
+ );
+
+ return $this;
+ }
+
+ public function showOnlyUsed()
+ {
+ $type = $this->getType();
+ $this->getQuery()->where(
+ "(EXISTS (SELECT ${type}_id FROM icinga_${type}_inheritance"
+ . " WHERE parent_${type}_id = o.id))"
+ );
+ }
+
+ public function showOnlyUnUsed()
+ {
+ $type = $this->getType();
+ $this->getQuery()->where(
+ "(NOT EXISTS (SELECT ${type}_id FROM icinga_${type}_inheritance"
+ . " WHERE parent_${type}_id = o.id))"
+ );
+ }
+
+ protected function applyRestrictions(ZfSelect $query)
+ {
+ $auth = Auth::getInstance();
+ $type = $this->type;
+ $restrictions = $auth->getRestrictions("director/$type/template/filter-by-name");
+ if (empty($restrictions)) {
+ return $query;
+ }
+
+ $filter = Filter::matchAny();
+ foreach ($restrictions as $restriction) {
+ $filter->addFilter(Filter::where('o.object_name', $restriction));
+ }
+
+ return FilterRenderer::applyToQuery($filter, $query);
+ }
+
+ protected function prepareQuery()
+ {
+ $type = $this->getType();
+ $used = "CASE WHEN EXISTS(SELECT 1 FROM icinga_${type}_inheritance oi"
+ . " WHERE oi.parent_${type}_id = o.id) THEN 'y' ELSE 'n' END";
+
+ $columns = [
+ 'object_name' => 'o.object_name',
+ 'uuid' => 'o.uuid',
+ 'id' => 'o.id',
+ 'is_used' => $used,
+ ];
+ $query = $this->db()->select()->from(
+ ['o' => "icinga_${type}"],
+ $columns
+ )->where(
+ "o.object_type = 'template'"
+ )->order('o.object_name');
+
+ return $this->applyRestrictions($query);
+ }
+}