Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 36 additions & 14 deletions css/monitor.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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; }

11 changes: 9 additions & 2 deletions includes/controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
}
}
}

Expand Down
110 changes: 108 additions & 2 deletions includes/render.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<div class='monitor_container'>";

foreach ($group['hosts'] as $host) {
Expand All @@ -255,7 +259,7 @@ function monitorRenderGroupCards(string $grouping, array $groups, int $maxlen):

$body .= '</div>';

$out .= "<div class='monitorPanel monitorGroupCard' data-group='" . html_escape((string) $gid) . "'>
$out .= "<div class='monitorPanel monitorGroupCard' data-group='" . html_escape((string) $gid) . "' data-span='$span' style='grid-column: span $span;'>
Comment thread
TheWitness marked this conversation as resolved.
<div class='monitorPanelHeader monitorGroupTitle $worst'><i class='fas fa-grip-vertical monitorGroupDrag'></i> " . html_escape($group['label']) . "</div>
<div class='monitorPanelBody'>$body</div>
</div>";
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -660,7 +673,7 @@ function monitorRenderNonTreeSection(): string {

$result .= "<div class='monitorTableHeader'>
<div class='navBarNavigation'>
<div class='navBarNavigationNone'>" . __('Non-Tree Devices', 'monitor') . "</div>
<div class='navBarNavigationNone'>" . monitorNonTreeLabel() . "</div>
</div>
</div>
<div class='monitor_container'>";
Expand All @@ -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);
Comment thread
TheWitness marked this conversation as resolved.
}

/**
* Render monitor tree grouping view, including tree and non-tree sections.
*
Expand All @@ -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)) {
Expand Down
Loading