Skip to content

Commit 986ba41

Browse files
Added the ability to search for tables and automatically bring them into view in the ERD tool. #4306
1 parent 08379d6 commit 986ba41

File tree

19 files changed

+218
-24
lines changed

19 files changed

+218
-24
lines changed

docs/en_US/erd_tool.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ Editing Options
8686
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
8787
| Icon | Behavior | Shortcut |
8888
+======================+===================================================================================================+================+
89+
| *Search table* | Click to search for a table in the diagram. Selecting a table from the search results will bring | Option/Alt + |
90+
| | it into view and highlight it. | Ctrl + F |
91+
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
8992
| *Add table* | Click this button to add a new table to the diagram. On clicking, this will open a table dialog | Option/Alt + |
9093
| | where you can put the table details. | Ctrl + A |
9194
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
@@ -109,11 +112,14 @@ Table Relationship Options
109112
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
110113
| Icon | Behavior | Shortcut |
111114
+======================+===================================================================================================+================+
112-
| *1M* | Click this button to open a one-to-many relationship dialog to add a relationship between the | Option/Alt + |
115+
| *1-1* | Click this button to open a one-to-one relationship dialog to add a relationship between the | Option/Alt + |
116+
| | two tables. The selected table becomes the referencing table. | Ctrl + B |
117+
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
118+
| *1-M* | Click this button to open a one-to-many relationship dialog to add a relationship between the | Option/Alt + |
113119
| | two tables. The selected table becomes the referencing table and will have the *many* endpoint of | Ctrl + O |
114120
| | the link. | |
115121
+----------------------+---------------------------------------------------------------------------------------------------+----------------+
116-
| *MM* | Click this button to open a many-to-many relationship dialog to add a relationship between the | Option/Alt + |
122+
| *M-M* | Click this button to open a many-to-many relationship dialog to add a relationship between the | Option/Alt + |
117123
| | two tables. This option will create a new table based on the selected columns for the two relating| Ctrl + M |
118124
| | tables and link them. | |
119125
+----------------------+---------------------------------------------------------------------------------------------------+----------------+

docs/en_US/images/erd_tool.png

339 KB
Loading
-163 Bytes
Loading

docs/en_US/release_notes_9_10.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Bundled PostgreSQL Utilities
2020
New features
2121
************
2222

23+
| `Issue #4306 <https://github.com/pgadmin-org/pgadmin4/issues/4306>`_ - Added the ability to search for tables and automatically bring them into view in the ERD tool.
2324
| `Issue #6698 <https://github.com/pgadmin-org/pgadmin4/issues/6698>`_ - Add support for setting image download resolution in the ERD tool.
2425
| `Issue #7885 <https://github.com/pgadmin-org/pgadmin4/issues/7885>`_ - Add support for displaying detailed Citus query plans instead of 'Custom Scan' placeholder.
2526
| `Issue #8912 <https://github.com/pgadmin-org/pgadmin4/issues/8912>`_ - Add support for formatting .pgerd ERD project file.
@@ -34,4 +35,6 @@ Bug fixes
3435

3536
| `Issue #8504 <https://github.com/pgadmin-org/pgadmin4/issues/8504>`_ - Fixed an issue where data output column resize is not sticking in Safari.
3637
| `Issue #9117 <https://github.com/pgadmin-org/pgadmin4/issues/9117>`_ - Fixed an issue where Schema Diff does not ignore Tablespace for indexes.
38+
| `Issue #9132 <https://github.com/pgadmin-org/pgadmin4/issues/9132>`_ - Fixed an issue where the 2FA window redirected to the login page after session expiration.
39+
| `Issue #9240 <https://github.com/pgadmin-org/pgadmin4/issues/9240>`_ - Fixed an issue where the Debian build process failed with a "Sphinx module not found" error when using a Python virtual environment.
3740
| `Issue #9304 <https://github.com/pgadmin-org/pgadmin4/issues/9304>`_ - Fixed an issue that prevented assigning multiple users to an RLS policy.

web/pgadmin/static/js/helpers/ModalProvider.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ const StyleDialog = styled(Dialog)(({theme}) => ({
295295
},
296296
}));
297297

298-
function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose, fullScreen = false, isFullWidth = false, showFullScreen = false, isResizeable = false, minHeight = MIN_HEIGHT, minWidth = MIN_WIDTH, showTitle=true }) {
298+
function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose, fullScreen = false, isFullWidth = false, showFullScreen = false, isResizeable = false, minHeight = MIN_HEIGHT, minWidth = MIN_WIDTH, showTitle=true, ...props }) {
299299
let useModalRef = useModal();
300300
let closeModal = (_e, reason) => {
301301
if(reason == 'backdropClick' && showTitle) {
@@ -321,6 +321,7 @@ function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose
321321
fullScreen={isFullScreen}
322322
fullWidth={isFullWidth}
323323
disablePortal
324+
{...props}
324325
>
325326
{ showTitle &&
326327
<DialogTitle className='modal-drag-area'>

web/pgadmin/tools/erd/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,24 @@ def register_preferences(self):
158158
fields=shortcut_fields
159159
)
160160

161+
self.preference.register(
162+
'keyboard_shortcuts',
163+
'search_table',
164+
gettext('Search table'),
165+
'keyboardshortcut',
166+
{
167+
'alt': True,
168+
'shift': False,
169+
'control': True,
170+
'key': {
171+
'key_code': 70,
172+
'char': 'f'
173+
}
174+
},
175+
category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
176+
fields=shortcut_fields
177+
)
178+
161179
self.preference.register(
162180
'keyboard_shortcuts',
163181
'add_table',

web/pgadmin/tools/erd/static/js/erd_tool/ERDConstants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ export const ERD_EVENTS = {
44
TRIGGER_SHOW_SQL: 'TRIGGER_SHOW_SQL',
55
SHOW_SQL: 'SHOW_SQL',
66
DOWNLOAD_IMAGE: 'DOWNLOAD_IMAGE',
7+
8+
SEARCH_NODE: 'SEARCH_NODE',
79
ADD_NODE: 'ADD_NODE',
810
EDIT_NODE: 'EDIT_NODE',
911
CLONE_NODE: 'CLONE_NODE',
1012
DELETE_NODE: 'DELETE_NODE',
13+
1114
SHOW_NOTE: 'SHOW_NOTE',
1215
ONE_TO_ONE: 'ONE_TO_ONE',
1316
ONE_TO_MANY: 'ONE_TO_MANY',

web/pgadmin/tools/erd/static/js/erd_tool/components/ERDTool.jsx

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { useApplicationState } from '../../../../../../settings/static/Applicati
4242
import { connectServerModal, connectServer } from '../../../../../sqleditor/static/js/components/connectServer';
4343
import { useEffect } from 'react';
4444
import { FileManagerUtils } from '../../../../../../misc/file_manager/static/js/components/FileManager';
45+
import SearchNode from './SearchNode';
4546

4647
/* Custom react-diagram action for keyboard events */
4748
export class KeyboardShortcutAction extends Action {
@@ -178,9 +179,9 @@ export default class ERDTool extends React.Component {
178179
this.eventBus = new EventBus();
179180

180181
_.bindAll(this, ['onLoadDiagram', 'onSaveDiagram', 'onSQLClick',
181-
'onImageClick', 'onAddNewNode', 'onEditTable', 'onCloneNode', 'onDeleteNode', 'onNoteClick',
182+
'onImageClick', 'onSearchNode', 'onAddNewNode', 'onEditTable', 'onCloneNode', 'onDeleteNode', 'onNoteClick',
182183
'onNoteClose', 'onOneToOneClick', 'onOneToManyClick', 'onManyToManyClick', 'onAutoDistribute', 'onDetailsToggle',
183-
'onChangeColors', 'onDropNode', 'onNotationChange', 'closePanel'
184+
'onChangeColors', 'onDropNode', 'onNotationChange', 'closePanel', 'scrollToNode'
184185
]);
185186

186187
this.diagram.zoomToFit = this.diagram.zoomToFit.bind(this.diagram);
@@ -249,6 +250,7 @@ export default class ERDTool extends React.Component {
249250
this.eventBus.registerListener(ERD_EVENTS.SAVE_DIAGRAM, this.onSaveDiagram);
250251
this.eventBus.registerListener(ERD_EVENTS.SHOW_SQL, this.onSQLClick);
251252
this.eventBus.registerListener(ERD_EVENTS.DOWNLOAD_IMAGE, this.onImageClick);
253+
this.eventBus.registerListener(ERD_EVENTS.SEARCH_NODE, this.onSearchNode);
252254
this.eventBus.registerListener(ERD_EVENTS.ADD_NODE, this.onAddNewNode);
253255
this.eventBus.registerListener(ERD_EVENTS.EDIT_NODE, this.onEditTable);
254256
this.eventBus.registerListener(ERD_EVENTS.CLONE_NODE, this.onCloneNode);
@@ -285,6 +287,9 @@ export default class ERDTool extends React.Component {
285287
[this.state.preferences.download_image, ()=>{
286288
this.eventBus.fireEvent(ERD_EVENTS.DOWNLOAD_IMAGE);
287289
}],
290+
[this.state.preferences.search_table, ()=>{
291+
this.eventBus.fireEvent(ERD_EVENTS.SEARCH_NODE);
292+
}],
288293
[this.state.preferences.add_table, ()=>{
289294
this.eventBus.fireEvent(ERD_EVENTS.ADD_NODE);
290295
}],
@@ -488,12 +493,69 @@ export default class ERDTool extends React.Component {
488493
}
489494
}
490495

496+
scrollToNode(node) {
497+
const engine = this.diagram.getEngine();
498+
const model = engine.getModel();
499+
const container = this.canvasEle;
500+
if (!node || !container) return;
501+
502+
const { x, y } = node.getPosition();
503+
const zoom = model.getZoomLevel() / 100;
504+
const offsetX = model.getOffsetX();
505+
const offsetY = model.getOffsetY();
506+
507+
const viewportWidth = container.clientWidth;
508+
const viewportHeight = container.clientHeight;
509+
510+
const nodeWidth = node.width; // Approximate width of a table node
511+
const nodeHeight = node.height; // Approximate height of a table node
512+
513+
// Node screen bounds
514+
const nodeLeft = x * zoom + offsetX;
515+
const nodeRight = nodeLeft + nodeWidth * zoom;
516+
const nodeTop = y * zoom + offsetY;
517+
const nodeBottom = nodeTop + nodeHeight * zoom;
518+
519+
let newOffsetX = offsetX;
520+
let newOffsetY = offsetY;
521+
522+
// Check horizontal visibility
523+
if (nodeLeft < 0) {
524+
newOffsetX += -nodeLeft + 20; // 20px padding
525+
} else if (nodeRight > viewportWidth) {
526+
newOffsetX -= nodeRight - viewportWidth + 20;
527+
}
528+
529+
// Check vertical visibility
530+
if (nodeHeight * zoom >= viewportHeight) {
531+
// Node taller than viewport: snap top of node to top of viewport
532+
newOffsetY = offsetY + viewportHeight / 2 - (nodeHeight * zoom) / 2;
533+
newOffsetY = offsetY - (nodeTop - 20); // aligns top
534+
} else {
535+
// Node fits in viewport: ensure fully visible
536+
if (nodeTop < 0) {
537+
newOffsetY += -nodeTop + 20;
538+
} else if (nodeBottom > viewportHeight) {
539+
newOffsetY -= nodeBottom - viewportHeight + 20;
540+
}
541+
}
542+
543+
// Update offset only if needed
544+
if (newOffsetX !== offsetX || newOffsetY !== offsetY) {
545+
model.setOffset(newOffsetX, newOffsetY);
546+
}
547+
548+
this.diagram.repaint();
549+
node.setSelected(true);
550+
node.fireEvent({}, 'highlightFlash');
551+
};
552+
553+
491554
addEditTable(node) {
492555
let dialog = this.getDialog('table_dialog');
493556
if(node) {
494-
let [schema, table] = node.getSchemaTableName();
495557
let oldData = node.getData();
496-
dialog(gettext('Table: %s (%s)', _.escape(table),_.escape(schema)), oldData, false, (newData)=>{
558+
dialog(gettext('Table: %s', node.getDisplayName()), oldData, false, (newData)=>{
497559
if(this.diagram.anyDuplicateNodeName(newData, oldData)) {
498560
return gettext('Table name already exists');
499561
}
@@ -560,6 +622,12 @@ export default class ERDTool extends React.Component {
560622
}
561623
}
562624

625+
onSearchNode() {
626+
this.context.showModal(gettext('Search'), (closeModal)=>(
627+
<SearchNode tableNodes={this.diagram.getModel().getNodesDict()} onClose={closeModal} scrollToNode={this.scrollToNode} />
628+
), {id: 'id-erd-search-node', showTitle: false, disableRestoreFocus: true});
629+
}
630+
563631
onAddNewNode() {
564632
this.addEditTable();
565633
}

web/pgadmin/tools/erd/static/js/erd_tool/components/FloatingNote.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ export default function FloatingNote({open, onClose, anchorEl, rows, noteNode})
6262

6363
const header = useMemo(()=>{
6464
if(noteNode) {
65-
let [schema, name] = noteNode.getSchemaTableName();
66-
return `${name} (${schema})`;
65+
return noteNode.getDisplayName();
6766
}
6867
return '';
6968
}, [open]);

web/pgadmin/tools/erd/static/js/erd_tool/components/MainToolBar.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import ImageRoundedIcon from '@mui/icons-material/ImageRounded';
2828
import FormatColorFillRoundedIcon from '@mui/icons-material/FormatColorFillRounded';
2929
import FormatColorTextRoundedIcon from '@mui/icons-material/FormatColorTextRounded';
3030
import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined';
31+
import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined';
3132

3233
import { PgMenu, PgMenuItem, usePgMenuGroup } from '../../../../../../static/js/components/Menu';
3334
import gettext from 'sources/gettext';
@@ -201,6 +202,11 @@ export function MainToolBar({preferences, eventBus, fillColor, textColor, notati
201202
}} />
202203
</PgButtonGroup>
203204
<PgButtonGroup size="small">
205+
<PgIconButton title={gettext('Search Table')} icon={<SearchOutlinedIcon />}
206+
shortcut={preferences.search_table}
207+
onClick={()=>{
208+
eventBus.fireEvent(ERD_EVENTS.SEARCH_NODE);
209+
}} />
204210
<PgIconButton title={gettext('Add Table')} icon={<AddBoxIcon />}
205211
shortcut={preferences.add_table}
206212
onClick={()=>{

0 commit comments

Comments
 (0)