diff --git a/docs/en_US/images/search_objects.png b/docs/en_US/images/search_objects.png index 4498a63ad..3add676e6 100644 Binary files a/docs/en_US/images/search_objects.png and b/docs/en_US/images/search_objects.png differ diff --git a/docs/en_US/release_notes_6_14.rst b/docs/en_US/release_notes_6_14.rst index 0916db3fe..947c477d4 100644 --- a/docs/en_US/release_notes_6_14.rst +++ b/docs/en_US/release_notes_6_14.rst @@ -13,6 +13,7 @@ New features Housekeeping ************ + | `Issue #7622 `_ - Port search object dialog to React. Bug fixes ********* diff --git a/web/pgadmin/browser/static/js/collection.js b/web/pgadmin/browser/static/js/collection.js index 9b894c9b6..ada8258fc 100644 --- a/web/pgadmin/browser/static/js/collection.js +++ b/web/pgadmin/browser/static/js/collection.js @@ -124,8 +124,8 @@ define([ } }, show_search_objects: function() { - if(pgAdmin.SearchObjects) { - pgAdmin.SearchObjects.show_search_objects('', pgAdmin.Browser.tree.selected()); + if(pgAdmin.Tools.SearchObjects) { + pgAdmin.Tools.SearchObjects.show_search_objects('', pgAdmin.Browser.tree.selected()); } }, show_psql_tool: function(args) { diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js index 199ee986f..46574d5e0 100644 --- a/web/pgadmin/browser/static/js/node.js +++ b/web/pgadmin/browser/static/js/node.js @@ -208,7 +208,7 @@ define('pgadmin.browser.node', [ // show search objects same as query tool pgAdmin.Browser.add_menus([{ - name: 'search_objects', node: self.type, module: pgAdmin.SearchObjects, + name: 'search_objects', node: self.type, module: pgAdmin.Tools.SearchObjects, applies: ['context'], callback: 'show_search_objects', priority: 997, label: gettext('Search Objects...'), icon: 'fa fa-search', enable: enable, diff --git a/web/pgadmin/browser/static/js/toolbar.js b/web/pgadmin/browser/static/js/toolbar.js index af3ad7d13..974b40e92 100644 --- a/web/pgadmin/browser/static/js/toolbar.js +++ b/web/pgadmin/browser/static/js/toolbar.js @@ -118,7 +118,7 @@ export function initializeToolbar(panel, wcDocker) { else if ('name' in data && data.name === gettext('Filtered Rows')) pgAdmin.Tools.SQLEditor.showFilteredRow({mnuid: 4}, pgAdmin.Browser.tree.selected()); else if ('name' in data && data.name === gettext('Search objects')) - pgAdmin.SearchObjects.show_search_objects('', pgAdmin.Browser.tree.selected()); + pgAdmin.Tools.SearchObjects.show_search_objects('', pgAdmin.Browser.tree.selected()); else if ('name' in data && data.name === gettext('PSQL Tool')){ var input = {}, t = pgAdmin.Browser.tree, diff --git a/web/pgadmin/feature_tests/xss_checks_roles_control_test.py b/web/pgadmin/feature_tests/xss_checks_roles_control_test.py index 7499f6b54..e8bc9ec19 100644 --- a/web/pgadmin/feature_tests/xss_checks_roles_control_test.py +++ b/web/pgadmin/feature_tests/xss_checks_roles_control_test.py @@ -56,7 +56,7 @@ class CheckRoleMembershipControlFeatureTest(BaseFeatureTest): self.page.remove_server(self.server) test_utils.drop_role(self.server, "postgres", self.role) - test_utils.drop_role(self.server, "postgres",self.xss_test_role) + test_utils.drop_role(self.server, "postgres", self.xss_test_role) def _role_node_expandable(self, role): retry = 3 diff --git a/web/pgadmin/misc/cloud/static/js/cloud.js b/web/pgadmin/misc/cloud/static/js/cloud.js index 2d8a32542..827814aa3 100644 --- a/web/pgadmin/misc/cloud/static/js/cloud.js +++ b/web/pgadmin/misc/cloud/static/js/cloud.js @@ -94,6 +94,7 @@ define('pgadmin.misc.cloud', [ { + ReactDOM.unmountComponentAtNode(j[0]); panel.close(); }}/> , j[0]); diff --git a/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx b/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx index 7b04a6216..d2f44319b 100644 --- a/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx +++ b/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx @@ -1,3 +1,11 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// import { Box, makeStyles } from '@material-ui/core'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { DefaultButton, PgButtonGroup, PgIconButton, PrimaryButton } from '../../../../../static/js/components/Buttons'; diff --git a/web/pgadmin/misc/file_manager/static/js/components/GridView.jsx b/web/pgadmin/misc/file_manager/static/js/components/GridView.jsx index 39254aeae..557c4644f 100644 --- a/web/pgadmin/misc/file_manager/static/js/components/GridView.jsx +++ b/web/pgadmin/misc/file_manager/static/js/components/GridView.jsx @@ -1,3 +1,11 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// import { Box, makeStyles } from '@material-ui/core'; import React, {useState, useEffect, useRef, useLayoutEffect} from 'react'; import FolderIcon from '@material-ui/icons/Folder'; @@ -5,6 +13,7 @@ import DescriptionIcon from '@material-ui/icons/Description'; import LockRoundedIcon from '@material-ui/icons/LockRounded'; import StorageRoundedIcon from '@material-ui/icons/StorageRounded'; import PropTypes from 'prop-types'; +import gettext from 'sources/gettext'; const useStyles = makeStyles((theme)=>({ @@ -128,7 +137,7 @@ export default function GridView({items, operation, onItemSelect, onItemEnter}) onItemEnter={onItemEnter} onEditComplete={operation.idx==i ? onEditComplete : null} />) )} - {items.length == 0 && No files/folders found} + {items.length == 0 && {gettext('No files/folders found')}} ); } diff --git a/web/pgadmin/misc/file_manager/static/js/components/ListView.jsx b/web/pgadmin/misc/file_manager/static/js/components/ListView.jsx index f8879be9a..55050c3d5 100644 --- a/web/pgadmin/misc/file_manager/static/js/components/ListView.jsx +++ b/web/pgadmin/misc/file_manager/static/js/components/ListView.jsx @@ -1,14 +1,20 @@ -import { Box, makeStyles } from '@material-ui/core'; -import React, { useContext, useRef, useEffect } from 'react'; -import { Row } from 'react-data-grid'; +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// +import { makeStyles } from '@material-ui/core'; +import React, { useRef, useEffect } from 'react'; import PgReactDataGrid from '../../../../../static/js/components/PgReactDataGrid'; import FolderIcon from '@material-ui/icons/Folder'; import StorageRoundedIcon from '@material-ui/icons/StorageRounded'; import DescriptionIcon from '@material-ui/icons/Description'; import LockRoundedIcon from '@material-ui/icons/LockRounded'; -import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; -import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; import PropTypes from 'prop-types'; +import gettext from 'sources/gettext'; const useStyles = makeStyles((theme)=>({ grid: { @@ -95,56 +101,6 @@ FileNameEditor.propTypes = { onRowChange: PropTypes.func, onClose: PropTypes.func, }; - -function CutomSortIcon({sortDirection}) { - if(sortDirection == 'DESC') { - return ; - } else if(sortDirection == 'ASC') { - return ; - } - return <>; -} -CutomSortIcon.propTypes = { - sortDirection: PropTypes.string, -}; - -export function CustomRow({inTest=false, ...props}) { - const gridUtils = useContext(GridContextUtils); - const handleKeyDown = (e)=>{ - if(e.code == 'Tab' || e.code == 'ArrowRight' || e.code == 'ArrowLeft') { - e.stopPropagation(); - } - if(e.code == 'Enter') { - gridUtils.onItemEnter(props.row); - } - }; - const isRowSelected = props.selectedCellIdx >= 0; - useEffect(()=>{ - if(isRowSelected) { - gridUtils.onItemSelect(props.rowIdx); - } - }, [props.selectedCellIdx]); - if(inTest) { - return
; - } - const onRowClick = (...args)=>{ - gridUtils.onItemClick?.(props.rowIdx); - props.onRowClick?.(...args); - }; - return ( - gridUtils.onItemEnter(row)} - selectCell={(row, column)=>props.selectCell(row, column)} aria-selected={isRowSelected}/> - ); -} -CustomRow.propTypes = { - inTest: PropTypes.bool, - row: PropTypes.object, - selectedCellIdx: PropTypes.number, - onRowClick: PropTypes.func, - rowIdx: PropTypes.number, - selectCell: PropTypes.func, -}; - function FileNameFormatter({row}) { const classes = useStyles(); let icon = ; @@ -166,7 +122,7 @@ FileNameFormatter.propTypes = { const columns = [ { key: 'Filename', - name: 'Name', + name: gettext('Name'), formatter: FileNameFormatter, editor: FileNameEditor, editorOptions: { @@ -175,17 +131,17 @@ const columns = [ } },{ key: 'Properties.DateModified', - name: 'Date Modified', + name: gettext('Date Modified'), formatter: ({row})=><>{row.Properties?.['Date Modified']} },{ key: 'Properties.Size', - name: 'Size', + name: gettext('Size'), formatter: ({row})=><>{row.file_type != 'dir' && row.Properties?.['Size']} } ]; -export default function ListView({items, operation, onItemSelect, onItemEnter, onItemClick, ...props}) { +export default function ListView({items, operation, ...props}) { const classes = useStyles(); const gridRef = useRef(); @@ -201,33 +157,27 @@ export default function ListView({items, operation, onItemSelect, onItemEnter, o }, [gridRef.current?.element]); return ( - - No files/folders found, - }} - onRowsChange={(rows)=>{ - operation?.onComplete?.(rows[operation.idx], operation.idx); - }} - {...props} - /> - + { + operation?.onComplete?.(rows[operation.idx], operation.idx); + }} + {...props} + /> ); } ListView.propTypes = { diff --git a/web/pgadmin/misc/file_manager/static/js/components/Uploader.jsx b/web/pgadmin/misc/file_manager/static/js/components/Uploader.jsx index f03223424..7bfa84415 100644 --- a/web/pgadmin/misc/file_manager/static/js/components/Uploader.jsx +++ b/web/pgadmin/misc/file_manager/static/js/components/Uploader.jsx @@ -1,3 +1,11 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// import React, { useCallback, useReducer, useEffect, useMemo } from 'react'; import { Box, List, ListItem, makeStyles } from '@material-ui/core'; import CloseIcon from '@material-ui/icons/CloseRounded'; diff --git a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx index 1e5c7210d..e7aa57907 100644 --- a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx +++ b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx @@ -105,7 +105,7 @@ const useStyles = makeStyles((theme) => marginLeft: '0.5em' }, footer: { - borderTop: '1px solid #dde0e6 !important', + borderTop: `1px solid ${theme.otherVars.inputBorderColor} !important`, padding: '0.5rem', display: 'flex', width: '100%', diff --git a/web/pgadmin/static/js/Theme/dark.js b/web/pgadmin/static/js/Theme/dark.js index 36ccb69ca..506434ad6 100644 --- a/web/pgadmin/static/js/Theme/dark.js +++ b/web/pgadmin/static/js/Theme/dark.js @@ -100,6 +100,7 @@ export default function(basicSettings) { cardHeaderBg: '#424242', colorFg: '#FFFFFF', emptySpaceBg: '#212121', + textMuted: '#8A8A8A' } }); } diff --git a/web/pgadmin/static/js/Theme/high_contrast.js b/web/pgadmin/static/js/Theme/high_contrast.js index 338f1ccb6..4fbd6c0ca 100644 --- a/web/pgadmin/static/js/Theme/high_contrast.js +++ b/web/pgadmin/static/js/Theme/high_contrast.js @@ -98,6 +98,7 @@ export default function(basicSettings) { cardHeaderBg: '#062F57', colorFg: '#FFFFFF', emptySpaceBg: '#010B15', + textMuted: '#8b9cad' } }); } diff --git a/web/pgadmin/static/js/Theme/standard.js b/web/pgadmin/static/js/Theme/standard.js index 8b8b1fe41..ddceb8b80 100644 --- a/web/pgadmin/static/js/Theme/standard.js +++ b/web/pgadmin/static/js/Theme/standard.js @@ -105,6 +105,7 @@ export default function(basicSettings) { qtDatagridSelectFg: '#222', cardHeaderBg: '#fff', emptySpaceBg: '#ebeef3', + textMuted: '#646B82', explain: { sev2: { color: '#222222', diff --git a/web/pgadmin/static/js/components/PgReactDataGrid.jsx b/web/pgadmin/static/js/components/PgReactDataGrid.jsx index 210d8c980..e37e5d365 100644 --- a/web/pgadmin/static/js/components/PgReactDataGrid.jsx +++ b/web/pgadmin/static/js/components/PgReactDataGrid.jsx @@ -1,9 +1,20 @@ -import React from 'react'; -import ReactDataGrid from 'react-data-grid'; -import { makeStyles } from '@material-ui/core'; +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// +import React, { useContext, useEffect } from 'react'; +import ReactDataGrid, { Row } from 'react-data-grid'; +import { Box, makeStyles } from '@material-ui/core'; import clsx from 'clsx'; import PropTypes from 'prop-types'; import CustomPropTypes from '../custom_prop_types'; +import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; +import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; +import gettext from 'sources/gettext'; const useStyles = makeStyles((theme)=>({ root: { @@ -30,7 +41,6 @@ const useStyles = makeStyles((theme)=>({ }, '& .rdg-header-row': { backgroundColor: theme.palette.background.default, - fontWeight: 'normal', }, '& .rdg-row': { backgroundColor: theme.palette.background.default, @@ -66,18 +76,78 @@ const useStyles = makeStyles((theme)=>({ } })); +export const GridContextUtils = React.createContext(); -export default function PgReactDataGrid({gridRef, className, hasSelectColumn=true, ...props}) { +function CutomSortIcon({sortDirection}) { + if(sortDirection == 'DESC') { + return ; + } else if(sortDirection == 'ASC') { + return ; + } + return <>; +} +CutomSortIcon.propTypes = { + sortDirection: PropTypes.string, +}; + +export function CustomRow({inTest=false, ...props}) { + const gridUtils = useContext(GridContextUtils); + const handleKeyDown = (e)=>{ + if(e.code == 'Tab' || e.code == 'ArrowRight' || e.code == 'ArrowLeft') { + e.stopPropagation(); + } + if(e.code == 'Enter') { + gridUtils.onItemEnter?.(props.row); + } + }; + const isRowSelected = props.selectedCellIdx >= 0; + useEffect(()=>{ + if(isRowSelected) { + gridUtils.onItemSelect?.(props.rowIdx); + } + }, [props.selectedCellIdx]); + if(inTest) { + return
; + } + const onRowClick = (...args)=>{ + gridUtils.onItemClick?.(props.rowIdx); + props.onRowClick?.(...args); + }; + return ( + gridUtils.onItemEnter?.(row)} + selectCell={(row, column)=>props.selectCell(row, column)} aria-selected={isRowSelected}/> + ); +} +CustomRow.propTypes = { + inTest: PropTypes.bool, + row: PropTypes.object, + selectedCellIdx: PropTypes.number, + onRowClick: PropTypes.func, + rowIdx: PropTypes.number, + selectCell: PropTypes.func, +}; + +export default function PgReactDataGrid({gridRef, className, hasSelectColumn=true, onItemEnter, onItemSelect, + onItemClick, noRowsText, ...props}) { const classes = useStyles(); let finalClassName = [classes.root]; hasSelectColumn && finalClassName.push(classes.hasSelectColumn); props.enableCellSelect && finalClassName.push(classes.cellSelection); finalClassName.push(className); - return ; + return ( + + {noRowsText || gettext('No rows found.')}, + }} + {...props} + /> + + ); } PgReactDataGrid.propTypes = { @@ -85,4 +155,8 @@ PgReactDataGrid.propTypes = { className: CustomPropTypes.className, hasSelectColumn: PropTypes.bool, enableCellSelect: PropTypes.bool, + onItemEnter: PropTypes.func, + onItemSelect: PropTypes.func, + onItemClick: PropTypes.func, + noRowsText: PropTypes.string }; diff --git a/web/pgadmin/static/js/helpers/wizard/Wizard.jsx b/web/pgadmin/static/js/helpers/wizard/Wizard.jsx index a4e5bd816..c349d6edd 100644 --- a/web/pgadmin/static/js/helpers/wizard/Wizard.jsx +++ b/web/pgadmin/static/js/helpers/wizard/Wizard.jsx @@ -99,7 +99,7 @@ const useStyles = makeStyles((theme) => flexWrap: 'wrap', }, wizardFooter: { - borderTop: '1px solid #dde0e6 !important', + borderTop: `1px solid ${theme.otherVars.inputBorderColor} !important`, padding: '0.5rem', display: 'flex', width: '100%', diff --git a/web/pgadmin/tools/debugger/static/js/components/DebuggerArgumentComponent.jsx b/web/pgadmin/tools/debugger/static/js/components/DebuggerArgumentComponent.jsx index 5f9f965c5..ab5b32925 100644 --- a/web/pgadmin/tools/debugger/static/js/components/DebuggerArgumentComponent.jsx +++ b/web/pgadmin/tools/debugger/static/js/components/DebuggerArgumentComponent.jsx @@ -59,7 +59,7 @@ const useStyles = makeStyles((theme) => fontSize: '1.12rem !important', }, footer: { - borderTop: '1px solid #dde0e6 !important', + borderTop: `1px solid ${theme.otherVars.inputBorderColor} !important`, padding: '0.5rem', display: 'flex', width: '100%', diff --git a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js index 8b2e00fdb..5a1157da9 100644 --- a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js +++ b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js @@ -93,6 +93,7 @@ define([ { + ReactDOM.unmountComponentAtNode(j[0]); panel.close(); }}/> , j[0]); diff --git a/web/pgadmin/tools/import_export_servers/static/js/import_export_servers.js b/web/pgadmin/tools/import_export_servers/static/js/import_export_servers.js index 587e744a0..a239d5989 100644 --- a/web/pgadmin/tools/import_export_servers/static/js/import_export_servers.js +++ b/web/pgadmin/tools/import_export_servers/static/js/import_export_servers.js @@ -56,6 +56,7 @@ export default class ImportExportServersModule { { + ReactDOM.unmountComponentAtNode(j[0]); panel.close(); }}/> , j[0]); diff --git a/web/pgadmin/tools/search_objects/static/js/SearchObjects.jsx b/web/pgadmin/tools/search_objects/static/js/SearchObjects.jsx new file mode 100644 index 000000000..fe83dd135 --- /dev/null +++ b/web/pgadmin/tools/search_objects/static/js/SearchObjects.jsx @@ -0,0 +1,426 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// +import { Box, makeStyles } from '@material-ui/core'; +import React, { useState, useMemo, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import HelpIcon from '@material-ui/icons/HelpRounded'; +import SearchRoundedIcon from '@material-ui/icons/SearchRounded'; +import pgAdmin from 'sources/pgadmin'; +import gettext from 'sources/gettext'; +import url_for from 'sources/url_for'; +import Loader from 'sources/components/Loader'; +import clsx from 'clsx'; +import Notify from '../../../../static/js/helpers/Notifier'; +import getApiInstance, { parseApiError } from '../../../../static/js/api_instance'; +import { PrimaryButton, PgIconButton } from '../../../../static/js/components/Buttons'; +import { useModalStyles } from '../../../../static/js/helpers/ModalProvider'; +import { FormFooterMessage, InputSelect, InputText, MESSAGE_TYPE } from '../../../../static/js/components/FormComponents'; +import PgReactDataGrid from '../../../../static/js/components/PgReactDataGrid'; + +const pgBrowser = pgAdmin.Browser; + +const useStyles = makeStyles((theme)=>({ + grid: { + fontSize: '13px', + '& .rdg-header-row': { + '& .rdg-cell': { + padding: '0px 4px', + } + }, + '& .rdg-cell': { + padding: '0px 4px', + '&[aria-colindex="1"]': { + padding: '0px 4px', + '&.rdg-editor-container': { + padding: '0px', + }, + } + } + }, + toolbar: { + padding: '4px', + display: 'flex', + ...theme.mixins.panelBorder?.bottom, + }, + inputSearch: { + lineHeight: 1, + }, + footer1: { + justifyContent: 'space-between', + padding: '4px 8px', + display: 'flex', + alignItems: 'center', + borderTop: `1px solid ${theme.otherVars.inputBorderColor}`, + }, + footer: { + borderTop: `1px solid ${theme.otherVars.inputBorderColor} !important`, + padding: '0.5rem', + display: 'flex', + width: '100%', + background: theme.otherVars.headerBg, + }, + gridCell: { + display: 'inline-block', + height: '1.3rem', + width: '1.3rem', + }, + funcArgs: { + cursor: 'pointer', + }, + cellMuted: { + color: `${theme.otherVars.textMuted} !important`, + cursor: 'default !important', + }, +})); + + + +const columns = [ + { + key: 'name', + name: gettext('Object name'), + width: 250, + formatter({row}) { + const classes = useStyles(); + return ( +
+ + + {row.name} + {row.other_info != null && row.other_info != '' && <> + {row.showArgs = true;}}> {row?.showArgs ? `(${row.other_info})` : '(...)'} + } + +
+ ); + } + },{ + key: 'type', + name: gettext('Type'), + width: 30, + formatter({row}) { + const classes = useStyles(); + return ( + {row.type_label} + ); + } + },{ + key: 'path', + name: gettext('Browser path'), + sortable: false, + formatter({row}) { + const classes = useStyles(); + return ( + {row.path} + ); + } + } +]; + +/* This function is used to get the final data with the proper icon + * based on the type and translated path. + */ +const finaliseData = (nodeData, datum)=> { + datum.icon = 'icon-' + datum.type; + /* finalise path */ + [datum.path, datum.id_path] = translateSearchObjectsPath(nodeData, datum.path, datum.catalog_level); + /* id is required by slickgrid dataview */ + datum.id = datum.id_path ? datum.id_path.join('.') : _.uniqueId(datum.name); + + datum.other_info = datum.other_info ? _.escape(datum.other_info) : datum.other_info; + + return datum; +}; + +const getCollNode = (node_type)=> { + if('coll-'+node_type in pgBrowser.Nodes) { + return pgBrowser.Nodes['coll-'+node_type]; + } else if(node_type in pgBrowser.Nodes && + typeof(pgBrowser.Nodes[node_type].collection_type) === 'string') { + return pgBrowser.Nodes[pgBrowser.Nodes[node_type].collection_type]; + } + + return null; +}; + +/* This function will translate the path given by search objects API into two parts + * 1. The display path on the UI + * 2. The tree search path to locate the object on the tree. + * + * Sample path returned by search objects API + * :schema.11:/pg_catalog/:table.2604:/pg_attrdef + * + * Sample path required by tree locator + * Normal object - server_group/1.server/3.coll-database/3.database/13258.coll-schema/13258.schema/2200.coll-table/2200.table/41773 + * pg_catalog schema - server_group/1.server/3.coll-database/3.database/13258.coll-catalog/13258.catalog/11.coll-table/11.table/2600 + * Information Schema, dbo, sys: + * server_group/1.server/3.coll-database/3.database/13258.coll-catalog/13258.catalog/12967.coll-catalog_object/12967.catalog_object/13204 + * server_group/1.server/11.coll-database/11.database/13258.coll-catalog/13258.catalog/12967.coll-catalog_object/12967.catalog_object/12997.coll-catalog_object_column/12997.catalog_object_column/13 + * + * Column catalog_level has values as + * N - Not a catalog schema + * D - Catalog schema with DB support - pg_catalog + * O - Catalog schema with object support only - info schema, dbo, sys + */ +const translateSearchObjectsPath = (nodeData, path, catalog_level)=> { + if (path === null) { + return [null, null]; + } + + catalog_level = catalog_level || 'N'; + + /* path required by tree locator */ + /* the path received from the backend is after the DB node, initial path setup */ + let id_path = [ + nodeData?.server_group?.id, + nodeData?.server?.id, + getCollNode('database').type + '_' + nodeData?.server?._id, + nodeData?.database?.id, + ]; + + let prev_node_id = nodeData?.database?._id; + + /* add the slash to match regex, remove it from display path later */ + path = '/' + path; + /* the below regex will match all /:schema.2200:/ */ + let new_path = path.replace(/\/:[a-zA-Z_]+\.[0-9]+:\//g, (token)=>{ + let orig_token = token; + /* remove the slash and colon */ + token = token.slice(2, -2); + let [node_type, node_oid, others] = token.split('.'); + if(typeof(others) !== 'undefined') { + return token; + } + + /* schema type is "catalog" for catalog schemas */ + node_type = (['D', 'O'].indexOf(catalog_level) != -1 && node_type == 'schema') ? 'catalog' : node_type; + + /* catalog like info schema will only have views and tables AKA catalog_object except for pg_catalog */ + node_type = (catalog_level === 'O' && ['view', 'table'].indexOf(node_type) != -1) ? 'catalog_object' : node_type; + + /* catalog_object will have column node as catalog_object_column */ + node_type = (catalog_level === 'O' && node_type == 'column') ? 'catalog_object_column' : node_type; + + /* If collection node present then add it */ + let coll_node = getCollNode(node_type); + if(coll_node) { + /* Add coll node to the path */ + if(prev_node_id != null) id_path.push(`${coll_node.type}_${prev_node_id}`); + + /* Add the node to the path */ + id_path.push(`${node_type}_${node_oid}`); + + /* This will be needed for coll node */ + prev_node_id = node_oid; + + /* This will be displayed in the grid */ + return `/${coll_node.label}/`; + } else if(node_type in pgBrowser.Nodes) { + /* Add the node to the path */ + id_path.push(`${node_type}_${node_oid}`); + + /* This will be need for coll node id path */ + prev_node_id = node_oid; + + /* Remove the token and replace with slash. This will be displayed in the grid */ + return '/'; + } + prev_node_id = null; + return orig_token; + }); + + /* Remove the slash we had added */ + new_path = new_path.substring(1); + + return [new_path, id_path]; +}; + +// This function is used to sort the column. +function getComparator(sortColumn) { + const key = sortColumn?.columnKey; + const dir = sortColumn?.direction == 'ASC' ? 1 : -1; + + if (!key) return ()=>0; + + return (a, b) => { + return dir*(a[key].localeCompare(b[key])); + }; +} +export default function SearchObjects({nodeData}) { + const classes = useStyles(); + const modalClasses = useModalStyles(); + const [type, setType] = React.useState('all'); + const [loaderText, setLoaderText] = useState(''); + const [search, setSearch] = useState(''); + const [footerText, setFooterText] = useState('0 matches found.'); + const [searchData, setSearchData] = useState([]); + const [sortColumns, setSortColumns] = useState([]); + const [errorMsg, setErrorMsg] = useState(''); + const api = getApiInstance(); + + const onDialogHelp = ()=> { + window.open(url_for('help.static', { 'filename': 'search_objects.html' }), 'pgadmin_help'); + }; + + const sortedItems = useMemo(()=>( + [...searchData].sort(getComparator(sortColumns[0])) + ), [searchData, sortColumns]); + + const onItemEnter = useCallback((rowData)=>{ + let tree = pgBrowser.tree; + setErrorMsg(''); + + if(!rowData.show_node) { + setErrorMsg( + gettext('%s objects are disabled in the browser. You can enable them in the preferences dialog.', rowData.type_label)); + + setTimeout(()=> { + document.getElementById('prefdlgid').addEventListener('click', ()=>{ + if(pgAdmin.Preferences) { + pgAdmin.Preferences.show(); + } + }); + }, 100); + + return false; + } + setLoaderText(gettext('Locating...')); + tree.findNodeWithToggle(rowData.id_path) + .then((treeItem)=>{ + setTimeout(() => { + tree.select(treeItem, true, 'center'); + }, 100); + setLoaderText(null); + }) + .catch(()=>{ + setLoaderText(null); + setErrorMsg(gettext('Unable to locate this object in the browser.')); + }); + }, []); + + const onSearch = ()=> { + // If user press the Enter key and the search characters are + // less than 3 characters then return from the function. + if (search.length < 3) + return; + setLoaderText(gettext('Searching....')); + setErrorMsg(''); + + let searchType = type; + if(type === 'constraints') { + searchType = ['constraints', 'check_constraint', 'foreign_key', 'primary_key', 'unique_constraint', 'exclusion_constraint']; + } + + api.get(url_for('search_objects.search',{ + sid: nodeData?.server?._id, + did: nodeData?.database?._id, + }), { params: { + text: search, + type: searchType, + }}) + .then(res=>{ + setLoaderText(null); + let finalData = []; + // Get the finalise list of data. + res?.data?.data.forEach((element) => { + finalData.push(finaliseData(nodeData, element)); + }); + setSearchData(finalData); + setFooterText(res?.data?.data?.length + ' matches found'); + }) + .catch((err)=>{ + setLoaderText(null); + Notify.error(parseApiError(err)); + }); + }; + + const onEnterPress = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + onSearch(); + } + }; + + const typeOptions = ()=> { + return new Promise((resolve, reject)=>{ + try { + api.get(url_for('search_objects.types', { + sid: nodeData?.server?._id, + did: nodeData?.database?._id, + })) + .then(res=>{ + let typeOpt = [{label:gettext('All types'), value:'all'}]; + let typesRes = Object.entries(res.data.data).sort(); + typesRes.forEach((element) => { + typeOpt.push({label:gettext(element[1]), value:element[0]}); + }); + + resolve(typeOpt); + }) + .catch((err)=>{ + Notify.error(parseApiError(err)); + reject(err); + }); + } catch (error) { + Notify.error(parseApiError(error)); + reject(error); + } + }); + }; + + return ( + + + + + + + setType(v)}/> + + } + onClick={onSearch} disabled={search.length >= 3 ? false : true}>{gettext('Search')} + + + + + + {footerText} + + setErrorMsg('')} /> + + + + } title={gettext('Help for this dialog.')} /> + + + + ); +} + +SearchObjects.propTypes = { + onClose: PropTypes.func, + nodeData: PropTypes.object, +}; \ No newline at end of file diff --git a/web/pgadmin/tools/search_objects/static/js/index.js b/web/pgadmin/tools/search_objects/static/js/index.js new file mode 100644 index 000000000..d971b4d8f --- /dev/null +++ b/web/pgadmin/tools/search_objects/static/js/index.js @@ -0,0 +1,109 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// +import pgAdmin from 'sources/pgadmin'; +import pgBrowser from 'top/browser/static/js/browser'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import gettext from 'sources/gettext'; +import Theme from 'sources/Theme'; +import * as toolBar from 'pgadmin.browser.toolbar'; +import SearchObjects from './SearchObjects'; +import {getPanelTitle} from '../../../sqleditor/static/js/sqleditor_title'; + +/* eslint-disable */ +/* This is used to change publicPath of webpack at runtime for loading chunks */ +/* Do not add let, var, const to this variable */ +__webpack_public_path__ = window.resourceBasePath; +/* eslint-enable */ + +export default class SearchObjectModule { + static instance; + + static getInstance(...args) { + if(!SearchObjectModule.instance) { + SearchObjectModule.instance = new SearchObjectModule(...args); + } + return SearchObjectModule.instance; + } + + init() { + if(this.initialized) + return; + this.initialized = true; + + // Define the nodes on which the menus to be appear + var menus = [{ + name: 'search_objects', + module: this, + applies: ['tools'], + callback: 'show_search_objects', + enable: this.search_objects_enabled, + priority: 3, + label: gettext('Search Objects...'), + below: true, + data: { + data_disabled: gettext('Please select a database from the browser tree to search the database objects.'), + }, + }]; + + pgBrowser.add_menus(menus); + } + + search_objects_enabled(obj) { + var isEnabled = (() => { + if (!_.isUndefined(obj) && !_.isNull(obj)) { + if (_.indexOf(pgAdmin.unsupported_nodes, obj._type) == -1) { + if (obj._type == 'database' && obj.allowConn) { + return true; + } else if (obj._type != 'database') { + return true; + } else { + return false; + } + } else { + return false; + } + } else { + return false; + } + })(); + + toolBar.enable(gettext('Search objects'), isEnabled); + return isEnabled; + } + + show_search_objects(action, treeItem) { + let dialogTitle = getPanelTitle(pgBrowser, treeItem); + dialogTitle = gettext('Search Objects - ') + dialogTitle; + + let nodeData = pgBrowser.tree.getTreeNodeHierarchy(treeItem); + + pgBrowser.Node.registerUtilityPanel(); + var panel = pgBrowser.Node.addUtilityPanel(pgBrowser.stdW.md, pgBrowser.stdH.lg), + j = panel.$container.find('.obj_properties').first(); + + panel.title(dialogTitle); + panel.focus(); + + ReactDOM.render( + + + , j[0]); + } +} + +if(!pgAdmin.Tools) { + pgAdmin.Tools = {}; +} + +pgAdmin.Tools.SearchObjects = SearchObjectModule.getInstance(); + +module.exports = { + SearchObjects: pgAdmin.Tools.SearchObjects, +}; \ No newline at end of file diff --git a/web/pgadmin/tools/search_objects/static/js/search_objects.js b/web/pgadmin/tools/search_objects/static/js/search_objects.js deleted file mode 100644 index db3e9f4d0..000000000 --- a/web/pgadmin/tools/search_objects/static/js/search_objects.js +++ /dev/null @@ -1,94 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -define([ - 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'pgadmin.alertifyjs', - 'sources/pgadmin', 'sources/csrf', 'pgadmin.browser.toolbar', - 'pgadmin.search_objects/search_objects_dialog', -], function( - gettext, url_for, $, _, alertify, pgAdmin, csrfToken, toolBar, SearchObjectsDialog -) { - - var pgBrowser = pgAdmin.Browser; - if (pgAdmin.SearchObjects) - return pgAdmin.SearchObjects; - - pgAdmin.SearchObjects = { - init: function() { - if (this.initialized) - return; - - this.initialized = true; - csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token); - - // Define the nodes on which the menus to be appear - var menus = [{ - name: 'search_objects', - module: this, - applies: ['tools'], - callback: 'show_search_objects', - enable: this.search_objects_enabled, - priority: 3, - label: gettext('Search Objects...'), - below: true, - data: { - data_disabled: gettext('Please select a database from the browser tree to search the database objects.'), - }, - }, { - name: 'search_objects', - module: this, - applies: ['context'], - callback: 'show_search_objects', - enable: this.search_objects_enabled, - priority: 1, - label: gettext('Search Objects...'), - }]; - - pgBrowser.add_menus(menus); - return this; - }, - - search_objects_enabled: function(obj) { - /* Same as query tool */ - var isEnabled = (() => { - if (!_.isUndefined(obj) && !_.isNull(obj)) { - if (_.indexOf(pgAdmin.unsupported_nodes, obj._type) == -1) { - if (obj._type == 'database' && obj.allowConn) { - return true; - } else if (obj._type != 'database') { - return true; - } else { - return false; - } - } else { - return false; - } - } else { - return false; - } - })(); - - toolBar.enable(gettext('Search objects'), isEnabled); - return isEnabled; - }, - - // Callback to show the dialog - show_search_objects: function(action, item) { - let dialog = new SearchObjectsDialog.default( - pgBrowser, - $, - alertify, - {}, - ); - dialog.draw(action, item, {}, pgBrowser.stdW.calc(pgBrowser.stdW.md), pgBrowser.stdH.calc(pgBrowser.stdH.lg)); - }, - }; - - return pgAdmin.SearchObjects; -}); diff --git a/web/pgadmin/tools/search_objects/static/js/search_objects_dialog.js b/web/pgadmin/tools/search_objects/static/js/search_objects_dialog.js deleted file mode 100644 index 7080c0bc7..000000000 --- a/web/pgadmin/tools/search_objects/static/js/search_objects_dialog.js +++ /dev/null @@ -1,41 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -import gettext from 'sources/gettext'; -import {Dialog} from 'sources/alertify/dialog'; -import {getPanelTitle} from 'tools/sqleditor/static/js/sqleditor_title'; -import {retrieveAncestorOfTypeDatabase} from 'sources/tree/tree_utils'; - -export default class SearchObjectsDialog extends Dialog { - constructor(pgBrowser, $, alertify, BackupModel, backform = null) { - super(gettext('Search Objects Error'), - '
', - pgBrowser, $, alertify, BackupModel, backform - ); - } - - dialogName() { - return 'search_objects'; - } - - draw(action, treeItem, params, width=0, height=0) { - let dbInfo = retrieveAncestorOfTypeDatabase(this.pgBrowser, treeItem, gettext('Search Objects Error'), this.alertify); - if (!dbInfo) { - return; - } - - let dialogTitle = getPanelTitle(this.pgBrowser, treeItem); - dialogTitle = gettext('Search Objects - ') + dialogTitle; - const dialog = this.createOrGetDialog( - gettext('Search Objects...'), - 'search_objects' - ); - dialog(dialogTitle).resizeTo(width, height); - } -} diff --git a/web/pgadmin/tools/search_objects/static/js/search_objects_dialog_wrapper.js b/web/pgadmin/tools/search_objects/static/js/search_objects_dialog_wrapper.js deleted file mode 100644 index 632f9980b..000000000 --- a/web/pgadmin/tools/search_objects/static/js/search_objects_dialog_wrapper.js +++ /dev/null @@ -1,684 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -import axios from 'axios/index'; -import gettext from 'sources/gettext'; -import url_for from 'sources/url_for'; -import 'select2'; -import {DialogWrapper} from 'sources/alertify/dialog_wrapper'; -import Slick from 'sources/../bundle/slickgrid'; -import pgAdmin from 'sources/pgadmin'; -import _ from 'underscore'; - - -export default class SearchObjectsDialogWrapper extends DialogWrapper { - constructor(dialogContainerSelector, dialogTitle, typeOfDialog, - jquery, pgBrowser, alertify, dialogModel, backform) { - super(dialogContainerSelector, dialogTitle, jquery, - pgBrowser, alertify, dialogModel, backform); - - this.grid = null; - this.dataview = null; - this.gridContainer = null; - } - - showMessage(text, is_error, call_after_show=()=>{/*This is intentional (SonarQube)*/}) { - if(text == '' || text == null) { - this.statusBar.classList.add('d-none'); - } else { - if(is_error) { - this.statusBar.innerHTML = ` - - `; - - this.statusBar.querySelector('.close-error').addEventListener('click', ()=>{ - this.showMessage(null); - }); - } else { - this.statusBar.innerHTML = ` - - `; - } - this.statusBar.classList.remove('d-none'); - call_after_show(this.statusBar); - } - } - - createDialogDOM(dialogContainer) { - dialogContainer.innerHTML = ` -
-
-
-
-
- -
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `; - - return dialogContainer; - } - - updateDimOfSearchResult() { - let dim = this.searchResultContainer.getBoundingClientRect(); - this.searchResult.style.height = dim.height + 'px'; - this.searchResult.style.width = dim.width + 'px'; - } - - setLoading(text) { - if(text != null) { - this.loader.classList.remove('d-none'); - this.loader.querySelector('.pg-sp-text').innerHTML = text; - } else { - this.loader.classList.add('d-none'); - } - } - - searchBtnEnabled(enabled) { - if(typeof(enabled) != 'undefined') { - this.searchBtn.disabled = !enabled; - } else { - return !this.searchBtn.disabled; - } - } - - searchBoxVal(val) { - if(typeof(val) != 'undefined') { - this.searchBox.value = val; - } else { - return this.searchBox.value.trim(); - } - } - - typesVal(val) { - if(typeof(val) != 'undefined') { - this.typesSelect.value = val; - } else { - return this.typesSelect.value; - } - } - - setTypes(data, enabled=true) { - if(this.typesSelect) { - this.jquery(this.typesSelect).empty().select2({ - data: data, - }); - - this.typesSelect.disabled = !enabled; - } - } - - setResultCount(count) { - if(count != 0 && !count) { - count = gettext('Unknown'); - } - this.searchResultCount.innerHTML = (count===1 ? gettext('%s match found.', count): gettext('%s matches found.', count)); - } - - showOtherInfo(rowno) { - let rowData = this.dataview.getItem(rowno); - rowData.name += ` (${rowData.other_info})`; - rowData.other_info = null; - this.dataview.updateItem(rowData.id, rowData); - } - - setGridData(data) { - this.dataview.setItems(data); - } - - prepareGrid() { - this.dataview = new Slick.Data.DataView(); - - this.dataview.getItemMetadata = (row)=>{ - let rowData = this.dataview.getItem(row); - if(!rowData.show_node){ - return { - cssClasses: 'object-muted', - }; - } - return null; - }; - - this.dataview.setFilter((item, args)=>{ - if(args && args.type != 'all') { - if(Array.isArray(args.type)) { - return (args.type.indexOf(item.type) != -1); - } else { - return args.type == item.type; - } - } - return true; - }); - - /* jquery required for select2 */ - this.jquery(this.typesSelect).on('change', ()=>{ - let type = this.typesVal(); - if(type === 'constraints') { - type = ['constraints', 'check_constraint', 'foreign_key', 'primary_key', 'unique_constraint', 'exclusion_constraint']; - } - this.dataview.setFilterArgs({ type: type }); - this.dataview.refresh(); - }); - - this.dataview.onRowCountChanged.subscribe((e, args) => { - this.grid.updateRowCount(); - this.grid.render(); - this.setResultCount(args.current); - }); - - this.dataview.onRowsChanged.subscribe((e, args) => { - this.grid.invalidateRows(args.rows); - this.grid.render(); - }); - - this.grid = new Slick.Grid( - this.searchResult, - this.dataview, - [ - { id: 'name', name: gettext('Object name'), field: 'name', sortable: true, width: 50, - formatter: (row, cell, value, columnDef, dataContext) => { - let ret_el = `${value}`; - - if(dataContext.other_info != null && dataContext.other_info != '') { - ret_el += ' (...)'; - } - - return ret_el; - }, - }, - { id: 'type', name: gettext('Type'), field: 'type_label', sortable: true, width: 35 }, - { id: 'path', name: gettext('Browser path'), field: 'path', sortable: false, formatter: (row, cell, value) => value }, - ], - { - enableCellNavigation: true, - enableColumnReorder: false, - multiColumnSort: true, - explicitInitialization: true, - } - ); - - this.grid.registerPlugin(new Slick.AutoColumnSize()); - - this.grid.setSelectionModel(new Slick.RowSelectionModel({selectActiveRow: true})); - - this.grid.onKeyDown.subscribe((event) => { - let activeRow = this.grid.getActiveCell(); - if(activeRow && !event.ctrlKey && !event.altKey && !event.metaKey && event.keyCode == 9) { - event.preventDefault(); - event.stopImmediatePropagation(); - - if(event.shiftKey) { - this.prevToGrid.focus(); - } else { - this.nextToGrid.focus(); - } - } - }); - - this.grid.onClick.subscribe((event, args) => { - if(event.target.classList.contains('object-other-info')) { - this.showOtherInfo(args.row); - } - }); - - this.grid.onDblClick.subscribe((event, args) => { - let rowData = this.dataview.getItem(args.row); - let tree = this.pgBrowser.tree; - - if(!rowData.show_node) { - this.showMessage( - gettext('%s objects are disabled in the browser. You can enable them in the preferences dialog.', rowData.type_label), - true, - (statusBar)=>{ - statusBar.querySelector('.pref-dialog-link').addEventListener('click', ()=>{ - if(pgAdmin.Preferences) { - pgAdmin.Preferences.show(); - } - }); - } - ); - return false; - } - this.showMessage(gettext('Locating...')); - tree.findNodeWithToggle(rowData.id_path) - .then((treeItem)=>{ - setTimeout(() => { - tree.select(treeItem, true, 'center'); - }, 100); - this.showMessage(null); - }) - .catch((error)=>{ - this.showMessage(gettext('Unable to locate this object in the browser.'), true); - console.warn(error, rowData.id_path); - }); - }); - - this.grid.onSort.subscribe((event, args) => { - let cols = args.sortCols; - - this.dataview.sort(function (dataRow1, dataRow2) { - for (var i = 0, l = cols.length; i < l; i++) { - var field = cols[i].sortCol.field; - var sign = cols[i].sortAsc ? 1 : -1; - var value1 = dataRow1[field], value2 = dataRow2[field]; - var result = 0; - if (value1 != value2) { - result = (value1 > value2 ? 1 : -1) * sign; - } - if (result != 0) { - return result; - } - } - return false; - }, true); - }); - } - - onDialogResize() { - this.updateDimOfSearchResult(); - - if(this.grid) { - this.grid.resizeCanvas(); - this.grid.autosizeColumns(); - } - } - - onDialogShow() { - this.focusOnDialog(this); - - setTimeout(()=>{ - if(!this.grid) { - this.prepareGrid(); - } - this.updateDimOfSearchResult(); - this.grid.init(); - this.setGridData([]); - this.onDialogResize(); - }, 500); - } - - getBaseUrl(endpoint) { - return url_for('search_objects.'+endpoint, { - sid: this.treeInfo.server._id, - did: this.treeInfo.database._id, - }); - } - - getCollNode(node_type) { - if('coll-'+node_type in this.pgBrowser.Nodes) { - return this.pgBrowser.Nodes['coll-'+node_type]; - } else if(node_type in this.pgBrowser.Nodes && - typeof(this.pgBrowser.Nodes[node_type].collection_type) === 'string') { - return this.pgBrowser.Nodes[this.pgBrowser.Nodes[node_type].collection_type]; - } - - return null; - } - - getSelectedNode() { - const tree = this.pgBrowser.tree; - const selectedNode = tree.selected(); - if (selectedNode) { - return tree.findNodeByDomElement(selectedNode); - } else { - return undefined; - } - } - - finaliseData(datum) { - datum.icon = 'icon-' + datum.type; - /* finalise path */ - [datum.path, datum.id_path] = this.translateSearchObjectsPath(datum.path, datum.catalog_level); - /* id is required by slickgrid dataview */ - datum.id = datum.id_path ? datum.id_path.join('.') : _.uniqueId(datum.name); - - /* Esacpe XSS */ - datum.name = _.escape(datum.name); - datum.path = _.escape(datum.path); - datum.other_info = datum.other_info ? _.escape(datum.other_info) : datum.other_info; - - return datum; - } - - /* This function will translate the path given by search objects API into two parts - * 1. The display path on the UI - * 2. The tree search path to locate the object on the tree. - * - * Sample path returned by search objects API - * :schema.11:/pg_catalog/:table.2604:/pg_attrdef - * - * Sample path required by tree locator - * Normal object - server_group/1.server/3.coll-database/3.database/13258.coll-schema/13258.schema/2200.coll-table/2200.table/41773 - * pg_catalog schema - server_group/1.server/3.coll-database/3.database/13258.coll-catalog/13258.catalog/11.coll-table/11.table/2600 - * Information Schema, dbo, sys: - * server_group/1.server/3.coll-database/3.database/13258.coll-catalog/13258.catalog/12967.coll-catalog_object/12967.catalog_object/13204 - * server_group/1.server/11.coll-database/11.database/13258.coll-catalog/13258.catalog/12967.coll-catalog_object/12967.catalog_object/12997.coll-catalog_object_column/12997.catalog_object_column/13 - * - * Column catalog_level has values as - * N - Not a catalog schema - * D - Catalog schema with DB support - pg_catalog - * O - Catalog schema with object support only - info schema, dbo, sys - */ - translateSearchObjectsPath(path, catalog_level) { - if (path === null) { - return [null, null]; - } - - catalog_level = catalog_level || 'N'; - - /* path required by tree locator */ - /* the path received from the backend is after the DB node, initial path setup */ - let id_path = [ - this.treeInfo.server_group.id, - this.treeInfo.server.id, - this.getCollNode('database').type + '_' + this.treeInfo.server._id, - this.treeInfo.database.id, - ]; - - let prev_node_id = this.treeInfo.database._id; - - /* add the slash to match regex, remove it from display path later */ - path = '/' + path; - /* the below regex will match all /:schema.2200:/ */ - let new_path = path.replace(/\/:[a-zA-Z_]+\.[0-9]+:\//g, (token)=>{ - let orig_token = token; - /* remove the slash and colon */ - token = token.slice(2, -2); - let [node_type, node_oid, others] = token.split('.'); - if(typeof(others) !== 'undefined') { - return token; - } - - /* schema type is "catalog" for catalog schemas */ - node_type = (['D', 'O'].indexOf(catalog_level) != -1 && node_type == 'schema') ? 'catalog' : node_type; - - /* catalog like info schema will only have views and tables AKA catalog_object except for pg_catalog */ - node_type = (catalog_level === 'O' && ['view', 'table'].indexOf(node_type) != -1) ? 'catalog_object' : node_type; - - /* catalog_object will have column node as catalog_object_column */ - node_type = (catalog_level === 'O' && node_type == 'column') ? 'catalog_object_column' : node_type; - - /* If collection node present then add it */ - let coll_node = this.getCollNode(node_type); - if(coll_node) { - /* Add coll node to the path */ - if(prev_node_id != null) id_path.push(`${coll_node.type}_${prev_node_id}`); - - /* Add the node to the path */ - id_path.push(`${node_type}_${node_oid}`); - - /* This will be needed for coll node */ - prev_node_id = node_oid; - - /* This will be displayed in the grid */ - return `/${coll_node.label}/`; - } else if(node_type in this.pgBrowser.Nodes) { - /* Add the node to the path */ - id_path.push(`${node_type}_${node_oid}`); - - /* This will be need for coll node id path */ - prev_node_id = node_oid; - - /* Remove the token and replace with slash. This will be displayed in the grid */ - return '/'; - } - prev_node_id = null; - return orig_token; - }); - - /* Remove the slash we had added */ - new_path = new_path.substring(1); - return [new_path, id_path]; - } - - prepareDialog() { - this.showMessage(null); - this.setResultCount(0); - if(this.grid) { - this.grid.destroy(); - this.grid = null; - } - - /* Load types */ - this.setTypes([{ - id: -1, - text: gettext('Loading...'), - value: null, - }], false); - - axios.get( - this.getBaseUrl('types') - ).then((res)=>{ - let types = [{ - id: 'all', - text: gettext('All types'), - }]; - - for (const key of Object.keys(res.data.data).sort()) { - types.push({ - id: key, - text: res.data.data[key], - }); - } - this.setTypes(types); - }).catch(()=>{ - this.setTypes([{ - id: -1, - text: gettext('Failed'), - value: null, - }], false); - }); - } - - main(title) { - this.set('title', title); - } - - setup() { - return { - buttons: [{ - text: '', - key: 112, - className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button', - attrs: { - name: 'dialog_help', - type: 'button', - label: gettext('Help'), - 'aria-label': gettext('Help'), - url: url_for('help.static', { - 'filename': 'search_objects.html', - }), - }, - }, { - text: gettext('Close'), - key: 27, - className: 'btn btn-secondary fa fa-lg fa-times pg-alertify-button', - 'data-btn-name': 'cancel', - }], - // Set options for dialog - options: { - title: this.dialogTitle, - //disable both padding and overflow control. - padding: !1, - overflow: !1, - model: 0, - resizable: true, - maximizable: true, - pinnable: false, - closableByDimmer: false, - modal: false, - }, - }; - } - - build() { - let tmpEle = document.createElement('div'); - tmpEle.innerHTML = this.dialogContainerSelector; - let dialogContainer = tmpEle.firstChild; - - // Append the container - this.elements.content.innerHTML = ''; - this.elements.content.appendChild(dialogContainer); - - this.createDialogDOM(dialogContainer); - this.alertify.pgDialogBuild.apply(this); - - this.loader = dialogContainer.getElementsByClassName('pg-sp-container')[0]; - - this.searchBox = dialogContainer.querySelector('#txtGridSearch'); - this.searchBtn = dialogContainer.querySelector('.btn-search'); - this.typesSelect = dialogContainer.querySelector('.node-types'); - this.searchResultContainer = dialogContainer.querySelector('.search-result-container'); - this.searchResult = dialogContainer.querySelector('.search-result'); - this.searchResultCount = dialogContainer.querySelector('.search-result-count'); - this.statusBar = dialogContainer.querySelector('.pg-prop-status-bar'); - - /* These two values are required to come out of grid when tab is - * pressed in the grid. Slickgrid does not allow any way to come out - */ - this.nextToGrid = this.elements.footer.querySelector('.ajs-button'); - this.prevToGrid = this.typesSelect; - - /* init select2 */ - this.setTypes([{ - id: -1, - text: gettext('Loading...'), - value: null, - }], false); - - /* on search box change */ - this.searchBox.addEventListener('input', ()=>{ - if(this.searchBoxVal().length >= 3) { - this.searchBtnEnabled(true); - } else { - this.searchBtnEnabled(false); - } - }); - - /* on enter key press */ - this.searchBox.addEventListener('keypress', (e)=>{ - if(e.keyCode == 13) { - e.stopPropagation(); - if(this.searchBtnEnabled()) { - this.searchBtn.dispatchEvent(new Event('click')); - } - } - }); - - /* on search button click */ - this.searchBtn.addEventListener('click', ()=>{ - this.searchBtnEnabled(false); - this.setGridData([]); - this.showMessage(null); - - this.setLoading(gettext('Searching....')); - axios.get(this.getBaseUrl('search'), { - params: { - text: this.searchBoxVal(), - type: this.typesVal(), - }, - }).then((res)=>{ - let grid_data = res.data.data.map((row)=>{ - return this.finaliseData(row); - }); - - this.setGridData(grid_data); - }).catch((error)=>{ - let errmsg = ''; - - if (error.response) { - if(error.response.data && error.response.data.errormsg) { - errmsg = error.response.data.errormsg; - } else { - errmsg = error.response.statusText; - } - } else if (error.request) { - errmsg = gettext('No response received'); - } else { - errmsg = error.message; - } - this.showMessage(gettext('An unexpected occurred: %s', errmsg), true); - console.warn(error); - }).finally(()=>{ - this.setLoading(null); - this.searchBtnEnabled(true); - }); - }); - - this.set({ - 'onresized': this.onDialogResize.bind(this), - 'onmaximized': this.onDialogResize.bind(this), - 'onrestored': this.onDialogResize.bind(this), - 'onshow': this.onDialogShow.bind(this), - }); - } - - prepare() { - let selectedTreeNode = this.getSelectedNode(); - if (!this.getSelectedNodeData(selectedTreeNode)) { - return; - } - - this.treeInfo = this.pgBrowser.tree.getTreeNodeHierarchy(selectedTreeNode); - this.prepareDialog(); - this.focusOnDialog(this); - } - - callback(event) { - if (this.wasHelpButtonPressed(event)) { - event.cancel = true; - this.pgBrowser.showHelp( - event.button.element.name, - event.button.element.getAttribute('url'), - null, - null, - ); - } - } -} diff --git a/web/pgadmin/tools/search_objects/static/scss/_search_objects.scss b/web/pgadmin/tools/search_objects/static/scss/_search_objects.scss deleted file mode 100644 index fc989a081..000000000 --- a/web/pgadmin/tools/search_objects/static/scss/_search_objects.scss +++ /dev/null @@ -1,129 +0,0 @@ -.search_objects_dialog { - height: 100%; - - .object-other-info { - &:hover { - font-weight: bold; - } - } - - .pref-dialog-link { - color: $color-fg !important; - text-decoration: underline !important; - cursor: pointer; - } - - .search-result-container { - width: 100%; - height: 100%; - min-height: 0; - } - - .node-types ~ .select2-container { - min-width: 100%; - } - - .search-result-count { - border-top: $panel-border; - } - - .ui-widget { - font-family: $font-family-primary; - font-size: $font-size-base; - - .slick-header.ui-state-default { - border: $table-border-width solid $table-border-color; - .slick-header-columns { - background: $table-bg; - color: $color-fg; - border-bottom: $panel-border; - - .slick-header-column-sorted { - font-style: unset; - } - - .ui-state-default { - background: $table-bg !important; - color: $color-fg !important; - padding: $table-header-cell-padding $table-cell-padding; - border-right: $table-border-width solid $table-border-color; - - .slick-column-name { - font-weight: bold; - } - - .slick-sort-indicator { - float: unset; - } - } - - .slick-header-sortable { - cursor: pointer !important; - - .slick-sort-indicator { - width: 0px; - height: 0px; - position: relative; - top: -2px; - } - - .slick-sort-indicator-asc { - background: none; - border-top: none; - border-right: 0.25rem solid transparent; - border-bottom: 0.25rem solid $color-fg; - border-left: 0.25rem solid transparent; - } - - .slick-sort-indicator-desc { - background: none; - border-top: 0.25rem solid $color-fg; - border-right: 0.25rem solid transparent; - border-bottom: none; - border-left: 0.25rem solid transparent; - } - } - } - } - .ui-widget-content { - color: $color-fg; - &.slick-row { - &.object-muted { - &.active, &.active:hover, &:hover, & { - .slick-cell { - color: $text-muted !important; - cursor: default !important; - } - } - } - - &.active, &.active:hover { - .slick-cell { - border-top: $table-border-width solid transparent !important; - background-color: $tree-bg-selected !important; - color: $tree-fg-selected !important; - } - } - - &:hover { - cursor: pointer; - .slick-cell { - border-top: $table-border-width solid transparent !important; - border-bottom: $table-border-width solid transparent !important; - background-color: $tree-bg-hover !important; - color: $tree-fg-hover !important; - cursor: pointer !important; - } - } - } - } - } - - - .pg-prop-status-bar { - position: absolute; - bottom: 0; - right: 0; - left: 0; - } -} diff --git a/web/regression/javascript/file_manager/ListView.spec.js b/web/regression/javascript/file_manager/ListView.spec.js index 8036579ae..a3b79b8a8 100644 --- a/web/regression/javascript/file_manager/ListView.spec.js +++ b/web/regression/javascript/file_manager/ListView.spec.js @@ -12,7 +12,7 @@ import React from 'react'; import '../helper/enzyme.helper'; import { createMount } from '@material-ui/core/test-utils'; import Theme from '../../../pgadmin/static/js/Theme'; -import { CustomRow, FileNameEditor, GridContextUtils } from '../../../pgadmin/misc/file_manager/static/js/components/ListView'; +import { FileNameEditor } from '../../../pgadmin/misc/file_manager/static/js/components/ListView'; describe('ListView', ()=>{ let mount; @@ -75,36 +75,4 @@ describe('ListView', ()=>{ }, 0); }); }); - - describe('CustomRow', ()=>{ - let row = {'Filename': 'test.sql', 'Size': '1KB'}, - ctrlMount = (onItemSelect, onItemEnter)=>{ - return mount( - - - - ); - }; - - it('init', (done)=>{ - let onItemSelect = jasmine.createSpy('onItemSelect'); - let onItemEnter = jasmine.createSpy('onItemEnter'); - let ctrl = ctrlMount(onItemSelect, onItemEnter); - setTimeout(()=>{ - ctrl.update(); - ctrl.find('div[data-test="test-div"]').simulate('keydown', { code: 'Enter'}); - setTimeout(()=>{ - ctrl.update(); - expect(onItemEnter).toHaveBeenCalled(); - ctrl?.unmount(); - done(); - }, 0); - }, 0); - }); - }); }); diff --git a/web/regression/javascript/search_objects/SearchObject.spec.js b/web/regression/javascript/search_objects/SearchObject.spec.js new file mode 100644 index 000000000..8e18206d1 --- /dev/null +++ b/web/regression/javascript/search_objects/SearchObject.spec.js @@ -0,0 +1,186 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2022, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import {TreeFake} from '../tree/tree_fake'; +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import Theme from '../../../pgadmin/static/js/Theme'; +import MockAdapter from 'axios-mock-adapter'; +import axios from 'axios/index'; +import pgAdmin from 'sources/pgadmin'; +import SearchObjects from '../../../pgadmin/tools/search_objects/static/js/SearchObjects'; +import { TreeNode } from '../../../pgadmin/static/js/tree/tree_nodes'; + +const nodeData = {server: {'_id' : 10}, database: {'_id': 123}}; + +describe('SearchObjects', ()=>{ + let mount, networkMock; + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + networkMock = new MockAdapter(axios); + networkMock.onGet('/search_objects/types/10/123').reply(200, {data: [{cast: 'Casts', function: 'Functions'}]}); + networkMock.onGet('/search_objects/search/10/123').reply(200, {data: [ + { + 'name': 'plpgsql', + 'type': 'extension', + 'type_label': 'Extensions', + 'path': ':extension.13315:/plpgsql', + 'show_node': true, + 'other_info': null, + 'catalog_level': 'N' + }, + { + 'name': 'plpgsql_call_handler', + 'type': 'function', + 'type_label': 'Functions', + 'path': ':schema.11:/PostgreSQL Catalog (pg_catalog)/:function.13316:/plpgsql_call_handler', + 'show_node': true, + 'other_info': '', + 'catalog_level': 'D' + }, + { + 'name': 'plpgsql_inline_handler', + 'type': 'function', + 'type_label': 'Functions', + 'path': ':schema.11:/PostgreSQL Catalog (pg_catalog)/:function.13317:/plpgsql_inline_handler', + 'show_node': true, + 'other_info': 'internal', + 'catalog_level': 'D' + }, + { + 'name': 'plpgsql_validator', + 'type': 'function', + 'type_label': 'Functions', + 'path': ':schema.11:/PostgreSQL Catalog (pg_catalog)/:function.13318:/plpgsql_validator', + 'show_node': true, + 'other_info': 'oid', + 'catalog_level': 'D' + }, + { + 'name': 'plpgsql', + 'type': 'language', + 'type_label': 'Languages', + 'path': ':language.13319:/plpgsql', + 'show_node': true, + 'other_info': null, + 'catalog_level': 'N' + } + ]}); + }); + + afterAll(() => { + mount.cleanUp(); + networkMock.restore(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + pgAdmin.Browser = pgAdmin.Browser || {}; + pgAdmin.Browser.Nodes = { + server: { + hasId: true, + getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'), + }, + database: { + hasId: true, + getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'), + }, + 'coll-sometype': { + type: 'coll-sometype', + hasId: false, + label: 'Some types coll', + }, + sometype: { + type: 'sometype', + hasId: true, + }, + someothertype: { + type: 'someothertype', + hasId: true, + collection_type: 'coll-sometype', + }, + 'coll-edbfunc': { + type: 'coll-edbfunc', + hasId: true, + label: 'Functions', + }, + 'coll-edbproc': { + type: 'coll-edbfunc', + hasId: true, + label: 'Procedures', + }, + 'coll-edbvar': { + type: 'coll-edbfunc', + hasId: true, + label: 'Variables', + }, + }; + pgAdmin.Browser.tree = new TreeFake(pgAdmin.Browser); + + let serverTreeNode = pgAdmin.Browser.tree.addNewNode('level2.1', { + _type: 'server', + _id: 10, + label: 'some-tree-label', + }, [{id: 'level2.1'}]), + databaseTreeNode = new TreeNode('database-tree-node', { + _type: 'database', + _id: 123, + _label: 'some-database-label', + }, [{id: 'database-tree-node'}]); + + pgAdmin.Browser.tree.addChild(serverTreeNode, databaseTreeNode); + }); + + describe('SearchObjects', ()=>{ + let ctrlMount = ()=>{ + return mount( + + ); + }; + + it('search', (done)=>{ + let ctrl = ctrlMount({}); + setTimeout(()=>{ + ctrl.update(); + ctrl.find('InputText').find('input').simulate('change', { + target: {value: 'plp'}, + }); + ctrl.update(); + setTimeout(()=>{ + ctrl.find('button[data-test="search"]').simulate('click'); + expect(ctrl.find('PgReactDataGrid').length).toBe(1); + done(); + }, 0); + }, 0); + }); + + it('search_on_enter', (done)=>{ + let ctrl = ctrlMount({}); + setTimeout(()=>{ + ctrl.update(); + ctrl.find('InputText').find('input').simulate('change', { + target: {value: 'plp'}, + }); + ctrl.update(); + setTimeout(()=>{ + ctrl.find('InputText').find('input').simulate('keypress', { + key: 'Enter' + }); + expect(ctrl.find('PgReactDataGrid').length).toBe(1); + done(); + }, 0); + }, 0); + }); + }); +}); \ No newline at end of file diff --git a/web/regression/javascript/search_objects/search_objects_dialog_spec.js b/web/regression/javascript/search_objects/search_objects_dialog_spec.js deleted file mode 100644 index 31fb68f1f..000000000 --- a/web/regression/javascript/search_objects/search_objects_dialog_spec.js +++ /dev/null @@ -1,175 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// -import SearchObjectsDialog from 'tools/search_objects/static/js/search_objects_dialog'; -import {TreeFake} from '../tree/tree_fake'; -import MockAdapter from 'axios-mock-adapter'; -import axios from 'axios/index'; -import gettext from 'sources/gettext'; -import Notify from '../../../pgadmin/static/js/helpers/Notifier'; - -const context = describe; - -var dummy_cache = [ - { - id: 1, - mid: 1, - module:'browser', - name:'qt_tab_title_placeholder', - value: '%DATABASE%/%USERNAME%@%SERVER%', - }, -]; - -describe('SearchObjectsDialog', () => { - let soDialog; - let jquerySpy; - let alertifySpy; - let pgBrowser = {}; - - beforeEach(() => { - pgBrowser.preferences_cache = dummy_cache; - pgBrowser.Nodes = { - server: { - hasId: true, - label: 'server', - getTreeNodeHierarchy: jasmine.createSpy('server.getTreeNodeHierarchy'), - }, - database: { - hasId: true, - label: 'database', - getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'), - }, - schema: { - hasId: true, - label: 'schema', - getTreeNodeHierarchy: jasmine.createSpy('db.getTreeNodeHierarchy'), - }, - }; - pgBrowser.tree = new TreeFake(pgBrowser); - pgBrowser.stdW = { - sm: 500, - md: 700, - lg: 900, - default: 500, - }; - - pgBrowser.stdH = { - sm: 200, - md: 400, - lg: 550, - default: 550, - }; - - pgBrowser.Nodes.server.hasId = true; - pgBrowser.Nodes.database.hasId = true; - jquerySpy = jasmine.createSpy('jquerySpy'); - spyOn(Notify, 'alert'); - - const hierarchy = { - children: [ - { - id: 'root', - children: [ - { - id: 'serverTreeNode', - data: { - _id: 10, - _type: 'server', - user: {name: 'username'}, - label: 'theserver', - _label: 'theserver', - }, - children: [ - { - id: 'some_database', - data: { - _type: 'database', - _id: 11, - label: 'thedatabase', - _label: 'thedatabase', - }, - }, - ], - }, - { - id: 'ppasServer', - data: { - _type: 'server', - server_type: 'ppas', - children: [ - {id: 'someNodeUnderneathPPASServer'}, - ], - }, - }, - ], - }, - ], - }; - - pgBrowser.tree = TreeFake.build(hierarchy, pgBrowser); - }); - - describe('#draw', () => { - let networkMock; - beforeEach(() => { - networkMock = new MockAdapter(axios); - alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']); - alertifySpy['search_objects'] = jasmine.createSpy('search_objects'); - soDialog = new SearchObjectsDialog( - pgBrowser, - jquerySpy, - alertifySpy, - null - ); - - pgBrowser.get_preference = jasmine.createSpy('get_preferences'); - pgBrowser.get_preferences_for_module = - jasmine.createSpy('get_preferences_for_module').and.returnValue({ - [dummy_cache[0]['name']]: dummy_cache[0]['value'], - }); - }); - - afterEach(() => { - networkMock.restore(); - }); - - context('there are no ancestors of the type database', () => { - it('does not create a dialog', () => { - pgBrowser.tree.selectNode([{id: 'serverTreeNode'}]); - soDialog.draw(null, null, null); - expect(alertifySpy['search_objects']).not.toHaveBeenCalled(); - }); - - it('display an alert with a Search object Error', () => { - soDialog.draw(null, [{id: 'serverTreeNode'}], null); - expect(Notify.alert).toHaveBeenCalledWith( - gettext('Search Objects Error'), - gettext('Please select a database or its child node from the browser.') - ); - }); - }); - - context('there is an ancestor of the type database', () => { - let soDialogResizeToSpy; - beforeEach(() => { - soDialogResizeToSpy = jasmine.createSpyObj('soDialogResizeToSpy', ['resizeTo']); - alertifySpy['search_objects'].and - .returnValue(soDialogResizeToSpy); - }); - - it('displays the dialog when database node selected', (done) => { - soDialog.draw(null, [{id: 'some_database'}], null, pgBrowser.stdW.md, pgBrowser.stdH.md); - setTimeout(() => { - expect(alertifySpy['search_objects']).toHaveBeenCalledWith('Search Objects - thedatabase/username@theserver'); - expect(soDialogResizeToSpy.resizeTo).toHaveBeenCalledWith(pgBrowser.stdW.md, pgBrowser.stdH.md); - done(); - }, 0); - }); - }); - }); -}); diff --git a/web/regression/javascript/search_objects/search_objects_dialog_wrapper_spec.js b/web/regression/javascript/search_objects/search_objects_dialog_wrapper_spec.js deleted file mode 100644 index cb19cb2cd..000000000 --- a/web/regression/javascript/search_objects/search_objects_dialog_wrapper_spec.js +++ /dev/null @@ -1,549 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2022, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -import {TreeFake} from '../tree/tree_fake'; -import SearchObjectsDialogWrapper from 'tools/search_objects/static/js/search_objects_dialog_wrapper'; -import axios from 'axios/index'; -import MockAdapter from 'axios-mock-adapter'; -import {TreeNode} from '../../../pgadmin/static/js/tree/tree_nodes'; - -let context = describe; - -describe('SearchObjectsDialogWrapper', () => { - let jquerySpy; - let pgBrowser; - let alertifySpy; - let dialogModelKlassSpy = null; - let backform; - let soDialogWrapper; - let noDataNode; - let serverTreeNode; - let databaseTreeNode; - let viewSchema; - let soJQueryContainerSpy; - let soNodeChildNodeSpy; - let soNode; - - beforeEach(() => { - pgBrowser = { - Nodes: { - server: { - hasId: true, - getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'), - }, - database: { - hasId: true, - getTreeNodeHierarchy: jasmine.createSpy('getTreeNodeHierarchy'), - }, - 'coll-sometype': { - type: 'coll-sometype', - hasId: false, - label: 'Some types coll', - }, - sometype: { - type: 'sometype', - hasId: true, - }, - someothertype: { - type: 'someothertype', - hasId: true, - collection_type: 'coll-sometype', - }, - 'coll-edbfunc': { - type: 'coll-edbfunc', - hasId: true, - label: 'Functions', - }, - 'coll-edbproc': { - type: 'coll-edbfunc', - hasId: true, - label: 'Procedures', - }, - 'coll-edbvar': { - type: 'coll-edbfunc', - hasId: true, - label: 'Variables', - }, - }, - keyboardNavigation: jasmine.createSpyObj('keyboardNavigation', ['getDialogTabNavigator']), - }; - pgBrowser.tree = new TreeFake(pgBrowser); - noDataNode = pgBrowser.tree.addNewNode('level1.1', undefined, [{id: 'level1'}]); - serverTreeNode = pgBrowser.tree.addNewNode('level2.1', { - _type: 'server', - _id: 10, - label: 'some-tree-label', - }, [{id: 'level2.1'}]); - databaseTreeNode = new TreeNode('database-tree-node', { - _type: 'database', - _id: 123, - _label: 'some-database-label', - }, [{id: 'database-tree-node'}]); - pgBrowser.tree.addChild(serverTreeNode, databaseTreeNode); - - jquerySpy = jasmine.createSpy('jquerySpy'); - soNode = { - __internal: { - buttons: [{}, {}, {}, { - element: { - disabled: false, - }, - }], - }, - elements: { - body: { - childNodes: [ - {}, - ], - }, - content: jasmine.createSpyObj('content', ['appendChild', 'attr']), - }, - }; - - soJQueryContainerSpy = jasmine.createSpyObj('soJQueryContainer', ['get', 'attr']); - soJQueryContainerSpy.get.and.returnValue(soJQueryContainerSpy); - - viewSchema = {}; - backform = jasmine.createSpyObj('backform', ['generateViewSchema', 'Dialog']); - backform.generateViewSchema.and.returnValue(viewSchema); - - soNodeChildNodeSpy = jasmine.createSpyObj('something', ['addClass']); - jquerySpy.and.callFake((selector) => { - if (selector === '
') { - return soJQueryContainerSpy; - } else if (selector === soNode.elements.body.childNodes[0]) { - return soNodeChildNodeSpy; - } - }); - alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']); - - }); - - describe('#prepare', () => { - beforeEach(() => { - soDialogWrapper = new SearchObjectsDialogWrapper( - '
', - 'soDialogTitle', - 'search_objects', - jquerySpy, - pgBrowser, - alertifySpy, - dialogModelKlassSpy, - backform - ); - soDialogWrapper = Object.assign(soDialogWrapper, soNode); - spyOn(soDialogWrapper, 'prepareDialog').and.callThrough(); - spyOn(soDialogWrapper, 'setTypes'); - spyOn(soDialogWrapper, 'setResultCount'); - }); - - let prepareAction = ()=> { - spyOn(soDialogWrapper, 'prepareDialog'); - soDialogWrapper.prepare(); - expect(soDialogWrapper.prepareDialog).not.toHaveBeenCalled(); - }; - - context('no tree element is selected', () => { - it('does not prepare dialog', () => { - prepareAction(); - }); - }); - - context('selected tree node has no data', () => { - beforeEach(() => { - pgBrowser.tree.selectNode(noDataNode.domNode); - }); - - it('does not prepare the dialog', () => { - prepareAction(); - }); - }); - - context('tree element is selected', () => { - let gridDestroySpy; - let networkMock; - - beforeEach(() => { - pgBrowser.tree.selectNode(databaseTreeNode.domNode); - soDialogWrapper.grid = jasmine.createSpyObj('grid', ['destroy']); - spyOn(soDialogWrapper, 'showMessage'); - gridDestroySpy = spyOn(soDialogWrapper.grid, 'destroy'); - - networkMock = new MockAdapter(axios); - - }); - - afterEach(() => { - networkMock.restore(); - }); - - it('creates dialog and displays it', () => { - soDialogWrapper.prepare(); - expect(soDialogWrapper.prepareDialog).toHaveBeenCalled(); - expect(soDialogWrapper.showMessage).toHaveBeenCalledWith(null); - }); - - - it('if grid set then destroy it', () => { - soDialogWrapper.prepare(); - expect(gridDestroySpy).toHaveBeenCalled(); - expect(soDialogWrapper.grid).toBe(null); - }); - - it('set result count to 0', () => { - soDialogWrapper.prepare(); - expect(soDialogWrapper.setResultCount).toHaveBeenCalledWith(0); - }); - - it('setTypes called before and after the ajax success', (done) => { - networkMock.onGet('/search_objects/types/10/123').reply(200, { - 'data': { - 'type1': 'Type Label 1', - 'type2': 'Type Label 2', - }, - }); - - soDialogWrapper.prepare(); - - expect(soDialogWrapper.setTypes.calls.argsFor(0)).toEqual([ - [{ id: -1, text: 'Loading...', value: null }], false, - ]); - - setTimeout(()=>{ - expect(soDialogWrapper.setTypes.calls.argsFor(1)).toEqual([ - [{id: 'all', text: 'All types'}, - {id: 'type1', text: 'Type Label 1'}, - {id: 'type2', text: 'Type Label 2'}], - ]); - done(); - }, 0); - }); - - it('setTypes called after the ajax fail', (done) => { - networkMock.onGet('/search_objects/types/10/123').reply(500); - - soDialogWrapper.prepare(); - - expect(soDialogWrapper.setTypes.calls.argsFor(0)).toEqual([ - [{ id: -1, text: 'Loading...', value: null }], false, - ]); - - setTimeout(()=>{ - expect(soDialogWrapper.setTypes.calls.argsFor(1)).toEqual([ - [{id: -1, text: 'Failed', value: null }], false, - ]); - done(); - }, 0); - }); - }); - }); - - describe('showMessage', () => { - beforeEach(() => { - soDialogWrapper = new SearchObjectsDialogWrapper( - '
', - 'soDialogTitle', - 'search_objects', - jquerySpy, - pgBrowser, - alertifySpy, - dialogModelKlassSpy, - backform - ); - soDialogWrapper.statusBar = document.createElement('div'); - soDialogWrapper.statusBar.classList.add('d-none'); - document.body.appendChild(soDialogWrapper.statusBar); - }); - - afterEach(() => { - document.body.removeChild(soDialogWrapper.statusBar); - }); - it('when info message', ()=>{ - soDialogWrapper.showMessage('locating', false); - expect(soDialogWrapper.statusBar.classList.contains('d-none')).toBe(false); - expect(soDialogWrapper.statusBar.querySelector('.error-in-footer')).toBe(null); - expect(soDialogWrapper.statusBar.querySelector('.info-in-footer')).not.toBe(null); - expect(soDialogWrapper.statusBar.querySelector('.alert-text').innerHTML).toEqual('locating'); - }); - - it('when error message', ()=>{ - soDialogWrapper.showMessage('some error', true); - expect(soDialogWrapper.statusBar.classList.contains('d-none')).toBe(false); - expect(soDialogWrapper.statusBar.querySelector('.error-in-footer')).not.toBe(null); - expect(soDialogWrapper.statusBar.querySelector('.info-in-footer')).toBe(null); - expect(soDialogWrapper.statusBar.querySelector('.alert-text').innerHTML).toEqual('some error'); - }); - - it('when no message', ()=>{ - soDialogWrapper.showMessage(null); - expect(soDialogWrapper.statusBar.classList.contains('d-none')).toBe(true); - }); - }); - - describe('function', () => { - beforeEach(() => { - soDialogWrapper = new SearchObjectsDialogWrapper( - '
', - 'soDialogTitle', - 'search_objects', - jquerySpy, - pgBrowser, - alertifySpy, - dialogModelKlassSpy, - backform - ); - }); - - it('updateDimOfSearchResult', ()=>{ - soDialogWrapper.searchResultContainer = document.createElement('div'); - soDialogWrapper.searchResult = document.createElement('div'); - spyOn(soDialogWrapper.searchResultContainer, 'getBoundingClientRect').and.returnValue({height:100, width: 50}); - - soDialogWrapper.updateDimOfSearchResult(); - expect(soDialogWrapper.searchResult.style.height).toEqual('100px'); - expect(soDialogWrapper.searchResult.style.width).toEqual('50px'); - }); - - it('setLoading', ()=>{ - soDialogWrapper.loader = document.createElement('div'); - soDialogWrapper.loader.innerHTML = ` -
- `; - - soDialogWrapper.setLoading('loading'); - expect(soDialogWrapper.loader.classList.contains('d-none')).toBe(false); - expect(soDialogWrapper.loader.querySelector('.pg-sp-text').innerHTML).toEqual('loading'); - - soDialogWrapper.setLoading(null); - expect(soDialogWrapper.loader.classList.contains('d-none')).toBe(true); - }); - - it('searchBtnEnabled', ()=>{ - soDialogWrapper.searchBtn = document.createElement('button'); - - soDialogWrapper.searchBtnEnabled(true); - expect(soDialogWrapper.searchBtn.disabled).toEqual(false); - expect(soDialogWrapper.searchBtnEnabled()).toEqual(true); - - soDialogWrapper.searchBtnEnabled(false); - expect(soDialogWrapper.searchBtn.disabled).toEqual(true); - expect(soDialogWrapper.searchBtnEnabled()).toEqual(false); - }); - - it('searchBoxVal', ()=>{ - soDialogWrapper.searchBox = document.createElement('input'); - soDialogWrapper.searchBoxVal('abc'); - expect(soDialogWrapper.searchBox.value).toEqual('abc'); - expect(soDialogWrapper.searchBoxVal()).toEqual('abc'); - }); - - it('typesVal', ()=>{ - soDialogWrapper.typesSelect = document.createElement('select'); - let opt = document.createElement('option'); - opt.appendChild( document.createTextNode('Some type') ); - opt.value = 'sometype'; - soDialogWrapper.typesSelect.appendChild(opt); - - soDialogWrapper.typesVal('sometype'); - expect(soDialogWrapper.typesSelect.value).toEqual('sometype'); - expect(soDialogWrapper.typesVal()).toEqual('sometype'); - }); - - it('setGridData', ()=>{ - soDialogWrapper.dataview = jasmine.createSpyObj('dataview', ['setItems']); - soDialogWrapper.setGridData([{id:'somedata'}]); - expect(soDialogWrapper.dataview.setItems).toHaveBeenCalled(); - }); - - it('setGridData', ()=>{ - soDialogWrapper.searchResultCount = document.createElement('span'); - - soDialogWrapper.setResultCount(0); - expect(soDialogWrapper.searchResultCount.innerHTML).toEqual('0 matches found.'); - - soDialogWrapper.setResultCount(1); - expect(soDialogWrapper.searchResultCount.innerHTML).toEqual('1 match found.'); - - soDialogWrapper.setResultCount(); - expect(soDialogWrapper.searchResultCount.innerHTML).toEqual('Unknown matches found.'); - }); - - it('onDialogResize', ()=>{ - soDialogWrapper.grid = jasmine.createSpyObj('grid', ['autosizeColumns', 'resizeCanvas']); - spyOn(soDialogWrapper, 'updateDimOfSearchResult'); - - soDialogWrapper.onDialogResize(); - expect(soDialogWrapper.updateDimOfSearchResult).toHaveBeenCalled(); - expect(soDialogWrapper.grid.resizeCanvas).toHaveBeenCalled(); - expect(soDialogWrapper.grid.autosizeColumns).toHaveBeenCalled(); - }); - - it('onDialogShow', (done)=>{ - spyOn(soDialogWrapper, 'prepareGrid').and.callFake(function() { - this.grid = jasmine.createSpyObj('grid', ['init']); - }); - - spyOn(soDialogWrapper, 'focusOnDialog'); - spyOn(soDialogWrapper, 'updateDimOfSearchResult'); - spyOn(soDialogWrapper, 'setGridData'); - spyOn(soDialogWrapper, 'onDialogResize'); - - - soDialogWrapper.onDialogShow(); - setTimeout(()=>{ - expect(soDialogWrapper.prepareGrid).toHaveBeenCalled(); - expect(soDialogWrapper.focusOnDialog).toHaveBeenCalled(); - expect(soDialogWrapper.setGridData).toHaveBeenCalledWith([]); - expect(soDialogWrapper.onDialogResize).toHaveBeenCalled(); - done(); - }, 750); - }); - - context('getCollNode', ()=>{ - it('type have same coll node', ()=>{ - let collNode = soDialogWrapper.getCollNode('sometype'); - expect(collNode.type).toEqual('coll-sometype'); - }); - - it('type does not same coll node', ()=>{ - let collNode = soDialogWrapper.getCollNode('someothertype'); - expect(collNode.type).toEqual('coll-sometype'); - }); - - it('type does not have coll node at all', ()=>{ - let collNode = soDialogWrapper.getCollNode('database'); - expect(collNode).toBe(null); - }); - }); - - it('finaliseData', ()=>{ - spyOn(soDialogWrapper, 'translateSearchObjectsPath').and.returnValue(['disp/path', ['obj1/123', 'obj2/432']]); - let data = soDialogWrapper.finaliseData({ - name: 'objname', - type: 'sometype', - type_label: 'Some types coll', - path: ':some.123:/path', - show_node: true, - other_info: null, - }); - expect(data).toEqual({ - id: 'obj1/123.obj2/432', - icon: 'icon-sometype', - name: 'objname', - type: 'sometype', - type_label: 'Some types coll', - path: 'disp/path', - id_path: ['obj1/123', 'obj2/432'], - show_node: true, - other_info: null, - }); - }); - - context('translateSearchObjectsPath', ()=>{ - let path = null, catalog_level = null; - beforeEach(()=>{ - pgBrowser.Nodes = { - 'server_group': { - type:'server_group', - label: 'Server group', - }, - 'server': { - type:'server', - label: 'Server', - }, - 'coll-database': { - type:'coll-database', - label: 'Databases', - }, - 'database': { - type:'database', - label: 'Database', - }, - 'coll-schema': { - type:'coll-schema', - label: 'Schemas', - }, - 'schema': { - type:'schema', - label: 'Schema', - }, - 'coll-table': { - type:'coll-table', - label: 'Tables', - }, - 'table': { - type:'table', - label: 'Table', - }, - 'sometype': { - type:'sometype', - label: 'Some type', - collection_type: 'coll-table', - }, - 'coll-catalog': { - type:'coll-catalog', - label: 'Catalogs', - }, - 'catalog': { - type:'catalog', - label: 'Catalog', - }, - 'coll-catalog_object': { - type:'coll-catalog_object', - label: 'Catalog Objects', - }, - 'catalog_object': { - type:'catalog_object', - label: 'catalog object', - }, - }; - - soDialogWrapper.treeInfo = { - 'server_group': {'id': 'server_group_1', '_id': 1}, - 'server': {'id': 'server_3', '_id': 3}, - 'database': {'id': 'database_18456', '_id': 18456}, - }; - }); - it('regular schema', ()=>{ - path = ':schema.2200:/test_db/:table.2604:/sampletab'; - catalog_level = 'N'; - - let retVal = soDialogWrapper.translateSearchObjectsPath(path, catalog_level); - expect(retVal).toEqual([ - 'Schemas/test_db/Tables/sampletab', - ['server_group_1','server_3','coll-database_3','database_18456','coll-schema_18456','schema_2200','coll-table_2200','table_2604'], - ]); - }); - - context('catalog schema', ()=>{ - it('with db support', ()=>{ - path = ':schema.11:/PostgreSQL Catalog (pg_catalog)/:table.2604:/pg_class'; - catalog_level = 'D'; - - let retVal = soDialogWrapper.translateSearchObjectsPath(path, catalog_level); - expect(retVal).toEqual([ - 'Catalogs/PostgreSQL Catalog (pg_catalog)/Tables/pg_class', - ['server_group_1','server_3','coll-database_3','database_18456','coll-catalog_18456','catalog_11','coll-table_11','table_2604'], - ]); - }); - - it('with object support only', ()=>{ - path = ':schema.11:/ANSI (information_schema)/:table.2604:/attributes'; - catalog_level = 'O'; - - let retVal = soDialogWrapper.translateSearchObjectsPath(path, catalog_level); - expect(retVal).toEqual([ - 'Catalogs/ANSI (information_schema)/Catalog Objects/attributes', - ['server_group_1','server_3','coll-database_3','database_18456','coll-catalog_18456','catalog_11','coll-catalog_object_11','catalog_object_2604'], - ]); - }); - }); - }); - }); -}); diff --git a/web/webpack.shim.js b/web/webpack.shim.js index ded8b0dcf..1e641339d 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -280,13 +280,12 @@ var webpackShimConfig = { 'pgadmin.tools.restore': path.join(__dirname, './pgadmin/tools/restore/static/js/restore'), 'pgadmin.tools.schema_diff': path.join(__dirname, './pgadmin/tools/schema_diff/static/js/schema_diff'), 'pgadmin.tools.schema_diff_ui': path.join(__dirname, './pgadmin/tools/schema_diff/static/js/schema_diff_ui'), - 'pgadmin.tools.search_objects': path.join(__dirname, './pgadmin/tools/search_objects/static/js/search_objects'), + 'pgadmin.tools.search_objects': path.join(__dirname, './pgadmin/tools/search_objects/static/js'), 'pgadmin.tools.erd_module': path.join(__dirname, './pgadmin/tools/erd/static/js/erd_module'), 'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'), 'pgadmin.tools.psql_module': path.join(__dirname, './pgadmin/tools/psql/static/js/psql_module'), 'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js'), 'pgadmin.tools.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/static/js'), - 'pgadmin.search_objects': path.join(__dirname, './pgadmin/tools/search_objects/static/js'), 'pgadmin.tools.user_management': path.join(__dirname, './pgadmin/tools/user_management/static/js/user_management'), 'pgadmin.user_management.current_user': '/user_management/current_user', 'slick.pgadmin.editors': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/editors'),