diff --git a/application/clicommands/ProcessCommand.php b/application/clicommands/ProcessCommand.php index af3cc29..0d00a37 100644 --- a/application/clicommands/ProcessCommand.php +++ b/application/clicommands/ProcessCommand.php @@ -167,21 +167,22 @@ class ProcessCommand extends Command } } - protected function renderProblemTree($tree, $useColors = false, $depth = 0) + protected function renderProblemTree($tree, $useColors = false, $depth = 0, BpNode $parent = null) { $output = ''; foreach ($tree as $name => $subtree) { /** @var Node $node */ $node = $subtree['node']; + $state = $parent !== null ? $parent->getChildState($node) : $node->getState(); if ($node instanceof HostNode) { - $colors = $this->hostColors[$node->getState()]; + $colors = $this->hostColors[$state]; } else { - $colors = $this->serviceColors[$node->getState()]; + $colors = $this->serviceColors[$state]; } - $state = sprintf('[%s]', $node->getStateName()); + $state = sprintf('[%s]', $node->getStateName($state)); if ($useColors) { $state = $this->screen->colorize($state, $colors[0], $colors[1]); } @@ -193,7 +194,10 @@ class ProcessCommand extends Command $state, $node->getAlias() ); - $output .= $this->renderProblemTree($subtree['children'], $useColors, $depth + 1); + + if ($node instanceof BpNode) { + $output .= $this->renderProblemTree($subtree['children'], $useColors, $depth + 1, $node); + } } return $output; diff --git a/application/forms/AddNodeForm.php b/application/forms/AddNodeForm.php index 591f37a..df9fe66 100644 --- a/application/forms/AddNodeForm.php +++ b/application/forms/AddNodeForm.php @@ -200,6 +200,11 @@ class AddNodeForm extends QuickForm ), 'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]] ]); + + $this->addHostOverrideCheckbox(); + if ($this->getSentValue('host_override') === '1') { + $this->addHostOverrideElement(); + } } protected function selectService() @@ -207,6 +212,11 @@ class AddNodeForm extends QuickForm $this->addHostElement(); if ($host = $this->getSentValue('host')) { $this->addServicesElement($host); + $this->addServiceOverrideCheckbox(); + + if ($this->getSentValue('service_override') === '1') { + $this->addServiceOverrideElement(); + } } else { $this->setSubmitLabel($this->translate('Next')); } @@ -253,6 +263,44 @@ class AddNodeForm extends QuickForm ]); } + protected function addHostOverrideCheckbox() + { + $this->addElement('checkbox', 'host_override', [ + 'ignore' => true, + 'class' => 'autosubmit', + 'label' => $this->translate('Override Host State'), + 'description' => $this->translate('Enable host state overrides') + ]); + } + + protected function addHostOverrideElement() + { + $this->addElement('stateOverrides', 'stateOverrides', [ + 'required' => true, + 'label' => $this->translate('State Overrides'), + 'states' => $this->enumHostStateList() + ]); + } + + protected function addServiceOverrideCheckbox() + { + $this->addElement('checkbox', 'service_override', [ + 'ignore' => true, + 'class' => 'autosubmit', + 'label' => $this->translate('Override Service State'), + 'description' => $this->translate('Enable service state overrides') + ]); + } + + protected function addServiceOverrideElement() + { + $this->addElement('stateOverrides', 'stateOverrides', [ + 'required' => true, + 'label' => $this->translate('State Overrides'), + 'states' => $this->enumServiceStateList() + ]); + } + protected function selectProcess() { if ($this->hasParentNode()) { @@ -372,6 +420,17 @@ class AddNodeForm extends QuickForm return $res; } + protected function enumHostStateList() + { + $hostStateList = [ + 0 => $this->translate('UP'), + 1 => $this->translate('DOWN'), + 99 => $this->translate('PENDING') + ]; + + return $hostStateList; + } + protected function enumServiceList($host) { $names = $this->backend @@ -391,6 +450,19 @@ class AddNodeForm extends QuickForm return $services; } + protected function enumServiceStateList() + { + $serviceStateList = [ + 0 => $this->translate('OK'), + 1 => $this->translate('WARNING'), + 2 => $this->translate('CRITICAL'), + 3 => $this->translate('UNKNOWN'), + 99 => $this->translate('PENDING'), + ]; + + return $serviceStateList; + } + protected function hasProcesses() { return count($this->enumProcesses()) > 0; @@ -466,6 +538,19 @@ class AddNodeForm extends QuickForm switch ($this->getValue('node_type')) { case 'host': case 'service': + $stateOverrides = $this->getValue('stateOverrides'); + if (! empty($stateOverrides)) { + $childOverrides = []; + foreach ($this->getValue('children') as $service) { + $childOverrides[$service] = $stateOverrides; + } + + $changes->modifyNode($this->parent, [ + 'stateOverrides' => array_merge($this->parent->getStateOverrides(), $childOverrides) + ]); + } + + // Fallthrough case 'process': if ($this->hasParentNode()) { $changes->addChildrenToNode($this->getValue('children'), $this->parent); diff --git a/application/forms/EditNodeForm.php b/application/forms/EditNodeForm.php index 429a8f2..588c6de 100644 --- a/application/forms/EditNodeForm.php +++ b/application/forms/EditNodeForm.php @@ -182,6 +182,14 @@ class EditNodeForm extends QuickForm 'description' => $this->translate('The host for this business process node'), 'validators' => [[new NoDuplicateChildrenValidator($this, $this->bp, $this->parent), true]] )); + + $this->addHostOverrideCheckbox(); + $hostOverrideSent = $this->getSentValue('host_override'); + if ($hostOverrideSent === '1' + || ($hostOverrideSent === null && $this->getElement('host_override')->isChecked()) + ) { + $this->addHostOverrideElement(); + } } protected function selectService() @@ -190,8 +198,16 @@ class EditNodeForm extends QuickForm if ($this->getSentValue('hosts') === null) { $this->addServicesElement($this->host); + $this->addServiceOverrideCheckbox(); + if ($this->getElement('service_override')->isChecked() || $this->getSentValue('service_override') === '1') { + $this->addServiceOverrideElement(); + } } elseif ($host = $this->getSentValue('hosts')) { $this->addServicesElement($host); + $this->addServiceOverrideCheckbox(); + if ($this->getSentValue('service_override') === '1') { + $this->addServiceOverrideElement(); + } } else { $this->setSubmitLabel($this->translate('Next')); } @@ -210,6 +226,27 @@ class EditNodeForm extends QuickForm $this->getElement('hosts')->setValue($this->host); } + protected function addHostOverrideCheckbox() + { + $this->addElement('checkbox', 'host_override', [ + 'ignore' => true, + 'class' => 'autosubmit', + 'value' => ! empty($this->parent->getStateOverrides($this->node->getName())), + 'label' => $this->translate('Override Host State'), + 'description' => $this->translate('Enable host state overrides') + ]); + } + + protected function addHostOverrideElement() + { + $this->addElement('stateOverrides', 'stateOverrides', [ + 'required' => true, + 'states' => $this->enumHostStateList(), + 'value' => $this->parent->getStateOverrides($this->node->getName()), + 'label' => $this->translate('State Overrides') + ]); + } + protected function addServicesElement($host) { $this->addElement('select', 'children', array( @@ -222,6 +259,27 @@ class EditNodeForm extends QuickForm )); } + protected function addServiceOverrideCheckbox() + { + $this->addElement('checkbox', 'service_override', [ + 'ignore' => true, + 'class' => 'autosubmit', + 'value' => ! empty($this->parent->getStateOverrides($this->node->getName())), + 'label' => $this->translate('Override Service State'), + 'description' => $this->translate('Enable service state overrides') + ]); + } + + protected function addServiceOverrideElement() + { + $this->addElement('stateOverrides', 'stateOverrides', [ + 'required' => true, + 'states' => $this->enumServiceStateList(), + 'value' => $this->parent->getStateOverrides($this->node->getName()), + 'label' => $this->translate('State Overrides') + ]); + } + protected function selectProcess() { $this->addElement('multiselect', 'children', array( @@ -322,6 +380,17 @@ class EditNodeForm extends QuickForm return $res; } + protected function enumHostStateList() + { + $hostStateList = [ + 0 => $this->translate('UP'), + 1 => $this->translate('DOWN'), + 99 => $this->translate('PENDING') + ]; + + return $hostStateList; + } + protected function enumServiceList($host) { $names = $this->backend @@ -341,6 +410,19 @@ class EditNodeForm extends QuickForm return $services; } + protected function enumServiceStateList() + { + $serviceStateList = [ + 0 => $this->translate('OK'), + 1 => $this->translate('WARNING'), + 2 => $this->translate('CRITICAL'), + 3 => $this->translate('UNKNOWN'), + 99 => $this->translate('PENDING'), + ]; + + return $serviceStateList; + } + protected function hasProcesses() { return count($this->enumProcesses()) > 0; @@ -407,6 +489,19 @@ class EditNodeForm extends QuickForm switch ($this->getValue('node_type')) { case 'host': case 'service': + $stateOverrides = $this->getValue('stateOverrides') ?: []; + if (! empty($stateOverrides)) { + $stateOverrides = array_merge( + $this->parent->getStateOverrides(), + [$this->getValue('children') => $stateOverrides] + ); + } else { + $stateOverrides = $this->parent->getStateOverrides(); + unset($stateOverrides[$this->getValue('children')]); + } + + $changes->modifyNode($this->parent, ['stateOverrides' => $stateOverrides]); + // Fallthrough case 'process': $changes->addChildrenToNode($this->getValue('children'), $this->parent); break; diff --git a/application/views/helpers/FormStateOverrides.php b/application/views/helpers/FormStateOverrides.php new file mode 100644 index 0000000..74ed2f4 --- /dev/null +++ b/application/views/helpers/FormStateOverrides.php @@ -0,0 +1,40 @@ + $label) { + if ($state === 0) { + continue; + } + + $chosen = $state; + if (isset($value[$state])) { + $chosen = $value[$state]; + } + + $options = [$state => t('Keep actual state')] + $states; + + $html .= ''; + } + + return $html; + } +} diff --git a/doc/07-State-Overrides.md b/doc/07-State-Overrides.md new file mode 100644 index 0000000..dfdbaeb --- /dev/null +++ b/doc/07-State-Overrides.md @@ -0,0 +1,45 @@ +# State Overrides + +Business processes utilize their children's states to calculate their own state. +While you can influence this with [operators](09-Operators.md), it's also possible +to override individual states. (This applies to host and service nodes.) + +## Configuring Overrides + +State overrides get configured per node. When adding or editing a node, you can +define which state should be overridden with another one. + +Below `WARNING` is chosen as a replacement for `CRITICAL`. + +![Service State Override Configuration](screenshot/07_state_overrides/0701_override_config.png "Service State Override Configuration") + +## Identifying Overrides + +In tile view overridden states are indicated by an additional state ball in the +lower left of a tile. This is then the actual state the object is in. + +![Overridden Tile State](screenshot/07_state_overrides/0702_overridden_tile.png "Overridden Tile State") + +In tree view overridden states are indicated on the very right of a row. There +the actual state is shown and which one it is replaced with. + +![Overridden Tree State](screenshot/07_state_overrides/0703_overridden_tree.png "Overridden Tree State") + +## File Format Extensions + +The configuration file format has slightly been changed to accommodate state +overrides. Though, previous configurations are perfectly upwards compatible. + +### New Extra Line + +For process nodes a new extra line is used to store state overrides. + +``` +state_overrides dev_database_servers!mysql;mysql|2-1 +``` + +The full syntax for this is as follows: + +``` +state_overrides !|n-n[!|n-n[,n-n]] +``` diff --git a/doc/screenshot/07_state_overrides/0701_override_config.png b/doc/screenshot/07_state_overrides/0701_override_config.png new file mode 100644 index 0000000..49ca2ad Binary files /dev/null and b/doc/screenshot/07_state_overrides/0701_override_config.png differ diff --git a/doc/screenshot/07_state_overrides/0702_overridden_tile.png b/doc/screenshot/07_state_overrides/0702_overridden_tile.png new file mode 100644 index 0000000..db2012e Binary files /dev/null and b/doc/screenshot/07_state_overrides/0702_overridden_tile.png differ diff --git a/doc/screenshot/07_state_overrides/0703_overridden_tree.png b/doc/screenshot/07_state_overrides/0703_overridden_tree.png new file mode 100644 index 0000000..ccf4fde Binary files /dev/null and b/doc/screenshot/07_state_overrides/0703_overridden_tree.png differ diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php index 9aeaab8..58a7c11 100644 --- a/library/Businessprocess/BpNode.php +++ b/library/Businessprocess/BpNode.php @@ -26,6 +26,7 @@ class BpNode extends Node protected $missing = null; protected $empty = null; protected $missingChildren; + protected $stateOverrides = []; protected static $emptyStateSummary = array( 'OK' => 0, @@ -71,7 +72,7 @@ class BpNode extends Node } elseif ($child->isMissing()) { $this->counters['MISSING']++; } else { - $state = $child->getStateName(); + $state = $child->getStateName($this->getChildState($child)); $this->counters[$state]++; } } @@ -127,9 +128,13 @@ class BpNode extends Node $problems = array(); foreach ($this->getChildren() as $child) { - if ($child->isProblem() - || ($child instanceof BpNode && $child->hasProblems()) - ) { + if (isset($this->stateOverrides[$child->getName()])) { + $problem = $this->getChildState($child) > 0; + } else { + $problem = $child->isProblem() || ($child instanceof BpNode && $child->hasProblems()); + } + + if ($problem) { $problems[] = $child; } } @@ -187,7 +192,8 @@ class BpNode extends Node if ($nodeState !== 0) { foreach ($this->getChildren() as $child) { - $childState = $rootCause ? $child->getSortingState() : $child->getState(); + $childState = $this->getChildState($child); + $childState = $rootCause ? $child->getSortingState($childState) : $childState; if (($rootCause ? $this->getSortingState() : $nodeState) === $childState) { $name = $child->getName(); $tree[$name] = [ @@ -327,6 +333,31 @@ class BpNode extends Node return $this->info_command; } + public function setStateOverrides(array $overrides, $name = null) + { + if ($name === null) { + $this->stateOverrides = $overrides; + } else { + $this->stateOverrides[$name] = $overrides; + } + + return $this; + } + + public function getStateOverrides($name = null) + { + $overrides = null; + if ($name !== null) { + if (isset($this->stateOverrides[$name])) { + $overrides = $this->stateOverrides[$name]; + } + } else { + $overrides = $this->stateOverrides; + } + + return $overrides; + } + public function getAlias() { return $this->alias ? preg_replace('~_~', ' ', $this->alias) : $this->name; @@ -354,6 +385,27 @@ class BpNode extends Node return $this->state; } + /** + * Get the given child's state, possibly adjusted by override rules + * + * @param Node|string $child + * @return int + */ + public function getChildState($child) + { + if (! $child instanceof Node) { + $child = $this->getChildByName($child); + } + + $childName = $child->getName(); + $childState = $child->getState(); + if (! isset($this->stateOverrides[$childName][$childState])) { + return $childState; + } + + return $this->stateOverrides[$childName][$childState]; + } + public function getHtmlId() { return 'businessprocess-' . preg_replace('/[\r\n\t\s]/', '_', $this->getName()); @@ -391,7 +443,7 @@ class BpNode extends Node $child->setMissing(); } - $sort_states[] = $child->getSortingState(); + $sort_states[] = $child->getSortingState($this->getChildState($child)); $lastStateChange = max($lastStateChange, $child->getLastStateChange()); $bp->endLoopDetection($this->name); } diff --git a/library/Businessprocess/Node.php b/library/Businessprocess/Node.php index 7849a7c..f22339b 100644 --- a/library/Businessprocess/Node.php +++ b/library/Businessprocess/Node.php @@ -249,9 +249,11 @@ abstract class Node return $this->state; } - public function getSortingState() + public function getSortingState($state = null) { - $state = $this->getState(); + if ($state === null) { + $state = $this->getState(); + } if (self::$ackIsOk && $this->isAcknowledged()) { $state = self::ICINGA_OK; diff --git a/library/Businessprocess/Renderer/Renderer.php b/library/Businessprocess/Renderer/Renderer.php index 4c9a8de..35e3225 100644 --- a/library/Businessprocess/Renderer/Renderer.php +++ b/library/Businessprocess/Renderer/Renderer.php @@ -180,9 +180,9 @@ abstract class Renderer extends HtmlDocument if ($node->isEmpty() && ! $node instanceof MonitoredNode) { $classes = array('empty'); } else { - $classes = array( - strtolower($node->getStateName()) - ); + $classes = [strtolower($node->getStateName( + $this->parent !== null ? $this->parent->getChildState($node) : null + ))]; } if ($node->hasMissingChildren()) { $classes[] = 'missing-children'; diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php index 8c16ed5..ef26693 100644 --- a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -113,6 +113,18 @@ class NodeTile extends BaseHtmlElement )); } + if ($this->renderer->rendersSubNode() + && $this->renderer->getParentNode()->getChildState($node) !== $node->getState() + ) { + $this->add((new StateBall(strtolower($node->getStateName())))->addAttributes([ + 'class' => 'overridden-state', + 'title' => sprintf( + '%s', + $node->getStateName() + ) + ])); + } + if ($node instanceof BpNode && !$renderer->isBreadcrumb()) { $this->add(Html::tag( 'p', diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php index 9322c26..efe8d77 100644 --- a/library/Businessprocess/Renderer/TreeRenderer.php +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -79,7 +79,7 @@ class TreeRenderer extends Renderer if ($node instanceof BpNode) { $html[] = $this->renderNode($bp, $node); } else { - $html[] = $this->renderChild($bp, $node); + $html[] = $this->renderChild($bp, $this->parent, $node); } } @@ -106,9 +106,10 @@ class TreeRenderer extends Renderer /** * @param Node $node * @param array $path + * @param BpNode $parent * @return BaseHtmlElement[] */ - public function getNodeIcons(Node $node, array $path = null) + public function getNodeIcons(Node $node, array $path = null, BpNode $parent = null) { $icons = []; if (empty($path) && $node instanceof BpNode) { @@ -116,7 +117,7 @@ class TreeRenderer extends Renderer } else { $icons[] = $node->getIcon(); } - $state = strtolower($node->getStateName()); + $state = strtolower($node->getStateName($parent !== null ? $parent->getChildState($node) : null)); if ($node->isHandled()) { $state = $state . '-handled'; } @@ -136,6 +137,27 @@ class TreeRenderer extends Renderer return $icons; } + public function getOverriddenState($fakeState, Node $node) + { + $overriddenState = Html::tag('div', ['class' => 'overridden-state']); + $overriddenState->add((new StateBall(strtolower($node->getStateName())))->addAttributes([ + 'title' => sprintf( + '%s', + $node->getStateName() + ) + ])); + $overriddenState->add(Html::tag('i', ['class' => 'icon icon-right-small'])); + $overriddenState->add((new StateBall(strtolower($node->getStateName($fakeState))))->addAttributes([ + 'title' => sprintf( + '%s', + $node->getStateName($fakeState) + ), + 'class' => 'last' + ])); + + return $overriddenState; + } + /** * @param BpConfig $bp * @param Node $node @@ -222,14 +244,14 @@ class TreeRenderer extends Renderer if ($child instanceof BpNode) { $ul->add($this->renderNode($bp, $child, $path)); } else { - $ul->add($this->renderChild($bp, $child, $path)); + $ul->add($this->renderChild($bp, $node, $child, $path)); } } return $li; } - protected function renderChild($bp, Node $node, $path = null) + protected function renderChild($bp, BpNode $parent, Node $node, $path = null) { $li = Html::tag('li', [ 'class' => 'movable', @@ -237,12 +259,16 @@ class TreeRenderer extends Renderer 'data-node-name' => $node->getName() ]); - $li->add($this->getNodeIcons($node, $path)); + $li->add($this->getNodeIcons($node, $path, $parent)); $link = $node->getLink(); $link->getAttributes()->set('data-base-target', '_next'); $li->add($link); + if (($overriddenState = $parent->getChildState($node)) !== $node->getState()) { + $li->add($this->getOverriddenState($overriddenState, $node)); + } + if (! $this->isLocked() && $node->getBpConfig()->getName() === $this->getBusinessProcess()->getName()) { $li->add($this->getActionIcons($bp, $node)); } diff --git a/library/Businessprocess/Storage/LegacyConfigParser.php b/library/Businessprocess/Storage/LegacyConfigParser.php index d4325cf..4142731 100644 --- a/library/Businessprocess/Storage/LegacyConfigParser.php +++ b/library/Businessprocess/Storage/LegacyConfigParser.php @@ -233,6 +233,24 @@ class LegacyConfigParser $bp->getBpNode($name)->setInfoUrl($url); } + protected function parseStateOverrides(&$line, BpConfig $bp) + { + // state_overrides !|n-n[,n-n]!|n-n[,n-n] + $segments = preg_split('~\s*!\s*~', substr($line, 16)); + $node = $bp->getNode(array_shift($segments)); + foreach ($segments as $overrideDef) { + list($childName, $overrides) = preg_split('~\s*\|\s*~', $overrideDef, 2); + + $stateOverrides = []; + foreach (preg_split('~\s*,\s*~', $overrides) as $override) { + list($from, $to) = preg_split('~\s*-\s*~', $override, 2); + $stateOverrides[(int) $from] = (int) $to; + } + + $node->setStateOverrides($stateOverrides, $childName); + } + } + protected function parseExtraLine(&$line, $typeLength, BpConfig $bp) { $type = substr($line, 0, $typeLength); @@ -251,6 +269,9 @@ class LegacyConfigParser case 'info_url': $this->parseInfoUrl($line, $bp); break; + case 'state_overrides': + $this->parseStateOverrides($line, $bp); + break; case 'template': // compat, ignoring for now break; @@ -282,9 +303,9 @@ class LegacyConfigParser return; } - // Space found in the first 14 cols? Might be a line with extra information + // Space found in the first 16 cols? Might be a line with extra information $pos = strpos($line, ' '); - if ($pos !== false && $pos < 14) { + if ($pos !== false && $pos < 16) { if ($this->parseExtraLine($line, $pos, $bp)) { return; } diff --git a/library/Businessprocess/Storage/LegacyConfigRenderer.php b/library/Businessprocess/Storage/LegacyConfigRenderer.php index 77a1d13..ebe9589 100644 --- a/library/Businessprocess/Storage/LegacyConfigRenderer.php +++ b/library/Businessprocess/Storage/LegacyConfigRenderer.php @@ -158,6 +158,7 @@ class LegacyConfigRenderer public static function renderSingleBpNode(BpNode $node) { return static::renderExpression($node) + . static::renderStateOverrides($node) . static::renderDisplay($node) . static::renderInfoUrl($node); } @@ -214,6 +215,27 @@ class LegacyConfigRenderer } } + public static function renderStateOverrides(BpNode $node) + { + $stateOverrides = ''; + foreach ($node->getStateOverrides() as $childName => $overrideRules) { + $overrides = []; + foreach ($overrideRules as $from => $to) { + $overrides[] = sprintf('%d-%d', $from, $to); + } + + if (! empty($overrides)) { + $stateOverrides .= '!' . $childName . '|' . join(',', $overrides); + } + } + + if (! $stateOverrides) { + return ''; + } + + return 'state_overrides ' . $node->getName() . $stateOverrides . "\n"; + } + /** * @param BpNode $node * @return string diff --git a/library/Businessprocess/Web/Form/Element/StateOverrides.php b/library/Businessprocess/Web/Form/Element/StateOverrides.php new file mode 100644 index 0000000..c2216c0 --- /dev/null +++ b/library/Businessprocess/Web/Form/Element/StateOverrides.php @@ -0,0 +1,55 @@ +states = $states; + + return $this; + } + + /** + * Get the overridable states + * + * @return array + */ + public function getStates() + { + return $this->states; + } + + public function init() + { + $this->setIsArray(true); + } + + public function setValue($value) + { + $cleanedValue = []; + + if (! empty($value)) { + foreach ($value as $from => $to) { + if ((int) $from !== (int) $to) { + $cleanedValue[$from] = $to; + } + } + } + + return parent::setValue($cleanedValue); + } +} diff --git a/public/css/module.less b/public/css/module.less index 40d71f7..3db49bf 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -214,13 +214,18 @@ ul.bp { margin-right: .5em; } - > a.action-link { + > :not(.overridden-state) + a.action-link { margin-left: auto; // Let the first action link move everything to the right & + a.action-link { margin-left: 0; // But really only the first one } } + + .overridden-state { + margin-left: auto; + margin-right: 1em; + } } // collapse handling @@ -416,6 +421,15 @@ td > a > .badges { font-size: 0.5em; } + .overridden-state { + font-size: .75em; + position: absolute; + left: 0; + bottom: 0; + margin: .5em; + border: 1px solid white; + } + > a { display: block; text-decoration: none; @@ -949,6 +963,29 @@ form dt label { } } +#stateOverrides-element { + display: inline-table; + table-layout: fixed; + border-spacing: .5em; + padding: 0; + + label { + display: table-row; + + span, select { + display: table-cell; + } + + span { + width: 10em; + } + + select { + width: 26em; + } + } +} + form fieldset { min-width: 36em; }