Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7fd7d67b3 | ||
|
|
96c360e728 | ||
|
|
3e3f06c55d | ||
|
|
1745aaaa0a | ||
|
|
c67ed7d7d2 | ||
|
|
f6d8f9f5c5 | ||
|
|
a4c12555cb | ||
|
|
8d1868305d | ||
|
|
2cdee919c1 | ||
|
|
3f8c36789f | ||
|
|
631a08189a | ||
|
|
4808df5e95 | ||
|
|
4585597388 | ||
|
|
8f73956d1e | ||
|
|
8f2bda2309 | ||
|
|
9dde195af4 | ||
|
|
9468c80a78 | ||
|
|
1fecf70123 | ||
|
|
a11f42b3e0 | ||
|
|
683528b81c | ||
|
|
02b0f35442 | ||
|
|
1a5e60c44f | ||
|
|
a16e3523bd | ||
|
|
50c930f5fa | ||
|
|
9cdcad7a88 | ||
|
|
59f5c0d955 | ||
|
|
64e6700228 | ||
|
|
7414394ceb | ||
|
|
8b62cd1f04 | ||
|
|
239e3bb6f1 | ||
|
|
b283c0ba18 | ||
|
|
e59471d87d | ||
|
|
6bebce7015 | ||
|
|
eb65ab6b4b | ||
|
|
3a69d50458 | ||
|
|
9c45a62b53 | ||
|
|
ed59584fe4 | ||
|
|
bf7de8e7a5 | ||
|
|
9c745db413 | ||
|
|
7d0ed90796 | ||
|
|
b0681f55e9 | ||
|
|
86b30b3c83 | ||
|
|
b92e2fcfc9 | ||
|
|
b1f6664292 | ||
|
|
cb635f6706 |
No files matched your search
@@ -195,6 +195,7 @@ aspen-core 1.0.5
|
||||
aspen-decorations 1.1.1 MIT git+https://github.com/neeksandhu/aspen-decorations.git
|
||||
aspen-tree-model 1.0.5 MIT git+https://github.com/neeksandhu/aspen-tree-model.git
|
||||
async 2.6.4 MIT https://github.com/caolan/async.git
|
||||
attr-accept 2.2.2 MIT https://github.com/react-dropzone/attr-accept.git
|
||||
axios 0.21.4 MIT https://github.com/axios/axios.git
|
||||
babel-plugin-styled-components 1.13.3 MIT https://github.com/styled-components/babel-plugin-styled-components.git
|
||||
babel-plugin-syntax-jsx 6.18.0 MIT https://github.com/babel/babel/tree/master/packages/babel-plugin-syntax-jsx
|
||||
@@ -239,6 +240,7 @@ commander 7.2.0
|
||||
concat-map 0.0.1 MIT git://github.com/substack/node-concat-map.git
|
||||
context-menu 2.0.0 MIT https://github.com/chipto/context-menu.git
|
||||
convert-source-map 1.8.0 MIT git://github.com/thlorenz/convert-source-map.git
|
||||
convert-units 2.3.4 MIT git://github.com/ben-ng/convert-units.git
|
||||
core-js 3.19.1 MIT https://github.com/zloirock/core-js.git
|
||||
css-color-names 1.0.1 MIT git://github.com/bahamas10/css-color-names.git
|
||||
css-line-break 2.0.1 MIT git+ssh://git@github.com/niklasvh/css-line-break.git
|
||||
@@ -255,8 +257,7 @@ csstype 2.6.18
|
||||
csstype 3.0.9 MIT https://github.com/frenic/csstype
|
||||
dagre 0.8.5 MIT https://github.com/dagrejs/dagre.git
|
||||
date-fns 2.25.0 MIT https://github.com/date-fns/date-fns
|
||||
debug 4.3.2 MIT git://github.com/visionmedia/debug.git
|
||||
debug 4.3.3 MIT git://github.com/debug-js/debug.git
|
||||
debug 4.3.4 MIT git://github.com/debug-js/debug.git
|
||||
decamelize 5.0.1 MIT https://github.com/sindresorhus/decamelize.git
|
||||
decamelize 1.2.0 MIT https://github.com/sindresorhus/decamelize.git
|
||||
decamelize-keys 1.1.0 MIT https://github.com/dsblv/decamelize-keys.git
|
||||
@@ -266,7 +267,6 @@ dir-glob 3.0.1
|
||||
dom-align 1.12.2 MIT git@github.com:yiminghe/dom-align.git
|
||||
dom-helpers 5.2.1 MIT git+https://github.com/react-bootstrap/dom-helpers.git
|
||||
dom-serializer 1.3.2 MIT git://github.com/cheeriojs/dom-renderer.git
|
||||
dropzone 5.9.3 MIT https://github.com/dropzone/dropzone.git
|
||||
emojis-list 3.0.0 MIT git+https://github.com/kikobeats/emojis-list.git
|
||||
engine.io-client 6.0.2 MIT https://github.com/socketio/engine.io-client.git
|
||||
engine.io-parser 5.0.3 MIT git@github.com:socketio/engine.io-parser.git
|
||||
@@ -277,6 +277,7 @@ fast-deep-equal 3.1.3
|
||||
fast-glob 3.2.11 MIT https://github.com/mrmlnc/fast-glob.git
|
||||
fast-json-stable-stringify 2.1.0 MIT git://github.com/epoberezkin/fast-json-stable-stringify.git
|
||||
fast-memoize 2.5.2 MIT git+https://github.com/caiogondim/fast-memoize.git
|
||||
file-selector 0.6.0 MIT https://github.com/react-dropzone/file-selector.git
|
||||
filename-reserved-regex 2.0.0 MIT https://github.com/sindresorhus/filename-reserved-regex.git
|
||||
fill-range 7.0.1 MIT https://github.com/jonschlinkert/fill-range.git
|
||||
find-up 5.0.0 MIT https://github.com/sindresorhus/find-up.git
|
||||
@@ -315,7 +316,7 @@ is-unc-path 1.0.0
|
||||
javascript-natural-sort 0.7.1 MIT https://github.com/Bill4Time/javascript-natural-sort.git
|
||||
jquery 3.6.0 MIT https://github.com/jquery/jquery.git
|
||||
jquery-contextmenu 2.9.2 MIT git://github.com/swisnl/jQuery-contextMenu.git
|
||||
jquery-ui 1.13.0 MIT git://github.com/jquery/jquery-ui.git
|
||||
jquery-ui 1.13.2 MIT git://github.com/jquery/jquery-ui.git
|
||||
js-tokens 4.0.0 MIT https://github.com/lydell/js-tokens.git
|
||||
jsbn 1.1.0 MIT https://github.com/andyperlitch/jsbn.git
|
||||
jsesc 2.5.2 MIT https://github.com/mathiasbynens/jsesc.git
|
||||
@@ -343,7 +344,26 @@ loader-utils 2.0.2
|
||||
loader-utils 1.4.0 MIT https://github.com/webpack/loader-utils.git
|
||||
locate-path 6.0.0 MIT https://github.com/sindresorhus/locate-path.git
|
||||
lodash 4.17.21 MIT https://github.com/lodash/lodash.git
|
||||
lodash._basebind 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash._basecreate 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash._basecreatecallback 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash._basecreatewrapper 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash._createwrapper 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash._objecttypes 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash._renative 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash._setbinddata 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash._shimkeys 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash._slice 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash.bind 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash.foreach 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash.forown 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash.identity 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash.isfunction 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash.isobject 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash.keys 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash.memoize 4.1.2 MIT https://github.com/lodash/lodash.git
|
||||
lodash.noop 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash.support 2.3.0 MIT https://github.com/lodash/lodash-cli.git
|
||||
lodash.uniq 4.5.0 MIT https://github.com/lodash/lodash.git
|
||||
loose-envify 1.4.0 MIT git://github.com/zertosh/loose-envify.git
|
||||
lunr 0.7.2 MIT https://github.com/olivernn/lunr.js.git
|
||||
@@ -364,9 +384,8 @@ ml-array-min 1.2.2
|
||||
ml-array-rescale 1.3.6 MIT git+https://github.com/mljs/array.git
|
||||
ml-matrix 6.8.0 MIT https://github.com/mljs/matrix.git
|
||||
mobius1-selectr 2.4.13 MIT git+https://github.com/Mobius1/Selectr.git
|
||||
moment 2.29.3 MIT https://github.com/moment/moment.git
|
||||
moment 2.29.4 MIT https://github.com/moment/moment.git
|
||||
moment-timezone 0.5.34 MIT https://github.com/moment/moment-timezone.git
|
||||
moment-timezone 0.5.33 MIT https://github.com/moment/moment-timezone.git
|
||||
ms 2.1.2 MIT https://github.com/zeit/ms.git
|
||||
nanoid 3.3.2 MIT https://github.com/ai/nanoid.git
|
||||
nanoid 3.2.0 MIT https://github.com/ai/nanoid.git
|
||||
@@ -425,6 +444,7 @@ postcss-svgo 5.0.3
|
||||
postcss-unique-selectors 5.0.1 MIT https://github.com/cssnano/cssnano.git
|
||||
postcss-value-parser 4.1.0 MIT https://github.com/TrySound/postcss-value-parser.git
|
||||
prop-types 15.7.2 MIT https://github.com/facebook/prop-types.git
|
||||
prop-types 15.8.1 MIT https://github.com/facebook/prop-types.git
|
||||
punycode 2.1.1 MIT https://github.com/bestiejs/punycode.js.git
|
||||
queue-microtask 1.2.3 MIT git://github.com/feross/queue-microtask.git
|
||||
quick-lru 5.1.1 MIT https://github.com/sindresorhus/quick-lru.git
|
||||
@@ -449,6 +469,7 @@ react-dom 16.14.0
|
||||
react-dom 17.0.2 MIT https://github.com/facebook/react.git
|
||||
react-draggable 4.4.4 MIT https://github.com/react-grid-layout/react-draggable.git
|
||||
react-draggable 4.4.3 MIT https://github.com/mzabriskie/react-draggable.git
|
||||
react-dropzone 14.2.1 MIT https://github.com/react-dropzone/react-dropzone.git
|
||||
react-input-autosize 3.0.0 MIT https://github.com/JedWatson/react-input-autosize.git
|
||||
react-is 16.13.1 MIT https://github.com/facebook/react.git
|
||||
react-is 17.0.2 MIT https://github.com/facebook/react.git
|
||||
@@ -545,6 +566,7 @@ postcss-modules-scope 3.0.0
|
||||
postcss-modules-values 4.0.0 ISC git+https://github.com/css-modules/postcss-modules-values.git
|
||||
react-timer-hook 3.0.5 ISC git+https://github.com/amrlabib/react-timer-hook.git
|
||||
semver 7.3.5 ISC https://github.com/npm/node-semver
|
||||
semver 7.3.7 ISC https://github.com/npm/node-semver.git
|
||||
semver 6.3.0 ISC https://github.com/npm/node-semver
|
||||
vanilla-picker 2.12.1 ISC https://github.com/Sphinxxxx/vanilla-picker.git
|
||||
yallist 4.0.0 ISC git+https://github.com/isaacs/yallist.git
|
||||
@@ -594,9 +616,9 @@ pako 1.0.11
|
||||
precond 0.2.3 MIT* https://github.com/MathieuTurcotte/node-precond.git
|
||||
webcabin-docker 2.2.5 MIT* Unknown
|
||||
spdx-exceptions 2.3.0 CC-BY-3.0 https://github.com/kemitchell/spdx-exceptions.json.git
|
||||
tablesorter 2.31.3 (MIT OR GPL-2.0) git://github.com/Mottie/tablesorter.git
|
||||
tslib 2.4.0 0BSD https://github.com/Microsoft/tslib.git
|
||||
tslib 2.3.0 0BSD https://github.com/Microsoft/tslib.git
|
||||
type-fest 1.4.0 (MIT OR CC0-1.0) https://github.com/sindresorhus/type-fest.git
|
||||
|
||||
518 dependencies listed.
|
||||
540 dependencies listed.
|
||||
|
||||
@@ -19,7 +19,7 @@ the restore point to add* to provide a descriptive name for the restore point.
|
||||
|
||||
For more information about using a restore point as a recovery target, please
|
||||
see the
|
||||
`PostgreSQL documentation <https://www.postgresql.org/docs/current/recovery-target-settings.html#RECOVERY-TARGET-NAME>`_.
|
||||
`PostgreSQL documentation <https://www.postgresql.org/docs/current/runtime-config-wal.html#RUNTIME-CONFIG-WAL-RECOVERY-TARGET>`_.
|
||||
|
||||
* Click the *OK* button to save the restore point.
|
||||
* Click the *Cancel* button to exit without saving work.
|
||||
* Click the *Cancel* button to exit without saving work.
|
||||
@@ -36,5 +36,8 @@ Use the *Change Password* dialog to change your password:
|
||||
* Enter the desired password for in the *New Password* field.
|
||||
* Re-enter the new password in the *Confirm Password* field.
|
||||
|
||||
Click the *OK* button to change your password; click *Cancel* to exit the dialog
|
||||
without changing your password.
|
||||
Click the *Save* button to change your password.
|
||||
|
||||
Click the *Close* button to exit without changing your password.
|
||||
|
||||
Click the *Reset* button to reset the values.
|
||||
@@ -10,8 +10,6 @@ To deploy a PostgreSQL server on the Azure cloud, follow the below steps.
|
||||
:alt: Cloud Deployment
|
||||
:align: center
|
||||
|
||||
**Note:** This feature is currently available in Desktop mode only.
|
||||
|
||||
Once you launch the tool, select the Azure PostgreSQL option.
|
||||
Click on the *Next* button to proceed further.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
@@ -8,39 +8,36 @@ Use the *Import/Export data* dialog to copy data from a table to a file, or copy
|
||||
data from a file into a table.
|
||||
|
||||
The *Import/Export data* dialog organizes the import/export of data through the
|
||||
*Options* and *Columns* tabs.
|
||||
*General*, *Options* and *Columns* tabs.
|
||||
|
||||
.. image:: images/import_export_general.png
|
||||
:alt: Import Export data dialog general tab
|
||||
:align: center
|
||||
|
||||
Use the fields in the *General* tab to specify import and export preferences:
|
||||
|
||||
* Move the *Import/Export* switch to the *Import* position to specify that the
|
||||
server should import data to a table from a file. The default is *Import*.
|
||||
|
||||
* Enter the name of the source or target file in the *Filename* field.
|
||||
Optionally, select the *Browser* icon (ellipsis) to the right to navigate
|
||||
into a directory and select a file.
|
||||
|
||||
* Use the drop-down listbox in the *Format* field to specify the file type.
|
||||
Select:
|
||||
|
||||
* *binary* for a .bin file.
|
||||
* *csv* for a .csv file.
|
||||
* *text* for a .txt file.
|
||||
|
||||
* Use the drop-down listbox in the *Encoding* field to specify the type of
|
||||
character encoding.
|
||||
|
||||
.. image:: images/import_export_options.png
|
||||
:alt: Import Export data dialog options tab
|
||||
:align: center
|
||||
|
||||
Use the fields in the *Options* tab to specify import and export preferences:
|
||||
|
||||
* Move the *Import/Export* switch to the *Import* position to specify that the
|
||||
server should import data to a table from a file. The default is *Export*.
|
||||
|
||||
* Use the fields in the *File Info* field box to specify information about the
|
||||
source or target file:
|
||||
|
||||
* Enter the name of the source or target file in the *Filename* field.
|
||||
Optionally, select the *Browser* icon (ellipsis) to the right to navigate
|
||||
into a directory and select a file.
|
||||
* Use the drop-down listbox in the *Format* field to specify the file type.
|
||||
Select:
|
||||
|
||||
* *binary* for a .bin file.
|
||||
* *csv* for a .csv file.
|
||||
* *text* for a .txt file.
|
||||
|
||||
* Use the drop-down listbox in the *Encoding* field to specify the type of
|
||||
character encoding.
|
||||
|
||||
.. image:: images/import_export_miscellaneous.png
|
||||
:alt: Import Export data dialog miscellaneous tab
|
||||
:align: center
|
||||
|
||||
* Use the fields in the *Miscellaneous* field box to specify additional
|
||||
information:
|
||||
* Use the fields in the *Options* tab to specify additional information:
|
||||
|
||||
* Move the *OID* switch to the *Yes* position to include the *OID* column.
|
||||
The *OID* is a system-assigned value that may not be modified. The default
|
||||
@@ -57,6 +54,8 @@ Use the fields in the *Options* tab to specify import and export preferences:
|
||||
be a single quote or a double quote.
|
||||
* Specify a character that should appear before a data character that matches
|
||||
the *QUOTE* value in the *Escape* field.
|
||||
* Use the *NULL Strings* field to specify a string that will represent a null
|
||||
value within the source or target file.
|
||||
|
||||
Click the *Columns* tab to continue.
|
||||
|
||||
@@ -71,9 +70,8 @@ or exported:
|
||||
columns from the drop-down listbox. To delete a selection, click the *x* to
|
||||
the left of the column name. Click an empty spot inside the field to access
|
||||
the drop-down list.
|
||||
* Use the *NULL Strings* field to specify a string that will represent a null
|
||||
value within the source or target file.
|
||||
* If enabled, click inside the *Not null columns* field to select one or more
|
||||
|
||||
* If enabled, click inside the *NOT NULL columns* field to select one or more
|
||||
columns that will not be checked for a NULL value. To delete a column, click
|
||||
the *x* to the left of the column name.
|
||||
|
||||
|
||||
@@ -290,6 +290,9 @@ preferences for the Query Editor tool.
|
||||
|
||||
Use the fields on the *Auto Completion* panel to set the auto completion options.
|
||||
|
||||
* When the *Autocomplete on key press* switch is set to *True* then autocomplete
|
||||
will be available on key press along with CTRL/CMD + Space. If it is set to
|
||||
*False* then autocomplete is only activated when CTRL/CMD + Space is pressed.
|
||||
* When the *Keywords in uppercase* switch is set to *True* then keywords are
|
||||
shown in upper case.
|
||||
|
||||
|
||||
@@ -331,13 +331,21 @@ Graph Visualiser Panel
|
||||
**********************
|
||||
|
||||
Click the Graph Visualiser button in the toolbar to generate the *Graphs* of
|
||||
the query results. The graph visualiser currently supports only Line Charts,
|
||||
but more charts (Bar, Stacked Bar, Pie...) will be added soon.
|
||||
the query results. The graph visualiser supports Line Charts, Stacked Line Charts,
|
||||
Bar Charts, Stacked Bar Charts, and Pie Charts.
|
||||
|
||||
.. image:: images/query_graph_visualiser_panel.png
|
||||
:alt: Query tool graph visualiser panel
|
||||
:align: center
|
||||
|
||||
* Graph Type
|
||||
|
||||
Choose the type of the graph that you would like to generate.
|
||||
|
||||
.. image:: images/query_graph_type.png
|
||||
:alt: Query tool graph visualiser graph type
|
||||
:align: center
|
||||
|
||||
* X Axis
|
||||
|
||||
Choose the column whose value you wish to display on X-axis from the *X Axis*
|
||||
@@ -358,15 +366,6 @@ from the drop-down menu to select all the columns.
|
||||
:alt: Query tool graph visualiser yaxis
|
||||
:align: center
|
||||
|
||||
* Graph Type
|
||||
|
||||
Choose the type of the graph that you would like to generate. Currently only
|
||||
*Line Charts* option is there, but more charts will be added soon.
|
||||
|
||||
.. image:: images/query_graph_type.png
|
||||
:alt: Query tool graph visualiser graph type
|
||||
:align: center
|
||||
|
||||
* Download and Zoom button
|
||||
|
||||
Zooming is performed by clicking and selecting an area over the chart with the
|
||||
@@ -382,7 +381,8 @@ Click the *Download* button on the button bar to download the chart.
|
||||
Line Chart
|
||||
==========
|
||||
|
||||
The *Line Chart* can be generated by selecting the X-axis and the Y-axis and
|
||||
The *Line Chart* can be generated by selecting the 'Line Chart'
|
||||
from the Graph Type drop-down, selecting the X-axis and the Y-axis, and
|
||||
clicking on the 'Generate' button. Below is an example of a chart of employee
|
||||
names and their salaries.
|
||||
|
||||
@@ -393,6 +393,52 @@ names and their salaries.
|
||||
Set *Use different data point styles?* option to true in the :ref:`preferences`,
|
||||
to show data points in a different style on each graph lines.
|
||||
|
||||
Stacked Line Chart
|
||||
==================
|
||||
|
||||
The *Stacked Line Chart* can be generated by selecting the 'Stacked Line Chart'
|
||||
from the Graph Type drop-down, selecting the X-axis and the Y-axis, and
|
||||
clicking on the 'Generate' button.
|
||||
|
||||
.. image:: images/query_stacked_line_chart.png
|
||||
:alt: Query tool graph visualiser stacked line chart
|
||||
:align: center
|
||||
|
||||
Bar Chart
|
||||
==========
|
||||
|
||||
The *Bar Chart* can be generated by selecting the 'Bar Chart'
|
||||
from the Graph Type drop-down, selecting the X-axis and the Y-axis, and
|
||||
clicking on the 'Generate' button.
|
||||
|
||||
.. image:: images/query_bar_chart.png
|
||||
:alt: Query tool graph visualiser bar chart
|
||||
:align: center
|
||||
|
||||
Stacked Bar Chart
|
||||
=================
|
||||
|
||||
The *Stacked Bar Chart* can be generated by selecting the 'Stacked Bar Chart'
|
||||
from the Graph Type drop-down, selecting the X-axis and the Y-axis, and
|
||||
clicking on the 'Generate' button.
|
||||
|
||||
|
||||
.. image:: images/query_stacked_bar_chart.png
|
||||
:alt: Query tool graph visualiser stacked bar chart
|
||||
:align: center
|
||||
|
||||
Pie Chart
|
||||
=========
|
||||
|
||||
The *Pie Chart* can be generated by selecting the 'Pie Chart'
|
||||
from the Graph Type drop-down, selecting the Label and Value, and
|
||||
clicking on the 'Generate' button.
|
||||
|
||||
|
||||
.. image:: images/query_pie_chart.png
|
||||
:alt: Query tool graph visualiser pie chart
|
||||
:align: center
|
||||
|
||||
Connection Status
|
||||
*****************
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ notes for it.
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
release_notes_6_12
|
||||
release_notes_6_11
|
||||
release_notes_6_10
|
||||
release_notes_6_9
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
************
|
||||
Version 6.12
|
||||
************
|
||||
|
||||
Release date: 2022-07-28
|
||||
|
||||
This release contains a number of bug fixes and new features since the release of pgAdmin 4 v6.11.
|
||||
|
||||
New features
|
||||
************
|
||||
|
||||
| `Issue #4488 <https://redmine.postgresql.org/issues/4488>`_ - Added option to trigger autocomplete on key press in the query tool.
|
||||
| `Issue #4607 <https://redmine.postgresql.org/issues/4607>`_ - Allow users to delete files/folders from the storage manager.
|
||||
| `Issue #7389 <https://redmine.postgresql.org/issues/7389>`_ - Allow users to search within the file/storage manager.
|
||||
| `Issue #7486 <https://redmine.postgresql.org/issues/7486>`_ - Added support for visualizing the graphs using Stacked Line, Bar, and Stacked Bar charts in the query tool.
|
||||
| `Issue #7487 <https://redmine.postgresql.org/issues/7487>`_ - Added support for visualise the graph using a Pie chart in the query tool.
|
||||
|
||||
Housekeeping
|
||||
************
|
||||
|
||||
| `Issue #7313 <https://redmine.postgresql.org/issues/7313>`_ - Port the file/storage manager to React.
|
||||
| `Issue #7341 <https://redmine.postgresql.org/issues/7341>`_ - Port change password dialog to React.
|
||||
| `Issue #7342 <https://redmine.postgresql.org/issues/7342>`_ - Port Master Password dialog to React.
|
||||
| `Issue #7492 <https://redmine.postgresql.org/issues/7492>`_ - Removing dynamic module loading and replacing it with static loading.
|
||||
| `Issue #7546 <https://redmine.postgresql.org/issues/7546>`_ - Port named restore point dialog to React.
|
||||
|
||||
Bug fixes
|
||||
*********
|
||||
|
||||
| `Issue #7428 <https://redmine.postgresql.org/issues/7428>`_ - Preserve the settings set by the user in the import/export data dialog.
|
||||
| `Issue #7471 <https://redmine.postgresql.org/issues/7471>`_ - Ensure that the splash screen can be moved.
|
||||
| `Issue #7508 <https://redmine.postgresql.org/issues/7508>`_ - Fixed an issue where comments on indexes are not displayed.
|
||||
| `Issue #7512 <https://redmine.postgresql.org/issues/7512>`_ - Ensure that notices should not disappear from the messages tab.
|
||||
| `Issue #7517 <https://redmine.postgresql.org/issues/7517>`_ - Enable the start debugging button once execution is completed.
|
||||
| `Issue #7518 <https://redmine.postgresql.org/issues/7518>`_ - Ensure that dashboard graph API is not called after the panel has been closed.
|
||||
| `Issue #7519 <https://redmine.postgresql.org/issues/7519>`_ - Ensure that geometry should be shown for all the selected cells.
|
||||
| `Issue #7520 <https://redmine.postgresql.org/issues/7520>`_ - Fixed the JSON editor issue of hiding the first record.
|
||||
| `Issue #7522 <https://redmine.postgresql.org/issues/7522>`_ - Added support for Azure PostgreSQL deployment in server mode.
|
||||
| `Issue #7523 <https://redmine.postgresql.org/issues/7523>`_ - Fixed typo error for Statistics on the table header.
|
||||
| `Issue #7524 <https://redmine.postgresql.org/issues/7524>`_ - Fixed an issue where new folders cannot be created in the save dialog.
|
||||
@@ -20,48 +20,54 @@ Use icons on the top of the *Storage Manager* window to manage storage:
|
||||
|
||||
Use the ``Home`` icon |home| to return to the home directory.
|
||||
|
||||
.. |home| image:: images/home.png
|
||||
.. |home| image:: images/sm_home.png
|
||||
|
||||
Use the ``Up Arrow`` icon |uparrow| to return to the previous directory.
|
||||
|
||||
.. |uparrow| image:: images/uparrow.png
|
||||
.. |uparrow| image:: images/sm_go_back.png
|
||||
|
||||
Use the ``Refresh`` icon |refresh| to display the most-recent files available.
|
||||
|
||||
.. |refresh| image:: images/refresh.png
|
||||
.. |refresh| image:: images/sm_refresh.png
|
||||
|
||||
Select the ``Download`` icon |download| to download the selected file.
|
||||
|
||||
.. |download| image:: images/download.png
|
||||
|
||||
Select the ``Delete`` icon |delete| to delete the selected file or folder.
|
||||
|
||||
.. |delete| image:: images/delete.png
|
||||
|
||||
Select the ``Edit`` icon |edit| to rename a file or folder.
|
||||
|
||||
.. |edit| image:: images/edit.png
|
||||
|
||||
Use the ``Upload`` icon |upload| to upload a file.
|
||||
|
||||
.. |upload| image:: images/upload.png
|
||||
.. |download| image:: images/sm_download.png
|
||||
|
||||
Use the ``New Folder`` icon |folder| to add a new folder.
|
||||
|
||||
.. |folder| image:: images/folder.png
|
||||
|
||||
Use the ``Grid View`` icon |gridview| to display all the files and folders in a grid view.
|
||||
|
||||
.. |gridview| image:: images/gridview.png
|
||||
|
||||
Use the ``Table View`` icon |tableview| to display all the files and folders in a list view.
|
||||
|
||||
.. |tableview| image:: images/tableview.png
|
||||
|
||||
Click on the check box next to *Show hidden files and folders* at the bottom of the window to view hidden files and folders.
|
||||
.. |folder| image:: images/sm_new_folder.png
|
||||
|
||||
Use the *Format* drop down list to select the format of the files to be displayed; choose from *sql*, *csv*, or *All Files*.
|
||||
|
||||
Other Options
|
||||
*********************
|
||||
|
||||
.. image:: images/sm_options.png
|
||||
:alt: Other options
|
||||
:align: center
|
||||
|
||||
.. table::
|
||||
:class: longtable
|
||||
:widths: 1 5
|
||||
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+
|
||||
| Menu | Behavior |
|
||||
+======================+===================================================================================================+
|
||||
| *Rename* | Click the *Rename* option to rename a file/folder. |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+
|
||||
| *Delete* | Click the *Delete* option to rename a file/folder. |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+
|
||||
| *Upload* | Click the *Upload* option to upload multiple files to the current folder. |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+
|
||||
| *List View* | Click the *List View* option to to display all the files and folders in a list view. |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+
|
||||
| *Grid View* | Click the *Grid View* option to to display all the files and folders in a grid view. |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+
|
||||
| *Show Hidden Files* | Click the *Show Hidden Files* option to view hidden files and folders. |
|
||||
+----------------------+---------------------------------------------------------------------------------------------------+
|
||||
|
||||
|
||||
You can also download backup files through *Storage Manager* at the successful completion of the backups taken through :ref:`Backup Dialog <backup_dialog>`, :ref:`Backup Global Dialog <backup_globals_dialog>`, or :ref:`Backup Server Dialog <backup_server_dialog>`.
|
||||
|
||||
At the successful completion of a backup, click on the icon to open the current backup file in *Storage Manager* on the *process watcher* window.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"license": "PostgreSQL",
|
||||
"chromium-args": "--disable-popup-blocking --disable-gpu",
|
||||
"user-agent": "Nwjs:%nwver-%osinfo-%chromium_ver",
|
||||
"nodejs": true,
|
||||
"window": {
|
||||
"width": 750,
|
||||
"height": 600,
|
||||
|
||||
@@ -296,7 +296,7 @@ const setZoomLevelForAllWindows = () => {
|
||||
for (let arr_val of winArray) {
|
||||
arr_val.zoomLevel = pgAdminWindowObject.zoomLevel;
|
||||
}
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// This function used to zoom in the pgAdmin window.
|
||||
|
||||
@@ -18,6 +18,7 @@ let startPageUrl = null;
|
||||
let serverCheckUrl = null;
|
||||
|
||||
let serverPort = 5050;
|
||||
let appStartTime = (new Date()).getTime();
|
||||
|
||||
let docsURLSubStrings = ['www.enterprisedb.com', 'www.postgresql.org', 'www.pgadmin.org', 'help/help'];
|
||||
|
||||
@@ -84,12 +85,15 @@ function startDesktopMode() {
|
||||
misc.writeServerLog('--------------------------------------------------------\n');
|
||||
|
||||
// Spawn the process to start pgAdmin4 server.
|
||||
let spawnStartTime = (new Date).getTime();
|
||||
pgadminServerProcess = spawn(path.resolve(pythonPath), ['-s', path.resolve(pgadminFile)]);
|
||||
pgadminServerProcess.on('error', function(err) {
|
||||
// Log the error into the log file if process failed to launch
|
||||
misc.writeServerLog('Failed to launch pgAdmin4. Error:');
|
||||
misc.writeServerLog(err);
|
||||
});
|
||||
let spawnEndTime = (new Date).getTime();
|
||||
misc.writeServerLog('Total spawn time to start the pgAdmin4 server: ' + (spawnEndTime - spawnStartTime)/1000 + ' Sec');
|
||||
|
||||
pgadminServerProcess.stdout.setEncoding('utf8');
|
||||
pgadminServerProcess.stdout.on('data', (chunk) => {
|
||||
@@ -149,6 +153,7 @@ function startDesktopMode() {
|
||||
let pingInProgress = false;
|
||||
|
||||
// ping pgAdmin server every 1 second.
|
||||
let pingStartTime = (new Date).getTime();
|
||||
let intervalID = setInterval(function() {
|
||||
// If ping request is already send and response is not
|
||||
// received no need to send another request.
|
||||
@@ -162,6 +167,12 @@ function startDesktopMode() {
|
||||
misc.setProcessObject(pgadminServerProcess);
|
||||
|
||||
clearInterval(intervalID);
|
||||
let appEndTime = (new Date).getTime();
|
||||
misc.writeServerLog('------------------------------------------');
|
||||
misc.writeServerLog('Total time taken to ping pgAdmin4 server: ' + (appEndTime - pingStartTime)/1000 + ' Sec');
|
||||
misc.writeServerLog('------------------------------------------');
|
||||
misc.writeServerLog('Total launch time of pgAdmin4: ' + (appEndTime - appStartTime)/1000 + ' Sec');
|
||||
misc.writeServerLog('------------------------------------------');
|
||||
launchPgAdminWindow();
|
||||
}).catch(() => {
|
||||
pingInProgress = false;
|
||||
@@ -195,7 +206,7 @@ function startDesktopMode() {
|
||||
});
|
||||
|
||||
pingInProgress = true;
|
||||
}, 1000);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
// This function is used to hide the splash screen and create/launch
|
||||
@@ -232,32 +243,32 @@ function launchPgAdminWindow() {
|
||||
// set up handler for new-win-policy event.
|
||||
// Set the width and height for the new window.
|
||||
pgadminWindow.on('new-win-policy', function(frame, url, policy) {
|
||||
if(!frame) {
|
||||
let openDocsInBrowser = misc.ConfigureStore.get('openDocsInBrowser', true);
|
||||
let isDocURL = false;
|
||||
docsURLSubStrings.forEach(function(key) {
|
||||
if(url.indexOf(key) >= 0) {
|
||||
isDocURL = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (openDocsInBrowser && isDocURL) {
|
||||
// Do not open the window
|
||||
policy.ignore();
|
||||
// Open URL in the external browser.
|
||||
nw.Shell.openExternal(url);
|
||||
} else {
|
||||
policy.setNewWindowManifest({
|
||||
'icon': '../../assets/pgAdmin4.png',
|
||||
'frame': true,
|
||||
'position': 'center',
|
||||
'min_width': 640,
|
||||
'min_height': 480,
|
||||
'width': pgadminWindow.width,
|
||||
'height': pgadminWindow.height,
|
||||
});
|
||||
if(!frame) {
|
||||
let openDocsInBrowser = misc.ConfigureStore.get('openDocsInBrowser', true);
|
||||
let isDocURL = false;
|
||||
docsURLSubStrings.forEach(function(key) {
|
||||
if(url.indexOf(key) >= 0) {
|
||||
isDocURL = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (openDocsInBrowser && isDocURL) {
|
||||
// Do not open the window
|
||||
policy.ignore();
|
||||
// Open URL in the external browser.
|
||||
nw.Shell.openExternal(url);
|
||||
} else {
|
||||
policy.setNewWindowManifest({
|
||||
'icon': '../../assets/pgAdmin4.png',
|
||||
'frame': true,
|
||||
'position': 'center',
|
||||
'min_width': 640,
|
||||
'min_height': 480,
|
||||
'width': pgadminWindow.width,
|
||||
'height': pgadminWindow.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pgadminWindow.on('loaded', function() {
|
||||
@@ -289,6 +300,24 @@ function launchPgAdminWindow() {
|
||||
let gui = require('nw.gui');
|
||||
let splashWindow = gui.Window.get();
|
||||
|
||||
// Enable dragging on the splash screen.
|
||||
let isDragging = false;
|
||||
let dragOrigin = {x:0, y:0};
|
||||
document.mouseleave = ()=> isDragging = false;
|
||||
document.onmouseup = ()=> isDragging = false;
|
||||
|
||||
document.onmousedown = (e)=> {
|
||||
isDragging = true;
|
||||
dragOrigin.x = e.x;
|
||||
dragOrigin.y = e.y;
|
||||
};
|
||||
|
||||
document.onmousemove = (e)=> {
|
||||
if (isDragging) {
|
||||
splashWindow.moveTo(e.screenX - dragOrigin.x, e.screenY - dragOrigin.y);
|
||||
}
|
||||
};
|
||||
|
||||
// Always clear the cache before starting the application.
|
||||
nw.App.clearCache();
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ APP_ICON = 'pg-icon'
|
||||
|
||||
# Application version number components
|
||||
APP_RELEASE = 6
|
||||
APP_REVISION = 11
|
||||
APP_REVISION = 12
|
||||
|
||||
# Application version suffix, e.g. 'beta1', 'dev'. Usually an empty string
|
||||
# for GA releases.
|
||||
@@ -52,7 +52,7 @@ APP_SUFFIX = ''
|
||||
# Numeric application version for upgrade checks. Should be in the format:
|
||||
# [X]XYYZZ, where X is the release version, Y is the revision, with a leading
|
||||
# zero if needed, and Z represents the suffix, with a leading zero if needed
|
||||
APP_VERSION_INT = 61100
|
||||
APP_VERSION_INT = 61200
|
||||
|
||||
# DO NOT CHANGE!
|
||||
# The application version string, constructed from the components
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"webpack": "^5.21.2",
|
||||
"webpack-bundle-analyzer": "^4.4.0",
|
||||
"webpack-cli": "^4.5.0",
|
||||
"yarn-audit-html": "^3.0.1"
|
||||
"yarn-audit-html": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
@@ -111,12 +111,12 @@
|
||||
"closest": "^0.0.1",
|
||||
"codemirror": "^5.59.2",
|
||||
"context-menu": "^2.0.0",
|
||||
"convert-units": "^2.3.4",
|
||||
"css-loader": "^5.0.1",
|
||||
"cssnano": "^5.0.2",
|
||||
"dagre": "^0.8.4",
|
||||
"date-fns": "^2.24.0",
|
||||
"diff-arrays-of-objects": "^1.1.8",
|
||||
"dropzone": "^5.9.3",
|
||||
"html2canvas": "^1.0.0-rc.7",
|
||||
"immutability-helper": "^3.0.0",
|
||||
"imports-loader": "^2.0.0",
|
||||
@@ -124,7 +124,7 @@
|
||||
"ip-address": "^7.1.0",
|
||||
"jquery": "^3.6.0",
|
||||
"jquery-contextmenu": "^2.9.2",
|
||||
"jquery-ui": "^1.13.0",
|
||||
"jquery-ui": "^1.13.2",
|
||||
"json-bignumber": "^1.0.1",
|
||||
"jsoneditor": "^9.5.4",
|
||||
"jsoneditor-react": "^3.1.1",
|
||||
@@ -132,7 +132,7 @@
|
||||
"leaflet": "^1.5.1",
|
||||
"lodash": "4.*",
|
||||
"ml-matrix": "^6.5.0",
|
||||
"moment": "^2.29.3",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.34",
|
||||
"mousetrap": "^1.6.3",
|
||||
"notificar": "^1.0.1",
|
||||
@@ -150,6 +150,7 @@
|
||||
"react-data-grid": "git+https://github.com/adityatoshniwal/react-data-grid.git/#8d9bc16ddd7c419acfbbd1c1cc2b70eb9f5b453c",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-draggable": "^4.4.4",
|
||||
"react-dropzone": "^14.2.1",
|
||||
"react-leaflet": "^3.2.2",
|
||||
"react-rnd": "^10.3.5",
|
||||
"react-router-dom": "^6.2.2",
|
||||
@@ -165,7 +166,6 @@
|
||||
"socket.io-client": "^4.0.0",
|
||||
"split.js": "^1.5.10",
|
||||
"styled-components": "^5.2.1",
|
||||
"tablesorter": "^2.31.2",
|
||||
"tempusdominus-bootstrap-4": "^5.1.2",
|
||||
"tempusdominus-core": "^5.19.3",
|
||||
"underscore": "^1.13.1",
|
||||
|
||||
@@ -14,7 +14,6 @@ to start a web server."""
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
if sys.version_info < (3, 4):
|
||||
raise RuntimeError('This application must be run under Python 3.4 '
|
||||
'or later.')
|
||||
@@ -37,8 +36,8 @@ else:
|
||||
builtins.SERVER_MODE = None
|
||||
|
||||
import config
|
||||
import setup
|
||||
from pgadmin import create_app, socketio
|
||||
from pgadmin.utils import u_encode, fs_encoding, file_quote
|
||||
from pgadmin.utils.constants import INTERNAL
|
||||
# Get the config database schema version. We store this in pgadmin.model
|
||||
# as it turns out that putting it in the config files isn't a great idea
|
||||
@@ -83,13 +82,9 @@ class ReverseProxied(object):
|
||||
config.SETTINGS_SCHEMA_VERSION = SCHEMA_VERSION
|
||||
|
||||
# Check if the database exists. If it does not, create it.
|
||||
setup_db_required = False
|
||||
if not os.path.isfile(config.SQLITE_PATH):
|
||||
setup_py = os.path.join(
|
||||
os.path.dirname(os.path.realpath(u_encode(__file__, fs_encoding))),
|
||||
'setup.py'
|
||||
)
|
||||
exec(open(file_quote(setup_py), 'r').read())
|
||||
|
||||
setup_db_required = True
|
||||
|
||||
##########################################################################
|
||||
# Create the app and configure it. It is created outside main so that
|
||||
@@ -99,6 +94,9 @@ app = create_app()
|
||||
app.debug = False
|
||||
app.config['sessions'] = dict()
|
||||
|
||||
if setup_db_required:
|
||||
setup.setup_db(app)
|
||||
|
||||
if config.SERVER_MODE:
|
||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
##########################################################################
|
||||
|
||||
""" Azure PostgreSQL provider """
|
||||
|
||||
from azure.mgmt.rdbms.postgresql_flexibleservers import \
|
||||
PostgreSQLManagementClient
|
||||
from azure.mgmt.rdbms.postgresql_flexibleservers.models import Sku, SkuTier, \
|
||||
CreateMode, Storage, Server, FirewallRule, HighAvailability
|
||||
from azure.identity import AzureCliCredential, InteractiveBrowserCredential, \
|
||||
from azure.identity import AzureCliCredential, DeviceCodeCredential, \
|
||||
AuthenticationRecord
|
||||
from azure.mgmt.resource import ResourceManagementClient
|
||||
from azure.core.exceptions import ResourceNotFoundError
|
||||
@@ -37,12 +36,13 @@ class AzureProvider(AbsProvider):
|
||||
self._client_secret = None
|
||||
self._subscription_id = None
|
||||
self._default_region = None
|
||||
self._use_interactive_browser_credential = False
|
||||
self._interactive_browser_credential = False
|
||||
self._available_capabilities = None
|
||||
self._credentials = None
|
||||
self._authentication_record_json = None
|
||||
self._cli_credentials = None
|
||||
self.azure_cred_cache_name = None
|
||||
self._azure_cred_cache_name = None
|
||||
self._azure_cred_cache_location = None
|
||||
|
||||
# Get the credentials
|
||||
if 'AUTHENTICATION_RECORD_JSON' in os.environ:
|
||||
@@ -56,7 +56,7 @@ class AzureProvider(AbsProvider):
|
||||
self._tenant_id = os.environ['AZURE_TENANT_ID']
|
||||
|
||||
if 'AUTH_TYPE' in os.environ:
|
||||
self._use_interactive_browser_credential = False \
|
||||
self._interactive_browser_credential = False \
|
||||
if os.environ['AUTH_TYPE'] == 'azure_cli_credential' else True
|
||||
|
||||
if 'AZURE_DATABASE_PASSWORD' in os.environ:
|
||||
@@ -150,49 +150,48 @@ class AzureProvider(AbsProvider):
|
||||
##########################################################################
|
||||
def _get_azure_credentials(self):
|
||||
try:
|
||||
if self._use_interactive_browser_credential:
|
||||
if self._authentication_record_json is None:
|
||||
_credentials = self._azure_interactive_browser_credential()
|
||||
_auth_record_ = _credentials.authenticate()
|
||||
self._authentication_record_json = \
|
||||
_auth_record_.serialize()
|
||||
else:
|
||||
deserialized_auth_record = AuthenticationRecord.\
|
||||
deserialize(self._authentication_record_json)
|
||||
_credentials = \
|
||||
self._azure_interactive_browser_credential(
|
||||
deserialized_auth_record)
|
||||
if self._interactive_browser_credential:
|
||||
_credentials = self._azure_interactive_auth()
|
||||
else:
|
||||
if self._cli_credentials is None:
|
||||
self._cli_credentials = AzureCliCredential()
|
||||
_credentials = self._cli_credentials
|
||||
_credentials = self._azure_cli_auth()
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
return True, _credentials
|
||||
|
||||
def _azure_interactive_browser_credential(
|
||||
self, deserialized_auth_record=None):
|
||||
if deserialized_auth_record:
|
||||
_credential = InteractiveBrowserCredential(
|
||||
def _azure_cli_auth(self):
|
||||
if self._cli_credentials is None:
|
||||
self._cli_credentials = AzureCliCredential()
|
||||
return self._cli_credentials
|
||||
|
||||
def _azure_interactive_auth(self):
|
||||
if self._authentication_record_json is None:
|
||||
_interactive_credential = DeviceCodeCredential(
|
||||
tenant_id=self._tenant_id,
|
||||
timeout=180,
|
||||
_cache=load_persistent_cache(
|
||||
TokenCachePersistenceOptions(
|
||||
name=self._azure_cred_cache_name,
|
||||
allow_unencrypted_storage=True,
|
||||
cache_location=self._azure_cred_cache_location)),
|
||||
authentication_record=deserialized_auth_record)
|
||||
else:
|
||||
_credential = InteractiveBrowserCredential(
|
||||
tenant_id=self._tenant_id,
|
||||
timeout=180,
|
||||
_cache=load_persistent_cache(
|
||||
TokenCachePersistenceOptions(
|
||||
name=self._azure_cred_cache_name,
|
||||
allow_unencrypted_storage=True,
|
||||
cache_location=self._azure_cred_cache_location))
|
||||
prompt_callback=None,
|
||||
_cache=load_persistent_cache(TokenCachePersistenceOptions(
|
||||
name=self._azure_cred_cache_name,
|
||||
allow_unencrypted_storage=True,
|
||||
cache_location=self._azure_cred_cache_location)
|
||||
)
|
||||
)
|
||||
return _credential
|
||||
_auth_record = _interactive_credential.authenticate()
|
||||
self._authentication_record_json = _auth_record.serialize()
|
||||
else:
|
||||
deserialized_auth_record = AuthenticationRecord.deserialize(
|
||||
self._authentication_record_json)
|
||||
_interactive_credential = DeviceCodeCredential(
|
||||
tenant_id=self._tenant_id,
|
||||
timeout=180,
|
||||
prompt_callback=None,
|
||||
_cache=load_persistent_cache(TokenCachePersistenceOptions(
|
||||
name=self._azure_cred_cache_name,
|
||||
allow_unencrypted_storage=True,
|
||||
cache_location=self._azure_cred_cache_location)
|
||||
),
|
||||
authentication_record=deserialized_auth_record
|
||||
)
|
||||
return _interactive_credential
|
||||
|
||||
def _get_azure_client(self, type):
|
||||
""" Create/cache/return an Azure client object """
|
||||
|
||||
@@ -713,7 +713,8 @@ def create_app(app_name=None):
|
||||
##########################################################################
|
||||
# Load plugin modules
|
||||
##########################################################################
|
||||
for module in app.find_submodules('pgadmin'):
|
||||
from .submodules import get_submodules
|
||||
for module in get_submodules():
|
||||
app.logger.info('Registering blueprint module: %s' % module)
|
||||
if app.blueprints.get(module.name) is None:
|
||||
app.register_blueprint(module)
|
||||
|
||||
@@ -41,13 +41,6 @@ class AboutModule(PgAdminModule):
|
||||
]
|
||||
}
|
||||
|
||||
def get_own_javascripts(self):
|
||||
return [{
|
||||
'name': 'pgadmin.about',
|
||||
'path': url_for('about.index') + 'about',
|
||||
'when': None
|
||||
}]
|
||||
|
||||
def get_exposed_url_endpoints(self):
|
||||
return ['about.index']
|
||||
|
||||
|
||||
@@ -22,8 +22,31 @@ class: MultiFactorAuthRegistry
|
||||
|
||||
An registry factory for the multi-factor authentication methods.
|
||||
"""
|
||||
|
||||
|
||||
@classmethod
|
||||
def load_modules(cls, app=None):
|
||||
submodules = []
|
||||
from . import authenticator as module
|
||||
submodules.append(module)
|
||||
|
||||
from . import email as module
|
||||
submodules.append(module)
|
||||
|
||||
from . import utils as module
|
||||
submodules.append(module)
|
||||
|
||||
from . import views as module
|
||||
submodules.append(module)
|
||||
|
||||
for module in submodules:
|
||||
if "init_app" in module.__dict__.keys():
|
||||
module.__dict__["init_app"](app)
|
||||
|
||||
|
||||
MultiFactorAuthRegistry = create_registry_metaclass(
|
||||
'MultiFactorAuthRegistry', __package__, decorate_as_module=True
|
||||
'MultiFactorAuthRegistry', __package__, load_modules=load_modules,
|
||||
decorate_as_module=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,33 @@
|
||||
from pgadmin.utils.dynamic_registry import create_registry_metaclass
|
||||
|
||||
|
||||
@classmethod
|
||||
def load_modules(cls, app=None):
|
||||
submodules = []
|
||||
from . import internal as module
|
||||
submodules.append(module)
|
||||
|
||||
from . import kerberos as module
|
||||
submodules.append(module)
|
||||
|
||||
from . import ldap as module
|
||||
submodules.append(module)
|
||||
|
||||
from . import mfa as module
|
||||
submodules.append(module)
|
||||
|
||||
from . import oauth2 as module
|
||||
submodules.append(module)
|
||||
|
||||
from . import webserver as module
|
||||
submodules.append(module)
|
||||
|
||||
for module in submodules:
|
||||
if "init_app" in module.__dict__.keys():
|
||||
module.__dict__["init_app"](app)
|
||||
|
||||
|
||||
AuthSourceRegistry = create_registry_metaclass(
|
||||
"AuthSourceRegistry", __package__, decorate_as_module=True
|
||||
"AuthSourceRegistry", __package__, load_modules=load_modules,
|
||||
decorate_as_module=True
|
||||
)
|
||||
@@ -102,103 +102,6 @@ class BrowserModule(PgAdminModule):
|
||||
stylesheets.append(url_for(endpoint, filename=filename))
|
||||
return stylesheets
|
||||
|
||||
def get_own_javascripts(self):
|
||||
scripts = list()
|
||||
scripts.append({
|
||||
'name': 'alertify',
|
||||
'path': url_for(
|
||||
'static',
|
||||
filename='vendor/alertifyjs/alertify' if current_app.debug
|
||||
else 'vendor/alertifyjs/alertify.min'
|
||||
),
|
||||
'exports': 'alertify',
|
||||
'preloaded': True
|
||||
})
|
||||
scripts.append({
|
||||
'name': 'jqueryui.position',
|
||||
'path': url_for(
|
||||
'static',
|
||||
filename='vendor/jQuery-contextMenu/jquery.ui.position' if
|
||||
current_app.debug else
|
||||
'vendor/jQuery-contextMenu/jquery.ui.position.min'
|
||||
),
|
||||
'deps': ['jquery'],
|
||||
'exports': 'jQuery.ui.position',
|
||||
'preloaded': True
|
||||
})
|
||||
scripts.append({
|
||||
'name': 'jquery.contextmenu',
|
||||
'path': url_for(
|
||||
'static',
|
||||
filename='vendor/jQuery-contextMenu/jquery.contextMenu' if
|
||||
current_app.debug else
|
||||
'vendor/jQuery-contextMenu/jquery.contextMenu.min'
|
||||
),
|
||||
'deps': ['jquery', 'jqueryui.position'],
|
||||
'exports': 'jQuery.contextMenu',
|
||||
'preloaded': True
|
||||
})
|
||||
scripts.append({
|
||||
'name': 'wcdocker',
|
||||
'path': url_for(
|
||||
'static',
|
||||
filename='vendor/wcDocker/wcDocker' if current_app.debug
|
||||
else 'vendor/wcDocker/wcDocker.min'
|
||||
),
|
||||
'deps': ['jquery.contextmenu'],
|
||||
'exports': '',
|
||||
'preloaded': True
|
||||
})
|
||||
|
||||
scripts.append({
|
||||
'name': 'pgadmin.browser.datamodel',
|
||||
'path': url_for(BROWSER_STATIC, filename='js/datamodel'),
|
||||
'preloaded': True
|
||||
})
|
||||
|
||||
for name, script in [
|
||||
[PGADMIN_BROWSER, 'js/browser'],
|
||||
['pgadmin.browser.endpoints', 'js/endpoints'],
|
||||
['pgadmin.browser.error', 'js/error'],
|
||||
['pgadmin.browser.constants', 'js/constants']
|
||||
]:
|
||||
scripts.append({
|
||||
'name': name,
|
||||
'path': url_for(BROWSER_INDEX) + script,
|
||||
'preloaded': True
|
||||
})
|
||||
|
||||
for name, script in [
|
||||
['pgadmin.browser.node', 'js/node'],
|
||||
['pgadmin.browser.messages', 'js/messages'],
|
||||
['pgadmin.browser.collection', 'js/collection']
|
||||
]:
|
||||
scripts.append({
|
||||
'name': name,
|
||||
'path': url_for(BROWSER_INDEX) + script,
|
||||
'preloaded': True,
|
||||
'deps': ['pgadmin.browser.datamodel']
|
||||
})
|
||||
|
||||
for name, end in [
|
||||
['pgadmin.browser.menu', 'js/menu'],
|
||||
['pgadmin.browser.panel', 'js/panel'],
|
||||
['pgadmin.browser.frame', 'js/frame']
|
||||
]:
|
||||
scripts.append({
|
||||
'name': name, 'path': url_for(BROWSER_STATIC, filename=end),
|
||||
'preloaded': True})
|
||||
|
||||
scripts.append({
|
||||
'name': 'pgadmin.browser.node.ui',
|
||||
'path': url_for(BROWSER_STATIC, filename='js/node.ui'),
|
||||
'when': 'server_group'
|
||||
})
|
||||
|
||||
for module in self.submodules:
|
||||
scripts.extend(module.get_own_javascripts())
|
||||
return scripts
|
||||
|
||||
def get_own_menuitems(self):
|
||||
menus = {
|
||||
'file_items': [
|
||||
@@ -311,6 +214,15 @@ class BrowserModule(PgAdminModule):
|
||||
'browser.lock_layout',
|
||||
'browser.signal_runtime']
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
from .server_groups import blueprint as module
|
||||
self.submodules.append(module)
|
||||
super(BrowserModule, self).register(app, options)
|
||||
|
||||
|
||||
blueprint = BrowserModule(MODULE_NAME, __name__)
|
||||
|
||||
@@ -377,51 +289,6 @@ class BrowserPluginModule(PgAdminModule):
|
||||
"""
|
||||
return False
|
||||
|
||||
def get_own_javascripts(self):
|
||||
"""
|
||||
Returns the list of javascripts information used by the module.
|
||||
|
||||
Each javascripts information must contain name, path of the script.
|
||||
|
||||
The name must be unique for each module, hence - in order to refer them
|
||||
properly, we do use 'pgadmin.node.<type>' as norm.
|
||||
|
||||
That can also refer to when to load the script.
|
||||
|
||||
i.e.
|
||||
We may not need to load the javascript of table node, when we're
|
||||
not yet connected to a server, and no database is loaded. Hence - it
|
||||
make sense to load them when a database is loaded.
|
||||
|
||||
We may also add 'deps', which also refers to the list of javascripts,
|
||||
it may depends on.
|
||||
"""
|
||||
scripts = []
|
||||
|
||||
if self.module_use_template_javascript:
|
||||
scripts.extend([{
|
||||
'name': PGADMIN_NODE % self.node_type,
|
||||
'path': url_for(BROWSER_INDEX
|
||||
) + '%s/module' % self.node_type,
|
||||
'when': self.script_load,
|
||||
'is_template': True
|
||||
}])
|
||||
else:
|
||||
scripts.extend([{
|
||||
'name': PGADMIN_NODE % self.node_type,
|
||||
'path': url_for(
|
||||
'%s.static' % self.name,
|
||||
filename=('js/%s' % self.node_type)
|
||||
),
|
||||
'when': self.script_load,
|
||||
'is_template': False
|
||||
}])
|
||||
|
||||
for module in self.submodules:
|
||||
scripts.extend(module.get_own_javascripts())
|
||||
|
||||
return scripts
|
||||
|
||||
def generate_browser_node(
|
||||
self, node_id, parent_id, label, icon, inode, node_type, **kwargs
|
||||
):
|
||||
@@ -508,14 +375,6 @@ class BrowserPluginModule(PgAdminModule):
|
||||
"""
|
||||
return self.browser_url_prefix + self.node_type
|
||||
|
||||
@property
|
||||
def javascripts(self):
|
||||
"""
|
||||
Override the javascript of PgAdminModule, so that - we don't return
|
||||
javascripts from the get_own_javascripts itself.
|
||||
"""
|
||||
return []
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
"""
|
||||
@@ -884,30 +743,11 @@ def get_nodes():
|
||||
|
||||
|
||||
def form_master_password_response(existing=True, present=False, errmsg=None):
|
||||
content_new = (
|
||||
gettext("Set Master Password"),
|
||||
"<br/>".join([
|
||||
gettext("Please set a master password for pgAdmin."),
|
||||
gettext("This will be used to secure and later unlock saved "
|
||||
"passwords and other credentials.")])
|
||||
)
|
||||
content_existing = (
|
||||
gettext("Unlock Saved Passwords"),
|
||||
"<br/>".join([
|
||||
gettext("Please enter your master password."),
|
||||
gettext("This is required to unlock saved passwords and "
|
||||
"reconnect to the database server(s).")])
|
||||
)
|
||||
|
||||
return make_json_response(data={
|
||||
'present': present,
|
||||
'title': content_existing[0] if existing else content_new[0],
|
||||
'content': render_template(
|
||||
'browser/master_password.html',
|
||||
content_text=content_existing[1] if existing else content_new[1],
|
||||
errmsg=errmsg
|
||||
),
|
||||
'reset': existing
|
||||
'reset': existing,
|
||||
'errmsg': errmsg,
|
||||
'is_error': True if errmsg else False
|
||||
})
|
||||
|
||||
|
||||
@@ -955,11 +795,15 @@ def set_master_password():
|
||||
|
||||
data = None
|
||||
|
||||
if hasattr(request.data, 'decode'):
|
||||
data = request.data.decode('utf-8')
|
||||
if request.form:
|
||||
data = request.form
|
||||
elif request.data:
|
||||
data = request.data
|
||||
if hasattr(request.data, 'decode'):
|
||||
data = request.data.decode('utf-8')
|
||||
|
||||
if data != '':
|
||||
data = json.loads(data)
|
||||
if data != '':
|
||||
data = json.loads(data)
|
||||
|
||||
# Master password is not applicable for server mode
|
||||
# Enable master password if oauth is used
|
||||
@@ -969,7 +813,7 @@ def set_master_password():
|
||||
and config.MASTER_PASSWORD_REQUIRED:
|
||||
# if master pass is set previously
|
||||
if current_user.masterpass_check is not None and \
|
||||
data.get('button_click') and \
|
||||
data.get('submit_password', False) and \
|
||||
not validate_master_password(data.get('password')):
|
||||
return form_master_password_response(
|
||||
existing=True,
|
||||
@@ -1005,7 +849,7 @@ def set_master_password():
|
||||
)
|
||||
elif not get_crypt_key()[1]:
|
||||
error_message = None
|
||||
if data.get('button_click') and data.get('password') == '':
|
||||
if data.get('submit_password') and data.get('password') == '':
|
||||
# If user attempted to enter a blank password, then throw error
|
||||
error_message = gettext("Master password cannot be empty")
|
||||
return form_master_password_response(
|
||||
|
||||
@@ -59,33 +59,6 @@ class CollectionNodeModule(PgAdminModule, PGChildModule):
|
||||
"""
|
||||
return True
|
||||
|
||||
def get_own_javascripts(self):
|
||||
scripts = []
|
||||
|
||||
if self.module_use_template_javascript:
|
||||
scripts.extend([{
|
||||
'name': PGADMIN_NODE % self.node_type,
|
||||
'path': url_for('browser.index'
|
||||
) + '%s/module' % self.node_type,
|
||||
'when': self.script_load,
|
||||
'is_template': True
|
||||
}])
|
||||
else:
|
||||
scripts.extend([{
|
||||
'name': PGADMIN_NODE % self.node_type,
|
||||
'path': url_for(
|
||||
'%s.static' % self.name,
|
||||
filename=('js/%s' % self.node_type)
|
||||
),
|
||||
'when': self.script_load,
|
||||
'is_template': False
|
||||
}])
|
||||
|
||||
for module in self.submodules:
|
||||
scripts.extend(module.get_own_javascripts())
|
||||
|
||||
return scripts
|
||||
|
||||
def generate_browser_node(
|
||||
self, node_id, parent_id, label, icon, **kwargs
|
||||
):
|
||||
|
||||
@@ -123,6 +123,15 @@ class ServerGroupModule(BrowserPluginModule):
|
||||
"""
|
||||
pass
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
from .servers import blueprint as module
|
||||
self.submodules.append(module)
|
||||
super(BrowserPluginModule, self).register(app, options)
|
||||
|
||||
|
||||
class ServerGroupMenuItem(MenuItem):
|
||||
def __init__(self, **kwargs):
|
||||
|
||||
@@ -283,30 +283,6 @@ class ServerModule(sg.ServerGroupPluginModule):
|
||||
|
||||
return snippets
|
||||
|
||||
def get_own_javascripts(self):
|
||||
scripts = []
|
||||
|
||||
scripts.extend([{
|
||||
'name': 'pgadmin.browser.server.privilege',
|
||||
'path': url_for('%s.static' % self.name, filename='js/privilege'),
|
||||
'when': self.node_type,
|
||||
'is_template': False,
|
||||
'deps': ['pgadmin.browser.node.ui']
|
||||
}, {
|
||||
'name': 'pgadmin.browser.server.variable',
|
||||
'path': url_for('%s.static' % self.name, filename='js/variable'),
|
||||
'when': self.node_type,
|
||||
'is_template': False
|
||||
}, {
|
||||
'name': 'pgadmin.server.supported_servers',
|
||||
'path': url_for('browser.index') + 'server/supported_servers',
|
||||
'is_template': True,
|
||||
'when': self.node_type
|
||||
}])
|
||||
scripts.extend(sg.ServerGroupPluginModule.get_own_javascripts(self))
|
||||
|
||||
return scripts
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
@@ -318,6 +294,23 @@ class ServerModule(sg.ServerGroupPluginModule):
|
||||
app.jinja_env.filters['qtTypeIdent'] = driver.qtTypeIdent
|
||||
app.jinja_env.filters['hasAny'] = has_any
|
||||
|
||||
from .ppas import PPAS
|
||||
|
||||
from .databases import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .pgagent import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .resource_groups import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .roles import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .tablespaces import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
super(ServerModule, self).register(app, options)
|
||||
|
||||
# We do not have any preferences for server node.
|
||||
@@ -1622,9 +1615,10 @@ class ServerNode(PGChildNodeView):
|
||||
sid: Server id
|
||||
"""
|
||||
try:
|
||||
if request.form and request.form['data']:
|
||||
data = json.loads(request.form['data'], encoding='utf-8')
|
||||
else:
|
||||
data = None
|
||||
if request.form:
|
||||
data = request.form
|
||||
elif request.data:
|
||||
data = json.loads(request.data, encoding='utf-8')
|
||||
|
||||
crypt_key = get_crypt_key()[1]
|
||||
|
||||
@@ -95,6 +95,40 @@ class DatabaseModule(CollectionNodeModule):
|
||||
"""
|
||||
return False
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
from .casts import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .event_triggers import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .extensions import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .foreign_data_wrappers import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .languages import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .publications import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .schemas import schema_blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .schemas import catalog_blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .subscriptions import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
super(DatabaseModule, self).register(app, options)
|
||||
|
||||
|
||||
blueprint = DatabaseModule(__name__)
|
||||
|
||||
|
||||
@@ -95,6 +95,15 @@ class ForeignDataWrapperModule(CollectionNodeModule):
|
||||
"""
|
||||
return False
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
from .foreign_servers import blueprint as module
|
||||
self.submodules.append(module)
|
||||
super(ForeignDataWrapperModule, self).register(app, options)
|
||||
|
||||
|
||||
blueprint = ForeignDataWrapperModule(__name__)
|
||||
|
||||
|
||||
@@ -96,6 +96,15 @@ class ForeignServerModule(CollectionNodeModule):
|
||||
"""
|
||||
return False
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
from .user_mappings import blueprint as module
|
||||
self.submodules.append(module)
|
||||
super(ForeignServerModule, self).register(app, options)
|
||||
|
||||
|
||||
blueprint = ForeignServerModule(__name__)
|
||||
|
||||
|
||||
@@ -103,6 +103,73 @@ class SchemaModule(CollectionNodeModule):
|
||||
"""
|
||||
return False
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
from .aggregates import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .catalog_objects import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .collations import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .domains import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .foreign_tables import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .fts_configurations import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .fts_dictionaries import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .fts_parsers import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .fts_templates import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .functions import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .functions import trigger_function_blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .functions import procedure_blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .operators import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .packages import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .sequences import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .synonyms import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .tables import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .types import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .views import view_blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .views import mview_blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
super(SchemaModule, self).register(app, options)
|
||||
|
||||
|
||||
class CatalogModule(SchemaModule):
|
||||
"""
|
||||
@@ -114,6 +181,13 @@ class CatalogModule(SchemaModule):
|
||||
_NODE_TYPE = 'catalog'
|
||||
_COLLECTION_LABEL = gettext("Catalogs")
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
super(SchemaModule, self).register(app, options)
|
||||
|
||||
|
||||
schema_blueprint = SchemaModule(__name__)
|
||||
catalog_blueprint = CatalogModule(__name__)
|
||||
|
||||
@@ -80,6 +80,16 @@ class CatalogObjectModule(SchemaChildModule):
|
||||
"""
|
||||
return database.DatabaseModule.node_type
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
super(CatalogObjectModule, self).register(app, options)
|
||||
|
||||
from .columns import blueprint as module
|
||||
app.register_blueprint(module)
|
||||
|
||||
|
||||
blueprint = CatalogObjectModule(__name__)
|
||||
|
||||
|
||||
@@ -71,6 +71,15 @@ class DomainModule(SchemaChildModule):
|
||||
"""
|
||||
return databases.DatabaseModule.node_type
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
from .domain_constraints import blueprint as module
|
||||
self.submodules.append(module)
|
||||
super(DomainModule, self).register(app, options)
|
||||
|
||||
|
||||
blueprint = DomainModule(__name__)
|
||||
|
||||
|
||||
@@ -76,6 +76,21 @@ class PackageModule(SchemaChildModule):
|
||||
"""
|
||||
return database.DatabaseModule.node_type
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
from .edbfuncs import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .edbfuncs import procedure_blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .edbvars import blueprint as module
|
||||
self.submodules.append(module)
|
||||
super(PackageModule, self).register(app, options)
|
||||
|
||||
|
||||
blueprint = PackageModule(__name__)
|
||||
|
||||
|
||||
@@ -118,17 +118,36 @@ class TableModule(SchemaChildModule):
|
||||
|
||||
return snippets
|
||||
|
||||
def get_own_javascripts(self):
|
||||
scripts = SchemaChildModule.get_own_javascripts(self)
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
from .columns import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
scripts.append({
|
||||
'name': 'pgadmin.browser.table.partition.utils',
|
||||
'path': url_for('browser.index') +
|
||||
'table/static/js/partition.utils',
|
||||
'when': 'database', 'is_template': False
|
||||
})
|
||||
from .compound_triggers import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
return scripts
|
||||
from .constraints import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .indexes import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .partitions import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .row_security_policies import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .rules import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .triggers import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
super(TableModule, self).register(app, options)
|
||||
|
||||
|
||||
blueprint = TableModule(__name__)
|
||||
|
||||
@@ -83,6 +83,28 @@ class ConstraintsModule(CollectionNodeModule):
|
||||
"""
|
||||
return False
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
from .check_constraint import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .exclusion_constraint import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .foreign_key import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .index_constraint import primary_key_blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .index_constraint import unique_constraint_blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
super(ConstraintsModule, self).register(app, options)
|
||||
|
||||
|
||||
blueprint = ConstraintsModule(__name__)
|
||||
|
||||
|
||||
@@ -122,13 +122,9 @@ class PartitionsModule(CollectionNodeModule):
|
||||
sub-modules of table node under partition table node.
|
||||
"""
|
||||
|
||||
self.submodules = list(app.find_submodules(self.import_name))
|
||||
self.submodules = []
|
||||
super(CollectionNodeModule, self).register(app, options)
|
||||
|
||||
for module in self.submodules:
|
||||
module.parentmodules.append(self)
|
||||
app.register_blueprint(module)
|
||||
|
||||
# Now add sub modules of table node to partition table node.
|
||||
# Exclude 'partition' module for now to avoid cyclic import issue.
|
||||
modules_to_skip = ['partition', 'column']
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
SELECT DISTINCT ON(cls.relname) cls.oid, cls.relname as name,
|
||||
(SELECT (CASE WHEN count(i.inhrelid) > 0 THEN true ELSE false END) FROM pg_inherits i WHERE i.inhrelid = cls.oid) as is_inherited
|
||||
(SELECT (CASE WHEN count(i.inhrelid) > 0 THEN true ELSE false END) FROM pg_inherits i WHERE i.inhrelid = cls.oid) as is_inherited,
|
||||
CASE WHEN contype IN ('p', 'u', 'x') THEN desp.description ELSE des.description END AS description
|
||||
FROM pg_catalog.pg_index idx
|
||||
JOIN pg_catalog.pg_class cls ON cls.oid=indexrelid
|
||||
JOIN pg_catalog.pg_class tab ON tab.oid=indrelid
|
||||
@@ -8,6 +9,8 @@ FROM pg_catalog.pg_index idx
|
||||
JOIN pg_catalog.pg_am am ON am.oid=cls.relam
|
||||
LEFT JOIN pg_catalog.pg_depend dep ON (dep.classid = cls.tableoid AND dep.objid = cls.oid AND dep.refobjsubid = '0' AND dep.refclassid=(SELECT oid FROM pg_catalog.pg_class WHERE relname='pg_constraint') AND dep.deptype='i')
|
||||
LEFT OUTER JOIN pg_catalog.pg_constraint con ON (con.tableoid = dep.refclassid AND con.oid = dep.refobjid)
|
||||
LEFT OUTER JOIN pg_catalog.pg_description des ON (des.objoid=cls.oid AND des.classoid='pg_class'::regclass)
|
||||
LEFT OUTER JOIN pg_catalog.pg_description desp ON (desp.objoid=con.oid AND desp.objsubid = 0 AND desp.classoid='pg_constraint'::regclass)
|
||||
WHERE indrelid = {{tid}}::OID
|
||||
AND conname is NULL
|
||||
{% if idx %}
|
||||
|
||||
@@ -130,6 +130,25 @@ class ViewModule(SchemaChildModule):
|
||||
|
||||
return snippets
|
||||
|
||||
def register(self, app, options):
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.\
|
||||
tables.columns import blueprint as module
|
||||
self.submodules.append(module)
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.\
|
||||
tables.indexes import blueprint as module
|
||||
self.submodules.append(module)
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.\
|
||||
tables.triggers import blueprint as module
|
||||
self.submodules.append(module)
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.\
|
||||
tables.rules import blueprint as module
|
||||
self.submodules.append(module)
|
||||
from pgadmin.browser.server_groups.servers.databases.schemas.\
|
||||
tables.compound_triggers import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
super(ViewModule, self).register(app, options)
|
||||
|
||||
|
||||
class Message(IProcessDesc):
|
||||
def __init__(self, _sid, _data, _query):
|
||||
|
||||
@@ -111,6 +111,19 @@ SELECT EXISTS(
|
||||
"""
|
||||
return False
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
from .schedules import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .steps import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
super(JobModule, self).register(app, options)
|
||||
|
||||
|
||||
blueprint = JobModule(__name__)
|
||||
|
||||
|
||||
@@ -10,18 +10,17 @@
|
||||
import { getNodeListById } from '../../../../static/js/node_ajax';
|
||||
import ServerSchema from './server.ui';
|
||||
import Notify from '../../../../../static/js/helpers/Notifier';
|
||||
import { showServerPassword } from '../../../../static/js/password_dialogs';
|
||||
import { showServerPassword, showChangeServerPassword, showNamedRestorePoint } from '../../../../static/js/password_dialogs';
|
||||
|
||||
define('pgadmin.node.server', [
|
||||
'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
|
||||
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
|
||||
'sources/pgadmin', 'pgadmin.browser',
|
||||
'pgadmin.user_management.current_user',
|
||||
'pgadmin.alertifyjs', 'pgadmin.backform',
|
||||
'pgadmin.authenticate.kerberos',
|
||||
'pgadmin.browser.server.privilege',
|
||||
], function(
|
||||
gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser,
|
||||
current_user, Alertify, Backform, Kerberos,
|
||||
gettext, url_for, $, _, pgAdmin, pgBrowser,
|
||||
current_user, Kerberos,
|
||||
) {
|
||||
|
||||
if (!pgBrowser.Nodes['server']) {
|
||||
@@ -377,34 +376,7 @@ define('pgadmin.node.server', [
|
||||
if (!d)
|
||||
return false;
|
||||
|
||||
Alertify.prompt(
|
||||
gettext('Enter the name of the restore point to add'), '',
|
||||
// We will execute this function when user clicks on the OK button
|
||||
function(evt, value) {
|
||||
// If user has provided a value, send it to the server
|
||||
if(!_.isUndefined(value) && !_.isNull(value) && value !== ''
|
||||
&& String(value).replace(/^\s+|\s+$/g, '') !== '') {
|
||||
$.ajax({
|
||||
url: obj.generate_url(i, 'restore_point', d, true),
|
||||
method:'POST',
|
||||
data:{ 'value': JSON.stringify(value) },
|
||||
})
|
||||
.done(function(res) {
|
||||
Notify.success(res.data.result, 10000);
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
Notify.pgRespErrorNotify(xhr, error);
|
||||
t.unload(i);
|
||||
});
|
||||
} else {
|
||||
evt.cancel = true;
|
||||
Notify.error(gettext('Please enter a valid name.'), 10000);
|
||||
}
|
||||
},
|
||||
// We will execute this function when user clicks on the Cancel
|
||||
// button. Do nothing just close it.
|
||||
function(evt) { evt.cancel = false; }
|
||||
).set({'title': gettext('Restore point name')});
|
||||
showNamedRestorePoint(gettext('Restore point name'), d, obj, i);
|
||||
},
|
||||
|
||||
/* Change password */
|
||||
@@ -414,162 +386,10 @@ define('pgadmin.node.server', [
|
||||
t = pgBrowser.tree,
|
||||
i = input.item || t.selected(),
|
||||
d = i ? t.itemData(i) : undefined,
|
||||
url = obj.generate_url(i, 'change_password', d, true),
|
||||
is_pgpass_file_used = false,
|
||||
check_pgpass_url = obj.generate_url(i, 'check_pgpass', d, true);
|
||||
|
||||
if (d) {
|
||||
if(!Alertify.changeServerPassword) {
|
||||
var newPasswordModel = Backbone.Model.extend({
|
||||
defaults: {
|
||||
user_name: undefined,
|
||||
password: undefined,
|
||||
newPassword: undefined,
|
||||
confirmPassword: undefined,
|
||||
},
|
||||
validate: function() {
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
passwordChangeFields = [{
|
||||
name: 'user_name', label: gettext('User'),
|
||||
type: 'text', readonly: true, control: 'input',
|
||||
},{
|
||||
name: 'password', label: gettext('Current Password'),
|
||||
type: 'password', disabled: function() { return is_pgpass_file_used; },
|
||||
control: 'input', required: true,
|
||||
},{
|
||||
name: 'newPassword', label: gettext('New Password'),
|
||||
type: 'password', disabled: false, control: 'input',
|
||||
required: true,
|
||||
},{
|
||||
name: 'confirmPassword', label: gettext('Confirm Password'),
|
||||
type: 'password', disabled: false, control: 'input',
|
||||
required: true,
|
||||
}];
|
||||
|
||||
|
||||
Alertify.dialog('changeServerPassword' ,function factory() {
|
||||
return {
|
||||
main: function(params) {
|
||||
var title = gettext('Change Password');
|
||||
this.set('title', title);
|
||||
this.user_name = params.user.name;
|
||||
},
|
||||
setup:function() {
|
||||
return {
|
||||
buttons: [{
|
||||
text: gettext('Cancel'), key: 27,
|
||||
className: 'btn btn-secondary fa fa-times pg-alertify-button', attrs: {name: 'cancel'},
|
||||
},{
|
||||
text: gettext('OK'), key: 13, className: 'btn btn-primary fa fa-check pg-alertify-button',
|
||||
attrs: {name:'submit'},
|
||||
}],
|
||||
// Set options for dialog
|
||||
options: {
|
||||
padding : !1,
|
||||
overflow: !1,
|
||||
modal:false,
|
||||
resizable: true,
|
||||
maximizable: true,
|
||||
pinnable: false,
|
||||
closableByDimmer: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
hooks: {
|
||||
// triggered when the dialog is closed
|
||||
onclose: function() {
|
||||
if (this.view) {
|
||||
this.view.remove({data: true, internal: true, silent: true});
|
||||
}
|
||||
},
|
||||
},
|
||||
prepare: function() {
|
||||
var self = this;
|
||||
// Disable Ok button until user provides input
|
||||
this.__internal.buttons[1].element.disabled = true;
|
||||
|
||||
var $container = $('<div class=\'change_password\'></div>'),
|
||||
newpasswordmodel = new newPasswordModel(
|
||||
{'user_name': self.user_name}
|
||||
),
|
||||
view = this.view = new Backform.Form({
|
||||
el: $container,
|
||||
model: newpasswordmodel,
|
||||
fields: passwordChangeFields,
|
||||
});
|
||||
|
||||
view.render();
|
||||
|
||||
this.elements.content.appendChild($container.get(0));
|
||||
|
||||
// Listen to model & if filename is provided then enable Backup button
|
||||
this.view.model.on('change', function() {
|
||||
var that = this,
|
||||
password = this.get('password'),
|
||||
newPassword = this.get('newPassword'),
|
||||
confirmPassword = this.get('confirmPassword');
|
||||
|
||||
// Only check password field if pgpass file is not available
|
||||
if ((!is_pgpass_file_used &&
|
||||
(_.isUndefined(password) || _.isNull(password) || password == '')) ||
|
||||
_.isUndefined(newPassword) || _.isNull(newPassword) || newPassword == '' ||
|
||||
_.isUndefined(confirmPassword) || _.isNull(confirmPassword) || confirmPassword == '') {
|
||||
self.__internal.buttons[1].element.disabled = true;
|
||||
} else if (newPassword != confirmPassword) {
|
||||
self.__internal.buttons[1].element.disabled = true;
|
||||
|
||||
this.errorTimeout && clearTimeout(this.errorTimeout);
|
||||
this.errorTimeout = setTimeout(function() {
|
||||
that.errorModel.set('confirmPassword', gettext('Passwords do not match.'));
|
||||
} ,400);
|
||||
}else {
|
||||
that.errorModel.clear();
|
||||
self.__internal.buttons[1].element.disabled = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
// Callback functions when click on the buttons of the Alertify dialogs
|
||||
callback: function(e) {
|
||||
if (e.button.element.name == 'submit') {
|
||||
var self = this,
|
||||
alertArgs = this.view.model.toJSON();
|
||||
|
||||
e.cancel = true;
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
method:'POST',
|
||||
data:{'data': JSON.stringify(alertArgs) },
|
||||
})
|
||||
.done(function(res) {
|
||||
if (res.success) {
|
||||
// Notify user to update pgpass file
|
||||
if(is_pgpass_file_used) {
|
||||
Notify.alert(
|
||||
gettext('Change Password'),
|
||||
gettext('Please make sure to disconnect the server'
|
||||
+ ' and update the new password in the pgpass file'
|
||||
+ ' before performing any other operation')
|
||||
);
|
||||
}
|
||||
|
||||
Notify.success(res.info);
|
||||
self.close();
|
||||
} else {
|
||||
Notify.error(res.errormsg);
|
||||
}
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
Notify.pgRespErrorNotify(xhr, error);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Call to check if server is using pgpass file or not
|
||||
$.ajax({
|
||||
url: check_pgpass_url,
|
||||
@@ -579,7 +399,7 @@ define('pgadmin.node.server', [
|
||||
if (res.success && res.data.is_pgpass) {
|
||||
is_pgpass_file_used = true;
|
||||
}
|
||||
Alertify.changeServerPassword(d).resizeTo('40%','52%');
|
||||
showChangeServerPassword(gettext('Change Password'), d, obj, i, is_pgpass_file_used);
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
Notify.pgRespErrorNotify(xhr, error);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import gettext from 'sources/gettext';
|
||||
import BaseUISchema from '../../../static/js/SchemaView/base_schema.ui';
|
||||
import SchemaView from '../../../static/js/SchemaView';
|
||||
|
||||
class ChangePasswordSchema extends BaseUISchema {
|
||||
constructor(user, isPgpassFileUsed) {
|
||||
super({
|
||||
user: user,
|
||||
password: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
this.isPgpassFileUsed = isPgpassFileUsed;
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
let self = this;
|
||||
return [
|
||||
{
|
||||
id: 'user', label: gettext('User'), type: 'text', disabled: true
|
||||
}, {
|
||||
id: 'password', label: gettext('Current Password'), type: 'password',
|
||||
disabled: self.isPgpassFileUsed, noEmpty: self.isPgpassFileUsed ? false : true,
|
||||
controlProps: {
|
||||
maxLength: null
|
||||
}
|
||||
}, {
|
||||
id: 'newPassword', label: gettext('New Password'), type: 'password',
|
||||
noEmpty: true,
|
||||
controlProps: {
|
||||
maxLength: null
|
||||
}
|
||||
}, {
|
||||
id: 'confirmPassword', label: gettext('Confirm Password'), type: 'password',
|
||||
noEmpty: true,
|
||||
controlProps: {
|
||||
maxLength: null
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
validate(state, setError) {
|
||||
let errmsg = null;
|
||||
if (state.newPassword !== state.confirmPassword) {
|
||||
errmsg = gettext('Passwords do not match.');
|
||||
setError('confirmPassword', errmsg);
|
||||
return true;
|
||||
} else {
|
||||
setError('confirmPassword', null);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
root: {
|
||||
...theme.mixins.tabPanel,
|
||||
},
|
||||
}));
|
||||
|
||||
export default function ChangePasswordContent({onSave, onClose, userName, isPgpassFileUsed}) {
|
||||
const classes = useStyles();
|
||||
|
||||
return<SchemaView
|
||||
formType={'dialog'}
|
||||
getInitData={() => { /*This is intentional (SonarQube)*/ }}
|
||||
schema={new ChangePasswordSchema(userName, isPgpassFileUsed)}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={onSave}
|
||||
onClose={onClose}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={true}
|
||||
disableDialogHelp={true}
|
||||
isTabView={false}
|
||||
formClassName={classes.root}
|
||||
/>;
|
||||
}
|
||||
ChangePasswordContent.propTypes = {
|
||||
onSave: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
userName: PropTypes.string,
|
||||
isPgpassFileUsed: PropTypes.bool
|
||||
};
|
||||
@@ -1,3 +1,12 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import gettext from 'sources/gettext';
|
||||
import { Box } from '@material-ui/core';
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import gettext from 'sources/gettext';
|
||||
import url_for from 'sources/url_for';
|
||||
|
||||
import { Box } from '@material-ui/core';
|
||||
import CloseIcon from '@material-ui/icons/CloseRounded';
|
||||
import DeleteForeverIcon from '@material-ui/icons/DeleteForever';
|
||||
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
|
||||
import HelpIcon from '@material-ui/icons/Help';
|
||||
|
||||
import { DefaultButton, PrimaryButton, PgIconButton } from '../../../static/js/components/Buttons';
|
||||
import { useModalStyles } from '../../../static/js/helpers/ModalProvider';
|
||||
import { FormFooterMessage, InputText, MESSAGE_TYPE } from '../../../static/js/components/FormComponents';
|
||||
|
||||
export default function MasterPasswordContent({ closeModal, onResetPassowrd, onOK, onCancel, setHeight, isPWDPresent, data}) {
|
||||
const classes = useModalStyles();
|
||||
const containerRef = useRef();
|
||||
const firstEleRef = useRef();
|
||||
const okBtnRef = useRef();
|
||||
const [formData, setFormData] = useState({
|
||||
password: ''
|
||||
});
|
||||
|
||||
const onTextChange = (e, id) => {
|
||||
let val = e;
|
||||
if (e && e.target) {
|
||||
val = e.target.value;
|
||||
}
|
||||
setFormData((prev) => ({ ...prev, [id]: val }));
|
||||
};
|
||||
|
||||
const onKeyDown = (e) => {
|
||||
// If enter key is pressed then click on OK button
|
||||
if (e.key === 'Enter') {
|
||||
okBtnRef.current?.click();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
firstEleRef.current && firstEleRef.current.focus();
|
||||
}, 275);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setHeight?.(containerRef.current?.offsetHeight);
|
||||
}, [containerRef.current]);
|
||||
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" className={classes.container} ref={containerRef}>
|
||||
<Box flexGrow="1" p={2}>
|
||||
<Box>
|
||||
<span style={{ fontWeight: 'bold' }}>
|
||||
{isPWDPresent ? gettext('Please enter your master password.') : gettext('Please set a master password for pgAdmin.')}
|
||||
</span>
|
||||
<br />
|
||||
<span style={{ fontWeight: 'bold' }}>
|
||||
{isPWDPresent ? gettext('This is required to unlock saved passwords and reconnect to the database server(s).') : gettext('This will be used to secure and later unlock saved passwords and other credentials.')}
|
||||
</span>
|
||||
</Box>
|
||||
<Box marginTop='12px'>
|
||||
<InputText inputRef={firstEleRef} type="password" value={formData['password']} maxLength={null}
|
||||
onChange={(e) => onTextChange(e, 'password')} onKeyDown={(e) => onKeyDown(e)}/>
|
||||
</Box>
|
||||
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={data.errmsg} closable={false} style={{
|
||||
position: 'unset', padding: '12px 0px 0px'
|
||||
}} />
|
||||
</Box>
|
||||
<Box className={classes.footer}>
|
||||
<Box style={{ marginRight: 'auto' }}>
|
||||
<PgIconButton data-test="help-masterpassword" title={gettext('Help')} style={{ padding: '0.3rem', paddingLeft: '0.7rem' }} startIcon={<HelpIcon />} onClick={() => {
|
||||
let _url = url_for('help.static', {
|
||||
'filename': 'master_password.html',
|
||||
});
|
||||
window.open(_url, 'pgadmin_help');
|
||||
}} >
|
||||
</PgIconButton>
|
||||
{isPWDPresent &&
|
||||
<DefaultButton data-test="reset-masterpassword" style={{ marginLeft: '0.5rem' }} startIcon={<DeleteForeverIcon />}
|
||||
onClick={() => {onResetPassowrd?.();}} >
|
||||
{gettext('Reset Master Password')}
|
||||
</DefaultButton>
|
||||
}
|
||||
</Box>
|
||||
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={() => {
|
||||
onCancel?.();
|
||||
closeModal();
|
||||
}} >{gettext('Cancel')}</DefaultButton>
|
||||
<PrimaryButton ref={okBtnRef} data-test="save" className={classes.margin} startIcon={<CheckRoundedIcon />}
|
||||
disabled={formData.password.length == 0}
|
||||
onClick={() => {
|
||||
let postFormData = new FormData();
|
||||
postFormData.append('password', formData.password);
|
||||
postFormData.append('submit_password', true);
|
||||
onOK?.(postFormData);
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
{gettext('OK')}
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
</Box>);
|
||||
}
|
||||
|
||||
MasterPasswordContent.propTypes = {
|
||||
closeModal: PropTypes.func,
|
||||
onResetPassowrd: PropTypes.func,
|
||||
onOK: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
setHeight: PropTypes.func,
|
||||
isPWDPresent: PropTypes.bool,
|
||||
data: PropTypes.object,
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2022, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import gettext from 'sources/gettext';
|
||||
import { Box } from '@material-ui/core';
|
||||
import { DefaultButton, PrimaryButton } from '../../../static/js/components/Buttons';
|
||||
import CloseIcon from '@material-ui/icons/CloseRounded';
|
||||
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useModalStyles } from '../../../static/js/helpers/ModalProvider';
|
||||
import { InputText } from '../../../static/js/components/FormComponents';
|
||||
import { isEmptyString } from '../../../static/js/validators';
|
||||
|
||||
export default function NamedRestoreContent({closeModal, onOK, setHeight}) {
|
||||
const classes = useModalStyles();
|
||||
const containerRef = useRef();
|
||||
const firstEleRef = useRef();
|
||||
const okBtnRef = useRef();
|
||||
const [formData, setFormData] = useState({
|
||||
namedRestorePoint: ''
|
||||
});
|
||||
|
||||
const onTextChange = (e, id) => {
|
||||
let val = e;
|
||||
if(e && e.target) {
|
||||
val = e.target.value;
|
||||
}
|
||||
setFormData((prev)=>({...prev, [id]: val}));
|
||||
};
|
||||
|
||||
const onKeyDown = (e) => {
|
||||
// If enter key is pressed then click on OK button
|
||||
if (e.key === 'Enter') {
|
||||
okBtnRef.current?.click();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(()=>{
|
||||
setTimeout(()=>{
|
||||
firstEleRef.current && firstEleRef.current.focus();
|
||||
}, 275);
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
setHeight?.(containerRef.current?.offsetHeight);
|
||||
}, [containerRef.current]);
|
||||
|
||||
const isOKDisabled = isEmptyString(formData.namedRestorePoint);
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" className={classes.container} ref={containerRef}>
|
||||
<Box flexGrow="1" p={2}>
|
||||
<Box>
|
||||
<span style={{fontWeight: 'bold'}}>
|
||||
{gettext('Enter the name of the restore point to add')}
|
||||
</span>
|
||||
</Box>
|
||||
<Box marginTop='12px'>
|
||||
<InputText inputRef={firstEleRef} type="text" value={formData['namedRestorePoint']}
|
||||
onChange={(e)=>onTextChange(e, 'namedRestorePoint')} onKeyDown={(e)=>onKeyDown(e)}/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className={classes.footer}>
|
||||
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={()=>{
|
||||
closeModal();
|
||||
}} >{gettext('Cancel')}</DefaultButton>
|
||||
<PrimaryButton ref={okBtnRef} data-test="save" disabled={isOKDisabled} className={classes.margin} startIcon={<CheckRoundedIcon />} onClick={()=>{
|
||||
let postFormData = new FormData();
|
||||
postFormData.append('value', formData.namedRestorePoint);
|
||||
onOK?.(postFormData);
|
||||
closeModal();
|
||||
}} >{gettext('OK')}</PrimaryButton>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
NamedRestoreContent.propTypes = {
|
||||
closeModal: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
onOK: PropTypes.func,
|
||||
setHeight: PropTypes.func
|
||||
};
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import { generateNodeUrl } from './node_ajax';
|
||||
import Notify, {initializeModalProvider, initializeNotifier} from '../../../static/js/helpers/Notifier';
|
||||
import { checkMasterPassword } from './password_dialogs';
|
||||
|
||||
define('pgadmin.browser', [
|
||||
'sources/gettext', 'sources/url_for', 'require', 'jquery', 'underscore',
|
||||
@@ -569,101 +570,6 @@ define('pgadmin.browser', [
|
||||
Notify.alert(error);
|
||||
});
|
||||
},
|
||||
init_master_password: function() {
|
||||
let self = this;
|
||||
// Master password dialog
|
||||
if (!Alertify.dlgMasterPass) {
|
||||
Alertify.dialog('dlgMasterPass', function factory() {
|
||||
return {
|
||||
main: function(title, message, reset) {
|
||||
this.set('title', title);
|
||||
this.message = message;
|
||||
this.reset = reset;
|
||||
},
|
||||
build: function() {
|
||||
Alertify.pgDialogBuild.apply(this);
|
||||
},
|
||||
setup:function() {
|
||||
return {
|
||||
buttons:[{
|
||||
text: '',
|
||||
className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button',
|
||||
attrs: {
|
||||
name: 'dialog_help',
|
||||
type: 'button',
|
||||
label: gettext('Master password'),
|
||||
url: url_for('help.static', {
|
||||
'filename': 'master_password.html',
|
||||
}),
|
||||
},
|
||||
},{
|
||||
text: gettext('Reset Master Password'), className: 'btn btn-secondary fa fa-trash-alt pg-alertify-button pull-left',
|
||||
},{
|
||||
text: gettext('Cancel'), className: 'btn btn-secondary fa fa-times pg-alertify-button',
|
||||
key: 27,
|
||||
},{
|
||||
text: gettext('OK'), key: 13, className: 'btn btn-primary fa fa-check pg-alertify-button',
|
||||
}],
|
||||
focus: {element: '#password', select: true},
|
||||
options: {
|
||||
modal: true, resizable: false, maximizable: false, pinnable: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
prepare:function() {
|
||||
let _self = this;
|
||||
_self.setContent(_self.message);
|
||||
/* Reset button hide */
|
||||
if(!_self.reset) {
|
||||
$(_self.__internal.buttons[1].element).addClass('d-none');
|
||||
} else {
|
||||
$(_self.__internal.buttons[1].element).removeClass('d-none');
|
||||
}
|
||||
},
|
||||
callback: function(event) {
|
||||
let parentDialog = this;
|
||||
|
||||
if (event.index == 3) {
|
||||
/* OK Button */
|
||||
self.set_master_password(
|
||||
$('#frmMasterPassword #password').val(),
|
||||
true,parentDialog.set_callback,
|
||||
);
|
||||
} else if(event.index == 2) {
|
||||
/* Cancel button */
|
||||
self.masterpass_callback_queue = [];
|
||||
self.cancel_callback();
|
||||
} else if(event.index == 1) {
|
||||
/* Reset Button */
|
||||
event.cancel = true;
|
||||
|
||||
Notify.confirm(gettext('Reset Master Password'),
|
||||
gettext('This will remove all the saved passwords. This will also remove established connections to '
|
||||
+ 'the server and you may need to reconnect again. Do you wish to continue?'),
|
||||
function() {
|
||||
/* If user clicks Yes */
|
||||
self.reset_master_password();
|
||||
parentDialog.close();
|
||||
return true;
|
||||
},
|
||||
function() {/* If user clicks No */ return true;}
|
||||
);
|
||||
} else if(event.index == 0) {
|
||||
/* help Button */
|
||||
event.cancel = true;
|
||||
self.showHelp(
|
||||
event.button.element.name,
|
||||
event.button.element.getAttribute('url'),
|
||||
null, null
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
check_master_password: function(on_resp_callback) {
|
||||
$.ajax({
|
||||
url: url_for('browser.check_master_password'),
|
||||
@@ -697,40 +603,21 @@ define('pgadmin.browser', [
|
||||
});
|
||||
},
|
||||
|
||||
set_master_password: function(password='', button_click=false,
|
||||
set_master_password: function(password='',
|
||||
set_callback=()=>{/*This is intentional (SonarQube)*/},
|
||||
cancel_callback=()=>{/*This is intentional (SonarQube)*/}) {
|
||||
let data=null, self = this;
|
||||
|
||||
data = JSON.stringify({
|
||||
// data = JSON.stringify({
|
||||
// 'password': password,
|
||||
// });
|
||||
data = {
|
||||
'password': password,
|
||||
'button_click': button_click,
|
||||
});
|
||||
};
|
||||
|
||||
self.masterpass_callback_queue.push(set_callback);
|
||||
self.cancel_callback = cancel_callback;
|
||||
|
||||
$.ajax({
|
||||
url: url_for('browser.set_master_password'),
|
||||
type: 'POST',
|
||||
data: data,
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
}).done((res)=> {
|
||||
if(!res.data.present) {
|
||||
self.init_master_password();
|
||||
Alertify.dlgMasterPass(res.data.title, res.data.content, res.data.reset);
|
||||
} else {
|
||||
setTimeout(()=>{
|
||||
while(self.masterpass_callback_queue.length > 0) {
|
||||
let callback = self.masterpass_callback_queue.shift();
|
||||
callback();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}).fail(function(xhr, status, error) {
|
||||
Notify.pgRespErrorNotify(xhr, error);
|
||||
});
|
||||
// Check master passowrd.
|
||||
checkMasterPassword(data, self.masterpass_callback_queue, cancel_callback);
|
||||
},
|
||||
|
||||
bind_beforeunload: function() {
|
||||
|
||||
@@ -514,12 +514,15 @@ define('pgadmin.browser.node', [
|
||||
new_height = height;
|
||||
}
|
||||
|
||||
let x = (body.offsetWidth - new_width) / 2;
|
||||
let y = (body.offsetHeight - new_height) / 4;
|
||||
|
||||
var new_panel = dockerObject.addPanel(
|
||||
'utility_props', window.wcDocker.DOCK.FLOAT, undefined, {
|
||||
w: new_width,
|
||||
h: new_height,
|
||||
x: (screen.width < 700 ? '2%' : '25%'),
|
||||
y: (screen.height < 500 ? '2%' : '25%'),
|
||||
x: (x),
|
||||
y: (y),
|
||||
}
|
||||
);
|
||||
/*set movable false to prevent dialog from docking,
|
||||
@@ -548,7 +551,25 @@ define('pgadmin.browser.node', [
|
||||
|
||||
}
|
||||
},
|
||||
registerDockerPanel: function(docker, name, params) {
|
||||
var w = docker || pgBrowser.docker,
|
||||
p = w.findPanels(name);
|
||||
|
||||
if (p && p.length == 1)
|
||||
return;
|
||||
|
||||
p = new pgBrowser.Panel({
|
||||
name: name,
|
||||
showTitle: true,
|
||||
isCloseable: true,
|
||||
isPrivate: true,
|
||||
isLayoutMember: false,
|
||||
canMaximise: true,
|
||||
content: '<div class="obj_properties container-fluid h-100"></div>',
|
||||
...params,
|
||||
});
|
||||
p.load(w);
|
||||
},
|
||||
registerUtilityPanel: function(docker) {
|
||||
var w = docker || pgBrowser.docker,
|
||||
p = w.findPanels('utility_props');
|
||||
|
||||
@@ -18,6 +18,7 @@ import { generateNodeUrl } from './node_ajax';
|
||||
import Notify from '../../../static/js/helpers/Notifier';
|
||||
import gettext from 'sources/gettext';
|
||||
import 'wcdocker';
|
||||
import Theme from '../../../static/js/Theme';
|
||||
|
||||
/* The entry point for rendering React based view in properties, called in node.js */
|
||||
export function getNodeView(nodeType, treeNodeInfo, actionType, itemNodeData, formType, container, containerPanel, onEdit, onSave) {
|
||||
@@ -204,24 +205,26 @@ export function getNodeView(nodeType, treeNodeInfo, actionType, itemNodeData, fo
|
||||
|
||||
/* Fire at will, mount the DOM */
|
||||
ReactDOM.render(
|
||||
<SchemaView
|
||||
formType={formType}
|
||||
getInitData={initData}
|
||||
schema={schema}
|
||||
viewHelperProps={viewHelperProps}
|
||||
onSave={onSaveClick}
|
||||
onClose={()=>containerPanel.close()}
|
||||
onHelp={onHelp}
|
||||
onEdit={onEdit}
|
||||
onDataChange={(dataChanged)=>{
|
||||
isDirty = dataChanged;
|
||||
}}
|
||||
confirmOnCloseReset={confirmOnCloseReset}
|
||||
hasSQL={nodeObj.hasSQL && (actionType === 'create' || actionType === 'edit')}
|
||||
getSQLValue={getSQLValue}
|
||||
disableSqlHelp={nodeObj.sqlAlterHelp == '' && nodeObj.sqlCreateHelp == '' && !nodeObj.epasHelp}
|
||||
disableDialogHelp={nodeObj.dialogHelp == undefined || nodeObj.dialogHelp == ''}
|
||||
/>, container);
|
||||
<Theme>
|
||||
<SchemaView
|
||||
formType={formType}
|
||||
getInitData={initData}
|
||||
schema={schema}
|
||||
viewHelperProps={viewHelperProps}
|
||||
onSave={onSaveClick}
|
||||
onClose={()=>containerPanel.close()}
|
||||
onHelp={onHelp}
|
||||
onEdit={onEdit}
|
||||
onDataChange={(dataChanged)=>{
|
||||
isDirty = dataChanged;
|
||||
}}
|
||||
confirmOnCloseReset={confirmOnCloseReset}
|
||||
hasSQL={nodeObj.hasSQL && (actionType === 'create' || actionType === 'edit')}
|
||||
getSQLValue={getSQLValue}
|
||||
disableSqlHelp={nodeObj.sqlAlterHelp == '' && nodeObj.sqlCreateHelp == '' && !nodeObj.epasHelp}
|
||||
disableDialogHelp={nodeObj.dialogHelp == undefined || nodeObj.dialogHelp == ''}
|
||||
/>
|
||||
</Theme>, container);
|
||||
}
|
||||
|
||||
/* When switching from normal node to collection node, clean up the React mounted DOM */
|
||||
|
||||
@@ -273,6 +273,15 @@ define(
|
||||
);
|
||||
}
|
||||
}
|
||||
if (eventName == 'panelClosed' && selectedPanel._type == 'dashboard') {
|
||||
getPanelView(
|
||||
pgBrowser.tree,
|
||||
$container[0],
|
||||
pgBrowser,
|
||||
this._type,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -24,7 +24,8 @@ export function getPanelView(
|
||||
tree,
|
||||
container,
|
||||
pgBrowser,
|
||||
panelType
|
||||
panelType,
|
||||
panelVisible = true
|
||||
) {
|
||||
let item = !_.isNull(tree)? tree.selected(): null,
|
||||
nodeData, node, treeNodeInfo, preferences, graphPref, dashPref;
|
||||
@@ -52,6 +53,7 @@ export function getPanelView(
|
||||
sid={!_.isUndefined(treeNodeInfo) && !_.isUndefined(treeNodeInfo['server']) ? treeNodeInfo['server']._id : ''}
|
||||
serverConnected={!_.isUndefined(treeNodeInfo) && !_.isUndefined(treeNodeInfo['server']) ? treeNodeInfo.server.connected: false}
|
||||
dbConnected={!_.isUndefined(treeNodeInfo) && !_.isUndefined(treeNodeInfo['database']) ? treeNodeInfo.database.connected: false}
|
||||
panelVisible={panelVisible}
|
||||
/>
|
||||
</Theme>,
|
||||
container
|
||||
|
||||
@@ -13,7 +13,13 @@ import pgAdmin from 'sources/pgadmin';
|
||||
import ConnectServerContent from './ConnectServerContent';
|
||||
import Theme from 'sources/Theme';
|
||||
import url_for from 'sources/url_for';
|
||||
import gettext from 'sources/gettext';
|
||||
|
||||
import getApiInstance from '../../../static/js/api_instance';
|
||||
import MasterPasswordContent from './MasterPassowrdContent';
|
||||
import ChangePasswordContent from './ChangePassowrdContent';
|
||||
import NamedRestoreContent from './NamedRestoreContent';
|
||||
import Notify from '../../../static/js/helpers/Notifier';
|
||||
|
||||
function setNewSize(panel, width, height) {
|
||||
// Add height of the header
|
||||
@@ -130,3 +136,169 @@ export function showSchemaDiffServerPassword() {
|
||||
/>
|
||||
</Theme>, j[0]);
|
||||
}
|
||||
|
||||
function masterPassCallbacks(masterpass_callback_queue) {
|
||||
while(masterpass_callback_queue.length > 0) {
|
||||
let callback = masterpass_callback_queue.shift();
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
export function checkMasterPassword(data, masterpass_callback_queue, cancel_callback) {
|
||||
const api = getApiInstance();
|
||||
api.post(url_for('browser.set_master_password'), data).then((res)=> {
|
||||
if(!res.data.data.present) {
|
||||
showMasterPassword(res.data.data.reset, res.data.data.errmsg, masterpass_callback_queue, cancel_callback);
|
||||
} else {
|
||||
masterPassCallbacks(masterpass_callback_queue);
|
||||
}
|
||||
}).catch(function(xhr, status, error) {
|
||||
Notify.pgRespErrorNotify(xhr, error);
|
||||
});
|
||||
}
|
||||
|
||||
// This functions is used to show the master password dialog.
|
||||
export function showMasterPassword(isPWDPresent, errmsg=null, masterpass_callback_queue, cancel_callback) {
|
||||
const api = getApiInstance();
|
||||
var pgBrowser = pgAdmin.Browser;
|
||||
|
||||
// Register dialog panel
|
||||
pgBrowser.Node.registerUtilityPanel();
|
||||
var panel = pgBrowser.Node.addUtilityPanel(pgBrowser.stdW.md),
|
||||
j = panel.$container.find('.obj_properties').first();
|
||||
|
||||
let title = isPWDPresent ? gettext('Unlock Saved Passwords') : gettext('Set Master Password');
|
||||
panel.title(title);
|
||||
|
||||
ReactDOM.render(
|
||||
<Theme>
|
||||
<MasterPasswordContent
|
||||
isPWDPresent= {isPWDPresent}
|
||||
data={{'errmsg': errmsg}}
|
||||
setHeight={(containerHeight) => {
|
||||
setNewSize(panel, pgBrowser.stdW.md, containerHeight);
|
||||
}}
|
||||
closeModal={() => {
|
||||
panel.close();
|
||||
}}
|
||||
onResetPassowrd={()=>{
|
||||
Notify.confirm(gettext('Reset Master Password'),
|
||||
gettext('This will remove all the saved passwords. This will also remove established connections to '
|
||||
+ 'the server and you may need to reconnect again. Do you wish to continue?'),
|
||||
function() {
|
||||
var _url = url_for('browser.reset_master_password');
|
||||
|
||||
api.delete(_url)
|
||||
.then(() => {
|
||||
panel.close();
|
||||
showMasterPassword(false, null, masterpass_callback_queue, cancel_callback);
|
||||
})
|
||||
.catch((err) => {
|
||||
Notify.error(err.message);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
function() {/* If user clicks No */ return true;}
|
||||
);
|
||||
}}
|
||||
onCancel={()=>{
|
||||
cancel_callback?.();
|
||||
}}
|
||||
onOK={(formData) => {
|
||||
panel.close();
|
||||
checkMasterPassword(formData, masterpass_callback_queue, cancel_callback);
|
||||
}}
|
||||
/>
|
||||
</Theme>, j[0]);
|
||||
}
|
||||
|
||||
export function showChangeServerPassword() {
|
||||
var pgBrowser = pgAdmin.Browser,
|
||||
title = arguments[0],
|
||||
nodeData = arguments[1],
|
||||
nodeObj = arguments[2],
|
||||
itemNodeData = arguments[3],
|
||||
isPgPassFileUsed = arguments[4];
|
||||
|
||||
// Register dialog panel
|
||||
pgBrowser.Node.registerUtilityPanel();
|
||||
var panel = pgBrowser.Node.addUtilityPanel(pgBrowser.stdW.md),
|
||||
j = panel.$container.find('.obj_properties').first();
|
||||
panel.title(title);
|
||||
|
||||
ReactDOM.render(
|
||||
<Theme>
|
||||
<ChangePasswordContent
|
||||
onClose={()=>{
|
||||
panel.close();
|
||||
}}
|
||||
onSave={(isNew, data)=>{
|
||||
return new Promise((resolve, reject)=>{
|
||||
const api = getApiInstance();
|
||||
var _url = nodeObj.generate_url(itemNodeData, 'change_password', nodeData, true);
|
||||
|
||||
api.post(_url, data)
|
||||
.then(({data: respData})=>{
|
||||
Notify.success(respData.info);
|
||||
// Notify user to update pgpass file
|
||||
if(isPgPassFileUsed) {
|
||||
Notify.alert(
|
||||
gettext('Change Password'),
|
||||
gettext('Please make sure to disconnect the server'
|
||||
+ ' and update the new password in the pgpass file'
|
||||
+ ' before performing any other operation')
|
||||
);
|
||||
}
|
||||
|
||||
resolve(respData.data);
|
||||
panel.close();
|
||||
})
|
||||
.catch((error)=>{
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}}
|
||||
userName={nodeData.user.name}
|
||||
isPgpassFileUsed={isPgPassFileUsed}
|
||||
/>
|
||||
</Theme>, j[0]);
|
||||
}
|
||||
|
||||
export function showNamedRestorePoint() {
|
||||
var pgBrowser = pgAdmin.Browser,
|
||||
title = arguments[0],
|
||||
nodeData = arguments[1],
|
||||
nodeObj = arguments[2],
|
||||
itemNodeData = arguments[3];
|
||||
|
||||
// Register dialog panel
|
||||
pgBrowser.Node.registerUtilityPanel();
|
||||
var panel = pgBrowser.Node.addUtilityPanel(pgBrowser.stdW.md),
|
||||
j = panel.$container.find('.obj_properties').first();
|
||||
panel.title(title);
|
||||
|
||||
ReactDOM.render(
|
||||
<Theme>
|
||||
<NamedRestoreContent
|
||||
setHeight={(containerHeight)=>{
|
||||
setNewSize(panel, pgBrowser.stdW.md, containerHeight);
|
||||
}}
|
||||
closeModal={()=>{
|
||||
panel.close();
|
||||
}}
|
||||
onOK={(formData)=>{
|
||||
const api = getApiInstance();
|
||||
var _url = nodeObj.generate_url(itemNodeData, 'restore_point', nodeData, true);
|
||||
|
||||
api.post(_url, formData)
|
||||
.then(res=>{
|
||||
panel.close();
|
||||
Notify.success(res.data.data.result);
|
||||
})
|
||||
.catch(function(xhr, status, error) {
|
||||
Notify.pgRespErrorNotify(xhr, error);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Theme>, j[0]);
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import getApiInstance from 'sources/api_instance';
|
||||
import {getHelpUrl, getEPASHelpUrl} from 'pgadmin.help';
|
||||
import SchemaView from 'sources/SchemaView';
|
||||
import 'wcdocker';
|
||||
import Theme from '../../../static/js/Theme';
|
||||
import url_for from 'sources/url_for';
|
||||
|
||||
/* The entry point for rendering React based view in properties, called in node.js */
|
||||
export function getUtilityView(schema, treeNodeInfo, actionType, formType, container, containerPanel,
|
||||
@@ -76,26 +78,51 @@ export function getUtilityView(schema, treeNodeInfo, actionType, formType, conta
|
||||
inCatalog: inCatalog,
|
||||
};
|
||||
|
||||
let initData = ()=>new Promise((resolve, reject)=>{
|
||||
if(actionType === 'create') {
|
||||
resolve({});
|
||||
}else{
|
||||
api.get(url_for('import_export.get_settings'))
|
||||
.then((res)=>{
|
||||
resolve(res.data.data);
|
||||
})
|
||||
.catch((err)=>{
|
||||
if(err.response){
|
||||
console.error('error resp', err.response);
|
||||
} else if(err.request){
|
||||
console.error('error req', err.request);
|
||||
} else if(err.message){
|
||||
console.error('error msg', err.message);
|
||||
}reject(err);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
let _schema = schema;
|
||||
|
||||
/* Fire at will, mount the DOM */
|
||||
ReactDOM.render(
|
||||
<SchemaView
|
||||
formType={formType}
|
||||
schema={_schema}
|
||||
viewHelperProps={viewHelperProps}
|
||||
customSaveBtnName={saveBtnName}
|
||||
customSaveBtnIconType={saveBtnIcon}
|
||||
onSave={onSaveClick}
|
||||
onClose={()=>containerPanel.close()}
|
||||
onHelp={onHelp}
|
||||
onDataChange={()=>{/*This is intentional (SonarQube)*/}}
|
||||
confirmOnCloseReset={confirmOnReset}
|
||||
hasSQL={false}
|
||||
isTabView={isTabView}
|
||||
disableSqlHelp={sqlHelpUrl == undefined || sqlHelpUrl == ''}
|
||||
disableDialogHelp={helpUrl == undefined || helpUrl == ''}
|
||||
/>, container);
|
||||
<Theme>
|
||||
<SchemaView
|
||||
formType={formType}
|
||||
getInitData={initData}
|
||||
schema={_schema}
|
||||
viewHelperProps={viewHelperProps}
|
||||
customSaveBtnName={saveBtnName}
|
||||
customSaveBtnIconType={saveBtnIcon}
|
||||
onSave={onSaveClick}
|
||||
onClose={()=>containerPanel.close()}
|
||||
onHelp={onHelp}
|
||||
onDataChange={()=>{/*This is intentional (SonarQube)*/}}
|
||||
confirmOnCloseReset={confirmOnReset}
|
||||
hasSQL={false}
|
||||
isTabView={isTabView}
|
||||
disableSqlHelp={sqlHelpUrl == undefined || sqlHelpUrl == ''}
|
||||
disableDialogHelp={helpUrl == undefined || helpUrl == ''}
|
||||
/>
|
||||
</Theme>, container);
|
||||
}
|
||||
|
||||
/* When switching from normal node to collection node, clean up the React mounted DOM */
|
||||
|
||||
@@ -16,3 +16,39 @@
|
||||
background-size: 20px !important;
|
||||
background-position: center left;
|
||||
}
|
||||
|
||||
.icon-at {
|
||||
background-image: url('../../../../static/img/at.svg') !important;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 15px !important;
|
||||
align-content: center;
|
||||
vertical-align: middle;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.icon-key {
|
||||
background-image: url('../../../../static/img/key.svg') !important;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 15px !important;
|
||||
align-content: center;
|
||||
vertical-align: middle;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.icon-join {
|
||||
background-image: url('../../../../static/img/join.svg') !important;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 20px !important;
|
||||
align-content: center;
|
||||
vertical-align: middle;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.icon-spinner {
|
||||
background-image: url('../../../../static/img/spinner.svg') !important;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 15px !important;
|
||||
align-content: center;
|
||||
vertical-align: middle;
|
||||
height: 15px;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<form name="frmMasterPassword" id="frmMasterPassword" style="height: 100%; width: 100%" onsubmit="return false;">
|
||||
<div>
|
||||
<div><strong>{{ content_text|safe }}</strong></div>
|
||||
<div class="input-group row py-2">
|
||||
<label for="password" class="col-sm-2 col-form-label">{{ _('Password') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="password" name="password">
|
||||
</div>
|
||||
</div>
|
||||
{% if errmsg %}
|
||||
<div class='pg-prop-status-bar p-0'>
|
||||
<div class="error-in-footer">
|
||||
<div class="d-flex px-2 py-1">
|
||||
<div class="pr-2">
|
||||
<i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="alert-text">{{ errmsg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -25,30 +25,20 @@ class MasterPasswordTestCase(BaseTestGenerator):
|
||||
# This testcase validates invalid confirmation password
|
||||
('TestCase for Create master password dialog', dict(
|
||||
password="",
|
||||
content=(
|
||||
"Set Master Password",
|
||||
[
|
||||
"Please set a master password for pgAdmin.",
|
||||
"This will be used to secure and later unlock saved "
|
||||
"passwords and other credentials."
|
||||
]
|
||||
)
|
||||
errmsg=None,
|
||||
is_error=False
|
||||
)),
|
||||
('TestCase for Setting Master Password', dict(
|
||||
password="masterpasstest",
|
||||
check_if_set=True,
|
||||
errmsg=None,
|
||||
is_error=False
|
||||
)),
|
||||
('TestCase for Resetting Master Password', dict(
|
||||
reset=True,
|
||||
password="",
|
||||
content=(
|
||||
"Set Master Password",
|
||||
[
|
||||
"Please set a master password for pgAdmin.",
|
||||
"This will be used to secure and later unlock saved "
|
||||
"passwords and other credentials."
|
||||
]
|
||||
)
|
||||
errmsg=None,
|
||||
is_error=False
|
||||
)),
|
||||
]
|
||||
|
||||
@@ -90,13 +80,6 @@ class MasterPasswordTestCase(BaseTestGenerator):
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
if hasattr(self, 'content'):
|
||||
self.assertEqual(response.json['data']['title'],
|
||||
self.content[0])
|
||||
|
||||
for text in self.content[1]:
|
||||
self.assertIn(text, response.json['data']['content'])
|
||||
|
||||
if hasattr(self, 'check_if_set'):
|
||||
response = self.tester.get(
|
||||
'/browser/master_password'
|
||||
|
||||
@@ -36,13 +36,6 @@ class DashboardModule(PgAdminModule):
|
||||
def get_own_menuitems(self):
|
||||
return {}
|
||||
|
||||
def get_own_javascripts(self):
|
||||
return [{
|
||||
'name': 'pgadmin.dashboard',
|
||||
'path': url_for('dashboard.index') + 'dashboard',
|
||||
'when': None
|
||||
}]
|
||||
|
||||
def get_own_stylesheets(self):
|
||||
"""
|
||||
Returns:
|
||||
|
||||
@@ -797,7 +797,7 @@ export default function Dashboard({
|
||||
preferences={preferences}
|
||||
sid={sid}
|
||||
did={did}
|
||||
pageVisible={true}
|
||||
pageVisible={props.panelVisible}
|
||||
></Graphs>
|
||||
)}
|
||||
{!_.isUndefined(preferences) && preferences.show_activity && (
|
||||
@@ -887,6 +887,7 @@ Dashboard.propTypes = {
|
||||
row: PropTypes.object,
|
||||
serverConnected: PropTypes.bool,
|
||||
dbConnected: PropTypes.bool,
|
||||
panelVisible: PropTypes.bool,
|
||||
};
|
||||
|
||||
export function ChartContainer(props) {
|
||||
|
||||
@@ -179,6 +179,9 @@ export default function Graphs({preferences, sid, did, pageVisible, enablePoll=t
|
||||
});
|
||||
|
||||
let path = getStatsUrl(sid, did, getFor);
|
||||
if (!pageVisible){
|
||||
return;
|
||||
}
|
||||
axios.get(path)
|
||||
.then((resp)=>{
|
||||
let data = resp.data;
|
||||
|
||||
@@ -81,67 +81,83 @@ class PGDataypeFeatureTest(BaseFeatureTest):
|
||||
connection.close()
|
||||
|
||||
def _update_preferences(self):
|
||||
file_menu = self.page.find_by_css_selector(
|
||||
NavMenuLocators.file_menu_css)
|
||||
file_menu.click()
|
||||
retry = 2
|
||||
while retry > 0:
|
||||
try:
|
||||
file_menu = self.page.find_by_css_selector(
|
||||
NavMenuLocators.file_menu_css)
|
||||
file_menu.click()
|
||||
|
||||
self.page.retry_click(
|
||||
(By.CSS_SELECTOR, NavMenuLocators.preference_menu_item_css),
|
||||
(By.XPATH, NavMenuLocators.specified_preference_tree_node
|
||||
.format('Browser'))
|
||||
)
|
||||
self.page.retry_click(
|
||||
(By.CSS_SELECTOR,
|
||||
NavMenuLocators.preference_menu_item_css),
|
||||
(By.XPATH,
|
||||
NavMenuLocators.specified_preference_tree_node
|
||||
.format('Browser'))
|
||||
)
|
||||
|
||||
wait = WebDriverWait(self.page.driver, 10)
|
||||
wait = WebDriverWait(self.page.driver, 10)
|
||||
|
||||
browser_node = self.page.find_by_xpath(
|
||||
NavMenuLocators.specified_preference_tree_node.format('Browser'))
|
||||
if self.page.find_by_xpath(
|
||||
NavMenuLocators.specified_pref_node_exp_status.
|
||||
format('Browser')).get_attribute('aria-expanded') == 'false':
|
||||
ActionChains(self.driver).double_click(browser_node).perform()
|
||||
browser_node = self.page.find_by_xpath(
|
||||
NavMenuLocators.specified_preference_tree_node.
|
||||
format('Browser'))
|
||||
if self.page.find_by_xpath(
|
||||
NavMenuLocators.specified_pref_node_exp_status.
|
||||
format('Browser')).\
|
||||
get_attribute('aria-expanded') == 'false':
|
||||
ActionChains(self.driver).\
|
||||
double_click(browser_node).perform()
|
||||
|
||||
self.page.retry_click(
|
||||
(By.XPATH, NavMenuLocators.specified_sub_node_of_pref_tree_node.
|
||||
format('Browser', 'Display')),
|
||||
(By.XPATH, NavMenuLocators.show_system_objects_pref_label_xpath))
|
||||
self.page.retry_click(
|
||||
(By.XPATH, NavMenuLocators.
|
||||
specified_sub_node_of_pref_tree_node.
|
||||
format('Browser', 'Display')),
|
||||
(By.XPATH,
|
||||
NavMenuLocators.show_system_objects_pref_label_xpath))
|
||||
|
||||
# Wait till the preference dialogue box is displayed by checking the
|
||||
# visibility of Show System Object label
|
||||
wait.until(EC.presence_of_element_located(
|
||||
(By.XPATH, NavMenuLocators.show_system_objects_pref_label_xpath))
|
||||
)
|
||||
# Wait till the preference dialogue
|
||||
# box is displayed by checking the
|
||||
# visibility of Show System Object label
|
||||
wait.until(EC.presence_of_element_located(
|
||||
(By.XPATH,
|
||||
NavMenuLocators.show_system_objects_pref_label_xpath))
|
||||
)
|
||||
|
||||
maximize_button = self.page.find_by_xpath(
|
||||
NavMenuLocators.maximize_pref_dialogue_css)
|
||||
maximize_button.click()
|
||||
maximize_button = self.page.find_by_xpath(
|
||||
NavMenuLocators.maximize_pref_dialogue_css)
|
||||
maximize_button.click()
|
||||
|
||||
specified_preference_tree_node_name = 'Query Tool'
|
||||
sql_editor = self.page.find_by_xpath(
|
||||
NavMenuLocators.specified_preference_tree_node.
|
||||
format(specified_preference_tree_node_name))
|
||||
if self.page.find_by_xpath(
|
||||
NavMenuLocators.specified_pref_node_exp_status.
|
||||
format(specified_preference_tree_node_name)).\
|
||||
get_attribute('aria-expanded') == 'false':
|
||||
ActionChains(self.driver).double_click(sql_editor).perform()
|
||||
specified_preference_tree_node_name = 'Query Tool'
|
||||
sql_editor = self.page.find_by_xpath(
|
||||
NavMenuLocators.specified_preference_tree_node.
|
||||
format(specified_preference_tree_node_name))
|
||||
if self.page.find_by_xpath(
|
||||
NavMenuLocators.specified_pref_node_exp_status.
|
||||
format(specified_preference_tree_node_name)).\
|
||||
get_attribute('aria-expanded') == 'false':
|
||||
ActionChains(self.driver).\
|
||||
double_click(sql_editor).perform()
|
||||
|
||||
option_node = \
|
||||
self.page.find_by_xpath("//*[@id='treeContainer']"
|
||||
"//div//span[text()='Editor']")
|
||||
option_node.click()
|
||||
option_node = \
|
||||
self.page.find_by_xpath("//*[@id='treeContainer']"
|
||||
"//div//span[text()='Editor']")
|
||||
option_node.click()
|
||||
|
||||
switch_box_element = self.page.find_by_xpath(
|
||||
NavMenuLocators.insert_bracket_pair_switch_btn)
|
||||
switch_box_element = self.page.find_by_xpath(
|
||||
NavMenuLocators.insert_bracket_pair_switch_btn)
|
||||
|
||||
switch_box_element.click()
|
||||
switch_box_element.click()
|
||||
|
||||
maximize_button = self.page.find_by_xpath(
|
||||
NavMenuLocators.maximize_pref_dialogue_css)
|
||||
maximize_button.click()
|
||||
time.sleep(0.5)
|
||||
maximize_button = self.page.find_by_xpath(
|
||||
NavMenuLocators.maximize_pref_dialogue_css)
|
||||
maximize_button.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
# save and close the preference dialog.
|
||||
self.page.click_modal('Save', react_dialog=True)
|
||||
# save and close the preference dialog.
|
||||
self.page.click_modal('Save', react_dialog=True)
|
||||
break
|
||||
except Exception:
|
||||
retry -= 1
|
||||
|
||||
def _create_enum_type(self):
|
||||
query = """CREATE TYPE public.rainbow AS ENUM ('red', 'orange',
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import sys
|
||||
import random
|
||||
import traceback
|
||||
|
||||
from selenium.webdriver import ActionChains
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
@@ -65,36 +66,42 @@ class QueryToolJourneyTest(BaseFeatureTest):
|
||||
self.wait = WebDriverWait(self.page.driver, 10)
|
||||
|
||||
def runTest(self):
|
||||
self._navigate_to_query_tool()
|
||||
self.page.execute_query(
|
||||
"SELECT * FROM %s ORDER BY value " % self.test_table_name)
|
||||
try:
|
||||
self._navigate_to_query_tool()
|
||||
self.page.execute_query(
|
||||
"SELECT * FROM %s ORDER BY value " % self.test_table_name)
|
||||
|
||||
print("Copy rows...", file=sys.stderr, end="")
|
||||
self._test_copies_rows()
|
||||
print(" OK.", file=sys.stderr)
|
||||
print("Copy rows...", file=sys.stderr, end="")
|
||||
self._test_copies_rows()
|
||||
print(" OK.", file=sys.stderr)
|
||||
|
||||
print("Copy columns...", file=sys.stderr, end="")
|
||||
self._test_copies_columns()
|
||||
print(" OK.", file=sys.stderr)
|
||||
print("Copy columns...", file=sys.stderr, end="")
|
||||
self._test_copies_columns()
|
||||
print(" OK.", file=sys.stderr)
|
||||
|
||||
print("History tab...", file=sys.stderr, end="")
|
||||
self._test_history_tab()
|
||||
print(" OK.", file=sys.stderr)
|
||||
print("History tab...", file=sys.stderr, end="")
|
||||
self._test_history_tab()
|
||||
print(" OK.", file=sys.stderr)
|
||||
|
||||
self._insert_data_into_test_editable_table()
|
||||
self._insert_data_into_test_editable_table()
|
||||
|
||||
print("History query source icons and generated queries toggle...",
|
||||
file=sys.stderr, end="")
|
||||
self._test_query_sources_and_generated_queries()
|
||||
print(" OK.", file=sys.stderr)
|
||||
print("History query source icons and generated queries toggle...",
|
||||
file=sys.stderr, end="")
|
||||
self._test_query_sources_and_generated_queries()
|
||||
print(" OK.", file=sys.stderr)
|
||||
|
||||
print("Updatable result sets...", file=sys.stderr, end="")
|
||||
self._test_updatable_resultset()
|
||||
print(" OK.", file=sys.stderr)
|
||||
print("Updatable result sets...", file=sys.stderr, end="")
|
||||
self._test_updatable_resultset()
|
||||
print(" OK.", file=sys.stderr)
|
||||
|
||||
print("Is editable column header icons...", file=sys.stderr, end="")
|
||||
self._test_is_editable_columns_icons()
|
||||
print(" OK.", file=sys.stderr)
|
||||
print("Is editable column header icons...",
|
||||
file=sys.stderr, end="")
|
||||
self._test_is_editable_columns_icons()
|
||||
print(" OK.", file=sys.stderr)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.assertTrue(False, 'Exception occurred in run test Tests the '
|
||||
'path through the query tool data' + str(e))
|
||||
|
||||
def _test_copies_rows(self):
|
||||
self.page.driver.switch_to.default_content()
|
||||
@@ -346,27 +353,6 @@ class QueryToolJourneyTest(BaseFeatureTest):
|
||||
|
||||
self._commit_transaction()
|
||||
self.page.wait_for_spinner_to_disappear()
|
||||
|
||||
# Turn on autocommit
|
||||
# self.page.check_execute_option("auto_commit")
|
||||
# query_options = self.page.find_by_css_selector(
|
||||
# QueryToolLocators.btn_query_dropdown)
|
||||
# query_options.click()
|
||||
# retry = 3
|
||||
# while retry > 0:
|
||||
# query_options = self.page.find_by_css_selector(
|
||||
# QueryToolLocators.btn_query_dropdown)
|
||||
# query_options.click()
|
||||
# expanded = query_options.get_attribute("aria-expanded")
|
||||
# if expanded == "false":
|
||||
# print("query option not yet expanded clicking commit again",
|
||||
# file=sys.stderr)
|
||||
# self._commit_transaction()
|
||||
# time.sleep(0.5)
|
||||
# query_options.click()
|
||||
# break
|
||||
# else:
|
||||
# retry -= 1
|
||||
self.page.check_execute_option("auto_commit")
|
||||
|
||||
def _check_history_queries_and_icons(self, history_queries, history_icons):
|
||||
@@ -396,16 +382,24 @@ class QueryToolJourneyTest(BaseFeatureTest):
|
||||
"""
|
||||
Updates a numeric cell in the first row of the resultset
|
||||
"""
|
||||
cell_el = self.page.find_by_css_selector(
|
||||
QueryToolLocators.output_row_col.format(2, 3))
|
||||
ActionChains(self.driver).double_click(cell_el).perform()
|
||||
ActionChains(self.driver).send_keys(value). \
|
||||
send_keys(Keys.ENTER).perform()
|
||||
|
||||
save_btn = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, QueryToolLocators.btn_save_data)))
|
||||
save_btn.click()
|
||||
retry = 3
|
||||
while retry > 0:
|
||||
cell_el = self.page.find_by_css_selector(
|
||||
QueryToolLocators.output_row_col.format(2, 3))
|
||||
cell_el.click()
|
||||
time.sleep(0.2)
|
||||
ActionChains(self.driver).double_click(cell_el).perform()
|
||||
ActionChains(self.driver).send_keys(value). \
|
||||
send_keys(Keys.ENTER).perform()
|
||||
try:
|
||||
save_btn = WebDriverWait(self.driver, 2).until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, QueryToolLocators.btn_save_data)))
|
||||
save_btn.click()
|
||||
break
|
||||
except Exception:
|
||||
print('Exception occurred')
|
||||
retry -= 1
|
||||
|
||||
def _insert_data_into_test_editable_table(self):
|
||||
self.page.click_tab(self.query_editor_tab_id, rc_dock=True)
|
||||
@@ -458,31 +452,46 @@ class QueryToolJourneyTest(BaseFeatureTest):
|
||||
# first column is rownum
|
||||
enumerated_should_be_editable = enumerate(cols_should_be_editable, 2)
|
||||
|
||||
import time
|
||||
time.sleep(0.5)
|
||||
for column_index, should_be_editable in enumerated_should_be_editable:
|
||||
is_editable = self._check_cell_editable(column_index)
|
||||
self.assertEqual(is_editable, should_be_editable)
|
||||
|
||||
def _check_cell_editable(self, cell_index):
|
||||
"""Checks if a cell in the first row of the resultset is editable"""
|
||||
cell_el = self.page.find_by_css_selector(
|
||||
QueryToolLocators.output_row_col.format(2, cell_index))
|
||||
|
||||
# Get existing value
|
||||
cell_value = int(cell_el.text)
|
||||
new_value = cell_value + 1
|
||||
# Try to update value
|
||||
ActionChains(self.driver).double_click(cell_el).perform()
|
||||
ActionChains(self.driver).send_keys(new_value). \
|
||||
send_keys(Keys.ENTER).perform()
|
||||
time.sleep(0.5)
|
||||
# Check if the value was updated
|
||||
# Finding element again to avoid stale element reference exception
|
||||
cell_el = self.page.\
|
||||
find_by_css_selector(QueryToolLocators.
|
||||
output_row_col.format(2, cell_index))
|
||||
return int(cell_el.text) == new_value
|
||||
retry = 2
|
||||
while retry > 0:
|
||||
try:
|
||||
cell_el = self.page.find_by_xpath(
|
||||
QueryToolLocators.output_cell_xpath.format(2, cell_index))
|
||||
# Get existing value
|
||||
cell_value = int(cell_el.text)
|
||||
new_value = cell_value + 1
|
||||
# Try to update value
|
||||
ActionChains(self.driver).double_click(cell_el).perform()
|
||||
cell_el = self.page.find_by_xpath(
|
||||
QueryToolLocators.output_cell_xpath.format(2, cell_index))
|
||||
if cell_el.get_attribute('aria-selected') and \
|
||||
cell_el.text == '':
|
||||
ActionChains(self.driver).send_keys(new_value).perform()
|
||||
ActionChains(self.driver).send_keys(Keys.ENTER).perform()
|
||||
# Check if the value was updated
|
||||
# Finding element again to avoid stale element
|
||||
# reference exception
|
||||
cell_el = self.page. \
|
||||
find_by_xpath(QueryToolLocators.
|
||||
output_cell_xpath.format(2, cell_index))
|
||||
return int(cell_el.text) == new_value
|
||||
else:
|
||||
print('Double click not succeeded in try- ' + str(
|
||||
retry), file=sys.stderr)
|
||||
retry -= 1
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
print('Exception while reading cell value in try ' +
|
||||
str(retry), file=sys.stderr)
|
||||
retry -= 1
|
||||
if retry == 0:
|
||||
raise Exception(e)
|
||||
|
||||
def _check_can_add_row(self):
|
||||
return self.page.check_if_element_exist_by_xpath(
|
||||
|
||||
@@ -118,9 +118,8 @@ class QueryToolFeatureTest(BaseFeatureTest):
|
||||
# this will set focus to correct iframe.
|
||||
self.page.fill_codemirror_area_with('')
|
||||
|
||||
self.page.retry_click(
|
||||
(By.CSS_SELECTOR, QueryToolLocators.btn_explain_options_dropdown),
|
||||
(By.CSS_SELECTOR, QueryToolLocators.btn_explain_verbose))
|
||||
self.assertTrue(self.page.open_explain_options(),
|
||||
'Unable to open Explain Options dropdown')
|
||||
|
||||
# disable Explain options and auto rollback only if they are enabled.
|
||||
for op in (QueryToolLocators.btn_explain_verbose,
|
||||
@@ -261,10 +260,8 @@ SELECT generate_series(1, 1000) as id order by id desc"""
|
||||
|
||||
self.page.fill_codemirror_area_with(query)
|
||||
time.sleep(0.5)
|
||||
explain_op_btn_click = self.page.retry_click(
|
||||
(By.CSS_SELECTOR, QueryToolLocators.btn_explain_options_dropdown),
|
||||
(By.CSS_SELECTOR, QueryToolLocators.btn_explain_verbose))
|
||||
self.assertTrue(explain_op_btn_click, 'Explain Op button click failed')
|
||||
self.assertTrue(self.page.open_explain_options(),
|
||||
'Unable to open Explain Options dropdown')
|
||||
|
||||
# disable Explain options and auto rollback only if they are enabled.
|
||||
for op in (QueryToolLocators.btn_explain_verbose,
|
||||
@@ -298,10 +295,8 @@ SELECT generate_series(1, 1000) as id order by id desc"""
|
||||
|
||||
self.page.fill_codemirror_area_with(query)
|
||||
|
||||
explain_op_btn_click = self.page.retry_click(
|
||||
(By.CSS_SELECTOR, QueryToolLocators.btn_explain_options_dropdown),
|
||||
(By.CSS_SELECTOR, QueryToolLocators.btn_explain_verbose))
|
||||
self.assertTrue(explain_op_btn_click, 'Explain Op button click failed')
|
||||
self.assertTrue(self.page.open_explain_options(),
|
||||
'Unable to open Explain Options')
|
||||
|
||||
# disable Explain options and auto rollback only if they are enabled.
|
||||
for op in (QueryToolLocators.btn_explain_buffers,
|
||||
@@ -684,9 +679,8 @@ SELECT 1, pg_sleep(300)"""
|
||||
self.page.fill_codemirror_area_with(
|
||||
"SELECT count(*) FROM pg_catalog.pg_class;")
|
||||
|
||||
self.page.retry_click(
|
||||
(By.CSS_SELECTOR, QueryToolLocators.btn_explain_options_dropdown),
|
||||
(By.CSS_SELECTOR, QueryToolLocators.btn_explain_verbose))
|
||||
self.assertTrue(self.page.open_explain_options(),
|
||||
'Unable to open Explain Options')
|
||||
|
||||
# disable Explain options and only enable COST option
|
||||
for op in (QueryToolLocators.btn_explain_verbose,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"comment": {
|
||||
"sample_test_case": {
|
||||
"colno": ["value to set", "value to verfiy", "data type"]
|
||||
"colno": ["value to set", "value to verify", "data type"]
|
||||
}
|
||||
},
|
||||
"table_insert_update_cases": {
|
||||
@@ -16,9 +16,9 @@
|
||||
"8": ["", "[null]", "text"],
|
||||
"9": ["", "[51, 52]", "json"],
|
||||
"10": ["[61,62]", "[61,62]", "json"],
|
||||
"11": ["", "true", "bool"],
|
||||
"11": ["true", "true", "bool"],
|
||||
"12": ["", "[null]", "bool"],
|
||||
"13": ["", "false", "bool"],
|
||||
"13": ["false", "false", "bool"],
|
||||
"14": ["", "[null]", "text[]"],
|
||||
"15": ["{}", "{}", "text[]"],
|
||||
"16": ["{data,NULL,'',\"\"}", "{data,NULL,'',\"\"}", "text[]"],
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from selenium.webdriver import ActionChains
|
||||
from regression.python_test_utils import test_utils
|
||||
@@ -52,7 +54,7 @@ class CheckForViewDataTest(BaseFeatureTest):
|
||||
dict())
|
||||
]
|
||||
|
||||
TIMEOUT_STRING = "Timed out waiting for div element to appear"
|
||||
TIMEOUT_STRING = "5"
|
||||
|
||||
# query for creating 'defaults_text' table
|
||||
defaults_query = """
|
||||
@@ -119,22 +121,29 @@ CREATE TABLE public.nonintpkey
|
||||
connection.close()
|
||||
|
||||
def runTest(self):
|
||||
self.page.wait_for_spinner_to_disappear()
|
||||
self.page.add_server(self.server)
|
||||
self.page.expand_tables_node("Server", self.server['name'],
|
||||
self.server['db_password'], self.test_db,
|
||||
'public')
|
||||
try:
|
||||
self.page.wait_for_spinner_to_disappear()
|
||||
self.page.add_server(self.server)
|
||||
self.page.expand_tables_node("Server", self.server['name'],
|
||||
self.server['db_password'],
|
||||
self.test_db,
|
||||
'public')
|
||||
|
||||
self._load_config_data('table_insert_update_cases')
|
||||
data_local = config_data
|
||||
# iterate on both tables
|
||||
for cnt in (1, 2):
|
||||
self._perform_test_for_table('defaults_{0}'.format(str(cnt)),
|
||||
data_local)
|
||||
# test nonint pkey table
|
||||
self._load_config_data('table_insert_update_nonint')
|
||||
data_local = config_data
|
||||
self._perform_test_for_table('nonintpkey', data_local)
|
||||
self._load_config_data('table_insert_update_cases')
|
||||
data_local = config_data
|
||||
# iterate on both tables
|
||||
for cnt in (1, 2):
|
||||
self._perform_test_for_table('defaults_{0}'.format(str(cnt)),
|
||||
data_local)
|
||||
# test nonint pkey table
|
||||
self._load_config_data('table_insert_update_nonint')
|
||||
data_local = config_data
|
||||
self._perform_test_for_table('nonintpkey', data_local)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
self.assertTrue(False, 'Exception occurred in run test '
|
||||
'Validate Insert, Update operations in '
|
||||
'View/Edit data with given test data')
|
||||
|
||||
def after(self):
|
||||
self.page.remove_server(self.server)
|
||||
@@ -153,9 +162,6 @@ CREATE TABLE public.nonintpkey
|
||||
config_data = config_data_json[config_key]
|
||||
|
||||
def _perform_test_for_table(self, table_name, config_data_local):
|
||||
# self.page.click_a_tree_node(
|
||||
# table_name,
|
||||
# TreeAreaLocators.sub_nodes_of_tables_node)
|
||||
table_node = self.page.check_if_element_exists_with_scroll(
|
||||
TreeAreaLocators.table_node(table_name))
|
||||
table_node.click()
|
||||
@@ -212,67 +218,125 @@ CREATE TABLE public.nonintpkey
|
||||
Returns: None
|
||||
|
||||
"""
|
||||
self.wait.until(EC.visibility_of_element_located(
|
||||
(By.XPATH, xpath)), CheckForViewDataTest.TIMEOUT_STRING
|
||||
)
|
||||
cell_el = self.page.find_by_xpath(xpath)
|
||||
self.page.driver.execute_script("arguments[0].scrollIntoView(false)",
|
||||
cell_el)
|
||||
ActionChains(self.driver).move_to_element(cell_el).double_click(
|
||||
cell_el
|
||||
).perform()
|
||||
cell_type = data[2]
|
||||
value = data[0]
|
||||
retry = 4
|
||||
while retry > 0:
|
||||
self.wait.until(EC.visibility_of_element_located(
|
||||
(By.XPATH, xpath)), CheckForViewDataTest.TIMEOUT_STRING
|
||||
)
|
||||
cell_el = self.page.find_by_xpath(xpath)
|
||||
self.page.driver.execute_script(
|
||||
"arguments[0].scrollIntoView(false)", cell_el)
|
||||
time.sleep(0.2)
|
||||
ActionChains(self.driver).move_to_element(cell_el).perform()
|
||||
ActionChains(self.driver).double_click(cell_el).perform()
|
||||
|
||||
if cell_type in ['int', 'int[]']:
|
||||
if value == 'clear':
|
||||
cell_el.find_element(By.CSS_SELECTOR, 'input').clear()
|
||||
if cell_type in ['int', 'int[]'] and \
|
||||
self._update_numeric_cell(xpath, value):
|
||||
break
|
||||
elif cell_type in ['text', 'text[]', 'boolean[]'] and \
|
||||
self._update_text_cell(cell_el, value):
|
||||
break
|
||||
elif cell_type in ['json', 'jsonb'] and \
|
||||
self._update_json_cell(cell_el, value):
|
||||
retry = 0
|
||||
elif cell_type in ['bool'] and \
|
||||
self._update_boolean_cell(cell_el, value):
|
||||
retry = 0
|
||||
else:
|
||||
ActionChains(self.driver).send_keys(value). \
|
||||
send_keys(Keys.ENTER).perform()
|
||||
elif cell_type in ['text', 'text[]', 'boolean[]']:
|
||||
text_area_ele = WebDriverWait(self.driver, 5).until(
|
||||
print('Unable to update cell in try ' + str(retry),
|
||||
file=sys.stderr)
|
||||
retry -= 1
|
||||
|
||||
def _update_numeric_cell(self, cell_el_xpath, value):
|
||||
try:
|
||||
cell_el = self.page.find_by_xpath(cell_el_xpath)
|
||||
if cell_el.get_attribute('aria-selected') and \
|
||||
cell_el.text == '':
|
||||
if value == 'clear':
|
||||
cell_el.find_element(By.CSS_SELECTOR, 'input').clear()
|
||||
else:
|
||||
ActionChains(self.driver).send_keys(value).perform()
|
||||
ActionChains(self.driver).send_keys(Keys.TAB).perform()
|
||||
return True
|
||||
else:
|
||||
print('Cell is NOT selected yet.', file=sys.stderr)
|
||||
return False
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print('Exception occurred while updating int cell',
|
||||
file=sys.stderr)
|
||||
return False
|
||||
|
||||
def _update_text_cell(self, cell_el, value):
|
||||
try:
|
||||
text_area_ele = WebDriverWait(self.driver, 3).until(
|
||||
EC.visibility_of_element_located(
|
||||
(By.CSS_SELECTOR,
|
||||
QueryToolLocators.row_editor_text_area_css)))
|
||||
text_area_ele.clear()
|
||||
time.sleep(0.3)
|
||||
text_area_ele.click()
|
||||
text_area_ele.send_keys(value)
|
||||
# Click on editor's Save button
|
||||
self.page.find_by_css_selector(
|
||||
QueryToolLocators.text_editor_ok_btn_css).click()
|
||||
elif cell_type in ['json', 'jsonb']:
|
||||
jsoneditor_area_ele = self.page.find_by_css_selector(
|
||||
QueryToolLocators.json_editor_text_area_css)
|
||||
platform = 'mac'
|
||||
if "platform" in self.driver.capabilities:
|
||||
platform = (self.driver.capabilities["platform"]).lower()
|
||||
elif "platformName" in self.driver.capabilities:
|
||||
platform = (self.driver.capabilities["platformName"]).lower()
|
||||
if 'mac' in platform:
|
||||
key_to_press = Keys.COMMAND
|
||||
else:
|
||||
key_to_press = Keys.CONTROL
|
||||
actions = ActionChains(self.driver)
|
||||
# actions.move_to_element(jsoneditor_area_ele).click().perform()
|
||||
actions.key_down(key_to_press).send_keys('a').key_up(key_to_press)\
|
||||
.send_keys(Keys.DELETE).perform()
|
||||
actions.send_keys(value) .perform()
|
||||
ActionChains(self.driver).send_keys(value).perform()
|
||||
# Click on editor's Save button
|
||||
self.page.find_by_css_selector(
|
||||
QueryToolLocators.text_editor_ok_btn_css).click()
|
||||
return True
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print('Exception occurred while updating text cell ',
|
||||
file=sys.stderr)
|
||||
return False
|
||||
|
||||
def _update_json_cell(self, cell_el, value):
|
||||
platform = 'mac'
|
||||
if "platform" in self.driver.capabilities:
|
||||
platform = (self.driver.capabilities["platform"]).lower()
|
||||
elif "platformName" in self.driver.capabilities:
|
||||
platform = (self.driver.capabilities["platformName"]).lower()
|
||||
if 'mac' in platform:
|
||||
key_to_press = Keys.COMMAND
|
||||
else:
|
||||
# Boolean editor test for to True click
|
||||
key_to_press = Keys.CONTROL
|
||||
try:
|
||||
WebDriverWait(self.driver, 3).until(
|
||||
EC.visibility_of_element_located(
|
||||
(By.CSS_SELECTOR,
|
||||
QueryToolLocators.json_editor_text_area_css)))
|
||||
actions = ActionChains(self.driver)
|
||||
actions.key_down(key_to_press).send_keys('a').\
|
||||
key_up(key_to_press).send_keys(Keys.DELETE).perform()
|
||||
actions.send_keys(value).perform()
|
||||
# Click on editor's Save button
|
||||
self.page.find_by_css_selector(
|
||||
QueryToolLocators.text_editor_ok_btn_css).click()
|
||||
return True
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print('Exception occurred while updating json cell ',
|
||||
file=sys.stderr)
|
||||
return False
|
||||
|
||||
def _update_boolean_cell(self, cell_el, value):
|
||||
# Boolean editor test for to True click
|
||||
try:
|
||||
checkbox_el = self.page.find_by_css_selector(
|
||||
QueryToolLocators.row_editor_checkbox_css)
|
||||
if data[1] == 'true':
|
||||
if value == 'true':
|
||||
checkbox_el.click()
|
||||
# Boolean editor test for to False click
|
||||
elif data[1] == 'false':
|
||||
elif value == 'false':
|
||||
# Sets true
|
||||
checkbox_el.click()
|
||||
# Sets false
|
||||
ActionChains(self.driver).click(checkbox_el).perform()
|
||||
return True
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
print('Exception occurred while updating boolean cell',
|
||||
file=sys.stderr)
|
||||
return False
|
||||
|
||||
def _view_data_grid(self, table_name):
|
||||
self.page.driver.find_element(By.LINK_TEXT, "Object").click()
|
||||
@@ -327,9 +391,9 @@ CREATE TABLE public.nonintpkey
|
||||
items.sort(reverse=False)
|
||||
for idx in items:
|
||||
# rowindex starts with 2 and 1st colindex is rownum
|
||||
time.sleep(0.2)
|
||||
cell_xpath = CheckForViewDataTest\
|
||||
._get_cell_xpath(str(idx + 1), row + 1)
|
||||
time.sleep(0.5)
|
||||
self._update_cell(cell_xpath, data[str(idx)])
|
||||
self.page.find_by_css_selector(
|
||||
QueryToolLocators.btn_save_data).click()
|
||||
@@ -359,7 +423,6 @@ CREATE TABLE public.nonintpkey
|
||||
"document.querySelector('.rdg').scrollLeft=0"
|
||||
)
|
||||
|
||||
xpath = QueryToolLocators.output_row_xpath.format(2)
|
||||
scroll_on_arg_for_js = "arguments[0].scrollIntoView(false)"
|
||||
|
||||
self.page.wait_for_query_tool_loading_indicator_to_disappear()
|
||||
@@ -383,7 +446,7 @@ CREATE TABLE public.nonintpkey
|
||||
except Exception:
|
||||
print("stale reference exception at id:", idx)
|
||||
retry -= 1
|
||||
time.sleep(0.4)
|
||||
time.sleep(0.4)
|
||||
self.assertEqual(element.text, config_check_data[str(idx)][1])
|
||||
|
||||
# scroll browser back to the left
|
||||
|
||||
@@ -29,22 +29,6 @@ MODULE_NAME = 'misc'
|
||||
class MiscModule(PgAdminModule):
|
||||
LABEL = gettext('Miscellaneous')
|
||||
|
||||
def get_own_javascripts(self):
|
||||
return [
|
||||
{
|
||||
'name': 'pgadmin.misc.explain',
|
||||
'path': url_for('misc.index') + 'explain/explain',
|
||||
'preloaded': False
|
||||
}, {
|
||||
'name': 'snap.svg',
|
||||
'path': url_for(
|
||||
'misc.static', filename='explain/vendor/snap.svg/' + (
|
||||
'snap.svg' if config.DEBUG else 'snap.svg-min'
|
||||
)),
|
||||
'preloaded': False
|
||||
}
|
||||
]
|
||||
|
||||
def get_own_stylesheets(self):
|
||||
stylesheets = []
|
||||
return stylesheets
|
||||
@@ -110,6 +94,34 @@ class MiscModule(PgAdminModule):
|
||||
return ['misc.ping', 'misc.index', 'misc.cleanup',
|
||||
'misc.validate_binary_path']
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Override the default register function to automagically register
|
||||
sub-modules at once.
|
||||
"""
|
||||
from .bgprocess import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .cloud import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .dependencies import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .dependents import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .file_manager import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .sql import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
from .statistics import blueprint as module
|
||||
self.submodules.append(module)
|
||||
|
||||
super(MiscModule, self).register(app, options)
|
||||
|
||||
|
||||
# Initialise the module
|
||||
blueprint = MiscModule(MODULE_NAME, __name__)
|
||||
|
||||