From ff9daec5ec8c068f2520e30c67e24518179f9e6c Mon Sep 17 00:00:00 2001 From: Aditya Toshniwal Date: Thu, 22 Dec 2022 14:25:18 +0530 Subject: [PATCH] Rewrite pgAdmin main menu bar to use React. #5615 --- runtime/src/js/pgadmin.js | 17 +- web/pgadmin/browser/__init__.py | 39 +- .../js/{main_menu.js => MainMenuFactory.js} | 91 +-- web/pgadmin/browser/static/js/browser.js | 306 +++------- web/pgadmin/browser/static/js/layout.js | 4 +- web/pgadmin/browser/static/js/menu.js | 521 ------------------ .../static/js/quick_search/menuitems_help.js | 16 +- .../browser/templates/browser/index.html | 92 +--- .../browser/templates/browser/js/utils.js | 74 ++- .../browser/macros/gravatar_icon.macro | 8 - .../browser/macros/static_user_icon.macro | 3 - web/pgadmin/static/bundle/app.js | 18 +- web/pgadmin/static/js/AppMenuBar.jsx | 146 +++++ web/pgadmin/static/js/Theme/index.jsx | 1 + web/pgadmin/static/js/components/Menu.jsx | 38 +- .../new_menu.js => static/js/helpers/Menu.js} | 59 +- web/pgadmin/static/scss/_pgadmin.style.scss | 14 +- .../pg_utilities_backup_restore_test.py | 6 +- .../pg_utilities_maintenance_test.py | 4 +- .../feature_tests/view_data_dml_queries.py | 8 +- .../xss_checks_pgadmin_debugger_test.py | 12 +- .../xss_checks_roles_control_test.py | 2 +- web/regression/feature_utils/locators.py | 18 +- web/regression/feature_utils/pgadmin_page.py | 52 +- .../javascript/browser/layout_spec.js | 4 +- web/webpack.shim.js | 3 +- 26 files changed, 510 insertions(+), 1046 deletions(-) rename web/pgadmin/browser/static/js/{main_menu.js => MainMenuFactory.js} (50%) delete mode 100644 web/pgadmin/browser/static/js/menu.js delete mode 100644 web/pgadmin/browser/templates/browser/macros/gravatar_icon.macro delete mode 100644 web/pgadmin/browser/templates/browser/macros/static_user_icon.macro create mode 100644 web/pgadmin/static/js/AppMenuBar.jsx rename web/pgadmin/{browser/static/js/new_menu.js => static/js/helpers/Menu.js} (78%) diff --git a/runtime/src/js/pgadmin.js b/runtime/src/js/pgadmin.js index 4b1ac26c8..b67c4d9eb 100644 --- a/runtime/src/js/pgadmin.js +++ b/runtime/src/js/pgadmin.js @@ -389,7 +389,7 @@ function addCommonMenus(menu) { let _menuItem = new gui.MenuItem({ label: menuItem.label, - enabled: !menuItem.is_disabled, + enabled: !menuItem.isDisabled, type: menuItem.type || 'normal', priority: menuItem.priority, ...(submenu.items.length > 0) && { @@ -435,7 +435,7 @@ function getSubMenu(menuItem) { let menuType = typeof item.checked == 'boolean' ? 'checkbox' : item.type; submenu.append(new gui.MenuItem({ label: item.label, - enabled: !item.is_disabled, + enabled: !item.isDisabled, priority: item.priority, type: menuType, checked: item.checked, @@ -472,7 +472,7 @@ function addMacMenu(menu) { new gui.MenuItem({ label: menuItem.label, type: menuItem.type || 'normal', - enabled: !menuItem.is_disabled, + enabled: !menuItem.isDisabled, priority: menuItem.priority, ...(submenu.items.length > 0) && { submenu: submenu, @@ -516,7 +516,7 @@ function enableDisableMenuItem(menu, menuItem) { if (el?.label == menu?.label) { el.submenu.items.forEach((sub) => { if (sub.label == menuItem.label) { - sub.enabled = !menuItem.is_disabled; + sub.enabled = !menuItem.isDisabled; } }); } @@ -560,7 +560,7 @@ function refreshMenuItems(menu) { item.menu_items.forEach((subItem) => { submenu.append(new gui.MenuItem({ label: subItem.label, - enabled: !subItem.is_disabled, + enabled: !subItem.isDisabled, priority: subItem.priority, type: [true, false].includes(subItem.checked) ? 'checkbox' : 'normal', checked: subItem.checked, @@ -572,8 +572,9 @@ function refreshMenuItems(menu) { } let _menuItem = new gui.MenuItem({ label: item.label, - enabled: !item.is_disabled, + enabled: !item.isDisabled, priority: item.priority, + type: item.type, ...(submenu.items.length > 0) && { submenu: submenu, }, @@ -583,10 +584,6 @@ function refreshMenuItems(menu) { }); el.submenu.append(_menuItem); - if (['create', 'register'].includes(item.category)) { - let separator_menu = new gui.MenuItem({ type: 'separator' }); - el.submenu.append(separator_menu); - } }); } }); diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index 00732c2bc..a42adf1f7 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -554,32 +554,18 @@ def index(): check_browser_upgrade() store_setting('LastUpdateCheck', today) - auth_only_internal = False - auth_source = [] - session['allow_save_password'] = True - if config.SERVER_MODE: - if session['auth_source_manager']['current_source'] == INTERNAL: - auth_only_internal = True - auth_source = session['auth_source_manager'][ - 'source_friendly_name'] - - if not config.MASTER_PASSWORD_REQUIRED and 'pass_enc_key' in session: - session['allow_save_password'] = False + if config.SERVER_MODE and not config.MASTER_PASSWORD_REQUIRED and \ + 'pass_enc_key' in session: + session['allow_save_password'] = False response = Response(render_template( MODULE_NAME + "/index.html", username=current_user.username, - auth_source=auth_source, - is_admin=current_user.has_role("Administrator"), - logout_url=get_logout_url(), requirejs=True, basejs=True, - mfa_enabled=is_mfa_enabled(), - login_url=login_url, - _=gettext, - auth_only_internal=auth_only_internal, + _=gettext )) # Set the language cookie after login, so next time the user will have that @@ -662,6 +648,16 @@ def utils(): for submodule in current_blueprint.submodules: snippets.extend(submodule.jssnippets) + + auth_only_internal = False + auth_source = [] + + if config.SERVER_MODE: + if session['auth_source_manager']['current_source'] == INTERNAL: + auth_only_internal = True + auth_source = session['auth_source_manager'][ + 'source_friendly_name'] + return make_response( render_template( 'browser/js/utils.js', @@ -684,6 +680,13 @@ def utils(): vw_edt_default_placeholder=VW_EDT_DEFAULT_PLACEHOLDER, enable_psql=config.ENABLE_PSQL, pgadmin_server_locale=default_locale, + _=gettext, + auth_only_internal=auth_only_internal, + mfa_enabled=is_mfa_enabled(), + is_admin=current_user.has_role("Administrator"), + login_url=login_url, + username=current_user.username, + auth_source=auth_source ), 200, {'Content-Type': MIMETYPE_APP_JS}) diff --git a/web/pgadmin/browser/static/js/main_menu.js b/web/pgadmin/browser/static/js/MainMenuFactory.js similarity index 50% rename from web/pgadmin/browser/static/js/main_menu.js rename to web/pgadmin/browser/static/js/MainMenuFactory.js index c0bef547e..6573c9445 100644 --- a/web/pgadmin/browser/static/js/main_menu.js +++ b/web/pgadmin/browser/static/js/MainMenuFactory.js @@ -8,55 +8,51 @@ ////////////////////////////////////////////////////////////// import gettext from 'sources/gettext'; import pgAdmin from 'sources/pgadmin'; -import { getBrowser } from '../../../static/js/utils'; -import Menu, { MenuItem } from './new_menu'; +import Menu, { MenuItem } from '../../../static/js/helpers/Menu'; import getApiInstance from '../../../static/js/api_instance'; import url_for from 'sources/url_for'; import Notifier from '../../../static/js/helpers/Notifier'; -export let MainMenus = [ +const MAIN_MENUS = [ { label: gettext('File'), name: 'file', id: 'mnu_file', index: 0, addSepratior: true }, { label: gettext('Object'), name: 'object', id: 'mnu_obj', index: 1, addSepratior: true }, { label: gettext('Tools'), name: 'tools', id: 'mnu_tools', index: 2, addSepratior: true }, { label: gettext('Help'), name: 'help', id: 'mnu_help', index: 5, addSepratior: false } ]; -let {name: browser} = getBrowser(); - -export default function createMainMenus() { - pgAdmin.Browser.MainMenus = []; - MainMenus.forEach((_menu) => { - let menuObj = Menu.create(_menu.name, _menu.label, _menu.id, _menu.index, _menu.addSepratior); - pgAdmin.Browser.MainMenus.push(menuObj); - // Don't add menuItems for Object menu as it's menuItems get changed on tree selection. - if(_menu.name !== 'object') { - menuObj.addMenuItems(Object.values(pgAdmin.Browser.menus[_menu.name])); - menuObj.menuItems.forEach((menuItem, index)=> { - menuItem?.menu_items?.forEach((item, indx)=> { - item.below && menuItem?.menu_items.splice(indx+1, 0, getSeparator()); +export default class MainMenuFactory { + static createMainMenus() { + pgAdmin.Browser.MainMenus = []; + MAIN_MENUS.forEach((_menu) => { + let menuObj = Menu.create(_menu.name, _menu.label, _menu.id, _menu.index, _menu.addSepratior); + pgAdmin.Browser.MainMenus.push(menuObj); + // Don't add menuItems for Object menu as it's menuItems get changed on tree selection. + if(_menu.name !== 'object') { + menuObj.addMenuItems(Object.values(pgAdmin.Browser.all_menus_cache[_menu.name])); + menuObj.getMenuItems().forEach((menuItem, index)=> { + menuItem?.getMenuItems()?.forEach((item, indx)=> { + item.below && menuItem?.getMenuItems().splice(indx+1, 0, MainMenuFactory.getSeparator()); + }); + if(menuItem.below) { + menuObj.addMenuItem(MainMenuFactory.getSeparator(), index+1); + } }); - if(menuItem.below) { - menuObj.addMenuItem(getSeparator(), index+1); - } - }); - } - }); -} + } + }); -function getSeparator() { - return new MenuItem({type: 'separator'}); -} + pgAdmin.Browser.enable_disable_menus(); + } -export function refreshMainMenuItems(menu, menuItems) { - if(browser == 'Nwjs') { + static getSeparator(label, priority) { + return new MenuItem({type: 'separator', label, priority}); + } + + static refreshMainMenuItems(menu, menuItems) { menu.setMenuItems(menuItems); pgAdmin.Browser.Events.trigger('pgadmin:nw-refresh-menu-item', menu); } -} -// Factory to create menu items for main menu. -export class MainMenuItemFactory { - static create(options) { + static createMenuItem(options) { return new MenuItem({...options, callback: () => { // Some callbacks registered in 'callbacks' check and call specifiec callback function if (options.module && 'callbacks' in options.module && options.module.callbacks[options.callback]) { @@ -83,4 +79,35 @@ export class MainMenuItemFactory { pgAdmin.Browser.Events.trigger('pgadmin:nw-update-checked-menu-item', item); }); } + + static getContextMenu(menuList, item, node) { + Menu.sortMenus(menuList); + + let ctxMenus = {}; + let ctxIndex = 1; + menuList.forEach(ctx => { + let ctx_uid = _.uniqueId('ctx_'); + let sub_ctx_item = {}; + ctx.checkAndSetDisabled(node, item); + if (ctx.getMenuItems()) { + // Menu.sortMenus(ctx.getMenuItems()); + ctx.getMenuItems().forEach((c) => { + c.checkAndSetDisabled(node, item); + if (!c.isDisabled) { + sub_ctx_item[ctx_uid + _.uniqueId('_sub_')] = c.getContextItem(c.label, c.isDisabled); + } + }); + } + if (!ctx.isDisabled) { + if(ctx.type == 'separator') { + ctxMenus[ctx_uid + '_' + ctx.priority + '_' + + ctxIndex + '_sep'] = '----'; + } else { + ctxMenus[ctx_uid + '_' + ctx.priority + '_' + + ctxIndex + '_itm'] = ctx.getContextItem(ctx.label, ctx.isDisabled, sub_ctx_item); + } + } + ctxIndex++; + }); + + return ctxMenus; + } } diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 9e736784d..25c3c6ee7 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -9,9 +9,7 @@ import React from 'react'; import { generateNodeUrl } from './node_ajax'; -import { getBrowser } from '../../../static/js/utils'; -import createMainMenus, {refreshMainMenuItems, MainMenuItemFactory} from './main_menu'; -import { getContextMenu} from './new_menu'; +import MainMenuFactory from './MainMenuFactory'; import _ from 'lodash'; import Notify, {initializeModalProvider, initializeNotifier} from '../../../static/js/helpers/Notifier'; import { checkMasterPassword } from '../../../static/js/Dialogs/index'; @@ -26,7 +24,7 @@ define('pgadmin.browser', [ 'sources/tree/tree_init', 'pgadmin.browser.utils', 'wcdocker', 'jquery.contextmenu', 'pgadmin.browser.preferences', 'pgadmin.browser.messages', - 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin.browser.layout', + 'pgadmin.browser.panel', 'pgadmin.browser.layout', 'pgadmin.browser.runtime', 'pgadmin.browser.error', 'pgadmin.browser.frame', 'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity', 'sources/codemirror/addon/fold/pgadmin-sqlfoldcode', @@ -290,9 +288,9 @@ define('pgadmin.browser', [ // We also support showing dashboards, HTML file, external URL frames: {}, /* Menus */ - // pgAdmin.Browser.MenuItem.add_menus(...) will register all the menus + // add_menus(...) will register all the menus // in this container - menus: { + all_menus_cache: { // All context menu goes here under certain menu types. // i.e. context: {'server': [...], 'server-group': [...]} context: {}, @@ -309,7 +307,8 @@ define('pgadmin.browser', [ // Help menus help: {}, }, - native_context_menus: {}, + MainMenus: [], + BrowserContextMenu: [], add_panels: function() { /* Add hooked-in panels by extensions */ @@ -378,40 +377,37 @@ define('pgadmin.browser', [ let category = { 'common': [] }; - for(let _key of Object.keys(obj.menus[name][d._type])){ - let menuCategory = obj.menus[name][d._type][_key].category; - let menuItem = obj.menus[name][d._type][_key]; - - if(menuCategory in this.menu_categories) { - category[menuCategory] ? category[menuCategory].push(menuItem): category[menuCategory] = [menuItem]; - } else if (!_.isUndefined(menuCategory)) { - category[menuCategory] ? category[menuCategory].push(menuItem): category[menuCategory] = [menuItem]; - } else { - category['common'].push(menuItem); - } + const nodeTypeMenus = obj.all_menus_cache[name][d._type]; + for(let key of Object.keys(nodeTypeMenus)) { + let menuItem = nodeTypeMenus[key]; + let menuCategory = menuItem.category ?? 'common'; + category[menuCategory] = category[menuCategory] ?? []; + category[menuCategory].push(menuItem); } let menuItemList = []; for(let c in category) { - if((c in obj.menu_categories || category[c].length > 1) && c != 'common' ) { - let is_all_option_dis = true; - category[c].forEach((c)=> { - c.is_disabled = c.disabled(d, item); - - c.setDisabled( c.is_disabled); - - if(is_all_option_dis) - is_all_option_dis = c.is_disabled; + let allMenuItemsDisabled = true; + category[c].forEach((mi)=> { + mi.checkAndSetDisabled(d, item); + if(allMenuItemsDisabled) { + allMenuItemsDisabled = mi.isDisabled; + } }); - let label = c in obj.menu_categories ? obj.menu_categories[c].label : c; - let priority = c in obj.menu_categories ? obj.menu_categories[c].priority || 10 : 10; - if(!is_all_option_dis && skipDisabled){ - let _menuItem = MainMenuItemFactory.create({ + const categoryMenuOptions = obj.menu_categories[c]; + let label = categoryMenuOptions?.label ?? c; + let priority = categoryMenuOptions?.priority ?? 10; + + if(categoryMenuOptions?.above) { + menuItemList.push(MainMenuFactory.getSeparator(label, priority)); + } + if(!allMenuItemsDisabled && skipDisabled) { + let _menuItem = MainMenuFactory.createMenuItem({ name: c, label: label, - module:c, + module: c, category: c, menu_items: category[c], priority: priority @@ -419,7 +415,7 @@ define('pgadmin.browser', [ menuItemList.push(_menuItem); } else if(!skipDisabled){ - let _menuItem = MainMenuItemFactory.create({ + let _menuItem = MainMenuFactory.createMenuItem({ name: c, label: label, module:c, @@ -430,15 +426,18 @@ define('pgadmin.browser', [ menuItemList.push(_menuItem); } + if(categoryMenuOptions?.below) { + menuItemList.push(MainMenuFactory.getSeparator(label, priority)); + } } else { category[c].forEach((c)=> { - c.is_disabled = c.disabled(d, item); + c.checkAndSetDisabled(d, item); }); category[c].forEach((m)=> { if(!skipDisabled) { menuItemList.push(m); - } else if(skipDisabled && !m.is_disabled){ + } else if(skipDisabled && !m.isDisabled){ menuItemList.push(m); } }); @@ -449,77 +448,37 @@ define('pgadmin.browser', [ }, // Enable/disable menu options enable_disable_menus: function(item) { - // Mechanism to enable/disable menus depending on the condition. - let obj = this, - // menu navigation bar - navbar = $('#navbar-menu > ul').first(), - // Drop down menu for objects - $obj_mnu = navbar.find('li#mnu_obj .dropdown-menu').first(), - // data for current selected object - d = item ? obj.tree.itemData(item) : undefined, - update_menuitem = function(m, o) { - if (m instanceof pgAdmin.Browser.MenuItem) { - m.update(d, item); - } - else { - for (let key in m) { - update_menuitem(m[key], o); - } - } - }; - + let obj = this; + let d = item ? obj.tree.itemData(item) : undefined; toolBar.enable(gettext('View Data'), false); toolBar.enable(gettext('Filtered Rows'), false); - // All menus from the object menus (except the create drop-down - // menu) needs to be removed. - $obj_mnu.empty(); - // All menus (except for the object menus) are already present. - // They will just require to check, wheather they are + // They will just require to check, whether they are // enabled/disabled. - let {name: browser} = getBrowser(); - if(browser == 'Nwjs') { - pgBrowser.MainMenus.forEach((menu) => { - menu.menuItems.forEach((mitem) => { - mitem.setDisabled(mitem.disabled(d, item)); - }); + pgBrowser.MainMenus.filter((m)=>m.name != 'object').forEach((menu) => { + menu.menuItems.forEach((mitem) => { + mitem.checkAndSetDisabled(d, item); }); - }else { - _.each([ - {name: 'file', id: '#mnu_file', label: gettext('File')}, - {name: 'management', id: '#mnu_management', label: gettext('Management')}, - {name: 'tools', id: '#mnu_tools', label: gettext('Tools')}, - {name: 'help', id:'#mnu_help', label: gettext('Help')}], function(o) { - _.each( obj.menus[o.name], function(m) { - update_menuitem(m, o); - }); - }); - } + }); // Create the object menu dynamically - if (item && obj.menus['object'] && obj.menus['object'][d._type]) { - if(browser == 'Nwjs') { - let menuItemList = obj.getMenuList('object', item, d); - let objectMenu = pgBrowser.MainMenus.filter((menu) => menu.name == 'object'); - objectMenu.length > 0 && refreshMainMenuItems(objectMenu[0], menuItemList); - let ctxMenuList = obj.getMenuList('context', item, d, true); - obj.native_context_menus = getContextMenu(ctxMenuList, item, d); - } else { - pgAdmin.Browser.MenuCreator( - obj.Nodes, $obj_mnu, obj.menus['object'][d._type], obj.menu_categories, d, item - ); - } + let objectMenu = pgBrowser.MainMenus.find((menu) => menu.name == 'object'); + if (item && obj.all_menus_cache['object']?.[d._type]) { + let menuItemList = obj.getMenuList('object', item, d); + objectMenu && MainMenuFactory.refreshMainMenuItems(objectMenu, menuItemList); + let ctxMenuList = obj.getMenuList('context', item, d, true); + obj.BrowserContextMenu = MainMenuFactory.getContextMenu(ctxMenuList, item, d); } else { - // Create a dummy 'no object seleted' menu - let create_submenu = pgAdmin.Browser.MenuGroup( - obj.menu_categories['create'], [{ - $el: $('
  • ' + gettext('No object selected') + '
  • '), - priority: 1, + objectMenu && MainMenuFactory.refreshMainMenuItems(objectMenu, [ + MainMenuFactory.createMenuItem({ + name: '', + label: gettext('No object selected'), category: 'create', - update: function() {/*This is intentional (SonarQube)*/}, - }], false); - $obj_mnu.append(create_submenu.$el); + priority: 1, + enable: false, + }) + ]); } }, init: function() { @@ -611,20 +570,10 @@ define('pgadmin.browser', [ autoHide: false, build: function(element) { let item = obj.tree.itemFrom(element), - d = obj.tree.itemData(item), - menus = obj.menus['context'][d._type], - $div = $('
    '), context_menu = {}; if(item) obj.tree.select(item); - let {name: browser} = getBrowser(); - if(browser == 'Nwjs'){ - context_menu = obj.native_context_menus; - } else { - pgAdmin.Browser.MenuCreator( - obj.Nodes, $div, menus, obj.menu_categories, d, item, context_menu - ); - } + context_menu = obj.BrowserContextMenu; return { autoHide: false, @@ -641,7 +590,6 @@ define('pgadmin.browser', [ // Register scripts and add menus pgBrowser.utils.registerScripts(this); - pgBrowser.utils.addMenus(obj); let headers = {}; headers[pgAdmin.csrf_token_header] = pgAdmin.csrf_token; @@ -780,16 +728,11 @@ define('pgadmin.browser', [ // Add menus of module/extension at appropriate menu add_menus: function(menus) { let self = this, - pgMenu = this.menus, - MenuItem = pgAdmin.Browser.MenuItem; - let {name: browser} = getBrowser(); - // MenuItem = pgAdmin.Browser.MenuItem; + pgMenu = this.all_menus_cache; + _.each(menus, function(m) { _.each(m.applies, function(a) { /* We do support menu type only from this list */ - // if ($.inArray(a, [ - // 'context', 'file', 'edit', 'object', - // 'management', 'tools', 'help']) >= 0) { if(['context', 'file', 'edit', 'object','management', 'tools', 'help'].indexOf(a) > -1){ let _menus; @@ -835,42 +778,23 @@ define('pgadmin.browser', [ }; } - if(browser == 'Nwjs') { - return MainMenuItemFactory.create({ - name: _m.name, - label: _m.label, - module: _m.module, - category: _m.category, - callback: typeof _m.module == 'object' && _m.module[_m.callback] && _m.callback in _m.module[_m.callback] ? _m.module[_m.callback] : _m.callback, - priority: _m.priority, - data: _m.data, - url: _m.url || '#', - target: _m.target, - icon: _m.icon, - enable: enable ? enable : true, - node: _m.node, - checked: _m.checked, - below: _m.below, - applies: _m.applies, - }); - } else { - return new MenuItem({ - name: _m.name, - label: _m.label, - module: _m.module, - category: _m.category, - callback: _m.callback, - priority: _m.priority, - data: _m.data, - url: _m.url || '#', - target: _m.target, - icon: _m.icon, - enable, - node: _m.node, - checked: _m.checked, - below: _m.below, - }); - } + return MainMenuFactory.createMenuItem({ + name: _m.name, + label: _m.label, + module: _m.module, + category: _m.category, + callback: typeof _m.module == 'object' && _m.module[_m.callback] && _m.callback in _m.module[_m.callback] ? _m.module[_m.callback] : _m.callback, + priority: _m.priority, + data: _m.data, + url: _m.url || '#', + target: _m.target, + icon: _m.icon, + enable: enable ? enable : true, + node: _m.node, + checked: _m.checked, + below: _m.below, + applies: _m.applies, + }); }; if (!_.has(_menus, m.name)) { @@ -895,62 +819,6 @@ define('pgadmin.browser', [ }); }); }, - // Create the menus - create_menus: function () { - let { name: browser } = getBrowser(); - // Add Native menus if NWjs app - if (browser == 'Nwjs') { - createMainMenus(); - this.enable_disable_menus(); - } else { - /* Create menus */ - let navbar = $('#navbar-menu > ul').first(); - let obj = this; - - _.each([ - { menu: 'file', id: '#mnu_file' }, - { menu: 'management', id: '#mnu_management' }, - { menu: 'tools', id: '#mnu_tools' }, - { menu: 'help', id: '#mnu_help' }], - function (o) { - let $mnu = navbar.children(o.id).first(), - $dropdown = $mnu.children('.dropdown-menu').first(); - $dropdown.empty(); - if (o.menu == 'help') { - $dropdown.append('
    '); - $dropdown.append(''); - } - - if (pgAdmin.Browser.MenuCreator( - obj.Nodes, $dropdown, obj.menus[o.menu], obj.menu_categories - )) { - $mnu.removeClass('d-none'); - } - }); - - navbar.children('#mnu_obj').removeClass('d-none'); - obj.enable_disable_menus(); - } - }, - // General function to handle callbacks for object or dialog help. - showHelp: function(type, url, node, item) { - if (type == 'object_help') { - // Construct the URL - let server = pgBrowser.tree.getTreeNodeHierarchy(item).server; - let baseUrl = pgBrowser.utils.pg_help_path; - let fullUrl = help.getHelpUrl(baseUrl, url, server.version); - - window.open(fullUrl, 'postgres_help'); - } else if(type == 'dialog_help') { - if (pgWindow && pgWindow.default) { - pgWindow.default.open(url, 'pgadmin_help'); - } - else { - window.open(url, 'pgadmin_help'); - } - } - $('#live-search-field').focus(); - }, _findTreeChildNode: function(_i, _d, _o) { let loaded = _o.t.wasLoad(_i), onLoad = function() { @@ -2224,27 +2092,6 @@ define('pgadmin.browser', [ brace_matching: pgBrowser.utils.braceMatching, indent_with_tabs: pgBrowser.utils.is_indent_with_tabs, }, - - // This function will return the name and version of the browser. - get_browser: function() { - let ua=navigator.userAgent,tem,M=ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; - if(/trident/i.test(M[1])) { - tem=/\brv[ :]+(\d+)/g.exec(ua) || []; - return {name:'IE', version:(tem[1]||'')}; - } - - if(M[1]==='Chrome') { - tem=ua.match(/\bOPR|Edge\/(\d+)/); - if(tem!=null) {return {name:tem[0], version:tem[1]};} - } - - M=M[2]? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?']; - if((tem=ua.match(/version\/(\d+)/i))!=null) {M.splice(1,1,tem[1]);} - return { - name: M[0], - version: M[1], - }; - }, }); /* Remove paste event mapping from CodeMirror's emacsy KeyMap binding @@ -2258,9 +2105,6 @@ define('pgadmin.browser', [ if (pgBrowser.utils.useSpaces == 'True') { pgAdmin.Browser.editor_shortcut_keys.Tab = 'insertSoftTab'; } - setTimeout(function(){ - $('#mnu_about').closest('li').before(''); - }, 100); return pgAdmin.Browser; }); diff --git a/web/pgadmin/browser/static/js/layout.js b/web/pgadmin/browser/static/js/layout.js index e2fe08a3e..64a7adea8 100644 --- a/web/pgadmin/browser/static/js/layout.js +++ b/web/pgadmin/browser/static/js/layout.js @@ -113,8 +113,8 @@ _.extend(pgBrowser, { lock_layout: function(docker, op) { let menu_items = []; - if('mnu_locklayout' in this.menus['file']) { - menu_items = this.menus['file']['mnu_locklayout']['menu_items']; + if('mnu_locklayout' in this.all_menus_cache['file']) { + menu_items = this.all_menus_cache['file']['mnu_locklayout']['menu_items']; } switch(op) { diff --git a/web/pgadmin/browser/static/js/menu.js b/web/pgadmin/browser/static/js/menu.js deleted file mode 100644 index 97de93e97..000000000 --- a/web/pgadmin/browser/static/js/menu.js +++ /dev/null @@ -1,521 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// -import _ from 'lodash'; -import {MenuItem as NewMenuItem} from './new_menu'; - -define([ - 'sources/pgadmin', 'jquery', 'sources/utils', 'sources/gettext', -], function(pgAdmin, $, pgadminUtils, gettext) { - 'use strict'; - - pgAdmin.Browser = pgAdmin.Browser || {}; - - // Individual menu-item class - let MenuItem = pgAdmin.Browser.MenuItem = function(opts) { - let menu_opts = [ - 'name', 'label', 'priority', 'module', 'callback', 'data', 'enable', - 'category', 'target', 'url' /* Do not show icon in the menus, 'icon' */ , 'node', - 'checked', 'below', 'menu_items', - ], - defaults = { - url: '#', - target: '_self', - enable: true, - }; - _.extend(this, defaults, _.pick(opts, menu_opts)); - }; - - _.extend(pgAdmin.Browser.MenuItem.prototype, { - /* - * Keeps track of the jQuery object representing this menu-item. This will - * be used by the update function to enable/disable the individual item. - */ - $el: null, - /* - * Generate the UI for this menu-item. enable/disable, based on the - * currently selected object. - */ - generate: function(node, item) { - this.create_el(node, item); - - this.context = { - name: this.label, - /* icon: this.icon || this.module && (this.module.type), */ - disabled: this.is_disabled, - callback: this.context_menu_callback.bind(this, item), - }; - - return this.$el; - }, - - /* - * Create the jquery element for the menu-item. - */ - create_el: function(node, item) { - - if(this.menu_items) { - _.each(this.menu_items, function(submenu_item){ - submenu_item.generate(node, item); - }); - let create_submenu = pgAdmin.Browser.MenuGroup({ - 'label': this.label, - 'id': this.name, - }, this.menu_items); - this.$el = create_submenu.$el; - } else { - let data_disabled = null; - if(this.data != undefined && this.data.data_disabled != undefined){ - data_disabled = this.data.data_disabled; - } - let url = $('', { - 'id': this.name, - 'href': this.url, - 'target': this.target, - 'data-toggle': 'pg-menu', - 'role': 'menuitem', - 'data-disabled': data_disabled, - }).data('pgMenu', { - module: this.module || pgAdmin.Browser, - cb: this.callback, - data: this.data, - }).addClass('dropdown-item'); - - this.is_disabled = this.disabled(node, item); - if (this.icon) { - url.append($('', { - 'class': this.icon, - })); - } else if(!_.isUndefined(this.checked)) { - url.append($('', { - 'class': 'fa fa-check '+ (this.checked?'':'visibility-hidden'), - })); - } - - url.addClass((this.is_disabled ? ' disabled' : '')); - - let textSpan = $('').text(' ' + this.label); - - url.append(textSpan); - - let mnu_element = $('
  • ').append(url); - // Check if below parameter is defined and true then we need to add - // separator. - if (!_.isUndefined(this.below) && this.below === true) { - mnu_element.append('
  • '); - } - - this.$el = mnu_element; - } - - }, - /* - * Updates the enable/disable state of the menu-item based on the current - * selection using the disabled function. This also creates a object - * for this menu, which can be used in the context-menu. - */ - update: function(node, item) { - - if (this.$el && !this.$el.find('.dropdown-item').hasClass('disabled')) { - this.$el.find('.dropdown-item').addClass('disabled'); - } - - this.is_disabled = this.disabled(node, item); - if (this.$el && !this.is_disabled) { - this.$el.find('.dropdown-item').removeClass('disabled'); - } - - this.context = { - name: this.label, - /* icon: this.icon || (this.module && this.module.type), */ - disabled: this.is_disabled, - callback: this.context_menu_callback.bind(this, item), - }; - }, - - /* - * This will be called when context-menu is clicked. - */ - context_menu_callback: function(item) { - let o = this, - cb; - - if (o.module['callbacks'] && ( - o.callback in o.module['callbacks'] - )) { - cb = o.module['callbacks'][o.callback]; - } else if (o.callback in o.module) { - cb = o.module[o.callback]; - } - if (cb) { - cb.apply(o.module, [o.data, item]); - } else { - pgAdmin.Browser.report_error( - pgadminUtils.sprintf('Developer Warning: Callback - "%s" not found!', o.cb) - ); - } - }, - - /* - * Checks this menu enable/disable state based on the selection. - */ - disabled: function(node, item) { - if (this.enable == undefined) { - return false; - } - - if (this.node) { - if (!node) { - return true; - } - if (_.isArray(this.node) ? ( - _.indexOf(this.node, node) == -1 - ) : (this.node != node._type)) { - return true; - } - } - - if (_.isBoolean(this.enable)) return !this.enable; - if (_.isFunction(this.enable)) return !this.enable.apply(this.module, [node, item, this.data]); - if (this.module && _.isBoolean(this.module[this.enable])) return !this.module[this.enable]; - if (this.module && _.isFunction(this.module[this.enable])) return !(this.module[this.enable]).apply(this.module, [node, item, this.data]); - - return false; - }, - - /* - * Change the checked value and update the checked icon on the menu - */ - change_checked(isChecked) { - if(!_.isUndefined(this.checked)) { - this.checked = isChecked; - if(this.checked) { - this.$el.find('.fa-check').removeClass('visibility-hidden'); - } else { - this.$el.find('.fa-check').addClass('visibility-hidden'); - } - } - }, - }); - - /* - * This a class for creating a menu group, mainly used by the submenu - * creation logic. - * - * Arguments: - * 1. Options to render the submenu DOM Element. - * i.e. label, icon, above (separator), below (separator) - * 2. List of menu-items comes under this submenu. - * 3. Did we rendered separator after the menu-item/submenu? - * 4. A unique-id for this menu-item. - * - * Returns a object, similar to the menu-item, which has his own jQuery - * Element, context menu representing object, etc. - * - */ - - pgAdmin.Browser.MenuGroup = function(opts, items, prev, ctx) { - let template = _.template([ - '<% if (above) { %><% } %>', - '', - '<% if (below) { %><% } %>', - ].join('\n')), - data = { - 'label': opts.label, - 'icon': opts.icon, - 'above': opts.above && !prev, - 'below': opts.below, - }, - m, - $el = $(template(data)), - $menu = $el.find('.dropdown-menu'), - submenus = {}, - ctxId = 1; - - ctx = _.uniqueId(ctx + '_sub_'); - - // Sort by alphanumeric ordered first - items.sort(function(a, b) { - return a.label.localeCompare(b.label); - }); - // Sort by priority - items.sort(function(a, b) { - return a.priority - b.priority; - }); - - for (let idx in items) { - m = items[idx]; - $menu.append(m.$el); - if (!m.is_disabled) { - submenus[ctx + ctxId] = m.context; - } - ctxId++; - } - - let is_disabled = (_.size(submenus) == 0); - - return { - $el: $el, - priority: opts.priority || 10, - label: opts.label, - above: data['above'], - below: opts.below, - is_disabled: is_disabled, - context: { - name: opts.label, - icon: opts.icon, - items: submenus, - disabled: is_disabled, - }, - }; - }; - - /* - * A function to generate menus (submenus) based on the categories. - * Attach the current selected browser tree node to each of the generated - * menu-items. - * - * Arguments: - * 1. nodes_obj - Nodes object contains each node object. - * 2. jQuery Element on which you may want to created the menus - * 3. list of menu-items - * 4. categories - metadata information about the categories, based on which - * the submenu (menu-group) will be created (if any). - * 5. d - Data object for the selected browser tree item. - * 6. item - The selected browser tree item - * 7. menu_items - A empty object on which the context menu for the given - * list of menu-items. - * - * Returns if any menu generated for the given input. - */ - pgAdmin.Browser.MenuCreator = function( - nodes_obj, $mnu, menus, categories, d, item, menu_items - ) { - let showMenu = true; - - /* We check showMenu function is defined by the respective node, if it is - * defined then call the function which will return true or false. - */ - if (d && nodes_obj[d._type] && !_.isUndefined(nodes_obj[d._type].showMenu)) - showMenu = nodes_obj[d._type].showMenu(d, item); - - if (!showMenu) { - menu_items = menu_items || {}; - menu_items[_.uniqueId('ctx_')+ '1_1_ms'] = { - disabled : true, - name: gettext('No menu available for this object.'), - }; - return; - } - - let groups = { - 'common': [], - }, - common, idx = 0, - ctxId = _.uniqueId('ctx_'), - update_menuitem = function(m) { - if (m instanceof MenuItem) { - if (m.$el) { - m.$el.remove(); - delete m.$el; - } - m.generate(d, item); - let group = groups[m.category || 'common'] = - groups[m.category || 'common'] || []; - group.push(m); - } else if(m instanceof NewMenuItem) { - if (m.$el) { - m.$el.remove(); - delete m.$el; - } - m.generate(this, self, this.context_menu_callback, item); - let group = groups[m.category || 'common'] = - groups[m.category || 'common'] || []; - group.push(m); - } - else { - for (let key in m) { - update_menuitem(m[key]); - } - } - }, - ctxIdx = 1; - - for (idx in menus) { - update_menuitem(menus[idx]); - } - - // Not all menu creator requires the context menu structure. - menu_items = menu_items || {}; - - common = groups['common']; - delete groups['common']; - - let prev = true; - - for (let name in groups) { - let g = groups[name], - c = categories[name] || { - 'label': name, - single: false, - }, - menu_group = pgAdmin.Browser.MenuGroup(c, g, prev, ctxId); - - if (g.length <= 1 && !c.single) { - prev = false; - for (idx in g) { - common.push(g[idx]); - } - } else { - prev = g.below; - common.push(menu_group); - } - } - - // The menus will be created based on the priority given. - // Menu with lowest value has the highest priority. If the priority is - // same, then - it will be ordered by label. - // Sort by alphanumeric ordered first - common.sort(function(a, b) { - return a.label.localeCompare(b.label); - }); - // Sort by priority - common.sort(function(a, b) { - return a.priority - b.priority; - }); - let len = _.size(common); - - for (idx in common) { - item = common[idx]; - - item.priority = (item.priority || 10); - $mnu.append(item.$el); - let prefix = ctxId + '_' + item.priority + '_' + ctxIdx; - - if (ctxIdx != 1 && item.above && !item.is_disabled) { - // For creatign the seprator any string will do. - menu_items[prefix + '_ma'] = '----'; - } - - if (!item.is_disabled) { - menu_items[prefix + '_ms'] = item.context; - } - - if (ctxId != len && item.below && !item.is_disabled) { - menu_items[prefix + '_mz'] = '----'; - } - ctxIdx++; - } - - return (len > 0); - }; - - // MENU PUBLIC CLASS DEFINITION - // ============================== - let Menu = function(element, options) { - this.$element = $(element); - this.options = $.extend({}, Menu.DEFAULTS, options); - this.isLoading = false; - }; - - Menu.DEFAULTS = {}; - - Menu.prototype.toggle = function(ev) { - let $parent = this.$element.closest('.dropdown-item'); - if ($parent.hasClass('disabled')) { - ev.preventDefault(); - return false; - } - - let d = this.$element.data('pgMenu'); - if (d.cb) { - let cb = d.module && d.module['callbacks'] && d.module['callbacks'][d.cb] || d.module && d.module[d.cb]; - cb = cb || d.cb; - if (cb) { - cb.apply(d.module, [d.data, pgAdmin.Browser.tree.selected()]); - ev.preventDefault(); - } else { - pgAdmin.Browser.report_error('Developer Warning: Callback - "' + d.cb + '" not found!'); - } - } - }; - - - // BUTTON PLUGIN DEFINITION - // ======================== - - function Plugin(option, ev) { - return this.each(function() { - let $this = $(this); - let data = $this.data('pg.menu'); - let options = typeof option == 'object' && option; - - if (!data) $this.data('pg.menu', (data = new Menu(this, options))); - - data.toggle(ev); - }); - } - - let old = $.fn.button; - - $.fn.pgmenu = Plugin; - $.fn.pgmenu.Constructor = Menu; - - - // BUTTON NO CONFLICT - // ================== - - $.fn.pgmenu.noConflict = function() { - $.fn.pgmenu = old; - return this; - }; - - // MENU DATA-API - // ============= - - $(document) - .on('click.pg.menu.data-api', '[data-toggle^="pg-menu"]', function(ev) { - let $menu = $(ev.target); - if (!$menu.hasClass('dropdown-item')) - $menu = $menu.closest('.dropdown-item'); - Plugin.call($menu, 'toggle', ev); - }) - .on( - 'focus.pg.menu.data-api blur.pg.menu.data-api', - '[data-toggle^="pg-menu"]', - function(ev) { - $(ev.target).closest('.menu').toggleClass( - 'focus', /^focus(in)?$/.test(ev.type) - ); - }) - .on('mouseenter', '.dropdown-submenu', function(ev) { - $(ev.currentTarget).removeClass('dropdown-submenu-visible') - .addClass('dropdown-submenu-visible'); - $(ev.currentTarget).find('.dropdown-menu').first().addClass('show'); - }) - .on('mouseleave', '.dropdown-submenu', function(ev) { - $(ev.currentTarget).removeClass('dropdown-submenu-visible'); - $(ev.currentTarget).find('.dropdown-menu').first().removeClass('show'); - }) - .on('hidden.bs.dropdown', function(ev) { - $(ev.target) - .find('.dropdown-submenu.dropdown-submenu-visible') - .removeClass('dropdown-submenu-visible') - .find('.dropdown-menu.show') - .removeClass('show'); - }); - - return pgAdmin.Browser.MenuItem; -}); diff --git a/web/pgadmin/browser/static/js/quick_search/menuitems_help.js b/web/pgadmin/browser/static/js/quick_search/menuitems_help.js index 6e2cac466..7e0f72674 100644 --- a/web/pgadmin/browser/static/js/quick_search/menuitems_help.js +++ b/web/pgadmin/browser/static/js/quick_search/menuitems_help.js @@ -6,8 +6,8 @@ // This software is released under the PostgreSQL Licence // ////////////////////////////////////////////////////////////// -import {MenuItem as NewMenuItem} from '../new_menu'; -import { MainMenus } from '../main_menu'; +import {MenuItem as NewMenuItem} from '../../../../static/js/helpers/Menu'; +import { MainMenus } from '../MainMenuFactory'; import pgAdmin from 'sources/pgadmin'; import { getBrowser } from '../../../../static/js/utils'; @@ -24,7 +24,7 @@ export function menuSearch(param, props) { const iterItem = (subMenus, path, parentPath) => { subMenus.forEach((subMenu) =>{ - if(subMenu instanceof NewMenuItem || subMenu instanceof pgAdmin.Browser.MenuItem) { + if(subMenu instanceof NewMenuItem) { if(subMenu.type != 'separator' && subMenu?.label?.toLowerCase().indexOf(param.toLowerCase()) != -1){ let localPath = path; if(parentPath) { @@ -40,11 +40,11 @@ export function menuSearch(param, props) { result.push(subMenu); } } - if(subMenu.menu_items) { - iterItem(subMenu.menu_items, getMenuName(subMenu), path); + if(subMenu.getMenuItems()) { + iterItem(subMenu.getMenuItems(), getMenuName(subMenu), path); } } else { - if(typeof(subMenu) == 'object' && !(subMenu instanceof NewMenuItem || subMenu instanceof pgAdmin.Browser.MenuItem)) { + if(typeof(subMenu) == 'object' && !(subMenu instanceof NewMenuItem)) { iterItem(Object.values(subMenu), path, parentPath); } else { iterItem(subMenu, path, parentPath); @@ -67,10 +67,10 @@ export function menuSearch(param, props) { if(menu.name == 'object') { let selectedNode = pgAdmin.Browser.tree.selected(); if(selectedNode) { - subMenus = pgAdmin.Browser.menus[menu.name][selectedNode._metadata.data._type]; + subMenus = pgAdmin.Browser.all_menus_cache[menu.name][selectedNode._metadata.data._type]; } } else { - subMenus = pgAdmin.Browser.menus[menu.name]; + subMenus = pgAdmin.Browser.all_menus_cache[menu.name]; } iterItem(Object.values(subMenus), getMenuName(menu)); }); diff --git a/web/pgadmin/browser/templates/browser/index.html b/web/pgadmin/browser/templates/browser/index.html index f8c420416..d293a3c6b 100644 --- a/web/pgadmin/browser/templates/browser/index.html +++ b/web/pgadmin/browser/templates/browser/index.html @@ -1,11 +1,5 @@ {% extends "base.html" %} -{% if config.SERVER_MODE and config.SHOW_GRAVATAR_IMAGE -%} -{% import 'browser/macros/gravatar_icon.macro' as IMG with context %} -{% elif config.SERVER_MODE %} -{% import 'browser/macros/static_user_icon.macro' as IMG with context %} -{% endif %} - {% block title %}{{ config.APP_NAME }}{% endblock %} {% block init_script %} @@ -74,17 +68,6 @@ require.onResourceLoad = function (context, map, depMaps) { } }; -{% if config.SERVER_MODE %} -window.onload = function(e){ - setTimeout(function() { - var gravatarImg = {{ IMG.PREPARE_HTML()|safe }} - var navbarUser = document.getElementById("navbar-user"); - if (navbarUser) { - navbarUser.innerHTML = gravatarImg; - } - }, 1000); -}; -{% endif %} {% endblock %} {% block css_link %} @@ -99,80 +82,7 @@ window.onload = function(e){ {% if current_app.PGADMIN_RUNTIME | string() == 'False' %} - - +
    {% else %}
    diff --git a/web/pgadmin/browser/templates/browser/js/utils.js b/web/pgadmin/browser/templates/browser/js/utils.js index 2fd7e91ae..a45f51f00 100644 --- a/web/pgadmin/browser/templates/browser/js/utils.js +++ b/web/pgadmin/browser/templates/browser/js/utils.js @@ -95,26 +95,74 @@ define('pgadmin.browser.utils', // after they all were loaded completely. }, - addMenus: function (obj) { + addBackendMenus: function (obj) { // Generate the menu items only when all the initial scripts // were loaded completely. // // First - register the menus from the other // modules/extensions. - let self = this; - if (this.counter.total == this.counter.loaded) { - {% for key in ('File', 'Edit', 'Object' 'Tools', 'Management', 'Help') %} - obj.add_menus({{ MENU_ITEMS(key, current_app.menu_items['%s_items' % key.lower()])}}); - {% endfor %} - if('{{current_app.PGADMIN_RUNTIME}}' == 'False') { - obj.create_menus(); - } - } else { - //recall after some time - setTimeout(function(){ self.addMenus(obj); }, 3000); - } + {% for key in ('File', 'Edit', 'Object' 'Tools', 'Management', 'Help') %} + obj.add_menus({{ MENU_ITEMS(key, current_app.menu_items['%s_items' % key.lower()])}}); + {% endfor %} }, + {% if current_app.config.get('SERVER_MODE') %} + userMenuInfo: { + username: '{{username}}', + auth_source: '{{auth_source}}', + gravatar: {% if config.SHOW_GRAVATAR_IMAGE %}'{{ username | gravatar }}'{% else %}''{% endif %}, + menus: [ + {% if auth_only_internal %} + { + label: '{{ _('Change Password') }}', + type: 'normal', + callback: ()=>{ + pgAdmin.UserManagement.change_password( + '{{ url_for('browser.change_password') }}' + ) + } + }, + { + type: 'separator', + }, + {% endif %} + {% if mfa_enabled is defined and mfa_enabled is true %} + { + label: '{{ _('Two-Factor Authentication') }}', + type: 'normal', + callback: ()=>{ + pgAdmin.UserManagement.show_mfa( + '{{ login_url("mfa.register", next_url="internal") }}' + ) + } + }, + { + type: 'separator', + }, + {% endif %} + {% if is_admin %} + { + label: '{{ _('Users') }}', + type: 'normal', + callback: ()=>{ + pgAdmin.UserManagement.show_users() + } + }, + { + type: 'separator', + }, + {% endif %} + { + label: '{{ _('Logout') }}', + type: 'normal', + callback: ()=>{ + window.location="{{ logout_url }}"; + } + }, + ], + }, + {% endif %} + // load the module right now load_module: function(name, path, c) { let obj = this; diff --git a/web/pgadmin/browser/templates/browser/macros/gravatar_icon.macro b/web/pgadmin/browser/templates/browser/macros/gravatar_icon.macro deleted file mode 100644 index eded8b68a..000000000 --- a/web/pgadmin/browser/templates/browser/macros/gravatar_icon.macro +++ /dev/null @@ -1,8 +0,0 @@ -{########################################################################## -We wrote separate macro because if user choose to disable Gravatar then -we will not associate our application with Gravatar module which will make -'gravatar' filter unavailable in Jinja templates -###########################################################################} -{% macro PREPARE_HTML() -%} -'Gravatar image for {{ username }} {{ username }} ({{auth_source}}) '; -{%- endmacro %} diff --git a/web/pgadmin/browser/templates/browser/macros/static_user_icon.macro b/web/pgadmin/browser/templates/browser/macros/static_user_icon.macro deleted file mode 100644 index 6b7284491..000000000 --- a/web/pgadmin/browser/templates/browser/macros/static_user_icon.macro +++ /dev/null @@ -1,3 +0,0 @@ -{% macro PREPARE_HTML() -%} -' {{ username }} '; -{%- endmacro %} diff --git a/web/pgadmin/static/bundle/app.js b/web/pgadmin/static/bundle/app.js index 28749fb67..c9586d5b6 100644 --- a/web/pgadmin/static/bundle/app.js +++ b/web/pgadmin/static/bundle/app.js @@ -7,6 +7,12 @@ // ////////////////////////////////////////////////////////////// +import React from 'react'; +import ReactDOM from 'react-dom'; +import MainMenuFactory from '../../browser/static/js/MainMenuFactory'; +import AppMenuBar from '../js/AppMenuBar'; +import Theme from '../js/Theme'; + define('app', [ 'sources/pgadmin', 'bundled_browser', ], function(pgAdmin) { @@ -38,6 +44,14 @@ define('app', [ initializeModules(pgAdmin.Browser); initializeModules(pgAdmin.Tools); - // create menus after all modules are initialized. - pgAdmin.Browser.create_menus(); + // Add menus from back end. + pgAdmin.Browser.utils.addBackendMenus(pgAdmin.Browser); + + // Create menus after all modules are initialized. + MainMenuFactory.createMainMenus(); + + const menuContainerEle = document.querySelector('#main-menu-container'); + if(menuContainerEle) { + ReactDOM.render(, document.querySelector('#main-menu-container')); + } }); diff --git a/web/pgadmin/static/js/AppMenuBar.jsx b/web/pgadmin/static/js/AppMenuBar.jsx new file mode 100644 index 000000000..038f07edd --- /dev/null +++ b/web/pgadmin/static/js/AppMenuBar.jsx @@ -0,0 +1,146 @@ +import { Box, makeStyles } from '@material-ui/core'; +import React, { useState } from 'react'; +import { PrimaryButton } from './components/Buttons'; +import { PgMenu, PgMenuDivider, PgMenuItem, PgSubMenu } from './components/Menu'; +import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; +import AccountCircleRoundedIcon from '@material-ui/icons/AccountCircleRounded'; +import pgAdmin from 'sources/pgadmin'; +import { useEffect } from 'react'; + +const useStyles = makeStyles((theme)=>({ + root: { + height: '32px', + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + padding: '0 0.5rem', + display: 'flex', + alignItems: 'center', + }, + logo: { + width: '96px', + height: '100%', + /* + * Using the SVG postgresql logo, modified to set the background color as #FFF + * https://wiki.postgresql.org/images/9/90/PostgreSQL_logo.1color_blue.svg + * background: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 42 42' style='enable-background:new 0 0 42 42;' xml:space='preserve'%3E%3Cstyle type='text/css'%3E .st0%7Bstroke:%23000000;stroke-width:3.3022;%7D .st1%7Bfill:%23336791;%7D .st2%7Bfill:none;stroke:%23FFFFFF;stroke-width:1.1007;stroke-linecap:round;stroke-linejoin:round;%7D .st3%7Bfill:none;stroke:%23FFFFFF;stroke-width:1.1007;stroke-linecap:round;stroke-linejoin:bevel;%7D .st4%7Bfill:%23FFFFFF;stroke:%23FFFFFF;stroke-width:0.3669;%7D .st5%7Bfill:%23FFFFFF;stroke:%23FFFFFF;stroke-width:0.1835;%7D .st6%7Bfill:none;stroke:%23FFFFFF;stroke-width:0.2649;stroke-linecap:round;stroke-linejoin:round;%7D%0A%3C/style%3E%3Cg id='orginal'%3E%3C/g%3E%3Cg id='Layer_x0020_3'%3E%3Cpath class='st0' d='M31.3,30c0.3-2.1,0.2-2.4,1.7-2.1l0.4,0c1.2,0.1,2.8-0.2,3.7-0.6c2-0.9,3.1-2.4,1.2-2 c-4.4,0.9-4.7-0.6-4.7-0.6c4.7-7,6.7-15.8,5-18c-4.6-5.9-12.6-3.1-12.7-3l0,0c-0.9-0.2-1.9-0.3-3-0.3c-2,0-3.5,0.5-4.7,1.4 c0,0-14.3-5.9-13.6,7.4c0.1,2.8,4,21.3,8.7,15.7c1.7-2,3.3-3.8,3.3-3.8c0.8,0.5,1.8,0.8,2.8,0.7l0.1-0.1c0,0.3,0,0.5,0,0.8 c-1.2,1.3-0.8,1.6-3.2,2.1c-2.4,0.5-1,1.4-0.1,1.6c1.1,0.3,3.7,0.7,5.5-1.8l-0.1,0.3c0.5,0.4,0.4,2.7,0.5,4.4 c0.1,1.7,0.2,3.2,0.5,4.1c0.3,0.9,0.7,3.3,3.9,2.6C29.1,38.3,31.1,37.5,31.3,30'/%3E%3Cpath class='st1' d='M38.3,25.3c-4.4,0.9-4.7-0.6-4.7-0.6c4.7-7,6.7-15.8,5-18c-4.6-5.9-12.6-3.1-12.7-3l0,0 c-0.9-0.2-1.9-0.3-3-0.3c-2,0-3.5,0.5-4.7,1.4c0,0-14.3-5.9-13.6,7.4c0.1,2.8,4,21.3,8.7,15.7c1.7-2,3.3-3.8,3.3-3.8 c0.8,0.5,1.8,0.8,2.8,0.7l0.1-0.1c0,0.3,0,0.5,0,0.8c-1.2,1.3-0.8,1.6-3.2,2.1c-2.4,0.5-1,1.4-0.1,1.6c1.1,0.3,3.7,0.7,5.5-1.8 l-0.1,0.3c0.5,0.4,0.8,2.4,0.7,4.3c-0.1,1.9-0.1,3.2,0.3,4.2c0.4,1,0.7,3.3,3.9,2.6c2.6-0.6,4-2,4.2-4.5c0.1-1.7,0.4-1.5,0.5-3 l0.2-0.7c0.3-2.3,0-3.1,1.7-2.8l0.4,0c1.2,0.1,2.8-0.2,3.7-0.6C39,26.4,40.2,24.9,38.3,25.3L38.3,25.3z'/%3E%3Cpath class='st2' d='M21.8,26.6c-0.1,4.4,0,8.8,0.5,9.8c0.4,1.1,1.3,3.2,4.5,2.5c2.6-0.6,3.6-1.7,4-4.1c0.3-1.8,0.9-6.7,1-7.7'/%3E%3Cpath class='st2' d='M18,4.7c0,0-14.3-5.8-13.6,7.4c0.1,2.8,4,21.3,8.7,15.7c1.7-2,3.2-3.7,3.2-3.7'/%3E%3Cpath class='st2' d='M25.7,3.6c-0.5,0.2,7.9-3.1,12.7,3c1.7,2.2-0.3,11-5,18'/%3E%3Cpath class='st3' d='M33.5,24.6c0,0,0.3,1.5,4.7,0.6c1.9-0.4,0.8,1.1-1.2,2c-1.6,0.8-5.3,0.9-5.3-0.1 C31.6,24.5,33.6,25.3,33.5,24.6c-0.1-0.6-1.1-1.2-1.7-2.7c-0.5-1.3-7.3-11.2,1.9-9.7c0.3-0.1-2.4-8.7-11-8.9 c-8.6-0.1-8.3,10.6-8.3,10.6'/%3E%3Cpath class='st2' d='M19.4,25.6c-1.2,1.3-0.8,1.6-3.2,2.1c-2.4,0.5-1,1.4-0.1,1.6c1.1,0.3,3.7,0.7,5.5-1.8c0.5-0.8,0-2-0.7-2.3 C20.5,25.1,20,24.9,19.4,25.6L19.4,25.6z'/%3E%3Cpath class='st2' d='M19.3,25.5c-0.1-0.8,0.3-1.7,0.7-2.8c0.6-1.6,2-3.3,0.9-8.5c-0.8-3.9-6.5-0.8-6.5-0.3c0,0.5,0.3,2.7-0.1,5.2 c-0.5,3.3,2.1,6,5,5.7'/%3E%3Cpath class='st4' d='M18,13.8c0,0.2,0.3,0.7,0.8,0.7c0.5,0.1,0.9-0.3,0.9-0.5c0-0.2-0.3-0.4-0.8-0.4C18.4,13.6,18,13.7,18,13.8 L18,13.8z'/%3E%3Cpath class='st5' d='M32,13.5c0,0.2-0.3,0.7-0.8,0.7c-0.5,0.1-0.9-0.3-0.9-0.5c0-0.2,0.3-0.4,0.8-0.4C31.6,13.2,32,13.3,32,13.5 L32,13.5z'/%3E%3Cpath class='st2' d='M33.7,12.2c0.1,1.4-0.3,2.4-0.4,3.9c-0.1,2.2,1,4.7-0.6,7.2'/%3E%3Cpath class='st6' d='M2.7,6.6'/%3E%3C/g%3E%3C/svg%3E%0A") 0 0 no-repeat; + */ + background: 'url(data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMDUgNTAiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojZmZmO30uY2xzLTJ7ZmlsbDojMzI2ODkzO308L3N0eWxlPjwvZGVmcz48dGl0bGU+cGdBZG1pbjwvdGl0bGU+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNTguOTQsNDEuNGEyLjQ4LDIuNDgsMCwwLDEtMi4yNy0zLjQ5TDY0LDIxLjI5VjZhNiw2LDAsMCwwLTYtNkg2QTYsNiwwLDAsMCwwLDZWNDRhNiw2LDAsMCwwLDYsNkg1OGE2LDYsMCwwLDAsNi02VjQxLjRaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMjkuMjUsMzAuMTdhMTMuMTMsMTMuMTMsMCwwLDEtMS44Mi02LjkzLDEzLDEzLDAsMCwxLDEuODItNi44OCwxMi41LDEyLjUsMCwwLDEsMS40OC0xLjk1LDEwLjQ0LDEwLjQ0LDAsMCwwLTMuMjUtMi44OSwxMS4xNiwxMS4xNiwwLDAsMC01LjY1LTEuNDVxLTQuNDgsMC02LjcyLDIuNjRWMTAuNDRINy41MVY0MC4zNmExLDEsMCwwLDAsMSwxaDZhMSwxLDAsMCwwLDEtMVYzMS4xOWE4LjQ3LDguNDcsMCwwLDAsNi4zNCwyLjQsMTEuMjYsMTEuMjYsMCwwLDAsNS42NS0xLjQ1LDEwLjUzLDEwLjUzLDAsMCwwLDIuMDYtMS41NkMyOS40NCwzMC40NCwyOS4zNCwzMC4zMSwyOS4yNSwzMC4xN1pNMjMuNiwyNS44YTQuNTIsNC41MiwwLDAsMS0zLjQ1LDEuNDQsNC40OCw0LjQ4LDAsMCwxLTMuNDQtMS40NCw1LjYsNS42LDAsMCwxLTEuMzUtNCw1LjU5LDUuNTksMCwwLDEsMS4zNS00LDQuNDYsNC40NiwwLDAsMSwzLjQ0LTEuNDUsNC40OSw0LjQ5LDAsMCwxLDMuNDUsMS40NSw1LjYzLDUuNjMsMCwwLDEsMS4zNCw0QTUuNjQsNS42NCwwLDAsMSwyMy42LDI1LjhaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNTYuNDksMTIuNjNWMzEuMjRxMCw2LjM1LTMuNDQsOS41MXQtOS45MiwzLjE3YTI1LjQyLDI1LjQyLDAsMCwxLTYuMy0uNzUsMTUsMTUsMCwwLDEtNS0yLjIzbDIuODktNS41OWExMC4xNywxMC4xNywwLDAsMCwzLjUxLDEuNzksMTQuMzcsMTQuMzcsMCwwLDAsNC4xOC42NUE2LjUzLDYuNTMsMCwwLDAsNDcsMzYuNGE1LjM3LDUuMzcsMCwwLDAsMS40Ny00LjExdi0uNzZjLTEuNTQsMS44LTMuNzksMi42OS02Ljc2LDIuNjlhMTEuNywxMS43LDAsMCwxLTUuNTktMS4zNkExMC4zNywxMC4zNywwLDAsMSwzMi4wOSwyOWExMC44OSwxMC44OSwwLDAsMS0xLjUxLTUuNzcsMTAuODYsMTAuODYsMCwwLDEsMS41MS01Ljc0LDEwLjQyLDEwLjQyLDAsMCwxLDQuMDctMy44NiwxMS43MSwxMS43MSwwLDAsMSw1LjU5LTEuMzdjMy4yNSwwLDUuNjMsMS4wNiw3LjE0LDMuMTVWMTIuNjNabS05LjMsMTMuOTVhNC40LDQuNCwwLDAsMCwxLjQtMy4zNiw0LjM0LDQuMzQsMCwwLDAtMS4zOC0zLjM0LDUuNjUsNS42NSwwLDAsMC03LjE2LDAsNC4zLDQuMywwLDAsMC0xLjQxLDMuMzQsNC4zNSw0LjM1LDAsMCwwLDEuNDMsMy4zNiw1LjA4LDUuMDgsMCwwLDAsMy41NywxLjNBNSw1LDAsMCwwLDQ3LjE5LDI2LjU4WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTgzLjQzLDMyLjg5SDcxbC0yLDUuMDlhMSwxLDAsMCwxLS45My42Mkg2MS43M2ExLDEsMCwwLDEtLjkxLTEuNEw3Mi45MSw5LjhhMSwxLDAsMCwxLC45Mi0uNmg2Ljg5YTEsMSwwLDAsMSwuOTEuNkw5My43NywzNy4yYTEsMSwwLDAsMS0uOTIsMS40SDg2LjQxYTEsMSwwLDAsMS0uOTMtLjYyWk04MSwyNi43NmwtMy43OC05LjQxLTMuNzgsOS40MVoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0xMjAuNDQsOC40NFYzNy42YTEsMSwwLDAsMS0xLDFoLTUuNmExLDEsMCwwLDEtMS0xVjM2LjMzUTExMC42MiwzOSwxMDYuMTYsMzlhMTEuMjksMTEuMjksMCwwLDEtNS42Ny0xLjQ1LDEwLjU0LDEwLjU0LDAsMCwxLTQtNC4xNEExMi42MiwxMi42MiwwLDAsMSw5NSwyNy4xOCwxMi41MywxMi41MywwLDAsMSw5Ni40NCwyMWExMC4zNSwxMC4zNSwwLDAsMSw0LTQuMDksMTEuNDgsMTEuNDgsMCwwLDEsNS42Ny0xLjQzLDguMjQsOC4yNCwwLDAsMSw2LjMsMi4zNVY4LjQ0YTEsMSwwLDAsMSwxLTFoNkExLDEsMCwwLDEsMTIwLjQ0LDguNDRabS05LjE5LDIyLjc1YTUuNzEsNS43MSwwLDAsMCwxLjM0LTQsNS42LDUuNiwwLDAsMC0xLjMyLTMuOTUsNC40Nyw0LjQ3LDAsMCwwLTMuNDMtMS40Myw0LjUzLDQuNTMsMCwwLDAtMy40NCwxLjQzLDUuNTEsNS41MSwwLDAsMC0xLjM0LDMuOTUsNS42Nyw1LjY3LDAsMCwwLDEuMzQsNCw0Ljc3LDQuNzcsMCwwLDAsNi44NSwwWiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTE2MSwxOGMxLjY2LDEuNjgsMi41LDQuMjEsMi41LDcuNnYxMmExLDEsMCwwLDEtMSwxaC02YTEsMSwwLDAsMS0xLTFWMjYuODhhNS42Nyw1LjY3LDAsMCwwLS45LTMuNTMsMy4wOSwzLjA5LDAsMCwwLTIuNTUtMS4xMywzLjYyLDMuNjIsMCwwLDAtMi44OSwxLjI2LDUuNzEsNS43MSwwLDAsMC0xLjEsMy44MlYzNy42YTEsMSwwLDAsMS0xLDFoLTZhMSwxLDAsMCwxLTEtMVYyNi44OGMwLTMuMTEtMS4xNC00LjY2LTMuNDQtNC42NmEzLjcsMy43LDAsMCwwLTIuOTQsMS4yNiw1LjcxLDUuNzEsMCwwLDAtMS4wOSwzLjgyVjM3LjZhMSwxLDAsMCwxLTEsMWgtNmExLDEsMCwwLDEtMS0xVjE2Ljg0YTEsMSwwLDAsMSwxLTFoNS42YTEsMSwwLDAsMSwxLDF2MS4zOWE4LDgsMCwwLDEsMy0yLjA4LDEwLjIzLDEwLjIzLDAsMCwxLDMuOC0uNjksMTAsMTAsMCwwLDEsNC4yOS44OEE3LjI4LDcuMjgsMCwwLDEsMTQ2LjQyLDE5YTguODUsOC44NSwwLDAsMSwzLjQxLTIuNjUsMTAuOTMsMTAuOTMsMCwwLDEsNC40OS0uOTJBOSw5LDAsMCwxLDE2MSwxOFoiLz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0xNjguMTIsMTIuMWEzLjkxLDMuOTEsMCwwLDEtMS4zNC0yLjc5QTQuMTYsNC4xNiwwLDAsMSwxNjgsNi4xOWE1LDUsMCwwLDEsMy42Ny0xLjM2QTUuMjUsNS4yNSwwLDAsMSwxNzUuMTgsNmEzLjc1LDMuNzUsMCwwLDEsMS4zNCwzLDQuMSw0LjEsMCwwLDEtMS4zNCwzLjEzLDUuNjgsNS42OCwwLDAsMS03LjA2LDBabS41NCwzLjc0aDZhMSwxLDAsMCwxLDEsMVYzNy42YTEsMSwwLDAsMS0xLDFoLTZhMSwxLDAsMCwxLTEtMVYxNi44NEExLDEsMCwwLDEsMTY4LjY2LDE1Ljg0WiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTIwMS41NSwxOHEyLjU5LDIuNTIsMi41OSw3LjZ2MTJhMSwxLDAsMCwxLTEsMWgtNmExLDEsMCwwLDEtMS0xVjI2Ljg4cTAtNC42Ni0zLjc0LTQuNjZhNC4zLDQuMywwLDAsMC0zLjMsMS4zNCw1LjgzLDUuODMsMCwwLDAtMS4yNCw0djEwYTEsMSwwLDAsMS0xLDFoLTZhMSwxLDAsMCwxLTEtMVYxNi44NGExLDEsMCwwLDEsMS0xaDUuNjFhMSwxLDAsMCwxLDEsMXYxLjQ3YTkuMDUsOS4wNSwwLDAsMSwzLjE5LTIuMTIsMTAuNzgsMTAuNzgsMCwwLDEsNC0uNzNBOS4zNCw5LjM0LDAsMCwxLDIwMS41NSwxOFoiLz48L3N2Zz4=) 0 0 no-repeat', + backgroundPositionY: 'center', + }, + menus: { + display: 'flex', + alignItems: 'center', + gap: '2px', + marginLeft: '16px', + + '& .MuiButton-containedPrimary': { + padding: '2px 8px', + } + }, + menuButton: { + fontSize: '0.925rem', + }, + userMenu: { + marginLeft: 'auto', + '& .MuiButton-containedPrimary': { + fontSize: '0.825rem', + } + }, + gravatar: { + marginRight: '4px', + } +})); + + + +export default function AppMenuBar() { + const classes = useStyles(); + const [,setRefresh] = useState(false); + + const reRenderMenus = ()=>setRefresh((prev)=>!prev); + + useEffect(()=>{ + pgAdmin.Browser.Events.on('pgadmin:nw-enable-disable-menu-items', ()=>{ + reRenderMenus(); + }); + pgAdmin.Browser.Events.on('pgadmin:nw-refresh-menu-item', ()=>{ + reRenderMenus(); + }); + }, []); + + const getPgMenuItem = (menuItem, i)=>{ + if(menuItem.type == 'separator') { + return ; + } + const hasCheck = typeof menuItem.checked == 'boolean'; + + return { + menuItem.callback(); + if(hasCheck) { + reRenderMenus(); + } + }} + hasCheck={hasCheck} + checked={menuItem.checked} + >{menuItem.label}; + }; + + const userMenuInfo = pgAdmin.Browser.utils.userMenuInfo; + + return( + <> + +
    +
    + {pgAdmin.Browser.MainMenus?.map((menu, i)=>{ + return ( + {menu.label}} + label={menu.label} + key={menu.name} + > + {menu.getMenuItems().map((menuItem, i)=>{ + const submenus = menuItem.getMenuItems(); + if(submenus) { + return + {submenus.map((submenuItem, si)=>{ + return getPgMenuItem(submenuItem, si); + })} + ; + } + return getPgMenuItem(menuItem, i); + })} + + ); + })} +
    + {userMenuInfo && +
    + +
    + {userMenuInfo.gravatar && + Gravatar image for {{ username }}} + {!userMenuInfo.gravatar && } +
    + { userMenuInfo.username } ({userMenuInfo.auth_source}) + + + } + label={userMenuInfo.username} + align="end" + > + {userMenuInfo.menus.map((menuItem, i)=>{ + return getPgMenuItem(menuItem, i); + })} +
    +
    } + + + ); +} diff --git a/web/pgadmin/static/js/Theme/index.jsx b/web/pgadmin/static/js/Theme/index.jsx index 7702818be..b867f809f 100644 --- a/web/pgadmin/static/js/Theme/index.jsx +++ b/web/pgadmin/static/js/Theme/index.jsx @@ -88,6 +88,7 @@ basicSettings = createMuiTheme(basicSettings, { root: { textTransform: 'none', padding: basicSettings.spacing(0.5, 1.5), + fontSize: 'inherit', '&.Mui-disabled': { opacity: 0.60, }, diff --git a/web/pgadmin/static/js/components/Menu.jsx b/web/pgadmin/static/js/components/Menu.jsx index 585c5a9a2..c6308cba4 100644 --- a/web/pgadmin/static/js/components/Menu.jsx +++ b/web/pgadmin/static/js/components/Menu.jsx @@ -7,6 +7,8 @@ import { MenuItem, ControlledMenu, applyStatics, + Menu, + SubMenu, } from '@szhsin/react-menu'; export {MenuDivider as PgMenuDivider} from '@szhsin/react-menu'; import { shortcutToString } from './ShortcutTitle'; @@ -25,14 +27,14 @@ const useStyles = makeStyles((theme)=>({ '& .szh-menu__divider': { margin: 0, background: theme.otherVars.borderColor, - } - }, - menuItem: { - display: 'flex', - padding: '4px 8px', - '&.szh-menu__item--active, &.szh-menu__item--hover': { - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastText, + }, + '& .szh-menu__item': { + display: 'flex', + padding: '4px 8px', + '&.szh-menu__item--active, &.szh-menu__item--hover': { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + } } }, checkIcon: { @@ -48,10 +50,19 @@ const useStyles = makeStyles((theme)=>({ } })); -export function PgMenu({open, className, label, ...props}) { +export function PgMenu({open, className, label, menuButton, ...props}) { const classes = useStyles(); const state = open ? 'open' : 'closed'; props.anchorRef?.current?.setAttribute('data-state', state); + + if(menuButton) { + return ; + } return ( { + return ( + + ); +}); + export const PgMenuItem = applyStatics(MenuItem)(({hasCheck=false, checked=false, accesskey, shortcut, children, ...props})=>{ const classes = useStyles(); let onClick = props.onClick; @@ -80,7 +98,7 @@ export const PgMenuItem = applyStatics(MenuItem)(({hasCheck=false, checked=false }; } const dataLabel = typeof(children) == 'string' ? children : undefined; - return + return {hasCheck && } {children} {(shortcut || accesskey) &&
    ({shortcutToString(shortcut, accesskey)})
    } diff --git a/web/pgadmin/browser/static/js/new_menu.js b/web/pgadmin/static/js/helpers/Menu.js similarity index 78% rename from web/pgadmin/browser/static/js/new_menu.js rename to web/pgadmin/static/js/helpers/Menu.js index 28a11d556..e78997253 100644 --- a/web/pgadmin/browser/static/js/new_menu.js +++ b/web/pgadmin/static/js/helpers/Menu.js @@ -12,7 +12,7 @@ import gettext from 'sources/gettext'; export default class Menu { constructor(name, label, id, index, addSepratior) { this.label = label; - this.name = name; + this.name = name; this.id = id; this.index = index || 1; this.menuItems = [], @@ -102,42 +102,11 @@ export default class Menu { getMenuItems() { return this.menuItems; } - - static getContextMenus(menuList, item, node) { - Menu.sortMenus(menuList); - - let ctxMenus = {}; - let ctxIndex = 1; - menuList.forEach(ctx => { - let ctx_uid = _.uniqueId('ctx_'); - let sub_ctx_item = {}; - ctx.is_disabled = ctx.disabled(node, item); - if ('menu_items' in ctx && ctx.menu_items) { - Menu.sortMenus(ctx.menu_items); - ctx.menu_items.forEach((c) => { - c.is_disabled = c.disabled(node, item); - if (!c.is_disabled) { - sub_ctx_item[ctx_uid + _.uniqueId('_sub_')] = c.getContextItem(c.label, c.is_disabled); - } - }); - } - if (!ctx.is_disabled) { - ctxMenus[ctx_uid + '_' + ctx.priority + '_' + + ctxIndex + '_itm'] = ctx.getContextItem(ctx.label, ctx.is_disabled, sub_ctx_item); - - if (_.size(sub_ctx_item) > 0 && ['register', 'create'].includes(ctx.category)) { - ctxMenus[ctx_uid + '_' + ctx.priority + '_' + + ctxIndex + '_sep'] = '----'; - } - } - ctxIndex++; - }); - - return ctxMenus; - } } export class MenuItem { - constructor(options, onDisableChange, onChangeChacked) { + constructor(options, onDisableChange, onChangeChecked) { let menu_opts = [ 'name', 'label', 'priority', 'module', 'callback', 'data', 'enable', 'category', 'target', 'url', 'node', @@ -158,7 +127,9 @@ export class MenuItem { }; } this.onDisableChange = onDisableChange; - this.changeChecked = onChangeChacked; + this.changeChecked = onChangeChecked; + this._isDisabled = true; + this.checkAndSetDisabled(); } static create(options) { @@ -170,6 +141,10 @@ export class MenuItem { this.changeChecked?.(this); } + getMenuItems() { + return this.menu_items; + } + contextMenuCallback(self) { self.callback(); } @@ -184,11 +159,19 @@ export class MenuItem { }; } - setDisabled(disabled) { - this.is_disabled = disabled; + checkAndSetDisabled(node, item, forceDisable) { + if(!_.isUndefined(forceDisable)) { + this._isDisabled = forceDisable; + } else { + this._isDisabled = this.disabled(node, item); + } this.onDisableChange?.(this.parentMenu, this); } + get isDisabled() { + return this._isDisabled; + } + /* * Checks this menu enable/disable state based on the selection. */ @@ -222,7 +205,3 @@ export class MenuItem { return false; } } - -export function getContextMenu(menu, item, node) { - return Menu.getContextMenus(menu, item, node); -} diff --git a/web/pgadmin/static/scss/_pgadmin.style.scss b/web/pgadmin/static/scss/_pgadmin.style.scss index 1ca4ff9e2..93caea3a1 100644 --- a/web/pgadmin/static/scss/_pgadmin.style.scss +++ b/web/pgadmin/static/scss/_pgadmin.style.scss @@ -130,18 +130,6 @@ .opacity-5 { opacity: 0.5; } -.pg-navbar { - font-size: $navbar-font-size; - background-color: $navbar-bg; - padding-left: 0rem; - padding-right: 0.5rem; - & .nav-item .nav-link{ - line-height: 1; - } -} - - - .pg-docker { position:absolute; left:0px; @@ -548,7 +536,7 @@ fieldset.inline-fieldset > div { .pg-panel-statistics-container, .pg-panel-dependencies-container, .pg-panel-dependents-container, -.pg-prop-coll-container, { +.pg-prop-coll-container { width: 100%; overflow: auto; border-radius: $card-border-radius; diff --git a/web/regression/feature_tests/pg_utilities_backup_restore_test.py b/web/regression/feature_tests/pg_utilities_backup_restore_test.py index e1ebeaf6b..3130d4753 100644 --- a/web/regression/feature_tests/pg_utilities_backup_restore_test.py +++ b/web/regression/feature_tests/pg_utilities_backup_restore_test.py @@ -160,8 +160,8 @@ class PGUtilitiesBackupFeatureTest(BaseFeatureTest): def initiate_backup(self): self.page.retry_click( - (By.LINK_TEXT, - NavMenuLocators.tools_menu_link_text), + (By.CSS_SELECTOR, + NavMenuLocators.tools_menu_css), (By.CSS_SELECTOR, NavMenuLocators.backup_obj_css)) @@ -197,7 +197,7 @@ class PGUtilitiesBackupFeatureTest(BaseFeatureTest): def initiate_restore(self): tools_menu = self.driver.find_element( - By.LINK_TEXT, NavMenuLocators.tools_menu_link_text) + By.CSS_SELECTOR, NavMenuLocators.tools_menu_css) tools_menu.click() restore_obj = self.page.find_by_css_selector( diff --git a/web/regression/feature_tests/pg_utilities_maintenance_test.py b/web/regression/feature_tests/pg_utilities_maintenance_test.py index 832f0e89f..fb7ff7204 100644 --- a/web/regression/feature_tests/pg_utilities_maintenance_test.py +++ b/web/regression/feature_tests/pg_utilities_maintenance_test.py @@ -109,8 +109,8 @@ class PGUtilitiesMaintenanceFeatureTest(BaseFeatureTest): self.server['db_password'], self.database_name) self.page.retry_click( - (By.LINK_TEXT, - NavMenuLocators.tools_menu_link_text), + (By.CSS_SELECTOR, + NavMenuLocators.tools_menu_css), (By.CSS_SELECTOR, NavMenuLocators.maintenance_obj_css)) maintenance_obj = self.wait.until(EC.visibility_of_element_located( (By.CSS_SELECTOR, NavMenuLocators.maintenance_obj_css))) diff --git a/web/regression/feature_tests/view_data_dml_queries.py b/web/regression/feature_tests/view_data_dml_queries.py index 5452e7be3..37df76666 100644 --- a/web/regression/feature_tests/view_data_dml_queries.py +++ b/web/regression/feature_tests/view_data_dml_queries.py @@ -335,14 +335,16 @@ CREATE TABLE public.nonintpkey return False def _view_data_grid(self, table_name): - self.page.driver.find_element(By.LINK_TEXT, "Object").click() + self.page.driver.find_element(By.CSS_SELECTOR, + NavMenuLocators.object_menu_css).click() ActionChains( self.page.driver ).move_to_element( self.page.driver.find_element( - By.LINK_TEXT, NavMenuLocators.view_data_link_text) + By.CSS_SELECTOR, NavMenuLocators.view_data_link_css) ).perform() - self.page.find_by_partial_link_text("All Rows").click() + + self.page.find_by_css_selector("li[data-label='All Rows']").click() # wait until datagrid frame is loaded. self.page.wait_for_query_tool_loading_indicator_to_appear() diff --git a/web/regression/feature_tests/xss_checks_pgadmin_debugger_test.py b/web/regression/feature_tests/xss_checks_pgadmin_debugger_test.py index 3d71e50ad..f70e13335 100644 --- a/web/regression/feature_tests/xss_checks_pgadmin_debugger_test.py +++ b/web/regression/feature_tests/xss_checks_pgadmin_debugger_test.py @@ -8,6 +8,7 @@ ########################################################################## import secrets +import time from selenium.webdriver import ActionChains from selenium.common.exceptions import TimeoutException @@ -17,6 +18,7 @@ from regression.feature_utils.tree_area_locators import TreeAreaLocators from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By +from regression.feature_utils.locators import NavMenuLocators class CheckDebuggerForXssFeatureTest(BaseFeatureTest): @@ -66,13 +68,17 @@ class CheckDebuggerForXssFeatureTest(BaseFeatureTest): function_node.click() def _debug_function(self): - self.page.driver.find_element(By.LINK_TEXT, "Object").click() + self.page.driver.find_element(By.CSS_SELECTOR, + NavMenuLocators.object_menu_css).click() ActionChains( self.page.driver ).move_to_element( - self.page.driver.find_element(By.LINK_TEXT, "Debugging") + self.page.driver.find_element( + By.CSS_SELECTOR, "div[data-label='Debugging']") ).perform() - self.page.driver.find_element(By.LINK_TEXT, "Debug").click() + time.sleep(2) + self.page.driver.find_element( + By.CSS_SELECTOR, "li[data-label='Debug']").click() # We need to check if debugger plugin is installed or not try: diff --git a/web/regression/feature_tests/xss_checks_roles_control_test.py b/web/regression/feature_tests/xss_checks_roles_control_test.py index e8bc9ec19..0d9e7a5c6 100644 --- a/web/regression/feature_tests/xss_checks_roles_control_test.py +++ b/web/regression/feature_tests/xss_checks_roles_control_test.py @@ -72,7 +72,7 @@ class CheckRoleMembershipControlFeatureTest(BaseFeatureTest): def _check_role_membership_control(self): self.page.driver.find_element( - By.LINK_TEXT, NavMenuLocators.object_menu_link_text).click() + By.CSS_SELECTOR, NavMenuLocators.object_menu_css).click() property_object = self.wait.until(EC.visibility_of_element_located( (By.CSS_SELECTOR, NavMenuLocators.properties_obj_css))) property_object.click() diff --git a/web/regression/feature_utils/locators.py b/web/regression/feature_utils/locators.py index 6b977a619..36ed67bfd 100644 --- a/web/regression/feature_utils/locators.py +++ b/web/regression/feature_utils/locators.py @@ -30,23 +30,23 @@ class BrowserToolBarLocators(): class NavMenuLocators: "This will contains element locators of navigation menu bar" - file_menu_css = "#mnu_file" + file_menu_css = "button[data-label='File']" - preference_menu_item_css = "#mnu_preferences" + preference_menu_item_css = "li[data-label='Preferences']" - tools_menu_link_text = "Tools" + tools_menu_css = "button[data-label='Tools']" - view_data_link_text = "View/Edit Data" + view_data_link_css = "div[data-label='View/Edit Data']" - object_menu_link_text = "Object" + object_menu_css = "button[data-label='Object']" - properties_obj_css = "#show_obj_properties.dropdown-item:not(.disabled)" + properties_obj_css = "li[data-label='Properties...']" - backup_obj_css = "#backup_object.dropdown-item:not(.disabled)" + backup_obj_css = "li[data-label='Backup...']" - restore_obj_css = "#restore_object.dropdown-item:not(.disabled)" + restore_obj_css = "li[data-label='Restore...']" - maintenance_obj_css = "#maintenance.dropdown-item:not(.disabled)" + maintenance_obj_css = "li[data-label='Maintenance...']" show_system_objects_pref_label_xpath = \ "//label[contains(text(), 'Show system objects?')]" diff --git a/web/regression/feature_utils/pgadmin_page.py b/web/regression/feature_utils/pgadmin_page.py index 4ac4ca50e..72605a343 100644 --- a/web/regression/feature_utils/pgadmin_page.py +++ b/web/regression/feature_utils/pgadmin_page.py @@ -40,8 +40,8 @@ class PgadminPage: # pgAdmin related methods def login_to_app(self, user_detail): self.driver.switch_to.default_content() - if not (self.check_if_element_exist_by_xpath( - '//a[@id="navbar-user"]', 1)): + if not (self.check_if_element_exist_by_css_selector( + 'button[data-test="loggedin-username"]', 1)): user_edt_box_el = self.driver.find_element(By.NAME, 'email') user_edt_box_el.send_keys(user_detail['login_username']) password_edt_box_el = self.driver.find_element(By.NAME, 'password') @@ -55,7 +55,8 @@ class PgadminPage: attempt = 0 while attempt < 4: try: - self.click_element(self.find_by_partial_link_text("File")) + self.click_element(self.find_by_css_selector( + "button[data-label='File']")) break except (TimeoutException, NoSuchWindowException): self.driver.refresh() @@ -66,7 +67,8 @@ class PgadminPage: except TimeoutException: attempt = attempt + 1 - self.find_by_partial_link_text("Reset Layout").click() + self.click_element(self.find_by_css_selector( + "li[data-label='Reset Layout']")) self.click_modal('OK') self.wait_for_reloading_indicator_to_disappear() @@ -136,26 +138,26 @@ class PgadminPage: (By.XPATH, server_tree_xpath))) def open_query_tool(self): - self.driver.find_element(By.LINK_TEXT, "Tools").click() - tools_menu = self.driver.find_element(By.ID, 'mnu_tools') - - query_tool = tools_menu.find_element(By.ID, 'query_tool') - - self.enable_menu_item(query_tool, 10) - - self.find_by_partial_link_text("Query Tool").click() + self.click_element(self.find_by_css_selector( + "button[data-label='Tools']")) + self.click_element(self.find_by_css_selector( + "li[data-label='Query Tool']")) self.wait_for_element_to_be_visible( self.driver, "//div[@id='btn-conn-status']", 5) def open_view_data(self, table_name): - self.driver.find_element(By.LINK_TEXT, "Object").click() + self.click_element(self.find_by_css_selector( + NavMenuLocators.object_menu_css)) + ActionChains( self.driver ).move_to_element( - self.driver.find_element(By.LINK_TEXT, "View/Edit Data") + self.driver.find_element( + By.CSS_SELECTOR, NavMenuLocators.view_data_link_css) ).perform() - self.find_by_partial_link_text("All Rows").click() + self.click_element(self.find_by_css_selector( + "li[data-label='All Rows']")) time.sleep(1) # wait until datagrid frame is loaded. @@ -327,10 +329,10 @@ class PgadminPage: self.driver.execute_script( self.js_executor_scrollintoview_arg, server_to_remove) self.click_element(server_to_remove) - object_menu_item = self.find_by_partial_link_text("Object") - self.click_element(object_menu_item) - delete_menu_item = self.find_by_partial_link_text("Remove Server") - self.click_element(delete_menu_item) + self.click_element(self.find_by_css_selector( + "button[data-label='Object']")) + self.click_element(self.find_by_css_selector( + "li[data-label='Remove Server']")) self.driver.switch_to.default_content() self.click_modal('Yes') time.sleep(1) @@ -969,6 +971,18 @@ class PgadminPage: pass return element_found + def check_if_element_exist_by_css_selector(self, selector, timeout=5): + """This function will verify if an element exist and on that basis + will return True or False. Will handle exception internally""" + element_found = False + try: + WebDriverWait(self.driver, timeout, .01).until( + EC.visibility_of_element_located((By.CSS_SELECTOR, selector))) + element_found = True + except Exception: + pass + return element_found + def wait_for_element(self, find_method_with_args): def element_if_it_exists(driver): try: diff --git a/web/regression/javascript/browser/layout_spec.js b/web/regression/javascript/browser/layout_spec.js index d62ea74b3..bec93974b 100644 --- a/web/regression/javascript/browser/layout_spec.js +++ b/web/regression/javascript/browser/layout_spec.js @@ -29,7 +29,7 @@ describe('layout related functions test', function() { }; _.extend(pgBrowser,{ - 'menus': { + 'all_menus_cache': { 'file': { 'mnu_locklayout': { 'menu_items': [ @@ -42,7 +42,7 @@ describe('layout related functions test', function() { }, }); - menu_items = pgBrowser.menus.file.mnu_locklayout.menu_items; + menu_items = pgBrowser.all_menus_cache.file.mnu_locklayout.menu_items; }); describe('for menu actions', function() { diff --git a/web/webpack.shim.js b/web/webpack.shim.js index ad08c2b2d..27a99284b 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -131,7 +131,6 @@ let webpackShimConfig = { 'pgadmin.browser.layout': path.join(__dirname, './pgadmin/browser/static/js/layout'), 'pgadmin.browser.runtime': path.join(__dirname, './pgadmin/browser/static/js/runtime'), 'pgadmin.browser.preferences': path.join(__dirname, './pgadmin/browser/static/js/preferences'), - 'pgadmin.browser.menu': path.join(__dirname, './pgadmin/browser/static/js/menu'), 'pgadmin.browser.activity': path.join(__dirname, './pgadmin/browser/static/js/activity'), 'pgadmin.browser.messages': '/browser/js/messages', 'pgadmin.browser.node': path.join(__dirname, './pgadmin/browser/static/js/node'), @@ -238,7 +237,7 @@ let webpackShimConfig = { pgLibs: [ 'pgadmin.browser.error', 'pgadmin.browser.collection', - 'pgadmin.browser.events', 'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin', + 'pgadmin.browser.events', 'pgadmin.browser.panel', 'pgadmin', 'pgadmin.browser.frame', 'pgadmin.browser', 'pgadmin.browser.node', 'pgadmin.settings', 'pgadmin.preferences', 'pgadmin.sqlfoldcode',