Fixed following issues for query tool after react porting:

1) Add New Server Connection > Server options keep loading(For empty Server group).
 2) After clicking indent/Unindent(for all operations) for large query option left as it is till operation completes
 3) Check sign beside options in Execute Option/Copy Header is little bit big
 4) In explain > Analysis tab does not show ROWS column
 5) In explain > Explain > analysis previous explain output is NOT cleared. New rows are appended. Same applies to the statistics tab.
 6) Update new query tool connection tool tip. Fixes #7289
 7) Explain-Analyze > Loops column is empty.
 8) Explain-Analyze with Verbose & Costs > in ROW X columns upward arrows are missing.
 9) Explain-Analyze with all option checked > background colors are missing for timing.
 10) Explain-Analyze > Additional bullet is added before Hash Cond.
 11) Browser Tree > Filtered rows icon is not working.
 12) Create table with timestamp and default value as function now() > Add new row > Enter mandatory columns except column where default value is function(now()) > Click Save > New row added but column with default value has value [default]. not updated to actual value. / Default values are not considered for any column while adding a new entry.
 13) Disable execute options in View/Edit data.
 14) The Boolean column always shows null.
 15) In Query history Remove & Remove all buttons are stuck to each other.
 16) On Remove all, the right panel is empty.
 17) Create a column with boolean[]/ text[], Try to add a new entry from data grid, enter “” quotes > Click Ok > Now try edit cell > You can not change value.
 18) In query history - Select queries are suffixed by ’Save Data’ icon
 19) Edit any table with PK > Try to insert duplicate PK > Error thrown > Correct pK value > Still old error shown > Not able to add new entry (This works when focus is moved from edited cell)
 20) Clicking arrows after opening dropdown options, does not collapse dropdown.

refs #6131
This commit is contained in:
Aditya Toshniwal 2022-04-18 12:50:51 +05:30 committed by Akshay Joshi
parent 74e3f976c1
commit 9c30d983bd
22 changed files with 343 additions and 185 deletions

View File

@ -116,7 +116,7 @@ export function initializeToolbar(panel, wcDocker) {
else if ('name' in data && data.name === gettext('View Data'))
pgAdmin.Tools.SQLEditor.showViewData({mnuid: 3}, pgAdmin.Browser.tree.selected());
else if ('name' in data && data.name === gettext('Filtered Rows'))
pgAdmin.Tools.SQLEditor.show_filtered_row({mnuid: 4}, pgAdmin.Browser.tree.selected());
pgAdmin.Tools.SQLEditor.showFilteredRow({mnuid: 4}, pgAdmin.Browser.tree.selected());
else if ('name' in data && data.name === gettext('Search objects'))
pgAdmin.SearchObjects.show_search_objects('', pgAdmin.Browser.tree.selected());
else if ('name' in data && data.name === gettext('PSQL Tool')){

View File

@ -24,16 +24,19 @@ const useStyles = makeStyles((theme)=>({
borderBottom: '2px dashed '+theme.palette.primary.main,
},
level2: {
backgroundColor: '#fff',
color: '#000',
backgroundColor: theme.otherVars.explain.sev2.bg,
color: theme.otherVars.explain.sev2.color,
},
level3: {
backgroundColor: '#fff',
color: '#000',
backgroundColor: theme.otherVars.explain.sev3.bg,
color: theme.otherVars.explain.sev3.color,
},
level4: {
backgroundColor: '#fff',
color: '#000',
backgroundColor: theme.otherVars.explain.sev4.bg,
color: theme.otherVars.explain.sev4.color,
},
textRight: {
textAlign: 'right',
},
}));
@ -43,15 +46,6 @@ function getRowClassname(data, collapseParent) {
if(data['Plans']?.length > 0) {
className.push(classes.collapsible);
}
if(!_.isEmpty(data['exclusive_flag'])) {
className.push(classes['level'+data['exclusive_flag']]);
}
if(!_.isEmpty(data['inclusive_flag'])) {
className.push(classes['level'+data['inclusive_flag']]);
}
if(!_.isEmpty(data['rowsx_flag'])) {
className.push(classes['level'+data['rowsx_flag']]);
}
if(collapseParent) {
className.push(classes.collapseParent);
}
@ -61,10 +55,10 @@ function getRowClassname(data, collapseParent) {
function NodeText({displayText, extraInfo}) {
return (
<>
<ArrowRightAltIcon fontSize="small" /> {displayText}
{extraInfo?.length > 0 && <ul>
<ArrowRightAltIcon fontSize="small" style={{marginLeft: '-24px'}} /> {displayText}
{extraInfo?.length > 0 && <ul style={{fontSize: '13px'}}>
{extraInfo.map((item, i)=>{
return <li key={i}>{HTMLReactParse(item)}</li>;
return <li key={i} style={{opacity: '0.8'}}>{HTMLReactParse(item)}</li>;
})}
</ul>}
</>);
@ -76,6 +70,7 @@ NodeText.propTypes = {
function ExplainRow({row, show, activeExId, setActiveExId, collapsedExId, toggleCollapseExId}) {
let data = row['data'];
const classes = useStyles();
const exId = `pga_ex_${data['level'].join('_')}`;
const parentExId = `pga_ex_${data['parent_node']}`;
const collapsed = collapsedExId.findIndex((v)=>parentExId.startsWith(v)) > -1;
@ -94,29 +89,28 @@ function ExplainRow({row, show, activeExId, setActiveExId, collapsedExId, toggle
<td>
<FiberManualRecordIcon fontSize="small" style={{visibility: activeExId==parentExId ? 'visible' : 'hidden'}} />
</td>
<td>{data['_serial']}.</td>
<td className={classes.textRight}>{data['_serial']}.</td>
<td style={{paddingLeft: data['level'].length*30+'px'}} title={row['tooltip_text']}>
<NodeText displayText={row['display_text']} extraInfo={row['node_extra_info']} />
</td>
<td style={show.show_timings ? {} : {display: 'none'}}>
<td className={clsx(classes.textRight, classes['level'+data['exclusive_flag']])} style={show.show_timings ? {} : {display: 'none'}}>
{data['exclusive'] && (data['exclusive']+' ms')}
</td>
<td style={show.show_timings ? {} : {display: 'none'}}>
<td className={clsx(classes.textRight, classes['level'+data['inclusive_flag']])} style={show.show_timings ? {} : {display: 'none'}}>
{data['inclusive'] && (data['inclusive']+' ms')}
</td>
<td style={{display: 'none'}}>{!_.isUndefined(data['rowsx_flag'])
&& (data['rowsx_direction'] == 'positive' ? '&uarr;' : '&darr;')
}</td>
<td style={show.show_rowsx ? {} : {display: 'none'}}>
{data['rowsx']}
<td className={clsx(classes.textRight, classes['level'+data['rowsx_flag']])} style={show.show_rowsx ? {} : {display: 'none'}}>
{!_.isUndefined(data['rowsx_flag'])
&& (data['rowsx_direction'] == 'positive' ? <>&uarr;</> : <>&darr;</>)
}&nbsp;{data['rowsx']}
</td>
<td style={(show.show_rowsx || show.show_rows) ? {} : {display: 'none'}}>
<td className={classes.textRight} style={(show.show_rowsx || show.show_rows) ? {} : {display: 'none'}}>
{data['Actual Rows']}
</td>
<td style={(show.show_rowsx || show.show_plan_rows) ? {} : {display: 'none'}}>
<td className={classes.textRight} style={(show.show_rowsx || show.show_plan_rows) ? {} : {display: 'none'}}>
{data['Plan Rows']}
</td>
<td style={(show.show_rowsx || show.show_rows) ? {} : {display: 'none'}}>
<td className={classes.textRight} style={(show.show_rowsx || show.show_rows) ? {} : {display: 'none'}}>
{data['Actual Loops']}
</td>
</tr>
@ -130,7 +124,9 @@ ExplainRow.propTypes = {
_serial: PropTypes.number,
parent_node: PropTypes.number,
exclusive: PropTypes.number,
exclusive_flag: PropTypes.string,
inclusive: PropTypes.number,
inclusive_flag: PropTypes.string,
rowsx_direction: PropTypes.string,
rowsx: PropTypes.number,
rowsx_flag: PropTypes.number,
@ -178,7 +174,8 @@ export default function Analysis({explainTable}) {
<th colSpan="2" style={explainTable.show_timings ? {} : {display: 'none'}}>
<button disabled="">Timings</button>
</th>
<th style={(explainTable.show_rowsx || explainTable.show_rows) ? {} : {display: 'none'}} colSpan="3">
<th style={(explainTable.show_rowsx || explainTable.show_rows) ? {} : {display: 'none'}}
colSpan={(explainTable.show_rowsx) ? '3' : '1'}>
<button disabled="">Rows</button>
</th>
<th style={(explainTable.show_rowsx || explainTable.show_rows) ? {} : {display: 'none'}} rowSpan="2">

View File

@ -250,6 +250,7 @@ function PlanContent({plan, pXpos, pYpos, ...props}) {
strokeWidth={1.2}
fill="gray"
fillOpacity={0.2}
pointerEvents="none"
/>
<tspan x={currentXpos + pWIDTH - (plan.width / 2) - xMargin}
y={currentYpos + pHEIGHT - (plan.height / 2) - yMargin}

View File

@ -23,7 +23,7 @@ const useStyles = makeStyles((theme)=>({
tabPanel: {
padding: 0,
backgroundColor: theme.palette.background.default,
}
},
}));
// Some predefined constants used to calculate image location and its border
@ -261,7 +261,11 @@ function parsePlan(data, ctx) {
totalCost = data['Total Cost'];
if (startCost != undefined && totalCost != undefined) {
arrowSize = Math.round(Math.log((startCost + totalCost) / 2 + startCost));
arrowSize = arrowSize < 1 ? 1 : arrowSize > 10 ? 10 : arrowSize;
if (arrowSize < 1) {
arrowSize = 1;
} else if (arrowSize > 10) {
arrowSize = 10;
}
}
data['arr_id'] = _.uniqueId('arr');
ctx.arrows[data['arr_id']] = arrowSize;
@ -292,15 +296,21 @@ function parsePlan(data, ctx) {
if ('Actual Total Time' in data && 'Actual Loops' in data) {
data['inclusive'] = Math.ceil10(
data['Actual Total Time'] * data['Actual Loops'], -3
data['Actual Total Time'], -3
);
data['exclusive'] = data['inclusive'];
data['inclusive_factor'] = data['inclusive'] / (
data['total_time'] || data['Actual Total Time']
);
data['inclusive_flag'] = data['inclusive_factor'] <= 0.1 ? '1' :
data['inclusive_factor'] < 0.5 ? '2' :
data['inclusive_factor'] <= 0.9 ? '3' : '4';
if (data['inclusive_factor'] <= 0.1) {
data['inclusive_flag'] = '1';
} else if (data['inclusive_factor'] < 0.5) {
data['inclusive_flag'] = '2';
} else if (data['inclusive_factor'] <= 0.9) {
data['inclusive_flag'] = '3';
} else {
data['inclusive_flag'] = '4';
}
}
if ('Actual Rows' in data && 'Plan Rows' in data) {
@ -316,10 +326,20 @@ function parsePlan(data, ctx) {
);
data['rowsx_direction'] = 'positive';
}
data['rowsx_flag'] = data['rowsx'] <= 10 ? '1' : (
data['rowsx'] <= 100 ? '2' : (data['rowsx'] <= 1000 ? '3' : '4')
);
data['rowsx'] = Math.ceil10(data['rowsx'], -2);
if (data['rowsx'] <= 10) {
data['rowsx_flag'] = '1';
} else if (data['rowsx'] <= 100 ) {
data['rowsx_flag'] = '2';
} else if (data['rowsx'] <= 1000 ) {
data['rowsx_flag'] = '3';
} else {
data['rowsx_flag'] = '4';
}
if('loops' in data) {
data['rowsx'] = Math.ceil10(data['rowsx'] / data['loops'] || 1, -2);
} else {
data['rowsx'] = Math.ceil10(data['rowsx'], -2);
}
}
// Start calculating xpos, ypos, width and height for child plans if any
@ -337,6 +357,7 @@ function parsePlan(data, ctx) {
ypos: ypos,
total_time: data['total_time'] || data['Actual Total Time'],
parent_node: lvl.join('_'),
loops: data['Actual Loops']
}, ctx);
if (maxChildWidth < plan.width) {
@ -344,7 +365,7 @@ function parsePlan(data, ctx) {
}
if ('exclusive' in data) {
if (plan.inclusive) {
if (plan.inclusive < data['exclusive']) {
data['exclusive'] -= plan.inclusive;
}
}
@ -361,6 +382,11 @@ function parsePlan(data, ctx) {
plans.push(plan);
idx++;
});
} else{
if('loops' in data && 'exclusive' in data) {
data['inclusive'] = Math.ceil10(data['Actual Total Time'] / data['loops'] || 1, -3);
data['exclusive'] = data['inclusive'];
}
}
if ('exclusive' in data) {
@ -368,9 +394,15 @@ function parsePlan(data, ctx) {
data['exclusive_factor'] = (
data['exclusive'] / (data['total_time'] || data['Actual Total Time'])
);
data['exclusive_flag'] = data['exclusive_factor'] <= 0.1 ? '1' :
data['exclusive_factor'] < 0.5 ? '2' :
data['exclusive_factor'] <= 0.9 ? '3' : '4';
if (data['exclusive_factor'] <= 0.1) {
data['exclusive_flag'] = '1';
} else if (data['exclusive_factor'] < 0.5) {
data['exclusive_flag'] = '2';
} else if (data['exclusive_factor'] <= 0.9) {
data['exclusive_flag'] = '3';
} else {
data['exclusive_flag'] = '4';
}
}
// Final Width and Height of current node
@ -389,6 +421,7 @@ function parsePlanData(data, ctx) {
...data['Plan'],
xpos: 0,
ypos: 0,
loops: 1,
}, ctx);
retPlan['Plan'] = plan;
retPlan['xpos'] = 0;
@ -431,20 +464,23 @@ export default function Explain({plans=[]}) {
const classes = useStyles();
const [tabValue, setTabValue] = React.useState(0);
let ctx = React.useRef({
totalNodes: 0,
totalDownloadedNodes: 0,
isDownloaded: 0,
explainTable: {
rows: [],
statistics: {
tables: {},
nodes: {},
let ctx = React.useRef({});
let planData = React.useMemo(()=>{
ctx.current = {
totalNodes: 0,
totalDownloadedNodes: 0,
isDownloaded: 0,
explainTable: {
rows: [],
statistics: {
tables: {},
nodes: {},
},
},
},
arrows: {},
});
let planData = React.useMemo(()=>(plans && parsePlanData(plans[0], ctx.current)), [plans]);
arrows: {},
};
return plans && parsePlanData(plans[0], ctx.current);
}, [plans]);
if(_.isEmpty(plans)) {
return <Box height="100%" display="flex" flexDirection="column">

View File

@ -19,6 +19,7 @@ import CustomPropTypes from '../custom_prop_types';
import getStandardTheme from './standard';
import getDarkTheme from './dark';
import getHightContrastTheme from './high_contrast';
import { CssBaseline } from '@material-ui/core';
/* Common settings across all themes */
let basicSettings = createMuiTheme();
@ -278,6 +279,19 @@ function getFinalTheme(baseTheme) {
return createMuiTheme({
mixins: mixins,
overrides: {
MuiCssBaseline: {
'@global': {
ul: {
margin: 0,
padding: 0,
},
li: {
listStyle: 'none',
margin: 0,
padding: 0,
}
},
},
MuiOutlinedInput: {
root: {
'&.Mui-disabled .MuiOutlinedInput-notchedOutline': {
@ -535,6 +549,7 @@ export default function Theme(props) {
}, []);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
{props.children}
</ThemeProvider>
);
@ -561,6 +576,7 @@ export const commonTableStyles = makeStyles((theme)=>({
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
userSelect: 'text',
maxWidth: '250px',
'&:first-of-type':{
borderLeft: 'none',
},

View File

@ -105,6 +105,20 @@ export default function(basicSettings) {
qtDatagridSelectFg: '#222',
cardHeaderBg: '#fff',
emptySpaceBg: '#ebeef3',
explain: {
sev2: {
color: '#222222',
bg: '#FFEE88',
},
sev3: {
color: '#FFFFFF',
bg: '#EE8800'
},
sev4: {
color: '#FFFFFF',
bg: '#880000'
},
}
}
});
}

View File

@ -405,6 +405,22 @@ export default function CodeMirror({currEditor, name, value, options, events, re
}
Object.keys(events||{}).forEach((eventName)=>{
if(eventName === 'change') {
let timeoutId;
const change = (...args)=>{
/* In case of indent, change is triggered for each line */
/* This can be avoided and taking only the latest */
if(timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(()=>{
events[eventName](...args);
timeoutId = null;
}, 0);
};
editor.current.on(eventName, change);
return;
}
editor.current.on(eventName, events[eventName]);
});
editor.current.on('drop', handleDrop);

View File

@ -1,5 +1,6 @@
import React from 'react';
import QueryToolSvg from '../../img/fonticon/query_tool.svg?svgr';
import ViewDataSvg from '../../img/fonticon/view_data.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';
@ -22,16 +23,44 @@ ExternalIcon.propTypes = {
Icon: PropTypes.elementType.isRequired,
};
export const QueryToolIcon = ()=><ExternalIcon Icon={QueryToolSvg} style={{height: '1rem'}} />;
export const SaveDataIcon = ()=><ExternalIcon Icon={SaveDataSvg} style={{height: '1rem'}} />;
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: '1rem'}} />;
export const DisonnectedIcon = ()=><ExternalIcon Icon={DisconnectedSvg} style={{height: '1rem'}} />;
export const RegexIcon = ()=><ExternalIcon Icon={RegexSvg} />;
export const FormatCaseIcon = ()=><ExternalIcon Icon={FormatCaseSvg} />;
export const ExpandDialogIcon = ()=><ExternalIcon Icon={Expand} style={{height: '1.2rem'}} />;
export const MinimizeDialogIcon = ()=><ExternalIcon Icon={Collapse} style={{height: '1.4rem'}} />;
export const QueryToolIcon = ({style})=><ExternalIcon Icon={QueryToolSvg} style={{height: '1rem', ...style}} />;
QueryToolIcon.propTypes = {style: PropTypes.object};
export const ViewDataIcon = ({style})=><ExternalIcon Icon={ViewDataSvg} style={{height: '0.8rem', ...style}} />;
ViewDataIcon.propTypes = {style: PropTypes.object};
export const SaveDataIcon = ({style})=><ExternalIcon Icon={SaveDataSvg} style={{height: '1rem', ...style}} />;
SaveDataIcon.propTypes = {style: PropTypes.object};
export const PasteIcon = ({style})=><ExternalIcon Icon={PasteSvg} style={style} />;
PasteIcon.propTypes = {style: PropTypes.object};
export const FilterIcon = ({style})=><ExternalIcon Icon={FilterSvg} style={style} />;
FilterIcon.propTypes = {style: PropTypes.object};
export const CommitIcon = ({style})=><ExternalIcon Icon={CommitSvg} style={style} />;
CommitIcon.propTypes = {style: PropTypes.object};
export const RollbackIcon = ({style})=><ExternalIcon Icon={RollbackSvg} style={style} />;
RollbackIcon.propTypes = {style: PropTypes.object};
export const ClearIcon = ({style})=><ExternalIcon Icon={ClearSvg} style={style} />;
ClearIcon.propTypes = {style: PropTypes.object};
export const ConnectedIcon = ({style})=><ExternalIcon Icon={ConnectedSvg} style={{height: '1rem', ...style}} />;
ConnectedIcon.propTypes = {style: PropTypes.object};
export const DisonnectedIcon = ({style})=><ExternalIcon Icon={DisconnectedSvg} style={{height: '1rem', ...style}} />;
DisonnectedIcon.propTypes = {style: PropTypes.object};
export const RegexIcon = ({style})=><ExternalIcon Icon={RegexSvg} style={style} />;
RegexIcon.propTypes = {style: PropTypes.object};
export const FormatCaseIcon = ({style})=><ExternalIcon Icon={FormatCaseSvg} style={style} />;
FormatCaseIcon.propTypes = {style: PropTypes.object};
export const ExpandDialogIcon = ({style})=><ExternalIcon Icon={Expand} style={{height: '1.2rem', ...style}} />;QueryToolIcon.propTypes = {style: PropTypes.object};
ExpandDialogIcon.propTypes = {style: PropTypes.object};
export const MinimizeDialogIcon = ({style})=><ExternalIcon Icon={Collapse} style={{height: '1.4rem', ...style}} />;
MinimizeDialogIcon.propTypes = {style: PropTypes.object};

View File

@ -1,5 +1,5 @@
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import React, { useRef } from 'react';
import CheckIcon from '@material-ui/icons/Check';
import PropTypes from 'prop-types';
@ -34,6 +34,9 @@ const useStyles = makeStyles((theme)=>({
color: theme.palette.primary.contrastText,
}
},
checkIcon: {
width: '1.3rem',
},
hideCheck: {
visibility: 'hidden',
},
@ -70,7 +73,7 @@ export const PgMenuItem = applyStatics(MenuItem)(({hasCheck=false, checked=false
};
}
return <MenuItem {...props} onClick={onClick} className={classes.menuItem}>
{hasCheck && <CheckIcon style={checked ? {} : {visibility: 'hidden'}}/>}
{hasCheck && <CheckIcon className={classes.checkIcon} style={checked ? {} : {visibility: 'hidden'}} />}
{children}
{(shortcut || accesskey) && <div className={classes.shortcut}>({shortcutToString(shortcut, accesskey)})</div>}
</MenuItem>;
@ -84,3 +87,32 @@ PgMenuItem.propTypes = {
children: CustomPropTypes.children,
onClick: PropTypes.func,
};
export function usePgMenuGroup() {
const [openMenuName, setOpenMenuName] = React.useState(null);
const prevMenuOpenIdRef = useRef(null);
const toggleMenu = React.useCallback((e)=>{
setOpenMenuName(()=>{
return prevMenuOpenIdRef.current == e.currentTarget?.name ? null : e.currentTarget?.name;
});
prevMenuOpenIdRef.current = null;
}, []);
const handleMenuClose = React.useCallback(()=>{
/* We have no way here to know if the menu was closed using menu button or not
We will keep the last menu name ref for sometime so that the menu does not
open again if menu button is clicked to close the menu */
prevMenuOpenIdRef.current = openMenuName;
setTimeout(()=>{
prevMenuOpenIdRef.current = null;
}, 300);
setOpenMenuName(null);
}, [openMenuName]);
return {
openMenuName: openMenuName,
toggleMenu: toggleMenu,
onMenuClose: handleMenuClose,
};
}

View File

@ -20,7 +20,7 @@ export default class EventBus {
if(e.event === event) {
return e.callback.toString()!=callback.toString();
}
return e.event!=event && e.callback.toString()!=callback.toString();
return true;
});
} else {
this._eventListeners = this._eventListeners.filter((e)=>e.event!=event);

View File

@ -422,7 +422,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
eventBus.current.deregisterListener(e[0], e[1]);
});
};
}, [qtState]);
}, [qtState.params, qtState.current_file]);
useEffect(()=>{
/* Fire query change so that title changes to latest */
@ -602,11 +602,12 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
onResetLayout={onResetLayout}
docker={docker.current}
/>
<MainToolBar
containerRef={containerRef}
onManageMacros={onManageMacros}
onFilterClick={onFilterClick}
/>
{React.useMemo(()=>(
<MainToolBar
containerRef={containerRef}
onManageMacros={onManageMacros}
onFilterClick={onFilterClick}
/>), [containerRef.current, onManageMacros, onFilterClick])}
<Layout
getLayoutInstance={(obj)=>docker.current=obj}
defaultLayout={defaultLayout}

View File

@ -170,8 +170,8 @@ export function TextEditor({row, column, onRowChange, onClose}) {
}, []);
const onOK = ()=>{
if(column.is_array && !isValidArray(value)) {
console.error(gettext('Arrays must start with "{" and end with "}"'));
if(column.is_array && !isValidArray(localVal)) {
Notifier.error(gettext('Arrays must start with "{" and end with "}"'));
} else {
let columnVal = textColumnFinalVal(localVal, column);
onRowChange({ ...row, [column.key]: columnVal}, true);
@ -205,14 +205,14 @@ TextEditor.propTypes = EditorPropTypes;
export function NumberEditor({row, column, onRowChange, onClose}) {
const classes = useStyles();
const value = row[column.key] ?? '';
const onBlur = ()=>{
const isValidData = ()=>{
if(!column.is_array && isNaN(value)){
Notifier.error(gettext('Please enter a valid number'));
return;
return false;
} else if(column.is_array) {
if(!isValidArray(value)) {
Notifier.error(gettext('Arrays must start with "{" and end with "}"'));
return;
return false;
}
let checkVal = value.trim().slice(1, -1);
if(checkVal == '') {
@ -223,16 +223,25 @@ export function NumberEditor({row, column, onRowChange, onClose}) {
for (const val of checkVal) {
if(isNaN(val)) {
Notifier.error(gettext('Arrays must start with "{" and end with "}"'));
return;
return false;
}
}
}
onClose(column.can_edit ? true : false);
return true;
};
const onBlur = ()=>{
if(isValidData()) {
onClose(column.can_edit ? true : false);
return true;
}
return false;
};
const onKeyDown = (e)=>{
if(e.code == 'Tab') {
e.preventDefault();
onBlur();
if(!onBlur()) {
e.stopPropagation();
}
}
};
return (
@ -245,7 +254,7 @@ export function NumberEditor({row, column, onRowChange, onClose}) {
onRowChange({ ...row, [column.key]: e.target.value });
}
}}
// onBlur={onBlur}
onBlur={onBlur}
onKeyDown={onKeyDown}
/>
);
@ -269,7 +278,7 @@ export function CheckboxEditor({row, column, onRowChange, onClose}) {
};
const onBlur = ()=>{onClose(true);};
let className = 'checked';
if(!value) {
if(!value && value != null) {
className = 'unchecked';
} else if(value == null){
className = 'intermediate';
@ -323,7 +332,7 @@ export function JsonTextEditor({row, column, onRowChange, onClose}) {
value={localVal}
options={{
onChange: onChange,
onError: (error)=>console.error('Invalid Json: ' + error.message.split(':')[0]),
onError: (error)=>Notifier.error('Invalid Json: ' + error.message.split(':')[0]),
}}
className={'jsoneditor-div'}
/>

View File

@ -63,6 +63,9 @@ class NewConnectionSchema extends BaseUISchema {
.then(({data: respData})=>{
let groupedOptions = [];
_.forIn(respData.data.result.server_list, (v, k)=>{
if(v.length == 0) {
return;
}
/* initial selection */
_.find(v, (o)=>o.value==obj.params.sid).selected = true;
groupedOptions.push({

View File

@ -106,15 +106,15 @@ export function ConnectionBar({connected, connecting, connectionStatus, connecti
>
<Tooltip title={queryToolCtx.params.is_query_tool ? '' : connTitle}>
<Box display="flex" width="100%">
<Box textOverflow="ellipsis" overflow="hidden" marginRight="auto">{connecting && '(Obtaining connection)'}{connTitle}</Box>
<Box textOverflow="ellipsis" overflow="hidden" marginRight="auto">{connecting && gettext('(Obtaining connection)')}{connTitle}</Box>
{queryToolCtx.params.is_query_tool && <Box><KeyboardArrowDownIcon /></Box>}
</Box>
</Tooltip>
</DefaultButton>
<PgIconButton title="New query tool" icon={<QueryToolIcon />} onClick={onNewQueryToolClick}/>
<PgIconButton title={gettext('New query tool for current connection')} icon={<QueryToolIcon />} onClick={onNewQueryToolClick}/>
</PgButtonGroup>
<PgButtonGroup size="small" variant="text" style={{marginLeft: 'auto'}}>
<PgIconButton title="Reset layout" icon={<RotateLeftRoundedIcon />} onClick={onResetLayout}/>
<PgIconButton title={gettext('Reset layout')} icon={<RotateLeftRoundedIcon />} onClick={onResetLayout}/>
</PgButtonGroup>
</Box>
<PgMenu

View File

@ -94,7 +94,7 @@ function parseEwkbData(rows, column) {
// generate map info content
if (tooLargeDataSize || tooManyGeometries) {
infoList.push(supportedGeometries.length + ' of ' + rows.length + ' geometries rendered.');
infoList.push(gettext('%s of %s geometries rendered.', supportedGeometries.length, rows.length));
}
if (geometries3D.length > 0) {
infoList.push(gettext('3D geometries not rendered.'));
@ -116,7 +116,7 @@ function parseData(rows, columns, column) {
'geoJSONs': [],
'selectedSRID': 0,
'getPopupContent': undefined,
'infoList': ['Empty row.'],
'infoList': [gettext('Empty row.')],
};
}
@ -294,19 +294,19 @@ function TheMap({data}) {
<>
{data.selectedSRID === 4326 &&
<LayersControl position="topright">
<LayersControl.BaseLayer checked name="Empty">
<LayersControl.BaseLayer checked name={gettext('Empty')}>
<TileLayer
url=""
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer checked name="Street">
<LayersControl.BaseLayer checked name={gettext('Street')}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
maxZoom={19}
attribution='&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Topography">
<LayersControl.BaseLayer name={gettext('Topography')}>
<TileLayer
url="https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png"
maxZoom={17}
@ -317,7 +317,7 @@ function TheMap({data}) {
}
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Gray Style">
<LayersControl.BaseLayer name={gettext('Gray Style')}>
<TileLayer
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{r}.png"
maxZoom={19}
@ -328,7 +328,7 @@ function TheMap({data}) {
subdomains='abcd'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Light Color">
<LayersControl.BaseLayer name={gettext('Light Color')}>
<TileLayer
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager/{z}/{x}/{y}{r}.pn"
maxZoom={19}
@ -339,7 +339,7 @@ function TheMap({data}) {
subdomains='abcd'
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Dark Matter">
<LayersControl.BaseLayer name={gettext('Dark Matter')}>
<TileLayer
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}{r}.png"
maxZoom={19}

View File

@ -23,7 +23,7 @@ import FormatListNumberedRoundedIcon from '@material-ui/icons/FormatListNumbered
import HelpIcon from '@material-ui/icons/HelpRounded';
import {QUERY_TOOL_EVENTS, CONNECTION_STATUS} from '../QueryToolConstants';
import { QueryToolConnectionContext, QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
import { PgMenu, PgMenuDivider, PgMenuItem } from '../../../../../../static/js/components/Menu';
import { PgMenu, PgMenuDivider, PgMenuItem, usePgMenuGroup } from '../../../../../../static/js/components/Menu';
import gettext from 'sources/gettext';
import { useKeyboardShortcuts } from '../../../../../../static/js/custom_hooks';
import {shortcut_key} from 'sources/keyboard_shortcuts';
@ -150,7 +150,7 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
'filter': true,
'limit': false,
});
const [menuOpenId, setMenuOpenId] = React.useState(null);
const {openMenuName, toggleMenu, onMenuClose} = usePgMenuGroup();
const [checkedMenuItems, setCheckedMenuItems] = React.useState({});
/* Menu button refs */
const saveAsMenuRef = React.useRef(null);
@ -189,14 +189,6 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
explain(true);
}, [explain]);
const openMenu = useCallback((e)=>{
setMenuOpenId(e.currentTarget.name);
}, []);
const handleMenuClose = useCallback(()=>{
setMenuOpenId(null);
}, []);
const checkMenuClick = useCallback((e)=>{
setCheckedMenuItems((prev)=>{
let newVal = !prev[e.value];
@ -453,21 +445,21 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
accesskey={shortcut_key(queryToolPref.btn_save_file)} disabled={buttonsDisabled['save'] || !queryToolCtx.params.is_query_tool}
onClick={()=>{saveFile(false);}} />
<PgIconButton title={gettext('File')} icon={<KeyboardArrowDownIcon />} splitButton disabled={!queryToolCtx.params.is_query_tool}
name="menu-saveas" ref={saveAsMenuRef} onClick={openMenu}
name="menu-saveas" ref={saveAsMenuRef} onClick={toggleMenu}
/>
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Edit')} icon={
<><EditRoundedIcon /><KeyboardArrowDownIcon style={{marginLeft: '-10px'}} /></>}
disabled={!queryToolCtx.params.is_query_tool}
name="menu-edit" ref={editMenuRef} onClick={openMenu} />
name="menu-edit" ref={editMenuRef} onClick={toggleMenu} />
</PgButtonGroup>
<PgButtonGroup size="small" color={highlightFilter ? 'primary' : 'default'}>
<PgIconButton title={gettext('Sort/Filter')} icon={<FilterIcon />}
onClick={onFilterClick} disabled={buttonsDisabled['filter']} accesskey={shortcut_key(queryToolPref.btn_filter_dialog)}/>
<PgIconButton title={gettext('Filter options')} icon={<KeyboardArrowDownIcon />} splitButton
disabled={buttonsDisabled['filter']} name="menu-filter" ref={filterMenuRef} accesskey={shortcut_key(queryToolPref.btn_filter_options)}
onClick={openMenu} />
onClick={toggleMenu} />
</PgButtonGroup>
<InputSelectNonSearch options={[
{label: gettext('No limit'), value: '-1'},
@ -482,7 +474,7 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
onClick={executeQuery} disabled={buttonsDisabled['execute']} shortcut={queryToolPref.execute_query}/>
<PgIconButton title={gettext('Execute options')} icon={<KeyboardArrowDownIcon />} splitButton
name="menu-autocommit" ref={autoCommitMenuRef} accesskey={shortcut_key(queryToolPref.btn_delete_row)}
onClick={openMenu} />
onClick={toggleMenu} disabled={!queryToolCtx.params.is_query_tool}/>
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Explain')} icon={<ExplicitRoundedIcon />}
@ -491,7 +483,7 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
onClick={()=>{explainAnalyse();}} disabled={buttonsDisabled['explain_analyse'] || !queryToolCtx.params.is_query_tool} shortcut={queryToolPref.explain_analyze_query}/>
<PgIconButton title={gettext('Explain Settings')} icon={<KeyboardArrowDownIcon />} splitButton
disabled={!queryToolCtx.params.is_query_tool}
name="menu-explain" ref={explainMenuRef} onClick={openMenu} />
name="menu-explain" ref={explainMenuRef} onClick={toggleMenu} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Commit')} icon={<CommitIcon />}
@ -502,7 +494,7 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
<PgButtonGroup size="small">
<PgIconButton title={gettext('Macros')} icon={
<><FormatListNumberedRoundedIcon /><KeyboardArrowDownIcon style={{marginLeft: '-10px'}} /></>}
disabled={!queryToolCtx.params.is_query_tool} name="menu-macros" ref={macrosMenuRef} onClick={openMenu} />
disabled={!queryToolCtx.params.is_query_tool} name="menu-macros" ref={macrosMenuRef} onClick={toggleMenu} />
</PgButtonGroup>
<PgButtonGroup size="small">
<PgIconButton title={gettext('Help')} icon={<HelpIcon />} onClick={onHelpClick} />
@ -510,15 +502,15 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
</Box>
<PgMenu
anchorRef={saveAsMenuRef}
open={menuOpenId=='menu-saveas'}
onClose={handleMenuClose}
open={openMenuName=='menu-saveas'}
onClose={onMenuClose}
>
<PgMenuItem onClick={()=>{saveFile(true);}}>{gettext('Save as')}</PgMenuItem>
</PgMenu>
<PgMenu
anchorRef={editMenuRef}
open={menuOpenId=='menu-edit'}
onClose={handleMenuClose}
open={openMenuName=='menu-edit'}
onClose={onMenuClose}
>
<PgMenuItem shortcut={FIXED_PREF.find}
onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.EDITOR_FIND_REPLACE, false);}}>{gettext('Find')}</PgMenuItem>
@ -540,8 +532,8 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
</PgMenu>
<PgMenu
anchorRef={filterMenuRef}
open={menuOpenId=='menu-filter'}
onClose={handleMenuClose}
open={openMenuName=='menu-filter'}
onClose={onMenuClose}
>
<PgMenuItem onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_INCLUDE_EXCLUDE_FILTER, true);}}>{gettext('Filter by Selection')}</PgMenuItem>
<PgMenuItem onClick={()=>{eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_INCLUDE_EXCLUDE_FILTER, false);}}>{gettext('Exclude by Selection')}</PgMenuItem>
@ -549,8 +541,8 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
</PgMenu>
<PgMenu
anchorRef={autoCommitMenuRef}
open={menuOpenId=='menu-autocommit'}
onClose={handleMenuClose}
open={openMenuName=='menu-autocommit'}
onClose={onMenuClose}
>
<PgMenuItem hasCheck value="auto_commit" checked={checkedMenuItems['auto_commit']}
onClick={checkMenuClick}>{gettext('Auto commit?')}</PgMenuItem>
@ -559,8 +551,8 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
</PgMenu>
<PgMenu
anchorRef={explainMenuRef}
open={menuOpenId=='menu-explain'}
onClose={handleMenuClose}
open={openMenuName=='menu-explain'}
onClose={onMenuClose}
>
<PgMenuItem hasCheck value="explain_verbose" checked={checkedMenuItems['explain_verbose']}
onClick={checkMenuClick}>{gettext('Verbose')}</PgMenuItem>
@ -577,8 +569,8 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros}) {
</PgMenu>
<PgMenu
anchorRef={macrosMenuRef}
open={menuOpenId=='menu-macros'}
onClose={handleMenuClose}
open={openMenuName=='menu-macros'}
onClose={onMenuClose}
>
<PgMenuItem onClick={onManageMacros}>{gettext('Manage macros')}</PgMenuItem>
<PgMenuDivider />

View File

@ -19,7 +19,7 @@ import moment from 'moment';
import PlayArrowRoundedIcon from '@material-ui/icons/PlayArrowRounded';
import AssessmentRoundedIcon from '@material-ui/icons/AssessmentRounded';
import ExplicitRoundedIcon from '@material-ui/icons/ExplicitRounded';
import { SaveDataIcon, CommitIcon, RollbackIcon } from '../../../../../../static/js/components/ExternalIcon';
import { SaveDataIcon, CommitIcon, RollbackIcon, ViewDataIcon } from '../../../../../../static/js/components/ExternalIcon';
import { InputSwitch } from '../../../../../../static/js/components/FormComponents';
import CodeMirror from '../../../../../../static/js/components/CodeMirror';
import { DefaultButton } from '../../../../../../static/js/components/Buttons';
@ -30,6 +30,7 @@ import { LayoutEventsContext, LAYOUT_EVENTS } from '../../../../../../static/js/
import PropTypes from 'prop-types';
import { parseApiError } from '../../../../../../static/js/api_instance';
import * as clipboard from '../../../../../../static/js/clipboard';
import EmptyPanelMessage from '../../../../../../static/js/components/EmptyPanelMessage';
const useStyles = makeStyles((theme)=>({
leftRoot: {
@ -145,9 +146,9 @@ class QueryHistoryUtils {
getDatePrefix(date) {
let prefix = '';
if (this.isDaysBefore(date, 0)) {
prefix = 'Today - ';
prefix = gettext('Today - ');
} else if (this.isDaysBefore(date, 1)) {
prefix = 'Yesterday - ';
prefix = gettext('Yesterday - ');
}
return prefix;
}
@ -247,7 +248,7 @@ function QuerySourceIcon({source}) {
case JSON.stringify(QuerySources.SAVE_DATA):
return <SaveDataIcon style={{marginLeft: '-4px'}}/>;
case JSON.stringify(QuerySources.VIEW_DATA):
return <SaveDataIcon style={{marginLeft: '-4px'}}/>;
return <ViewDataIcon style={{marginLeft: '-4px'}}/>;
default:
return <></>;
}
@ -312,7 +313,9 @@ function QueryHistoryDetails({entry}) {
}, [entry]);
if(!entry) {
return <></>;
return <Box display="flex" height="100%">
<EmptyPanelMessage text={gettext('Select an history entry to see details.')} />
</Box>;
}
return (
@ -460,39 +463,45 @@ export function QueryHistory() {
<Loader message={loaderText} />
{React.useMemo(()=>(
<Box display="flex" height="100%">
<Box flexBasis="50%" maxWidth="50%" className={classes.leftRoot}>
<Box padding="0.25rem" display="flex">
{gettext('Show queries generated internally by pgAdmin?')}
<InputSwitch value={showInternal} onChange={(e)=>{
setShowInternal(e.target.checked);
qhu.current.showInternal = e.target.checked;
setSelectedItemKey(qhu.current.getNextItemKey());
}} />
<Box marginLeft="auto">
<DefaultButton size="small" disabled={!selectedItemKey} onClick={onRemove}>Remove</DefaultButton>
<DefaultButton size="small" disabled={!qhu.current?.getGroups()?.length}
className={classes.removeBtnMargin} onClick={onRemoveAll}>Remove All</DefaultButton>
{qhu.current.size() == 0 ?
<EmptyPanelMessage text={gettext('No history found')} />:
<>
<Box flexBasis="50%" maxWidth="50%" className={classes.leftRoot}>
<Box padding="0.25rem" display="flex" flexWrap="wrap">
<Box marginRight="auto">
{gettext('Show queries generated internally by pgAdmin?')}
<InputSwitch value={showInternal} onChange={(e)=>{
setShowInternal(e.target.checked);
qhu.current.showInternal = e.target.checked;
setSelectedItemKey(qhu.current.getNextItemKey());
}} />
</Box>
<Box>
<DefaultButton size="small" disabled={!selectedItemKey} onClick={onRemove}>{gettext('Remove')}</DefaultButton>
<DefaultButton size="small" disabled={!qhu.current?.getGroups()?.length}
className={classes.removeBtnMargin} onClick={onRemoveAll}>{gettext('Remove All')}</DefaultButton>
</Box>
</Box>
<Box flexGrow="1" overflow="auto" className={classes.listRoot}>
<List innerRef={listRef} className={classes.root} subheader={<li />} tabIndex="0" onKeyDown={onKeyPressed}>
{qhu.current.getGroups().map(([groupKey, groupHeader]) => (
<ListItem key={`section-${groupKey}`} className={classes.removePadding}>
<List className={classes.removePadding}>
<ListSubheader className={classes.listSubheader}>{groupHeader}</ListSubheader>
{qhu.current.getGroupEntries(groupKey).map((entry) => (
<HistoryEntry key={entry.itemKey} entry={entry} formatEntryDate={qhu.current.formatEntryDate}
itemKey={entry.itemKey} selectedItemKey={selectedItemKey} onClick={()=>{setSelectedItemKey(entry.itemKey);}}/>
))}
</List>
</ListItem>
))}
</List>
</Box>
</Box>
</Box>
<Box flexGrow="1" overflow="auto" className={classes.listRoot}>
<List innerRef={listRef} className={classes.root} subheader={<li />} tabIndex="0" onKeyDown={onKeyPressed}>
{qhu.current.getGroups().map(([groupKey, groupHeader]) => (
<ListItem key={`section-${groupKey}`} className={classes.removePadding}>
<List className={classes.removePadding}>
<ListSubheader className={classes.listSubheader}>{groupHeader}</ListSubheader>
{qhu.current.getGroupEntries(groupKey).map((entry) => (
<HistoryEntry key={entry.itemKey} entry={entry} formatEntryDate={qhu.current.formatEntryDate}
itemKey={entry.itemKey} selectedItemKey={selectedItemKey} onClick={()=>{setSelectedItemKey(entry.itemKey);}}/>
))}
</List>
</ListItem>
))}
</List>
</Box>
</Box>
<Box flexBasis="50%" maxWidth="50%" overflow="auto">
<QueryHistoryDetails entry={selectedEntry}/>
</Box>
<Box flexBasis="50%" maxWidth="50%" overflow="auto">
<QueryHistoryDetails entry={selectedEntry}/>
</Box>
</>}
</Box>
), [selectedItemKey, showInternal, qhu.current.size()])}
</>

View File

@ -627,7 +627,7 @@ export class ResultSetUtils {
data.primary_keys = (_.isEmpty(data.primary_keys) && data.has_oids) ? data.oids : data.primary_keys;
data.can_edit = !_.isEmpty(data.primary_keys);
let procColumns = this.processColumns(data);
onResultsAvailable(data, procColumns, this.processRows(result, procColumns, this.processRows(result, procColumns)));
onResultsAvailable(data, procColumns, this.processRows(result, procColumns));
this.setStartData(null);
let planJson = this.getPlanJson(result, data);
if(planJson) {
@ -886,12 +886,14 @@ export function ResultSet() {
};
useEffect(()=>{
eventBus.registerListener(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, fetchMoreRows);
return ()=>eventBus.deregisterListener(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, fetchMoreRows);
}, [queryData, columns]);
return ()=>{
eventBus.deregisterListener(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, fetchMoreRows);
};
}, [queryData?.has_more_rows, columns]);
useEffect(()=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.ROWS_FETCHED, queryData?.rows_fetched_to, queryData?.rows_affected);
}, [queryData]);
}, [queryData?.rows_fetched_to, queryData?.rows_affected]);
const warnSaveDataClose = ()=>{
// No changes.
@ -975,7 +977,7 @@ export function ResultSet() {
}
eventBus.fireEvent(QUERY_TOOL_EVENTS.SAVE_DATA_DONE, true);
if(!_.size(dataChangeStore.added)) {
if(_.size(dataChangeStore.added)) {
// Update the rows in a grid after addition
respData.data.query_results.forEach((qr)=>{
if(!_.isNull(qr.row_added)) {

View File

@ -158,7 +158,7 @@ export function ResultSetToolbar({containerRef, canEdit}) {
open={menuOpenId=='menu-copyheader'}
onClose={handleMenuClose}
>
<PgMenuItem hasCheck value="copy_with_headers" checked={checkedMenuItems['copy_with_headers']} onClick={checkMenuClick}>Copy with headers</PgMenuItem>
<PgMenuItem hasCheck value="copy_with_headers" checked={checkedMenuItems['copy_with_headers']} onClick={checkMenuClick}>{gettext('Copy with headers')}</PgMenuItem>
</PgMenu>
</>
);

View File

@ -15,6 +15,7 @@ import _ from 'lodash';
import { QUERY_TOOL_EVENTS } from '../QueryToolConstants';
import { useStopwatch } from '../../../../../../static/js/custom_hooks';
import { QueryToolEventsContext } from '../QueryToolComponent';
import gettext from 'sources/gettext';
const useStyles = makeStyles((theme)=>({
@ -86,18 +87,18 @@ export function StatusBar() {
let stagedText = '';
if(dataRowChangeCounts.added > 0) {
stagedText += ` Added: ${dataRowChangeCounts.added};`;
stagedText += ' ' + gettext('Added: %s', dataRowChangeCounts.added);
}
if(dataRowChangeCounts.updated > 0) {
stagedText += ` Updated: ${dataRowChangeCounts.updated};`;
stagedText += ' ' + gettext('Updated: %s', dataRowChangeCounts.updated);
}
if(dataRowChangeCounts.deleted > 0) {
stagedText += ` Deleted: ${dataRowChangeCounts.deleted};`;
stagedText += ' ' + gettext('Deleted: %s', dataRowChangeCounts.deleted);
}
return (
<Box className={classes.root}>
<Box className={clsx(classes.padding, classes.divider)}>Total rows: {rowsCount[0]} of {rowsCount[1]}</Box>
<Box className={clsx(classes.padding, classes.divider)}>{gettext('Total rows: %s of %s', rowsCount[0], rowsCount[1])}</Box>
{lastTaskText &&
<Box className={clsx(classes.padding, classes.divider)}>{lastTaskText} {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}.{msec.toString().padStart(3, '0')}</Box>
}
@ -105,13 +106,13 @@ export function StatusBar() {
<Box className={clsx(classes.padding, classes.divider)}>{lastTaskText} {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}.{msec.toString().padStart(3, '0')}</Box>
}
{Boolean(selectedRowsCount) &&
<Box className={clsx(classes.padding, classes.divider)}>Rows selected: {selectedRowsCount}</Box>}
<Box className={clsx(classes.padding, classes.divider)}>{gettext('Rows selected: %s',selectedRowsCount)}</Box>}
{stagedText &&
<Box className={clsx(classes.padding, classes.divider)}>
<span>Changes staged: {stagedText}</span>
<span>{gettext('Changes staged: %s', stagedText)}</span>
</Box>
}
<Box className={clsx(classes.padding, classes.mlAuto)}>Ln {position[0]}, Col {position[1]}</Box>
<Box className={clsx(classes.padding, classes.mlAuto)}>{gettext('Ln %s, Col %s', position[0], position[1])}</Box>
</Box>
);
}

View File

@ -37,14 +37,14 @@ describe('components Buttons', ()=>{
it('PrimaryButton', ()=>{
let ThemedBtn = withTheme(PrimaryButton);
let btn = mount(<ThemedBtn>Test</ThemedBtn>);
expect(btn.getDOMNode().classList.contains('MuiButton-containedPrimary')).toBe(true);
expect(btn.find('button').getDOMNode().classList.contains('MuiButton-containedPrimary')).toBe(true);
});
it('DefaultButton', ()=>{
let ThemedBtn = withTheme(DefaultButton);
let btn = mount(<ThemedBtn className="testClass">Test</ThemedBtn>);
expect(btn.getDOMNode().classList.contains('MuiButton-outlined')).toBe(true);
expect(btn.getDOMNode().classList.contains('testClass')).toBe(true);
expect(btn.find('button').getDOMNode().classList.contains('MuiButton-outlined')).toBe(true);
expect(btn.find('button').getDOMNode().classList.contains('testClass')).toBe(true);
});
it('PgIconButton', ()=>{

View File

@ -37,12 +37,12 @@ describe('TabPanel', ()=>{
});
it('init', ()=>{
expect(panelInst.getDOMNode().hidden).toBeTrue();
expect(panelInst.find('div').at(0).getDOMNode().hidden).toBeTrue();
expect(panelInst.find('h1')).not.toBe(null);
});
it('tab select', ()=>{
panelInst.setProps({value: 0});
expect(panelInst.getDOMNode().hidden).toBeFalse();
expect(panelInst.find('div').at(0).getDOMNode().hidden).toBeFalse();
});
});