public inbox for [email protected]  
help / color / mirror / Atom feed
From: Aditya Toshniwal <[email protected]>
To: pgadmin-hackers <[email protected]>
To: Akshay Joshi <[email protected]>
Subject: Re: [pgAdmin][RM6131] Port query tool to React
Date: Mon, 18 Apr 2022 11:27:14 +0530
Message-ID: <CAM9w-_m2m5WFOroN_mkbbT39mYHWWTXjniv=Vcqrxv6Y=+ZpfA@mail.gmail.com> (raw)
In-Reply-To: <CAM9w-_k4sam2RbKxgKKp2ceNBQQn72zAOU4W7uFGWD_CEWpWCQ@mail.gmail.com>
References: <CAM9w-_=7+yaGeh=m=-jo8ZMtqQBVYeyKBFJgarGUU09Ff+w68w@mail.gmail.com>
	<CANxoLDdeFxtgGVAJZvyoCZ8kCFEHVCRyi2eBL35HqGuR6301AA@mail.gmail.com>
	<CAM9w-_kASJCjN1pHrMeTe3+eFc2DYDT7qdTCGj+EuhC54AxJNw@mail.gmail.com>
	<CAM9w-_=pGojP3aG_FY56BNJjZFK1ziOJrZXyVJ1COKtaFtqfpQ@mail.gmail.com>
	<CANxoLDc+cNawZ=LCbfU_2Cx4rCWkbfjMVfv8mmY=Wk7GSUjcJg@mail.gmail.com>
	<CAM9w-_mvwZ221u8ZnY7i4f_3ujKTyoJ3=4qiSEUdqAegScFxaw@mail.gmail.com>
	<CAM9w-_=F5vUWkeAL+52NfcQvf+W3MoLCa0Kac2N031qA9eet2g@mail.gmail.com>
	<CAM9w-_k4sam2RbKxgKKp2ceNBQQn72zAOU4W7uFGWD_CEWpWCQ@mail.gmail.com>

Hi,
Attached is the patch to fix the issues raised. Few of them are pending and
will send it later.
Fixed:

   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. Also 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.(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.

I was not able to reproduce some of the bugs on webpack dev mode, but
reproducible on webpack prod mode bundles. After a lot of debugging it
turned out that webpack/babel transpile was changing the meaning of a piece
of code in prod mode. I tweaked the code then most issues were not
reproducible anymore.
That said, following issues were not reproducible and this fix could be the
reason:

   1. Not able to load more than 1000 rows.
   2. Find/Replace both opens the same dialogue box.
   3. Try to edit cell with char varying data type(which opens text editor)
   > Scroll result grid > Click on another cell > query edior shows white
   screen & refresh is the only option left.(TypeError: Cannot read properties
   of null (reading 'querySelector')
   at getCellElement (sqleditor.js?ver=60800:1:995456))
   4. Generate script is not working for schema diff for tables with target
   only/ not working for any.(TypeError: Cannot read properties of undefined
   (reading 'database'))
   5. Query results are appended in the Notification tab.
   6. Panel name is NOT updated on opening file. Panel-name should be
   filename
   7. Open a file in query tool > Open another file > Check panel name > It
   is the first file name.
   8. Incorrect CSV downloaded (film table) when CSV quotes select single
   quote from preferences. *CSV generated at backend. No changes done.*
   9. In Data grid > Add New data to cell > without clicking on other cell
   click on Add New row > previous data is gone.


Please find the comment inline for other issues:

   1. Small white line is added below Total rows status bar.
   This is an existing issue with wcDocker. It is somehow not getting the
   correct size for the query tool. You can verify this by opening query
   tool in new tab.
   2. In explain > Data output > Query Plan is editable.
   To be precise, the JSON editor is editable but does not allow saving it.
   This is inline with other editors like text editor which allows editing but
   no save.
   3. Color is NOT fainted in View/Edit data when query tool is NOT
   editable.
   This is inline with other places where the SQL is read only like the
   properties dialog SQL tab or the RE-SQL tab. Plus, greying out of SQL
   affects query readability.
   4. If data in result grid is edited & changes are reverted, then also
   Save button remain enabled/ Cell is shown in bold indicating data is edited.
   This is based on existing behaviour. A separate RM can be raised to have
   any improvement in this.
   5. When the Save button is disabled then 'Save as' should be disabled as
   well.
   Save and Save As are different in behaviour. You can change a file and
   save it. The save will be disabled but the user should be allowed. I also
   checked the behaviour of VS-Code and PyCharm. They never disable the "save
   as" button. After all, there is no harm in allowing a user to save as even
   if it is empty.
   6. Manage Macros - Help button is disabled. Remove SQL help button(Not
   sure).
   As I already mentioned in the review by Askhay, the existing help button
   opens the query tool help. Query tool help is already added on the
   toolbar and so this one is disabled.
   7. Macros defined in one database are shown for other databases
   also/even across servers.
   As per the existing design.
   8. Query tool notifier setting is missing in preferences.
   Previously, the total time and number of rows were shown in the
   notifier. And so, the notifier setting was added so that users can tweak it
   to keep it open for a longer time. Now, we do not show those details on the
   notifier since we have a fixed status bar for that. This setting is not
   relevant anymore.


Issues that need to be checked and pending:

   1. Explain-Analyze with all option checked > Statistics tab > % of query
   is always 0 for node type. Need to check all the calculations.
   2. In the Result grid multiple rows can not be selected with shift +
   down arrow.
   3. In Geometry Viewer , map disappears if taken to bottom.
   4. Keyboard shortcut - Focus in query tool and try Previous/Next tab is
   Not working add quotes in query tool
   5. Keyboard shortcut Switch Panel is not working


On Thu, Apr 7, 2022 at 3:37 PM Aditya Toshniwal <
[email protected]> wrote:

> Hi,
>
> Please find an updated patch with PEP8 issues fixed.
>
> On Thu, Apr 7, 2022 at 3:12 PM Aditya Toshniwal <
> [email protected]> wrote:
>
>> Hi Hackers,
>>
>> Attached is updated patch which now also includes:
>> Can't copy and paste row correctly if first column contains no data #7294
>>
>> On Tue, Apr 5, 2022 at 5:45 PM Aditya Toshniwal <
>> [email protected]> wrote:
>>
>>> Hi Akshay,
>>>
>>> Thank you for doing such a detailed review. Please find my comments
>>> inline below and attached patch.
>>>
>>> On Thu, Mar 17, 2022 at 4:05 PM Akshay Joshi <
>>> [email protected]> wrote:
>>>
>>>> Hi Aditya
>>>>
>>>> Following are the review comments:
>>>>
>>>> *GUI:*
>>>>
>>>>    - The Maximize/Minimize button on the panel should be consistent
>>>>    with other panels.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Scratch Pad is missing or is there any new setting to add a
>>>>    scratch pad?
>>>>
>>>> Added. The layout lib currently does not have a context menu on the
>>> header to add a panel. For now, you can use the reset layout button to add
>>> the scratch pad again if closed. Context menu can be added separately
>>> later. Reset layout will not refresh the page.
>>>
>>>>
>>>>    - Press "Cmd + G" on Query Tool it opens the old search bar is it
>>>>    still valid
>>>>    - [image: Screenshot 2022-03-16 at 7.02.34 PM.png]
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    -
>>>>    - In case of any error, we should move the cursor to the error
>>>>    location. We are highlighting the error row but it should be scroll to that
>>>>    location in the editor.
>>>>
>>>> Existing behaviour. Improvement done.
>>>
>>>>
>>>>    - Error highlighting color should be aligned with the theme, check
>>>>    the existing color.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Error highlighting color is not cleared when running the
>>>>    successful query. Run "SELECT * from pg_class123" and then run "SELECT *
>>>>    from pg_class".
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - *ToolBar buttons*:
>>>>       - When the Save button is disabled then 'Save as' should be
>>>>       disabled as well.
>>>>
>>>> This is not correct. A user should be allowed to "Save as" a file even
>>> if it is saved and save button is disabled.
>>>
>>>>
>>>>    - Open any SQL file, change some text and click on the 'Save'
>>>>       button, No notifier message has been flashed that 'File saved successfully'
>>>>       and the button does not get disabled as well.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Format SQL not working.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Clear Query keyboard shortcut not working.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    -
>>>>       - Clicking on the 'Clear Query' menu should pop up a
>>>>       confirmation dialog 'Are you sure you wish to discard the current changes?'
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Open any SQL file, change some text, and try to open another
>>>>       file, it should pop up a confirmation dialog 'Are you sure you wish to
>>>>       discard the current changes?'
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - When clicking on the New Query Tool button it is not opening the
>>>>       new query tool window.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Shortcut Key for Replace is not correct on *Windows* (Tooltip
>>>>       showing Alt + Ctrl + F) but actual is (Shift + Ctrl + F)
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Most of the Keyboard shortcuts are not working on Windows at all.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - *New Connection Dialog:*
>>>>       - The close button should be right-aligned.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Unable to test further because when selecting any disconnected
>>>>       server it should pop up the password dialog to connect and then fetch the
>>>>       details like databases, users, roles.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Help buttons are disabled.
>>>>
>>>> The existing help button opens the query tool help. Query tool help is
>>> already added on the toolbar.
>>>
>>>>
>>>>    - *Query History*:
>>>>       - For some queries like 'ROLLBACK' and 'COMMIT,' Rows affected
>>>>       shows in the negative (-1).
>>>>
>>>> Based on existing. I have made it blank.
>>>
>>>>
>>>>    - *Remove All* should warn the user before removing everything "Are
>>>>       you sure you wish to remove all the history? This will remove all of your
>>>>       query histories from this and other sessions for this database."
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Duration is missing. It should be there with the Date and Rows
>>>>       affected.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - *Status Bar*:
>>>>       - We should display Milliseconds as well in Query Complete.
>>>>
>>>> It will now display in <hr>:<min>:<sec>.<msec> format. Fixed.
>>>
>>>>
>>>>    - Total Rows: *N of N, *I always observe the same, can you please
>>>>       change it to *N *only, Or am I missing some scenario where it is
>>>>       changed?
>>>>
>>>> We're fetching rows on demand. "X of N" says total X rows fetched till
>>> now but total N are available.
>>>
>>>>
>>>>    - *Data output*:
>>>>       - Default message "No data output......" should be shown when
>>>>       there are no rows/data, check the old behavior.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Extra space in the JSON Editor, even if I resize it, its height
>>>>       is not adjusted. Check the second screenshot
>>>>       - [image: Screenshot 2022-03-17 at 11.29.21 AM.png]             [image:
>>>>       Screenshot 2022-03-17 at 11.29.59 AM.png]
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    -
>>>>    - *Explain*:
>>>>       - Old content should be cleared from the panel if we run the new
>>>>       query by clicking the play button. It should show an informative message,
>>>>       check the old behavior.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - *Macros*:
>>>>       - The close button should be right-aligned.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Help buttons are disabled.
>>>>
>>>> The existing help button opens the query tool help. Query tool help is
>>> already added on the toolbar.
>>>
>>>>
>>>>    - *View/Edit Data*:
>>>>    - Clipboard issues.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Unable to edit table data even if the primary key is defined.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Select All rows is missing at the left corner of the 'Data
>>>>       Output' Panel.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Change some data and try to save if an error comes then Spinner
>>>>       is not cleared.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    -  Filtered Rows not working. Click on the 'Filtered Rows ...'
>>>>       context menu.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Sort/Filter Dialog is missing.
>>>>
>>>> The sort/filter dialog is present and opens when you click the filter
>>> button. I have removed the "Sort/Filter" menu item which does the same
>>> thing.
>>>
>>>>
>>>>    - Select any cell of the table content, click on any filter menu
>>>>       'Filter by selection' or 'Exclude by selection' multiple times. It updates
>>>>       the query every time and adds the condition which is not there previously.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Limit (100 rows, 500 rows ...) not working. View data of any
>>>>       table having 1000+ rows and then apply the limit.
>>>>
>>>> I am not able to reproduce this. Works fine.
>>>
>>>>
>>>>    - *Themes*:
>>>>       - Check and Fix all the issues related to the theme.
>>>>       - [image: Screenshot 2022-03-17 at 1.43.01 PM.png].
>>>>                     [image: Screenshot 2022-03-17 at 1.44.10 PM.png].
>>>>       -
>>>>       - [image: Screenshot 2022-03-17 at 1.44.53 PM.png]
>>>>                      [image: Screenshot 2022-03-17 at 1.45.54 PM.png]
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    -
>>>>
>>>>
>>>> *Code:*
>>>>
>>>>    - Fix pep8 issues.
>>>>
>>>> Fixed.
>>>
>>>>
>>>>    - Can we fix the *Deprecation Warnings?*
>>>>
>>>> I think those are coming from bootstrap. Need to check that separately.
>>> Not related to the query tool.
>>>
>>>> *Note: *Code review still remains, meanwhile you can start fixing the
>>>> above issues.
>>>>
>>>
>>> Forgot to mention in initial mail - I have removed dependency on
>>> Snap.svg and have written Explain SVG codes from scratch.
>>>
>>>>
>>>> On Wed, Mar 16, 2022 at 5:54 PM Aditya Toshniwal <
>>>> [email protected]> wrote:
>>>>
>>>>> Hi,
>>>>>
>>>>> Attached is an updated one with few more improvements and fixes.
>>>>>
>>>>> On Wed, Mar 16, 2022 at 1:40 PM Aditya Toshniwal <
>>>>> [email protected]> wrote:
>>>>>
>>>>>> Please find the attached patch :)
>>>>>>
>>>>>> On Wed, Mar 16, 2022 at 12:16 PM Akshay Joshi <
>>>>>> [email protected]> wrote:
>>>>>>
>>>>>>> Hi Aditya
>>>>>>>
>>>>>>> I think you forgot to attach the patch.
>>>>>>>
>>>>>>> On Tue, Mar 15, 2022 at 4:00 PM Aditya Toshniwal <
>>>>>>> [email protected]> wrote:
>>>>>>>
>>>>>>>> Hi Hackers,
>>>>>>>>
>>>>>>>> Attached is the initial patch that migrates the SQL Editor tool to
>>>>>>>> React based. Change highlights:
>>>>>>>> 1. Complete rewrite to React code.
>>>>>>>> 2. UI improvements based on suggestions and requests.
>>>>>>>> 3. Work towards stability and performance improvement.
>>>>>>>> 4. Keep row numbers in view when scrolling horizontally. Fixes #3989
>>>>>>>> 5. Fixed status bar at the bottom with useful details. Fixes #3253
>>>>>>>> 6. Relocate GIS Viewer Button to the Left Side of Results Table.
>>>>>>>> Fixed #6830
>>>>>>>> 7. Allow to remove single history records. Refs #4113
>>>>>>>> 8. Macros usability improvements. Ref #6969
>>>>>>>> 9. Connection bar visibility issue. Fixes #7188
>>>>>>>> 10. Query tool layout issues. Fixes #6725
>>>>>>>>
>>>>>>>> Please note, there are still few minor niggles at some places but
>>>>>>>> the patch qualified to be reviewed. We will need a good amount of time to
>>>>>>>> test this properly. So, I am sending the feature patch. JS test
>>>>>>>> cases and documentation patches will follow soon.
>>>>>>>>
>>>>>>>> Please review.
>>>>>>>>
>>>>>>>> [image: image.png]
>>>>>>>> --
>>>>>>>> Thanks,
>>>>>>>> Aditya Toshniwal
>>>>>>>> pgAdmin Hacker | Software Architect | *edbpostgres.com*
>>>>>>>> <http://edbpostgres.com;
>>>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>>>
>>>>>>>
>>>>>>>
>>>>>>> --
>>>>>>> *Thanks & Regards*
>>>>>>> *Akshay Joshi*
>>>>>>> *pgAdmin Hacker | Principal Software Architect*
>>>>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>>>>
>>>>>>> *Mobile: +91 976-788-8246*
>>>>>>>
>>>>>>
>>>>>>
>>>>>> --
>>>>>> Thanks,
>>>>>> Aditya Toshniwal
>>>>>> pgAdmin Hacker | Software Architect | *edbpostgres.com*
>>>>>> <http://edbpostgres.com;
>>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>>
>>>>>
>>>>>
>>>>> --
>>>>> Thanks,
>>>>> Aditya Toshniwal
>>>>> pgAdmin Hacker | Software Architect | *edbpostgres.com*
>>>>> <http://edbpostgres.com;
>>>>> "Don't Complain about Heat, Plant a TREE"
>>>>>
>>>>
>>>>
>>>> --
>>>> *Thanks & Regards*
>>>> *Akshay Joshi*
>>>> *pgAdmin Hacker | Principal Software Architect*
>>>> *EDB Postgres <http://edbpostgres.com>*
>>>>
>>>> *Mobile: +91 976-788-8246*
>>>>
>>>
>>>
>>> --
>>> Thanks,
>>> Aditya Toshniwal
>>> pgAdmin Hacker | Software Architect | *edbpostgres.com*
>>> <http://edbpostgres.com;
>>> "Don't Complain about Heat, Plant a TREE"
>>>
>>
>>
>> --
>> Thanks,
>> Aditya Toshniwal
>> pgAdmin Hacker | Software Architect | *edbpostgres.com*
>> <http://edbpostgres.com;
>> "Don't Complain about Heat, Plant a TREE"
>>
>
>
> --
> Thanks,
> Aditya Toshniwal
> pgAdmin Hacker | Software Architect | *edbpostgres.com*
> <http://edbpostgres.com;
> "Don't Complain about Heat, Plant a TREE"
>


-- 
Thanks,
Aditya Toshniwal
pgAdmin Hacker | Software Architect | *edbpostgres.com*
<http://edbpostgres.com;
"Don't Complain about Heat, Plant a TREE"


Attachments:

  [image/png] image.png (177.9K, 3-image.png)
  download | view image

  [image/png] Screenshot 2022-03-16 at 7.02.34 PM.png (25.0K, 4-Screenshot%202022-03-16%20at%207.02.34%20PM.png)
  download | view image

  [image/png] Screenshot 2022-03-17 at 11.29.21 AM.png (70.5K, 5-Screenshot%202022-03-17%20at%2011.29.21%20AM.png)
  download | view image

  [image/png] Screenshot 2022-03-17 at 11.29.59 AM.png (82.7K, 6-Screenshot%202022-03-17%20at%2011.29.59%20AM.png)
  download | view image

  [image/png] Screenshot 2022-03-17 at 1.43.01 PM.png (315.4K, 7-Screenshot%202022-03-17%20at%201.43.01%20PM.png)
  download | view image

  [image/png] Screenshot 2022-03-17 at 1.44.10 PM.png (105.7K, 8-Screenshot%202022-03-17%20at%201.44.10%20PM.png)
  download | view image

  [image/png] Screenshot 2022-03-17 at 1.44.53 PM.png (49.7K, 9-Screenshot%202022-03-17%20at%201.44.53%20PM.png)
  download | view image

  [image/png] Screenshot 2022-03-17 at 1.45.54 PM.png (158.6K, 10-Screenshot%202022-03-17%20at%201.45.54%20PM.png)
  download | view image

  [application/octet-stream] RM6131.part2.patch (55.8K, 11-RM6131.part2.patch)
  download | inline diff:
diff --git a/web/pgadmin/browser/static/js/toolbar.js b/web/pgadmin/browser/static/js/toolbar.js
index fc915026e..af3ad7d13 100644
--- a/web/pgadmin/browser/static/js/toolbar.js
+++ b/web/pgadmin/browser/static/js/toolbar.js
@@ -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')){
diff --git a/web/pgadmin/static/js/Explain/Analysis.jsx b/web/pgadmin/static/js/Explain/Analysis.jsx
index fa822b156..bf4553dd2 100644
--- a/web/pgadmin/static/js/Explain/Analysis.jsx
+++ b/web/pgadmin/static/js/Explain/Analysis.jsx
@@ -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">
diff --git a/web/pgadmin/static/js/Explain/Graphical.jsx b/web/pgadmin/static/js/Explain/Graphical.jsx
index 2c7ba2101..be722f4b6 100644
--- a/web/pgadmin/static/js/Explain/Graphical.jsx
+++ b/web/pgadmin/static/js/Explain/Graphical.jsx
@@ -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}
diff --git a/web/pgadmin/static/js/Explain/index.jsx b/web/pgadmin/static/js/Explain/index.jsx
index f5845a9e8..c5ff0bbe5 100644
--- a/web/pgadmin/static/js/Explain/index.jsx
+++ b/web/pgadmin/static/js/Explain/index.jsx
@@ -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">
diff --git a/web/pgadmin/static/js/Theme/index.jsx b/web/pgadmin/static/js/Theme/index.jsx
index 9124743ec..402e82439 100644
--- a/web/pgadmin/static/js/Theme/index.jsx
+++ b/web/pgadmin/static/js/Theme/index.jsx
@@ -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',
       },
diff --git a/web/pgadmin/static/js/Theme/standard.js b/web/pgadmin/static/js/Theme/standard.js
index 1efe55300..8b8b1fe41 100644
--- a/web/pgadmin/static/js/Theme/standard.js
+++ b/web/pgadmin/static/js/Theme/standard.js
@@ -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'
+        },
+      }
     }
   });
 }
diff --git a/web/pgadmin/static/js/components/CodeMirror.jsx b/web/pgadmin/static/js/components/CodeMirror.jsx
index de891d62b..218efc8a3 100644
--- a/web/pgadmin/static/js/components/CodeMirror.jsx
+++ b/web/pgadmin/static/js/components/CodeMirror.jsx
@@ -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);
diff --git a/web/pgadmin/static/js/components/ExternalIcon.jsx b/web/pgadmin/static/js/components/ExternalIcon.jsx
index 3fa91b895..249feee7d 100644
--- a/web/pgadmin/static/js/components/ExternalIcon.jsx
+++ b/web/pgadmin/static/js/components/ExternalIcon.jsx
@@ -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};
diff --git a/web/pgadmin/static/js/components/Menu.jsx b/web/pgadmin/static/js/components/Menu.jsx
index 02b3e0b3c..84f51f65c 100644
--- a/web/pgadmin/static/js/components/Menu.jsx
+++ b/web/pgadmin/static/js/components/Menu.jsx
@@ -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,
+  };
+}
diff --git a/web/pgadmin/static/js/helpers/EventBus.js b/web/pgadmin/static/js/helpers/EventBus.js
index 9a6906429..848d2822c 100644
--- a/web/pgadmin/static/js/helpers/EventBus.js
+++ b/web/pgadmin/static/js/helpers/EventBus.js
@@ -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);
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx
index 6d2af6045..a2b000bed 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx
@@ -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}
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/Editors.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/Editors.jsx
index b48152b11..48739c644 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/Editors.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/Editors.jsx
@@ -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'}
         />
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/dialogs/NewConnectionDialog.jsx b/web/pgadmin/tools/sqleditor/static/js/components/dialogs/NewConnectionDialog.jsx
index fc35c0964..f0763d543 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/dialogs/NewConnectionDialog.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/dialogs/NewConnectionDialog.jsx
@@ -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({
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/ConnectionBar.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/ConnectionBar.jsx
index da1493e69..cf9db036a 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/sections/ConnectionBar.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/ConnectionBar.jsx
@@ -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
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx
index 755ba4662..97006107e 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/GeometryViewer.jsx
@@ -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}
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx
index 42d1222c5..fe0f60a5e 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx
@@ -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 />
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/QueryHistory.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/QueryHistory.jsx
index cbbca3486..b3c4dc18a 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/sections/QueryHistory.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/QueryHistory.jsx
@@ -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 flexBasis="50%" maxWidth="50%" overflow="auto">
+                <QueryHistoryDetails entry={selectedEntry}/>
               </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>
       ), [selectedItemKey, showInternal, qhu.current.size()])}
     </>
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx
index ee5b2245a..553f2b647 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx
@@ -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)) {
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSetToolbar.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSetToolbar.jsx
index e51e2ad5a..3fd5cd034 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSetToolbar.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSetToolbar.jsx
@@ -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>
     </>
   );
diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/StatusBar.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/StatusBar.jsx
index df562884e..446075cd9 100644
--- a/web/pgadmin/tools/sqleditor/static/js/components/sections/StatusBar.jsx
+++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/StatusBar.jsx
@@ -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>
   );
 }
diff --git a/web/regression/javascript/components/Buttons.spec.js b/web/regression/javascript/components/Buttons.spec.js
index 9bedc95ef..7e2269c51 100644
--- a/web/regression/javascript/components/Buttons.spec.js
+++ b/web/regression/javascript/components/Buttons.spec.js
@@ -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', ()=>{
diff --git a/web/regression/javascript/components/TabPanel.spec.js b/web/regression/javascript/components/TabPanel.spec.js
index e99a062b4..1f1067860 100644
--- a/web/regression/javascript/components/TabPanel.spec.js
+++ b/web/regression/javascript/components/TabPanel.spec.js
@@ -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();
   });
 });


view thread (25+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected]
  Subject: Re: [pgAdmin][RM6131] Port query tool to React
  In-Reply-To: <CAM9w-_m2m5WFOroN_mkbbT39mYHWWTXjniv=Vcqrxv6Y=+ZpfA@mail.gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox