Skip to content
Open
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
60 changes: 10 additions & 50 deletions css/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,55 +86,14 @@
align-items: start;
}

/* Card surfaces, headers, titles and tools reuse the Status tab classes. */
.syslogDashboardCard {
box-sizing: border-box;
position: relative;
border: 1px solid var(--search-border, #cbd8d1);
border-radius: 8px;
background: var(--search-card, #fff);
color: var(--search-text, #23362f);
box-shadow: 0 1px 2px rgba(35, 54, 47, 0.08);
overflow: hidden;
}

.syslogDashboardCardHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid var(--search-tint, #e2ede6);
background: var(--search-tint, #e2ede6);
}

.syslogDashboardCardTitle {
margin: 0;
font-size: 13px;
font-weight: 600;
line-height: 1.4;
overflow-wrap: anywhere;
}

.syslogDashboardCardHeader .syslogDashboardCardButton {
border: 0;
background: transparent;
color: var(--search-muted, #596d64);
cursor: pointer;
padding: 3px 4px;
border-radius: 4px;
font-size: 12px;
}

.syslogDashboardCardHeader .syslogDashboardCardButton:hover,
.syslogDashboardCardHeader .syslogDashboardCardButton:focus {
background: var(--search-accent, #276448);
color: #fff;
outline: none;
}

.syslogChart {
min-height: 260px;
padding: 6px 6px 0 0;
padding: 6px 0 0;
}

/* Bottom-right drag handle; expands the pointer target to the corner. */
Expand Down Expand Up @@ -182,7 +141,7 @@
}

.syslogDashboardCardStatus {
margin: 40px 12px;
margin: 8px 0;
font-size: 12px;
color: var(--search-muted, #596d64);
}
Expand All @@ -204,12 +163,8 @@
}

/* Drag-and-drop reorder affordances. */
.syslogDashboardCardHeader {
cursor: grab;
}

.syslogDashboardCardDragging {
opacity: 0.45;
opacity: 0.5;
}

.syslogDashboardCardDropBefore {
Expand Down Expand Up @@ -266,7 +221,12 @@
grid-template-columns: 1fr;
}

.syslogDashboardCard {
/* Fit saved multi-column panels into the mobile grid. */
grid-column: 1 / -1 !important;
}

.syslogDashboardPanelActions {
margin-left: 0;
}
}
}
70 changes: 54 additions & 16 deletions js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,35 +50,47 @@ function syslogDashboardLoad(data) {

function syslogDashboardPanelCard(panel) {
var card = document.createElement('article');
card.className = 'syslogDashboardCard';
card.className = 'syslogDashboardCard syslogStatusCard';
card.id = 'syslog_panel_' + panel.id;

var header = document.createElement('header');
header.className = 'syslogDashboardCardHeader';
if (syslogDashboard.canManage) header.setAttribute('draggable', 'true');
header.className = 'syslogDashboardCardHeader syslogStatusCardHeader syslogStatusCardHeader--none';
if (syslogDashboard.canManage) {
var handle = document.createElement('button');
handle.type = 'button';
handle.className = 'syslogStatusCardDrag';
handle.setAttribute('aria-label', syslogDashboard.text.dragPanel);
handle.innerHTML = '<i class="fa fa-bars" aria-hidden="true"></i>';
header.append(handle);
}

var title = document.createElement('h3');
title.className = 'syslogDashboardCardTitle';
title.className = 'syslogDashboardCardTitle syslogStatusCardTitle';
title.textContent = panel.title;
header.append(title);

var actions = document.createElement('div');
actions.className = 'syslogStatusCardTools';
actionButtons().forEach(function(button) {
actions.append(button);
});
header.append(actions);
card.append(header);

var body = document.createElement('div');
body.className = 'syslogStatusCardBody';
card.append(body);

var chart = document.createElement('div');
chart.className = 'syslogChart';
chart.id = 'syslog_chart_' + panel.id;
card.append(chart);
body.append(chart);

var status = document.createElement('p');
status.className = 'syslogDashboardCardStatus';
status.setAttribute('role', 'status');
status.textContent = '...';
card.append(status);
body.append(status);

// Shared dashboards are view-only: no resize or drag affordances.
if (syslogDashboard.canManage) {
Expand Down Expand Up @@ -223,7 +235,7 @@ function actionButtons() {
actions.forEach(function(entry) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'syslogDashboardCardButton';
button.className = 'syslogDashboardCardButton syslogStatusCardTool';
button.dataset.action = entry[0];
button.innerHTML = '<i class="fa ' + entry[1] + '" aria-hidden="true"></i>';
button.title = entry[2];
Expand Down Expand Up @@ -337,27 +349,53 @@ function syslogDashboardRenderGrid() {
/* ===================== Drag-and-drop reorder ===================== */

/**
* Make a card reorderable: dragging its header shows a placeholder, and
* Make a card reorderable: dragging its handle shows a placeholder, and
* dropping posts the new 1-based position. Drop targets are the card
* midpoints, so moving a panel underneath another works in both
* directions without up/down buttons.
*/
function syslogDashboardInitDrag(card, panel) {
if (!syslogDashboard.canManage) return;

var header = card.querySelector('.syslogDashboardCardHeader');
if (!header) return;
var handle = card.querySelector('.syslogStatusCardDrag');
if (!handle) return;

var disarm = function() { card.draggable = false; };
handle.addEventListener('mousedown', function() {
card.draggable = true;
document.addEventListener('mouseup', disarm, {once: true});
});
handle.addEventListener('touchstart', function() {
card.draggable = true;
document.addEventListener('touchend', disarm, {once: true});
document.addEventListener('touchcancel', disarm, {once: true});
}, {passive: true});
handle.addEventListener('keydown', function(event) {
var direction = ['ArrowLeft', 'ArrowUp'].includes(event.key) ? -1
: (['ArrowRight', 'ArrowDown'].includes(event.key) ? 1 : 0);
if (!direction) return;
event.preventDefault();
var order = syslogDashboard.panels.map(function(item) { return item.id; });
var from = order.indexOf(panel.id);
var to = from + direction;
if (from < 0 || to < 0 || to >= order.length) return;
order.splice(from, 1);
order.splice(to, 0, panel.id);
syslogDashboardApplyOrder(order, panel.id, to + 1);
handle.focus();
});

header.addEventListener('dragstart', function(event) {
card.addEventListener('dragstart', function(event) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', String(panel.id));
card.classList.add('syslogDashboardCardDragging');
});

header.addEventListener('dragend', function() {
card.addEventListener('dragend', function() {
card.draggable = false;
card.classList.remove('syslogDashboardCardDragging');
document.querySelectorAll('.syslogDashboardCardDropBefore')
.forEach(function(el) { el.classList.remove('syslogDashboardCardDropBefore'); });
document.querySelectorAll('.syslogDashboardCardDropBefore, .syslogDashboardCardDropAfter')
.forEach(function(el) { el.classList.remove('syslogDashboardCardDropBefore', 'syslogDashboardCardDropAfter'); });
});

card.addEventListener('dragover', function(event) {
Expand All @@ -366,7 +404,7 @@ function syslogDashboardInitDrag(card, panel) {
event.dataTransfer.dropEffect = 'move';

var rect = card.getBoundingClientRect();
var before = event.clientY < rect.top + rect.height / 2;
var before = event.clientY < rect.top || (event.clientY <= rect.bottom && event.clientX < rect.left + rect.width / 2);
card.classList.toggle('syslogDashboardCardDropBefore', before);
card.classList.toggle('syslogDashboardCardDropAfter', !before);
});
Expand All @@ -390,7 +428,7 @@ function syslogDashboardInitDrag(card, panel) {
if (draggedId === panel.id) return;

var rect = card.getBoundingClientRect();
var before = event.clientY < rect.top + rect.height / 2;
var before = event.clientY < rect.top || (event.clientY <= rect.bottom && event.clientX < rect.left + rect.width / 2);
var panels = syslogDashboard.panels || [];
var order = panels.map(function(p) { return p.id; });
var from = order.indexOf(draggedId);
Expand Down
1 change: 1 addition & 0 deletions lib/syslog_dashboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,7 @@ function syslog_dashboard(): void {
canShare: <?php print $can_share ? 'true' : 'false'; ?>,
shared: <?php print $is_shared ? 'true' : 'false'; ?>,
text: {
dragPanel: <?php print syslog_json_safe(__('Drag to reorder card', 'syslog')); ?>,
refreshPanel: <?php print syslog_json_safe(__('Refresh Panel', 'syslog')); ?>,
totalMessages: <?php print syslog_json_safe(__('Total messages', 'syslog')); ?>,
libraryUnavailable: <?php print syslog_json_safe(__('Chart library unavailable on this Cacti installation.', 'syslog')); ?>,
Expand Down
10 changes: 5 additions & 5 deletions locales/po/cacti.pot
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Cacti 1.2.32\n"
"Report-Msgid-Bugs-To: developers@cacti.net\n"
"POT-Creation-Date: 2026-10-10 09:25-0400\n"
"POT-Creation-Date: 2026-10-12 16:41-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Cacti Developers <developers@cacti.net>>\n"
"Language-Team: Cacti Developers <developers@cacti.net>\n"
Expand Down Expand Up @@ -1079,6 +1079,10 @@ msgstr ""
msgid "A title of up to 128 characters is required."
msgstr ""

#: lib/syslog_dashboard.php syslog.php
msgid "Drag to reorder card"
msgstr ""

#: lib/syslog_dashboard.php
msgid "Refresh Panel"
msgstr ""
Expand Down Expand Up @@ -1808,10 +1812,6 @@ msgstr ""
msgid "Remove from page"
msgstr ""

#: syslog.php
msgid "Drag to reorder card"
msgstr ""

#: syslog.php
msgid "Unknown card."
msgstr ""
Expand Down
6 changes: 2 additions & 4 deletions tests/Unit/DashboardDefaultTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@

it('seeds valid starter panels atomically and does not reseed an existing default', function () {
syslog_load_plugin_source('setup.php');
syslog_load_plugin_source('includes/schema.php');
syslog_load_plugin_source('lib/syslog_dashboard.php');
test_override('syslog_ensure_dashboard_tables', fn () => null);
test_override('syslog_db_table_exists', fn () => true);
test_override('syslog_db_column_exists', fn () => true);
test_override('db_update_table', fn () => true);
$GLOBALS['syslogdb_default'] = 'syslog';
test_override('syslog_dashboard_username', fn () => 'admin');
test_override('syslog_db_fetch_insert_id', fn () => 42);
$existing = false;
$calls = [];
Expand Down
26 changes: 26 additions & 0 deletions tests/Unit/DashboardRenderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

it('renders the dashboard drag label and retains per-panel refresh text', function () {
syslog_test_reset_globals();
syslog_load_plugin_source('includes/functions.php');
syslog_load_plugin_source('lib/syslog_dashboard.php');
$GLOBALS['syslogdb_default'] = 'syslog';
$GLOBALS['page_refresh_interval'] = [0 => 'Never'];
$GLOBALS['request']['refresh'] = 0;
$GLOBALS['request']['dashboard_id'] = 0;
test_override('syslog_db_fetch_assoc_prepared', fn () => []);
test_override('syslog_db_fetch_assoc', fn () => []);
test_override('syslog_db_fetch_row_prepared', fn () => []);

ob_start();
try {
syslog_dashboard();
$html = ob_get_contents();
} finally {
ob_end_clean();
}

expect($html)->toContain('dragPanel: "Drag to reorder card"')
->toContain('refreshPanel: "Refresh Panel"')
->toContain("id='syslog_dashboard_grid'");
});
1 change: 1 addition & 0 deletions tests/Unit/SetupModuleBoundaryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
'syslog_upgrade_explicit_realm_grants',
'syslog_upgrade_create_permission_realms',
'syslog_check_upgrade',
'syslog_dashboard_seed_default',
'plugin_syslog_version',
'syslog_refresh_plugin_version',
'syslog_upgrade_hook_module_paths',
Expand Down
10 changes: 10 additions & 0 deletions tests/browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,13 @@ CACTI_ROOT=/path/to/cacti node tests/browser/compact_workspace.cjs
Optional environment variables: `PLAYWRIGHT_MODULE` (installed module path), `CHROMIUM_PATH` (browser executable), and `TEST_ARTIFACT_DIR` (existing screenshot directory).

Covers builder date and negation submission, severity color preservation, opening the message details from any cell of a row, selected-message rule links and unavailable actions, escaped raw messages, inspector keyboard close, filter collapse, the results limit beside the search buttons, view options, refresh without detaching a saved search, and narrow-screen overflow.

The Dashboard card fixture checks shared Status styling in light/dark themes,
existing tools, drag/keyboard ordering requests, and narrow-screen panel sizing:

```sh
node tests/browser/dashboard_cards.cjs
```

It uses the actual Dashboard renderer and styles with fixture panels and records
ordering requests; it does not verify a live database or chart data.
Loading
Loading