Port preferences dialog to React. Fixes #7149

This commit is contained in:
Nikhil Mohite
2022-03-21 13:29:26 +05:30
committed by Akshay Joshi
parent 3299b0c1b0
commit 74e794b416
65 changed files with 2646 additions and 1006 deletions

View File

@@ -11,9 +11,11 @@ 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';
import Expand from '../../img/fonticon/expand.svg?svgr';
import Collapse from '../../img/fonticon/minimize_collapse.svg?svgr';
export default function ExternalIcon({Icon, ...props}) {
return <Icon className='MuiSvgIcon-root' {...props} />;
return <Icon className={'MuiSvgIcon-root'} {...props} />;
}
ExternalIcon.propTypes = {
@@ -31,4 +33,6 @@ export const ConnectedIcon = ()=><ExternalIcon Icon={ConnectedSvg} style={{heigh
export const DisonnectedIcon = ()=><ExternalIcon Icon={DisconnectedSvg} style={{height: '0.7em'}} />;
export const RegexIcon = ()=><ExternalIcon Icon={RegexSvg} />;
export const FormatCaseIcon = ()=><ExternalIcon Icon={FormatCaseSvg} />;
export const ExpandDialog = ()=><ExternalIcon Icon={Expand} style={{height: '1em', width: '1em'}} />;
export const MinimizeDialog = ()=><ExternalIcon Icon={Collapse} style={{height: 'auto'}} />;

View File

@@ -10,8 +10,10 @@
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, Select as MuiSelect } from '@material-ui/core';
import {
Box, FormControl, OutlinedInput, FormHelperText,
Grid, IconButton, FormControlLabel, Switch, Checkbox, useTheme, InputLabel, Paper, Select as MuiSelect, Radio,
} 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';
@@ -20,13 +22,14 @@ import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import WarningRoundedIcon from '@material-ui/icons/WarningRounded';
import FolderOpenRoundedIcon from '@material-ui/icons/FolderOpenRounded';
import DescriptionIcon from '@material-ui/icons/Description';
import Select, {components as RSComponents} from 'react-select';
import AssignmentTurnedIn from '@material-ui/icons/AssignmentTurnedIn';
import Select, { components as RSComponents } from 'react-select';
import CreatableSelect from 'react-select/creatable';
import Pickr from '@simonwep/pickr';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import HTMLReactParse from 'html-react-parser';
import { KeyboardDateTimePicker, KeyboardDatePicker, KeyboardTimePicker, MuiPickersUtilsProvider} from '@material-ui/pickers';
import { KeyboardDateTimePicker, KeyboardDatePicker, KeyboardTimePicker, MuiPickersUtilsProvider } from '@material-ui/pickers';
import DateFnsUtils from '@date-io/date-fns';
import * as DateFns from 'date-fns';
@@ -36,6 +39,9 @@ import { showFileDialog } from '../helpers/legacyConnector';
import _ from 'lodash';
import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons';
import CustomPropTypes from '../custom_prop_types';
import KeyboardShortcuts from './KeyboardShortcuts';
import QueryThresholds from './QueryThresholds';
import Themes from './Themes';
const useStyles = makeStyles((theme) => ({
@@ -55,7 +61,7 @@ const useStyles = makeStyles((theme) => ({
margin: theme.spacing(0.75, 0.75, 0.75, 0.75),
display: 'flex',
},
formLabelError: {
formLabelError: {
color: theme.palette.error.main,
},
sql: {
@@ -95,17 +101,17 @@ export const MESSAGE_TYPE = {
};
/* Icon based on MESSAGE_TYPE */
function FormIcon({type, close=false, ...props}) {
function FormIcon({ type, close = false, ...props }) {
let TheIcon = null;
if(close) {
if (close) {
TheIcon = CloseIcon;
} else if(type === MESSAGE_TYPE.SUCCESS) {
} else if (type === MESSAGE_TYPE.SUCCESS) {
TheIcon = CheckRoundedIcon;
} else if(type === MESSAGE_TYPE.ERROR) {
} else if (type === MESSAGE_TYPE.ERROR) {
TheIcon = ErrorRoundedIcon;
} else if(type === MESSAGE_TYPE.INFO) {
} else if (type === MESSAGE_TYPE.INFO) {
TheIcon = InfoRoundedIcon;
} else if(type === MESSAGE_TYPE.WARNING) {
} else if (type === MESSAGE_TYPE.WARNING) {
TheIcon = WarningRoundedIcon;
}
@@ -117,21 +123,21 @@ FormIcon.propTypes = {
};
/* Wrapper on any form component to add label, error indicator and help message */
export function FormInput({children, error, className, label, helpMessage, required, testcid}) {
export function FormInput({ children, error, className, label, helpMessage, required, testcid }) {
const classes = useStyles();
const cid = testcid || _.uniqueId('c');
const helpid = `h${cid}`;
return (
<Grid container spacing={0} className={className}>
<Grid item lg={3} md={3} sm={3} xs={12}>
<InputLabel htmlFor={cid} className={clsx(classes.formLabel, error?classes.formLabelError : null)} required={required}>
<InputLabel htmlFor={cid} className={clsx(classes.formLabel, error ? classes.formLabelError : null)} required={required}>
{label}
<FormIcon type={MESSAGE_TYPE.ERROR} style={{marginLeft: 'auto', visibility: error ? 'unset' : 'hidden'}}/>
<FormIcon type={MESSAGE_TYPE.ERROR} style={{ marginLeft: 'auto', visibility: error ? 'unset' : 'hidden' }} />
</InputLabel>
</Grid>
<Grid item lg={9} md={9} sm={9} xs={12}>
<FormControl error={Boolean(error)} fullWidth>
{React.cloneElement(children, {cid, helpid})}
{React.cloneElement(children, { cid, helpid })}
</FormControl>
<FormHelperText id={helpid} variant="outlined">{HTMLReactParse(helpMessage || '')}</FormHelperText>
</Grid>
@@ -148,17 +154,22 @@ FormInput.propTypes = {
testcid: PropTypes.any,
};
export function InputSQL({value, onChange, className, controlProps, ...props}) {
export function InputSQL({ value, options, onChange, className, controlProps, ...props }) {
const classes = useStyles();
const editor = useRef();
return (
<CodeMirror
currEditor={(obj)=>editor.current=obj}
value={value||''}
currEditor={(obj) => editor.current = obj}
value={value || ''}
options={{
lineNumbers: true,
mode: 'text/x-pgsql',
...options,
}}
className={clsx(classes.sql, className)}
events={{
change: (cm)=>{
change: (cm) => {
onChange && onChange(cm.getValue());
},
}}
@@ -176,13 +187,13 @@ InputSQL.propTypes = {
controlProps: PropTypes.object,
};
export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, noLabel, ...props}) {
if(noLabel) {
return <InputSQL value={value} {...props}/>;
export function FormInputSQL({ hasError, required, label, className, helpMessage, testcid, value, controlProps, noLabel, ...props }) {
if (noLabel) {
return <InputSQL value={value} options={controlProps} {...props} />;
} else {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} >
<InputSQL value={value} {...props}/>
<InputSQL value={value} options={controlProps} {...props} />
</FormInput>
);
}
@@ -208,7 +219,7 @@ const DATE_TIME_FORMAT = {
TIME_24: 'HH:mm:ss',
};
export function InputDateTimePicker({value, onChange, readonly, controlProps, ...props}) {
export function InputDateTimePicker({ value, onChange, readonly, controlProps, ...props }) {
let format = '';
let placeholder = '';
if (controlProps?.pickerType === 'Date') {
@@ -222,15 +233,15 @@ export function InputDateTimePicker({value, onChange, readonly, controlProps, ..
placeholder = controlProps.placeholder || 'YYYY-MM-DD HH:mm:ss Z';
}
const handleChange = (dateVal, stringVal)=> {
const handleChange = (dateVal, stringVal) => {
onChange(stringVal);
};
/* Value should be a date object instead of string */
value = _.isUndefined(value) ? null : value;
if(!_.isNull(value)) {
if (!_.isNull(value)) {
let parseValue = DateFns.parse(value, format, new Date());
if(!DateFns.isValid(parseValue)) {
if (!DateFns.isValid(parseValue)) {
parseValue = DateFns.parseISO(value);
}
value = !DateFns.isValid(parseValue) ? value : parseValue;
@@ -238,7 +249,7 @@ export function InputDateTimePicker({value, onChange, readonly, controlProps, ..
if (readonly) {
return (<InputText value={value ? DateFns.format(value, format) : value}
readonly={readonly} controlProps={{placeholder: controlProps.placeholder}} {...props}/>);
readonly={readonly} controlProps={{ placeholder: controlProps.placeholder }} {...props} />);
}
let commonProps = {
@@ -262,20 +273,20 @@ export function InputDateTimePicker({value, onChange, readonly, controlProps, ..
if (controlProps?.pickerType === 'Date') {
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDatePicker {...commonProps}/>
<KeyboardDatePicker {...commonProps} />
</MuiPickersUtilsProvider>
);
} else if (controlProps?.pickerType === 'Time') {
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardTimePicker {...commonProps}/>
<KeyboardTimePicker {...commonProps} />
</MuiPickersUtilsProvider>
);
}
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<KeyboardDateTimePicker {...commonProps}/>
<KeyboardDateTimePicker {...commonProps} />
</MuiPickersUtilsProvider>
);
}
@@ -287,10 +298,10 @@ InputDateTimePicker.propTypes = {
controlProps: PropTypes.object,
};
export function FormInputDateTimePicker({hasError, required, label, className, helpMessage, testcid, ...props}) {
export function FormInputDateTimePicker({ hasError, required, label, className, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputDateTimePicker {...props}/>
<InputDateTimePicker {...props} />
</FormInput>
);
}
@@ -308,23 +319,23 @@ FormInputDateTimePicker.propTypes = {
/* Use forwardRef to pass ref prop to OutlinedInput */
export const InputText = forwardRef(({
cid, helpid, readonly, disabled, maxlength=255, value, onChange, controlProps, type, ...props}, ref)=>{
cid, helpid, readonly, disabled, maxlength = 255, value, onChange, controlProps, type, ...props }, ref) => {
const classes = useStyles();
const patterns = {
'numeric': '^-?[0-9]\\d*\\.?\\d*$',
'int': '^-?[0-9]\\d*$',
};
let onChangeFinal = (e)=>{
let onChangeFinal = (e) => {
let changeVal = e.target.value;
/* For type number, we set type as tel with number regex to get validity.*/
if(['numeric', 'int', 'tel'].indexOf(type) > -1) {
if(!e.target.validity.valid && changeVal !== '' && changeVal !== '-') {
if (['numeric', 'int', 'tel'].indexOf(type) > -1) {
if (!e.target.validity.valid && changeVal !== '' && changeVal !== '-') {
return;
}
}
if(controlProps?.formatter) {
if (controlProps?.formatter) {
changeVal = controlProps.formatter.toRaw(changeVal);
}
onChange && onChange(changeVal);
@@ -332,11 +343,11 @@ export const InputText = forwardRef(({
let finalValue = (_.isNull(value) || _.isUndefined(value)) ? '' : value;
if(controlProps?.formatter) {
if (controlProps?.formatter) {
finalValue = controlProps.formatter.fromRaw(finalValue);
}
return(
return (
<OutlinedInput
ref={ref}
color="primary"
@@ -346,7 +357,7 @@ 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] } : {})
}}
readOnly={Boolean(readonly)}
disabled={Boolean(disabled)}
@@ -354,9 +365,12 @@ export const InputText = forwardRef(({
notched={false}
value={(_.isNull(finalValue) || _.isUndefined(finalValue)) ? '' : finalValue}
onChange={onChangeFinal}
{
...(controlProps?.onKeyDown && { onKeyDown: controlProps.onKeyDown })
}
{...controlProps}
{...props}
{...(['numeric', 'int'].indexOf(type) > -1 ? {type: 'tel'} : {type: type})}
{...(['numeric', 'int'].indexOf(type) > -1 ? { type: 'tel' } : { type: type })}
/>
);
});
@@ -374,10 +388,10 @@ InputText.propTypes = {
type: PropTypes.string,
};
export function FormInputText({hasError, required, label, className, helpMessage, testcid, ...props}) {
export function FormInputText({ hasError, required, label, className, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputText label={label} {...props}/>
<InputText label={label} {...props} />
</FormInput>
);
}
@@ -391,16 +405,21 @@ FormInputText.propTypes = {
};
/* Using the existing file dialog functions using showFileDialog */
export function InputFileSelect({controlProps, onChange, disabled, readonly, ...props}) {
export function InputFileSelect({ controlProps, onChange, disabled, readonly, isvalidate = false, validate, ...props }) {
const inpRef = useRef();
const onFileSelect = (value)=>{
const onFileSelect = (value) => {
onChange && onChange(decodeURI(value));
inpRef.current.focus();
};
return (
<InputText ref={inpRef} disabled={disabled} readonly={readonly} onChange={onChange} {...props} endAdornment={
<IconButton onClick={()=>showFileDialog(controlProps, onFileSelect)}
disabled={disabled||readonly} aria-label={gettext('Select a file')}><FolderOpenRoundedIcon /></IconButton>
<>
<IconButton onClick={() => showFileDialog(controlProps, onFileSelect)}
disabled={disabled || readonly} aria-label={gettext('Select a file')}><FolderOpenRoundedIcon /></IconButton>
{isvalidate &&
<PgIconButton title={gettext('Validate')} style={{ border: 'none' }} disabled={!props.value} onClick={() => { validate(props.value); }} icon={<AssignmentTurnedIn />}></PgIconButton>
}
</>
} />
);
}
@@ -409,14 +428,17 @@ InputFileSelect.propTypes = {
onChange: PropTypes.func,
disabled: PropTypes.bool,
readonly: PropTypes.bool,
isvalidate: PropTypes.bool,
validate: PropTypes.func,
value: PropTypes.string
};
export function FormInputFileSelect({
hasError, required, label, className, helpMessage, testcid, ...props}) {
hasError, required, label, className, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputFileSelect required={required} label={label} {...props}/>
<InputFileSelect required={required} label={label} {...props} />
</FormInput>
);
}
@@ -429,13 +451,13 @@ FormInputFileSelect.propTypes = {
testcid: PropTypes.string,
};
export function InputSwitch({cid, helpid, value, onChange, readonly, controlProps, ...props}) {
export function InputSwitch({ cid, helpid, value, onChange, readonly, controlProps, ...props }) {
const classes = useStyles();
return (
<Switch color="primary"
checked={Boolean(value)}
onChange={
readonly ? ()=>{/*This is intentional (SonarQube)*/} : onChange
readonly ? () => {/*This is intentional (SonarQube)*/ } : onChange
}
id={cid}
inputProps={{
@@ -457,11 +479,11 @@ InputSwitch.propTypes = {
controlProps: PropTypes.object,
};
export function FormInputSwitch({hasError, required, label, className, helpMessage, testcid, ...props}) {
export function FormInputSwitch({ hasError, required, label, className, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputSwitch {...props}/>
<InputSwitch {...props} />
</FormInput>
);
}
@@ -474,7 +496,7 @@ FormInputSwitch.propTypes = {
testcid: PropTypes.string,
};
export function InputCheckbox({cid, helpid, value, onChange, controlProps, readonly, ...props}) {
export function InputCheckbox({ cid, helpid, value, onChange, controlProps, readonly, ...props }) {
controlProps = controlProps || {};
return (
<FormControlLabel
@@ -482,12 +504,13 @@ export function InputCheckbox({cid, helpid, value, onChange, controlProps, reado
<Checkbox
id={cid}
checked={Boolean(value)}
onChange={readonly ? ()=>{/*This is intentional (SonarQube)*/} : onChange}
onChange={readonly ? () => {/*This is intentional (SonarQube)*/ } : onChange}
color="primary"
inputProps={{'aria-describedby': helpid}}
{...props}/>
inputProps={{ 'aria-describedby': helpid }}
{...props} />
}
label={controlProps.label}
labelPlacement={props?.labelPlacement ? props.labelPlacement : 'end'}
/>
);
}
@@ -498,14 +521,15 @@ InputCheckbox.propTypes = {
controlProps: PropTypes.object,
onChange: PropTypes.func,
readonly: PropTypes.bool,
labelPlacement: PropTypes.string
};
export function FormInputCheckbox({hasError, required, label,
className, helpMessage, testcid, ...props}) {
export function FormInputCheckbox({ hasError, required, label,
className, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputCheckbox {...props}/>
<InputCheckbox {...props} />
</FormInput>
);
}
@@ -518,24 +542,61 @@ FormInputCheckbox.propTypes = {
testcid: PropTypes.string,
};
export function InputRadio({ helpid, value, onChange, controlProps, readonly, ...props }) {
const classes = useStyles();
controlProps = controlProps || {};
return (
<FormControlLabel
control={
<Radio
color="primary"
checked={props?.disabled ? false : value }
onChange={
readonly ? () => {
/*This is intentional (SonarQube)*/ } : onChange
}
value={value}
name="radio-button-demo"
inputProps={{ 'aria-label': value, 'aria-describedby': helpid }}
style={{ padding: 0 }}
disableRipple
{...props}
/>
}
label={controlProps.label}
className={(readonly || props.disabled) ? classes.readOnlySwitch : null}
/>
);
}
InputRadio.propTypes = {
helpid: PropTypes.string,
value: PropTypes.bool,
controlProps: PropTypes.object,
onChange: PropTypes.func,
readonly: PropTypes.bool,
disabled: PropTypes.bool,
labelPlacement: PropTypes.string
};
export const InputToggle = forwardRef(({cid, value, onChange, options, disabled, readonly, ...props}, ref) => {
export const InputToggle = forwardRef(({ cid, value, onChange, options, disabled, readonly, ...props }, ref) => {
return (
<ToggleButtonGroup
id={cid}
value={value}
exclusive
onChange={(e, val)=>{val!==null && onChange(val);}}
onChange={(e, val) => { val !== null && onChange(val); }}
{...props}
>
{
(options||[]).map((option, i)=>{
(options || []).map((option, i) => {
const isSelected = option.value === value;
const isDisabled = disabled || option.disabled || (readonly && !isSelected);
return (
<ToggleButton ref={i==0 ? ref : null} key={option.label} value={option.value} component={isSelected ? PrimaryButton : DefaultButton}
<ToggleButton ref={i == 0 ? ref : null} key={option.label} value={option.value} component={isSelected ? PrimaryButton : DefaultButton}
disabled={isDisabled} aria-label={option.label}>
<CheckRoundedIcon style={{visibility: isSelected ? 'visible': 'hidden'}}/>&nbsp;{option.label}
<CheckRoundedIcon style={{ visibility: isSelected ? 'visible' : 'hidden' }} />&nbsp;{option.label}
</ToggleButton>
);
})
@@ -554,11 +615,11 @@ InputToggle.propTypes = {
readonly: PropTypes.bool,
};
export function FormInputToggle({hasError, required, label,
className, helpMessage, testcid, inputRef, ...props}) {
export function FormInputToggle({ hasError, required, label,
className, helpMessage, testcid, inputRef, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputToggle ref={inputRef} {...props}/>
<InputToggle ref={inputRef} {...props} />
</FormInput>
);
}
@@ -575,9 +636,9 @@ FormInputToggle.propTypes = {
/* react-select package is used for select input
* Customizing the select styles to fit existing theme
*/
const customReactSelectStyles = (theme, readonly)=>({
const customReactSelectStyles = (theme, readonly) => ({
input: (provided) => {
return {...provided, padding: 0, margin: 0, color: 'inherit'};
return { ...provided, padding: 0, margin: 0, color: 'inherit' };
},
singleValue: (provided) => {
return {
@@ -593,35 +654,35 @@ const customReactSelectStyles = (theme, readonly)=>({
borderColor: theme.otherVars.inputBorderColor,
...(state.isFocused ? {
borderColor: theme.palette.primary.main,
boxShadow: 'inset 0 0 0 1px '+theme.palette.primary.main,
boxShadow: 'inset 0 0 0 1px ' + theme.palette.primary.main,
'&:hover': {
borderColor: theme.palette.primary.main,
}
} : {}),
}),
dropdownIndicator: (provided)=>({
dropdownIndicator: (provided) => ({
...provided,
padding: '0rem 0.25rem',
}),
indicatorsContainer: (provided)=>({
indicatorsContainer: (provided) => ({
...provided,
...(readonly ? {display: 'none'} : {})
...(readonly ? { display: 'none' } : {})
}),
clearIndicator: (provided)=>({
clearIndicator: (provided) => ({
...provided,
padding: '0rem 0.25rem',
}),
valueContainer: (provided)=>({
valueContainer: (provided) => ({
...provided,
padding: theme.otherVars.reactSelect.padding,
}),
groupHeading: (provided)=>({
groupHeading: (provided) => ({
...provided,
color: 'inherit',
fontSize: '0.85em',
textTransform: 'none',
}),
menu: (provided)=>({
menu: (provided) => ({
...provided,
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
@@ -629,12 +690,12 @@ const customReactSelectStyles = (theme, readonly)=>({
border: '1px solid ' + theme.otherVars.inputBorderColor,
marginTop: '2px',
}),
menuPortal: (provided)=>({
menuPortal: (provided) => ({
...provided, zIndex: 9999,
backgroundColor: 'inherit',
color: 'inherit',
}),
option: (provided, state)=>{
option: (provided, state) => {
let bgColor = 'inherit';
if (state.isFocused) {
bgColor = theme.palette.grey[400];
@@ -648,27 +709,27 @@ const customReactSelectStyles = (theme, readonly)=>({
backgroundColor: bgColor,
};
},
multiValue: (provided)=>({
multiValue: (provided) => ({
...provided,
backgroundColor: theme.palette.grey[400],
}),
multiValueLabel: (provided)=>({
multiValueLabel: (provided) => ({
...provided,
fontSize: '1em',
zIndex: 99,
color: theme.palette.text.primary
}),
multiValueRemove: (provided)=>({
multiValueRemove: (provided) => ({
...provided,
'&:hover': {
backgroundColor: 'unset',
color: theme.palette.error.main,
},
...(readonly ? {display: 'none'} : {})
...(readonly ? { display: 'none' } : {})
}),
});
function OptionView({image, label}) {
function OptionView({ image, label }) {
const classes = useStyles();
return (
<>
@@ -705,8 +766,8 @@ CustomSelectSingleValue.propTypes = {
};
export function flattenSelectOptions(options) {
return _.flatMap(options, (option)=>{
if(option.options) {
return _.flatMap(options, (option) => {
if (option.options) {
return option.options;
} else {
return option;
@@ -716,28 +777,28 @@ export function flattenSelectOptions(options) {
function getRealValue(options, value, creatable, formatter) {
let realValue = null;
if(_.isArray(value)) {
if (_.isArray(value)) {
realValue = [...value];
/* If multi select options need to be in some format by UI, use formatter */
if(formatter) {
if (formatter) {
realValue = formatter.fromRaw(realValue, options);
} else {
if(creatable) {
realValue = realValue.map((val)=>({label:val, value: val}));
if (creatable) {
realValue = realValue.map((val) => ({ label: val, value: val }));
} else {
realValue = realValue.map((val)=>(_.find(options, (option)=>_.isEqual(option.value, val))));
realValue = realValue.map((val) => (_.find(options, (option) => _.isEqual(option.value, val))));
}
}
} else {
let flatOptions = flattenSelectOptions(options);
realValue = _.find(flatOptions, (option)=>option.value==value) ||
(creatable && !_.isUndefined(value) && !_.isNull(value) ? {label:value, value: value} : null);
realValue = _.find(flatOptions, (option) => option.value == value) ||
(creatable && !_.isUndefined(value) && !_.isNull(value) ? { label: value, value: value } : null);
}
return realValue;
}
export function InputSelectNonSearch({options, ...props}) {
export function InputSelectNonSearch({ options, ...props }) {
return <MuiSelect native {...props} variant="outlined">
{(options||[]).map((o)=><option key={o.value} value={o.value}>{o.label}</option>)}
{(options || []).map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
</MuiSelect>;
}
InputSelectNonSearch.propTypes = {
@@ -748,7 +809,7 @@ InputSelectNonSearch.propTypes = {
};
export const InputSelect = forwardRef(({
cid, onChange, options, readonly=false, value, controlProps={}, optionsLoaded, optionsReloadBasis, disabled, ...props}, ref) => {
cid, onChange, options, readonly = false, value, controlProps = {}, optionsLoaded, optionsReloadBasis, disabled, ...props }, ref) => {
const [[finalOptions, isLoading], setFinalOptions] = useState([[], true]);
const theme = useTheme();
@@ -757,33 +818,33 @@ export const InputSelect = forwardRef(({
loading the options. optionsReloadBasis is helpful to avoid repeated
options load. If optionsReloadBasis value changes, then options will be loaded again.
*/
useEffect(()=>{
let optPromise = options, umounted=false;
if(typeof options === 'function') {
useEffect(() => {
let optPromise = options, umounted = false;
if (typeof options === 'function') {
optPromise = options();
}
setFinalOptions([[], true]);
Promise.resolve(optPromise)
.then((res)=>{
.then((res) => {
/* If component unmounted, dont update state */
if(!umounted) {
if (!umounted) {
optionsLoaded && optionsLoaded(res, value);
/* Auto select if any option has key as selected */
const flatRes = flattenSelectOptions(res || []);
let selectedVal;
if(controlProps.multiple) {
selectedVal = _.filter(flatRes, (o)=>o.selected)?.map((o)=>o.value);
if (controlProps.multiple) {
selectedVal = _.filter(flatRes, (o) => o.selected)?.map((o) => o.value);
} else {
selectedVal = _.find(flatRes, (o)=>o.selected)?.value;
selectedVal = _.find(flatRes, (o) => o.selected)?.value;
}
if((!_.isUndefined(selectedVal) && !_.isArray(selectedVal)) || (_.isArray(selectedVal) && selectedVal.length != 0)) {
if ((!_.isUndefined(selectedVal) && !_.isArray(selectedVal)) || (_.isArray(selectedVal) && selectedVal.length != 0)) {
onChange && onChange(selectedVal);
}
setFinalOptions([res || [], false]);
}
});
return ()=>umounted=true;
return () => umounted = true;
}, [optionsReloadBasis]);
@@ -791,7 +852,7 @@ export const InputSelect = forwardRef(({
const filteredOptions = (controlProps.filter && controlProps.filter(finalOptions)) || finalOptions;
const flatFiltered = flattenSelectOptions(filteredOptions);
let realValue = getRealValue(flatFiltered, value, controlProps.creatable, controlProps.formatter);
if(realValue && _.isPlainObject(realValue) && _.isUndefined(realValue.value)) {
if (realValue && _.isPlainObject(realValue) && _.isUndefined(realValue.value)) {
console.error('Undefined option value not allowed', realValue, filteredOptions);
}
const otherProps = {
@@ -802,17 +863,17 @@ export const InputSelect = forwardRef(({
const styles = customReactSelectStyles(theme, readonly || disabled);
const onChangeOption = useCallback((selectVal)=>{
if(_.isArray(selectVal)) {
const onChangeOption = useCallback((selectVal) => {
if (_.isArray(selectVal)) {
// Check if select all option is selected
if (!_.isUndefined(selectVal.find(x => x.label === 'Select All'))) {
selectVal = filteredOptions;
}
/* If multi select options need to be in some format by UI, use formatter */
if(controlProps.formatter) {
if (controlProps.formatter) {
selectVal = controlProps.formatter.toRaw(selectVal, filteredOptions);
} else {
selectVal = selectVal.map((option)=>option.value);
selectVal = selectVal.map((option) => option.value);
}
onChange && onChange(selectVal);
} else {
@@ -838,13 +899,13 @@ export const InputSelect = forwardRef(({
...otherProps,
...props,
};
if(!controlProps.creatable) {
if (!controlProps.creatable) {
return (
<Select ref={ref} {...commonProps}/>
<Select ref={ref} {...commonProps} />
);
} else {
return (
<CreatableSelect ref={ref} {...commonProps}/>
<CreatableSelect ref={ref} {...commonProps} />
);
}
});
@@ -863,10 +924,10 @@ InputSelect.propTypes = {
export function FormInputSelect({
hasError, required, className, label, helpMessage, testcid, ...props}) {
hasError, required, className, label, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputSelect ref={props.inputRef} {...props}/>
<InputSelect ref={props.inputRef} {...props} />
</FormInput>
);
}
@@ -881,7 +942,7 @@ FormInputSelect.propTypes = {
};
/* React wrapper on color pickr */
export function InputColor({value, controlProps, disabled, onChange, currObj}) {
export function InputColor({ value, controlProps, disabled, onChange, currObj }) {
const pickrOptions = {
showPalette: true,
allowEmpty: true,
@@ -896,19 +957,19 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) {
const pickrObj = useRef();
const classes = useStyles();
const setColor = (newVal)=>{
const setColor = (newVal) => {
pickrObj.current &&
pickrObj.current.setColor((_.isUndefined(newVal) || newVal == '') ? pickrOptions.defaultColor : newVal);
};
const destroyPickr = ()=>{
if(pickrObj.current) {
const destroyPickr = () => {
if (pickrObj.current) {
pickrObj.current.destroy();
pickrObj.current = null;
}
};
const initPickr = ()=>{
const initPickr = () => {
/* pickr does not have way to update options, need to
destroy and recreate pickr to reflect options */
destroyPickr();
@@ -920,7 +981,7 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) {
swatches: [
'#000', '#666', '#ccc', '#fff', '#f90', '#ff0', '#0f0',
'#f0f', '#f4cccc', '#fce5cd', '#d0e0e3', '#cfe2f3', '#ead1dc', '#ea9999',
'#b6d7a8', '#a2c4c9', '#d5a6bd', '#e06666','#93c47d', '#76a5af', '#c27ba0',
'#b6d7a8', '#a2c4c9', '#d5a6bd', '#e06666', '#93c47d', '#76a5af', '#c27ba0',
'#f1c232', '#6aa84f', '#45818e', '#a64d79', '#bf9000', '#0c343d', '#4c1130',
],
position: pickrOptions.position,
@@ -941,20 +1002,20 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) {
setColor(value);
disabled && instance.disable();
const {lastColor} = instance.getRoot().preview;
const {clear} = instance.getRoot().interaction;
const { lastColor } = instance.getRoot().preview;
const { clear } = instance.getRoot().interaction;
/* Cycle the keyboard navigation within the color picker */
clear.addEventListener('keydown', (e)=>{
if(e.keyCode === 9) {
clear.addEventListener('keydown', (e) => {
if (e.keyCode === 9) {
e.preventDefault();
e.stopPropagation();
lastColor.focus();
}
});
lastColor.addEventListener('keydown', (e)=>{
if(e.keyCode === 9 && e.shiftKey) {
lastColor.addEventListener('keydown', (e) => {
if (e.keyCode === 9 && e.shiftKey) {
e.preventDefault();
e.stopPropagation();
clear.focus();
@@ -965,32 +1026,32 @@ export function InputColor({value, controlProps, disabled, onChange, currObj}) {
}).on('change', (color) => {
onChange && onChange(color.toHEXA().toString());
}).on('show', (color, instance) => {
const {palette} = instance.getRoot().palette;
const { palette } = instance.getRoot().palette;
palette.focus();
}).on('hide', (instance) => {
const button = instance.getRoot().button;
button.focus();
});
if(currObj) {
if (currObj) {
currObj(pickrObj.current);
}
};
useEffect(()=>{
useEffect(() => {
initPickr();
return ()=>{
return () => {
destroyPickr();
};
}, [...Object.values(pickrOptions)]);
useEffect(()=>{
if(pickrObj.current) {
useEffect(() => {
if (pickrObj.current) {
setColor(value);
}
}, [value]);
let btnStyles = {backgroundColor: value};
let btnStyles = { backgroundColor: value };
return (
<PgIconButton ref={eleRef} title={gettext('Select the color')} className={classes.colorBtn} style={btnStyles} disabled={pickrOptions.disabled}
icon={(_.isUndefined(value) || _.isNull(value) || value === '') && <CloseIcon />}
@@ -1006,11 +1067,11 @@ InputColor.propTypes = {
};
export function FormInputColor({
hasError, required, className, label, helpMessage, testcid, ...props}) {
hasError, required, className, label, helpMessage, testcid, ...props }) {
return (
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<InputColor {...props}/>
<InputColor {...props} />
</FormInput>
);
}
@@ -1023,9 +1084,9 @@ FormInputColor.propTypes = {
testcid: PropTypes.string,
};
export function PlainString({controlProps, value}) {
export function PlainString({ controlProps, value }) {
let finalValue = value;
if(controlProps?.formatter) {
if (controlProps?.formatter) {
finalValue = controlProps.formatter.fromRaw(finalValue);
}
return <span>{finalValue}</span>;
@@ -1035,7 +1096,7 @@ PlainString.propTypes = {
value: PropTypes.any,
};
export function FormNote({text, className}) {
export function FormNote({ text, className }) {
const classes = useStyles();
return (
<Box className={className}>
@@ -1051,7 +1112,7 @@ FormNote.propTypes = {
className: CustomPropTypes.className,
};
const useStylesFormFooter = makeStyles((theme)=>({
const useStylesFormFooter = makeStyles((theme) => ({
root: {
padding: theme.spacing(0.5),
position: 'absolute',
@@ -1108,7 +1169,7 @@ const useStylesFormFooter = makeStyles((theme)=>({
export function FormFooterMessage(props) {
const classes = useStylesFormFooter();
if(!props.message) {
if (!props.message) {
return <></>;
}
return (
@@ -1122,15 +1183,81 @@ FormFooterMessage.propTypes = {
message: PropTypes.string,
};
export function NotifierMessage({type=MESSAGE_TYPE.SUCCESS, message, closable=true, onClose=()=>{/*This is intentional (SonarQube)*/}}) {
const useStylesKeyboardShortcut = makeStyles(() => ({
customRow: {
paddingTop: 5
}
}));
export function FormInputKeyboardShortcut({ hasError, label, className, helpMessage, testcid, onChange, ...props }) {
const cid = _.uniqueId('c');
const helpid = `h${cid}`;
const classes = useStylesKeyboardShortcut();
return (
<FormInput label={label} error={hasError} className={clsx(classes.customRow, className)} helpMessage={helpMessage} testcid={testcid}>
<KeyboardShortcuts cid={cid} helpid={helpid} onChange={onChange} {...props} />
</FormInput>
);
}
FormInputKeyboardShortcut.propTypes = {
hasError: PropTypes.bool,
label: PropTypes.string,
className: CustomPropTypes.className,
helpMessage: PropTypes.string,
testcid: PropTypes.string,
onChange: PropTypes.func
};
export function FormInputQueryThreshold({ hasError, label, className, helpMessage, testcid, onChange, ...props }) {
const cid = _.uniqueId('c');
const helpid = `h${cid}`;
return (
<FormInput label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<QueryThresholds cid={cid} helpid={helpid} onChange={onChange} {...props} />
</FormInput>
);
}
FormInputQueryThreshold.propTypes = {
hasError: PropTypes.bool,
label: PropTypes.string,
className: CustomPropTypes.className,
helpMessage: PropTypes.string,
testcid: PropTypes.string,
onChange: PropTypes.func
};
export function FormInputThemes({ hasError, label, className, helpMessage, testcid, onChange, ...props }) {
const cid = _.uniqueId('c');
const helpid = `h${cid}`;
return (
<FormInput label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
<Themes cid={cid} helpid={helpid} onChange={onChange} {...props} />
</FormInput>
);
}
FormInputThemes.propTypes = {
hasError: PropTypes.bool,
label: PropTypes.string,
className: CustomPropTypes.className,
helpMessage: PropTypes.string,
testcid: PropTypes.string,
onChange: PropTypes.func
};
export function NotifierMessage({ type = MESSAGE_TYPE.SUCCESS, message, closable = true, onClose = () => {/*This is intentional (SonarQube)*/ } }) {
const classes = useStylesFormFooter();
return (
<Box className={clsx(classes.container, classes[`container${type}`])}>
<FormIcon type={type} className={classes[`icon${type}`]}/>
<FormIcon type={type} className={classes[`icon${type}`]} />
<Box className={classes.message}>{message}</Box>
{closable && <IconButton className={clsx(classes.closeButton, classes[`icon${type}`])} onClick={onClose}>
<FormIcon close={true}/>
<FormIcon close={true} />
</IconButton>}
</Box>
);

View File

@@ -0,0 +1,130 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import { makeStyles, Grid, Typography, Box } from '@material-ui/core';
import React from 'react';
import { InputCheckbox, InputText } from './FormComponents';
import PropTypes from 'prop-types';
const useStyles = makeStyles((theme) => ({
inputLabel: {
textAlign: 'center',
padding: 2,
paddingLeft: 10
},
inputCheckboxClass: {
border: '1px solid',
borderRadius: theme.shape.borderRadius,
padding: 3
}
}));
export default function KeyboardShortcuts({ value, onChange, fields }) {
const classes = useStyles();
const keyCid = _.uniqueId('c');
const keyhelpid = `h${keyCid}`;
const shiftCid = _.uniqueId('c');
const shifthelpid = `h${shiftCid}`;
const ctrlCid = _.uniqueId('c');
const ctrlhelpid = `h${ctrlCid}`;
const altCid = _.uniqueId('c');
const althelpid = `h${altCid}`;
const onKeyDown = (e) => {
let newVal = { ...value };
let _val = e.key;
if (e.keyCode == 32) {
_val = 'Space';
}
newVal.key = {
char: _val,
key_code: e.keyCode
};
onChange(newVal);
};
const onShiftChange = (e) => {
let newVal = { ...value };
newVal.shift = e.target.checked;
onChange(newVal);
};
const onCtrlChange = (e) => {
let newVal = { ...value };
newVal.ctrl = e.target.checked;
onChange(newVal);
};
const onAltChange = (e) => {
let newVal = { ...value };
newVal.alt = e.target.checked;
onChange(newVal);
};
return (
<Grid
container
direction="row"
justifyContent="center"
alignItems="center">
{fields.map(element => {
let ctrlProps = {
label: element.label
};
if (element.type == 'keyCode') {
return <Grid item container lg={4} md={4} sm={4} xs={12}>
<Grid item lg={4} md={4} sm={4} xs={12} className={classes.inputLabel}>
<Typography>{element.label}</Typography>
</Grid>
<Grid item lg={8} md={8} sm={8} xs={12}>
<InputText cid={keyCid} helpid={keyhelpid} type='text' value={value?.key?.char} controlProps={
{
onKeyDown: onKeyDown,
}
}></InputText>
</Grid>
</Grid>;
} else if (element.name == 'shift') {
return <Grid item lg={2} md={2} sm={2} xs={12} className={classes.inputLabel}>
<Box className={classes.inputCheckboxClass}>
<InputCheckbox cid={shiftCid} helpid={shifthelpid} value={value?.shift}
controlProps={ctrlProps}
onChange={onShiftChange} labelPlacement="end" ></InputCheckbox>
</Box>
</Grid>;
} else if (element.name == 'control') {
return <Grid item lg={2} md={2} sm={2} xs={12} className={classes.inputLabel}>
<Box className={classes.inputCheckboxClass}>
<InputCheckbox cid={ctrlCid} helpid={ctrlhelpid} value={value?.ctrl}
controlProps={ctrlProps}
onChange={onCtrlChange} labelPlacement="end" ></InputCheckbox>
</Box>
</Grid>;
} else if (element.name == 'alt') {
return <Grid item lg={3} md={3} sm={3} xs={12} className={classes.inputLabel}>
<Box className={classes.inputCheckboxClass}>
<InputCheckbox cid={altCid} helpid={althelpid} value={value?.alt}
controlProps={ctrlProps}
onChange={onAltChange} labelPlacement="end" ></InputCheckbox>
</Box>
</Grid>;
}
})}
</Grid>
);
}
KeyboardShortcuts.propTypes = {
value: PropTypes.object,
onChange: PropTypes.func,
controlProps: PropTypes.object,
fields: PropTypes.array
};

View File

@@ -0,0 +1,90 @@
/////////////////////////////////////////////////////////////
//
// 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 _ from 'lodash';
import { FormGroup, makeStyles, Grid, Typography } from '@material-ui/core';
import React from 'react';
import { InputText } from './FormComponents';
import PropTypes from 'prop-types';
const useStyles = makeStyles(() => ({
formControlLabel: {
padding: '3px',
},
formInput: {
marginLeft: '5px'
},
formCheckboxControl: {
padding: '3px',
border: '1px solid',
borderRadius: '0.25rem',
},
formGroup: {
padding: '5px'
},
contentTextAlign: {
textAlign: 'center'
},
contentStyle: {
paddingLeft: 10,
}
}));
export default function QueryThresholds({ value, onChange }) {
const classes = useStyles();
const warningCid = _.uniqueId('c');
const warninghelpid = `h${warningCid}`;
const alertCid = _.uniqueId('c');
const alerthelpid = `h${alertCid}`;
const onWarningChange = (val) => {
let new_val = { ...value };
new_val['warning'] = val;
onChange(new_val);
};
const onAlertChange = (val) => {
let new_val = { ...value };
new_val['alert'] = val;
onChange(new_val);
};
return (
<FormGroup>
<Grid
container
direction="row"
justifyContent="center"
alignItems="center"
>
<Grid item lg={2} md={2} sm={2} xs={12}>
<Typography>{gettext('Warning')}</Typography>
</Grid>
<Grid item lg={2} md={2} sm={2} xs={12}>
<InputText cid={warningCid} helpid={warninghelpid} type='numeric' value={value?.warning} onChange={onWarningChange} />
</Grid>
<Grid item lg={2} md={2} sm={2} xs={12} className={classes.contentTextAlign}>
<Typography>{gettext('Alert')}</Typography>
</Grid>
<Grid item lg={2} md={2} sm={2} xs={12}>
<InputText cid={alertCid} helpid={alerthelpid} type='numeric' value={value?.alert} onChange={onAlertChange} />
</Grid>
<Grid item lg={4} md={4} sm={4} xs={12} className={classes.contentStyle}>
<Typography>{gettext('(in minuts)')}</Typography>
</Grid>
</Grid>
</FormGroup >
);
}
QueryThresholds.propTypes = {
value: PropTypes.object,
onChange: PropTypes.func,
};

View File

@@ -0,0 +1,58 @@
/////////////////////////////////////////////////////////////
//
// 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 { makeStyles, Grid } from '@material-ui/core';
import React, { useState } from 'react';
import {InputSelect } from './FormComponents';
import PropTypes from 'prop-types';
import CustomPropTypes from '../custom_prop_types';
const useStyles = makeStyles(() => ({
preview: {
paddingTop: 10
}
}));
export default function Themes({onChange, ...props}) {
const classes = useStyles();
const [previewSrc, setPreviewSrc] = useState(null);
const themeChange = (e) => {
props.options.forEach((opt)=> {
if(opt.value == e) {
setPreviewSrc(opt.preview_src);
}
});
onChange(e);
};
return (
<Grid
container
direction="column"
justifyContent="center">
<Grid item lg={12} md={12} sm={12} xs={12}>
<InputSelect ref={props.inputRef} onChange={themeChange} {...props} />
</Grid>
<Grid item lg={12} md={12} sm={12} xs={12} className={classes.preview}>
<img className='img-fluid mx-auto d-block border' src={previewSrc} alt={gettext('Preview not available...')} />
</Grid>
</Grid>
);
}
Themes.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
controlProps: PropTypes.object,
fields: PropTypes.array,
options: PropTypes.array,
inputRef: CustomPropTypes.ref
};