Skip to content

Commit 3cfa44b

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

File tree

9 files changed

+127
-14
lines changed

9 files changed

+127
-14
lines changed

web/pgadmin/browser/register_browser_preferences.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ 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', False,
107+
category_label=PREF_LABEL_DISPLAY,
108+
help_str=gettext(
109+
'If enabled, a colored circle indicator will be shown in panel '
110+
'tabs (Query Tool, ERD Tool, etc.) matching the server\'s custom '
111+
'background color.'
112+
)
113+
)
114+
104115
self.table_row_count_threshold = self.preference.register(
105116
'properties', 'table_row_count_threshold',
106117
gettext("Count rows if estimated less than"), 'integer', 2000,

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -708,9 +708,8 @@ define('pgadmin.browser.node', [
708708

709709
// Go further only if node type is a Server
710710
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] || '';
711+
// Extract bgcolor and fgcolor from server icon
712+
const { bgcolor, fgcolor } = commonUtils.getServerColors(serverData.icon);
714713

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

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

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,21 @@ 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 { getServerColors } from '../../utils';
32+
import pgWindow from 'sources/window';
3133

3234
export function TabTitle({id, closable, defaultInternal}) {
3335
const layoutDocker = React.useContext(LayoutDockerContext);
3436
const internal = layoutDocker?.find(id)?.internal ?? defaultInternal;
37+
const showServerColorIndicator = usePreferences(
38+
(state) => state.getPreferencesForModule('browser')?.show_server_color_indicator ?? false
39+
);
3540
const [attrs, setAttrs] = useState({
3641
icon: internal.icon,
3742
title: internal.title,
3843
tooltip: internal.tooltip ?? internal.title,
44+
bgcolor: internal.bgcolor,
45+
fgcolor: internal.fgcolor,
3946
});
4047
const onContextMenu = useCallback((e)=>{
4148
const g = layoutDocker.find(id)?.group??'';
@@ -53,17 +60,62 @@ export function TabTitle({id, closable, defaultInternal}) {
5360
icon: internal.icon,
5461
title: internal.title,
5562
tooltip: internal.tooltip ?? internal.title,
63+
bgcolor: internal.bgcolor,
64+
fgcolor: internal.fgcolor,
5665
});
5766
layoutDocker.saveLayout();
5867
}
5968
});
6069

61-
return ()=>deregister?.();
70+
// Listen for server property updates to refresh colors
71+
// When a server's bgcolor/fgcolor is changed, this event is triggered
72+
// and we update the tab colors accordingly
73+
const serverUpdatedHandler = (_item, newNodeData) => {
74+
if (newNodeData._type === 'server') {
75+
const panelData = layoutDocker?.find(id);
76+
if (panelData?.internal) {
77+
const serverId = panelData.internal.server_id;
78+
if (serverId && serverId === newNodeData._id) {
79+
const { bgcolor, fgcolor } = getServerColors(newNodeData.icon);
80+
// Update internal data and attrs
81+
panelData.internal.bgcolor = bgcolor;
82+
panelData.internal.fgcolor = fgcolor;
83+
setAttrs(prev => ({
84+
...prev,
85+
bgcolor: bgcolor,
86+
fgcolor: fgcolor,
87+
}));
88+
}
89+
}
90+
}
91+
};
92+
93+
pgWindow.pgAdmin?.Browser?.Events?.on('pgadmin:browser:node:updated', serverUpdatedHandler);
94+
95+
return ()=>{
96+
deregister?.();
97+
pgWindow.pgAdmin?.Browser?.Events?.off('pgadmin:browser:node:updated', serverUpdatedHandler);
98+
};
6299
}, []);
63100

64101
return (
65102
<Box display="flex" alignItems="center" title={attrs.tooltip} onContextMenu={onContextMenu} width="100%">
66103
{attrs.icon && <span className={`dock-tab-icon ${attrs.icon}`}></span>}
104+
{showServerColorIndicator && attrs.bgcolor && !layoutDocker.isTabVisible(id) && (
105+
<Box
106+
component="span"
107+
sx={{
108+
width: '12px',
109+
height: '12px',
110+
borderRadius: '50%',
111+
backgroundColor: attrs.bgcolor,
112+
marginLeft: '2px',
113+
marginRight: '4px',
114+
flexShrink: 0,
115+
border: '1px solid rgba(0, 0, 0, 0.1)',
116+
}}
117+
/>
118+
)}
67119
<span style={{textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap'}} data-visible={layoutDocker.isTabVisible(id)}>{attrs.title}</span>
68120
{closable && <PgIconButton title={gettext('Close')} icon={<CloseIcon style={{height: '0.7em'}} />} size="xs" noBorder onClick={()=>{
69121
layoutDocker.close(id);
@@ -368,14 +420,16 @@ export class LayoutDocker {
368420
this.saveLayout();
369421
}
370422

371-
static getPanel({icon, title, closable, tooltip, renamable, manualClose, ...attrs}) {
423+
static getPanel({icon, title, closable, tooltip, renamable, manualClose, bgcolor, fgcolor, ...attrs}) {
372424
const internal = {
373425
icon: icon,
374426
title: title,
375427
tooltip: tooltip,
376428
closable: _.isUndefined(closable) ? manualClose : closable,
377429
renamable: renamable,
378430
manualClose: manualClose,
431+
bgcolor: bgcolor,
432+
fgcolor: fgcolor,
379433
};
380434
return {
381435
cached: true,

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)