Rewrite pgAdmin main menu bar to use React. #5615

pull/5667/head
Aditya Toshniwal 2022-12-22 14:25:18 +05:30 committed by GitHub
parent d5e6786bc7
commit ff9daec5ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 510 additions and 1046 deletions

View File

@ -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);
}
});
}
});

View File

@ -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})

View File

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

View File

@ -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: $('<li><a class="dropdown-item disabled" href="#" role="menuitem">' + gettext('No object selected') + '</a></li>'),
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 = $('<div></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('<div id="quick-search-component"></div>');
$dropdown.append('<div class="menu-groups"><span class="fa fa-list" style="font-weight:900 !important;"></span> &nbsp;' + gettext('SUGGESTED SITES') + '</div>');
}
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('<li class="dropdown-divider"></li>');
}, 100);
return pgAdmin.Browser;
});

View File

@ -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) {

View File

@ -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 = $('<a></a>', {
'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($('<i></i>', {
'class': this.icon,
}));
} else if(!_.isUndefined(this.checked)) {
url.append($('<i></i>', {
'class': 'fa fa-check '+ (this.checked?'':'visibility-hidden'),
}));
}
url.addClass((this.is_disabled ? ' disabled' : ''));
let textSpan = $('<span data-test="menu-item-text"></span>').text(' ' + this.label);
url.append(textSpan);
let mnu_element = $('<li/>').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('<li class="dropdown-divider"></li>');
}
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) { %><li class="dropdown-divider"></li><% } %>',
'<li class="dropdown-submenu" role="menuitem">',
' <a href="#" class="dropdown-item">',
' <% if (icon) { %><i class="<%= icon %>"></i><% } %>',
' <span><%= label %></span>',
' </a>',
' <ul class="dropdown-menu">',
' </ul>',
'</li>',
'<% if (below) { %><li class="dropdown-divider"></li><% } %>',
].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;
});

View File

@ -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));
});

View File

@ -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){
</div>
</div>
{% if current_app.PGADMIN_RUNTIME | string() == 'False' %}
<nav class="navbar fixed-top navbar-expand-lg navbar-dark pg-navbar">
<a class="navbar-brand pgadmin_header_logo" onClick="return false;" href="{{ '#' }}"
title="{{ config.APP_NAME }} {{ _('logo') }}" aria-label="{ config.APP_NAME }} {{ _('logo') }}">
<i class="app-icon {{ config.APP_ICON }}" aria-hidden="true"></i>
</a>
<button type="button" class="navbar-toggler collapsed" data-toggle="collapse" data-target="#navbar-menu" aria-controls="navbar-menu">
<span class="sr-only">{{ _('Toggle navigation') }}</span>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbar-menu" role="navigation">
<ul class="navbar-nav mr-auto">
<li id="mnu_file" class="nav-item active dropdown d-none">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{
_('File') }} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu"></ul>
</li>
<li id="mnu_obj" class="nav-item active dropdown ">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{
_('Object') }} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu"></ul>
</li>
<li id="mnu_management" class="nav-item active dropdown d-none">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{
_('Management') }} <span class="caret"></span></a>
<ul class="dropdown-menu" aria-hidden="true"></ul>
</li>
<li id="mnu_tools" class="nav-item active dropdown d-none">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{
_('Tools') }} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu"></ul>
</li>
<li id="mnu_help" class="nav-item active dropdown d-none">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">{{
_('Help') }} <span class="caret"></span></a>
<ul class="dropdown-menu help_menu" role="menu"></ul>
</li>
</ul>
{% if config.SERVER_MODE %}
<ul class="navbar-nav">
<li class="nav-item active dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown"
role="button" aria-expanded="false" id="navbar-user"></a>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
{% if auth_only_internal %}
<li>
<a class="dropdown-item" href="#" role="menuitem" onclick="pgAdmin.UserManagement.change_password(
'{{ url_for('browser.change_password') }}'
)">
{{ _('Change Password') }}
</a>
</li>
<li class="dropdown-divider"></li>
{% endif %}
{% if mfa_enabled is defined and mfa_enabled is true %}
<li>
<a class="dropdown-item" href="#" role="menuitem" onclick="pgAdmin.UserManagement.show_mfa(
'{{ login_url("mfa.register", next_url="internal") }}'
)">{{ _('Two-Factor Authentication') }}</a>
</li>
<li class="dropdown-divider"></li>
{% endif %}
{% if is_admin %}
<li><a class="dropdown-item" href="#" role="menuitem" onclick="pgAdmin.UserManagement.show_users()">{{ _('Users') }}</a></li>
<li class="dropdown-divider"></li>
{% endif %}
<li><a class="dropdown-item" role="menuitem" href="{{ logout_url }}">{{ _('Logout') }}</a></li>
</ul>
</li>
</ul>
{% endif %}
</div>
</nav>
<div id="main-menu-container"></div>
<div id="dockerContainer" class="pg-docker"></div>
{% else %}
<div id="dockerContainer" class="pg-docker pg-docker-native"></div>

View File

@ -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;

View File

@ -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() -%}
'<img src = "{{ username | gravatar }}" width = "18" height = "18" alt = "Gravatar image for {{ username }}" > {{ username }} ({{auth_source}}) <span class="caret"></span>';
{%- endmacro %}

View File

@ -1,3 +0,0 @@
{% macro PREPARE_HTML() -%}
'<i class="fa fa-user-circle pg-login-icon" aria-hidden="true"></i> {{ username }} <span class="caret"></span>';
{%- endmacro %}

View File

@ -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(<Theme><AppMenuBar /></Theme>, document.querySelector('#main-menu-container'));
}
});

View File

@ -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 <PgMenuDivider key={i}/>;
}
const hasCheck = typeof menuItem.checked == 'boolean';
return <PgMenuItem
key={i}
disabled={menuItem.isDisabled}
onClick={()=>{
menuItem.callback();
if(hasCheck) {
reRenderMenus();
}
}}
hasCheck={hasCheck}
checked={menuItem.checked}
>{menuItem.label}</PgMenuItem>;
};
const userMenuInfo = pgAdmin.Browser.utils.userMenuInfo;
return(
<>
<Box className={classes.root}>
<div className={classes.logo} />
<div className={classes.menus}>
{pgAdmin.Browser.MainMenus?.map((menu, i)=>{
return (
<PgMenu
menuButton={<PrimaryButton key={i} className={classes.menuButton} data-label={menu.label}>{menu.label}<KeyboardArrowDownIcon fontSize="small" /></PrimaryButton>}
label={menu.label}
key={menu.name}
>
{menu.getMenuItems().map((menuItem, i)=>{
const submenus = menuItem.getMenuItems();
if(submenus) {
return <PgSubMenu key={i} label={menuItem.label}>
{submenus.map((submenuItem, si)=>{
return getPgMenuItem(submenuItem, si);
})}
</PgSubMenu>;
}
return getPgMenuItem(menuItem, i);
})}
</PgMenu>
);
})}
</div>
{userMenuInfo &&
<div className={classes.userMenu}>
<PgMenu
menuButton={
<PrimaryButton className={classes.menuButton} data-test="loggedin-username">
<div className={classes.gravatar}>
{userMenuInfo.gravatar &&
<img src={userMenuInfo.gravatar} width = "18" height = "18"
alt = "Gravatar image for {{ username }}" />}
{!userMenuInfo.gravatar && <AccountCircleRoundedIcon />}
</div>
{ userMenuInfo.username } ({userMenuInfo.auth_source})
<KeyboardArrowDownIcon fontSize="small" />
</PrimaryButton>
}
label={userMenuInfo.username}
align="end"
>
{userMenuInfo.menus.map((menuItem, i)=>{
return getPgMenuItem(menuItem, i);
})}
</PgMenu>
</div>}
</Box>
</>
);
}

View File

@ -88,6 +88,7 @@ basicSettings = createMuiTheme(basicSettings, {
root: {
textTransform: 'none',
padding: basicSettings.spacing(0.5, 1.5),
fontSize: 'inherit',
'&.Mui-disabled': {
opacity: 0.60,
},

View File

@ -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 <Menu
{...props}
menuButton={menuButton}
className={clsx(classes.menu, className)}
aria-label={label || 'Menu'}
/>;
}
return (
<ControlledMenu
state={state}
@ -68,8 +79,15 @@ PgMenu.propTypes = {
className: CustomPropTypes.className,
label: PropTypes.string,
anchorRef: CustomPropTypes.ref,
menuButton: PropTypes.oneOfType([React.ReactNode, undefined]),
};
export const PgSubMenu = applyStatics(SubMenu)(({label, ...props})=>{
return (
<SubMenu label={label} itemProps={{'data-label': label}} {...props} />
);
});
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 <MenuItem {...props} onClick={onClick} className={classes.menuItem} data-label={dataLabel} data-checked={checked}>
return <MenuItem {...props} onClick={onClick} data-label={dataLabel} data-checked={checked}>
{hasCheck && <CheckIcon className={classes.checkIcon} style={checked ? {} : {visibility: 'hidden'}} />}
{children}
{(shortcut || accesskey) && <div className={classes.shortcut}>({shortcutToString(shortcut, accesskey)})</div>}

View File

@ -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);
}

View File

@ -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;

View File

@ -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(

View File

@ -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)))

View File

@ -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()

View File

@ -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:

View File

@ -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()

View File

@ -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?')]"

View File

@ -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:

View File

@ -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() {

View File

@ -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',