mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
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:
parent
74e3f976c1
commit
9c30d983bd
@ -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')){
|
||||
|
@ -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' ? '↑' : '↓')
|
||||
}</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' ? <>↑</> : <>↓</>)
|
||||
} {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">
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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',
|
||||
},
|
||||
|
@ -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'
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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};
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
|
@ -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'}
|
||||
/>
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
|
@ -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='© <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}
|
||||
|
@ -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 />
|
||||
|
@ -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()])}
|
||||
</>
|
||||
|
@ -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)) {
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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', ()=>{
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user