Basket: initial import of the main components

refs #1630
This commit is contained in:
Thomas Gelf 2018-10-06 16:55:52 +02:00
parent 5d309b3dc7
commit f4220016d8
10 changed files with 1080 additions and 0 deletions

View File

@ -0,0 +1,285 @@
<?php
namespace Icinga\Module\Director\Controllers;
use dipl\Html\Link;
use dipl\Web\Widget\NameValueTable;
use Exception;
use Icinga\Module\Director\ConfigDiff;
use Icinga\Module\Director\Core\Json;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\DirectorObject\Automation\Basket;
use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
use Icinga\Module\Director\Forms\BasketCreateSnapshotForm;
use Icinga\Module\Director\Forms\BasketForm;
use Icinga\Module\Director\Forms\RestoreBasketForm;
use Icinga\Module\Director\Web\Controller\ActionController;
use dipl\Html\Html;
use Icinga\Module\Director\Web\Table\BasketSnapshotTable;
class BasketController extends ActionController
{
protected $isApified = true;
protected function basketTabs()
{
$uuid = $this->params->get('uuid');
return $this->tabs()->add('show', [
'label' => $this->translate('Basket'),
'url' => 'director/basket',
'urlParams' => ['uuid' => $uuid]
])->add('snapshots', [
'label' => $this->translate('Snapshots'),
'url' => 'director/basket/snapshots',
'urlParams' => ['uuid' => $uuid]
]);
}
/**
* @throws \Icinga\Exception\NotFoundError
*/
public function indexAction()
{
$this->actions()->add(
Link::create(
$this->translate('Back'),
'director/baskets',
null,
[
'class' => 'icon-left-big'
]
)
);
$uuid = hex2bin($this->params->get('uuid'));
$basket = Basket::load($uuid, $this->db());
$this->basketTabs()->activate('show');
$this->addTitle($basket->get('basket_name'));
if ($basket->isEmpty()) {
$this->content()->add(Html::tag('p', [
'class' => 'information'
], $this->translate('This basket is empty')));
}
$this->content()->add(
(new BasketForm())->setObject($basket)->handleRequest()
);
}
public function createAction()
{
$this->actions()->add(
Link::create(
$this->translate('back'),
'director/baskets',
null,
[
'class' => 'icon-left-big'
]
)
);
$this->addSingleTab($this->translate('Create Basket'));
$this->addTitle($this->translate('Create a new Configuration Basket'));
$form = (new BasketForm())
->setDb($this->db())
->handleRequest();
$this->content()->add($form);
}
public function snapshotsAction()
{
$uuid = $this->params->get('uuid');
if ($uuid === null || $uuid === '') {
$basket = null;
} else {
$uuid = hex2bin($uuid);
$basket = Basket::load($uuid, $this->db());
}
if ($basket === null) {
$this->addTitle($this->translate('Basket Snapshots'));
$this->addSingleTab($this->translate('Snapshots'));
} else {
$this->addTitle(sprintf(
$this->translate('%: Snapshots'),
$basket->get('basket_name')
));
$this->basketTabs()->activate('snapshots');
}
if ($basket !== null) {
$this->content()->add(
(new BasketCreateSnapshotForm())
->setBasket($basket)
->handleRequest()
);
}
$table = new BasketSnapshotTable($this->db());
if ($basket !== null) {
$table->setBasket($basket);
}
$table->renderTo($this);
}
/**
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
public function snapshotAction()
{
$hexUuid = $this->params->getRequired('uuid');
$binUuid = hex2bin($hexUuid);
$basket = Basket::load($binUuid, $this->db());
$snapshot = BasketSnapshot::load([
'basket_uuid' => $binUuid,
'ts_create' => $this->params->getRequired('ts'),
], $this->db());
$this->addTitle(
$this->translate('%s: %s (Snapshot)'),
$basket->get('basket_name'),
substr($hexUuid, 0, 7)
);
$this->actions()->add([
Link::create(
$this->translate('Show Basket'),
'director/basket',
['uuid' => $hexUuid],
['data-base-target' => '_next']
),
Link::create(
$this->translate('Restore'),
$this->url()->with('action', 'restore'),
null,
['class' => 'icon-rewind']
)
] );
if ($this->params->get('action') === 'restore') {
$form = new RestoreBasketForm();
$form
->setSnapshot($snapshot)
->handleRequest();
$this->content()->add($form);
$targetDbName = $form->getValue('target_db');
$connection = $form->getDb();
} else {
$targetDbName = null;
$connection = $this->db();
}
$json = $snapshot->getJsonDump();
$this->addSingleTab($this->translate('Snapshot'));
$all = Json::decode($json);
foreach ($all as $type => $objects) {
$table = new NameValueTable();
$table->setAttribute('data-base-target', '_next');
foreach ($objects as $key => $object) {
$linkParams = [
'uuid' => $hexUuid,
'checksum' => $this->params->get('checksum'),
'ts' => $this->params->get('ts'),
'type' => $type,
'key' => $key,
];
if ($targetDbName !== null) {
$linkParams['target_db'] = $targetDbName;
}
try {
$current = BasketSnapshot::instanceByIdentifier($type, $key, $connection);
if ($current === null) {
$table->addNameValueRow(
$key,
Link::create(
Html::tag('strong', ['style' => 'color: green'], $this->translate('new')),
'director/basket/snapshotobject',
$linkParams
)
);
continue;
}
$hasChanged = Json::encode($current->export()) !== Json::encode($object);
$table->addNameValueRow(
$key,
$hasChanged
? Link::create(
Html::tag('strong', ['style' => 'color: orange'], $this->translate('modified')),
'director/basket/snapshotobject',
$linkParams
)
: Html::tag('span', ['style' => 'color: green'], $this->translate('unchanged'))
);
} catch (Exception $e) {
$table->addNameValueRow(
$key,
$e->getMessage()
);
}
}
$this->content()->add(Html::tag('h2', $type));
$this->content()->add($table);
}
}
/**
* @throws \Icinga\Exception\MissingParameterException
* @throws \Icinga\Exception\NotFoundError
*/
public function snapshotobjectAction()
{
$hexUuid = $this->params->getRequired('uuid');
$binUuid = hex2bin($hexUuid);
$snapshot = BasketSnapshot::load([
'basket_uuid' => $binUuid,
'ts_create' => $this->params->getRequired('ts'),
], $this->db());
$snapshotUrl = $this->url()->without('type')->without('key')->setPath('director/basket/snapshot');
$type = $this->params->get('type');
$key = $this->params->get('key');
$this->addTitle($this->translate('Single Object Diff'));
$this->content()->add(Html::tag('p', [
'class' => 'information'
], Html::sprintf(
$this->translate('Comparing %s "%s" from Snapshot "%s" to current config'),
$type,
$key,
Link::create(
substr($hexUuid, 0, 7),
$snapshotUrl,
null,
['data-base-target' => '_next']
)
)));
$this->actions()->add([
Link::create(
$this->translate('back'),
$snapshotUrl,
null,
['class' => 'icon-left-big']
),
Link::create(
$this->translate('Restore'),
$this->url()->with('action', 'restore'),
null,
['class' => 'icon-rewind']
)
]);
$json = $snapshot->getJsonDump();
$this->addSingleTab($this->translate('Snapshot'));
$objects = Json::decode($json);
$targetDbName = $this->params->get('target_db');
if ($targetDbName === null) {
$connection = $this->db();
} else {
$connection = Db::fromResourceName($targetDbName);
}
$object = $objects->$type->$key;
$current = BasketSnapshot::instanceByIdentifier($type, $key, $connection);
$this->content()->add(
ConfigDiff::create(
Json::encode($object, JSON_PRETTY_PRINT),
Json::encode($current->export(), JSON_PRETTY_PRINT)
)->setHtmlRenderer('Inline')
);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Icinga\Module\Director\Controllers;
use dipl\Html\Html;
use dipl\Html\Link;
use Icinga\Module\Director\Web\Controller\ActionController;
use Icinga\Module\Director\Web\Table\BasketTable;
class BasketsController extends ActionController
{
protected $isApified = true;
public function indexAction()
{
$this->setAutorefreshInterval(10);
$this->addSingleTab($this->translate('Baskets'));
$this->actions()->add([
Link::create(
$this->translate('Create'),
'director/basket/create',
null,
['class' => 'icon-plus']
)
]);
$this->addTitle($this->translate('Configuration Baskets'));
$this->content()->add(Html::tag('p', $this->translate(
'A Configuration Basket references a specific Configuration'
. ' Objects or all objects of a specific type. It has been'
. ' designed to share Templates, Import/Sync strategies and'
. ' other base Configuration Objects. It is not a tool to'
. ' operate with single Hosts or Services.'
)));
$this->content()->add(Html::tag('p', $this->translate(
'You can create Basket snapshots at any time, this will persist'
. ' a serialized representation of all involved objects at that'
. ' moment in time. Snapshots can be exported, imported, shared'
. ' and restored - to the very same or another Director instance.'
)));
$table = (new BasketTable($this->db()))
->setAttribute('data-base-target', '_self');
if ($table->hasSearch() || count($table)) {
$table->renderTo($this);
}
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Icinga\Module\Director\Forms;
use Icinga\Module\Director\DirectorObject\Automation\Basket;
use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot;
use Icinga\Module\Director\Web\Form\DirectorForm;
class BasketCreateSnapshotForm extends DirectorForm
{
/** @var Basket */
private $basket;
public function setBasket(Basket $basket)
{
$this->basket = $basket;
return $this;
}
public function setup()
{
$this->setSubmitLabel($this->translate('Create Snapshot'));
}
/**
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
*/
public function onSuccess()
{
/** @var \Icinga\Module\Director\Db $connection */
$connection = $this->basket->getConnection();
$snapshot = BasketSnapshot::createForBasket($this->basket, $connection);
$snapshot->store();
parent::onSuccess();
}
}

View File

@ -0,0 +1,132 @@
<?php
namespace Icinga\Module\Director\Forms;
use Icinga\Module\Director\Data\Db\DbObject;
use Icinga\Module\Director\DirectorObject\Automation\Basket;
use Icinga\Module\Director\Web\Form\DirectorObjectForm;
use Zend_Form_SubForm as ZfSubForm;
class BasketForm extends DirectorObjectForm
{
protected $listUrl = 'director/baskets';
protected function getAvailableTypes()
{
return [
'ImportSource' => $this->translate('Import Sources'),
'SyncRule' => $this->translate('Sync Rules'),
'Job' => $this->translate('Job Definitions'),
'HostTemplate' => $this->translate('Host Templates'),
'ServiceSet' => $this->translate('Service Sets'),
'ServiceTemplate' => $this->translate('Service Templates'),
'Basket' => $this->translate('Basket Definitions'),
];
}
/**
* @throws \Zend_Form_Exception
*/
public function setup()
{
$this->addElement('text', 'basket_name', [
'label' => $this->translate('Basket Name'),
'required' => true,
]);
$types = $this->getAvailableTypes();
$options = [
'IGNORE' => $this->translate('Ignore'),
'ALL' => $this->translate('All of them'),
'[]' => $this->translate('Custom Selection'),
];
$this->addHtmlHint($this->translate(
'What should we place into this Basket every time we create'
. ' new snapshot?'
));
$sub = new ZfSubForm();
$sub->setDecorators([
['HtmlTag', ['tag' => 'dl']],
'FormElements'
]);
foreach ($types as $name => $label) {
$sub->addElement('select', $name, [
'label' => $label,
'multiOptions' => $options,
]);
}
$this->addSubForm($sub, 'objects');
$this->addDeleteButton();
$this->addHtmlHint($this->translate(
'Choose "All" to always add all of them,'
. ' "Ignore" to not care about a specific Type at all and'
. ' opt for "Custom Selection" in case you want to choose'
. ' just some specific Objects.'
));
}
protected function setDefaultsFromObject(DbObject $object)
{
parent::setDefaultsFromObject($object);
/** @var Basket $object */
$values = [];
foreach ($this->getAvailableTypes() as $type => $label) {
$values[$type] = 'IGNORE';
}
foreach ($object->getChosenObjects() as $type => $selection) {
if ($selection === true) {
$values[$type] = 'ALL';
} elseif (is_array($selection)) {
$values[$type] = '[]';
}
}
$this->populate([
'objects' => $values
]);
}
protected function onRequest()
{
parent::onRequest(); // TODO: Change the autogenerated stub
}
protected function getObjectClassname()
{
return '\\Icinga\\Module\\Director\\DirectorObject\\Automation\\Basket';
}
public function onSuccess()
{
/** @var Basket $basket */
$basket = $this->object();
if ($basket->isEmpty()) {
$this->addError($this->translate("It's not allowed to store an empty basket"));
return;
}
if (! $basket->hasBeenLoadedFromDb()) {
$basket->set('owner_type', 'user');
$basket->set('owner_value', $this->getAuth()->getUser()->getUsername());
}
parent::onSuccess();
}
protected function setObjectSuccessUrl()
{
/** @var Basket $basket */
$basket = $this->object();
$this->setSuccessUrl(
'director/basket',
['uuid' => $basket->getHexUuid()]
);
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace Icinga\Module\Director\DirectorObject\Automation;
use Icinga\Module\Director\Core\Json;
use Icinga\Module\Director\Data\Db\DbObject;
/**
* Class Basket
*
* TODO
* - create a UUID like in RFC4122
*/
class Basket extends DbObject
{
const SELECTION_ALL = true;
const SELECTION_NONE = false;
protected $validTypes = [
'host_template',
'host_object',
'service_template',
'service_object',
'service_apply',
'import_source',
'sync_rule'
];
protected $table = 'director_basket';
protected $keyName = 'uuid';
protected $chosenObjects = [];
protected $defaultProperties = [
'uuid' => null,
'basket_name' => null,
'objects' => null,
'owner_type' => null,
'owner_value' => null,
];
public function getHexUuid()
{
return bin2hex($this->get('uuid'));
}
public function listObjectTypes()
{
return array_keys($this->objects);
}
public function getChosenObjects()
{
return $this->chosenObjects;
}
public function isEmpty()
{
return count($this->getChosenObjects()) === 0;
}
protected function onLoadFromDb()
{
$this->chosenObjects = Json::decode($this->get('objects'));
}
public function setObjects($objects)
{
if (empty($objects)) {
$this->chosenObjects = [];
} else {
$this->chosenObjects = [];
foreach ((array) $objects as $type => $object) {
$this->addObjects($type, $object);
}
}
return $this;
}
/**
* @param $type
* @param ExportInterface[]|bool $objects
*/
public function addObjects($type, $objects = true)
{
// '1' -> from Form!
if ($objects === 'ALL') {
$objects = true;
} elseif ($objects === null || $objects === 'IGNORE') {
return;
} elseif ($objects === '[]') {
$objects = [];
}
if ($objects === true) {
$this->chosenObjects[$type] = true;
} elseif ($objects === '0') {
// nothing
} else {
foreach ($objects as $object) {
$this->addObject($type, $object);
}
if (array_key_exists($type, $this->chosenObjects)) {
ksort($this->chosenObjects[$type]);
}
}
$this->reallySet('objects', Json::encode($this->chosenObjects));
}
/**
* @param $type
* @param string $object
*/
public function addObject($type, $object)
{
// TODO: make sure array exists - and is not boolean
$this->chosenObjects[$type][] = $object;
}
public function hasType($type)
{
return isset($this->chosenObjects[$type]);
}
protected function beforeStore()
{
if (! $this->hasBeenLoadedFromDb()) {
// TODO: This is BS, use a real UUID
$this->set('uuid', hex2bin(substr(sha1(microtime(true) . rand(1, 100000)), 0, 32)));
}
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Icinga\Module\Director\DirectorObject\Automation;
use Icinga\Module\Director\Data\Db\DbObject;
class BasketContent extends DbObject
{
protected $objects;
protected $table = 'director_basket_content';
protected $keyName = 'checksum';
protected $defaultProperties = [
'checksum' => null,
'summary' => null,
'content' => null,
];
}

View File

@ -0,0 +1,229 @@
<?php
namespace Icinga\Module\Director\DirectorObject\Automation;
use Icinga\Module\Director\Core\Json;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\Data\Db\DbObject;
use Icinga\Module\Director\Objects\IcingaObject;
use RuntimeException;
class BasketSnapshot extends DbObject
{
protected $objects = [];
protected $content;
protected $table = 'director_basket_snapshot';
protected $keyName = [
'basket_uuid',
'ts_create',
];
protected $defaultProperties = [
'basket_uuid' => null,
'content_checksum' => null,
'ts_create' => null,
];
public static function getClassForType($type)
{
$types = [
'ImportSource' => '\\Icinga\\Module\\Director\\Objects\\ImportSource',
'SyncRule' => '\\Icinga\\Module\\Director\\Objects\\SyncRule',
'DirectorJob' => '\\Icinga\\Module\\Director\\Objects\\DirectorJob',
'ServiceSet' => '\\Icinga\\Module\\Director\\Objects\\IcingaServiceSet',
'HostTemplate' => '\\Icinga\\Module\\Director\\Objects\\IcingaHost',
'ServiceTemplate' => '\\Icinga\\Module\\Director\\Objects\\IcingaService',
'Basket' => '\\Icinga\\Module\\Director\\DirectorObject\\Automation\\Automation',
];
return $types[$type];
}
public static function createForBasket(Basket $basket, Db $db)
{
$snapshot = static::create([
'basket_uuid' => $basket->get('uuid')
], $db);
$snapshot->addObjectsChosenByBasket($basket);
return $snapshot;
}
protected function addObjectsChosenByBasket(Basket $basket)
{
foreach ($basket->getChosenObjects() as $typeName => $selection) {
if ($selection === true) {
$this->addAll($typeName);
} elseif (! empty($selection)) {
$this->addByIdentifiers($typeName, $selection);
}
}
}
/**
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
*/
protected function beforeStore()
{
if ($this->hasBeenLoadedFromDb()) {
throw new RuntimeException('A basket snapshot cannot be modified');
}
$json = $this->getJsonDump();
$checksum = sha1($json, true);
if (! BasketContent::exists($checksum, $this->getConnection())) {
BasketContent::create([
'checksum' => $checksum,
'summary' => $this->getJsonSummary(),
'content' => $json,
], $this->getConnection())->store();
}
$this->set('content_checksum', $checksum);
$this->set('ts_create', round(microtime(true) * 1000));
}
/**
* @param Db $connection
* @param bool $replace
* @throws \Icinga\Module\Director\Exception\DuplicateKeyException
*/
public function restoreTo(Db $connection, $replace = true)
{
$all = Json::decode($this->getJsonDump());
$db = $connection->getDbAdapter();
$db->beginTransaction();
foreach ($all as $typeName => $objects) {
$class = static::getClassForType($typeName);
foreach ($objects as $object) {
/** @var DbObject $new */
$new = $class::import($object, $connection, $replace);
if ($new->hasBeenModified()) {
$new->store();
}
}
}
$db->commit();
}
/**
* @return BasketContent
* @throws \Icinga\Exception\NotFoundError
*/
protected function getContent()
{
if ($this->content === null) {
$this->content = BasketContent::load($this->get('content_checksum'), $this->getConnection());
}
return $this->content;
}
protected function onDelete()
{
$db = $this->getDb();
$db->delete(
['bc' => 'director_basket_content'],
'NOT EXISTS (SELECT director_basket_checksum WHERE content_checksum = bc.checksum)'
);
}
public function getJsonSummary()
{
if ($this->hasBeenLoadedFromDb()) {
return $this->getContent()->get('summary');
} else {
return Json::encode($this->getSummary(), JSON_PRETTY_PRINT);
}
}
public function getSummary()
{
if ($this->hasBeenLoadedFromDb()) {
return Json::decode($this->getContent()->get('summary'));
} else {
$summary = [];
foreach (array_keys($this->objects) as $key) {
$summary[$key] = count($this->objects[$key]);
}
return $summary;
}
}
public function getJsonDump()
{
if ($this->hasBeenLoadedFromDb()) {
return $this->getContent()->get('content');
} else {
return Json::encode($this->objects, JSON_PRETTY_PRINT);
}
}
protected static function classWantsTemplate($class)
{
return strpos($class, '\\Icinga\\Module\\Director\\Objects\\Icinga') === 0;
}
protected function addAll($typeName)
{
$class = static::getClassForType($typeName);
/** @var ExportInterface $object */
if (static::classWantsTemplate($class)) {
/** @var IcingaObject $dummy */
$dummy = $class::create();
$db = $this->getDb();
$select = $db->select()->from($dummy->getTableName())
->where('object_type = ?', 'template');
$all = $class::loadAll($this->getConnection(), $select);
} else {
$all = $class::loadAll($this->getConnection());
}
foreach ($all as $object) {
$this->objects[$typeName][$object->getUniqueIdentifier()] = $object->export();
}
}
protected function addByIdentifiers($typeName, $identifiers)
{
foreach ($identifiers as $identifier) {
$this->addByIdentifier($typeName, $identifier);
}
}
/**
* @param $typeName
* @param $identifier
* @return ExportInterface
*/
public static function instanceByIdentifier($typeName, $identifier, Db $connection)
{
$class = static::getClassForType($typeName);
if (static::classWantsTemplate($class)) {
$identifier = [
'object_type' => 'template',
'object_name' => $identifier,
];
}
/** @var ExportInterface $object */
if ($class::exists($identifier, $connection)) {
$object = $class::load($identifier, $connection);
} else {
$object = null;
}
return $object;
}
protected function addByIdentifier($typeName, $identifier)
{
$object = static::instanceByIdentifier(
$typeName,
$identifier,
$this->getConnection()
);
$this->objects[$typeName][$identifier] = $object->export();
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Icinga\Module\Director\DirectorObject\Automation;
interface ExportInterface
{
/**
* @return \stdClass
*/
public function export();
// TODO:
// public function getXyzChecksum();
// public function getUniqueIdentifier();
}

View File

@ -0,0 +1,129 @@
<?php
namespace Icinga\Module\Director\Web\Table;
use dipl\Html\Link;
use dipl\Web\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
{
protected $searchColumns = [
'basket_name',
];
/** @var Basket */
protected $basket;
public function setBasket(Basket $basket)
{
$this->basket = $basket;
$this->searchColumns = [];
return $this;
}
public function renderRow($row)
{
$hexUuid = bin2hex($row->uuid);
$link = $this->linkToSnapshot($this->renderSummary($row->summary), $row);
if ($this->basket === null) {
$columns = [
$link,
new Link(
$row->basket_name,
'director/basket',
['uuid' => $hexUuid]
),
DateFormatter::formatDateTime($row->ts_create / 1000),
];
} else {
$columns = [
$link,
DateFormatter::formatDateTime($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
);
}
return implode(', ', $result);
}
protected function linkToSnapshot($caption, $row)
{
return new Link($caption, 'director/basket/snapshot', [
'checksum' => bin2hex($row->content_checksum),
'ts' => $row->ts_create,
'uuid' => bin2hex($row->uuid),
]);
}
public function getColumnsToBeRendered()
{
if ($this->basket === null) {
return [
$this->translate('Content'),
$this->translate('Basket'),
$this->translate('Created'),
];
} else {
return [
$this->translate('Content'),
$this->translate('Created'),
];
}
}
public function prepareQuery()
{
$query = $this->db()->select()->from([
'b' => 'director_basket'
], [
'b.uuid',
'b.basket_name',
'bs.ts_create',
'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->basket->get('uuid'));
}
return $query;
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Icinga\Module\Director\Web\Table;
use dipl\Html\Link;
use dipl\Web\Table\ZfQueryBasedTable;
class BasketTable extends ZfQueryBasedTable
{
protected $searchColumns = [
'basket_name',
];
public function renderRow($row)
{
$hexUuid = bin2hex($row->uuid);
$tr = $this::row([
new Link(
$row->basket_name,
'director/basket',
['uuid' => $hexUuid]
),
$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');
}
}