Skip to content

Commit 5ce2809

Browse files
committed
Feat: Added a option to colorize panel/tab header based on server color #2431
1 parent d8a078a commit 5ce2809

File tree

10 files changed

+160
-15
lines changed

10 files changed

+160
-15
lines changed

web/pgadmin/browser/register_browser_preferences.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,18 @@ def register_browser_preferences(self):
101101
)
102102
)
103103

104+
self.preference.register(
105+
'display', 'show_server_color_indicator',
106+
gettext("Show server color indicator in panel tabs?"), 'boolean',
107+
False,
108+
category_label=PREF_LABEL_DISPLAY,
109+
help_str=gettext(
110+
'If enabled, a colored circle indicator will be shown in panel '
111+
'tabs (Query Tool, ERD Tool, etc.) matching the server\'s custom '
112+
'background color.'
113+
)
114+
)
115+
104116
self.table_row_count_threshold = self.preference.register(
105117
'properties', 'table_row_count_threshold',
106118
gettext("Count rows if estimated less than"), 'integer', 2000,

web/pgadmin/browser/static/js/node.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,23 @@ define('pgadmin.browser.node', [
479479
'pgadmin:browser:node:' + _newNodeData._type + ':updated',
480480
_item, _newNodeData, _oldNodeData
481481
);
482+
483+
// Trigger a specific event for server color updates
484+
// This allows tabs to update their color indicators when server colors change
485+
if (_newNodeData._type === 'server') {
486+
// Extract colors from the icon string using getServerColors utility
487+
const newColors = commonUtils.getServerColors(_newNodeData?.icon);
488+
489+
pgBrowser.Events.trigger(
490+
'pgadmin:server:colors:updated',
491+
_newNodeData._id,
492+
{
493+
bgcolor: newColors.bgcolor,
494+
fgcolor: newColors.fgcolor,
495+
icon: _newNodeData.icon
496+
}
497+
);
498+
}
482499
},
483500
}
484501
);
@@ -708,9 +725,8 @@ define('pgadmin.browser.node', [
708725

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

715731
if (bgcolor) {
716732
let dynamic_class = 'pga_server_' + serverData._id + '_bgcolor';

web/pgadmin/static/js/ToolView.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export function getToolTabParams(panelId, toolUrl, formParams, tabParams, restor
4242
if(tabParams?.internal?.orig_title){
4343
tabParams.title = tabParams.internal.isDirty ? tabParams.internal.title.slice(0, -1): tabParams.internal.title;
4444
}
45+
4546
return {
4647
id: panelId,
4748
title: panelId,

web/pgadmin/static/js/helpers/Layout/index.jsx

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,24 @@ import UtilityView from '../../UtilityView';
2828
import ToolView, { getToolTabParams } from '../../ToolView';
2929
import { ApplicationStateProvider, useApplicationState } from '../../../../settings/static/ApplicationStateProvider';
3030
import { BROWSER_PANELS, WORKSPACES } from '../../../../browser/static/js/constants';
31+
import pgWindow from 'sources/window';
3132

3233
export function TabTitle({id, closable, defaultInternal}) {
3334
const layoutDocker = React.useContext(LayoutDockerContext);
3435
const internal = layoutDocker?.find(id)?.internal ?? defaultInternal;
36+
const showServerColorIndicator = usePreferences(
37+
(state) => state.getPreferencesForModule('browser')?.show_server_color_indicator ?? false
38+
);
3539
const [attrs, setAttrs] = useState({
3640
icon: internal.icon,
3741
title: internal.title,
3842
tooltip: internal.tooltip ?? internal.title,
43+
bgcolor: internal.bgcolor,
44+
fgcolor: internal.fgcolor,
3945
});
46+
// Track visibility state to trigger re-renders when tabs switch
47+
const [isVisible, setIsVisible] = useState(layoutDocker?.isTabVisible(id) ?? false);
48+
4049
const onContextMenu = useCallback((e)=>{
4150
const g = layoutDocker.find(id)?.group??'';
4251
if((layoutDocker.noContextGroups??[]).includes(g)) return;
@@ -53,18 +62,72 @@ export function TabTitle({id, closable, defaultInternal}) {
5362
icon: internal.icon,
5463
title: internal.title,
5564
tooltip: internal.tooltip ?? internal.title,
65+
bgcolor: internal.bgcolor,
66+
fgcolor: internal.fgcolor,
5667
});
5768
layoutDocker.saveLayout();
5869
}
5970
});
6071

61-
return ()=>deregister?.();
72+
// Listen for tab activation to update visibility state
73+
// This ensures the color indicator appears/disappears when switching tabs
74+
const activeListener = layoutDocker.eventBus.registerListener(LAYOUT_EVENTS.ACTIVE, () => {
75+
const visible = layoutDocker?.isTabVisible(id);
76+
setIsVisible(visible);
77+
});
78+
79+
// Listen for server color updates
80+
// This custom event is triggered specifically when server bgcolor/fgcolor changes
81+
const serverColorsUpdatedHandler = (serverId, colorData) => {
82+
const panelData = layoutDocker?.find(id);
83+
if (!panelData?.internal) {
84+
return;
85+
}
86+
87+
const tabServerId = panelData.internal.server_id;
88+
if (!tabServerId || tabServerId !== serverId) {
89+
return;
90+
}
91+
92+
// Update internal data and attrs with new colors
93+
panelData.internal.bgcolor = colorData.bgcolor || null;
94+
panelData.internal.fgcolor = colorData.fgcolor || null;
95+
setAttrs(prev => ({
96+
...prev,
97+
bgcolor: colorData.bgcolor || null,
98+
fgcolor: colorData.fgcolor || null,
99+
}));
100+
};
101+
102+
// Listen to the custom server color update event
103+
pgWindow.pgAdmin?.Browser?.Events?.on('pgadmin:server:colors:updated', serverColorsUpdatedHandler);
104+
105+
return ()=>{
106+
deregister?.();
107+
activeListener?.();
108+
pgWindow.pgAdmin?.Browser?.Events?.off('pgadmin:server:colors:updated', serverColorsUpdatedHandler);
109+
};
62110
}, []);
63111

64112
return (
65113
<Box display="flex" alignItems="center" title={attrs.tooltip} onContextMenu={onContextMenu} width="100%">
66114
{attrs.icon && <span className={`dock-tab-icon ${attrs.icon}`}></span>}
67-
<span style={{textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap'}} data-visible={layoutDocker.isTabVisible(id)}>{attrs.title}</span>
115+
{showServerColorIndicator && attrs.bgcolor && !isVisible && (
116+
<Box
117+
component="span"
118+
sx={{
119+
width: '12px',
120+
height: '12px',
121+
borderRadius: '50%',
122+
backgroundColor: attrs.bgcolor,
123+
marginLeft: '2px',
124+
marginRight: '4px',
125+
flexShrink: 0,
126+
border: '1px solid rgba(0, 0, 0, 0.1)',
127+
}}
128+
/>
129+
)}
130+
<span style={{textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap'}} data-visible={isVisible}>{attrs.title}</span>
68131
{closable && <PgIconButton title={gettext('Close')} icon={<CloseIcon style={{height: '0.7em'}} />} size="xs" noBorder onClick={()=>{
69132
layoutDocker.close(id);
70133
}} style={{margin: '-1px -10px -1px 0'}} />}
@@ -368,15 +431,19 @@ export class LayoutDocker {
368431
this.saveLayout();
369432
}
370433

371-
static getPanel({icon, title, closable, tooltip, renamable, manualClose, ...attrs}) {
434+
static getPanel({icon, title, closable, tooltip, renamable, manualClose, bgcolor, fgcolor, server_id, ...attrs}) {
372435
const internal = {
373436
icon: icon,
374437
title: title,
375438
tooltip: tooltip,
376439
closable: _.isUndefined(closable) ? manualClose : closable,
377440
renamable: renamable,
378441
manualClose: manualClose,
442+
bgcolor: bgcolor,
443+
fgcolor: fgcolor,
444+
server_id: server_id, // Store server_id to enable color updates when server properties change
379445
};
446+
380447
return {
381448
cached: true,
382449
group: 'default',

web/pgadmin/static/js/utils.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,27 @@ export function getRandomInt(min, max) {
233233
return min + (intArray[0] % range);
234234
}
235235

236+
/*
237+
* Extracts the background and foreground colors from a server icon string.
238+
*
239+
* This is a workaround for a historical design decision where the backend encodes
240+
* structured data (icon CSS class, bgcolor, fgcolor) into a single space-separated
241+
* string via `server_icon_and_background()`.
242+
* The format is: "icon-class bgcolor fgcolor"
243+
* Ref: web/pgadmin/browser/server_groups/servers/__init__.py:server_icon_and_background()
244+
*/
245+
export function getServerColors(serverIcon) {
246+
if (!serverIcon) {
247+
return { bgcolor: null, fgcolor: null };
248+
}
249+
250+
const parts = serverIcon.split(' ');
251+
return {
252+
bgcolor: parts[1] || null,
253+
fgcolor: parts[2] || null,
254+
};
255+
}
256+
236257
export function titleize(i_str) {
237258
if(i_str === '' || i_str === null) return i_str;
238259
return i_str.split(' ')

web/pgadmin/tools/debugger/static/js/DebuggerModule.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import _ from 'lodash';
1111
import ReactDOM from 'react-dom/client';
1212

1313
import gettext from 'sources/gettext';
14-
import { sprintf } from 'sources/utils';
14+
import { sprintf, getServerColors } from 'sources/utils';
1515
import url_for from 'sources/url_for';
1616
import pgWindow from 'sources/window';
1717
import Kerberos from 'pgadmin.authenticate.kerberos';
@@ -378,13 +378,17 @@ export default class DebuggerModule {
378378
let open_new_tab = browser_preferences.new_browser_tab_open;
379379
const db_label = self.checkDbNameChange(data, dbNode, newTreeInfo);
380380
let label = getAppropriateLabel(newTreeInfo);
381+
382+
// Extract bgcolor and fgcolor from server icon
383+
const { bgcolor, fgcolor } = getServerColors(newTreeInfo?.server?.icon);
384+
381385
pgAdmin.Browser.Events.trigger(
382386
'pgadmin:tool:show',
383387
`${BROWSER_PANELS.DEBUGGER_TOOL}_${trans_id}`,
384388
url,
385389
null,
386390
{title: getDebuggerTitle(browser_preferences, label, newTreeInfo.schema.label, db_label, null, self.pgBrowser),
387-
icon: 'fa fa-bug', manualClose: false, renamable: true},
391+
icon: 'fa fa-bug', manualClose: false, renamable: true, bgcolor: bgcolor, fgcolor: fgcolor, server_id: newTreeInfo?.server?._id},
388392
Boolean(open_new_tab?.includes('debugger'))
389393
);
390394
})
@@ -527,13 +531,16 @@ export default class DebuggerModule {
527531

528532
let label = getAppropriateLabel(treeInfo);
529533

534+
// Extract bgcolor and fgcolor from server icon
535+
const { bgcolor, fgcolor } = getServerColors(treeInfo?.server?.icon);
536+
530537
pgAdmin.Browser.Events.trigger(
531538
'pgadmin:tool:show',
532539
`${BROWSER_PANELS.DEBUGGER_TOOL}_${res.data.data.debuggerTransId}`,
533540
url,
534541
null,
535542
{title: getDebuggerTitle(browser_preferences, label, db_label, db_label, null, self.pgBrowser),
536-
icon: 'fa fa-bug', manualClose: false, renamable: true},
543+
icon: 'fa fa-bug', manualClose: false, renamable: true, bgcolor: bgcolor, fgcolor: fgcolor, server_id: treeInfo?.server?._id},
537544
Boolean(open_new_tab?.includes('debugger'))
538545
);
539546
})

web/pgadmin/tools/erd/static/js/ERDModule.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//////////////////////////////////////////////////////////////
99
import pgWindow from 'sources/window';
1010
import {getPanelTitle} from 'tools/sqleditor/static/js/sqleditor_title';
11-
import {getRandomInt} from 'sources/utils';
11+
import {getRandomInt, getServerColors} from 'sources/utils';
1212
import url_for from 'sources/url_for';
1313
import gettext from 'sources/gettext';
1414
import ReactDOM from 'react-dom/client';
@@ -142,12 +142,15 @@ export default class ERDModule {
142142
const panelUrl = this.getPanelUrl(transId, parentData, gen);
143143
const open_new_tab = usePreferences.getState().getPreferencesForModule('browser').new_browser_tab_open;
144144

145+
// Extract bgcolor and fgcolor from server icon
146+
const { bgcolor, fgcolor } = getServerColors(parentData?.server?.icon);
147+
145148
pgAdmin.Browser.Events.trigger(
146149
'pgadmin:tool:show',
147150
`${BROWSER_PANELS.ERD_TOOL}_${transId}`,
148151
panelUrl,
149152
{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},
150-
{title: 'Untitled', icon: 'fa fa-sitemap'},
153+
{title: 'Untitled', icon: 'fa fa-sitemap', bgcolor: bgcolor, fgcolor: fgcolor, server_id: parentData?.server?._id},
151154
Boolean(open_new_tab?.includes('erd_tool'))
152155
);
153156

web/pgadmin/tools/psql/static/js/PsqlModule.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//
88
//////////////////////////////////////////////////////////////
99

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

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

176+
// Extract bgcolor and fgcolor from server icon
177+
const { bgcolor, fgcolor } = getServerColors(parentData?.server?.icon);
178+
176179
pgAdmin.Browser.Events.trigger(
177180
'pgadmin:tool:show',
178181
`${BROWSER_PANELS.PSQL_TOOL}_${transId}`,
179182
panelUrl,
180183
{title: panelTitle, db: db_label, server_name: parentData.server.label, 'user': parentData.server.user.name },
181-
{title: panelTitle, icon: 'pg-font-icon icon-terminal', manualClose: false, renamable: true},
184+
{title: panelTitle, icon: 'pg-font-icon icon-terminal', manualClose: false, renamable: true, bgcolor: bgcolor, fgcolor: fgcolor, server_id: parentData?.server?._id},
182185
Boolean(open_new_tab?.includes('psql_tool'))
183186
);
184187

web/pgadmin/tools/schema_diff/static/js/SchemaDiffModule.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,23 @@ export default class SchemaDiff {
7575
let browserPreferences = usePreferences.getState().getPreferencesForModule('browser');
7676
let openInNewTab = browserPreferences.new_browser_tab_open;
7777

78+
// Extract bgcolor and fgcolor from server icon
79+
let bgcolor = null;
80+
let fgcolor = null;
81+
let serverId = null;
82+
const selectedItem = pgAdmin.Browser.tree?.selected();
83+
if (selectedItem) {
84+
const selectedNodeInfo = pgAdmin.Browser.tree?.getTreeNodeHierarchy(selectedItem);
85+
({ bgcolor, fgcolor } = commonUtils.getServerColors(selectedNodeInfo?.server?.icon));
86+
serverId = selectedNodeInfo?.server?._id;
87+
}
88+
7889
pgAdmin.Browser.Events.trigger(
7990
'pgadmin:tool:show',
8091
`${BROWSER_PANELS.SCHEMA_DIFF_TOOL}_${trans_id}`,
8192
baseUrl,
8293
{...params},
83-
{title: panelTitle, icon: 'pg-font-icon icon-compare', manualClose: false, renamable: true},
94+
{title: panelTitle, icon: 'pg-font-icon icon-compare', manualClose: false, renamable: true, bgcolor: bgcolor, fgcolor: fgcolor, server_id: serverId},
8495
Boolean(openInNewTab?.includes('schema_diff'))
8596
);
8697
return true;

web/pgadmin/tools/sqleditor/static/js/SQLEditorModule.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import usePreferences, { listenPreferenceBroadcast } from '../../../../preferenc
2828
import { PgAdminProvider } from '../../../../static/js/PgAdminProvider';
2929
import { ApplicationStateProvider } from '../../../../settings/static/ApplicationStateProvider';
3030
import ToolErrorView from '../../../../static/js/ToolErrorView';
31+
import { getServerColors } from '../../../../static/js/utils';
3132

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

227+
// Extract bgcolor and fgcolor from server icon
228+
const { bgcolor, fgcolor } = getServerColors(selectedNodeInfo?.server?.icon);
229+
226230
pgAdmin.Browser.Events.trigger(
227231
'pgadmin:tool:show',
228232
`${BROWSER_PANELS.QUERY_TOOL}_${trans_id}`,
229233
panel_url,
230234
{...params, title: panel_title, selectedNodeInfo: JSON.stringify(selectedNodeInfo)},
231-
{title: panel_title, icon: icon, tooltip: tooltip, renamable: true},
235+
{title: panel_title, icon: icon, tooltip: tooltip, renamable: true, bgcolor: bgcolor, fgcolor: fgcolor, server_id: selectedNodeInfo?.server?._id},
232236
Boolean(open_new_tab?.includes('qt'))
233237
);
234238
return true;

0 commit comments

Comments
 (0)