From 2f05ed3d4907a36ea034657577ac99acf0a700e3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 29 Oct 2014 15:40:34 +0100 Subject: [PATCH] Add monitoring module wizard refs #7163 --- .../application/forms/Setup/BackendPage.php | 63 ++++++ .../forms/Setup/IdoResourcePage.php | 89 ++++++++ .../application/forms/Setup/InstancePage.php | 40 ++++ .../forms/Setup/LivestatusResourcePage.php | 89 ++++++++ .../application/forms/Setup/SecurityPage.php | 36 ++++ .../application/forms/Setup/WelcomePage.php | 65 ++++++ .../Monitoring/Installation/BackendStep.php | 177 ++++++++++++++++ .../Monitoring/Installation/InstanceStep.php | 105 ++++++++++ .../Monitoring/Installation/SecurityStep.php | 81 ++++++++ .../monitoring/library/Monitoring/Setup.php | 192 ++++++++++++++++++ public/css/icinga/setup.less | 14 +- 11 files changed, 949 insertions(+), 2 deletions(-) create mode 100644 modules/monitoring/application/forms/Setup/BackendPage.php create mode 100644 modules/monitoring/application/forms/Setup/IdoResourcePage.php create mode 100644 modules/monitoring/application/forms/Setup/InstancePage.php create mode 100644 modules/monitoring/application/forms/Setup/LivestatusResourcePage.php create mode 100644 modules/monitoring/application/forms/Setup/SecurityPage.php create mode 100644 modules/monitoring/application/forms/Setup/WelcomePage.php create mode 100644 modules/monitoring/library/Monitoring/Installation/BackendStep.php create mode 100644 modules/monitoring/library/Monitoring/Installation/InstanceStep.php create mode 100644 modules/monitoring/library/Monitoring/Installation/SecurityStep.php create mode 100644 modules/monitoring/library/Monitoring/Setup.php diff --git a/modules/monitoring/application/forms/Setup/BackendPage.php b/modules/monitoring/application/forms/Setup/BackendPage.php new file mode 100644 index 000000000..95c5f9fe4 --- /dev/null +++ b/modules/monitoring/application/forms/Setup/BackendPage.php @@ -0,0 +1,63 @@ +setName('setup_monitoring_backend'); + } + + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'monitoring', + 'Please configure below how Icinga Web 2 should retrieve monitoring information.' + ) + ) + ) + ); + + $this->addElement( + 'text', + 'name', + array( + 'required' => true, + 'label' => mt('monitoring', 'Backend Name'), + 'description' => mt('monitoring', 'The identifier of this backend') + ) + ); + + $resourceTypes = array('livestatus' => 'Livestatus'); + if ( + Platform::extensionLoaded('pdo') && ( + Platform::zendClassExists('Zend_Db_Adapter_Pdo_Mysql') + || Platform::zendClassExists('Zend_Db_Adapter_Pdo_Pgsql') + ) + ) { + $resourceTypes['ido'] = 'IDO'; + } + + $this->addElement( + 'select', + 'type', + array( + 'required' => true, + 'label' => mt('monitoring', 'Backend Type'), + 'description' => mt('monitoring', 'The data source used for retrieving monitoring information'), + 'multiOptions' => $resourceTypes + ) + ); + } +} diff --git a/modules/monitoring/application/forms/Setup/IdoResourcePage.php b/modules/monitoring/application/forms/Setup/IdoResourcePage.php new file mode 100644 index 000000000..353bbadcd --- /dev/null +++ b/modules/monitoring/application/forms/Setup/IdoResourcePage.php @@ -0,0 +1,89 @@ +setName('setup_monitoring_ido'); + } + + public function createElements(array $formData) + { + $this->addElement( + 'hidden', + 'type', + array( + 'required' => true, + 'value' => 'db' + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'monitoring', + 'Please fill out the connection details below to access' + . ' the IDO database of your monitoring environment.' + ) + ) + ) + ); + + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + $this->addSkipValidationCheckbox(); + } else { + $this->addElement( + 'hidden', + 'skip_validation', + array( + 'required' => true, + 'value' => 0 + ) + ); + } + + $livestatusResourceForm = new DbResourceForm(); + $this->addElements($livestatusResourceForm->createElements($formData)->getElements()); + } + + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) { + if (false === DbResourceForm::isValidResource($this)) { + $this->addSkipValidationCheckbox(); + return false; + } + } + + return true; + } + + /** + * Add a checkbox to the form by which the user can skip the connection validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'required' => true, + 'label' => t('Skip Validation'), + 'description' => t('Check this to not to validate connectivity with the given database server') + ) + ); + } +} diff --git a/modules/monitoring/application/forms/Setup/InstancePage.php b/modules/monitoring/application/forms/Setup/InstancePage.php new file mode 100644 index 000000000..4f41ddd9c --- /dev/null +++ b/modules/monitoring/application/forms/Setup/InstancePage.php @@ -0,0 +1,40 @@ +setName('setup_monitoring_instance'); + } + + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'monitoring', + 'Please define the settings specific to your monitoring instance below.' + ) + ) + ) + ); + + if (isset($formData['host'])) { + $formData['type'] = 'remote'; // This is necessary as the type element gets ignored by Form::getValues() + } + + $instanceConfigForm = new InstanceConfigForm(); + $instanceConfigForm->createElements($formData); + $this->addElements($instanceConfigForm->getElements()); + } +} diff --git a/modules/monitoring/application/forms/Setup/LivestatusResourcePage.php b/modules/monitoring/application/forms/Setup/LivestatusResourcePage.php new file mode 100644 index 000000000..4b52cb5c4 --- /dev/null +++ b/modules/monitoring/application/forms/Setup/LivestatusResourcePage.php @@ -0,0 +1,89 @@ +setName('setup_monitoring_livestatus'); + } + + public function createElements(array $formData) + { + $this->addElement( + 'hidden', + 'type', + array( + 'required' => true, + 'value' => 'livestatus' + ) + ); + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'monitoring', + 'Please fill out the connection details below to access the Livestatus' + . ' socket interface for your monitoring environment.' + ) + ) + ) + ); + + if (isset($formData['skip_validation']) && $formData['skip_validation']) { + $this->addSkipValidationCheckbox(); + } else { + $this->addElement( + 'hidden', + 'skip_validation', + array( + 'required' => true, + 'value' => 0 + ) + ); + } + + $livestatusResourceForm = new LivestatusResourceForm(); + $this->addElements($livestatusResourceForm->createElements($formData)->getElements()); + } + + public function isValid($data) + { + if (false === parent::isValid($data)) { + return false; + } + + if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) { + if (false === LivestatusResourceForm::isValidResource($this)) { + $this->addSkipValidationCheckbox(); + return false; + } + } + + return true; + } + + /** + * Add a checkbox to the form by which the user can skip the connection validation + */ + protected function addSkipValidationCheckbox() + { + $this->addElement( + 'checkbox', + 'skip_validation', + array( + 'required' => true, + 'label' => t('Skip Validation'), + 'description' => t('Check this to not to validate connectivity with the given Livestatus socket') + ) + ); + } +} diff --git a/modules/monitoring/application/forms/Setup/SecurityPage.php b/modules/monitoring/application/forms/Setup/SecurityPage.php new file mode 100644 index 000000000..ed639aefd --- /dev/null +++ b/modules/monitoring/application/forms/Setup/SecurityPage.php @@ -0,0 +1,36 @@ +setName('setup_monitoring_security'); + } + + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'monitoring', + 'To protect your monitoring environment against prying eyes please fill out the settings below.' + ) + ) + ) + ); + + $securityConfigForm = new SecurityConfigForm(); + $securityConfigForm->createElements($formData); + $this->addElements($securityConfigForm->getElements()); + } +} diff --git a/modules/monitoring/application/forms/Setup/WelcomePage.php b/modules/monitoring/application/forms/Setup/WelcomePage.php new file mode 100644 index 000000000..6a6e92983 --- /dev/null +++ b/modules/monitoring/application/forms/Setup/WelcomePage.php @@ -0,0 +1,65 @@ +setName('setup_monitoring_welcome'); + } + + public function createElements(array $formData) + { + $this->addElement( + new Note( + 'welcome', + array( + 'value' => mt('monitoring', 'Welcome to the installation of the monitoring module for Icinga Web 2!'), + 'decorators' => array( + 'ViewHelper', + array('HtmlTag', array('tag' => 'h2')) + ) + ) + ) + ); + + $this->addElement( + new Note( + 'core_hint', + array( + 'value' => mt('monitoring', 'This is the core module for Icinga Web 2.') + ) + ) + ); + + $this->addElement( + new Note( + 'description', + array( + 'value' => mt( + 'monitoring', + 'It offers various status and reporting views with powerful filter capabilities that allow' + . ' you to keep track of the most important events in your monitoring environment.' + ) + ) + ) + ); + + $this->addDisplayGroup( + array('core_hint', 'description'), + 'info', + array( + 'decorators' => array( + 'FormElements', + array('HtmlTag', array('tag' => 'div', 'class' => 'info')) + ) + ) + ); + } +} diff --git a/modules/monitoring/library/Monitoring/Installation/BackendStep.php b/modules/monitoring/library/Monitoring/Installation/BackendStep.php new file mode 100644 index 000000000..f16cadc23 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Installation/BackendStep.php @@ -0,0 +1,177 @@ +data = $data; + } + + public function apply() + { + $success = $this->createBackendsIni(); + $success &= $this->createResourcesIni(); + return $success; + } + + protected function createBackendsIni() + { + $config = array(); + $config[$this->data['backendConfig']['name']] = array( + 'type' => $this->data['backendConfig']['type'], + 'resource' => $this->data['resourceConfig']['name'] + ); + + try { + $writer = new PreservingIniWriter(array( + 'config' => new Zend_Config($config), + 'filename' => Config::resolvePath('modules/monitoring/backends.ini'), + 'filemode' => octdec($this->data['fileMode']) + )); + $writer->write(); + } catch (Exception $e) { + $this->backendIniError = $e; + return false; + } + + $this->backendIniError = false; + return true; + } + + protected function createResourcesIni() + { + $resourceConfig = $this->data['resourceConfig']; + $resourceName = $resourceConfig['name']; + unset($resourceConfig['name']); + + try { + $config = Config::app('resources', true); + $config->merge(new Zend_Config(array($resourceName => $resourceConfig))); + + $writer = new PreservingIniWriter(array( + 'config' => $config, + 'filename' => Config::resolvePath('resources.ini'), + 'filemode' => octdec($this->data['fileMode']) + )); + $writer->write(); + } catch (Exception $e) { + $this->resourcesIniError = $e; + return false; + } + + $this->resourcesIniError = false; + return true; + } + + public function getSummary() + { + $pageTitle = '

' . mt('monitoring', 'Monitoring Backend') . '

'; + $backendDescription = '

' . sprintf( + mt( + 'monitoring', + 'Icinga Web 2 will retrieve information from your monitoring environment' + . ' using a backend called "%s" and the specified resource below:' + ), + $this->data['backendConfig']['name'] + ) . '

'; + + if ($this->data['resourceConfig']['type'] === 'db') { + $resourceTitle = '

' . mt('monitoring', 'Database Resource') . '

'; + $resourceHtml = '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
' . t('Resource Name') . '' . $this->data['resourceConfig']['name'] . '
' . t('Database Type') . '' . $this->data['resourceConfig']['db'] . '
' . t('Host') . '' . $this->data['resourceConfig']['host'] . '
' . t('Port') . '' . $this->data['resourceConfig']['port'] . '
' . t('Database Name') . '' . $this->data['resourceConfig']['dbname'] . '
' . t('Username') . '' . $this->data['resourceConfig']['username'] . '
' . t('Password') . '' . str_repeat('*', strlen($this->data['resourceConfig']['password'])) . '
'; + } else { // $this->data['resourceConfig']['type'] === 'livestatus' + $resourceTitle = '

' . mt('monitoring', 'Livestatus Resource') . '

'; + $resourceHtml = '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
' . t('Resource Name') . '' . $this->data['resourceConfig']['name'] . '
' . t('Socket') . '' . $this->data['resourceConfig']['socket'] . '
'; + } + + return $pageTitle . '
' . $backendDescription . $resourceTitle . $resourceHtml . '
'; + } + + public function getReport() + { + $report = ''; + if ($this->backendIniError === false) { + $message = mt('monitoring', 'Monitoring backend configuration has been successfully written to: %s'); + $report .= '

' . sprintf($message, Config::resolvePath('modules/monitoring/backends.ini')) . '

'; + } elseif ($this->backendIniError !== null) { + $message = mt( + 'monitoring', + 'Monitoring backend configuration could not be written to: %s; An error occured:' + ); + $report .= '

' . sprintf( + $message, + Config::resolvePath('modules/monitoring/backends.ini') + ) . '

' . $this->backendIniError->getMessage() . '

'; + } + + if ($this->resourcesIniError === false) { + $message = mt('monitoring', 'Resource configuration has been successfully updated: %s'); + $report .= '

' . sprintf($message, Config::resolvePath('resources.ini')) . '

'; + } elseif ($this->resourcesIniError !== null) { + $message = mt('monitoring', 'Resource configuration could not be udpated: %s; An error occured:'); + $report .= '

' . sprintf($message, Config::resolvePath('resources.ini')) . '

' + . '

' . $this->resourcesIniError->getMessage() . '

'; + } + + return $report; + } +} diff --git a/modules/monitoring/library/Monitoring/Installation/InstanceStep.php b/modules/monitoring/library/Monitoring/Installation/InstanceStep.php new file mode 100644 index 000000000..a86fe060d --- /dev/null +++ b/modules/monitoring/library/Monitoring/Installation/InstanceStep.php @@ -0,0 +1,105 @@ +data = $data; + } + + public function apply() + { + $instanceConfig = $this->data['instanceConfig']; + $instanceName = $instanceConfig['name']; + unset($instanceConfig['name']); + + try { + $writer = new PreservingIniWriter(array( + 'config' => new Zend_Config(array($instanceName => $instanceConfig)), + 'filename' => Config::resolvePath('modules/monitoring/instances.ini'), + 'filemode' => octdec($this->data['fileMode']) + )); + $writer->write(); + } catch (Exception $e) { + $this->error = $e; + return false; + } + + $this->error = false; + return true; + } + + public function getSummary() + { + $pageTitle = '

' . mt('monitoring', 'Monitoring Instance') . '

'; + + if (isset($this->data['instanceConfig']['host'])) { + $pipeHtml = '

' . sprintf( + mt( + 'monitoring', + 'Icinga Web 2 will use the named pipe located on a remote machine at "%s" to send commands' + . ' to your monitoring instance by using the connection details listed below:' + ), + $this->data['instanceConfig']['path'] + ) . '

'; + + $pipeHtml .= '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
' . mt('monitoring', 'Remote Host') . '' . $this->data['instanceConfig']['host'] . '
' . mt('monitoring', 'Remote SSH Port') . '' . $this->data['instanceConfig']['port'] . '
' . mt('monitoring', 'Remote SSH User') . '' . $this->data['instanceConfig']['user'] . '
'; + } else { + $pipeHtml = '

' . sprintf( + mt( + 'monitoring', + 'Icinga Web 2 will use the named pipe located at "%s"' + . ' to send commands to your monitoring instance.' + ), + $this->data['instanceConfig']['path'] + ) . '

'; + } + + return $pageTitle . '
' . $pipeHtml . '
'; + } + + public function getReport() + { + if ($this->error === false) { + $message = mt('monitoring', 'Monitoring instance configuration has been successfully created: %s'); + return '

' . sprintf($message, Config::resolvePath('modules/monitoring/instances.ini')) . '

'; + } elseif ($this->error !== null) { + $message = mt( + 'monitoring', + 'Monitoring instance configuration could not be written to: %s; An error occured:' + ); + return '

' . sprintf($message, Config::resolvePath('modules/monitoring/instances.ini')) + . '

' . $this->error->getMessage() . '

'; + } + } +} diff --git a/modules/monitoring/library/Monitoring/Installation/SecurityStep.php b/modules/monitoring/library/Monitoring/Installation/SecurityStep.php new file mode 100644 index 000000000..d070e7dc6 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Installation/SecurityStep.php @@ -0,0 +1,81 @@ +data = $data; + } + + public function apply() + { + $config = array(); + $config['security'] = $this->data['securityConfig']; + + try { + $writer = new PreservingIniWriter(array( + 'config' => new Zend_Config($config), + 'filename' => Config::resolvePath('modules/monitoring/config.ini'), + 'filemode' => octdec($this->data['fileMode']) + )); + $writer->write(); + } catch (Exception $e) { + $this->error = $e; + return false; + } + + $this->error = false; + return true; + } + + public function getSummary() + { + $pageTitle = '

' . mt('monitoring', 'Monitoring Security') . '

'; + $pageDescription = '

' . mt( + 'monitoring', + 'Icinga Web 2 will protect your monitoring environment against' + . ' prying eyes using the configuration specified below:' + ) . '

'; + + $pageHtml = '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
' . mt('monitoring', 'Protected Custom Variables') . '' . $this->data['securityConfig']['protected_customvars'] . '
'; + + return $pageTitle . '
' . $pageDescription . $pageHtml . '
'; + } + + public function getReport() + { + if ($this->error === false) { + $message = mt('monitoring', 'Monitoring security configuration has been successfully created: %s'); + return '

' . sprintf($message, Config::resolvePath('modules/monitoring/config.ini')) . '

'; + } elseif ($this->error !== null) { + $message = mt( + 'monitoring', + 'Monitoring security configuration could not be written to: %s; An error occured:' + ); + return '

' . sprintf($message, Config::resolvePath('modules/monitoring/config.ini')) + . '

' . $this->error->getMessage() . '

'; + } + } +} \ No newline at end of file diff --git a/modules/monitoring/library/Monitoring/Setup.php b/modules/monitoring/library/Monitoring/Setup.php new file mode 100644 index 000000000..81c39d894 --- /dev/null +++ b/modules/monitoring/library/Monitoring/Setup.php @@ -0,0 +1,192 @@ +addPage(new WelcomePage()); + $this->addPage(new RequirementsPage()); + $this->addPage(new BackendPage()); + $this->addPage(new IdoResourcePage()); + $this->addPage(new LivestatusResourcePage()); + $this->addPage(new InstancePage()); + $this->addPage(new SecurityPage()); + $this->addPage(new SummaryPage()); + } + + /** + * @see Wizard::setupPage() + */ + public function setupPage(Form $page, Request $request) + { + if ($page->getName() === 'setup_requirements') { + $page->setRequirements($this->getRequirements()); + } elseif ($page->getName() === 'setup_summary') { + $page->setSummary($this->getInstaller()->getSummary()); + $page->setSubjectTitle(mt('monitoring', 'the monitoring module', 'setup.summary.subject')); + } elseif ( + $this->getDirection() === static::FORWARD + && ($page->getName() === 'setup_monitoring_ido' || $page->getName() === 'setup_monitoring_livestatus') + ) { + if ((($dbResourceData = $this->getPageData('setup_db_resource')) !== null + && $dbResourceData['name'] === $request->getPost('name')) + || (($ldapResourceData = $this->getPageData('setup_ldap_resource')) !== null + && $ldapResourceData['name'] === $request->getPost('name')) + ) { + $page->addError(mt('monitoring', 'The given resource name is already in use.')); + } + } + } + + /** + * @see Wizard::getNewPage() + */ + protected function getNewPage($requestedPage, Form $originPage) + { + $skip = false; + $newPage = parent::getNewPage($requestedPage, $originPage); + if ($newPage->getName() === 'setup_monitoring_ido') { + $backendData = $this->getPageData('setup_monitoring_backend'); + $skip = $backendData['type'] !== 'ido'; + } elseif ($newPage->getName() === 'setup_monitoring_livestatus') { + $backendData = $this->getPageData('setup_monitoring_backend'); + $skip = $backendData['type'] !== 'livestatus'; + } + + if ($skip) { + if ($this->hasPageData($newPage->getName())) { + $pageData = & $this->getPageData(); + unset($pageData[$newPage->getName()]); + } + + $pages = $this->getPages(); + if ($this->getDirection() === static::FORWARD) { + $nextPage = $pages[array_search($newPage, $pages, true) + 1]; + $newPage = $this->getNewPage($nextPage->getName(), $newPage); + } else { // $this->getDirection() === static::BACKWARD + $previousPage = $pages[array_search($newPage, $pages, true) - 1]; + $newPage = $this->getNewPage($previousPage->getName(), $newPage); + } + } + + return $newPage; + } + + /** + * @see Wizard::addButtons() + */ + protected function addButtons(Form $page) + { + parent::addButtons($page); + + $pages = $this->getPages(); + $index = array_search($page, $pages, true); + if ($index === 0) { + // Used t() here as "Start" is too generic and already translated in the icinga domain + $page->getElement(static::BTN_NEXT)->setLabel(t('Start', 'setup.welcome.btn.next')); + } elseif ($index === count($pages) - 1) { + $page->getElement(static::BTN_NEXT)->setLabel( + mt('monitoring', 'Install the monitoring module for Icinga Web 2', 'setup.summary.btn.finish') + ); + } + } + + /** + * @see SetupWizard::getInstaller() + */ + public function getInstaller() + { + $pageData = $this->getPageData(); + $installer = new Installer(); + + $installer->addStep( + new MakeDirStep( + array($this->getConfigDir() . '/modules/monitoring'), + $pageData['setup_general_config']['global_filemode'] + ) + ); + + $installer->addStep( + new BackendStep(array( + 'backendConfig' => $pageData['setup_monitoring_backend'], + 'resourceConfig' => isset($pageData['setup_monitoring_ido']) + ? array_diff_key($pageData['setup_monitoring_ido'], array('skip_validation' => null)) + : array_diff_key($pageData['setup_monitoring_livestatus'], array('skip_validation' => null)), + 'fileMode' => $pageData['setup_general_config']['global_filemode'] + )) + ); + + $installer->addStep( + new InstanceStep(array( + 'instanceConfig' => $pageData['setup_monitoring_instance'], + 'fileMode' => $pageData['setup_general_config']['global_filemode'] + )) + ); + + $installer->addStep( + new SecurityStep(array( + 'securityConfig' => $pageData['setup_monitoring_security'], + 'fileMode' => $pageData['setup_general_config']['global_filemode'] + )) + ); + + $installer->addStep(new EnableModuleStep('monitoring')); + + return $installer; + } + + /** + * @see SetupWizard::getRequirements() + */ + public function getRequirements() + { + return new Requirements(); + } + + /** + * Return the configuration directory of Icinga Web 2 + * + * @return string + */ + protected function getConfigDir() + { + if (array_key_exists('ICINGAWEB_CONFIGDIR', $_SERVER)) { + $configDir = $_SERVER['ICINGAWEB_CONFIGDIR']; + } else { + $configDir = '/etc/icingaweb'; + } + + $canonical = realpath($configDir); + return $canonical ? $canonical : $configDir; + } +} diff --git a/public/css/icinga/setup.less b/public/css/icinga/setup.less index 4e9837dfd..79baf142d 100644 --- a/public/css/icinga/setup.less +++ b/public/css/icinga/setup.less @@ -149,8 +149,7 @@ } #setup table.requirements { - margin-top: -1em; - margin-left: -1em; + margin: -1em -1em 0; border-spacing: 1em; border-collapse: separate; @@ -300,6 +299,7 @@ div.info { display: inline-block; + max-width: 66%; padding: 0 1em; border-radius: 1em; background-color: #eee; @@ -346,6 +346,16 @@ } } +#setup_monitoring_welcome { + .welcome-page; + margin-top: 0; + padding: 1em; + + h2 { + margin-top: 0; + } +} + #setup { div.module-wizard { width: auto;