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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions web/pgadmin/browser/register_browser_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ def register_browser_preferences(self):
)
)

self.preference.register(
'display', 'show_server_color_indicator',
gettext("Show server color indicator in panel tabs?"), 'boolean',
False,
category_label=PREF_LABEL_DISPLAY,
help_str=gettext(
'If enabled, a colored circle indicator will be shown in panel '
'tabs (Query Tool, ERD Tool, etc.) matching the server\'s custom '
'background color.'
)
)

self.table_row_count_threshold = self.preference.register(
'properties', 'table_row_count_threshold',
gettext("Count rows if estimated less than"), 'integer', 2000,
Expand Down
22 changes: 19 additions & 3 deletions web/pgadmin/browser/static/js/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,23 @@ define('pgadmin.browser.node', [
'pgadmin:browser:node:' + _newNodeData._type + ':updated',
_item, _newNodeData, _oldNodeData
);

// Trigger a specific event for server color updates
// This allows tabs to update their color indicators when server colors change
if (_newNodeData._type === 'server') {
// Extract colors from the icon string using getServerColors utility
const newColors = commonUtils.getServerColors(_newNodeData?.icon);

pgBrowser.Events.trigger(
'pgadmin:server:colors:updated',
_newNodeData._id,
{
bgcolor: newColors.bgcolor,
fgcolor: newColors.fgcolor,
icon: _newNodeData.icon
}
);
}
},
}
);
Expand Down Expand Up @@ -708,9 +725,8 @@ define('pgadmin.browser.node', [

// Go further only if node type is a Server
if (index !== -1) {
// First element will be icon and second will be colour code
let bgcolor = serverData.icon.split(' ')[1] || null,
fgcolor = serverData.icon.split(' ')[2] || '';
// Extract bgcolor and fgcolor from server icon
const { bgcolor, fgcolor } = commonUtils.getServerColors(serverData.icon);

if (bgcolor) {
let dynamic_class = 'pga_server_' + serverData._id + '_bgcolor';
Expand Down
1 change: 1 addition & 0 deletions web/pgadmin/static/js/ToolView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function getToolTabParams(panelId, toolUrl, formParams, tabParams, restor
if(tabParams?.internal?.orig_title){
tabParams.title = tabParams.internal.isDirty ? tabParams.internal.title.slice(0, -1): tabParams.internal.title;
}

return {
id: panelId,
title: panelId,
Expand Down
78 changes: 75 additions & 3 deletions web/pgadmin/static/js/helpers/Layout/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,24 @@ import UtilityView from '../../UtilityView';
import ToolView, { getToolTabParams } from '../../ToolView';
import { ApplicationStateProvider, useApplicationState } from '../../../../settings/static/ApplicationStateProvider';
import { BROWSER_PANELS, WORKSPACES } from '../../../../browser/static/js/constants';
import pgWindow from 'sources/window';

export function TabTitle({id, closable, defaultInternal}) {
const layoutDocker = React.useContext(LayoutDockerContext);
const internal = layoutDocker?.find(id)?.internal ?? defaultInternal;
const showServerColorIndicator = usePreferences(
(state) => state.getPreferencesForModule('browser')?.show_server_color_indicator ?? false
);
const [attrs, setAttrs] = useState({
icon: internal.icon,
title: internal.title,
tooltip: internal.tooltip ?? internal.title,
bgcolor: internal.bgcolor,
fgcolor: internal.fgcolor,
});
// Track visibility state to trigger re-renders when tabs switch
const [isVisible, setIsVisible] = useState(layoutDocker?.isTabVisible(id) ?? false);

const onContextMenu = useCallback((e)=>{
const g = layoutDocker.find(id)?.group??'';
if((layoutDocker.noContextGroups??[]).includes(g)) return;
Expand All @@ -46,25 +55,84 @@ export function TabTitle({id, closable, defaultInternal}) {
}, []);

useEffect(()=>{
// Initialize visibility immediately once the effect runs and layoutObj is available
setIsVisible(layoutDocker?.isTabVisible(id) ?? false);

const deregister = layoutDocker.eventBus.registerListener(LAYOUT_EVENTS.REFRESH_TITLE, (panelId)=>{
if(panelId == id) {
const internal = layoutDocker?.find(id)?.internal??{};
setAttrs({
icon: internal.icon,
title: internal.title,
tooltip: internal.tooltip ?? internal.title,
bgcolor: internal.bgcolor,
fgcolor: internal.fgcolor,
});
layoutDocker.saveLayout();
}
});

return ()=>deregister?.();
// Listen for tab activation to update visibility state
// This ensures the color indicator appears/disappears when switching tabs
const activeListener = layoutDocker.eventBus.registerListener(LAYOUT_EVENTS.ACTIVE, () => {
const visible = layoutDocker?.isTabVisible(id);
setIsVisible(visible);
});

// Listen for server color updates
// This custom event is triggered specifically when server bgcolor/fgcolor changes
const serverColorsUpdatedHandler = (serverId, colorData) => {
const panelData = layoutDocker?.find(id);
if (!panelData?.internal) {
return;
}

const tabServerId = panelData.internal.server_id;
if (!tabServerId || tabServerId !== serverId) {
return;
}

// Update internal data and attrs with new colors
panelData.internal.bgcolor = colorData.bgcolor || null;
panelData.internal.fgcolor = colorData.fgcolor || null;
setAttrs(prev => ({
...prev,
bgcolor: colorData.bgcolor || null,
fgcolor: colorData.fgcolor || null,
}));
// Persist the updated colors so they survive reloads
layoutDocker.saveLayout();
};

// Listen to the custom server color update event
pgWindow.pgAdmin?.Browser?.Events?.on('pgadmin:server:colors:updated', serverColorsUpdatedHandler);

return ()=>{
deregister?.();
activeListener?.();
pgWindow.pgAdmin?.Browser?.Events?.off('pgadmin:server:colors:updated', serverColorsUpdatedHandler);
};
}, []);

return (
<Box display="flex" alignItems="center" title={attrs.tooltip} onContextMenu={onContextMenu} width="100%">
{attrs.icon && <span className={`dock-tab-icon ${attrs.icon}`}></span>}
<span style={{textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap'}} data-visible={layoutDocker.isTabVisible(id)}>{attrs.title}</span>
{showServerColorIndicator && attrs.bgcolor && !isVisible && (
<Box
component="span"
sx={{
width: '12px',
height: '12px',
borderRadius: '50%',
backgroundColor: attrs.bgcolor,
marginLeft: '2px',
marginRight: '4px',
flexShrink: 0,
border: '1px solid rgba(0, 0, 0, 0.1)',
}}
/>
)}
<span style={{textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap'}} data-visible={isVisible}>{attrs.title}</span>
{closable && <PgIconButton title={gettext('Close')} icon={<CloseIcon style={{height: '0.7em'}} />} size="xs" noBorder onClick={()=>{
layoutDocker.close(id);
}} style={{margin: '-1px -10px -1px 0'}} />}
Expand Down Expand Up @@ -368,15 +436,19 @@ export class LayoutDocker {
this.saveLayout();
}

static getPanel({icon, title, closable, tooltip, renamable, manualClose, ...attrs}) {
static getPanel({icon, title, closable, tooltip, renamable, manualClose, bgcolor, fgcolor, server_id, ...attrs}) {
const internal = {
icon: icon,
title: title,
tooltip: tooltip,
closable: _.isUndefined(closable) ? manualClose : closable,
renamable: renamable,
manualClose: manualClose,
bgcolor: bgcolor,
fgcolor: fgcolor,
server_id: server_id, // Store server_id to enable color updates when server properties change
};

return {
cached: true,
group: 'default',
Expand Down
21 changes: 21 additions & 0 deletions web/pgadmin/static/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,27 @@ export function getRandomInt(min, max) {
return min + (intArray[0] % range);
}

/*
* Extracts the background and foreground colors from a server icon string.
*
* This is a workaround for a historical design decision where the backend encodes
* structured data (icon CSS class, bgcolor, fgcolor) into a single space-separated
* string via `server_icon_and_background()`.
* The format is: "icon-class bgcolor fgcolor"
* Ref: web/pgadmin/browser/server_groups/servers/__init__.py:server_icon_and_background()
*/
export function getServerColors(serverIcon) {
if (!serverIcon) {
return { bgcolor: null, fgcolor: null };
}

const parts = serverIcon.split(' ');
return {
bgcolor: parts[1] || null,
fgcolor: parts[2] || null,
};
}

export function titleize(i_str) {
if(i_str === '' || i_str === null) return i_str;
return i_str.split(' ')
Expand Down
13 changes: 10 additions & 3 deletions web/pgadmin/tools/debugger/static/js/DebuggerModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import _ from 'lodash';
import ReactDOM from 'react-dom/client';

import gettext from 'sources/gettext';
import { sprintf } from 'sources/utils';
import { sprintf, getServerColors } from 'sources/utils';
import url_for from 'sources/url_for';
import pgWindow from 'sources/window';
import Kerberos from 'pgadmin.authenticate.kerberos';
Expand Down Expand Up @@ -378,13 +378,17 @@ export default class DebuggerModule {
let open_new_tab = browser_preferences.new_browser_tab_open;
const db_label = self.checkDbNameChange(data, dbNode, newTreeInfo);
let label = getAppropriateLabel(newTreeInfo);

// Extract bgcolor and fgcolor from server icon
const { bgcolor, fgcolor } = getServerColors(newTreeInfo?.server?.icon);

pgAdmin.Browser.Events.trigger(
'pgadmin:tool:show',
`${BROWSER_PANELS.DEBUGGER_TOOL}_${trans_id}`,
url,
null,
{title: getDebuggerTitle(browser_preferences, label, newTreeInfo.schema.label, db_label, null, self.pgBrowser),
icon: 'fa fa-bug', manualClose: false, renamable: true},
icon: 'fa fa-bug', manualClose: false, renamable: true, bgcolor: bgcolor, fgcolor: fgcolor, server_id: newTreeInfo?.server?._id},
Boolean(open_new_tab?.includes('debugger'))
);
})
Expand Down Expand Up @@ -527,13 +531,16 @@ export default class DebuggerModule {

let label = getAppropriateLabel(treeInfo);

// Extract bgcolor and fgcolor from server icon
const { bgcolor, fgcolor } = getServerColors(treeInfo?.server?.icon);

pgAdmin.Browser.Events.trigger(
'pgadmin:tool:show',
`${BROWSER_PANELS.DEBUGGER_TOOL}_${res.data.data.debuggerTransId}`,
url,
null,
{title: getDebuggerTitle(browser_preferences, label, db_label, db_label, null, self.pgBrowser),
icon: 'fa fa-bug', manualClose: false, renamable: true},
icon: 'fa fa-bug', manualClose: false, renamable: true, bgcolor: bgcolor, fgcolor: fgcolor, server_id: treeInfo?.server?._id},
Boolean(open_new_tab?.includes('debugger'))
);
})
Expand Down
7 changes: 5 additions & 2 deletions web/pgadmin/tools/erd/static/js/ERDModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//////////////////////////////////////////////////////////////
import pgWindow from 'sources/window';
import {getPanelTitle} from 'tools/sqleditor/static/js/sqleditor_title';
import {getRandomInt} from 'sources/utils';
import {getRandomInt, getServerColors} from 'sources/utils';
import url_for from 'sources/url_for';
import gettext from 'sources/gettext';
import ReactDOM from 'react-dom/client';
Expand Down Expand Up @@ -142,12 +142,15 @@ export default class ERDModule {
const panelUrl = this.getPanelUrl(transId, parentData, gen);
const open_new_tab = usePreferences.getState().getPreferencesForModule('browser').new_browser_tab_open;

// Extract bgcolor and fgcolor from server icon
const { bgcolor, fgcolor } = getServerColors(parentData?.server?.icon);

pgAdmin.Browser.Events.trigger(
'pgadmin:tool:show',
`${BROWSER_PANELS.ERD_TOOL}_${transId}`,
panelUrl,
{sql_id: toolDataId, connectionTitle: _.escape(panelTitle), db_name:parentData.database.label, server_name: parentData.server.label, user: parentData.server.user.name, server_type: parentData.server.server_type},
{title: 'Untitled', icon: 'fa fa-sitemap'},
{title: 'Untitled', icon: 'fa fa-sitemap', bgcolor: bgcolor, fgcolor: fgcolor, server_id: parentData?.server?._id},
Boolean(open_new_tab?.includes('erd_tool'))
);

Expand Down
7 changes: 5 additions & 2 deletions web/pgadmin/tools/psql/static/js/PsqlModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//
//////////////////////////////////////////////////////////////

import { getRandomInt, hasBinariesConfiguration } from 'sources/utils';
import { getRandomInt, hasBinariesConfiguration, getServerColors } from 'sources/utils';
import { retrieveAncestorOfTypeServer } from 'sources/tree/tree_utils';
import { generateTitle } from 'tools/sqleditor/static/js/sqleditor_title';
import { AllPermissionTypes, BROWSER_PANELS, WORKSPACES } from '../../../../browser/static/js/constants';
Expand Down Expand Up @@ -173,12 +173,15 @@ export default class Psql {

const open_new_tab = usePreferences.getState().getPreferencesForModule('browser').new_browser_tab_open;

// Extract bgcolor and fgcolor from server icon
const { bgcolor, fgcolor } = getServerColors(parentData?.server?.icon);

pgAdmin.Browser.Events.trigger(
'pgadmin:tool:show',
`${BROWSER_PANELS.PSQL_TOOL}_${transId}`,
panelUrl,
{title: panelTitle, db: db_label, server_name: parentData.server.label, 'user': parentData.server.user.name },
{title: panelTitle, icon: 'pg-font-icon icon-terminal', manualClose: false, renamable: true},
{title: panelTitle, icon: 'pg-font-icon icon-terminal', manualClose: false, renamable: true, bgcolor: bgcolor, fgcolor: fgcolor, server_id: parentData?.server?._id},
Boolean(open_new_tab?.includes('psql_tool'))
);

Expand Down
13 changes: 12 additions & 1 deletion web/pgadmin/tools/schema_diff/static/js/SchemaDiffModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,23 @@ export default class SchemaDiff {
let browserPreferences = usePreferences.getState().getPreferencesForModule('browser');
let openInNewTab = browserPreferences.new_browser_tab_open;

// Extract bgcolor and fgcolor from server icon
let bgcolor = null;
let fgcolor = null;
let serverId = null;
const selectedItem = pgAdmin.Browser.tree?.selected();
if (selectedItem) {
const selectedNodeInfo = pgAdmin.Browser.tree?.getTreeNodeHierarchy(selectedItem);
({ bgcolor, fgcolor } = commonUtils.getServerColors(selectedNodeInfo?.server?.icon));
serverId = selectedNodeInfo?.server?._id;
}

pgAdmin.Browser.Events.trigger(
'pgadmin:tool:show',
`${BROWSER_PANELS.SCHEMA_DIFF_TOOL}_${trans_id}`,
baseUrl,
{...params},
{title: panelTitle, icon: 'pg-font-icon icon-compare', manualClose: false, renamable: true},
{title: panelTitle, icon: 'pg-font-icon icon-compare', manualClose: false, renamable: true, bgcolor: bgcolor, fgcolor: fgcolor, server_id: serverId},
Boolean(openInNewTab?.includes('schema_diff'))
);
return true;
Expand Down
6 changes: 5 additions & 1 deletion web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import usePreferences, { listenPreferenceBroadcast } from '../../../../preferenc
import { PgAdminProvider } from '../../../../static/js/PgAdminProvider';
import { ApplicationStateProvider } from '../../../../settings/static/ApplicationStateProvider';
import ToolErrorView from '../../../../static/js/ToolErrorView';
import { getServerColors } from '../../../../static/js/utils';

export default class SQLEditor {
static instance;
Expand Down Expand Up @@ -223,12 +224,15 @@ export default class SQLEditor {
const [icon, tooltip] = panelTitleFunc.getQueryToolIcon(panel_title, is_query_tool);
let selectedNodeInfo = pgAdmin.Browser.tree?.selected() ? pgAdmin.Browser.tree?.getTreeNodeHierarchy(pgAdmin.Browser.tree.selected()) : null;

// Extract bgcolor and fgcolor from server icon
const { bgcolor, fgcolor } = getServerColors(selectedNodeInfo?.server?.icon);

pgAdmin.Browser.Events.trigger(
'pgadmin:tool:show',
`${BROWSER_PANELS.QUERY_TOOL}_${trans_id}`,
panel_url,
{...params, title: panel_title, selectedNodeInfo: JSON.stringify(selectedNodeInfo)},
{title: panel_title, icon: icon, tooltip: tooltip, renamable: true},
{title: panel_title, icon: icon, tooltip: tooltip, renamable: true, bgcolor: bgcolor, fgcolor: fgcolor, server_id: selectedNodeInfo?.server?._id},
Boolean(open_new_tab?.includes('qt'))
);
return true;
Expand Down
Loading