diff --git a/CHANGELOG.md b/CHANGELOG.md index abc8f19..8d92784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ * feature: Restructure plugin_monitor_dashboards to store filter state as a JSON `properties` object (migrating the legacy raw `url` into it and dropping the url column on upgrade), and construct dashboard URLs from that structure instead of persisting raw urls * feature: Render the Site and Device Template groupings as a responsive full-width grid of draggable card panels (up to 8 columns, shrinking on narrower screens) whose titles are coloured by each group's worst device status, persisting card order per dashboard or per user +* bug: Let a grouping card with many devices span several grid columns so it flows across the page instead of stacking into one tall, narrow column that scrolls off the screen +* bug: Render the Tree grouping as card panels (one per tree, plus a Non-Tree card) like the Site and Device Template groupings, instead of leaving it on the legacy section layout +* bug: Force the grouping card title colours with !important so Cacti core's device-status classes can no longer override them and leave unreadable low-contrast text on a coloured header (e.g. a device template with down devices) * feature: Add a distinct 'Service Check Failed' device status with its own legend chip, tile/list colour and status-filter coverage, driven by a realm-gated, schema-aware servcheck host set * feature: Add an Actions column to the List view that opens a device detail dialog of intropage-style panels (host information, data collection timing, service checks, thresholds, last-hour syslog, and device links) * feature: Add a plugin_servcheck integration so a triggered/failing service check on a device promotes its hover status and adds a hover glyph linking to the failing check, alongside the existing thold and syslog integrations diff --git a/css/monitor.css b/css/monitor.css index ef83dc0..6c4bd7e 100644 --- a/css/monitor.css +++ b/css/monitor.css @@ -644,13 +644,32 @@ } /* Grouping card grid: responsive, flows full screen up to ~8 columns and - shrinks to fewer columns as the viewport narrows. */ + shrinks to fewer columns as the viewport narrows. Large groups span several + columns (see data-span) so their devices flow across the page. */ .monitorGroupGrid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + grid-auto-flow: row dense; gap: 10px; align-items: start; width: 100%; + container-type: inline-size; +} + +/* Cap each card's span to the number of 260px tracks the grid can actually + show at its current width (track + 10px gap), so a wide card never forces an + extra implicit track and horizontal overflow on narrow layouts. */ +@container (max-width: 529px) { + .monitorGroupCard { grid-column: auto !important; } +} + +@container (min-width: 530px) and (max-width: 799px) { + .monitorGroupCard[data-span="3"], + .monitorGroupCard[data-span="4"] { grid-column: span 2 !important; } +} + +@container (min-width: 800px) and (max-width: 1069px) { + .monitorGroupCard[data-span="4"] { grid-column: span 3 !important; } } .monitorGroupCard .monitorPanelBody { @@ -673,16 +692,19 @@ min-height: 60px; } -/* Group card titles coloured by the group's worst device status. */ -.monitorGroupTitle.deviceUp { background-color: #2b8235; color: #ffffff; } -.monitorGroupTitle.deviceDown { background-color: #a12820; color: #ffffff; } -.monitorGroupTitle.deviceDownMuted { background-color: #93302a; color: #ffffff; } -.monitorGroupTitle.deviceRecovering { background-color: #9a6a1a; color: #ffffff; } -.monitorGroupTitle.deviceThreshold { background-color: #a85712; color: #ffffff; } -.monitorGroupTitle.deviceThresholdMuted { background-color: #6a4a9c; color: #ffffff; } -.monitorGroupTitle.deviceWarning { background-color: #7d6f18; color: #ffffff; } -.monitorGroupTitle.deviceAlert { background-color: #86407a; color: #ffffff; } -.monitorGroupTitle.deviceUnknown { background-color: #5f5f57; color: #ffffff; } -.monitorGroupTitle.deviceUnmonitored { background-color: #3f6a8a; color: #ffffff; } -.monitorGroupTitle.deviceServiceCheck { background-color: #157f87; color: #ffffff; } +/* Group card titles coloured by the group's worst device status. Marked + !important so the Cacti core device-status classes (e.g. .deviceDown, which + force a red text/background) cannot override the title's palette and leave + unreadable low-contrast text on the coloured header. */ +.monitorGroupTitle.deviceUp { background-color: #2b8235 !important; color: #ffffff !important; } +.monitorGroupTitle.deviceDown { background-color: #a12820 !important; color: #ffffff !important; } +.monitorGroupTitle.deviceDownMuted { background-color: #93302a !important; color: #ffffff !important; } +.monitorGroupTitle.deviceRecovering { background-color: #9a6a1a !important; color: #ffffff !important; } +.monitorGroupTitle.deviceThreshold { background-color: #a85712 !important; color: #ffffff !important; } +.monitorGroupTitle.deviceThresholdMuted { background-color: #6a4a9c !important; color: #ffffff !important; } +.monitorGroupTitle.deviceWarning { background-color: #7d6f18 !important; color: #ffffff !important; } +.monitorGroupTitle.deviceAlert { background-color: #86407a !important; color: #ffffff !important; } +.monitorGroupTitle.deviceUnknown { background-color: #5f5f57 !important; color: #ffffff !important; } +.monitorGroupTitle.deviceUnmonitored { background-color: #3f6a8a !important; color: #ffffff !important; } +.monitorGroupTitle.deviceServiceCheck { background-color: #157f87 !important; color: #ffffff !important; } diff --git a/includes/controller.php b/includes/controller.php index 86622bb..7bd9501 100644 --- a/includes/controller.php +++ b/includes/controller.php @@ -105,7 +105,7 @@ function monitorDashboardOwnedByUser(int $dashboard): bool { function saveCardOrder(): void { $grouping = get_nfilter_request_var('grouping'); - if (!in_array($grouping, ['site', 'template'], true)) { + if (!in_array($grouping, ['site', 'template', 'tree'], true)) { return; } @@ -114,7 +114,14 @@ function saveCardOrder(): void { if (is_array($order)) { foreach ($order as $gid) { - $ids[] = (string) (int) $gid; + // Group ids may be numeric (site/template) or string tokens such as + // "tree_12" and "nontree", so preserve the string while stripping + // anything outside a safe id character set. + $gid = (string) preg_replace('/[^a-zA-Z0-9_]/', '', (string) $gid); + + if ($gid !== '') { + $ids[] = $gid; + } } } diff --git a/includes/render.php b/includes/render.php index 276a223..8a4d4c2 100644 --- a/includes/render.php +++ b/includes/render.php @@ -247,6 +247,10 @@ function monitorRenderGroupCards(string $grouping, array $groups, int $maxlen): foreach ($groups as $gid => $group) { $worst = monitorGroupWorstStatusClass($group['hosts']); + // Let groups with many devices span several grid columns so their tiles + // flow across the page instead of stacking into one tall, narrow column. + $span = (int) min(4, max(1, (int) ceil(cacti_sizeof($group['hosts']) / 6))); + $body = "
"; foreach ($group['hosts'] as $host) { @@ -255,7 +259,7 @@ function monitorRenderGroupCards(string $grouping, array $groups, int $maxlen): $body .= '
'; - $out .= "
+ $out .= "
" . html_escape($group['label']) . "
$body
"; @@ -623,6 +627,15 @@ function monitorRenderTreeTitleSections(array $titles, int $maxlen): string { return $result; } +/** + * Translatable label for the card/section holding devices not on any tree. + * + * @return string + */ +function monitorNonTreeLabel(): string { + return __('Non-Tree Devices', 'monitor'); +} + /** * Render section for monitored hosts that are not attached to any tree. * @@ -660,7 +673,7 @@ function monitorRenderNonTreeSection(): string { $result .= "
"; @@ -674,6 +687,93 @@ function monitorRenderNonTreeSection(): string { return $result; } +/** + * Render tree grouping as device cards (one card per tree, plus a Non-Tree + * card), mirroring the Site and Device Template card layouts. + * + * @param array $tree_list Allowed trees for the current filter. + * + * @return string + */ +function monitorRenderTreeCards(array $tree_list): string { + $groups = []; + $host_ids = []; + + if (cacti_sizeof($tree_list)) { + $tree_ids = []; + + foreach ($tree_list as $tree) { + $tree_ids[$tree['id']] = $tree['id']; + } + + $sql_where = ''; + $sql_join = ''; + renderWhereJoin($sql_where, $sql_join); + + $hosts = db_fetch_assoc('SELECT DISTINCT h.*, gt.id AS graph_tree_id, gt.name AS graph_tree_name + FROM host AS h + INNER JOIN graph_tree_items AS gti + ON h.id = gti.host_id + INNER JOIN graph_tree AS gt + ON gt.id = gti.graph_tree_id + ' . $sql_join . ' + ' . $sql_where . ' + AND gti.host_id > 0 + AND gti.graph_tree_id IN (' . implode(',', $tree_ids) . ') + ORDER BY gt.sequence, h.description'); + + if (cacti_sizeof($hosts)) { + [$hosts, $ids] = monitorFilterAllowedHosts($hosts); + + $host_ids = array_merge($host_ids, $ids); + + foreach ($hosts as $host) { + $gid = 'tree_' . $host['graph_tree_id']; + + if (!isset($groups[$gid])) { + $groups[$gid] = ['label' => $host['graph_tree_name'], 'hosts' => []]; + } + + $groups[$gid]['hosts'][] = $host; + } + } + } + + // Devices not attached to any tree get their own card, unless a specific + // tree is selected in the filter. + if (get_request_var('tree') < 0) { + $nontree = getHostNonTreeArray(); + + if (cacti_sizeof($nontree)) { + [$nontree, $ids] = monitorFilterAllowedHosts($nontree); + + if (cacti_sizeof($nontree)) { + $host_ids = array_merge($host_ids, $ids); + + $groups['nontree'] = ['label' => monitorNonTreeLabel(), 'hosts' => $nontree]; + } + } + } + + if (!cacti_sizeof($groups)) { + return ''; + } + + // Size the trim length from the hosts actually rendered here (including the + // Non-Tree card) so labels aren't trimmed against unrelated tree devices. + $maxlen = 10; + + if (get_request_var('view') == 'default' && cacti_sizeof($host_ids)) { + $maxlen = (int) db_fetch_cell('SELECT MAX(LENGTH(description)) + FROM host AS h + WHERE id IN (' . implode(',', $host_ids) . ')'); + } + + $maxlen = getMonitorTrimLength($maxlen); + + return monitorRenderGroupCards('tree', $groups, $maxlen); +} + /** * Render monitor tree grouping view, including tree and non-tree sections. * @@ -694,6 +794,12 @@ function renderTree(): string { $tree_list = []; } + // Card-based views group each tree (and non-tree devices) into panels, the + // same as the Site and Device Template groupings. + if (monitorUseCardLayout()) { + return monitorRenderTreeCards($tree_list); + } + $function = 'renderHeader' . ucfirst(get_request_var('view')); if (function_exists($function)) {