Compare commits

..

278 Commits

Author SHA1 Message Date
Jan Prochazka a75e463ef5 v5.1.0 2022-08-08 20:20:31 +02:00
Jan Prochazka 7eb59ad3a0 changelog - docs link 2022-08-08 19:50:07 +02:00
Jan Prochazka 7a9f8a460f v5.1.0-beta.4 2022-08-08 19:47:03 +02:00
Jan Prochazka 289752c023 Merge branch 'develop' 2022-08-08 19:46:39 +02:00
Jan Prochazka 98f2c06c21 perspectives: default column algorithm 2022-08-08 19:46:12 +02:00
Jan Prochazka 530b1cade3 perspective UX 2022-08-08 19:40:50 +02:00
Jan Prochazka 65aa8fb4e3 fixed multi - database structure store 2022-08-08 19:37:40 +02:00
Jan Prochazka 4c0f17a0b2 changelog 2022-08-07 20:35:04 +02:00
Jan Prochazka e4371c526b v5.1.0-beta.3 2022-08-07 20:29:04 +02:00
Jan Prochazka e39f0a1f4b perspective parent filter fix 2022-08-07 20:28:41 +02:00
Jan Prochazka 842f77d02b v5.1.0-beta.2 2022-08-07 19:37:04 +02:00
Jan Prochazka 2571e6ac7e fixed tests 2022-08-07 19:36:37 +02:00
Jan Prochazka 1599a7ea01 v5.1.0-beta.1 2022-08-07 19:30:25 +02:00
Jan Prochazka cb1d81b586 perspectives: parent filter switch in filters 2022-08-07 19:30:00 +02:00
Jan Prochazka 339588b8a0 parent filter implementation 2022-08-07 19:16:23 +02:00
Jan Prochazka 1731b7e4a3 parentFilter - declarative support 2022-08-07 18:12:10 +02:00
Jan Prochazka 5418bb932c removed commented code 2022-08-07 16:49:16 +02:00
Jan Prochazka 6154b4c780 perspective:removed filterInfos,using only filters 2022-08-07 16:48:27 +02:00
Jan Prochazka 3f9bd100e1 perspective refactor 2022-08-07 16:40:37 +02:00
Jan Prochazka b5c6ddce59 perspective: filter this value, open filtered table 2022-08-07 16:03:20 +02:00
Jan Prochazka 51c72efb34 fixed NTLM auth in SQL server #305 2022-08-07 15:13:10 +02:00
Jan Prochazka 00df20e350 imrpoved editing data with keyboard #331 2022-08-07 12:01:02 +02:00
Jan Prochazka f3a7e3af74 option to skip table save confirmation #329 2022-08-07 11:21:08 +02:00
Jan Prochazka 04c37c2b4f v5.0.10-beta.14 2022-08-07 10:42:39 +02:00
Jan Prochazka 12df0993c0 fixed sqlite3 native module version 2022-08-07 10:42:27 +02:00
Jan Prochazka ac3ec5c11e #324 fixed column type syntax for mysql 2022-08-07 10:17:32 +02:00
Jan Prochazka b565e981e4 v5.0.10-beta.13 2022-08-07 10:05:17 +02:00
Jan Prochazka f7ada698e4 fix 2022-08-07 10:04:32 +02:00
Jan Prochazka bc4c146389 remove msnodesqlv8 from mac+linux build 2022-08-07 10:02:11 +02:00
Jan Prochazka 7c80ca1374 Revert "try to fix mac build"
This reverts commit 1974243ed5.
2022-08-07 09:52:59 +02:00
Jan Prochazka 8c5cc7dcc1 v5.0.10-beta.12 2022-08-07 09:03:18 +02:00
Jan Prochazka 1974243ed5 try to fix mac build 2022-08-07 09:02:51 +02:00
Jan Prochazka 71c9071cb8 default action on connection click: connect #332 2022-08-07 08:44:31 +02:00
Jan Prochazka c28e55132a v5.0.10-beta.11 2022-08-07 08:28:38 +02:00
Jan Prochazka 2b2a4debd4 sqlite prebuild for mac 2022-08-07 08:28:21 +02:00
Jan Prochazka 563a35560b save perspective to file 2022-08-06 17:43:49 +02:00
Jan Prochazka cc019281d4 perspective custom joins supports views 2022-08-06 17:03:48 +02:00
Jan Prochazka 86d7d61cc5 perspectives: custom join over different databases 2022-08-06 16:44:37 +02:00
Jan Prochazka aff1fe0b3d perspectives: prefer not circular lookups 2022-08-06 15:30:49 +02:00
Jan Prochazka 137631b5b5 sort references 2022-08-06 14:37:00 +02:00
Jan Prochazka 090ffa064d perspective: open table ctx menu 2022-08-06 14:05:18 +02:00
Jan Prochazka f77cc1023b perspective column filter 2022-08-06 13:49:05 +02:00
Jan Prochazka c6dbb31748 perspective filters supports lookup 2022-08-06 13:24:51 +02:00
Jan Prochazka ae6c486db5 perspective load fix 2022-08-06 11:37:44 +02:00
Jan Prochazka 9a2c12d558 perspective context menu 2022-08-05 20:55:04 +02:00
Jan Prochazka 1ed01e9839 perspective cell highlight 2022-08-05 20:17:49 +02:00
Jan Prochazka 25d2c129cd perspective ctx menu 2022-08-05 19:55:14 +02:00
Jan Prochazka 7dc7af0cdb v5.0.10-beta.10 2022-08-04 21:32:20 +02:00
Jan Prochazka 80fea3b01b style 2022-08-04 21:32:06 +02:00
Jan Prochazka 97dc92e413 perspectives:add to filter ctx menu 2022-08-04 08:26:35 +02:00
Jan Prochazka 9051ba2ee1 filter list in perspective 2022-08-04 08:16:22 +02:00
Jan Prochazka 7dcbe6c7c1 v5.0.10-beta.9 2022-08-03 20:51:22 +02:00
Jan Prochazka e6fe8a6379 persp: fixed loading 2022-08-03 20:31:29 +02:00
Jan Prochazka b793e4131d perspectives: scroll optimalization 2022-08-03 20:23:36 +02:00
Jan Prochazka b737eaac13 v5.0.10-beta.8 2022-07-31 21:10:14 +02:00
Jan Prochazka cb5cce2ea3 custom joins working on the same DB 2022-07-31 21:09:58 +02:00
Jan Prochazka b05d260caa perspective custom join editing & removing 2022-07-31 20:53:22 +02:00
Jan Prochazka 091e91556d custom join dialog 2022-07-31 20:09:48 +02:00
Jan Prochazka 2b4120435b perspectives support views 2022-07-31 16:56:09 +02:00
Jan Prochazka c8d031e2c4 removed debug code 2022-07-31 16:10:57 +02:00
Jan Prochazka ac07b7e1ba code cleanup 2022-07-31 15:30:06 +02:00
Jan Prochazka bf51f45934 removed perspective intersection observer 2022-07-31 15:28:04 +02:00
Jan Prochazka fe31cfb552 css fix 2022-07-31 12:28:08 +02:00
Jan Prochazka d505be09ca perspectives - sort 2022-07-31 12:24:06 +02:00
Jan Prochazka 44668b8017 perspective sort - divided by table 2022-07-31 12:22:13 +02:00
Jan Prochazka 452dba7f32 perspective sorting 2022-07-31 12:10:56 +02:00
Jan Prochazka 7694864fe7 perspective loading indicator 2022-07-31 10:12:22 +02:00
Jan Prochazka 37d5c6fbf9 cell data formatting in perspectives 2022-07-31 09:40:20 +02:00
Jan Prochazka 802f231e43 ts fix 2022-07-31 08:52:10 +02:00
Jan Prochazka 53c39e6a43 run perspectives test on CI 2022-07-31 08:49:25 +02:00
Jan Prochazka 65f550023a removed obsolete code 2022-07-31 08:47:32 +02:00
Jan Prochazka abe7a20960 perspective - changed display table algorithm 2022-07-31 08:46:24 +02:00
Jan Prochazka d686206fe2 perspective display test WIP 2022-07-31 08:11:04 +02:00
Jan Prochazka 27b2fdb507 Copy as JSON in JSON tab 2022-07-31 08:10:19 +02:00
Jan Prochazka 88f522084d show table name in perspective 2022-07-30 09:21:04 +02:00
Jan Prochazka 8472c8be79 fixed filter parser test (upgraded jestm, ts-jest) 2022-07-30 08:39:35 +02:00
Jan Prochazka 03f8a93dd0 perspective fix 2022-07-30 08:16:07 +02:00
Jan Prochazka 2889f79120 v5.0.10-beta.7 2022-07-30 08:08:12 +02:00
Jan Prochazka 8a312181a3 upgraded all dependencies 2022-07-30 08:07:23 +02:00
Jan Prochazka e7236de078 v5.0.10-beta.6 2022-07-30 07:35:44 +02:00
Jan Prochazka 1fe2269b11 upgraded better-sqlite3 2022-07-30 07:35:34 +02:00
Jan Prochazka 10ea8ca3a6 v5.0.10-beta.5 2022-07-29 21:32:54 +02:00
Jan Prochazka 491d24984d upgraded electron to v17 2022-07-29 21:31:45 +02:00
Jan Prochazka b0279dd315 display fix 2022-07-29 21:16:07 +02:00
Jan Prochazka 9d6b581809 remove commented code 2022-07-29 21:08:50 +02:00
Jan Prochazka 3f748df1ec perspective: fixed some table scenarios 2022-07-29 21:04:09 +02:00
Jan Prochazka 7ca835765c v5.0.10-beta.4 2022-07-28 21:41:01 +02:00
Jan Prochazka a76530155d filter child tables 2022-07-28 21:02:24 +02:00
Jan Prochazka 96b82b690e v5.0.10-beta.3 2022-07-28 20:43:26 +02:00
Jan Prochazka d3a40e52fc perspective defaults - FK columns 2022-07-28 20:43:03 +02:00
Jan Prochazka 513b2ba42f default checked columns 2022-07-28 20:35:39 +02:00
Jan Prochazka d23371f642 ref loading ref column 2022-07-28 20:03:48 +02:00
Jan Prochazka 5ac6e12c3e v5.0.10-beta.2 2022-07-28 19:24:26 +02:00
Jan Prochazka 4468c0ed3b fix perspective refresh 2022-07-28 19:23:17 +02:00
Jan Prochazka 06bd9bcabe perspective - show error, ability to reset filters 2022-07-28 18:57:15 +02:00
Jan Prochazka 66d15abcab last row render fix 2022-07-28 18:46:33 +02:00
Jan Prochazka 3bdb5c0152 perspective filters 2022-07-25 21:42:01 +02:00
Jan Prochazka f504283002 use position:sticky for table header 2022-07-25 20:48:33 +02:00
Jan Prochazka f07c7909ef fix 2022-07-25 20:37:15 +02:00
Jan Prochazka c809f58349 v5.0.10-beta.1 2022-07-24 21:25:06 +02:00
Jan Prochazka 3e91ecd141 perspective filters control 2022-07-24 21:17:52 +02:00
Jan Prochazka 857185a78b Merge branch 'master' into develop 2022-07-24 21:10:19 +02:00
Jan Prochazka c189c12cae changelog 2022-07-24 21:07:02 +02:00
Jan Prochazka 96106e6aac refresh perspective command 2022-07-24 15:56:29 +02:00
Jan Prochazka 088ca231f3 uniqe binding values 2022-07-24 15:34:26 +02:00
Jan Prochazka 5395d1343b nested incomplete loading fix 2022-07-24 15:16:02 +02:00
Jan Prochazka d48c34a4a5 perspctives: nested incremental loading 2022-07-24 14:23:56 +02:00
Jan Prochazka 53ee1d87c2 Merge branch 'master' into develop 2022-07-24 10:17:47 +02:00
Jan Prochazka b5d97c8181 v5.0.9 2022-07-24 10:17:17 +02:00
Jan Prochazka 28e06166e0 perspectives - prepare for nested incremental load 2022-07-21 18:10:43 +02:00
Jan Prochazka 8f1343bc42 perspectives: fixed incremental loading 2022-07-21 17:14:27 +02:00
Jan Prochazka 2080a23b69 incremental loading 2022-07-21 17:05:07 +02:00
Jan Prochazka d71294621b perspective cache - basic design 2022-07-21 15:43:17 +02:00
Jan Prochazka 0f6ec420d2 delete commented code 2022-07-21 12:33:44 +02:00
Jan Prochazka 35152a2796 perspective loader class 2022-07-21 12:33:29 +02:00
Jan Prochazka 1abfab950e perspectives: added data provider layer 2022-07-21 11:26:44 +02:00
Jan Prochazka 6e6d0bb616 Merge branch 'master' into develop 2022-07-21 09:33:26 +02:00
Jan Prochazka 93e264e9ec v5.0.9-beta.1 2022-07-21 09:33:11 +02:00
Jan Prochazka 29257f9bf9 added missing dependency 2022-07-21 09:32:03 +02:00
Jan Prochazka 8dd90ce5e4 fixed problem with SSE #323 2022-07-21 09:31:58 +02:00
Jan Prochazka f2f7421971 new diagram, new query design added to menu 2022-07-21 09:31:52 +02:00
Jan Prochazka 8a10beef52 added missing dependency 2022-07-21 09:31:02 +02:00
Jan Prochazka df33b43e90 fixed problem with SSE #323 2022-07-21 07:49:55 +02:00
Jan Prochazka 153cba3779 new diagram, new query design added to menu 2022-07-21 07:27:28 +02:00
Jan Prochazka 8f110355c4 Merge branch 'master' into develop 2022-07-18 22:46:31 +02:00
Jan Prochazka b570f873fe changelog 2022-07-18 20:51:38 +02:00
Jan Prochazka c07e26c036 v5.0.8 2022-07-18 20:46:40 +02:00
Jan Prochazka 995bc6f16a v5.0.8-beta.4 2022-07-17 19:16:27 +02:00
Jan Prochazka 5b4339889f OR in group filter 2022-07-17 19:15:42 +02:00
Jan Prochazka ae963d7a3b v5.0.8-beta.3 2022-07-17 17:28:17 +02:00
Jan Prochazka c426cd825f configurable editor font #308 2022-07-17 17:26:58 +02:00
Jan Prochazka 62c2b3f5f4 settings could be set from env variables #304 2022-07-17 16:57:56 +02:00
Jan Prochazka ab3584dc23 v5.0.8-beta.2 2022-07-17 10:07:13 +02:00
Jan Prochazka 3a5301af6b permissions for connections 2022-07-17 10:03:17 +02:00
Jan Prochazka 55efdef181 v5.0.8-beta.1 2022-07-17 08:51:42 +02:00
Jan Prochazka e9ea1edd21 fixed adding tables to empty designer 2022-07-17 08:51:02 +02:00
Jan Prochazka d9b91f2122 or conditions in query designer #321 2022-07-17 08:38:27 +02:00
Jan Prochazka 15da5fb95e table control - noCellPadding option 2022-07-17 07:59:16 +02:00
Jan Prochazka d563a40d0f qury designer - improved style 2022-07-16 21:03:08 +02:00
Jan Prochazka a4e5630f89 custom expressions in query designer #306 2022-07-16 20:53:11 +02:00
Jan Prochazka c368ad8d54 support specifying windows domain #305 2022-07-16 11:41:08 +02:00
Jan Prochazka 01d1f08597 changelog 2022-07-16 07:28:43 +02:00
Jan Prochazka 8c934355ab v5.0.7 2022-07-16 07:21:54 +02:00
Jan Prochazka c6e3b52bc6 v5.0.7-beta.5 2022-07-16 07:21:35 +02:00
Jan Prochazka e117caf708 typo 2022-07-16 07:21:23 +02:00
Jan Prochazka 2b4d5c026e Merge branch 'master' into develop 2022-07-14 21:26:07 +02:00
Jan Prochazka 93a736f1f8 v5.0.7-beta.4 2022-07-14 19:22:53 +02:00
Jan Prochazka 1f8ef8e20e fixed changing editor theme #300 2022-07-14 19:22:37 +02:00
Jan Prochazka bef8cdbee4 removed log 2022-07-14 16:50:00 +02:00
Jan Prochazka 763391e73b datagrid: clone rows #309 2022-07-14 16:27:36 +02:00
Jan Prochazka b1cd16b095 v5.0.7-beta.3 2022-07-14 15:25:49 +02:00
Jan Prochazka 2ee1b3105f #315 ssh2 client upgrade 2022-07-14 15:25:22 +02:00
Jan Prochazka 51fa652851 v5.0.7-beta.2 2022-07-14 14:21:07 +02:00
Jan Prochazka 755781bca6 trust server certificate option #305 2022-07-14 14:03:50 +02:00
Jan Prochazka 1a90729f66 date time interval filtering #311 2022-07-14 08:51:31 +02:00
Jan Prochazka 9e520e04b2 refactor: datetime filter parsed extracted 2022-07-14 07:57:06 +02:00
Jan Prochazka ded0c8398c mongo better UX 2022-07-14 07:31:11 +02:00
Jan Prochazka dc31552f9e fixed read mongo query #312 2022-07-14 07:27:38 +02:00
Jan Prochazka e0376a708c ssh tunnel logging #315 2022-07-14 05:38:59 +02:00
Jan Prochazka 1becb89ff0 code format 2022-07-14 05:10:11 +02:00
Jan Prochazka 4d7365828e perspective - styles 2022-07-01 14:54:41 +02:00
Jan Prochazka 29ccb09ba6 perspectives: loading only neccessary columns 2022-07-01 10:24:35 +02:00
Jan Prochazka eadd3feba0 fixed header fixes 2022-07-01 09:55:54 +02:00
Jan Prochazka 93269fe314 perspectives: fixed table header 2022-07-01 08:00:21 +02:00
Jan Prochazka 34ca4c501a fixed table header 2022-06-30 21:46:45 +02:00
Jan Prochazka 34084d0e94 perspective styling 2022-06-30 21:14:56 +02:00
Jan Prochazka 07fc551383 perspective row spans 2022-06-30 21:01:27 +02:00
Jan Prochazka b0eed05a1a perspective rows 2022-06-30 19:13:01 +02:00
Jan Prochazka 8228afd725 perspective rows WIP 2022-06-30 18:10:50 +02:00
Jan Prochazka 301222d118 perspectives: show nested columns 2022-06-30 10:38:03 +02:00
Jan Prochazka 9b741b415a v5.0.7-beta.1 2022-06-30 09:09:54 +02:00
Jan Prochazka cc8438ef66 configurable auto refresh inteval 2022-06-30 08:57:20 +02:00
Jan Prochazka 179bd1f6b1 table data auto refresh 2022-06-30 08:53:01 +02:00
Jan Prochazka 08b7b1870c refresh with ctrl+r #303 2022-06-30 07:40:36 +02:00
Jan Prochazka 2c7da1d3f8 changelog 2022-06-27 20:26:20 +02:00
Jan Prochazka 2a8a2c8652 v5.0.6 2022-06-27 20:22:48 +02:00
Jan Prochazka b6b75f0743 perspectives WIP 2022-06-23 16:50:56 +02:00
Jan Prochazka aca92f3889 perspectives: render simple table 2022-06-23 16:04:05 +02:00
Jan Prochazka 4672540f82 Merge branch 'master' into develop 2022-06-23 15:33:14 +02:00
Jan Prochazka 261cec7ec2 v5.0.6-beta.6 2022-06-23 14:58:41 +02:00
Jan Prochazka de444e8485 changed table ctx menu for redonly connections 2022-06-23 14:58:26 +02:00
Jan Prochazka f4fb92be91 v5.0.6-beta.5 2022-06-23 14:42:28 +02:00
Jan Prochazka 571c928234 handler script errors 2022-06-23 14:32:34 +02:00
Jan Prochazka 2fcc4b1ff0 perspectives WIP 2022-06-23 14:24:06 +02:00
Jan Prochazka c0b0ca22aa Merge branch 'master' of https://github.com/dbgate/dbgate 2022-06-23 14:23:53 +02:00
Jan Prochazka d862762758 runscript WIP 2022-06-23 14:23:46 +02:00
Jan Prochazka 7ca8880c3c Merge branch 'master' into develop 2022-06-23 11:18:36 +02:00
Jan Prochazka 21ccc55e3f v5.0.6-beta.4 2022-06-23 10:44:25 +02:00
Jan Prochazka 8662353071 removed incorrent readonly check 2022-06-23 10:42:12 +02:00
Jan Prochazka faedcfa64d v5.0.6-beta.3 2022-06-23 10:09:05 +02:00
Jan Prochazka 7ad1796db5 filtering by XML columsn in MSSQL 2022-06-23 10:06:09 +02:00
Jan Prochazka 717ec5293b Merge branch 'master' of https://github.com/dbgate/dbgate 2022-06-23 09:57:50 +02:00
Jan Prochazka d437e171fb fixed connection tab styling 2022-06-23 09:57:36 +02:00
Jan Prochazka 97ae7ae0d6 filtering works for complex columns 2022-06-23 09:52:41 +02:00
Jan Prochazka e9a8f3ee84 ability to reset view when grid load error occurs 2022-06-23 09:08:43 +02:00
Jan Prochazka 1fb237417a v5.0.6-alpha.2 2022-06-23 08:49:39 +02:00
Jan Prochazka cd65fa16ed fixes 2022-06-23 08:48:27 +02:00
Jan Prochazka 1e5a740a52 upgraded mongodb driver 2022-06-23 08:32:12 +02:00
Jan Prochazka 42badf17eb perspective - load hiearchic JSON 2022-06-20 22:14:48 +02:00
Jan Prochazka 2ec3c2c24f perspective tre shows dependencies 2022-06-18 08:46:40 +02:00
Jan Prochazka f3ab06d3b8 refactor 2022-06-18 08:00:00 +02:00
Jan Prochazka 2b78a8dcae perspective WIP 2022-06-17 22:30:10 +02:00
Jan Prochazka 389ef98c66 v5.0.6-beta.1 2022-06-16 17:06:06 +02:00
Jan Prochazka 75bf0e53fc perspectives WIP 2022-06-16 17:05:42 +02:00
Jan Prochazka ff4dd18c1b Merge branch 'master' into develop 2022-06-16 13:37:12 +02:00
Jan Prochazka 4c535289a4 connection type label 2022-06-16 13:36:50 +02:00
Jan Prochazka d24886c73b fix 2022-06-16 13:29:51 +02:00
Jan Prochazka 9883a2982a search in columns 2022-06-16 13:07:24 +02:00
Jan Prochazka 24191870e8 Merge branch 'master' into develop 2022-06-13 19:47:06 +02:00
Jan Prochazka b9dae8928e version fixed 2022-06-13 19:36:08 +02:00
Jan Prochazka 7bed880003 v5.0.5 2022-06-13 19:35:49 +02:00
Jan Prochazka e2b95ad372 changelog 2022-06-13 19:35:31 +02:00
Jan Prochazka 18710bc67d v5.0.4 2022-06-13 19:31:31 +02:00
Jan Prochazka 02e8bba999 Merge branch 'master' into develop 2022-06-12 20:20:37 +02:00
Jan Prochazka e770ca3eef v5.0.4-beta.9 2022-06-12 20:09:35 +02:00
Jan Prochazka aaa72426c3 v5.0.4-alpha.8 2022-06-12 20:02:58 +02:00
Jan Prochazka 53e5f1378c shell scripts blocked by default only when listen-api 2022-06-12 20:02:48 +02:00
Jan Prochazka 773abc6dff v5.0.4-alpha.7 2022-06-12 19:46:35 +02:00
Jan Prochazka 8abb311623 v5.0.4-beta.6 2022-06-12 19:45:55 +02:00
Jan Prochazka 2d83fb7dc4 start api => listen api 2022-06-12 19:45:27 +02:00
Jan Prochazka ae69ca9ebd explicit start api 2022-06-12 19:42:51 +02:00
Jan Prochazka 0cb4ec54bc perspective WIP 2022-06-12 19:30:54 +02:00
Jan Prochazka d34cff234c v5.0.4-beta.5 2022-06-12 17:59:05 +02:00
Jan Prochazka 50abead104 map fixes 2022-06-12 17:58:22 +02:00
Jan Prochazka 3b0ed7df8b v5.0.4-beta.4 2022-06-12 11:28:31 +02:00
Jan Prochazka ce925337f1 fix 2022-06-12 11:27:57 +02:00
Jan Prochazka a911f5048f v5.0.4-beta.3 2022-06-12 11:22:56 +02:00
Jan Prochazka 096cbc13d8 fixed filter bool values in postgres 2022-06-12 11:14:39 +02:00
Jan Prochazka a2cf1cd340 v5.0.4-alpha.2 2022-06-12 09:47:22 +02:00
Jan Prochazka 44827ea504 cacth error when reading archive 2022-06-12 09:16:10 +02:00
Jan Prochazka 13b549ca2c Merge branch 'develop' 2022-06-12 08:19:24 +02:00
Jan Prochazka c104122a50 default value support in table yaml files #296 2022-06-12 08:15:35 +02:00
Jan Prochazka 6794b79d0e postgre fix 2022-06-12 07:30:44 +02:00
Jan Prochazka 42200ec04a v5.0.4-beta.1 2022-06-11 22:21:35 +02:00
Jan Prochazka 2944d0fa39 postgis - analyse geo columns, show in map 2022-06-11 22:21:09 +02:00
Jan Prochazka 34496ced0e support for geograpghy view in mssql 2022-06-11 19:19:50 +02:00
Jan Prochazka fa0680a8ee changed export map name 2022-06-11 18:43:19 +02:00
Jan Prochazka f2402cadb0 show tooltip on map 2022-06-11 18:41:17 +02:00
Jan Prochazka ffe82a82fa export map to file 2022-06-11 18:34:57 +02:00
Jan Prochazka 6e1a1edac0 map on standalone tab 2022-06-11 17:57:18 +02:00
Jan Prochazka 427e25b3c0 map invalidate size 2022-06-11 17:24:01 +02:00
Jan Prochazka fca2bf8ddb show popup on map 2022-06-11 17:14:22 +02:00
Jan Prochazka f65c15d2e5 map - show points 2022-06-11 16:06:49 +02:00
Jan Prochazka 343cf84a58 map - show geometry in MySQL 2022-06-11 09:45:23 +02:00
Jan Prochazka e67a94b5d7 changelog 2022-06-10 20:29:41 +02:00
Jan Prochazka cc1916eba3 v5.0.3 2022-06-10 20:28:59 +02:00
Jan Prochazka 0a0ce6ad98 v5.0.3 2022-06-10 20:25:55 +02:00
Jan Prochazka fd21157c2d v5.0.3-beta.5 2022-06-09 15:11:39 +02:00
Jan Prochazka 8b3697e71e v5.0.3-beta.4 2022-06-09 15:10:18 +02:00
Jan Prochazka f3bebcfa8f fix 2022-06-09 15:10:11 +02:00
Jan Prochazka 4c145f1f0a Merge branch 'master' of github.com:dbgate/dbgate 2022-06-09 15:09:20 +02:00
Jan Prochazka cfce4e6ece v5.0.3-beta.4 2022-06-09 15:08:21 +02:00
Jan Prochazka 13d778586e fix mssql import (+integration test) 2022-06-09 14:57:08 +02:00
Jan Prochazka 77b85fa42b changelog 2022-06-09 14:17:45 +02:00
Jan Prochazka fb89c47563 v5.0.3-beta.3 2022-06-09 14:01:38 +02:00
Jan Prochazka 8ffbdfa01d open JSON file 2022-06-09 13:39:48 +02:00
Jan Prochazka 94788454a9 multiple sort criteria #235 2022-06-09 13:12:31 +02:00
Jan Prochazka a92bd1c840 configurable object actions #255 2022-06-09 11:31:31 +02:00
Jan Prochazka 610e9f4e60 settings - default connection action 2022-06-09 10:13:05 +02:00
Jan Prochazka 6e9dace360 fix 2022-06-09 09:37:26 +02:00
Jan Prochazka 148222e239 code style 2022-06-09 09:25:47 +02:00
Jan Prochazka 5e2279cd10 unsaved connection workflow fix 2022-06-09 09:24:05 +02:00
Jan Prochazka b54026b039 connection tabs - improved UX 2022-06-09 09:16:40 +02:00
Jan Prochazka 6f3076fddb #294 fixed statusbar doesn't match active tab 2022-06-09 08:04:09 +02:00
Jan Prochazka 92c336624a v5.0.3-beta.2 2022-06-02 19:18:29 +02:00
Jan Prochazka 07d4b248bf fix 2022-06-02 19:18:15 +02:00
Jan Prochazka 1534099dc4 v5.0.3-beta.1 2022-06-02 17:32:37 +02:00
Jan Prochazka d483869aa6 reorder pinned tables #227 2022-06-02 17:32:07 +02:00
Jan Prochazka 8bb40e991b drag & drop - change order in pinned dbs #227 2022-06-02 17:26:45 +02:00
Jan Prochazka 5c6989bf91 fix 2022-06-02 16:57:23 +02:00
Jan Prochazka 5b503ae802 reset pk name in duplicate table 2022-06-02 16:52:45 +02:00
Jan Prochazka 5feb018e22 upgraded tedious driver 2022-06-02 16:47:46 +02:00
Jan Prochazka 97d259cd1e app object menu from tab 2022-06-02 16:09:20 +02:00
Jan Prochazka fa357cf8ce better UX when define SSH port #291 2022-06-02 15:48:45 +02:00
Jan Prochazka 7a0f5e171e correct handling null values in update keys 2022-06-02 15:44:03 +02:00
Jan Prochazka 24cfb23b39 fix 2022-06-02 15:42:38 +02:00
Jan Prochazka 06b6a5d3ae ability to close file uploader 2022-06-02 15:31:25 +02:00
Jan Prochazka 301ba1df60 upgraded mysql driver #293 2022-06-02 14:56:36 +02:00
167 changed files with 13373 additions and 5417 deletions
+3
View File
@@ -27,6 +27,9 @@ jobs:
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: yarn adjustPackageJson
run: |
yarn adjustPackageJson
- name: yarn install
run: |
yarn install
+3
View File
@@ -31,6 +31,9 @@ jobs:
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: yarn adjustPackageJson
run: |
yarn adjustPackageJson
- name: yarn install
run: |
# yarn --version
+11
View File
@@ -31,6 +31,11 @@ jobs:
run: |
cd packages/filterparser
yarn test:ci
- name: Datalib (perspective) tests
if: always()
run: |
cd packages/datalib
yarn test:ci
- uses: tanmen/jest-reporter@v1
if: always()
with:
@@ -43,6 +48,12 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
result-file: packages/filterparser/result.json
action-name: Filter parser test results
- uses: tanmen/jest-reporter@v1
if: always()
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
result-file: packages/datalib/result.json
action-name: Datalib (perspectives) test results
services:
postgres:
+4 -1
View File
@@ -1,3 +1,6 @@
{
"jestrunner.jestCommand": "node_modules/.bin/cross-env DEVMODE=1 LOCALTEST=1 node_modules/.bin/jest"
"jestrunner.jestCommand": "node_modules/.bin/cross-env DEVMODE=1 LOCALTEST=1 node_modules/.bin/jest",
"cSpell.words": [
"dbgate"
]
}
+61
View File
@@ -8,6 +8,67 @@ Builds:
- linux - application for linux
- win - application for Windows
### 5.1.0
- ADDED: Perspectives (docs: https://dbgate.org/docs/perspectives.html )
- CHANGED: Upgraded SQLite engine version (driver better-sqlite3: 7.6.2)
- CHANGED: Upgraded ElectronJS version (from version 13 to version 17)
- CHANGED: Upgraded all dependencies with current available minor version updates
- CHANGED: By deffault, connect on click #332˝
- CHANGED: Improved keyboard navigation, when editing table data #331
- ADDED: Option to skip Save changes dialog #329
- FIXED: Unsigned column doesn't work correctly. #324
- FIXED: Connect to MS SQL with doamin user now works also under Linux and Mac #305
### 5.0.9
- FIXED: Fixed problem with SSE events on web version
- ADDED: Added menu command "New query designer"
- ADDED: Added menu command "New ER diagram"
### 5.0.8
- ADDED: SQL Server - support using domain logins under Linux and Mac #305
- ADDED: Permissions for connections #318
- ADDED: Ability to change editor front #308
- ADDED: Custom expression in query designer #306
- ADDED: OR conditions in query designer #321
- ADDED: Ability to configure settings view environment variables #304
### 5.0.7
- FIXED: Fixed some problems with SSH tunnel (upgraded SSH client) #315
- FIXED: Fixed MognoDB executing find query #312
- ADDED: Interval filters for date/time columns #311
- ADDED: Ability to clone rows #309
- ADDED: connecting option Trust server certificate for SQL Server #305
- ADDED: Autorefresh, reload table every x second #303
- FIXED(app): Changing editor theme and font size in Editor Themes #300
### 5.0.6
- ADDED: Search in columns
- CHANGED: Upgraded mongodb driver
- ADDED: Ability to reset view, when data load fails
- FIXED: Filtering works for complex types (geography, xml under MSSQL)
- FIXED: Fixed some NPM package problems
### 5.0.5
- ADDED: Visualisation geographics objects on map #288
- ADDED: Support for native SQL as default value inside yaml files #296
- FIXED: Postgres boolean columns don't filter correctly #298
- FIXED: Importing dbgate-api as NPM package now works correctly
- FIXED: Handle error when reading deleted archive
### 5.0.3
- CHANGED: Optimalization of loading DB structure for PostgreSQL, MySQL #273
- CHANGED: Upgraded mysql driver #293
- CHANGED: Better UX when defining SSH port #291
- ADDED: Database object menu from tab
- CHANGED: Ability to close file uploader
- FIXED: Correct handling of NUL values in update keys
- CHANGED: Upgraded MS SQL tedious driver
- ADDED: Change order of pinned tables & databases #227
- FIXED: #294 Statusbar doesn't match active tab
- CHANGED: Improved connection worklflow, disconnecting shws confirmations, when it leads to close any tabs
- ADDED: Configurable object actions #255
- ADDED: Multiple sort criteria #235
- ADDED(app): Open JSON file
### 5.0.2
- FIXED: Cannot use SSH Tunnel after update #291
+12
View File
@@ -0,0 +1,12 @@
const fs = require('fs');
function adjustFile(file) {
const json = JSON.parse(fs.readFileSync(file, { encoding: 'utf-8' }));
if (process.platform != 'win32') {
delete json.optionalDependencies.msnodesqlv8;
}
fs.writeFileSync(file, JSON.stringify(json, null, 2), 'utf-8');
}
adjustFile('packages/api/package.json');
adjustFile('app/package.json');
+5 -5
View File
@@ -107,12 +107,12 @@
"devDependencies": {
"copyfiles": "^2.2.0",
"cross-env": "^6.0.3",
"electron": "13.6.3",
"electron-builder": "22.14.5",
"electron-builder-notarize": "^1.4.0"
"electron": "17.4.10",
"electron-builder": "23.1.0",
"electron-builder-notarize": "^1.5.0"
},
"optionalDependencies": {
"better-sqlite3": "7.5.0",
"msnodesqlv8": "^2.4.4"
"better-sqlite3": "7.6.2",
"msnodesqlv8": "^2.6.0"
}
}
+8 -14
View File
@@ -22,7 +22,6 @@ const configRootPath = path.join(app.getPath('userData'), 'config-root.json');
let initialConfig = {};
let apiLoaded = false;
let mainModule;
let winCounter = 0;
const isMac = () => os.platform() == 'darwin';
@@ -35,8 +34,8 @@ try {
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
const mainWindows = {};
const mainMenus = {};
let mainWindow;
let mainMenu;
let runCommandOnLoad = null;
log.transports.file.level = 'debug';
@@ -135,7 +134,6 @@ ipcMain.on('update-commands', async (event, arg) => {
}
});
ipcMain.on('quit-app', async (event, arg) => {
app.quit();
if (isMac()) {
app.quit();
} else {
@@ -157,9 +155,6 @@ ipcMain.on('app-started', async (event, arg) => {
runCommandOnLoad = null;
}
});
ipcMain.on('new-window', async (event, arg) => {
createWindow();
});
ipcMain.on('window-action', async (event, arg) => {
if (!mainWindow) {
return;
@@ -219,18 +214,18 @@ ipcMain.on('window-action', async (event, arg) => {
}
});
ipcMain.handle('showOpenDialog', async (event, { winid, options }) => {
ipcMain.handle('showOpenDialog', async (event, options) => {
const res = electron.dialog.showOpenDialogSync(mainWindow, options);
return res;
});
ipcMain.handle('showSaveDialog', async (event, { winid, options }) => {
ipcMain.handle('showSaveDialog', async (event, options) => {
const res = electron.dialog.showSaveDialogSync(mainWindow, options);
return res;
});
ipcMain.handle('showItemInFolder', async (event, { winid, path }) => {
ipcMain.handle('showItemInFolder', async (event, path) => {
electron.shell.showItemInFolder(path);
});
ipcMain.handle('openExternal', async (event, { winid, url }) => {
ipcMain.handle('openExternal', async (event, url) => {
electron.shell.openExternal(url);
});
@@ -273,7 +268,6 @@ function createWindow() {
nodeIntegration: true,
contextIsolation: false,
spellcheck: false,
additionalArguments: [`--winid=${++winCounter}`],
},
});
@@ -338,7 +332,7 @@ function createWindow() {
mainModule = main;
apiLoaded = true;
}
mainModule.addElectronSender(mainWindow.webContents);
mainModule.setElectronSender(mainWindow.webContents);
loadMainWindow();
@@ -347,8 +341,8 @@ function createWindow() {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainModule.removeElectronSender(mainWindow.webContents);
mainWindow = null;
mainModule.setElectronSender(null);
});
}
+2 -1
View File
@@ -4,9 +4,10 @@ module.exports = ({ editMenu }) => [
submenu: [
{ command: 'new.connection', hideDisabled: true },
{ command: 'new.sqliteDatabase', hideDisabled: true },
{ command: 'new.window', hideDisabled: true },
{ divider: true },
{ command: 'new.query', hideDisabled: true },
{ command: 'new.queryDesign', hideDisabled: true },
{ command: 'new.diagram', hideDisabled: true },
{ command: 'new.freetable', hideDisabled: true },
{ command: 'new.shell', hideDisabled: true },
{ command: 'new.jsonl', hideDisabled: true },
+479 -510
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,4 +8,4 @@ then
echo "$HOST_IP $HOST_DOMAIN" >> /etc/hosts
fi
node bundle.js
node bundle.js --listen-api
@@ -297,4 +297,33 @@ describe('Deploy database', () => {
expect(res.rows[0].val.toString()).toEqual('5');
})
);
test.each(engines.enginesPostgre.map(engine => [engine.label, engine]))(
'Current timestamp default value - %s',
testWrapper(async (conn, driver, engine) => {
await testDatabaseDeploy(conn, driver, [
[
{
name: 't1.table.yaml',
json: {
name: 't1',
columns: [
{ name: 'id', type: 'int' },
{
name: 'val',
type: 'timestamp',
default: 'current_timestamp',
},
],
primaryKey: ['id'],
},
},
],
]);
await driver.query(conn, `insert into t1 (id) values (1)`);
const res = await driver.query(conn, ` select val from t1 where id = 1`);
expect(res.rows[0].val.toString().substring(0, 2)).toEqual('20');
})
);
});
+24 -24
View File
@@ -1,21 +1,21 @@
version: '3'
services:
postgres:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: Pwd2020Db
ports:
- 15000:5432
# postgres:
# image: postgres
# restart: always
# environment:
# POSTGRES_PASSWORD: Pwd2020Db
# ports:
# - 15000:5432
mariadb:
image: mariadb
command: --default-authentication-plugin=mysql_native_password
restart: always
ports:
- 15004:3306
environment:
- MYSQL_ROOT_PASSWORD=Pwd2020Db
# mariadb:
# image: mariadb
# command: --default-authentication-plugin=mysql_native_password
# restart: always
# ports:
# - 15004:3306
# environment:
# - MYSQL_ROOT_PASSWORD=Pwd2020Db
# mysql:
# image: mysql:8.0.18
@@ -26,15 +26,15 @@ services:
# environment:
# - MYSQL_ROOT_PASSWORD=Pwd2020Db
# mssql:
# image: mcr.microsoft.com/mssql/server
# restart: always
# ports:
# - 15002:1433
# environment:
# - ACCEPT_EULA=Y
# - SA_PASSWORD=Pwd2020Db
# - MSSQL_PID=Express
mssql:
image: mcr.microsoft.com/mssql/server
restart: always
ports:
- 15002:1433
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=Pwd2020Db
- MSSQL_PID=Express
# cockroachdb:
# image: cockroachdb/cockroach
+6 -2
View File
@@ -135,12 +135,16 @@ const filterLocal = [
// filter local testing
'-MySQL',
'-MariaDB',
'PostgreSQL',
'-SQL Server',
'-PostgreSQL',
'SQL Server',
'-SQLite',
'-CockroachDB',
];
const enginesPostgre = engines.filter(x => x.label == 'PostgreSQL');
module.exports = process.env.CITEST
? engines.filter(x => !x.skipOnCI)
: engines.filter(x => filterLocal.find(y => x.label == y));
module.exports.enginesPostgre = enginesPostgre;
+6 -1
View File
@@ -1,6 +1,6 @@
{
"private": true,
"version": "5.0.2",
"version": "5.1.0",
"name": "dbgate-all",
"workspaces": [
"packages/*",
@@ -10,6 +10,10 @@
"scripts": {
"start:api": "yarn workspace dbgate-api start",
"start:app": "cd app && yarn start",
"start:api:debug": "cross-env DEBUG=* yarn workspace dbgate-api start",
"start:app:debug": "cd app && cross-env DEBUG=* yarn start",
"start:api:debug:ssh": "cross-env DEBUG=ssh yarn workspace dbgate-api start",
"start:app:debug:ssh": "cd app && cross-env DEBUG=ssh yarn start",
"start:api:portal": "yarn workspace dbgate-api start:portal",
"start:api:singledb": "yarn workspace dbgate-api start:singledb",
"start:web": "yarn workspace dbgate-web dev",
@@ -32,6 +36,7 @@
"start:app:local": "cd app && yarn start:local",
"setCurrentVersion": "node setCurrentVersion",
"generatePadFile": "node generatePadFile",
"adjustPackageJson": "node adjustPackageJson",
"fillNativeModules": "node fillNativeModules",
"fillNativeModulesElectron": "node fillNativeModules --electron",
"fillPackagedPlugins": "node fillPackagedPlugins",
+2
View File
@@ -1,4 +1,6 @@
DEVMODE=1
SHELL_SCRIPTING=1
# PERMISSIONS=~widgets/app,~widgets/plugins
# DISABLE_SHELL=1
# HIDE_APP_EDITOR=1
+11
View File
@@ -48,4 +48,15 @@ PASSWORD_relational=relational
ENGINE_relational=mariadb@dbgate-plugin-mysql
READONLY_relational=1
# SETTINGS_dataGrid.showHintColumns=1
# docker run -p 3000:3000 -e CONNECTIONS=mongo -e URL_mongo=mongodb://localhost:27017 -e ENGINE_mongo=mongo@dbgate-plugin-mongo -e LABEL_mongo=mongo dbgate/dbgate:beta
# LOGINS=x,y
# LOGIN_PASSWORD_x=x
# LOGIN_PASSWORD_y=LOGIN_PASSWORD_y
# LOGIN_PERMISSIONS_x=~*
# LOGIN_PERMISSIONS_y=~*
# PERMISSIONS=~*,connections/relational
# PERMISSIONS=~*
+10 -8
View File
@@ -28,6 +28,7 @@
"dbgate-query-splitter": "^4.9.0",
"dbgate-sqltree": "^5.0.0-alpha.1",
"dbgate-tools": "^5.0.0-alpha.1",
"debug": "^4.3.4",
"diff": "^5.0.0",
"diff2html": "^3.4.13",
"eslint": "^6.8.0",
@@ -45,18 +46,19 @@
"lodash": "^4.17.21",
"ncp": "^2.0.0",
"node-cron": "^2.0.3",
"node-ssh-forward": "^0.7.2",
"on-finished": "^2.4.1",
"portfinder": "^1.0.28",
"simple-encryptor": "^4.0.0",
"ssh2": "^1.11.0",
"tar": "^6.0.5",
"uuid": "^3.4.0"
},
"scripts": {
"start": "env-cmd node src/index.js",
"start:portal": "env-cmd -f env/portal/.env node src/index.js",
"start:singledb": "env-cmd -f env/singledb/.env node src/index.js",
"start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db",
"start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test",
"start": "env-cmd node src/index.js --listen-api",
"start:portal": "env-cmd -f env/portal/.env node src/index.js --listen-api",
"start:singledb": "env-cmd -f env/singledb/.env node src/index.js --listen-api",
"start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db --listen-api",
"start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api",
"ts": "tsc",
"build": "webpack"
},
@@ -72,7 +74,7 @@
"webpack-cli": "^3.3.11"
},
"optionalDependencies": {
"better-sqlite3": "7.5.0",
"msnodesqlv8": "^2.4.4"
"better-sqlite3": "7.6.2",
"msnodesqlv8": "^2.6.0"
}
}
+26 -24
View File
@@ -1,11 +1,8 @@
const fs = require('fs-extra');
const stream = require('stream');
const readline = require('readline');
const path = require('path');
const { formatWithOptions } = require('util');
const { archivedir, clearArchiveLinksCache, resolveArchiveFolder } = require('../utility/directories');
const socket = require('../utility/socket');
const JsonLinesDatastore = require('../utility/JsonLinesDatastore');
const { saveFreeTableData } = require('../utility/freeTableStorage');
const loadFilesRecursive = require('../utility/loadFilesRecursive');
@@ -45,29 +42,34 @@ module.exports = {
files_meta: true,
async files({ folder }) {
const dir = resolveArchiveFolder(folder);
if (!(await fs.exists(dir))) return [];
const files = await loadFilesRecursive(dir); // fs.readdir(dir);
try {
const dir = resolveArchiveFolder(folder);
if (!(await fs.exists(dir))) return [];
const files = await loadFilesRecursive(dir); // fs.readdir(dir);
function fileType(ext, type) {
return files
.filter(name => name.endsWith(ext))
.map(name => ({
name: name.slice(0, -ext.length),
label: path.parse(name.slice(0, -ext.length)).base,
type,
}));
function fileType(ext, type) {
return files
.filter(name => name.endsWith(ext))
.map(name => ({
name: name.slice(0, -ext.length),
label: path.parse(name.slice(0, -ext.length)).base,
type,
}));
}
return [
...fileType('.jsonl', 'jsonl'),
...fileType('.table.yaml', 'table.yaml'),
...fileType('.view.sql', 'view.sql'),
...fileType('.proc.sql', 'proc.sql'),
...fileType('.func.sql', 'func.sql'),
...fileType('.trigger.sql', 'trigger.sql'),
...fileType('.matview.sql', 'matview.sql'),
];
} catch (err) {
console.log('Error reading archive files', err.message);
return [];
}
return [
...fileType('.jsonl', 'jsonl'),
...fileType('.table.yaml', 'table.yaml'),
...fileType('.view.sql', 'view.sql'),
...fileType('.proc.sql', 'proc.sql'),
...fileType('.func.sql', 'func.sql'),
...fileType('.trigger.sql', 'trigger.sql'),
...fileType('.matview.sql', 'matview.sql'),
];
},
refreshFiles_meta: true,
+25 -11
View File
@@ -29,14 +29,14 @@ module.exports = {
async get(_params, req) {
const logins = getLogins();
const login = logins ? logins.find(x => x.login == (req.auth && req.auth.user)) : null;
const permissions = login ? login.permissions : null;
const permissions = login ? login.permissions : process.env.PERMISSIONS;
return {
runAsPortal: !!connections.portalConnections,
singleDatabase: connections.singleDatabase,
// hideAppEditor: !!process.env.HIDE_APP_EDITOR,
allowShellConnection: platformInfo.allowShellConnection,
allowShellScripting: platformInfo.allowShellConnection,
allowShellScripting: platformInfo.allowShellScripting,
isDocker: platformInfo.isDocker,
permissions,
login,
@@ -59,13 +59,10 @@ module.exports = {
getSettings_meta: true,
async getSettings() {
try {
return this.fillMissingSettings(
JSON.parse(await fs.readFile(path.join(datadir(), 'settings.json'), { encoding: 'utf-8' }))
);
} catch (err) {
return this.fillMissingSettings({});
}
const res = await lock.acquire('settings', async () => {
return await this.loadSettings();
});
return res;
},
fillMissingSettings(value) {
@@ -76,15 +73,32 @@ module.exports = {
// res['app.useNativeMenu'] = os.platform() == 'darwin' ? true : false;
res['app.useNativeMenu'] = false;
}
for (const envVar in process.env) {
if (envVar.startsWith('SETTINGS_')) {
const key = envVar.substring('SETTINGS_'.length);
if (!res[key]) {
res[key] = process.env[envVar];
}
}
}
return res;
},
async loadSettings() {
try {
const settingsText = await fs.readFile(path.join(datadir(), 'settings.json'), { encoding: 'utf-8' });
return this.fillMissingSettings(JSON.parse(settingsText));
} catch (err) {
return this.fillMissingSettings({});
}
},
updateSettings_meta: true,
async updateSettings(values, req) {
if (!hasPermission(`settings/change`, req)) return false;
const res = await lock.acquire('update', async () => {
const currentValue = await this.getSettings();
const res = await lock.acquire('settings', async () => {
const currentValue = await this.loadSettings();
try {
const updated = {
...currentValue,
+15 -8
View File
@@ -13,6 +13,7 @@ const JsonLinesDatabase = require('../utility/JsonLinesDatabase');
const processArgs = require('../utility/processArgs');
const { safeJsonParse } = require('dbgate-tools');
const platformInfo = require('../utility/platformInfo');
const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission');
function getNamedArgs() {
const res = {};
@@ -165,10 +166,12 @@ module.exports = {
},
list_meta: true,
async list() {
return portalConnections && !platformInfo.allowShellConnection
? portalConnections.map(maskConnection)
: this.datastore.find();
async list(_params, req) {
if (portalConnections) {
if (platformInfo.allowShellConnection) return portalConnections;
return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, req));
}
return (await this.datastore.find()).filter(x => connectionHasPermission(x, req));
},
test_meta: true,
@@ -215,16 +218,18 @@ module.exports = {
},
update_meta: true,
async update({ _id, values }) {
async update({ _id, values }, req) {
if (portalConnections) return;
testConnectionPermission(_id, req);
const res = await this.datastore.patch(_id, values);
socket.emitChanged('connection-list-changed');
return res;
},
updateDatabase_meta: true,
async updateDatabase({ conid, database, values }) {
async updateDatabase({ conid, database, values }, req) {
if (portalConnections) return;
testConnectionPermission(conid, req);
const conn = await this.datastore.get(conid);
let databases = (conn && conn.databases) || [];
if (databases.find(x => x.name == database)) {
@@ -240,8 +245,9 @@ module.exports = {
},
delete_meta: true,
async delete(connection) {
async delete(connection, req) {
if (portalConnections) return;
testConnectionPermission(connection, req);
const res = await this.datastore.remove(connection._id);
socket.emitChanged('connection-list-changed');
return res;
@@ -258,7 +264,8 @@ module.exports = {
},
get_meta: true,
async get({ conid }) {
async get({ conid }, req) {
testConnectionPermission(conid, req);
return this.getCore({ conid, mask: true });
},
@@ -26,6 +26,7 @@ const generateDeploySql = require('../shell/generateDeploySql');
const { createTwoFilesPatch } = require('diff');
const diff2htmlPage = require('../utility/diff2htmlPage');
const processArgs = require('../utility/processArgs');
const { testConnectionPermission } = require('../utility/hasPermission');
module.exports = {
/** @type {import('dbgate-types').OpenedDatabaseConnection[]} */
@@ -130,7 +131,8 @@ module.exports = {
},
queryData_meta: true,
async queryData({ conid, database, sql }) {
async queryData({ conid, database, sql }, req) {
testConnectionPermission(conid, req);
console.log(`Processing query, conid=${conid}, database=${database}, sql=${sql}`);
const opened = await this.ensureOpened(conid, database);
// if (opened && opened.status && opened.status.name == 'error') {
@@ -141,14 +143,16 @@ module.exports = {
},
sqlSelect_meta: true,
async sqlSelect({ conid, database, select }) {
async sqlSelect({ conid, database, select }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'sqlSelect', select });
return res;
},
runScript_meta: true,
async runScript({ conid, database, sql }) {
async runScript({ conid, database, sql }, req) {
testConnectionPermission(conid, req);
console.log(`Processing script, conid=${conid}, database=${database}, sql=${sql}`);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'runScript', sql });
@@ -156,13 +160,15 @@ module.exports = {
},
collectionData_meta: true,
async collectionData({ conid, database, options }) {
async collectionData({ conid, database, options }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'collectionData', options });
return res.result || null;
},
async loadDataCore(msgtype, { conid, database, ...args }) {
async loadDataCore(msgtype, { conid, database, ...args }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype, ...args });
if (res.errorMessage) {
@@ -176,32 +182,38 @@ module.exports = {
},
loadKeys_meta: true,
async loadKeys({ conid, database, root, filter }) {
async loadKeys({ conid, database, root, filter }, req) {
testConnectionPermission(conid, req);
return this.loadDataCore('loadKeys', { conid, database, root, filter });
},
exportKeys_meta: true,
async exportKeys({ conid, database, options }) {
async exportKeys({ conid, database, options }, req) {
testConnectionPermission(conid, req);
return this.loadDataCore('exportKeys', { conid, database, options });
},
loadKeyInfo_meta: true,
async loadKeyInfo({ conid, database, key }) {
async loadKeyInfo({ conid, database, key }, req) {
testConnectionPermission(conid, req);
return this.loadDataCore('loadKeyInfo', { conid, database, key });
},
loadKeyTableRange_meta: true,
async loadKeyTableRange({ conid, database, key, cursor, count }) {
async loadKeyTableRange({ conid, database, key, cursor, count }, req) {
testConnectionPermission(conid, req);
return this.loadDataCore('loadKeyTableRange', { conid, database, key, cursor, count });
},
loadFieldValues_meta: true,
async loadFieldValues({ conid, database, schemaName, pureName, field, search }) {
async loadFieldValues({ conid, database, schemaName, pureName, field, search }, req) {
testConnectionPermission(conid, req);
return this.loadDataCore('loadFieldValues', { conid, database, schemaName, pureName, field, search });
},
callMethod_meta: true,
async callMethod({ conid, database, method, args }) {
async callMethod({ conid, database, method, args }, req) {
testConnectionPermission(conid, req);
return this.loadDataCore('callMethod', { conid, database, method, args });
// const opened = await this.ensureOpened(conid, database);
@@ -213,7 +225,8 @@ module.exports = {
},
updateCollection_meta: true,
async updateCollection({ conid, database, changeSet }) {
async updateCollection({ conid, database, changeSet }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, { msgtype: 'updateCollection', changeSet });
if (res.errorMessage) {
@@ -225,7 +238,14 @@ module.exports = {
},
status_meta: true,
async status({ conid, database }) {
async status({ conid, database }, req) {
if (!conid) {
return {
name: 'error',
message: 'No connection',
};
}
testConnectionPermission(conid, req);
const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (existing) {
return {
@@ -247,7 +267,8 @@ module.exports = {
},
ping_meta: true,
async ping({ conid, database }) {
async ping({ conid, database }, req) {
testConnectionPermission(conid, req);
let existing = this.opened.find(x => x.conid == conid && x.database == database);
if (existing) {
@@ -263,7 +284,8 @@ module.exports = {
},
refresh_meta: true,
async refresh({ conid, database, keepOpen }) {
async refresh({ conid, database, keepOpen }, req) {
testConnectionPermission(conid, req);
if (!keepOpen) this.close(conid, database);
await this.ensureOpened(conid, database);
@@ -271,7 +293,8 @@ module.exports = {
},
syncModel_meta: true,
async syncModel({ conid, database, isFullRefresh }) {
async syncModel({ conid, database, isFullRefresh }, req) {
testConnectionPermission(conid, req);
const conn = await this.ensureOpened(conid, database);
conn.subprocess.send({ msgtype: 'syncModel', isFullRefresh });
return { status: 'ok' };
@@ -301,13 +324,15 @@ module.exports = {
},
disconnect_meta: true,
async disconnect({ conid, database }) {
async disconnect({ conid, database }, req) {
testConnectionPermission(conid, req);
await this.close(conid, database, true);
return { status: 'ok' };
},
structure_meta: true,
async structure({ conid, database }) {
async structure({ conid, database }, req) {
testConnectionPermission(conid, req);
if (conid == '__model') {
const model = await importDbModel(database);
return model;
@@ -324,14 +349,19 @@ module.exports = {
},
serverVersion_meta: true,
async serverVersion({ conid, database }) {
async serverVersion({ conid, database }, req) {
if (!conid) {
return null;
}
testConnectionPermission(conid, req);
if (!conid) return null;
const opened = await this.ensureOpened(conid, database);
return opened.serverVersion || null;
},
sqlPreview_meta: true,
async sqlPreview({ conid, database, objects, options }) {
async sqlPreview({ conid, database, objects, options }, req) {
testConnectionPermission(conid, req);
// wait for structure
await this.structure({ conid, database });
@@ -341,7 +371,8 @@ module.exports = {
},
exportModel_meta: true,
async exportModel({ conid, database }) {
async exportModel({ conid, database }, req) {
testConnectionPermission(conid, req);
const archiveFolder = await archive.getNewArchiveFolder({ database });
await fs.mkdir(path.join(archivedir(), archiveFolder));
const model = await this.structure({ conid, database });
@@ -351,7 +382,8 @@ module.exports = {
},
generateDeploySql_meta: true,
async generateDeploySql({ conid, database, archiveFolder }) {
async generateDeploySql({ conid, database, archiveFolder }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(opened, {
msgtype: 'generateDeploySql',
+7
View File
@@ -8,6 +8,7 @@ const socket = require('../utility/socket');
const scheduler = require('./scheduler');
const getDiagramExport = require('../utility/getDiagramExport');
const apps = require('./apps');
const getMapExport = require('../utility/getMapExport');
function serialize(format, data) {
if (format == 'text') return data;
@@ -187,6 +188,12 @@ module.exports = {
return true;
},
exportMap_meta: true,
async exportMap({ filePath, geoJson }) {
await fs.writeFile(filePath, getMapExport(geoJson));
return true;
},
exportDiagram_meta: true,
async exportDiagram({ filePath, html, css, themeType, themeClassName }) {
await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName));
@@ -7,6 +7,7 @@ const { handleProcessCommunication } = require('../utility/processComm');
const lock = new AsyncLock();
const config = require('./config');
const processArgs = require('../utility/processArgs');
const { testConnectionPermission } = require('../utility/hasPermission');
module.exports = {
opened: [],
@@ -90,19 +91,22 @@ module.exports = {
},
disconnect_meta: true,
async disconnect({ conid }) {
async disconnect({ conid }, req) {
testConnectionPermission(conid, req);
await this.close(conid, true);
return { status: 'ok' };
},
listDatabases_meta: true,
async listDatabases({ conid }) {
async listDatabases({ conid }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid);
return opened.databases;
},
version_meta: true,
async version({ conid }) {
async version({ conid }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid);
return opened.version;
},
@@ -132,7 +136,8 @@ module.exports = {
},
refresh_meta: true,
async refresh({ conid, keepOpen }) {
async refresh({ conid, keepOpen }, req) {
testConnectionPermission(conid, req);
if (!keepOpen) this.close(conid);
await this.ensureOpened(conid);
@@ -140,7 +145,8 @@ module.exports = {
},
createDatabase_meta: true,
async createDatabase({ conid, name }) {
async createDatabase({ conid, name }, req) {
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid);
if (opened.connection.isReadOnly) return false;
opened.subprocess.send({ msgtype: 'createDatabase', name });
+3 -2
View File
@@ -8,9 +8,10 @@ if (processArgs.startProcess) {
const proc = require('./proc');
const module = proc[processArgs.startProcess];
module.start();
} else if (!processArgs.checkParent && !global['API_PACKAGE']) {
const main = require('./main');
}
if (processArgs.listenApi) {
const main = require('./main');
main.start();
}
+8 -13
View File
@@ -25,6 +25,7 @@ const plugins = require('./controllers/plugins');
const files = require('./controllers/files');
const scheduler = require('./controllers/scheduler');
const queryHistory = require('./controllers/queryHistory');
const onFinished = require('on-finished');
const { rundir } = require('./utility/directories');
const platformInfo = require('./utility/platformInfo');
@@ -63,7 +64,10 @@ function start() {
// Tell the client to retry every 10 seconds if connectivity is lost
res.write('retry: 10000\n\n');
socket.setSseResponse(res);
socket.addSseResponse(res);
onFinished(req, () => {
socket.removeSseResponse(res);
});
});
app.use(bodyParser.json({ limit: '50mb' }));
@@ -155,17 +159,8 @@ function useAllControllers(app, electron) {
useController(app, electron, '/apps', apps);
}
function addElectronSender(electronSender) {
socket.addElectronSender(electronSender);
}
function removeElectronSender(electronSender) {
socket.removeElectronSender(electronSender);
function setElectronSender(electronSender) {
socket.setElectronSender(electronSender);
}
module.exports = {
start,
useAllControllers,
addElectronSender,
removeElectronSender,
configController: config,
};
module.exports = { start, useAllControllers, setElectronSender, configController: config };
@@ -156,11 +156,11 @@ function resolveAnalysedPromises() {
afterAnalyseCallbacks = [];
}
async function handleRunScript({ msgid, sql }) {
async function handleRunScript({ msgid, sql }, skipReadonlyCheck = false) {
await waitConnected();
const driver = requireEngineDriver(storedConnection);
try {
ensureExecuteCustomScript(driver);
if (!skipReadonlyCheck) ensureExecuteCustomScript(driver);
await driver.script(systemConnection, sql);
process.send({ msgtype: 'response', msgid });
} catch (err) {
@@ -168,11 +168,12 @@ async function handleRunScript({ msgid, sql }) {
}
}
async function handleQueryData({ msgid, sql }) {
async function handleQueryData({ msgid, sql }, skipReadonlyCheck = false) {
await waitConnected();
const driver = requireEngineDriver(storedConnection);
try {
ensureExecuteCustomScript(driver);
if (!skipReadonlyCheck) ensureExecuteCustomScript(driver);
// console.log(sql);
const res = await driver.query(systemConnection, sql);
process.send({ msgtype: 'response', msgid, ...res });
} catch (err) {
@@ -184,7 +185,7 @@ async function handleSqlSelect({ msgid, select }) {
const driver = requireEngineDriver(storedConnection);
const dmp = driver.createDumper();
dumpSqlSelect(dmp, select);
return handleQueryData({ msgid, sql: dmp.s });
return handleQueryData({ msgid, sql: dmp.s }, true);
}
async function handleDriverDataCore(msgid, callMethod) {
+3 -1
View File
@@ -1,8 +1,8 @@
const fs = require('fs-extra');
const platformInfo = require('../utility/platformInfo');
const childProcessChecker = require('../utility/childProcessChecker');
const { SSHConnection } = require('node-ssh-forward');
const { handleProcessCommunication } = require('../utility/processComm');
const { SSHConnection } = require('../utility/SSHConnection');
async function getSshConnection(connection) {
const sshConfig = {
@@ -35,6 +35,8 @@ async function handleStart({ connection, tunnelConfig }) {
tunnelConfig,
});
} catch (err) {
console.log('Error creating SSH tunnel connection:', err.message);
process.send({
msgtype: 'error',
connection,
+251
View File
@@ -0,0 +1,251 @@
/*
* Copyright 2018 Stocard GmbH.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { Client } = require('ssh2');
const net = require('net');
const fs = require('fs');
const os = require('os');
const path = require('path');
const debug = require('debug');
// interface Options {
// username?: string;
// password?: string;
// privateKey?: string | Buffer;
// agentForward?: boolean;
// bastionHost?: string;
// passphrase?: string;
// endPort?: number;
// endHost: string;
// agentSocket?: string;
// skipAutoPrivateKey?: boolean;
// noReadline?: boolean;
// }
// interface ForwardingOptions {
// fromPort: number;
// toPort: number;
// toHost?: string;
// }
class SSHConnection {
constructor(options) {
this.options = options;
this.debug = debug('ssh');
this.connections = [];
this.isWindows = process.platform === 'win32';
if (!options.username) {
this.options.username = process.env['SSH_USERNAME'] || process.env['USER'];
}
if (!options.endPort) {
this.options.endPort = 22;
}
if (!options.privateKey && !options.agentForward && !options.skipAutoPrivateKey) {
const defaultFilePath = path.join(os.homedir(), '.ssh', 'id_rsa');
if (fs.existsSync(defaultFilePath)) {
this.options.privateKey = fs.readFileSync(defaultFilePath);
}
}
}
async shutdown() {
this.debug('Shutdown connections');
for (const connection of this.connections) {
connection.removeAllListeners();
connection.end();
}
return new Promise(resolve => {
if (this.server) {
this.server.close(resolve);
}
return resolve();
});
}
async tty() {
const connection = await this.establish();
this.debug('Opening tty');
await this.shell(connection);
}
async executeCommand(command) {
const connection = await this.establish();
this.debug('Executing command "%s"', command);
await this.shell(connection, command);
}
async shell(connection, command) {
return new Promise((resolve, reject) => {
connection.shell((err, stream) => {
if (err) {
return reject(err);
}
stream
.on('close', async () => {
stream.end();
process.stdin.unpipe(stream);
process.stdin.destroy();
connection.end();
await this.shutdown();
return resolve();
})
.stderr.on('data', data => {
return reject(data);
});
stream.pipe(process.stdout);
if (command) {
stream.end(`${command}\nexit\n`);
} else {
process.stdin.pipe(stream);
}
});
});
}
async establish() {
let connection;
if (this.options.bastionHost) {
connection = await this.connectViaBastion(this.options.bastionHost);
} else {
connection = await this.connect(this.options.endHost);
}
return connection;
}
async connectViaBastion(bastionHost) {
this.debug('Connecting to bastion host "%s"', bastionHost);
const connectionToBastion = await this.connect(bastionHost);
return new Promise((resolve, reject) => {
connectionToBastion.forwardOut(
'127.0.0.1',
22,
this.options.endHost,
this.options.endPort || 22,
async (err, stream) => {
if (err) {
return reject(err);
}
const connection = await this.connect(this.options.endHost, stream);
return resolve(connection);
}
);
});
}
async connect(host, stream) {
this.debug('Connecting to "%s"', host);
const connection = new Client();
return new Promise(async (resolve, reject) => {
const options = {
host,
port: this.options.endPort,
username: this.options.username,
password: this.options.password,
privateKey: this.options.privateKey,
};
if (this.options.agentForward) {
options['agentForward'] = true;
// see https://github.com/mscdex/ssh2#client for agents on Windows
// guaranteed to give the ssh agent sock if the agent is running (posix)
let agentDefault = process.env['SSH_AUTH_SOCK'];
if (this.isWindows) {
// null or undefined
if (agentDefault == null) {
agentDefault = 'pageant';
}
}
const agentSock = this.options.agentSocket ? this.options.agentSocket : agentDefault;
if (agentSock == null) {
throw new Error('SSH Agent Socket is not provided, or is not set in the SSH_AUTH_SOCK env variable');
}
options['agent'] = agentSock;
}
if (stream) {
options['sock'] = stream;
}
// PPK private keys can be encrypted, but won't contain the word 'encrypted'
// in fact they always contain a `encryption` header, so we can't do a simple check
options['passphrase'] = this.options.passphrase;
const looksEncrypted = this.options.privateKey
? this.options.privateKey.toString().toLowerCase().includes('encrypted')
: false;
if (looksEncrypted && !options['passphrase'] && !this.options.noReadline) {
// options['passphrase'] = await this.getPassphrase();
}
connection.on('ready', () => {
this.connections.push(connection);
return resolve(connection);
});
connection.on('error', error => {
reject(error);
});
try {
connection.connect(options);
} catch (error) {
reject(error);
}
});
}
// private async getPassphrase() {
// return new Promise(resolve => {
// const rl = readline.createInterface({
// input: process.stdin,
// output: process.stdout,
// });
// rl.question('Please type in the passphrase for your private key: ', answer => {
// return resolve(answer);
// });
// });
// }
async forward(options) {
const connection = await this.establish();
return new Promise((resolve, reject) => {
this.server = net
.createServer(socket => {
this.debug(
'Forwarding connection from "localhost:%d" to "%s:%d"',
options.fromPort,
options.toHost,
options.toPort
);
connection.forwardOut(
'localhost',
options.fromPort,
options.toHost || 'localhost',
options.toPort,
(error, stream) => {
if (error) {
return reject(error);
}
socket.pipe(stream);
stream.pipe(socket);
}
);
})
.listen(options.fromPort, 'localhost', () => {
return resolve();
});
});
}
}
module.exports = { SSHConnection };
@@ -1,8 +1,5 @@
const { SSHConnection } = require('node-ssh-forward');
const portfinder = require('portfinder');
const fs = require('fs-extra');
const { decryptConnection } = require('./crypting');
const { getSshTunnel } = require('./sshTunnel');
const { getSshTunnelProxy } = require('./sshTunnelProxy');
const platformInfo = require('../utility/platformInfo');
const connections = require('../controllers/connections');
+77
View File
@@ -0,0 +1,77 @@
const getMapExport = (geoJson) => {
return `<html>
<meta charset='utf-8'>
<head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css"
integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"
integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ=="
crossorigin=""></script>
<script>
function createMap() {
map = leaflet.map('map').setView([50, 15], 13);
leaflet
.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '<a href="https://dbgate.org" title="Exported from DbGate">DbGate</a> | © OpenStreetMap',
})
.addTo(map);
const geoJsonObj = leaflet
.geoJSON(${JSON.stringify(geoJson)}, {
style: function () {
return {
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4,
};
},
pointToLayer: (feature, latlng) => {
return leaflet.circleMarker(latlng, {
radius: 7,
weight: 2,
fillColor: '#ff0000',
color: '#ff0000',
opacity: 0.9,
fillOpacity: 0.9,
});
},
onEachFeature: (feature, layer) => {
// does this feature have a property named popupContent?
if (feature.properties && feature.properties.popupContent) {
layer.bindPopup(feature.properties.popupContent);
layer.bindTooltip(feature.properties.popupContent);
}
},
})
.addTo(map);
map.fitBounds(geoJsonObj.getBounds());
}
</script>
<style>
#map {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
</style>
</head>
<body onload='createMap()'>
<div id='map'></div>
</body>
</html>`;
};
module.exports = getMapExport;
+31 -3
View File
@@ -4,12 +4,21 @@ const _ = require('lodash');
const userPermissions = {};
function hasPermission(tested, req) {
if (!req) {
// request object not available, allow all
return true;
}
const { user } = (req && req.auth) || {};
const key = user || '';
const logins = getLogins();
if (!userPermissions[key] && logins) {
const login = logins.find(x => x.login == user);
userPermissions[key] = compilePermissions(login ? login.permissions : null);
if (!userPermissions[key]) {
if (logins) {
const login = logins.find(x => x.login == user);
userPermissions[key] = compilePermissions(login ? login.permissions : null);
} else {
userPermissions[key] = compilePermissions(process.env.PERMISSIONS);
}
}
return testPermission(tested, userPermissions[key]);
}
@@ -50,7 +59,26 @@ function getLogins() {
return loginsCache;
}
function connectionHasPermission(connection, req) {
if (!connection) {
return true;
}
if (_.isString(connection)) {
return hasPermission(`connections/${connection}`, req);
} else {
return hasPermission(`connections/${connection._id}`, req);
}
}
function testConnectionPermission(connection, req) {
if (!connectionHasPermission(connection, req)) {
throw new Error('Connection permission not granted');
}
}
module.exports = {
hasPermission,
getLogins,
connectionHasPermission,
testConnectionPermission,
};
+2 -2
View File
@@ -39,8 +39,8 @@ const platformInfo = {
environment: process.env.NODE_ENV,
platform,
runningInWebpack: !!process.env.WEBPACK_DEV_SERVER_URL,
allowShellConnection: !!process.env.SHELL_CONNECTION || !!isElectron(),
allowShellScripting: !!process.env.SHELL_SCRIPTING || !!isElectron(),
allowShellConnection: !processArgs.listenApiChild || !!process.env.SHELL_CONNECTION || !!isElectron(),
allowShellScripting: !processArgs.listenApiChild || !!process.env.SHELL_SCRIPTING || !!isElectron(),
defaultKeyfile: path.join(os.homedir(), '.ssh/id_rsa'),
};
+7
View File
@@ -11,6 +11,8 @@ const startProcess = getNamedArg('--start-process');
const isForkedApi = process.argv.includes('--is-forked-api');
const pluginsDir = getNamedArg('--plugins-dir');
const workspaceDir = getNamedArg('--workspace-dir');
const listenApi = process.argv.includes('--listen-api');
const listenApiChild = process.argv.includes('--listen-api-child') || listenApi;
function getPassArgs() {
const res = [];
@@ -20,6 +22,9 @@ function getPassArgs() {
if (global['PLUGINS_DIR']) {
res.push('--plugins-dir', global['PLUGINS_DIR']);
}
if (listenApiChild) {
res.push('listen-api-child');
}
return res;
}
@@ -30,4 +35,6 @@ module.exports = {
getPassArgs,
pluginsDir,
workspaceDir,
listenApi,
listenApiChild,
};
@@ -16,7 +16,9 @@ function requireEngineDriver(connection) {
if (engine.includes('@')) {
const [shortName, packageName] = engine.split('@');
const plugin = requirePlugin(packageName);
return plugin.drivers.find(x => x.engine == engine);
if (plugin.drivers) {
return plugin.drivers.find(x => x.engine == engine);
}
}
throw new Error(`Could not find engine driver ${engine}`);
}
+24 -34
View File
@@ -1,43 +1,33 @@
let sseResponse = null;
let electronSenders = [];
let init = [];
const _ = require('lodash');
const sseResponses = [];
let electronSender = null;
let pingConfigured = false;
module.exports = {
setSseResponse(value) {
sseResponse = value;
setInterval(() => this.emit('ping'), 29 * 1000);
ensurePing() {
if (!pingConfigured) {
setInterval(() => this.emit('ping'), 29 * 1000);
pingConfigured = true;
}
},
addElectronSender(value) {
electronSenders = [...electronSenders, value];
addSseResponse(value) {
sseResponses.push(value);
this.ensurePing();
},
removeElectronSender(value) {
electronSenders = electronSenders.filter(x => x != value);
removeSseResponse(value) {
_.remove(sseResponses, x => x == value);
},
setElectronSender(value) {
electronSender = value;
this.ensurePing();
},
emit(message, data) {
if (electronSenders.length > 0) {
if (init.length > 0) {
for (const item of init) {
for (const sender of electronSenders) {
sender.send(item.message, item.data == null ? null : item.data);
}
}
init = [];
}
for (const sender of electronSenders) {
sender.send(message, data == null ? null : data);
}
} else if (sseResponse) {
if (init.length > 0) {
for (const item of init) {
sseResponse.write(
`event: ${item.message}\ndata: ${JSON.stringify(item.data == null ? null : item.data)}\n\n`
);
}
init = [];
}
sseResponse.write(`event: ${message}\ndata: ${JSON.stringify(data == null ? null : data)}\n\n`);
} else {
init.push([{ message, data }]);
if (electronSender) {
electronSender.send(message, data == null ? null : data);
}
for (const res of sseResponses) {
res.write(`event: ${message}\ndata: ${JSON.stringify(data == null ? null : data)}\n\n`);
}
},
emitChanged(key) {
+1 -5
View File
@@ -47,7 +47,6 @@ module.exports = function useController(app, electron, route, controller) {
let method = 'post';
let raw = false;
let rawParams = false;
// if (_.isString(meta)) {
// method = meta;
@@ -55,7 +54,6 @@ module.exports = function useController(app, electron, route, controller) {
if (_.isPlainObject(meta)) {
method = meta.method;
raw = meta.raw;
rawParams = meta.rawParams;
}
if (raw) {
@@ -67,9 +65,7 @@ module.exports = function useController(app, electron, route, controller) {
// controller._init_called = true;
// }
try {
let params = [{ ...req.body, ...req.query }, req];
if (rawParams) params = [req, res];
const data = await controller[key](...params);
const data = await controller[key]({ ...req.body, ...req.query }, req);
res.json(data);
} catch (e) {
console.log(e);
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['js'],
};
+5
View File
@@ -5,6 +5,8 @@
"typings": "lib/index.d.ts",
"scripts": {
"build": "tsc",
"test": "jest",
"test:ci": "jest --json --outputFile=result.json --testLocationInResults",
"start": "tsc --watch"
},
"files": [
@@ -12,11 +14,14 @@
],
"dependencies": {
"dbgate-sqltree": "^5.0.0-alpha.1",
"dbgate-tools": "^5.0.0-alpha.1",
"dbgate-filterparser": "^5.0.0-alpha.1"
},
"devDependencies": {
"dbgate-types": "^5.0.0-alpha.1",
"@types/node": "^13.7.0",
"jest": "^28.1.3",
"ts-jest": "^28.0.7",
"typescript": "^4.4.3"
}
}
+41 -20
View File
@@ -1,5 +1,14 @@
import _ from 'lodash';
import { Command, Insert, Update, Delete, UpdateField, Condition, AllowIdentityInsert } from 'dbgate-sqltree';
import {
Command,
Insert,
Update,
Delete,
UpdateField,
Condition,
AllowIdentityInsert,
Expression,
} from 'dbgate-sqltree';
import { NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
export interface ChangeSetItem {
@@ -262,27 +271,39 @@ function changeSetInsertToSql(
}
export function extractChangeSetCondition(item: ChangeSetItem, alias?: string): Condition {
function getColumnCondition(columnName: string): Condition {
const value = item.condition[columnName];
const expr: Expression = {
exprType: 'column',
columnName,
source: {
name: {
pureName: item.pureName,
schemaName: item.schemaName,
},
alias,
},
};
if (value == null) {
return {
conditionType: 'isNull',
expr,
};
} else {
return {
conditionType: 'binary',
operator: '=',
left: expr,
right: {
exprType: 'value',
value,
},
};
}
}
return {
conditionType: 'and',
conditions: _.keys(item.condition).map(columnName => ({
conditionType: 'binary',
operator: '=',
left: {
exprType: 'column',
columnName,
source: {
name: {
pureName: item.pureName,
schemaName: item.schemaName,
},
alias,
},
},
right: {
exprType: 'value',
value: item.condition[columnName],
},
})),
conditions: _.keys(item.condition).map(columnName => getColumnCondition(columnName)),
};
}
+57 -14
View File
@@ -1,5 +1,5 @@
import _ from 'lodash';
import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc } from './GridConfig';
import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc, createGridConfig } from './GridConfig';
import {
ForeignKeyInfo,
TableInfo,
@@ -24,7 +24,7 @@ export interface DisplayColumn {
headerText: string;
uniqueName: string;
uniquePath: string[];
notNull: boolean;
notNull?: boolean;
autoIncrement?: boolean;
isPrimaryKey?: boolean;
foreignKey?: ForeignKeyInfo;
@@ -194,12 +194,14 @@ export abstract class GridDisplay {
if (condition) {
conditions.push(
_.cloneDeepWith(condition, (expr: Expression) => {
if (expr.exprType == 'placeholder')
return {
exprType: 'column',
columnName: column.columnName,
source: { alias: column.sourceAlias },
};
if (expr.exprType == 'placeholder') {
return this.createColumnExpression(column, { alias: column.sourceAlias });
}
// return {
// exprType: 'column',
// columnName: column.columnName,
// source: { alias: column.sourceAlias },
// };
})
);
}
@@ -372,6 +374,22 @@ export abstract class GridDisplay {
this.reload();
}
addToSort(uniqueName, order) {
this.setConfig(cfg => ({
...cfg,
sort: [...(cfg.sort || []), { uniqueName, order }],
}));
this.reload();
}
clearSort() {
this.setConfig(cfg => ({
...cfg,
sort: [],
}));
this.reload();
}
setGrouping(uniqueName, groupFunc: GroupFunc) {
this.setConfig(cfg => ({
...cfg,
@@ -408,6 +426,15 @@ export abstract class GridDisplay {
return this.config.sort.find(x => x.uniqueName == uniqueName)?.order;
}
getSortOrderIndex(uniqueName) {
if (this.config.sort.length <= 1) return -1;
return _.findIndex(this.config.sort, x => x.uniqueName == uniqueName);
}
isSortDefined() {
return (this.config.sort || []).length > 0;
}
get filterCount() {
return _.compact(_.values(this.config.filters)).length;
}
@@ -420,6 +447,11 @@ export abstract class GridDisplay {
this.reload();
}
resetConfig() {
this.setConfig(cfg => createGridConfig());
this.reload();
}
getChangeSetCondition(row) {
if (!this.changeSetKeyFields) return null;
return _.pick(row, this.changeSetKeyFields);
@@ -458,6 +490,22 @@ export abstract class GridDisplay {
processReferences(select: Select, displayedColumnInfo: DisplayedColumnInfo, options) {}
createColumnExpression(col, source, alias?) {
let expr = null;
if (this.dialect.createColumnViewExpression) {
expr = this.dialect.createColumnViewExpression(col.columnName, col.dataType, source, alias);
if (expr) {
return expr;
}
}
return {
exprType: 'column',
alias: alias || col.columnName,
source,
...col,
};
}
createSelectBase(name: NamedObjectInfo, columns: ColumnInfo[], options) {
if (!columns) return null;
const orderColumnName = columns[0].columnName;
@@ -467,12 +515,7 @@ export abstract class GridDisplay {
name: _.pick(name, ['schemaName', 'pureName']),
alias: 'basetbl',
},
columns: columns.map(col => ({
exprType: 'column',
alias: col.columnName,
source: { alias: 'basetbl' },
...col,
})),
columns: columns.map(col => this.createColumnExpression(col, { alias: 'basetbl' })),
orderBy: [
{
exprType: 'column',
+116
View File
@@ -0,0 +1,116 @@
import { RangeDefinition } from 'dbgate-types';
import { PerspectiveDataLoadProps } from './PerspectiveDataProvider';
import _pick from 'lodash/pick';
import _zip from 'lodash/zip';
import _difference from 'lodash/difference';
import debug from 'debug';
import stableStringify from 'json-stable-stringify';
const dbg = debug('dbgate:PerspectiveCache');
export class PerspectiveBindingGroup {
constructor(public table: PerspectiveCacheTable) {}
groupSize?: number;
loadedAll: boolean;
loadedRows: any[] = [];
bindingValues: any[];
matchRow(row) {
return this.table.bindingColumns.every((column, index) => row[column] == this.bindingValues[index]);
}
}
export class PerspectiveCacheTable {
constructor(props: PerspectiveDataLoadProps, public cache: PerspectiveCache) {
this.schemaName = props.schemaName;
this.pureName = props.pureName;
this.bindingColumns = props.bindingColumns;
this.dataColumns = props.dataColumns;
this.loadedAll = false;
}
schemaName: string;
pureName: string;
bindingColumns?: string[];
dataColumns: string[];
loadedAll: boolean;
loadedRows: any[] = [];
bindingGroups: { [bindingKey: string]: PerspectiveBindingGroup } = {};
get loadedCount() {
return this.loadedRows.length;
}
getRowsResult(props: PerspectiveDataLoadProps): { rows: any[]; incomplete: boolean } {
return {
rows: this.loadedRows.slice(0, props.topCount),
incomplete: props.topCount < this.loadedCount || !this.loadedAll,
};
}
getBindingGroup(groupValues: any[]) {
const key = stableStringify(groupValues);
return this.bindingGroups[key];
}
getUncachedBindingGroups(props: PerspectiveDataLoadProps): any[][] {
const uncached = [];
for (const group of props.bindingValues) {
const key = stableStringify(group);
const item = this.bindingGroups[key];
if (!item) {
uncached.push(group);
}
}
return uncached;
}
storeGroupSize(props: PerspectiveDataLoadProps, bindingValues: any[], count: number) {
const originalBindingValue = props.bindingValues.find(v => _zip(v, bindingValues).every(([x, y]) => x == y));
if (originalBindingValue) {
const key = stableStringify(originalBindingValue);
// console.log('SET SIZE', originalBindingValue, bindingValues, key, count);
const group = new PerspectiveBindingGroup(this);
group.bindingValues = bindingValues;
group.groupSize = count;
this.bindingGroups[key] = group;
} else {
dbg('Group not found', bindingValues);
}
}
}
export class PerspectiveCache {
constructor() {}
tables: { [tableKey: string]: PerspectiveCacheTable } = {};
getTableCache(props: PerspectiveDataLoadProps) {
const tableKey = stableStringify(
_pick(props, ['schemaName', 'pureName', 'bindingColumns', 'databaseConfig', 'orderBy', 'condition'])
);
let res = this.tables[tableKey];
if (res && _difference(props.dataColumns, res.dataColumns).length > 0) {
dbg('Delete cache because incomplete columns', props.pureName, res.dataColumns);
// we have incomplete cache
delete this.tables[tableKey];
res = null;
}
if (!res) {
res = new PerspectiveCacheTable(props, this);
this.tables[tableKey] = res;
return res;
}
// cache could be used
return res;
}
clear() {
this.tables = {};
}
}
+87
View File
@@ -0,0 +1,87 @@
import { DatabaseInfo, ForeignKeyInfo, NamedObjectInfo } from 'dbgate-types';
export interface PerspectiveConfigColumns {
expandedColumns: string[];
checkedColumns: string[];
uncheckedColumns: string[];
}
export interface PerspectiveCustomJoinConfig {
joinid: string;
joinName: string;
baseUniqueName: string;
conid?: string;
database?: string;
refSchemaName?: string;
refTableName: string;
columns: {
baseColumnName: string;
refColumnName: string;
}[];
}
export interface PerspectiveFilterColumnInfo {
columnName: string;
filterType: string;
pureName: string;
schemaName: string;
foreignKey: ForeignKeyInfo;
}
export interface PerspectiveParentFilterConfig {
uniqueName: string;
}
export interface PerspectiveConfig extends PerspectiveConfigColumns {
rootObject: { schemaName?: string; pureName: string };
filters: { [uniqueName: string]: string };
sort: {
[parentUniqueName: string]: {
uniqueName: string;
order: 'ASC' | 'DESC';
}[];
};
customJoins: PerspectiveCustomJoinConfig[];
parentFilters: PerspectiveParentFilterConfig[];
}
export function createPerspectiveConfig(rootObject: { schemaName?: string; pureName: string }): PerspectiveConfig {
return {
expandedColumns: [],
checkedColumns: [],
uncheckedColumns: [],
customJoins: [],
filters: {},
sort: {},
rootObject,
parentFilters: [],
};
}
export type ChangePerspectiveConfigFunc = (
changeFunc: (config: PerspectiveConfig) => PerspectiveConfig,
reload?: boolean
) => void;
export function extractPerspectiveDatabases(
{ conid, database },
cfg: PerspectiveConfig
): { conid: string; database: string }[] {
const res: { conid: string; database: string }[] = [];
res.push({ conid, database });
function add(conid, database) {
if (res.find(x => x.conid == conid && x.database == database)) return;
res.push({ conid, database });
}
for (const custom of cfg.customJoins) {
add(custom.conid || conid, custom.database || database);
}
return res;
}
export interface MultipleDatabaseInfo {
[conid: string]: {
[database: string]: DatabaseInfo;
};
}
@@ -0,0 +1,141 @@
import { Condition, Expression, Select } from 'dbgate-sqltree';
import { PerspectiveDataLoadProps } from './PerspectiveDataProvider';
import debug from 'debug';
const dbg = debug('dbgate:PerspectiveDataLoader');
export class PerspectiveDataLoader {
constructor(public apiCall) {}
buildCondition(props: PerspectiveDataLoadProps): Condition {
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props;
const conditions = [];
if (condition) {
conditions.push(condition);
}
if (bindingColumns?.length == 1) {
conditions.push({
conditionType: 'in',
expr: {
exprType: 'column',
columnName: bindingColumns[0],
source: {
name: { schemaName, pureName },
},
},
values: bindingValues.map(x => x[0]),
});
}
return conditions.length > 0
? {
conditionType: 'and',
conditions,
}
: null;
}
async loadGrouping(props: PerspectiveDataLoadProps) {
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns } = props;
const bindingColumnExpressions = bindingColumns.map(
columnName =>
({
exprType: 'column',
columnName,
source: {
name: { schemaName, pureName },
},
} as Expression)
);
const select: Select = {
commandType: 'select',
from: {
name: { schemaName, pureName },
},
columns: [
{
exprType: 'call',
func: 'COUNT',
args: [
{
exprType: 'raw',
sql: '*',
},
],
alias: '_perspective_group_size_',
},
...bindingColumnExpressions,
],
where: this.buildCondition(props),
};
select.groupBy = bindingColumnExpressions;
if (dbg?.enabled) {
dbg(`LOAD COUNTS, table=${props.pureName}, columns=${props.dataColumns?.join(',')}`);
}
const response = await this.apiCall('database-connections/sql-select', {
conid: props.databaseConfig.conid,
database: props.databaseConfig.database,
select,
});
if (response.errorMessage) return response;
return response.rows.map(row => ({
...row,
_perspective_group_size_: parseInt(row._perspective_group_size_),
}));
}
async loadData(props: PerspectiveDataLoadProps) {
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props;
const select: Select = {
commandType: 'select',
from: {
name: { schemaName, pureName },
},
columns: dataColumns?.map(columnName => ({
exprType: 'column',
columnName,
source: {
name: { schemaName, pureName },
},
})),
selectAll: !dataColumns,
orderBy: orderBy?.map(({ columnName, order }) => ({
exprType: 'column',
columnName,
direction: order,
source: {
name: { schemaName, pureName },
},
})),
range: props.range,
where: this.buildCondition(props),
};
if (dbg?.enabled) {
dbg(
`LOAD DATA, table=${props.pureName}, columns=${props.dataColumns?.join(',')}, range=${props.range?.offset},${
props.range?.limit
}`
);
}
const response = await this.apiCall('database-connections/sql-select', {
conid: props.databaseConfig.conid,
database: props.databaseConfig.database,
select,
});
if (response.errorMessage) return response;
return response.rows;
}
}
@@ -0,0 +1,205 @@
import debug from 'debug';
import { Condition } from 'dbgate-sqltree';
import { RangeDefinition } from 'dbgate-types';
import { format } from 'path';
import { PerspectiveBindingGroup, PerspectiveCache } from './PerspectiveCache';
import { PerspectiveDataLoader } from './PerspectiveDataLoader';
export const PERSPECTIVE_PAGE_SIZE = 100;
const dbg = debug('dbgate:PerspectiveDataProvider');
export interface PerspectiveDatabaseConfig {
conid: string;
database: string;
}
export interface PerspectiveDataLoadProps {
databaseConfig: PerspectiveDatabaseConfig;
schemaName: string;
pureName: string;
dataColumns: string[];
orderBy: {
columnName: string;
order: 'ASC' | 'DESC';
}[];
bindingColumns?: string[];
bindingValues?: any[][];
range?: RangeDefinition;
topCount?: number;
condition?: Condition;
}
export class PerspectiveDataProvider {
constructor(public cache: PerspectiveCache, public loader: PerspectiveDataLoader) {}
async loadData(props: PerspectiveDataLoadProps): Promise<{ rows: any[]; incomplete: boolean }> {
dbg('load data', props);
// console.log('LOAD DATA', props);
if (props.bindingColumns) {
return this.loadDataNested(props);
} else {
return this.loadDataFlat(props);
}
}
async loadDataNested(props: PerspectiveDataLoadProps): Promise<{ rows: any[]; incomplete: boolean }> {
const tableCache = this.cache.getTableCache(props);
const uncached = tableCache.getUncachedBindingGroups(props);
if (uncached.length > 0) {
const counts = await this.loader.loadGrouping({
...props,
bindingValues: uncached,
});
// console.log('COUNTS', counts);
for (const resetItem of uncached) {
tableCache.storeGroupSize(props, resetItem, 0);
}
for (const countItem of counts) {
const { _perspective_group_size_, ...fields } = countItem;
tableCache.storeGroupSize(
props,
props.bindingColumns.map(col => fields[col]),
_perspective_group_size_
);
}
}
const rows = [];
// console.log('CACHE', tableCache.bindingGroups);
let groupIndex = 0;
let loadCalled = false;
let shouldReturn = false;
for (; groupIndex < props.bindingValues.length; groupIndex++) {
const groupValues = props.bindingValues[groupIndex];
const group = tableCache.getBindingGroup(groupValues);
if (!group.loadedAll) {
if (loadCalled) {
shouldReturn = true;
} else {
// we need to load next data
await this.loadNextGroup(props, groupIndex);
loadCalled = true;
}
}
// console.log('GRP', groupValues, group);
rows.push(...group.loadedRows);
if (rows.length >= props.topCount || shouldReturn) {
return {
rows: rows.slice(0, props.topCount),
incomplete: props.topCount < rows.length || !group.loadedAll || groupIndex < props.bindingValues.length - 1,
};
}
}
if (groupIndex >= props.bindingValues.length) {
// all groups are fully loaded
return { rows, incomplete: false };
}
}
async loadNextGroup(props: PerspectiveDataLoadProps, groupIndex: number) {
const tableCache = this.cache.getTableCache(props);
const planLoadingGroupIndexes: number[] = [];
const planLoadingGroups: PerspectiveBindingGroup[] = [];
let planLoadRowCount = 0;
const loadPlanned = async () => {
// console.log(
// 'LOAD PLANNED',
// planLoadingGroupIndexes,
// planLoadingGroupIndexes.map(idx => props.bindingValues[idx])
// );
const rows = await this.loader.loadData({
...props,
bindingValues: planLoadingGroupIndexes.map(idx => props.bindingValues[idx]),
});
// console.log('LOADED PLANNED', rows);
// distribute rows into groups
for (const row of rows) {
const group = planLoadingGroups.find(x => x.matchRow(row));
if (group) {
group.loadedRows.push(row);
}
}
for (const group of planLoadingGroups) {
group.loadedAll = true;
}
};
for (; groupIndex < props.bindingValues.length; groupIndex++) {
const groupValues = props.bindingValues[groupIndex];
const group = tableCache.getBindingGroup(groupValues);
if (group.loadedAll) continue;
if (group.groupSize == 0) {
group.loadedAll = true;
continue;
}
if (group.groupSize >= PERSPECTIVE_PAGE_SIZE) {
if (planLoadingGroupIndexes.length > 0) {
await loadPlanned();
return;
}
const nextRows = await this.loader.loadData({
...props,
topCount: null,
range: {
offset: group.loadedRows.length,
limit: PERSPECTIVE_PAGE_SIZE,
},
bindingValues: [group.bindingValues],
});
group.loadedRows = [...group.loadedRows, ...nextRows];
group.loadedAll = nextRows.length < PERSPECTIVE_PAGE_SIZE;
return;
} else {
if (planLoadRowCount + group.groupSize > PERSPECTIVE_PAGE_SIZE) {
await loadPlanned();
return;
}
planLoadingGroupIndexes.push(groupIndex);
planLoadingGroups.push(group);
planLoadRowCount += group.groupSize;
}
}
if (planLoadingGroupIndexes.length > 0) {
await loadPlanned();
}
}
async loadDataFlat(props: PerspectiveDataLoadProps): Promise<{ rows: any[]; incomplete: boolean }> {
const tableCache = this.cache.getTableCache(props);
if (props.topCount <= tableCache.loadedCount) {
return tableCache.getRowsResult(props);
}
// load missing rows
tableCache.dataColumns = props.dataColumns;
const nextRows = await this.loader.loadData({
...props,
topCount: null,
range: {
offset: tableCache.loadedCount,
limit: props.topCount - tableCache.loadedCount,
},
});
if (nextRows.errorMessage) {
throw new Error(nextRows.errorMessage);
}
tableCache.loadedRows = [...tableCache.loadedRows, ...nextRows];
tableCache.loadedAll = nextRows.length < props.topCount - tableCache.loadedCount;
// const rows=tableCache.getRows(props);
return tableCache.getRowsResult(props);
}
}
+264
View File
@@ -0,0 +1,264 @@
import { getTableChildPerspectiveNodes, PerspectiveTableNode, PerspectiveTreeNode } from './PerspectiveTreeNode';
import _max from 'lodash/max';
import _range from 'lodash/max';
import _fill from 'lodash/fill';
import _findIndex from 'lodash/findIndex';
import debug from 'debug';
const dbg = debug('dbgate:PerspectiveDisplay');
let lastJoinId = 0;
function getJoinId(): number {
lastJoinId += 1;
return lastJoinId;
}
export class PerspectiveDisplayColumn {
title: string;
dataField: string;
parentNodes: PerspectiveTreeNode[] = [];
colSpanAtLevel = {};
columnIndex = 0;
dataNode: PerspectiveTreeNode = null;
constructor(public display: PerspectiveDisplay) {}
get rowSpan() {
return this.display.columnLevelCount - this.parentNodes.length;
}
showParent(level: number) {
return !!this.colSpanAtLevel[level];
}
getColSpan(level: number) {
return this.colSpanAtLevel[level];
}
isVisible(level: number) {
return level == this.columnLevel;
}
get columnLevel() {
return this.parentNodes.length;
}
getParentName(level) {
return this.parentNodes[level]?.title;
}
getParentNode(level) {
return this.parentNodes[level];
}
getParentTableUniqueName(level) {
return this.parentNodes[level]?.headerTableAttributes ? this.parentNodes[level]?.uniqueName : '';
}
// hasParentNode(node: PerspectiveTreeNode) {
// return this.parentNodes.includes(node);
// }
}
interface PerspectiveSubRowCollection {
rows: CollectedPerspectiveDisplayRow[];
}
interface CollectedPerspectiveDisplayRow {
columnIndexes: number[];
rowData: any[];
subRowCollections: PerspectiveSubRowCollection[];
incompleteRowsIndicator?: string[];
}
export class PerspectiveDisplayRow {
constructor(public display: PerspectiveDisplay) {
this.rowData = _fill(Array(display.columns.length), undefined);
this.rowSpans = _fill(Array(display.columns.length), 1);
this.rowJoinIds = _fill(Array(display.columns.length), 0);
this.rowCellSkips = _fill(Array(display.columns.length), false);
}
rowData: any[] = [];
rowSpans: number[] = null;
rowCellSkips: boolean[] = null;
rowJoinIds: number[] = [];
}
export class PerspectiveDisplay {
columns: PerspectiveDisplayColumn[] = [];
rows: PerspectiveDisplayRow[] = [];
readonly columnLevelCount: number;
loadIndicatorsCounts: { [uniqueName: string]: number } = {};
constructor(public root: PerspectiveTreeNode, rows: any[]) {
// dbg('source rows', rows);
this.fillColumns(root.childNodes, [root]);
if (this.columns.length > 0) {
this.columns[0].colSpanAtLevel[0] = this.columns.length;
}
this.columnLevelCount = _max(this.columns.map(x => x.parentNodes.length)) + 1;
const collectedRows = this.collectRows(rows, root.childNodes);
dbg('collected rows', collectedRows);
// console.log('COLLECTED', JSON.stringify(collectedRows, null, 2));
// this.mergeRows(collectedRows);
this.mergeRows(collectedRows);
// dbg('merged rows', this.rows);
// console.log(
// 'MERGED',
// this.rows.map(r =>
// r.incompleteRowsIndicator
// ? `************************************ ${r.incompleteRowsIndicator.join('|')}`
// : r.rowData.join('|')
// )
// );
}
private getRowAt(rowIndex) {
while (this.rows.length <= rowIndex) {
this.rows.push(new PerspectiveDisplayRow(this));
}
return this.rows[rowIndex];
}
fillColumns(children: PerspectiveTreeNode[], parentNodes: PerspectiveTreeNode[]) {
for (const child of children) {
if (child.isChecked) {
this.processColumn(child, parentNodes);
}
}
}
processColumn(node: PerspectiveTreeNode, parentNodes: PerspectiveTreeNode[]) {
if (node.isExpandable) {
const countBefore = this.columns.length;
this.fillColumns(node.childNodes, [...parentNodes, node]);
if (this.columns.length > countBefore) {
this.columns[countBefore].colSpanAtLevel[parentNodes.length] = this.columns.length - countBefore;
}
} else {
const column = new PerspectiveDisplayColumn(this);
column.title = node.columnTitle;
column.dataField = node.dataField;
column.parentNodes = parentNodes;
column.display = this;
column.columnIndex = this.columns.length;
column.dataNode = node;
this.columns.push(column);
}
}
findColumnIndexFromNode(node: PerspectiveTreeNode) {
return _findIndex(this.columns, x => x.dataNode.uniqueName == node.uniqueName);
}
collectRows(sourceRows: any[], nodes: PerspectiveTreeNode[]): CollectedPerspectiveDisplayRow[] {
const columnNodes = nodes.filter(x => x.isChecked && !x.isExpandable);
const treeNodes = nodes.filter(x => x.isChecked && x.isExpandable);
const columnIndexes = columnNodes.map(node => this.findColumnIndexFromNode(node));
const res: CollectedPerspectiveDisplayRow[] = [];
for (const sourceRow of sourceRows) {
// console.log('PROCESS SOURCE', sourceRow);
// row.startIndex = startIndex;
const rowData = columnNodes.map(node => sourceRow[node.codeName]);
const subRowCollections = [];
for (const node of treeNodes) {
if (sourceRow[node.fieldName]) {
const subrows = {
rows: this.collectRows(sourceRow[node.fieldName], node.childNodes),
};
subRowCollections.push(subrows);
}
}
res.push({
rowData,
columnIndexes,
subRowCollections,
incompleteRowsIndicator: sourceRow.incompleteRowsIndicator,
});
}
return res;
}
fillRowSpans() {
for (let col = 0; col < this.columns.length; col++) {
// let lastFilledJoinId = null;
let lastFilledRow = 0;
let rowIndex = 0;
for (const row of this.rows) {
if (
row.rowData[col] === undefined &&
row.rowJoinIds[col] == this.rows[lastFilledRow].rowJoinIds[col] &&
row.rowJoinIds[col]
) {
row.rowCellSkips[col] = true;
this.rows[lastFilledRow].rowSpans[col] = rowIndex - lastFilledRow + 1;
} else {
lastFilledRow = rowIndex;
}
rowIndex++;
}
}
}
mergeRows(collectedRows: CollectedPerspectiveDisplayRow[]) {
let rowIndex = 0;
for (const collectedRow of collectedRows) {
const count = this.mergeRow(collectedRow, rowIndex);
rowIndex += count;
}
this.fillRowSpans();
}
mergeRow(collectedRow: CollectedPerspectiveDisplayRow, rowIndex: number): number {
if (collectedRow.incompleteRowsIndicator?.length > 0) {
for (const indicator of collectedRow.incompleteRowsIndicator) {
if (!this.loadIndicatorsCounts[indicator]) {
this.loadIndicatorsCounts[indicator] = rowIndex;
}
if (rowIndex < this.loadIndicatorsCounts[indicator]) {
this.loadIndicatorsCounts[indicator] = rowIndex;
}
}
return 0;
}
const mainRow = this.getRowAt(rowIndex);
for (let i = 0; i < collectedRow.columnIndexes.length; i++) {
mainRow.rowData[collectedRow.columnIndexes[i]] = collectedRow.rowData[i];
}
let rowCount = 1;
for (const subrows of collectedRow.subRowCollections) {
let additionalRowCount = 0;
let currentRowIndex = rowIndex;
for (const subrow of subrows.rows) {
const count = this.mergeRow(subrow, currentRowIndex);
additionalRowCount += count;
currentRowIndex += count;
}
if (additionalRowCount > rowCount) {
rowCount = additionalRowCount;
}
}
const joinId = getJoinId();
for (let radd = 0; radd < rowCount; radd++) {
const row = this.getRowAt(rowIndex + radd);
for (let i = 0; i < collectedRow.columnIndexes.length; i++) {
row.rowJoinIds[collectedRow.columnIndexes[i]] = joinId;
}
}
return rowCount;
}
}
+916
View File
@@ -0,0 +1,916 @@
import {
ColumnInfo,
DatabaseInfo,
ForeignKeyInfo,
NamedObjectInfo,
RangeDefinition,
TableInfo,
ViewInfo,
} from 'dbgate-types';
import {
ChangePerspectiveConfigFunc,
MultipleDatabaseInfo,
PerspectiveConfig,
PerspectiveConfigColumns,
PerspectiveCustomJoinConfig,
PerspectiveFilterColumnInfo,
} from './PerspectiveConfig';
import _isEqual from 'lodash/isEqual';
import _cloneDeep from 'lodash/cloneDeep';
import _compact from 'lodash/compact';
import _uniq from 'lodash/uniq';
import _flatten from 'lodash/flatten';
import _uniqBy from 'lodash/uniqBy';
import _sortBy from 'lodash/sortBy';
import _cloneDeepWith from 'lodash/cloneDeepWith';
import {
PerspectiveDatabaseConfig,
PerspectiveDataLoadProps,
PerspectiveDataProvider,
} from './PerspectiveDataProvider';
import stableStringify from 'json-stable-stringify';
import { getFilterType, parseFilter } from 'dbgate-filterparser';
import { FilterType } from 'dbgate-filterparser/lib/types';
import { Condition, Expression, Select } from 'dbgate-sqltree';
import { getPerspectiveDefaultColumns } from './getPerspectiveDefaultColumns';
export interface PerspectiveDataLoadPropsWithNode {
props: PerspectiveDataLoadProps;
node: PerspectiveTreeNode;
}
// export function groupPerspectiveLoadProps(
// ...list: PerspectiveDataLoadPropsWithNode[]
// ): PerspectiveDataLoadPropsWithNode[] {
// const res: PerspectiveDataLoadPropsWithNode[] = [];
// for (const item of list) {
// const existing = res.find(
// x =>
// x.node == item.node &&
// x.props.schemaName == item.props.schemaName &&
// x.props.pureName == item.props.pureName &&
// _isEqual(x.props.bindingColumns, item.props.bindingColumns)
// );
// if (existing) {
// existing.props.bindingValues.push(...item.props.bindingValues);
// } else {
// res.push(_cloneDeep(item));
// }
// }
// return res;
// }
export abstract class PerspectiveTreeNode {
constructor(
public dbs: MultipleDatabaseInfo,
public config: PerspectiveConfig,
public setConfig: ChangePerspectiveConfigFunc,
public parentNode: PerspectiveTreeNode,
public dataProvider: PerspectiveDataProvider,
public databaseConfig: PerspectiveDatabaseConfig
) {}
defaultChecked: boolean;
abstract get title();
abstract get codeName();
abstract get isExpandable();
abstract get childNodes(): PerspectiveTreeNode[];
abstract get icon(): string;
get fieldName() {
return this.codeName;
}
get headerTableAttributes() {
return null;
}
get dataField() {
return this.codeName;
}
get tableCode() {
return null;
}
get namedObject(): NamedObjectInfo {
return null;
}
abstract getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps;
get isRoot() {
return this.parentNode == null;
}
get rootNode(): PerspectiveTreeNode {
if (this.isRoot) return this;
return this.parentNode?.rootNode;
}
matchChildRow(parentRow: any, childRow: any): boolean {
return true;
}
hasTableCode(code: string) {
return code == this.tableCode || this.parentNode?.hasTableCode(code);
}
get uniqueName() {
if (this.parentNode) return `${this.parentNode.uniqueName}::${this.codeName}`;
return this.codeName;
}
get level() {
if (this.parentNode) return this.parentNode.level + 1;
return 0;
}
get isExpanded() {
return this.config.expandedColumns.includes(this.uniqueName);
}
get isChecked() {
if (this.config.checkedColumns.includes(this.uniqueName)) return true;
if (this.config.uncheckedColumns.includes(this.uniqueName)) return false;
return this.defaultChecked;
}
get columnTitle() {
return this.title;
}
get filterType(): FilterType {
return 'string';
}
get columnName() {
return null;
}
get customJoinConfig(): PerspectiveCustomJoinConfig {
return null;
}
get db(): DatabaseInfo {
return this.dbs?.[this.databaseConfig.conid]?.[this.databaseConfig.database];
}
getChildMatchColumns() {
return [];
}
getParentMatchColumns() {
return [];
}
parseFilterCondition(source = null) {
return null;
}
get childDataColumn() {
if (!this.isExpandable && this.isChecked) {
return this.codeName;
}
return null;
}
toggleExpanded(value?: boolean) {
this.includeInColumnSet('expandedColumns', this.uniqueName, value == null ? !this.isExpanded : value);
}
toggleChecked(value?: boolean) {
if (this.defaultChecked) {
this.includeInColumnSet('uncheckedColumns', this.uniqueName, value == null ? this.isChecked : value);
} else {
this.includeInColumnSet('checkedColumns', this.uniqueName, value == null ? !this.isChecked : value);
}
}
includeInColumnSet(field: keyof PerspectiveConfigColumns, uniqueName: string, isIncluded: boolean) {
if (isIncluded) {
this.setConfig(cfg => ({
...cfg,
[field]: [...(cfg[field] || []), uniqueName],
}));
} else {
this.setConfig(cfg => ({
...cfg,
[field]: (cfg[field] || []).filter(x => x != uniqueName),
}));
}
}
getFilter() {
return this.config.filters[this.uniqueName];
}
getDataLoadColumns() {
return _compact(
_uniq([
...this.childNodes.map(x => x.childDataColumn),
..._flatten(this.childNodes.filter(x => x.isExpandable && x.isChecked).map(x => x.getChildMatchColumns())),
...this.getParentMatchColumns(),
])
);
}
getChildrenCondition(source = null): Condition {
const conditions = _compact([
...this.childNodes.map(x => x.parseFilterCondition(source)),
...this.buildParentFilterConditions(),
]);
if (conditions.length == 0) {
return null;
}
if (conditions.length == 1) {
return conditions[0];
}
return {
conditionType: 'and',
conditions,
};
}
getOrderBy(table: TableInfo | ViewInfo): PerspectiveDataLoadProps['orderBy'] {
const res = _compact(
this.childNodes.map(node => {
const sort = this.config?.sort?.[node?.parentNode?.uniqueName]?.find(x => x.uniqueName == node.uniqueName);
if (sort) {
return {
columnName: node.columnName,
order: sort.order,
};
}
})
);
return res.length > 0
? res
: (table as TableInfo)?.primaryKey?.columns.map(x => ({ columnName: x.columnName, order: 'ASC' })) || [
{ columnName: table?.columns[0].columnName, order: 'ASC' },
];
}
getBaseTables() {
const res = [];
const table = this.getBaseTableFromThis();
if (table) res.push({ table, node: this });
for (const child of this.childNodes) {
if (!child.isChecked) continue;
res.push(...child.getBaseTables());
}
return res;
}
getBaseTableFromThis() {
return null;
}
get filterInfo(): PerspectiveFilterColumnInfo {
return null;
}
findChildNodeByUniquePath(uniquePath: string[]) {
if (uniquePath.length == 0) {
return this;
}
const child = this.childNodes.find(x => x.codeName == uniquePath[0]);
return child?.findChildNodeByUniquePath(uniquePath.slice(1));
}
findNodeByUniqueName(uniqueName: string): PerspectiveTreeNode {
if (!uniqueName) return null;
const uniquePath = uniqueName.split('::');
if (uniquePath[0] != this.codeName) return null;
return this.findChildNodeByUniquePath(uniquePath.slice(1));
}
get supportsParentFilter() {
return (
(this.parentNode?.isRoot || this.parentNode?.supportsParentFilter) &&
this.parentNode?.databaseConfig?.conid == this.databaseConfig?.conid &&
this.parentNode?.databaseConfig?.database == this.databaseConfig?.database
);
}
get isParentFilter() {
return !!(this.config.parentFilters || []).find(x => x.uniqueName == this.uniqueName);
}
buildParentFilterConditions(): Condition[] {
const leafNodes = _compact(
(this.config?.parentFilters || []).map(x => this.rootNode.findNodeByUniqueName(x.uniqueName))
);
const conditions: Condition[] = _compact(
leafNodes.map(leafNode => {
if (leafNode == this) return null;
const select: Select = {
commandType: 'select',
from: {
name: leafNode.namedObject,
alias: 'pert_0',
relations: [],
},
selectAll: true,
};
let lastNode = leafNode;
let node = leafNode;
let index = 1;
let lastAlias = 'pert_0';
while (node?.parentNode && node?.parentNode?.uniqueName != this?.uniqueName) {
node = node.parentNode;
let alias = `pert_${index}`;
select.from.relations.push({
joinType: 'INNER JOIN',
alias,
name: node.namedObject,
conditions: lastNode.getParentJoinCondition(lastAlias, alias),
});
lastAlias = alias;
lastNode = node;
}
if (node?.parentNode?.uniqueName != this?.uniqueName) return null;
select.where = {
conditionType: 'and',
conditions: _compact([
...lastNode.getParentJoinCondition(lastAlias, this.namedObject.pureName),
leafNode.getChildrenCondition({ alias: 'pert_0' }),
]),
};
return {
conditionType: 'exists',
subQuery: select,
};
})
);
return conditions;
}
getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
return [];
}
}
export class PerspectiveTableColumnNode extends PerspectiveTreeNode {
foreignKey: ForeignKeyInfo;
refTable: TableInfo;
isView: boolean;
isTable: boolean;
constructor(
public column: ColumnInfo,
public table: TableInfo | ViewInfo,
dbs: MultipleDatabaseInfo,
config: PerspectiveConfig,
setConfig: ChangePerspectiveConfigFunc,
dataProvider: PerspectiveDataProvider,
databaseConfig: PerspectiveDatabaseConfig,
parentNode: PerspectiveTreeNode
) {
super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig);
this.isTable = !!this.db?.tables?.find(x => x.schemaName == table.schemaName && x.pureName == table.pureName);
this.isView = !!this.db?.views?.find(x => x.schemaName == table.schemaName && x.pureName == table.pureName);
this.foreignKey = (table as TableInfo)?.foreignKeys?.find(
fk => fk.columns.length == 1 && fk.columns[0].columnName == column.columnName
);
this.refTable = this.db.tables.find(
x => x.pureName == this.foreignKey?.refTableName && x.schemaName == this.foreignKey?.refSchemaName
);
}
matchChildRow(parentRow: any, childRow: any): boolean {
if (!this.foreignKey) return false;
return parentRow[this.foreignKey.columns[0].columnName] == childRow[this.foreignKey.columns[0].refColumnName];
}
getChildMatchColumns() {
if (!this.foreignKey) return [];
return [this.foreignKey.columns[0].columnName];
}
getParentMatchColumns() {
if (!this.foreignKey) return [];
return [this.foreignKey.columns[0].refColumnName];
}
getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
if (!this.foreignKey) return [];
return this.foreignKey.columns.map(column => {
const res: Condition = {
conditionType: 'binary',
operator: '=',
left: {
exprType: 'column',
columnName: column.columnName,
source: { alias: parentAlias },
},
right: {
exprType: 'column',
columnName: column.refColumnName,
source: { alias },
},
};
return res;
});
}
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
if (!this.foreignKey) return null;
return {
schemaName: this.foreignKey.refSchemaName,
pureName: this.foreignKey.refTableName,
bindingColumns: [this.foreignKey.columns[0].refColumnName],
bindingValues: _uniqBy(
parentRows.map(row => [row[this.foreignKey.columns[0].columnName]]),
stableStringify
),
dataColumns: this.getDataLoadColumns(),
databaseConfig: this.databaseConfig,
orderBy: this.getOrderBy(this.refTable),
condition: this.getChildrenCondition(),
};
}
get icon() {
if (this.isCircular) return 'img circular';
if (this.column.autoIncrement) return 'img autoincrement';
if (this.foreignKey) return 'img foreign-key';
return 'img column';
}
get codeName() {
return this.column.columnName;
}
get columnName() {
return this.column.columnName;
}
get fieldName() {
return this.codeName + 'Ref';
}
get title() {
return this.column.columnName;
}
get isExpandable() {
return !!this.foreignKey;
}
get filterType(): FilterType {
return getFilterType(this.column.dataType);
}
get isCircular() {
return !!this.parentNode?.parentNode?.hasTableCode(this.tableCode);
}
get childNodes(): PerspectiveTreeNode[] {
if (!this.foreignKey) return [];
const tbl = this?.db?.tables?.find(
x => x.pureName == this.foreignKey?.refTableName && x.schemaName == this.foreignKey?.refSchemaName
);
return getTableChildPerspectiveNodes(
tbl,
this.dbs,
this.config,
this.setConfig,
this.dataProvider,
this.databaseConfig,
this
);
}
getBaseTableFromThis() {
return this.refTable;
}
get filterInfo(): PerspectiveFilterColumnInfo {
return {
columnName: this.columnName,
filterType: this.filterType,
pureName: this.column.pureName,
schemaName: this.column.schemaName,
foreignKey: this.foreignKey,
};
}
parseFilterCondition(source = null): Condition {
const filter = this.getFilter();
if (!filter) return null;
const condition = parseFilter(filter, this.filterType);
if (!condition) return null;
return _cloneDeepWith(condition, (expr: Expression) => {
if (expr.exprType == 'placeholder') {
return {
exprType: 'column',
columnName: this.column.columnName,
source,
};
}
});
}
get headerTableAttributes() {
if (this.foreignKey) {
return {
schemaName: this.foreignKey.refSchemaName,
pureName: this.foreignKey.refTableName,
conid: this.databaseConfig.conid,
database: this.databaseConfig.database,
};
}
return null;
}
get tableCode() {
if (this.foreignKey) {
return `${this.foreignKey.refSchemaName}|${this.foreignKey.refTableName}`;
}
return `${this.table.schemaName}|${this.table.pureName}`;
}
get namedObject(): NamedObjectInfo {
if (this.foreignKey) {
return {
schemaName: this.foreignKey.refSchemaName,
pureName: this.foreignKey.refTableName,
};
}
return null;
}
}
export class PerspectiveTableNode extends PerspectiveTreeNode {
constructor(
public table: TableInfo | ViewInfo,
dbs: MultipleDatabaseInfo,
config: PerspectiveConfig,
setConfig: ChangePerspectiveConfigFunc,
public dataProvider: PerspectiveDataProvider,
databaseConfig: PerspectiveDatabaseConfig,
parentNode: PerspectiveTreeNode
) {
super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig);
}
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
return {
schemaName: this.table.schemaName,
pureName: this.table.pureName,
dataColumns: this.getDataLoadColumns(),
databaseConfig: this.databaseConfig,
orderBy: this.getOrderBy(this.table),
condition: this.getChildrenCondition(),
};
}
get codeName() {
return this.table.schemaName ? `${this.table.schemaName}:${this.table.pureName}` : this.table.pureName;
}
get title() {
return this.table.pureName;
}
get isExpandable() {
return true;
}
get childNodes(): PerspectiveTreeNode[] {
return getTableChildPerspectiveNodes(
this.table,
this.dbs,
this.config,
this.setConfig,
this.dataProvider,
this.databaseConfig,
this
);
}
get icon() {
return 'img table';
}
getBaseTableFromThis() {
return this.table;
}
get headerTableAttributes() {
return {
schemaName: this.table.schemaName,
pureName: this.table.pureName,
conid: this.databaseConfig.conid,
database: this.databaseConfig.database,
};
}
get tableCode() {
return `${this.table.schemaName}|${this.table.pureName}`;
}
get namedObject(): NamedObjectInfo {
return {
schemaName: this.table.schemaName,
pureName: this.table.pureName,
};
}
}
// export class PerspectiveViewNode extends PerspectiveTreeNode {
// constructor(
// public view: ViewInfo,
// dbs: MultipleDatabaseInfo,
// config: PerspectiveConfig,
// setConfig: ChangePerspectiveConfigFunc,
// public dataProvider: PerspectiveDataProvider,
// databaseConfig: PerspectiveDatabaseConfig,
// parentNode: PerspectiveTreeNode
// ) {
// super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig);
// }
// getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
// return {
// schemaName: this.view.schemaName,
// pureName: this.view.pureName,
// dataColumns: this.getDataLoadColumns(),
// databaseConfig: this.databaseConfig,
// orderBy: this.getOrderBy(this.view),
// condition: this.getChildrenCondition(),
// };
// }
// get codeName() {
// return this.view.schemaName ? `${this.view.schemaName}:${this.view.pureName}` : this.view.pureName;
// }
// get title() {
// return this.view.pureName;
// }
// get isExpandable() {
// return true;
// }
// get childNodes(): PerspectiveTreeNode[] {
// return getTableChildPerspectiveNodes(
// this.view,
// this.dbs,
// this.config,
// this.setConfig,
// this.dataProvider,
// this.databaseConfig,
// this
// );
// }
// get icon() {
// return 'img table';
// }
// getBaseTableFromThis() {
// return this.view;
// }
// }
export class PerspectiveTableReferenceNode extends PerspectiveTableNode {
constructor(
public foreignKey: ForeignKeyInfo,
table: TableInfo,
dbs: MultipleDatabaseInfo,
config: PerspectiveConfig,
setConfig: ChangePerspectiveConfigFunc,
public dataProvider: PerspectiveDataProvider,
databaseConfig: PerspectiveDatabaseConfig,
public isMultiple: boolean,
parentNode: PerspectiveTreeNode
) {
super(table, dbs, config, setConfig, dataProvider, databaseConfig, parentNode);
}
matchChildRow(parentRow: any, childRow: any): boolean {
if (!this.foreignKey) return false;
return parentRow[this.foreignKey.columns[0].refColumnName] == childRow[this.foreignKey.columns[0].columnName];
}
getChildMatchColumns() {
if (!this.foreignKey) return [];
return [this.foreignKey.columns[0].refColumnName];
}
getParentMatchColumns() {
if (!this.foreignKey) return [];
return [this.foreignKey.columns[0].columnName];
}
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
if (!this.foreignKey) return null;
return {
schemaName: this.table.schemaName,
pureName: this.table.pureName,
bindingColumns: [this.foreignKey.columns[0].columnName],
bindingValues: _uniqBy(
parentRows.map(row => [row[this.foreignKey.columns[0].refColumnName]]),
stableStringify
),
dataColumns: this.getDataLoadColumns(),
databaseConfig: this.databaseConfig,
orderBy: this.getOrderBy(this.table),
condition: this.getChildrenCondition(),
};
}
get columnTitle() {
return this.table.pureName;
}
get title() {
if (this.isMultiple) {
return `${super.title} (${this.foreignKey.columns.map(x => x.columnName).join(', ')})`;
}
return super.title;
}
get codeName() {
if (this.isMultiple) {
return `${super.codeName}-${this.foreignKey.columns.map(x => x.columnName).join('_')}`;
}
return super.codeName;
}
getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
if (!this.foreignKey) return [];
return this.foreignKey.columns.map(column => {
const res: Condition = {
conditionType: 'binary',
operator: '=',
left: {
exprType: 'column',
columnName: column.refColumnName,
source: { alias: parentAlias },
},
right: {
exprType: 'column',
columnName: column.columnName,
source: { alias },
},
};
return res;
});
}
}
export class PerspectiveCustomJoinTreeNode extends PerspectiveTableNode {
constructor(
public customJoin: PerspectiveCustomJoinConfig,
table: TableInfo | ViewInfo,
dbs: MultipleDatabaseInfo,
config: PerspectiveConfig,
setConfig: ChangePerspectiveConfigFunc,
public dataProvider: PerspectiveDataProvider,
databaseConfig: PerspectiveDatabaseConfig,
parentNode: PerspectiveTreeNode
) {
super(table, dbs, config, setConfig, dataProvider, databaseConfig, parentNode);
}
matchChildRow(parentRow: any, childRow: any): boolean {
for (const column of this.customJoin.columns) {
if (parentRow[column.baseColumnName] != childRow[column.refColumnName]) {
return false;
}
}
return true;
}
getChildMatchColumns() {
return this.customJoin.columns.map(x => x.baseColumnName);
}
getParentMatchColumns() {
return this.customJoin.columns.map(x => x.refColumnName);
}
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
// console.log('CUSTOM JOIN', this.customJoin);
// console.log('this.getDataLoadColumns()', this.getDataLoadColumns());
return {
schemaName: this.table.schemaName,
pureName: this.table.pureName,
bindingColumns: this.getParentMatchColumns(),
bindingValues: _uniqBy(
parentRows.map(row => this.customJoin.columns.map(x => row[x.baseColumnName])),
stableStringify
),
dataColumns: this.getDataLoadColumns(),
databaseConfig: this.databaseConfig,
orderBy: this.getOrderBy(this.table),
condition: this.getChildrenCondition(),
};
}
get title() {
return this.customJoin.joinName;
}
get icon() {
return 'icon custom-join';
}
get codeName() {
return this.customJoin.joinid;
}
get customJoinConfig(): PerspectiveCustomJoinConfig {
return this.customJoin;
}
getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
return this.customJoin.columns.map(column => {
const res: Condition = {
conditionType: 'binary',
operator: '=',
left: {
exprType: 'column',
columnName: column.baseColumnName,
source: { alias: parentAlias },
},
right: {
exprType: 'column',
columnName: column.refColumnName,
source: { alias },
},
};
return res;
});
}
}
export function getTableChildPerspectiveNodes(
table: TableInfo | ViewInfo,
dbs: MultipleDatabaseInfo,
config: PerspectiveConfig,
setConfig: ChangePerspectiveConfigFunc,
dataProvider: PerspectiveDataProvider,
databaseConfig: PerspectiveDatabaseConfig,
parentColumn: PerspectiveTreeNode
) {
if (!table) return [];
const db = parentColumn.db;
const columnNodes = table.columns.map(
col =>
new PerspectiveTableColumnNode(col, table, dbs, config, setConfig, dataProvider, databaseConfig, parentColumn)
);
const circularColumns = columnNodes.filter(x => x.isCircular).map(x => x.columnName);
const defaultColumns = getPerspectiveDefaultColumns(table, db, circularColumns);
for (const node of columnNodes) {
node.defaultChecked = defaultColumns.includes(node.columnName);
}
const res = [];
res.push(...columnNodes);
const dependencies = [];
if (db && (table as TableInfo)?.dependencies) {
for (const fk of (table as TableInfo)?.dependencies) {
const tbl = db.tables.find(x => x.pureName == fk.pureName && x.schemaName == fk.schemaName);
if (tbl) {
const isMultiple =
(table as TableInfo)?.dependencies.filter(x => x.pureName == fk.pureName && x.schemaName == fk.schemaName)
.length >= 2;
dependencies.push(
new PerspectiveTableReferenceNode(
fk,
tbl,
dbs,
config,
setConfig,
dataProvider,
databaseConfig,
isMultiple,
parentColumn
)
);
}
}
}
res.push(..._sortBy(dependencies, 'title'));
const customs = [];
for (const join of config.customJoins || []) {
if (join.baseUniqueName == parentColumn.uniqueName) {
const newConfig = { ...databaseConfig };
if (join.conid) newConfig.conid = join.conid;
if (join.database) newConfig.database = join.database;
const db = dbs?.[newConfig.conid]?.[newConfig.database];
const table = db?.tables?.find(x => x.pureName == join.refTableName && x.schemaName == join.refSchemaName);
const view = db?.views?.find(x => x.pureName == join.refTableName && x.schemaName == join.refSchemaName);
if (table || view) {
customs.push(
new PerspectiveCustomJoinTreeNode(
join,
table || view,
dbs,
config,
setConfig,
dataProvider,
newConfig,
parentColumn
)
);
}
}
}
res.push(..._sortBy(customs, 'title'));
return res;
}
+3 -6
View File
@@ -267,12 +267,9 @@ export class TableGridDisplay extends GridDisplay {
) {
for (const column of columns) {
if (this.addAllExpandedColumnsToSelected || this.config.addedColumns.includes(column.uniqueName)) {
select.columns.push({
exprType: 'column',
columnName: column.columnName,
alias: column.uniqueName,
source: { name: column, alias: parentAlias },
});
select.columns.push(
this.createColumnExpression(column, { name: column, alias: parentAlias }, column.uniqueName)
);
displayedColumnInfo[column.uniqueName] = {
...column,
sourceAlias: parentAlias,
@@ -0,0 +1,33 @@
import { findForeignKeyForColumn } from 'dbgate-tools';
import { DatabaseInfo, TableInfo, ViewInfo } from 'dbgate-types';
export function getPerspectiveDefaultColumns(
table: TableInfo | ViewInfo,
db: DatabaseInfo,
circularColumns: string[]
): string[] {
const columns = table.columns.map(x => x.columnName);
const predicates = [
x => x.toLowerCase() == 'name',
x => x.toLowerCase() == 'title',
x => x.toLowerCase().includes('name'),
x => x.toLowerCase().includes('title'),
x => x.toLowerCase().includes('subject'),
// x => x.toLowerCase().includes('text'),
// x => x.toLowerCase().includes('desc'),
x =>
table.columns
.find(y => y.columnName == x)
?.dataType?.toLowerCase()
?.includes('char'),
x => findForeignKeyForColumn(table as TableInfo, x)?.columns?.length == 1 && !circularColumns.includes(x),
x => findForeignKeyForColumn(table as TableInfo, x)?.columns?.length == 1,
];
for (const predicate of predicates) {
const col = columns.find(predicate);
if (col) return [col];
}
return [columns[0]];
}
+6
View File
@@ -1,5 +1,7 @@
export * from './GridDisplay';
export * from './GridConfig';
export * from './PerspectiveConfig';
export * from './PerspectiveTreeNode';
export * from './TableGridDisplay';
export * from './ViewGridDisplay';
export * from './JslGridDisplay';
@@ -12,3 +14,7 @@ export * from './FormViewDisplay';
export * from './TableFormViewDisplay';
export * from './CollectionGridDisplay';
export * from './deleteCascade';
export * from './PerspectiveDisplay';
export * from './PerspectiveDataProvider';
export * from './PerspectiveCache';
export * from './PerspectiveConfig';
@@ -0,0 +1,122 @@
import { TableInfo } from 'dbgate-types';
import { PerspectiveDisplay } from '../PerspectiveDisplay';
import { PerspectiveTableNode } from '../PerspectiveTreeNode';
import { chinookDbInfo } from './chinookDbInfo';
import { createPerspectiveConfig } from '../PerspectiveConfig';
import artistDataFlat from './artistDataFlat';
import artistDataAlbum from './artistDataAlbum';
import artistDataAlbumTrack from './artistDataAlbumTrack';
test('test flat view', () => {
const artistTable = chinookDbInfo.tables.find(x => x.pureName == 'Artist');
const root = new PerspectiveTableNode(
artistTable,
{ conid: { db: chinookDbInfo } },
createPerspectiveConfig({ pureName: 'Artist' }),
null,
null,
{ conid: 'conid', database: 'db' },
null
);
const display = new PerspectiveDisplay(root, artistDataFlat);
// console.log(display.loadIndicatorsCounts);
// console.log(display.rows);
expect(display.rows.length).toEqual(4);
expect(display.rows[0]).toEqual(
expect.objectContaining({
rowData: ['AC/DC'],
})
);
expect(display.loadIndicatorsCounts).toEqual({
Artist: 4,
});
});
test('test one level nesting', () => {
const artistTable = chinookDbInfo.tables.find(x => x.pureName == 'Artist');
const root = new PerspectiveTableNode(
artistTable,
{ conid: { db: chinookDbInfo } },
{ ...createPerspectiveConfig({ pureName: 'Artist' }), checkedColumns: ['Artist::Album'] },
null,
null,
{ conid: 'conid', database: 'db' },
null
);
const display = new PerspectiveDisplay(root, artistDataAlbum);
console.log(display.loadIndicatorsCounts);
// console.log(display.rows);
expect(display.rows.length).toEqual(6);
expect(display.rows[0]).toEqual(
expect.objectContaining({
rowData: ['AC/DC', 'For Those About To Rock We Salute You'],
rowSpans: [2, 1],
rowCellSkips: [false, false],
})
);
expect(display.rows[1]).toEqual(
expect.objectContaining({
rowData: [undefined, 'Let There Be Rock'],
rowSpans: [1, 1],
rowCellSkips: [true, false],
})
);
expect(display.rows[2]).toEqual(
expect.objectContaining({
rowData: ['Accept', 'Balls to the Wall'],
rowSpans: [2, 1],
rowCellSkips: [false, false],
})
);
expect(display.rows[5]).toEqual(
expect.objectContaining({
rowData: ['Alanis Morissette', 'Jagged Little Pill'],
rowSpans: [1, 1],
})
);
expect(display.loadIndicatorsCounts).toEqual({
Artist: 6,
'Artist.Album': 6,
});
});
test('test two level nesting', () => {
const artistTable = chinookDbInfo.tables.find(x => x.pureName == 'Artist');
const root = new PerspectiveTableNode(
artistTable,
{ conid: { db: chinookDbInfo } },
{ ...createPerspectiveConfig({ pureName: 'Artist' }), checkedColumns: ['Artist::Album', 'Artist::Album::Track'] },
null,
null,
{ conid: 'conid', database: 'db' },
null
);
const display = new PerspectiveDisplay(root, artistDataAlbumTrack);
console.log(display.rows);
expect(display.rows.length).toEqual(8);
expect(display.rows[0]).toEqual(
expect.objectContaining({
rowData: ['AC/DC', 'For Those About To Rock We Salute You', 'For Those About To Rock (We Salute You)'],
rowSpans: [4, 2, 1],
rowCellSkips: [false, false, false],
})
);
expect(display.rows[1]).toEqual(
expect.objectContaining({
rowData: [undefined, undefined, 'Put The Finger On You'],
rowSpans: [1, 1, 1],
rowCellSkips: [true, true, false],
})
);
expect(display.rows[2]).toEqual(
expect.objectContaining({
rowData: [undefined, 'Let There Be Rock', 'Go Down'],
rowSpans: [1, 2, 1],
rowCellSkips: [true, false, false],
})
);
});
@@ -0,0 +1,56 @@
export default [
{
ArtistId: 1,
Name: 'AC/DC',
Album: [
{
Title: 'For Those About To Rock We Salute You',
ArtistId: 1,
},
{
Title: 'Let There Be Rock',
ArtistId: 1,
},
],
},
{
ArtistId: 2,
Name: 'Accept',
Album: [
{
Title: 'Balls to the Wall',
ArtistId: 2,
},
{
Title: 'Restless and Wild',
ArtistId: 2,
},
],
},
{
ArtistId: 3,
Name: 'Aerosmith',
Album: [
{
Title: 'Big Ones',
ArtistId: 3,
},
],
},
{
ArtistId: 4,
Name: 'Alanis Morissette',
Album: [
{
Title: 'Jagged Little Pill',
ArtistId: 4,
},
{
incompleteRowsIndicator: ['Artist.Album'],
},
],
},
{
incompleteRowsIndicator: ['Artist'],
},
];
@@ -0,0 +1,78 @@
export default [
{
ArtistId: 1,
Name: 'AC/DC',
Album: [
{
Title: 'For Those About To Rock We Salute You',
AlbumId: 1,
ArtistId: 1,
Track: [
{
Name: 'For Those About To Rock (We Salute You)',
AlbumId: 1,
},
{
Name: 'Put The Finger On You',
AlbumId: 1,
},
],
},
{
Title: 'Let There Be Rock',
AlbumId: 4,
ArtistId: 1,
Track: [
{
Name: 'Go Down',
AlbumId: 4,
},
{
Name: 'Dog Eat Dog',
AlbumId: 4,
},
],
},
],
},
{
ArtistId: 2,
Name: 'Accept',
Album: [
{
Title: 'Balls to the Wall',
AlbumId: 2,
ArtistId: 2,
Track: [
{
Name: 'Balls to the Wall',
AlbumId: 2,
},
],
},
{
Title: 'Restless and Wild',
AlbumId: 3,
ArtistId: 2,
Track: [
{
Name: 'Fast As a Shark',
AlbumId: 3,
},
{
Name: 'Restless and Wild',
AlbumId: 3,
},
{
Name: 'Princess of the Dawn',
AlbumId: 3,
},
],
},
],
},
{
incompleteRowsIndicator: ['Artist'],
},
];
@@ -0,0 +1,21 @@
export default [
{
ArtistId: 1,
Name: 'AC/DC',
},
{
ArtistId: 2,
Name: 'Accept',
},
{
ArtistId: 3,
Name: 'Aerosmith',
},
{
ArtistId: 4,
Name: 'Alanis Morissette',
},
{
incompleteRowsIndicator: ['Artist'],
},
];
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -16,8 +16,8 @@
"dbgate-types": "^5.0.0-alpha.1",
"@types/jest": "^25.1.4",
"@types/node": "^13.7.0",
"jest": "^24.9.0",
"ts-jest": "^25.2.1",
"jest": "^28.1.3",
"ts-jest": "^28.0.7",
"typescript": "^4.4.3"
},
"dependencies": {
+279
View File
@@ -0,0 +1,279 @@
import P from 'parsimmon';
import moment from 'moment';
import { FilterType } from './types';
import { Condition } from 'dbgate-sqltree';
import { TransformType } from 'dbgate-types';
import { interpretEscapes, token, word, whitespace } from './common';
const compoudCondition = conditionType => conditions => {
if (conditions.length == 1) return conditions[0];
return {
[conditionType]: conditions,
};
};
function getTransformCondition(transform: TransformType, value) {
return {
conditionType: 'binary',
operator: '=',
left: {
exprType: 'transform',
transform,
expr: {
exprType: 'placeholder',
},
},
right: {
exprType: 'value',
value,
},
};
}
const yearCondition = () => value => {
return getTransformCondition('YEAR', value);
};
const yearMonthCondition = () => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)/);
return {
conditionType: 'and',
conditions: [getTransformCondition('YEAR', m[1]), getTransformCondition('MONTH', m[2])],
};
};
const yearMonthDayCondition = () => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)/);
return {
conditionType: 'and',
conditions: [
getTransformCondition('YEAR', m[1]),
getTransformCondition('MONTH', m[2]),
getTransformCondition('DAY', m[3]),
],
};
};
const yearEdge = edgeFunction => value => {
return moment(new Date(parseInt(value), 0, 1))
[edgeFunction]('year')
.format('YYYY-MM-DDTHH:mm:ss.SSS');
};
const yearMonthEdge = edgeFunction => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)/);
return moment(new Date(parseInt(m[1]), parseInt(m[2]) - 1, 1))
[edgeFunction]('month')
.format('YYYY-MM-DDTHH:mm:ss.SSS');
};
const yearMonthDayEdge = edgeFunction => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)/);
return moment(new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3])))
[edgeFunction]('day')
.format('YYYY-MM-DDTHH:mm:ss.SSS');
};
const yearMonthDayMinuteEdge = edgeFunction => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)\s+(\d\d?):(\d\d?)/);
const year = m[1];
const month = m[2];
const day = m[3];
const hour = m[4];
const minute = m[5];
const dateObject = new Date(year, month - 1, day, hour, minute);
return moment(dateObject)[edgeFunction]('minute').format('YYYY-MM-DDTHH:mm:ss.SSS');
};
const yearMonthDayMinuteSecondEdge = edgeFunction => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)(T|\s+)(\d\d?):(\d\d?):(\d\d?)/);
const year = m[1];
const month = m[2];
const day = m[3];
const hour = m[5];
const minute = m[6];
const second = m[7];
const dateObject = new Date(year, month - 1, day, hour, minute, second);
return moment(dateObject)[edgeFunction]('second').format('YYYY-MM-DDTHH:mm:ss.SSS');
};
const createIntervalCondition = (start, end) => {
return {
conditionType: 'and',
conditions: [
{
conditionType: 'binary',
operator: '>=',
left: {
exprType: 'placeholder',
},
right: {
exprType: 'value',
value: start,
},
},
{
conditionType: 'binary',
operator: '<=',
left: {
exprType: 'placeholder',
},
right: {
exprType: 'value',
value: end,
},
},
],
};
};
const createDateIntervalCondition = (start, end) => {
return createIntervalCondition(start.format('YYYY-MM-DDTHH:mm:ss.SSS'), end.format('YYYY-MM-DDTHH:mm:ss.SSS'));
};
const fixedMomentIntervalCondition = (intervalType, diff) => () => {
return createDateIntervalCondition(
moment().add(intervalType, diff).startOf(intervalType),
moment().add(intervalType, diff).endOf(intervalType)
);
};
const yearMonthDayMinuteCondition = () => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)\s+(\d\d?):(\d\d?)/);
const year = m[1];
const month = m[2];
const day = m[3];
const hour = m[4];
const minute = m[5];
const dateObject = new Date(year, month - 1, day, hour, minute);
return createDateIntervalCondition(moment(dateObject).startOf('minute'), moment(dateObject).endOf('minute'));
};
const yearMonthDaySecondCondition = () => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)(T|\s+)(\d\d?):(\d\d?):(\d\d?)/);
const year = m[1];
const month = m[2];
const day = m[3];
const hour = m[5];
const minute = m[6];
const second = m[7];
const dateObject = new Date(year, month - 1, day, hour, minute, second);
return createDateIntervalCondition(moment(dateObject).startOf('second'), moment(dateObject).endOf('second'));
};
const binaryCondition = operator => value => ({
conditionType: 'binary',
operator,
left: {
exprType: 'placeholder',
},
right: {
exprType: 'value',
value,
},
});
const createParser = () => {
const langDef = {
comma: () => word(','),
yearNum: () => P.regexp(/\d\d\d\d/).map(yearCondition()),
yearMonthNum: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthCondition()),
yearMonthDayNum: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayCondition()),
yearMonthDayMinute: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteCondition()),
yearMonthDaySecond: () =>
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDaySecondCondition()),
yearNumStart: () => P.regexp(/\d\d\d\d/).map(yearEdge('startOf')),
yearNumEnd: () => P.regexp(/\d\d\d\d/).map(yearEdge('endOf')),
yearMonthStart: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthEdge('startOf')),
yearMonthEnd: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthEdge('endOf')),
yearMonthDayStart: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayEdge('startOf')),
yearMonthDayEnd: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayEdge('endOf')),
yearMonthDayMinuteStart: () =>
P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteEdge('startOf')),
yearMonthDayMinuteEnd: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteEdge('endOf')),
yearMonthDayMinuteSecondStart: () =>
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDayMinuteSecondEdge('startOf')),
yearMonthDayMinuteSecondEnd: () =>
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDayMinuteSecondEdge('endOf')),
this: () => word('THIS'),
last: () => word('LAST'),
next: () => word('NEXT'),
week: () => word('WEEK'),
month: () => word('MONTH'),
year: () => word('YEAR'),
yesterday: () => word('YESTERDAY').map(fixedMomentIntervalCondition('day', -1)),
today: () => word('TODAY').map(fixedMomentIntervalCondition('day', 0)),
tomorrow: () => word('TOMORROW').map(fixedMomentIntervalCondition('day', 1)),
lastWeek: r => r.last.then(r.week).map(fixedMomentIntervalCondition('week', -1)),
thisWeek: r => r.this.then(r.week).map(fixedMomentIntervalCondition('week', 0)),
nextWeek: r => r.next.then(r.week).map(fixedMomentIntervalCondition('week', 1)),
lastMonth: r => r.last.then(r.month).map(fixedMomentIntervalCondition('month', -1)),
thisMonth: r => r.this.then(r.month).map(fixedMomentIntervalCondition('month', 0)),
nextMonth: r => r.next.then(r.month).map(fixedMomentIntervalCondition('month', 1)),
lastYear: r => r.last.then(r.year).map(fixedMomentIntervalCondition('year', -1)),
thisYear: r => r.this.then(r.year).map(fixedMomentIntervalCondition('year', 0)),
nextYear: r => r.next.then(r.year).map(fixedMomentIntervalCondition('year', 1)),
valueStart: r =>
P.alt(
r.yearMonthDayMinuteSecondStart,
r.yearMonthDayMinuteStart,
r.yearMonthDayStart,
r.yearMonthStart,
r.yearNumStart
),
valueEnd: r =>
P.alt(r.yearMonthDayMinuteSecondEnd, r.yearMonthDayMinuteEnd, r.yearMonthDayEnd, r.yearMonthEnd, r.yearNumEnd),
le: r => word('<=').then(r.valueEnd).map(binaryCondition('<=')),
ge: r => word('>=').then(r.valueStart).map(binaryCondition('>=')),
lt: r => word('<').then(r.valueStart).map(binaryCondition('<')),
gt: r => word('>').then(r.valueEnd).map(binaryCondition('>')),
element: r =>
P.alt(
r.yearMonthDaySecond,
r.yearMonthDayMinute,
r.yearMonthDayNum,
r.yearMonthNum,
r.yearNum,
r.yesterday,
r.today,
r.tomorrow,
r.lastWeek,
r.thisWeek,
r.nextWeek,
r.lastMonth,
r.thisMonth,
r.nextMonth,
r.lastYear,
r.thisYear,
r.nextYear,
r.le,
r.lt,
r.ge,
r.gt
).trim(whitespace),
factor: r => r.element.sepBy(whitespace).map(compoudCondition('$and')),
list: r => r.factor.sepBy(r.comma).map(compoudCondition('$or')),
};
return P.createLanguage(langDef);
};
export const datetimeParser = createParser();
+1 -1
View File
@@ -3,7 +3,7 @@ import moment from 'moment';
export type FilterMultipleValuesMode = 'is' | 'is_not' | 'contains' | 'begins' | 'ends';
export function getFilterValueExpression(value, dataType) {
export function getFilterValueExpression(value, dataType?) {
if (value == null) return 'NULL';
if (isTypeDateTime(dataType)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
if (value === true) return 'TRUE';
+7 -166
View File
@@ -5,6 +5,7 @@ import { Condition } from 'dbgate-sqltree';
import { TransformType } from 'dbgate-types';
import { interpretEscapes, token, word, whitespace } from './common';
import { mongoParser } from './mongoParser';
import { datetimeParser } from './datetimeParser';
const binaryCondition = operator => value => ({
conditionType: 'binary',
@@ -67,116 +68,6 @@ const negateCondition = condition => {
};
};
function getTransformCondition(transform: TransformType, value) {
return {
conditionType: 'binary',
operator: '=',
left: {
exprType: 'transform',
transform,
expr: {
exprType: 'placeholder',
},
},
right: {
exprType: 'value',
value,
},
};
}
const yearCondition = () => value => {
return getTransformCondition('YEAR', value);
};
const yearMonthCondition = () => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)/);
return {
conditionType: 'and',
conditions: [getTransformCondition('YEAR', m[1]), getTransformCondition('MONTH', m[2])],
};
};
const yearMonthDayCondition = () => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)/);
return {
conditionType: 'and',
conditions: [
getTransformCondition('YEAR', m[1]),
getTransformCondition('MONTH', m[2]),
getTransformCondition('DAY', m[3]),
],
};
};
const createIntervalCondition = (start, end) => {
return {
conditionType: 'and',
conditions: [
{
conditionType: 'binary',
operator: '>=',
left: {
exprType: 'placeholder',
},
right: {
exprType: 'value',
value: start,
},
},
{
conditionType: 'binary',
operator: '<=',
left: {
exprType: 'placeholder',
},
right: {
exprType: 'value',
value: end,
},
},
],
};
};
const createDateIntervalCondition = (start, end) => {
return createIntervalCondition(start.format('YYYY-MM-DDTHH:mm:ss.SSS'), end.format('YYYY-MM-DDTHH:mm:ss.SSS'));
};
const fixedMomentIntervalCondition = (intervalType, diff) => () => {
return createDateIntervalCondition(
moment().add(intervalType, diff).startOf(intervalType),
moment().add(intervalType, diff).endOf(intervalType)
);
};
const yearMonthDayMinuteCondition = () => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)\s+(\d\d?):(\d\d?)/);
const year = m[1];
const month = m[2];
const day = m[3];
const hour = m[4];
const minute = m[5];
const dateObject = new Date(year, month - 1, day, hour, minute);
return createDateIntervalCondition(moment(dateObject).startOf('minute'), moment(dateObject).endOf('minute'));
};
const yearMonthDaySecondCondition = () => value => {
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)(T|\s+)(\d\d?):(\d\d?):(\d\d?)/);
const year = m[1];
const month = m[2];
const day = m[3];
const hour = m[5];
const minute = m[6];
const second = m[7];
const dateObject = new Date(year, month - 1, day, hour, minute, second);
return createDateIntervalCondition(moment(dateObject).startOf('second'), moment(dateObject).endOf('second'));
};
const createParser = (filterType: FilterType) => {
const langDef = {
string1: () =>
@@ -206,13 +97,6 @@ const createParser = (filterType: FilterType) => {
noQuotedString: () => P.regexp(/[^\s^,^'^"]+/).desc('string unquoted'),
yearNum: () => P.regexp(/\d\d\d\d/).map(yearCondition()),
yearMonthNum: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthCondition()),
yearMonthDayNum: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayCondition()),
yearMonthDayMinute: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteCondition()),
yearMonthDaySecond: () =>
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDaySecondCondition()),
value: r => P.alt(...allowedValues.map(x => r[x])),
valueTestEq: r => r.value.map(binaryCondition('=')),
valueTestStr: r => r.value.map(likeCondition('like', '%#VALUE#%')),
@@ -223,33 +107,10 @@ const createParser = (filterType: FilterType) => {
null: () => word('NULL').map(unaryCondition('isNull')),
empty: () => word('EMPTY').map(unaryCondition('isEmpty')),
notEmpty: r => r.not.then(r.empty).map(unaryCondition('isNotEmpty')),
true: () => P.regexp(/true/i).map(binaryFixedValueCondition(1)),
false: () => P.regexp(/false/i).map(binaryFixedValueCondition(0)),
trueNum: () => word('1').map(binaryFixedValueCondition(1)),
falseNum: () => word('0').map(binaryFixedValueCondition(0)),
this: () => word('THIS'),
last: () => word('LAST'),
next: () => word('NEXT'),
week: () => word('WEEK'),
month: () => word('MONTH'),
year: () => word('YEAR'),
yesterday: () => word('YESTERDAY').map(fixedMomentIntervalCondition('day', -1)),
today: () => word('TODAY').map(fixedMomentIntervalCondition('day', 0)),
tomorrow: () => word('TOMORROW').map(fixedMomentIntervalCondition('day', 1)),
lastWeek: r => r.last.then(r.week).map(fixedMomentIntervalCondition('week', -1)),
thisWeek: r => r.this.then(r.week).map(fixedMomentIntervalCondition('week', 0)),
nextWeek: r => r.next.then(r.week).map(fixedMomentIntervalCondition('week', 1)),
lastMonth: r => r.last.then(r.month).map(fixedMomentIntervalCondition('month', -1)),
thisMonth: r => r.this.then(r.month).map(fixedMomentIntervalCondition('month', 0)),
nextMonth: r => r.next.then(r.month).map(fixedMomentIntervalCondition('month', 1)),
lastYear: r => r.last.then(r.year).map(fixedMomentIntervalCondition('year', -1)),
thisYear: r => r.this.then(r.year).map(fixedMomentIntervalCondition('year', 0)),
nextYear: r => r.next.then(r.year).map(fixedMomentIntervalCondition('year', 1)),
true: () => P.regexp(/true/i).map(binaryFixedValueCondition('1')),
false: () => P.regexp(/false/i).map(binaryFixedValueCondition('0')),
trueNum: () => word('1').map(binaryFixedValueCondition('1')),
falseNum: () => word('0').map(binaryFixedValueCondition('0')),
eq: r => word('=').then(r.value).map(binaryCondition('=')),
ne: r => word('!=').then(r.value).map(binaryCondition('<>')),
@@ -294,27 +155,7 @@ const createParser = (filterType: FilterType) => {
if (filterType == 'eval') {
allowedElements.push('true', 'false');
}
if (filterType == 'datetime') {
allowedElements.push(
'yearMonthDaySecond',
'yearMonthDayMinute',
'yearMonthDayNum',
'yearMonthNum',
'yearNum',
'yesterday',
'today',
'tomorrow',
'lastWeek',
'thisWeek',
'nextWeek',
'lastMonth',
'thisMonth',
'nextMonth',
'lastYear',
'thisYear',
'nextYear'
);
}
// must be last
if (filterType == 'string' || filterType == 'eval') {
allowedElements.push('valueTestStr');
@@ -328,10 +169,10 @@ const createParser = (filterType: FilterType) => {
const parsers = {
number: createParser('number'),
string: createParser('string'),
datetime: createParser('datetime'),
logical: createParser('logical'),
eval: createParser('eval'),
mongo: mongoParser,
datetime: datetimeParser,
};
export function parseFilter(value: string, filterType: FilterType): Condition {
@@ -1,4 +1,4 @@
import { parseFilter } from './parseFilter';
const { parseFilter } = require('./parseFilter');
test('parse string', () => {
const ast = parseFilter('"123"', 'string');
+4 -1
View File
@@ -1,7 +1,6 @@
import { SqlDumper } from 'dbgate-types';
import { Condition, BinaryCondition } from './types';
import { dumpSqlExpression } from './dumpSqlExpression';
import { link } from 'fs';
import { dumpSqlSelect } from './dumpSqlCommand';
export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
@@ -69,5 +68,9 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
dmp.put(' ^and ');
dumpSqlExpression(dmp, condition.right);
break;
case 'in':
dumpSqlExpression(dmp, condition.expr);
dmp.put(' ^in (%,v)', condition.values);
break;
}
}
+9 -2
View File
@@ -35,17 +35,24 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
dmp.put(')');
break;
case 'methodCall':
dumpSqlExpression(dmp, expr.thisObject)
dmp.put('.%s(', expr.method);
dmp.putCollection(',', expr.args, x => dumpSqlExpression(dmp, x));
dmp.put(')');
break;
case 'transform':
dmp.transform(expr.transform, () => dumpSqlExpression(dmp, expr.expr));
break;
case 'rowNumber':
dmp.put(" ^row_number() ^over (^order ^by ");
dmp.put(' ^row_number() ^over (^order ^by ');
dmp.putCollection(', ', expr.orderBy, x => {
dumpSqlExpression(dmp, x);
dmp.put(' %k', x.direction);
});
dmp.put(")");
dmp.put(')');
break;
}
}
@@ -20,6 +20,9 @@ export function evaluateExpression(expr: Expression, values) {
case 'call':
return null;
case 'methodCall':
return null;
case 'transform':
return null;
}
+16 -1
View File
@@ -99,6 +99,12 @@ export interface BetweenCondition {
right: Expression;
}
export interface InCondition {
conditionType: 'in';
expr: Expression;
values: any[];
}
export type Condition =
| BinaryCondition
| NotCondition
@@ -107,7 +113,8 @@ export type Condition =
| LikeCondition
| ExistsCondition
| NotExistsCondition
| BetweenCondition;
| BetweenCondition
| InCondition;
export interface Source {
name?: NamedObjectInfo;
@@ -155,6 +162,13 @@ export interface CallExpression {
argsPrefix?: string; // DISTINCT in case of COUNT DISTINCT
}
export interface MethodCallExpression {
exprType: 'methodCall';
method: string;
args: Expression[];
thisObject: Expression;
}
export interface TranformExpression {
exprType: 'transform';
expr: Expression;
@@ -172,6 +186,7 @@ export type Expression =
| PlaceholderExpression
| RawExpression
| CallExpression
| MethodCallExpression
| TranformExpression
| RowNumberExpression;
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
+3 -1
View File
@@ -31,9 +31,11 @@
"typescript": "^4.4.3"
},
"dependencies": {
"lodash": "^4.17.21",
"dbgate-query-splitter": "^4.9.0",
"dbgate-sqltree": "^5.0.0-alpha.1",
"debug": "^4.3.4",
"json-stable-stringify": "^1.0.1",
"lodash": "^4.17.21",
"uuid": "^3.4.0"
}
}
+20 -3
View File
@@ -1,4 +1,9 @@
import _compact from 'lodash/compact';
import _isString from 'lodash/isString';
export interface FilterNameDefinition {
childName: string;
}
// original C# variant
// public bool Match(string value)
@@ -54,17 +59,29 @@ function fuzzysearch(needle, haystack) {
return true;
}
export function filterName(filter: string, ...names: string[]) {
export function filterName(filter: string, ...names: (string | FilterNameDefinition)[]) {
if (!filter) return true;
// const camelVariants = [name.replace(/[^A-Z]/g, '')]
const tokens = filter.split(' ').map(x => x.trim());
const namesCompacted = _compact(names);
// @ts-ignore
const namesOwn: string[] = namesCompacted.filter(x => _isString(x));
// @ts-ignore
const namesChild: string[] = namesCompacted.filter(x => x.childName).map(x => x.childName);
for (const token of tokens) {
const tokenUpper = token.toUpperCase();
const found = namesCompacted.find(name => fuzzysearch(tokenUpper, name.toUpperCase()));
if (!found) return false;
if (tokenUpper.startsWith('#')) {
const tokenUpperSub = tokenUpper.substring(1);
const found = namesChild.find(name => fuzzysearch(tokenUpperSub, name.toUpperCase()));
if (!found) return false;
} else {
const found = namesOwn.find(name => fuzzysearch(tokenUpper, name.toUpperCase()));
if (!found) return false;
}
}
return true;
+7 -3
View File
@@ -1,4 +1,5 @@
import _ from 'lodash';
import _cloneDeep from 'lodash/cloneDeep';
import _isString from 'lodash/isString';
import { ColumnInfo, ColumnReference, DatabaseInfo, DatabaseInfoObjects, SqlDialect, TableInfo } from 'dbgate-types';
export function fullNameFromString(name) {
@@ -54,7 +55,10 @@ export function findObjectLike(
return dbinfo[objectTypeField]?.find(x => equalStringLike(x.pureName, pureName));
}
export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo) {
export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo | string) {
if (_isString(column)) {
return (table.foreignKeys || []).find(fk => fk.columns.find(col => col.columnName == column));
}
return (table.foreignKeys || []).find(fk => fk.columns.find(col => col.columnName == column.columnName));
}
@@ -76,7 +80,7 @@ function columnsConstraintName(prefix: string, table: TableInfo, columns: Column
export function fillConstraintNames(table: TableInfo, dialect: SqlDialect) {
if (!table) return table;
const res = _.cloneDeep(table);
const res = _cloneDeep(table);
if (res.primaryKey && !res.primaryKey.constraintName && !dialect.anonymousPrimaryKey) {
res.primaryKey.constraintName = `PK_${res.pureName}`;
}
+9
View File
@@ -84,3 +84,12 @@ export function getIconForRedisType(type) {
return null;
}
}
export function isWktGeometry(s) {
if (!_isString(s)) return false;
// return !!s.match(/^POINT\s*\(|/)
return !!s.match(
/^POINT\s*\(|^LINESTRING\s*\(|^POLYGON\s*\(|^MULTIPOINT\s*\(|^MULTILINESTRING\s*\(|^MULTIPOLYGON\s*\(|^GEOMCOLLECTION\s*\(|^GEOMETRYCOLLECTION\s*\(/
);
}
+4
View File
@@ -1,4 +1,5 @@
import { ColumnInfo, TableInfo, ForeignKeyInfo, DatabaseInfo } from 'dbgate-types';
import { StringNullableChain } from 'lodash';
import _cloneDeep from 'lodash/cloneDeep';
import _compact from 'lodash/compact';
import { DatabaseAnalyser } from './DatabaseAnalyser';
@@ -11,6 +12,7 @@ export interface ColumnInfoYaml {
autoIncrement?: boolean;
references?: string;
primaryKey?: boolean;
default?: string;
}
export interface DatabaseModelFile {
@@ -39,6 +41,7 @@ function columnInfoToYaml(column: ColumnInfo, table: TableInfo): ColumnInfoYaml
const res: ColumnInfoYaml = {
name: column.columnName,
type: column.dataType,
default: column.defaultValue,
};
if (column.autoIncrement) res.autoIncrement = true;
if (column.notNull) res.notNull = true;
@@ -71,6 +74,7 @@ function columnInfoFromYaml(column: ColumnInfoYaml, table: TableInfoYaml): Colum
dataType: column.length ? `${column.type}(${column.length})` : column.type,
autoIncrement: column.autoIncrement,
notNull: column.notNull || (table.primaryKey && table.primaryKey.includes(column.name)),
defaultValue: column.default,
};
return res;
}
+7 -4
View File
@@ -1,6 +1,8 @@
export interface NamedObjectInfo {
pureName: string;
schemaName?: string;
contentHash?: string;
engine?: string;
}
export interface ColumnReference {
@@ -31,7 +33,8 @@ export interface ForeignKeyInfo extends ColumnsConstraintInfo {
export interface IndexInfo extends ColumnsConstraintInfo {
isUnique: boolean;
indexType: 'normal' | 'clustered' | 'xml' | 'spatial' | 'fulltext';
// indexType: 'normal' | 'clustered' | 'xml' | 'spatial' | 'fulltext';
indexType: string;
}
export interface UniqueInfo extends ColumnsConstraintInfo {}
@@ -43,8 +46,8 @@ export interface CheckInfo extends ConstraintInfo {
export interface ColumnInfo extends NamedObjectInfo {
pairingId?: string;
columnName: string;
notNull: boolean;
autoIncrement: boolean;
notNull?: boolean;
autoIncrement?: boolean;
dataType: string;
precision?: number;
scale?: number;
@@ -119,7 +122,7 @@ export interface DatabaseInfoObjects {
}
export interface DatabaseInfo extends DatabaseInfoObjects {
schemas: SchemaInfo[];
schemas?: SchemaInfo[];
engine?: string;
defaultSchema?: string;
}
+3
View File
@@ -34,4 +34,7 @@ export interface SqlDialect {
disableExplicitTransaction?: boolean;
predefinedDataTypes: string[];
// create sql-tree expression
createColumnViewExpression(columnName: string, dataType: string, source: { alias: string }, alias?: string): any;
}
+4 -1
View File
@@ -57,6 +57,9 @@
"dependencies": {
"chartjs-plugin-zoom": "^1.2.0",
"date-fns": "^2.28.0",
"interval-operations": "^1.0.7"
"debug": "^4.3.4",
"interval-operations": "^1.0.7",
"leaflet": "^1.8.0",
"wellknown": "^0.5.0"
}
}
+18 -1
View File
@@ -2,7 +2,7 @@
import _ from 'lodash';
import FontIcon from './icons/FontIcon.svelte';
import { extensions } from './stores';
import { extensions, isFileDragActive } from './stores';
import getElectron from './utility/getElectron';
@@ -21,6 +21,15 @@
</div>
<div class="title">Drop the files to upload to DbGate</div>
<div class="info">Supported file types: {fileTypeNames.join(', ')}</div>
<div
class="class-button"
on:click={() => {
$isFileDragActive = false;
}}
>
<FontIcon icon="icon close" padRight />
Close
</div>
</div>
</div>
@@ -53,4 +62,12 @@
display: flex;
justify-content: space-around;
}
.class-button {
position: fixed;
top: 20px;
right: 20px;
font-size: 14pt;
cursor: pointer;
}
</style>
@@ -76,6 +76,10 @@
on:dragstart={e => {
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
}}
on:dragstart
on:dragenter
on:dragend
on:drop
>
{#if checkedObjectsStore}
<CheckboxField
@@ -27,11 +27,51 @@
conid: connection._id,
keepOpen: true,
});
if (!config.runAsPortal) {
expandedConnections.update(x => _.uniq([...x, connection._id]));
expandedConnections.update(x => _.uniq([...x, connection._id]));
// if (!config.runAsPortal && getCurrentSettings()['defaultAction.connectionClick'] != 'connect') {
// expandedConnections.update(x => _.uniq([...x, connection._id]));
// }
}
// closeMultipleTabs(x => x.tabComponent == 'ConnectionTab' && x.props?.conid == connection._id, true);
}
export function disconnectServerConnection(conid, showConfirmation = true) {
const closeCondition = x => x.props?.conid == conid && x.tabComponent != 'ConnectionTab' && x.closedTime == null;
if (showConfirmation) {
const count = getOpenedTabs().filter(closeCondition).length;
if (count > 0) {
showModal(ConfirmModal, {
message: `Closing connection will close ${count} opened tabs, continue?`,
onConfirm: () => disconnectServerConnection(conid, false),
});
return;
}
}
closeMultipleTabs(x => x.tabComponent == 'ConnectionTab' && x.props?.conid == connection._id, true);
const electron = getElectron();
const currentDb = getCurrentDatabase();
openedConnections.update(list => list.filter(x => x != conid));
if (electron) {
apiCall('server-connections/disconnect', { conid });
}
if (currentDb?.connection?._id == conid) {
if (electron) {
apiCall('database-connections/disconnect', { conid, database: currentDb.name });
}
currentDatabase.set(null);
}
closeMultipleTabs(closeCondition);
// if (data.unsaved) {
// openNewTab({
// title: 'New Connection',
// icon: 'img connection',
// tabComponent: 'ConnectionTab',
// props: {
// conid: data._id,
// },
// });
// }
}
</script>
@@ -43,7 +83,10 @@
expandedConnections,
extensions,
getCurrentConfig,
getCurrentDatabase,
getCurrentSettings,
getOpenedConnections,
getOpenedTabs,
openedConnections,
openedSingleDatabaseConnections,
} from '../stores';
@@ -61,6 +104,7 @@
import ImportDatabaseDumpModal from '../modals/ImportDatabaseDumpModal.svelte';
import { closeMultipleTabs } from '../widgets/TabsPanel.svelte';
import AboutModal from '../modals/AboutModal.svelte';
import { tick } from 'svelte';
export let data;
export let passProps;
@@ -88,9 +132,10 @@
});
};
const handleClick = () => {
const handleClick = async () => {
const config = getCurrentConfig();
if (config.runAsPortal) {
await tick();
handleConnect();
return;
}
@@ -102,7 +147,12 @@
return;
}
handleOpenConnectionTab();
if (getCurrentSettings()['defaultAction.connectionClick'] == 'openDetails') {
handleOpenConnectionTab();
} else {
await tick();
handleConnect();
}
};
const handleSqlRestore = () => {
@@ -118,27 +168,7 @@
apiCall('server-connections/refresh', { conid: data._id });
};
const handleDisconnect = () => {
openedConnections.update(list => list.filter(x => x != data._id));
if (electron) {
apiCall('server-connections/disconnect', { conid: data._id });
}
if (_.get($currentDatabase, 'connection._id') == data._id) {
if (electron) {
apiCall('database-connections/disconnect', { conid: data._id, database: $currentDatabase.name });
}
currentDatabase.set(null);
}
closeMultipleTabs(x => x.props.conid == data._id);
if (data.unsaved) {
openNewTab({
title: 'New Connection',
icon: 'img connection',
tabComponent: 'ConnectionTab',
props: {
conid: data._id,
},
});
}
disconnectServerConnection(data._id);
};
const handleDelete = () => {
showModal(ConfirmModal, {
@@ -180,8 +210,8 @@
return [
config.runAsPortal == false && [
!$openedConnections.includes(data._id) && {
text: 'Edit',
{
text: $openedConnections.includes(data._id) ? 'View details' : 'Edit',
onClick: handleOpenConnectionTab,
},
!$openedConnections.includes(data._id) && {
@@ -1,6 +1,35 @@
<script lang="ts" context="module">
export const extractKey = props => props.name;
export function disconnectDatabaseConnection(conid, database, showConfirmation = true) {
const closeCondition = x =>
x.props?.conid == conid &&
x.props?.database == database &&
x.tabComponent != 'ConnectionTab' &&
x.closedTime == null;
if (showConfirmation) {
const count = getOpenedTabs().filter(closeCondition).length;
if (count > 0) {
showModal(ConfirmModal, {
message: `Closing connection will close ${count} opened tabs, continue?`,
onConfirm: () => disconnectDatabaseConnection(conid, database, false),
});
return;
}
}
const electron = getElectron();
if (electron) {
apiCall('database-connections/disconnect', { conid, database });
}
if (getCurrentDatabase()?.connection?._id == conid && getCurrentDatabase()?.name == database) {
currentDatabase.set(null);
}
openedSingleDatabaseConnections.update(list => list.filter(x => x != conid));
closeMultipleTabs(closeCondition);
}
export function getDatabaseMenuItems(
connection,
name,
@@ -54,12 +83,7 @@
label: 'New collection name',
header: 'Create collection',
onConfirm: async newCollection => {
const dbid = { conid: connection._id, database: name };
await apiCall('database-connections/run-script', {
...dbid,
sql: `db.createCollection('${newCollection}')`,
});
await apiCall('database-connections/sync-model', dbid);
saveScriptToDatabase({ conid: connection._id, database: name }, `db.createCollection('${newCollection}')`);
},
});
};
@@ -135,14 +159,7 @@
};
const handleDisconnect = () => {
const electron = getElectron();
if (electron) {
apiCall('database-connections/disconnect', { conid: connection._id, database: name });
}
if (getCurrentDatabase()?.connection?._id == connection._id && getCurrentDatabase()?.name == name) {
currentDatabase.set(null);
}
openedSingleDatabaseConnections.update(list => list.filter(x => x != connection._id));
disconnectDatabaseConnection(connection._id, name);
};
const handleExportModel = async () => {
@@ -202,13 +219,7 @@
};
async function handleConfirmSql(sql) {
const resp = await apiCall('database-connections/run-script', { conid: connection._id, database: name, sql });
const { errorMessage } = resp || {};
if (errorMessage) {
showModal(ErrorMessageModal, { title: 'Error when executing script', message: errorMessage });
} else {
showSnackbarSuccess('Saved to database');
}
saveScriptToDatabase({ conid: connection._id, database: name }, sql, false);
}
const driver = findEngineDriver(connection, getExtensions());
@@ -279,6 +290,7 @@
extensions,
getCurrentDatabase,
getExtensions,
getOpenedTabs,
openedConnections,
openedSingleDatabaseConnections,
pinnedDatabases,
@@ -294,12 +306,14 @@
import { openJsonDocument } from '../tabs/JsonTab.svelte';
import { apiCall } from '../utility/api';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
import ConfirmSqlModal, { saveScriptToDatabase } from '../modals/ConfirmSqlModal.svelte';
import { filterAppsForDatabase } from '../utility/appTools';
import newQuery from '../query/newQuery';
import { exportSqlDump } from '../utility/exportFileTools';
import ImportDatabaseDumpModal from '../modals/ImportDatabaseDumpModal.svelte';
import ExportDatabaseDumpModal from '../modals/ExportDatabaseDumpModal.svelte';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import { closeMultipleTabs } from '../widgets/TabsPanel.svelte';
export let data;
export let passProps;
@@ -315,7 +329,7 @@
);
}
$: isPinned = !!$pinnedDatabases.find(x => x.name == data.name && x.connection?._id == data.connection?._id);
$: isPinned = !!$pinnedDatabases.find(x => x?.name == data.name && x?.connection?._id == data.connection?._id);
$: apps = useUsedApps();
</script>
@@ -330,6 +344,10 @@
isBold={_.get($currentDatabase, 'connection._id') == _.get(data.connection, '_id') &&
_.get($currentDatabase, 'name') == data.name}
on:click={() => ($currentDatabase = data)}
on:dragstart
on:dragenter
on:dragend
on:drop
on:middleclick={() => {
createMenu()
.find(x => x.isNewQuery)
@@ -1,7 +1,8 @@
<script lang="ts" context="module">
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
export const createMatcher = ({ schemaName, pureName }) => filter => filterName(filter, pureName, schemaName);
const electron = getElectron();
export const createMatcher = ({ schemaName, pureName, columns }) => filter =>
filterName(filter, pureName, schemaName, ...(columns?.map(({ columnName }) => ({ childName: columnName })) || []));
export const createTitle = ({ pureName }) => pureName;
export const databaseObjectIcons = {
tables: 'img table',
@@ -43,21 +44,34 @@
tab: 'TableStructureTab',
icon: 'img table-structure',
},
{
label: 'Open perspective',
tab: 'PerspectiveTab',
forceNewTab: true,
icon: 'img perspective',
},
{
divider: true,
},
{
label: 'Drop table',
isDrop: true,
requiresWriteAccess: true,
},
{
label: 'Rename table',
isRename: true,
requiresWriteAccess: true,
},
{
label: 'Create table backup',
isDuplicateTable: true,
requiresWriteAccess: true,
},
{
label: 'Query designer',
isQueryDesigner: true,
requiresWriteAccess: true,
},
{
label: 'Show diagram',
@@ -74,6 +88,7 @@
{
label: 'Import',
isImport: true,
requiresWriteAccess: true,
},
{
label: 'Open as data sheet',
@@ -127,6 +142,12 @@
tab: 'TableStructureTab',
icon: 'img view-structure',
},
{
label: 'Open perspective',
tab: 'PerspectiveTab',
forceNewTab: true,
icon: 'img perspective',
},
{
label: 'Drop view',
isDrop: true,
@@ -339,13 +360,213 @@
],
};
async function databaseObjectMenuClickHandler(data, menu) {
const getDriver = async () => {
const conn = await getConnectionInfo(data);
if (!conn) return;
const driver = findEngineDriver(conn, getExtensions());
return driver;
};
if (menu.isOpenFreeTable) {
const coninfo = await getConnectionInfo(data);
openNewTab({
title: data.pureName,
icon: 'img free-table',
tabComponent: 'FreeTableTab',
props: {
initialArgs: {
functionName: 'tableReader',
props: {
connection: {
...coninfo,
database: data.database,
},
schemaName: data.schemaName,
pureName: data.pureName,
},
},
},
});
} else if (menu.isActiveChart) {
const driver = await getDriver();
const dmp = driver.createDumper();
dmp.put('^select * from %f', data);
openNewTab(
{
title: data.pureName,
icon: 'img chart',
tabComponent: 'ChartTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
config: { chartType: 'bar' },
sql: dmp.s,
},
}
);
} else if (menu.isQueryDesigner) {
openNewTab(
{
title: 'Query #',
icon: 'img query-design',
tabComponent: 'QueryDesignTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
tables: [
{
...data,
designerId: uuidv1(),
left: 50,
top: 50,
},
],
},
}
);
} else if (menu.isDiagram) {
openNewTab(
{
title: 'Diagram #',
icon: 'img diagram',
tabComponent: 'DiagramTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
tables: [
{
...data,
designerId: `${data.pureName}-${uuidv1()}`,
autoAddReferences: true,
},
],
references: [],
autoLayout: true,
},
}
);
} else if (menu.sqlGeneratorProps) {
showModal(SqlGeneratorModal, {
initialObjects: [data],
initialConfig: menu.sqlGeneratorProps,
conid: data.conid,
database: data.database,
});
} else if (menu.isDrop) {
const { conid, database } = data;
alterDatabaseDialog(conid, database, db => {
_.remove(
db[data.objectTypeField] as any[],
x => x.schemaName == data.schemaName && x.pureName == data.pureName
);
});
} else if (menu.isRename) {
const { conid, database } = data;
renameDatabaseObjectDialog(conid, database, data.pureName, (db, newName) => {
const obj = db[data.objectTypeField].find(x => x.schemaName == data.schemaName && x.pureName == data.pureName);
obj.pureName = newName;
});
} else if (menu.isDropCollection) {
showModal(ConfirmModal, {
message: `Really drop collection ${data.pureName}?`,
onConfirm: async () => {
saveScriptToDatabase(_.pick(data, ['conid', 'database']), `db.dropCollection('${data.pureName}')`);
const dbid = _.pick(data, ['conid', 'database']);
},
});
} else if (menu.isRenameCollection) {
showModal(InputTextModal, {
label: 'New collection name',
header: 'Rename collection',
value: data.pureName,
onConfirm: async newName => {
const dbid = _.pick(data, ['conid', 'database']);
await apiCall('database-connections/run-script', {
...dbid,
sql: `db.renameCollection('${data.pureName}', '${newName}')`,
});
apiCall('database-connections/sync-model', dbid);
},
});
} else if (menu.isDuplicateTable) {
const driver = await getDriver();
const dmp = driver.createDumper();
const newTable = _.cloneDeep(data);
const { conid, database } = data;
newTable.pureName = `_${newTable.pureName}_${dateFormat(new Date(), 'yyyy-MM-dd-hh-mm-ss')}`;
newTable.columns.forEach(x => {
x.autoIncrement = false;
x.defaultConstraint = null;
});
newTable.foreignKeys = [];
newTable.checks = [];
newTable.uniques = [];
newTable.indexes = [];
if (newTable.primaryKey) {
newTable.primaryKey.constraintName = null;
}
dmp.createTable(newTable);
dmp.putCmd(
'^insert ^into %f(%,i) ^select %,i from %f',
newTable,
newTable.columns.map(x => x.columnName),
data.columns.map(x => x.columnName),
data
);
showModal(ConfirmSqlModal, {
sql: dmp.s,
onConfirm: async () => {
saveScriptToDatabase({ conid, database }, dmp.s);
},
engine: driver.engine,
});
} else if (menu.isImport) {
const { conid, database } = data;
showModal(ImportExportModal, {
initialValues: {
sourceStorageType: getDefaultFileFormat(getExtensions()).storageType,
targetStorageType: 'database',
targetConnectionId: conid,
targetDatabaseName: database,
fixedTargetPureName: data.pureName,
},
});
} else {
openDatabaseObjectDetail(
menu.tab,
menu.scriptTemplate,
data,
menu.forceNewTab,
menu.initialData,
menu.icon,
data
);
}
}
export async function openDatabaseObjectDetail(
tabComponent,
scriptTemplate,
{ schemaName, pureName, conid, database, objectTypeField },
forceNewTab?,
initialData?,
icon?
icon?,
appObjectData?
) {
const connection = await getConnectionInfo({ conid });
const tooltip = `${getConnectionLabel(connection)}\n${database}\n${fullDisplayName({
@@ -359,6 +580,8 @@
tooltip,
icon: icon || (scriptTemplate ? 'img sql-file' : databaseObjectIcons[objectTypeField]),
tabComponent: scriptTemplate ? 'QueryTab' : tabComponent,
appObject: 'DatabaseObjectAppObject',
appObjectData,
props: {
schemaName,
pureName,
@@ -376,6 +599,13 @@
export function handleDatabaseObjectClick(data, forceNewTab = false) {
const { schemaName, pureName, conid, database, objectTypeField } = data;
const configuredAction = getCurrentSettings()[`defaultAction.dbObjectClick.${objectTypeField}`];
const overrideMenu = menus[objectTypeField].find(x => x.label && x.label == configuredAction);
if (overrideMenu) {
databaseObjectMenuClickHandler(data, overrideMenu);
return;
}
openDatabaseObjectDetail(
defaultTabs[objectTypeField],
defaultTabs[objectTypeField] ? null : 'CREATE OBJECT',
@@ -388,7 +618,8 @@
},
forceNewTab,
null,
null
null,
data
);
}
@@ -402,14 +633,7 @@
);
}
export function createDatabaseObjectMenu(data) {
const getDriver = async () => {
const conn = await getConnectionInfo(data);
if (!conn) return;
const driver = findEngineDriver(conn, getExtensions());
return driver;
};
export function createDatabaseObjectMenu(data, connection = null) {
const { objectTypeField } = data;
return menus[objectTypeField]
.filter(x => x)
@@ -448,198 +672,13 @@
);
}
if (connection?.isReadOnly && menu.requiresWriteAccess) {
return null;
}
return {
text: menu.label,
onClick: async () => {
if (menu.isOpenFreeTable) {
const coninfo = await getConnectionInfo(data);
openNewTab({
title: data.pureName,
icon: 'img free-table',
tabComponent: 'FreeTableTab',
props: {
initialArgs: {
functionName: 'tableReader',
props: {
connection: {
...coninfo,
database: data.database,
},
schemaName: data.schemaName,
pureName: data.pureName,
},
},
},
});
} else if (menu.isActiveChart) {
const driver = await getDriver();
const dmp = driver.createDumper();
dmp.put('^select * from %f', data);
openNewTab(
{
title: data.pureName,
icon: 'img chart',
tabComponent: 'ChartTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
config: { chartType: 'bar' },
sql: dmp.s,
},
}
);
} else if (menu.isQueryDesigner) {
openNewTab(
{
title: 'Query #',
icon: 'img query-design',
tabComponent: 'QueryDesignTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
tables: [
{
...data,
designerId: uuidv1(),
left: 50,
top: 50,
},
],
},
}
);
} else if (menu.isDiagram) {
openNewTab(
{
title: 'Diagram #',
icon: 'img diagram',
tabComponent: 'DiagramTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
tables: [
{
...data,
designerId: `${data.pureName}-${uuidv1()}`,
autoAddReferences: true,
},
],
references: [],
autoLayout: true,
},
}
);
} else if (menu.sqlGeneratorProps) {
showModal(SqlGeneratorModal, {
initialObjects: [data],
initialConfig: menu.sqlGeneratorProps,
conid: data.conid,
database: data.database,
});
} else if (menu.isDrop) {
const { conid, database } = data;
alterDatabaseDialog(conid, database, db => {
_.remove(
db[data.objectTypeField] as any[],
x => x.schemaName == data.schemaName && x.pureName == data.pureName
);
});
} else if (menu.isRename) {
const { conid, database } = data;
renameDatabaseObjectDialog(conid, database, data.pureName, (db, newName) => {
const obj = db[data.objectTypeField].find(
x => x.schemaName == data.schemaName && x.pureName == data.pureName
);
obj.pureName = newName;
});
} else if (menu.isDropCollection) {
showModal(ConfirmModal, {
message: `Really drop collection ${data.pureName}?`,
onConfirm: async () => {
const dbid = _.pick(data, ['conid', 'database']);
await apiCall('database-connections/run-script', {
...dbid,
sql: `db.dropCollection('${data.pureName}')`,
});
apiCall('database-connections/sync-model', dbid);
},
});
} else if (menu.isRenameCollection) {
showModal(InputTextModal, {
label: 'New collection name',
header: 'Rename collection',
value: data.pureName,
onConfirm: async newName => {
const dbid = _.pick(data, ['conid', 'database']);
await apiCall('database-connections/run-script', {
...dbid,
sql: `db.renameCollection('${data.pureName}', '${newName}')`,
});
apiCall('database-connections/sync-model', dbid);
},
});
} else if (menu.isDuplicateTable) {
const driver = await getDriver();
const dmp = driver.createDumper();
const newTable = _.cloneDeep(data);
const { conid, database } = data;
newTable.pureName = `_${newTable.pureName}_${dateFormat(new Date(), 'yyyy-MM-dd-hh-mm-ss')}`;
newTable.columns.forEach(x => {
x.autoIncrement = false;
});
newTable.foreignKeys = [];
newTable.indexes = [];
dmp.createTable(newTable);
dmp.putCmd(
'^insert ^into %f(%,i) ^select %,i from %f',
newTable,
newTable.columns.map(x => x.columnName),
data.columns.map(x => x.columnName),
data
);
showModal(ConfirmSqlModal, {
sql: dmp.s,
onConfirm: async () => {
const resp = await apiCall('database-connections/run-script', { conid, database, sql: dmp.s });
await apiCall('database-connections/sync-model', { conid, database });
},
engine: driver.engine,
});
} else if (menu.isImport) {
const { conid, database } = data;
showModal(ImportExportModal, {
initialValues: {
sourceStorageType: getDefaultFileFormat(getExtensions()).storageType,
targetStorageType: 'database',
targetConnectionId: conid,
targetDatabaseName: database,
fixedTargetPureName: data.pureName,
},
});
} else {
openDatabaseObjectDetail(
menu.tab,
menu.scriptTemplate,
data,
menu.forceNewTab,
menu.initialData,
menu.icon
);
}
onClick: () => {
databaseObjectMenuClickHandler(data, menu);
},
};
});
@@ -650,12 +689,23 @@
if (_.isNaN(num)) return value;
return num.toLocaleString();
}
export function createAppObjectMenu(data) {
return createDatabaseObjectMenu(data);
}
</script>
<script lang="ts">
import _ from 'lodash';
import AppObjectCore from './AppObjectCore.svelte';
import { currentDatabase, extensions, getExtensions, openedConnections, pinnedTables } from '../stores';
import {
currentDatabase,
extensions,
getCurrentSettings,
getExtensions,
openedConnections,
pinnedTables,
} from '../stores';
import openNewTab from '../utility/openNewTab';
import { filterName, generateDbPairingId, getAlterDatabaseScript } from 'dbgate-tools';
import { getConnectionInfo, getDatabaseInfo } from '../utility/metadataLoaders';
@@ -666,10 +716,9 @@
import uuidv1 from 'uuid/v1';
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
import getConnectionLabel from '../utility/getConnectionLabel';
import getElectron from '../utility/getElectron';
import { exportQuickExportFile } from '../utility/exportFileTools';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
import ConfirmSqlModal, { saveScriptToDatabase } from '../modals/ConfirmSqlModal.svelte';
import { alterDatabaseDialog, renameDatabaseObjectDialog } from '../utility/alterDatabaseTools';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import { apiCall } from '../utility/api';
@@ -686,7 +735,7 @@
}
function createMenu() {
return createDatabaseObjectMenu(data);
return createDatabaseObjectMenu(data, passProps?.connection);
}
$: isPinned = !!$pinnedTables.find(x => testEqual(data, x));
@@ -706,4 +755,8 @@
on:click={() => handleClick()}
on:middleclick={() => handleClick(true)}
on:expand
on:dragstart
on:dragenter
on:dragend
on:drop
/>
+57 -4
View File
@@ -8,14 +8,67 @@
}
return data.connection._id;
};
function dragExchange(dragged, data, pinned, setPinned, compare) {
if (!compare(dragged, data)) {
const i1 = _.findIndex(pinned, x => compare(x, dragged));
const i2 = _.findIndex(pinned, x => compare(x, data));
if (i1 >= 0 && i2 >= 0 && i1 != i2) {
const newPinned = [...pinned];
const tmp = newPinned[i1];
newPinned[i1] = newPinned[i2];
newPinned[i2] = tmp;
setPinned(newPinned);
}
}
}
</script>
<script lang="ts">
import _, { values } from 'lodash';
import { draggedPinnedObject, pinnedDatabases, pinnedTables } from '../stores';
export let data;
</script>
{#if data.objectTypeField}
<DatabaseObjectAppObject {...$$props} />
{:else}
<DatabaseAppObject {...$$props} />
{#if data}
{#if data.objectTypeField}
<DatabaseObjectAppObject
{...$$props}
on:dragstart={() => {
$draggedPinnedObject = data;
}}
on:dragenter={e => {
dragExchange(
$draggedPinnedObject,
data,
$pinnedTables,
value => ($pinnedTables = value),
(a, b) => a?.pureName == b?.pureName && a?.schemaName == b?.schemaName
);
}}
on:dragend={() => {
$draggedPinnedObject = null;
}}
/>
{:else}
<DatabaseAppObject
{...$$props}
on:dragstart={() => {
$draggedPinnedObject = data;
}}
on:dragenter={e => {
dragExchange(
$draggedPinnedObject,
data,
$pinnedDatabases,
value => ($pinnedDatabases = value),
(a, b) => a?.name == b?.name && a?.connection?._id == b?.connection?._id
);
}}
on:dragend={() => {
$draggedPinnedObject = null;
}}
/>
{/if}
{/if}
@@ -65,6 +65,14 @@
currentConnection: true,
};
const perspectives: FileTypeHandler = {
icon: 'img perspective',
format: 'json',
tabComponent: 'PerspectiveTab',
folder: 'pesrpectives',
currentConnection: true,
};
export const SAVED_FILE_HANDLERS = {
sql,
shell,
@@ -73,10 +81,14 @@
query,
sqlite,
diagrams,
perspectives,
};
export const extractKey = data => data.file;
export const createMatcher = ({ file }) => filter => filterName(filter, file);
export const createMatcher =
({ file }) =>
filter =>
filterName(filter, file);
</script>
<script lang="ts">
+5
View File
@@ -0,0 +1,5 @@
import * as DatabaseObjectAppObject from './DatabaseObjectAppObject.svelte';
export default {
DatabaseObjectAppObject,
};
@@ -0,0 +1,33 @@
<script context="module">
function getCommandTitle(command) {
let res = command.text;
if (command.keyText || command.keyTextFromGroup) {
res += ` (${formatKeyText(command.keyText || command.keyTextFromGroup)})`;
}
return res;
}
</script>
<script lang="ts">
import { commandsCustomized } from '../stores';
import { formatKeyText } from '../utility/common';
import FormStyledButton from './FormStyledButton.svelte';
export let command;
export let component = FormStyledButton;
export let hideDisabled = false;
$: cmd = Object.values($commandsCustomized).find((x: any) => x.id == command) as any;
</script>
{#if cmd && (!hideDisabled || cmd.enabled)}
<svelte:component
this={component}
title={getCommandTitle(cmd)}
icon={cmd.icon}
on:click={cmd.onClick}
disabled={!cmd.enabled}
value={cmd.toolbarName || cmd.name}
{...$$restProps}
/>
{/if}
@@ -16,6 +16,7 @@
export let command;
export let component = ToolStripButton;
export let hideDisabled = false;
export let buttonLabel = null;
$: cmd = Object.values($commandsCustomized).find((x: any) => x.id == command) as any;
</script>
@@ -29,6 +30,6 @@
disabled={!cmd.enabled}
{...$$restProps}
>
{cmd.toolbarName || cmd.name}
{buttonLabel || cmd.toolbarName || cmd.name}
</svelte:component>
{/if}
@@ -5,7 +5,16 @@
import ToolStripSplitDropDownButton from './ToolStripSplitDropDownButton.svelte';
export let commands;
$: menu = _.compact(commands).map(command => ({ command }));
export let hideDisabled = false;
export let buttonLabel = null;
$: menu = _.compact(commands).map(command => (_.isString(command) ? { command } : command));
</script>
<ToolStripCommandButton command={commands[0]} component={ToolStripSplitDropDownButton} {menu} />
<ToolStripCommandButton
command={commands[0]}
component={ToolStripSplitDropDownButton}
{menu}
{hideDisabled}
{buttonLabel}
/>
@@ -0,0 +1,7 @@
<script lang="ts">
import MapView from '../elements/MapView.svelte';
export let selection;
</script>
<MapView {selection} />
+19 -10
View File
@@ -6,7 +6,7 @@ import SettingsModal from '../settings/SettingsModal.svelte';
import ImportExportModal from '../modals/ImportExportModal.svelte';
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
import { showModal } from '../modals/modalTools';
import newQuery from '../query/newQuery';
import newQuery, { newDiagram, newQueryDesign } from '../query/newQuery';
import saveTabFile from '../utility/saveTabFile';
import openNewTab from '../utility/openNewTab';
import getElectron from '../utility/getElectron';
@@ -120,6 +120,24 @@ registerCommand({
},
});
registerCommand({
id: 'new.queryDesign',
category: 'New',
icon: 'img query-design',
name: 'Query design',
menuName: 'New query design',
onClick: () => newQueryDesign(),
});
registerCommand({
id: 'new.diagram',
category: 'New',
icon: 'img diagram',
name: 'ER Diagram',
menuName: 'New ER diagram',
onClick: () => newDiagram(),
});
registerCommand({
id: 'new.archiveFolder',
category: 'New',
@@ -467,15 +485,6 @@ registerCommand({
onClick: () => getElectron().send('quit-app'),
});
registerCommand({
id: 'new.window',
category: 'New',
name: 'New Window',
keyText: 'CtrlOrCommand+N',
testEnabled: () => getElectron() != null,
onClick: () => getElectron().send('new-window'),
});
registerCommand({
id: 'app.logout',
category: 'App',
@@ -0,0 +1,99 @@
<script context="module">
function makeBulletString(value) {
return _.pad('', value.length, '•');
}
function highlightSpecialCharacters(value) {
value = value.replace(/\n/g, '↲');
value = value.replace(/\r/g, '');
value = value.replace(/^(\s+)/, makeBulletString);
value = value.replace(/(\s+)$/, makeBulletString);
value = value.replace(/(\s\s+)/g, makeBulletString);
return value;
}
// const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
const dateTimeRegex =
/^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|()|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))$/;
function formatNumber(value) {
if (value >= 10000 || value <= -10000) {
if (getBoolSettingsValue('dataGrid.thousandsSeparator', false)) {
return value.toLocaleString();
} else {
return value.toString();
}
}
return value.toString();
}
function formatDateTime(testedString) {
const m = testedString.match(dateTimeRegex);
return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}`;
}
</script>
<script lang="ts">
import _ from 'lodash';
import { getBoolSettingsValue } from '../settings/settingsTools';
import { arrayToHexString } from 'dbgate-tools';
export let rowData;
export let value;
export let jsonParsedValue = undefined;
</script>
{#if rowData == null}
<span class="null">(No row)</span>
{:else if value === null}
<span class="null">(NULL)</span>
{:else if value === undefined}
<span class="null">(No field)</span>
{:else if _.isDate(value)}
{value.toString()}
{:else if value === true}
<span class="value">true</span>
{:else if value === false}
<span class="value">false</span>
{:else if _.isNumber(value)}
<span class="value">{formatNumber(value)}</span>
{:else if _.isString(value) && !jsonParsedValue}
{#if dateTimeRegex.test(value)}
<span class="value">
{formatDateTime(value)}
</span>
{:else}
{highlightSpecialCharacters(value)}
{/if}
{:else if value?.type == 'Buffer' && _.isArray(value.data)}
{#if value.data.length <= 16}
<span class="value">{'0x' + arrayToHexString(value.data)}</span>
{:else}
<span class="null">({value.data.length} bytes)</span>
{/if}
{:else if value.$oid}
<span class="value">ObjectId("{value.$oid}")</span>
{:else if _.isPlainObject(value)}
<span class="null" title={JSON.stringify(value, undefined, 2)}>(JSON)</span>
{:else if _.isArray(value)}
<span class="null" title={value.map(x => JSON.stringify(x)).join('\n')}>[{value.length} items]</span>
{:else if _.isPlainObject(jsonParsedValue)}
<span class="null" title={JSON.stringify(jsonParsedValue, undefined, 2)}>(JSON)</span>
{:else if _.isArray(jsonParsedValue)}
<span class="null" title={jsonParsedValue.map(x => JSON.stringify(x)).join('\n')}
>[{jsonParsedValue.length} items]</span
>
{:else}
{value.toString()}
{/if}
<style>
.null {
color: var(--theme-font-3);
font-style: italic;
}
.value {
color: var(--theme-icon-green);
}
</style>
@@ -105,19 +105,13 @@
</script>
<script lang="ts">
import { changeSetToSql, createChangeSet } from 'dbgate-datalib';
import { parseFilter } from 'dbgate-filterparser';
import { scriptToSql } from 'dbgate-sqltree';
import _ from 'lodash';
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
import registerCommand from '../commands/registerCommand';
import ErrorInfo from '../elements/ErrorInfo.svelte';
import { extractShellConnection } from '../impexp/createImpExpScript';
import ConfirmNoSqlModal from '../modals/ConfirmNoSqlModal.svelte';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import ImportExportModal from '../modals/ImportExportModal.svelte';
import { showModal } from '../modals/modalTools';
import { extensions } from '../stores';
import { apiCall } from '../utility/api';
import { registerMenu } from '../utility/contextMenu';
@@ -14,9 +14,13 @@
export let column;
export let conid = undefined;
export let database = undefined;
export let setSort;
export let setSort = undefined;
export let addToSort = undefined;
export let clearSort = undefined;
export let grouping = undefined;
export let order = undefined;
export let orderIndex = undefined;
export let isSortDefined = false;
export let allowDefineVirtualReferences = false;
export let setGrouping;
@@ -44,6 +48,9 @@
return [
setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' },
setSort && { onClick: () => setSort('DESC'), text: 'Sort descending' },
isSortDefined && addToSort && !order && { onClick: () => addToSort('ASC'), text: 'Add to sort - ascending' },
isSortDefined && addToSort && !order && { onClick: () => addToSort('DESC'), text: 'Add to sort - descending' },
order && clearSort && { onClick: () => clearSort(), text: 'Clear sort criteria' },
{ onClick: () => copyTextToClipboard(column.columnName), text: 'Copy column name' },
column.foreignKey && [{ divider: true }, { onClick: openReferencedTable, text: column.foreignKey.refTableName }],
@@ -90,11 +97,17 @@
{#if order == 'ASC'}
<span class="icon">
<FontIcon icon="img sort-asc" />
{#if orderIndex >= 0}
<span class="color-icon-green order-index">{orderIndex + 1}</span>
{/if}
</span>
{/if}
{#if order == 'DESC'}
<span class="icon">
<FontIcon icon="img sort-desc" />
{#if orderIndex >= 0}
<span class="color-icon-green order-index">{orderIndex + 1}</span>
{/if}
</span>
{/if}
<DropDownButton menu={getMenu} narrow />
@@ -106,6 +119,13 @@
display: flex;
flex-wrap: nowrap;
}
.order-index {
font-size: 10pt;
margin-left: -3px;
margin-right: 2px;
top: -1px;
position: relative;
}
.label {
flex: 1;
min-width: 10px;
@@ -23,13 +23,16 @@
export let filter;
export let setFilter;
export let showResizeSplitter = false;
export let onFocusGrid;
export let onGetReference;
export let onFocusGrid = null;
export let onGetReference = null;
export let foreignKey = null;
export let conid = null;
export let database = null;
export let driver = null;
export let jslid = null;
export let customCommandIcon = null;
export let onCustomCommand = null;
export let customCommandTooltip = null;
export let pureName = null;
export let schemaName = null;
@@ -295,6 +298,11 @@
class:isOk
placeholder="Filter"
/>
{#if customCommandIcon && onCustomCommand}
<InlineButton on:click={onCustomCommand} title={customCommandTooltip} narrow square>
<FontIcon icon={customCommandIcon} />
</InlineButton>
{/if}
{#if conid && database && driver}
{#if driver?.databaseEngineTypes?.includes('sql') && foreignKey}
<InlineButton on:click={handleShowDictionary} narrow square>
@@ -320,6 +328,7 @@
input {
flex: 1;
min-width: 10px;
width: 1px;
}
input.isError {
+4 -90
View File
@@ -1,47 +1,10 @@
<script context="module">
function makeBulletString(value) {
return _.pad('', value.length, '•');
}
function highlightSpecialCharacters(value) {
value = value.replace(/\n/g, '↲');
value = value.replace(/\r/g, '');
value = value.replace(/^(\s+)/, makeBulletString);
value = value.replace(/(\s+)$/, makeBulletString);
value = value.replace(/(\s\s+)/g, makeBulletString);
return value;
}
// const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
const dateTimeRegex = /^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|()|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))$/;
function formatNumber(value) {
if (value >= 10000 || value <= -10000) {
if (getBoolSettingsValue('dataGrid.thousandsSeparator', false)) {
return value.toLocaleString();
} else {
return value.toString();
}
}
return value.toString();
}
function formatDateTime(testedString) {
const m = testedString.match(dateTimeRegex);
return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}`;
}
</script>
<script lang="ts">
import _, { isPlainObject, join } from 'lodash';
import _ from 'lodash';
import ShowFormButton from '../formview/ShowFormButton.svelte';
import { getBoolSettingsValue } from '../settings/settingsTools';
import { arrayToHexString, isJsonLikeLongString, safeJsonParse } from 'dbgate-tools';
import { showModal } from '../modals/modalTools';
import DictionaryLookupModal from '../modals/DictionaryLookupModal.svelte';
import { isJsonLikeLongString, safeJsonParse } from 'dbgate-tools';
import { openJsonDocument } from '../tabs/JsonTab.svelte';
import openNewTab from '../utility/openNewTab';
import CellValue from './CellValue.svelte';
export let rowIndex;
export let col;
@@ -101,49 +64,7 @@
class:isFocusedColumn
{style}
>
{#if rowData == null}
<span class="null">(No row)</span>
{:else if value === null}
<span class="null">(NULL)</span>
{:else if value === undefined}
<span class="null">(No field)</span>
{:else if _.isDate(value)}
{value.toString()}
{:else if value === true}
<span class="value">true</span>
{:else if value === false}
<span class="value">false</span>
{:else if _.isNumber(value)}
<span class="value">{formatNumber(value)}</span>
{:else if _.isString(value) && !jsonParsedValue}
{#if dateTimeRegex.test(value)}
<span class="value">
{formatDateTime(value)}
</span>
{:else}
{highlightSpecialCharacters(value)}
{/if}
{:else if value?.type == 'Buffer' && _.isArray(value.data)}
{#if value.data.length <= 16}
<span class="value">{'0x' + arrayToHexString(value.data)}</span>
{:else}
<span class="null">({value.data.length} bytes)</span>
{/if}
{:else if value.$oid}
<span class="value">ObjectId("{value.$oid}")</span>
{:else if _.isPlainObject(value)}
<span class="null" title={JSON.stringify(value, undefined, 2)}>(JSON)</span>
{:else if _.isArray(value)}
<span class="null" title={value.map(x => JSON.stringify(x)).join('\n')}>[{value.length} items]</span>
{:else if _.isPlainObject(jsonParsedValue)}
<span class="null" title={JSON.stringify(jsonParsedValue, undefined, 2)}>(JSON)</span>
{:else if _.isArray(jsonParsedValue)}
<span class="null" title={jsonParsedValue.map(x => JSON.stringify(x)).join('\n')}
>[{jsonParsedValue.length} items]</span
>
{:else}
{value.toString()}
{/if}
<CellValue {rowData} {value} {jsonParsedValue} />
{#if allowHintField && rowData && _.some(col.hintColumnNames, hintColumnName => rowData[hintColumnName])}
<span class="hint"
@@ -256,13 +177,6 @@
color: var(--theme-font-3);
margin-left: 5px;
}
.null {
color: var(--theme-font-3);
font-style: italic;
}
.value {
color: var(--theme-icon-green);
}
.autoFillMarker {
width: 8px;
+153 -35
View File
@@ -5,7 +5,7 @@
id: 'dataGrid.refresh',
category: 'Data grid',
name: 'Refresh',
keyText: 'F5',
keyText: 'F5 | CtrlOrCommand+R',
toolbar: true,
isRelatedToTab: true,
icon: 'icon reload',
@@ -17,7 +17,7 @@
id: 'dataGrid.revertRowChanges',
category: 'Data grid',
name: 'Revert row changes',
keyText: 'CtrlOrCommand+R',
keyText: 'CtrlOrCommand+U',
testEnabled: () => getCurrentDataGrid()?.getGrider()?.containsChanges,
onClick: () => getCurrentDataGrid().revertRowChanges(),
});
@@ -52,6 +52,16 @@
onClick: () => getCurrentDataGrid().insertNewRow(),
});
registerCommand({
id: 'dataGrid.cloneRows',
category: 'Data grid',
name: 'Clone rows',
toolbarName: 'Clone row(s)',
keyText: 'CtrlOrCommand+Shift+C',
testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable,
onClick: () => getCurrentDataGrid().cloneRows(),
});
registerCommand({
id: 'dataGrid.setNull',
category: 'Data grid',
@@ -112,6 +122,14 @@
onClick: () => getCurrentDataGrid().editJsonDocument(),
});
registerCommand({
id: 'dataGrid.openSelectionInMap',
category: 'Data grid',
name: 'Open selection in map',
testEnabled: () => getCurrentDataGrid() != null, // ?.openSelectionInMapEnabled(),
onClick: () => getCurrentDataGrid().openSelectionInMap(),
});
registerCommand({
id: 'dataGrid.viewJsonDocument',
category: 'Data grid',
@@ -306,6 +324,8 @@
import { apiCall } from '../utility/api';
import getElectron from '../utility/getElectron';
import { isCtrlOrCommandKey, isMac } from '../utility/common';
import { selectionCouldBeShownOnMap } from '../elements/MapView.svelte';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
export let onLoadNextData = undefined;
export let grider = undefined;
@@ -334,7 +354,8 @@
export let collapsedLeftColumnStore;
export let multipleGridsOnTab = false;
export let tabControlHiddenTab = false;
export let onCustomGridRefresh;
export let onCustomGridRefresh = null;
export let onOpenQuery = null;
export let useEvalFilters = false;
export let jslid;
// export let generalAllowSave = false;
@@ -407,16 +428,44 @@
}
export async function insertNewRow() {
if (grider.canInsert) {
const rowIndex = grider.insertRow();
const cell = [rowIndex, (currentCell && currentCell[1]) || 0];
// @ts-ignore
currentCell = cell;
// @ts-ignore
selectedCells = [cell];
await tick();
scrollIntoView(cell);
if (!grider.canInsert) return;
const rowIndex = grider.insertRow();
const cell = [rowIndex, (currentCell && currentCell[1]) || 0];
// @ts-ignore
currentCell = cell;
// @ts-ignore
selectedCells = [cell];
await tick();
scrollIntoView(cell);
}
export async function cloneRows() {
if (!grider.canInsert) return;
let rowIndex = null;
grider.beginUpdate();
for (const index of _.sortBy(getSelectedRowIndexes(), x => x)) {
if (_.isNumber(index)) {
rowIndex = grider.insertRow();
for (const column of display.columns) {
if (column.uniquePath.length > 1) continue;
if (column.autoIncrement) continue;
grider.setCellValue(rowIndex, column.uniqueName, grider.getRowData(index)[column.uniqueName]);
}
}
}
grider.endUpdate();
if (rowIndex == null) return;
const cell = [rowIndex, (currentCell && currentCell[1]) || 0];
// @ts-ignore
currentCell = cell;
// @ts-ignore
selectedCells = [cell];
await tick();
scrollIntoView(cell);
}
export function setFixedValue(value) {
@@ -530,6 +579,23 @@
openJsonDocument(json);
}
export function openSelectionInMap() {
const selection = getCellsPublished(selectedCells);
if (!selectionCouldBeShownOnMap(selection)) {
showModal(ErrorMessageModal, { message: 'There is nothing to be shown on map' });
return;
}
openNewTab(
{
title: 'Map',
icon: 'img map',
tabComponent: 'MapTab',
},
{ editor: selection.map(x => _.omit(x, ['engine'])) }
);
return;
}
function getSelectedExportableCell() {
const electron = getElectron();
if (electron && selectedCells.length == 1) {
@@ -1128,7 +1194,7 @@
// console.log('event', event.nativeEvent);
}
if (event.keyCode == keycodes.f2) {
if (event.keyCode == keycodes.f2 || event.keyCode == keycodes.enter) {
// @ts-ignore
dispatchInsplaceEditor({ type: 'show', cell: currentCell, selectAll: true });
}
@@ -1143,7 +1209,20 @@
handleCursorMove(event);
if (event.shiftKey && event.keyCode != keycodes.shift && event.keyCode != keycodes.tab) {
if (
event.shiftKey &&
event.keyCode != keycodes.shift &&
event.keyCode != keycodes.tab &&
event.keyCode != keycodes.ctrl &&
event.keyCode != keycodes.leftWindowKey &&
event.keyCode != keycodes.rightWindowKey &&
!(
(event.keyCode >= keycodes.a && event.keyCode <= keycodes.z) ||
(event.keyCode >= keycodes.n0 && event.keyCode <= keycodes.n9) ||
(event.keyCode >= keycodes.numPad0 && event.keyCode <= keycodes.numPad9) ||
event.keyCode == keycodes.dash
)
) {
selectedCells = getCellRange(shiftDragStartCell || currentCell, currentCell);
}
}
@@ -1178,8 +1257,10 @@
if (currentCell[0] == 0) return focusFilterEditor(currentCell[1]);
return moveCurrentCell(currentCell[0] - 1, currentCell[1], event);
case keycodes.downArrow:
case keycodes.enter:
return moveCurrentCell(currentCell[0] + 1, currentCell[1], event);
case keycodes.enter:
if (!grider.editable) return moveCurrentCell(currentCell[0] + 1, currentCell[1], event);
break;
case keycodes.leftArrow:
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
case keycodes.rightArrow:
@@ -1193,25 +1274,31 @@
case keycodes.pageDown:
return moveCurrentCell(currentCell[0] + visibleRowCountLowerBound, currentCell[1], event);
case keycodes.tab: {
if (event.shiftKey) {
if (currentCell[1] > 0) {
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
} else {
return moveCurrentCell(currentCell[0] - 1, columnSizes.realCount - 1, event);
}
} else {
if (currentCell[1] < columnSizes.realCount - 1) {
return moveCurrentCell(currentCell[0], currentCell[1] + 1, event);
} else {
return moveCurrentCell(currentCell[0] + 1, 0, event);
}
}
return moveCurrentCellWithTabKey(event.shiftKey);
}
}
}
return null;
}
function moveCurrentCellWithTabKey(isShift) {
if (!isRegularCell(currentCell)) return null;
if (isShift) {
if (currentCell[1] > 0) {
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
} else {
return moveCurrentCell(currentCell[0] - 1, columnSizes.realCount - 1, event);
}
} else {
if (currentCell[1] < columnSizes.realCount - 1) {
return moveCurrentCell(currentCell[0], currentCell[1] + 1, event);
} else {
return moveCurrentCell(currentCell[0] + 1, 0, event);
}
}
}
function setCellValue(cell, value) {
grider.setCellValue(cell[0], realColumnUniqueNames[cell[1]], value);
}
@@ -1352,10 +1439,24 @@
selectAll: action.selectAll,
};
case 'close': {
const [row, col] = currentCell || [];
if (domFocusField) domFocusField.focus();
// @ts-ignore
if (action.mode == 'enter' && row) setTimeout(() => moveCurrentCell(row + 1, col), 0);
if (action.mode == 'enter' || action.mode == 'tab' || action.mode == 'shiftTab') {
setTimeout(() => {
if (isRegularCell(currentCell)) {
switch (action.mode) {
case 'enter':
moveCurrentCell(currentCell[0] + 1, currentCell[1]);
break;
case 'tab':
moveCurrentCellWithTabKey(false);
break;
case 'shiftTab':
moveCurrentCellWithTabKey(true);
break;
}
}
}, 0);
}
// if (action.mode == 'save') setTimeout(handleSave, 0);
return {};
}
@@ -1404,6 +1505,7 @@
{ command: 'dataGrid.revertAllChanges', hideDisabled: true },
{ command: 'dataGrid.deleteSelectedRows' },
{ command: 'dataGrid.insertNewRow' },
{ command: 'dataGrid.cloneRows' },
{ command: 'dataGrid.setNull' },
{ placeTag: 'edit' },
{ divider: true },
@@ -1426,6 +1528,7 @@
{ command: 'dataGrid.generateSqlFromData' },
{ command: 'dataGrid.openFreeTable' },
{ command: 'dataGrid.openChartFromSelection' },
{ command: 'dataGrid.openSelectionInMap', hideDisabled: true },
{ placeTag: 'chart' }
);
@@ -1457,17 +1560,28 @@
{#if !display || (!isDynamicStructure && (!columns || columns.length == 0))}
<LoadingInfo wrapper message="Waiting for structure" />
{:else if errorMessage}
<ErrorInfo message={errorMessage} alignTop />
<div>
<ErrorInfo message={errorMessage} alignTop />
<FormStyledButton value="Reset filter" on:click={() => display.clearFilters()} />
<FormStyledButton value="Reset view" on:click={() => display.resetConfig()} />
{#if onOpenQuery}
<FormStyledButton value="Open Query" on:click={onOpenQuery} />
{/if}
</div>
{:else if isDynamicStructure && isLoadedAll && grider?.rowCount == 0}
<div>
<ErrorInfo
alignTop
message="No rows loaded, check filter or add new documents. You could copy documents from ohter collections/tables with Copy advanved/Copy as JSON command."
message={grider.editable
? 'No rows loaded, check filter or add new documents. You could copy documents from ohter collections/tables with Copy advanved/Copy as JSON command.'
: 'No rows loaded'}
/>
{#if display.filterCount > 0}
<FormStyledButton value="Reset filter" on:click={() => display.clearFilters()} />
{/if}
<FormStyledButton value="Add document" on:click={addJsonDocument} />
{#if grider.editable}
<FormStyledButton value="Add document" on:click={addJsonDocument} />
{/if}
</div>
{:else if grider.errors && grider.errors.length > 0}
<div>
@@ -1526,7 +1640,11 @@
{conid}
{database}
setSort={display.sortable ? order => display.setSort(col.uniqueName, order) : null}
order={display.getSortOrder(col.uniqueName)}
addToSort={display.sortable ? order => display.addToSort(col.uniqueName, order) : null}
order={display.sortable ? display.getSortOrder(col.uniqueName) : null}
orderIndex={display.sortable ? display.getSortOrderIndex(col.uniqueName) : -1}
isSortDefined={display.sortable ? display.isSortDefined() : false}
clearSort={display.sortable ? () => display.clearSort() : null}
on:resizeSplitter={e => {
// @ts-ignore
display.resizeColumn(col.uniqueName, col.width, e.detail);
+10 -2
View File
@@ -36,18 +36,26 @@
break;
case keycodes.enter:
if (isChangedRef.get()) {
// grider.setCellValue(rowIndex, uniqueName, editor.value);
onSetValue(parseCellValue(domEditor.value));
isChangedRef.set(false);
}
domEditor.blur();
event.preventDefault();
dispatchInsplaceEditor({ type: 'close', mode: 'enter' });
break;
case keycodes.tab:
if (isChangedRef.get()) {
onSetValue(parseCellValue(domEditor.value));
isChangedRef.set(false);
}
domEditor.blur();
event.preventDefault();
dispatchInsplaceEditor({ type: 'close', mode: event.shiftKey ? 'shiftTab' : 'tab' });
break;
case keycodes.s:
if (isCtrlOrCommandKey(event)) {
if (isChangedRef.get()) {
onSetValue(parseCellValue(domEditor.value));
// grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.set(false);
}
event.preventDefault();
@@ -224,4 +224,5 @@
frameSelection={!!macroPreview}
{grider}
{display}
onOpenQuery={openQuery}
/>
+2 -2
View File
@@ -167,8 +167,8 @@
async function detectSize(tables, domTables) {
await tick();
const rects = _.values(domTables).map(x => x.getRect());
const maxX = _.max(rects.map(x => x.right));
const maxY = _.max(rects.map(x => x.bottom));
const maxX = rects.length > 0 ? _.max(rects.map(x => x.right)) : 0;
const maxY = rects.length > 0 ? _.max(rects.map(x => x.bottom)) : 0;
canvasWidth = Math.max(3000, maxX + 50);
canvasHeight = Math.max(3000, maxY + 50);
@@ -8,6 +8,7 @@ import {
mergeConditions,
Source,
ResultField,
Expression,
} from 'dbgate-sqltree';
import { EngineDriver } from 'dbgate-types';
import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types';
@@ -78,25 +79,27 @@ export class DesignerQueryDumper {
return select;
}
addConditions(select: Select, tables: DesignerTableInfo[]) {
buildConditionFromFilterField(tables: DesignerTableInfo[], filterField: string, getExpression?: Function): Condition {
const conditions = [];
for (const column of this.designer.columns || []) {
if (!column.filter) continue;
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
if (!table) continue;
if (!tables.find(x => x.designerId == table.designerId)) continue;
if (!column[filterField]) continue;
if (!column.isCustomExpression) {
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
if (!table) continue;
if (!tables.find(x => x.designerId == table.designerId)) continue;
}
try {
const condition = parseFilter(column.filter, findDesignerFilterType(column, this.designer));
const condition = parseFilter(column[filterField], findDesignerFilterType(column, this.designer));
if (condition) {
select.where = mergeConditions(
select.where,
conditions.push(
_.cloneDeepWith(condition, expr => {
if (expr.exprType == 'placeholder')
return {
exprType: 'column',
columnName: column.columnName,
source: findQuerySource(this.designer, column.designerId),
};
if (expr.exprType == 'placeholder') {
if (getExpression) return getExpression(column);
return this.getColumnExpression(column);
}
})
);
}
@@ -105,33 +108,79 @@ export class DesignerQueryDumper {
continue;
}
}
if (conditions.length == 0) {
return null;
}
if (conditions.length == 1) {
return conditions[0];
}
return {
conditionType: 'and',
conditions,
};
}
addConditionsCore(select: Select, tables: DesignerTableInfo[], filterFields, selectField, getExpression?) {
const conditions: Condition[] = _.compact(
filterFields.map(field => this.buildConditionFromFilterField(tables, field, getExpression))
);
if (conditions.length == 0) {
return;
}
if (conditions.length == 0) {
select[selectField] = mergeConditions(select[selectField], conditions[0]);
return;
}
select[selectField] = mergeConditions(select[selectField], {
conditionType: 'or',
conditions,
});
}
addConditions(select: Select, tables: DesignerTableInfo[]) {
const additionalFilterCount = this.designer.settings?.additionalFilterCount || 0;
const filterFields = ['filter', ..._.range(additionalFilterCount).map(index => `additionalFilter${index + 1}`)];
this.addConditionsCore(select, tables, filterFields, 'where');
}
addGroupConditions(select: Select, tables: DesignerTableInfo[], selectIsGrouped: boolean) {
for (const column of this.designer.columns || []) {
if (!column.groupFilter) continue;
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
if (!table) continue;
if (!tables.find(x => x.designerId == table.designerId)) continue;
const condition = parseFilter(column.groupFilter, findDesignerFilterType(column, this.designer));
if (condition) {
select.having = mergeConditions(
select.having,
_.cloneDeepWith(condition, expr => {
if (expr.exprType == 'placeholder') {
return this.getColumnOutputExpression(column, selectIsGrouped);
}
})
);
}
}
const additionalGroupFilterCount = this.designer.settings?.additionalGroupFilterCount || 0;
const filterFields = [
'groupFilter',
..._.range(additionalGroupFilterCount).map(index => `additionalGroupFilter${index + 1}`),
];
this.addConditionsCore(select, tables, filterFields, 'having', column =>
this.getColumnResultField(column, selectIsGrouped)
);
}
getColumnOutputExpression(col, selectIsGrouped): ResultField {
getColumnExpression(col): Expression {
const source = findQuerySource(this.designer, col.designerId);
const { columnName, isCustomExpression, customExpression } = col;
const res: Expression = isCustomExpression
? {
exprType: 'raw',
sql: customExpression,
}
: {
exprType: 'column',
columnName,
source,
};
return res;
}
getColumnResultField(col, selectIsGrouped): ResultField {
const { columnName } = col;
let { alias } = col;
const exprCore = this.getColumnExpression(col);
if (selectIsGrouped && !col.isGrouped) {
// use aggregate
const aggregate = col.aggregate == null || col.aggregate == '---' ? 'MAX' : col.aggregate;
@@ -142,20 +191,12 @@ export class DesignerQueryDumper {
func: aggregate == 'COUNT DISTINCT' ? 'COUNT' : aggregate,
argsPrefix: aggregate == 'COUNT DISTINCT' ? 'DISTINCT' : null,
alias,
args: [
{
exprType: 'column',
columnName,
source,
},
],
args: [exprCore],
};
} else {
return {
exprType: 'column',
columnName,
...exprCore,
alias,
source,
};
}
}
@@ -179,24 +220,21 @@ export class DesignerQueryDumper {
}
}
const topLevelColumns = (this.designer.columns || []).filter(col =>
topLevelTables.find(tbl => tbl.designerId == col.designerId)
const topLevelColumns = (this.designer.columns || []).filter(
col =>
topLevelTables.find(tbl => tbl.designerId == col.designerId) || (col.isCustomExpression && col.customExpression)
);
const selectIsGrouped = !!topLevelColumns.find(x => x.isGrouped || (x.aggregate && x.aggregate != '---'));
const outputColumns = topLevelColumns.filter(x => x.isOutput);
if (outputColumns.length == 0) {
res.selectAll = true;
} else {
res.columns = outputColumns.map(col => this.getColumnOutputExpression(col, selectIsGrouped));
res.columns = outputColumns.map(col => this.getColumnResultField(col, selectIsGrouped));
}
const groupedColumns = topLevelColumns.filter(x => x.isGrouped);
if (groupedColumns.length > 0) {
res.groupBy = groupedColumns.map(col => ({
exprType: 'column',
columnName: col.columnName,
source: findQuerySource(this.designer, col.designerId),
}));
res.groupBy = groupedColumns.map(col => this.getColumnExpression(col));
}
const orderColumns = _.sortBy(
@@ -205,10 +243,8 @@ export class DesignerQueryDumper {
);
if (orderColumns.length > 0) {
res.orderBy = orderColumns.map(col => ({
exprType: 'column',
...this.getColumnExpression(col),
direction: col.sortOrder < 0 ? 'DESC' : 'ASC',
columnName: col.columnName,
source: findQuerySource(this.designer, col.designerId),
}));
}
+4
View File
@@ -31,10 +31,14 @@ export type DesignerColumnInfo = {
sortOrder?: number;
filter?: string;
groupFilter?: string;
isCustomExpression?: boolean;
customExpression?: string;
};
export type DesignerSettings = {
isDistinct?: boolean;
additionalFilterCount?: number;
additionalGroupFilterCount?: number;
};
export type DesignerInfo = {
@@ -1,8 +1,9 @@
<script lang="ts">
export let width;
export let isFlex = false;
</script>
<div style={`max-width: ${width}px`}>
<div style={`max-width: ${width}px`} class:isFlex>
<slot />
</div>
@@ -12,4 +13,8 @@
overflow-y: auto;
overflow-x: auto;
}
div.isFlex {
display: flex;
}
</style>
+201
View File
@@ -0,0 +1,201 @@
<script lang="ts" context="module">
export function selectionCouldBeShownOnMap(selection) {
if (selection.length > 0 && _.find(selection, x => isWktGeometry(x.value))) {
return true;
}
if (
selection.find(x => x.column.toLowerCase().includes('lat')) &&
(selection.find(x => x.column.toLowerCase().includes('lon')) ||
selection.find(x => x.column.toLowerCase().includes('lng')))
) {
return true;
}
return false;
}
</script>
<script lang="ts">
import _ from 'lodash';
import { onMount, tick } from 'svelte';
import 'leaflet/dist/leaflet.css';
import leaflet from 'leaflet';
import wellknown from 'wellknown';
import { isWktGeometry, ScriptWriter, ScriptWriterJson } from 'dbgate-tools';
import resizeObserver from '../utility/resizeObserver';
import openNewTab from '../utility/openNewTab';
import contextMenu from '../utility/contextMenu';
import { saveExportedFile, saveFileToDisk } from '../utility/exportFileTools';
import { getCurrentConfig } from '../stores';
import { apiCall } from '../utility/api';
export let selection;
let refContainer;
let map;
let selectionLayers = [];
let geoJson;
function createColumnsTable(cells) {
if (cells.length == 0) return '';
return `<table>${cells.map(cell => `<tr><td>${cell.column}</td><td>${cell.value}</td></tr>`).join('\n')}</table>`;
}
function addSelectionToMap() {
if (!map) return;
if (!selection) return;
for (const selectionLayer of selectionLayers) {
selectionLayer.remove();
}
selectionLayers = [];
const selectedRows = _.groupBy(selection || [], 'row');
const features = [];
for (const rowKey of _.keys(selectedRows)) {
const cells = selectedRows[rowKey];
const lat = cells.find(x => x.column.toLowerCase().includes('lat'));
const lon = cells.find(x => x.column.toLowerCase().includes('lon') || x.column.toLowerCase().includes('lng'));
const geoValues = cells.map(x => x.value).filter(isWktGeometry);
if (lat && lon) {
features.push({
type: 'Feature',
properties: {
popupContent: createColumnsTable(cells),
},
geometry: {
type: 'Point',
coordinates: [lon.value, lat.value],
},
});
}
if (geoValues.length > 0) {
// parse WKT to geoJSON array
features.push(
...geoValues.map(wellknown).map(geometry => ({
type: 'Feature',
properties: {
popupContent: createColumnsTable(cells.filter(x => !isWktGeometry(x.value))),
},
geometry,
}))
);
}
}
if (features.length == 0) {
return;
}
geoJson = {
type: 'FeatureCollection',
features,
};
const geoJsonObj = leaflet
.geoJSON(geoJson, {
style: function () {
return {
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4,
};
},
pointToLayer: (feature, latlng) => {
return leaflet.circleMarker(latlng, {
radius: 7,
weight: 2,
fillColor: '#ff0000',
color: '#ff0000',
opacity: 0.9,
fillOpacity: 0.9,
});
},
onEachFeature: (feature, layer) => {
// does this feature have a property named popupContent?
if (feature.properties && feature.properties.popupContent) {
layer.bindPopup(feature.properties.popupContent);
layer.bindTooltip(feature.properties.popupContent);
}
},
})
.addTo(map);
// geoJsonObj.bindPopup('This is the Transamerica Pyramid'); //.openPopup();
map.fitBounds(geoJsonObj.getBounds());
selectionLayers.push(geoJsonObj);
}
onMount(() => {
map = leaflet.map(refContainer).setView([50, 15], 13);
leaflet
.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap',
})
.addTo(map);
addSelectionToMap();
// map.fitBounds([
// [50, 15],
// [50.1, 15],
// [50, 15.1],
// ]);
// const marker = leaflet.marker([50, 15]).addTo(map);
// <div bind:this={refContainer} class="flex1 map-container" />
});
$: {
selection;
addSelectionToMap();
}
function createMenu() {
return [
{
text: 'Open on new tab',
onClick: () => {
openNewTab(
{
title: 'Map',
icon: 'img map',
tabComponent: 'MapTab',
},
{ editor: selection.map(x => _.omit(x, ['engine'])) }
);
},
},
{
text: 'Export to HTML file',
onClick: () => {
saveFileToDisk(async filePath => {
await apiCall('files/export-map', {
geoJson,
filePath,
});
});
},
},
];
}
</script>
<div
bind:this={refContainer}
use:contextMenu={createMenu}
class="flex1"
use:resizeObserver={true}
on:resize={async e => {
await tick();
map.invalidateSize();
}}
/>
@@ -13,8 +13,11 @@
import SelectField from '../forms/SelectField.svelte';
import TextField from '../forms/TextField.svelte';
import InlineButton from '../buttons/InlineButton.svelte';
import uuidv1 from 'uuid/v1';
import TableControl from './TableControl.svelte';
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import _ from 'lodash';
export let value;
export let onChange;
@@ -35,8 +38,56 @@
}));
};
const addExpressionColumn = () => {
onChange(current => ({
...current,
columns: [...(current.columns || []), { isCustomExpression: true, isOutput: true, designerId: uuidv1() }],
}));
};
const addOrCondition = () => {
onChange(current => ({
...current,
settings: {
...current?.settings,
additionalFilterCount: (current?.settings?.additionalFilterCount ?? 0) + 1,
},
}));
};
const removeOrCondition = () => {
onChange(current => ({
...current,
settings: {
...current?.settings,
additionalFilterCount: (current?.settings?.additionalFilterCount ?? 1) - 1,
},
}));
};
const addGroupOrCondition = () => {
onChange(current => ({
...current,
settings: {
...current?.settings,
additionalGroupFilterCount: (current?.settings?.additionalGroupFilterCount ?? 0) + 1,
},
}));
};
const removeGroupOrCondition = () => {
onChange(current => ({
...current,
settings: {
...current?.settings,
additionalGroupFilterCount: (current?.settings?.additionalGroupFilterCount ?? 1) - 1,
},
}));
};
$: columns = value?.columns;
$: tables = value?.tables;
$: settings = value?.settings;
$: hasGroupedColumn = !!(columns || []).find(x => x.isGrouped);
</script>
@@ -44,18 +95,49 @@
<TableControl
rows={columns || []}
columns={[
{ fieldName: 'columnName', header: 'Column/Expression' },
{ fieldName: 'columnName', slot: 8, header: 'Column/Expression' },
{ fieldName: 'tableDisplayName', header: 'Table', formatter: row => getTableDisplayName(row, tables) },
{ fieldName: 'isOutput', header: 'Output', slot: 0 },
{ fieldName: 'alias', header: 'Alias', slot: 1 },
{ fieldName: 'isGrouped', header: 'Group by', slot: 2 },
{ fieldName: 'aggregate', header: 'Aggregate', slot: 3 },
{ fieldName: 'sortOrder', header: 'Sort order', slot: 4 },
{ fieldName: 'filter', header: 'Filter', slot: 5 },
hasGroupedColumn && { fieldName: 'groupFilter', header: 'Group filter', slot: 6 },
{ fieldName: 'filter', header: 'Filter', slot: 5, props: { filterField: 'filter' } },
..._.range(settings?.additionalFilterCount || 0).map(index => ({
fieldName: `additionalFilter${index + 1}`,
header: `OR Filter ${index + 2}`,
slot: 5,
props: { filterField: `additionalFilter${index + 1}` },
})),
hasGroupedColumn && {
fieldName: 'groupFilter',
header: 'Group filter',
slot: 5,
props: { filterField: 'groupFilter' },
},
..._.range(hasGroupedColumn ? settings?.additionalGroupFilterCount || 0 : 0).map(index => ({
fieldName: `additionalGroupFilter${index + 1}`,
header: `OR group filter ${index + 2}`,
slot: 5,
props: { filterField: `additionalGroupFilter${index + 1}` },
})),
{ fieldName: 'actions', header: '', slot: 7 },
]}
>
<svelte:fragment slot="8" let:row>
{#if row.isCustomExpression}
<TextField
style="min-width:calc(100% - 9px)"
value={row.customExpression}
on:input={e => {
changeColumn({ ...row, customExpression: e.target.value });
}}
/>
{:else}
{row.columnName}
{/if}
</svelte:fragment>
<svelte:fragment slot="0" let:row>
<CheckboxField
checked={row.isOutput}
@@ -67,6 +149,7 @@
</svelte:fragment>
<svelte:fragment slot="1" let:row>
<TextField
style="min-width:calc(100% - 9px)"
value={row.alias}
on:input={e => {
changeColumn({ ...row, alias: e.target.value });
@@ -86,6 +169,7 @@
{#if !row.isGrouped}
<SelectField
isNative
style="min-width:calc(100% - 9px)"
value={row.aggregate}
on:change={e => {
changeColumn({ ...row, aggregate: e.detail });
@@ -97,6 +181,7 @@
<svelte:fragment slot="4" let:row>
<SelectField
isNative
style="min-width:calc(100% - 9px)"
value={row.sortOrder}
on:change={e => {
changeColumn({ ...row, sortOrder: parseInt(e.detail) });
@@ -112,21 +197,12 @@
]}
/>
</svelte:fragment>
<svelte:fragment slot="5" let:row>
<svelte:fragment slot="5" let:row let:filterField>
<DataFilterControl
filterType={findDesignerFilterType(row, value)}
filter={row.filter}
filter={row[filterField]}
setFilter={filter => {
changeColumn({ ...row, filter });
}}
/>
</svelte:fragment>
<svelte:fragment slot="6" let:row>
<DataFilterControl
filterType={findDesignerFilterType(row, value)}
filter={row.groupFilter}
setFilter={groupFilter => {
changeColumn({ ...row, groupFilter });
changeColumn({ ...row, [filterField]: filter });
}}
/>
</svelte:fragment>
@@ -134,6 +210,17 @@
<InlineButton on:click={() => removeColumn(row)}>Remove</InlineButton>
</svelte:fragment>
</TableControl>
<FormStyledButton value="Add custom expression" on:click={addExpressionColumn} style="width:200px" />
<FormStyledButton value="Add OR condition" on:click={addOrCondition} style="width:200px" />
{#if settings?.additionalFilterCount > 0}
<FormStyledButton value="Remove OR condition" on:click={removeOrCondition} style="width:200px" />
{/if}
{#if hasGroupedColumn}
<FormStyledButton value="Add group OR condition" on:click={addGroupOrCondition} style="width:200px" />
{/if}
{#if hasGroupedColumn && settings?.additionalGroupFilterCount > 0}
<FormStyledButton value="Remove group OR condition" on:click={removeGroupOrCondition} style="width:200px" />
{/if}
</div>
<style>
@@ -141,4 +228,4 @@
overflow: auto;
flex: 1;
}
</style>
</style>

Some files were not shown because too many files have changed in this diff Show More