mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Implemented utilities in React to make porting easier for pgAdmin tools.
This commit is contained in:
committed by
Akshay Joshi
parent
76a4dee451
commit
bc4e8a3c82
@@ -7,20 +7,16 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { Button, makeStyles, Tooltip } from '@material-ui/core';
|
||||
import { Button, ButtonGroup, makeStyles, Tooltip } from '@material-ui/core';
|
||||
import React, { forwardRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
import ShortcutTitle from './ShortcutTitle';
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
primaryButton: {
|
||||
'&.MuiButton-outlinedSizeSmall': {
|
||||
height: '28px',
|
||||
'& .MuiSvgIcon-root': {
|
||||
height: '0.8em',
|
||||
}
|
||||
},
|
||||
border: '1px solid '+theme.palette.primary.main,
|
||||
'&.Mui-disabled': {
|
||||
color: theme.palette.primary.contrastText,
|
||||
backgroundColor: theme.palette.primary.disabledMain,
|
||||
@@ -35,12 +31,6 @@ const useStyles = makeStyles((theme)=>({
|
||||
color: theme.palette.default.contrastText,
|
||||
border: '1px solid '+theme.palette.default.borderColor,
|
||||
whiteSpace: 'nowrap',
|
||||
'&.MuiButton-outlinedSizeSmall': {
|
||||
height: '28px',
|
||||
'& .MuiSvgIcon-root': {
|
||||
height: '0.8em',
|
||||
}
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
color: theme.palette.default.disabledContrastText,
|
||||
borderColor: theme.palette.default.disabledBorderColor
|
||||
@@ -53,6 +43,11 @@ const useStyles = makeStyles((theme)=>({
|
||||
},
|
||||
iconButton: {
|
||||
padding: '3px 6px',
|
||||
'&.MuiButton-sizeSmall, &.MuiButton-outlinedSizeSmall, &.MuiButton-containedSizeSmall': {
|
||||
padding: '1px 4px',
|
||||
},
|
||||
},
|
||||
iconButtonDefault: {
|
||||
borderColor: theme.custom.icon.borderColor,
|
||||
color: theme.custom.icon.contrastText,
|
||||
backgroundColor: theme.custom.icon.main,
|
||||
@@ -64,7 +59,15 @@ const useStyles = makeStyles((theme)=>({
|
||||
'&:hover': {
|
||||
backgroundColor: theme.custom.icon.hoverMain,
|
||||
color: theme.custom.icon.hoverContrastText,
|
||||
}
|
||||
},
|
||||
},
|
||||
splitButton: {
|
||||
'&.MuiButton-sizeSmall, &.MuiButton-outlinedSizeSmall, &.MuiButton-containedSizeSmall': {
|
||||
width: '20px',
|
||||
'& svg': {
|
||||
height: '0.8em',
|
||||
}
|
||||
},
|
||||
},
|
||||
xsButton: {
|
||||
padding: '2px 1px',
|
||||
@@ -89,7 +92,7 @@ export const PrimaryButton = forwardRef((props, ref)=>{
|
||||
}
|
||||
noBorder && allClassName.push(classes.noBorder);
|
||||
return (
|
||||
<Button ref={ref} variant="contained" color="primary" size={size} className={clsx(allClassName)} {...otherProps}>{children}</Button>
|
||||
<Button ref={ref} size={size} className={clsx(allClassName)} {...otherProps} variant="contained" color="primary" >{children}</Button>
|
||||
);
|
||||
});
|
||||
PrimaryButton.displayName = 'PrimaryButton';
|
||||
@@ -111,7 +114,7 @@ export const DefaultButton = forwardRef((props, ref)=>{
|
||||
}
|
||||
noBorder && allClassName.push(classes.noBorder);
|
||||
return (
|
||||
<Button ref={ref} variant="outlined" color="default" size={size} className={clsx(allClassName)} {...otherProps}>{children}</Button>
|
||||
<Button variant="outlined" color="default" ref={ref} size={size} className={clsx(allClassName)} {...otherProps} >{children}</Button>
|
||||
);
|
||||
});
|
||||
DefaultButton.displayName = 'DefaultButton';
|
||||
@@ -122,24 +125,77 @@ DefaultButton.propTypes = {
|
||||
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
};
|
||||
|
||||
|
||||
/* pgAdmin Icon button, takes Icon component as input */
|
||||
export const PgIconButton = forwardRef(({icon, title, className, ...props}, ref)=>{
|
||||
export const PgIconButton = forwardRef(({icon, title, shortcut, accessKey, className, splitButton, style, color, ...props}, ref)=>{
|
||||
const classes = useStyles();
|
||||
|
||||
let shortcutTitle = null;
|
||||
if(accessKey || shortcut) {
|
||||
shortcutTitle = <ShortcutTitle title={title} accessKey={accessKey} shortcut={shortcut}/>;
|
||||
}
|
||||
|
||||
/* Tooltip does not work for disabled items */
|
||||
return (
|
||||
<Tooltip title={title || ''} aria-label={title || ''}>
|
||||
<span>
|
||||
<DefaultButton ref={ref} style={{minWidth: 0}} className={clsx(classes.iconButton, className)} {...props}>
|
||||
if(props.disabled) {
|
||||
if(color == 'primary') {
|
||||
return (
|
||||
<PrimaryButton ref={ref} style={{minWidth: 0, ...style}}
|
||||
className={clsx(classes.iconButton, (splitButton ? classes.splitButton : ''), className)} {...props}>
|
||||
{icon}
|
||||
</PrimaryButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DefaultButton ref={ref} style={{minWidth: 0, ...style}}
|
||||
className={clsx(classes.iconButton, classes.iconButtonDefault, (splitButton ? classes.splitButton : ''), className)} {...props}>
|
||||
{icon}
|
||||
</DefaultButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if(color == 'primary') {
|
||||
return (
|
||||
<Tooltip title={shortcutTitle || title || ''} aria-label={title || ''}>
|
||||
<PrimaryButton ref={ref} style={{minWidth: 0, ...style}}
|
||||
className={clsx(classes.iconButton, (splitButton ? classes.splitButton : ''), className)} {...props}>
|
||||
{icon}
|
||||
</PrimaryButton>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tooltip title={shortcutTitle || title || ''} aria-label={title || ''}>
|
||||
<DefaultButton ref={ref} style={{minWidth: 0, ...style}}
|
||||
className={clsx(classes.iconButton, classes.iconButtonDefault, (splitButton ? classes.splitButton : ''), className)} {...props}>
|
||||
{icon}
|
||||
</DefaultButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
PgIconButton.displayName = 'PgIconButton';
|
||||
PgIconButton.propTypes = {
|
||||
icon: CustomPropTypes.children,
|
||||
title: PropTypes.string.isRequired,
|
||||
shortcut: CustomPropTypes.shortcut,
|
||||
accessKey: PropTypes.string,
|
||||
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
style: PropTypes.object,
|
||||
color: PropTypes.oneOf(['primary', 'default', undefined]),
|
||||
disabled: PropTypes.bool,
|
||||
splitButton: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const PgButtonGroup = forwardRef(({children, ...props}, ref)=>{
|
||||
/* Tooltip does not work for disabled items */
|
||||
return (
|
||||
<ButtonGroup disableElevation innerRef={ref} {...props}>
|
||||
{children}
|
||||
</ButtonGroup>
|
||||
);
|
||||
});
|
||||
PgButtonGroup.displayName = 'PgButtonGroup';
|
||||
PgButtonGroup.propTypes = {
|
||||
children: CustomPropTypes.children,
|
||||
};
|
||||
|
||||
@@ -7,11 +7,258 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {default as OrigCodeMirror} from 'bundled_codemirror';
|
||||
import {useOnScreen} from 'sources/custom_hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import gettext from 'sources/gettext';
|
||||
import { Box, InputAdornment, makeStyles } from '@material-ui/core';
|
||||
import clsx from 'clsx';
|
||||
import { InputText } from './FormComponents';
|
||||
import { PgIconButton } from './Buttons';
|
||||
import CloseIcon from '@material-ui/icons/CloseRounded';
|
||||
import ArrowDownwardRoundedIcon from '@material-ui/icons/ArrowDownwardRounded';
|
||||
import ArrowUpwardRoundedIcon from '@material-ui/icons/ArrowUpwardRounded';
|
||||
import SwapHorizRoundedIcon from '@material-ui/icons/SwapHorizRounded';
|
||||
import SwapCallsRoundedIcon from '@material-ui/icons/SwapCallsRounded';
|
||||
import _ from 'lodash';
|
||||
import { RegexIcon, FormatCaseIcon } from './ExternalIcon';
|
||||
import { isMac } from '../keyboard_shortcuts';
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
root: {
|
||||
position: 'relative',
|
||||
},
|
||||
findDialog: {
|
||||
position: 'absolute',
|
||||
zIndex: 99,
|
||||
right: '4px',
|
||||
...theme.mixins.panelBorder.all,
|
||||
borderTop: 'none',
|
||||
padding: '2px 4px',
|
||||
width: '250px',
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
marginTop: {
|
||||
marginTop: '0.25rem',
|
||||
}
|
||||
}));
|
||||
|
||||
function parseString(string) {
|
||||
return string.replace(/\\([nrt\\])/g, function(match, ch) {
|
||||
if (ch == 'n') return '\n';
|
||||
if (ch == 'r') return '\r';
|
||||
if (ch == 't') return '\t';
|
||||
if (ch == '\\') return '\\';
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
function parseQuery(query, useRegex=false, matchCase=false) {
|
||||
if (useRegex) {
|
||||
query = new RegExp(query, matchCase ? 'g': 'gi');
|
||||
} else {
|
||||
query = parseString(query);
|
||||
if(!matchCase) {
|
||||
query = query.toLowerCase();
|
||||
}
|
||||
}
|
||||
if (typeof query == 'string' ? query == '' : query.test(''))
|
||||
query = /x^/;
|
||||
return query;
|
||||
}
|
||||
|
||||
function searchOverlay(query, matchCase) {
|
||||
return {
|
||||
token: typeof query == 'string' ?
|
||||
(stream) =>{
|
||||
var matchIndex = (matchCase ? stream.string : stream.string.toLowerCase()).indexOf(query, stream.pos);
|
||||
if(matchIndex == -1) {
|
||||
stream.skipToEnd();
|
||||
} else if(matchIndex == stream.pos) {
|
||||
stream.pos += query.length;
|
||||
return 'searching';
|
||||
} else {
|
||||
stream.pos = matchIndex;
|
||||
}
|
||||
} : (stream) => {
|
||||
query.lastIndex = stream.pos;
|
||||
var match = query.exec(stream.string);
|
||||
if (match && match.index == stream.pos) {
|
||||
stream.pos += match[0].length || 1;
|
||||
return 'searching';
|
||||
} else if (match) {
|
||||
stream.pos = match.index;
|
||||
} else {
|
||||
stream.skipToEnd();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const CodeMirrorInstancType = PropTypes.shape({
|
||||
getCursor: PropTypes.func,
|
||||
getSearchCursor: PropTypes.func,
|
||||
removeOverlay: PropTypes.func,
|
||||
addOverlay: PropTypes.func,
|
||||
setSelection: PropTypes.func,
|
||||
scrollIntoView: PropTypes.func,
|
||||
});
|
||||
|
||||
export function FindDialog({editor, show, replace, onClose}) {
|
||||
const [findVal, setFindVal] = useState('');
|
||||
const [replaceVal, setReplaceVal] = useState('');
|
||||
const [useRegex, setUseRegex] = useState(false);
|
||||
const [matchCase, setMatchCase] = useState(false);
|
||||
const findInputRef = useRef();
|
||||
const highlightsearch = useRef();
|
||||
const searchCursor = useRef();
|
||||
const classes = useStyles();
|
||||
|
||||
const search = ()=>{
|
||||
if(editor) {
|
||||
let query = parseQuery(findVal, useRegex, matchCase);
|
||||
searchCursor.current = editor.getSearchCursor(query, editor.getCursor(true), !matchCase);
|
||||
if(findVal != '') {
|
||||
editor.removeOverlay(highlightsearch.current);
|
||||
highlightsearch.current = searchOverlay(query, matchCase);
|
||||
editor.addOverlay(highlightsearch.current);
|
||||
onFindNext();
|
||||
} else {
|
||||
editor.removeOverlay(highlightsearch.current);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(()=>{
|
||||
if(show) {
|
||||
findInputRef.current && findInputRef.current.select();
|
||||
search();
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
useEffect(()=>{
|
||||
search();
|
||||
}, [findVal, useRegex, matchCase]);
|
||||
|
||||
const clearAndClose = ()=>{
|
||||
editor.removeOverlay(highlightsearch.current);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const toggle = (name)=>{
|
||||
if(name == 'regex') {
|
||||
setUseRegex((prev)=>!prev);
|
||||
} else if(name == 'case') {
|
||||
setMatchCase((prev)=>!prev);
|
||||
}
|
||||
};
|
||||
|
||||
const onFindEnter = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if(e.shiftKey) {
|
||||
onFindPrev();
|
||||
} else {
|
||||
onFindNext();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onReplaceEnter = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onReplace();
|
||||
}
|
||||
};
|
||||
|
||||
const onEscape = (e)=>{
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
clearAndClose();
|
||||
}
|
||||
};
|
||||
|
||||
const onFindNext = ()=>{
|
||||
if(searchCursor.current && searchCursor.current.find()) {
|
||||
editor.setSelection(searchCursor.current.from(), searchCursor.current.to());
|
||||
editor.scrollIntoView({
|
||||
from: searchCursor.current.from(),
|
||||
to: searchCursor.current.to()
|
||||
}, 20);
|
||||
}
|
||||
};
|
||||
|
||||
const onFindPrev = ()=>{
|
||||
if(searchCursor.current && searchCursor.current.find(true)) {
|
||||
editor.setSelection(searchCursor.current.from(), searchCursor.current.to());
|
||||
editor.scrollIntoView({
|
||||
from: searchCursor.current.from(),
|
||||
to: searchCursor.current.to()
|
||||
}, 20);
|
||||
}
|
||||
};
|
||||
|
||||
const onReplace = ()=>{
|
||||
searchCursor.current.replace(replaceVal);
|
||||
onFindNext();
|
||||
};
|
||||
|
||||
const onReplaceAll = ()=>{
|
||||
while(searchCursor.current.from()) {
|
||||
onReplace();
|
||||
}
|
||||
};
|
||||
|
||||
if(!editor) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={classes.findDialog} visibility={show ? 'visible' : 'hidden'} tabIndex="0" onKeyDown={onEscape}>
|
||||
<InputText value={findVal}
|
||||
inputRef={(ele)=>{findInputRef.current = ele;}}
|
||||
onChange={(value)=>setFindVal(value)}
|
||||
onKeyPress={onFindEnter}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<PgIconButton data-test="case" title="Match case" icon={<FormatCaseIcon />} size="xs" noBorder
|
||||
onClick={()=>toggle('case')} color={matchCase ? 'primary' : 'default'} style={{marginRight: '2px'}}/>
|
||||
<PgIconButton data-test="regex" title="Use regex" icon={<RegexIcon />} size="xs" noBorder
|
||||
onClick={()=>toggle('regex')} color={useRegex ? 'primary' : 'default'}/>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
{replace &&
|
||||
<InputText value={replaceVal}
|
||||
className={classes.marginTop}
|
||||
onChange={(value)=>setReplaceVal(value)}
|
||||
onKeyPress={onReplaceEnter}
|
||||
/>}
|
||||
|
||||
<Box display="flex" className={classes.marginTop}>
|
||||
<PgIconButton title={gettext('Previous')} icon={<ArrowUpwardRoundedIcon />} size="xs" noBorder onClick={onFindPrev} />
|
||||
<PgIconButton title={gettext('Next')} icon={<ArrowDownwardRoundedIcon />} size="xs" noBorder onClick={onFindNext}/>
|
||||
{replace && <>
|
||||
<PgIconButton title={gettext('Replace')} icon={<SwapHorizRoundedIcon style={{height: 'unset'}}/>} size="xs" noBorder onClick={onReplace} />
|
||||
<PgIconButton title={gettext('Replace All')} icon={<SwapCallsRoundedIcon />} size="xs" noBorder onClick={onReplaceAll}/>
|
||||
</>}
|
||||
<Box marginLeft="auto">
|
||||
<PgIconButton title={gettext('Close')} icon={<CloseIcon />} size="xs" noBorder onClick={clearAndClose}/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
FindDialog.propTypes = {
|
||||
editor: CodeMirrorInstancType,
|
||||
show: PropTypes.bool,
|
||||
replace: PropTypes.bool,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
/* React wrapper for CodeMirror */
|
||||
export default function CodeMirror({currEditor, name, value, options, events, readonly, disabled, className}) {
|
||||
@@ -19,13 +266,34 @@ export default function CodeMirror({currEditor, name, value, options, events, re
|
||||
const editor = useRef();
|
||||
const cmWrapper = useRef();
|
||||
const isVisibleTrack = useRef();
|
||||
const classes = useStyles();
|
||||
const [[showFind, isReplace], setShowFind] = useState([false, false]);
|
||||
const defaultOptions = {
|
||||
tabindex: '0',
|
||||
lineNumbers: true,
|
||||
styleSelectedText: true,
|
||||
mode: 'text/x-pgsql',
|
||||
foldOptions: {
|
||||
widget: '\u2026',
|
||||
},
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
extraKeys: pgAdmin.Browser.editor_shortcut_keys,
|
||||
dragDrop: false,
|
||||
screenReaderLabel: gettext('SQL editor'),
|
||||
};
|
||||
|
||||
useEffect(()=>{
|
||||
const finalOptions = {...defaultOptions, ...options};
|
||||
/* Create the object only once on mount */
|
||||
editor.current = new OrigCodeMirror.fromTextArea(
|
||||
taRef.current, options);
|
||||
taRef.current, finalOptions);
|
||||
|
||||
editor.current.setValue(value);
|
||||
if(!_.isEmpty(value)) {
|
||||
editor.current.setValue(value);
|
||||
} else {
|
||||
editor.current.setValue('');
|
||||
}
|
||||
currEditor && currEditor(editor.current);
|
||||
if(editor.current) {
|
||||
try {
|
||||
@@ -33,11 +301,33 @@ export default function CodeMirror({currEditor, name, value, options, events, re
|
||||
} catch(e) {
|
||||
cmWrapper.current = null;
|
||||
}
|
||||
|
||||
let findKey = 'Ctrl-F', replaceKey = 'Shift-Ctrl-F';
|
||||
if(isMac()) {
|
||||
findKey = 'Cmd-F';
|
||||
replaceKey = 'Cmd-Alt-F';
|
||||
}
|
||||
editor.current.addKeyMap({
|
||||
[findKey]: ()=>{
|
||||
setShowFind([false, false]);
|
||||
setShowFind([true, false]);
|
||||
},
|
||||
[replaceKey]: ()=>{
|
||||
if(!finalOptions.readOnly) {
|
||||
setShowFind([false, false]);
|
||||
setShowFind([true, true]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(events||{}).forEach((eventName)=>{
|
||||
editor.current.on(eventName, events[eventName]);
|
||||
});
|
||||
|
||||
return ()=>{
|
||||
editor.current?.toTextArea();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
@@ -62,7 +352,11 @@ export default function CodeMirror({currEditor, name, value, options, events, re
|
||||
useMemo(() => {
|
||||
if(editor.current) {
|
||||
if(value != editor.current.getValue()) {
|
||||
editor.current.setValue(value);
|
||||
if(!_.isEmpty(value)) {
|
||||
editor.current.setValue(value);
|
||||
} else {
|
||||
editor.current.setValue('');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
@@ -75,8 +369,16 @@ export default function CodeMirror({currEditor, name, value, options, events, re
|
||||
isVisibleTrack.current = false;
|
||||
}
|
||||
|
||||
const closeFind = ()=>{
|
||||
setShowFind([false, false]);
|
||||
editor.current?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}><textarea ref={taRef} name={name} /></div>
|
||||
<div className={clsx(className, classes.root)}>
|
||||
<FindDialog editor={editor.current} show={showFind} replace={isReplace} onClose={closeFind}/>
|
||||
<textarea ref={taRef} name={name} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
34
web/pgadmin/static/js/components/ExternalIcon.jsx
Normal file
34
web/pgadmin/static/js/components/ExternalIcon.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import QueryToolSvg from '../../img/fonticon/query_tool.svg?svgr';
|
||||
import SaveDataSvg from '../../img/fonticon/save_data_changes.svg?svgr';
|
||||
import PasteSvg from '../../img/content_paste.svg?svgr';
|
||||
import FilterSvg from '../../img/filter_alt_black.svg?svgr';
|
||||
import ClearSvg from '../../img/cleaning_services_black.svg?svgr';
|
||||
import CommitSvg from '../../img/fonticon/commit.svg?svgr';
|
||||
import RollbackSvg from '../../img/fonticon/rollback.svg?svgr';
|
||||
import ConnectedSvg from '../../img/fonticon/connected.svg?svgr';
|
||||
import DisconnectedSvg from '../../img/fonticon/disconnected.svg?svgr';
|
||||
import RegexSvg from '../../img/fonticon/regex.svg?svgr';
|
||||
import FormatCaseSvg from '../../img/fonticon/format_case.svg?svgr';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function ExternalIcon({Icon, ...props}) {
|
||||
return <Icon className='MuiSvgIcon-root' {...props} />;
|
||||
}
|
||||
|
||||
ExternalIcon.propTypes = {
|
||||
Icon: PropTypes.elementType.isRequired,
|
||||
};
|
||||
|
||||
export const QueryToolIcon = ()=><ExternalIcon Icon={QueryToolSvg} style={{height: '0.7em'}} />;
|
||||
export const SaveDataIcon = ()=><ExternalIcon Icon={SaveDataSvg} style={{height: '0.7em'}} />;
|
||||
export const PasteIcon = ()=><ExternalIcon Icon={PasteSvg} />;
|
||||
export const FilterIcon = ()=><ExternalIcon Icon={FilterSvg} />;
|
||||
export const CommitIcon = ()=><ExternalIcon Icon={CommitSvg} />;
|
||||
export const RollbackIcon = ()=><ExternalIcon Icon={RollbackSvg} />;
|
||||
export const ClearIcon = ()=><ExternalIcon Icon={ClearSvg} />;
|
||||
export const ConnectedIcon = ()=><ExternalIcon Icon={ConnectedSvg} style={{height: '0.7em'}} />;
|
||||
export const DisonnectedIcon = ()=><ExternalIcon Icon={DisconnectedSvg} style={{height: '0.7em'}} />;
|
||||
export const RegexIcon = ()=><ExternalIcon Icon={RegexSvg} />;
|
||||
export const FormatCaseIcon = ()=><ExternalIcon Icon={FormatCaseSvg} />;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Box, FormControl, OutlinedInput, FormHelperText,
|
||||
Grid, IconButton, FormControlLabel, Switch, Checkbox, useTheme, InputLabel, Paper } from '@material-ui/core';
|
||||
Grid, IconButton, FormControlLabel, Switch, Checkbox, useTheme, InputLabel, Paper, Select as MuiSelect } from '@material-ui/core';
|
||||
import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab';
|
||||
import ErrorRoundedIcon from '@material-ui/icons/ErrorOutlineRounded';
|
||||
import InfoRoundedIcon from '@material-ui/icons/InfoRounded';
|
||||
@@ -148,7 +148,7 @@ FormInput.propTypes = {
|
||||
testcid: PropTypes.any,
|
||||
};
|
||||
|
||||
export function InputSQL({value, options, onChange, className, ...props}) {
|
||||
export function InputSQL({value, onChange, className, controlProps, ...props}) {
|
||||
const classes = useStyles();
|
||||
const editor = useRef();
|
||||
|
||||
@@ -156,17 +156,13 @@ export function InputSQL({value, options, onChange, className, ...props}) {
|
||||
<CodeMirror
|
||||
currEditor={(obj)=>editor.current=obj}
|
||||
value={value||''}
|
||||
options={{
|
||||
lineNumbers: true,
|
||||
mode: 'text/x-pgsql',
|
||||
...options,
|
||||
}}
|
||||
className={clsx(classes.sql, className)}
|
||||
events={{
|
||||
change: (cm)=>{
|
||||
onChange && onChange(cm.getValue());
|
||||
},
|
||||
}}
|
||||
{...controlProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -177,15 +173,16 @@ InputSQL.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
readonly: PropTypes.bool,
|
||||
className: CustomPropTypes.className,
|
||||
controlProps: PropTypes.object,
|
||||
};
|
||||
|
||||
export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, controlProps, noLabel, ...props}) {
|
||||
export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, noLabel, ...props}) {
|
||||
if(noLabel) {
|
||||
return <InputSQL value={value} options={controlProps} {...props}/>;
|
||||
return <InputSQL value={value} {...props}/>;
|
||||
} else {
|
||||
return (
|
||||
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} >
|
||||
<InputSQL value={value} options={controlProps} {...props}/>
|
||||
<InputSQL value={value} {...props}/>
|
||||
</FormInput>
|
||||
);
|
||||
}
|
||||
@@ -198,7 +195,6 @@ FormInputSQL.propTypes = {
|
||||
helpMessage: PropTypes.string,
|
||||
testcid: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
controlProps: PropTypes.object,
|
||||
noLabel: PropTypes.bool,
|
||||
change: PropTypes.func,
|
||||
};
|
||||
@@ -739,6 +735,17 @@ function getRealValue(options, value, creatable, formatter) {
|
||||
}
|
||||
return realValue;
|
||||
}
|
||||
export function InputSelectNonSearch({options, ...props}) {
|
||||
return <MuiSelect native {...props} variant="outlined">
|
||||
{(options||[]).map((o)=><option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</MuiSelect>;
|
||||
}
|
||||
InputSelectNonSearch.propTypes = {
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.shape,
|
||||
value: PropTypes.any,
|
||||
})),
|
||||
};
|
||||
|
||||
export const InputSelect = forwardRef(({
|
||||
cid, onChange, options, readonly=false, value, controlProps={}, optionsLoaded, optionsReloadBasis, disabled, ...props}, ref) => {
|
||||
|
||||
60
web/pgadmin/static/js/components/JsonEditor.jsx
Normal file
60
web/pgadmin/static/js/components/JsonEditor.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import {default as OrigJsonEditor} from 'jsoneditor.min';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
|
||||
/* React wrapper for JsonEditor */
|
||||
export default function JsonEditor({currEditor, value, options, className}) {
|
||||
const eleRef = useRef();
|
||||
const editor = useRef();
|
||||
const defaultOptions = {
|
||||
modes: ['code', 'form', 'tree','preview'],
|
||||
};
|
||||
|
||||
useEffect(()=>{
|
||||
/* Create the object only once on mount */
|
||||
editor.current = new OrigJsonEditor(eleRef.current, {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
onChange: ()=>{
|
||||
let currVal = editor.current.getText();
|
||||
if(currVal == '') {
|
||||
currVal = null;
|
||||
}
|
||||
options.onChange(currVal);
|
||||
}
|
||||
});
|
||||
editor.current.setText(value);
|
||||
currEditor && currEditor(editor.current);
|
||||
editor.current.focus();
|
||||
return ()=>editor.current?.destroy();
|
||||
}, []);
|
||||
|
||||
useMemo(() => {
|
||||
if(editor.current) {
|
||||
if(value != editor.current.getText()) {
|
||||
editor.current.setText(value ?? '');
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div ref={eleRef} className={className}></div>
|
||||
);
|
||||
}
|
||||
|
||||
JsonEditor.propTypes = {
|
||||
currEditor: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
options: PropTypes.object,
|
||||
className: CustomPropTypes.className,
|
||||
};
|
||||
@@ -40,13 +40,13 @@ const useStyles = makeStyles((theme)=>({
|
||||
}
|
||||
}));
|
||||
|
||||
export default function Loader({message}) {
|
||||
export default function Loader({message, style}) {
|
||||
const classes = useStyles();
|
||||
if(!message) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Box className={classes.root} style={style}>
|
||||
<Box className={classes.loaderRoot}>
|
||||
<CircularProgress className={classes.loader} />
|
||||
<Typography className={classes.message}>{message}</Typography>
|
||||
@@ -57,4 +57,5 @@ export default function Loader({message}) {
|
||||
|
||||
Loader.propTypes = {
|
||||
message: PropTypes.string,
|
||||
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
|
||||
};
|
||||
|
||||
83
web/pgadmin/static/js/components/Menu.jsx
Normal file
83
web/pgadmin/static/js/components/Menu.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import React from 'react';
|
||||
import CheckIcon from '@material-ui/icons/Check';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
MenuItem,
|
||||
ControlledMenu,
|
||||
applyStatics,
|
||||
} from '@szhsin/react-menu';
|
||||
export {MenuDivider as PgMenuDivider} from '@szhsin/react-menu';
|
||||
import { shortcutToString } from './ShortcutTitle';
|
||||
import clsx from 'clsx';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
menu: {
|
||||
'& .szh-menu': {
|
||||
padding: '4px 0px',
|
||||
zIndex: 1000,
|
||||
},
|
||||
'& .szh-menu__divider': {
|
||||
margin: 0,
|
||||
}
|
||||
},
|
||||
menuItem: {
|
||||
display: 'flex',
|
||||
padding: '4px 8px',
|
||||
'&.szh-menu__item--active, &.szh-menu__item--hover': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
}
|
||||
},
|
||||
hideCheck: {
|
||||
visibility: 'hidden',
|
||||
},
|
||||
shortcut: {
|
||||
marginLeft: 'auto',
|
||||
fontSize: '0.8em',
|
||||
paddingLeft: '12px',
|
||||
}
|
||||
}));
|
||||
|
||||
export function PgMenu({open, className, ...props}) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<ControlledMenu
|
||||
state={open ? 'open' : 'closed'}
|
||||
{...props}
|
||||
className={clsx(classes.menu, className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
PgMenu.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
className: CustomPropTypes.className,
|
||||
};
|
||||
|
||||
export const PgMenuItem = applyStatics(MenuItem)(({hasCheck=false, checked=false, accesskey, shortcut, children, ...props})=>{
|
||||
const classes = useStyles();
|
||||
let onClick = props.onClick;
|
||||
if(hasCheck) {
|
||||
onClick = (e)=>{
|
||||
e.keepOpen = true;
|
||||
props.onClick(e);
|
||||
};
|
||||
}
|
||||
return <MenuItem {...props} onClick={onClick} className={classes.menuItem}>
|
||||
{hasCheck && <CheckIcon style={checked ? {} : {visibility: 'hidden'}}/>}
|
||||
{children}
|
||||
{(shortcut || accesskey) && <div className={classes.shortcut}>({shortcutToString(shortcut, accesskey)})</div>}
|
||||
</MenuItem>;
|
||||
});
|
||||
|
||||
PgMenuItem.propTypes = {
|
||||
hasCheck: PropTypes.bool,
|
||||
checked: PropTypes.bool,
|
||||
accesskey: PropTypes.string,
|
||||
shortcut: CustomPropTypes.shortcut,
|
||||
children: CustomPropTypes.children,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
63
web/pgadmin/static/js/components/ShortcutTitle.jsx
Normal file
63
web/pgadmin/static/js/components/ShortcutTitle.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isMac } from '../keyboard_shortcuts';
|
||||
import _ from 'lodash';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
shortcut: {
|
||||
justifyContent: 'center',
|
||||
marginTop: '0.125rem',
|
||||
display: 'flex',
|
||||
},
|
||||
key: {
|
||||
padding: '0 0.25rem',
|
||||
border: `1px solid ${theme.otherVars.borderColor}`,
|
||||
marginRight: '0.125rem',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}));
|
||||
|
||||
export function shortcutToString(shortcut, accesskey=null, asArray=false) {
|
||||
let keys = [];
|
||||
if(accesskey) {
|
||||
keys.push('Accesskey');
|
||||
keys.push(_.capitalize(accesskey?.toUpperCase()));
|
||||
} else if(shortcut) {
|
||||
shortcut.alt && keys.push((isMac() ? 'Option' : 'Alt'));
|
||||
if(isMac() && shortcut.ctrl_is_meta) {
|
||||
shortcut.control && keys.push('Cmd');
|
||||
} else {
|
||||
shortcut.control && keys.push('Ctrl');
|
||||
}
|
||||
shortcut.shift && keys.push('Shift');
|
||||
keys.push(_.capitalize(shortcut.key.char));
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
||||
return asArray ? keys : keys.join(' + ');
|
||||
}
|
||||
|
||||
/* The tooltip content to show shortcut details */
|
||||
export default function ShortcutTitle({title, shortcut, accessKey}) {
|
||||
const classes = useStyles();
|
||||
let keys = shortcutToString(shortcut, accessKey, true);
|
||||
return (
|
||||
<>
|
||||
<div>{title}</div>
|
||||
<div className={classes.shortcut}>
|
||||
{keys.map((key, i)=>{
|
||||
return <div key={i} className={classes.key}>{key}</div>;
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ShortcutTitle.propTypes = {
|
||||
title: PropTypes.string,
|
||||
shortcut: CustomPropTypes.shortcut,
|
||||
accessKey: PropTypes.string,
|
||||
};
|
||||
@@ -13,12 +13,9 @@ import clsx from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
export const tabPanelStyles = makeStyles((theme)=>({
|
||||
root: {
|
||||
height: '100%',
|
||||
padding: theme.spacing(1),
|
||||
overflow: 'auto',
|
||||
backgroundColor: theme.palette.grey[400]
|
||||
...theme.mixins.tabPanel,
|
||||
},
|
||||
content: {
|
||||
height: '100%',
|
||||
@@ -27,7 +24,7 @@ const useStyles = makeStyles((theme)=>({
|
||||
|
||||
/* Material UI does not have any tabpanel component, we create one for us */
|
||||
export default function TabPanel({children, classNameRoot, className, value, index}) {
|
||||
const classes = useStyles();
|
||||
const classes = tabPanelStyles();
|
||||
const active = value === index;
|
||||
return (
|
||||
<Box className={clsx(classes.root, classNameRoot)} component="div" hidden={!active}>
|
||||
|
||||
Reference in New Issue
Block a user