Add documentation, add errorhandling and sizing

The optional width/height parameter can be set in the ini to change the dashboard components
size, documentation has been added and in the error case a message with the configuration is shown

refs #4192
This commit is contained in:
Jannis Moßhammer 2013-08-07 17:44:18 +02:00
parent f8bb478f0e
commit 2b25757e20
11 changed files with 473 additions and 130 deletions

View File

@ -52,7 +52,6 @@ class ConfigurationController extends ActionController
);
$tabBuilder->build();
$this->view->tabs = $tabBuilder->getTabs();
}
}

View File

@ -6,11 +6,25 @@ use Icinga\Web\Url;
use Icinga\Application\Icinga;
use Icinga\Web\Widget\Dashboard;
use Icinga\Application\Config as IcingaConfig;
use Icinga\Exception\ConfigurationError;
use Icinga\Form\Dashboard\AddUrlForm;
use Icinga\Exception\ConfigurationError;
/**
* Handle creation, removal and displaying of dashboards, panes and components
*
* @see Icinga\Web\Widget\Dashboard for more information about dashboards
*/
class DashboardController extends ActionController
{
/**
* Retrieve a dashboard from the provided config
*
* @param string $config The config to read the dashboard from, or 'dashboard/dashboard' if none is given
*
* @return Dashboard
*/
private function getDashboard($config = 'dashboard/dashboard')
{
$dashboard = new Dashboard();
@ -18,6 +32,10 @@ class DashboardController extends ActionController
return $dashboard;
}
/**
* Remove a component from the pane identified by the 'pane' parameter
*
*/
public function removecomponentAction()
{
$pane = $this->_getParam('pane');
@ -34,14 +52,18 @@ class DashboardController extends ActionController
}
$this->redirectNow(Url::fromPath('dashboard', array('pane' => $pane)));
}
/**
* Display the form for adding new components or add the new component if submitted
*
*/
public function addurlAction()
{
$form = new AddUrlForm();
$form->setRequest($this->_request);
$this->view->form = $form;
if ($form->isSubmittedAndValid()) {
$dashboard = $this->getDashboard();
$dashboard->setComponentUrl(
@ -49,23 +71,26 @@ class DashboardController extends ActionController
$form->getValue('component'),
ltrim($form->getValue('url'), '/')
);
$this->persistDashboard($dashboard);
$this->redirectNow(
Url::fromPath(
'dashboard',
array(
'pane' => $form->getValue('pane')
)
)
);
try {
$dashboard->store();
$this->redirectNow(
Url::fromPath('dashboard',array('pane' => $form->getValue('pane')))
);
} catch (ConfigurationError $exc) {
$this->_helper->viewRenderer('show_configuration');
$this->view->exceptionMessage = $exc->getMessage();
$this->view->iniConfigurationString = $dashboard->toIni();
}
}
}
private function persistDashboard(Dashboard $dashboard)
{
$dashboard->store();
}
/**
* Display the dashboard with the pane set in the 'pane' request parameter
*
* If no pane is submitted or the submitted one doesn't exist, the default pane is
* displayed (normally the first one)
*
*/
public function indexAction()
{
$dashboard = $this->getDashboard();
@ -75,10 +100,10 @@ class DashboardController extends ActionController
$dashboard->activate($dashboardName);
}
$this->view->tabs = $dashboard->getTabs();
$this->view->tabs->add("Add", array(
"title" => "Add Url",
"iconCls" => "plus",
"url" => Url::fromPath("dashboard/addurl")
$this->view->tabs->add('Add', array(
'title' => 'Add Url',
'iconCls' => 'plus',
'url' => Url::fromPath('dashboard/addurl')
));
$this->view->dashboard = $dashboard;
}

View File

@ -0,0 +1,29 @@
<br/>
<div class="alert alert-error">
<h4><i class="icon-warning-sign"> </i>Saving dashboard failed</h4>
<br/>
<p>
Your dashboard couldn't be stored (error: "<?= $this->exceptionMessage ?>"). This could have one or more
of the following reasons:
</p>
<ul>
<li>You don't have permissions to write to the dashboard file</li>
<li>Something went wrong while writing the file</li>
<li>There's an application error preventing you from persisting the configuration</li>
</ul>
</div>
<p>
Details can be seen in your application log (if you don't have access to this file, call your administrator in this case).
<br/>
In case you can access the configuration file (config/dashboard/dashboard.ini) by yourself, you can open it and
insert the config manually:
</p>
<p>
<pre>
<code>
<?= $this->escape($this->iniConfigurationString) ?>
</code>
</pre>
</p>

View File

@ -1,11 +0,0 @@
[test]
title = "test"
[test.test]
url = "test"
[test.test2]
url = "test2"
[test.dsgdgs]
url = "dsdsgdsg"

43
doc/dashboard.md Normal file
View File

@ -0,0 +1,43 @@
# The dashboard
The icingaweb dashboard allows you to display different views on one page. You can create customized overviews over
the objects you're interested in and can add and remove elements.
## Dashboard, Panes and Components
![Dashboard structure][dashboards1]
* The building blocks of dashboards are components - those represent a single URL and display it's content (often in
a more condensed layout)
* Different components can be added to a pane and will be shown their. All panes are shown as tabs on top of the dashboard,
whereas the title is used for the text in the tab
* The dashboard itself is just the view containing the panes
## Configuration files
By default, the config/dashboard/dashboard.ini is used for storing dashboards in the following format:
[PaneName] ; Define a new Pane
title = "PaneTitle" ; The title of the pane as displayed in the tabls
[PaneName.Component1] ; Define a new component 'Component 1' underneat the pane
url = "/url/for/component1" ; the url that will be displayed, with view=compact as URL parameter appended
height = "500px" ; optional height setting
width = "400px" ; optional width setting
[test.My hosts] ; Another component, here with host
url = "monitoring/list/hosts" ; the url of the component
; Notice the missing height/width definition
[test.My services] ; And another pane
url = "monitoring/list/services" ; With service url
[test2] ; Define a second pane
title = "test2" ; with the title
[test2.test] ; Add a component to the second pane
url = "/monitoring/show/host/host1" ; ...and define it's url
[dashboards1]: res/Dashboard.png

BIN
doc/res/Dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -48,7 +48,7 @@ class Dimension
private $unit = self::UNIT_PX;
/**
* Creates a new Dimension object with the given size and unit
* Create a new Dimension object with the given size and unit
*
* @param int $value The new value
* @param string $unit The unit to use (default: px)
@ -71,7 +71,7 @@ class Dimension
}
/**
* Returns true when the value is > 0
* Return true when the value is > 0
*
* @return bool
*/
@ -81,7 +81,7 @@ class Dimension
}
/**
* Returns the underlying value without unit information
* Return the underlying value without unit information
*
* @return int
*/
@ -91,7 +91,17 @@ class Dimension
}
/**
* Returns this value with it's according unit as a string
* Return the unit used for the value
*
* @return string
*/
public function getUnit()
{
return $this->unit;
}
/**
* Return this value with it's according unit as a string
*
* @return string
*/
@ -103,12 +113,19 @@ class Dimension
return $this->value.$this->unit;
}
/**
* Create a new Dimension object from a string containing the numeric value and the dimension (e.g. 200px, 20%)
*
* @param $string The string to parse
*
* @return Dimension
*/
public static function fromString($string)
{
$matches = array();
if (!preg_match_all('/^ *([0-9]+)(px|pt|em|\%) */i', $string, $matches)) {
throw new InvalidArgumentException($string.' is not a valid dimension');
return new Dimension(0);
}
return new Dimension(intval($matches[1]), $matches[2]);
return new Dimension(intval($matches[1][0]), $matches[2][0]);
}
}

View File

@ -12,80 +12,121 @@ use Icinga\Web\Widget\Dashboard\Component as DashboardComponent;
use Icinga\Web\Url;
/**
* Dashboards display multiple views on a single page
*
* The terminology is as follows:
* - Component: A single view showing a specific url
* - Pane: Aggregates one or more components on one page, displays it's title as a tab
* - Dashboard: Shows all panes
*
*/
class Dashboard implements Widget
{
/**
* The configuration containing information about this dashboard
*
* @var IcingaConfig;
*/
private $config;
private $configfile;
/**
* An array containing all panes of this dashboard
*
* @var array
*/
private $panes = array();
/**
* The @see Icinga\Web\Widget\Tabs object for displaying displayable panes
*
* @var Tabs
*/
private $tabs;
private $url = null;
/**
* The parameter that will be added to identify panes
*
* @var string
*/
private $tabParam = 'pane';
public function __construct()
{
if ($this->url === null) {
$this->url = Url::fromRequest()->getUrlWithout($this->tabParam);
}
}
/**
* Set the given tab name as active.
*
* @param string $name The tab name to activate
*
*/
public function activate($name)
{
$this->getTabs()->activate($name);
}
/**
* Return the tab object used to navigate through this dashboard
*
* @return Tabs
*/
public function getTabs()
{
$url = Url::fromRequest()->getUrlWithout($this->tabParam);
if ($this->tabs === null) {
$this->tabs = new Tabs();
foreach ($this->panes as $key => $pane) {
$this->tabs->add($key, array(
'title' => $pane->getTitle(),
'url' => clone($this->url),
'url' => clone($url),
'urlParams' => array($this->tabParam => $key)
));
}
}
return $this->tabs;
}
public function isWritable()
{
return is_writable($this->configfile);
}
/**
* Store the current dashboard with all it's panes and components to the given file (or the default one if none is
* given)
*
*
* @param string $file The filename to store this dashboard as an ini
*
* @return $this
* @throws \Icinga\Exception\ConfigurationError If persisting fails, details are written to the log
*
*/
public function store($file = null)
{
if ($file === null) {
$file = IcingaConfig::app('dashboard/dashboard')->getConfigFile();
}
$this->configfile = $file;
if (!$this->isWritable()) {
Logger::error("Tried to persist dashboard to %s, but path is not writeable", $this->configfile);
if (!is_writable($file)) {
Logger::error('Tried to persist dashboard to %s, but path is not writeable', $file);
throw new ConfigurationError('Can\'t persist dashboard');
}
if (! @file_put_contents($this->configfile, $this->toIni())) {
if (! @file_put_contents($file, $this->toIni())) {
$error = error_get_last();
if ($error == NULL) {
$error = "Unknown error";
$error = 'Unknown error';
} else {
$error = $error["message"];
$error = $error['message'];
}
Logger::error("Tried to persist dashboard to %s, but got error: %s", $this->configfile, $error);
Logger::error('Tried to persist dashboard to %s, but got error: %s', $file, $error);
throw new ConfigurationError('Can\'t persist dashboard');
} else {
return $this;
}
}
/**
* Populate this dashboard via the given configuration file
*
* @param IcingaConfig $config The configuration file to populate this dashboard with
*
* @return $this
*/
public function readConfig(IcingaConfig $config)
{
$this->config = $config;
@ -94,6 +135,11 @@ class Dashboard implements Widget
return $this;
}
/**
* Creates a new empty pane with the given title
*
* @param $title
*/
public function createPane($title)
{
$pane = new Pane($title);
@ -102,12 +148,22 @@ class Dashboard implements Widget
}
/**
* Update or adds a new component with the given url to a pane
*
* @TODO: Should only allow component objects to be added directly as soon as we store more information
*
* @param string $pane The pane to add the component to
* @param Component|string $component The component to add or the title of the newly created component
* @param $url The url to use for the component
*
* @return $this
*/
public function setComponentUrl($pane, $component, $url)
{
if ($component === null && strpos($pane, '.')) {
list($pane, $component) = preg_split('~\.~', $pane, 2);
}
if (!isset($this->panes[$pane])) {
$this->createPane($pane);
}
@ -120,6 +176,13 @@ class Dashboard implements Widget
return $this;
}
/**
* Return true if a pane doesn't exist or doesn't have any components in it
*
* @param string $pane The name of the pane to check for emptyness
*
* @return bool
*/
public function isEmptyPane($pane)
{
$paneObj = $this->getPane($pane);
@ -130,6 +193,15 @@ class Dashboard implements Widget
return !empty($cmps);
}
/**
* Remove a component $component from the given pane
*
* @param string $pane The pane to remove the component from
* @param Component|string $component The component to remove or it's name
*
* @return $this
*/
public function removeComponent($pane, $component)
{
if ($component === null && strpos($pane, '.')) {
@ -143,6 +215,11 @@ class Dashboard implements Widget
return $this;
}
/**
* Return an array with pane name=>title format used for comboboxes
*
* @return array
*/
public function getPaneKeyTitleArray()
{
$list = array();
@ -152,40 +229,49 @@ class Dashboard implements Widget
return $list;
}
public function getComponentEnum()
{
$list = array();
foreach ($this->panes as $name => $pane) {
foreach ($pane->getComponents() as $component) {
$list[$name . '.' . $component->getTitle()] =
$pane->getTitle() . ': ' . $component->getTitle();
}
}
return $list;
}
/**
* Add a pane object to this dashboard
*
* @param Pane $pane The pane to add
*
* @return $this
*/
public function addPane(Pane $pane)
{
$this->panes[$pane->getName()] = $pane;
return $this;
}
/**
* Return the pane with the provided name or null if it doesn't exit
*
* @param string $name The name of the pane to return
*
* @return null|Pane The pane or null if no pane with the given name exists
*/
public function getPane($name)
{
if (!isset($this->panes[$name]))
return null;
return $this->panes[$name];
}
/**
* @see Icinga\Web\Widget::render
*/
public function render(\Zend_View_Abstract $view)
{
if (empty($this->panes)) {
return '';
}
return $this->getActivePane()->render($view);
return $this->determineActivePane()->render($view);
}
/**
* Activates the default pane of this dashboard and returns it's name
*
* @return mixed
*/
private function setDefaultPane()
{
reset($this->panes);
@ -194,7 +280,12 @@ class Dashboard implements Widget
return $active;
}
public function getActivePane()
/**
* Determine the active pane either by the selected tab or the current request
*
* @return Pane The currently active pane
*/
public function determineActivePane()
{
$active = $this->getTabs()->getActiveName();
if (! $active) {
@ -211,6 +302,11 @@ class Dashboard implements Widget
return $this->panes[$active];
}
/**
* Return the ini string describing this dashboard
*
* @return string
*/
public function toIni()
{
$ini = '';
@ -219,23 +315,24 @@ class Dashboard implements Widget
}
return $ini;
}
protected function loadConfigPanes()
/**
* Load all config panes from @see Dashboard::$config
*
*/
private function loadConfigPanes()
{
$items = $this->config;
$app = Icinga::app();
foreach ($items->keys() as $key) {
$item = $this->config->get($key, false);
if (false === strstr($key, '.')) {
$this->addPane(Pane::fromIni($key, $item));
} else {
list($paneName, $title) = explode('.', $key , 2);
list($paneName, $title) = explode('.', $key, 2);
$pane = $this->getPane($paneName);
$pane->addComponent(DashboardComponent::fromIni($title, $item, $pane));
}
}
}
}

View File

@ -10,21 +10,49 @@ use Zend_Config;
/**
* A dashboard pane component
*
* Needs a title and an URL
* This is the element displaying a specific view in icinga2web
*
*/
class Component implements Widget
{
/**
* The url of this Component
*
* @var \Icinga\Web\Url
*/
private $url;
private $title;
private $width;
private $height;
/**
* The title being displayed on top of the component
* @var
*/
private $title;
/**
* The width of the component, if set
*
* @var Dimension|null
*/
private $width = null;
/**
* The height of the component, if set
*
* @var Dimension|null
*/
private $height = null;
/**
* The pane containing this component, needed for the 'remove button'
* @var Pane
*/
private $pane;
/**
* The template string used for rendering this widget
*
* @var string
*/
private $template =<<<'EOD'
<div class="icinga-container dashboard" icingatitle="{TITLE}" style="{DIMENSION}">
@ -39,7 +67,13 @@ class Component implements Widget
</div>
EOD;
/**
* Create a new component displaying the given url in the provided pane
*
* @param string $title The title to use for this component
* @param Url|string $url The url this component uses for displaying information
* @param Pane $pane The pane this Component will be added to
*/
public function __construct($title, $url, Pane $pane)
{
$this->title = $title;
@ -51,18 +85,28 @@ EOD;
}
}
/**
* Set the with for this component or use the default width if null is provided
*
* @param Dimension|null $width The width to use or null to use the default width
*/
public function setWidth(Dimension $width = null)
{
$this->width = $width;
}
/**
* Set the with for this component or use the default height if null is provided
*
* @param Dimension|null $height The height to use or null to use the default height
*/
public function setHeight(Dimension $height = null)
{
$this->height = $height;
}
/**
* Retrieve this components title
* Retrieve the components title
*
* @return string
*/
@ -72,7 +116,7 @@ EOD;
}
/**
* Retrieve my url
* Retrieve the components url
*
* @return Url
*/
@ -82,10 +126,11 @@ EOD;
}
/**
* Set this components URL
* Set the components URL
*
* @param string|Url $url Component URL
* @return self
* @param string|Url $url The url to use, either as an Url object or as a path
*
* @return $this
*/
public function setUrl($url)
{
@ -97,35 +142,29 @@ EOD;
return $this;
}
protected function iniPair($key, $val)
{
return sprintf(
"%s = %s\n",
$key,
$this->quoteIni($val)
);
}
protected function quoteIni($str)
{
return '"' . $str . '"';
}
/**
* Return this component in a suitable format and encoding for ini files
*
* @return string
*/
public function toIni()
{
$ini = $this->iniPair('url', $this->url->getRelativeUrl());
$ini = 'url = "'.$this->url->getRelativeUrl().'"'.PHP_EOL;
foreach ($this->url->getParams() as $key => $val) {
$ini .= $this->iniPair($key, $val);
$ini .= $key.' = "'.$val.'"'.PHP_EOL;
}
if ($this->height !== null) {
$ini .= 'height: '.((string) $this->height).'\n';
$ini .= 'height = "'.((string) $this->height).'"'.PHP_EOL;
}
if ($this->width !== null) {
$ini .= 'width: '.((string) $this->width).'\n';
$ini .= 'width = "'.((string) $this->width).'"'.PHP_EOL;
}
return $ini;
}
/**
* @see Widget::render()
*/
public function render(\Zend_View_Abstract $view)
{
$url = clone($this->url);
@ -141,14 +180,18 @@ EOD;
)
);
$html = str_replace("{URL}", $url->getAbsoluteUrl(), $this->template);
$html = str_replace("{REMOVE_URL}", $removeUrl, $html);
$html = str_replace("{STYLE}", $this->getBoxSizeAsCSS(), $html);
$html = str_replace("{DIMENSION}", $this->getBoxSizeAsCSS(), $html);
$html = str_replace("{TITLE}", $view->escape($this->getTitle()), $html);
return $html;
}
/**
* Return the height and width deifnition (if given) in CSS format
*
* @return string
*/
private function getBoxSizeAsCSS()
{
$style = "";
@ -158,8 +201,18 @@ EOD;
if ($this->width) {
$style .= 'width:'.(string) $this->width.';';
}
return $style;
}
/**
* Create a @see Component instance from the given Zend config, using the provided title
*
* @param $title The title for this component
* @param Zend_Config $config The configuration defining url, parameters, height, width, etc.
* @param Pane $pane The pane this component belongs to
*
* @return Component A newly created Component for use in the Dashboard
*/
public static function fromIni($title, Zend_Config $config, Pane $pane)
{
$height = null;

View File

@ -7,39 +7,97 @@ use Icinga\Exception\ConfigurationError;
use Icinga\Web\Widget\Widget;
use Zend_Config;
/**
* A pane, displaying different Dashboard components
*
*/
class Pane implements Widget
{
protected $name;
protected $title;
protected $components = array();
/**
* The name of this pane, as defined in the ini file
*
* @var string
*/
private $name;
/**
* The title of this pane, as displayed in the dashboard tabs
* @TODO: Currently the same as $name, evaluate if distinguishing is needed
*
* @var string
*/
private $title;
/**
* An array of @see Components that are displayed in this pane
*
* @var array
*/
private $components = array();
/**
* Create a new pane
*
* @param $name The pane to create
*/
public function __construct($name)
{
$this->name = $name;
$this->title = $name;
}
/**
* Returns the name of this pane
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Returns the title of this pane
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Overwrite the title of this pane
*
* @param string $title The new title to use for this pane
* @return Pane $this
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
/**
* Return true if a component with the given title exists in this pane
*
* @param string $title The title of the component to check for existence
*
* @return bool
*/
public function hasComponent($title)
{
return array_key_exists($title, $this->components);
}
/**
* Return a component with the given name if existing
*
* @param string $title The title of the component to return
*
* @return Component The component with the given title
* @throws ProgrammingError If the component doesn't exist
*/
public function getComponent($title)
{
if ($this->hasComponent($title)) {
@ -51,6 +109,12 @@ class Pane implements Widget
));
}
/**
* Removes the component with the given title if it exists in this pane
*
* @param string $title The pane
* @return Pane $this
*/
public function removeComponent($title)
{
if ($this->hasComponent($title)) {
@ -59,11 +123,19 @@ class Pane implements Widget
return $this;
}
/**
* Return all components added at this pane
*
* @return array
*/
public function getComponents()
{
return $this->components;
}
/**
* @see Widget::render
*/
public function render(\Zend_View_Abstract $view)
{
$html = PHP_EOL;
@ -73,6 +145,15 @@ class Pane implements Widget
return $html;
}
/**
* Add a component to this pane, optionally creating it if $component is a string
*
* @param string|Component $component The component object or title (if a new component will be created)
* @param string|null $url An Url to be used when component is a string
*
* @return Pane $this
* @throws \Icinga\Exception\ConfigurationError
*/
public function addComponent($component, $url = null)
{
if ($component instanceof Component) {
@ -80,24 +161,24 @@ class Pane implements Widget
} elseif (is_string($component) && $url !== null) {
$this->components[$component] = new Component($component, $url, $this);
} else{
throw new ConfigurationError('You messed up your dashboard');
throw new ConfigurationError('Invalid component added: '.$component);
}
return $this;
}
protected function quoteIni($str)
{
return '"' . $str . '"';
}
/**
* Return the ini representation of this pane as a string
*
* @return string
*/
public function toIni()
{
if (empty($this->components))
{
return "";
return '';
}
$ini = '['.$this->getName().']'.PHP_EOL.
'title = '.$this->quoteIni($this->getTitle()).PHP_EOL;
'title = "'.$this->getTitle().'"'.PHP_EOL;
foreach ($this->components as $title => $component) {
// component header
@ -108,6 +189,14 @@ class Pane implements Widget
return $ini;
}
/**
* Create a new pane with the title $title from the given configuration
*
* @param $title The title for this pane
* @param Zend_Config $config The configuration to use for setup
*
* @return Pane
*/
public static function fromIni($title, Zend_Config $config)
{
$pane = new Pane($title);

View File

@ -60,6 +60,8 @@ class Monitoring_ListController extends ModuleActionController
$state_column = 'service_hard_state';
$state_change_column = 'service_last_hard_state_change';
}
$this->compactView = "services-compact";
$this->view->services = $this->query('status', array(
'host_name',