summaryrefslogtreecommitdiffstats
path: root/library/Icingadb/Web/Control
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:36:40 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:36:40 +0000
commita0901c4b7f2db488cb4fb3be2dd921a0308f4659 (patch)
treefafb393cf330a60df129ff10d0059eb7b14052a7 /library/Icingadb/Web/Control
parentInitial commit. (diff)
downloadicingadb-web-a0901c4b7f2db488cb4fb3be2dd921a0308f4659.tar.xz
icingadb-web-a0901c4b7f2db488cb4fb3be2dd921a0308f4659.zip
Adding upstream version 1.0.2.upstream/1.0.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Icingadb/Web/Control')
-rw-r--r--library/Icingadb/Web/Control/ProblemToggle.php74
-rw-r--r--library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php400
-rw-r--r--library/Icingadb/Web/Control/ViewModeSwitcher.php203
3 files changed, 677 insertions, 0 deletions
diff --git a/library/Icingadb/Web/Control/ProblemToggle.php b/library/Icingadb/Web/Control/ProblemToggle.php
new file mode 100644
index 0000000..c5aed82
--- /dev/null
+++ b/library/Icingadb/Web/Control/ProblemToggle.php
@@ -0,0 +1,74 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Web\Control;
+
+use ipl\Web\Common\FormUid;
+use ipl\Web\Compat\CompatForm;
+
+class ProblemToggle extends CompatForm
+{
+ use FormUid;
+
+ protected $filter;
+
+ protected $protector;
+
+ protected $defaultAttributes = [
+ 'name' => 'problem-toggle',
+ 'class' => 'icinga-form icinga-controls inline'
+ ];
+
+ public function __construct($filter)
+ {
+ $this->filter = $filter;
+ }
+
+ /**
+ * Set callback to protect ids with
+ *
+ * @param callable $protector
+ *
+ * @return $this
+ */
+ public function setIdProtector(callable $protector): self
+ {
+ $this->protector = $protector;
+
+ return $this;
+ }
+
+ /**
+ * Get whether the toggle is checked
+ *
+ * @return bool
+ */
+ public function isChecked(): bool
+ {
+ $this->ensureAssembled();
+
+ return $this->getElement('problems')->isChecked();
+ }
+
+ protected function assemble()
+ {
+ $this->addElement('checkbox', 'problems', [
+ 'class' => 'autosubmit',
+ 'id' => $this->protectId('problems'),
+ 'label' => t('Problems Only'),
+ 'value' => $this->filter !== null
+ ]);
+
+ $this->add($this->createUidElement());
+ }
+
+ private function protectId($id)
+ {
+ if (is_callable($this->protector)) {
+ return call_user_func($this->protector, $id);
+ }
+
+ return $id;
+ }
+}
diff --git a/library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php b/library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php
new file mode 100644
index 0000000..c4bdb6d
--- /dev/null
+++ b/library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php
@@ -0,0 +1,400 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Web\Control\SearchBar;
+
+use Generator;
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database;
+use Icinga\Module\Icingadb\Model\Behavior\ReRoute;
+use Icinga\Module\Icingadb\Model\CustomvarFlat;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Util\ObjectSuggestionsCursor;
+use ipl\Html\HtmlElement;
+use ipl\Orm\Exception\InvalidColumnException;
+use ipl\Orm\Exception\InvalidRelationException;
+use ipl\Orm\Model;
+use ipl\Orm\Relation;
+use ipl\Orm\Relation\BelongsToMany;
+use ipl\Orm\Relation\HasOne;
+use ipl\Orm\Resolver;
+use ipl\Orm\UnionModel;
+use ipl\Sql\Expression;
+use ipl\Sql\Select;
+use ipl\Stdlib\Filter;
+use ipl\Stdlib\Seq;
+use ipl\Web\Control\SearchBar\SearchException;
+use ipl\Web\Control\SearchBar\Suggestions;
+use PDO;
+
+class ObjectSuggestions extends Suggestions
+{
+ use Auth;
+ use Database;
+
+ /** @var Model */
+ protected $model;
+
+ /** @var array */
+ protected $customVarSources;
+
+ public function __construct()
+ {
+ $this->customVarSources = [
+ 'checkcommand' => t('Checkcommand %s', '..<customvar-name>'),
+ 'eventcommand' => t('Eventcommand %s', '..<customvar-name>'),
+ 'host' => t('Host %s', '..<customvar-name>'),
+ 'hostgroup' => t('Hostgroup %s', '..<customvar-name>'),
+ 'notification' => t('Notification %s', '..<customvar-name>'),
+ 'notificationcommand' => t('Notificationcommand %s', '..<customvar-name>'),
+ 'service' => t('Service %s', '..<customvar-name>'),
+ 'servicegroup' => t('Servicegroup %s', '..<customvar-name>'),
+ 'timeperiod' => t('Timeperiod %s', '..<customvar-name>'),
+ 'user' => t('User %s', '..<customvar-name>'),
+ 'usergroup' => t('Usergroup %s', '..<customvar-name>')
+ ];
+ }
+
+ /**
+ * Set the model to show suggestions for
+ *
+ * @param string|Model $model
+ *
+ * @return $this
+ */
+ public function setModel($model): self
+ {
+ if (is_string($model)) {
+ $model = new $model();
+ }
+
+ $this->model = $model;
+
+ return $this;
+ }
+
+ /**
+ * Get the model to show suggestions for
+ *
+ * @return Model
+ */
+ public function getModel(): Model
+ {
+ if ($this->model === null) {
+ throw new \LogicException(
+ 'You are accessing an unset property. Please make sure to set it beforehand.'
+ );
+ }
+
+ return $this->model;
+ }
+
+ protected function shouldShowRelationFor(string $column): bool
+ {
+ if (strpos($column, '.vars.') !== false) {
+ return false;
+ }
+
+ $tableName = $this->getModel()->getTableName();
+ $columnPath = explode('.', $column);
+
+ switch (count($columnPath)) {
+ case 3:
+ if ($columnPath[1] !== 'state' || ! in_array($tableName, ['host', 'service'])) {
+ return true;
+ }
+
+ // For host/service state relation columns apply the same rules
+ case 2:
+ return $columnPath[0] !== $tableName;
+ default:
+ return true;
+ }
+ }
+
+ protected function createQuickSearchFilter($searchTerm)
+ {
+ $model = $this->getModel();
+ $resolver = $model::on($this->getDb())->getResolver();
+
+ $quickFilter = Filter::any();
+ foreach ($model->getSearchColumns() as $column) {
+ $where = Filter::like($resolver->qualifyColumn($column, $model->getTableName()), $searchTerm);
+ $where->metaData()->set('columnLabel', $resolver->getColumnDefinition($where->getColumn())->getLabel());
+ $quickFilter->add($where);
+ }
+
+ return $quickFilter;
+ }
+
+ protected function fetchValueSuggestions($column, $searchTerm, Filter\Chain $searchFilter)
+ {
+ $model = $this->getModel();
+ $query = $model::on($this->getDb());
+ $query->limit(static::DEFAULT_LIMIT);
+
+ if (strpos($column, ' ') !== false) {
+ // $column may be a label
+ list($path, $_) = Seq::find(
+ self::collectFilterColumns($query->getModel(), $query->getResolver()),
+ $column,
+ false
+ );
+ if ($path !== null) {
+ $column = $path;
+ }
+ }
+
+ $columnPath = $query->getResolver()->qualifyPath($column, $model->getTableName());
+ list($targetPath, $columnName) = preg_split('/(?<=vars)\.|\.(?=[^.]+$)/', $columnPath);
+
+ $isCustomVar = false;
+ if (substr($targetPath, -5) === '.vars') {
+ $isCustomVar = true;
+ $targetPath = substr($targetPath, 0, -4) . 'customvar_flat';
+ }
+
+ if (strpos($targetPath, '.') !== false) {
+ try {
+ $query->with($targetPath); // TODO: Remove this, once ipl/orm does it as early
+ } catch (InvalidRelationException $e) {
+ throw new SearchException(sprintf(t('"%s" is not a valid relation'), $e->getRelation()));
+ }
+ }
+
+ if ($isCustomVar) {
+ $columnPath = $targetPath . '.flatvalue';
+ $query->filter(Filter::like($targetPath . '.flatname', $columnName));
+ }
+
+ $inputFilter = Filter::like($columnPath, $searchTerm);
+ $query->columns($columnPath);
+ $query->orderBy($columnPath);
+
+ // This had so many iterations, if it still doesn't work, consider removing it entirely :(
+ if ($searchFilter instanceof Filter\None) {
+ $query->filter($inputFilter);
+ } elseif ($searchFilter instanceof Filter\All) {
+ $searchFilter->add($inputFilter);
+
+ // There may be columns part of $searchFilter which target the base table. These must be
+ // optimized, otherwise they influence what we'll suggest to the user. (i.e. less)
+ // The $inputFilter on the other hand must not be optimized, which it wouldn't, but since
+ // we force optimization on its parent chain, we have to negate that.
+ $searchFilter->metaData()->set('forceOptimization', true);
+ $inputFilter->metaData()->set('forceOptimization', false);
+ } else {
+ $searchFilter = $inputFilter;
+ }
+
+ $query->filter($searchFilter);
+ $this->applyRestrictions($query);
+
+ try {
+ return (new ObjectSuggestionsCursor($query->getDb(), $query->assembleSelect()->distinct()))
+ ->setFetchMode(PDO::FETCH_COLUMN);
+ } catch (InvalidColumnException $e) {
+ throw new SearchException(sprintf(t('"%s" is not a valid column'), $e->getColumn()));
+ }
+ }
+
+ protected function fetchColumnSuggestions($searchTerm)
+ {
+ $model = $this->getModel();
+ $query = $model::on($this->getDb());
+
+ // Ordinary columns first
+ foreach (self::collectFilterColumns($model, $query->getResolver()) as $columnName => $columnMeta) {
+ yield $columnName => $columnMeta;
+ }
+
+ // Custom variables only after the columns are exhausted and there's actually a chance the user sees them
+ $titleAdded = false;
+ foreach ($this->getDb()->select($this->queryCustomvarConfig($searchTerm)) as $customVar) {
+ $search = $name = $customVar->flatname;
+ if (preg_match('/\w+\[(\d+)]$/', $search, $matches)) {
+ // array vars need to be specifically handled
+ if ($matches[1] !== '0') {
+ continue;
+ }
+
+ $name = substr($search, 0, -3);
+ $search = $name . '[*]';
+ }
+
+ foreach ($this->customVarSources as $relation => $label) {
+ if (isset($customVar->$relation)) {
+ if (! $titleAdded) {
+ $titleAdded = true;
+ $this->addHtml(HtmlElement::create(
+ 'li',
+ ['class' => static::SUGGESTION_TITLE_CLASS],
+ t('Custom Variables')
+ ));
+ }
+
+ yield $relation . '.vars.' . $search => sprintf($label, $name);
+ }
+ }
+ }
+ }
+
+ protected function matchSuggestion($path, $label, $searchTerm)
+ {
+ if (preg_match('/[_.](id|bin|checksum)$/', $path)) {
+ // Only suggest exotic columns if the user knows about them
+ $trimmedSearch = trim($searchTerm, ' *');
+ return substr($path, -strlen($trimmedSearch)) === $trimmedSearch;
+ }
+
+ return parent::matchSuggestion($path, $label, $searchTerm);
+ }
+
+ /**
+ * Create a query to fetch all available custom variables matching the given term
+ *
+ * @param string $searchTerm
+ *
+ * @return Select
+ */
+ protected function queryCustomvarConfig(string $searchTerm): Select
+ {
+ $customVars = CustomvarFlat::on($this->getDb());
+ $tableName = $customVars->getModel()->getTableName();
+ $resolver = $customVars->getResolver();
+
+ $scalarQueries = [];
+ $aggregates = ['flatname'];
+ foreach ($resolver->getRelations($customVars->getModel()) as $name => $relation) {
+ if (isset($this->customVarSources[$name]) && $relation instanceof BelongsToMany) {
+ $query = $customVars->createSubQuery(
+ $relation->getTarget(),
+ $resolver->qualifyPath($name, $tableName)
+ );
+
+ $this->applyRestrictions($query);
+
+ $aggregates[$name] = new Expression("MAX($name)");
+ $scalarQueries[$name] = $query->assembleSelect()
+ ->resetColumns()->columns(new Expression('1'))
+ ->limit(1);
+ }
+ }
+
+ $customVars->columns('flatname');
+ $this->applyRestrictions($customVars);
+ $customVars->filter(Filter::like('flatname', $searchTerm));
+ $idColumn = $resolver->qualifyColumn('id', $resolver->getAlias($customVars->getModel()));
+ $customVars = $customVars->assembleSelect();
+
+ $customVars->columns($scalarQueries);
+ $customVars->groupBy($idColumn);
+ $customVars->limit(static::DEFAULT_LIMIT);
+
+ // This outer query exists only because there's no way to combine aggregates and sub queries (yet)
+ return (new Select())->columns($aggregates)->from(['results' => $customVars])->groupBy('flatname');
+ }
+
+ /**
+ * Collect all columns of this model and its relations that can be used for filtering
+ *
+ * @param Model $model
+ * @param Resolver $resolver
+ *
+ * @return Generator
+ */
+ public static function collectFilterColumns(Model $model, Resolver $resolver): Generator
+ {
+ if ($model instanceof UnionModel) {
+ $models = [];
+ foreach ($model->getUnions() as $union) {
+ /** @var Model $unionModel */
+ $unionModel = new $union[0]();
+ $models[$unionModel->getTableName()] = $unionModel;
+ self::collectRelations($resolver, $unionModel, $models, []);
+ }
+ } else {
+ $models = [$model->getTableName() => $model];
+ self::collectRelations($resolver, $model, $models, []);
+ }
+
+ foreach ($models as $path => $targetModel) {
+ /** @var Model $targetModel */
+ foreach ($resolver->getColumnDefinitions($targetModel) as $columnName => $definition) {
+ yield $path . '.' . $columnName => $definition->getLabel();
+ }
+ }
+
+ foreach ($resolver->getBehaviors($model) as $behavior) {
+ if ($behavior instanceof ReRoute) {
+ foreach ($behavior->getRoutes() as $name => $route) {
+ $relation = $resolver->resolveRelation(
+ $resolver->qualifyPath($route, $model->getTableName()),
+ $model
+ );
+ foreach ($resolver->getColumnDefinitions($relation->getTarget()) as $columnName => $definition) {
+ yield $name . '.' . $columnName => $definition->getLabel();
+ }
+ }
+ }
+ }
+
+ if ($model instanceof UnionModel) {
+ $queries = $model->getUnions();
+ $baseModelClass = end($queries)[0];
+ $model = new $baseModelClass();
+ }
+
+ $foreignMetaDataSources = [];
+ if (! $model instanceof Host) {
+ $foreignMetaDataSources[] = 'host.user';
+ $foreignMetaDataSources[] = 'host.usergroup';
+ }
+
+ if (! $model instanceof Service) {
+ $foreignMetaDataSources[] = 'service.user';
+ $foreignMetaDataSources[] = 'service.usergroup';
+ }
+
+ foreach ($foreignMetaDataSources as $path) {
+ $foreignColumnDefinitions = $resolver->getColumnDefinitions($resolver->resolveRelation(
+ $resolver->qualifyPath($path, $model->getTableName()),
+ $model
+ )->getTarget());
+ foreach ($foreignColumnDefinitions as $columnName => $columnDefinition) {
+ yield "$path.$columnName" => $columnDefinition->getLabel();
+ }
+ }
+ }
+
+ /**
+ * Collect all direct relations of the given model
+ *
+ * A direct relation is either a direct descendant of the model
+ * or a descendant of such related in a to-one cardinality.
+ *
+ * @param Resolver $resolver
+ * @param Model $subject
+ * @param array $models
+ * @param array $path
+ */
+ protected static function collectRelations(Resolver $resolver, Model $subject, array &$models, array $path)
+ {
+ foreach ($resolver->getRelations($subject) as $name => $relation) {
+ /** @var Relation $relation */
+ $isHasOne = $relation instanceof HasOne;
+ if (empty($path) || $name === 'state' || $name === 'last_comment') {
+ $relationPath = [$name];
+ if ($isHasOne && empty($path)) {
+ array_unshift($relationPath, $subject->getTableName());
+ }
+
+ $relationPath = array_merge($path, $relationPath);
+ $models[join('.', $relationPath)] = $relation->getTarget();
+ self::collectRelations($resolver, $relation->getTarget(), $models, $relationPath);
+ }
+ }
+ }
+}
diff --git a/library/Icingadb/Web/Control/ViewModeSwitcher.php b/library/Icingadb/Web/Control/ViewModeSwitcher.php
new file mode 100644
index 0000000..669bc32
--- /dev/null
+++ b/library/Icingadb/Web/Control/ViewModeSwitcher.php
@@ -0,0 +1,203 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Web\Control;
+
+use ipl\Html\Attributes;
+use ipl\Html\Form;
+use ipl\Html\FormElement\HiddenElement;
+use ipl\Html\FormElement\InputElement;
+use ipl\Html\HtmlElement;
+use ipl\Web\Common\FormUid;
+use ipl\Web\Widget\IcingaIcon;
+
+class ViewModeSwitcher extends Form
+{
+ use FormUid;
+
+ protected $defaultAttributes = [
+ 'class' => 'view-mode-switcher',
+ 'name' => 'view-mode-switcher'
+ ];
+
+ /** @var string Default view mode */
+ const DEFAULT_VIEW_MODE = 'common';
+
+ /** @var string Default view mode param */
+ const DEFAULT_VIEW_MODE_PARAM = 'view';
+
+ /** @var array View mode-icon pairs */
+ public static $viewModes = [
+ 'minimal' => 'minimal',
+ 'common' => 'default',
+ 'detailed' => 'detailed',
+ 'tabular' => 'tabular'
+ ];
+
+ /** @var string */
+ protected $defaultViewMode;
+
+ /** @var string */
+ protected $method = 'POST';
+
+ /** @var callable */
+ protected $protector;
+
+ /** @var string */
+ protected $viewModeParam = self::DEFAULT_VIEW_MODE_PARAM;
+
+ /**
+ * Get the default mode
+ *
+ * @return string
+ */
+ public function getDefaultViewMode(): string
+ {
+ return $this->defaultViewMode ?: static::DEFAULT_VIEW_MODE;
+ }
+
+ /**
+ * Set the default view mode
+ *
+ * @param string $defaultViewMode
+ *
+ * @return $this
+ */
+ public function setDefaultViewMode(string $defaultViewMode): self
+ {
+ $this->defaultViewMode = $defaultViewMode;
+
+ return $this;
+ }
+
+ /**
+ * Get the view mode URL parameter
+ *
+ * @return string
+ */
+ public function getViewModeParam(): string
+ {
+ return $this->viewModeParam;
+ }
+
+ /**
+ * Set the view mode URL parameter
+ *
+ * @param string $viewModeParam
+ *
+ * @return $this
+ */
+ public function setViewModeParam(string $viewModeParam): self
+ {
+ $this->viewModeParam = $viewModeParam;
+
+ return $this;
+ }
+
+ /**
+ * Get the view mode
+ *
+ * @return string
+ */
+ public function getViewMode(): string
+ {
+ $viewMode = $this->getPopulatedValue($this->getViewModeParam(), $this->getDefaultViewMode());
+
+ if (array_key_exists($viewMode, static::$viewModes)) {
+ return $viewMode;
+ }
+
+ return $this->getDefaultViewMode();
+ }
+
+ /**
+ * Set the view mode
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setViewMode(string $name)
+ {
+ $this->populate([$this->getViewModeParam() => $name]);
+
+ return $this;
+ }
+
+ /**
+ * Set callback to protect ids with
+ *
+ * @param callable $protector
+ *
+ * @return $this
+ */
+ public function setIdProtector(callable $protector): self
+ {
+ $this->protector = $protector;
+
+ return $this;
+ }
+
+ private function protectId($id)
+ {
+ if (is_callable($this->protector)) {
+ return call_user_func($this->protector, $id);
+ }
+
+ return $id;
+ }
+
+ protected function assemble()
+ {
+ $viewModeParam = $this->getViewModeParam();
+
+ $this->addElement($this->createUidElement());
+ $this->addElement(new HiddenElement($viewModeParam));
+
+ foreach (static::$viewModes as $viewMode => $icon) {
+ if ($viewMode === 'tabular') {
+ continue;
+ }
+
+ $protectedId = $this->protectId('view-mode-switcher-' . $icon);
+ $input = new InputElement($viewModeParam, [
+ 'class' => 'autosubmit',
+ 'id' => $protectedId,
+ 'name' => $viewModeParam,
+ 'type' => 'radio',
+ 'value' => $viewMode
+ ]);
+ $input->getAttributes()->registerAttributeCallback('checked', function () use ($viewMode) {
+ return $viewMode === $this->getViewMode();
+ });
+
+ $label = new HtmlElement(
+ 'label',
+ Attributes::create([
+ 'for' => $protectedId
+ ]),
+ new IcingaIcon($icon)
+ );
+ $label->getAttributes()->registerAttributeCallback('title', function () use ($viewMode) {
+ switch ($viewMode) {
+ case 'minimal':
+ $active = t('Minimal view active');
+ $inactive = t('Switch to minimal view');
+ break;
+ case 'common':
+ $active = t('Common view active');
+ $inactive = t('Switch to common view');
+ break;
+ case 'detailed':
+ $active = t('Detailed view active');
+ $inactive = t('Switch to detailed view');
+ }
+
+ return $viewMode === $this->getViewMode() ? $active : $inactive;
+ });
+
+ $this->addHtml($input, $label);
+ }
+ }
+}