Skip to content
Merged
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
Binary file modified docs/en_US/images/main_left_pane.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/en_US/images/object_explorer_filter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/en_US/images/toolbar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions docs/en_US/toolbar.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ the selected object node.
:alt: pgAdmin Toolbar
:align: center

* Use the :ref:`Object filter <object-explorer-filter>` button to access
the Object Filter popup. It helps you filter objects in the Object Explorer tree.
* Use the :ref:`Query Tool <query_tool>` button to open the Query Tool in the
current database context.
* Use the :ref:`View Data <editgrid>` button to view/edit the data stored in a
Expand All @@ -23,3 +25,24 @@ the selected object node.
dialog. It helps you search any database object.
* Use the :ref:`PSQL Tool <psql_tool>` button to open the PSQL in the current
database context.



Comment thread
adityatoshniwal marked this conversation as resolved.
.. _object-explorer-filter:

*******************************
`Object Explorer Filter`:index:
*******************************
.. image:: /images/object_explorer_filter.png
:alt: Object Explorer Filter Dialog
:align: center

Use this tool to filter objects in the Object Explorer by
following fields:

* Use the *Tags* field to filter the servers with one or more server tags. The
servers with any of the selected tags will be displayed in the Object Explorer.
You can also create a new tag by typing in the field and pressing Enter.

Click the **Apply** button to apply the filter. Please note the object explorer will
refresh after applying the filter.
26 changes: 24 additions & 2 deletions web/pgadmin/browser/server_groups/servers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from pgadmin.utils.preferences import Preferences
from .... import socketio as sio
from pgadmin.utils import get_complete_file_path
from pgadmin.settings.utils import with_object_filters


def has_any(data, keys):
Expand Down Expand Up @@ -220,8 +221,25 @@ def get_servers(self, all_servers, hide_shared_server, gid):

return servers

def has_tag(self, server, object_filters):
try:
# No tags filter, show all
if len(object_filters['tags']) == 0:
return True

# No tags on server, don't show
if server.tags is None or len(server.tags) == 0:
return False

# Check if any of the tag exists
return any([t['text'] in object_filters['tags']
for t in server.tags])
except Exception as _:
return True

@with_object_filters
@pga_login_required
def get_nodes(self, gid):
def get_nodes(self, gid, object_filters):
"""Return a JSON document listing the server groups for the user"""

hide_shared_server = get_preferences()
Expand All @@ -241,6 +259,10 @@ def get_nodes(self, gid):
wal_paused = None
server_type = 'pg'
user_info = None

if not self.has_tag(server, object_filters):
continue

try:
manager = driver.connection_manager(server.id)
conn = manager.connection()
Expand Down Expand Up @@ -926,7 +948,7 @@ def _server_modify_disallowed_when_connected(
)

@pga_login_required
def list(self, gid):
def list(self, gid, object_filters):
"""
Return list of attributes of all servers.
"""
Expand Down
28 changes: 27 additions & 1 deletion web/pgadmin/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ def get_exposed_url_endpoints(self):
'settings.save_application_state',
'settings.get_application_state',
'settings.delete_application_state',
'settings.get_tool_data'
'settings.get_tool_data',
'settings.object_explorer_filter'
]


Expand Down Expand Up @@ -440,6 +441,31 @@ def get_tool_data(trans_id):
))


@blueprint.route(
'/object_explorer_filter',
methods=["GET", "PUT"], endpoint='object_explorer_filter')
Comment thread
akshay-joshi marked this conversation as resolved.
@pga_login_required
def object_explorer_filter():
if request.method == 'GET':
result = get_setting('Object Explorer/Filter', '{}')
return make_json_response(
data={
'status': True,
'msg': '',
'result': json.loads(result)
}
)
else:
data = json.loads(request.data.decode('utf-8'))
store_setting('Object Explorer/Filter', json.dumps(data))
return make_json_response(
data={
'status': True,
'msg': 'Success',
}
)


@blueprint.route(
'/delete_application_state/',
methods=["DELETE"], endpoint='delete_application_state')
Expand Down
26 changes: 26 additions & 0 deletions web/pgadmin/settings/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2025, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################

from flask_login import current_user
import functools
Comment thread
adityatoshniwal marked this conversation as resolved.
import json
from pgadmin.model import Setting


Expand Down Expand Up @@ -34,3 +45,18 @@ def get_file_type_setting(file_types):
return '*'
else:
return data.value


def with_object_filters(f):
@functools.wraps(f)
def wrapped(*args, **kwargs):
data = Setting.query.filter_by(
user_id=current_user.id, setting='Object Explorer/Filter').first()
if not data or data.value is None:
data = {}
else:
data = json.loads(data.value)

return f(*args, **kwargs, object_filters=data)

return wrapped
8 changes: 5 additions & 3 deletions web/pgadmin/static/js/BrowserComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import Dependencies from '../../misc/dependencies/static/js/Dependencies';
import Dependents from '../../misc/dependents/static/js/Dependents';
import ModalProvider from './helpers/ModalProvider';
import { NotifierProvider } from './helpers/Notifier';
import ObjectExplorerToolbar from './helpers/ObjectExplorerToolbar';
import ObjectExplorerToolbar from './tree/ObjectExplorer/ObjectExplorerToolbar';
import MainMoreToolbar from './helpers/MainMoreToolbar';
import Dashboard from '../../dashboard/static/js/Dashboard';
import usePreferences from '../../preferences/static/js/store';
Expand Down Expand Up @@ -91,8 +91,10 @@ let defaultLayout = {
size: 20,
tabs: [
LayoutDocker.getPanel({
id: BROWSER_PANELS.OBJECT_EXPLORER, title: gettext('Object Explorer'),
content: <ObjectExplorer />, group: 'object-explorer'
id: BROWSER_PANELS.OBJECT_EXPLORER,
title: gettext('Object Explorer'),
content: <ObjectExplorer />,
group: 'object-explorer'
}),
],
},
Expand Down
1 change: 0 additions & 1 deletion web/pgadmin/static/js/Explain/Graphical.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const StyledBox = styled(Box)(({theme}) => ({
bottom: '0.25rem',
right: '0.25rem',
borderColor: theme.otherVars.borderColor,
// box-shadow: 0 0.125rem 0.5rem rgb(132 142 160 / 28%);
wordBreak: 'break-all',
display: 'flex',
flexDirection: 'column',
Expand Down
14 changes: 14 additions & 0 deletions web/pgadmin/static/js/Theme/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,20 @@ basicSettings = createTheme(basicSettings, {
}
}
}
},
MuiBadge: {
defaultProps: {
overlap: 'circular',
color: 'success',
variant: 'dot',
},
styleOverrides: {
badge: {
height: '6px',
minWidth: '6px',
right: '16%',
},
}
}
},
});
Expand Down
160 changes: 160 additions & 0 deletions web/pgadmin/static/js/tree/ObjectExplorer/ObjectExplorerFilter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////

import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { usePgAdmin } from '../../PgAdminProvider';
import { Box, styled } from '@mui/material';
import { DefaultButton, PrimaryButton } from '../../components/Buttons';
import gettext from 'sources/gettext';
import { FormInputSelect, FormNote } from '../../components/FormComponents';
import getApiInstance, { parseApiError } from '../../api_instance';
import url_for from 'sources/url_for';
import Loader from '../../components/Loader';

const ObjectExplorerFilterRoot = styled('div')(({ theme }) => ({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
display: 'flex',
backgroundColor: theme.palette.background.paper,
padding: '8px',
flexDirection: 'column',
gap: '8px',
...theme.mixins.panelBorder?.bottom,
}));


export default function ObjectExplorerFilter() {
const [open, setOpen] = useState(false);
const appliedFiltersRef = useRef(null);
const [currFilter, setCurrFilter] = useState({
tags: [],
});
const [loadingText, setLoadingText] = useState('');
const api = useMemo(()=>getApiInstance(), []);
const pgAdmin = usePgAdmin();
const firstEleRef = useRef(null);
const openServersRef = useRef({});

const onClose = ()=>{
setOpen(false);
};

const updateAppliedFilters = (filters) => {
appliedFiltersRef.current = filters;
const hasFilters = filters?.tags?.length > 0;
pgAdmin.Browser.Events.trigger('pgadmin:object-explorer:filter:apply', hasFilters);
};

const fetchFilter = async () => {
try {
setLoadingText(gettext('Loading...'));
const {data: resp} = await api.get(url_for('settings.object_explorer_filter'));
setCurrFilter(resp.data.result);
updateAppliedFilters(resp.data.result);
} catch(error) {
console.error('Error fetching object explorer filter:', error);
}
setLoadingText('');
};

const applyFilter = async () => {
try {
setLoadingText(gettext('Applying filter...'));
await api.put(url_for('settings.object_explorer_filter'), currFilter);
updateAppliedFilters(currFilter);

// Save the state of the browser tree
await pgAdmin.Browser.browserTreeState.save_state();

// register to add event to open the server
const deregister = pgAdmin.Browser.Events.on('pgadmin-browser:tree:added', async (item, d)=>{
if(d._type == 'server' && openServersRef.current[d._id]) {
delete openServersRef.current[d._id];
await pgAdmin.Browser.tree.ensureLoaded(item);
await pgAdmin.Browser.tree.open(item);
}
if(Object.keys(openServersRef.current).length === 0) {
// all servers are opened, deregister the event to avoid unnecessary calls
deregister();
};
});

// lets do one server group at a time
(pgAdmin.Browser.tree.children()||[]).forEach(async (serverGroup)=>{
// restore tree state works after a server is opened
// We will note server open state here before refreshing
pgAdmin.Browser.tree.children(serverGroup).forEach((server)=>{
const serverData = server._metadata.data;
if(pgAdmin.Browser.tree.isOpen(server)) {
openServersRef.current[serverData._id] = serverData._id;
} else {
delete openServersRef.current[serverData._id];
}
});

// refresh the server group to apply the filter
await pgAdmin.Browser.tree.refresh(serverGroup);
});
setLoadingText('');
onClose();
} catch(error) {
console.error('Error applying object explorer filter:', error);
pgAdmin.Browser.notifier.error(parseApiError(error));
setLoadingText('');
}
};

const onChange = (v) => {
setCurrFilter((prev)=>({...prev, tags: v}));
};

useEffect(()=>{
fetchFilter();
const deregister = pgAdmin.Browser.Events.on('pgadmin:object-explorer:filter:show', ()=>{
setOpen(true);
});
return ()=>{
deregister();
};
}, []);

useEffect(()=>{
if(!open) return;
setCurrFilter(appliedFiltersRef.current);
}, [open]);

useLayoutEffect(()=>{
if(!open) return;
// Focus on the first element when the filter is opened
firstEleRef.current?.focus();
}, [open]);

if(!open) {
return <></>;
}

return (
<ObjectExplorerFilterRoot sx={{ display: open ? 'flex' : 'none' }}>
<Loader message={loadingText} />
<FormInputSelect inputRef={firstEleRef} label={gettext('Tags')} controlProps={{
multiple: true,
allowClear: true,
creatable: true,
noDropdown: true,
}} value={currFilter.tags} onChange={onChange} placeholder={gettext('Specify the tags...')} />
<FormNote text={gettext('Applying the filter will only hide the servers from view, it won\'t close any active connections.')} />
<Box sx={{display: 'flex', justifyContent: 'flex-end'}}>
<DefaultButton size="small" onClick={()=>onClose()}>Close</DefaultButton>
<PrimaryButton size="small" onClick={applyFilter} sx={{marginLeft: '8px'}}>Apply</PrimaryButton>
</Box>
</ObjectExplorerFilterRoot>
);
}
Loading
Loading