diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php new file mode 100644 index 0000000..9db9841 --- /dev/null +++ b/application/controllers/IndexController.php @@ -0,0 +1,30 @@ +tabs()->add('welcome', array( + 'label' => $this->translate('Business Processes'), + 'url' => $this->getRequest()->getUrl() + ))->activate('welcome'); + + $configs = $this->storage()->listProcesses(); + + if (! empty($configs)) { + // Redirect to show the first process if there is any + $this->redirectNow( + 'businessprocess/process/show', + array('config' => key($configs)) + ); + } + + // Check back from time to time, maybe someone created a process + $this->setAutorefreshInterval(30); + } +} diff --git a/application/controllers/NodeController.php b/application/controllers/NodeController.php index eeca69d..a00944b 100644 --- a/application/controllers/NodeController.php +++ b/application/controllers/NodeController.php @@ -1,52 +1,91 @@ +process = + +*/ class Businessprocess_NodeController extends Controller { + // rename to config public function editAction() { - $bp = $this->loadBp(); + $bp = $this->loadModifiedBpConfig(); $node = $bp->getNode($this->getParam('node')); + $detail = Url::fromPath( + 'businessprocess/node/edit', + array( + 'config' => $this->view->configName, + 'node' => $node + ) + ); - $form = new ProcessForm(); - $form->setProcess($bp) - ->setSession($this->session()) - ->setNode($node) - ->handleRequest(); + $this->view->form = ProcessForm::construct() + ->setProcess($bp) + ->setSession($this->session()) + ->setNode($node) + ->setRedirectUrl( + sprintf( + 'businessprocess/process/show?config=%s&unlocked#!%s', + $bp->getName(), + $detail->getAbsoluteUrl() + ) + ) + ->handleRequest(); - $this->view->form = $form; $this->view->node = $node; } public function simulateAction() { - $bp = $this->loadBp(); + $bp = $this->loadBpConfig(); $nodename = $this->getParam('node'); $node = $bp->getNode($nodename); - $detail = Url::fromPath( + $details = Url::fromPath( 'businessprocess/node/simulate', - array('node' => $nodename) + array( + 'config' => $this->view->configName, + 'node' => $nodename + ) + ); + $url = sprintf( + 'businessprocess/process/show?unlocked&config=%s#!%s', + $bp->getName(), + $details->getAbsoluteUrl() ); - $form = new SimulationForm(); - $form->setProcess($bp) - ->setSession($this->session()) + $this->view->form = SimulationForm::construct() + ->setSimulation(new Simulation($bp, $this->session())) ->setNode($node) // TODO: find a better way to handle redirects - ->setRedirectUrl( - sprintf( - 'businessprocess/process/show?simulation=1&processName=%s#!%s', - $bp->getName(), - $detail->getAbsoluteUrl() - ) - ) + ->setRedirectUrl($url) ->handleRequest(); - - $this->view->form = $form; $this->view->node = $node; } + + public function addAction() + { + $bp = $this->loadBpConfig(); + + $redirectUrl = Url::fromPath( + 'businessprocess/process/show', + array('config' => $bp->getName()) + ); + + $this->view->form = ProcessForm::construct() + ->setProcess($bp) + ->setSession($this->session()) + ->setRedirectUrl($redirectUrl) + ->handleRequest(); + } + + public function deleteAction() + { + } } diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php index 2301dba..90e14ff 100644 --- a/application/controllers/ProcessController.php +++ b/application/controllers/ProcessController.php @@ -1,14 +1,144 @@ setTitle($this->translate('Create a new business process')); + $this->tabsForCreate()->activate('create'); + + $this->view->form = BpConfigForm::construct() + ->setStorage($this->storage()) + ->setRedirectUrl('businessprocess/process/show') + ->handleRequest(); + } + + /** + * Upload an existing business process configuration + */ + public function uploadAction() + { + $this->setTitle($this->translate('Upload a business process config file')); + $this->tabsForCreate()->activate('upload'); + } + + /** + * Show a business process tree + */ + public function showAction() + { + $this->redirectIfConfigChosen(); + + if ($this->params->get('unlocked')) { + $bp = $this->loadModifiedBpConfig(); + $bp->unlock(); + } else { + $bp = $this->loadBpConfig(); + } + + $this->setTitle('Business Process "%s"', $bp->getTitle()); + $this->tabsForShow()->activate('show'); + + // Do not lock empty configs + if ($bp->isEmpty() && ! $this->view->compact && $bp->isLocked()) { + $this->redirectNow($this->url()->with('unlocked', true)); + } + + if ($node = $this->params->get('node')) { + // Render a specific node + $this->view->nodeName = $node; + $this->view->bp = $bp->getNode($node); + } else { + // Render a single process + $this->view->bp = $bp; + if ($bp->hasWarnings()) { + $this->view->warnings = $bp->getWarnings(); + } + } + $this->setAutorefreshInterval(10); + + $bp->retrieveStatesFromBackend(); + + if ($bp->isLocked()) { + $this->tabs()->extend(new DashboardAction()); + } else { + $simulation = new Simulation($bp, $this->session()); + if ($this->params->get('dismissSimulations')) { + Notification::success( + sprintf( + $this->translate('%d applied simulation(s) have been dropped'), + $simulation->count() + ) + ); + $simulation->clear(); + $this->redirectNow($this->url()->without('dismissSimulations')->without('unlocked')); + } + + $bp->applySimulation($simulation); + } + + if ($this->params->get('mode') === 'toplevel') { + $this->render('toplevel'); + } + } + + /** + * Show a business process from a toplevel perspective + */ + public function toplevelAction() + { + $this->redirectIfConfigChosen(); + } + + /** + * Show the source code for a process + */ + public function sourceAction() + { + $this->tabsForConfig()->activate('source'); + $bp = $this->loadModifiedBpConfig(); + + $this->view->source = $bp->toLegacyConfigString(); + $this->view->showDiff = (bool) $this->params->get('showDiff', false); + + if ($this->view->showDiff) { + $this->view->diff = ConfigDiff::create( + $this->storage()->getSource($this->view->configName), + $this->view->source + ); + $this->view->title = sprintf( + $this->translate('%s: Source Code Differences'), + $bp->getTitle() + ); + } else { + $this->view->title = sprintf( + $this->translate('%s: Source Code'), + $bp->getTitle() + ); + } + } + + /** + * Download a process configuration file + */ public function downloadAction() { - $bp = $this->loadBp(); + $bp = $this->loadModifiedBpConfig(); + header( sprintf( 'Content-Disposition: attachment; filename="%s.conf";', @@ -20,73 +150,83 @@ class Businessprocess_ProcessController extends Controller echo $bp->toLegacyConfigString(); // Didn't have time to lookup how to correctly disable our renderers // TODO: no exit :) - exit; + $this->doNotRender(); } - public function showAction() + /** + * Modify a business process configuration + */ + public function configAction() { - if ($this->getRequest()->isPost()) { - $this->redirectNow($this->getRequest()->getUrl()->with('processName', $this->getRequest()->getPost('processName'))); - } - $this->view->compact = $this->params->get('view') === 'compact'; - $storage = new LegacyStorage($this->Config()->getSection('global')); - $this->view->processList = $storage->listProcesses(); - $process = $this->params->get('processName', key($this->view->processList)); - $this->view->processName = $process; + $this->tabsForConfig()->activate('config'); + $bp = $this->loadModifiedBpConfig(); - $this->view->tabs = $this->tabs()->activate('show'); - $this->view->title = 'Business Processes'; + $this->setTitle( + $this->translate('%s: Configuration'), + $bp->getTitle() + ); - $bp = $this->loadBp(); - try { - $bp->retrieveStatesFromBackend(); - } catch (Exception $e) { - $this->view->errors = array( - 'Could not retrieve process state: ' . $e->getMessage() + $url = sprintf( + 'businessprocess/process/show?config=%s&unlocked#!%s', + $bp->getName(), + $this->getRequest()->getUrl() + ); + $this->view->form = BpConfigForm::construct() + ->setProcessConfig($bp) + ->setSession($this->session()) + ->setRedirectUrl($url) + ->handleRequest(); + + $this->view->deleteForm = DeleteConfigForm::construct() + ->setStorage($this->storage()) + ->setController($this) + ->setBpConfig($bp) + ->handleRequest(); + } + + /** + * Redirect to our URL plus the chosen config if someone switched the + * config in the appropriate dropdown list + */ + protected function redirectIfConfigChosen() + { + $request = $this->getRequest(); + if ($request->isPost()) { + // We switched the process in the config dropdown list + $params = array( + 'config' => $request->getPost('config') ); + $this->redirectNow($this->url()->with($params)); } - if ($process = $this->params->get('process')) { - $this->view->bp = $bp->getNode($process); - } else { - $this->view->bp = $bp; - if ($bp->hasWarnings()) { - $this->view->warnings = $bp->getWarnings(); - } - } - $this->setAutorefreshInterval(10); - - if ($this->params->get('showSource')) { - $this->view->source = $bp->toLegacyConfigString(); - $this->render('source'); - } - if ($this->params->get('simulation')) { - $bp->setSimulationMode(); - $this->addSimulation($bp); - } - - if ($this->params->get('edit')) { - $bp->setEditMode(); - } - - if ($this->params->get('store')) { - $storage->storeProcess($bp); - $this->redirectNow($this->getRequest()->getUrl()->without('store')); - } - - if ($this->params->get('mode') === 'toplevel') { - $this->render('toplevel'); - } - } - protected function addSimulation($bp) + protected function tabsForShow() { - $simulations = $this->session()->get('simulations', array()); - foreach ($simulations as $node => $s) { - $bp->getNode($node) - ->setState($s->state) - ->setAck($s->acknowledged) - ->setDowntime($s->in_downtime); - } + return $this->tabs()->add('show', array( + 'label' => $this->translate('Business Process'), + 'url' => $this->url() + )); + } + + protected function tabsForCreate() + { + return $this->tabs()->add('create', array( + 'label' => $this->translate('Create'), + 'url' => 'businessprocess/process/create' + ))->add('upload', array( + 'label' => $this->translate('Upload'), + 'url' => 'businessprocess/process/upload' + )); + } + + protected function tabsForConfig() + { + return $this->tabs()->add('config', array( + 'label' => $this->translate('Process Configuration'), + 'url' => $this->getRequest()->getUrl()->without('nix')->setPath('businessprocess/process/config') + ))->add('source', array( + 'label' => $this->translate('Source'), + 'url' => $this->getRequest()->getUrl()->without('nix')->setPath('businessprocess/process/source') + )); } } diff --git a/application/forms/BpConfigForm.php b/application/forms/BpConfigForm.php new file mode 100644 index 0000000..c89d232 --- /dev/null +++ b/application/forms/BpConfigForm.php @@ -0,0 +1,164 @@ +addElement('text', 'name', array( + 'label' => $this->translate('Name'), + 'required' => true, + 'description' => $this->translate( + 'This is the unique identifier of this process' + ), + )); + + $this->addElement('text', 'title', array( + 'label' => $this->translate('Title'), + 'description' => $this->translate( + 'Usually this title will be shown for this process. Equals name' + . ' if not given' + ), + )); + + $this->addElement('select', 'backend_name', array( + 'label' => $this->translate('Backend'), + 'description' => $this->translate( + 'Icinga Web Monitoring Backend where current object states for' + . ' this process should be retrieved from' + ), + 'multiOptions' => array( + null => $this->translate('Use the configured default backend'), + ) + $this->listAvailableBackends() + )); + + $this->addElement('select', 'state_type', array( + 'label' => $this->translate('State Type'), + 'required' => true, + 'description' => $this->translate( + 'Whether this process should be based on Icinga hard or soft states' + ), + 'multiOptions' => array( + 'hard' => $this->translate('Use HARD states'), + 'soft' => $this->translate('Use SOFT states'), + ) + )); + $this->addElement('submit', $this->translate('Store')); + } + + protected function listAvailableBackends() + { + $keys = array_keys(Config::module('monitoring', 'backends')->toArray()); + return array_combine($keys, $keys); + } + + public function XXsetBackend($backend) + { + $this->backend = $backend; + return $this; + } + + public function setStorage($storage) + { + $this->storage = $storage; + return $this; + } + + public function setProcessConfig($config) + { + $this->config = $config; + $this->getElement('name')->setValue($config->getName()); + + if ($config->hasTitle()) { + $this->getElement('title')->setValue($config->getTitle()); + } + + if ($config->hasBackend()) { + $this->getElement('backend_name')->setValue( + $config->getBackend()->getName() + ); + } + + if ($config->usesSoftStates()) { + $this->getElement('state_type')->setValue('soft'); + } else { + $this->getElement('state_type')->setValue('hard'); + } + + return $this; + } + + public function setSession($session) + { + $this->session = $session; + return $this; + } + + public function onSuccess() + { + $name = $this->getValue('name'); + $title = $this->getValue('title'); + $backend = $this->getValue('backend'); + + if ($this->config === null) { + // New config + $config = new BusinessProcess(); + $config->setName($name); + if ($title) { + $config->setTitle($title); + } + if ($backend) { + $config->setBackendName($backend); + } + if ($this->getValue('state_type') === 'soft') { + $config->useSoftStates(); + } else { + $config->useHardStates(); + } + $this->storage->storeProcess($config); + $this->setRedirectUrl( + Url::fromPath( + $this->getRedirectUrl(), + array('config' => $name, 'unlocked' => true) + ) + ); + + Notification::success(sprintf('Process %s has been created', $name)); + } else { + Notification::success(sprintf('Process %s has NOT YET been modified', $name)); + } +/* + $storage->storeProcess($bp); + $modifications = $this->session->get('modifications', array()); + $node = $this->config->getNode($this->getValue('name')); + $node->setChildNames($this->getValue('children')); + $node->setOperator($this->getValue('operator')); + $modifications[$this->config->getName()] = $this->config->toLegacyConfigString(); + $this->session->set('modifications', $modifications); + $message = 'Process %s has been modified'; + Notification::success(sprintf($message, $this->config->getName())); +*/ + } +} diff --git a/application/forms/DeleteConfigForm.php b/application/forms/DeleteConfigForm.php new file mode 100644 index 0000000..02f5dc4 --- /dev/null +++ b/application/forms/DeleteConfigForm.php @@ -0,0 +1,46 @@ +addHidden('name'); + $this->addElement('submit', $this->translate('Delete this process')); + } + + public function setStorage($storage) + { + $this->storage = $storage; + return $this; + } + + public function setController($controller) + { + $this->controller = $controller; + return $this; + } + + public function setBpConfig($bp) + { + $this->getElement('name')->setValue($bp->getName()); + return $this; + } + + public function onSuccess() + { + $name = $this->getValue('name'); + $this->storage->deleteProcess($name); + $this->setRedirectUrl('businessprocess'); + Notification::success(sprintf('Process %s has been deleted', $name)); + } +} diff --git a/application/forms/ProcessForm.php b/application/forms/ProcessForm.php index c6c1bf7..e533d95 100644 --- a/application/forms/ProcessForm.php +++ b/application/forms/ProcessForm.php @@ -2,10 +2,12 @@ namespace Icinga\Module\Businessprocess\Forms; -use Icinga\Web\Form; use Icinga\Web\Notification; use Icinga\Web\Request; +use Icinga\Module\Businessprocess\Node; use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\Form; +use Icinga\Module\Businessprocess\ProcessChanges; class ProcessForm extends Form { @@ -21,17 +23,22 @@ class ProcessForm extends Form protected $session; - public function __construct($options = null) - { - parent::__construct($options); - $this->setup(); - } - public function setup() { $this->addElement('text', 'name', array( - 'label' => $this->translate('Process name'), + 'label' => $this->translate('Name'), 'required' => true, + 'description' => $this->translate( + 'This is the unique identifier of this process' + ), + )); + + $this->addElement('text', 'alias', array( + 'label' => $this->translate('Title'), + 'description' => $this->translate( + 'Usually this title will be shown for this node. Equals name' + . ' if not given' + ), )); $this->addElement('select', 'operator', array( @@ -40,7 +47,27 @@ class ProcessForm extends Form 'multiOptions' => array( '&' => $this->translate('AND'), '|' => $this->translate('OR'), - 'min' => $this->translate('min') + '1' => $this->translate('MIN 1'), + '2' => $this->translate('MIN 2'), + '3' => $this->translate('MIN 3'), + '4' => $this->translate('MIN 4'), + '5' => $this->translate('MIN 5'), + '6' => $this->translate('MIN 6'), + '7' => $this->translate('MIN 7'), + '8' => $this->translate('MIN 8'), + '9' => $this->translate('MIN 9'), + ) + )); + + $this->addElement('select', 'display', array( + 'label' => $this->translate('Visualization'), + 'required' => true, + 'description' => $this->translate( + 'Where to show this process' + ), + 'multiOptions' => array( + '0' => $this->translate('Subprocess only'), + '1' => $this->translate('Toplevel Process'), ) )); @@ -49,8 +76,19 @@ class ProcessForm extends Form 'required' => true, 'size' => 14, 'style' => 'width: 25em;', - 'description' => $this->translate('Hosts, services or other processes that should be part of this business process') + 'description' => $this->translate( + 'Hosts, services or other processes that should be part of this' + . ' business process' + ) )); + + $this->addElement('text', 'url', array( + 'label' => $this->translate('Info URL'), + 'description' => $this->translate( + 'URL pointing to more information about this node' + ) + )); + $this->addElement('submit', $this->translate('Store')); } @@ -64,12 +102,15 @@ class ProcessForm extends Form protected function fillAvailableChildren() { - $this->getElement('children')->setMultiOptions( - array( + if (empty($this->processList)) { + $children = $this->objectList; + } else { + $children = array( $this->translate('Other Business Processes') => $this->processList - ) + $this->objectList - ); + ) + $this->objectList; + } + $this->getElement('children')->setMultiOptions($children); } public function setProcess($process) @@ -88,14 +129,19 @@ class ProcessForm extends Form return $this; } - public function setNode($node) + public function setNode(Node $node) { $this->node = $node; $this->setDefaults(array( 'name' => (string) $node, + 'alias' => $node->hasAlias() ? $node->getAlias() : '', + 'display' => $node->getDisplay(), + 'operator' => $node->getOperator(), + 'url' => $node->getInfoUrl(), 'children' => array_keys($node->getChildren()) )); + $this->getElement('name')->setAttrib('readonly', true); return $this; } @@ -140,13 +186,63 @@ class ProcessForm extends Form public function onSuccess() { - $modifications = $this->session->get('modifications', array()); - $node = $this->process->getNode($this->getValue('name')); - $node->setChildNames($this->getValue('children')); - $node->setOperator($this->getValue('operator')); - $modifications[$this->process->getName()] = $this->process->toLegacyConfigString(); - $this->session->set('modifications', $modifications); - $message = 'Process %s has been modified'; - Notification::success(sprintf($message, $this->process->getName())); + $changes = ProcessChanges::construct($this->process, $this->session); + + $modifications = array(); + $children = $this->getValue('children'); + $alias = $this->getValue('alias'); + $operator = $this->getValue('operator'); + $display = $this->getValue('display'); + $url = $this->getValue('url'); + if (empty($url)) { + $url = null; + } + if (empty($alias)) { + $alias = null; + } + ksort($children); + // TODO: rename + + if ($node = $this->node) { + + if ($display !== $node->getDisplay()) { + $modifications['display'] = $display; + } + if ($operator !== $node->getOperator()) { + $modifications['operator'] = $operator; + } + if ($children !== $node->getChildNames()) { + $modifications['childNames'] = $children; + } + if ($url !== $node->getInfoUrl()) { + $modifications['infoUrl'] = $url; + } + if ($alias !== $node->getAlias()) { + $modifications['alias'] = $alias; + } + } else { + $modifications = array( + 'display' => $display, + 'operator' => $operator, + 'childNames' => $children, + 'infoUrl' => $url, + 'alias' => $alias, + ); + } + if (! empty($modifications)) { + + if ($this->node === null) { + $changes->createNode($this->getValue('name'), $modifications); + } else { + $changes->modifyNode($this->node, $modifications); + } + + Notification::success( + sprintf( + 'Process %s has been modified', + $this->process->getName() + ) + ); + } } } diff --git a/application/forms/SimulationForm.php b/application/forms/SimulationForm.php index 0e140bd..ff49aa6 100644 --- a/application/forms/SimulationForm.php +++ b/application/forms/SimulationForm.php @@ -2,20 +2,17 @@ namespace Icinga\Module\Businessprocess\Forms; -use Icinga\Web\Form; -use Icinga\Web\Request; -use Icinga\Web\Notification; use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\Form; +use Icinga\Module\Businessprocess\Simulation; +use Icinga\Web\Notification; +use Icinga\Web\Request; class SimulationForm extends Form { - protected $backend; - - protected $process; - protected $node; - protected $session; + protected $simulation; public function __construct($options = null) { @@ -33,6 +30,7 @@ class SimulationForm extends Form '1' => $this->translate('WARNING'), '2' => $this->translate('CRITICAL'), '3' => $this->translate('UNKNOWN'), + '99' => $this->translate('PENDING'), ) )); @@ -51,19 +49,6 @@ class SimulationForm extends Form $this->addElement('submit', $this->translate('Apply')); } - public function setBackend($backend) - { - $this->backend = $backend; - return $this; - } - - public function setProcess($process) - { - $this->process = $process; - $this->setBackend($process->getBackend()); - return $this; - } - public function setNode($node) { $this->node = $node; @@ -72,53 +57,41 @@ class SimulationForm extends Form 'acknowledged' => $node->isAcknowledged(), 'in_downtime' => $node->isInDowntime(), )); - return $this->checkNodeSession(); + return $this->checkDefaults(); } - public function setSession($session) + public function setSimulation($simulation) { - $this->session = $session; - return $this->checkNodeSession(); + $this->simulation = $simulation; + return $this->checkDefaults(); } - protected function checkNodeSession() + protected function checkDefaults() { - if ($this->node === null || $this->session === null) { - return $this; + if ($this->node !== null + && $this->simulation !== null + && $this->simulation->hasNode((string) $this->node) + ) { + $this->setDefaults((array) $this->simulation->getNode((string) $this->node)); } - - $simulations = $this->session->get('simulations', array()); - $node = (string) $this->node; - if (array_key_exists($node, $simulations)) { - $this->setDefaults(array( - 'simulate' => true, - 'state' => $simulations[$node]->state, - 'in_downtime' => $simulations[$node]->in_downtime, - 'acknowledged' => $simulations[$node]->acknowledged, - )); - } - return $this; } public function onSuccess() { $node = (string) $this->node; - $simulations = $this->session->get('simulations', array()); if ($this->getValue('state') === '') { - if (array_key_exists($node, $simulations)) { + if ($this->simulation->remove($node)) { Notification::success($this->translate('Simulation has been removed')); - unset($simulations[$node]); - $this->session->set('simulations', $simulations); } } else { - $simulations[$node] = (object) array( + Notification::success($this->translate('Simulation has been set')); + $this->simulation->set($node, (object) array( 'state' => $this->getValue('state'), 'acknowledged' => $this->getValue('acknowledged'), - 'in_downtime' => $this->getValue('in_downtime'), - ); - $this->session->set('simulations', $simulations); + 'in_downtime' => $this->getValue('in_downtime'), + )); } } } diff --git a/application/forms/_CreateConfigForm.php b/application/forms/_CreateConfigForm.php new file mode 100644 index 0000000..4182833 --- /dev/null +++ b/application/forms/_CreateConfigForm.php @@ -0,0 +1,101 @@ +addElement('text', 'name', array( + 'label' => $this->translate('Name'), + 'required' => true, + 'description' => $this->translate('This is the unique identifier of this process'), + )); + + $this->addElement('text', 'title', array( + 'label' => $this->translate('Title'), + 'description' => $this->translate('Usually this title will be shown for this process. Equals name if not given'), + )); + + $this->addElement('select', 'backend_name', array( + 'label' => $this->translate('Backend'), + 'required' => true, + 'description' => $this->translate('Icinga Web Monitoring Backend where current object states for this process should be retrieved from'), + 'multiOptions' => array( + null => $this->translate('Use current default backend'), + ) + $this->listAvailableBackends() + )); + + $this->addElement('select', 'state_type', array( + 'label' => $this->translate('State Type'), + 'required' => true, + 'description' => $this->translate('Whether this process should be based on Icinga hard or soft states'), + 'multiOptions' => array( + 'hard' => $this->translate('Use HARD states'), + 'soft' => $this->translate('Use SOFT states'), + ) + )); + $this->addElement('submit', $this->translate('Store')); + } + + protected function listAvailableBackends() + { + $keys = array_keys(Config::module('monitoring', 'backends')->toArray()); + return array_combine($keys, $keys); + } + + public function setBackend($backend) + { + $this->backend = $backend; + return $this; + } + + public function setProcessConfig($config) + { + $this->process = $config; + $this->getElement('name')->setValue($config->getName()); + + if ($config->hasTitle()) { + $this->getElement('title')->setValue($config->getTitle()); + } + + if ($config->hasBackend()) { + $this->getElement('backend_name')->setValue($config->getBackend()->getName()); + } + + if ($config->usesSoftStates()) { + $this->getElement('state_type')->setValue('soft'); + } else { + $this->getElement('state_type')->setValue('hard'); + } + + return $this; + } + + public function setSession($session) + { + $this->session = $session; + return $this; + } + + public function onSuccess() + { +/* + $storage->storeProcess($bp); + $modifications = $this->session->get('modifications', array()); + $node = $this->process->getNode($this->getValue('name')); + $node->setChildNames($this->getValue('children')); + $node->setOperator($this->getValue('operator')); + $modifications[$this->process->getName()] = $this->process->toLegacyConfigString(); + $this->session->set('modifications', $modifications); + $message = 'Process %s has been modified'; + Notification::success(sprintf($message, $this->process->getName())); +*/ + } +} diff --git a/application/locale/de_DE/LC_MESSAGES/businessprocess.mo b/application/locale/de_DE/LC_MESSAGES/businessprocess.mo index 6748469..9dde630 100644 Binary files a/application/locale/de_DE/LC_MESSAGES/businessprocess.mo and b/application/locale/de_DE/LC_MESSAGES/businessprocess.mo differ diff --git a/application/locale/de_DE/LC_MESSAGES/businessprocess.po b/application/locale/de_DE/LC_MESSAGES/businessprocess.po index 0c15354..2f2c880 100644 --- a/application/locale/de_DE/LC_MESSAGES/businessprocess.po +++ b/application/locale/de_DE/LC_MESSAGES/businessprocess.po @@ -2,42 +2,125 @@ # Copyright (C) 2015 Icinga Development Team # This file is distributed under the same license as Businessprocess Module. # FIRST AUTHOR , YEAR. -# -#, fuzzy +# msgid "" msgstr "" "Project-Id-Version: Businessprocess Module (2.0.0-beta1)\n" "Report-Msgid-Bugs-To: dev@icinga.org\n" -"POT-Creation-Date: 2015-03-03 13:25+0100\n" -"PO-Revision-Date: 2015-03-03 13:25+0100\n" +"POT-Creation-Date: 2015-03-16 06:19+0100\n" +"PO-Revision-Date: 2015-03-16 09:04+0100\n" "Last-Translator: Thomas Gelf \n" "Language: de_DE\n" +"Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.5.4\n" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:41 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:83 +#, php-format +msgid "%d applied simulation(s) have been dropped" +msgstr "%d angewendete Simulation(en) wurde(n) entfernt" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Controller.php:73 +#, php-format +msgid "%d pending change(s) have been dropped" +msgstr "%d vorgenommene Änderung(en) wurde(n) verworfen" + +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:165 +#, php-format +msgid "%s: Configuration" +msgstr "%s: Konfiguration" + +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:129 +#, php-format +msgid "%s: Source Code" +msgstr "%s: Quellcode" + +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:124 +#, php-format +msgid "%s: Source Code Differences" +msgstr "%s: Quellcode Unterschiede" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:48 msgid "AND" msgstr "UND" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:44 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:42 msgid "Acknowledged" msgstr "Bestätigt" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:51 +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/node/add.phtml:8 +msgid "Add new process node" +msgstr "Neuen Knoten hinzufügen" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:49 msgid "Apply" msgstr "Anwenden" +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/index/index.phtml:8 +#, php-format +msgid "" +"As no business process has been defined yet you might want to create a %s or " +"upload an %s." +msgstr "" +"Nachdem noch kein Business-Prozess definiert wurde, möchtest du vermutlich " +"einen %s erstellen oder einen %s hochladen." + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:47 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:27 +msgid "Backend" +msgstr "Backend" + +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:206 +msgid "Business Process" +msgstr "Business-Prozess" + #: /usr/local/icingaweb-modules/businessprocess/configuration.php:4 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/IndexController.php:13 msgid "Business Processes" msgstr "Business-Prozesse" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:34 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:31 msgid "CRITICAL" msgstr "KRITISCH" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:52 +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/index/index.phtml:6 +msgid "Configure your first business process" +msgstr "Konfiguriere deinen ersten Business-Prozess" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BusinessProcess.php:297 +#, php-format +msgid "Could not retrieve process state: %s" +msgstr "Konnte den Prozess-Status nicht ermitteln: %s" + +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:214 +msgid "Create" +msgstr "Erstelle" + +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:21 +msgid "Create a new business process" +msgstr "Neuen Business-Prozess erstellen" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/DeleteConfigForm.php:18 +msgid "Delete this process" +msgstr "Lösche diesen Prozess" + +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:40 +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:51 +msgid "Dismiss" +msgstr "Verwerfen" + +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/source.phtml:6 +msgid "Download process configuration" +msgstr "Prozess-Konfiguration herunterladen" + +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/source.phtml:10 +msgid "Highlight changes" +msgstr "Änderungen hervorheben" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:80 msgid "" "Hosts, services or other processes that should be part of this business " "process" @@ -45,71 +128,261 @@ msgstr "" "Hosts, Services oder andere Prozesse welche Teil dieses Business-Prozesses " "sein sollen" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:48 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:49 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:29 +msgid "" +"Icinga Web Monitoring Backend where current object states for this process " +"should be retrieved from" +msgstr "" +"Das Icinga Web Monitoring Backend von welchem die Status-Informationen für " +"diesen Prozess bezogen werden sollen" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:46 msgid "In downtime" msgstr "In Downtime" -#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Node.php:304 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:86 +msgid "Info URL" +msgstr "Info-URL" + +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:13 +msgid "Lock this process" +msgstr "Sperre diesen Prozess" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:50 +msgid "MIN 1" +msgstr "MIN 1" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:51 +msgid "MIN 2" +msgstr "MIN 2" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:52 +msgid "MIN 3" +msgstr "MIN 3" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:53 +msgid "MIN 4" +msgstr "MIN 4" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:54 +msgid "MIN 5" +msgstr "MIN 5" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:55 +msgid "MIN 6" +msgstr "MIN 6" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:56 +msgid "MIN 7" +msgstr "MIN 7" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:57 +msgid "MIN 8" +msgstr "MIN 8" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:58 +msgid "MIN 9" +msgstr "MIN 9" + +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/node/edit.phtml:10 +#, php-format +msgid "Modify process node: %s" +msgstr "Bearbeite Knoten: %s" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BpNode.php:268 +msgid "Modify this node" +msgstr "Bearbeite diesen Knoten" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Node.php:350 msgid "More information" msgstr "Weitere Informationen" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:32 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:31 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:16 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:29 +msgid "Name" +msgstr "Name" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BusinessProcess.php:519 +#, php-format +msgid "No business process nodes for \"%s\" have been defined yet" +msgstr "Es wurden noch keine Business-Prozess Knoten für \"%s\" definiert" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BusinessProcess.php:467 +#, php-format +msgid "Node \"%s\" has been defined twice" +msgstr "Der Knoden \"%s\" wurde doppelt definiert" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:29 msgid "OK" msgstr "OK" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:42 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:49 msgid "OR" msgstr "ODER" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:38 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:45 msgid "Operator" msgstr "Operator" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:69 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:109 msgid "Other Business Processes" msgstr "Andere Business-Prozesse" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:40 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:33 +msgid "PENDING" +msgstr "PENDING" + +#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/BusinessProcess.php:538 +#, php-format +msgid "Parser waring on %s:%s: %s" +msgstr "Parse-Warnung an %s:%s: %s" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:38 msgid "Plugin output" msgstr "Plugin-Ausgabe" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:48 +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:225 +msgid "Process Configuration" +msgstr "Prozess-Konfiguration" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:75 msgid "Process components" msgstr "Prozess-Komponenten" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:33 -msgid "Process name" -msgstr "Prozess-Name" - #: /usr/local/icingaweb-modules/businessprocess/configuration.php:3 msgid "Reporting" msgstr "Reporting" -#: /usr/local/icingaweb-modules/businessprocess/library/Businessprocess/Controller.php:40 -msgid "Show" -msgstr "Zeige" +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/source.phtml:8 +msgid "Show source code" +msgstr "Quellcode anzeigen" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:111 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:86 msgid "Simulation has been removed" msgstr "Simulation wurde entfernt" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:29 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:89 +msgid "Simulation has been set" +msgstr "Simulation wurde gesended" + +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:228 +msgid "Source" +msgstr "Quelle" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:26 msgid "State" msgstr "Status" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:54 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:58 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:36 +msgid "State Type" +msgstr "Statustyp" + +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:36 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:68 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:44 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:92 msgid "Store" msgstr "Speichern" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:35 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:69 +msgid "Subprocess only" +msgstr "Nur Sub-Prozess" + +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/upload.phtml:8 +msgid "This has not been implemented yet" +msgstr "Das wurde noch nicht implementiert" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:34 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:18 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:32 +msgid "This is the unique identifier of this process" +msgstr "Das ist der eindeutige Identifier dieses Prozesses" + +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:33 +#, php-format +msgid "This process has %d pending change(s)." +msgstr "Dieser Prozess hat %d ungespeicherte Änderung(en)" + +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:48 +#, php-format +msgid "This process shows %d simulated state(s)." +msgstr "Dieser Prozess zeigt %d simulierte Zustände" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:39 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:22 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:37 +msgid "Title" +msgstr "Titel" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:70 +msgid "Toplevel Process" +msgstr "Toplevel-Prozess" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:32 msgid "UNKNOWN" msgstr "UNBEKANNT" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:31 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:88 +msgid "URL pointing to more information about this node" +msgstr "URL zu mehr Informationen über diesen Knoten" + +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/process/show.phtml:10 +msgid "Unlock this process" +msgstr "Entsperre diesen Prozess" + +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:217 +msgid "Upload" +msgstr "Upload" + +#: /usr/local/icingaweb-modules/businessprocess/application/controllers/ProcessController.php:35 +msgid "Upload a business process config file" +msgstr "Lade eine Business-Prozess Konfigurationsdatei hoch" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:64 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:40 +msgid "Use HARD states" +msgstr "HARD-States benutzen" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:65 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:41 +msgid "Use SOFT states" +msgstr "SOFT-States benutzen" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:31 +msgid "Use current default backend" +msgstr "Aktuelles Standard-Backend benutzen" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:28 msgid "Use current state" msgstr "Aktuellen Status benutzen" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:33 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:53 +msgid "Use the configured default backend" +msgstr "Benutze das konfigurierte Standard-Backend" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:39 +msgid "" +"Usually this title will be shown for this node. Equals name if not given" +msgstr "" +"Für gewöhnlich wird dieser Titel für diesen Knoten angezeigt. Entspricht dem " +"Namen, wenn nicht angegeben" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:41 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:23 +msgid "" +"Usually this title will be shown for this process. Equals name if not given" +msgstr "" +"Für gewöhnlich wird dieser Titel für diesen Prozess angezeigt. Entspricht " +"dem Namen, wenn nicht angegeben" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:63 +msgid "Visualization" +msgstr "Darstellung" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/SimulationForm.php:30 msgid "WARNING" msgstr "WARNUNG" @@ -117,6 +390,25 @@ msgstr "WARNUNG" msgid "Warnings" msgstr "Warnungen" -#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:43 -msgid "min" -msgstr "min" +#: /usr/local/icingaweb-modules/businessprocess/application/forms/ProcessForm.php:66 +msgid "Where to show this process" +msgstr "Wo soll dieser Prozess angezeigt werden" + +#: /usr/local/icingaweb-modules/businessprocess/application/forms/BpConfigForm.php:61 +#: /usr/local/icingaweb-modules/businessprocess/application/forms/_CreateConfigForm.php:38 +msgid "Whether this process should be based on Icinga hard or soft states" +msgstr "Ob dieser Prozess auf Icinga's hard- oder soft-states basieren soll" + +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/index/index.phtml:10 +msgid "existing one" +msgstr "einen existierenden" + +#: /usr/local/icingaweb-modules/businessprocess/application/views/scripts/index/index.phtml:9 +msgid "new business process" +msgstr "neuen Business-Prozess" + +#~ msgid "Show" +#~ msgstr "Zeige" + +#~ msgid "min" +#~ msgstr "min" diff --git a/application/views/scripts/downloadlink.phtml b/application/views/scripts/downloadlink.phtml deleted file mode 100644 index 4575343..0000000 --- a/application/views/scripts/downloadlink.phtml +++ /dev/null @@ -1,4 +0,0 @@ - icon('download') ?> - diff --git a/application/views/scripts/index/index.phtml b/application/views/scripts/index/index.phtml new file mode 100644 index 0000000..4f090f3 --- /dev/null +++ b/application/views/scripts/index/index.phtml @@ -0,0 +1,12 @@ +
+tabs ?> +
+ +
+

translate('Configure your first business process') ?>

+

translate('As no business process has been defined yet you might want to create a %s or upload an %s.'), + $this->qlink($this->translate('new business process'), 'businessprocess/process/create'), + $this->qlink($this->translate('existing one'), 'businessprocess/process/upload') +) ?>

+
diff --git a/application/views/scripts/node/add.phtml b/application/views/scripts/node/add.phtml new file mode 100644 index 0000000..a765433 --- /dev/null +++ b/application/views/scripts/node/add.phtml @@ -0,0 +1,10 @@ +
+

translate('Add new process node') ?>

+form ?> +
diff --git a/application/views/scripts/node/edit.phtml b/application/views/scripts/node/edit.phtml index cf6f657..ad2ea47 100644 --- a/application/views/scripts/node/edit.phtml +++ b/application/views/scripts/node/edit.phtml @@ -5,6 +5,11 @@ use Icinga\Module\Businessprocess\ServiceNode; use Icinga\Module\Businessprocess\BpNode; ?>
-

escape($node) ?>

+

escape( + sprintf( + $this->translate('Modify process node: %s'), + $node + ) +) ?>

form ?>
diff --git a/application/views/scripts/process/config.phtml b/application/views/scripts/process/config.phtml new file mode 100644 index 0000000..c788759 --- /dev/null +++ b/application/views/scripts/process/config.phtml @@ -0,0 +1,16 @@ +
+tabs ?> +

escape($this->title) ?> + icon('doc-text') ?> +

+
+ +
+form ?> +
+deleteForm ?> +
+
+ diff --git a/application/views/scripts/process/create.phtml b/application/views/scripts/process/create.phtml new file mode 100644 index 0000000..6cd98f7 --- /dev/null +++ b/application/views/scripts/process/create.phtml @@ -0,0 +1,9 @@ +
+tabs ?> +

escape($this->title) ?>

+
+ +
+form ?> +
+ diff --git a/application/views/scripts/process/show.phtml b/application/views/scripts/process/show.phtml index b51d1d7..e9d888b 100644 --- a/application/views/scripts/process/show.phtml +++ b/application/views/scripts/process/show.phtml @@ -1,29 +1,66 @@ compact): ?>
tabs ?> -

escape($this->title) ?> - icon('dashboard') ?> - render('simulationlink.phtml') ?> - render('editlink.phtml') ?> - render('downloadlink.phtml') ?> +

+ formSelect('config', $this->configName, array('class' => 'autosubmit'), $this->processList) ?> +
+ +icon('dashboard') ?> +bpconfig->isLocked()): ?> +icon('lock') ?> + +bpconfig->isEmpty()): ?> +icon('lock-open') ?> + +icon('wrench') ?> + +icon('plus') ?>

-compact): ?> -
- formSelect('processName', $this->processName, array('class' => 'autosubmit'), $this->processList) ?> -
+bpconfig->isLocked()): ?> +qlink('Add new node', 'businessprocess/node/add', array('config' => $this->configName)) ?> -errors)): ?> +bpconfig->hasErrors() || $this->bpconfig->hasChanges() || $this->bpconfig->hasSimulations()): ?>
    -errors as $error): ?> +bpconfig->getErrors() as $error): ?>
  • escape($error) ?>
  • +bpconfig->hasChanges()): ?> +
  • translate('This process has %d pending change(s).'), + $this->bpconfig->countChanges() +) ?> qlink( + $this->translate('Store'), + 'businessprocess/process/config', + array('config' => $this->configName) +) ?> qlink( + $this->translate('Dismiss'), + $this->url()->with('dismissChanges', true), + null, + array('data-base-target' => '_main') +) ?>
  • + +bpconfig->hasSimulations()): ?> +
  • translate('This process shows %d simulated state(s).'), + $this->bpconfig->countSimulations() +) ?> qlink( + $this->translate('Dismiss'), + $this->url()->with('dismissSimulations', true), + null, + array('data-base-target' => '_main') +) ?>
  • +
+
bp->renderHtml($this) ?> +bpconfig->isLocked()): ?> +bp->renderUnbound($this) ?> + +
render('warnings.phtml') ?>
- diff --git a/application/views/scripts/process/source.phtml b/application/views/scripts/process/source.phtml index 69c2a0a..d4f7323 100644 --- a/application/views/scripts/process/source.phtml +++ b/application/views/scripts/process/source.phtml @@ -1,25 +1,37 @@
tabs ?>

escape($this->title) ?> - icon('sitemap') ?> - render('simulationlink.phtml') ?> - render('editlink.phtml') ?> + icon('download') ?> +showDiff): ?> + icon('doc-text') ?> + + icon('flapping') ?> +

-
-
+
+showDiff): ?> +
+diff->renderHtml() ?> +
+ +source); $len = ceil(log(count($lines), 10)); +$rowhtml = sprintf('', $len); foreach ($lines as $line) { $cnt++; - printf("%0" . $len . "d: %s\n", $cnt, $this->escape($line)); + printf($rowhtml, $cnt, $this->escape($line)); } ?> - +
%%0%dd: %%s
+
diff --git a/application/views/scripts/process/toplevel.phtml b/application/views/scripts/process/toplevel.phtml index 126e349..7e18fd0 100644 --- a/application/views/scripts/process/toplevel.phtml +++ b/application/views/scripts/process/toplevel.phtml @@ -14,28 +14,22 @@ if ($count < 20) { compact): ?>
tabs ?> -

escape($this->title) ?> +

+ formSelect('node', $this->nodeName, array('class' => 'autosubmit'), $this->processList) ?> +
icon('sitemap') ?> - render('simulationlink.phtml') ?> - render('editlink.phtml') ?> - render('downloadlink.phtml') ?>

-compact): ?> -
- formSelect('processName', $this->processName, array('class' => 'autosubmit'), $this->processList) ?> -
-
bp->getChildren() as $name => $node): ?> diff --git a/application/views/scripts/process/upload.phtml b/application/views/scripts/process/upload.phtml new file mode 100644 index 0000000..36262ad --- /dev/null +++ b/application/views/scripts/process/upload.phtml @@ -0,0 +1,11 @@ +
+tabs ?> +

escape($this->title) ?>

+
+ +
+
    +
  • translate('This has not been implemented yet') ?>
  • +
+
+ diff --git a/configuration.php b/configuration.php index dd71700..1be4969 100644 --- a/configuration.php +++ b/configuration.php @@ -2,5 +2,5 @@ $section = $this->menuSection($this->translate('Reporting')) ->add($this->translate('Business Processes')) - ->setUrl('businessprocess/process/show'); + ->setUrl('businessprocess'); diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php index 767193d..9c1a11b 100644 --- a/library/Businessprocess/BpNode.php +++ b/library/Businessprocess/BpNode.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Businessprocess; use Icinga\Exception\ConfigurationError; +use Icinga\Exception\ProgrammingError; class BpNode extends Node { @@ -37,7 +38,7 @@ class BpNode extends Node { if ($this->counters === null) { $this->getState(); - $this->counters = array(0, 0, 0, 0); + $this->counters = array(0, 0, 0, 0, 99 => 0); foreach ($this->children as $child) { if ($child instanceof BpNode) { $counters = $child->getStateSummary(); @@ -160,36 +161,42 @@ class BpNode extends Node } switch ($this->operator) { case self::OP_AND: - $state = max($sort_states); + $sort_state = max($sort_states); break; case self::OP_OR: - $state = min($sort_states); + $sort_state = min($sort_states); break; default: // MIN: sort($sort_states); // default -> unknown - $state = 2 << self::SHIFT_FLAGS; + $sort_state = 3 << self::SHIFT_FLAGS; for ($i = 1; $i <= $this->operator; $i++) { - $state = array_shift($sort_states); + $sort_state = array_shift($sort_states); } } - if ($state & self::FLAG_DOWNTIME) { + if ($sort_state & self::FLAG_DOWNTIME) { $this->setDowntime(true); } - if ($state & self::FLAG_ACK) { + if ($sort_state & self::FLAG_ACK) { $this->setAck(true); } - $state = $state >> self::SHIFT_FLAGS; + $sort_state = $sort_state >> self::SHIFT_FLAGS; - if ($state === 3) { + if ($sort_state === 4) { $this->state = 2; - } elseif ($state === 2) { + } elseif ($sort_state === 3) { $this->state = 3; + } elseif ($sort_state === 2) { + $this->state = 1; + } elseif ($sort_state === 1) { + $this->state = 99; + } elseif ($sort_state === 0) { + $this->state = 0; } else { - $this->state = $state; + throw new ProgrammingError('Got invalid sorting state %s', $sort_state); } } @@ -209,13 +216,24 @@ class BpNode extends Node return $this; } + public function getDisplay() + { + return $this->display; + } + public function setChildNames($names) { + sort($names); $this->child_names = $names; $this->children = null; return $this; } + public function getChildNames() + { + return $this->child_names; + } + public function getChildren() { if ($this->children === null) { @@ -223,6 +241,7 @@ class BpNode extends Node natsort($this->child_names); foreach ($this->child_names as $name) { $this->children[$name] = $this->bp->getNode($name); + $this->children[$name]->addParent($this); } } return $this->children; @@ -235,15 +254,21 @@ class BpNode extends Node } } - public function renderLink($view) + protected function getActionIcons($view) { - if (! $this->bp->isEditMode()) { - return parent::renderLink($view); + $icons = array(); + if (! $this->bp->isLocked()) { + $icons[] = $this->actionIcon( + $view, + 'wrench', + $view->url('businessprocess/node/edit', array( + 'config' => $this->bp->getName(), + 'node' => $this->name + )), + mt('businessprocess', 'Modify this node') + ); } - return $view->qlink($this->name, 'businessprocess/node/edit', array( - 'node' => $this->name, - 'processName' => $this->bp->getName() - )); + return $icons; } public function toLegacyConfigString(& $rendered = array()) diff --git a/library/Businessprocess/BusinessProcess.php b/library/Businessprocess/BusinessProcess.php index 316b625..3d1df51 100644 --- a/library/Businessprocess/BusinessProcess.php +++ b/library/Businessprocess/BusinessProcess.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Businessprocess; +use Icinga\Application\Benchmark; use Icinga\Module\Monitoring\Backend\MonitoringBackend; use Icinga\Data\Filter\Filter; use Exception; @@ -12,6 +13,13 @@ class BusinessProcess const HARD_STATE = 1; + /** + * Name of the configured monitoring backend + * + * @var string + */ + protected $backendName; + /** * Monitoring backend to retrieve states from * @@ -47,6 +55,13 @@ class BusinessProcess */ protected $warnings = array(); + /** + * Errors, usually filled at process build time + * + * @var array + */ + protected $errors = array(); + /** * All used node objects * @@ -76,11 +91,11 @@ class BusinessProcess protected $hosts = array(); /** - * Whether we are in simulation mode + * Applied state simulation * - * @var boolean + * @var Simulation */ - protected $simulationMode = false; + protected $simulation; /** * Whether we are in edit mode @@ -89,10 +104,55 @@ class BusinessProcess */ protected $editMode = false; + protected $locked = true; + + protected $changeCount = 0; + + protected $simulationCount = 0; + public function __construct() { } + public function applyChanges(ProcessChanges $changes) + { + $cnt = 0; + foreach ($changes->getChanges() as $change) { + $cnt++; + $change->applyTo($this); + } + $this->changeCount = $cnt; + + return $this; + } + + public function applySimulation(Simulation $simulation) + { + $cnt = 0; + + foreach ($simulation->simulations() as $node => $s) { + if (! $this->hasNode($node)) { + continue; + } + $cnt++; + $this->getNode($node) + ->setState($s->state) + ->setAck($s->acknowledged) + ->setDowntime($s->in_downtime); + } + + $this->simulationCount = $cnt; + } + public function countChanges() + { + return $this->changeCount; + } + + public function hasChanges() + { + return $this->countChanges() > 0; + } + public function setName($name) { $this->name = $name; @@ -115,12 +175,27 @@ class BusinessProcess return $this->title ?: $this->getName(); } - public function loadBackendByName($name) + public function hasTitle() { - $this->backend = MonitoringBackend::createBackend($name); + return $this->title !== null; + } + + public function setBackendName($name) + { + $this->backendName = $name; return $this; } + public function getBackendName() + { + return $this->backendName; + } + + public function hasBackendName() + { + return $this->backendName !== null; + } + public function setBackend(MonitoringBackend $backend) { $this->backend = $backend; @@ -130,8 +205,11 @@ class BusinessProcess public function getBackend() { if ($this->backend === null) { - $this->backend = MonitoringBackend::createBackend(); + $this->backend = MonitoringBackend::createBackend( + $this->getBackendName() + ); } + return $this->backend; } @@ -145,15 +223,30 @@ class BusinessProcess return false; } - public function setSimulationMode($mode = true) + public function isLocked() { - $this->simulationMode = (bool) $mode; + return $this->locked; + } + + public function lock($lock = true) + { + $this->locked = (bool) $lock; return $this; } - public function isSimulationMode() + public function unlock() { - return $this->simulationMode; + return $this->lock(false); + } + + public function hasSimulations() + { + return $this->countSimulations() > 0; + } + + public function countSimulations() + { + return $this->simulationCount; } public function setEditMode($mode = true) @@ -197,6 +290,19 @@ class BusinessProcess public function retrieveStatesFromBackend() { + try { + $this->reallyRetrieveStatesFromBackend(); + } catch (Exception $e) { + $this->error( + $this->translate('Could not retrieve process state: %s'), + $e->getMessage() + ); + } + } + + public function reallyRetrieveStatesFromBackend() + { + Benchmark::measure('Retrieving states for business process ' . $this->getName()); $backend = $this->getBackend(); // TODO: Split apart, create a dedicated function. // Separate "parse-logic" from "retrieve-state-logic" @@ -221,6 +327,10 @@ class BusinessProcess $filter->addFilter(Filter::where('host_name', $host)); } + if ($filter->isEmpty()) { + return $this; + } + $hostStatus = $backend->select()->from('hostStatus', array( 'hostname' => 'host_name', 'last_state_change' => $hostStateChangeColumn, @@ -263,6 +373,8 @@ class BusinessProcess } } ksort($this->root_nodes); + Benchmark::measure('Got states for business process ' . $this->getName()); + return $this; } @@ -352,7 +464,7 @@ class BusinessProcess if (array_key_exists($name, $this->nodes)) { $this->warn( sprintf( - 'Node "%s" has been defined twice', + mt('businessprocess', 'Node "%s" has been defined twice'), $name ) ); @@ -362,6 +474,27 @@ class BusinessProcess return $this; } + public function getUnboundNodes() + { + $nodes = array(); + + foreach ($this->nodes as $node) { + if (! $node instanceof BpNode) { + continue; + } + + if ($node->hasParents()) { + continue; + } + + if ($node->getDisplay() === '0') { + $nodes[(string) $node] = $node; + } + } + + return $nodes; + } + public function hasWarnings() { return ! empty($this->warnings); @@ -372,35 +505,73 @@ class BusinessProcess return $this->warnings; } + public function hasErrors() + { + return ! empty($this->errors) || $this->isEmpty(); + } + + public function getErrors() + { + $errors = $this->errors; + if ($this->isEmpty()) { + $errors[] = sprintf( + $this->translate( + 'No business process nodes for "%s" have been defined yet' + ), + $this->getTitle() + ); + } + return $errors; + } + + protected function translate($msg) + { + return mt('businessprocess', $msg); + } + protected function warn($msg) { + $args = func_get_args(); + array_shift($args); if (isset($this->parsing_line_number)) { $this->warnings[] = sprintf( - 'Parser waring on %s:%s: %s', + $this->translate('Parser waring on %s:%s: %s'), $this->filename, $this->parsing_line_number, - $msg + vsprintf($msg, $args) ); } else { - $this->warnings[] = $msg; + $this->warnings[] = vsprintf($msg, $args); } } + protected function error($msg) + { + $args = func_get_args(); + array_shift($args); + $this->errors[] = vsprintf($msg, $args); + } + public function toLegacyConfigString() { - $conf = sprintf( - "### Busines Process Config File ###\n" - . "#\n" - . "# Title : %s\n" - // . "# Owner : %s\n" - . "# Backend : %s\n" - . "# Statetype : %s\n", - $this->getTitle(), - $this->backend->getName(), - $this->state_type = self::SOFT_STATE ? 'soft' : 'hard' - ); + $settings = array(); + if ($this->hasTitle()) { + $settings['Title'] = $this->getTitle(); + } + // TODO: backendName? + if ($this->backend) { + $settings['Backend'] = $this->backend->getName(); + } + $settings['Statetype'] = $this->usesSoftStates() ? 'soft' : 'hard'; - if (false) $conf .= sprintf("# xSLA Hosts: %s\n", implode(', ', array())); + if (false) { + $settings['SLA Hosts'] = implode(', ', array()); + } + + $conf = "### Business Process Config File ###\n#\n"; + foreach ($settings as $key => $value) { + $conf .= sprintf("# %-9s : %s\n", $key, $value); + } $conf .= sprintf( "#\n" @@ -412,17 +583,46 @@ class BusinessProcess foreach ($this->getChildren() as $child) { $conf .= $child->toLegacyConfigString($rendered); } + foreach ($this->getUnboundNodes() as $node) { + $conf .= $node->toLegacyConfigString($rendered); + } return $conf . "\n"; } + public function isEmpty() + { + return $this->countChildren() === 0; + } + public function renderHtml($view) { - $html = '
'; + $html = ''; foreach ($this->getRootNodes() as $name => $node) { - // showNode($this, $node, $this->slas, $this->opened, 'bp_') $html .= $node->renderHtml($view); } - $html .= '
'; + return $html; + } + + public function renderUnbound($view) + { + $html = ''; + $unbound = $this->getUnboundNodes(); + if (empty($unbound)) { + return $html; + } + + $parent = new BpNode($this, (object) array( + 'name' => '__unbound__', + 'operator' => '|', + 'child_names' => array_keys($unbound) + )); + $parent->getState(); + $parent->setMissing() + ->setDowntime(false) + ->setAck(false) + ->setAlias('Unbound nodes'); + + $html .= $parent->renderHtml($view); return $html; } } diff --git a/library/Businessprocess/ConfigDiff.php b/library/Businessprocess/ConfigDiff.php new file mode 100644 index 0000000..355e9b1 --- /dev/null +++ b/library/Businessprocess/ConfigDiff.php @@ -0,0 +1,40 @@ +a = $a; + $this->b = $b; + require_once dirname(__DIR__) . '/vendor/PHP-FineDiff/finediff.php'; + $granularity = FineDiff::$paragraphGranularity; // character, word, sentence, paragraph + $this->diff = new FineDiff($a, $b, $granularity); + } + + public function renderHtml() + { + return $this->diff->renderDiffToHTML(); + } + + public function __toString() + { + return $this->renderHtml(); + } + + public static function create($a, $b) + { + $diff = new static($a, $b); + return $diff; + } +} diff --git a/library/Businessprocess/Controller.php b/library/Businessprocess/Controller.php index 38f98a5..da77a21 100644 --- a/library/Businessprocess/Controller.php +++ b/library/Businessprocess/Controller.php @@ -2,26 +2,27 @@ namespace Icinga\Module\Businessprocess; +use Exception; use Icinga\Application\Icinga; -use Icinga\Web\Controller\ModuleActionController; -use Icinga\Module\Monitoring\Backend; -use Icinga\Module\Businessprocess\Storage\LegacyStorage; use Icinga\Module\Businessprocess\BusinessProcess; use Icinga\Module\Businessprocess\Form\ProcessForm; use Icinga\Module\Businessprocess\Form\SimulationForm; +use Icinga\Module\Businessprocess\Storage\LegacyStorage; +use Icinga\Module\Monitoring\Backend; +use Icinga\Web\Controller as ModuleController; +use Icinga\Web\Notification; use Icinga\Web\Url; use Icinga\Web\Widget; -use Exception; -class Controller extends ModuleActionController +class Controller extends ModuleController { protected $config; protected $backend; - protected $views; + private $storage; - protected $aliases; + private $url; public function init() { @@ -29,26 +30,25 @@ class Controller extends ModuleActionController if (! $m->hasLoaded('monitoring') && $m->hasInstalled('monitoring')) { $m->loadModule('monitoring'); } + $this->view->errors = array(); - $this->config = $this->Config(); + $this->view->compact = $this->params->get('view') === 'compact'; + } + + protected function url() + { + if ($this->url === null) { + $this->url = clone $this->getRequest()->getUrl(); + } + return $this->url; } protected function tabs() { - $url = Url::fromRequest(); - $tabs = Widget::create('tabs')->add('show', array( - 'title' => $this->translate('Show'), - 'url' => 'businessprocess/process/show', - )); - foreach (array('processName', 'process') as $param) { - if ($process = $this->params->get($param)) { - foreach ($tabs->getTabs() as $tab) { - $tab->setUrlParams(array($param => $process)); - } - } + if ($this->view->tabs === null) { + $this->view->tabs = Widget::create('tabs'); } - - return $tabs; + return $this->view->tabs; } protected function session() @@ -56,15 +56,54 @@ class Controller extends ModuleActionController return $this->Window()->getSessionNamespace('businessprocess'); } - protected function loadBp() + protected function setTitle($title) { - $storage = new LegacyStorage($this->Config()->getSection('global')); + $args = func_get_args(); + array_shift($args); + $this->view->title = vsprintf($title, $args); + } + + protected function loadModifiedBpConfig() + { + $bp = $this->loadBpConfig(); + $changes = ProcessChanges::construct($bp, $this->session()); + if ($this->params->get('dismissChanges')) { + Notification::success( + sprintf( + $this->translate('%d pending change(s) have been dropped'), + $changes->count() + ) + ); + $changes->clear(); + $this->redirectNow($this->url()->without('dismissChanges')->without('unlocked')); + } + $bp->applyChanges($changes); + return $bp; + } + + protected function loadBpConfig() + { + $storage = $this->storage(); $this->view->processList = $storage->listProcesses(); - $process = $this->params->get('processName', key($this->view->processList)); - $this->view->processName = $process; - $bp = $storage->loadProcess($process); + // No process found? Go to welcome page + if (empty($this->view->processList)) { + $this->redirectNow('businessprocess'); + } + $name = $this->params->get( + 'config', + key($this->view->processList) + ); + + $modifications = $this->session()->get('modifications', array()); + if (array_key_exists($name, $modifications)) { + $bp = $storage->loadFromString($name, $modifications[$name]); + } else { + $bp = $storage->loadProcess($name); + } + + // allow URL parameter to override configured state type if (null !== ($stateType = $this->params->get('state_type'))) { if ($stateType === 'soft') { $bp->useSoftStates(); @@ -74,9 +113,23 @@ class Controller extends ModuleActionController } } + $this->view->bpconfig = $bp; + $this->view->configName = $bp->getName(); + return $bp; } + protected function storage() + { + if ($this->storage === null) { + $this->storage = new LegacyStorage( + $this->Config()->getSection('global') + ); + } + + return $this->storage; + } + protected function loadSlas() { $bpconf = $this->bpconf; diff --git a/library/Businessprocess/Form.php b/library/Businessprocess/Form.php new file mode 100644 index 0000000..3270b38 --- /dev/null +++ b/library/Businessprocess/Form.php @@ -0,0 +1,36 @@ +setup(); + } + + public function addHidden($name, $value = null) + { + $this->addElement('hidden', $name); + $this->getElement($name)->setDecorators(array('ViewHelper')); + if ($value !== null) { + $this->setDefault($name, $value); + } + return $this; + } + + public function handleRequest(Request $request = null) + { + parent::handleRequest(); + return $this; + } + + public static function construct() + { + return new static; + } +} diff --git a/library/Businessprocess/HostNode.php b/library/Businessprocess/HostNode.php index bcd9c61..c0af438 100644 --- a/library/Businessprocess/HostNode.php +++ b/library/Businessprocess/HostNode.php @@ -22,13 +22,13 @@ class HostNode extends Node { if ($this->bp->isSimulationMode()) { return $view->qlink($this->getHostname(), 'businessprocess/host/simulate', array( - 'node' => $this->name, - 'processName' => $this->bp->getName() + 'config' => $this->bp->getName(), + 'node' => $this->name )); } else { return $view->qlink($this->getHostname(), 'monitoring/host/show', array( - 'host' => $this->getHostname(), - 'processName' => $this->bp->getName() + 'config' => $this->bp->getName(), + 'host' => $this->getHostname() )); } } diff --git a/library/Businessprocess/ImportedNode.php b/library/Businessprocess/ImportedNode.php index fa43d2b..fd636d5 100644 --- a/library/Businessprocess/ImportedNode.php +++ b/library/Businessprocess/ImportedNode.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Businessprocess; use Icinga\Application\Config; +use Icinga\Web\Url; use Icinga\Module\Businessprocess\Storage\LegacyStorage; class ImportedNode extends Node @@ -38,11 +39,6 @@ class ImportedNode extends Node return $this->state; } - public function getSortingState() - { - return $this->importedNode()->getSortingState(); - } - public function getAlias() { return $this->importedNode()->getAlias(); @@ -50,7 +46,8 @@ class ImportedNode extends Node public function isMissing() { - return $this->getState() === null; + return $this->importedNode()->isMissing(); + // TODO: WHY? return $this->getState() === null; } public function isInDowntime() @@ -86,19 +83,34 @@ class ImportedNode extends Node } return $this->importedNode; } + + protected function getActionIcons($view) + { + $icons = array(); + + if (! $this->bp->isLocked()) { + + $url = Url::fromPath( 'businessprocess/node/simulate', array( + 'config' => $this->bp->getName(), + 'node' => $this->name + )); + + $icons[] = $this->actionIcon( + $view, + 'magic', + $url, + 'Simulation' + ); + } + + return $icons; + } public function renderLink($view) { - if ($this->bp->isSimulationMode()) { - return $view->qlink($this->getAlias(), 'businessprocess/importednode/simulate', array( - 'processName' => $this->configName, - 'node' => $this->name - )); - } else { - return $view->qlink($this->getAlias(), 'businessprocess/process/show', array( - 'processName' => $this->configName, - 'process' => $this->name - )); - } + return $view->qlink($this->getAlias(), 'businessprocess/process/show', array( + 'config' => $this->configName, + 'process' => $this->name + )); } } diff --git a/library/Businessprocess/Node.php b/library/Businessprocess/Node.php index 0366b9c..5278da2 100644 --- a/library/Businessprocess/Node.php +++ b/library/Businessprocess/Node.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Businessprocess; +use Icinga\Web\Url; +use Icinga\Exception\ProgrammingError; use Exception; abstract class Node @@ -12,6 +14,15 @@ abstract class Node const FLAG_NONE = 8; const SHIFT_FLAGS = 4; + const ICINGA_OK = 0; + const ICINGA_WARNING = 1; + const ICINGA_CRITICAL = 2; + const ICINGA_UNKNOWN = 3; + const ICINGA_UP = 0; + const ICINGA_DOWN = 1; + const ICINGA_UNREACHABLE = 2; + const ICINGA_PENDING = 99; + /** * Main business process object * @@ -20,11 +31,11 @@ abstract class Node protected $bp; /** - * Parent node + * Parent nodes * - * @var Node + * @var array */ - protected $parent; + protected $parents = array(); /** * Node identifier @@ -70,7 +81,8 @@ abstract class Node 'OK', 'WARNING', 'CRITICAL', - 'UNKNOWN' + 'UNKNOWN', + 99 => 'PENDING' ); abstract public function __construct(BusinessProcess $bp, $object); @@ -107,7 +119,7 @@ abstract class Node ); } $this->childs[(string) $node] = $node; - $node->setParent($this); + $node->addParent($this); return $this; } @@ -137,7 +149,7 @@ abstract class Node public function getState() { if ($this->state === null) { - throw new Exception( + throw new ProgrammingError( sprintf( 'Node %s is unable to retrieve it\'s state', $this->name @@ -150,18 +162,32 @@ abstract class Node public function getSortingState() { $state = $this->getState(); - if ($state === 3) { - $state = 2; - } elseif ($state === 2) { - $state = 3; + switch ($state) { + case 0: + $sort = 0; + break; + case 1: + $sort = 2; + break; + case 2: + $sort = 4; + break; + case 3: + $sort = 3; + break; + case 99: + $sort = 1; + break; + default: + throw new ProgrammingError('Got invalid state %d', $state); } - $state = ($state << self::SHIFT_FLAGS) + $sort = ($sort << self::SHIFT_FLAGS) + ($this->isInDowntime() ? self::FLAG_DOWNTIME : 0) + ($this->isAcknowledged() ? self::FLAG_ACK : 0); - if (! ($state & (self::FLAG_DOWNTIME | self::FLAG_ACK))) { - $state |= self::FLAG_NONE; + if (! ($sort & (self::FLAG_DOWNTIME | self::FLAG_ACK))) { + $sort |= self::FLAG_NONE; } - return $state; + return $sort; } public function getLastStateChange() @@ -175,9 +201,9 @@ abstract class Node return $this; } - public function setParent(Node $parent) + public function addParent(Node $parent) { - $this->parent = $parent; + $this->parents[] = $parent; return $this; } @@ -242,6 +268,11 @@ abstract class Node return $this->name; } + public function hasParents() + { + return count($this->parents) > 0; + } + protected function renderHtmlForChildren($view) { $html = ''; @@ -304,19 +335,22 @@ abstract class Node $this->renderLink($view) ); + $icons = array(); + + foreach ($this->getActionIcons($view) as $icon) { + $icons[] = $icon; + } + if ($this->hasInfoUrl()) { $url = $this->getInfoUrl(); - $target = preg_match('~^https?://~', $url) ? ' target="_blank"' : ''; - $title = sprintf( - ' %s%s', + $icons[] = $this->actionIcon( + $view, + 'help', $url, - $target, - mt('businessprocess', 'More information'), - $url, - $view->icon('help'), - $title + sprintf('%s: %s', mt('businessprocess', 'More information'), $url) ); } + $title = implode("\n", $icons) . $title; $html .= sprintf( '%s', @@ -329,6 +363,28 @@ abstract class Node return $html; } + protected function getActionIcons($view) + { + return array(); + } + + protected function actionIcon($view, $icon, $url, $title) + { + if ($url instanceof Url || ! preg_match('~^https?://~', $url)) { + $target = ''; + } else { + $target = ' target="_blank"'; + } + + return sprintf( + ' %s', + $url, + $target, + $view->escape($title), + $view->icon($icon) + ); + } + public function renderLink($view) { return '' . ($this->hasAlias() ? $this->getAlias() : $this->name) . ''; @@ -361,6 +417,6 @@ abstract class Node public function __destruct() { - unset($this->parent); + $this->parents = array(); } } diff --git a/library/Businessprocess/NodeAction.php b/library/Businessprocess/NodeAction.php new file mode 100644 index 0000000..88e0e34 --- /dev/null +++ b/library/Businessprocess/NodeAction.php @@ -0,0 +1,93 @@ +nodeName = (string) $node; + } + + abstract public function applyTo(BusinessProcess $bp); + + public function getNodeName() + { + return $this->nodeName; + } + + public function is($actionName) + { + return $this->getActionName() === $actionName; + } + + public static function create($actionName, $nodeName) + { + $classname = __NAMESPACE__ . '\\Node' . ucfirst($actionName) . 'Action'; + $object = new $classname($nodeName); + return $object; + } + + public function serialize() + { + $object = (object) array( + 'actionName' => $this->getActionName(), + 'nodeName' => $this->getNodeName(), + 'properties' => array() + ); + + foreach ($this->preserveProperties as $key) { + $func = 'get' . ucfirst($key); + $object->properties[$key] = $this->$func(); + } + + return json_encode($object); + } + + public static function unserialize($string) + { + $object = json_decode($string); + $action = self::create($object->actionName, $object->nodeName); + + foreach ($object->properties as $key => $val) { + $func = 'set' . ucfirst($key); + $action->$func($val); + } + + return $action; + } + + public function getActionName() + { + if ($this->actionName === null) { + if (! preg_match('/\\\Node(\w+)Action$/', get_class($this), $m)) { + throw new ProgrammingError( + '"%s" is not a NodeAction class', + get_class($this) + ); + } + $this->actionName = lcfirst($m[1]); + } + + return $this->actionName; + } + +} diff --git a/library/Businessprocess/NodeCreateAction.php b/library/Businessprocess/NodeCreateAction.php new file mode 100644 index 0000000..c6e5a58 --- /dev/null +++ b/library/Businessprocess/NodeCreateAction.php @@ -0,0 +1,69 @@ +parentName = $name; + } + + public function hasParent() + { + return $this->parentName !== null; + } + + public function getParentName() + { + return $this->parentName; + } + + public function setParentName($name) + { + $this->parentName = $name; + } + + public function getProperties() + { + return $this->properties; + } + + public function setProperties($properties) + { + $this->properties = $properties; + return $this; + } + + public function appliesTo(BusinessProcess $bp) + { + return ! $bp->hasNode($this->getNodeName()); + } + + public function applyTo(BusinessProcess $bp) + { + $node = new BpNode($bp, (object) array( + 'name' => $this->getNodeName(), + 'operator' => $this->properties->operator, + 'child_names' => $this->properties->childNames + )); + + foreach ($this->properties as $key => $val) { + $func = 'set' . ucfirst($key); + $node->$func($val); + } + + $bp->addNode($this->getNodeName(), $node); + if ($this->hasParent()) { + $node->addParent($bp->getNode($this->getParentName())); + } + + return $node; + } +} diff --git a/library/Businessprocess/NodeModifyAction.php b/library/Businessprocess/NodeModifyAction.php new file mode 100644 index 0000000..da2e7fa --- /dev/null +++ b/library/Businessprocess/NodeModifyAction.php @@ -0,0 +1,86 @@ +properties[$key] = $properties[$key]; + + if (array_key_exists($key, $this->formerProperties)) { + continue; + } + + $func = 'get' . ucfirst($key); + $this->formerProperties[$key] = $node->$func(); + } + + return $this; + } + + public function appliesTo(BusinessProcess $bp) + { + $name = $this->getNodeName(); + + if (! $bp->hasNode($name)) { + return false; + } + + $node = $bp->getNode($name); + + foreach ($this->properties as $key => $val) { + $func = 'get' . ucfirst($key); + if ($this->formerProperties[$key] !== $node->$func()) { + return false; + } + } + + return true; + } + + public function applyTo(BusinessProcess $bp) + { + $node = $bp->getNode($this->getNodeName()); + + foreach ($this->properties as $key => $val) { + $func = 'set' . ucfirst($key); + $node->$func($val); + } + + return $this; + } + + public function setProperties($properties) + { + $this->properties = $properties; + return $this; + } + + public function setFormerProperties($properties) + { + $this->formerProperties = $properties; + return $this; + } + + public function getProperties() + { + return $this->properties; + } + + public function getFormerProperties() + { + return $this->formerProperties; + } +} diff --git a/library/Businessprocess/NodeRemoveAction.php b/library/Businessprocess/NodeRemoveAction.php new file mode 100644 index 0000000..916265f --- /dev/null +++ b/library/Businessprocess/NodeRemoveAction.php @@ -0,0 +1,16 @@ +hasNode($this->getNodeName()); + } + + public function applyTo(BusinessProcess $bp) + { + $bp->removeNode($this->getNodeName()); + } +} diff --git a/library/Businessprocess/ProcessChanges.php b/library/Businessprocess/ProcessChanges.php new file mode 100644 index 0000000..1fcf17c --- /dev/null +++ b/library/Businessprocess/ProcessChanges.php @@ -0,0 +1,143 @@ +getName(); + $changes = new ProcessChanges(); + $changes->sessionKey = $key; + + if ($actions = $session->get($key)) { + foreach ($actions as $string) { + $changes->push(NodeAction::unserialize($string)); + } + } + $changes->session = $session; + + return $changes; + } + + public function modifyNode(Node $node, $properties) + { + $action = new NodeModifyAction($node); + $action->setNodeProperties($node, $properties); + return $this->push($action); + } + + public function createNode($nodeName, $properties, Node $parent = null) + { + $action = new NodeCreateAction($nodeName); + $action->setProperties($properties); + if ($parent !== null) { + $action->setParent($parent); + } + return $this->push($action); + } + + public function deleteNode(Node $node) + { + return $this->push(new NodeDeleteAction($node)); + } + + public function push(NodeAction $change) + { + $this->changes[] = $change; + $this->hasBeenModified = true; + return $this; + } + + public function getChanges() + { + return $this->changes; + } + + public function clear() + { + $this->hasBeenModified = true; + $this->changes = array(); + $this->session->set($this->getSessionKey(), null); + return $this; + } + + public function isEmpty() + { + return $this->count() === 0; + } + + public function count() + { + return count($this->changes); + } + + public function shift() + { + if ($this->isEmpty()) { + return false; + } + + $this->hasBeenModified = true; + return array_shift($this->changes); + } + + public function pop() + { + if ($this->isEmpty()) { + return false; + } + + $this->hasBeenModified = true; + return array_pop($this->changes); + } + + protected function getSessionKey() + { + return $this->sessionKey; + } + + protected function hasBeenModified() + { + return $this->hasBeenModified; + } + + public function serialize() + { + $serialized = array(); + foreach ($this->getChanges() as $change) { + $serialized[] = $change->serialize(); + } + + return $serialized; + } + + public function __destruct() + { + if (! $this->hasBeenModified()) { + unset($this->session); + return; + } + $session = $this->session; + $key = $this->getSessionKey(); + if (! $this->isEmpty()) { + $session->set($key, $this->serialize()); + } + unset($this->session); + } +} diff --git a/library/Businessprocess/ServiceNode.php b/library/Businessprocess/ServiceNode.php index 603761a..8b3d748 100644 --- a/library/Businessprocess/ServiceNode.php +++ b/library/Businessprocess/ServiceNode.php @@ -2,9 +2,12 @@ namespace Icinga\Module\Businessprocess; +use Icinga\Web\Url; + class ServiceNode extends Node { protected $hostname; + protected $service; public function __construct(BusinessProcess $bp, $object) @@ -22,18 +25,40 @@ class ServiceNode extends Node public function renderLink($view) { - if ($this->bp->isSimulationMode()) { - return $view->qlink($this->getAlias(), 'businessprocess/node/simulate', array( - 'node' => $this->name, - 'processName' => $this->bp->getName() - )); - } else { - return $view->qlink($this->getAlias(), 'monitoring/service/show', array( - 'host' => $this->getHostname(), - 'service' => $this->getServiceDescription(), - 'processName' => $this->bp->getName() - )); + if ($this->isMissing()) { + return '' . $view->escape($this->getAlias()) . ''; } + + $params = array( + 'host' => $this->getHostname(), + 'service' => $this->getServiceDescription() + ); + if ($this->bp->hasBackendName()) { + $params['backend'] = $this->bp->getBackendName(); + } + return $view->qlink($this->getAlias(), 'monitoring/service/show', $params); + } + + protected function getActionIcons($view) + { + $icons = array(); + + if (! $this->bp->isLocked()) { + + $url = Url::fromPath( 'businessprocess/node/simulate', array( + 'config' => $this->bp->getName(), + 'node' => $this->name + )); + + $icons[] = $this->actionIcon( + $view, + 'magic', + $url, + 'Simulation' + ); + } + + return $icons; } public function getHostname() diff --git a/library/Businessprocess/Simulation.php b/library/Businessprocess/Simulation.php new file mode 100644 index 0000000..4babc48 --- /dev/null +++ b/library/Businessprocess/Simulation.php @@ -0,0 +1,106 @@ +bp = $bp; + $this->session = $session; + $this->key = 'simulations.' . $bp->getName(); + } + + public function simulations() + { + if ($this->simulations === null) { + $this->simulations = $this->fetchSimulations(); + } + + return $this->simulations; + } + + protected function setSimulations($simulations) + { + $this->simulations = $simulations; + $this->session->set($this->key, $simulations); + return $this; + } + + protected function fetchSimulations() + { + return $this->session->get($this->key, array()); + } + + public function clear() + { + $this->simulations = array(); + $this->session->set($this->key, array()); + } + + public function count() + { + return count($this->simulations()); + } + + public function isEmpty() + { + return $this->count() === 0; + } + + public function set($node, $properties) + { + $simulations = $this->simulations(); + $simulations[$node] = $properties; + $this->setSimulations($simulations); + } + + public function hasNode($name) + { + $simulations = $this->simulations(); + return array_key_exists($name, $simulations); + } + + public function getNode($name) + { + $simulations = $this->simulations(); + if (! array_key_exists($name, $simulations)) { + throw new ProgrammingError('Trying to access invalid node %s', $name); + } + return $simulations[$name]; + } + + public function remove($node) + { + $simulations = $this->simulations(); + if (array_key_exists($node, $simulations)) { + + unset($simulations[$node]); + $this->setSimulations($simulations); + + return true; + } else { + + return false; + } + } +} diff --git a/library/Businessprocess/Storage/LegacyStorage.php b/library/Businessprocess/Storage/LegacyStorage.php index df2c5df..70fa6f1 100644 --- a/library/Businessprocess/Storage/LegacyStorage.php +++ b/library/Businessprocess/Storage/LegacyStorage.php @@ -10,6 +10,7 @@ use Icinga\Module\Businessprocess\BpNode; use Icinga\Module\Businessprocess\Storage\Storage; use DirectoryIterator; use Icinga\Exception\SystemPermissionException; +use Icinga\Application\Benchmark; class LegacyStorage extends Storage { @@ -61,7 +62,11 @@ class LegacyStorage extends Storage if (substr($filename, -5) === '.conf') { $name = substr($filename, 0, -5); $header = $this->readHeader($file->getPathname(), $name); - $files[$name] = $header['Title']; + if ($header['Title'] === null) { + $files[$name] = $name; + } else { + $files[$name] = sprintf('%s (%s)', $header['Title'], $name); + } } } return $files; @@ -72,7 +77,7 @@ class LegacyStorage extends Storage $fh = fopen($file, 'r'); $cnt = 0; $header = array( - 'Title' => $name, + 'Title' => null, 'Owner' => null, 'Backend' => null, 'Statetype' => 'soft', @@ -101,6 +106,11 @@ class LegacyStorage extends Storage ); } + public function getSource($name) + { + return file_get_contents($this->getFilename($name)); + } + public function getFilename($name) { return $this->getConfigDir() . '/' . $name . '.conf'; @@ -115,15 +125,22 @@ class LegacyStorage extends Storage return $bp; } + public function deleteProcess($name) + { + unlink($this->getFilename($name)); + } + /** * @return BusinessProcess */ public function loadProcess($name) { + Benchmark::measure('Loading business process ' . $name); $bp = new BusinessProcess(); $bp->setName($name); $this->parseFile($name, $bp); $this->loadHeader($name, $bp); + Benchmark::measure('Business process ' . $name . ' loaded'); return $bp; } @@ -134,7 +151,10 @@ class LegacyStorage extends Storage $header = $this->readHeader($file, $name); $bp->setTitle($header['Title']); if ($header['Backend']) { - $bp->loadBackendByName($header['Backend']); + $bp->setBackendName($header['Backend']); + } + if ($header['Statetype'] === 'soft') { + $bp->useSoftStates(); } } diff --git a/library/Businessprocess/Storage/Storage.php b/library/Businessprocess/Storage/Storage.php index 1140818..21d130f 100644 --- a/library/Businessprocess/Storage/Storage.php +++ b/library/Businessprocess/Storage/Storage.php @@ -32,4 +32,6 @@ abstract class Storage /** */ abstract public function storeProcess(BusinessProcess $name); + + abstract public function deleteProcess($name); } diff --git a/library/vendor/PHP-FineDiff/finediff.php b/library/vendor/PHP-FineDiff/finediff.php new file mode 100644 index 0000000..920365a --- /dev/null +++ b/library/vendor/PHP-FineDiff/finediff.php @@ -0,0 +1,688 @@ +copy->insert +* command (swap) for when the inserted segment is exactly the same +* as the deleted one, and with only a copy operation in between. +* TODO: How often this case occurs? Is it worth it? Can only +* be done as a postprocessing method (->optimize()?) +*/ +abstract class FineDiffOp { + abstract public function getFromLen(); + abstract public function getToLen(); + abstract public function getOpcode(); + } + +class FineDiffDeleteOp extends FineDiffOp { + public function __construct($len) { + $this->fromLen = $len; + } + public function getFromLen() { + return $this->fromLen; + } + public function getToLen() { + return 0; + } + public function getOpcode() { + if ( $this->fromLen === 1 ) { + return 'd'; + } + return "d{$this->fromLen}"; + } + } + +class FineDiffInsertOp extends FineDiffOp { + public function __construct($text) { + $this->text = $text; + } + public function getFromLen() { + return 0; + } + public function getToLen() { + return strlen($this->text); + } + public function getText() { + return $this->text; + } + public function getOpcode() { + $to_len = strlen($this->text); + if ( $to_len === 1 ) { + return "i:{$this->text}"; + } + return "i{$to_len}:{$this->text}"; + } + } + +class FineDiffReplaceOp extends FineDiffOp { + public function __construct($fromLen, $text) { + $this->fromLen = $fromLen; + $this->text = $text; + } + public function getFromLen() { + return $this->fromLen; + } + public function getToLen() { + return strlen($this->text); + } + public function getText() { + return $this->text; + } + public function getOpcode() { + if ( $this->fromLen === 1 ) { + $del_opcode = 'd'; + } + else { + $del_opcode = "d{$this->fromLen}"; + } + $to_len = strlen($this->text); + if ( $to_len === 1 ) { + return "{$del_opcode}i:{$this->text}"; + } + return "{$del_opcode}i{$to_len}:{$this->text}"; + } + } + +class FineDiffCopyOp extends FineDiffOp { + public function __construct($len) { + $this->len = $len; + } + public function getFromLen() { + return $this->len; + } + public function getToLen() { + return $this->len; + } + public function getOpcode() { + if ( $this->len === 1 ) { + return 'c'; + } + return "c{$this->len}"; + } + public function increase($size) { + return $this->len += $size; + } + } + +/** +* FineDiff ops +* +* Collection of ops +*/ +class FineDiffOps { + public function appendOpcode($opcode, $from, $from_offset, $from_len) { + if ( $opcode === 'c' ) { + $edits[] = new FineDiffCopyOp($from_len); + } + else if ( $opcode === 'd' ) { + $edits[] = new FineDiffDeleteOp($from_len); + } + else /* if ( $opcode === 'i' ) */ { + $edits[] = new FineDiffInsertOp(substr($from, $from_offset, $from_len)); + } + } + public $edits = array(); + } + +/** +* FineDiff class +* +* TODO: Document +* +*/ +class FineDiff { + + /**------------------------------------------------------------------------ + * + * Public section + * + */ + + /** + * Constructor + * ... + * The $granularityStack allows FineDiff to be configurable so that + * a particular stack tailored to the specific content of a document can + * be passed. + */ + public function __construct($from_text = '', $to_text = '', $granularityStack = null) { + // setup stack for generic text documents by default + $this->granularityStack = $granularityStack ? $granularityStack : FineDiff::$characterGranularity; + $this->edits = array(); + $this->from_text = $from_text; + $this->doDiff($from_text, $to_text); + } + + public function getOps() { + return $this->edits; + } + + public function getOpcodes() { + $opcodes = array(); + foreach ( $this->edits as $edit ) { + $opcodes[] = $edit->getOpcode(); + } + return implode('', $opcodes); + } + + public function renderDiffToHTML() { + $in_offset = 0; + ob_start(); + foreach ( $this->edits as $edit ) { + $n = $edit->getFromLen(); + if ( $edit instanceof FineDiffCopyOp ) { + FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n); + } + else if ( $edit instanceof FineDiffDeleteOp ) { + FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n); + } + else if ( $edit instanceof FineDiffInsertOp ) { + FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen()); + } + else /* if ( $edit instanceof FineDiffReplaceOp ) */ { + FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n); + FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen()); + } + $in_offset += $n; + } + return ob_get_clean(); + } + + /**------------------------------------------------------------------------ + * Return an opcodes string describing the diff between a "From" and a + * "To" string + */ + public static function getDiffOpcodes($from, $to, $granularities = null) { + $diff = new FineDiff($from, $to, $granularities); + return $diff->getOpcodes(); + } + + /**------------------------------------------------------------------------ + * Return an iterable collection of diff ops from an opcodes string + */ + public static function getDiffOpsFromOpcodes($opcodes) { + $diffops = new FineDiffOps(); + FineDiff::renderFromOpcodes(null, $opcodes, array($diffops,'appendOpcode')); + return $diffops->edits; + } + + /**------------------------------------------------------------------------ + * Re-create the "To" string from the "From" string and an "Opcodes" string + */ + public static function renderToTextFromOpcodes($from, $opcodes) { + ob_start(); + FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode')); + return ob_get_clean(); + } + + /**------------------------------------------------------------------------ + * Render the diff to an HTML string + */ + public static function renderDiffToHTMLFromOpcodes($from, $opcodes) { + ob_start(); + FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode')); + return ob_get_clean(); + } + + /**------------------------------------------------------------------------ + * Generic opcodes parser, user must supply callback for handling + * single opcode + */ + public static function renderFromOpcodes($from, $opcodes, $callback) { + if ( !is_callable($callback) ) { + return; + } + $opcodes_len = strlen($opcodes); + $from_offset = $opcodes_offset = 0; + while ( $opcodes_offset < $opcodes_len ) { + $opcode = substr($opcodes, $opcodes_offset, 1); + $opcodes_offset++; + $n = intval(substr($opcodes, $opcodes_offset)); + if ( $n ) { + $opcodes_offset += strlen(strval($n)); + } + else { + $n = 1; + } + if ( $opcode === 'c' ) { // copy n characters from source + call_user_func($callback, 'c', $from, $from_offset, $n, ''); + $from_offset += $n; + } + else if ( $opcode === 'd' ) { // delete n characters from source + call_user_func($callback, 'd', $from, $from_offset, $n, ''); + $from_offset += $n; + } + else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes + call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n); + $opcodes_offset += 1 + $n; + } + } + } + + /** + * Stock granularity stacks and delimiters + */ + + const paragraphDelimiters = "\n\r"; + public static $paragraphGranularity = array( + FineDiff::paragraphDelimiters + ); + const sentenceDelimiters = ".\n\r"; + public static $sentenceGranularity = array( + FineDiff::paragraphDelimiters, + FineDiff::sentenceDelimiters + ); + const wordDelimiters = " \t.\n\r"; + public static $wordGranularity = array( + FineDiff::paragraphDelimiters, + FineDiff::sentenceDelimiters, + FineDiff::wordDelimiters + ); + const characterDelimiters = ""; + public static $characterGranularity = array( + FineDiff::paragraphDelimiters, + FineDiff::sentenceDelimiters, + FineDiff::wordDelimiters, + FineDiff::characterDelimiters + ); + + public static $textStack = array( + ".", + " \t.\n\r", + "" + ); + + /**------------------------------------------------------------------------ + * + * Private section + * + */ + + /** + * Entry point to compute the diff. + */ + private function doDiff($from_text, $to_text) { + $this->last_edit = false; + $this->stackpointer = 0; + $this->from_text = $from_text; + $this->from_offset = 0; + // can't diff without at least one granularity specifier + if ( empty($this->granularityStack) ) { + return; + } + $this->_processGranularity($from_text, $to_text); + } + + /** + * This is the recursive function which is responsible for + * handling/increasing granularity. + * + * Incrementally increasing the granularity is key to compute the + * overall diff in a very efficient way. + */ + private function _processGranularity($from_segment, $to_segment) { + $delimiters = $this->granularityStack[$this->stackpointer++]; + $has_next_stage = $this->stackpointer < count($this->granularityStack); + foreach ( FineDiff::doFragmentDiff($from_segment, $to_segment, $delimiters) as $fragment_edit ) { + // increase granularity + if ( $fragment_edit instanceof FineDiffReplaceOp && $has_next_stage ) { + $this->_processGranularity( + substr($this->from_text, $this->from_offset, $fragment_edit->getFromLen()), + $fragment_edit->getText() + ); + } + // fuse copy ops whenever possible + else if ( $fragment_edit instanceof FineDiffCopyOp && $this->last_edit instanceof FineDiffCopyOp ) { + $this->edits[count($this->edits)-1]->increase($fragment_edit->getFromLen()); + $this->from_offset += $fragment_edit->getFromLen(); + } + else { + /* $fragment_edit instanceof FineDiffCopyOp */ + /* $fragment_edit instanceof FineDiffDeleteOp */ + /* $fragment_edit instanceof FineDiffInsertOp */ + $this->edits[] = $this->last_edit = $fragment_edit; + $this->from_offset += $fragment_edit->getFromLen(); + } + } + $this->stackpointer--; + } + + /** + * This is the core algorithm which actually perform the diff itself, + * fragmenting the strings as per specified delimiters. + * + * This function is naturally recursive, however for performance purpose + * a local job queue is used instead of outright recursivity. + */ + private static function doFragmentDiff($from_text, $to_text, $delimiters) { + // Empty delimiter means character-level diffing. + // In such case, use code path optimized for character-level + // diffing. + if ( empty($delimiters) ) { + return FineDiff::doCharDiff($from_text, $to_text); + } + + $result = array(); + + // fragment-level diffing + $from_text_len = strlen($from_text); + $to_text_len = strlen($to_text); + $from_fragments = FineDiff::extractFragments($from_text, $delimiters); + $to_fragments = FineDiff::extractFragments($to_text, $delimiters); + + $jobs = array(array(0, $from_text_len, 0, $to_text_len)); + + $cached_array_keys = array(); + + while ( $job = array_pop($jobs) ) { + + // get the segments which must be diff'ed + list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job; + + // catch easy cases first + $from_segment_length = $from_segment_end - $from_segment_start; + $to_segment_length = $to_segment_end - $to_segment_start; + if ( !$from_segment_length || !$to_segment_length ) { + if ( $from_segment_length ) { + $result[$from_segment_start * 4] = new FineDiffDeleteOp($from_segment_length); + } + else if ( $to_segment_length ) { + $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_length)); + } + continue; + } + + // find longest copy operation for the current segments + $best_copy_length = 0; + + $from_base_fragment_index = $from_segment_start; + + $cached_array_keys_for_current_segment = array(); + + while ( $from_base_fragment_index < $from_segment_end ) { + $from_base_fragment = $from_fragments[$from_base_fragment_index]; + $from_base_fragment_length = strlen($from_base_fragment); + // performance boost: cache array keys + if ( !isset($cached_array_keys_for_current_segment[$from_base_fragment]) ) { + if ( !isset($cached_array_keys[$from_base_fragment]) ) { + $to_all_fragment_indices = $cached_array_keys[$from_base_fragment] = array_keys($to_fragments, $from_base_fragment, true); + } + else { + $to_all_fragment_indices = $cached_array_keys[$from_base_fragment]; + } + // get only indices which falls within current segment + if ( $to_segment_start > 0 || $to_segment_end < $to_text_len ) { + $to_fragment_indices = array(); + foreach ( $to_all_fragment_indices as $to_fragment_index ) { + if ( $to_fragment_index < $to_segment_start ) { continue; } + if ( $to_fragment_index >= $to_segment_end ) { break; } + $to_fragment_indices[] = $to_fragment_index; + } + $cached_array_keys_for_current_segment[$from_base_fragment] = $to_fragment_indices; + } + else { + $to_fragment_indices = $to_all_fragment_indices; + } + } + else { + $to_fragment_indices = $cached_array_keys_for_current_segment[$from_base_fragment]; + } + // iterate through collected indices + foreach ( $to_fragment_indices as $to_base_fragment_index ) { + $fragment_index_offset = $from_base_fragment_length; + // iterate until no more match + for (;;) { + $fragment_from_index = $from_base_fragment_index + $fragment_index_offset; + if ( $fragment_from_index >= $from_segment_end ) { + break; + } + $fragment_to_index = $to_base_fragment_index + $fragment_index_offset; + if ( $fragment_to_index >= $to_segment_end ) { + break; + } + if ( $from_fragments[$fragment_from_index] !== $to_fragments[$fragment_to_index] ) { + break; + } + $fragment_length = strlen($from_fragments[$fragment_from_index]); + $fragment_index_offset += $fragment_length; + } + if ( $fragment_index_offset > $best_copy_length ) { + $best_copy_length = $fragment_index_offset; + $best_from_start = $from_base_fragment_index; + $best_to_start = $to_base_fragment_index; + } + } + $from_base_fragment_index += strlen($from_base_fragment); + // If match is larger than half segment size, no point trying to find better + // TODO: Really? + if ( $best_copy_length >= $from_segment_length / 2) { + break; + } + // no point to keep looking if what is left is less than + // current best match + if ( $from_base_fragment_index + $best_copy_length >= $from_segment_end ) { + break; + } + } + + if ( $best_copy_length ) { + $jobs[] = array($from_segment_start, $best_from_start, $to_segment_start, $best_to_start); + $result[$best_from_start * 4 + 2] = new FineDiffCopyOp($best_copy_length); + $jobs[] = array($best_from_start + $best_copy_length, $from_segment_end, $best_to_start + $best_copy_length, $to_segment_end); + } + else { + $result[$from_segment_start * 4 ] = new FineDiffReplaceOp($from_segment_length, substr($to_text, $to_segment_start, $to_segment_length)); + } + } + + ksort($result, SORT_NUMERIC); + return array_values($result); + } + + /** + * Perform a character-level diff. + * + * The algorithm is quite similar to doFragmentDiff(), except that + * the code path is optimized for character-level diff -- strpos() is + * used to find out the longest common subequence of characters. + * + * We try to find a match using the longest possible subsequence, which + * is at most the length of the shortest of the two strings, then incrementally + * reduce the size until a match is found. + * + * I still need to study more the performance of this function. It + * appears that for long strings, the generic doFragmentDiff() is more + * performant. For word-sized strings, doCharDiff() is somewhat more + * performant. + */ + private static function doCharDiff($from_text, $to_text) { + $result = array(); + $jobs = array(array(0, strlen($from_text), 0, strlen($to_text))); + while ( $job = array_pop($jobs) ) { + // get the segments which must be diff'ed + list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job; + $from_segment_len = $from_segment_end - $from_segment_start; + $to_segment_len = $to_segment_end - $to_segment_start; + + // catch easy cases first + if ( !$from_segment_len || !$to_segment_len ) { + if ( $from_segment_len ) { + $result[$from_segment_start * 4 + 0] = new FineDiffDeleteOp($from_segment_len); + } + else if ( $to_segment_len ) { + $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_len)); + } + continue; + } + if ( $from_segment_len >= $to_segment_len ) { + $copy_len = $to_segment_len; + while ( $copy_len ) { + $to_copy_start = $to_segment_start; + $to_copy_start_max = $to_segment_end - $copy_len; + while ( $to_copy_start <= $to_copy_start_max ) { + $from_copy_start = strpos(substr($from_text, $from_segment_start, $from_segment_len), substr($to_text, $to_copy_start, $copy_len)); + if ( $from_copy_start !== false ) { + $from_copy_start += $from_segment_start; + break 2; + } + $to_copy_start++; + } + $copy_len--; + } + } + else { + $copy_len = $from_segment_len; + while ( $copy_len ) { + $from_copy_start = $from_segment_start; + $from_copy_start_max = $from_segment_end - $copy_len; + while ( $from_copy_start <= $from_copy_start_max ) { + $to_copy_start = strpos(substr($to_text, $to_segment_start, $to_segment_len), substr($from_text, $from_copy_start, $copy_len)); + if ( $to_copy_start !== false ) { + $to_copy_start += $to_segment_start; + break 2; + } + $from_copy_start++; + } + $copy_len--; + } + } + // match found + if ( $copy_len ) { + $jobs[] = array($from_segment_start, $from_copy_start, $to_segment_start, $to_copy_start); + $result[$from_copy_start * 4 + 2] = new FineDiffCopyOp($copy_len); + $jobs[] = array($from_copy_start + $copy_len, $from_segment_end, $to_copy_start + $copy_len, $to_segment_end); + } + // no match, so delete all, insert all + else { + $result[$from_segment_start * 4] = new FineDiffReplaceOp($from_segment_len, substr($to_text, $to_segment_start, $to_segment_len)); + } + } + ksort($result, SORT_NUMERIC); + return array_values($result); + } + + /** + * Efficiently fragment the text into an array according to + * specified delimiters. + * No delimiters means fragment into single character. + * The array indices are the offset of the fragments into + * the input string. + * A sentinel empty fragment is always added at the end. + * Careful: No check is performed as to the validity of the + * delimiters. + */ + private static function extractFragments($text, $delimiters) { + // special case: split into characters + if ( empty($delimiters) ) { + $chars = str_split($text, 1); + $chars[strlen($text)] = ''; + return $chars; + } + $fragments = array(); + $start = $end = 0; + for (;;) { + $end += strcspn($text, $delimiters, $end); + $end += strspn($text, $delimiters, $end); + if ( $end === $start ) { + break; + } + $fragments[$start] = substr($text, $start, $end - $start); + $start = $end; + } + $fragments[$start] = ''; + return $fragments; + } + + /** + * Stock opcode renderers + */ + private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) { + if ( $opcode === 'c' || $opcode === 'i' ) { + echo substr($from, $from_offset, $from_len); + } + } + + private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) { + if ( $opcode === 'c' ) { + echo htmlentities(substr($from, $from_offset, $from_len)); + } + else if ( $opcode === 'd' ) { + $deletion = substr($from, $from_offset, $from_len); + if ( strcspn($deletion, " \n\r") === 0 ) { + $deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion); + } + echo '', htmlentities($deletion), ''; + } + else /* if ( $opcode === 'i' ) */ { + echo '', htmlentities(substr($from, $from_offset, $from_len)), ''; + } + } + } + diff --git a/public/css/module.less b/public/css/module.less index d535338..fd52c9a 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -9,6 +9,10 @@ h1 { font-weight: normal; } +.controls h1 form { + display: inline; +} + h1 a:focus { outline: none; } @@ -52,11 +56,6 @@ table.bp { } } -/* We try our best to avoid flickering */ -table.bp th, table.bp td a { -/* SHIT margin-bottom: 5px; */ -} - table.bp th { font-weight: bold; } @@ -123,7 +122,7 @@ table.bp.process { } } -table.bp th > a, table.bp td > a { +table.bp th > a, table.bp td > a, table.bp td > span { display: block; text-decoration: none; } @@ -174,26 +173,28 @@ table.bp { border-color: transparent; } -table.bp tr, table.bp tbody, table.bp th, table.bp td, table.bp.node td > a { +table.bp tr, table.bp tbody, table.bp th, table.bp td, table.bp.node td > a, table.node.missing td > span { border-width: 0; border-style: inherit; border-color: inherit; } -table.bp td > a { +table.bp td > a, table.node.missing td > span { line-height: 2em; padding-left: 0.5em; display: block; } -table.bp.node td > a:last-child { +table.bp.node td > a:last-child, table.node.missing td > span { padding-left: 2.5em; background-repeat: no-repeat; background-position: 0.5em 0.5em; border-left-width: 0.8em; } -table.bp.node.handled td > a:last-child, table.bp.node.ok td > a:last-child { +table.bp.node.handled td > a:last-child, table.bp.node.ok td > a:last-child, + table.node.missing td > span +{ border-left-width: 0.3em; background-position: 1em 0.5em; padding-left: 3em; @@ -214,6 +215,7 @@ table.bp.operator > tbody > tr:first-child > * { border-top-width: 1px; border-top-style: dotted; } + table.bp.operator.hovered > tbody > tr:first-child > * { border-top-style: solid; } @@ -227,6 +229,7 @@ table.bp { &.critical.handled { border-color: @colorCriticalHandled; } &.unknown { border-color: @colorUnknown; } &.unknown.handled { border-color: @colorUnknownHandled; } + &.pending { border-color: @colorPending; } &.missing { border-color: #ccc; } &.hovered { &.ok > tbody > tr > { @@ -250,8 +253,11 @@ table.bp { &.unknown.handled > tbody > tr > { th, td > a { background-color: @colorCUnknownHandled; } } + &.pending > tbody > tr > { + th, td > a { background-color: @colorPending; } + } &.missing > tbody > tr > { - th, td > a { background-color: #ccc; } + th, td > a, td > span { background-color: #ccc; } } } } @@ -275,7 +281,7 @@ table.bp { -o-transition: @val1, @val2; -webkit-transition: @val1, @val2; } - > tbody > tr > td > a:last-child, > tbody > tr > th { + > tbody > tr > td > a:last-child, > tbody > tr > td > span, > tbody > tr > th { // Fade out .transition(color 0.5s 0.1s step-start, background-color 0.5s 0.1s ease-out @@ -283,7 +289,7 @@ table.bp { } &.hovered > tbody > tr { - > td > a:last-child, > th { + > td > a:last-child, > td > span, > th { // Fade in .transition(color 0.0s 0.0s step-start, background-color 0.0s 0.0s ease-out @@ -401,4 +407,47 @@ ul.error { color: white; padding: 0.3em 0.8em; } + + li a { + color: inherit; + } } + +table.sourcecode { + font-family: monospace; + white-space: pre-wrap; + + th { + vertical-align: top; + padding-right: 0.5em; + user-select: none; + -moz-user-select: none; + -o-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + font-weight: bold; + } + td { + vertical-align: top; + } +} + +.diff { + font-family: monospace; + white-space: pre-wrap; + + del, ins { + text-decoration: none; + } + + del { + color: @colorCritical; + background-color: #fdd; + } + + ins { + color: @colorOk; + background-color: #dfd; + } +} + diff --git a/public/js/module.js b/public/js/module.js index 8dfa2ec..a612aa3 100644 --- a/public/js/module.js +++ b/public/js/module.js @@ -29,6 +29,7 @@ this.module.on('mouseenter', 'table.bp > tbody > tr > td > a', this.procMouseOver); this.module.on('mouseenter', 'table.bp > tbody > tr > th', this.procMouseOver); + this.module.on('mouseenter', 'table.node.missing > tbody > tr > td > span', this.procMouseOver); this.module.on('mouseleave', 'div.bp', this.procMouseOut); this.module.icinga.logger.debug('BP module loaded');