Port About dialog to React. Fixes #7567

This commit is contained in:
Akshay Joshi 2022-07-28 10:11:40 +05:30
parent a7fd7d67b3
commit 81f52a82c8
9 changed files with 242 additions and 133 deletions

View File

@ -11,6 +11,7 @@ notes for it.
.. toctree::
:maxdepth: 1
release_notes_6_13
release_notes_6_12
release_notes_6_11
release_notes_6_10

View File

@ -0,0 +1,20 @@
************
Version 6.13
************
Release date: 2022-08-25
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v6.12.
New features
************
Housekeeping
************
| `Issue #7567 <https://redmine.postgresql.org/issues/7567>`_ - Port About dialog to React.
Bug fixes
*********

View File

@ -9,12 +9,13 @@
"""A blueprint module implementing the about box."""
from flask import Response, render_template, url_for, request
from flask import Response, render_template, request
from flask_babel import gettext
from flask_security import current_user, login_required
from pgadmin.utils import PgAdminModule
from pgadmin.utils.menu import MenuItem
from pgadmin.utils.constants import MIMETYPE_APP_JS
from pgadmin.utils.ajax import make_json_response
import config
import httpagentparser
from pgadmin.model import User
@ -66,6 +67,7 @@ def index():
info['os_details'] = os_details
info['config_db'] = config.SQLITE_PATH
info['log_file'] = config.LOG_FILE
info['version'] = config.APP_VERSION
if config.SERVER_MODE:
info['app_mode'] = gettext('Server')
@ -98,8 +100,9 @@ def index():
info['settings'] = settings
return render_template(
MODULE_NAME + '/index.html', info=info, _=gettext
return make_json_response(
data=info,
status=200
)

View File

@ -0,0 +1,151 @@
/////////////////////////////////////////////////////////////
//
// 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 url_for from 'sources/url_for';
import React, { useEffect, useState, useRef } from 'react';
import { Box, Grid, InputLabel } from '@material-ui/core';
import PropTypes from 'prop-types';
import { DefaultButton } from '../../../static/js/components/Buttons';
import { makeStyles } from '@material-ui/styles';
import { InputText } from '../../../static/js/components/FormComponents';
import getApiInstance from '../../../static/js/api_instance';
import { copyToClipboard } from '../../../static/js/clipboard';
import Notify from '../../../static/js/helpers/Notifier';
import { useDelayedCaller } from '../../../static/js/custom_hooks';
const useStyles = makeStyles((theme)=>({
container: {
padding: '16px',
height: '100%',
display: 'flex',
flexDirection: 'column',
},
copyBtn: {
marginRight: '1px',
float: 'right',
borderColor: theme.otherVars.borderColor,
fontSize: '13px',
},
}));
export default function AboutComponent() {
const classes = useStyles();
const containerRef = useRef();
const [aboutData, setAboutData] = useState([]);
const [copyText, setCopyText] = useState(gettext('Copy'));
const revertCopiedText = useDelayedCaller(()=>{
setCopyText(gettext('Copy'));
});
useEffect(() => {
const about_url = url_for('about.index');
const api = getApiInstance();
api.get(about_url).then((res)=>{
setAboutData(res.data.data);
}).catch((err)=>{
Notify.error(err);
});
}, []);
return (
<Box className={classes.container} ref={containerRef}>
<Grid container spacing={0}>
<Grid item lg={3} md={3} sm={3} xs={12}>
<InputLabel style={{fontWeight: 'bold'}}>{gettext('Version')}</InputLabel>
</Grid>
<Grid item lg={9} md={9} sm={9} xs={12}>
<InputLabel>{aboutData.version}</InputLabel>
</Grid>
</Grid>
<Grid container spacing={0}>
<Grid item lg={3} md={3} sm={3} xs={12}>
<InputLabel style={{fontWeight: 'bold'}}>{gettext('Application Mode')}</InputLabel>
</Grid>
<Grid item lg={9} md={9} sm={9} xs={12}>
<InputLabel>{aboutData.app_mode}</InputLabel>
</Grid>
</Grid>
<Grid container spacing={0}>
<Grid item lg={3} md={3} sm={3} xs={12}>
<InputLabel style={{fontWeight: 'bold'}}>{gettext('Current User')}</InputLabel>
</Grid>
<Grid item lg={9} md={9} sm={9} xs={12}>
<InputLabel>{aboutData.current_user}</InputLabel>
</Grid>
</Grid>
{ aboutData.nwjs &&
<Grid container spacing={0}>
<Grid item lg={3} md={3} sm={3} xs={12}>
<InputLabel style={{fontWeight: 'bold'}}>{gettext('NW.js Version')}</InputLabel>
</Grid>
<Grid item lg={9} md={9} sm={9} xs={12}>
<InputLabel>{aboutData.nwjs}</InputLabel>
</Grid>
</Grid>
}
<Grid container spacing={0}>
<Grid item lg={3} md={3} sm={3} xs={12}>
<InputLabel style={{fontWeight: 'bold'}}>{gettext('Browser')}</InputLabel>
</Grid>
<Grid item lg={9} md={9} sm={9} xs={12}>
<InputLabel>{aboutData.browser_details}</InputLabel>
</Grid>
</Grid>
<Grid container spacing={0}>
<Grid item lg={3} md={3} sm={3} xs={12}>
<InputLabel style={{fontWeight: 'bold'}}>{gettext('Operating System')}</InputLabel>
</Grid>
<Grid item lg={9} md={9} sm={9} xs={12}>
<InputLabel>{aboutData.os_details}</InputLabel>
</Grid>
</Grid>
<Grid container spacing={0}>
<Grid item lg={3} md={3} sm={3} xs={12}>
<InputLabel style={{fontWeight: 'bold'}}>{gettext('pgAdmin Database File')}</InputLabel>
</Grid>
<Grid item lg={9} md={9} sm={9} xs={12}>
<InputLabel>{aboutData.config_db}</InputLabel>
</Grid>
</Grid>
<Grid container spacing={0}>
<Grid item lg={3} md={3} sm={3} xs={12}>
<InputLabel style={{fontWeight: 'bold'}}>{gettext('Log File')}</InputLabel>
</Grid>
<Grid item lg={9} md={9} sm={9} xs={12}>
<InputLabel>{aboutData.log_file}</InputLabel>
</Grid>
</Grid>
{ (aboutData.app_mode == 'Desktop' || (aboutData.app_mode == 'Server' && aboutData.admin)) &&
<>
<Box flexGrow="1" display="flex" flexDirection="column">
<Box>
<span style={{fontWeight: 'bold'}}>{gettext('Server Configuration')}</span>
<DefaultButton className={classes.copyBtn} onClick={()=>{
copyToClipboard(aboutData.settings);
setCopyText(gettext('Copied!'));
revertCopiedText(1500);
}}>{copyText}</DefaultButton>
</Box>
<Box flexGrow="1" paddingTop="1px">
<InputText style={{height: '100%'}} controlProps={{multiline: true}} inputStyle={{resize: 'none'}}
value={aboutData.settings}/>
</Box>
</Box>
</>
}
</Box>
);
}
AboutComponent.propTypes = {
closeModal: PropTypes.func
};

View File

@ -7,85 +7,56 @@
//
//////////////////////////////////////////////////////////////
define(
['jquery', 'alertify', 'sources/pgadmin', 'sources/gettext',
'sources/url_for','sources/utils','pgadmin.user_management.current_user',
],
function($, alertify, pgAdmin, gettext, url_for, commonUtils, current_user) {
pgAdmin = pgAdmin || window.pgAdmin || {};
import React from 'react';
import gettext from 'sources/gettext';
import Notify from '../../../static/js/helpers/Notifier';
import pgAdmin from 'sources/pgadmin';
import pgBrowser from 'top/browser/static/js/browser';
import AboutComponent from './AboutComponent';
import current_user from 'pgadmin.user_management.current_user';
/* Return back, this has been called more than once */
if (pgAdmin.About)
class About {
static instance;
static getInstance(...args) {
if (!About.instance) {
About.instance = new About(...args);
}
return About.instance;
}
constructor(pgAdmin, pgBrowser) {
this.pgAdmin = pgAdmin;
this.pgBrowser = pgBrowser;
}
init() {
if (this.initialized)
return;
this.initialized = true;
}
pgAdmin.About = {
about_show: function() {
if (!alertify.aboutDialog) {
alertify.dialog('aboutDialog', function factory() {
return {
main: function(title, message) {
this.set('title', title);
this.message = message;
},
setup: function() {
return {
buttons:[{ text: gettext('OK'), key: 27,
className: 'btn btn-primary fa fa-lg fa-check pg-alertify-button' }],
options: {
modal: false,
resizable: true,
maximizable: true,
pinnable: false,
closableByDimmer: false,
},
};
},
build: function() {
alertify.pgDialogBuild.apply(this);
},
hooks:{
onshow:function(){
var self = this;
var container = $(this.elements.footer).find('button:not([disabled])');
commonUtils.findAndSetFocus(container);
$('#copy_textarea').on('click', function(){
//Copy the server configuration details
let textarea = document.getElementById('about-textarea');
textarea.select();
document.execCommand('copy');
$('#copy_textarea').text('Copied');
});
// This is a callback function to show about dialog.
about_show() {
let dlgHeight = 470,
dlgWidth = 750;
$(this.elements.resizeHandle).on('click', function(){
// Set the height of the Textarea
var height = self.elements.dialog.scrollHeight - 300;
if (height < 0)
height = self.elements.dialog.scrollHeight - 150;
$('#about-textarea').css({'height':height});
});
},
},
prepare:function() {
this.setContent(this.message);
},
};
});
}
if(!current_user.is_admin && pgAdmin.server_mode) {
dlgWidth = pgAdmin.Browser.stdW.md;
dlgHeight = 300;
}
$.get(url_for('about.index'),
function(data) {
if(!current_user.is_admin && pgAdmin.server_mode){
alertify.aboutDialog(
gettext('About %s', pgAdmin.Browser.utils.app_name), data
).resizeTo(pgAdmin.Browser.stdW.md, 300);
}else{
alertify.aboutDialog(
gettext('About %s', pgAdmin.Browser.utils.app_name), data
).resizeTo(750, 470);
}
});
},
};
// Render About component
Notify.showModal(gettext('About %s', pgAdmin.Browser.utils.app_name), () => {
return <AboutComponent />;
}, { isFullScreen: false, isResizeable: true, showFullScreen: true,
isFullWidth: true, dialogWidth: dlgWidth, dialogHeight: dlgHeight, minHeight: dlgHeight
});
}
}
return pgAdmin.About;
});
pgAdmin.About = About.getInstance(pgAdmin, pgBrowser);
module.exports = {
About: About,
};

View File

@ -1,47 +0,0 @@
<div class="container-fluid enable-selection">
<div class="row">
<div class="col-sm-3"><strong>{{ _('Version') }}</strong></div>
<div class="col-sm-9">{{ config.APP_VERSION }}</div>
</div>
<div class="row">
<div class="col-sm-3"><strong>{{ _('Application Mode') }}</strong></div>
<div class="col-sm-9">{{ info.app_mode }}</div>
</div>
<div class="row">
<div class="col-sm-3"><strong>{{ _('Current User') }}</strong></div>
<div class="col-sm-9">{{ info.current_user }}</div>
</div>
{% if info.nwjs %}
<div class="row">
<div class="col-sm-3"><strong>{{ _('NW.js Version') }}</strong></div>
<div class="col-sm-9">{{ info.nwjs }}</div>
</div>
{% endif %}
<div class="row">
<div class="col-sm-3"><strong>{{ _('Browser') }}</strong></div>
<div class="col-sm-9">{{ info.browser_details }}</div>
</div>
<div class="row">
<div class="col-sm-3"><strong>{{ _('Operating System') }}</strong></div>
<div class="col-sm-9">{{ info.os_details }}</div>
</div>
<div class="row">
<div class="col-sm-3"><strong>{{ _('pgAdmin Database File') }}</strong></div>
<div class="col-sm-9">{{ info.config_db }}</div>
</div>
<div class="row">
<div class="col-sm-3"><strong>{{ _('Log File') }}</strong></div>
<div class="col-sm-9">{{ info.log_file }}</div>
</div>
{%if (info.app_mode == 'Desktop') or (info.app_mode == 'Server' and info.admin)%}
<div class="row">
<div class="col-sm-3"><b>{{ _('Server Configuration') }}</b></div>
</div>
<div class="row">
<div class="col-sm-9">
<textarea id="about-textarea" rows="7" cols="150" style="position: relative;width:105%; white-space: pre-wrap;" readonly>{{ info.settings|safe }}</textarea>
<button class="btn btn-secondary about-copy" id="copy_textarea" style="position:absolute;top:0;right:0;z-index:20;">Copy</button>
</div>
</div>
{% endif %}
</div>

View File

@ -122,6 +122,8 @@ basicSettings = createMuiTheme(basicSettings, {
inputMultiline: {
padding: basicSettings.spacing(0.75, 1.5),
resize: 'vertical',
height: '100%',
boxSizing: 'border-box',
},
adornedEnd: {
paddingRight: basicSettings.spacing(0.75),

View File

@ -326,7 +326,7 @@ FormInputDateTimePicker.propTypes = {
/* Use forwardRef to pass ref prop to OutlinedInput */
export const InputText = forwardRef(({
cid, helpid, readonly, disabled, value, onChange, controlProps, type, size, ...props }, ref) => {
cid, helpid, readonly, disabled, value, onChange, controlProps, type, size, inputStyle, ...props }, ref) => {
const maxlength = typeof(controlProps?.maxLength) != 'undefined' ? controlProps.maxLength : 255;
@ -370,7 +370,8 @@ export const InputText = forwardRef(({
id: cid,
maxLength: controlProps?.multiline ? null : maxlength,
'aria-describedby': helpid,
...(type ? { pattern: !_.isUndefined(controlProps) && !_.isUndefined(controlProps.pattern) ? controlProps.pattern : patterns[type] } : {})
...(type ? { pattern: !_.isUndefined(controlProps) && !_.isUndefined(controlProps.pattern) ? controlProps.pattern : patterns[type] } : {}),
style: inputStyle || {}
}}
readOnly={Boolean(readonly)}
disabled={Boolean(disabled)}
@ -399,6 +400,7 @@ InputText.propTypes = {
controlProps: PropTypes.object,
type: PropTypes.string,
size: PropTypes.string,
inputStyle: PropTypes.object
};
export function FormInputText({ hasError, required, label, className, helpMessage, testcid, ...props }) {

View File

@ -155,7 +155,7 @@ const dialogStyle = makeStyles((theme) => ({
}
}));
function PaperComponent(props) {
function PaperComponent({minHeight, minWidth, ...props}) {
let classes = dialogStyle();
let [dialogPosition, setDialogPosition] = useState(null);
let resizeable = props.isresizeable == 'true' ? true : false;
@ -182,8 +182,10 @@ function PaperComponent(props) {
...(props.width && { width: props.width }),
...(props.height && { height: props.height }),
}}
{...(props.width && { minWidth: MIN_WIDTH })}
{...(props.height && { minHeight: MIN_HEIGHT })}
minWidth = { minWidth || MIN_WIDTH }
minHeight = { minHeight || MIN_HEIGHT }
// {...(props.width && { minWidth: MIN_WIDTH })}
// {...(props.height && { minHeight: MIN_HEIGHT })}
bounds="window"
enableResizing={setEnableResizing()}
position={setConditionalPosition()}
@ -215,6 +217,8 @@ PaperComponent.propTypes = {
isresizeable: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
minWidth: PropTypes.number,
minHeight: PropTypes.number,
};
export const useModalStyles = makeStyles((theme) => ({
@ -254,7 +258,7 @@ export const useModalStyles = makeStyles((theme) => ({
}
}));
function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose, fullScreen = false, isFullWidth = false, showFullScreen = false, isResizeable = false }) {
function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose, fullScreen = false, isFullWidth = false, showFullScreen = false, isResizeable = false, minHeight = MIN_HEIGHT, minWidth = MIN_WIDTH }) {
let useModalRef = useModal();
const classes = useModalStyles();
let closeModal = (_e, reason) => {
@ -270,7 +274,7 @@ function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose
open={true}
onClose={closeModal}
PaperComponent={PaperComponent}
PaperProps={{ 'isfullscreen': isfullScreen.toString(), 'isresizeable': isResizeable.toString(), width: dialogWidth, height: dialogHeight }}
PaperProps={{ 'isfullscreen': isfullScreen.toString(), 'isresizeable': isResizeable.toString(), width: dialogWidth, height: dialogHeight, minHeight: minHeight, minWidth: minWidth }}
fullScreen={isfullScreen}
fullWidth={isFullWidth}
disableBackdropClick
@ -309,4 +313,6 @@ ModalContainer.propTypes = {
dialogHeight: PropTypes.number,
dialogWidth: PropTypes.number,
onClose: PropTypes.func,
minWidth: PropTypes.number,
minHeight: PropTypes.number,
};