mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Port search object dialog to React. Fixes #7622
This commit is contained in:
parent
71a4b20d90
commit
e58b9c767b
Binary file not shown.
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 148 KiB |
@ -13,6 +13,7 @@ New features
|
|||||||
Housekeeping
|
Housekeeping
|
||||||
************
|
************
|
||||||
|
|
||||||
|
| `Issue #7622 <https://redmine.postgresql.org/issues/7622>`_ - Port search object dialog to React.
|
||||||
|
|
||||||
Bug fixes
|
Bug fixes
|
||||||
*********
|
*********
|
||||||
|
@ -124,8 +124,8 @@ define([
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
show_search_objects: function() {
|
show_search_objects: function() {
|
||||||
if(pgAdmin.SearchObjects) {
|
if(pgAdmin.Tools.SearchObjects) {
|
||||||
pgAdmin.SearchObjects.show_search_objects('', pgAdmin.Browser.tree.selected());
|
pgAdmin.Tools.SearchObjects.show_search_objects('', pgAdmin.Browser.tree.selected());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
show_psql_tool: function(args) {
|
show_psql_tool: function(args) {
|
||||||
|
@ -208,7 +208,7 @@ define('pgadmin.browser.node', [
|
|||||||
|
|
||||||
// show search objects same as query tool
|
// show search objects same as query tool
|
||||||
pgAdmin.Browser.add_menus([{
|
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',
|
applies: ['context'], callback: 'show_search_objects',
|
||||||
priority: 997, label: gettext('Search Objects...'),
|
priority: 997, label: gettext('Search Objects...'),
|
||||||
icon: 'fa fa-search', enable: enable,
|
icon: 'fa fa-search', enable: enable,
|
||||||
|
@ -118,7 +118,7 @@ export function initializeToolbar(panel, wcDocker) {
|
|||||||
else if ('name' in data && data.name === gettext('Filtered Rows'))
|
else if ('name' in data && data.name === gettext('Filtered Rows'))
|
||||||
pgAdmin.Tools.SQLEditor.showFilteredRow({mnuid: 4}, pgAdmin.Browser.tree.selected());
|
pgAdmin.Tools.SQLEditor.showFilteredRow({mnuid: 4}, pgAdmin.Browser.tree.selected());
|
||||||
else if ('name' in data && data.name === gettext('Search objects'))
|
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')){
|
else if ('name' in data && data.name === gettext('PSQL Tool')){
|
||||||
var input = {},
|
var input = {},
|
||||||
t = pgAdmin.Browser.tree,
|
t = pgAdmin.Browser.tree,
|
||||||
|
@ -94,6 +94,7 @@ define('pgadmin.misc.cloud', [
|
|||||||
<Theme>
|
<Theme>
|
||||||
<CloudWizard nodeInfo={info} nodeData={d}
|
<CloudWizard nodeInfo={info} nodeData={d}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
ReactDOM.unmountComponentAtNode(j[0]);
|
||||||
panel.close();
|
panel.close();
|
||||||
}}/>
|
}}/>
|
||||||
</Theme>, j[0]);
|
</Theme>, j[0]);
|
||||||
|
@ -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 { Box, makeStyles } from '@material-ui/core';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { DefaultButton, PgButtonGroup, PgIconButton, PrimaryButton } from '../../../../../static/js/components/Buttons';
|
import { DefaultButton, PgButtonGroup, PgIconButton, PrimaryButton } from '../../../../../static/js/components/Buttons';
|
||||||
|
@ -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 { Box, makeStyles } from '@material-ui/core';
|
||||||
import React, {useState, useEffect, useRef, useLayoutEffect} from 'react';
|
import React, {useState, useEffect, useRef, useLayoutEffect} from 'react';
|
||||||
import FolderIcon from '@material-ui/icons/Folder';
|
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 LockRoundedIcon from '@material-ui/icons/LockRounded';
|
||||||
import StorageRoundedIcon from '@material-ui/icons/StorageRounded';
|
import StorageRoundedIcon from '@material-ui/icons/StorageRounded';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import gettext from 'sources/gettext';
|
||||||
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme)=>({
|
const useStyles = makeStyles((theme)=>({
|
||||||
@ -128,7 +137,7 @@ export default function GridView({items, operation, onItemSelect, onItemEnter})
|
|||||||
onItemEnter={onItemEnter} onEditComplete={operation.idx==i ? onEditComplete : null} />)
|
onItemEnter={onItemEnter} onEditComplete={operation.idx==i ? onEditComplete : null} />)
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
{items.length == 0 && <Box textAlign="center" p={1}>No files/folders found</Box>}
|
{items.length == 0 && <Box textAlign="center" p={1}>{gettext('No files/folders found')}</Box>}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 PgReactDataGrid from '../../../../../static/js/components/PgReactDataGrid';
|
||||||
import FolderIcon from '@material-ui/icons/Folder';
|
import FolderIcon from '@material-ui/icons/Folder';
|
||||||
import StorageRoundedIcon from '@material-ui/icons/StorageRounded';
|
import StorageRoundedIcon from '@material-ui/icons/StorageRounded';
|
||||||
import DescriptionIcon from '@material-ui/icons/Description';
|
import DescriptionIcon from '@material-ui/icons/Description';
|
||||||
import LockRoundedIcon from '@material-ui/icons/LockRounded';
|
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 PropTypes from 'prop-types';
|
||||||
|
import gettext from 'sources/gettext';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme)=>({
|
const useStyles = makeStyles((theme)=>({
|
||||||
grid: {
|
grid: {
|
||||||
@ -95,56 +101,6 @@ FileNameEditor.propTypes = {
|
|||||||
onRowChange: PropTypes.func,
|
onRowChange: PropTypes.func,
|
||||||
onClose: PropTypes.func,
|
onClose: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CutomSortIcon({sortDirection}) {
|
|
||||||
if(sortDirection == 'DESC') {
|
|
||||||
return <KeyboardArrowDownIcon style={{fontSize: '1.2rem'}} />;
|
|
||||||
} else if(sortDirection == 'ASC') {
|
|
||||||
return <KeyboardArrowUpIcon style={{fontSize: '1.2rem'}} />;
|
|
||||||
}
|
|
||||||
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 <div data-test='test-div' tabIndex={0} onKeyDown={handleKeyDown}></div>;
|
|
||||||
}
|
|
||||||
const onRowClick = (...args)=>{
|
|
||||||
gridUtils.onItemClick?.(props.rowIdx);
|
|
||||||
props.onRowClick?.(...args);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Row {...props} onKeyDown={handleKeyDown} onRowClick={onRowClick} onRowDoubleClick={(row)=>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}) {
|
function FileNameFormatter({row}) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
let icon = <DescriptionIcon style={{fontSize: '1.2rem'}} />;
|
let icon = <DescriptionIcon style={{fontSize: '1.2rem'}} />;
|
||||||
@ -166,7 +122,7 @@ FileNameFormatter.propTypes = {
|
|||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
key: 'Filename',
|
key: 'Filename',
|
||||||
name: 'Name',
|
name: gettext('Name'),
|
||||||
formatter: FileNameFormatter,
|
formatter: FileNameFormatter,
|
||||||
editor: FileNameEditor,
|
editor: FileNameEditor,
|
||||||
editorOptions: {
|
editorOptions: {
|
||||||
@ -175,17 +131,17 @@ const columns = [
|
|||||||
}
|
}
|
||||||
},{
|
},{
|
||||||
key: 'Properties.DateModified',
|
key: 'Properties.DateModified',
|
||||||
name: 'Date Modified',
|
name: gettext('Date Modified'),
|
||||||
formatter: ({row})=><>{row.Properties?.['Date Modified']}</>
|
formatter: ({row})=><>{row.Properties?.['Date Modified']}</>
|
||||||
},{
|
},{
|
||||||
key: 'Properties.Size',
|
key: 'Properties.Size',
|
||||||
name: 'Size',
|
name: gettext('Size'),
|
||||||
formatter: ({row})=><>{row.file_type != 'dir' && row.Properties?.['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 classes = useStyles();
|
||||||
const gridRef = useRef();
|
const gridRef = useRef();
|
||||||
|
|
||||||
@ -201,7 +157,6 @@ export default function ListView({items, operation, onItemSelect, onItemEnter, o
|
|||||||
}, [gridRef.current?.element]);
|
}, [gridRef.current?.element]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridContextUtils.Provider value={{onItemEnter, onItemSelect, onItemClick}}>
|
|
||||||
<PgReactDataGrid
|
<PgReactDataGrid
|
||||||
gridRef={gridRef}
|
gridRef={gridRef}
|
||||||
id="files"
|
id="files"
|
||||||
@ -217,17 +172,12 @@ export default function ListView({items, operation, onItemSelect, onItemEnter, o
|
|||||||
rowHeight={28}
|
rowHeight={28}
|
||||||
mincolumnWidthBy={25}
|
mincolumnWidthBy={25}
|
||||||
enableCellSelect={false}
|
enableCellSelect={false}
|
||||||
components={{
|
noRowsText={gettext('No files/folders found')}
|
||||||
sortIcon: CutomSortIcon,
|
|
||||||
rowRenderer: CustomRow,
|
|
||||||
noRowsFallback: <Box textAlign="center" gridColumn="1/-1" p={1}>No files/folders found</Box>,
|
|
||||||
}}
|
|
||||||
onRowsChange={(rows)=>{
|
onRowsChange={(rows)=>{
|
||||||
operation?.onComplete?.(rows[operation.idx], operation.idx);
|
operation?.onComplete?.(rows[operation.idx], operation.idx);
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</GridContextUtils.Provider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ListView.propTypes = {
|
ListView.propTypes = {
|
||||||
|
@ -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 React, { useCallback, useReducer, useEffect, useMemo } from 'react';
|
||||||
import { Box, List, ListItem, makeStyles } from '@material-ui/core';
|
import { Box, List, ListItem, makeStyles } from '@material-ui/core';
|
||||||
import CloseIcon from '@material-ui/icons/CloseRounded';
|
import CloseIcon from '@material-ui/icons/CloseRounded';
|
||||||
|
@ -105,7 +105,7 @@ const useStyles = makeStyles((theme) =>
|
|||||||
marginLeft: '0.5em'
|
marginLeft: '0.5em'
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
borderTop: '1px solid #dde0e6 !important',
|
borderTop: `1px solid ${theme.otherVars.inputBorderColor} !important`,
|
||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
@ -100,6 +100,7 @@ export default function(basicSettings) {
|
|||||||
cardHeaderBg: '#424242',
|
cardHeaderBg: '#424242',
|
||||||
colorFg: '#FFFFFF',
|
colorFg: '#FFFFFF',
|
||||||
emptySpaceBg: '#212121',
|
emptySpaceBg: '#212121',
|
||||||
|
textMuted: '#8A8A8A'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,7 @@ export default function(basicSettings) {
|
|||||||
cardHeaderBg: '#062F57',
|
cardHeaderBg: '#062F57',
|
||||||
colorFg: '#FFFFFF',
|
colorFg: '#FFFFFF',
|
||||||
emptySpaceBg: '#010B15',
|
emptySpaceBg: '#010B15',
|
||||||
|
textMuted: '#8b9cad'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -105,6 +105,7 @@ export default function(basicSettings) {
|
|||||||
qtDatagridSelectFg: '#222',
|
qtDatagridSelectFg: '#222',
|
||||||
cardHeaderBg: '#fff',
|
cardHeaderBg: '#fff',
|
||||||
emptySpaceBg: '#ebeef3',
|
emptySpaceBg: '#ebeef3',
|
||||||
|
textMuted: '#646B82',
|
||||||
explain: {
|
explain: {
|
||||||
sev2: {
|
sev2: {
|
||||||
color: '#222222',
|
color: '#222222',
|
||||||
|
@ -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 clsx from 'clsx';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import CustomPropTypes from '../custom_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)=>({
|
const useStyles = makeStyles((theme)=>({
|
||||||
root: {
|
root: {
|
||||||
@ -30,7 +41,6 @@ const useStyles = makeStyles((theme)=>({
|
|||||||
},
|
},
|
||||||
'& .rdg-header-row': {
|
'& .rdg-header-row': {
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
fontWeight: 'normal',
|
|
||||||
},
|
},
|
||||||
'& .rdg-row': {
|
'& .rdg-row': {
|
||||||
backgroundColor: theme.palette.background.default,
|
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 <KeyboardArrowDownIcon style={{fontSize: '1.2rem'}} />;
|
||||||
|
} else if(sortDirection == 'ASC') {
|
||||||
|
return <KeyboardArrowUpIcon style={{fontSize: '1.2rem'}} />;
|
||||||
|
}
|
||||||
|
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 <div data-test='test-div' tabIndex={0} onKeyDown={handleKeyDown}></div>;
|
||||||
|
}
|
||||||
|
const onRowClick = (...args)=>{
|
||||||
|
gridUtils.onItemClick?.(props.rowIdx);
|
||||||
|
props.onRowClick?.(...args);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Row {...props} onKeyDown={handleKeyDown} onRowClick={onRowClick} onRowDoubleClick={(row)=>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();
|
const classes = useStyles();
|
||||||
let finalClassName = [classes.root];
|
let finalClassName = [classes.root];
|
||||||
hasSelectColumn && finalClassName.push(classes.hasSelectColumn);
|
hasSelectColumn && finalClassName.push(classes.hasSelectColumn);
|
||||||
props.enableCellSelect && finalClassName.push(classes.cellSelection);
|
props.enableCellSelect && finalClassName.push(classes.cellSelection);
|
||||||
finalClassName.push(className);
|
finalClassName.push(className);
|
||||||
return <ReactDataGrid
|
return (
|
||||||
|
<GridContextUtils.Provider value={{onItemEnter, onItemSelect, onItemClick}}>
|
||||||
|
<ReactDataGrid
|
||||||
ref={gridRef}
|
ref={gridRef}
|
||||||
className={clsx(finalClassName)}
|
className={clsx(finalClassName)}
|
||||||
|
components={{
|
||||||
|
sortIcon: CutomSortIcon,
|
||||||
|
rowRenderer: CustomRow,
|
||||||
|
noRowsFallback: <Box textAlign="center" gridColumn="1/-1" p={1}>{noRowsText || gettext('No rows found.')}</Box>,
|
||||||
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>;
|
/>
|
||||||
|
</GridContextUtils.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PgReactDataGrid.propTypes = {
|
PgReactDataGrid.propTypes = {
|
||||||
@ -85,4 +155,8 @@ PgReactDataGrid.propTypes = {
|
|||||||
className: CustomPropTypes.className,
|
className: CustomPropTypes.className,
|
||||||
hasSelectColumn: PropTypes.bool,
|
hasSelectColumn: PropTypes.bool,
|
||||||
enableCellSelect: PropTypes.bool,
|
enableCellSelect: PropTypes.bool,
|
||||||
|
onItemEnter: PropTypes.func,
|
||||||
|
onItemSelect: PropTypes.func,
|
||||||
|
onItemClick: PropTypes.func,
|
||||||
|
noRowsText: PropTypes.string
|
||||||
};
|
};
|
||||||
|
@ -99,7 +99,7 @@ const useStyles = makeStyles((theme) =>
|
|||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
},
|
},
|
||||||
wizardFooter: {
|
wizardFooter: {
|
||||||
borderTop: '1px solid #dde0e6 !important',
|
borderTop: `1px solid ${theme.otherVars.inputBorderColor} !important`,
|
||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
@ -59,7 +59,7 @@ const useStyles = makeStyles((theme) =>
|
|||||||
fontSize: '1.12rem !important',
|
fontSize: '1.12rem !important',
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
borderTop: '1px solid #dde0e6 !important',
|
borderTop: `1px solid ${theme.otherVars.inputBorderColor} !important`,
|
||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
@ -93,6 +93,7 @@ define([
|
|||||||
<Theme>
|
<Theme>
|
||||||
<GrantWizard sid={sid} did={did} nodeInfo={info} nodeData={d}
|
<GrantWizard sid={sid} did={did} nodeInfo={info} nodeData={d}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
ReactDOM.unmountComponentAtNode(j[0]);
|
||||||
panel.close();
|
panel.close();
|
||||||
}}/>
|
}}/>
|
||||||
</Theme>, j[0]);
|
</Theme>, j[0]);
|
||||||
|
@ -56,6 +56,7 @@ export default class ImportExportServersModule {
|
|||||||
<Theme>
|
<Theme>
|
||||||
<ImportExportServers
|
<ImportExportServers
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
ReactDOM.unmountComponentAtNode(j[0]);
|
||||||
panel.close();
|
panel.close();
|
||||||
}}/>
|
}}/>
|
||||||
</Theme>, j[0]);
|
</Theme>, j[0]);
|
||||||
|
426
web/pgadmin/tools/search_objects/static/js/SearchObjects.jsx
Normal file
426
web/pgadmin/tools/search_objects/static/js/SearchObjects.jsx
Normal file
@ -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 (
|
||||||
|
<div className='rdg-cell-value'>
|
||||||
|
<Box className={row.show_node ? '' : classes.cellMuted}>
|
||||||
|
<span className={clsx(classes.gridCell, row.icon)}></span>
|
||||||
|
{row.name}
|
||||||
|
{row.other_info != null && row.other_info != '' && <>
|
||||||
|
<span className={classes.funcArgs}onClick={()=>{row.showArgs = true;}}> {row?.showArgs ? `(${row.other_info})` : '(...)'}</span>
|
||||||
|
</>}
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
key: 'type',
|
||||||
|
name: gettext('Type'),
|
||||||
|
width: 30,
|
||||||
|
formatter({row}) {
|
||||||
|
const classes = useStyles();
|
||||||
|
return (
|
||||||
|
<Box className={row.show_node ? '' : classes.cellMuted}>{row.type_label}</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
key: 'path',
|
||||||
|
name: gettext('Browser path'),
|
||||||
|
sortable: false,
|
||||||
|
formatter({row}) {
|
||||||
|
const classes = useStyles();
|
||||||
|
return (
|
||||||
|
<Box className={row.show_node ? '' : classes.cellMuted}>{row.path}</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/* 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 <a id="prefdlgid" class="pref-dialog-link">preferences dialog</a>.', 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 (
|
||||||
|
<Box display="flex" flexDirection="column" height="100%" className={modalClasses.container}>
|
||||||
|
<Box flexGrow="1" display="flex" flexDirection="column" position="relative" overflow="hidden">
|
||||||
|
<Loader message={loaderText} />
|
||||||
|
<Box className={classes.toolbar}>
|
||||||
|
<InputText type="search" className={classes.inputSearch} data-label="search" placeholder={gettext('Type at least 3 characters')} value={search} onChange={setSearch} onKeyPress={onEnterPress}/>
|
||||||
|
<Box style={{marginLeft: '4px', width: '50%'}}>
|
||||||
|
<InputSelect value={type} controlProps={{allowClear: false}} options={typeOptions} onChange={(v)=>setType(v)}/>
|
||||||
|
</Box>
|
||||||
|
<PrimaryButton style={{width: '120px'}} data-test="search" className={modalClasses.margin} startIcon={<SearchRoundedIcon />}
|
||||||
|
onClick={onSearch} disabled={search.length >= 3 ? false : true}>{gettext('Search')}</PrimaryButton>
|
||||||
|
</Box>
|
||||||
|
<Box flexGrow="1" display="flex" flexDirection="column" position="relative" overflow="hidden">
|
||||||
|
<PgReactDataGrid
|
||||||
|
id="searchobjects"
|
||||||
|
className={classes.grid}
|
||||||
|
hasSelectColumn={false}
|
||||||
|
columns={columns}
|
||||||
|
rows={sortedItems}
|
||||||
|
defaultColumnOptions={{
|
||||||
|
sortable: true,
|
||||||
|
resizable: true
|
||||||
|
}}
|
||||||
|
headerRowHeight={28}
|
||||||
|
rowHeight={28}
|
||||||
|
mincolumnWidthBy={25}
|
||||||
|
enableCellSelect={false}
|
||||||
|
sortColumns={sortColumns}
|
||||||
|
onSortColumnsChange={setSortColumns}
|
||||||
|
onItemEnter={onItemEnter}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box className={classes.footer1}>
|
||||||
|
<Box>{footerText}</Box>
|
||||||
|
</Box>
|
||||||
|
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={errorMsg} closable onClose={()=>setErrorMsg('')} />
|
||||||
|
</Box>
|
||||||
|
<Box className={classes.footer}>
|
||||||
|
<Box>
|
||||||
|
<PgIconButton data-test="dialog-help" onClick={onDialogHelp} icon={<HelpIcon />} title={gettext('Help for this dialog.')} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchObjects.propTypes = {
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
nodeData: PropTypes.object,
|
||||||
|
};
|
109
web/pgadmin/tools/search_objects/static/js/index.js
Normal file
109
web/pgadmin/tools/search_objects/static/js/index.js
Normal file
@ -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(
|
||||||
|
<Theme>
|
||||||
|
<SearchObjects nodeData={nodeData}/>
|
||||||
|
</Theme>, j[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!pgAdmin.Tools) {
|
||||||
|
pgAdmin.Tools = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
pgAdmin.Tools.SearchObjects = SearchObjectModule.getInstance();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
SearchObjects: pgAdmin.Tools.SearchObjects,
|
||||||
|
};
|
@ -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;
|
|
||||||
});
|
|
@ -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'),
|
|
||||||
'<div class=\'search_objects_dialog\'></div>',
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 = `
|
|
||||||
<div class="error-in-footer">
|
|
||||||
<div class="d-flex px-2 py-1">
|
|
||||||
<div class="pr-2">
|
|
||||||
<i class="fa fa-exclamation-triangle" aria-hidden="true" role="img"></i>
|
|
||||||
</div>
|
|
||||||
<div role="alert" class="alert-text">${text}</div>
|
|
||||||
<div class="ml-auto close-error-bar">
|
|
||||||
<a class="close-error fa fa-times text-danger"></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.statusBar.querySelector('.close-error').addEventListener('click', ()=>{
|
|
||||||
this.showMessage(null);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.statusBar.innerHTML = `
|
|
||||||
<div class="info-in-footer">
|
|
||||||
<div class="d-flex px-2 py-1">
|
|
||||||
<div class="pr-2">
|
|
||||||
<i class="fa fa-info-circle" aria-hidden="true"></i>
|
|
||||||
</div>
|
|
||||||
<div class="alert-text" role="alert">${text}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
this.statusBar.classList.remove('d-none');
|
|
||||||
call_after_show(this.statusBar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createDialogDOM(dialogContainer) {
|
|
||||||
dialogContainer.innerHTML = `
|
|
||||||
<div class="d-flex flex-column w-100 h-100">
|
|
||||||
<div class="p-2">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-6">
|
|
||||||
<div class="input-group pgadmin-controls">
|
|
||||||
<input type="search" class="form-control" id="txtGridSearch" placeholder="` + gettext('Type at least 3 characters') + `"
|
|
||||||
tabindex="0" aria-describedby="labelSearch" aria-labelledby="labelSearch" autocomplete="off">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-4 d-flex">
|
|
||||||
<select aria-label="` + gettext('Object types') + `" class="node-types"></select>
|
|
||||||
</div>
|
|
||||||
<div class="col-2">
|
|
||||||
<button class="btn btn-primary btn-search w-100" disabled><span class="fa fa-search"></span> `+ gettext('Search') +`</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="search-result-container flex-grow-1">
|
|
||||||
<div class="pg-sp-container d-none">
|
|
||||||
<div class="pg-sp-content">
|
|
||||||
<div class="row"><div class="col-12 pg-sp-icon"></div></div>
|
|
||||||
<div class="row"><div class="col-12 pg-sp-text"></div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="search-result"></div>
|
|
||||||
</div>
|
|
||||||
<div class='search-result-count p-1'>
|
|
||||||
</div>
|
|
||||||
<div class="pg-prop-status-bar">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
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 = `<i class='wcTabIcon ${dataContext.icon}'></i>${value}`;
|
|
||||||
|
|
||||||
if(dataContext.other_info != null && dataContext.other_info != '') {
|
|
||||||
ret_el += ' <span class="object-other-info">(...)</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <a class="pref-dialog-link">preferences dialog</a>.', 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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,7 +12,7 @@ import React from 'react';
|
|||||||
import '../helper/enzyme.helper';
|
import '../helper/enzyme.helper';
|
||||||
import { createMount } from '@material-ui/core/test-utils';
|
import { createMount } from '@material-ui/core/test-utils';
|
||||||
import Theme from '../../../pgadmin/static/js/Theme';
|
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', ()=>{
|
describe('ListView', ()=>{
|
||||||
let mount;
|
let mount;
|
||||||
@ -75,36 +75,4 @@ describe('ListView', ()=>{
|
|||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CustomRow', ()=>{
|
|
||||||
let row = {'Filename': 'test.sql', 'Size': '1KB'},
|
|
||||||
ctrlMount = (onItemSelect, onItemEnter)=>{
|
|
||||||
return mount(<Theme>
|
|
||||||
<GridContextUtils.Provider value={{onItemSelect, onItemEnter}}>
|
|
||||||
<CustomRow
|
|
||||||
row={row}
|
|
||||||
selectedCellIdx={0}
|
|
||||||
rowIdx={0}
|
|
||||||
inTest={true}
|
|
||||||
/>
|
|
||||||
</GridContextUtils.Provider>
|
|
||||||
</Theme>);
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
186
web/regression/javascript/search_objects/SearchObject.spec.js
Normal file
186
web/regression/javascript/search_objects/SearchObject.spec.js
Normal file
@ -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(<Theme>
|
||||||
|
<SearchObjects nodeData={nodeData}/>
|
||||||
|
</Theme>);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -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 === '<div class=\'search_objects_dialog\'></div>') {
|
|
||||||
return soJQueryContainerSpy;
|
|
||||||
} else if (selector === soNode.elements.body.childNodes[0]) {
|
|
||||||
return soNodeChildNodeSpy;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
alertifySpy = jasmine.createSpyObj('alertify', ['alert', 'dialog']);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#prepare', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
soDialogWrapper = new SearchObjectsDialogWrapper(
|
|
||||||
'<div class=\'search_objects_dialog\'></div>',
|
|
||||||
'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(
|
|
||||||
'<div class=\'search_objects_dialog\'></div>',
|
|
||||||
'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(
|
|
||||||
'<div class=\'search_objects_dialog\'></div>',
|
|
||||||
'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 = `
|
|
||||||
<div class="pg-sp-text"></div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
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'],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -280,13 +280,12 @@ var webpackShimConfig = {
|
|||||||
'pgadmin.tools.restore': path.join(__dirname, './pgadmin/tools/restore/static/js/restore'),
|
'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': 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.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_module': path.join(__dirname, './pgadmin/tools/erd/static/js/erd_module'),
|
||||||
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'),
|
'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_module': path.join(__dirname, './pgadmin/tools/psql/static/js/psql_module'),
|
||||||
'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js'),
|
'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js'),
|
||||||
'pgadmin.tools.sqleditor': path.join(__dirname, './pgadmin/tools/sqleditor/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.tools.user_management': path.join(__dirname, './pgadmin/tools/user_management/static/js/user_management'),
|
||||||
'pgadmin.user_management.current_user': '/user_management/current_user',
|
'pgadmin.user_management.current_user': '/user_management/current_user',
|
||||||
'slick.pgadmin.editors': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/editors'),
|
'slick.pgadmin.editors': path.join(__dirname, './pgadmin/tools/../static/js/slickgrid/editors'),
|
||||||
|
Loading…
Reference in New Issue
Block a user