diff --git a/web/package.json b/web/package.json index 6cdfc1596..22aa90523 100644 --- a/web/package.json +++ b/web/package.json @@ -144,6 +144,7 @@ "react": "^17.0.1", "react-aspen": "^1.1.0", "react-dom": "^17.0.1", + "react-draggable": "^4.4.4", "react-select": "^4.2.1", "react-table": "^7.6.3", "react-virtualized-auto-sizer": "^1.0.6", diff --git a/web/pgadmin/static/js/Theme/index.jsx b/web/pgadmin/static/js/Theme/index.jsx index 08fbb758d..b9f9b1860 100644 --- a/web/pgadmin/static/js/Theme/index.jsx +++ b/web/pgadmin/static/js/Theme/index.jsx @@ -167,7 +167,7 @@ basicSettings = createMuiTheme(basicSettings, { top: 0, zIndex: 9999, } - } + }, }, transitions: { duration: { @@ -199,6 +199,9 @@ basicSettings = createMuiTheme(basicSettings, { }, MuiCheckbox: { disableTouchRipple: true, + }, + MuiDialogTitle: { + disableTypography: true, } }, }); @@ -390,6 +393,22 @@ function getFinalTheme(baseTheme) { color: baseTheme.palette.text.muted, }, }, + MuiDialogContent: { + root: { + padding: 0, + userSelect: 'text', + } + }, + MuiDialogTitle: { + root: { + fontWeight: 'bold', + padding: '5px 10px', + cursor: 'move', + display: 'flex', + alignItems: 'center', + ...mixins.panelBorder.bottom, + } + }, } }, baseTheme); } diff --git a/web/pgadmin/static/js/components/Buttons.jsx b/web/pgadmin/static/js/components/Buttons.jsx index 4c9c14b54..2554692dc 100644 --- a/web/pgadmin/static/js/components/Buttons.jsx +++ b/web/pgadmin/static/js/components/Buttons.jsx @@ -15,6 +15,12 @@ import CustomPropTypes from '../custom_prop_types'; const useStyles = makeStyles((theme)=>({ primaryButton: { + '&.MuiButton-outlinedSizeSmall': { + height: '28px', + '& .MuiSvgIcon-root': { + height: '0.8em', + } + }, '&.Mui-disabled': { color: theme.palette.primary.contrastText, backgroundColor: theme.palette.primary.disabledMain, @@ -28,6 +34,13 @@ const useStyles = makeStyles((theme)=>({ backgroundColor: theme.palette.default.main, 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 @@ -52,35 +65,59 @@ const useStyles = makeStyles((theme)=>({ backgroundColor: theme.custom.icon.hoverMain, color: theme.custom.icon.hoverContrastText, } + }, + xsButton: { + padding: '2px 1px', + height: '24px', + '& .MuiSvgIcon-root': { + height: '0.8em', + } + }, + noBorder: { + border: 0, } })); /* pgAdmin primary button */ export const PrimaryButton = forwardRef((props, ref)=>{ - let {children, className, ...otherProps} = props; + let {children, className, size, noBorder, ...otherProps} = props; const classes = useStyles(); - + let allClassName = [classes.primaryButton, className]; + if(size == 'xs') { + size = undefined; + allClassName.push(classes.xsButton); + } + noBorder && allClassName.push(classes.noBorder); return ( - + ); }); PrimaryButton.displayName = 'PrimaryButton'; PrimaryButton.propTypes = { + size: PropTypes.string, + noBorder: PropTypes.bool, children: CustomPropTypes.children, className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), }; /* pgAdmin default button */ export const DefaultButton = forwardRef((props, ref)=>{ - let {children, className, ...otherProps} = props; + let {children, className, size, noBorder, ...otherProps} = props; const classes = useStyles(); - + let allClassName = [classes.defaultButton, className]; + if(size == 'xs') { + size = undefined; + allClassName.push(classes.xsButton); + } + noBorder && allClassName.push(classes.noBorder); return ( - + ); }); DefaultButton.displayName = 'DefaultButton'; DefaultButton.propTypes = { + size: PropTypes.string, + noBorder: PropTypes.bool, children: CustomPropTypes.children, className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), }; diff --git a/web/pgadmin/static/js/helpers/ModalProvider.jsx b/web/pgadmin/static/js/helpers/ModalProvider.jsx new file mode 100644 index 000000000..c831760b0 --- /dev/null +++ b/web/pgadmin/static/js/helpers/ModalProvider.jsx @@ -0,0 +1,92 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { Box, Dialog, DialogContent, DialogTitle, Paper } from '@material-ui/core'; +import React from 'react'; +import {getEpoch} from 'sources/utils'; +import { PgIconButton } from '../components/Buttons'; +import Draggable from 'react-draggable'; +import CloseIcon from '@material-ui/icons/CloseRounded'; +import CustomPropTypes from '../custom_prop_types'; +import PropTypes from 'prop-types'; + +const ModalContext = React.createContext({}); + +export function useModal() { + return React.useContext(ModalContext); +} + +export default function ModalProvider({children}) { + const [modals, setModals] = React.useState([]); + + const showModal = (title, content, modalOptions)=>{ + let id = getEpoch().toString() + Math.random(); + setModals((prev)=>[...prev, { + id: id, + title: title, + content: content, + ...modalOptions, + }]); + }; + const closeModal = (id)=>{ + setModals((prev)=>{ + return prev.filter((o)=>o.id!=id); + }); + }; + const modalContext = React.useMemo(()=>({ + showModal: showModal, + closeModal: closeModal, + }), []); + return ( + + {children} + {modals.map((modalOptions, i)=>( + + ))} + + ); +} + +ModalProvider.propTypes = { + children: CustomPropTypes.children, +}; + +function PaperComponent(props) { + return ( + + + + ); +} + +function ModalContainer({id, title, content}) { + let useModalRef = useModal(); + let closeModal = ()=>useModalRef.closeModal(id); + return ( + + + {title} + } size="xs" noBorder onClick={closeModal}/> + + + {content(closeModal)} + + + ); +} +ModalContainer.propTypes = { + id: PropTypes.string, + title: CustomPropTypes.children, + content: CustomPropTypes.children, +}; diff --git a/web/pgadmin/static/js/helpers/Notifier.jsx b/web/pgadmin/static/js/helpers/Notifier.jsx index 749c55ce6..98b8848fc 100644 --- a/web/pgadmin/static/js/helpers/Notifier.jsx +++ b/web/pgadmin/static/js/helpers/Notifier.jsx @@ -16,30 +16,54 @@ import CustomPropTypes from '../custom_prop_types'; import gettext from 'sources/gettext'; import pgWindow from 'sources/window'; import Alertify from 'pgadmin.alertifyjs'; +import ModalProvider, { useModal } from './ModalProvider'; +import { DefaultButton, PrimaryButton } from '../components/Buttons'; +import { Box } from '@material-ui/core'; +import { makeStyles } from '@material-ui/styles'; +import HTMLReactParse from 'html-react-parser'; +import CloseIcon from '@material-ui/icons/CloseRounded'; +import CheckRoundedIcon from '@material-ui/icons/CheckRounded'; +import PropTypes from 'prop-types'; const AUTO_HIDE_DURATION = 3000; // In milliseconds let snackbarRef; -function SnackbarUtilsConfigurator() { - snackbarRef = useSnackbar(); - return <>; -} - let notifierInitialized = false; export function initializeNotifier(notifierContainer) { notifierInitialized = true; + const RefLoad = ()=>{ + snackbarRef = useSnackbar(); + return <>; + }; ReactDOM.render( - + , notifierContainer ); } -export const FinalNotifyContent = React.forwardRef(({children}, ref) => { +let modalRef; +let modalInitialized = false; +export function initializeModalProvider(modalContainer) { + modalInitialized = true; + const RefLoad = ()=>{ + modalRef = useModal(); + return <>; + }; + ReactDOM.render( + + + + + , modalContainer + ); +} + +const FinalNotifyContent = React.forwardRef(({children}, ref) => { return {children}; }); FinalNotifyContent.displayName = 'FinalNotifyContent'; @@ -47,6 +71,38 @@ FinalNotifyContent.propTypes = { children: CustomPropTypes.children, }; +const useAlertStyles = makeStyles((theme)=>({ + footer: { + display: 'flex', + justifyContent: 'flex-end', + padding: '0.5rem', + ...theme.mixins.panelBorder.top, + }, + margin: { + marginLeft: '0.25rem', + } +})); +function AlertContent({text, confirm, onOkClick, onCancelClick}) { + const classes = useAlertStyles(); + return ( + + {HTMLReactParse(text)} + + } onClick={onCancelClick} autoFocus={!confirm}>Close + {confirm && + } onClick={onOkClick} autoFocus={confirm}>OK + } + + + ); +} +AlertContent.propTypes = { + text: PropTypes.string, + confirm: PropTypes.bool, + onOkClick: PropTypes.func, + onCancelClick: PropTypes.func, +}; + var Notifier = { success(msg, autoHideDuration = AUTO_HIDE_DURATION) { this._callNotify(msg, MESSAGE_TYPE.SUCCESS, autoHideDuration); @@ -170,7 +226,39 @@ var Notifier = { Alertify.alert().show().set( 'message', msg.replace(new RegExp(/\r?\n/, 'g'), '
') ).set('title', promptmsg).set('closable', true); - } + }, + alert: (title, text, onCancelClick)=>{ + if(!modalInitialized) { + initializeModalProvider(document.getElementById('modalContainer')); + } + modalRef.showModal(title, (closeModal)=>{ + const onCancelClickClose = ()=>{ + onCancelClick && onCancelClick(); + closeModal(); + }; + return ( + + ); + }); + }, + confirm: (title, text, onOkClick, onCancelClick)=>{ + if(!modalInitialized) { + initializeModalProvider(document.getElementById('modalContainer')); + } + modalRef.showModal(title, (closeModal)=>{ + const onCancelClickClose = ()=>{ + onCancelClick && onCancelClick(); + closeModal(); + }; + const onOkClickClose = ()=>{ + onOkClick && onOkClick(); + closeModal(); + }; + return ( + + ); + }); + }, }; if(window.frameElement) { diff --git a/web/pgadmin/templates/base.html b/web/pgadmin/templates/base.html index 8fce32a8e..665cab9e4 100644 --- a/web/pgadmin/templates/base.html +++ b/web/pgadmin/templates/base.html @@ -75,6 +75,7 @@ {% block body %}{% endblock %}
+
diff --git a/web/yarn.lock b/web/yarn.lock index d68f34ea4..a156a8490 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2907,7 +2907,7 @@ closest@^0.0.1: dependencies: matches-selector "0.0.1" -clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: +clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0, clsx@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== @@ -7782,6 +7782,14 @@ react-dom@^17.0.1: object-assign "^4.1.1" scheduler "^0.20.2" +react-draggable@^4.4.4: + version "4.4.4" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.4.tgz#5b26d9996be63d32d285a426f41055de87e59b2f" + integrity sha512-6e0WdcNLwpBx/YIDpoyd2Xb04PB0elrDrulKUgdrIlwuYvxh5Ok9M+F8cljm8kPXXs43PmMzek9RrB1b7mLMqA== + dependencies: + clsx "^1.1.1" + prop-types "^15.6.0" + react-input-autosize@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85"