diff --git a/superset/assets/package-lock.json b/superset/assets/package-lock.json
index 22c5b1d74..d508f6dbb 100644
--- a/superset/assets/package-lock.json
+++ b/superset/assets/package-lock.json
@@ -2222,6 +2222,41 @@
"minimist": "^1.2.0"
}
},
+ "@cypress/listr-verbose-renderer": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz",
+ "integrity": "sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo=",
+ "requires": {
+ "chalk": "^1.1.3",
+ "cli-cursor": "^1.0.2",
+ "date-fns": "^1.27.2",
+ "figures": "^1.7.0"
+ }
+ },
+ "@cypress/xvfb": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.3.tgz",
+ "integrity": "sha512-yYrK+/bgL3hwoRHMZG4r5fyLniCy1pXex5fimtewAY6vE/jsVs8Q37UsEO03tFlcmiLnQ3rBNMaZBYTi/+C1cw==",
+ "requires": {
+ "debug": "^3.1.0",
+ "lodash.once": "^4.1.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
"@data-ui/event-flow": {
"version": "0.0.80",
"resolved": "https://registry.npmjs.org/@data-ui/event-flow/-/event-flow-0.0.80.tgz",
@@ -3600,6 +3635,13 @@
"requires": {
"@babel/runtime": "^7.1.2",
"whatwg-fetch": "^3.0.0"
+ },
+ "dependencies": {
+ "whatwg-fetch": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
+ "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
+ }
}
},
"@superset-ui/core": {
@@ -4534,6 +4576,30 @@
}
}
},
+ "@types/blob-util": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@types/blob-util/-/blob-util-1.3.3.tgz",
+ "integrity": "sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w=="
+ },
+ "@types/bluebird": {
+ "version": "3.5.18",
+ "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.18.tgz",
+ "integrity": "sha512-OTPWHmsyW18BhrnG5x8F7PzeZ2nFxmHGb42bZn79P9hl+GI5cMzyPgQTwNjbem0lJhoru/8vtjAFCUOu3+gE2w=="
+ },
+ "@types/chai": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.8.tgz",
+ "integrity": "sha512-m812CONwdZn/dMzkIJEY0yAs4apyTkTORgfB2UsMOxgkUbC205AHnm4T8I0I5gPg9MHrFc1dJ35iS75c0CJkjg=="
+ },
+ "@types/chai-jquery": {
+ "version": "1.1.35",
+ "resolved": "https://registry.npmjs.org/@types/chai-jquery/-/chai-jquery-1.1.35.tgz",
+ "integrity": "sha512-7aIt9QMRdxuagLLI48dPz96YJdhu64p6FCa6n4qkGN5DQLHnrIjZpD9bXCvV2G0NwgZ1FAmfP214dxc5zNCfgQ==",
+ "requires": {
+ "@types/chai": "*",
+ "@types/jquery": "*"
+ }
+ },
"@types/clone": {
"version": "0.1.30",
"resolved": "https://registry.npmjs.org/@types/clone/-/clone-0.1.30.tgz",
@@ -4635,6 +4701,11 @@
"integrity": "sha512-DC8xTuW/6TYgvEg3HEXS7cu9OijFqprVDXXiOcdOKZCU/5PJNLZU37VVvmZHdtMiGOa8wAA/We+JzbdxFzQTRQ==",
"dev": true
},
+ "@types/jquery": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.6.tgz",
+ "integrity": "sha512-403D4wN95Mtzt2EoQHARf5oe/jEPhzBOBNrunk+ydQGW8WmkQ/E8rViRAEB1qEt/vssfGfNVD6ujP4FVeegrLg=="
+ },
"@types/lodash": {
"version": "4.14.146",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.146.tgz",
@@ -4645,6 +4716,16 @@
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-1.15.2.tgz",
"integrity": "sha512-zHPoyVrLvNaiMRYdhmh88Rn489ZgAgbc6iLxR5Yi0VCNfeNYHcszbhJV2vDHLNrVGy35BPtWBRn4OP2F9BBvFw=="
},
+ "@types/minimatch": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
+ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA=="
+ },
+ "@types/mocha": {
+ "version": "2.2.44",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.44.tgz",
+ "integrity": "sha512-k2tWTQU8G4+iSMvqKi0Q9IIsWAp/n8xzdZS4Q4YVIltApoMA00wFBFdlJnmoaK1/z7B0Cy0yPe6GgXteSmdUNw=="
+ },
"@types/node": {
"version": "10.12.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.15.tgz",
@@ -4712,6 +4793,20 @@
"@types/react": "*"
}
},
+ "@types/sinon": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.0.tgz",
+ "integrity": "sha512-kcYoPw0uKioFVC/oOqafk2yizSceIQXCYnkYts9vJIwQklFRsMubTObTDrjQamUyBRd47332s85074cd/hCwxg=="
+ },
+ "@types/sinon-chai": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.2.tgz",
+ "integrity": "sha512-5zSs2AslzyPZdOsbm2NRtuSNAI2aTWzNKOHa/GRecKo7a5efYD7qGcPxMZXQDayVXT2Vnd5waXxBvV31eCZqiA==",
+ "requires": {
+ "@types/chai": "*",
+ "@types/sinon": "*"
+ }
+ },
"@types/source-list-map": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
@@ -5540,14 +5635,12 @@
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
- "dev": true
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"ansi-styles": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
- "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
- "dev": true
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
},
"ansicolors": {
"version": "0.2.1",
@@ -6103,7 +6196,6 @@
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/async/-/async-2.4.0.tgz",
"integrity": "sha1-SZAgDxjqW4N8LMT4wDGmmFw4VhE=",
- "dev": true,
"requires": {
"lodash": "^4.14.0"
}
@@ -6857,6 +6949,11 @@
}
}
},
+ "buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
+ },
"buffer-equal": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz",
@@ -6962,6 +7059,14 @@
"schema-utils": "^0.4.2"
}
},
+ "cachedir": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-1.3.0.tgz",
+ "integrity": "sha512-O1ji32oyON9laVPJL1IZ5bmwd2cB46VfpxkDequezH+15FDzzVddEyrGEeX4WusDSqKxdyFdDQDEG1yo1GoWkg==",
+ "requires": {
+ "os-homedir": "^1.0.1"
+ }
+ },
"caller-callsite": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
@@ -7036,7 +7141,6 @@
"version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
- "dev": true,
"requires": {
"ansi-styles": "^2.2.1",
"escape-string-regexp": "^1.0.2",
@@ -7076,6 +7180,11 @@
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
},
+ "check-more-types": {
+ "version": "2.24.0",
+ "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
+ "integrity": "sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA="
+ },
"check-types": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz",
@@ -7853,12 +7962,25 @@
}
},
"cli-cursor": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
- "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
- "dev": true,
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz",
+ "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
"requires": {
- "restore-cursor": "^2.0.0"
+ "restore-cursor": "^1.0.1"
+ }
+ },
+ "cli-spinners": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-0.1.2.tgz",
+ "integrity": "sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw="
+ },
+ "cli-truncate": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz",
+ "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=",
+ "requires": {
+ "slice-ansi": "0.0.4",
+ "string-width": "^1.0.1"
}
},
"cli-width": {
@@ -7982,8 +8104,7 @@
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
- "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
- "dev": true
+ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"collapse-white-space": {
"version": "1.0.4",
@@ -8054,6 +8175,14 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg=="
},
+ "common-tags": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.4.0.tgz",
+ "integrity": "sha1-EYe+Tz1M8MBCfUP3Tu8fc1AWFMA=",
+ "requires": {
+ "babel-runtime": "^6.18.0"
+ }
+ },
"commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -8352,7 +8481,6 @@
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
- "dev": true,
"requires": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
@@ -8889,6 +9017,232 @@
"integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=",
"dev": true
},
+ "cypress": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/cypress/-/cypress-3.1.5.tgz",
+ "integrity": "sha512-jzYGKJqU1CHoNocPndinf/vbG28SeU+hg+4qhousT/HDBMJxYgjecXOmSgBX/ga9/TakhqSrIrSP2r6gW/OLtg==",
+ "requires": {
+ "@cypress/listr-verbose-renderer": "0.4.1",
+ "@cypress/xvfb": "1.2.3",
+ "@types/blob-util": "1.3.3",
+ "@types/bluebird": "3.5.18",
+ "@types/chai": "4.0.8",
+ "@types/chai-jquery": "1.1.35",
+ "@types/jquery": "3.3.6",
+ "@types/lodash": "4.14.87",
+ "@types/minimatch": "3.0.3",
+ "@types/mocha": "2.2.44",
+ "@types/sinon": "7.0.0",
+ "@types/sinon-chai": "3.2.2",
+ "bluebird": "3.5.0",
+ "cachedir": "1.3.0",
+ "chalk": "2.4.1",
+ "check-more-types": "2.24.0",
+ "commander": "2.11.0",
+ "common-tags": "1.4.0",
+ "debug": "3.1.0",
+ "execa": "0.10.0",
+ "executable": "4.1.1",
+ "extract-zip": "1.6.6",
+ "fs-extra": "4.0.1",
+ "getos": "3.1.0",
+ "glob": "7.1.2",
+ "is-ci": "1.0.10",
+ "is-installed-globally": "0.1.0",
+ "lazy-ass": "1.6.0",
+ "listr": "0.12.0",
+ "lodash": "4.17.11",
+ "log-symbols": "2.2.0",
+ "minimist": "1.2.0",
+ "moment": "2.22.2",
+ "ramda": "0.24.1",
+ "request": "2.87.0",
+ "request-progress": "0.3.1",
+ "supports-color": "5.1.0",
+ "tmp": "0.0.31",
+ "url": "0.11.0",
+ "yauzl": "2.8.0"
+ },
+ "dependencies": {
+ "@types/lodash": {
+ "version": "4.14.87",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.87.tgz",
+ "integrity": "sha512-AqRC+aEF4N0LuNHtcjKtvF9OTfqZI0iaBoe3dA6m/W+/YZJBZjBmW/QIZ8fBeXC6cnytSY9tBoFBqZ9uSCeVsw=="
+ },
+ "ajv": {
+ "version": "5.5.2",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+ "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+ "requires": {
+ "co": "^4.6.0",
+ "fast-deep-equal": "^1.0.0",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.3.0"
+ }
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "bluebird": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz",
+ "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw="
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "ci-info": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
+ "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A=="
+ },
+ "commander": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
+ "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ=="
+ },
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "fast-deep-equal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+ "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ="
+ },
+ "glob": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "har-validator": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
+ "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
+ "requires": {
+ "ajv": "^5.1.0",
+ "har-schema": "^2.0.0"
+ }
+ },
+ "is-ci": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.0.10.tgz",
+ "integrity": "sha1-9zkzayYyNlBhqdSCcM1WrjNpMY4=",
+ "requires": {
+ "ci-info": "^1.0.0"
+ }
+ },
+ "json-schema-traverse": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
+ "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
+ },
+ "lodash": {
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
+ },
+ "moment": {
+ "version": "2.22.2",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
+ "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
+ },
+ "oauth-sign": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
+ "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM="
+ },
+ "punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
+ },
+ "request": {
+ "version": "2.87.0",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz",
+ "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==",
+ "requires": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.6.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.5",
+ "extend": "~3.0.1",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.1",
+ "har-validator": "~5.0.3",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.17",
+ "oauth-sign": "~0.8.2",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.1",
+ "safe-buffer": "^5.1.1",
+ "tough-cookie": "~2.3.3",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.1.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz",
+ "integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==",
+ "requires": {
+ "has-flag": "^2.0.0"
+ },
+ "dependencies": {
+ "has-flag": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
+ "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE="
+ }
+ }
+ },
+ "tough-cookie": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
+ "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
+ "requires": {
+ "punycode": "^1.4.1"
+ }
+ }
+ }
+ },
"d3": {
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz",
@@ -9181,6 +9535,11 @@
"jquery": ">=1.7"
}
},
+ "date-fns": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
+ "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw=="
+ },
"date-now": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
@@ -9196,7 +9555,6 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
"requires": {
"ms": "2.0.0"
}
@@ -9618,6 +9976,11 @@
"integrity": "sha512-LQJmt0QcUzC/mLjG+ha5QhXgNQ2T2BOxRecuaU/hd92RnZt6G3ZGONsAe7Xvo9SoBvre/POElMoyK77mXjrr3w==",
"dev": true
},
+ "elegant-spinner": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz",
+ "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4="
+ },
"elliptic": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
@@ -10437,7 +10800,6 @@
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz",
"integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==",
- "dev": true,
"requires": {
"cross-spawn": "^6.0.0",
"get-stream": "^3.0.0",
@@ -10448,6 +10810,14 @@
"strip-eof": "^1.0.0"
}
},
+ "executable": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
+ "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==",
+ "requires": {
+ "pify": "^2.2.0"
+ }
+ },
"exif-parser": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz",
@@ -10459,6 +10829,11 @@
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
"dev": true
},
+ "exit-hook": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
+ "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g="
+ },
"expand-brackets": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@@ -10608,6 +10983,17 @@
"chardet": "^0.7.0",
"iconv-lite": "^0.4.24",
"tmp": "^0.0.33"
+ },
+ "dependencies": {
+ "tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "dev": true,
+ "requires": {
+ "os-tmpdir": "~1.0.2"
+ }
+ }
}
},
"extglob": {
@@ -10672,6 +11058,50 @@
}
}
},
+ "extract-zip": {
+ "version": "1.6.6",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz",
+ "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=",
+ "requires": {
+ "concat-stream": "1.6.0",
+ "debug": "2.6.9",
+ "mkdirp": "0.5.0",
+ "yauzl": "2.4.1"
+ },
+ "dependencies": {
+ "concat-stream": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz",
+ "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=",
+ "requires": {
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+ },
+ "mkdirp": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz",
+ "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=",
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "yauzl": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
+ "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
+ "requires": {
+ "fd-slicer": "~1.0.1"
+ }
+ }
+ }
+ },
"extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
@@ -10774,6 +11204,14 @@
}
}
},
+ "fd-slicer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
+ "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
+ "requires": {
+ "pend": "~1.2.0"
+ }
+ },
"fetch-mock": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-7.2.5.tgz",
@@ -10801,12 +11239,12 @@
"dev": true
},
"figures": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
- "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
- "dev": true,
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+ "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
"requires": {
- "escape-string-regexp": "^1.0.5"
+ "escape-string-regexp": "^1.0.5",
+ "object-assign": "^4.1.0"
}
},
"file-entry-cache": {
@@ -11989,6 +12427,16 @@
"readable-stream": "^2.0.0"
}
},
+ "fs-extra": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.1.tgz",
+ "integrity": "sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=",
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^3.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
"fs-readdir-recursive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
@@ -12110,8 +12558,7 @@
"get-stream": {
"version": "3.0.0",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
- "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
- "dev": true
+ "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"get-value": {
"version": "2.0.6",
@@ -12119,6 +12566,14 @@
"integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
"dev": true
},
+ "getos": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/getos/-/getos-3.1.0.tgz",
+ "integrity": "sha512-i9vrxtDu5DlLVFcrbqUqGWYlZN/zZ4pGMICCAcZoYsX3JA54nYp8r5EThw5K+m2q3wszkx4Th746JstspB0H4Q==",
+ "requires": {
+ "async": "2.4.0"
+ }
+ },
"getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
@@ -12222,6 +12677,14 @@
"is-symbol": "^1.0.1"
}
},
+ "global-dirs": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz",
+ "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=",
+ "requires": {
+ "ini": "^1.3.4"
+ }
+ },
"global-modules-path": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/global-modules-path/-/global-modules-path-2.3.1.tgz",
@@ -12258,8 +12721,7 @@
"graceful-fs": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
- "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
- "dev": true
+ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA=="
},
"grid-index": {
"version": "1.1.0",
@@ -12369,7 +12831,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
"integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
- "dev": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -13146,6 +13607,14 @@
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
"dev": true
},
+ "indent-string": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+ "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+ "requires": {
+ "repeating": "^2.0.0"
+ }
+ },
"indexes-of": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
@@ -13172,6 +13641,11 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
+ "ini": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
+ },
"inline-style-prefixer": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-3.0.8.tgz",
@@ -13228,12 +13702,58 @@
"supports-color": "^5.3.0"
}
},
+ "cli-cursor": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+ "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^2.0.0"
+ }
+ },
+ "figures": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+ "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
+ "onetime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+ "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^1.0.0"
+ }
+ },
+ "restore-cursor": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+ "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+ "dev": true,
+ "requires": {
+ "onetime": "^2.0.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "rxjs": {
+ "version": "6.5.3",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz",
+ "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
@@ -13601,11 +14121,18 @@
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
},
+ "is-finite": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+ "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
- "dev": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -13635,6 +14162,15 @@
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz",
"integrity": "sha512-but/G3sapV3MNyqiDBLrOi4x8uCIw0RY3o/Vb5GT0sMFHrVV7731wFSVy41T5FO1og7G0gXLJh0MkgPRouko/A=="
},
+ "is-installed-globally": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz",
+ "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=",
+ "requires": {
+ "global-dirs": "^0.1.0",
+ "is-path-inside": "^1.0.0"
+ }
+ },
"is-number": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@@ -13686,7 +14222,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
"integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
- "dev": true,
"requires": {
"path-is-inside": "^1.0.1"
}
@@ -13708,8 +14243,7 @@
"is-promise": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
- "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
- "dev": true
+ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o="
},
"is-regex": {
"version": "1.0.4",
@@ -13798,8 +14332,7 @@
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
- "dev": true
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"isobject": {
"version": "3.0.1",
@@ -15814,6 +16347,14 @@
"resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE="
},
+ "jsonfile": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz",
+ "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=",
+ "requires": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -15875,6 +16416,11 @@
"webpack-sources": "^1.1.0"
}
},
+ "lazy-ass": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
+ "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM="
+ },
"lcid": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
@@ -15950,6 +16496,85 @@
"type-check": "~0.3.2"
}
},
+ "listr": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/listr/-/listr-0.12.0.tgz",
+ "integrity": "sha1-a84sD1YD+klYDqF81qAMwOX6RRo=",
+ "requires": {
+ "chalk": "^1.1.3",
+ "cli-truncate": "^0.2.1",
+ "figures": "^1.7.0",
+ "indent-string": "^2.1.0",
+ "is-promise": "^2.1.0",
+ "is-stream": "^1.1.0",
+ "listr-silent-renderer": "^1.1.1",
+ "listr-update-renderer": "^0.2.0",
+ "listr-verbose-renderer": "^0.4.0",
+ "log-symbols": "^1.0.2",
+ "log-update": "^1.0.2",
+ "ora": "^0.2.3",
+ "p-map": "^1.1.1",
+ "rxjs": "^5.0.0-beta.11",
+ "stream-to-observable": "^0.1.0",
+ "strip-ansi": "^3.0.1"
+ },
+ "dependencies": {
+ "log-symbols": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz",
+ "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=",
+ "requires": {
+ "chalk": "^1.0.0"
+ }
+ }
+ }
+ },
+ "listr-silent-renderer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz",
+ "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4="
+ },
+ "listr-update-renderer": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz",
+ "integrity": "sha1-yoDhd5tOcCZoB+ju0a1qvjmFUPk=",
+ "requires": {
+ "chalk": "^1.1.3",
+ "cli-truncate": "^0.2.1",
+ "elegant-spinner": "^1.0.1",
+ "figures": "^1.7.0",
+ "indent-string": "^3.0.0",
+ "log-symbols": "^1.0.2",
+ "log-update": "^1.0.2",
+ "strip-ansi": "^3.0.1"
+ },
+ "dependencies": {
+ "indent-string": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz",
+ "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok="
+ },
+ "log-symbols": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz",
+ "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=",
+ "requires": {
+ "chalk": "^1.0.0"
+ }
+ }
+ }
+ },
+ "listr-verbose-renderer": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz",
+ "integrity": "sha1-ggb0z21S3cWCfl/RSYng6WWTOjU=",
+ "requires": {
+ "chalk": "^1.1.3",
+ "cli-cursor": "^1.0.2",
+ "date-fns": "^1.27.2",
+ "figures": "^1.7.0"
+ }
+ },
"load-bmfont": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.0.tgz",
@@ -16092,6 +16717,11 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
+ "lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
+ },
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@@ -16109,6 +16739,58 @@
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
"dev": true
},
+ "log-symbols": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
+ "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
+ "requires": {
+ "chalk": "^2.0.1"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "log-update": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz",
+ "integrity": "sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE=",
+ "requires": {
+ "ansi-escapes": "^1.0.0",
+ "cli-cursor": "^1.0.2"
+ },
+ "dependencies": {
+ "ansi-escapes": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz",
+ "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4="
+ }
+ }
+ },
"loglevel": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz",
@@ -16700,8 +17382,7 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"multicast-dns": {
"version": "6.2.3",
@@ -16845,8 +17526,7 @@
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
- "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
- "dev": true
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"nise": {
"version": "1.4.8",
@@ -17030,7 +17710,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
"integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
- "dev": true,
"requires": {
"path-key": "^2.0.0"
}
@@ -17047,8 +17726,7 @@
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
- "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
- "dev": true
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"nvd3": {
"version": "1.8.6",
@@ -17233,13 +17911,9 @@
}
},
"onetime": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
- "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
- "dev": true,
- "requires": {
- "mimic-fn": "^1.0.0"
- }
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
+ "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k="
},
"opener": {
"version": "1.5.1",
@@ -17294,6 +17968,17 @@
"wordwrap": "~1.0.0"
}
},
+ "ora": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-0.2.3.tgz",
+ "integrity": "sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q=",
+ "requires": {
+ "chalk": "^1.1.1",
+ "cli-cursor": "^1.0.2",
+ "cli-spinners": "^0.1.2",
+ "object-assign": "^4.0.1"
+ }
+ },
"original": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
@@ -17309,6 +17994,11 @@
"integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
"dev": true
},
+ "os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
+ },
"os-locale": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
@@ -17349,8 +18039,7 @@
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
- "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
- "dev": true
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
},
"output-file-sync": {
"version": "2.0.1",
@@ -17381,8 +18070,7 @@
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
- "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
- "dev": true
+ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
},
"p-is-promise": {
"version": "1.1.0",
@@ -17411,8 +18099,7 @@
"p-map": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz",
- "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==",
- "dev": true
+ "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA=="
},
"p-reduce": {
"version": "1.0.0",
@@ -17572,14 +18259,12 @@
"path-is-inside": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
- "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
- "dev": true
+ "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM="
},
"path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
- "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
- "dev": true
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
},
"path-parse": {
"version": "1.0.6",
@@ -17631,6 +18316,11 @@
"sha.js": "^2.4.8"
}
},
+ "pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
+ },
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -17644,8 +18334,7 @@
"pify": {
"version": "2.3.0",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
- "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
- "dev": true
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
},
"pinkie": {
"version": "2.0.4",
@@ -20084,8 +20773,7 @@
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
- "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
- "dev": true
+ "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
},
"querystring-es3": {
"version": "0.2.1",
@@ -20128,6 +20816,11 @@
"integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=",
"dev": true
},
+ "ramda": {
+ "version": "0.24.1",
+ "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.24.1.tgz",
+ "integrity": "sha1-w7d1UZfzW43DUCIoJixMkd22uFc="
+ },
"randexp": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
@@ -21438,6 +22131,14 @@
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
},
+ "repeating": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+ "requires": {
+ "is-finite": "^1.0.0"
+ }
+ },
"replace-ext": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz",
@@ -21470,6 +22171,14 @@
"uuid": "^3.3.2"
}
},
+ "request-progress": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-0.3.1.tgz",
+ "integrity": "sha1-ByHBBdipasayzossia4tXs/Pazo=",
+ "requires": {
+ "throttleit": "~0.0.2"
+ }
+ },
"request-promise-core": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz",
@@ -21568,13 +22277,12 @@
"dev": true
},
"restore-cursor": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
- "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
- "dev": true,
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz",
+ "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
"requires": {
- "onetime": "^2.0.0",
- "signal-exit": "^3.0.2"
+ "exit-hook": "^1.0.0",
+ "onetime": "^1.0.0"
}
},
"ret": {
@@ -21662,12 +22370,18 @@
"integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
},
"rxjs": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz",
- "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==",
- "dev": true,
+ "version": "5.5.12",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz",
+ "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==",
"requires": {
- "tslib": "^1.9.0"
+ "symbol-observable": "1.0.1"
+ },
+ "dependencies": {
+ "symbol-observable": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz",
+ "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ="
+ }
}
},
"safe-buffer": {
@@ -21961,7 +22675,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
- "dev": true,
"requires": {
"shebang-regex": "^1.0.0"
}
@@ -21969,8 +22682,7 @@
"shebang-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
- "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
- "dev": true
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
},
"shellwords": {
"version": "0.1.1",
@@ -21989,8 +22701,7 @@
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
- "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
- "dev": true
+ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"simple-swizzle": {
"version": "0.2.2",
@@ -22048,32 +22759,9 @@
"dev": true
},
"slice-ansi": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
- "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
- "dev": true,
- "requires": {
- "ansi-styles": "^3.2.0",
- "astral-regex": "^1.0.0",
- "is-fullwidth-code-point": "^2.0.0"
- },
- "dependencies": {
- "ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "dev": true,
- "requires": {
- "color-convert": "^1.9.0"
- }
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- }
- }
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
+ "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU="
},
"snapdragon": {
"version": "0.8.2",
@@ -22665,6 +23353,11 @@
"stream-to": "~0.2.0"
}
},
+ "stream-to-observable": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/stream-to-observable/-/stream-to-observable-0.1.0.tgz",
+ "integrity": "sha1-Rb8dny19wJvtgfHDB8Qw5ouEz/4="
+ },
"strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
@@ -22706,7 +23399,6 @@
"version": "1.0.2",
"resolved": "http://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
- "dev": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -22752,7 +23444,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
- "dev": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -22766,8 +23457,7 @@
"strip-eof": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
- "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
- "dev": true
+ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
},
"strip-json-comments": {
"version": "3.0.1",
@@ -22889,8 +23579,7 @@
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
- "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
- "dev": true
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
},
"svgo": {
"version": "1.2.1",
@@ -23020,6 +23709,15 @@
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
@@ -23032,6 +23730,17 @@
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
+ "slice-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
+ "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.0",
+ "astral-regex": "^1.0.0",
+ "is-fullwidth-code-point": "^2.0.0"
+ }
+ },
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
@@ -23250,6 +23959,11 @@
"integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=",
"dev": true
},
+ "throttleit": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz",
+ "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8="
+ },
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -23301,12 +24015,11 @@
"integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA=="
},
"tmp": {
- "version": "0.0.33",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
- "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
- "dev": true,
+ "version": "0.0.31",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz",
+ "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=",
"requires": {
- "os-tmpdir": "~1.0.2"
+ "os-tmpdir": "~1.0.1"
}
},
"tmpl": {
@@ -24209,6 +24922,11 @@
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz",
"integrity": "sha512-yvo+MMLjEwdc3RhhPYSximset7rwjMrdt9E41Smmvg25UQIenzrN83cRnF1JMzoMi9zZOQeYXHSDf7p+IQkW3Q=="
},
+ "universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
+ },
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -24290,7 +25008,6 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
"integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
- "dev": true,
"requires": {
"punycode": "1.3.2",
"querystring": "0.2.0"
@@ -24299,8 +25016,7 @@
"punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
- "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
- "dev": true
+ "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
}
}
},
@@ -27156,7 +27872,6 @@
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
- "dev": true,
"requires": {
"isexe": "^2.0.0"
}
@@ -27411,6 +28126,15 @@
"dev": true
}
}
+ },
+ "yauzl": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.8.0.tgz",
+ "integrity": "sha1-eUUK/yKyqcWkHvVOAtuQfM+/nuI=",
+ "requires": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.0.1"
+ }
}
}
}
diff --git a/superset/assets/spec/javascripts/sqllab/TabbedSqlEditors_spec.jsx b/superset/assets/spec/javascripts/sqllab/TabbedSqlEditors_spec.jsx
index 20fb5deb7..21e0ba331 100644
--- a/superset/assets/spec/javascripts/sqllab/TabbedSqlEditors_spec.jsx
+++ b/superset/assets/spec/javascripts/sqllab/TabbedSqlEditors_spec.jsx
@@ -39,7 +39,7 @@ describe('TabbedSqlEditors', () => {
'newEditorId',
];
- const tables = [Object.assign({}, table[0], {
+ const tables = [Object.assign({}, table, {
dataPreviewQueryId: 'B1-VQU1zW',
queryEditorId: 'newEditorId',
})];
@@ -58,6 +58,7 @@ describe('TabbedSqlEditors', () => {
'B1-VQU1zW': {
id: 'B1-VQU1zW',
sqlEditorId: 'newEditorId',
+ tableName: 'ab_user',
},
};
const mockedProps = {
@@ -133,7 +134,7 @@ describe('TabbedSqlEditors', () => {
});
it('should update queriesArray and dataPreviewQueries', () => {
expect(wrapper.state().queriesArray.slice(-1)[0]).toBe(queries['B1-VQU1zW']);
- expect(wrapper.state().dataPreviewQueries.slice(-1)[0]).toBe(queries['B1-VQU1zW']);
+ expect(wrapper.state().dataPreviewQueries.slice(-1)[0]).toEqual(queries['B1-VQU1zW']);
});
});
it('should rename Tab', () => {
@@ -171,16 +172,21 @@ describe('TabbedSqlEditors', () => {
.toBe(queryEditors[0]);
});
it('should handle select', () => {
+ const mockEvent = {
+ target: {
+ getAttribute: () => null,
+ },
+ };
wrapper = getWrapper();
sinon.spy(wrapper.instance(), 'newQueryEditor');
- sinon.stub(wrapper.instance().props.actions, 'setActiveQueryEditor');
+ sinon.stub(wrapper.instance().props.actions, 'switchQueryEditor');
- wrapper.instance().handleSelect('add_tab');
+ wrapper.instance().handleSelect('add_tab', mockEvent);
expect(wrapper.instance().newQueryEditor.callCount).toBe(1);
- wrapper.instance().handleSelect('123');
- expect(wrapper.instance().props.actions.setActiveQueryEditor.getCall(0).args[0].id)
- .toContain(123);
+ // cannot switch to current tab, switchQueryEditor never gets called
+ wrapper.instance().handleSelect('dfsadfs', mockEvent);
+ expect(wrapper.instance().props.actions.switchQueryEditor.callCount).toEqual(0);
wrapper.instance().newQueryEditor.restore();
});
it('should render', () => {
diff --git a/superset/assets/spec/javascripts/sqllab/actions/sqlLab_spec.js b/superset/assets/spec/javascripts/sqllab/actions/sqlLab_spec.js
index 32b0900b0..cb8c3b491 100644
--- a/superset/assets/spec/javascripts/sqllab/actions/sqlLab_spec.js
+++ b/superset/assets/spec/javascripts/sqllab/actions/sqlLab_spec.js
@@ -19,12 +19,29 @@
/* eslint no-unused-expressions: 0 */
import sinon from 'sinon';
import fetchMock from 'fetch-mock';
+import configureMockStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import shortid from 'shortid';
+import * as featureFlags from 'src/featureFlags';
import * as actions from '../../../../src/SqlLab/actions/sqlLab';
-import { query } from '../fixtures';
+import { defaultQueryEditor, query } from '../fixtures';
+
+const middlewares = [thunk];
+const mockStore = configureMockStore(middlewares);
describe('async actions', () => {
const mockBigNumber = '9223372036854775807';
+ const queryEditor = {
+ id: 'abcd',
+ autorun: false,
+ dbId: null,
+ latestQueryId: null,
+ selectedText: null,
+ sql: 'SELECT *\nFROM\nWHERE',
+ title: 'Untitled Query',
+ schemaOptions: [{ value: 'main', label: 'main', title: 'main' }],
+ };
let dispatch;
@@ -34,23 +51,31 @@ describe('async actions', () => {
afterEach(fetchMock.resetHistory);
+ const fetchQueryEndpoint = 'glob:*/superset/results/*';
+ fetchMock.get(
+ fetchQueryEndpoint,
+ JSON.stringify({ data: mockBigNumber, query: { sqlEditorId: 'dfsadfs' } }),
+ );
+
+ const runQueryEndpoint = 'glob:*/superset/sql_json/*';
+ fetchMock.post(runQueryEndpoint, '{ "data": ' + mockBigNumber + ' }');
+
describe('saveQuery', () => {
const saveQueryEndpoint = 'glob:*/savedqueryviewapi/api/create';
fetchMock.post(saveQueryEndpoint, 'ok');
it('posts to the correct url', () => {
expect.assertions(1);
- const thunk = actions.saveQuery(query);
- return thunk((/* mockDispatch */) => ({})).then(() => {
+ const store = mockStore({});
+ return store.dispatch(actions.saveQuery(query)).then(() => {
expect(fetchMock.calls(saveQueryEndpoint)).toHaveLength(1);
});
});
it('posts the correct query object', () => {
- const thunk = actions.saveQuery(query);
-
- return thunk((/* mockDispatch */) => ({})).then(() => {
+ const store = mockStore({});
+ return store.dispatch(actions.saveQuery(query)).then(() => {
const call = fetchMock.calls(saveQueryEndpoint)[0];
const formData = call[1].body;
Object.keys(query).forEach((key) => {
@@ -61,12 +86,9 @@ describe('async actions', () => {
});
describe('fetchQueryResults', () => {
- const fetchQueryEndpoint = 'glob:*/superset/results/*';
- fetchMock.get(fetchQueryEndpoint, '{ "data": ' + mockBigNumber + ' }');
-
const makeRequest = () => {
- const actionThunk = actions.fetchQueryResults(query);
- return actionThunk(dispatch);
+ const request = actions.fetchQueryResults(query);
+ return request(dispatch);
};
it('makes the fetch request', () => {
@@ -92,31 +114,40 @@ describe('async actions', () => {
expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe(mockBigNumber);
}));
- it('calls querySuccess on fetch success', () =>
- makeRequest().then(() => {
- expect(dispatch.callCount).toBe(2);
- expect(dispatch.getCall(1).args[0].type).toBe(actions.QUERY_SUCCESS);
- }));
+ it('calls querySuccess on fetch success', () => {
+ expect.assertions(1);
+
+ const store = mockStore({});
+ const expectedActionTypes = [
+ actions.REQUEST_QUERY_RESULTS,
+ actions.QUERY_SUCCESS,
+ ];
+ return store.dispatch(actions.fetchQueryResults(query)).then(() => {
+ expect(store.getActions().map(a => a.type)).toEqual(expectedActionTypes);
+ });
+ });
it('calls queryFailed on fetch error', () => {
- expect.assertions(2);
+ expect.assertions(1);
+
fetchMock.get(
fetchQueryEndpoint,
{ throws: { error: 'error text' } },
{ overwriteRoutes: true },
);
- return makeRequest().then(() => {
- expect(dispatch.callCount).toBe(2);
- expect(dispatch.getCall(1).args[0].type).toBe(actions.QUERY_FAILED);
+ const store = mockStore({});
+ const expectedActionTypes = [
+ actions.REQUEST_QUERY_RESULTS,
+ actions.QUERY_FAILED,
+ ];
+ return store.dispatch(actions.fetchQueryResults(query)).then(() => {
+ expect(store.getActions().map(a => a.type)).toEqual(expectedActionTypes);
});
});
});
describe('runQuery', () => {
- const runQueryEndpoint = 'glob:*/superset/sql_json/';
- fetchMock.post(runQueryEndpoint, '{ "data": ' + mockBigNumber + ' }');
-
const makeRequest = () => {
const request = actions.runQuery(query);
return request(dispatch);
@@ -146,17 +177,20 @@ describe('async actions', () => {
}));
it('calls querySuccess on fetch success', () => {
- expect.assertions(3);
+ expect.assertions(1);
- return makeRequest().then(() => {
- expect(dispatch.callCount).toBe(2);
- expect(dispatch.getCall(0).args[0].type).toBe(actions.START_QUERY);
- expect(dispatch.getCall(1).args[0].type).toBe(actions.QUERY_SUCCESS);
+ const store = mockStore({});
+ const expectedActionTypes = [
+ actions.START_QUERY,
+ actions.QUERY_SUCCESS,
+ ];
+ return store.dispatch(actions.runQuery(query)).then(() => {
+ expect(store.getActions().map(a => a.type)).toEqual(expectedActionTypes);
});
});
it('calls queryFailed on fetch error', () => {
- expect.assertions(2);
+ expect.assertions(1);
fetchMock.post(
runQueryEndpoint,
@@ -164,9 +198,13 @@ describe('async actions', () => {
{ overwriteRoutes: true },
);
- return makeRequest().then(() => {
- expect(dispatch.callCount).toBe(2);
- expect(dispatch.getCall(1).args[0].type).toBe(actions.QUERY_FAILED);
+ const store = mockStore({});
+ const expectedActionTypes = [
+ actions.START_QUERY,
+ actions.QUERY_FAILED,
+ ];
+ return store.dispatch(actions.runQuery(query)).then(() => {
+ expect(store.getActions().map(a => a.type)).toEqual(expectedActionTypes);
});
});
});
@@ -206,4 +244,516 @@ describe('async actions', () => {
});
});
});
+
+ describe('cloneQueryToNewTab', () => {
+ let stub;
+ beforeEach(() => {
+ stub = sinon.stub(shortid, 'generate').returns('abcd');
+ });
+ afterEach(() => {
+ stub.restore();
+ });
+
+ it('creates new query editor', () => {
+ expect.assertions(1);
+
+ const id = 'id';
+ const state = {
+ sqlLab: {
+ tabHistory: [id],
+ queryEditors: [{ id, title: 'Dummy query editor' }],
+ },
+ };
+ const store = mockStore(state);
+ const expectedActions = [{
+ type: actions.ADD_QUERY_EDITOR,
+ queryEditor: {
+ title: 'Copy of Dummy query editor',
+ dbId: 1,
+ schema: null,
+ autorun: true,
+ sql: 'SELECT * FROM something',
+ queryLimit: undefined,
+ maxRow: undefined,
+ id: 'abcd',
+ },
+ }];
+ return store.dispatch(actions.cloneQueryToNewTab(query)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ });
+ });
+
+ describe('addQueryEditor', () => {
+ let stub;
+ beforeEach(() => {
+ stub = sinon.stub(shortid, 'generate').returns('abcd');
+ });
+ afterEach(() => {
+ stub.restore();
+ });
+
+ it('creates new query editor', () => {
+ expect.assertions(1);
+
+ const store = mockStore({});
+ const expectedActions = [{
+ type: actions.ADD_QUERY_EDITOR,
+ queryEditor,
+ }];
+ return store.dispatch(actions.addQueryEditor(defaultQueryEditor)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ });
+ });
+
+ describe('backend sync', () => {
+ const updateTabStateEndpoint = 'glob:*/tabstateview/*';
+ fetchMock.put(updateTabStateEndpoint, {});
+ fetchMock.delete(updateTabStateEndpoint, {});
+ fetchMock.post(updateTabStateEndpoint, JSON.stringify({ id: 1 }));
+
+ const updateTableSchemaEndpoint = 'glob:*/tableschemaview/*';
+ fetchMock.put(updateTableSchemaEndpoint, {});
+ fetchMock.delete(updateTableSchemaEndpoint, {});
+ fetchMock.post(updateTableSchemaEndpoint, JSON.stringify({ id: 1 }));
+
+ const getTableMetadataEndpoint = 'glob:*/superset/table/*';
+ fetchMock.get(getTableMetadataEndpoint, {});
+ const getExtraTableMetadataEndpoint = 'glob:*/superset/extra_table_metadata/*';
+ fetchMock.get(getExtraTableMetadataEndpoint, {});
+
+ let isFeatureEnabledMock;
+
+ beforeAll(() => {
+ isFeatureEnabledMock = jest.spyOn(featureFlags, 'isFeatureEnabled')
+ .mockImplementation(feature => feature === 'SQLLAB_BACKEND_PERSISTENCE');
+ });
+
+ afterAll(() => {
+ isFeatureEnabledMock.mockRestore();
+ });
+
+ afterEach(fetchMock.resetHistory);
+
+ describe('querySuccess', () => {
+ it('updates the tab state in the backend', () => {
+ expect.assertions(2);
+
+ const store = mockStore({});
+ const results = { query: { sqlEditorId: 'abcd' } };
+ const expectedActions = [
+ {
+ type: actions.QUERY_SUCCESS,
+ query,
+ results,
+ },
+ ];
+ return store.dispatch(actions.querySuccess(query, results)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('fetchQueryResults', () => {
+ it('updates the tab state in the backend', () => {
+ expect.assertions(2);
+
+ const results = {
+ data: mockBigNumber,
+ query: { sqlEditorId: 'abcd' },
+ query_id: 'efgh',
+ };
+ fetchMock.get(
+ fetchQueryEndpoint,
+ JSON.stringify(results),
+ { overwriteRoutes: true },
+ );
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.REQUEST_QUERY_RESULTS,
+ query,
+ },
+ // missing below
+ {
+ type: actions.QUERY_SUCCESS,
+ query,
+ results,
+ },
+ ];
+ return store.dispatch(actions.fetchQueryResults(query)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('addQueryEditor', () => {
+ it('updates the tab state in the backend', () => {
+ expect.assertions(2);
+
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.ADD_QUERY_EDITOR,
+ queryEditor: { ...queryEditor, id: '1' },
+ },
+ ];
+ return store.dispatch(actions.addQueryEditor(queryEditor)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('setActiveQueryEditor', () => {
+ it('updates the tab state in the backend', () => {
+ expect.assertions(2);
+
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.SET_ACTIVE_QUERY_EDITOR,
+ queryEditor,
+ },
+ ];
+ return store.dispatch(actions.setActiveQueryEditor(queryEditor)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('removeQueryEditor', () => {
+ it('updates the tab state in the backend', () => {
+ expect.assertions(2);
+
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.REMOVE_QUERY_EDITOR,
+ queryEditor,
+ },
+ ];
+ return store.dispatch(actions.removeQueryEditor(queryEditor)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('queryEditorSetDb', () => {
+ it('updates the tab state in the backend', () => {
+ expect.assertions(2);
+
+ const dbId = 42;
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.QUERY_EDITOR_SETDB,
+ queryEditor,
+ dbId,
+ },
+ ];
+ return store.dispatch(actions.queryEditorSetDb(queryEditor, dbId)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('queryEditorSetSchema', () => {
+ it('updates the tab state in the backend', () => {
+ expect.assertions(2);
+
+ const schema = 'schema';
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.QUERY_EDITOR_SET_SCHEMA,
+ queryEditor,
+ schema,
+ },
+ ];
+ return store.dispatch(actions.queryEditorSetSchema(queryEditor, schema)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('queryEditorSetAutorun', () => {
+ it('updates the tab state in the backend', () => {
+ expect.assertions(2);
+
+ const autorun = true;
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.QUERY_EDITOR_SET_AUTORUN,
+ queryEditor,
+ autorun,
+ },
+ ];
+ return store.dispatch(actions.queryEditorSetAutorun(queryEditor, autorun)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('queryEditorSetTitle', () => {
+ it('updates the tab state in the backend', () => {
+ expect.assertions(2);
+
+ const title = 'title';
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.QUERY_EDITOR_SET_TITLE,
+ queryEditor,
+ title,
+ },
+ ];
+ return store.dispatch(actions.queryEditorSetTitle(queryEditor, title)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('queryEditorSetSql', () => {
+ it('updates the tab state in the backend', () => {
+ expect.assertions(2);
+
+ const sql = 'SELECT * ';
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.QUERY_EDITOR_SET_SQL,
+ queryEditor,
+ sql,
+ },
+ ];
+ return store.dispatch(actions.queryEditorSetSql(queryEditor, sql)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('queryEditorSetQueryLimit', () => {
+ it('updates the tab state in the backend', () => {
+ expect.assertions(2);
+
+ const queryLimit = 10;
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.QUERY_EDITOR_SET_QUERY_LIMIT,
+ queryEditor,
+ queryLimit,
+ },
+ ];
+ return store.dispatch(
+ actions.queryEditorSetQueryLimit(queryEditor, queryLimit))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('queryEditorSetTemplateParams', () => {
+ it('updates the tab state in the backend', () => {
+ expect.assertions(2);
+
+ const templateParams = '{"foo": "bar"}';
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.QUERY_EDITOR_SET_TEMPLATE_PARAMS,
+ queryEditor,
+ templateParams,
+ },
+ ];
+ return store.dispatch(
+ actions.queryEditorSetTemplateParams(queryEditor, templateParams))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('addTable', () => {
+ it('updates the table schema state in the backend', () => {
+ expect.assertions(5);
+
+ const results = {
+ data: mockBigNumber,
+ query: { sqlEditorId: 'null' },
+ query_id: 'efgh',
+ };
+ fetchMock.post(
+ runQueryEndpoint,
+ JSON.stringify(results),
+ { overwriteRoutes: true },
+ );
+
+ const tableName = 'table';
+ const schemaName = 'schema';
+ const store = mockStore({});
+ const expectedActionTypes = [
+ actions.MERGE_TABLE, // addTable
+ actions.MERGE_TABLE, // getTableMetadata
+ actions.START_QUERY, // runQuery (data preview)
+ actions.MERGE_TABLE, // getTableExtendedMetadata
+ actions.QUERY_SUCCESS, // querySuccess
+ actions.MERGE_TABLE, // addTable
+ ];
+ return store.dispatch(
+ actions.addTable(query, tableName, schemaName))
+ .then(() => {
+ expect(store.getActions().map(a => a.type)).toEqual(expectedActionTypes);
+ expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
+ expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1);
+ expect(fetchMock.calls(getExtraTableMetadataEndpoint)).toHaveLength(1);
+
+ // tab state is not updated, since the query is a data preview
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
+ });
+ });
+ });
+
+ describe('expandTable', () => {
+ it('updates the table schema state in the backend', () => {
+ expect.assertions(2);
+
+ const table = { id: 1 };
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.EXPAND_TABLE,
+ table,
+ },
+ ];
+ return store.dispatch(actions.expandTable(table)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('collapseTable', () => {
+ it('updates the table schema state in the backend', () => {
+ expect.assertions(2);
+
+ const table = { id: 1 };
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.COLLAPSE_TABLE,
+ table,
+ },
+ ];
+ return store.dispatch(actions.collapseTable(table)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('removeTable', () => {
+ it('updates the table schema state in the backend', () => {
+ expect.assertions(2);
+
+ const table = { id: 1 };
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.REMOVE_TABLE,
+ table,
+ },
+ ];
+ return store.dispatch(actions.removeTable(table)).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('migrateQueryEditorFromLocalStorage', () => {
+ it('updates the tab state in the backend', () => {
+ expect.assertions(3);
+
+ const results = {
+ data: mockBigNumber,
+ query: { sqlEditorId: 'null' },
+ query_id: 'efgh',
+ };
+ fetchMock.post(
+ runQueryEndpoint,
+ JSON.stringify(results),
+ { overwriteRoutes: true },
+ );
+
+ const tables = [
+ { id: 'one', dataPreviewQueryId: 'previewOne' },
+ { id: 'two', dataPreviewQueryId: 'previewTwo' },
+ ];
+ const queries = [
+ { ...query, id: 'previewOne' },
+ { ...query, id: 'previewTwo' },
+ ];
+ const store = mockStore({});
+ const expectedActions = [
+ {
+ type: actions.MIGRATE_QUERY_EDITOR,
+ oldQueryEditor: queryEditor,
+ // new qe has a different id
+ newQueryEditor: { ...queryEditor, id: '1' },
+ },
+ {
+ type: actions.MIGRATE_TAB_HISTORY,
+ newId: '1',
+ oldId: 'abcd',
+ },
+ {
+ type: actions.MIGRATE_TABLE,
+ oldTable: tables[0],
+ // new table has a different id and points to new query editor
+ newTable: { ...tables[0], id: 1, queryEditorId: '1' },
+ },
+ {
+ type: actions.MIGRATE_TABLE,
+ oldTable: tables[1],
+ // new table has a different id and points to new query editor
+ newTable: { ...tables[1], id: 1, queryEditorId: '1' },
+ },
+ {
+ type: actions.MIGRATE_QUERY,
+ queryId: 'previewOne',
+ queryEditorId: '1',
+ },
+ {
+ type: actions.MIGRATE_QUERY,
+ queryId: 'previewTwo',
+ queryEditorId: '1',
+ },
+ ];
+ return store.dispatch(
+ actions.migrateQueryEditorFromLocalStorage(queryEditor, tables, queries))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(3);
+
+ // query editor has 2 tables loaded in the schema viewer
+ expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(2);
+ });
+ });
+ });
+ });
});
diff --git a/superset/assets/spec/javascripts/sqllab/reducers/sqlLab_spec.js b/superset/assets/spec/javascripts/sqllab/reducers/sqlLab_spec.js
index abab7376b..d7aa11895 100644
--- a/superset/assets/spec/javascripts/sqllab/reducers/sqlLab_spec.js
+++ b/superset/assets/spec/javascripts/sqllab/reducers/sqlLab_spec.js
@@ -19,36 +19,11 @@
import sqlLabReducer from '../../../../src/SqlLab/reducers/sqlLab';
import * as actions from '../../../../src/SqlLab/actions/sqlLab';
import { table, initialState as mockState } from '../fixtures';
+import { now } from '../../../../src/modules/dates';
const initialState = mockState.sqlLab;
describe('sqlLabReducer', () => {
- describe('CLONE_QUERY_TO_NEW_TAB', () => {
- const testQuery = { sql: 'SELECT * FROM...', dbId: 1, id: 'flasj233' };
- let newState = {
- ...initialState,
- queries: { [testQuery.id]: testQuery },
- };
- beforeEach(() => {
- newState = sqlLabReducer(newState, actions.cloneQueryToNewTab(testQuery));
- });
-
- it('should have at most one more tab', () => {
- expect(newState.queryEditors).toHaveLength(2);
- });
-
- it('should have the same SQL as the cloned query', () => {
- expect(newState.queryEditors[1].sql).toBe(testQuery.sql);
- });
-
- it('should prefix the new tab title with "Copy of"', () => {
- expect(newState.queryEditors[1].title).toContain('Copy of');
- });
-
- it('should push the cloned tab onto tab history stack', () => {
- expect(newState.tabHistory[1]).toBe(newState.queryEditors[1].id);
- });
- });
describe('Query editors actions', () => {
let newState;
let defaultQueryEditor;
@@ -56,59 +31,107 @@ describe('sqlLabReducer', () => {
beforeEach(() => {
newState = { ...initialState };
defaultQueryEditor = newState.queryEditors[0];
- qe = Object.assign({}, defaultQueryEditor);
- newState = sqlLabReducer(newState, actions.addQueryEditor(qe));
- qe = newState.queryEditors[newState.queryEditors.length - 1];
+ const action = {
+ type: actions.ADD_QUERY_EDITOR,
+ queryEditor: { ...initialState.queryEditors[0], id: 'abcd' },
+ };
+ newState = sqlLabReducer(newState, action);
+ qe = newState.queryEditors.find(e => e.id === 'abcd');
});
it('should add a query editor', () => {
expect(newState.queryEditors).toHaveLength(2);
});
it('should remove a query editor', () => {
expect(newState.queryEditors).toHaveLength(2);
- newState = sqlLabReducer(newState, actions.removeQueryEditor(qe));
+ const action = {
+ type: actions.REMOVE_QUERY_EDITOR,
+ queryEditor: qe,
+ };
+ newState = sqlLabReducer(newState, action);
expect(newState.queryEditors).toHaveLength(1);
});
it('should set q query editor active', () => {
- newState = sqlLabReducer(newState, actions.addQueryEditor(qe));
- newState = sqlLabReducer(newState, actions.setActiveQueryEditor(defaultQueryEditor));
+ const addQueryEditorAction = {
+ type: actions.ADD_QUERY_EDITOR,
+ queryEditor: { ...initialState.queryEditors[0], id: 'abcd' },
+ };
+ newState = sqlLabReducer(newState, addQueryEditorAction);
+ const setActiveQueryEditorAction = {
+ type: actions.SET_ACTIVE_QUERY_EDITOR,
+ queryEditor: defaultQueryEditor,
+ };
+ newState = sqlLabReducer(newState, setActiveQueryEditorAction);
expect(newState.tabHistory[newState.tabHistory.length - 1]).toBe(defaultQueryEditor.id);
});
it('should not fail while setting DB', () => {
const dbId = 9;
- newState = sqlLabReducer(newState, actions.queryEditorSetDb(qe, dbId));
+ const action = {
+ type: actions.QUERY_EDITOR_SETDB,
+ queryEditor: qe,
+ dbId,
+ };
+ newState = sqlLabReducer(newState, action);
expect(newState.queryEditors[1].dbId).toBe(dbId);
});
it('should not fail while setting schema', () => {
const schema = 'foo';
- newState = sqlLabReducer(newState, actions.queryEditorSetSchema(qe, schema));
+ const action = {
+ type: actions.QUERY_EDITOR_SET_SCHEMA,
+ queryEditor: qe,
+ schema,
+ };
+ newState = sqlLabReducer(newState, action);
expect(newState.queryEditors[1].schema).toBe(schema);
});
it('should not fail while setting autorun ', () => {
- newState = sqlLabReducer(newState, actions.queryEditorSetAutorun(qe, false));
+ const action = {
+ type: actions.QUERY_EDITOR_SET_AUTORUN,
+ queryEditor: qe,
+ };
+ newState = sqlLabReducer(newState, { ...action, autorun: false });
expect(newState.queryEditors[1].autorun).toBe(false);
- newState = sqlLabReducer(newState, actions.queryEditorSetAutorun(qe, true));
+ newState = sqlLabReducer(newState, { ...action, autorun: true });
expect(newState.queryEditors[1].autorun).toBe(true);
});
it('should not fail while setting title', () => {
const title = 'a new title';
- newState = sqlLabReducer(newState, actions.queryEditorSetTitle(qe, title));
+ const action = {
+ type: actions.QUERY_EDITOR_SET_TITLE,
+ queryEditor: qe,
+ title,
+ };
+ newState = sqlLabReducer(newState, action);
expect(newState.queryEditors[1].title).toBe(title);
});
it('should not fail while setting Sql', () => {
const sql = 'SELECT nothing from dev_null';
- newState = sqlLabReducer(newState, actions.queryEditorSetSql(qe, sql));
+ const action = {
+ type: actions.QUERY_EDITOR_SET_SQL,
+ queryEditor: qe,
+ sql,
+ };
+ newState = sqlLabReducer(newState, action);
expect(newState.queryEditors[1].sql).toBe(sql);
});
it('should not fail while setting queryLimit', () => {
const queryLimit = 101;
- newState = sqlLabReducer(newState, actions.queryEditorSetQueryLimit(qe, queryLimit));
+ const action = {
+ type: actions.QUERY_EDITOR_SET_QUERY_LIMIT,
+ queryEditor: qe,
+ queryLimit,
+ };
+ newState = sqlLabReducer(newState, action);
expect(newState.queryEditors[1].queryLimit).toEqual(queryLimit);
});
it('should set selectedText', () => {
const selectedText = 'TEST';
+ const action = {
+ type: actions.QUERY_EDITOR_SET_SELECTED_TEXT,
+ queryEditor: newState.queryEditors[0],
+ sql: selectedText,
+ };
expect(newState.queryEditors[0].selectedText).toBeNull();
- newState = sqlLabReducer(
- newState, actions.queryEditorSetSelectedText(newState.queryEditors[0], 'TEST'));
+ newState = sqlLabReducer(newState, action);
expect(newState.queryEditors[0].selectedText).toBe(selectedText);
});
});
@@ -117,7 +140,11 @@ describe('sqlLabReducer', () => {
let newTable;
beforeEach(() => {
newTable = Object.assign({}, table);
- newState = sqlLabReducer(initialState, actions.mergeTable(newTable));
+ const action = {
+ type: actions.MERGE_TABLE,
+ table: newTable,
+ };
+ newState = sqlLabReducer(initialState, action);
newTable = newState.tables[0];
});
it('should add a table', () => {
@@ -127,42 +154,91 @@ describe('sqlLabReducer', () => {
it('should merge the table attributes', () => {
// Merging the extra attribute
newTable.extra = true;
- newState = sqlLabReducer(newState, actions.mergeTable(newTable));
+ const action = {
+ type: actions.MERGE_TABLE,
+ table: newTable,
+ };
+ newState = sqlLabReducer(newState, action);
expect(newState.tables).toHaveLength(1);
expect(newState.tables[0].extra).toBe(true);
});
it('should expand and collapse a table', () => {
- newState = sqlLabReducer(newState, actions.collapseTable(newTable));
+ const collapseTableAction = {
+ type: actions.COLLAPSE_TABLE,
+ table: newTable,
+ };
+ newState = sqlLabReducer(newState, collapseTableAction);
expect(newState.tables[0].expanded).toBe(false);
- newState = sqlLabReducer(newState, actions.expandTable(newTable));
+ const expandTableAction = {
+ type: actions.EXPAND_TABLE,
+ table: newTable,
+ };
+ newState = sqlLabReducer(newState, expandTableAction);
expect(newState.tables[0].expanded).toBe(true);
});
it('should remove a table', () => {
- newState = sqlLabReducer(newState, actions.removeTable(newTable));
+ const action = {
+ type: actions.REMOVE_TABLE,
+ table: newTable,
+ };
+ newState = sqlLabReducer(newState, action);
expect(newState.tables).toHaveLength(0);
});
});
describe('Run Query', () => {
let newState;
let query;
- let newQuery;
beforeEach(() => {
newState = { ...initialState };
- newQuery = { ...query };
+ query = {
+ id: 'abcd',
+ progress: 0,
+ startDttm: now(),
+ state: 'running',
+ cached: false,
+ sqlEditorId: 'dfsadfs',
+ };
});
it('should start a query', () => {
- newState = sqlLabReducer(newState, actions.startQuery(newQuery));
+ const action = {
+ type: actions.START_QUERY,
+ query: {
+ id: 'abcd',
+ progress: 0,
+ startDttm: now(),
+ state: 'running',
+ cached: false,
+ sqlEditorId: 'dfsadfs',
+ },
+ };
+ newState = sqlLabReducer(newState, action);
expect(Object.keys(newState.queries)).toHaveLength(1);
});
it('should stop the query', () => {
- newState = sqlLabReducer(newState, actions.startQuery(newQuery));
- newState = sqlLabReducer(newState, actions.stopQuery(newQuery));
+ const startQueryAction = {
+ type: actions.START_QUERY,
+ query,
+ };
+ newState = sqlLabReducer(newState, startQueryAction);
+ const stopQueryAction = {
+ type: actions.STOP_QUERY,
+ query,
+ };
+ newState = sqlLabReducer(newState, stopQueryAction);
const q = newState.queries[Object.keys(newState.queries)[0]];
expect(q.state).toBe('stopped');
});
it('should remove a query', () => {
- newState = sqlLabReducer(newState, actions.startQuery(newQuery));
- newState = sqlLabReducer(newState, actions.removeQuery(newQuery));
+ const startQueryAction = {
+ type: actions.START_QUERY,
+ query,
+ };
+ newState = sqlLabReducer(newState, startQueryAction);
+ const removeQueryAction = {
+ type: actions.REMOVE_QUERY,
+ query,
+ };
+ newState = sqlLabReducer(newState, removeQueryAction);
expect(Object.keys(newState.queries)).toHaveLength(0);
});
it('should refresh queries when polling returns empty', () => {
diff --git a/superset/assets/src/SqlLab/App.jsx b/superset/assets/src/SqlLab/App.jsx
index 54c4711e4..59e40b6b2 100644
--- a/superset/assets/src/SqlLab/App.jsx
+++ b/superset/assets/src/SqlLab/App.jsx
@@ -22,7 +22,7 @@ import { Provider } from 'react-redux';
import thunkMiddleware from 'redux-thunk';
import { hot } from 'react-hot-loader';
-import { initFeatureFlags } from 'src/featureFlags';
+import { initFeatureFlags, isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import getInitialState from './reducers/getInitialState';
import rootReducer from './reducers/index';
import { initEnhancer } from '../reduxUtils';
@@ -79,7 +79,10 @@ const store = createStore(
initialState,
compose(
applyMiddleware(thunkMiddleware),
- initEnhancer(true, sqlLabPersistStateConfig),
+ initEnhancer(
+ !isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE),
+ sqlLabPersistStateConfig,
+ ),
),
);
diff --git a/superset/assets/src/SqlLab/actions/sqlLab.js b/superset/assets/src/SqlLab/actions/sqlLab.js
index a540cec28..5381ff70a 100644
--- a/superset/assets/src/SqlLab/actions/sqlLab.js
+++ b/superset/assets/src/SqlLab/actions/sqlLab.js
@@ -22,12 +22,14 @@ import { t } from '@superset-ui/translation';
import { SupersetClient } from '@superset-ui/connection';
import invert from 'lodash/invert';
import mapKeys from 'lodash/mapKeys';
+import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import { now } from '../../modules/dates';
import {
- addSuccessToast as addSuccessToastAction,
addDangerToast as addDangerToastAction,
addInfoToast as addInfoToastAction,
+ addSuccessToast as addSuccessToastAction,
+ addWarningToast as addWarningToastAction,
} from '../../messageToasts/actions/index';
import getClientErrorObject from '../../utils/getClientErrorObject';
import COMMON_ERR_MESSAGES from '../../utils/errorMessages';
@@ -55,9 +57,15 @@ export const QUERY_EDITOR_SET_QUERY_LIMIT = 'QUERY_EDITOR_SET_QUERY_LIMIT';
export const QUERY_EDITOR_SET_TEMPLATE_PARAMS = 'QUERY_EDITOR_SET_TEMPLATE_PARAMS';
export const QUERY_EDITOR_SET_SELECTED_TEXT = 'QUERY_EDITOR_SET_SELECTED_TEXT';
export const QUERY_EDITOR_PERSIST_HEIGHT = 'QUERY_EDITOR_PERSIST_HEIGHT';
+export const MIGRATE_QUERY_EDITOR = 'MIGRATE_QUERY_EDITOR';
+export const MIGRATE_TAB_HISTORY = 'MIGRATE_TAB_HISTORY';
+export const MIGRATE_TABLE = 'MIGRATE_TABLE';
+export const MIGRATE_QUERY = 'MIGRATE_QUERY';
export const SET_DATABASES = 'SET_DATABASES';
export const SET_ACTIVE_QUERY_EDITOR = 'SET_ACTIVE_QUERY_EDITOR';
+export const LOAD_QUERY_EDITOR = 'LOAD_QUERY_EDITOR';
+export const SET_TABLES = 'SET_TABLES';
export const SET_ACTIVE_SOUTHPANE_TAB = 'SET_ACTIVE_SOUTHPANE_TAB';
export const REFRESH_QUERIES = 'REFRESH_QUERIES';
export const SET_USER_OFFLINE = 'SET_USER_OFFLINE';
@@ -85,6 +93,7 @@ export const CREATE_DATASOURCE_FAILED = 'CREATE_DATASOURCE_FAILED';
export const addInfoToast = addInfoToastAction;
export const addSuccessToast = addSuccessToastAction;
export const addDangerToast = addDangerToastAction;
+export const addWarningToast = addWarningToastAction;
// a map of SavedQuery field names to the different names used client-side,
// because for now making the names consistent is too complicated
@@ -201,11 +210,39 @@ export function startQuery(query) {
}
export function querySuccess(query, results) {
- return { type: QUERY_SUCCESS, query, results };
+ return function (dispatch) {
+ const sync = (!query.isDataPreview && isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE))
+ ? SupersetClient.put({
+ endpoint: encodeURI(`/tabstateview/${results.query.sqlEditorId}`),
+ postPayload: { latest_query_id: query.id },
+ })
+ : Promise.resolve();
+
+ return sync
+ .then(() => dispatch({ type: QUERY_SUCCESS, query, results }))
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while storing the latest query id in the backend. ' +
+ 'Please contact your administrator if this problem persists.'))));
+ };
}
export function queryFailed(query, msg, link) {
- return { type: QUERY_FAILED, query, msg, link };
+ return function (dispatch) {
+ const sync = (!query.isDataPreview && isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE))
+ ? SupersetClient.put({
+ endpoint: encodeURI(`/tabstateview/${query.sqlEditorId}`),
+ postPayload: { latest_query_id: query.id },
+ })
+ : Promise.resolve();
+
+ return sync
+ .then(() => dispatch({ type: QUERY_FAILED, query, msg, link }))
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while storing the latest query id in the backend. ' +
+ 'Please contact your administrator if this problem persists.'))));
+ };
}
export function stopQuery(query) {
@@ -234,7 +271,7 @@ export function fetchQueryResults(query, displayLimit) {
})
.then(({ text = '{}' }) => {
const bigIntJson = JSONbig.parse(text);
- dispatch(querySuccess(query, bigIntJson));
+ return dispatch(querySuccess(query, bigIntJson));
})
.catch(response =>
getClientErrorObject(response).then((error) => {
@@ -309,9 +346,7 @@ export function validateQuery(query) {
postPayload,
stringify: false,
})
- .then(({ json }) => {
- dispatch(queryValidationReturned(query, json));
- })
+ .then(({ json }) => dispatch(queryValidationReturned(query, json)))
.catch(response =>
getClientErrorObject(response).then((error) => {
let message = error.error || error.statusText || t('Unknown error');
@@ -341,20 +376,189 @@ export function setDatabases(databases) {
return { type: SET_DATABASES, databases };
}
-export function addQueryEditor(queryEditor) {
- const newQueryEditor = {
- ...queryEditor,
- id: shortid.generate(),
+function migrateTable(table, queryEditorId, dispatch) {
+ return SupersetClient.post({
+ endpoint: encodeURI('/tableschemaview/'),
+ postPayload: { table: { ...table, queryEditorId } },
+ })
+ .then(({ json }) => {
+ const newTable = {
+ ...table,
+ id: json.id,
+ queryEditorId,
+ };
+ return dispatch({ type: MIGRATE_TABLE, oldTable: table, newTable });
+ })
+ .catch(() => dispatch(addWarningToast(t(
+ 'Unable to migrate table schema state to backend. Superset will retry ' +
+ 'later. Please contact your administrator if this problem persists.'))));
+}
+
+function migrateQuery(queryId, queryEditorId, dispatch) {
+ return SupersetClient.post({
+ endpoint: encodeURI(`/tabstateview/${queryEditorId}/migrate_query`),
+ postPayload: { queryId },
+ })
+ .then(() => dispatch({ type: MIGRATE_QUERY, queryId, queryEditorId }))
+ .catch(() => dispatch(addWarningToast(t(
+ 'Unable to migrate query state to backend. Superset will retry later. ' +
+ 'Please contact your administrator if this problem persists.'))));
+}
+
+export function migrateQueryEditorFromLocalStorage(queryEditor, tables, queries) {
+ return function (dispatch) {
+ return SupersetClient.post({ endpoint: '/tabstateview/', postPayload: { queryEditor } })
+ .then(({ json }) => {
+ const newQueryEditor = {
+ ...queryEditor,
+ id: json.id.toString(),
+ };
+ dispatch({ type: MIGRATE_QUERY_EDITOR, oldQueryEditor: queryEditor, newQueryEditor });
+ dispatch({ type: MIGRATE_TAB_HISTORY, oldId: queryEditor.id, newId: newQueryEditor.id });
+ return Promise.all([
+ ...tables.map(table => migrateTable(table, newQueryEditor.id, dispatch)),
+ ...queries.map(query => migrateQuery(query.id, newQueryEditor.id, dispatch)),
+ ]);
+ })
+ .catch(() => dispatch(addWarningToast(t(
+ 'Unable to migrate query editor state to backend. Superset will retry ' +
+ 'later. Please contact your administrator if this problem persists.'))));
+ };
+}
+
+export function addQueryEditor(queryEditor) {
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.post({ endpoint: '/tabstateview/', postPayload: { queryEditor } })
+ : Promise.resolve({ json: { id: shortid.generate() } });
+
+ return sync
+ .then(({ json }) => {
+ const newQueryEditor = {
+ ...queryEditor,
+ id: json.id.toString(),
+ };
+ return dispatch({ type: ADD_QUERY_EDITOR, queryEditor: newQueryEditor });
+ })
+ .catch(() => dispatch(addDangerToast(t(
+ 'Unable to add a new tab to the backend. Please contact your administrator.'))));
};
- return { type: ADD_QUERY_EDITOR, queryEditor: newQueryEditor };
}
export function cloneQueryToNewTab(query) {
- return { type: CLONE_QUERY_TO_NEW_TAB, query };
+ return function (dispatch, getState) {
+ const state = getState();
+ const { queryEditors, tabHistory } = state.sqlLab;
+ const sourceQueryEditor = queryEditors.find(qe => qe.id === tabHistory[tabHistory.length - 1]);
+ const queryEditor = {
+ title: t('Copy of %s', sourceQueryEditor.title),
+ dbId: query.dbId ? query.dbId : null,
+ schema: query.schema ? query.schema : null,
+ autorun: true,
+ sql: query.sql,
+ queryLimit: sourceQueryEditor.queryLimit,
+ maxRow: sourceQueryEditor.maxRow,
+ };
+ return dispatch(addQueryEditor(queryEditor));
+ };
}
export function setActiveQueryEditor(queryEditor) {
- return { type: SET_ACTIVE_QUERY_EDITOR, queryEditor };
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.post({ endpoint: encodeURI(`/tabstateview/${queryEditor.id}/activate`) })
+ : Promise.resolve();
+
+ return sync
+ .then(() => dispatch({ type: SET_ACTIVE_QUERY_EDITOR, queryEditor }))
+ .catch((response) => {
+ if (response.status !== 404) {
+ return dispatch(addDangerToast(t(
+ 'An error occurred while setting the active tab. Please contact ' +
+ 'your administrator.')));
+ }
+ return dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor });
+ });
+ };
+}
+
+export function loadQueryEditor(queryEditor) {
+ return { type: LOAD_QUERY_EDITOR, queryEditor };
+}
+
+export function setTables(tableSchemas) {
+ const tables = tableSchemas.map((tableSchema) => {
+ const {
+ columns,
+ selectStar,
+ primaryKey,
+ foreignKeys,
+ indexes,
+ dataPreviewQueryId,
+ } = tableSchema.description;
+ return {
+ dbId: tableSchema.database_id,
+ queryEditorId: tableSchema.tab_state_id.toString(),
+ schema: tableSchema.schema,
+ name: tableSchema.table,
+ expanded: tableSchema.expanded,
+ id: tableSchema.id,
+ dataPreviewQueryId,
+ columns,
+ selectStar,
+ primaryKey,
+ foreignKeys,
+ indexes,
+ isMetadataLoading: false,
+ isExtraMetadataLoading: false,
+ };
+ });
+ return { type: SET_TABLES, tables };
+}
+
+export function switchQueryEditor(queryEditor, displayLimit) {
+ return function (dispatch) {
+ if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) && !queryEditor.loaded) {
+ SupersetClient.get({
+ endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
+ })
+ .then(({ json }) => {
+ const loadedQueryEditor = {
+ id: json.id.toString(),
+ loaded: true,
+ title: json.label,
+ sql: json.sql,
+ selectedText: null,
+ latestQueryId: json.latest_query ? json.latest_query.id : null,
+ autorun: json.autorun,
+ dbId: json.database_id,
+ templateParams: json.template_params,
+ schema: json.schema,
+ queryLimit: json.query_limit,
+ validationResult: {
+ id: null,
+ errors: [],
+ completed: false,
+ },
+ };
+ dispatch(loadQueryEditor(loadedQueryEditor));
+ dispatch(setTables(json.table_schemas || []));
+ dispatch(setActiveQueryEditor(loadedQueryEditor));
+ if (json.latest_query && json.latest_query.resultsKey) {
+ dispatch(fetchQueryResults(json.latest_query, displayLimit));
+ }
+ })
+ .catch((response) => {
+ if (response.status !== 404) {
+ return dispatch(addDangerToast(t(
+ 'An error occurred while fetching tab state')));
+ }
+ return dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor });
+ });
+ } else {
+ dispatch(setActiveQueryEditor(queryEditor));
+ }
+ };
}
export function setActiveSouthPaneTab(tabId) {
@@ -362,19 +566,75 @@ export function setActiveSouthPaneTab(tabId) {
}
export function removeQueryEditor(queryEditor) {
- return { type: REMOVE_QUERY_EDITOR, queryEditor };
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.delete({ endpoint: encodeURI(`/tabstateview/${queryEditor.id}`) })
+ : Promise.resolve();
+
+ return sync
+ .then(() =>
+ dispatch({ type: REMOVE_QUERY_EDITOR, queryEditor }),
+ )
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while removing tab. Please contact your administrator.'))),
+ );
+ };
}
export function removeQuery(query) {
- return { type: REMOVE_QUERY, query };
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.delete({
+ endpoint: encodeURI(`/tabstateview/${query.sqlEditorId}/query/${query.id}`),
+ })
+ : Promise.resolve();
+
+ return sync
+ .then(() =>
+ dispatch({ type: REMOVE_QUERY, query }),
+ )
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while removing query. Please contact your administrator.'))),
+ );
+ };
}
export function queryEditorSetDb(queryEditor, dbId) {
- return { type: QUERY_EDITOR_SETDB, queryEditor, dbId };
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.put({
+ endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
+ postPayload: { database_id: dbId },
+ })
+ : Promise.resolve();
+
+ return sync
+ .then(() => dispatch({ type: QUERY_EDITOR_SETDB, queryEditor, dbId }))
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while setting the tab database ID. Please contact your administrator.'))),
+ );
+ };
}
export function queryEditorSetSchema(queryEditor, schema) {
- return { type: QUERY_EDITOR_SET_SCHEMA, queryEditor, schema };
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.put({
+ endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
+ postPayload: { schema },
+ })
+ : Promise.resolve();
+
+ return sync
+ .then(() => dispatch({ type: QUERY_EDITOR_SET_SCHEMA, queryEditor, schema }))
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while setting the tab schema. Please contact your administrator.'))),
+ );
+ };
}
export function queryEditorSetSchemaOptions(queryEditor, options) {
@@ -386,23 +646,96 @@ export function queryEditorSetTableOptions(queryEditor, options) {
}
export function queryEditorSetAutorun(queryEditor, autorun) {
- return { type: QUERY_EDITOR_SET_AUTORUN, queryEditor, autorun };
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.put({
+ endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
+ postPayload: { autorun },
+ })
+ : Promise.resolve();
+
+ return sync
+ .then(() => dispatch({ type: QUERY_EDITOR_SET_AUTORUN, queryEditor, autorun }))
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while setting the tab autorun. Please contact your administrator.'))),
+ );
+ };
}
export function queryEditorSetTitle(queryEditor, title) {
- return { type: QUERY_EDITOR_SET_TITLE, queryEditor, title };
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.put({
+ endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
+ postPayload: { label: title },
+ })
+ : Promise.resolve();
+
+ return sync
+ .then(() => dispatch({ type: QUERY_EDITOR_SET_TITLE, queryEditor, title }))
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while setting the tab title. Please contact your administrator.'))),
+ );
+ };
}
export function queryEditorSetSql(queryEditor, sql) {
- return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql };
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.put({
+ endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
+ postPayload: { sql },
+ })
+ : Promise.resolve();
+
+ return sync
+ .then(() => dispatch({ type: QUERY_EDITOR_SET_SQL, queryEditor, sql }))
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while storing your query in the backend. To ' +
+ 'avoid losing your changes, please save your query using the ' +
+ '"Save Query" button.'))),
+ );
+ };
}
export function queryEditorSetQueryLimit(queryEditor, queryLimit) {
- return { type: QUERY_EDITOR_SET_QUERY_LIMIT, queryEditor, queryLimit };
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.put({
+ endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
+ postPayload: { query_limit: queryLimit },
+ })
+ : Promise.resolve();
+
+ return sync
+ .then(() => dispatch({ type: QUERY_EDITOR_SET_QUERY_LIMIT, queryEditor, queryLimit }))
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while setting the tab title. Please contact your administrator.'))),
+ );
+ };
}
export function queryEditorSetTemplateParams(queryEditor, templateParams) {
- return { type: QUERY_EDITOR_SET_TEMPLATE_PARAMS, queryEditor, templateParams };
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.put({
+ endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
+ postPayload: { template_params: templateParams },
+ })
+ : Promise.resolve();
+
+ return sync
+ .then(() => dispatch({ type: QUERY_EDITOR_SET_TEMPLATE_PARAMS, queryEditor, templateParams }))
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while setting the tab template parameters. ' +
+ 'Please contact your administrator.'))),
+ );
+ };
}
export function queryEditorSetSelectedText(queryEditor, sql) {
@@ -413,6 +746,64 @@ export function mergeTable(table, query) {
return { type: MERGE_TABLE, table, query };
}
+function getTableMetadata(table, query, dispatch) {
+ return SupersetClient.get({ endpoint: encodeURI(`/superset/table/${query.dbId}/` +
+ `${encodeURIComponent(table.name)}/${encodeURIComponent(table.schema)}/`) })
+ .then(({ json }) => {
+ const dataPreviewQuery = {
+ id: shortid.generate(),
+ dbId: query.dbId,
+ sql: json.selectStar,
+ tableName: table.name,
+ sqlEditorId: null,
+ tab: '',
+ runAsync: false,
+ ctas: false,
+ isDataPreview: true,
+ };
+ const newTable = {
+ ...table,
+ ...json,
+ expanded: true,
+ isMetadataLoading: false,
+ dataPreviewQueryId: dataPreviewQuery.id,
+ };
+ Promise.all([
+ dispatch(mergeTable(newTable, dataPreviewQuery)), // Merge table to tables in state
+ dispatch(runQuery(dataPreviewQuery)), // Run query to get preview data for table
+ ]);
+ return newTable;
+ })
+ .catch(() =>
+ Promise.all([
+ dispatch(
+ mergeTable({
+ ...table,
+ isMetadataLoading: false,
+ }),
+ ),
+ dispatch(addDangerToast(t('An error occurred while fetching table metadata'))),
+ ]),
+ );
+}
+
+function getTableExtendedMetadata(table, query, dispatch) {
+ return SupersetClient.get({
+ endpoint: encodeURI(`/superset/extra_table_metadata/${query.dbId}/` +
+ `${encodeURIComponent(table.name)}/${encodeURIComponent(table.schema)}/`),
+ })
+ .then(({ json }) => {
+ dispatch(mergeTable({ ...table, ...json, isExtraMetadataLoading: false }));
+ return json;
+ })
+ .catch(() =>
+ Promise.all([
+ dispatch(mergeTable({ ...table, isExtraMetadataLoading: false })),
+ dispatch(addDangerToast(t('An error occurred while fetching table metadata'))),
+ ]),
+ );
+}
+
export function addTable(query, tableName, schemaName) {
return function (dispatch) {
const table = {
@@ -430,56 +821,28 @@ export function addTable(query, tableName, schemaName) {
}),
);
- SupersetClient.get({ endpoint: encodeURI(`/superset/table/${query.dbId}/` +
- `${encodeURIComponent(tableName)}/${encodeURIComponent(schemaName)}/`) })
- .then(({ json }) => {
- const dataPreviewQuery = {
- id: shortid.generate(),
- dbId: query.dbId,
- sql: json.selectStar,
- tableName,
- sqlEditorId: null,
- tab: '',
- runAsync: false,
- ctas: false,
- };
- const newTable = {
- ...table,
- ...json,
- expanded: true,
- isMetadataLoading: false,
- };
+ return Promise.all([
+ getTableMetadata(table, query, dispatch),
+ getTableExtendedMetadata(table, query, dispatch),
+ ])
+ .then(([newTable, json]) => {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.post({
+ endpoint: encodeURI('/tableschemaview/'),
+ postPayload: { table: { ...newTable, ...json } },
+ })
+ : Promise.resolve({ json: { id: shortid.generate() } });
- return Promise.all([
- dispatch(mergeTable(newTable, dataPreviewQuery)), // Merge table to tables in state
- dispatch(runQuery(dataPreviewQuery)), // Run query to get preview data for table
- ]);
- })
- .catch(() =>
- Promise.all([
- dispatch(
- mergeTable({
- ...table,
- isMetadataLoading: false,
- }),
- ),
- dispatch(addDangerToast(t('An error occurred while fetching table metadata'))),
- ]),
- );
-
- SupersetClient.get({
- endpoint: encodeURI(`/superset/extra_table_metadata/${query.dbId}/` +
- `${encodeURIComponent(tableName)}/${encodeURIComponent(schemaName)}/`),
- })
- .then(({ json }) =>
- dispatch(mergeTable({ ...table, ...json, isExtraMetadataLoading: false })),
- )
- .catch(() =>
- Promise.all([
- dispatch(mergeTable({ ...table, isExtraMetadataLoading: false })),
- dispatch(addDangerToast(t('An error occurred while fetching table metadata'))),
- ]),
- );
+ return sync
+ .then(({ json: resultJson }) =>
+ dispatch(mergeTable({ ...table, id: resultJson.id })),
+ )
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while fetching table metadata. ' +
+ 'Please contact your administrator.'))),
+ );
+ });
};
}
@@ -499,6 +862,7 @@ export function reFetchQueryResults(query) {
runAsync: false,
ctas: false,
queryLimit: query.queryLimit,
+ isDataPreview: query.isDataPreview,
};
dispatch(runQuery(newQuery));
dispatch(changeDataPreviewId(query.id, newQuery));
@@ -506,15 +870,57 @@ export function reFetchQueryResults(query) {
}
export function expandTable(table) {
- return { type: EXPAND_TABLE, table };
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.post({
+ endpoint: encodeURI(`/tableschemaview/${table.id}/expanded`),
+ postPayload: { expanded: true },
+ })
+ : Promise.resolve();
+
+ return sync
+ .then(() => dispatch({ type: EXPAND_TABLE, table }))
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while expanding the table schema. ' +
+ 'Please contact your administrator.'))),
+ );
+ };
}
export function collapseTable(table) {
- return { type: COLLAPSE_TABLE, table };
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.post({
+ endpoint: encodeURI(`/tableschemaview/${table.id}/expanded`),
+ postPayload: { expanded: false },
+ })
+ : Promise.resolve();
+
+ return sync
+ .then(() => dispatch({ type: COLLAPSE_TABLE, table }))
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while collapsing the table schema. ' +
+ 'Please contact your administrator.'))),
+ );
+ };
}
export function removeTable(table) {
- return { type: REMOVE_TABLE, table };
+ return function (dispatch) {
+ const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? SupersetClient.delete({ endpoint: encodeURI(`/tableschemaview/${table.id}`) })
+ : Promise.resolve();
+
+ return sync
+ .then(() => dispatch({ type: REMOVE_TABLE, table }))
+ .catch(() =>
+ dispatch(addDangerToast(t(
+ 'An error occurred while removing the table schema. ' +
+ 'Please contact your administrator.'))),
+ );
+ };
}
export function refreshQueries(alteredQueries) {
diff --git a/superset/assets/src/SqlLab/components/App.jsx b/superset/assets/src/SqlLab/components/App.jsx
index 0124f8d8c..418a03f9c 100644
--- a/superset/assets/src/SqlLab/components/App.jsx
+++ b/superset/assets/src/SqlLab/components/App.jsx
@@ -126,15 +126,15 @@ class App extends React.PureComponent {
App.propTypes = {
actions: PropTypes.object,
- localStorageUsageInKilobytes: PropTypes.number.isRequired,
common: PropTypes.object,
+ localStorageUsageInKilobytes: PropTypes.number.isRequired,
};
function mapStateToProps(state) {
- const { localStorageUsageInKilobytes, common } = state;
+ const { common, localStorageUsageInKilobytes } = state;
return {
- localStorageUsageInKilobytes,
common,
+ localStorageUsageInKilobytes,
};
}
diff --git a/superset/assets/src/SqlLab/components/LimitControl.jsx b/superset/assets/src/SqlLab/components/LimitControl.jsx
index 7b9734ba3..185d76a6a 100644
--- a/superset/assets/src/SqlLab/components/LimitControl.jsx
+++ b/superset/assets/src/SqlLab/components/LimitControl.jsx
@@ -42,7 +42,7 @@ export default class LimitControl extends React.PureComponent {
super(props);
const { value, defaultQueryLimit } = props;
this.state = {
- textValue: value.toString() || defaultQueryLimit.toString(),
+ textValue: (value || defaultQueryLimit).toString(),
showOverlay: false,
};
this.handleHide = this.handleHide.bind(this);
diff --git a/superset/assets/src/SqlLab/components/ResultSet.jsx b/superset/assets/src/SqlLab/components/ResultSet.jsx
index b0f5c7e42..7db4a06b9 100644
--- a/superset/assets/src/SqlLab/components/ResultSet.jsx
+++ b/superset/assets/src/SqlLab/components/ResultSet.jsx
@@ -200,7 +200,7 @@ export default class ResultSet extends React.PureComponent {
);
- } else if (query.state === 'success') {
+ } else if (query.state === 'success' && query.results) {
const results = query.results;
let data;
if (this.props.cache && query.cached) {
@@ -229,13 +229,13 @@ export default class ResultSet extends React.PureComponent {
return {t('The query returned no data')};
}
}
- if (query.cached) {
+ if (query.cached || (query.state === 'success' && !query.results)) {
return (
diff --git a/superset/assets/src/SqlLab/components/SouthPane.jsx b/superset/assets/src/SqlLab/components/SouthPane.jsx
index e68d05c9a..92c9934de 100644
--- a/superset/assets/src/SqlLab/components/SouthPane.jsx
+++ b/superset/assets/src/SqlLab/components/SouthPane.jsx
@@ -23,6 +23,7 @@ import { Alert, Label, Tab, Tabs } from 'react-bootstrap';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { t } from '@superset-ui/translation';
+import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import * as Actions from '../actions/sqlLab';
import QueryHistory from './QueryHistory';
@@ -88,19 +89,25 @@ export class SouthPane extends React.PureComponent {
latestQuery = props.editorQueries.find(q => q.id === this.props.latestQueryId);
}
let results;
- if (latestQuery &&
- (Date.now() - latestQuery.startDttm) <= LOCALSTORAGE_MAX_QUERY_AGE_MS) {
- results = (
-
- );
+ if (latestQuery) {
+ if (
+ isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
+ (!latestQuery.resultsKey && !latestQuery.results)
+ ) {
+ results = {t('No stored results found, you need to re-run your query')};
+ } else if ((Date.now() - latestQuery.startDttm) <= LOCALSTORAGE_MAX_QUERY_AGE_MS) {
+ results = (
+
+ );
+ }
} else {
results = {t('Run a query to display results here')};
}
diff --git a/superset/assets/src/SqlLab/components/SqlEditor.jsx b/superset/assets/src/SqlLab/components/SqlEditor.jsx
index d4cc424a4..f6c52c231 100644
--- a/superset/assets/src/SqlLab/components/SqlEditor.jsx
+++ b/superset/assets/src/SqlLab/components/SqlEditor.jsx
@@ -57,6 +57,7 @@ import { FeatureFlag, isFeatureEnabled } from '../../featureFlags';
const SQL_EDITOR_PADDING = 10;
const INITIAL_NORTH_PERCENT = 30;
const INITIAL_SOUTH_PERCENT = 70;
+const SET_QUERY_EDITOR_SQL_DEBOUNCE_MS = 2000;
const VALIDATION_DEBOUNCE_MS = 600;
const WINDOW_RESIZE_THROTTLE_MS = 100;
@@ -104,6 +105,10 @@ class SqlEditor extends React.PureComponent {
this.stopQuery = this.stopQuery.bind(this);
this.onSqlChanged = this.onSqlChanged.bind(this);
this.setQueryEditorSql = this.setQueryEditorSql.bind(this);
+ this.setQueryEditorSqlWithDebounce = debounce(
+ this.setQueryEditorSql.bind(this),
+ SET_QUERY_EDITOR_SQL_DEBOUNCE_MS,
+ );
this.queryPane = this.queryPane.bind(this);
this.getAceEditorAndSouthPaneHeights = this.getAceEditorAndSouthPaneHeights.bind(this);
this.getSqlEditorHeight = this.getSqlEditorHeight.bind(this);
@@ -151,6 +156,7 @@ class SqlEditor extends React.PureComponent {
}
onSqlChanged(sql) {
this.setState({ sql });
+ this.setQueryEditorSqlWithDebounce(sql);
// Request server-side validation of the query text
if (this.canValidateQuery()) {
// NB. requestValidation is debounced
@@ -274,6 +280,7 @@ class SqlEditor extends React.PureComponent {
queryLimit: qe.queryLimit || this.props.defaultQueryLimit,
runAsync: this.props.database ? this.props.database.allow_run_async : false,
ctas,
+ updateTabState: !qe.selectedText,
};
this.props.actions.runQuery(query);
this.props.actions.setActiveSouthPaneTab('Results');
diff --git a/superset/assets/src/SqlLab/components/TabbedSqlEditors.jsx b/superset/assets/src/SqlLab/components/TabbedSqlEditors.jsx
index 3e37123b3..0ee19ffb6 100644
--- a/superset/assets/src/SqlLab/components/TabbedSqlEditors.jsx
+++ b/superset/assets/src/SqlLab/components/TabbedSqlEditors.jsx
@@ -23,6 +23,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import URI from 'urijs';
import { t } from '@superset-ui/translation';
+import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import * as Actions from '../actions/sqlLab';
import SqlEditor from './SqlEditor';
@@ -70,6 +71,20 @@ class TabbedSqlEditors extends React.PureComponent {
this.duplicateQueryEditor = this.duplicateQueryEditor.bind(this);
}
componentDidMount() {
+ // migrate query editor and associated tables state to server
+ if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) {
+ const localStorageTables = this.props.tables.filter(table => table.inLocalStorage);
+ const localStorageQueries = Object.values(this.props.queries)
+ .filter(query => query.inLocalStorage);
+ this.props.queryEditors.filter(qe => qe.inLocalStorage).forEach((qe) => {
+ // get all queries associated with the query editor
+ const queries = localStorageQueries
+ .filter(query => query.sqlEditorId === qe.id);
+ const tables = localStorageTables.filter(table => table.queryEditorId === qe.id);
+ this.props.actions.migrateQueryEditorFromLocalStorage(qe, tables, queries);
+ });
+ }
+
const query = URI(window.location).search(true);
// Popping a new tab based on the querystring
if (query.id || query.sql || query.savedQueryId || query.datasourceKey) {
@@ -104,6 +119,19 @@ class TabbedSqlEditors extends React.PureComponent {
this.props.actions.addQueryEditor(newQueryEditor);
}
this.popNewTab();
+ } else if (this.props.queryEditors.length === 0) {
+ this.newQueryEditor();
+ } else {
+ const qe = this.activeQueryEditor();
+ const latestQuery = this.props.queries[qe.latestQueryId];
+ if (
+ isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
+ latestQuery && latestQuery.resultsKey
+ ) {
+ // when results are not stored in localStorage they need to be
+ // fetched from the results backend (if configured)
+ this.props.actions.fetchQueryResults(latestQuery, this.props.displayLimit);
+ }
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
@@ -122,7 +150,7 @@ class TabbedSqlEditors extends React.PureComponent {
nextProps.tables.forEach((table) => {
const queryId = table.dataPreviewQueryId;
if (queryId && nextProps.queries[queryId] && table.queryEditorId === nextActiveQeId) {
- dataPreviewQueries.push(nextProps.queries[queryId]);
+ dataPreviewQueries.push({ ...nextProps.queries[queryId], tableName: table.name });
}
});
if (!areArraysShallowEqual(dataPreviewQueries, this.state.dataPreviewQueries)) {
@@ -142,29 +170,31 @@ class TabbedSqlEditors extends React.PureComponent {
}
}
activeQueryEditor() {
- const qeid = this.props.tabHistory[this.props.tabHistory.length - 1];
- for (let i = 0; i < this.props.queryEditors.length; i++) {
- const qe = this.props.queryEditors[i];
- if (qe.id === qeid) {
- return qe;
- }
+ if (this.props.tabHistory.length === 0) {
+ return this.props.queryEditors[0];
}
- return null;
+ const qeid = this.props.tabHistory[this.props.tabHistory.length - 1];
+ return this.props.queryEditors.find(qe => qe.id === qeid) || null;
}
newQueryEditor() {
queryCount++;
const activeQueryEditor = this.activeQueryEditor();
+ const firstDbId = Math.min(
+ ...Object.values(this.props.databases).map(database => database.id));
+ const warning = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
+ ? ''
+ : `${t(
+ '-- Note: Unless you save your query, these tabs will NOT persist if you clear your cookies or change browsers.',
+ )}\n\n`;
const qe = {
title: t('Untitled Query %s', queryCount),
dbId:
activeQueryEditor && activeQueryEditor.dbId
? activeQueryEditor.dbId
- : this.props.defaultDbId,
+ : (this.props.defaultDbId || firstDbId),
schema: activeQueryEditor ? activeQueryEditor.schema : null,
autorun: false,
- sql: `${t(
- '-- Note: Unless you save your query, these tabs will NOT persist if you clear your cookies or change browsers.',
- )}\n\nSELECT ...`,
+ sql: `${warning}SELECT ...`,
queryLimit: this.props.defaultQueryLimit,
};
this.props.actions.addQueryEditor(qe);
@@ -173,7 +203,11 @@ class TabbedSqlEditors extends React.PureComponent {
if (key === 'add_tab') {
this.newQueryEditor();
} else {
- this.props.actions.setActiveQueryEditor({ id: key });
+ const qeid = this.props.tabHistory[this.props.tabHistory.length - 1];
+ if (key !== qeid) {
+ const queryEditor = this.props.queryEditors.find(qe => qe.id === key);
+ this.props.actions.switchQueryEditor(queryEditor, this.props.displayLimit);
+ }
}
}
removeQueryEditor(qe) {
@@ -191,7 +225,7 @@ class TabbedSqlEditors extends React.PureComponent {
}
render() {
const editors = this.props.queryEditors.map((qe, i) => {
- const isSelected = qe.id === this.activeQueryEditor().id;
+ const isSelected = this.activeQueryEditor() && this.activeQueryEditor().id === qe.id;
let latestQuery;
if (qe.latestQueryId) {
diff --git a/superset/assets/src/SqlLab/components/TemplateParamsEditor.jsx b/superset/assets/src/SqlLab/components/TemplateParamsEditor.jsx
index 1ee3e4f3b..c945b060b 100644
--- a/superset/assets/src/SqlLab/components/TemplateParamsEditor.jsx
+++ b/superset/assets/src/SqlLab/components/TemplateParamsEditor.jsx
@@ -70,10 +70,9 @@ export default class TemplateParamsEditor extends React.Component {
isValid = false;
}
this.setState({ parsedJSON, isValid, codeText });
- if (isValid) {
- this.props.onChange(codeText);
- } else {
- this.props.onChange('{}');
+ const newValue = isValid ? codeText : '{}';
+ if (newValue !== this.props.code) {
+ this.props.onChange(newValue);
}
}
renderDoc() {
diff --git a/superset/assets/src/SqlLab/reducers/getInitialState.js b/superset/assets/src/SqlLab/reducers/getInitialState.js
index fb3212f97..220d59813 100644
--- a/superset/assets/src/SqlLab/reducers/getInitialState.js
+++ b/superset/assets/src/SqlLab/reducers/getInitialState.js
@@ -16,18 +16,28 @@
* specific language governing permissions and limitations
* under the License.
*/
-import shortid from 'shortid';
import { t } from '@superset-ui/translation';
import getToastsFromPyFlashMessages from '../../messageToasts/utils/getToastsFromPyFlashMessages';
export default function getInitialState({ defaultDbId, ...restBootstrapData }) {
+ /*
+ * Before YYYY-MM-DD, the state for SQL Lab was stored exclusively in the
+ * browser's localStorage. The feature flag `SQLLAB_BACKEND_PERSISTENCE`
+ * moves the state to the backend instead, migrating it from local storage.
+ *
+ * To allow for a transparent migration, the initial state is a combination
+ * of the backend state (if any) with the browser state (if any).
+ */
+ const queryEditors = [];
const defaultQueryEditor = {
- id: shortid.generate(),
+ id: null,
+ loaded: true,
title: t('Untitled Query'),
sql: 'SELECT *\nFROM\nWHERE',
selectedText: null,
latestQueryId: null,
autorun: false,
+ templateParams: null,
dbId: defaultDbId,
queryLimit: restBootstrapData.common.conf.DEFAULT_SQLLAB_LIMIT,
validationResult: {
@@ -42,16 +52,114 @@ export default function getInitialState({ defaultDbId, ...restBootstrapData }) {
},
};
+ /* Load state from the backend. This will be empty if the feature flag
+ * `SQLLAB_BACKEND_PERSISTENCE` is off.
+ */
+ const activeTab = restBootstrapData.active_tab;
+ restBootstrapData.tab_state_ids.forEach(({ id, label }) => {
+ let queryEditor;
+ if (activeTab && activeTab.id === id) {
+ queryEditor = {
+ id: id.toString(),
+ loaded: true,
+ title: activeTab.label,
+ sql: activeTab.sql,
+ selectedText: null,
+ latestQueryId: activeTab.latest_query ? activeTab.latest_query.id : null,
+ autorun: activeTab.autorun,
+ templateParams: activeTab.template_params,
+ dbId: activeTab.database_id,
+ schema: activeTab.schema,
+ queryLimit: activeTab.query_limit,
+ validationResult: {
+ id: null,
+ errors: [],
+ completed: false,
+ },
+ };
+ } else {
+ // dummy state, actual state will be loaded on tab switch
+ queryEditor = {
+ ...defaultQueryEditor,
+ id: id.toString(),
+ loaded: false,
+ title: label,
+ };
+ }
+ queryEditors.push(queryEditor);
+ });
+
+ const tabHistory = activeTab ? [activeTab.id.toString()] : [];
+
+ const tables = [];
+ if (activeTab) {
+ activeTab.table_schemas.forEach((tableSchema) => {
+ const {
+ columns,
+ selectStar,
+ primaryKey,
+ foreignKeys,
+ indexes,
+ dataPreviewQueryId,
+ } = tableSchema.description;
+ const table = {
+ dbId: tableSchema.database_id,
+ queryEditorId: tableSchema.tab_state_id.toString(),
+ schema: tableSchema.schema,
+ name: tableSchema.table,
+ expanded: tableSchema.expanded,
+ id: tableSchema.id,
+ isMetadataLoading: false,
+ isExtraMetadataLoading: false,
+ dataPreviewQueryId,
+ columns,
+ selectStar,
+ primaryKey,
+ foreignKeys,
+ indexes,
+ };
+ tables.push(table);
+ });
+ }
+
+ const { databases, queries } = restBootstrapData;
+
+ /* If the `SQLLAB_BACKEND_PERSISTENCE` feature flag is off, or if the user
+ * hasn't used SQL Lab after it has been turned on, the state will be stored
+ * in the browser's local storage.
+ */
+ if (localStorage.getItem('redux') && JSON.parse(localStorage.getItem('redux')).sqlLab) {
+ const sqlLab = JSON.parse(localStorage.getItem('redux')).sqlLab;
+
+ if (sqlLab.queryEditors.length === 0) {
+ // migration was successful
+ localStorage.removeItem('redux');
+ } else {
+ // add query editors and tables to state with a special flag so they can
+ // be migrated if the `SQLLAB_BACKEND_PERSISTENCE` feature flag is on
+ sqlLab.queryEditors.forEach(qe => queryEditors.push({
+ ...qe,
+ inLocalStorage: true,
+ loaded: true,
+ }));
+ sqlLab.tables.forEach(table => tables.push({ ...table, inLocalStorage: true }));
+ Object.values(sqlLab.queries).forEach((query) => {
+ queries[query.id] = { ...query, inLocalStorage: true };
+ });
+ tabHistory.push(...sqlLab.tabHistory);
+ }
+ }
+
return {
sqlLab: {
activeSouthPaneTab: 'Results',
alerts: [],
- databases: {},
+ databases,
offline: false,
- queries: {},
- queryEditors: [defaultQueryEditor],
- tabHistory: [defaultQueryEditor.id],
- tables: [],
+ queries,
+ queryEditors,
+ tabHistory,
+ tables,
queriesLastUpdate: Date.now(),
},
messageToasts: getToastsFromPyFlashMessages(
diff --git a/superset/assets/src/SqlLab/reducers/sqlLab.js b/superset/assets/src/SqlLab/reducers/sqlLab.js
index 3424bda42..03547b36f 100644
--- a/superset/assets/src/SqlLab/reducers/sqlLab.js
+++ b/superset/assets/src/SqlLab/reducers/sqlLab.js
@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
-import shortid from 'shortid';
import { t } from '@superset-ui/translation';
import getInitialState from './getInitialState';
@@ -29,6 +28,7 @@ import {
removeFromArr,
getFromArr,
addToArr,
+ extendArr,
} from '../../reduxUtils';
export default function sqlLabReducer(state = {}, action) {
@@ -59,7 +59,6 @@ export default function sqlLabReducer(state = {}, action) {
);
const qe = {
remoteId: progenitor.remoteId,
- id: shortid.generate(),
title: t('Copy of %s', progenitor.title),
dbId: action.query.dbId ? action.query.dbId : null,
schema: action.query.schema ? action.query.schema : null,
@@ -68,13 +67,13 @@ export default function sqlLabReducer(state = {}, action) {
queryLimit: action.query.queryLimit,
maxRow: action.query.maxRow,
};
-
return sqlLabReducer(state, actions.addQueryEditor(qe));
},
[actions.REMOVE_QUERY_EDITOR]() {
let newState = removeFromArr(state, 'queryEditors', action.queryEditor);
// List of remaining queryEditor ids
const qeIds = newState.queryEditors.map(qe => qe.id);
+
const queries = {};
Object.keys(state.queries).forEach((k) => {
const query = state.queries[k];
@@ -82,9 +81,14 @@ export default function sqlLabReducer(state = {}, action) {
queries[k] = query;
}
});
+
let tabHistory = state.tabHistory.slice();
tabHistory = tabHistory.filter(id => qeIds.indexOf(id) > -1);
- newState = Object.assign({}, newState, { tabHistory, queries });
+
+ // Remove associated table schemas
+ const tables = state.tables.filter(table => table.queryEditorId !== action.queryEditor.id);
+
+ newState = Object.assign({}, newState, { tabHistory, tables, queries });
return newState;
},
[actions.REMOVE_QUERY]() {
@@ -114,7 +118,6 @@ export default function sqlLabReducer(state = {}, action) {
}
return alterInArr(state, 'tables', existingTable, at);
}
- at.id = shortid.generate();
// for new table, associate Id of query for data preview
at.dataPreviewQueryId = null;
let newState = addToArr(state, 'tables', at);
@@ -318,16 +321,77 @@ export default function sqlLabReducer(state = {}, action) {
},
[actions.SET_ACTIVE_QUERY_EDITOR]() {
const qeIds = state.queryEditors.map(qe => qe.id);
- if (qeIds.indexOf(action.queryEditor.id) > -1) {
+ if (
+ (qeIds.indexOf(action.queryEditor.id) > -1) &&
+ (state.tabHistory[state.tabHistory.length - 1] !== action.queryEditor.id)
+ ) {
const tabHistory = state.tabHistory.slice();
tabHistory.push(action.queryEditor.id);
return Object.assign({}, state, { tabHistory });
}
return state;
},
+ [actions.LOAD_QUERY_EDITOR]() {
+ return alterInArr(state, 'queryEditors', action.queryEditor, { ...action.queryEditor });
+ },
+ [actions.SET_TABLES]() {
+ return extendArr(state, 'tables', action.tables);
+ },
[actions.SET_ACTIVE_SOUTHPANE_TAB]() {
return Object.assign({}, state, { activeSouthPaneTab: action.tabId });
},
+ [actions.MIGRATE_QUERY_EDITOR]() {
+ // remove migrated query editor from localStorage
+ const sqlLab = JSON.parse(localStorage.getItem('redux')).sqlLab;
+ sqlLab.queryEditors = sqlLab.queryEditors.filter(qe => qe.id !== action.oldQueryEditor.id);
+ localStorage.setItem('redux', JSON.stringify({ sqlLab }));
+
+ // replace localStorage query editor with the server backed one
+ return addToArr(
+ removeFromArr(
+ state,
+ 'queryEditors',
+ action.oldQueryEditor,
+ ),
+ 'queryEditors',
+ action.newQueryEditor,
+ );
+ },
+ [actions.MIGRATE_TABLE]() {
+ // remove migrated table from localStorage
+ const sqlLab = JSON.parse(localStorage.getItem('redux')).sqlLab;
+ sqlLab.tables = sqlLab.tables.filter(table => table.id !== action.oldTable.id);
+ localStorage.setItem('redux', JSON.stringify({ sqlLab }));
+
+ // replace localStorage table with the server backed one
+ return addToArr(
+ removeFromArr(
+ state,
+ 'tables',
+ action.oldTable,
+ ),
+ 'tables',
+ action.newTable,
+ );
+ },
+ [actions.MIGRATE_TAB_HISTORY]() {
+ // remove migrated tab from localStorage tabHistory
+ const sqlLab = JSON.parse(localStorage.getItem('redux')).sqlLab;
+ sqlLab.tabHistory = sqlLab.tabHistory.filter(tabId => tabId !== action.oldId);
+ localStorage.setItem('redux', JSON.stringify({ sqlLab }));
+ const tabHistory = state.tabHistory.filter(tabId => tabId !== action.oldId);
+ tabHistory.push(action.newId);
+ return Object.assign({}, state, { tabHistory });
+ },
+ [actions.MIGRATE_QUERY]() {
+ const query = {
+ ...state.queries[action.queryId],
+ // point query to migrated query editor
+ sqlEditorId: action.queryEditorId,
+ };
+ const queries = Object.assign({}, state.queries, { [query.id]: query });
+ return Object.assign({}, state, { queries });
+ },
[actions.QUERY_EDITOR_SETDB]() {
return alterInArr(state, 'queryEditors', action.queryEditor, { dbId: action.dbId });
},
diff --git a/superset/assets/src/components/TableSelector.jsx b/superset/assets/src/components/TableSelector.jsx
index 1b83acc32..f2111b2fd 100644
--- a/superset/assets/src/components/TableSelector.jsx
+++ b/superset/assets/src/components/TableSelector.jsx
@@ -127,9 +127,8 @@ export default class TableSelector extends React.PureComponent {
}));
}
fetchTables(force, substr) {
- // This can be large so it shouldn't be put in the Redux store
const forceRefresh = force || false;
- const { dbId, schema } = this.props;
+ const { dbId, schema } = this.state;
if (dbId && schema) {
this.setState(() => ({ tableLoading: true, tableOptions: [] }));
const endpoint = encodeURI(`/superset/tables/${dbId}/` +
diff --git a/superset/assets/src/featureFlags.ts b/superset/assets/src/featureFlags.ts
index 01b5ac2c4..07e4da52c 100644
--- a/superset/assets/src/featureFlags.ts
+++ b/superset/assets/src/featureFlags.ts
@@ -25,6 +25,7 @@ export enum FeatureFlag {
SCHEDULED_QUERIES = 'SCHEDULED_QUERIES',
SQL_VALIDATORS_BY_ENGINE = 'SQL_VALIDATORS_BY_ENGINE',
ESTIMATE_QUERY_COST = 'ESTIMATE_QUERY_COST',
+ SQLLAB_BACKEND_PERSISTENCE = 'SQLLAB_BACKEND_PERSISTENCE',
}
export type FeatureFlagMap = {
diff --git a/superset/assets/src/reduxUtils.js b/superset/assets/src/reduxUtils.js
index 96fe1cec9..49ed4eca9 100644
--- a/superset/assets/src/reduxUtils.js
+++ b/superset/assets/src/reduxUtils.js
@@ -86,6 +86,23 @@ export function addToArr(state, arrKey, obj, prepend = false) {
return Object.assign({}, state, newState);
}
+export function extendArr(state, arrKey, obj, prepend = false) {
+ const newObj = [...obj];
+ newObj.forEach((el) => {
+ if (!el.id) {
+ /* eslint-disable no-param-reassign */
+ el.id = shortid.generate();
+ }
+ });
+ const newState = {};
+ if (prepend) {
+ newState[arrKey] = [...newObj, ...state[arrKey]];
+ } else {
+ newState[arrKey] = [...state[arrKey], ...newObj];
+ }
+ return Object.assign({}, state, newState);
+}
+
export function initEnhancer(persist = true, persistConfig = {}) {
const { paths, config } = persistConfig;
const composeEnhancers = process.env.WEBPACK_MODE === 'development'
diff --git a/superset/config.py b/superset/config.py
index c5728f87a..132258ef2 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -486,7 +486,7 @@ RESULTS_BACKEND = None
# rather than JSON. This feature requires additional testing from the
# community before it is fully adopted, so this config option is provided
# in order to disable should breaking issues be discovered.
-RESULTS_BACKEND_USE_MSGPACK = True
+RESULTS_BACKEND_USE_MSGPACK = False
# The S3 bucket where you want to store your external hive tables created
# from CSV files. For example, 'companyname-superset'
diff --git a/superset/migrations/versions/db4b49eb0782_add_tables_for_sql_lab_state.py b/superset/migrations/versions/db4b49eb0782_add_tables_for_sql_lab_state.py
new file mode 100644
index 000000000..53afac7d6
--- /dev/null
+++ b/superset/migrations/versions/db4b49eb0782_add_tables_for_sql_lab_state.py
@@ -0,0 +1,94 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+"""Add tables for SQL Lab state
+
+Revision ID: db4b49eb0782
+Revises: 78ee127d0d1d
+Create Date: 2019-11-13 11:05:30.122167
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = "db4b49eb0782"
+down_revision = "78ee127d0d1d"
+
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy.dialects import mysql
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table(
+ "tab_state",
+ sa.Column("created_on", sa.DateTime(), nullable=True),
+ sa.Column("changed_on", sa.DateTime(), nullable=True),
+ sa.Column("extra_json", sa.Text(), nullable=True),
+ sa.Column("id", sa.Integer(), nullable=False, autoincrement=True),
+ sa.Column("user_id", sa.Integer(), nullable=True),
+ sa.Column("label", sa.String(length=256), nullable=True),
+ sa.Column("active", sa.Boolean(), nullable=True),
+ sa.Column("database_id", sa.Integer(), nullable=True),
+ sa.Column("schema", sa.String(length=256), nullable=True),
+ sa.Column("sql", sa.Text(), nullable=True),
+ sa.Column("query_limit", sa.Integer(), nullable=True),
+ sa.Column("latest_query_id", sa.String(11), nullable=True),
+ sa.Column("autorun", sa.Boolean(), nullable=False, default=False),
+ sa.Column("template_params", sa.Text(), nullable=True),
+ sa.Column("created_by_fk", sa.Integer(), nullable=True),
+ sa.Column("changed_by_fk", sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(["changed_by_fk"], ["ab_user.id"]),
+ sa.ForeignKeyConstraint(["created_by_fk"], ["ab_user.id"]),
+ sa.ForeignKeyConstraint(["database_id"], ["dbs.id"]),
+ sa.ForeignKeyConstraint(["latest_query_id"], ["query.client_id"]),
+ sa.ForeignKeyConstraint(["user_id"], ["ab_user.id"]),
+ sa.PrimaryKeyConstraint("id"),
+ sqlite_autoincrement=True,
+ )
+ op.create_index(op.f("ix_tab_state_id"), "tab_state", ["id"], unique=True)
+ op.create_table(
+ "table_schema",
+ sa.Column("created_on", sa.DateTime(), nullable=True),
+ sa.Column("changed_on", sa.DateTime(), nullable=True),
+ sa.Column("extra_json", sa.Text(), nullable=True),
+ sa.Column("id", sa.Integer(), nullable=False, autoincrement=True),
+ sa.Column("tab_state_id", sa.Integer(), nullable=True),
+ sa.Column("database_id", sa.Integer(), nullable=False),
+ sa.Column("schema", sa.String(length=256), nullable=True),
+ sa.Column("table", sa.String(length=256), nullable=True),
+ sa.Column("description", sa.Text(), nullable=True),
+ sa.Column("expanded", sa.Boolean(), nullable=True),
+ sa.Column("created_by_fk", sa.Integer(), nullable=True),
+ sa.Column("changed_by_fk", sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(["changed_by_fk"], ["ab_user.id"]),
+ sa.ForeignKeyConstraint(["created_by_fk"], ["ab_user.id"]),
+ sa.ForeignKeyConstraint(["database_id"], ["dbs.id"]),
+ sa.ForeignKeyConstraint(["tab_state_id"], ["tab_state.id"], ondelete="CASCADE"),
+ sa.PrimaryKeyConstraint("id"),
+ sqlite_autoincrement=True,
+ )
+ op.create_index(op.f("ix_table_schema_id"), "table_schema", ["id"], unique=True)
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index(op.f("ix_table_schema_id"), table_name="table_schema")
+ op.drop_table("table_schema")
+ op.drop_index(op.f("ix_tab_state_id"), table_name="tab_state")
+ op.drop_table("tab_state")
+ # ### end Alembic commands ###
diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py
index a1afaca4f..3b9789067 100644
--- a/superset/models/sql_lab.py
+++ b/superset/models/sql_lab.py
@@ -19,6 +19,7 @@
import re
from datetime import datetime
+import simplejson as json
import sqlalchemy as sqla
from flask import Markup
from flask_appbuilder import Model
@@ -188,6 +189,87 @@ class SavedQuery(Model, AuditMixinNullable, ExtraJSONMixin):
return "/superset/sqllab?savedQueryId={0}".format(self.id)
+class TabState(Model, AuditMixinNullable, ExtraJSONMixin):
+
+ __tablename__ = "tab_state"
+
+ # basic info
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ user_id = Column(Integer, ForeignKey("ab_user.id"))
+ label = Column(String(256))
+ active = Column(Boolean, default=False)
+
+ # selected DB and schema
+ database_id = Column(Integer, ForeignKey("dbs.id"))
+ database = relationship("Database", foreign_keys=[database_id])
+ schema = Column(String(256))
+
+ # tables that are open in the schema browser and their data previews
+ table_schemas = relationship(
+ "TableSchema",
+ cascade="all, delete-orphan",
+ backref="tab_state",
+ passive_deletes=True,
+ )
+
+ # the query in the textarea, and results (if any)
+ sql = Column(Text)
+ query_limit = Column(Integer)
+
+ # latest query that was run
+ latest_query_id = Column(Integer, ForeignKey("query.client_id"))
+ latest_query = relationship("Query")
+
+ # other properties
+ autorun = Column(Boolean, default=False)
+ template_params = Column(Text)
+
+ def to_dict(self):
+ return {
+ "id": self.id,
+ "user_id": self.user_id,
+ "label": self.label,
+ "active": self.active,
+ "database_id": self.database_id,
+ "schema": self.schema,
+ "table_schemas": [ts.to_dict() for ts in self.table_schemas],
+ "sql": self.sql,
+ "query_limit": self.query_limit,
+ "latest_query": self.latest_query.to_dict() if self.latest_query else None,
+ "autorun": self.autorun,
+ "template_params": self.template_params,
+ }
+
+
+class TableSchema(Model, AuditMixinNullable, ExtraJSONMixin):
+
+ __tablename__ = "table_schema"
+
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ tab_state_id = Column(Integer, ForeignKey("tab_state.id", ondelete="CASCADE"))
+
+ database_id = Column(Integer, ForeignKey("dbs.id"), nullable=False)
+ database = relationship("Database", foreign_keys=[database_id])
+ schema = Column(String(256))
+ table = Column(String(256))
+
+ # JSON describing the schema, partitions, latest partition, etc.
+ description = Column(Text)
+
+ expanded = Column(Boolean, default=False)
+
+ def to_dict(self):
+ return {
+ "id": self.id,
+ "tab_state_id": self.tab_state_id,
+ "database_id": self.database_id,
+ "schema": self.schema,
+ "table": self.table,
+ "description": json.loads(self.description),
+ "expanded": self.expanded,
+ }
+
+
# events for updating tags
sqla.event.listen(SavedQuery, "after_insert", QueryUpdater.after_insert)
sqla.event.listen(SavedQuery, "after_update", QueryUpdater.after_update)
diff --git a/superset/sql_lab.py b/superset/sql_lab.py
index f64a4307d..df4ecc53f 100644
--- a/superset/sql_lab.py
+++ b/superset/sql_lab.py
@@ -318,7 +318,7 @@ def execute_sql_statements(
db_engine_spec = database.db_engine_spec
db_engine_spec.patch()
- if store_results and not results_backend:
+ if database.allow_run_async and not results_backend:
raise SqlLabException("Results backend isn't configured.")
# Breaking down into multiple statements
@@ -394,7 +394,7 @@ def execute_sql_statements(
)
payload["query"]["state"] = QueryStatus.SUCCESS
- if store_results:
+ if store_results and results_backend:
key = str(uuid.uuid4())
logging.info(
f"Query {query_id}: Storing results in results backend, key: {key}"
diff --git a/superset/views/core.py b/superset/views/core.py
index 3db014bbd..52550695e 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -77,7 +77,7 @@ from superset.exceptions import (
SupersetTimeoutException,
)
from superset.jinja_context import get_template_processor
-from superset.models.sql_lab import Query
+from superset.models.sql_lab import Query, TabState
from superset.models.user_attributes import UserAttribute
from superset.sql_parse import ParsedQuery
from superset.sql_validators import get_validator_by_name
@@ -117,6 +117,20 @@ stats_logger = config["STATS_LOGGER"]
DAR = models.DatasourceAccessRequest
QueryStatus = utils.QueryStatus
+DATABASE_KEYS = [
+ "allow_csv_upload",
+ "allow_ctas",
+ "allow_dml",
+ "allow_multi_schema_metadata_fetch",
+ "allow_run_async",
+ "allows_subquery",
+ "backend",
+ "database_name",
+ "expose_in_sqllab",
+ "force_ctas_schema",
+ "id",
+]
+
ALL_DATASOURCE_ACCESS_ERR = __(
"This endpoint requires the `all_datasource_access` permission"
@@ -2644,12 +2658,17 @@ class Superset(BaseSupersetView):
try:
timeout = config["SQLLAB_TIMEOUT"]
timeout_msg = f"The query exceeded the {timeout} seconds timeout."
+ store_results = (
+ is_feature_enabled("SQLLAB_BACKEND_PERSISTENCE")
+ and not query.select_as_cta
+ )
with utils.timeout(seconds=timeout, error_message=timeout_msg):
# pylint: disable=no-value-for-parameter
data = sql_lab.get_sql_results(
query.id,
rendered_query,
return_results=True,
+ store_results=store_results,
user_name=g.user.username if g.user else None,
expand_data=expand_data,
)
@@ -2997,9 +3016,38 @@ class Superset(BaseSupersetView):
@expose("/sqllab")
def sqllab(self):
"""SQL Editor"""
+
+ # send list of tab state ids
+ tab_state_ids = (
+ db.session.query(TabState.id, TabState.label)
+ .filter_by(user_id=g.user.get_id())
+ .all()
+ )
+ # return first active tab, or fallback to another one if no tab is active
+ active_tab = (
+ db.session.query(TabState)
+ .filter_by(user_id=g.user.get_id())
+ .order_by(TabState.active.desc())
+ .first()
+ )
+ databases = {
+ database.id: {
+ k: v for k, v in database.to_json().items() if k in DATABASE_KEYS
+ }
+ for database in db.session.query(models.Database).all()
+ }
+ user_queries = db.session.query(Query).filter_by(user_id=g.user.get_id()).all()
+ queries = {
+ query.client_id: {k: v for k, v in query.to_dict().items()}
+ for query in user_queries
+ }
d = {
"defaultDbId": config["SQLLAB_DEFAULT_DBID"],
"common": self.common_bootstrap_payload(),
+ "tab_state_ids": tab_state_ids,
+ "active_tab": active_tab.to_dict() if active_tab else None,
+ "databases": databases,
+ "queries": queries,
}
return self.render_template(
"superset/basic.html",
diff --git a/superset/views/sql_lab.py b/superset/views/sql_lab.py
index cdb51cf64..fe072956f 100644
--- a/superset/views/sql_lab.py
+++ b/superset/views/sql_lab.py
@@ -18,18 +18,24 @@
from typing import Callable
import simplejson as json
-from flask import g, redirect
+from flask import g, redirect, request, Response
from flask_appbuilder import expose
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.security.decorators import has_access, has_access_api
from flask_babel import gettext as __, lazy_gettext as _
from flask_sqlalchemy import BaseQuery
-from superset import appbuilder, get_feature_flags, security_manager
-from superset.models.sql_lab import Query, SavedQuery
+from superset import appbuilder, db, get_feature_flags, security_manager
+from superset.models.sql_lab import Query, SavedQuery, TableSchema, TabState
from superset.utils import core as utils
-from .base import BaseSupersetView, DeleteMixin, SupersetFilter, SupersetModelView
+from .base import (
+ BaseSupersetView,
+ DeleteMixin,
+ json_success,
+ SupersetFilter,
+ SupersetModelView,
+)
class QueryFilter(SupersetFilter):
@@ -169,6 +175,165 @@ class SavedQueryViewApi(SavedQueryView):
appbuilder.add_view_no_menu(SavedQueryViewApi)
appbuilder.add_view_no_menu(SavedQueryView)
+
+class TabStateView(BaseSupersetView):
+ def _get_owner_id(self, tab_state_id):
+ return db.session.query(TabState.user_id).filter_by(id=tab_state_id).scalar()
+
+ @has_access_api
+ @expose("/", methods=["POST"])
+ def post(self):
+ query_editor = json.loads(request.form["queryEditor"])
+ tab_state = TabState(
+ user_id=g.user.get_id(),
+ label=query_editor.get("title", "Untitled Query"),
+ active=True,
+ database_id=query_editor["dbId"],
+ schema=query_editor.get("schema"),
+ sql=query_editor.get("sql", "SELECT ..."),
+ query_limit=query_editor.get("queryLimit"),
+ )
+ (
+ db.session.query(TabState)
+ .filter_by(user_id=g.user.get_id())
+ .update({"active": False})
+ )
+ db.session.add(tab_state)
+ db.session.commit()
+ return json_success(json.dumps({"id": tab_state.id}))
+
+ @has_access_api
+ @expose("/", methods=["DELETE"])
+ def delete(self, tab_state_id):
+ if self._get_owner_id(tab_state_id) != int(g.user.get_id()):
+ return Response(status=403)
+
+ db.session.query(TabState).filter(TabState.id == tab_state_id).delete(
+ synchronize_session=False
+ )
+ db.session.query(TableSchema).filter(
+ TableSchema.tab_state_id == tab_state_id
+ ).delete(synchronize_session=False)
+ db.session.commit()
+ return json_success(json.dumps("OK"))
+
+ @has_access_api
+ @expose("/", methods=["GET"])
+ def get(self, tab_state_id):
+ if self._get_owner_id(tab_state_id) != int(g.user.get_id()):
+ return Response(status=403)
+
+ tab_state = db.session.query(TabState).filter_by(id=tab_state_id).first()
+ if tab_state is None:
+ return Response(status=404)
+ return json_success(
+ json.dumps(tab_state.to_dict(), default=utils.json_iso_dttm_ser)
+ )
+
+ @has_access_api
+ @expose("/activate", methods=["POST"])
+ def activate(self, tab_state_id):
+ owner_id = self._get_owner_id(tab_state_id)
+ if owner_id is None:
+ return Response(status=404)
+ if owner_id != int(g.user.get_id()):
+ return Response(status=403)
+
+ (
+ db.session.query(TabState)
+ .filter_by(user_id=g.user.get_id())
+ .update({"active": TabState.id == tab_state_id})
+ )
+ db.session.commit()
+ return json_success(json.dumps(tab_state_id))
+
+ @has_access_api
+ @expose("", methods=["PUT"])
+ def put(self, tab_state_id):
+ if self._get_owner_id(tab_state_id) != int(g.user.get_id()):
+ return Response(status=403)
+
+ fields = {k: json.loads(v) for k, v in request.form.to_dict().items()}
+ db.session.query(TabState).filter_by(id=tab_state_id).update(fields)
+ db.session.commit()
+ return json_success(json.dumps(tab_state_id))
+
+ @has_access_api
+ @expose("/migrate_query", methods=["POST"])
+ def migrate_query(self, tab_state_id):
+ if self._get_owner_id(tab_state_id) != int(g.user.get_id()):
+ return Response(status=403)
+
+ client_id = json.loads(request.form["queryId"])
+ db.session.query(Query).filter_by(client_id=client_id).update(
+ {"sql_editor_id": tab_state_id}
+ )
+ db.session.commit()
+ return json_success(json.dumps(tab_state_id))
+
+ @has_access_api
+ @expose("/query/", methods=["DELETE"])
+ def delete_query(self, tab_state_id, client_id):
+ db.session.query(Query).filter_by(
+ client_id=client_id, user_id=g.user.get_id(), sql_editor_id=tab_state_id
+ ).delete(synchronize_session=False)
+ db.session.commit()
+ return json_success(json.dumps("OK"))
+
+
+class TableSchemaView(BaseSupersetView):
+ @has_access_api
+ @expose("/", methods=["POST"])
+ def post(self):
+ table = json.loads(request.form["table"])
+
+ # delete any existing table schema
+ db.session.query(TableSchema).filter(
+ TableSchema.tab_state_id == table["queryEditorId"],
+ TableSchema.database_id == table["dbId"],
+ TableSchema.schema == table["schema"],
+ TableSchema.table == table["name"],
+ ).delete(synchronize_session=False)
+
+ table_schema = TableSchema(
+ tab_state_id=table["queryEditorId"],
+ database_id=table["dbId"],
+ schema=table["schema"],
+ table=table["name"],
+ description=json.dumps(table),
+ expanded=True,
+ )
+ db.session.add(table_schema)
+ db.session.commit()
+ return json_success(json.dumps({"id": table_schema.id}))
+
+ @has_access_api
+ @expose("/", methods=["DELETE"])
+ def delete(self, table_schema_id):
+ db.session.query(TableSchema).filter(TableSchema.id == table_schema_id).delete(
+ synchronize_session=False
+ )
+ db.session.commit()
+ return json_success(json.dumps("OK"))
+
+ @has_access_api
+ @expose("//expanded", methods=["POST"])
+ def expanded(self, table_schema_id):
+ payload = json.loads(request.form["expanded"])
+ (
+ db.session.query(TableSchema)
+ .filter_by(id=table_schema_id)
+ .update({"expanded": payload})
+ )
+ db.session.commit()
+ response = json.dumps({"id": table_schema_id, "expanded": payload})
+ return json_success(response)
+
+
+appbuilder.add_view_no_menu(TabStateView)
+appbuilder.add_view_no_menu(TableSchemaView)
+
+
appbuilder.add_link(
__("Saved Queries"), href="/sqllab/my_queries/", icon="fa-save", category="SQL Lab"
)
diff --git a/tests/superset_test_config_sqllab_backend_persist.py b/tests/superset_test_config_sqllab_backend_persist.py
new file mode 100644
index 000000000..ace73b85b
--- /dev/null
+++ b/tests/superset_test_config_sqllab_backend_persist.py
@@ -0,0 +1,63 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+# flake8: noqa
+import os
+from copy import copy
+
+from superset.config import * # type: ignore
+
+AUTH_USER_REGISTRATION_ROLE = "alpha"
+SQLALCHEMY_DATABASE_URI = "sqlite:///" + os.path.join(DATA_DIR, "unittests.db")
+DEBUG = True
+SUPERSET_WEBSERVER_PORT = 8081
+
+# Allowing SQLALCHEMY_DATABASE_URI to be defined as an env var for
+# continuous integration
+if "SUPERSET__SQLALCHEMY_DATABASE_URI" in os.environ:
+ SQLALCHEMY_DATABASE_URI = os.environ["SUPERSET__SQLALCHEMY_DATABASE_URI"]
+
+SQL_SELECT_AS_CTA = True
+SQL_MAX_ROW = 666
+FEATURE_FLAGS = {"foo": "bar"}
+
+
+def GET_FEATURE_FLAGS_FUNC(ff):
+ ff_copy = copy(ff)
+ ff_copy["super"] = "set"
+ return ff_copy
+
+
+TESTING = True
+SECRET_KEY = "thisismyscretkey"
+WTF_CSRF_ENABLED = False
+PUBLIC_ROLE_LIKE_GAMMA = True
+AUTH_ROLE_PUBLIC = "Public"
+EMAIL_NOTIFICATIONS = False
+
+CACHE_CONFIG = {"CACHE_TYPE": "simple"}
+
+
+class CeleryConfig(object):
+ BROKER_URL = "redis://localhost"
+ CELERY_IMPORTS = ("superset.sql_lab",)
+ CELERY_ANNOTATIONS = {"sql_lab.add": {"rate_limit": "10/s"}}
+ CONCURRENCY = 1
+
+
+CELERY_CONFIG = CeleryConfig
+
+DEFAULT_FEATURE_FLAGS = {"SQLLAB_BACKEND_PERSISTENCE": True}
diff --git a/tox.ini b/tox.ini
index 8a8096df6..6873d2913 100644
--- a/tox.ini
+++ b/tox.ini
@@ -78,6 +78,19 @@ setenv =
SUPERSET_CONFIG = tests.superset_test_config
SUPERSET_HOME = {envtmpdir}
+[testenv:cypress-sqllab-backend-persist]
+commands =
+ npm install -g npm@'>=6.5.0'
+ pip install -e {toxinidir}/
+ {toxinidir}/superset/assets/cypress_build.sh sqllab
+deps =
+ -rrequirements.txt
+ -rrequirements-dev.txt
+setenv =
+ PYTHONPATH = {toxinidir}
+ SUPERSET_CONFIG = tests.superset_test_config_sqllab_backend_persist
+ SUPERSET_HOME = {envtmpdir}
+
[testenv:eslint]
changedir = {toxinidir}/superset/assets
commands =
@@ -142,6 +155,7 @@ envlist =
cypress-dashboard
cypress-explore
cypress-sqllab
+ cypress-sqllab-backend-persist
eslint
isort
javascript