Added React framework for the properties dialog and port Server Group, Server, and Database dialogs. 

Following changes done for the framework:
 - Framework for creating React based dynamic form view out of a pre-defined UI schema. Previously, it was based on Backform/Backbone.
 - The new framework and components will use MaterialUI as the base. Previously, Bootstrap/Backform/jQuery components were used.
 - The new code uses JSS instead of CSS since material UI and most modern React libraries also use JSS. In the future, this will allow us to change the theme in real-time without refresh.
 - 90% code covered by 80-85 new jasmine test cases.
 - Server group node UI Schema migration to new, with schema test cases.
 - Server node UI Schema migration to new, with schema test cases.
 - Database node UI Schema migration to new, with schema test cases.
 - Few other UI changes.

Fixes #6130
This commit is contained in:
Aditya Toshniwal
2021-06-29 14:33:36 +05:30
committed by Akshay Joshi
parent a10b0c7786
commit 764677431f
63 changed files with 8489 additions and 1181 deletions

View File

@@ -0,0 +1,257 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Box, makeStyles, Tab, Tabs } from '@material-ui/core';
import _ from 'lodash';
import PropTypes from 'prop-types';
import { MappedFormControl } from './MappedControl';
import TabPanel from '../components/TabPanel';
import DataGridView from './DataGridView';
import { SCHEMA_STATE_ACTIONS } from '.';
import { InputSQL } from '../components/FormComponents';
import gettext from 'sources/gettext';
import { evalFunc } from 'sources/utils';
import CustomPropTypes from '../custom_prop_types';
const useStyles = makeStyles((theme)=>({
fullSpace: {
padding: 0,
height: '100%'
},
controlRow: {
paddingBottom: theme.spacing(1),
},
}));
/* Optional SQL tab */
function SQLTab({active, getSQLValue}) {
const [sql, setSql] = useState('Loading...');
useEffect(()=>{
let unmounted = false;
if(active) {
setSql('Loading...');
getSQLValue().then((value)=>{
if(!unmounted) {
setSql(value);
}
});
}
return ()=>{unmounted=true;};
}, [active]);
return <InputSQL
value={sql}
options={{
readOnly: true,
}}
/>;
}
SQLTab.propTypes = {
active: PropTypes.bool,
getSQLValue: PropTypes.func.isRequired,
};
/* The first component of schema view form */
export default function FormView({
value, formErr, schema={}, viewHelperProps, isNested=false, accessPath, dataDispatch, hasSQLTab, getSQLValue, onTabChange, firstEleRef}) {
let defaultTab = 'General';
let tabs = {};
let tabsClassname = {};
const [tabValue, setTabValue] = useState(0);
const classes = useStyles();
const firstElement = useRef();
schema = schema || {fields: []};
/* Calculate the fields which depends on the current field
deps has info on fields which the current field depends on. */
const dependsOnField = useMemo(()=>{
let res = {};
schema.fields.forEach((field)=>{
(field.deps || []).forEach((dep)=>{
res[dep] = res[dep] || [];
res[dep].push(field.id);
});
});
return res;
}, []);
/* Prepare the array of components based on the types */
schema.fields.forEach((f)=>{
let modeSuppoted = true;
if(f.mode) {
modeSuppoted = (f.mode.indexOf(viewHelperProps.mode) > -1);
}
if(modeSuppoted) {
let {visible, disabled, group, readonly, ...field} = f;
group = group || defaultTab;
let verInLimit = (_.isUndefined(viewHelperProps.serverInfo) ? true :
((_.isUndefined(field.server_type) ? true :
(viewHelperProps.serverInfo.type in field.server_type)) &&
(_.isUndefined(field.min_version) ? true :
(viewHelperProps.serverInfo.version >= field.min_version)) &&
(_.isUndefined(field.max_version) ? true :
(viewHelperProps.serverInfo.version <= field.max_version))));
let _readonly = viewHelperProps.inCatalog || (viewHelperProps.mode == 'properties');
if(!_readonly) {
_readonly = evalFunc(readonly, value);
}
let _visible = true;
if(visible) {
_visible = evalFunc(visible, value);
}
_visible = _visible && verInLimit;
disabled = evalFunc(disabled, value);
if(!tabs[group]) tabs[group] = [];
/* Lets choose the path based on type */
if(field.type === 'nested-tab') {
/* Pass on the top schema */
field.schema.top = schema.top;
tabs[group].push(
<FormView key={`nested${tabs[group].length}`} value={value} viewHelperProps={viewHelperProps} formErr={formErr}
schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} />
);
} else if(field.type === 'collection') {
/* Pass on the top schema */
field.schema.top = schema.top;
/* If its a collection, let data grid view handle it */
tabs[group].push(
useMemo(()=><DataGridView key={field.id} value={value[field.id]} viewHelperProps={viewHelperProps} formErr={formErr}
schema={field.schema} accessPath={accessPath.concat(field.id)} dataDispatch={dataDispatch} containerClassName={classes.controlRow}
{...field}/>, [value[field.id]])
);
} else {
/* Its a form control */
const hasError = field.id == formErr.name;
/* When there is a change, the dependent values can change
* lets pass the new changes to dependent and get the new values
* from there as well.
*/
tabs[group].push(
useMemo(()=><MappedFormControl
inputRef={(ele)=>{
if(firstEleRef && !firstEleRef.current) {
firstEleRef.current = ele;
}
}}
key={field.id}
viewHelperProps={viewHelperProps}
name={field.id}
value={value[field.id]}
readonly={_readonly}
disabled={disabled}
visible={_visible}
{...field}
onChange={(value)=>{
/* Get the changes on dependent fields as well */
const depChange = (state)=>{
field.depChange && _.merge(state, field.depChange(state) || {});
(dependsOnField[field.id] || []).forEach((d)=>{
d = _.find(schema.fields, (f)=>f.id==d);
if(d.depChange) {
_.merge(state, d.depChange(state) || {});
}
});
return state;
};
dataDispatch({
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
path: accessPath.concat(field.id),
value: value,
depChange: depChange,
});
}}
hasError={hasError}
className={classes.controlRow}
/>, [
value[field.id],
_readonly,
disabled,
_visible,
hasError,
classes.controlRow,
...(field.deps || []).map((dep)=>value[dep])
])
);
}
}
});
/* Add the SQL tab if required */
let sqlTabActive = false;
if(hasSQLTab) {
let sqlTabName = gettext('SQL');
sqlTabActive = (Object.keys(tabs).length === tabValue);
/* Re-render and fetch the SQL tab when it is active */
tabs[sqlTabName] = [
useMemo(()=><SQLTab key="sqltab" active={sqlTabActive} getSQLValue={getSQLValue} />, [sqlTabActive]),
];
tabsClassname[sqlTabName] = classes.fullSpace;
}
useEffect(()=>{
firstElement.current && firstElement.current.focus();
}, []);
useEffect(()=>{
onTabChange && onTabChange(tabValue, Object.keys(tabs)[tabValue], sqlTabActive);
}, [tabValue]);
return (
<>
<Box>
<Tabs
value={tabValue}
onChange={(event, selTabValue) => {
setTabValue(selTabValue);
}}
// indicatorColor="primary"
variant="scrollable"
scrollButtons="auto"
action={(ref)=>ref && ref.updateIndicator()}
>
{Object.keys(tabs).map((tabName)=>{
return <Tab key={tabName} label={tabName} />;
})}
</Tabs>
</Box>
{Object.keys(tabs).map((tabName, i)=>{
return (
<TabPanel key={tabName} value={tabValue} index={i} classNameRoot={isNested ? classes.fullSpace : tabsClassname[tabName]}>
{tabs[tabName]}
</TabPanel>
);
})}
</>);
}
FormView.propTypes = {
value: PropTypes.any,
formErr: PropTypes.object,
schema: CustomPropTypes.schemaUI.isRequired,
viewHelperProps: PropTypes.object,
isNested: PropTypes.bool,
accessPath: PropTypes.array.isRequired,
dataDispatch: PropTypes.func,
hasSQLTab: PropTypes.bool,
getSQLValue: PropTypes.func,
onTabChange: PropTypes.func,
firstEleRef: CustomPropTypes.ref,
};