diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b0144..5e1e81f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ IMPORTANT NOTE: This version only works on CACTI 1.x++! * issue#322: Refresh the bundled manual with current installation, upgrade, editor, image management and troubleshooting guides; retain the configuration and data-source references and repair documentation links * issue#322: Update editor and map-management screenshots, including map creation, management, the viewer, hover graphs and Advanced settings; use compact previews with full-size links and refresh cached documentation assets * feature#322: Add themed Docs access from the map heading and a Docs button after Rebuild All in map management, opening the bundled manual in a new tab +* feature#321: Group default link width and appearance controls under Link Appearance Defaults in Map Style, and clarify Default Link Capacity in Map Properties +* feature#321: Add Edge and Centre comment styles to Map Style, with an individual override and Use map default option in Link Properties +* feature#321: Add IN/OUT traffic-label positions to Map Style and per-link Advanced settings, with 75%/25% defaults and an option to restore inheritance; keep traffic-label and comment positions separate --- 2.0 --- diff --git a/js/editor.js b/js/editor.js index eafdbb2..ee48c20 100644 --- a/js/editor.js +++ b/js/editor.js @@ -1312,6 +1312,11 @@ function prime_link_form(name) { $('#link_hover').val(wmDecodeEditorUrl(mylink.overliburl)); $('#viastyle').val(mylink.viastyle); + $('#link_commentstyle').val(mylink.commentstylechoice); + $('#link_labelposmode').val(mylink.labelposmode); + $('#link_labelposin').val(mylink.labelposin); + $('#link_labelposout').val(mylink.labelposout); + $('#link_labelposmode').trigger('change'); $('#link_commentin').val(mylink.commentin); $('#link_commentout').val(mylink.commentout); $('#link_commentposin').val(mylink.commentposin); @@ -1768,6 +1773,9 @@ function wmBuildLinkSections() { dlg.find(fields).closest('tr').appendTo(table); return panel; } + $('#link_labelposmode').on('change.wmPositions', function() { + $('#link_labelposin,#link_labelposout').prop('disabled', $(this).val() !== 'custom'); + }); var hovers = group('wm-link-hovers', wmEditorText.additionalGraphs, '#link_template,#link_picker'); dlg.find('.wm-link-section p').addClass('wm-link-group-hint').prependTo(hovers); dlg.find('.wm-link-section').remove(); @@ -1777,7 +1785,8 @@ function wmBuildLinkSections() { wmRenderLinkExtraGraphs(); $('#link_target_picker_rep').remove(); var advanced = group('wm-link-advanced', wmEditorText.advanced, - '#link_target,#link_infourl,#wm-link-primary-hover,#wm-link-extra-hover', wmEditorText.linkAdvancedHint); + '#link_labelposmode,#link_target,#link_infourl,#wm-link-primary-hover,#wm-link-extra-hover', wmEditorText.linkAdvancedHint); + $('#link_labelposmode').closest('tr').prependTo(advanced.find('table')); var sourceRow = $('').insertBefore($('#link_target').closest('tr')); $('').text(wmEditorText.chooseSource).appendTo(sourceRow); var sourceActions = $('').appendTo($('').appendTo(sourceRow)) @@ -1800,7 +1809,7 @@ function wmBuildLinkSections() { ++wmInterfaceLookupGeneration; wmSyncLinkHoverParts(); }); - dlg.find('#viastyle,#link_width,#link_commentin,#link_commentout').closest('tr') + dlg.find('#viastyle,#link_width,#link_commentin,#link_commentout,#link_commentstyle').closest('tr') .insertAfter($('#link_target_picker').closest('tr')); $('#link_edit').appendTo(controls); var help = dlg.find('.dlgHelp').attr('id', 'wm-link-full-help').insertAfter(sections).hide(); diff --git a/lib/WeatherMapLink.class.php b/lib/WeatherMapLink.class.php index 1028422..9f053b4 100644 --- a/lib/WeatherMapLink.class.php +++ b/lib/WeatherMapLink.class.php @@ -307,6 +307,8 @@ function CopyFrom(&$source) { $this->$fld = $source->$fld; } } + // These describe this link's explicit editor choices, not inherited settings. + unset($this->hints['editor_commentstyle_override'], $this->hints['editor_labelpos_override']); } /** @@ -935,7 +937,7 @@ function WriteConfig() { $keyword = $param[1]; // $output .= "# For $keyword: ".$this->$field." vs ".$dd->$field."\n"; - if ($this->$field != $dd->$field) { + if ($this->$field != $dd->$field || ($field === 'commentstyle' && !empty($this->hints['editor_commentstyle_override']))) { if ($param[2] == CONFIG_TYPE_COLOR) { $output .= TAB . "$keyword " . render_colour($this->$field) . EOL; } elseif ($param[2] == CONFIG_TYPE_LITERAL) { @@ -1027,7 +1029,7 @@ function WriteConfig() { $comparison = $dd->labeloffset_in; $comparison2 = $dd->labeloffset_out; - if ($this->labeloffset_in != $comparison || $this->labeloffset_out != $comparison2) { + if ($this->labeloffset_in != $comparison || $this->labeloffset_out != $comparison2 || !empty($this->hints['editor_labelpos_override'])) { $output .= TAB . 'BWLABELPOS ' . $this->labeloffset_in . ' ' . $this->labeloffset_out . EOL; } @@ -1107,7 +1109,7 @@ function WriteConfig() { foreach ($this->hints as $hintname=>$hint) { // all hints for DEFAULT node are for writing // only changed ones, or unique ones, otherwise - if (($this->name == 'DEFAULT') || (isset($dd->hints[$hintname]) && $dd->hints[$hintname] != $hint) || (!isset($dd->hints[$hintname]))) { + if (in_array($hintname, ['editor_commentstyle_override', 'editor_labelpos_override'], true) || ($this->name == 'DEFAULT') || (isset($dd->hints[$hintname]) && $dd->hints[$hintname] != $hint) || (!isset($dd->hints[$hintname]))) { $output .= TAB . "SET $hintname $hint" . EOL; } } @@ -1157,6 +1159,12 @@ function asJS() { $js .= 'overlibheight:"' . $this->overlibheight . '", '; $js .= 'overlibcaption:' . js_escape($this->overlibcaption[IN]) . ', '; + $base = $this->owner->links[$this->template]; + $customStyle = !empty($this->hints['editor_commentstyle_override']) || $this->commentstyle !== $base->commentstyle; + $customPositions = !empty($this->hints['editor_labelpos_override']) || $this->labeloffset_in != $base->labeloffset_in || $this->labeloffset_out != $base->labeloffset_out; + $js .= 'commentstylechoice:' . js_escape($customStyle ? $this->commentstyle : 'default') . ', '; + $js .= 'labelposmode:' . js_escape($customPositions ? 'custom' : 'default') . ', '; + $js .= 'labelposin:' . (float)$this->labeloffset_in . ', labelposout:' . (float)$this->labeloffset_out . ', '; $js .= 'commentin:' . js_escape($this->comments[IN]) . ', '; $js .= 'commentposin:' . intval($this->commentoffset_in) . ', '; diff --git a/lib/editor.actions.php b/lib/editor.actions.php index 20dcd46..2c8a606 100644 --- a/lib/editor.actions.php +++ b/lib/editor.actions.php @@ -440,6 +440,8 @@ function setLinkProperties($mapfile) { $map->links[$link_name]->commentoffset_in = intval(get_nfilter_request_var('link_commentposin')); $map->links[$link_name]->commentoffset_out = intval(get_nfilter_request_var('link_commentposout')); + wm_link_apply_overrides($map, $map->links[$link_name], get_nfilter_request_var('link_commentstyle'), get_nfilter_request_var('link_labelposmode'), get_nfilter_request_var('link_labelposin'), get_nfilter_request_var('link_labelposout')); + $targets = preg_split('/\s+/', trim(get_nfilter_request_var('link_target')), -1, PREG_SPLIT_NO_EMPTY) ?: []; $new_target_list = []; @@ -539,16 +541,6 @@ function setMapProperties($mapfile) { } - $inheritables = [ - ['link', 'width', 'map_linkdefaultwidth', 'float'] - ]; - - handle_inheritance($map, $inheritables); - - $map->links['DEFAULT']->width = intval(get_nfilter_request_var('map_linkdefaultwidth')); - - $map->links['DEFAULT']->add_note('my_width', get_filter_request_var('map_linkdefaultwidth')); - $bwin = get_nfilter_request_var('map_linkdefaultbwin'); $bwout = get_nfilter_request_var('map_linkdefaultbwout'); @@ -621,8 +613,13 @@ function setMapStyle($mapfile) { handle_inheritance($map, $inheritables); + $width = get_nfilter_request_var('mapstyle_defaultwidth'); + if (is_scalar($width) && is_numeric($width) && (float)$width > 0 && is_finite((float)$width)) { + wm_comment_update_default($map, 'width', (float)$width); + } + wm_link_apply_default_positions($map, get_nfilter_request_var('mapstyle_labelposin'), get_nfilter_request_var('mapstyle_labelposout')); wm_comment_update_default($map, 'bwfont', wm_comment_resolve_font($map, get_nfilter_request_var('mapstyle_linkfont'))); - wm_comment_apply_style($map, get_nfilter_request_var('mapstyle_commentfont'), null, isset_request_var('mapstyle_commentcurve') ? get_nfilter_request_var('mapstyle_commentcurve') : '0'); + wm_comment_apply_style($map, get_nfilter_request_var('mapstyle_commentfont'), null, isset_request_var('mapstyle_commentcurve') ? get_nfilter_request_var('mapstyle_commentcurve') : '0', get_nfilter_request_var('mapstyle_commentstyle')); if (!wm_editor_write_config($map, $mapfile)) return; } diff --git a/lib/editor.comment-style.php b/lib/editor.comment-style.php index 18f5362..218bbd0 100644 --- a/lib/editor.comment-style.php +++ b/lib/editor.comment-style.php @@ -137,7 +137,7 @@ function wm_style_font_select($map, $name, $current) { return ''; } /** - * Render the link comment font control. + * Render the link comment font, placement and curve controls. * * @param WeatherMap $map * @@ -148,6 +148,11 @@ function wm_comment_style_fields($map) { $out = ''; + $out .= '
' . html_escape(__('Edge places comments beside the link; Centre places them over the arrow.', 'weathermap')) . ''; $out .= '
' . html_escape(__('Uses a TrueType font. Tight bends or text longer than the link use straight text.', 'weathermap')) . ''; @@ -235,6 +240,12 @@ function wm_comment_update_default($map, $field, $new) { if ($new === null) { return; } + // New layout controls follow templates; retain established font/colour behaviour. + if (in_array($field, ['commentstyle', 'width'], true)) { + $marker = $field === 'commentstyle' ? 'editor_commentstyle_override' : null; + wm_link_propagate_defaults($map, [$field => $new], $marker); + return; + } $old = $map->links['DEFAULT']->$field; if ($new == $old) { @@ -251,19 +262,117 @@ function wm_comment_update_default($map, $field, $new) { } } } +/** + * Follow changed template relationships, comparing values before any mutation. + * Included templates and explicit overrides stop propagation down their branch. + */ +function wm_link_propagate_defaults($map, $values, $marker = null) { + $previous = []; + $children = []; + foreach ($map->links as $name => $link) { + $children[$link->template ?? 'DEFAULT'][] = $name; + foreach ($values as $field => $value) { + $previous[$name][$field] = $link->$field; + } + } + if ($previous['DEFAULT'] == $values) return; + + $pending = ['DEFAULT']; + $changed = ['DEFAULT' => true]; + for ($i = 0; $i < count($pending); $i++) { + $parent = $pending[$i]; + foreach ($values as $field => $value) { + $map->links[$parent]->$field = $value; + } + foreach ($children[$parent] ?? [] as $name) { + $link = $map->links[$name]; + $inherits = $previous[$name] == $previous[$parent] && ($marker === null || empty($link->hints[$marker])); + if (isset($changed[$name]) || $name === ':: DEFAULT ::' || $link->defined_in !== $map->configfile || !$inherits) continue; + $changed[$name] = true; + $pending[] = $name; + } + } +} + /** * Apply validated link comment style defaults. * * @param WeatherMap $map * @param mixed $font * @param mixed $colour + * @param mixed $curve + * @param mixed $style * * @return void */ -function wm_comment_apply_style($map, $font, $colour, $curve = null) { +function wm_comment_apply_style($map, $font, $colour, $curve = null, $style = null) { + if (in_array($style, ['edge', 'center'], true)) { + wm_comment_update_default($map, 'commentstyle', $style); + } if ($curve === '0' || $curve === '1') { wm_comment_update_default($map, 'commentcurve', (int)$curve); } wm_comment_update_default($map, 'commentfont', wm_comment_resolve_font($map, $font)); wm_comment_update_default($map, 'commentfontcolour', wm_comment_resolve_colour($colour)); } + +/** Render the map-wide traffic-label positions. */ +function wm_link_position_fields($map) { + $link = $map->links['DEFAULT']; + return '' . html_escape(__('Traffic Label Positions', 'weathermap')) . '' . wm_link_position_inputs('mapstyle', $link->labeloffset_in, $link->labeloffset_out) . '
' . html_escape(__('Positions along the link: 0% is the first node and 100% is the second. These move the traffic labels, not the comments.', 'weathermap')) . ''; +} + +/** Render labelled IN/OUT percentage inputs without rounding custom values. */ +function wm_link_position_inputs($prefix, $in, $out) { + $html = ''; + foreach (['in' => $in, 'out' => $out] as $direction => $value) { + $id = $prefix . '_labelpos' . $direction; + $label = $direction === 'in' ? __('IN', 'weathermap') : __('OUT', 'weathermap'); + $html .= ' % '; + } + return $html; +} + +/** Render per-link controls; the editor moves the position row into Advanced. */ +function wm_link_override_fields() { + $html = '
' . html_escape(__('Edge places comments beside the link; Centre places them over the arrow.', 'weathermap')) . ''; + $html .= '
' . wm_link_position_inputs('link', 75, 25) . '
' . html_escape(__('Positions along the link: 0% is the first node and 100% is the second. These move the traffic labels, not the comments.', 'weathermap')) . ''; + return $html; +} + +/** Only whole percentages accepted by BWLABELPOS are valid. */ +function wm_link_valid_position($value) { + return (is_int($value) || is_string($value)) && preg_match('/^\d{1,3}$/D', (string)$value) && (int)$value <= 100; +} + +/** Update matching root defaults as a pair, preserving custom or included links. */ +function wm_link_apply_default_positions($map, $in, $out) { + if (!wm_link_valid_position($in) || !wm_link_valid_position($out)) return; + wm_link_propagate_defaults($map, ['labeloffset_in' => (int)$in, 'labeloffset_out' => (int)$out], 'editor_labelpos_override'); +} + +/** Apply validated per-link overrides, or restore the inherited template values. */ +function wm_link_apply_overrides($map, $link, $style, $mode, $in, $out) { + // Named templates retain their inheritance when restoring defaults. + $base = $map->links[$link->template]; + if ($style === 'default') { + $link->commentstyle = $base->commentstyle; + unset($link->hints['editor_commentstyle_override']); + } elseif (in_array($style, ['edge', 'center'], true)) { + $link->commentstyle = $style; + $link->add_hint('editor_commentstyle_override', '1'); + } + if ($mode === 'default') { + $link->labeloffset_in = $base->labeloffset_in; + $link->labeloffset_out = $base->labeloffset_out; + unset($link->hints['editor_labelpos_override']); + } elseif ($mode === 'custom' && wm_link_valid_position($in) && wm_link_valid_position($out)) { + $link->labeloffset_in = (int)$in; + $link->labeloffset_out = (int)$out; + $link->add_hint('editor_labelpos_override', '1'); + } +} diff --git a/lib/editor.inc.php b/lib/editor.inc.php index e3bcea8..bdf824d 100644 --- a/lib/editor.inc.php +++ b/lib/editor.inc.php @@ -1118,7 +1118,7 @@ function getEditorJs() { 'primarySource' => __('Primary source — use "Use interface" at the top to change', 'weathermap'), 'choosePrimaryFirst' => __('Choose the primary traffic interface in the main form first.', 'weathermap'), 'chooseSource' => __('Add additional traffic sources', 'weathermap'), - 'linkAdvancedHint' => __('Custom sources and URLs override the automatic interface setup. Leave these unchanged for a normal traffic link.', 'weathermap'), + 'linkAdvancedHint' => __('Adjust traffic label positions, or use custom sources and URLs. Leave sources and URLs unchanged for a normal traffic link.', 'weathermap'), 'customSourcesChanged' => __('Custom traffic sources changed. Save to apply. Hover graphs and click destination are unchanged.', 'weathermap'), 'extraUrlsHint' => __('One additional image URL maximum. This is the same preview managed under Additional hover graphs.', 'weathermap'), 'trafficSources' => __('Traffic sources', 'weathermap'), diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot index 9f9967e..65c5077 100644 --- a/locales/po/cacti.pot +++ b/locales/po/cacti.pot @@ -2755,7 +2755,7 @@ msgid "Add additional traffic sources" msgstr "" #: lib/editor.inc.php -msgid "Custom sources and URLs override the automatic interface setup. Leave these unchanged for a normal traffic link." +msgid "Adjust traffic label positions, or use custom sources and URLs. Leave sources and URLs unchanged for a normal traffic link." msgstr "" #: lib/editor.inc.php @@ -3072,6 +3072,71 @@ msgstr "" msgid "Undo and Redo require browser session storage." msgstr "" +#: lib/editor.comment-style.php +msgid "Link Comment Style" +msgstr "" + +#: lib/editor.comment-style.php +msgid "Edge" +msgstr "" + +#: lib/editor.comment-style.php +msgid "Edge places comments beside the link; Centre places them over the arrow." +msgstr "" + +#: lib/editor.comment-style.php +msgid "Traffic Label Positions" +msgstr "" + +#: lib/editor.comment-style.php +msgid "Positions along the link: 0% is the first node and 100% is the second. These move the traffic labels, not the comments." +msgstr "" + +#: lib/editor.comment-style.php +msgid "IN" +msgstr "" + +#: lib/editor.comment-style.php +msgid "OUT" +msgstr "" + +#: lib/editor.comment-style.php +msgid "Comment Style" +msgstr "" + +#: lib/editor.comment-style.php +msgid "Use map default" +msgstr "" + +#: lib/editor.comment-style.php +msgid "Custom positions" +msgstr "" + +#: weathermap-cacti-plugin-editor.php +msgid "" +"

Bandwidth: Set the capacity used to calculate utilisation, for example 100M or 1G. K, M, G and T are supported. Select Same As "In" when both directions have equal capacity.

\n" +"

Traffic interface: Search by device, port or description, or select Browse all. Choose an interface, then click Use interface to set the primary measured source, click destination and main hover graph. Additional measured sources and hover graphs are kept.

\n" +"

Appearance and comments: Via Style controls curved or angled bends; Link Width controls arrow thickness. IN Comment and OUT Comment add text along each direction. The percentage sets the text position along the link. Comment Style places comments beside the link (Edge) or over the arrow (Centre); Use map default follows Map Style.

\n" +"

Additional hover graphs: Show a maximum of two hover graphs: the main traffic graph and one extra, such as CPU, latency or another interface. Filter by graph type narrows the choices. Add hover graph adds the extra; Replace hover graphs changes it; Remove graph removes it. The main traffic graph, measured traffic and link click destination stay unchanged. In the viewer, each Cacti preview opens its own full graph when clicked.

\n" +"

Advanced settings: Optional controls for moving the IN/OUT traffic labels, combining measured sources, changing the click destination and entering custom hover image URLs. Traffic label positions move the utilisation boxes along the link, independently of comment positions. Use map default follows Map Style. A normal traffic link usually needs no changes here.

\n" +"

Combining traffic sources: Add traffic source adds another interface to the map total, separately for IN and OUT. Remove source removes an additional source. Change the primary source using Use interface at the top. The combined traffic is compared with the configured bandwidth to calculate utilisation. No combined Cacti graph is created automatically; create a matching aggregate graph in Cacti and enter its click and hover URLs if needed. Separate map links are usually clearer for primary and backup connections.

\n" +"

Manual sources and URLs: The source text box edits the complete measured-source list, including the primary source. Click destination URL controls where clicking the map link goes. Main hover graph URL sets the first preview; Additional graph URL(s) accepts one extra image URL. This extra is also managed under Additional hover graphs.

\n" +"

Tidy and Via: Tidy adjusts the link layout. Via lets you place a bend point on the map.

\n" +"

Edit Raw Configuration: Edit the underlying map syntax for settings beyond this form.

" +msgstr "" + +#: weathermap-cacti-plugin-editor.php +msgid "Default Link Capacity" +msgstr "" + +#: weathermap-cacti-plugin-editor.php +msgid "Set the map’s title, legend, timestamp, background and canvas size, plus the default bandwidth for links. Map dimensions are measured in pixels; link appearance defaults are in Map Style." +msgstr "" + +#: weathermap-cacti-plugin-editor.php +msgid "Link Appearance Defaults" +msgstr "" + #: lib/viewer.controls.php weathermap-cacti-plugin-mgmt.php msgid "Docs" msgstr "" diff --git a/tests/Support/LinkLayoutControlsRegression.php b/tests/Support/LinkLayoutControlsRegression.php new file mode 100644 index 0000000..1caadba --- /dev/null +++ b/tests/Support/LinkLayoutControlsRegression.php @@ -0,0 +1,89 @@ +ReadConfig($file); + $request['config_revision'] = \WeathermapConfig\snapshotRevision($map->config_revision, $map->config_dependencies, $map->config_root_identity); + $GLOBALS['__test_nfilter_request'] = $request; + $action($file); + $saved = new WeatherMap(); + layout_assert($saved->ReadConfig($file), 'reload'); + return $saved; +} +$file = sys_get_temp_dir() . '/wm-layout-' . bin2hex(random_bytes(8)) . '.conf'; +$include = $file . '.included'; +try { + file_put_contents($file, "NODE A\n POSITION 10 10\nNODE B\n POSITION 100 100\nLINK DEFAULT\n WIDTH 7\nLINK main\n NODES A B\nLINK other\n NODES A B\n WIDTH 12\n"); + $style = ['mapstyle_htmlstyle'=>'overlib', 'mapstyle_legendfont'=>'4', 'mapstyle_keystyle'=>'classic', 'mapstyle_linklabels'=>'percent', 'mapstyle_arrowstyle'=>'compact', 'mapstyle_nodefont'=>'3', 'mapstyle_linkfont'=>'2', 'mapstyle_commentfont'=>'1', 'mapstyle_nodewidth'=>'0', 'mapstyle_nodeheight'=>'0', 'mapstyle_linkwidth'=>'0', 'mapstyle_linkheight'=>'0', 'mapstyle_defaultwidth'=>'9', 'mapstyle_commentstyle'=>'center', 'mapstyle_labelposin'=>'80', 'mapstyle_labelposout'=>'20']; + $saved = layout_submit($file, $style, 'setMapStyle'); + layout_assert($saved->links['main']->width == 9 && $saved->links['other']->width == 12, 'width moved to Map Style and preserves custom width'); + layout_assert($saved->links['main']->commentstyle === 'center' && $saved->links['main']->labeloffset_in == 80, 'style defaults applied'); + $link = ['link_name'=>'main', 'link_width'=>'9', 'link_infourl'=>'', 'link_hover'=>'', 'link_commentin'=>'In', 'link_commentout'=>'Out', 'link_commentposin'=>'95', 'link_commentposout'=>'5', 'link_target'=>'', 'link_bandwidth_in'=>'1G', 'link_bandwidth_out'=>'1G', 'viastyle'=>'curved', 'link_commentstyle'=>'edge', 'link_labelposmode'=>'custom', 'link_labelposin'=>'90', 'link_labelposout'=>'10']; + $saved = layout_submit($file, $link, 'setLinkProperties'); + layout_assert($saved->links['main']->commentstyle === 'edge' && $saved->links['main']->labeloffset_in == 90, 'per-link override applied'); + layout_assert($saved->links['other']->commentstyle === 'center' && $saved->links['other']->labeloffset_in == 80, 'other link unchanged'); + $link['link_commentstyle'] = 'default'; $link['link_labelposmode'] = 'default'; + $saved = layout_submit($file, $link, 'setLinkProperties'); + layout_assert($saved->links['main']->commentstyle === 'center' && $saved->links['main']->labeloffset_in == 80, 'default restored'); + $style['mapstyle_defaultwidth'] = '0'; $style['mapstyle_labelposin'] = '101'; + $saved = layout_submit($file, $style, 'setMapStyle'); + layout_assert($saved->links['DEFAULT']->width == 9 && $saved->links['DEFAULT']->labeloffset_in == 80, 'invalid layout values ignored'); + // Saving Map Properties must no longer reset the width now owned by Map Style. + $saved = layout_submit($file, ['map_title'=>'Example', 'map_legend'=>'Load', 'map_stamp'=>'', 'map_htmlfile'=>'', 'map_pngfile'=>'', 'map_width'=>'600', 'map_height'=>'300', 'map_bgfile'=>'--NONE--', 'map_linkdefaultbwin'=>'1G', 'map_linkdefaultbwout'=>'1G'], 'setMapProperties'); + layout_assert($saved->links['DEFAULT']->width == 9, 'Map Properties preserves width'); + // An override belongs to the link that chose it, never to its descendants. + file_put_contents($file, "NODE A\n POSITION 10 10\nNODE B\n POSITION 100 100\nLINK parent\n COMMENTSTYLE center\n BWLABELPOS 90 10\n SET editor_commentstyle_override 1\n SET editor_labelpos_override 1\nLINK child\n TEMPLATE parent\n NODES A B\nLINK fixed_child\n TEMPLATE parent\n NODES A B\n COMMENTSTYLE center\n BWLABELPOS 90 10\n SET editor_commentstyle_override 1\n SET editor_labelpos_override 1\n"); + $map = new WeatherMap(); layout_assert($map->ReadConfig($file), 'template marker fixture'); + layout_assert($map->links['child']->get_hint('editor_commentstyle_override') === null && $map->links['child']->get_hint('editor_labelpos_override') === null, 'template markers are not inherited'); + layout_assert(str_contains($map->links['child']->asJS(), 'commentstylechoice:' . js_escape('default')), 'inherited child is shown as default'); + layout_assert($map->WriteConfig($file), 'save explicit child markers matching template markers'); + $map = new WeatherMap(); layout_assert($map->ReadConfig($file), 'reload matching markers'); + layout_assert($map->links['fixed_child']->get_hint('editor_commentstyle_override') === '1' && $map->links['fixed_child']->get_hint('editor_labelpos_override') === '1', 'explicit matching markers survive serialization'); + wm_link_apply_overrides($map, $map->links['child'], 'edge', 'custom', '75', '25'); + layout_assert($map->WriteConfig($file), 'save child override'); + $map = new WeatherMap(); layout_assert($map->ReadConfig($file), 'reload child override'); + wm_link_apply_overrides($map, $map->links['child'], 'default', 'default', null, null); + layout_assert($map->WriteConfig($file), 'save reset'); + $map = new WeatherMap(); layout_assert($map->ReadConfig($file), 'reload reset'); + layout_assert($map->links['child']->get_hint('editor_commentstyle_override') === null && $map->links['child']->get_hint('editor_labelpos_override') === null, 'reset survives marked template reload'); + layout_assert($map->links['child']->commentstyle === 'center' && $map->links['child']->labeloffset_in == 90, 'reset inherits the named template values'); + + // Default edits follow local template chains, but cannot cross an unchanged INCLUDE. + $included = "LINK included\n COMMENTSTYLE edge\n BWLABELPOS 75 25\n WIDTH 7\n"; + file_put_contents($include, $included); + file_put_contents($file, "INCLUDE $include\nNODE A\n POSITION 10 10\nNODE B\n POSITION 100 100\nLINK DEFAULT\n COMMENTSTYLE edge\n BWLABELPOS 75 25\n WIDTH 7\nLINK external_child\n TEMPLATE included\n NODES A B\nLINK external_grandchild\n TEMPLATE external_child\n NODES A B\nLINK local_parent\n SET site_role local\nLINK local_child\n TEMPLATE local_parent\n NODES A B\nLINK local_grandchild\n TEMPLATE local_child\n NODES A B\nLINK fixed_parent\n SET editor_commentstyle_override 1\n SET editor_labelpos_override 1\nLINK fixed_descendant\n TEMPLATE fixed_parent\n NODES A B\n"); + $map = new WeatherMap(); layout_assert($map->ReadConfig($file), 'included template fixture'); + wm_comment_apply_style($map, null, null, null, 'center'); + wm_link_apply_default_positions($map, '85', '15'); + wm_comment_update_default($map, 'width', 9); + foreach (['included', 'external_child', 'external_grandchild'] as $name) { + layout_assert($map->links[$name]->commentstyle === 'edge' && $map->links[$name]->labeloffset_in == 75 && $map->links[$name]->width == 7, 'included branch unchanged: ' . $name); + } + foreach (['local_parent', 'local_child', 'local_grandchild'] as $name) { + layout_assert($map->links[$name]->commentstyle === 'center' && $map->links[$name]->labeloffset_in == 85 && $map->links[$name]->labeloffset_out == 15 && $map->links[$name]->width == 9, 'local template chain updated: ' . $name); + } + layout_assert($map->links['fixed_descendant']->commentstyle === 'edge' && $map->links['fixed_descendant']->labeloffset_in == 75, 'explicit parent stops propagation to descendants'); + layout_assert($map->WriteConfig($file), 'save inherited default edits'); + $map = new WeatherMap(); layout_assert($map->ReadConfig($file), 'reload included template edits'); + layout_assert(file_get_contents($include) === $included, 'included file is untouched'); + foreach (['external_child', 'external_grandchild'] as $name) { + layout_assert($map->links[$name]->commentstyle === 'edge' && $map->links[$name]->labeloffset_in == 75 && $map->links[$name]->width == 7, 'included branch stable after reload: ' . $name); + preg_match('/LINK ' . $name . '\n(.*?)(?=\nLINK |$)/s', file_get_contents($file), $block); + layout_assert(!preg_match('/COMMENTSTYLE|BWLABELPOS|WIDTH|editor_.*override/', $block[1]), 'no accidental serialized override: ' . $name); + } + + echo "Link layout controls regression PASS\n"; +} finally { + @unlink($file); + @unlink($include); +} diff --git a/tests/Unit/LinkLayoutControlsTest.php b/tests/Unit/LinkLayoutControlsTest.php new file mode 100644 index 0000000..bd8b048 --- /dev/null +++ b/tests/Unit/LinkLayoutControlsTest.php @@ -0,0 +1,46 @@ +WriteConfig($file); $saved = new WeatherMap(); $saved->ReadConfig($file); @@ -59,6 +59,54 @@ function cacti_count($values) { self::assertEquals(9, $font->size); self::assertEquals([0, 0, 255], $saved->links['DEFAULT']->commentfontcolour); self::assertStringContainsString('FONTDEFINE', file_get_contents($file)); + self::assertSame('center', $saved->links['DEFAULT']->commentstyle); + self::assertStringContainsString('COMMENTSTYLE center', file_get_contents($file)); + wm_comment_apply_style($saved, null, null, null, 'edge'); + self::assertTrue($saved->WriteConfig($file)); + $edge = new WeatherMap(); + self::assertTrue($edge->ReadConfig($file)); + self::assertSame('edge', $edge->links['DEFAULT']->commentstyle); + + // Explicit overrides equal to the default must survive save/reload and later default edits. + file_put_contents($file, "NODE A\n POSITION 10 10\nNODE B\n POSITION 100 100\nLINK DEFAULT\n COMMENTSTYLE edge\n BWLABELPOS 75 25\nLINK ordinary\n NODES A B\nLINK fixed\n NODES A B\nLINK rawcustom\n NODES A B\n COMMENTSTYLE center\n BWLABELPOS 90 10\nLINK named\n COMMENTSTYLE center\n BWLABELPOS 80 20\nLINK templated\n TEMPLATE named\n NODES A B\n"); + $layout = new WeatherMap(); + self::assertTrue($layout->ReadConfig($file)); + wm_link_apply_overrides($layout, $layout->links['fixed'], 'edge', 'custom', '75', '25'); + self::assertTrue($layout->WriteConfig($file)); + $layout = new WeatherMap(); + self::assertTrue($layout->ReadConfig($file)); + self::assertSame('1', $layout->links['fixed']->get_hint('editor_commentstyle_override')); + self::assertStringContainsString('commentstylechoice:' . js_escape('edge'), $layout->links['fixed']->asJS()); + self::assertStringContainsString('labelposmode:' . js_escape('custom'), $layout->links['fixed']->asJS()); + wm_comment_apply_style($layout, null, null, null, 'center'); + wm_link_apply_default_positions($layout, '85', '15'); + self::assertSame('edge', $layout->links['fixed']->commentstyle); + self::assertEquals(75, $layout->links['fixed']->labeloffset_in); + self::assertSame('center', $layout->links['ordinary']->commentstyle); + self::assertEquals(85, $layout->links['ordinary']->labeloffset_in); + self::assertEquals(90, $layout->links['rawcustom']->labeloffset_in); + self::assertEquals(80, $layout->links['templated']->labeloffset_in); + $before = serialize($layout->links['fixed']); + wm_link_apply_overrides($layout, $layout->links['fixed'], 'invalid', 'custom', '-1', '101'); + self::assertSame($before, serialize($layout->links['fixed'])); + wm_link_apply_default_positions($layout, '10.5', '25'); + self::assertEquals(85, $layout->links['DEFAULT']->labeloffset_in); + wm_link_apply_overrides($layout, $layout->links['fixed'], 'default', 'default', null, null); + self::assertSame('center', $layout->links['fixed']->commentstyle); + self::assertEquals(85, $layout->links['fixed']->labeloffset_in); + self::assertNull($layout->links['fixed']->get_hint('editor_commentstyle_override')); + self::assertTrue($layout->WriteConfig($file)); + $restored = new WeatherMap(); + self::assertTrue($restored->ReadConfig($file)); + wm_comment_apply_style($restored, null, null, null, 'edge'); + wm_link_apply_default_positions($restored, '70', '30'); + self::assertSame('edge', $restored->links['fixed']->commentstyle); + self::assertEquals(70, $restored->links['fixed']->labeloffset_in); + $html = wm_link_override_fields() . wm_link_position_fields($restored); + self::assertStringContainsString('Use map default', $html); + self::assertStringContainsString('id="mapstyle_labelposin"', $html); + self::assertStringContainsString('id="link_commentstyle"', $html); + } finally { if (file_exists($file)) { unlink($file); diff --git a/tests/Unit/MapStyleDefaultsTest.php b/tests/Unit/MapStyleDefaultsTest.php index 2359177..9b27d37 100644 --- a/tests/Unit/MapStyleDefaultsTest.php +++ b/tests/Unit/MapStyleDefaultsTest.php @@ -39,9 +39,9 @@ function wm_style_test_map() { $builtin = new WMFont(); $builtin->type = 'GD builtin'; $builtin->gdnumber = 2; - $default = (object)['defined_in'=>'/fixture/map.conf', 'bwfont'=> 100, 'commentfont'=> 100, 'commentfontcolour'=>['0', '0', '255']]; + $default = (object)['defined_in'=>'/fixture/map.conf', 'bwfont'=> 100, 'commentfont'=> 100, 'commentfontcolour'=>['0', '0', '255'], 'commentstyle'=>'edge']; - return (object)['configfile'=>'/fixture/map.conf', 'fonts'=>[2=>$builtin, 100=>$font], 'links'=>[':: DEFAULT ::'=>clone $default, 'DEFAULT'=>clone $default, 'ordinary'=>clone $default, 'custom'=>(object)['defined_in'=>'/fixture/map.conf', 'bwfont'=>2, 'commentfont'=>2, 'commentfontcolour'=>[255, 0, 0]]]]; + return (object)['configfile'=>'/fixture/map.conf', 'fonts'=>[2=>$builtin, 100=>$font], 'links'=>[':: DEFAULT ::'=>clone $default, 'DEFAULT'=>clone $default, 'ordinary'=>clone $default, 'custom'=>(object)['defined_in'=>'/fixture/map.conf', 'bwfont'=>2, 'commentfont'=>2, 'commentfontcolour'=>[255, 0, 0], 'commentstyle'=>'center']]]; } it('UpdatesMatchingDefaultsAndPreservesDifferentStyles', function () { $map = wm_style_test_map(); @@ -121,7 +121,7 @@ function wm_style_test_map() { $document = new DOMDocument(); $document->loadHTML('' . wm_comment_style_fields(wm_style_test_map()) . '
', LIBXML_NOERROR | LIBXML_NOWARNING); $xpath = new DOMXPath($document); - foreach (['mapstyle_commentfont' => 'Link Comment Font'] as $id => $text) { + foreach (['mapstyle_commentfont' => 'Link Comment Font', 'mapstyle_commentstyle' => 'Link Comment Style'] as $id => $text) { $label = $xpath->query('//label[@for="' . $id . '"]')->item(0); expect($label)->not->toBeNull()->and($label->textContent)->toBe($text) ->and($xpath->query('//select[@id="' . $id . '"]')->length)->toBe(1); @@ -191,3 +191,37 @@ function wm_style_test_map() { } expect(substr_count($html, '<img'))->toBe(2)->and($html)->not->toContain('links['DEFAULT']->commentstyle = $style; + $doc = new DOMDocument(); + $doc->loadHTML('' . wm_comment_style_fields($map) . '
', LIBXML_NOERROR | LIBXML_NOWARNING); + $xp = new DOMXPath($doc); + $selected = $xp->query('//select[@name="mapstyle_commentstyle"]/option[@selected]'); + expect($selected->length)->toBe(1)->and($selected[0]->getAttribute('value'))->toBe($style); +})->with(['edge', 'center']); + +it('UpdatesDefaultCommentPlacementWithoutChangingCustomOrIncludedLinks', function () { + $map = wm_style_test_map(); + $map->links['included'] = clone $map->links['ordinary']; + $map->links['included']->defined_in = '/fixture/shared.conf'; + wm_comment_apply_style($map, null, null, null, 'center'); + expect($map->links['DEFAULT']->commentstyle)->toBe('center') + ->and($map->links['ordinary']->commentstyle)->toBe('center') + ->and($map->links['custom']->commentstyle)->toBe('center') + ->and($map->links['included']->commentstyle)->toBe('edge') + ->and($map->links[':: DEFAULT ::']->commentstyle)->toBe('edge'); + $map->links['custom']->commentstyle = 'edge'; + wm_comment_apply_style($map, null, null, null, 'edge'); + expect($map->links['ordinary']->commentstyle)->toBe('edge') + ->and($map->links['custom']->commentstyle)->toBe('edge'); +}); + +it('IgnoresMissingOrInvalidCommentPlacement', function ($style) { + $map = wm_style_test_map(); + $before = serialize($map); + wm_comment_apply_style($map, null, null, null, $style); + expect(serialize($map))->toBe($before); +})->with([[null], [''], ['centre'], ['invalid'], ['EDGE'], [1], [['edge']]]); diff --git a/weathermap-cacti-plugin-editor.php b/weathermap-cacti-plugin-editor.php index f4ef8db..d85a435 100644 --- a/weathermap-cacti-plugin-editor.php +++ b/weathermap-cacti-plugin-editor.php @@ -622,6 +622,7 @@ ' id='link_picker' name='link_picker' type='text' class='selectmenu-ajax ui-state-default ui-corner-all' data-action='graphs' data-add-label='' data-replace-label='' /> + @@ -664,9 +665,9 @@
Bandwidth: Set the capacity used to calculate utilisation, for example 100M or 1G. K, M, G and T are supported. Select Same As "In" when both directions have equal capacity.

Traffic interface: Search by device, port or description, or select Browse all. Choose an interface, then click Use interface to set the primary measured source, click destination and main hover graph. Additional measured sources and hover graphs are kept.

-

Appearance and comments: Via Style controls curved or angled bends; Link Width controls arrow thickness. IN Comment and OUT Comment add text along each direction. The percentage sets the text position along the link.

+

Appearance and comments: Via Style controls curved or angled bends; Link Width controls arrow thickness. IN Comment and OUT Comment add text along each direction. The percentage sets the text position along the link. Comment Style places comments beside the link (Edge) or over the arrow (Centre); Use map default follows Map Style.

Additional hover graphs: Show a maximum of two hover graphs: the main traffic graph and one extra, such as CPU, latency or another interface. Filter by graph type narrows the choices. Add hover graph adds the extra; Replace hover graphs changes it; Remove graph removes it. The main traffic graph, measured traffic and link click destination stay unchanged. In the viewer, each Cacti preview opens its own full graph when clicked.

-

Advanced settings: Optional controls for combining measured sources, changing the click destination and entering custom hover image URLs. A normal traffic link usually needs no changes here.

+

Advanced settings: Optional controls for moving the IN/OUT traffic labels, combining measured sources, changing the click destination and entering custom hover image URLs. Traffic label positions move the utilisation boxes along the link, independently of comment positions. Use map default follows Map Style. A normal traffic link usually needs no changes here.

Combining traffic sources: Add traffic source adds another interface to the map total, separately for IN and OUT. Remove source removes an additional source. Change the primary source using Use interface at the top. The combined traffic is compared with the configured bandwidth to calculate utilisation. No combined Cacti graph is created automatically; create a matching aggregate graph in Cacti and enter its click and hover URLs if needed. Separate map links are usually clearer for primary and backup connections.

Manual sources and URLs: The source text box edits the complete measured-source list, including the primary source. Click destination URL controls where clicking the map link goes. Main hover graph URL sets the first preview; Additional graph URL(s) accepts one extra image URL. This extra is also managed under Additional hover graphs.

Tidy and Via: Tidy adjusts the link layout. Via lets you place a bend point on the map.

@@ -745,11 +746,8 @@ -
- - - width) ?>' /> - +
+ @@ -766,7 +764,7 @@
- +
@@ -823,8 +821,12 @@ -
+
+ + + width) ?>' /> + @@ -848,9 +850,12 @@ links['DEFAULT']->bwfont); ?> + + +