diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in
index 4b5e40f75..1ca0e2440 100644
--- a/db/zm_create.sql.in
+++ b/db/zm_create.sql.in
@@ -1348,6 +1348,39 @@ CREATE TABLE `Notifications` (
CONSTRAINT `Notifications_ibfk_1` FOREIGN KEY (`UserId`) REFERENCES `Users` (`Id`) ON DELETE CASCADE
) ENGINE=@ZM_MYSQL_ENGINE@;
+--
+-- Table structure for table `Menu_Items`
+--
+
+DROP TABLE IF EXISTS `Menu_Items`;
+CREATE TABLE `Menu_Items` (
+ `Id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `MenuKey` varchar(32) NOT NULL,
+ `Enabled` tinyint(1) NOT NULL DEFAULT 1,
+ `Label` varchar(64) DEFAULT NULL,
+ `SortOrder` smallint NOT NULL DEFAULT 0,
+ `Icon` varchar(128) DEFAULT NULL,
+ `IconType` enum('material','fontawesome','image','none') NOT NULL DEFAULT 'material',
+ PRIMARY KEY (`Id`),
+ UNIQUE KEY `Menu_Items_MenuKey_idx` (`MenuKey`)
+) ENGINE=@ZM_MYSQL_ENGINE@;
+
+INSERT INTO `Menu_Items` (`MenuKey`, `Enabled`, `SortOrder`) VALUES
+ ('Console', 1, 10),
+ ('Montage', 1, 20),
+ ('MontageReview', 1, 30),
+ ('Events', 1, 40),
+ ('Options', 1, 50),
+ ('Log', 1, 60),
+ ('Devices', 1, 70),
+ ('IntelGpu', 1, 80),
+ ('Groups', 1, 90),
+ ('Filters', 1, 100),
+ ('Snapshots', 1, 110),
+ ('Reports', 1, 120),
+ ('ReportEventAudit', 1, 130),
+ ('Map', 1, 140);
+
source @PKGDATADIR@/db/Object_Types.sql
-- We generally don't alter triggers, we drop and re-create them, so let's keep them in a separate file that we can just source in update scripts.
source @PKGDATADIR@/db/triggers.sql
diff --git a/db/zm_update-1.39.3.sql b/db/zm_update-1.39.3.sql
new file mode 100644
index 000000000..1863941ac
--- /dev/null
+++ b/db/zm_update-1.39.3.sql
@@ -0,0 +1,81 @@
+--
+-- Add Menu_Items table for customizable navbar/sidebar menu
+--
+
+SET @s = (SELECT IF(
+ (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = DATABASE()
+ AND table_name = 'Menu_Items'
+ ) > 0,
+"SELECT 'Table Menu_Items already exists'",
+"CREATE TABLE `Menu_Items` (
+ `Id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `MenuKey` varchar(32) NOT NULL,
+ `Enabled` tinyint(1) NOT NULL DEFAULT 1,
+ `Label` varchar(64) DEFAULT NULL,
+ `SortOrder` smallint NOT NULL DEFAULT 0,
+ `Icon` varchar(128) DEFAULT NULL,
+ `IconType` enum('material','fontawesome','image','none') NOT NULL DEFAULT 'material',
+ PRIMARY KEY (`Id`),
+ UNIQUE KEY `Menu_Items_MenuKey_idx` (`MenuKey`)
+) ENGINE=InnoDB"
+));
+
+PREPARE stmt FROM @s;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+--
+-- Seed default menu items if table is empty
+--
+
+SET @s = (SELECT IF(
+ (SELECT COUNT(*) FROM `Menu_Items`) > 0,
+"SELECT 'Menu_Items already has data'",
+"INSERT INTO `Menu_Items` (`MenuKey`, `Enabled`, `SortOrder`) VALUES
+ ('Console', 1, 10),
+ ('Montage', 1, 20),
+ ('MontageReview', 1, 30),
+ ('Events', 1, 40),
+ ('Options', 1, 50),
+ ('Log', 1, 60),
+ ('Devices', 1, 70),
+ ('IntelGpu', 1, 80),
+ ('Groups', 1, 90),
+ ('Filters', 1, 100),
+ ('Snapshots', 1, 110),
+ ('Reports', 1, 120),
+ ('ReportEventAudit', 1, 130),
+ ('Map', 1, 140)"
+));
+
+PREPARE stmt FROM @s;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+--
+-- Add Icon and IconType columns if they don't exist
+--
+
+SET @s = (SELECT IF(
+ (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
+ AND table_name = 'Menu_Items' AND column_name = 'Icon'
+ ) > 0,
+"SELECT 'Column Icon already exists'",
+"ALTER TABLE `Menu_Items` ADD `Icon` varchar(128) DEFAULT NULL AFTER `SortOrder`"
+));
+
+PREPARE stmt FROM @s;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @s = (SELECT IF(
+ (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
+ AND table_name = 'Menu_Items' AND column_name = 'IconType'
+ ) > 0,
+"SELECT 'Column IconType already exists'",
+"ALTER TABLE `Menu_Items` ADD `IconType` enum('material','fontawesome','image','none') NOT NULL DEFAULT 'material' AFTER `Icon`"
+));
+
+PREPARE stmt FROM @s;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
diff --git a/web/includes/MenuItem.php b/web/includes/MenuItem.php
new file mode 100644
index 000000000..290664ea6
--- /dev/null
+++ b/web/includes/MenuItem.php
@@ -0,0 +1,68 @@
+ null,
+ 'MenuKey' => '',
+ 'Enabled' => 1,
+ 'Label' => null,
+ 'SortOrder' => 0,
+ 'Icon' => null,
+ 'IconType' => 'material',
+ );
+
+ // Default material icons for each menu key
+ public static $defaultIcons = array(
+ 'Console' => 'dashboard',
+ 'Montage' => 'live_tv',
+ 'MontageReview' => 'movie',
+ 'Events' => 'event',
+ 'Options' => 'settings',
+ 'Log' => 'notification_important',
+ 'Devices' => 'devices_other',
+ 'IntelGpu' => 'memory',
+ 'Groups' => 'group',
+ 'Filters' => 'filter_alt',
+ 'Snapshots' => 'preview',
+ 'Reports' => 'report',
+ 'ReportEventAudit' => 'shield',
+ 'Map' => 'language',
+ );
+
+ public function effectiveIcon() {
+ if ($this->{'Icon'} !== null && $this->{'Icon'} !== '') {
+ return $this->{'Icon'};
+ }
+ return isset(self::$defaultIcons[$this->{'MenuKey'}]) ? self::$defaultIcons[$this->{'MenuKey'}] : 'menu';
+ }
+
+ public function effectiveIconType() {
+ if ($this->{'IconType'} == 'none') {
+ return 'none';
+ }
+ if ($this->{'Icon'} !== null && $this->{'Icon'} !== '') {
+ return $this->{'IconType'};
+ }
+ return 'material';
+ }
+
+ public static function find($parameters = array(), $options = array()) {
+ return ZM_Object::_find(self::class, $parameters, $options);
+ }
+
+ public static function find_one($parameters = array(), $options = array()) {
+ return ZM_Object::_find_one(self::class, $parameters, $options);
+ }
+
+ public function displayLabel() {
+ if ($this->{'Label'} !== null && $this->{'Label'} !== '') {
+ return $this->{'Label'};
+ }
+ return translate($this->{'MenuKey'});
+ }
+}
diff --git a/web/includes/actions/options.php b/web/includes/actions/options.php
index e90d3ed1c..600dfc68f 100644
--- a/web/includes/actions/options.php
+++ b/web/includes/actions/options.php
@@ -189,5 +189,95 @@ if ( $action == 'delete' ) {
}
}
}
+} else if ($action == 'menuitems') {
+ if (!canEdit('System')) {
+ ZM\Warning('Need System permission to edit menu items');
+ } else if (isset($_REQUEST['items'])) {
+ require_once('includes/MenuItem.php');
+ $allItems = ZM\MenuItem::find();
+ foreach ($allItems as $item) {
+ $id = $item->Id();
+ $enabled = isset($_REQUEST['items'][$id]['Enabled']) ? 1 : 0;
+ $label = isset($_REQUEST['items'][$id]['Label']) ? trim($_REQUEST['items'][$id]['Label']) : null;
+ $sortOrder = isset($_REQUEST['items'][$id]['SortOrder']) ? intval($_REQUEST['items'][$id]['SortOrder']) : $item->SortOrder();
+ if ($label === '') $label = null;
+
+ $iconType = isset($_REQUEST['items'][$id]['IconType']) ? $_REQUEST['items'][$id]['IconType'] : $item->IconType();
+ if (!in_array($iconType, ['material', 'fontawesome', 'image', 'none'])) $iconType = 'material';
+ $icon = isset($_REQUEST['items'][$id]['Icon']) ? trim($_REQUEST['items'][$id]['Icon']) : $item->Icon();
+ if ($icon === '') $icon = null;
+
+ // Handle image upload
+ if (isset($_FILES['items']['name'][$id]['IconFile'])
+ && $_FILES['items']['error'][$id]['IconFile'] == UPLOAD_ERR_OK) {
+ $uploadDir = ZM_PATH_WEB.'/graphics/menu/';
+ if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
+
+ $tmpName = $_FILES['items']['tmp_name'][$id]['IconFile'];
+ $origName = basename($_FILES['items']['name'][$id]['IconFile']);
+ $ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION));
+ $allowedExts = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico'];
+ if (in_array($ext, $allowedExts)) {
+ // Validate it's actually an image (except SVG/ICO)
+ if ($ext == 'svg' || $ext == 'ico' || getimagesize($tmpName) !== false) {
+ $safeName = 'menu_'.$id.'_'.time().'.'.$ext;
+ $destPath = $uploadDir.$safeName;
+ if (move_uploaded_file($tmpName, $destPath)) {
+ // Remove old uploaded icon if it exists
+ if ($item->IconType() == 'image' && $item->Icon() && file_exists(ZM_PATH_WEB.'/'.$item->Icon())) {
+ unlink(ZM_PATH_WEB.'/'.$item->Icon());
+ }
+ $icon = 'graphics/menu/'.$safeName;
+ $iconType = 'image';
+ }
+ }
+ }
+ }
+
+ // If user cleared icon, reset to default
+ if ($iconType != 'image' && ($icon === null || $icon === '')) {
+ $icon = null;
+ }
+
+ $item->save([
+ 'Enabled' => $enabled,
+ 'Label' => $label,
+ 'SortOrder' => $sortOrder,
+ 'Icon' => $icon,
+ 'IconType' => $iconType,
+ ]);
+ }
+ }
+ $redirect = '?view=options&tab=menu';
+} else if ($action == 'resetmenu') {
+ if (!canEdit('System')) {
+ ZM\Warning('Need System permission to reset menu items');
+ } else {
+ // Clean up any uploaded icon files
+ require_once('includes/MenuItem.php');
+ $oldItems = ZM\MenuItem::find();
+ foreach ($oldItems as $item) {
+ if ($item->IconType() == 'image' && $item->Icon() && file_exists(ZM_PATH_WEB.'/'.$item->Icon())) {
+ unlink(ZM_PATH_WEB.'/'.$item->Icon());
+ }
+ }
+ dbQuery('DELETE FROM Menu_Items');
+ dbQuery("INSERT INTO `Menu_Items` (`MenuKey`, `Enabled`, `SortOrder`) VALUES
+ ('Console', 1, 10),
+ ('Montage', 1, 20),
+ ('MontageReview', 1, 30),
+ ('Events', 1, 40),
+ ('Options', 1, 50),
+ ('Log', 1, 60),
+ ('Devices', 1, 70),
+ ('IntelGpu', 1, 80),
+ ('Groups', 1, 90),
+ ('Filters', 1, 100),
+ ('Snapshots', 1, 110),
+ ('Reports', 1, 120),
+ ('ReportEventAudit', 1, 130),
+ ('Map', 1, 140)");
+ }
+ $redirect = '?view=options&tab=menu';
} // end if object vs action
?>
diff --git a/web/skins/classic/includes/functions.php b/web/skins/classic/includes/functions.php
index d462eeae1..23c1fb18b 100644
--- a/web/skins/classic/includes/functions.php
+++ b/web/skins/classic/includes/functions.php
@@ -179,48 +179,139 @@ function getBodyTopHTML() {
}
} // end function getBodyTopHTML
-function buildMenuItem($viewItemName, $id, $itemName, $href, $icon, $classNameForTag_A = '', $subMenu = '') {
+function renderMenuIcon($icon, $iconType = 'material') {
+ if ($iconType == 'none') {
+ return '';
+ } else if ($iconType == 'fontawesome') {
+ return '';
+ } else if ($iconType == 'image') {
+ return '
';
+ }
+ return ''.htmlspecialchars($icon).'';
+}
+
+// Returns icon HTML from the current $menuIconOverride, or empty string if not set
+function getNavbarIcon() {
+ global $menuIconOverride;
+ if (!isset($menuIconOverride)) return '';
+ $html = renderMenuIcon($menuIconOverride['icon'], $menuIconOverride['iconType']);
+ if ($html !== '') $html .= ' ';
+ return $html;
+}
+
+function buildMenuItem($viewItemName, $id, $itemName, $href, $icon, $classNameForTag_A = '', $subMenu = '', $skipTranslate = false, $iconType = 'material') {
global $view;
+ global $menuIconOverride;
+
+ // Check for icon override from renderMenuItems
+ if (isset($menuIconOverride)) {
+ $icon = $menuIconOverride['icon'];
+ $iconType = $menuIconOverride['iconType'];
+ }
+
/* Highlighting the active menu section */
if ($viewItemName == 'watch') {
$activeClass = ($view == $viewItemName && (isset($_REQUEST['cycle']) && $_REQUEST['cycle'] == "true")) ? ' active' : '';
} else {
$activeClass = $view == $viewItemName ? ' active' : '';
}
- $itemName = translate($itemName);
+ if (!$skipTranslate) $itemName = translate($itemName);
$result = '
'.PHP_EOL;
return $result;
}
+function getMenuItemFunctions() {
+ return [
+ 'Console' => 'getConsoleHTML',
+ 'Montage' => 'getMontageHTML',
+ 'MontageReview' => 'getMontageReviewHTML',
+ 'Events' => 'getEventsHTML',
+ 'Options' => 'getOptionsHTML',
+ 'Log' => 'getLogHTML',
+ 'Devices' => 'getDevicesHTML',
+ 'IntelGpu' => 'getIntelGpuHTML',
+ 'Groups' => 'getGroupsHTML',
+ 'Filters' => 'getFilterHTML',
+ 'Snapshots' => 'getSnapshotsHTML',
+ 'Reports' => 'getReportsHTML',
+ 'ReportEventAudit' => 'getRprtEvntAuditHTML',
+ 'Map' => 'getMapHTML',
+ ];
+}
+
+function getMenuItems() {
+ static $cached = null;
+ if ($cached === null) {
+ require_once('includes/MenuItem.php');
+ $cached = ZM\MenuItem::find([], ['order' => 'SortOrder ASC']);
+ }
+ return $cached;
+}
+
+// Functions that take only ($forLeftBar, $customLabel) - no $view parameter
+function getMenuFuncsNoView() {
+ return ['getConsoleHTML', 'getOptionsHTML', 'getLogHTML', 'getDevicesHTML', 'getIntelGpuHTML'];
+}
+
+function renderMenuItems($forLeftBar = false) {
+ global $view;
+ $menuItems = getMenuItems();
+ $funcMap = getMenuItemFunctions();
+ $result = '';
+
+ if (empty($menuItems)) {
+ // Fallback: no DB rows yet, render all in default order
+ foreach ($funcMap as $key => $funcName) {
+ $noViewFuncs = getMenuFuncsNoView();
+ if (in_array($funcName, $noViewFuncs)) {
+ $result .= $funcName($forLeftBar);
+ } else {
+ $result .= $funcName($view, $forLeftBar);
+ }
+ }
+ } else {
+ global $menuIconOverride;
+ $noViewFuncs = getMenuFuncsNoView();
+ foreach ($menuItems as $item) {
+ if (!$item->Enabled()) continue;
+ $key = $item->MenuKey();
+ if (!isset($funcMap[$key])) continue;
+
+ $funcName = $funcMap[$key];
+ $customLabel = ($item->Label() !== null && $item->Label() !== '') ? $item->displayLabel() : null;
+
+ // Set icon override for buildMenuItem/getOptionsHTML to pick up
+ $effIcon = $item->effectiveIcon();
+ $effIconType = $item->effectiveIconType();
+ $menuIconOverride = ['icon' => $effIcon, 'iconType' => $effIconType];
+
+ if (in_array($funcName, $noViewFuncs)) {
+ $result .= $funcName($forLeftBar, $customLabel);
+ } else {
+ $result .= $funcName($view, $forLeftBar, $customLabel);
+ }
+ }
+ $menuIconOverride = null;
+ }
+
+ $result .= getAdditionalLinksHTML($view, $forLeftBar);
+ return $result;
+}
+
function buildSidebarMenu() {
global $view;
global $user;
if ( $user and $user->Username() ) {
$menuForAuthUser = '
' .
- getConsoleHTML($forLeftBar = true) .
- getMontageHTML($view, $forLeftBar = true) .
- getCycleHTML($view, $forLeftBar = true) .
- getMontageReviewHTML($view, $forLeftBar = true) .
- getEventsHTML($view, $forLeftBar = true) .
- getOptionsHTML($forLeftBar = true) .
- getLogHTML($forLeftBar = true) .
- getDevicesHTML($forLeftBar = true) .
- getIntelGpuHTML($forLeftBar = true) .
- getGroupsHTML($view, $forLeftBar = true) .
- getFilterHTML($view, $forLeftBar = true) .
- getSnapshotsHTML($view, $forLeftBar = true) .
- getReportsHTML($view, $forLeftBar = true) .
- getRprtEvntAuditHTML($view, $forLeftBar = true) .
- getMapHTML($view, $forLeftBar = true) .
- getAdditionalLinksHTML($view, $forLeftBar = true)
+ renderMenuItems($forLeftBar = true)
;
} else { // USER IS NOT AUTHORIZED!
$menuForAuthUser = '';
@@ -511,21 +602,7 @@ function getNormalNavBarHTML($running, $user, $bandwidth_options, $view, $skin)
// *** Build the navigation bar menu items ***
echo '';
- echo getConsoleHTML();
- echo getOptionsHTML();
- echo getLogHTML();
- echo getDevicesHTML();
- echo getIntelGpuHTML();
- echo getGroupsHTML($view);
- echo getFilterHTML($view);
- echo getCycleHTML($view);
- echo getMontageHTML($view);
- echo getMontageReviewHTML($view);
- echo getSnapshotsHTML($view);
- echo getReportsHTML($view);
- echo getRprtEvntAuditHTML($view);
- echo getMapHTML($view);
- echo getAdditionalLinksHTML($view);
+ echo renderMenuItems($forLeftBar = false);
echo getHeaderFlipHTML();
echo '
';
echo '
';
@@ -669,21 +746,7 @@ function getCollapsedNavBarHTML($running, $user, $bandwidth_options, $view, $ski
Username() ) {
echo '';
- echo getConsoleHTML();
- echo getOptionsHTML();
- echo getLogHTML();
- echo getDevicesHTML();
- echo getIntelGpuHTML();
- echo getGroupsHTML($view);
- echo getFilterHTML($view);
- echo getCycleHTML($view);
- echo getMontageHTML($view);
- echo getMontageReviewHTML($view);
- echo getSnapshotsHTML($view);
- echo getReportsHTML($view);
- echo getRprtEvntAuditHTML($view);
- echo getMapHTML($view);
- echo getAdditionalLinksHTML($view);
+ echo renderMenuItems($forLeftBar = false);
echo '
';
}
?>
@@ -963,23 +1026,26 @@ function getNavBrandHTML() {
}
// Returns the html representing the Console menu item
-function getConsoleHTML($forLeftBar = false) {
+function getConsoleHTML($forLeftBar = false, $customLabel = null) {
global $user;
$result = '';
+ $label = $customLabel !== null ? $customLabel : 'Console';
+ $skipTranslate = $customLabel !== null;
if (count($user->viewableMonitorIds()) or !ZM\Monitor::find_one()) {
if ($forLeftBar) {
$result .= buildMenuItem(
$viewItemName = 'console',
$id = 'getConsoleHTML',
- $itemName = 'Console',
+ $itemName = $label,
$href = '?view=console',
$icon = 'dashboard',
$classNameForTag_A = '',
- $subMenu = ''
+ $subMenu = '',
+ $skipTranslate
);
} else {
- $result .= '- '.translate('Console').'
'.PHP_EOL;
+ $result .= '- '.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).'
'.PHP_EOL;
}
}
@@ -987,9 +1053,10 @@ function getConsoleHTML($forLeftBar = false) {
}
// Returns the html representing the Options menu item
-function getOptionsHTML($forLeftBar = false) {
+function getOptionsHTML($forLeftBar = false, $customLabel = null) {
global $zmMenu;
$result = '';
+ $label = $customLabel !== null ? $customLabel : translate('Options');
// Sorting order of the "Options" submenu items. If a submenu item is in the DB but is not here, it will be automatically added to the end of the list.
$categoryDisplayOrder = [
@@ -1026,7 +1093,8 @@ function getOptionsHTML($forLeftBar = false) {
'privacy',
'MQTT',
'telemetry',
- 'version'
+ 'version',
+ 'menu'
]);
$zmMenu::buildSubMenuOptions($categoryDisplayOrder);
@@ -1055,16 +1123,23 @@ function getOptionsHTML($forLeftBar = false) {
';
+ $optIcon = 'settings';
+ $optIconType = 'material';
+ if (isset($menuIconOverride)) {
+ $optIcon = $menuIconOverride['icon'];
+ $optIconType = $menuIconOverride['iconType'];
+ }
+
$result .= '
'.PHP_EOL;
} else {
- $result .= ''.translate('Options').''.PHP_EOL;
+ $result .= ''.getNavbarIcon().htmlspecialchars($label).''.PHP_EOL;
}
}
@@ -1072,9 +1147,11 @@ function getOptionsHTML($forLeftBar = false) {
}
// Returns the html representing the Log menu item
-function getLogHTML($forLeftBar = false) {
+function getLogHTML($forLeftBar = false, $customLabel = null) {
$result = '';
-
+ $label = $customLabel !== null ? $customLabel : 'Log';
+ $skipTranslate = $customLabel !== null;
+
if ( canView('System') ) {
if ( ZM\logToDatabase() > ZM\Logger::NOLOG ) {
$logstate = logState();
@@ -1083,18 +1160,19 @@ function getLogHTML($forLeftBar = false) {
$result .= buildMenuItem(
$viewItemName = 'log',
$id = 'getLogHTML',
- $itemName = 'Log',
+ $itemName = $label,
$href = '?view=log',
$icon = 'notification_important',
$classNameForTag_A = $class,
- $subMenu = ''
+ $subMenu = '',
+ $skipTranslate
);
} else {
- $result .= ''.translate('Log').''.PHP_EOL;
+ $result .= ''.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).''.PHP_EOL;
}
}
}
-
+
return $result;
}
@@ -1116,22 +1194,25 @@ function getLogIconHTML() {
}
// Returns the html representing the X10 Devices menu item
-function getDevicesHTML($forLeftBar = false) {
+function getDevicesHTML($forLeftBar = false, $customLabel = null) {
$result = '';
+ $label = $customLabel !== null ? $customLabel : 'Devices';
+ $skipTranslate = $customLabel !== null;
if ( ZM_OPT_X10 && canView('Devices') ) {
if ($forLeftBar) {
$result .= buildMenuItem(
$viewItemName = 'devices',
$id = 'getDevicesHTML',
- $itemName = 'Devices',
+ $itemName = $label,
$href = '?view=devices',
$icon = 'devices_other',
$classNameForTag_A = '',
- $subMenu = ''
+ $subMenu = '',
+ $skipTranslate
);
} else {
- $result .= ''.translate('Devices').''.PHP_EOL;
+ $result .= ''.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).''.PHP_EOL;
}
}
@@ -1139,8 +1220,10 @@ function getDevicesHTML($forLeftBar = false) {
}
// Returns the html representing the Intel GPU status menu item
-function getIntelGpuHTML($forLeftBar = false) {
+function getIntelGpuHTML($forLeftBar = false, $customLabel = null) {
$result = '';
+ $label = $customLabel !== null ? $customLabel : 'Intel GPU';
+ $skipTranslate = $customLabel !== null;
// Only show if intel_gpu_top is available and user can view System
if (canView('System')) {
@@ -1151,14 +1234,15 @@ function getIntelGpuHTML($forLeftBar = false) {
$result .= buildMenuItem(
$viewItemName = 'intelgpu',
$id = 'getIntelGpuHTML',
- $itemName = 'Intel GPU',
+ $itemName = $label,
$href = '?view=intelgpu',
$icon = 'memory',
$classNameForTag_A = '',
- $subMenu = ''
+ $subMenu = '',
+ $skipTranslate
);
} else {
- $result .= 'Intel GPU'.PHP_EOL;
+ $result .= ''.getNavbarIcon().htmlspecialchars($label).''.PHP_EOL;
}
}
}
@@ -1167,80 +1251,63 @@ function getIntelGpuHTML($forLeftBar = false) {
}
// Returns the html representing the Groups menu item
-function getGroupsHTML($view, $forLeftBar = false) {
+function getGroupsHTML($view, $forLeftBar = false, $customLabel = null) {
$result = '';
if ( !canView('Groups') ) return $result;
+ $label = $customLabel !== null ? $customLabel : 'Groups';
+ $skipTranslate = $customLabel !== null;
$class = $view == 'groups' ? ' selected' : '';
if ($forLeftBar) {
$result .= buildMenuItem(
$viewItemName = 'groups',
$id = 'getGroupsHTML',
- $itemName = 'Groups',
+ $itemName = $label,
$href = '?view=groups',
$icon = 'group',
$classNameForTag_A = '',
- $subMenu = ''
+ $subMenu = '',
+ $skipTranslate
);
} else {
- $result .= ''. translate('Groups') .''.PHP_EOL;
+ $result .= ''.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).''.PHP_EOL;
}
return $result;
}
// Returns the html representing the Filter menu item
-function getFilterHTML($view, $forLeftBar = false) {
+function getFilterHTML($view, $forLeftBar = false, $customLabel = null) {
$result = '';
if ( !canView('Events') ) return $result;
-
+ $label = $customLabel !== null ? $customLabel : 'Filters';
+ $skipTranslate = $customLabel !== null;
+
$class = $view == 'filter' ? ' selected' : '';
if ($forLeftBar) {
$result .= buildMenuItem(
$viewItemName = 'filter',
$id = 'getFilterHTML',
- $itemName = 'Filters',
+ $itemName = $label,
$href = '?view=filter',
$icon = 'filter_alt',
$classNameForTag_A = '',
- $subMenu = ''
+ $subMenu = '',
+ $skipTranslate
);
} else {
- $result .= ''.translate('Filters').''.PHP_EOL;
- }
-
- return $result;
-}
-
-// Returns the html representing the Cycle menu item
-function getCycleHTML($view, $forLeftBar = false) {
- $result = '';
-
- if ( canView('Stream') ) {
- $class = $view == 'cycle' ? ' selected' : '';
- if ($forLeftBar) {
- $result .= buildMenuItem(
- $viewItemName = 'watch',
- $id = 'getCycleHTML',
- $itemName = 'Cycle',
- $href = '?view=watch&cycle=true',
- //$icon = 'cyclone',
- $icon = 'repeat',
- $classNameForTag_A = '',
- $subMenu = ''
- );
- } else {
- $result .= '' .translate('Cycle'). ''.PHP_EOL;
- }
+ $result .= ''.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).''.PHP_EOL;
}
return $result;
}
// Returns the html representing the Montage menu item
-function getMontageHTML($view, $forLeftBar = false) {
+function getMontageHTML($view, $forLeftBar = false, $customLabel = null) {
global $user;
$result = '';
+ $label = $customLabel !== null ? $customLabel : 'Montage';
+ $skipTranslate = $customLabel !== null;
if (canView('Stream') and count($user->viewableMonitorIds())) {
$class = $view == 'montage' ? ' selected' : '';
@@ -1248,14 +1315,15 @@ function getMontageHTML($view, $forLeftBar = false) {
$result .= buildMenuItem(
$viewItemName = 'montage',
$id = 'getMontageHTML',
- $itemName = 'Montage',
+ $itemName = $label,
$href = '?view=montage',
$icon = 'live_tv',
$classNameForTag_A = '',
- $subMenu = ''
+ $subMenu = '',
+ $skipTranslate
);
} else {
- $result .= '' .translate('Montage'). ''.PHP_EOL;
+ $result .= ''.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).''.PHP_EOL;
}
}
@@ -1263,9 +1331,11 @@ function getMontageHTML($view, $forLeftBar = false) {
}
// Returns the html representing the MontageReview menu item
-function getMontageReviewHTML($view, $forLeftBar = false) {
+function getMontageReviewHTML($view, $forLeftBar = false, $customLabel = null) {
$result = '';
-
+ $label = $customLabel !== null ? $customLabel : 'MontageReview';
+ $skipTranslate = $customLabel !== null;
+
if ( canView('Events') ) {
if ( isset($_REQUEST['filter']['Query']['terms']['attr']) ) {
$terms = $_REQUEST['filter']['Query']['terms'];
@@ -1287,14 +1357,15 @@ function getMontageReviewHTML($view, $forLeftBar = false) {
$result .= buildMenuItem(
$viewItemName = 'montagereview',
$id = 'getMontageReviewHTML',
- $itemName = 'MontageReview',
+ $itemName = $label,
$href = '?view=montagereview' .$live,
$icon = 'movie',
$classNameForTag_A = '',
- $subMenu = ''
+ $subMenu = '',
+ $skipTranslate
);
} else {
- $result .= ''.translate('MontageReview').''.PHP_EOL;
+ $result .= ''.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).''.PHP_EOL;
}
}
@@ -1302,8 +1373,10 @@ function getMontageReviewHTML($view, $forLeftBar = false) {
}
// Returns the html representing the Montage menu item
-function getSnapshotsHTML($view, $forLeftBar = false) {
+function getSnapshotsHTML($view, $forLeftBar = false, $customLabel = null) {
$result = '';
+ $label = $customLabel !== null ? $customLabel : 'Snapshots';
+ $skipTranslate = $customLabel !== null;
if (defined('ZM_FEATURES_SNAPSHOTS') and ZM_FEATURES_SNAPSHOTS and canView('Snapshots')) {
$class = $view == 'snapshots' ? ' selected' : '';
@@ -1311,14 +1384,15 @@ function getSnapshotsHTML($view, $forLeftBar = false) {
$result .= buildMenuItem(
$viewItemName = 'snapshots',
$id = 'getSnapshotsHTML',
- $itemName = 'Snapshots',
+ $itemName = $label,
$href = '?view=snapshots',
$icon = 'preview',
$classNameForTag_A = '',
- $subMenu = ''
+ $subMenu = '',
+ $skipTranslate
);
} else {
- $result .= '' .translate('Snapshots'). ''.PHP_EOL;
+ $result .= ''.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).''.PHP_EOL;
}
}
@@ -1326,9 +1400,11 @@ function getSnapshotsHTML($view, $forLeftBar = false) {
}
// Returns the html representing the Events menu item
-function getEventsHTML($view, $forLeftBar = false) {
+function getEventsHTML($view, $forLeftBar = false, $customLabel = null) {
global $user;
$result = '';
+ $label = $customLabel !== null ? $customLabel : 'Events';
+ $skipTranslate = $customLabel !== null;
if (canView('Events')) {
$class = $view == 'events' ? ' selected' : '';
@@ -1336,22 +1412,25 @@ function getEventsHTML($view, $forLeftBar = false) {
$result .= buildMenuItem(
$viewItemName = 'events',
$id = 'getEventsHTML',
- $itemName = 'Events',
+ $itemName = $label,
$href = '?view=events',
$icon = 'event',
$classNameForTag_A = '',
- $subMenu = ''
+ $subMenu = '',
+ $skipTranslate
);
} else {
- $result .= '' .translate('Events'). ''.PHP_EOL;
+ $result .= ''.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).''.PHP_EOL;
}
}
return $result;
}
-function getReportsHTML($view, $forLeftBar = false) {
+function getReportsHTML($view, $forLeftBar = false, $customLabel = null) {
$result = '';
+ $label = $customLabel !== null ? $customLabel : 'Reports';
+ $skipTranslate = $customLabel !== null;
if (canView('Events')) {
$class = ($view == 'reports' or $view == 'report') ? ' selected' : '';
@@ -1359,14 +1438,15 @@ function getReportsHTML($view, $forLeftBar = false) {
$result .= buildMenuItem(
$viewItemName = 'reports',
$id = 'getReportsHTML',
- $itemName = 'Reports',
+ $itemName = $label,
$href = '?view=reports',
$icon = 'report',
$classNameForTag_A = '',
- $subMenu = ''
+ $subMenu = '',
+ $skipTranslate
);
} else {
- $result .= ''.translate('Reports').''.PHP_EOL;
+ $result .= ''.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).''.PHP_EOL;
}
}
@@ -1374,8 +1454,10 @@ function getReportsHTML($view, $forLeftBar = false) {
}
// Returns the html representing the Audit Events Report menu item
-function getRprtEvntAuditHTML($view, $forLeftBar = false) {
+function getRprtEvntAuditHTML($view, $forLeftBar = false, $customLabel = null) {
$result = '';
+ $label = $customLabel !== null ? $customLabel : 'ReportEventAudit';
+ $skipTranslate = $customLabel !== null;
if ( canView('Events') ) {
$class = $view == 'report_event_audit' ? ' selected' : '';
@@ -1383,14 +1465,15 @@ function getRprtEvntAuditHTML($view, $forLeftBar = false) {
$result .= buildMenuItem(
$viewItemName = 'report_event_audit',
$id = 'getRprtEvntAuditHTML',
- $itemName = 'ReportEventAudit',
+ $itemName = $label,
$href = '?view=report_event_audit',
$icon = 'shield',
$classNameForTag_A = '',
- $subMenu = ''
+ $subMenu = '',
+ $skipTranslate
);
} else {
- $result .= ''.translate('ReportEventAudit').''.PHP_EOL;
+ $result .= ''.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).''.PHP_EOL;
}
}
@@ -1398,8 +1481,10 @@ function getRprtEvntAuditHTML($view, $forLeftBar = false) {
}
// Returns the html representing the Audit Events Report menu item
-function getMapHTML($view, $forLeftBar = false) {
+function getMapHTML($view, $forLeftBar = false, $customLabel = null) {
$result = '';
+ $label = $customLabel !== null ? $customLabel : 'Map';
+ $skipTranslate = $customLabel !== null;
if (defined('ZM_OPT_USE_GEOLOCATION') and ZM_OPT_USE_GEOLOCATION) {
$class = $view == 'map' ? ' selected' : '';
@@ -1407,14 +1492,15 @@ function getMapHTML($view, $forLeftBar = false) {
$result .= buildMenuItem(
$viewItemName = 'map',
$id = 'getMapHTML',
- $itemName = 'Map',
+ $itemName = $label,
$href = '?view=map',
$icon = 'language',
$classNameForTag_A = '',
- $subMenu = ''
+ $subMenu = '',
+ $skipTranslate
);
} else {
- $result .= ''.translate('Map').''.PHP_EOL;
+ $result .= ''.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).''.PHP_EOL;
}
}
diff --git a/web/skins/classic/views/_options_menu.php b/web/skins/classic/views/_options_menu.php
new file mode 100644
index 000000000..292fc70c6
--- /dev/null
+++ b/web/skins/classic/views/_options_menu.php
@@ -0,0 +1,107 @@
+ 'SortOrder ASC']);
+$canEdit = canEdit('System');
+?>
+
diff --git a/web/skins/classic/views/console.php b/web/skins/classic/views/console.php
index 8d8e8285c..0bd0a7661 100644
--- a/web/skins/classic/views/console.php
+++ b/web/skins/classic/views/console.php
@@ -267,14 +267,14 @@ echo $navbar ?>
|
- videocam |
+ |
|
|
|
|
- settings |
+ |
|
: array('cnj'=>'and', 'attr'=>'Monitor')
);
parseFilter($filter);
- echo '' : '')
- .$eventCounts[$i]['title']
- .' | '.PHP_EOL;
+ $eventsLink = canView('Events') ? '?view='.ZM_WEB_EVENTS_VIEW.'&page=1'.$filter['querystring'] : '';
+ echo ''
+ .htmlspecialchars($eventCounts[$i]['title'])
+ .' | '.PHP_EOL;
} // end foreach eventCounts
?>
- |
+ |
|
diff --git a/web/skins/classic/views/js/console.js b/web/skins/classic/views/js/console.js
index 31c85c658..31aacb77b 100644
--- a/web/skins/classic/views/js/console.js
+++ b/web/skins/classic/views/js/console.js
@@ -552,6 +552,22 @@ function initPage() {
// Make the table visible after initialization
table.show();
+
+ // Add icons to column headers after bootstrap-table init so they don't
+ // leak into the Columns dropdown (which uses the plain text title).
+ var headerIcons = {Name: 'videocam', Source: 'settings'};
+ table.find('thead th').each(function() {
+ var field = $j(this).data('field');
+ var inner = $j(this).find('.th-inner');
+ if (!inner.length) return;
+ if (headerIcons[field]) {
+ inner.prepend('' + headerIcons[field] + ' ');
+ }
+ if (field === 'ZoneCount') {
+ var text = inner.text();
+ inner.html('' + text + '');
+ }
+ });
} // end function initPage
function sortMonitors(button) {
diff --git a/web/skins/classic/views/js/options.js b/web/skins/classic/views/js/options.js
index 4668e1950..1f982cd4f 100644
--- a/web/skins/classic/views/js/options.js
+++ b/web/skins/classic/views/js/options.js
@@ -54,6 +54,19 @@ function AddNewRole(el) {
window.location.assign(url);
}
+function sortMenuItems(button) {
+ if (button.classList.contains('btn-success')) {
+ $j('#menuItemsBody').sortable('disable');
+ // Update hidden sort order fields based on new row positions
+ $j('#menuItemsBody tr').each(function(index) {
+ $j(this).find('.sortOrderInput').val((index + 1) * 10);
+ });
+ } else {
+ $j('#menuItemsBody').sortable('enable');
+ }
+ button.classList.toggle('btn-success');
+}
+
function initPage() {
const NewStorageBtn = $j('#NewStorageBtn');
const NewServerBtn = $j('#NewServerBtn');
@@ -65,6 +78,36 @@ function initPage() {
NewServerBtn.prop('disabled', !canEdit.System);
$j('.bootstraptable').bootstrapTable({icons: icons}).show();
+
+ // Menu items tab: sortable drag-and-drop and icon type toggle
+ if ($j('#menuItemsBody').length) {
+ $j('#menuItemsBody').sortable({
+ disabled: true,
+ axis: 'y',
+ cursor: 'move',
+ update: function() {
+ $j('#menuItemsBody tr').each(function(index) {
+ $j(this).find('.sortOrderInput').val((index + 1) * 10);
+ });
+ }
+ });
+
+ // Toggle between text input and file input based on icon type
+ $j('.iconTypeSelect').on('change', function() {
+ const id = $j(this).data('item-id');
+ const type = $j(this).val();
+ if (type === 'image') {
+ $j('#iconName-' + id).hide();
+ $j('#iconFile-' + id).show();
+ } else if (type === 'none') {
+ $j('#iconName-' + id).hide();
+ $j('#iconFile-' + id).hide();
+ } else {
+ $j('#iconName-' + id).show();
+ $j('#iconFile-' + id).hide();
+ }
+ });
+ }
}
$j(document).ready(function() {
diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php
index 910de6086..5f63c3afe 100644
--- a/web/skins/classic/views/options.php
+++ b/web/skins/classic/views/options.php
@@ -141,7 +141,9 @@ foreach (array_map('basename', glob('skins/'.$skin.'/css/*', GLOB_ONLYDIR)) as $
include('_options_roles.php');
} else if ($tab == 'API') {
include('_options_api.php');
-} // $tab == API
+} else if ($tab == 'menu') {
+ include('_options_menu.php');
+} // $tab == API/menu
else {
$config = array();
$configCats = array();