From ec838947b0969e2da086ce4cd0c8ea7ab26ab0e4 Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Tue, 17 Jan 2023 09:57:21 -0800 Subject: [PATCH] Add datavirtualization extension (#21594) * initial * cleanup * Add typings ref * fix compile * remove unused * add missing * another unused * Use newer vscodetestcover * newer dataprotocol * format * cleanup ignores * fix out path * fix entry point * more cleanup * Move into src folder * Handle service client log messages * remove unused --- build/filters.js | 1 + build/lib/extensions.js | 2 +- build/lib/extensions.ts | 1 + build/npm/dirs.js | 1 + extensions/datavirtualization/.gitignore | 20 + extensions/datavirtualization/.vscodeignore | 21 + extensions/datavirtualization/.yarnrc | 1 + extensions/datavirtualization/LICENSE.txt | 47 + extensions/datavirtualization/README.md | 34 + .../datavirtualization/ThirdPartyNotices.txt | 427 ++++ extensions/datavirtualization/config.json | 24 + .../datavirtualization/coverConfig.json | 23 + .../extension.webpack.config.js | 17 + extensions/datavirtualization/package.json | 119 ++ .../datavirtualization/package.nls.json | 10 + .../resources/dark/database_inverse.svg | 1 + .../resources/dark/delete_inverse.svg | 1 + .../resources/dark/file_inverse.svg | 1 + .../resources/dark/folder_inverse.svg | 1 + .../resources/dark/new_spark_job_inverse.svg | 1 + .../resources/dark/nottrusted_inverse.svg | 1 + .../resources/dark/open-file.svg | 4 + .../resources/dark/polybase_inverse.svg | 1 + .../resources/dark/refresh_inverse.svg | 1 + .../resources/dark/server_inverse.svg | 1 + .../resources/dark/sql_database_inverse.svg | 1 + .../resources/dark/sql_server_inverse.svg | 1 + .../resources/dark/start_inverse.svg | 1 + .../resources/dark/stop_cell_inverse.svg | 1 + .../resources/dark/stop_inverse.svg | 1 + .../resources/dark/trusted_inverse.svg | 1 + .../resources/extension.png | Bin 0 -> 2890 bytes .../resources/light/Folder.svg | 1 + .../resources/light/database.svg | 1 + .../resources/light/database_OE.svg | 1 + .../resources/light/delete.svg | 1 + .../resources/light/file.svg | 1 + .../resources/light/hadoop.svg | 1 + .../resources/light/new_spark_job.svg | 1 + .../resources/light/nottrusted.svg | 1 + .../resources/light/open-file.svg | 4 + .../resources/light/polybase.svg | 1 + .../resources/light/refresh.svg | 1 + .../resources/light/server.svg | 1 + .../resources/light/sql_database.svg | 1 + .../resources/light/sql_server.svg | 1 + .../resources/light/start.svg | 1 + .../resources/light/stop.svg | 1 + .../resources/light/stop_cell.svg | 1 + .../resources/light/table.svg | 1 + .../resources/light/trusted.svg | 1 + .../datavirtualization/src/apiWrapper.ts | 202 ++ .../datavirtualization/src/appContext.ts | 26 + .../src/cancelableStream.ts | 25 + extensions/datavirtualization/src/command.ts | 175 ++ .../datavirtualization/src/constants.ts | 92 + .../datavirtualization/src/extension.ts | 41 + .../datavirtualization/src/fileSources.ts | 23 + .../datavirtualization/src/hdfsCommands.ts | 28 + .../datavirtualization/src/hdfsProvider.ts | 133 ++ .../src/localizedConstants.ts | 22 + .../src/prompts/question.ts | 70 + .../src/services/contracts.ts | 332 +++ .../src/services/features.ts | 189 ++ .../src/services/serviceApiManager.ts | 75 + .../src/services/serviceClient.ts | 171 ++ .../src/services/serviceUtils.ts | 62 + .../src/services/telemetry.ts | 216 ++ .../datavirtualization/src/test/index.ts | 28 + .../src/test/mockFileSource.ts | 47 + .../datavirtualization/src/test/stubs.ts | 1263 ++++++++++++ .../src/test/tableFromFile.test.ts | 563 ++++++ .../datavirtualization/src/test/testUtils.ts | 19 + .../src/test/virtualizeData.test.ts | 375 ++++ .../datavirtualization/src/treeNodes.ts | 76 + extensions/datavirtualization/src/types.d.ts | 63 + .../src/typings/globals/istanbul/index.d.ts | 72 + .../src/typings/globals/istanbul/typings.json | 8 + .../typings/markdown-it-named-headers.d.ts | 5 + .../src/typings/mssqlapis.d.ts | 65 + .../datavirtualization/src/typings/ref.d.ts | 9 + extensions/datavirtualization/src/utils.ts | 232 +++ .../wizards/tableFromFile/api/importPage.ts | 38 + .../src/wizards/tableFromFile/api/models.ts | 40 + .../tableFromFile/pages/fileConfigPage.ts | 548 +++++ .../tableFromFile/pages/modifyColumnsPage.ts | 154 ++ .../tableFromFile/pages/prosePreviewPage.ts | 159 ++ .../tableFromFile/pages/summaryPage.ts | 79 + .../tableFromFile/tableFromFileWizard.ts | 329 +++ .../virtualizeData/connectionDetailsPage.ts | 312 +++ .../virtualizeData/createMasterKeyPage.ts | 123 ++ .../virtualizeData/objectMappingPage.ts | 1763 ++++++++++++++++ .../virtualizeData/selectDataSourcePage.ts | 225 +++ .../src/wizards/virtualizeData/summaryPage.ts | 200 ++ .../virtualizeDataInputManager.ts | 171 ++ .../virtualizeData/virtualizeDataModel.ts | 271 +++ .../virtualizeData/virtualizeDataTree.ts | 364 ++++ .../virtualizeData/virtualizeDataWizard.ts | 187 ++ .../src/wizards/wizardCommands.ts | 119 ++ .../src/wizards/wizardPageWrapper.ts | 23 + extensions/datavirtualization/tsconfig.json | 22 + extensions/datavirtualization/tslint.json | 14 + extensions/datavirtualization/yarn.lock | 1796 +++++++++++++++++ 103 files changed, 12432 insertions(+), 1 deletion(-) create mode 100644 extensions/datavirtualization/.gitignore create mode 100644 extensions/datavirtualization/.vscodeignore create mode 100644 extensions/datavirtualization/.yarnrc create mode 100644 extensions/datavirtualization/LICENSE.txt create mode 100644 extensions/datavirtualization/README.md create mode 100644 extensions/datavirtualization/ThirdPartyNotices.txt create mode 100644 extensions/datavirtualization/config.json create mode 100644 extensions/datavirtualization/coverConfig.json create mode 100644 extensions/datavirtualization/extension.webpack.config.js create mode 100644 extensions/datavirtualization/package.json create mode 100644 extensions/datavirtualization/package.nls.json create mode 100644 extensions/datavirtualization/resources/dark/database_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/delete_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/file_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/folder_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/new_spark_job_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/nottrusted_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/open-file.svg create mode 100644 extensions/datavirtualization/resources/dark/polybase_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/refresh_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/server_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/sql_database_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/sql_server_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/start_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/stop_cell_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/stop_inverse.svg create mode 100644 extensions/datavirtualization/resources/dark/trusted_inverse.svg create mode 100644 extensions/datavirtualization/resources/extension.png create mode 100644 extensions/datavirtualization/resources/light/Folder.svg create mode 100644 extensions/datavirtualization/resources/light/database.svg create mode 100644 extensions/datavirtualization/resources/light/database_OE.svg create mode 100644 extensions/datavirtualization/resources/light/delete.svg create mode 100644 extensions/datavirtualization/resources/light/file.svg create mode 100644 extensions/datavirtualization/resources/light/hadoop.svg create mode 100644 extensions/datavirtualization/resources/light/new_spark_job.svg create mode 100644 extensions/datavirtualization/resources/light/nottrusted.svg create mode 100644 extensions/datavirtualization/resources/light/open-file.svg create mode 100644 extensions/datavirtualization/resources/light/polybase.svg create mode 100644 extensions/datavirtualization/resources/light/refresh.svg create mode 100644 extensions/datavirtualization/resources/light/server.svg create mode 100644 extensions/datavirtualization/resources/light/sql_database.svg create mode 100644 extensions/datavirtualization/resources/light/sql_server.svg create mode 100644 extensions/datavirtualization/resources/light/start.svg create mode 100644 extensions/datavirtualization/resources/light/stop.svg create mode 100644 extensions/datavirtualization/resources/light/stop_cell.svg create mode 100644 extensions/datavirtualization/resources/light/table.svg create mode 100644 extensions/datavirtualization/resources/light/trusted.svg create mode 100644 extensions/datavirtualization/src/apiWrapper.ts create mode 100644 extensions/datavirtualization/src/appContext.ts create mode 100644 extensions/datavirtualization/src/cancelableStream.ts create mode 100644 extensions/datavirtualization/src/command.ts create mode 100644 extensions/datavirtualization/src/constants.ts create mode 100644 extensions/datavirtualization/src/extension.ts create mode 100644 extensions/datavirtualization/src/fileSources.ts create mode 100644 extensions/datavirtualization/src/hdfsCommands.ts create mode 100644 extensions/datavirtualization/src/hdfsProvider.ts create mode 100644 extensions/datavirtualization/src/localizedConstants.ts create mode 100644 extensions/datavirtualization/src/prompts/question.ts create mode 100644 extensions/datavirtualization/src/services/contracts.ts create mode 100644 extensions/datavirtualization/src/services/features.ts create mode 100644 extensions/datavirtualization/src/services/serviceApiManager.ts create mode 100644 extensions/datavirtualization/src/services/serviceClient.ts create mode 100644 extensions/datavirtualization/src/services/serviceUtils.ts create mode 100644 extensions/datavirtualization/src/services/telemetry.ts create mode 100644 extensions/datavirtualization/src/test/index.ts create mode 100644 extensions/datavirtualization/src/test/mockFileSource.ts create mode 100644 extensions/datavirtualization/src/test/stubs.ts create mode 100644 extensions/datavirtualization/src/test/tableFromFile.test.ts create mode 100644 extensions/datavirtualization/src/test/testUtils.ts create mode 100644 extensions/datavirtualization/src/test/virtualizeData.test.ts create mode 100644 extensions/datavirtualization/src/treeNodes.ts create mode 100644 extensions/datavirtualization/src/types.d.ts create mode 100644 extensions/datavirtualization/src/typings/globals/istanbul/index.d.ts create mode 100644 extensions/datavirtualization/src/typings/globals/istanbul/typings.json create mode 100644 extensions/datavirtualization/src/typings/markdown-it-named-headers.d.ts create mode 100644 extensions/datavirtualization/src/typings/mssqlapis.d.ts create mode 100644 extensions/datavirtualization/src/typings/ref.d.ts create mode 100644 extensions/datavirtualization/src/utils.ts create mode 100644 extensions/datavirtualization/src/wizards/tableFromFile/api/importPage.ts create mode 100644 extensions/datavirtualization/src/wizards/tableFromFile/api/models.ts create mode 100644 extensions/datavirtualization/src/wizards/tableFromFile/pages/fileConfigPage.ts create mode 100644 extensions/datavirtualization/src/wizards/tableFromFile/pages/modifyColumnsPage.ts create mode 100644 extensions/datavirtualization/src/wizards/tableFromFile/pages/prosePreviewPage.ts create mode 100644 extensions/datavirtualization/src/wizards/tableFromFile/pages/summaryPage.ts create mode 100644 extensions/datavirtualization/src/wizards/tableFromFile/tableFromFileWizard.ts create mode 100644 extensions/datavirtualization/src/wizards/virtualizeData/connectionDetailsPage.ts create mode 100644 extensions/datavirtualization/src/wizards/virtualizeData/createMasterKeyPage.ts create mode 100644 extensions/datavirtualization/src/wizards/virtualizeData/objectMappingPage.ts create mode 100644 extensions/datavirtualization/src/wizards/virtualizeData/selectDataSourcePage.ts create mode 100644 extensions/datavirtualization/src/wizards/virtualizeData/summaryPage.ts create mode 100644 extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataInputManager.ts create mode 100644 extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataModel.ts create mode 100644 extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataTree.ts create mode 100644 extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataWizard.ts create mode 100644 extensions/datavirtualization/src/wizards/wizardCommands.ts create mode 100644 extensions/datavirtualization/src/wizards/wizardPageWrapper.ts create mode 100644 extensions/datavirtualization/tsconfig.json create mode 100644 extensions/datavirtualization/tslint.json create mode 100644 extensions/datavirtualization/yarn.lock diff --git a/build/filters.js b/build/filters.js index 708bf25078..2d3a6cca20 100644 --- a/build/filters.js +++ b/build/filters.js @@ -152,6 +152,7 @@ module.exports.indentationFilter = [ '!extensions/sql-database-projects/src/test/baselines/*.json', '!extensions/sql-database-projects/src/test/baselines/*.sqlproj', '!extensions/sql-database-projects/BuildDirectory/SystemDacpacs/**', + '!extensions/datavirtualization/scaleoutdataservice/**', '!resources/linux/snap/electron-launch', '!extensions/markdown-language-features/media/*.js', '!extensions/simple-browser/media/*.js', diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 31634ef622..98f96e429d 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -261,6 +261,7 @@ const externalExtensions = [ 'azuremonitor', 'cms', 'dacpac', + 'datavirtualization', 'import', 'kusto', 'machine-learning', @@ -281,7 +282,6 @@ exports.vscodeExternalExtensions = [ ]; // extensions that require a rebuild since they have native parts const rebuildExtensions = [ - 'big-data-cluster', 'mssql' ]; const marketplaceWebExtensionsExclude = new Set([ diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index f72cd0dea4..71728fe3e8 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -306,6 +306,7 @@ const externalExtensions = [ 'azuremonitor', 'cms', 'dacpac', + 'datavirtualization', 'import', 'kusto', 'machine-learning', diff --git a/build/npm/dirs.js b/build/npm/dirs.js index d80d0c98b9..7edd0cfe3e 100644 --- a/build/npm/dirs.js +++ b/build/npm/dirs.js @@ -21,6 +21,7 @@ exports.dirs = [ 'extensions/configuration-editing', 'extensions/dacpac', 'extensions/data-workspace', + 'extensions/datavirtualization', 'extensions/git', 'extensions/git-base', 'extensions/github', diff --git a/extensions/datavirtualization/.gitignore b/extensions/datavirtualization/.gitignore new file mode 100644 index 0000000000..55d05f5f04 --- /dev/null +++ b/extensions/datavirtualization/.gitignore @@ -0,0 +1,20 @@ + +out +node_modules +*.vsix +.DS_Store +.idea +.rpt2_cache +test-reports/** + +# Jupyter paths +wheels/** + +# service paths +scaleoutdataservice/** + +# python paths +python/**/** + +# coverage folder +coverage/** diff --git a/extensions/datavirtualization/.vscodeignore b/extensions/datavirtualization/.vscodeignore new file mode 100644 index 0000000000..2ccd881f61 --- /dev/null +++ b/extensions/datavirtualization/.vscodeignore @@ -0,0 +1,21 @@ +# Unwanted compiled files +out +*.vsix + +# Build/Source files +src +test +typings +.gitignore +tsconfig.json +coverConfig.json +tslint.json + +# Reference files +**/*.ts +**/*.map +.gitignore +tsconfig.json +extension.webpack.config.js +yarn.lock + diff --git a/extensions/datavirtualization/.yarnrc b/extensions/datavirtualization/.yarnrc new file mode 100644 index 0000000000..f757a6ac58 --- /dev/null +++ b/extensions/datavirtualization/.yarnrc @@ -0,0 +1 @@ +--ignore-engines true \ No newline at end of file diff --git a/extensions/datavirtualization/LICENSE.txt b/extensions/datavirtualization/LICENSE.txt new file mode 100644 index 0000000000..b8761be14e --- /dev/null +++ b/extensions/datavirtualization/LICENSE.txt @@ -0,0 +1,47 @@ +MICROSOFT SOFTWARE LICENSE TERMS +MICROSOFT AZURE DATA STUDIO – DATA VIRTUALIZATION +MICROSOFT PACKAGES FOR AZURE DATA STUDIO + IF YOU LIVE IN (OR ARE A BUSINESS WITH A PRINCIPAL PLACE OF BUSINESS IN) THE UNITED STATES, PLEASE READ THE “BINDING ARBITRATION AND CLASS ACTION WAIVER” SECTION BELOW. IT AFFECTS HOW DISPUTES ARE RESOLVED. +These license terms are an agreement between you and Microsoft Corporation (or one of its affiliates). They apply to the software named above and any Microsoft services or software updates (except to the extent such services or updates are accompanied by new or additional terms, in which case those different terms apply prospectively and do not alter your or Microsoft’s rights relating to pre-updated software or services). IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW. BY USING THE SOFTWARE, YOU ACCEPT THESE TERMS. +1. INSTALLATION AND USE RIGHTS. +a) General. You may install and use any number of copies of the software solely for supported use with Azure Data Studio and other Azure services, as described in the software documentation. +b) Included Microsoft Applications. The software may include other Microsoft applications. These license terms apply to those included applications, if any, unless other license terms are provided with the other Microsoft applications. +c) Third Party Software. The software may include third party applications that are licensed to you under this agreement or under their own terms. License terms, notices, and acknowledgements, if any, for the third party applications may be accessible online at http://aka.ms/thirdpartynotices or in an accompanying notices file. Even if such applications are governed by other agreements, the disclaimer, limitations on, and exclusions of damages below also apply to the extent allowed by applicable law. +2. PRE-RELEASE SOFTWARE. The software is a pre-release version. It may not operate correctly. It may be different from the commercially released version. +3. FEEDBACK. If you give feedback about the software to Microsoft, you give to Microsoft, without charge, the right to use, share and commercialize your feedback in any way and for any purpose. You will not give feedback that is subject to a license that requires Microsoft to license its software or documentation to third parties because Microsoft includes your feedback in them. These rights survive this agreement. +4. DATA COLLECTION. The software may collect information about you and your use of the software and send that to Microsoft. Microsoft may use this information to provide services and improve Microsoft’s products and services. Your opt-out rights, if any, are described in the product documentation. Some features in the software may enable collection of data from users of your applications that access or use the software. If you use these features to enable data collection in your applications, you must comply with applicable law, including getting any required user consent, and maintain a prominent privacy policy that accurately informs users about how you use, collect, and share their data. You can learn more about Microsoft’s data collection and use in the product documentation and the Microsoft Privacy Statement at https://go.microsoft.com/fwlink/?LinkId=512132. You agree to comply with all applicable provisions of the Microsoft Privacy Statement. +a) Processing of personal data: To the extent Microsoft is a processor or sub processor of personal data in connection with the software, Microsoft makes the commitments in the European Union General Data Protection Regulation Terms of the Online Services Terms to all customers effective 25 May 2018, at http://go.microsoft.com/?linkid=9840733. +5. SCOPE OF LICENSE. The software is licensed, not sold. Microsoft reserves all other rights. Unless applicable law gives you more rights despite this limitation, you will not (and have no right to): +a) work around any technical limitations in the software that only allow you to use it in certain ways; +b) reverse engineer, decompile or disassemble the software; +c) remove, minimize, block, or modify any notices of Microsoft or its suppliers in the software; +d) use the software in any way that is against the law or to create or propagate malware; or +e) share, publish, distribute, or lend the software, provide the software as a stand-alone hosted solution for others to use, or transfer the software or this agreement to any third party. + +6. EXPORT RESTRICTIONS. You must comply with all domestic and international export laws and regulations that apply to the software, which include restrictions on destinations, end users, and end use. For further information on export restrictions, visit http://aka.ms/exporting. +7. SUPPORT SERVICES. Microsoft is not obligated under this agreement to provide any support services for the software. Any support provided is “as is”, “with all faults”, and without warranty of any kind. +8. UPDATES. The software may periodically check for updates, and download and install them for you. You may obtain updates only from Microsoft or authorized sources. Microsoft may need to update your system to provide you with updates. You agree to receive these automatic updates without any additional notice. Updates may not include or support all existing software features, services, or peripheral devices. +9. ENTIRE AGREEMENT. This agreement, and any other terms Microsoft may provide for supplements, updates, or third-party applications, is the entire agreement for the software. +10. APPLICABLE LAW AND PLACE TO RESOLVE DISPUTES. If you acquired the software in the United States or Canada, the laws of the state or province where you live (or, if a business, where your principal place of business is located) govern the interpretation of this agreement, claims for its breach, and all other claims (including consumer protection, unfair competition, and tort claims), regardless of conflict of laws principles, except that the FAA governs everything related to arbitration. If you acquired the software in any other country, its laws apply, except that the FAA governs everything related to arbitration. If U.S. federal jurisdiction exists, you and Microsoft consent to exclusive jurisdiction and venue in the federal court in King County, Washington for all disputes heard in court (excluding arbitration). If not, you and Microsoft consent to exclusive jurisdiction and venue in the Superior Court of King County, Washington for all disputes heard in court (excluding arbitration). +11. CONSUMER RIGHTS; REGIONAL VARIATIONS. This agreement describes certain legal rights. You may have other rights, including consumer rights, under the laws of your state, province, or country. Separate and apart from your relationship with Microsoft, you may also have rights with respect to the party from which you acquired the software. This agreement does not change those other rights if the laws of your state, province, or country do not permit it to do so. For example, if you acquired the software in one of the below regions, or mandatory country law applies, then the following provisions apply to you: +a) Australia. You have statutory guarantees under the Australian Consumer Law and nothing in this agreement is intended to affect those rights. +b) Canada. If you acquired this software in Canada, you may stop receiving updates by turning off the automatic update feature, disconnecting your device from the Internet (if and when you re-connect to the Internet, however, the software will resume checking for and installing updates), or uninstalling the software. The product documentation, if any, may also specify how to turn off updates for your specific device or software. +c) Germany and Austria. +i. Warranty. The properly licensed software will perform substantially as described in any Microsoft materials that accompany the software. However, Microsoft gives no contractual guarantee in relation to the licensed software. +ii. Limitation of Liability. In case of intentional conduct, gross negligence, claims based on the Product Liability Act, as well as, in case of death or personal or physical injury, Microsoft is liable according to the statutory law. +Subject to the foregoing clause ii., Microsoft will only be liable for slight negligence if Microsoft is in breach of such material contractual obligations, the fulfillment of which facilitate the due performance of this agreement, the breach of which would endanger the purpose of this agreement and the compliance with which a party may constantly trust in (so-called "cardinal obligations"). In other cases of slight negligence, Microsoft will not be liable for slight negligence. +12. DISCLAIMER OF WARRANTY. THE SOFTWARE IS LICENSED “AS IS.” YOU BEAR THE RISK OF USING IT. MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTEES, OR CONDITIONS. TO THE EXTENT PERMITTED UNDER APPLICABLE LAWS, MICROSOFT EXCLUDES ALL IMPLIED WARRANTIES, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. +13. LIMITATION ON AND EXCLUSION OF DAMAGES. IF YOU HAVE ANY BASIS FOR RECOVERING DAMAGES DESPITE THE PRECEDING DISCLAIMER OF WARRANTY, YOU CAN RECOVER FROM MICROSOFT AND ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S. $5.00. YOU CANNOT RECOVER ANY OTHER DAMAGES, INCLUDING CONSEQUENTIAL, LOST PROFITS, SPECIAL, INDIRECT OR INCIDENTAL DAMAGES. +This limitation applies to (a) anything related to the software, services, content (including code) on third party Internet sites, or third party applications; and (b) claims for breach of contract, warranty, guarantee, or condition; strict liability, negligence, or other tort; or any other claim; in each case to the extent permitted by applicable law. +It also applies even if Microsoft knew or should have known about the possibility of the damages. The above limitation or exclusion may not apply to you because your state, province, or country may not allow the exclusion or limitation of incidental, consequential, or other damages. + +Please note: As this software is distributed in Canada, some of the clauses in this agreement are provided below in French. +Remarque: Ce logiciel étant distribué au Canada, certaines des clauses dans ce contrat sont fournies ci-dessous en français. +EXONÉRATION DE GARANTIE. Le logiciel visé par une licence est offert « tel quel ». Toute utilisation de ce logiciel est à votre seule risque et péril. Microsoft n’accorde aucune autre garantie expresse. Vous pouvez bénéficier de droits additionnels en vertu du droit local sur la protection des consommateurs, que ce contrat ne peut modifier. La ou elles sont permises par le droit locale, les garanties implicites de qualité marchande, d’adéquation à un usage particulier et d’absence de contrefaçon sont exclues. +LIMITATION DES DOMMAGES-INTÉRÊTS ET EXCLUSION DE RESPONSABILITÉ POUR LES DOMMAGES. Vous pouvez obtenir de Microsoft et de ses fournisseurs une indemnisation en cas de dommages directs uniquement à hauteur de 5,00 $ US. Vous ne pouvez prétendre à aucune indemnisation pour les autres dommages, y compris les dommages spéciaux, indirects ou accessoires et pertes de bénéfices. +Cette limitation concerne: +• tout ce qui est relié au logiciel, aux services ou au contenu (y compris le code) figurant sur des sites Internet tiers ou dans des programmes tiers; et +• les réclamations au titre de violation de contrat ou de garantie, ou au titre de responsabilité stricte, de négligence ou d’une autre faute dans la limite autorisée par la loi en vigueur. +Elle s’applique également, même si Microsoft connaissait ou devrait connaître l’éventualité d’un tel dommage. Si votre pays n’autorise pas l’exclusion ou la limitation de responsabilité pour les dommages indirects, accessoires ou de quelque nature que ce soit, il se peut que la limitation ou l’exclusion ci-dessus ne s’appliquera pas à votre égard. +EFFET JURIDIQUE. Le présent contrat décrit certains droits juridiques. Vous pourriez avoir d’autres droits prévus par les lois de votre pays. Le présent contrat ne modifie pas les droits que vous confèrent les lois de votre pays si celles-ci ne le permettent pas. + diff --git a/extensions/datavirtualization/README.md b/extensions/datavirtualization/README.md new file mode 100644 index 0000000000..585089e2d3 --- /dev/null +++ b/extensions/datavirtualization/README.md @@ -0,0 +1,34 @@ +# Data Virtualization extension for Azure Data Studio +This extension adds Data Virtualization support for SQL Server 2019 and above. This includes support for creating new SQL Server, Oracle, MongoDB, Teradata and HDFS External Data Sources and External Tables using interactive wizards. + +## Supported Features +* SQL Server Polybase Data Virtualization Wizard + * Create an external table and its supporting metadata structures with an easy to use wizard. + * Remote SQL Server and Oracle servers are supported for all versions of SQL Server 2019 and above + * Remote MongoDB and Teradata servers are supported for versions of SQL Server 2019 with CU5 and above + * Launch Virtualize Data from CSV Files wizard in HDFS, which lets you create an external table in your SQL Server Master instance associated with the cluster. You can virtualize the data from the remote HDFS Data sources without ever needing to now move the data. + +# Usage + +### Polybase Data Virtualization Wizard +* From a SQL Server 2019 instance the Data Virtualization Wizard may be opened in three ways: + * Right click on a server, choose Manage, click on the tab for SQL Server 2019, and choose Virtualize Data + * With a SQL Server 2019 instance selected in the Object Explorer, bring up Data Virtualization Wizard via the Command Palette + * Right click on a SQL Server 2019 database in the Object Explorer and choose Virtualize Data +* In this version of the extension, external tables may be created to access remote SQL Server, Oracle, MongoDB and Teradata tables. *Note: While the External Table functionality is a SQL 2019 feature, the remote SQL Server may be running an earlier version of SQL Server* +* Choose the server type you are connecting to on the first page of the wizard and continue +* You will be prompted to create a Database Master Key if one has not already been created. Passwords of insufficient complexity will be blocked. +* Create a data source connection and named credential for the remote server +* Choose which objects to map to your new external table +* Choose Generate Script or Create to finish the wizard +* After creation of the external table, it will appear in the object tree of the database where it was created immediately + +### Polybase Virtualize Data From CSV Files Wizard +* From connecting to the SQL Server big data cluster end point, you might need to create an external table over the files in your HDFS and this can be done in two ways: + * Browse to the .csv file in HDFS over which you would like to create an External Table from, right click on the file and then launch the Virtualize Data From CSV Files wizard. + * Next choose the active SQL Server connections from the drop down and this will fill in the connection details. You will need to pass in the credentials to the SQL Server Master Instance which is associated with this end point for the SQL Server big data cluster connection end point. + * Browse to the folder in HDFS which has the files with the same file extension and same schema and now when you launch the Virtualize Data From CSV Files then it will create an external table over all the files in the folder. + +# Known Issues +* You will not be able to preview files in HDFS which are over 30MB. +* All the files in the HDFS folder for Virtualize Data From CSV Files to work properly would need to have the same file extension (.csv) and conform to the same schema. If there are .csv files which are of different schema then the wizard will still open but you will not be able to create the external table. diff --git a/extensions/datavirtualization/ThirdPartyNotices.txt b/extensions/datavirtualization/ThirdPartyNotices.txt new file mode 100644 index 0000000000..7b99686ba4 --- /dev/null +++ b/extensions/datavirtualization/ThirdPartyNotices.txt @@ -0,0 +1,427 @@ +MICROSOFT SQL SERVER 2019 PREVIEW EXTENSION + +THIRD-PARTY SOFTWARE NOTICES AND INFORMATION +Do Not Translate or Localize + +This project incorporates third party material (and other Microsoft material) from the projects +listed below. The original copyright notice and the license under which Microsoft received such +third party material are set forth below. Microsoft reserves all other rights not expressly granted, +whether by implication, estoppel or otherwise. + + DonJayamanne/bowerVSCode: https://github.com/DonJayamanne/bowerVSCode + fs-extra: https://registry.npmjs.org/fs-extra + Pandas: https://pypi.org/project/pandas/ + Python: https://registry.npmjs.org/python + vscode-extension-telemetry: https://registry.npmjs.org/vscode-extension-telemetry + vscode-nls: https://registry.npmjs.org/vscode-nls + which: https://registry.npmjs.org/which + +%% DonJayamanne/bowerVSCode NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2015 DonJayamanne + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +========================================= +END OF DonJayamanne/bowerVSCode NOTICES AND INFORMATION + +%% fs-extra NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2011-2017 JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF fs-extra NOTICES AND INFORMATION + +%% Pandas NOTICES AND INFORMATION BEGIN HERE +========================================= +BSD 3-Clause License + +Copyright (c) 2008-2012, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF Pandas NOTICES AND INFORMATION + +%% Python NOTICES AND INFORMATION BEGIN HERE +========================================= +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Python Software Foundation; All +Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF Python NOTICES AND INFORMATION + +%% vscode-extension-telemetry NOTICES AND INFORMATION BEGIN HERE +========================================= +vscode-extension-telemetry + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF vscode-extension-telemetry NOTICES AND INFORMATION + +%% vscode-nls NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF vscode-nls NOTICES AND INFORMATION + +%% which NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF which NOTICES AND INFORMATION diff --git a/extensions/datavirtualization/config.json b/extensions/datavirtualization/config.json new file mode 100644 index 0000000000..59db5f6a1c --- /dev/null +++ b/extensions/datavirtualization/config.json @@ -0,0 +1,24 @@ +{ + "downloadUrl": "https://sqlopsextensions.blob.core.windows.net/extensions/datavirtualization/service/{#version#}/scaleoutdataservice-{#fileName#}", + "version": "1.10.0", + "downloadFileNames": { + "Windows_64": "win-x64.zip", + "Windows_86": "win-x86.zip", + "OSX": "osx-x64.tar.gz", + "Linux_64": "linux-x64.tar.gz", + "CentOS_7": "linux-x64.tar.gz", + "Debian_8": "linux-x64.tar.gz", + "Fedora_23": "linux-x64.tar.gz", + "OpenSUSE_13_2": "linux-x64.tar.gz", + "RHEL_7": "linux-x64.tar.gz", + "SLES_12_2": "linux-x64.tar.gz", + "Ubuntu_14": "linux-x64.tar.gz", + "Ubuntu_16": "linux-x64.tar.gz", + "Ubuntu_18": "linux-x64.tar.gz" + }, + "installDirectory": "scaleoutdataservice/{#platform#}/{#version#}", + "executableFiles": [ + "MicrosoftSqlToolsScaleOutData", + "MicrosoftSqlToolsScaleOutData.exe" + ] +} diff --git a/extensions/datavirtualization/coverConfig.json b/extensions/datavirtualization/coverConfig.json new file mode 100644 index 0000000000..8cabab2608 --- /dev/null +++ b/extensions/datavirtualization/coverConfig.json @@ -0,0 +1,23 @@ +{ + "enabled": true, + "relativeSourcePath": "..", + "relativeCoverageDir": "../../coverage", + "ignorePatterns": [ + "**/node_modules/**", + "**/libs/**", + "**/lib/**", + "**/htmlcontent/**/*.js", + "**/*.bundle.js", + "**/markdown-language-features/**", + "**/media/*.js" + ], + "includePid": false, + "reports": [ + "json" + ], + "verbose": false, + "remapOptions": { + "basePath": "../..", + "useAbsolutePaths": true + } +} diff --git a/extensions/datavirtualization/extension.webpack.config.js b/extensions/datavirtualization/extension.webpack.config.js new file mode 100644 index 0000000000..35b95ccffc --- /dev/null +++ b/extensions/datavirtualization/extension.webpack.config.js @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts' + } +}); diff --git a/extensions/datavirtualization/package.json b/extensions/datavirtualization/package.json new file mode 100644 index 0000000000..7be35a09da --- /dev/null +++ b/extensions/datavirtualization/package.json @@ -0,0 +1,119 @@ +{ + "name": "datavirtualization", + "displayName": "%title.datavirtualization%", + "description": "%config.extensionDescription%", + "version": "1.13.0", + "publisher": "Microsoft", + "icon": "resources/extension.png", + "aiKey": "29a207bb14f84905966a8f22524cb730-25407f35-11b6-4d4e-8114-ab9e843cb52f-7380", + "engines": { + "vscode": "^1.48.0", + "azdata": "^1.22.0" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/azuredatastudio.git" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "*" + ], + "main": "./out/extension", + "capabilities": { + "virtualWorkspaces": false, + "untrustedWorkspaces": { + "supported": true + } + }, + "contributes": { + "configuration": [ + { + "type": "object", + "title": "%title.datavirtualization%", + "properties": { + "dataManagement.logDebugInfo": { + "type": "boolean", + "default": false, + "description": "%config.logDebugInfo%" + }, + "dataManagement.proseParsingMaxLines": { + "type": "number", + "default": 10000, + "description": "%config.proseParsingMaxLines%" + } + } + } + ], + "commands": [ + { + "command": "virtualizedatawizard.task.open", + "title": "%title.openVirtualizeDataWizard%", + "icon": { + "dark": "resources/dark/polybase_inverse.svg", + "light": "resources/light/polybase.svg" + } + }, + { + "command": "virtualizedatawizard.cmd.open", + "title": "%title.openVirtualizeDataWizard%", + "icon": { + "dark": "resources/dark/polybase_inverse.svg", + "light": "resources/light/polybase.svg" + } + }, + { + "command": "mssqlHdfsTableWizard.cmd.open", + "title": "%title.openMssqlHdfsTableWizard%" + } + ], + "menus": { + "commandPalette": [ + { + "command": "virtualizedatawizard.cmd.open", + "when": "wizardservice:enabled" + }, + { + "command": "virtualizedatawizard.task.open", + "when": "false" + }, + { + "command": "mssqlHdfsTableWizard.cmd.open", + "when": "false" + } + ], + "objectExplorer/item/context": [ + { + "command": "virtualizedatawizard.cmd.open", + "when": "connectionProvider == MSSQL && nodeType && nodeType == Database && serverMajorVersion >= 15", + "group": "1data@1" + }, + { + "command": "mssqlHdfsTableWizard.cmd.open", + "when": "nodeType =~ /(mssqlCluster:file|mssqlCluster:folder)/", + "group": "1ads@1" + } + ], + "dashboard/toolbar": [ + { + "command": "virtualizedatawizard.task.open", + "when": "connectionProvider == 'MSSQL' && mssql:servermajorversion >= 15" + } + ] + } + }, + "dependencies": { + "@microsoft/ads-extension-telemetry": "^1.3.2", + "@microsoft/ads-service-downloader": "^1.0.4", + "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.3.2", + "vscode-nls": "^5.2.0" + }, + "devDependencies": { + "@microsoft/vscodetestcover": "^1.2.1", + "mocha": "^7.1.1", + "should": "^13.2.1", + "typemoq": "^2.1.0" + } +} diff --git a/extensions/datavirtualization/package.nls.json b/extensions/datavirtualization/package.nls.json new file mode 100644 index 0000000000..3d94cf2875 --- /dev/null +++ b/extensions/datavirtualization/package.nls.json @@ -0,0 +1,10 @@ +{ + "config.logDebugInfo": "Enable/disable logging debug information to the developer console.", + "config.proseParsingMaxLines": "Number of lines to read from a file to run PROSE parsing on.", + "config.extensionDescription": "Support for Data Virtualization in SQL Server, including wizards for creating an External Table from an External Data Source or CSV files.", + "title.datavirtualization": "Data Virtualization", + "title.openVirtualizeDataWizard": "Virtualize Data", + "title.openMssqlHdfsTableWizard": "Virtualize Data From CSV Files", + "title.tasks": "Tasks", + "description.datavirtualization": "Support for Data Virtualization features in SQL Server" +} diff --git a/extensions/datavirtualization/resources/dark/database_inverse.svg b/extensions/datavirtualization/resources/dark/database_inverse.svg new file mode 100644 index 0000000000..4b300aff60 --- /dev/null +++ b/extensions/datavirtualization/resources/dark/database_inverse.svg @@ -0,0 +1 @@ +database_inverse \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/delete_inverse.svg b/extensions/datavirtualization/resources/dark/delete_inverse.svg new file mode 100644 index 0000000000..b32a9936fe --- /dev/null +++ b/extensions/datavirtualization/resources/dark/delete_inverse.svg @@ -0,0 +1 @@ +delete_inverse_16x16 \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/file_inverse.svg b/extensions/datavirtualization/resources/dark/file_inverse.svg new file mode 100644 index 0000000000..8276c545aa --- /dev/null +++ b/extensions/datavirtualization/resources/dark/file_inverse.svg @@ -0,0 +1 @@ +file_inverse_16x16 \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/folder_inverse.svg b/extensions/datavirtualization/resources/dark/folder_inverse.svg new file mode 100644 index 0000000000..f94d427cb1 --- /dev/null +++ b/extensions/datavirtualization/resources/dark/folder_inverse.svg @@ -0,0 +1 @@ +folder_inverse_16x16 \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/new_spark_job_inverse.svg b/extensions/datavirtualization/resources/dark/new_spark_job_inverse.svg new file mode 100644 index 0000000000..e5ed4b3190 --- /dev/null +++ b/extensions/datavirtualization/resources/dark/new_spark_job_inverse.svg @@ -0,0 +1 @@ +new_spark_job_inverse \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/nottrusted_inverse.svg b/extensions/datavirtualization/resources/dark/nottrusted_inverse.svg new file mode 100644 index 0000000000..bd3832e82a --- /dev/null +++ b/extensions/datavirtualization/resources/dark/nottrusted_inverse.svg @@ -0,0 +1 @@ +nontrust-inverse \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/open-file.svg b/extensions/datavirtualization/resources/dark/open-file.svg new file mode 100644 index 0000000000..d431e90460 --- /dev/null +++ b/extensions/datavirtualization/resources/dark/open-file.svg @@ -0,0 +1,4 @@ + + ' + + \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/polybase_inverse.svg b/extensions/datavirtualization/resources/dark/polybase_inverse.svg new file mode 100644 index 0000000000..8992798b48 --- /dev/null +++ b/extensions/datavirtualization/resources/dark/polybase_inverse.svg @@ -0,0 +1 @@ +polybase_inverse \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/refresh_inverse.svg b/extensions/datavirtualization/resources/dark/refresh_inverse.svg new file mode 100644 index 0000000000..d79fdaa4e8 --- /dev/null +++ b/extensions/datavirtualization/resources/dark/refresh_inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/server_inverse.svg b/extensions/datavirtualization/resources/dark/server_inverse.svg new file mode 100644 index 0000000000..c11b61e1cd --- /dev/null +++ b/extensions/datavirtualization/resources/dark/server_inverse.svg @@ -0,0 +1 @@ +server_inverse_16x16 \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/sql_database_inverse.svg b/extensions/datavirtualization/resources/dark/sql_database_inverse.svg new file mode 100644 index 0000000000..5eaaf7e5f0 --- /dev/null +++ b/extensions/datavirtualization/resources/dark/sql_database_inverse.svg @@ -0,0 +1 @@ +sql_database_inverse \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/sql_server_inverse.svg b/extensions/datavirtualization/resources/dark/sql_server_inverse.svg new file mode 100644 index 0000000000..1ce3f48b39 --- /dev/null +++ b/extensions/datavirtualization/resources/dark/sql_server_inverse.svg @@ -0,0 +1 @@ +sql_server_inverse \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/start_inverse.svg b/extensions/datavirtualization/resources/dark/start_inverse.svg new file mode 100644 index 0000000000..def21c19f6 --- /dev/null +++ b/extensions/datavirtualization/resources/dark/start_inverse.svg @@ -0,0 +1 @@ +start_filled_inverse \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/stop_cell_inverse.svg b/extensions/datavirtualization/resources/dark/stop_cell_inverse.svg new file mode 100644 index 0000000000..aa175704ce --- /dev/null +++ b/extensions/datavirtualization/resources/dark/stop_cell_inverse.svg @@ -0,0 +1 @@ +stop_cell_inverse \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/stop_inverse.svg b/extensions/datavirtualization/resources/dark/stop_inverse.svg new file mode 100644 index 0000000000..1c6c26cc09 --- /dev/null +++ b/extensions/datavirtualization/resources/dark/stop_inverse.svg @@ -0,0 +1 @@ +stop_filled_inverse \ No newline at end of file diff --git a/extensions/datavirtualization/resources/dark/trusted_inverse.svg b/extensions/datavirtualization/resources/dark/trusted_inverse.svg new file mode 100644 index 0000000000..25967f45aa --- /dev/null +++ b/extensions/datavirtualization/resources/dark/trusted_inverse.svg @@ -0,0 +1 @@ +trust_inverse \ No newline at end of file diff --git a/extensions/datavirtualization/resources/extension.png b/extensions/datavirtualization/resources/extension.png new file mode 100644 index 0000000000000000000000000000000000000000..ea530a5d28b8f780f14cdc8a72c60d9bd09bbf28 GIT binary patch literal 2890 zcmV-Q3$^r#P)T*d9qct2J8M`cd(0kl z>`eE^!+4MB>6zK-s_Fi^r+yFCj=QJ&tFOQQj;iV!fw=IsSC7FEUj)P{5Ox4ahb{1P06mLz^(mY?F}HdoOtF-3jtmsXpM=0y02pD569J|H@fm#k$O}Lm6jr|cGz7ki zIP@hLh5-{x=A1aKqrB+Xb;fiXaneR8bZLC zAI-cAgb#znK>*nhWZDpgN_8Y(u7oW{V3Zxgbg_(^^93wbs><43olpJz52klkP2(95 z9#hu3MmY`A|2#LmV<|X0(;$=aD_>;?$*Tp3yt)aQFtNBe07@IE9@@nJb-%2E zcplB;3CHg3wLL5<;A}O z0IXCJ4?q5ay5oH0m6OAYNk5cBj_{M=9Do_Qks%!a=wl!P%!J%{dsopOpnIhHnlFjuBCm86pkiH_3b@FzLZw*7|Ni-J z5D^mT6t*4OvwDn(7|BcV@BrK><`;11@&o|Du*^CG062L7*&KjRPof1EdE=4;a3j^1 z#qij60JRX{hKt@v8$fqR^<}Z|-48WA&PQ*OA%IOXQ?|K{1UF}yPZAg(4^VV~t4k1X z$pBUpQ?SI^d;tNdwvgTcuzpstO06H1jhTJb-)|s7ewA&_n^4C^S88JUR$5 zl&GDXufqA$Y}0eao(zm5eXF1CKg`sOtXbQ;GZ07Do&U#KV98?$m>rU3ARANmLo3s&j$j~e1NMRUGo6e?hMS4##uBO zjNIDNtWKZMPCY>Kfqsy!P&*Ex{I|Ti_U<9=0lMYr%s~s*cC`l3NcT*@ykBnPg$5j; zjJfvbb&PZps7{pi1mQkghJX%TdJx{e8#*CJuHnFvKq{!6SfP@m^!axxq+CWM4TyC! zn_bg2M+2y9*Cs5y@J5%ew1MlRy#Z8i6;ZiWR4POIct4VR)>amGL)Y@t!2zYZLD$so z>xEL+d&r>v5)Mxf;3n*fDvY{bc0$Vkl{StZ3XVF`>j46&0>T(nX}BDN*}bk*0Z~~G zphG|xlR%Cuf*Ly?a1!Xc9x+(q)1B+fP6h>}lfd1t$GkzvML>5#faESwu?+znXZO1D z{Ig90-JcmGU>syn-{|T;zUbnr;KmpnBwjfP2N~2~L&~yCo*ux=nCO3a5axjYVFDwy zHgziuf#DC`-+3c8p?I@^rPmfa{;F5}^`oc6P!hT0128h3?+mtH2N3Y5gL_bgu=Lsj zO0MB*$tvKsXAZFGa1N;*X}^s&A^@w;pE2bg96)KZgzAF)#*BW{)c?pJ5}Om!*Ot-2 z3IiuMqxgCWaFi98zx50Oeym*<$F@OsZDos@tXH^^F+dBQby8IsXoRpQpk1lfo@yar zCf1Bbm;xdx;FQ)@01>c<+z581 zidTN|o7IV<6CXc?WOmIPyZa&WUch(o!%vTUhJY~iM9$h=ynA0hKP4Q;GH@f1 zQX5_PECaufeK)=iG%Ewc(3EJtFQ2ccm%%`gFNERx*hUu}bxgbhhrWrQoq2BMf7rYn zdgO!eM0L5A=%xDHzl7C!{N_*w`oT(Cc<2+SU{@U&ra&@VuM2lS(wR*cK=^LUMomnk zQoW9!f8ln+^}LKH3^iG)B&?M8LQFqTuV)D0fByjhiA)+z>kgML5~<82etPD--)pgX zG34&`TPQ6qK|o++|E{`C8db#a$m9S-n7niyj%~v*3~WETABMRW22}ThO$hk!l?jv< zYP*AE26HY0@Dl>uWnl25k$$k3E0}!s+Un;mdq$DUZCd@@s0+EBmDq%UcE~1y92SJA zz`G3e51SA$G`1b(g(Uz$b}(0WEX*=6X#JQ83-|5bQQNLw7_eM>6sivaOv=EW$KTrY zSTB`<_*$%A?bjmi6;jvvjIRtyztqVD8@bm!Fm-4W3 z7Sfv)zaQ5jj!nopQ zW<7vv8bUxs%D_&M-C>>0P_TN`tO(+cY@g!m0U|mO)G=`x3hp*>%>m>?KpeG#-pK(J zj040_D~RrjprFwqAmYe-0H#_=4fMcKB<@>w&*9*AuyQ$i~yfK4q z2pBm$pzP+$GBBD&-_{i7@2K5Hj00>vfRPOTjhw>V0;;7l03bUw2rIentsKi>nMB5B zMUa~?fizv*_0|55C;{j~hS06UG>PiN-9v}>Dw_0l#rDf%vz%~a6!vfNq(pWCk z;(Q`gzZiyV87V&$tVj^mgdx{~_nmBcoU5*6;2Q#%5!aDKwjm%4scv7qipA;Lk2;J# zcmTOALv`|S)s+m|1FU0q@(mQ{YE?jSYEIky=b)8%oKiM{V zY(L72wf7STw%u1R4|uLA6p}7Ldw_KejcsdsEVdqip-}?>ih22$L6kv2Azxvc11MM# z#IaYv&40{5m_VjK+_#ee;LZOmVsW|@wiup>xULDbD-I@Jelu(_*rVSJ(nt~MACK1P zDxl|)9|uq(h9)c&l!257FrY#hP4Ez5R_6N_k+=pyZ&M|ZCTPTUnQvPT=vhI~1`th1 zkn<~<2CQnb{FJqv{1Sj+MM7x;LICHNpFDC!vJJpGJARr3o(I5VHzdtQ_?NwGy&c~> zC^cHPT18rC$4@%~KZf=M>=D3AwqZSvZylUe(AGN6T^vNwdO{fDBXDp40)w&_&?vzH oFa_Z61o{cmN6+J&P*TDFAN~mdYEKU}7XSbN07*qoM6N<$f<>4z5&!@I literal 0 HcmV?d00001 diff --git a/extensions/datavirtualization/resources/light/Folder.svg b/extensions/datavirtualization/resources/light/Folder.svg new file mode 100644 index 0000000000..517c9b185d --- /dev/null +++ b/extensions/datavirtualization/resources/light/Folder.svg @@ -0,0 +1 @@ +folder_16x16 \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/database.svg b/extensions/datavirtualization/resources/light/database.svg new file mode 100644 index 0000000000..7625beec4f --- /dev/null +++ b/extensions/datavirtualization/resources/light/database.svg @@ -0,0 +1 @@ +database_16x16 \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/database_OE.svg b/extensions/datavirtualization/resources/light/database_OE.svg new file mode 100644 index 0000000000..60fcdb4136 --- /dev/null +++ b/extensions/datavirtualization/resources/light/database_OE.svg @@ -0,0 +1 @@ +database_16x \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/delete.svg b/extensions/datavirtualization/resources/light/delete.svg new file mode 100644 index 0000000000..0bccf34124 --- /dev/null +++ b/extensions/datavirtualization/resources/light/delete.svg @@ -0,0 +1 @@ +delete_16x16 \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/file.svg b/extensions/datavirtualization/resources/light/file.svg new file mode 100644 index 0000000000..69412f5c61 --- /dev/null +++ b/extensions/datavirtualization/resources/light/file.svg @@ -0,0 +1 @@ +file_16x16 \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/hadoop.svg b/extensions/datavirtualization/resources/light/hadoop.svg new file mode 100644 index 0000000000..0757489a17 --- /dev/null +++ b/extensions/datavirtualization/resources/light/hadoop.svg @@ -0,0 +1 @@ +hadoop \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/new_spark_job.svg b/extensions/datavirtualization/resources/light/new_spark_job.svg new file mode 100644 index 0000000000..3775bf4da3 --- /dev/null +++ b/extensions/datavirtualization/resources/light/new_spark_job.svg @@ -0,0 +1 @@ +new_spark_job \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/nottrusted.svg b/extensions/datavirtualization/resources/light/nottrusted.svg new file mode 100644 index 0000000000..cc199359bd --- /dev/null +++ b/extensions/datavirtualization/resources/light/nottrusted.svg @@ -0,0 +1 @@ +nontrust \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/open-file.svg b/extensions/datavirtualization/resources/light/open-file.svg new file mode 100644 index 0000000000..8217810885 --- /dev/null +++ b/extensions/datavirtualization/resources/light/open-file.svg @@ -0,0 +1,4 @@ + + ' + + \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/polybase.svg b/extensions/datavirtualization/resources/light/polybase.svg new file mode 100644 index 0000000000..18d3b8f263 --- /dev/null +++ b/extensions/datavirtualization/resources/light/polybase.svg @@ -0,0 +1 @@ +polybase \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/refresh.svg b/extensions/datavirtualization/resources/light/refresh.svg new file mode 100644 index 0000000000..e034574819 --- /dev/null +++ b/extensions/datavirtualization/resources/light/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/server.svg b/extensions/datavirtualization/resources/light/server.svg new file mode 100644 index 0000000000..4a387986cb --- /dev/null +++ b/extensions/datavirtualization/resources/light/server.svg @@ -0,0 +1 @@ +server \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/sql_database.svg b/extensions/datavirtualization/resources/light/sql_database.svg new file mode 100644 index 0000000000..e0c1584d98 --- /dev/null +++ b/extensions/datavirtualization/resources/light/sql_database.svg @@ -0,0 +1 @@ +sql_database \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/sql_server.svg b/extensions/datavirtualization/resources/light/sql_server.svg new file mode 100644 index 0000000000..03cff4d932 --- /dev/null +++ b/extensions/datavirtualization/resources/light/sql_server.svg @@ -0,0 +1 @@ +sql_server \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/start.svg b/extensions/datavirtualization/resources/light/start.svg new file mode 100644 index 0000000000..b3e45763a7 --- /dev/null +++ b/extensions/datavirtualization/resources/light/start.svg @@ -0,0 +1 @@ +start_filled \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/stop.svg b/extensions/datavirtualization/resources/light/stop.svg new file mode 100644 index 0000000000..7792bafd21 --- /dev/null +++ b/extensions/datavirtualization/resources/light/stop.svg @@ -0,0 +1 @@ +stop_filled \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/stop_cell.svg b/extensions/datavirtualization/resources/light/stop_cell.svg new file mode 100644 index 0000000000..790114755e --- /dev/null +++ b/extensions/datavirtualization/resources/light/stop_cell.svg @@ -0,0 +1 @@ +stop_cell \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/table.svg b/extensions/datavirtualization/resources/light/table.svg new file mode 100644 index 0000000000..025d9bd136 --- /dev/null +++ b/extensions/datavirtualization/resources/light/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/datavirtualization/resources/light/trusted.svg b/extensions/datavirtualization/resources/light/trusted.svg new file mode 100644 index 0000000000..721fc0bd8f --- /dev/null +++ b/extensions/datavirtualization/resources/light/trusted.svg @@ -0,0 +1 @@ +trust \ No newline at end of file diff --git a/extensions/datavirtualization/src/apiWrapper.ts b/extensions/datavirtualization/src/apiWrapper.ts new file mode 100644 index 0000000000..59d9e030fb --- /dev/null +++ b/extensions/datavirtualization/src/apiWrapper.ts @@ -0,0 +1,202 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; + +import * as constants from './constants'; + +/** + * Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into + * this API from our code + * + * @export + * @class ApiWrapper + */ +export class ApiWrapper { + // Data APIs + public registerConnectionProvider(provider: azdata.ConnectionProvider): vscode.Disposable { + return azdata.dataprotocol.registerConnectionProvider(provider); + } + + public registerObjectExplorerProvider(provider: azdata.ObjectExplorerProvider): vscode.Disposable { + return azdata.dataprotocol.registerObjectExplorerProvider(provider); + } + + public registerTaskServicesProvider(provider: azdata.TaskServicesProvider): vscode.Disposable { + return azdata.dataprotocol.registerTaskServicesProvider(provider); + } + + public registerFileBrowserProvider(provider: azdata.FileBrowserProvider): vscode.Disposable { + return azdata.dataprotocol.registerFileBrowserProvider(provider); + } + + public registerCapabilitiesServiceProvider(provider: azdata.CapabilitiesProvider): vscode.Disposable { + return azdata.dataprotocol.registerCapabilitiesServiceProvider(provider); + } + + public registerModelViewProvider(widgetId: string, handler: (modelView: azdata.ModelView) => void): void { + return azdata.ui.registerModelViewProvider(widgetId, handler); + } + + public registerWebviewProvider(widgetId: string, handler: (webview: azdata.DashboardWebview) => void): void { + return azdata.dashboard.registerWebviewProvider(widgetId, handler); + } + + public createDialog(title: string): azdata.window.Dialog { + return azdata.window.createModelViewDialog(title); + } + + public openDialog(dialog: azdata.window.Dialog): void { + return azdata.window.openDialog(dialog); + } + + public closeDialog(dialog: azdata.window.Dialog): void { + return azdata.window.closeDialog(dialog); + } + + public registerTaskHandler(taskId: string, handler: (profile: azdata.IConnectionProfile) => void): void { + azdata.tasks.registerTask(taskId, handler); + } + + public startBackgroundOperation(operationInfo: azdata.BackgroundOperationInfo): void { + azdata.tasks.startBackgroundOperation(operationInfo); + } + + public getActiveConnections(): Thenable { + return azdata.connection.getActiveConnections(); + } + + public getCurrentConnection(): Thenable { + return azdata.connection.getCurrentConnection(); + } + + public createModelViewEditor(title: string, options?: azdata.ModelViewEditorOptions): azdata.workspace.ModelViewEditor { + return azdata.workspace.createModelViewEditor(title, options); + } + + // VSCode APIs + public createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { + return vscode.window.createTerminal(name, shellPath, shellArgs); + } + + public createTerminalWithOptions(options: vscode.TerminalOptions): vscode.Terminal { + return vscode.window.createTerminal(options); + } + + public executeCommand(command: string, ...rest: any[]): Thenable { + return vscode.commands.executeCommand(command, ...rest); + } + + public getFilePathRelativeToWorkspace(uri: vscode.Uri): string { + return vscode.workspace.asRelativePath(uri); + } + + public getWorkspacePathFromUri(uri: vscode.Uri): string | undefined { + let workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); + return workspaceFolder ? workspaceFolder.uri.fsPath : undefined; + } + + public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable { + return vscode.commands.registerCommand(command, callback, thisArg); + } + + public registerDocumentOpenHandler(handler: (doc: vscode.TextDocument) => any): vscode.Disposable { + return vscode.workspace.onDidOpenTextDocument(handler); + } + + public registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { + return vscode.window.registerTreeDataProvider(viewId, treeDataProvider); + } + + public setCommandContext(key: constants.CommandContext | string, value: any): Thenable { + return vscode.commands.executeCommand(constants.BuiltInCommands.SetContext, key, value); + } + + /** + * Get the configuration for a extensionName + * @param extensionName The string name of the extension to get the configuration for + * @param resource The optional URI, as a URI object or a string, to use to get resource-scoped configurations + */ + public getConfiguration(extensionName?: string, resource?: vscode.Uri | string): vscode.WorkspaceConfiguration { + if (typeof resource === 'string') { + try { + resource = this.parseUri(resource); + } catch (e) { + resource = undefined; + } + } else if (!resource) { + // Fix to avoid adding lots of errors to debug console. Expects a valid resource or null, not undefined + resource = null; + } + return vscode.workspace.getConfiguration(extensionName, resource as vscode.Uri); + } + + public getExtensionConfiguration(): vscode.WorkspaceConfiguration { + return this.getConfiguration(constants.extensionConfigSectionName); + } + + /** + * Parse uri + */ + public parseUri(uri: string): vscode.Uri { + return vscode.Uri.parse(uri); + } + + public showOpenDialog(options: vscode.OpenDialogOptions): Thenable { + return vscode.window.showOpenDialog(options); + } + + public showSaveDialog(options: vscode.SaveDialogOptions): Thenable { + return vscode.window.showSaveDialog(options); + } + + public openTextDocument(uri: vscode.Uri): Thenable; + public openTextDocument(options: { language?: string; content?: string; }): Thenable; + public openTextDocument(uriOrOptions): Thenable { + return vscode.workspace.openTextDocument(uriOrOptions); + } + + public showDocument(document: vscode.TextDocument): Thenable { + return this.executeCommand('vscode.open', document.uri); + } + + public showErrorMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showErrorMessage(message, ...items); + } + + public showWarningMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showWarningMessage(message, ...items); + } + + public showInformationMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showInformationMessage(message, ...items); + } + + public createStatusBarItem(alignment?: vscode.StatusBarAlignment, priority?: number): vscode.StatusBarItem { + return vscode.window.createStatusBarItem(alignment, priority); + } + + public createOutputChannel(name: string): vscode.OutputChannel { + return vscode.window.createOutputChannel(name); + } + + public createWizardPage(title: string): azdata.window.WizardPage { + return azdata.window.createWizardPage(title); + } + + public registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable { + return vscode.languages.registerCompletionItemProvider(selector, provider, ...triggerCharacters); + } + + public createTab(title: string): azdata.window.DialogTab { + return azdata.window.createTab(title); + } + + // Connection APIs + public openConnectionDialog(providers: string[], initialConnectionProfile?: azdata.IConnectionProfile, connectionCompletionOptions?: azdata.IConnectionCompletionOptions): Thenable { + return azdata.connection.openConnectionDialog(providers, initialConnectionProfile, connectionCompletionOptions); + } +} diff --git a/extensions/datavirtualization/src/appContext.ts b/extensions/datavirtualization/src/appContext.ts new file mode 100644 index 0000000000..df03917215 --- /dev/null +++ b/extensions/datavirtualization/src/appContext.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { ApiWrapper } from './apiWrapper'; + +/** + * Global context for the application + */ +export class AppContext { + + private serviceMap: Map = new Map(); + constructor(public readonly extensionContext: vscode.ExtensionContext, public readonly apiWrapper: ApiWrapper) { + this.apiWrapper = apiWrapper || new ApiWrapper(); + } + + public getService(serviceName: string): T { + return this.serviceMap.get(serviceName) as T; + } + + public registerService(serviceName: string, service: T): void { + this.serviceMap.set(serviceName, service); + } +} diff --git a/extensions/datavirtualization/src/cancelableStream.ts b/extensions/datavirtualization/src/cancelableStream.ts new file mode 100644 index 0000000000..3836661b02 --- /dev/null +++ b/extensions/datavirtualization/src/cancelableStream.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Transform } from 'stream'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +export class CancelableStream extends Transform { + constructor(private cancelationToken: vscode.CancellationTokenSource) { + super(); + } + + public _transform(chunk: any, encoding: string, callback: Function): void { + if (this.cancelationToken && this.cancelationToken.token.isCancellationRequested) { + callback(new Error(localize('streamCanceled', 'Stream operation canceled by the user'))); + } else { + this.push(chunk); + callback(); + } + } +} diff --git a/extensions/datavirtualization/src/command.ts b/extensions/datavirtualization/src/command.ts new file mode 100644 index 0000000000..5f1d9e0941 --- /dev/null +++ b/extensions/datavirtualization/src/command.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { ApiWrapper } from './apiWrapper'; +import { TreeNode } from './treeNodes'; +import { QuestionTypes, IPrompter, IQuestion } from './prompts/question'; +import * as utils from './utils'; +import * as constants from './constants'; +import { AppContext } from './appContext'; + +export interface ICommandContextParsingOptions { + editor: boolean; + uri: boolean; +} + +export interface ICommandBaseContext { + command: string; + editor?: vscode.TextEditor; + uri?: vscode.Uri; +} + +export interface ICommandUnknownContext extends ICommandBaseContext { + type: 'unknown'; +} + +export interface ICommandUriContext extends ICommandBaseContext { + type: 'uri'; +} + +export interface ICommandViewContext extends ICommandBaseContext { + type: 'view'; + node: TreeNode; +} + +export interface ICommandObjectExplorerContext extends ICommandBaseContext { + type: 'objectexplorer'; + explorerContext: azdata.ObjectExplorerContext; +} + +export type CommandContext = ICommandObjectExplorerContext | ICommandViewContext | ICommandUriContext | ICommandUnknownContext; + +function isTextEditor(editor: any): editor is vscode.TextEditor { + if (editor === undefined) { return false; } + + return editor.id !== undefined && ((editor as vscode.TextEditor).edit !== undefined || (editor as vscode.TextEditor).document !== undefined); +} + +export abstract class Command extends vscode.Disposable { + + + protected readonly contextParsingOptions: ICommandContextParsingOptions = { editor: false, uri: false }; + + private disposable: vscode.Disposable; + + constructor(command: string | string[], protected appContext: AppContext) { + super(() => this.dispose()); + + if (typeof command === 'string') { + this.disposable = this.apiWrapper.registerCommand(command, (...args: any[]) => this._execute(command, ...args), this); + + return; + } + + const subscriptions = command.map(cmd => this.apiWrapper.registerCommand(cmd, (...args: any[]) => this._execute(cmd, ...args), this)); + this.disposable = vscode.Disposable.from(...subscriptions); + } + + dispose(): void { + this.disposable && this.disposable.dispose(); + } + + protected get apiWrapper(): ApiWrapper { + return this.appContext.apiWrapper; + } + + protected async preExecute(context: CommandContext, ...args: any[]): Promise { + return this.execute(...args); + } + + abstract execute(...args: any[]): any; + + protected _execute(command: string, ...args: any[]): any { + // TODO consider using Telemetry.trackEvent(command); + + const [context, rest] = Command.parseContext(command, this.contextParsingOptions, ...args); + return this.preExecute(context, ...rest); + } + + private static parseContext(command: string, options: ICommandContextParsingOptions, ...args: any[]): [CommandContext, any[]] { + let editor: vscode.TextEditor | undefined = undefined; + + let firstArg = args[0]; + if (options.editor && (firstArg === undefined || isTextEditor(firstArg))) { + editor = firstArg; + args = args.slice(1); + firstArg = args[0]; + } + + if (options.uri && (firstArg === undefined || firstArg instanceof vscode.Uri)) { + const [uri, ...rest] = args as [vscode.Uri, any]; + return [{ command: command, type: 'uri', editor: editor, uri: uri }, rest]; + } + + if (firstArg instanceof TreeNode) { + const [node, ...rest] = args as [TreeNode, any]; + return [{ command: command, type: constants.ViewType, node: node }, rest]; + } + + if (firstArg && utils.isObjectExplorerContext(firstArg)) { + const [explorerContext, ...rest] = args as [azdata.ObjectExplorerContext, any]; + return [{ command: command, type: constants.ObjectExplorerService, explorerContext: explorerContext }, rest]; + } + + return [{ command: command, type: 'unknown', editor: editor }, args]; + } +} + +export abstract class ProgressCommand extends Command { + static progressId = 0; + constructor(private command: string, protected prompter: IPrompter, appContext: AppContext) { + super(command, appContext); + } + + protected async executeWithProgress( + execution: (cancelToken: vscode.CancellationTokenSource) => Promise, + label: string, + isCancelable: boolean = false, + onCanceled?: () => void + ): Promise { + let disposables: vscode.Disposable[] = []; + const tokenSource = new vscode.CancellationTokenSource(); + const statusBarItem = this.apiWrapper.createStatusBarItem(vscode.StatusBarAlignment.Left); + disposables.push(vscode.Disposable.from(statusBarItem)); + statusBarItem.text = localize('progress', '$(sync~spin) {0}...', label); + if (isCancelable) { + const cancelCommandId = `cancelProgress${ProgressCommand.progressId++}`; + disposables.push(this.apiWrapper.registerCommand(cancelCommandId, async () => { + if (await this.confirmCancel()) { + tokenSource.cancel(); + } + })); + statusBarItem.tooltip = localize('cancelTooltip', 'Cancel'); + statusBarItem.command = cancelCommandId; + } + statusBarItem.show(); + + try { + await execution(tokenSource); + } catch (error) { + if (isCancelable && onCanceled && tokenSource.token.isCancellationRequested) { + // The error can be assumed to be due to cancelation occurring. Do the callback + onCanceled(); + } else { + throw error; + } + } finally { + disposables.forEach(d => d.dispose()); + } + } + + private async confirmCancel(): Promise { + return await this.prompter.promptSingle({ + type: QuestionTypes.confirm, + message: localize('cancel', 'Cancel operation?'), + default: true + }); + } +} diff --git a/extensions/datavirtualization/src/constants.ts b/extensions/datavirtualization/src/constants.ts new file mode 100644 index 0000000000..2b8610acea --- /dev/null +++ b/extensions/datavirtualization/src/constants.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as loc from './localizedConstants'; + +// CONFIG VALUES /////////////////////////////////////////////////////////// +export const extensionConfigSectionName = 'dataManagement'; +export const sqlConfigSectionName = 'sql'; +export const configLogDebugInfo = 'logDebugInfo'; +export const configProseParsingMaxLines = 'proseParsingMaxLines'; + +// SERVICE NAMES ////////////////////////////////////////////////////////// +export const ObjectExplorerService = 'objectexplorer'; +export const ViewType = 'view'; + +export enum BuiltInCommands { + SetContext = 'setContext' +} + +export enum CommandContext { + WizardServiceEnabled = 'wizardservice:enabled' +} + +export enum MssqlClusterItems { + Connection = 'mssqlCluster:connection', + Folder = 'mssqlCluster:folder', + File = 'mssqlCluster:file', + Error = 'mssqlCluster:error' +} + +export enum HdfsItems { + Connection = 'hdfs:connection', + Folder = 'hdfs:folder', + File = 'hdfs:file', + Message = 'hdfs:message' +} + +export enum HdfsItemsSubType { + Spark = 'hdfs:spark' +} + +export enum AuthenticationType { + IntegratedAuthentication = 'Integrated', + UsernamePasswordAuthentication = 'Username Password', + SqlAuthentication = 'SqlLogin' +} + +export const serviceCrashLink = 'https://github.com/Microsoft/vscode-mssql/wiki/SqlToolsService-Known-Issues'; +export const serviceName = 'Data Virtualization Service'; +export const providerId = 'dataManagement'; +export const sqlFileExtension = 'sql'; +export const virtualizeDataCommand = 'virtualizedatawizard.cmd.open'; +export const virtualizeDataTask = 'virtualizedatawizard.task.open'; +export const mssqlHdfsTableFromFileCommand = 'mssqlHdfsTableWizard.cmd.open'; + +export const ctp24Version = 'CTP2.4'; +export const ctp25Version = 'CTP2.5'; +export const ctp3Version = 'CTP3.0'; +export const sql2019MajorVersion = 15; + +export const delimitedTextFileType = 'DELIMITEDTEXT'; + +export enum DataSourceType { + SqlServer = 'SQL Server', + Oracle = 'Oracle', + SqlHDFS = 'SqlHDFS', + MongoDb = 'MongoDB', + Teradata = 'Teradata' +} + +export const dataSourcePrefixMapping: Map = new Map([ + [DataSourceType.SqlServer, 'sqlserver://'], + [DataSourceType.Oracle, 'oracle://'], + [DataSourceType.MongoDb, 'mongodb://'], + [DataSourceType.Teradata, 'teradata://'] +]); + +export type ConnectionPageInfo = { + serverNameTitle: string, + databaseNameTitle: string, + isDbRequired: boolean +}; + +export const connectionPageInfoMapping: Map = new Map([ + [DataSourceType.SqlServer, { serverNameTitle: loc.serverNameTitle, databaseNameTitle: loc.databaseNameTitle, isDbRequired: false }], + [DataSourceType.Oracle, { serverNameTitle: loc.hostnameTitle, databaseNameTitle: loc.serviceNameTitle, isDbRequired: true }], + [DataSourceType.MongoDb, { serverNameTitle: loc.serverNameTitle, databaseNameTitle: loc.databaseNameTitle, isDbRequired: false }], + [DataSourceType.Teradata, { serverNameTitle: loc.serverNameTitle, databaseNameTitle: loc.databaseNameTitle, isDbRequired: false }] +]); + +export const proseMaxLinesDefault = 10000; diff --git a/extensions/datavirtualization/src/extension.ts b/extensions/datavirtualization/src/extension.ts new file mode 100644 index 0000000000..cdf19db1a5 --- /dev/null +++ b/extensions/datavirtualization/src/extension.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; + + +import * as constants from './constants'; +import * as utils from './utils'; + +import { ApiWrapper } from './apiWrapper'; +import { AppContext } from './appContext'; +import { DataSourceWizardService } from './services/contracts'; +import { managerInstance, ApiType } from './services/serviceApiManager'; +import { OpenVirtualizeDataWizardCommand, OpenVirtualizeDataWizardTask, OpenMssqlHdfsTableFromFileWizardCommand } from './wizards/wizardCommands'; +import { ServiceClient } from './services/serviceClient'; + +export function activate(extensionContext: vscode.ExtensionContext): void { + let apiWrapper = new ApiWrapper(); + let appContext = new AppContext(extensionContext, apiWrapper); + + let wizard = managerInstance.onRegisteredApi(ApiType.DataSourceWizard); + wizard((wizardService: DataSourceWizardService) => { + apiWrapper.setCommandContext(constants.CommandContext.WizardServiceEnabled, true); + + extensionContext.subscriptions.push(new OpenVirtualizeDataWizardCommand(appContext, wizardService)); + apiWrapper.registerTaskHandler(constants.virtualizeDataTask, (profile: azdata.IConnectionProfile) => { + new OpenVirtualizeDataWizardTask(appContext, wizardService).execute(profile); + }); + + extensionContext.subscriptions.push(new OpenMssqlHdfsTableFromFileWizardCommand(appContext, wizardService)); + }); + + const outputChannel = apiWrapper.createOutputChannel(constants.serviceName); + let serviceClient = new ServiceClient(apiWrapper, outputChannel); + serviceClient.startService(extensionContext).then(success => undefined, err => { + apiWrapper.showErrorMessage(utils.getErrorMessage(err)); + }); +} diff --git a/extensions/datavirtualization/src/fileSources.ts b/extensions/datavirtualization/src/fileSources.ts new file mode 100644 index 0000000000..e5666b5f87 --- /dev/null +++ b/extensions/datavirtualization/src/fileSources.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; + +export interface IFile { + path: string; + isDirectory: boolean; +} + +export interface IFileSource { + + enumerateFiles(path: string): Promise; + mkdir(dirName: string, remoteBasePath: string): Promise; + createReadStream(path: string): fs.ReadStream; + readFile(path: string, maxBytes?: number): Promise; + readFileLines(path: string, maxLines: number): Promise; + writeFile(localFile: IFile, remoteDir: string): Promise; + delete(path: string, recursive?: boolean): Promise; + exists(path: string): Promise; +} diff --git a/extensions/datavirtualization/src/hdfsCommands.ts b/extensions/datavirtualization/src/hdfsCommands.ts new file mode 100644 index 0000000000..804e09b2ca --- /dev/null +++ b/extensions/datavirtualization/src/hdfsCommands.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +import { ICommandViewContext, ICommandObjectExplorerContext } from './command'; +import * as constants from './constants'; +import * as LocalizedConstants from './localizedConstants'; +import { AppContext } from './appContext'; +import { TreeNode } from './treeNodes'; +import { MssqlExtensionApi } from './typings/mssqlapis'; + + +export async function getNodeFromMssqlProvider(context: ICommandViewContext | ICommandObjectExplorerContext, appContext: AppContext): Promise { + let node: T = undefined; + if (context && context.type === constants.ViewType && context.node) { + node = context.node as T; + } else if (context && context.type === constants.ObjectExplorerService) { + let extensionApi: MssqlExtensionApi = vscode.extensions.getExtension('Microsoft.mssql').exports; + let mssqlObjectExplorerBrowser = extensionApi.getMssqlObjectExplorerBrowser(); + node = await mssqlObjectExplorerBrowser.getNode(context.explorerContext); + } else { + throw new Error(LocalizedConstants.msgMissingNodeContext); + } + return node; +} diff --git a/extensions/datavirtualization/src/hdfsProvider.ts b/extensions/datavirtualization/src/hdfsProvider.ts new file mode 100644 index 0000000000..d9ab0839fe --- /dev/null +++ b/extensions/datavirtualization/src/hdfsProvider.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import * as fspath from 'path'; +import * as fs from 'fs'; +import * as Constants from './constants'; +import { IFileSource } from './fileSources'; +import { CancelableStream } from './cancelableStream'; +import { TreeNode } from './treeNodes'; +import { IFileNode } from './types'; + +export interface ITreeChangeHandler { + notifyNodeChanged(node: TreeNode): void; +} +export class TreeDataContext { + constructor(public extensionContext: vscode.ExtensionContext, public changeHandler: ITreeChangeHandler) { } +} + +export abstract class HdfsFileSourceNode extends TreeNode { + constructor(protected context: TreeDataContext, protected _path: string, protected fileSource: IFileSource) { + super(); + } + + public get hdfsPath(): string { + return this._path; + } + + public get nodePathValue(): string { + return this.getDisplayName(); + } + + getDisplayName(): string { + return fspath.basename(this._path); + } + + public async delete(recursive: boolean = false): Promise { + await this.fileSource.delete(this.hdfsPath, recursive); + // Notify parent should be updated. If at top, will return undefined which will refresh whole tree + (this.parent).onChildRemoved(); + this.context.changeHandler.notifyNodeChanged(this.parent); + } + public abstract onChildRemoved(): void; +} + +export class FileNode extends HdfsFileSourceNode implements IFileNode { + + constructor(context: TreeDataContext, path: string, fileSource: IFileSource) { + super(context, path, fileSource); + } + + public onChildRemoved(): void { + // do nothing + } + + getChildren(refreshChildren: boolean): TreeNode[] | Promise { + return []; + } + + getTreeItem(): vscode.TreeItem | Promise { + let item = new vscode.TreeItem(this.getDisplayName(), vscode.TreeItemCollapsibleState.None); + item.iconPath = { + dark: this.context.extensionContext.asAbsolutePath('resources/dark/file_inverse.svg'), + light: this.context.extensionContext.asAbsolutePath('resources/light/file.svg') + }; + item.contextValue = Constants.HdfsItems.File; + return item; + } + + + getNodeInfo(): azdata.NodeInfo { + // TODO improve node type handling so it's not tied to SQL Server types + let nodeInfo: azdata.NodeInfo = { + label: this.getDisplayName(), + isLeaf: true, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: Constants.HdfsItems.File, + nodeSubType: this.getSubType(), + iconType: 'FileGroupFile' + }; + return nodeInfo; + } + + public async getFileContentsAsString(maxBytes?: number): Promise { + let contents: Buffer = await this.fileSource.readFile(this.hdfsPath, maxBytes); + return contents ? contents.toString('utf8') : ''; + } + + public async getFileLinesAsString(maxLines: number): Promise { + let contents: Buffer = await this.fileSource.readFileLines(this.hdfsPath, maxLines); + return contents ? contents.toString('utf8') : ''; + } + + public writeFileContentsToDisk(localPath: string, cancelToken?: vscode.CancellationTokenSource): Promise { + return new Promise((resolve, reject) => { + let readStream: fs.ReadStream = this.fileSource.createReadStream(this.hdfsPath); + let writeStream = fs.createWriteStream(localPath, { + encoding: 'utf8' + }); + let cancelable = new CancelableStream(cancelToken); + cancelable.on('error', (err) => { + reject(err); + }); + readStream.pipe(cancelable).pipe(writeStream); + + let error: string | Error = undefined; + + writeStream.on('error', (err) => { + error = err; + reject(error); + }); + writeStream.on('finish', (location) => { + if (!error) { + resolve(vscode.Uri.file(localPath)); + } + }); + }); + } + + private getSubType(): string { + if (this.getDisplayName().toLowerCase().endsWith('.jar') || this.getDisplayName().toLowerCase().endsWith('.py')) { + return Constants.HdfsItemsSubType.Spark; + } + + return undefined; + } +} diff --git a/extensions/datavirtualization/src/localizedConstants.ts b/extensions/datavirtualization/src/localizedConstants.ts new file mode 100644 index 0000000000..0f5d95098a --- /dev/null +++ b/extensions/datavirtualization/src/localizedConstants.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +// General Constants /////////////////////////////////////////////////////// + +export const SqlServerName = localize('sqlServerTypeName', 'SQL Server'); + +export const msgMissingNodeContext = localize('msgMissingNodeContext', 'Node Command called without any node passed'); +// External Table +export const sourceSchemaTitle = localize('externalTable.sourceSchemaTitle', "Source Schema"); +export const sourceTableTitle = localize('externalTable.sourceTableTitle', "Source Table"); +export const externalSchemaTitle = localize('externalTable.externalSchemaTitle', "External Schema"); +export const externalTableTitle = localize('externalTable.externalTableTitle', "External Table"); +export const serverNameTitle = localize('externalTable.serverNameTitle', "Server Name"); +export const hostnameTitle = localize('externalTable.hostnameTitle', "Hostname"); +export const databaseNameTitle = localize('externalTable.databaseNameTitle', "Database Name"); +export const serviceNameTitle = localize('externalTable.serviceNameTitle', "Service name / SID"); diff --git a/extensions/datavirtualization/src/prompts/question.ts b/extensions/datavirtualization/src/prompts/question.ts new file mode 100644 index 0000000000..1c3019234a --- /dev/null +++ b/extensions/datavirtualization/src/prompts/question.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export class QuestionTypes { + public static get input(): string { return 'input'; } + public static get password(): string { return 'password'; } + public static get list(): string { return 'list'; } + public static get confirm(): string { return 'confirm'; } + public static get checkbox(): string { return 'checkbox'; } + public static get expand(): string { return 'expand'; } +} + +// Question interface to clarify how to use the prompt feature +// based on Bower Question format: https://github.com/bower/bower/blob/89069784bb46bfd6639b4a75e98a0d7399a8c2cb/packages/bower-logger/README.md +export interface IQuestion { + // Type of question (see QuestionTypes) + type: string; + // Name of the question for disambiguation + name: string; + // Message to display to the user + message: string; + // Optional placeHolder to give more detailed information to the user + placeHolder?: any; + // Optional default value - this will be used instead of placeHolder + default?: any; + // optional set of choices to be used. Can be QuickPickItems or a simple name-value pair + choices?: Array; + // Optional validation function that returns an error string if validation fails + validate?: (value: any) => string; + // Optional pre-prompt function. Takes in set of answers so far, and returns true if prompt should occur + shouldPrompt?: (answers: { [id: string]: any }) => boolean; + // Optional action to take on the question being answered + onAnswered?: (value: any) => void; + // Optional set of options to support matching choices. + matchOptions?: vscode.QuickPickOptions; +} + +// Pair used to display simple choices to the user +export interface INameValueChoice { + name: string; + value: any; +} + +// Generic object that can be used to define a set of questions and handle the result +export interface IQuestionHandler { + // Set of questions to be answered + questions: IQuestion[]; + // Optional callback, since questions may handle themselves + callback?: IPromptCallback; +} + +export interface IPrompter { + promptSingle(question: IQuestion, ignoreFocusOut?: boolean): Promise; + /** + * Prompts for multiple questions + * + * @returns {[questionId: string]: T} Map of question IDs to results, or undefined if + * the user canceled the question session + */ + prompt(questions: IQuestion[], ignoreFocusOut?: boolean): Promise<{ [questionId: string]: any }>; + promptCallback(questions: IQuestion[], callback: IPromptCallback): void; +} + +export interface IPromptCallback { + (answers: { [id: string]: any }): void; +} diff --git a/extensions/datavirtualization/src/services/contracts.ts b/extensions/datavirtualization/src/services/contracts.ts new file mode 100644 index 0000000000..450bf84589 --- /dev/null +++ b/extensions/datavirtualization/src/services/contracts.ts @@ -0,0 +1,332 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ClientCapabilities as VSClientCapabilities, RequestType, NotificationType } from 'vscode-languageclient'; +import * as types from 'dataprotocol-client/lib/types'; +import * as azdata from 'azdata'; + +/** + * @interface IMessage + */ +export interface IMessage { + jsonrpc: string; +} + +// ------------------------------- < Telemetry Sent Event > ------------------------------------ + +/** + * Event sent when the language service send a telemetry event + */ +export namespace TelemetryNotification { + export const type = new NotificationType('telemetry/sqlevent'); +} + +/** + * Update event parameters + */ +export class TelemetryParams { + public params: { + eventName: string; + properties: ITelemetryEventProperties; + measures: ITelemetryEventMeasures; + }; +} + +export interface ITelemetryEventProperties { + [key: string]: string; +} + +export interface ITelemetryEventMeasures { + [key: string]: number; +} + + +// ------------------------------- ---------------------------------- + +/* +* DataSourceWizardCreateSessionRequest +*/ +export namespace DataSourceWizardCreateSessionRequest { + export const type = new RequestType('datasourcewizard/createsession'); +} + +export interface DataSourceWizardConfigInfoResponse { + sessionId: string; + supportedSourceTypes: DataSourceType[]; + databaseList: DatabaseOverview[]; + serverMajorVersion: number; + productLevel: string; +} + +export interface DatabaseOverview { + name: string; + hasMasterKey: boolean; +} + +// Defines the important information about a type of data source - its name, configuration properties, etc. +export interface DataSourceType { + typeName: string; + authenticationTypes: string[]; +} + + +/* +* DisposeWizardSessionRequest +*/ +export namespace DisposeWizardSessionRequest { + export const type = new RequestType('datasourcewizard/disposewizardsession'); +} + + +/* +* ValidateVirtualizeDataInputRequest +*/ +export namespace ValidateVirtualizeDataInputRequest { + export const type = new RequestType('datasourcewizard/validatevirtualizedatainput'); +} + +export interface ValidateVirtualizeDataInputResponse { + isValid: boolean; + errorMessages: string[]; +} + +export interface VirtualizeDataInput { + sessionId: string; + destDatabaseName: string; + sourceServerType: string; + destDbMasterKeyPwd: string; + existingDataSourceName: string; + newDataSourceName: string; + sourceServerName: string; + sourceDatabaseName: string; + sourceAuthenticationType: string; + existingCredentialName: string; + newCredentialName: string; + sourceUsername: string; + sourcePassword: string; + externalTableInfoList: ExternalTableInfo[]; + newSchemas: string[]; +} + +export interface FileFormat { + formatName: string; + formatType: string; + fieldTerminator: string; // string token that separates columns on each line of the file + stringDelimiter: string; // string token that marks beginning/end of strings in the file + firstRow: number; +} + +export interface ExternalTableInfo { + externalTableName: string[]; + columnDefinitionList: ColumnDefinition[]; + sourceTableLocation: string[]; + fileFormat?: FileFormat; +} + +export interface ColumnDefinition { + columnName: string; + dataType: string; + collationName: string; + isNullable: boolean; + isSupported?: boolean; +} + +// TODO: All response objects for data-source-browsing request have this format, and can be formed with this generic class. +// Replace response objects with this class. +export interface ExecutionResult { + isSuccess: boolean; + returnValue: T; + errorMessages: string[]; +} + +// TODO: All parameter objects for querying list of database, list of tables, and list of column definitions have this format, +// and can be formed with this generic class. Replace parameter objects with this class for those query requests. +export interface DataSourceBrowsingParams { + virtualizeDataInput: VirtualizeDataInput; + querySubject: T; +} + +export namespace GetSourceViewListRequest { + export const type = new RequestType, ExecutionResult, void, void>('datasourcewizard/getsourceviewlist'); +} + +/* +* GetDatabaseInfoRequest +*/ +export namespace GetDatabaseInfoRequest { + export const type = new RequestType('datasourcewizard/getdatabaseinfo'); +} + +export interface GetDatabaseInfoResponse { + isSuccess: boolean; + errorMessages: string[]; + databaseInfo: DatabaseInfo; +} + +export interface DatabaseInfo { + hasMasterKey: boolean; + defaultSchema: string; + schemaList: string[]; + existingCredentials: CredentialInfo[]; + externalDataSources: DataSourceInstance[]; + externalTables: TableInfo[]; + externalFileFormats: string[]; +} + +export interface CredentialInfo { + credentialName: string; + username: string; +} + +export interface TableInfo { + schemaName: string; + tableName: string; +} + +export interface GetDatabaseInfoRequestParams { + sessionId: string; + databaseName: string; +} + + +// Defines the important information about an external data source that has already been created. +export interface DataSourceInstance { + name: string; + location: string; + authenticationType: string; + username?: string; + credentialName?: string; +} + + +/* +* ProcessVirtualizeDataInputRequest +*/ +export namespace ProcessVirtualizeDataInputRequest { + export const type = new RequestType('datasourcewizard/processvirtualizedatainput'); +} + +export interface ProcessVirtualizeDataInputResponse { + isSuccess: boolean; + errorMessages: string[]; +} + +export namespace GenerateScriptRequest { + export const type = new RequestType('datasourcewizard/generatescript'); +} + +export interface GenerateScriptResponse { + isSuccess: boolean; + errorMessages: string[]; + script: string; +} + + +/* +* GetSourceDatabasesRequest +*/ +export namespace GetSourceDatabasesRequest { + export const type = new RequestType('datasourcewizard/getsourcedatabaselist'); +} + +export interface GetSourceDatabasesResponse { + isSuccess: boolean; + errorMessages: string[]; + databaseNames: string[]; +} + + +/* +* GetSourceTablesRequest +*/ +export namespace GetSourceTablesRequest { + export const type = new RequestType('datasourcewizard/getsourcetablelist'); +} + +export interface GetSourceTablesRequestParams { + sessionId: string; + virtualizeDataInput: VirtualizeDataInput; + sourceDatabaseName: string; +} + +export interface GetSourceTablesResponse { + isSuccess: boolean; + errorMessages: string[]; + schemaTablesList: SchemaTables[]; +} + +export interface SchemaTables { + schemaName: string; + tableNames: string[]; +} + +export interface SchemaViews { + schemaName: string; + viewNames: string[]; +} + +/* +* GetSourceColumnDefinitionsRequest +*/ +export namespace GetSourceColumnDefinitionsRequest { + export const type = new RequestType('datasourcewizard/getsourcecolumndefinitionlist'); +} + +export interface GetSourceColumnDefinitionsRequestParams { + sessionId: string; + virtualizeDataInput: VirtualizeDataInput; + location: string[]; +} + +export interface GetSourceColumnDefinitionsResponse { + isSuccess: boolean; + errorMessages: string[]; + columnDefinitions: ColumnDefinition[]; +} + +/* +* Prose +*/ +export interface ColumnInfo { + name: string; + sqlType: string; + isNullable: boolean; +} + +export interface ProseDiscoveryParams { + filePath: string; + tableName: string; + schemaName?: string; + fileType?: string; + fileContents?: string; +} + +export interface ProseDiscoveryResponse { + dataPreview: string[][]; + columnInfo: ColumnInfo[]; + columnDelimiter: string; + firstRow: number; + quoteCharacter: string; +} + +export namespace ProseDiscoveryRequest { + export const type = new RequestType('flatfile/proseDiscovery'); +} + +// ------------------------------- < Data Source Wizard API definition > ------------------------------------ +export interface DataSourceWizardService { + providerId?: string; + createDataSourceWizardSession(requestParams: azdata.connection.ConnectionProfile): Thenable; + disposeWizardSession(sessionId: string): Thenable; + validateVirtualizeDataInput(requestParams: VirtualizeDataInput): Thenable; + getDatabaseInfo(requestParams: GetDatabaseInfoRequestParams): Thenable; + processVirtualizeDataInput(requestParams: VirtualizeDataInput): Thenable; + generateScript(requestParams: VirtualizeDataInput): Thenable; + getSourceDatabases(requestParams: VirtualizeDataInput): Thenable; + getSourceTables(requestParams: GetSourceTablesRequestParams): Thenable; + getSourceViewList(requestParams: DataSourceBrowsingParams): Thenable>; + getSourceColumnDefinitions(requestParams: GetSourceColumnDefinitionsRequestParams): Thenable; + sendProseDiscoveryRequest(requestParams: ProseDiscoveryParams): Thenable; +} diff --git a/extensions/datavirtualization/src/services/features.ts b/extensions/datavirtualization/src/services/features.ts new file mode 100644 index 0000000000..9c737fc364 --- /dev/null +++ b/extensions/datavirtualization/src/services/features.ts @@ -0,0 +1,189 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SqlOpsDataClient, SqlOpsFeature } from 'dataprotocol-client'; +import { ClientCapabilities, StaticFeature, RPCMessageType, ServerCapabilities } from 'vscode-languageclient'; +import * as UUID from 'vscode-languageclient/lib/utils/uuid'; +import { Disposable } from 'vscode'; +import * as azdata from 'azdata'; + +import { Telemetry } from './telemetry'; +import * as serviceUtils from './serviceUtils'; +import { + TelemetryNotification, + DataSourceWizardCreateSessionRequest, DataSourceWizardConfigInfoResponse, + DataSourceWizardService, DisposeWizardSessionRequest, VirtualizeDataInput, + ValidateVirtualizeDataInputResponse, GetDatabaseInfoRequestParams, GetDatabaseInfoResponse, + ProcessVirtualizeDataInputResponse, GenerateScriptResponse, ValidateVirtualizeDataInputRequest, + GetDatabaseInfoRequest, ProcessVirtualizeDataInputRequest, GenerateScriptRequest, GetSourceDatabasesResponse, + GetSourceDatabasesRequest, GetSourceTablesResponse, GetSourceTablesRequestParams, GetSourceTablesRequest, + GetSourceColumnDefinitionsRequestParams, GetSourceColumnDefinitionsResponse, GetSourceColumnDefinitionsRequest, + ProseDiscoveryParams, ProseDiscoveryResponse, ProseDiscoveryRequest, DataSourceBrowsingParams, ExecutionResult, GetSourceViewListRequest, SchemaViews +} from './contracts'; +import { managerInstance, ApiType } from './serviceApiManager'; +export class TelemetryFeature implements StaticFeature { + + constructor(private _client: SqlOpsDataClient) { } + + fillClientCapabilities(capabilities: ClientCapabilities): void { + serviceUtils.ensure(capabilities, 'telemetry')!.telemetry = true; + } + + initialize(): void { + this._client.onNotification(TelemetryNotification.type, e => { + Telemetry.sendTelemetryEvent(e.params.eventName, e.params.properties, e.params.measures); + }); + } +} + + +export class DataSourceWizardFeature extends SqlOpsFeature { + private static readonly messagesTypes: RPCMessageType[] = [ + DataSourceWizardCreateSessionRequest.type + ]; + + constructor(client: SqlOpsDataClient) { + super(client, DataSourceWizardFeature.messagesTypes); + } + + public fillClientCapabilities(capabilities: ClientCapabilities): void { + // ensure(ensure(capabilities, 'connection')!, 'objectExplorer')!.dynamicRegistration = true; + } + + public initialize(capabilities: ServerCapabilities): void { + this.register(this.messages, { + id: UUID.generateUuid(), + registerOptions: undefined + }); + } + + protected registerProvider(options: undefined): Disposable { + const client = this._client; + let createDataSourceWizardSession = (requestParams: azdata.connection.ConnectionProfile): Thenable => { + return client.sendRequest(DataSourceWizardCreateSessionRequest.type, requestParams).then( + r => r, + e => { + client.logFailedRequest(DataSourceWizardCreateSessionRequest.type, e); + return Promise.reject(e); + } + ); + }; + + let disposeWizardSession = (sessionId: string): Thenable => { + return client.sendRequest(DisposeWizardSessionRequest.type, sessionId).then( + r => r, + e => { + client.logFailedRequest(DisposeWizardSessionRequest.type, e); + return Promise.reject(e); + } + ); + }; + + let validateVirtualizeDataInput = (requestParams: VirtualizeDataInput): Thenable => { + return client.sendRequest(ValidateVirtualizeDataInputRequest.type, requestParams).then( + r => r, + e => { + client.logFailedRequest(ValidateVirtualizeDataInputRequest.type, e); + return Promise.reject(e); + } + ); + }; + + let getDatabaseInfo = (requestParams: GetDatabaseInfoRequestParams): Thenable => { + return client.sendRequest(GetDatabaseInfoRequest.type, requestParams).then( + r => r, + e => { + client.logFailedRequest(GetDatabaseInfoRequest.type, e); + return Promise.reject(e); + } + ); + }; + + let processVirtualizeDataInput = (requestParams: VirtualizeDataInput): Thenable => { + return client.sendRequest(ProcessVirtualizeDataInputRequest.type, requestParams).then( + r => r, + e => { + client.logFailedRequest(ProcessVirtualizeDataInputRequest.type, e); + return Promise.reject(e); + } + ); + }; + + let generateScript = (requestParams: VirtualizeDataInput): Thenable => { + return client.sendRequest(GenerateScriptRequest.type, requestParams).then( + r => r, + e => { + client.logFailedRequest(GenerateScriptRequest.type, e); + return Promise.reject(e); + } + ); + }; + + let getSourceDatabases = (requestParams: VirtualizeDataInput): Thenable => { + return client.sendRequest(GetSourceDatabasesRequest.type, requestParams).then( + r => r, + e => { + client.logFailedRequest(GetSourceDatabasesRequest.type, e); + return Promise.reject(e); + } + ); + }; + + let getSourceTables = (requestParams: GetSourceTablesRequestParams): Thenable => { + return client.sendRequest(GetSourceTablesRequest.type, requestParams).then( + r => r, + e => { + client.logFailedRequest(GetSourceTablesRequest.type, e); + return Promise.reject(e); + } + ); + }; + + let getSourceViewList = (requestParams: DataSourceBrowsingParams): Thenable> => { + return client.sendRequest(GetSourceViewListRequest.type, requestParams).then( + r => r, + e => { + client.logFailedRequest(GetSourceViewListRequest.type, e); + return Promise.reject(e); + } + ); + }; + + let getSourceColumnDefinitions = (requestParams: GetSourceColumnDefinitionsRequestParams): Thenable => { + return client.sendRequest(GetSourceColumnDefinitionsRequest.type, requestParams).then( + r => r, + e => { + client.logFailedRequest(GetSourceColumnDefinitionsRequest.type, e); + return Promise.reject(e); + } + ); + }; + + let sendProseDiscoveryRequest = (requestParams: ProseDiscoveryParams): Thenable => { + return client.sendRequest(ProseDiscoveryRequest.type, requestParams).then( + r => r, + e => { + client.logFailedRequest(ProseDiscoveryRequest.type, e); + return Promise.reject(e); + } + ); + }; + + return managerInstance.registerApi(ApiType.DataSourceWizard, { + providerId: client.providerId, + createDataSourceWizardSession, + disposeWizardSession, + validateVirtualizeDataInput, + getDatabaseInfo, + processVirtualizeDataInput, + generateScript, + getSourceDatabases, + getSourceTables, + getSourceViewList, + getSourceColumnDefinitions, + sendProseDiscoveryRequest + }); + } +} diff --git a/extensions/datavirtualization/src/services/serviceApiManager.ts b/extensions/datavirtualization/src/services/serviceApiManager.ts new file mode 100644 index 0000000000..b63208924f --- /dev/null +++ b/extensions/datavirtualization/src/services/serviceApiManager.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import { ApiWrapper } from '../apiWrapper'; + +export enum ApiType { + ExplorerProvider = 'ExplorerProvider', + DataSourceWizard = 'DataSourceWizard' +} + +export interface IServiceApi { + onRegisteredApi(type: ApiType): vscode.Event; + registerApi(type: ApiType, feature: T): vscode.Disposable; +} + +export interface IModelViewDefinition { + id: string; + modelView: azdata.ModelView; +} + +class ServiceApiManager implements IServiceApi { + private modelViewRegistrations: { [id: string]: boolean } = {}; + private featureEventChannels: { [type: string]: vscode.EventEmitter } = {}; + private _onRegisteredModelView = new vscode.EventEmitter(); + + public onRegisteredApi(type: ApiType): vscode.Event { + let featureEmitter = this.featureEventChannels[type]; + if (!featureEmitter) { + featureEmitter = new vscode.EventEmitter(); + this.featureEventChannels[type] = featureEmitter; + } + return featureEmitter.event; + } + + public registerApi(type: ApiType, feature: T): vscode.Disposable { + let featureEmitter = this.featureEventChannels[type]; + if (featureEmitter) { + featureEmitter.fire(feature); + } + // TODO handle unregistering API on close + return { + dispose: () => undefined + }; + } + + public get onRegisteredModelView(): vscode.Event { + return this._onRegisteredModelView.event; + } + + public registerModelView(id: string, modelView: azdata.ModelView): void { + this._onRegisteredModelView.fire({ + id: id, + modelView: modelView + }); + } + + /** + * Performs a one-time registration of a model view provider, where this will be + * hooked to an event handler instead of having a predefined method that uses the model view + */ + public ensureModelViewRegistered(id: string, apiWrapper: ApiWrapper): any { + if (!this.modelViewRegistrations[id]) { + apiWrapper.registerModelViewProvider(id, (modelView) => { + this.registerModelView(id, modelView); + }); + this.modelViewRegistrations[id] = true; + } + } +} + +export let managerInstance = new ServiceApiManager(); diff --git a/extensions/datavirtualization/src/services/serviceClient.ts b/extensions/datavirtualization/src/services/serviceClient.ts new file mode 100644 index 0000000000..0a3d65f0e0 --- /dev/null +++ b/extensions/datavirtualization/src/services/serviceClient.ts @@ -0,0 +1,171 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SqlOpsDataClient, ClientOptions } from 'dataprotocol-client'; +import { IConfig, ServerProvider, Events, LogLevel } from '@microsoft/ads-service-downloader'; +import { ServerOptions, TransportKind } from 'vscode-languageclient'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); +import * as path from 'path'; +import { EventAndListener } from 'eventemitter2'; + +import { Telemetry, LanguageClientErrorHandler } from './telemetry'; +import { ApiWrapper } from '../apiWrapper'; +import * as Constants from '../constants'; +import { TelemetryFeature, DataSourceWizardFeature } from './features'; +import { promises as fs } from 'fs'; + +export class ServiceClient { + private statusView: vscode.StatusBarItem; + + constructor(private apiWrapper: ApiWrapper, private outputChannel: vscode.OutputChannel) { + this.statusView = this.apiWrapper.createStatusBarItem(vscode.StatusBarAlignment.Left); + } + + public async startService(context: vscode.ExtensionContext): Promise { + const rawConfig = await fs.readFile(path.join(context.extensionPath, 'config.json')); + const config: IConfig = JSON.parse(rawConfig.toString()); + config.installDirectory = path.join(context.extensionPath, config.installDirectory); + config.proxy = this.apiWrapper.getConfiguration('http').get('proxy'); + config.strictSSL = this.apiWrapper.getConfiguration('http').get('proxyStrictSSL') || true; + + const serverdownloader = new ServerProvider(config); + serverdownloader.eventEmitter.onAny(this.generateHandleServerProviderEvent()); + + let clientOptions: ClientOptions = this.createClientOptions(); + + const installationStart = Date.now(); + let client: SqlOpsDataClient; + return new Promise((resolve, reject) => { + serverdownloader.getOrDownloadServer().then(e => { + const installationComplete = Date.now(); + let serverOptions = this.generateServerOptions(e, context); + client = new SqlOpsDataClient(Constants.serviceName, serverOptions, clientOptions); + const processStart = Date.now(); + client.onReady().then(() => { + const processEnd = Date.now(); + this.statusView.text = localize('serviceStarted', 'Service Started'); + setTimeout(() => { + this.statusView.hide(); + }, 1500); + Telemetry.sendTelemetryEvent('startup/LanguageClientStarted', { + installationTime: String(installationComplete - installationStart), + processStartupTime: String(processEnd - processStart), + totalTime: String(processEnd - installationStart), + beginningTimestamp: String(installationStart) + }); + }); + this.statusView.show(); + this.statusView.text = localize('serviceStarting', 'Starting service'); + let disposable = client.start(); + context.subscriptions.push(disposable); + resolve(); + }, e => { + Telemetry.sendTelemetryEvent('ServiceInitializingFailed'); + this.apiWrapper.showErrorMessage(localize('serviceStartFailed', 'Failed to start Scale Out Data service:{0}', e)); + // Just resolve to avoid unhandled promise. We show the error to the user. + resolve(); + }); + }); + } + + + private createClientOptions(): ClientOptions { + return { + providerId: Constants.providerId, + errorHandler: new LanguageClientErrorHandler(), + synchronize: { + configurationSection: [Constants.extensionConfigSectionName, Constants.sqlConfigSectionName] + }, + features: [ + // we only want to add new features + TelemetryFeature, + DataSourceWizardFeature + ], + outputChannel: new CustomOutputChannel() + }; + } + + private generateServerOptions(executablePath: string, context: vscode.ExtensionContext): ServerOptions { + let launchArgs = []; + launchArgs.push('--log-dir'); + let logFileLocation = context['logPath']; + launchArgs.push(logFileLocation); + let config = vscode.workspace.getConfiguration(Constants.extensionConfigSectionName); + if (config) { + let logDebugInfo = config[Constants.configLogDebugInfo]; + if (logDebugInfo) { + launchArgs.push('--enable-logging'); + } + } + + return { command: executablePath, args: launchArgs, transport: TransportKind.stdio }; + } + + private generateHandleServerProviderEvent(): EventAndListener { + let dots = 0; + return (e: string, ...args: any[]) => { + switch (e) { + case Events.INSTALL_START: + this.outputChannel.show(true); + this.statusView.show(); + this.outputChannel.appendLine(localize('installingServiceDetailed', "Installing {0} to {1}", Constants.serviceName, args[0])); + this.statusView.text = localize('installingService', "Installing {0}", Constants.serviceName); + break; + case Events.INSTALL_END: + this.outputChannel.appendLine(localize('serviceInstalled', "Installed {0}", Constants.serviceName)); + break; + case Events.DOWNLOAD_START: + this.outputChannel.appendLine(localize('downloadingService', "Downloading {0}", args[0])); + this.outputChannel.append(localize('downloadingServiceSize', "({0} KB)", Math.ceil(args[1] / 1024).toLocaleString(vscode.env.language))); + this.statusView.text = localize('downloadingServiceStatus', "Downloading {0}", Constants.serviceName); + break; + case Events.DOWNLOAD_PROGRESS: + let newDots = Math.ceil(args[0] / 5); + if (newDots > dots) { + this.outputChannel.append('.'.repeat(newDots - dots)); + dots = newDots; + } + break; + case Events.DOWNLOAD_END: + this.outputChannel.appendLine(localize('downloadingServiceComplete', "Done downloading {0}", Constants.serviceName)); + break; + case Events.LOG_EMITTED: + if (args[0] >= LogLevel.Warning) { + this.outputChannel.appendLine(args[1]); + } + break; + default: + console.error(`Unknown event from Server Provider ${e}`); + break; + } + }; + } +} + +class CustomOutputChannel implements vscode.OutputChannel { + name: string; + append(value: string): void { + console.log(value); + } + appendLine(value: string): void { + console.log(value); + } + clear(): void { + } + show(preserveFocus?: boolean): void; + show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; + show(column?: any, preserveFocus?: any): void { + } + hide(): void { + } + dispose(): void { + } + replace(value: string): void { + throw new Error('Method not implemented.'); + } +} + diff --git a/extensions/datavirtualization/src/services/serviceUtils.ts b/extensions/datavirtualization/src/services/serviceUtils.ts new file mode 100644 index 0000000000..6d02dba185 --- /dev/null +++ b/extensions/datavirtualization/src/services/serviceUtils.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as crypto from 'crypto'; +import * as os from 'os'; + +export function ensure(target: object, key: string): any { + if (target[key] === void 0) { + target[key] = {} as any; + } + return target[key]; +} + +export function generateUserId(): Promise { + return new Promise(resolve => { + try { + let interfaces = os.networkInterfaces(); + let mac; + for (let key of Object.keys(interfaces)) { + let item = interfaces[key][0]; + if (!item.internal) { + mac = item.mac; + break; + } + } + if (mac) { + resolve(crypto.createHash('sha256').update(mac + os.homedir(), 'utf8').digest('hex')); + } else { + resolve(generateGuid()); + } + } catch (err) { + resolve(generateGuid()); // fallback + } + }); +} + +export function generateGuid(): string { + let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; + // c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx) + let oct: string = ''; + let tmp: number; + /* tslint:disable:no-bitwise */ + for (let a: number = 0; a < 4; a++) { + tmp = (4294967296 * Math.random()) | 0; + oct += hexValues[tmp & 0xF] + + hexValues[tmp >> 4 & 0xF] + + hexValues[tmp >> 8 & 0xF] + + hexValues[tmp >> 12 & 0xF] + + hexValues[tmp >> 16 & 0xF] + + hexValues[tmp >> 20 & 0xF] + + hexValues[tmp >> 24 & 0xF] + + hexValues[tmp >> 28 & 0xF]; + } + + // 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively' + let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0]; + return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12); + /* tslint:enable:no-bitwise */ +} diff --git a/extensions/datavirtualization/src/services/telemetry.ts b/extensions/datavirtualization/src/services/telemetry.ts new file mode 100644 index 0000000000..df8c594fee --- /dev/null +++ b/extensions/datavirtualization/src/services/telemetry.ts @@ -0,0 +1,216 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ErrorAction, CloseAction } from 'vscode-languageclient'; +import TelemetryReporter from '@microsoft/ads-extension-telemetry'; +import { PlatformInformation } from '@microsoft/ads-service-downloader/out/platform'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { ApiWrapper } from '../apiWrapper'; +import * as constants from '../constants'; +import * as serviceUtils from './serviceUtils'; +import { IMessage, ITelemetryEventProperties, ITelemetryEventMeasures } from './contracts'; + + +/** + * Handle Language Service client errors + * @class LanguageClientErrorHandler + */ +export class LanguageClientErrorHandler { + + /** + * Creates an instance of LanguageClientErrorHandler. + * @memberOf LanguageClientErrorHandler + */ + constructor(private apiWrapper?: ApiWrapper) { + if (!this.apiWrapper) { + this.apiWrapper = new ApiWrapper(); + } + } + + /** + * Show an error message prompt with a link to known issues wiki page + * @memberOf LanguageClientErrorHandler + */ + showOnErrorPrompt(): void { + // TODO add telemetry + // Telemetry.sendTelemetryEvent('SqlToolsServiceCrash'); + let crashButtonText = localize('serviceCrashButton', 'View Known Issues'); + this.apiWrapper.showErrorMessage( + localize('serviceCrashMessage', 'service component could not start'), + crashButtonText + ).then(action => { + if (action && action === crashButtonText) { + vscode.env.openExternal(vscode.Uri.parse(constants.serviceCrashLink)); + } + }); + } + + /** + * Callback for language service client error + * + * @param error + * @param message + * @param count + * @returns + * + * @memberOf LanguageClientErrorHandler + */ + error(error: Error, message: IMessage, count: number): ErrorAction { + this.showOnErrorPrompt(); + + // we don't retry running the service since crashes leave the extension + // in a bad, unrecovered state + return ErrorAction.Shutdown; + } + + /** + * Callback for language service client closed + * + * @returns + * + * @memberOf LanguageClientErrorHandler + */ + closed(): CloseAction { + this.showOnErrorPrompt(); + + // we don't retry running the service since crashes leave the extension + // in a bad, unrecovered state + return CloseAction.DoNotRestart; + } +} + +/** + * Filters error paths to only include source files. Exported to support testing + */ +export function FilterErrorPath(line: string): string { + if (line) { + let values: string[] = line.split('/out/'); + if (values.length <= 1) { + // Didn't match expected format + return line; + } else { + return values[1]; + } + } +} + +export class Telemetry { + private static reporter: TelemetryReporter; + private static userId: string; + private static platformInformation: PlatformInformation; + private static disabled: boolean; + + // Get the unique ID for the current user of the extension + public static getUserId(): Promise { + return new Promise(resolve => { + // Generate the user id if it has not been created already + if (typeof this.userId === 'undefined') { + let id = serviceUtils.generateUserId(); + id.then(newId => { + this.userId = newId; + resolve(this.userId); + }); + } else { + resolve(this.userId); + } + }); + } + + public static getPlatformInformation(): Promise { + if (this.platformInformation) { + return Promise.resolve(this.platformInformation); + } else { + return new Promise(resolve => { + PlatformInformation.getCurrent().then(info => { + this.platformInformation = info; + resolve(this.platformInformation); + }); + }); + } + } + + /** + * Disable telemetry reporting + */ + public static disable(): void { + this.disabled = true; + } + + /** + * Initialize the telemetry reporter for use. + */ + public static initialize(): void { + if (typeof this.reporter === 'undefined') { + // Check if the user has opted out of telemetry + if (!vscode.workspace.getConfiguration('telemetry').get('enableTelemetry', true)) { + this.disable(); + return; + } + let packageInfo = vscode.extensions.getExtension('Microsoft.datavirtualization').packageJSON; + this.reporter = new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); + } + } + + /** + * Send a telemetry event for an exception + */ + public static sendTelemetryEventForException( + err: any, methodName: string, extensionConfigName: string): void { + try { + let stackArray: string[]; + let firstLine: string = ''; + if (err !== undefined && err.stack !== undefined) { + stackArray = err.stack.split('\n'); + if (stackArray !== undefined && stackArray.length >= 2) { + firstLine = stackArray[1]; // The fist line is the error message and we don't want to send that telemetry event + firstLine = FilterErrorPath(firstLine); + } + } + + // Only adding the method name and the fist line of the stack trace. We don't add the error message because it might have PII + this.sendTelemetryEvent('Exception', { methodName: methodName, errorLine: firstLine }); + // Utils.logDebug('Unhandled Exception occurred. error: ' + err + ' method: ' + methodName, extensionConfigName); + } catch (telemetryErr) { + // If sending telemetry event fails ignore it so it won't break the extension + // Utils.logDebug('Failed to send telemetry event. error: ' + telemetryErr, extensionConfigName); + } + } + + /** + * Send a telemetry event using application insights + */ + public static sendTelemetryEvent( + eventName: string, + properties?: ITelemetryEventProperties, + measures?: ITelemetryEventMeasures): void { + + if (typeof this.disabled === 'undefined') { + this.disabled = false; + } + + if (this.disabled || typeof (this.reporter) === 'undefined') { + // Don't do anything if telemetry is disabled + return; + } + + if (!properties || typeof properties === 'undefined') { + properties = {}; + } + + // Augment the properties structure with additional common properties before sending + Promise.all([this.getUserId(), this.getPlatformInformation()]).then(() => { + properties['userId'] = this.userId; + properties['distribution'] = (this.platformInformation && this.platformInformation.distribution) ? + `${this.platformInformation.distribution.name}, ${this.platformInformation.distribution.version}` : ''; + + this.reporter.sendTelemetryEvent(eventName, properties, measures); + }); + } +} + +Telemetry.initialize(); diff --git a/extensions/datavirtualization/src/test/index.ts b/extensions/datavirtualization/src/test/index.ts new file mode 100644 index 0000000000..5090ffff4e --- /dev/null +++ b/extensions/datavirtualization/src/test/index.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as IstanbulTestRunner from '@microsoft/vscodetestcover'; +let testRunner: any = IstanbulTestRunner; + +// You can directly control Mocha options by uncommenting the following lines +// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info +testRunner.configure( + // Mocha Options + { + ui: 'bdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) + reporter: 'pm-mocha-jenkins-reporter', + reporterOptions: { + junit_report_name: 'Extension Tests', + junit_report_path: __dirname + '/../../test-reports/extension_tests.xml', + junit_report_stack: 1 + }, + useColors: true // colored output from test results + }, + // Coverage configuration options + { + coverConfig: '../../coverconfig.json' + }); + +module.exports = testRunner; diff --git a/extensions/datavirtualization/src/test/mockFileSource.ts b/extensions/datavirtualization/src/test/mockFileSource.ts new file mode 100644 index 0000000000..29573a3487 --- /dev/null +++ b/extensions/datavirtualization/src/test/mockFileSource.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; + +import { IFileSource, IFile } from '../fileSources'; + +export class MockFileSource implements IFileSource { + filesToReturn: Map; + constructor() { + this.filesToReturn = new Map(); + } + enumerateFiles(filePath: string): Promise { + let files: IFile[] = this.filesToReturn.get(filePath); + return Promise.resolve(files); + } + + mkdir(dirName: string, remoteBasePath: string): Promise { + return Promise.resolve(undefined); + } + + writeFile(localFile: IFile, remoteDir: string): Promise { + return Promise.resolve(undefined); + } + + delete(filePath: string): Promise { + throw new Error('Method not implemented.'); + } + + readFile(filePath: string, maxBytes?: number): Promise { + throw new Error('Method not implemented.'); + } + + readFileLines(path: string, maxLines: number): Promise { + throw new Error("Method not implemented."); + } + + createReadStream(filePath: string): fs.ReadStream { + throw new Error('Method not implemented.'); + } + + exists(filePath: string): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/extensions/datavirtualization/src/test/stubs.ts b/extensions/datavirtualization/src/test/stubs.ts new file mode 100644 index 0000000000..d8c46203ac --- /dev/null +++ b/extensions/datavirtualization/src/test/stubs.ts @@ -0,0 +1,1263 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; +import * as TypeMoq from 'typemoq'; + +import { IPrompter, IQuestion, IPromptCallback } from '../prompts/question'; +import { CheckboxTreeNode } from '../wizards/virtualizeData/virtualizeDataTree'; +import { ObjectMappingPage } from '../wizards/virtualizeData/objectMappingPage'; +import { ConnectionDetailsPage } from '../wizards/virtualizeData/connectionDetailsPage'; +import { CreateMasterKeyPage, MasterKeyUiElements } from '../wizards/virtualizeData/createMasterKeyPage'; +import { SelectDataSourcePage } from '../wizards/virtualizeData/selectDataSourcePage'; +import { + DataSourceWizardService, DataSourceWizardConfigInfoResponse, VirtualizeDataInput, + ValidateVirtualizeDataInputResponse, GetDatabaseInfoResponse, GetDatabaseInfoRequestParams, + ProcessVirtualizeDataInputResponse, GenerateScriptResponse, GetSourceDatabasesResponse, GetSourceTablesRequestParams, + GetSourceTablesResponse, GetSourceColumnDefinitionsRequestParams, GetSourceColumnDefinitionsResponse, + ColumnDefinition, ProseDiscoveryParams, ProseDiscoveryResponse, ExternalTableInfo, DataSourceType, SchemaTables, + ExecutionResult, DataSourceBrowsingParams, SchemaViews, DatabaseOverview +} from '../services/contracts'; +import { VirtualizeDataModel } from '../wizards/virtualizeData/virtualizeDataModel'; +import { AppContext } from '../appContext'; +import { ApiWrapper } from '../apiWrapper'; +import { VDIManager } from '../wizards/virtualizeData/virtualizeDataInputManager'; +import { VirtualizeDataWizard } from '../wizards/virtualizeData/virtualizeDataWizard'; + +// Dummy implementation to simplify mocking +export class TestPrompter implements IPrompter { + public promptSingle(question: IQuestion): Promise { + return Promise.resolve(undefined); + } + public prompt(questions: IQuestion[]): Promise<{ [key: string]: T }> { + return Promise.resolve(undefined); + } + public promptCallback(questions: IQuestion[], callback: IPromptCallback): void { + callback({}); + } +} + +export class MockExtensionContext implements vscode.ExtensionContext { + logger: undefined; + logDirectory: './'; + subscriptions: { dispose(): any; }[]; + workspaceState: vscode.Memento; + globalState: vscode.Memento & { setKeysForSync(keys: readonly string[]): void; }; + extensionPath: string; + asAbsolutePath(relativePath: string): string { + return relativePath; + } + storagePath: string; + + constructor() { + this.subscriptions = []; + } + secrets: vscode.SecretStorage; + extension: vscode.Extension; + extensionUri: vscode.Uri; + environmentVariableCollection: vscode.EnvironmentVariableCollection; + storageUri: vscode.Uri; + globalStorageUri: vscode.Uri; + globalStoragePath: string; + logUri: vscode.Uri; + logPath: string; + extensionMode: vscode.ExtensionMode; +} + +export class MockWizard implements azdata.window.Wizard { + displayPageTitles: boolean; + title: string; + pages: azdata.window.WizardPage[]; + currentPage: number; + doneButton: azdata.window.Button; + cancelButton: azdata.window.Button; + generateScriptButton: azdata.window.Button; + nextButton: azdata.window.Button; + backButton: azdata.window.Button; + customButtons: azdata.window.Button[]; + onPageChanged: vscode.Event; + message: azdata.window.DialogMessage; + + backgroundOpRegistered: boolean; + + addPage(page: azdata.window.WizardPage, index?: number): Thenable { + throw new Error('Method not implemented.'); + } + removePage(index: number): Thenable { + throw new Error('Method not implemented.'); + } + setCurrentPage(index: number): Thenable { + throw new Error('Method not implemented.'); + } + open(): Thenable { + throw new Error('Method not implemented.'); + } + close(): Thenable { + throw new Error('Method not implemented.'); + } + registerNavigationValidator(validator: (pageChangeInfo: azdata.window.WizardPageChangeInfo) => boolean | Thenable): void { + throw new Error('Method not implemented.'); + } + registerOperation(operationInfo: azdata.BackgroundOperationInfo): void { + throw new Error('Method not implemented.'); + } +} + +export class MockWizardPage implements azdata.window.WizardPage { + title: string; + content: string; + customButtons: azdata.window.Button[]; + enabled: boolean; + description: string; + modelView: azdata.ModelView; + valid: boolean; + onValidityChanged: vscode.Event; + + registerContent(handler: (view: azdata.ModelView) => void): void { + throw new Error('Method not implemented.'); + } +} + +export class MockDataSourceService implements DataSourceWizardService { + providerId?: string; + + createDataSourceWizardSession(requestParams: azdata.connection.ConnectionProfile): Thenable { + return Promise.resolve({ + sessionId: 'TestSessionId', + supportedSourceTypes: [{ + typeName: 'SQL Server', + authenticationTypes: ['SqlLogin'] + }], + databaseList: [{ name: 'TestDb', hasMasterKey: false }], + serverMajorVersion: 15, + productLevel: 'CTP3.1' + }); + } + + disposeWizardSession(sessionId: string): Thenable { + return Promise.resolve(true); + } + + validateVirtualizeDataInput(requestParams: VirtualizeDataInput): Thenable { + throw new Error('Method not implemented.'); + } + + getDatabaseInfo(requestParams: GetDatabaseInfoRequestParams): Thenable { + return Promise.resolve({ + isSuccess: true, + errorMessages: undefined, + databaseInfo: { + hasMasterKey: false, + defaultSchema: 'TestSchema', + schemaList: ['TestSchema'], + existingCredentials: undefined, + externalDataSources: [{ + name: 'TestSource', + location: 'sqlhdfs://controller-svc/default/', + authenticationType: undefined, + username: undefined + }], + externalTables: [{ + schemaName: 'TestSchema', + tableName: 'TestExternalTable' + }], + externalFileFormats: ['TestExternalFileFormat'], + } + }); + } + + processVirtualizeDataInput(virtualizeDataInput: VirtualizeDataInput): Thenable { + return Promise.resolve({ + isSuccess: true, + errorMessages: [] + }); + } + + generateScript(requestParams: VirtualizeDataInput): Thenable { + throw new Error('Method not implemented.'); + } + + getSourceDatabases(requestParams: VirtualizeDataInput): Thenable { + throw new Error('Method not implemented.'); + } + + getSourceTables(requestParams: GetSourceTablesRequestParams): Thenable { + throw new Error('Method not implemented.'); + } + + getSourceViewList(requestParams: DataSourceBrowsingParams): Thenable> { + throw new Error("Method not implemented."); + } + + getSourceColumnDefinitions(requestParams: GetSourceColumnDefinitionsRequestParams): Thenable { + throw new Error('Method not implemented.'); + } + + getSourceOriginalColumnDefinitions(requestParams: GetSourceColumnDefinitionsRequestParams): Thenable { + throw new Error("Method not implemented."); + } + + public readonly proseTestData: string = 'TestId, TestStr\n1, abc'; + sendProseDiscoveryRequest(requestParams: ProseDiscoveryParams): Thenable { + return Promise.resolve({ + dataPreview: [['1', 'abc']], + columnInfo: [{ + name: 'TestId', + sqlType: 'int', + isNullable: false + }, { + name: 'TestStr', + sqlType: 'varchar(50)', + isNullable: false + }], + columnDelimiter: ',', + firstRow: 2, + quoteCharacter: '"' + }); + } +} + +export class MockUIComponent implements azdata.Component { + id: string; + enabled: boolean; + onValidityChanged: vscode.Event; + valid: boolean; + validate(): Thenable { + return Promise.resolve(undefined); + } + updateProperties(properties: { [key: string]: any }): Thenable { + Object.assign(this, properties); + return Promise.resolve(); + } + updateProperty(key: string, value: any): Thenable { + return Promise.resolve(); + } + updateCssStyles(cssStyles: { [key: string]: string }): Thenable { + Object.assign('CSSStyles', cssStyles); + return Promise.resolve(); + } + focus(): Thenable { + return Promise.resolve(); + } +} + +export class MockInputBoxComponent extends MockUIComponent implements azdata.InputBoxComponent { + onEnterKeyPressed: vscode.Event; + value?: string; + ariaLabel?: string; + ariaLive?: string; + placeHolder?: string; + inputType?: azdata.InputBoxInputType; + required?: boolean; + multiline?: boolean; + rows?: number; + columns?: number; + min?: number; + max?: number; + stopEnterPropagation?: boolean; + height?: string | number; + width?: string | number; + position?: azdata.PositionType; + CSSStyles?: { [key: string]: string; }; + onTextChanged: vscode.Event; +} + +export class MockDropdownComponent extends MockUIComponent implements azdata.DropDownComponent { + onValueChanged: vscode.Event; + value: string | azdata.CategoryValue; + values: string[] | azdata.CategoryValue[]; + editable?: boolean; + height?: number | string; + width?: number | string; +} + +export class MockTableComponent extends MockUIComponent implements azdata.TableComponent { + display?: azdata.DisplayType; + ariaLabel?: string; + ariaSelected?: boolean; + ariaHidden?: boolean; + updateCells?: azdata.TableCell[]; + headerFilter?: boolean; + onRowSelected: vscode.Event; + onCellAction?: vscode.Event; + data: any[][]; + columns: string[] | azdata.TableColumn[]; + fontSize?: string | number; + selectedRows?: number[]; + forceFitColumns?: azdata.ColumnSizingMode; + title?: string; + ariaRowCount?: number; + ariaColumnCount?: number; + ariaRole?: string; + focused?: boolean; + moveFocusOutWithTab?: boolean; + height?: string | number; + width?: string | number; + position?: azdata.PositionType; + CSSStyles?: { [key: string]: string; }; + appendData(data: any[][]): Thenable { + throw new Error('Method not implemented.'); + } +} + +export class MockDeclarativeTableComponent extends MockUIComponent implements azdata.DeclarativeTableComponent { + enableRowSelection?: boolean; + selectedRow?: number; + onRowSelected: vscode.Event; + position?: azdata.PositionType; + display?: azdata.DisplayType; + ariaLabel?: string; + ariaRole?: string; + ariaSelected?: boolean; + CSSStyles?: { [key: string]: string; }; + ariaHidden?: boolean; + dataValues?: azdata.DeclarativeTableCellValue[][]; + selectEffect?: boolean; + onDataChanged: vscode.Event; + data: any[][]; + columns: azdata.DeclarativeTableColumn[]; + height?: number | string; + width?: number | string; + setFilter(rowIndexes: number[]): void { + throw new Error('Method not implemented.'); + } + setDataValues(v: azdata.DeclarativeTableCellValue[][]): Promise { + throw new Error('Method not implemented.'); + } +} + +export class MockTreeComponent extends MockUIComponent implements azdata.TreeComponent { + withCheckbox: boolean; + height?: number | string; + width?: number | string; + registerDataProvider(dataProvider: azdata.TreeComponentDataProvider): azdata.TreeComponentView { + return new MockTreeComponentView(() => { }) as azdata.TreeComponentView; + } +} + +export class MockTreeComponentView extends vscode.Disposable implements azdata.TreeComponentView { + onCheckChangedEmitter = new vscode.EventEmitter>(); + onNodeCheckedChanged: vscode.Event> = this.onCheckChangedEmitter.event; + onDidChangeSelectionEmitter = new vscode.EventEmitter>(); + onDidChangeSelection: vscode.Event> = this.onDidChangeSelectionEmitter.event; +} + +export class MockTextComponent extends MockUIComponent implements azdata.TextComponent { + value: string; + id: string; + enabled: boolean; + onValidityChanged: vscode.Event; + valid: boolean; + onDidClick: vscode.Event; +} + +export class MockContainer extends MockUIComponent implements azdata.Container { + setItemLayout(component: azdata.Component, layout: TItemLayout): void { + throw new Error('Method not implemented.'); + } + height?: string | number; + width?: string | number; + position?: azdata.PositionType; + display?: azdata.DisplayType; + ariaLabel?: string; + ariaRole?: string; + ariaSelected?: boolean; + CSSStyles?: { [key: string]: string; }; + ariaHidden?: boolean; + items: azdata.Component[] = []; + clearItems(): void { + this.items = []; + } + addItems(itemConfigs: azdata.Component[], itemLayout?: any): void { + this.items.push(...itemConfigs); + } + addItem(component: azdata.Component, itemLayout?: any): void { + this.items.push(component); + } + setLayout(layout: any): void { + // Do nothing. + } + insertItem(component: azdata.Component, index: number, itemLayout?: TItemLayout): void { + throw new Error('Method not implemented.'); + } + removeItem(component: azdata.Component): boolean { + throw new Error('Method not implemented.'); + } +} +export class MockToolbarContainer extends MockContainer implements azdata.ToolbarContainer { + setItemLayout(component: azdata.Component, layout: any): void { + throw new Error('Method not implemented.'); + } + height?: string | number; + width?: string | number; + position?: azdata.PositionType; + display?: azdata.DisplayType; + ariaLabel?: string; + ariaRole?: string; + ariaSelected?: boolean; + CSSStyles?: { [key: string]: string; }; + ariaHidden?: boolean; +} + +export class MockDivContainer extends MockContainer implements azdata.DivContainer { + setItemLayout(component: azdata.Component, layout: azdata.DivItemLayout): void { + throw new Error('Method not implemented.'); + } + height?: string | number; + width?: string | number; + position?: azdata.PositionType; + display?: azdata.DisplayType; + ariaLabel?: string; + ariaRole?: string; + ariaSelected?: boolean; + CSSStyles?: { [key: string]: string; }; + ariaHidden?: boolean; + overflowY?: string; + yOffsetChange?: number; + clickable?: boolean; + onDidClick: vscode.Event; +} + +export class MockFlexContainer extends MockContainer implements azdata.FlexContainer { + setItemLayout(component: azdata.Component, layout: azdata.FlexItemLayout): void { + throw new Error('Method not implemented.'); + } + height?: string | number; + width?: string | number; + position?: azdata.PositionType; + display?: azdata.DisplayType; + ariaLabel?: string; + ariaRole?: string; + ariaSelected?: boolean; + CSSStyles?: { [key: string]: string; }; + ariaHidden?: boolean; +} + +export class MockFormContainer extends MockContainer implements azdata.FormContainer { + setItemLayout(component: azdata.Component, layout: azdata.FormItemLayout): void { + throw new Error('Method not implemented.'); + } + height?: string | number; + width?: string | number; + position?: azdata.PositionType; + display?: azdata.DisplayType; + ariaLabel?: string; + ariaRole?: string; + ariaSelected?: boolean; + CSSStyles?: { [key: string]: string; }; + ariaHidden?: boolean; +} + +export class MockLoadingComponent extends MockUIComponent implements azdata.LoadingComponent { + loading: boolean; + component: azdata.Component; +} + +export class MockComponentBuilder implements azdata.ComponentBuilder { + public properties: any; + constructor(private _component?: azdata.Component) { } + withProps(properties: any): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + component(): any { + return this._component; + } + + withProperties(properties: U): azdata.ComponentBuilder { + this.properties = properties; + return this; + } + + withValidation(validation: (component: any) => boolean): azdata.ComponentBuilder { + return this; + } +} + +export class MockModelBuilder implements azdata.ModelBuilder { + infoBox(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + listView(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + slider(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + propertiesContainer(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + splitViewContainer(): azdata.SplitViewBuilder { + throw new Error("Method not implemented."); + } + diffeditor(): azdata.ComponentBuilder { + throw new Error("Method not implemented."); + } + hyperlink(): azdata.ComponentBuilder { + throw new Error("Method not implemented."); + } + navContainer(): azdata.ContainerBuilder { + throw new Error('Method not implemented.'); + } + divContainer(): azdata.DivBuilder { + throw new Error('Method not implemented.'); + } + flexContainer(): azdata.FlexBuilder { + throw new Error('Method not implemented.'); + } + card(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + inputBox(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + checkBox(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + radioButton(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + webView(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + editor(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + text(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + image(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + button(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + dropDown(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + tree(): azdata.ComponentBuilder, any> { + return new MockComponentBuilder(new MockTreeComponent()); + } + listBox(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + table(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + declarativeTable(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + dashboardWidget(widgetId: string): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + dashboardWebview(webviewId: string): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + formContainer(): azdata.FormBuilder { + throw new Error('Method not implemented.'); + } + groupContainer(): azdata.GroupBuilder { + throw new Error('Method not implemented.'); + } + toolbarContainer(): azdata.ToolbarBuilder { + throw new Error('Method not implemented.'); + } + loadingComponent(): azdata.LoadingComponentBuilder { + throw new Error('Method not implemented.'); + } + fileBrowserTree(): azdata.ComponentBuilder { + throw new Error('Method not implemented.'); + } + radioCardGroup(): azdata.ComponentBuilder { + throw new Error('Method not implemented'); + } + tabbedPanel(): azdata.TabbedPanelComponentBuilder { + throw new Error('Method not implemented'); + } + separator(): azdata.ComponentBuilder { + throw new Error('Method not implemented'); + } +} + +export class MockModelViewEditor implements azdata.workspace.ModelViewEditor { + contentHandler: (view: azdata.ModelView) => void; + saveHandler: () => Thenable; + openEditor(position?: vscode.ViewColumn): Thenable { + return Promise.resolve(); + } + registerContent(handler: (view: azdata.ModelView) => void): void { + this.contentHandler = handler; + } + registerSaveHandler(handler: () => Thenable) { + this.saveHandler = handler; + } + modelView: azdata.ModelView; + valid: boolean; + onValidityChanged: vscode.Event; + isDirty: boolean; +} + +export class MockModelView implements azdata.ModelView { + private onClosedEmitter = new vscode.EventEmitter(); + public get onClosed(): vscode.Event { + return this.onClosedEmitter.event; + } + public get connection(): azdata.connection.Connection { + return undefined; + } + public get serverInfo(): azdata.ServerInfo { + return undefined; + } + public get modelBuilder(): azdata.ModelBuilder { + return undefined; + } + public get valid(): boolean { + return undefined; + } + public get onValidityChanged(): vscode.Event { + return undefined; + } + validate(): Thenable { + throw new Error('Method not implemented.'); + } + initializeModel(root: T): Thenable { + throw new Error('Method not implemented.'); + } +} + +export class MockButtonComponent extends MockUIComponent implements azdata.ButtonComponent { + label: string; + title: string; + iconPath: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri; }; + isFile?: boolean; + fileContent?: string; + height?: string | number; + width?: string | number; + iconHeight?: string | number; + iconWidth?: string | number; + public onDidClickEmitter = new vscode.EventEmitter(); + public get onDidClick(): vscode.Event { + return this.onDidClickEmitter.event; + } +} + +export class MockEditorComponent extends MockUIComponent implements azdata.EditorComponent { + content: string; + languageMode: string; + editorUri: string; + CSSStyles: { [key: string]: string }; + onContentChanged: vscode.Event; + onEditorCreated: vscode.Event; + isAutoResizable: boolean; + minimumHeight: 106; +} + +export interface ISqlServerEnv { + databases: { + dbName: string; + tables?: { + schemaName: string; + tableName: string; + columnDefinitionList: ColumnDefinition[]; + }[]; + views?: { + schemaName: string; + viewName: string; + columnDefinitionList: ColumnDefinition[]; + }[]; + }[]; +} + +export class SqlServerEnvFactory { + private static sqlServerEnv: ISqlServerEnv = + { + databases: + [ + { + dbName: 'NorthWind', + tables: + [ + { + schemaName: 'dbo', + tableName: 'Employees', + columnDefinitionList: + [ + { columnName: 'ID', dataType: 'INT', collationName: undefined, isNullable: false }, + { columnName: 'FirstName', dataType: 'NVARCHAR(10)', collationName: 'SQL_Latin1_General_CP1_CI_AS', isNullable: false }, + { columnName: 'LastName', dataType: 'NVARCHAR(20)', collationName: 'SQL_Latin1_General_CP1_CI_AS', isNullable: false }, + { columnName: 'BirthDate', dataType: 'DATETIME2(3)', collationName: undefined, isNullable: true } + ] + }, + { + schemaName: 'dbo', + tableName: 'Customers', + columnDefinitionList: + [ + { columnName: 'ID', dataType: 'INT', collationName: undefined, isNullable: false }, + { columnName: 'LoginID', dataType: 'NVARCHAR(10)', collationName: 'SQL_Latin1_General_CP1_CI_AS', isNullable: false }, + { columnName: 'Nickname', dataType: 'NVARCHAR(20)', collationName: 'SQL_Latin1_General_CP1_CI_AS', isNullable: false }, + { columnName: 'JoinDate', dataType: 'DATETIME2(3)', collationName: undefined, isNullable: true } + ] + } + ], + views: + [ + { + schemaName: 'dbo', + viewName: 'MyView1', + columnDefinitionList: + [ + { columnName: 'ID', dataType: 'INT', collationName: undefined, isNullable: false }, + { columnName: 'FirstName', dataType: 'NVARCHAR(10)', collationName: 'SQL_Latin1_General_CP1_CI_AS', isNullable: false }, + { columnName: 'LastName', dataType: 'NVARCHAR(20)', collationName: 'SQL_Latin1_General_CP1_CI_AS', isNullable: false }, + { columnName: 'BirthDate', dataType: 'DATETIME2(3)', collationName: undefined, isNullable: true } + ] + }, + { + schemaName: 'dbo', + viewName: 'MyView2', + columnDefinitionList: + [ + { columnName: 'ID', dataType: 'INT', collationName: undefined, isNullable: false }, + { columnName: 'LoginID', dataType: 'NVARCHAR(10)', collationName: 'SQL_Latin1_General_CP1_CI_AS', isNullable: false }, + { columnName: 'Nickname', dataType: 'NVARCHAR(20)', collationName: 'SQL_Latin1_General_CP1_CI_AS', isNullable: false }, + { columnName: 'JoinDate', dataType: 'DATETIME2(3)', collationName: undefined, isNullable: true } + ] + } + ], + }, + { + dbName: 'pub', + tables: + [ + { + schemaName: 'dbo', + tableName: 'Orders', + columnDefinitionList: + [ + { columnName: 'OrderID', dataType: 'INT', collationName: undefined, isNullable: false }, + { columnName: 'CustomerID', dataType: 'INT', collationName: undefined, isNullable: false }, + { columnName: 'ProductID', dataType: 'INT', collationName: undefined, isNullable: false }, + { columnName: 'SoldPrice', dataType: 'INT', collationName: undefined, isNullable: false }, + { columnName: 'Note', dataType: 'NVARCHAR(10)', collationName: 'SQL_Latin1_General_CP1_CI_AS', isNullable: false }, + { columnName: 'OrderDate', dataType: 'DATETIME2(3)', collationName: undefined, isNullable: true } + ] + }, + { + schemaName: 'dbo', + tableName: 'Products', + columnDefinitionList: + [ + { columnName: 'ProductID', dataType: 'INT', collationName: undefined, isNullable: false }, + { columnName: 'ProductName', dataType: 'NVARCHAR(20)', collationName: 'SQL_Latin1_General_CP1_CI_AS', isNullable: false }, + { columnName: 'MSRP', dataType: 'INT', collationName: undefined, isNullable: false }, + { columnName: 'ManufacturedDate', dataType: 'DATETIME2(3)', collationName: undefined, isNullable: true }, + { columnName: 'StockCount', dataType: 'INT', collationName: undefined, isNullable: false } + ] + } + ] + } + ] + }; + + public static getSqlServerEnv(): ISqlServerEnv { + return SqlServerEnvFactory.sqlServerEnv; + } +} + +interface IMockEnvConstants { + sessionId: string; + destDatabaseList: DatabaseOverview[]; + destDbNameSelected: string; + sourceServerType: string; + newDataSourceName: string; + sourceServerName: string; + sourceDatabaseName: string; + sourceAuthenticationType: string; + newCredentialName: string; + sourceUsername: string; + sourcePassword: string; + destDbMasterKeyPwd: string; + externalTableInfoList: ExternalTableInfo[]; + supportedSourceTypes: DataSourceType[]; + sqlServerEnv: ISqlServerEnv; + serverMajorVersion: number; + productLevel: string; +} + +export class MockEnvConstantsFactory { + private static mockEnvConstants: IMockEnvConstants = { + sessionId: 'datasourcewizard://ade21baa-df28-4b03-b8e4-90e9c01c0142', + destDatabaseList: [{ name: 'DestDB1', hasMasterKey: false }, { name: 'DestDB2', hasMasterKey: true }], + destDbNameSelected: 'DestDB1', + sourceServerType: 'SQL Server', + destDbMasterKeyPwd: 'Pwd1234#', // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Unit test - not actually used to authenticate")] + newDataSourceName: 'MyDataSource', + sourceServerName: '192.168.0.11:1433', + sourceDatabaseName: 'NorthWind', + sourceAuthenticationType: undefined, + newCredentialName: 'MyCred', + sourceUsername: 'testuser', + sourcePassword: 'GiveMe$500', // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Unit test - not actually used to authenticate")] + externalTableInfoList: undefined, + supportedSourceTypes: undefined, + sqlServerEnv: undefined, + serverMajorVersion: 15, + productLevel: 'CTP2.5' + }; + + public static getMockEnvConstants(sqlServerEnv: ISqlServerEnv): IMockEnvConstants { + MockEnvConstantsFactory.mockEnvConstants.sqlServerEnv = sqlServerEnv; + + let externalTableInfoList: ExternalTableInfo[] = []; + sqlServerEnv.databases.forEach(db => { + db.tables.forEach(table => { + let externalTableName: string[] = []; + let location: string[] = [db.dbName]; + for (let i = 0; i < table.tableName.length; ++i) { + externalTableName.push(i < table.tableName.length - 1 ? table.tableName[i] : `v${table.tableName[i]}`); + location.push(table.tableName[i]); + } + externalTableInfoList.push( + { + externalTableName: externalTableName, + columnDefinitionList: table.columnDefinitionList, + sourceTableLocation: location, + fileFormat: undefined + } + ); + }); + }); + MockEnvConstantsFactory.mockEnvConstants.externalTableInfoList = externalTableInfoList; + + return MockEnvConstantsFactory.mockEnvConstants; + } +} + +export class VirtualizeDataMockEnv { + private _virtualizeDataWizardlMock: TypeMoq.IMock; + private _virtualizeDataModelMock: TypeMoq.IMock; + private _wizardMock: TypeMoq.IMock; + private _wizardPageMock: TypeMoq.IMock; + private _apiWrapperMock: TypeMoq.IMock; + private _vdiManagerMock: TypeMoq.IMock; + private _selectDataSourcePageMock: TypeMoq.IMock; + + private _mockedWizard: MockWizard; + private _mockedWizardPage: MockWizardPage; + private _mockedApiWrapper: ApiWrapper; + private _mockedAppContext: AppContext; + private _mockedVirtualizeDataWizard: VirtualizeDataWizard; + private _mockedVirtualizeDataModel: VirtualizeDataModel; + private _mockedVDIManager: VDIManager; + + private _mockedSelectDataSourcePage: SelectDataSourcePage; + private _mockedCreateMasterKeyPage: CreateMasterKeyPage; + private _mockedConnectionDetailsPage: ConnectionDetailsPage; + private _mockedObjectMappingPage: ObjectMappingPage; + + private _mockEnvConstants: IMockEnvConstants; + + constructor(mockEnvConstants?: IMockEnvConstants) { + if (mockEnvConstants) { + this._mockEnvConstants = mockEnvConstants; + } else { + this._mockEnvConstants = MockEnvConstantsFactory.getMockEnvConstants(SqlServerEnvFactory.getSqlServerEnv()); + } + } + + public getMockEnvConstants(): IMockEnvConstants { + return this._mockEnvConstants; + } + + public getWizardMock(): TypeMoq.IMock { + if (!this._wizardMock) { + this._wizardMock = TypeMoq.Mock.ofType(MockWizard); + this._wizardMock + .setup(x => x.registerOperation(TypeMoq.It.isAny())); + } + return this._wizardMock; + } + + public getMockedWizard(): MockWizard { + if (!this._mockedWizard) { + this._mockedWizard = this.getWizardMock().object; + } + return this._mockedWizard; + } + + public getWizardPageMock(): TypeMoq.IMock { + if (!this._wizardPageMock) { + this._wizardPageMock = TypeMoq.Mock.ofType(MockWizardPage); + this._wizardPageMock + .setup(x => x.registerContent(TypeMoq.It.isAny())); + } + return this._wizardPageMock; + } + + public getMockedWizardPage(): MockWizardPage { + if (!this._mockedWizardPage) { + this._mockedWizardPage = this.getWizardPageMock().object; + } + return this._mockedWizardPage; + } + + public getApiWrapperMock(): TypeMoq.IMock { + if (!this._apiWrapperMock) { + this._apiWrapperMock = TypeMoq.Mock.ofType(ApiWrapper); + this._apiWrapperMock + .setup(x => x.createWizardPage(TypeMoq.It.isAnyString())) + .returns(() => { + return this.getMockedWizardPage(); + }); + } + return this._apiWrapperMock; + } + + public getMockedApiWrapper(): ApiWrapper { + if (!this._mockedApiWrapper) { + this._mockedApiWrapper = this.getApiWrapperMock().object; + } + return this._mockedApiWrapper; + } + + public getMockedAppContext(): AppContext { + if (!this._mockedAppContext) { + let extensionContext = new MockExtensionContext(); + this._mockedAppContext = new AppContext(extensionContext, this.getApiWrapperMock().object); + } + return this._mockedAppContext; + } + + public getVirtualizeDataWizardMock(): TypeMoq.IMock { + if (!this._virtualizeDataWizardlMock) { + this._virtualizeDataWizardlMock = TypeMoq.Mock.ofType(VirtualizeDataWizard, TypeMoq.MockBehavior.Loose); + + this._virtualizeDataWizardlMock + .setup(x => x.dataModel) + .returns(() => { + return this.getMockedVirtualizeDataModel(); + }); + + this._virtualizeDataWizardlMock + .setup(x => x.appContext) + .returns(() => { + return this.getMockedAppContext(); + }); + + this._virtualizeDataWizardlMock + .setup(x => x.vdiManager) + .returns(() => { + return this.getMockedVDIManager(); + }); + } + return this._virtualizeDataWizardlMock; + } + + public getMockedVirtualizeDataWizard(): VirtualizeDataWizard { + if (!this._mockedVirtualizeDataWizard) { + this._mockedVirtualizeDataWizard = this.getVirtualizeDataWizardMock().object; + } + return this._mockedVirtualizeDataWizard; + } + + public getVirtualizeDataModelMock(): TypeMoq.IMock { + if (!this._virtualizeDataModelMock) { + this._virtualizeDataModelMock = TypeMoq.Mock.ofType(VirtualizeDataModel, TypeMoq.MockBehavior.Loose); + + this._virtualizeDataModelMock + .setup(x => x.configInfoResponse) + .returns(() => { + return { + sessionId: this._mockEnvConstants.sessionId, + supportedSourceTypes: this._mockEnvConstants.supportedSourceTypes, + databaseList: this._mockEnvConstants.destDatabaseList, + serverMajorVersion: this._mockEnvConstants.serverMajorVersion, + productLevel: this._mockEnvConstants.productLevel + }; + }); + + this._virtualizeDataModelMock + .setup(x => x.destDatabaseList) + .returns(() => { + return this._mockEnvConstants.destDatabaseList; + }); + + this._virtualizeDataModelMock + .setup(x => x.sessionId) + .returns(() => this._mockEnvConstants.sessionId); + + this._virtualizeDataModelMock + .setup(x => x.getSourceDatabases(TypeMoq.It.isAny())) + .returns(_ => { + return Promise.resolve({ + isSuccess: true, + errorMessages: undefined, + databaseNames: this._mockEnvConstants.sqlServerEnv.databases.map(e => e.dbName) + }); + }); + + this._virtualizeDataModelMock + .setup(x => x.getSourceTables(TypeMoq.It.isAny())) + .returns(_ => { + let tables = this._mockEnvConstants.sqlServerEnv.databases[0].tables; + let schemaTablesList: SchemaTables[] = []; + tables.forEach(table => { + let correspondingSchemaTables: SchemaTables = schemaTablesList.find(e => e.schemaName === table.schemaName); + if (correspondingSchemaTables) { + correspondingSchemaTables.tableNames.push(table.tableName); + } else { + schemaTablesList.push({ schemaName: table.schemaName, tableNames: [table.tableName] }); + } + }); + + return Promise.resolve({ + isSuccess: true, + errorMessages: undefined, + schemaTablesList: schemaTablesList + }); + }); + + this._virtualizeDataModelMock + .setup(x => x.getSourceViewList(TypeMoq.It.isAny())) + .returns(_ => { + let views = this._mockEnvConstants.sqlServerEnv.databases[0].views; + let schemaViewsList: SchemaViews[] = []; + views.forEach(view => { + let correspondingSchemaViews: SchemaViews = schemaViewsList.find(e => e.schemaName === view.schemaName); + if (correspondingSchemaViews) { + correspondingSchemaViews.viewNames.push(view.viewName); + } else { + schemaViewsList.push({ schemaName: view.schemaName, viewNames: [view.viewName] }); + } + }); + + return Promise.resolve>({ + isSuccess: true, + errorMessages: undefined, + returnValue: schemaViewsList + }); + }); + + this._virtualizeDataModelMock + .setup(x => x.getSourceColumnDefinitions(TypeMoq.It.isAny())) + .returns(() => { + return Promise.resolve>({ + isSuccess: true, + errorMessages: undefined, + returnValue: this._mockEnvConstants.sqlServerEnv.databases[0].tables[0].columnDefinitionList + }); + }); + + this._virtualizeDataModelMock + .setup(x => x.defaultSchema) + .returns(() => { return 'dbo'; }); + + this._virtualizeDataModelMock + .setup(x => x.hasMasterKey()) + .returns(() => { + return Promise.resolve(this._mockEnvConstants.destDbMasterKeyPwd === undefined); + }); + + this._virtualizeDataModelMock + .setup(x => x.showWizardError(TypeMoq.It.isAny())); + + this._virtualizeDataModelMock + .setup(x => x.validateInput(TypeMoq.It.isAny())) + .returns(() => { + return Promise.resolve(true); + }); + + this._virtualizeDataModelMock + .setup(x => x.wizard) + .returns(() => { + return this.getMockedWizard(); + }); + + this._virtualizeDataModelMock + .setup(x => x.schemaList) + .returns(() => { + return ['TestSchema']; + }); + } + return this._virtualizeDataModelMock; + } + + public getMockedVirtualizeDataModel(): VirtualizeDataModel { + if (!this._mockedVirtualizeDataModel) { + this._mockedVirtualizeDataModel = this.getVirtualizeDataModelMock().object; + } + return this._mockedVirtualizeDataModel; + } + + private getVirtualizedDataInput(): VirtualizeDataInput { + return { + sessionId: this._mockEnvConstants.sessionId, + destDatabaseName: this._mockEnvConstants.destDbNameSelected, + sourceServerType: this._mockEnvConstants.sourceServerType, + destDbMasterKeyPwd: this._mockEnvConstants.destDbMasterKeyPwd, + existingDataSourceName: undefined, + newDataSourceName: this._mockEnvConstants.newDataSourceName, + sourceServerName: this._mockEnvConstants.sourceServerName, + sourceDatabaseName: this._mockEnvConstants.sourceDatabaseName, + sourceAuthenticationType: this._mockEnvConstants.sourceAuthenticationType, + existingCredentialName: undefined, + newCredentialName: this._mockEnvConstants.newCredentialName, + sourceUsername: this._mockEnvConstants.sourceUsername, + sourcePassword: this._mockEnvConstants.sourcePassword, + externalTableInfoList: this._mockEnvConstants.externalTableInfoList, + newSchemas: undefined + }; + } + + public getVDIManagerMock(): TypeMoq.IMock { + if (!this._vdiManagerMock) { + this._vdiManagerMock = TypeMoq.Mock.ofType(VDIManager, TypeMoq.MockBehavior.Loose); + + this._vdiManagerMock + .setup(x => x.getVirtualizeDataInput(TypeMoq.It.isAny())) + .returns(() => this.getVirtualizedDataInput()); + + this._vdiManagerMock + .setup(x => x.inputUptoConnectionDetailsPage) + .returns(() => { + let inputValues = this.getVirtualizedDataInput(); + inputValues.externalTableInfoList = undefined; + return inputValues; + }); + + this._vdiManagerMock + .setup(x => x.dataSourceName) + .returns(() => { return this._mockEnvConstants.newDataSourceName; }); + + this._vdiManagerMock + .setup(x => x.sourceServerName) + .returns(() => { return this._mockEnvConstants.sourceServerName; }); + } + return this._vdiManagerMock; + } + + public getMockedVDIManager(): VDIManager { + if (!this._mockedVDIManager) { + this._mockedVDIManager = this.getVDIManagerMock().object; + } + return this._mockedVDIManager; + } + + public getMockedSelecteDataSourcePage(): SelectDataSourcePage { + if (!this._mockedSelectDataSourcePage) { + let virtualizeDataWizard = this.getMockedVirtualizeDataWizard(); + let selectDataSourcePage = new SelectDataSourcePage(virtualizeDataWizard); + let sdsPage = selectDataSourcePage; + sdsPage._destDBDropDown = new MockDropdownComponent(); + sdsPage._destDBDropDown.value = 'MyDestinationDB'; + sdsPage._selectedSourceType = 'SQL Server'; + this._mockedSelectDataSourcePage = selectDataSourcePage; + } + return this._mockedSelectDataSourcePage; + } + + public getMockedCreateMasterKeyPage(): CreateMasterKeyPage { + if (!this._mockedCreateMasterKeyPage) { + let dataModel = this.getMockedVirtualizeDataModel(); + let vdiManager = this.getMockedVDIManager(); + let appContext = this.getMockedAppContext(); + let createMasterKeyPage = new CreateMasterKeyPage(dataModel, vdiManager, appContext); + let cmkPage = createMasterKeyPage; + cmkPage._uiElements = new MasterKeyUiElements(); + cmkPage._uiElements.masterKeyPasswordInput = new MockInputBoxComponent(); + cmkPage._uiElements.masterKeyPasswordConfirmInput = new MockInputBoxComponent(); + cmkPage._uiElements.masterKeyPasswordInput.value = 'GiveMe$500'; + cmkPage._uiElements.masterKeyPasswordConfirmInput.value = 'GiveMe$500'; + this._mockedCreateMasterKeyPage = createMasterKeyPage; + } + return this._mockedCreateMasterKeyPage; + } + + public getMockedConnectionDetailsPage(): ConnectionDetailsPage { + if (!this._mockedConnectionDetailsPage) { + let dataModel = this.getMockedVirtualizeDataModel(); + let vdiManager = this.getMockedVDIManager(); + let appContext = this.getMockedAppContext(); + let connectionDetailsPage = new ConnectionDetailsPage(dataModel, vdiManager, appContext); + let cdPage = connectionDetailsPage; + cdPage._sourceNameInput = new MockInputBoxComponent(); + cdPage._serverNameInput = new MockInputBoxComponent(); + cdPage._databaseNameInput = new MockInputBoxComponent(); + cdPage._existingCredDropdown = new MockDropdownComponent(); + cdPage._credentialNameInput = new MockInputBoxComponent(); + cdPage._usernameInput = new MockInputBoxComponent(); + cdPage._passwordInput = new MockInputBoxComponent(); + cdPage._sourceNameInput.value = 'MyDataSource'; + cdPage._serverNameInput.value = '192.168.0.101'; + cdPage._databaseNameInput.value = undefined; + cdPage._existingCredDropdown.value = cdPage._createCredLabel; + cdPage._credentialNameInput.value = 'MyCred'; + cdPage._usernameInput.value = 'testuser'; + cdPage._passwordInput.value = 'Pwd1234!'; + this._mockedConnectionDetailsPage = connectionDetailsPage; + } + return this._mockedConnectionDetailsPage; + } + + public getMockedObjectMappingPage(): ObjectMappingPage { + if (!this._mockedObjectMappingPage) { + let dataModel = this.getMockedVirtualizeDataModel(); + let appContext = this.getMockedAppContext(); + let vdiManager = this.getMockedVDIManager(); + let objectMappingPage = new ObjectMappingPage(dataModel, vdiManager, appContext); + let omPage = objectMappingPage; + omPage._modelBuilder = new MockModelBuilder(); + omPage._dataSourceTreeContainer = new MockFlexContainer(); + omPage._dataSourceTableTree = new MockTreeComponent(); + + const mockSpinner = TypeMoq.Mock.ofType(); + mockSpinner.setup(x => x.loading); + omPage._objectMappingWrapperSpinner = mockSpinner; + omPage._objectMappingWrapper = new MockFlexContainer(); + omPage._objectMappingContainer = new MockFlexContainer(); + omPage._tableHelpTextContainer = new MockFlexContainer(); + + omPage._tableNameMappingContainer = new MockFlexContainer(); + omPage._sourceTableNameContainer = new MockFlexContainer(); + omPage._sourceSchemaInputBox = new MockInputBoxComponent(); + omPage._sourceTableNameInputBox = new MockInputBoxComponent(); + omPage._destTableNameInputContainer = new MockFormContainer(); + omPage._destTableSchemaDropdown = new MockDropdownComponent(); + omPage._destTableNameInputBox = new MockInputBoxComponent(); + + omPage._columnMappingTableSpinner = mockSpinner; + omPage._columnMappingTableContainer = new MockFormContainer(); + omPage._columnMappingTable = new MockDeclarativeTableComponent(); + + let treeComponentView = new MockTreeComponentView(undefined); + let treeComponentMock = TypeMoq.Mock.ofType(MockTreeComponent); + treeComponentMock + .setup(x => x.registerDataProvider(TypeMoq.It.isAny())) + .returns(() => { + return treeComponentView; + }); + omPage._dataSourceTableTree = treeComponentMock.object; + + this._mockedObjectMappingPage = objectMappingPage; + } + return this._mockedObjectMappingPage; + } +} + +export class MockConnectionProfile extends azdata.connection.ConnectionProfile { + providerId: 'TestProvider'; + connectionName: 'TestConnectionId'; + databaseName: undefined; + userName: undefined; + password: undefined; + authenticationType: undefined; + savePassword: false; + groupFullName: undefined; + groupId: undefined; + saveProfile: false; + azureTenantId?: undefined; + serverName: 'TestServer'; +} diff --git a/extensions/datavirtualization/src/test/tableFromFile.test.ts b/extensions/datavirtualization/src/test/tableFromFile.test.ts new file mode 100644 index 0000000000..1f6f933d49 --- /dev/null +++ b/extensions/datavirtualization/src/test/tableFromFile.test.ts @@ -0,0 +1,563 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as TypeMoq from 'typemoq'; +import * as should from 'should'; +import * as path from 'path'; +import * as azdata from 'azdata'; +import { VirtualizeDataMockEnv, MockDataSourceService, MockInputBoxComponent, MockDropdownComponent, MockTextComponent, MockLoadingComponent, MockDeclarativeTableComponent, MockTableComponent, MockConnectionProfile, MockButtonComponent } from './stubs'; +import { TableFromFileWizard } from '../wizards/tableFromFile/tableFromFileWizard'; +import { ImportDataModel } from '../wizards/tableFromFile/api/models'; +import { FileConfigPage, FileConfigPageUiElements } from '../wizards/tableFromFile/pages/fileConfigPage'; +import { ProsePreviewPage, ProsePreviewPageUiElements } from '../wizards/tableFromFile/pages/prosePreviewPage'; +import { ModifyColumnsPage, ModifyColumnsPageUiElements } from '../wizards/tableFromFile/pages/modifyColumnsPage'; +import { SummaryPage, SummaryPageUiElements } from '../wizards/tableFromFile/pages/summaryPage'; +import { FileNode } from '../hdfsProvider'; +import { DataSourceInstance } from '../services/contracts'; +import { stripUrlPathSlashes } from '../utils'; +import { DataSourceType, delimitedTextFileType } from '../constants'; + +describe('Table From File Wizard:', function () { + let env = new VirtualizeDataMockEnv(); + let appContext = env.getMockedAppContext(); + let service = new MockDataSourceService(); + + let mockWizard = TypeMoq.Mock.ofType(TableFromFileWizard, undefined, undefined, appContext, service); + + describe('File Config Page Tests', function () { + let mockPage = env.getMockedWizardPage(); + let model = {}; + + let mockService = TypeMoq.Mock.ofType(MockDataSourceService); + let page = new FileConfigPage(mockWizard.object, mockPage, model, undefined, mockService.object); + let ui: FileConfigPageUiElements = { + fileTextBox: new MockTextComponent(), + serverTextBox: new MockTextComponent(), + databaseDropdown: new MockDropdownComponent(), + dataSourceDropdown: new MockDropdownComponent(), + tableNameTextBox: new MockInputBoxComponent(), + schemaDropdown: new MockDropdownComponent(), + databaseLoader: new MockLoadingComponent(), + dataSourceLoader: new MockLoadingComponent(), + schemaLoader: new MockLoadingComponent(), + fileFormatNameTextBox: new MockInputBoxComponent(), + refreshButton: new MockButtonComponent() + }; + page.setUi(ui); + + model.allDatabases = ['TestDb']; + model.serverConn = new MockConnectionProfile(); + + mockService.setup(s => s.createDataSourceWizardSession(TypeMoq.It.isAny())).returns(() => service.createDataSourceWizardSession(undefined)); + mockService.setup(s => s.getDatabaseInfo(TypeMoq.It.isAny())).returns(() => service.getDatabaseInfo(undefined)); + + let onPageEnterTest = async function (tableName: string) { + (page).pageSetupComplete = false; + await page.onPageEnter(); + + should(ui.fileTextBox.value).be.equal(model.parentFile.filePath); + should(ui.serverTextBox.value).be.equal(model.serverConn.serverName); + should(ui.databaseDropdown.value).be.equal('TestDb'); + should(ui.dataSourceDropdown.value).be.equal('TestSource'); + should(ui.tableNameTextBox.value).be.equal(tableName); + should(ui.schemaDropdown.value).be.equal('TestSchema'); + should(ui.fileFormatNameTextBox.value).be.equal(`FileFormat_${tableName}`); + + should(ui.databaseLoader.loading).be.false(); + should(ui.databaseDropdown.enabled).be.true(); + + should(ui.dataSourceLoader.loading).be.false(); + should(ui.schemaLoader.loading).be.false(); + should(ui.refreshButton.enabled).be.true(); + + should(model.sessionId).be.equal('TestSessionId'); + should(model.allDatabases.length).be.equal(1); + should(model.allDatabases[0]).be.equal('TestDb'); + should(model.fileType).be.equal('TXT'); + should(model.database).be.equal('TestDb'); + should(model.existingDataSource).be.equal('TestSource'); + should(model.table).be.equal(tableName); + should(model.existingSchema).be.equal('TestSchema'); + should(model.newSchema).be.undefined(); + should(model.fileFormat).be.equal(`FileFormat_${tableName}`); + }; + + let mockFileNode = TypeMoq.Mock.ofType(FileNode); + model.proseParsingFile = mockFileNode.object; + + it('OnPageEnter Test', async function () { + // With file + let tableName = 'TestFile'; + model.parentFile = { + isFolder: false, + filePath: path.join('BaseDir', 'AnotherDir', `${tableName}.csv`) + }; + mockFileNode.setup(node => node.hdfsPath).returns(() => model.parentFile.filePath); + await onPageEnterTest(tableName); + + // With existing session + model.sessionId = 'OldTestId'; + model.allDatabases = ['OldTestDb1', 'OldTestDb2']; + mockService.setup(s => s.disposeWizardSession(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + await onPageEnterTest(tableName); + mockService.verify(s => s.disposeWizardSession(TypeMoq.It.isAny()), TypeMoq.Times.once()); + + // With folder + tableName = 'CsvTest'; + model.parentFile = { + isFolder: true, + filePath: path.join('BaseDir', 'AnotherDir', tableName) + }; + mockFileNode.setup(node => node.hdfsPath).returns(() => path.join(model.parentFile.filePath, 'TestFile.csv')); + await onPageEnterTest(tableName); + }); + + it('OnPageLeave Test', async function () { + model.existingDataSource = undefined; + (await page.onPageLeave(true)).should.be.false(); + + model.existingDataSource = ''; + (await page.onPageLeave(true)).should.be.false(); + + model.existingDataSource = 'TestSource'; + model.existingSchema = undefined; + (await page.onPageLeave(true)).should.be.false(); + + model.existingSchema = ''; + (await page.onPageLeave(true)).should.be.false(); + + model.existingSchema = 'TestSchema'; + (await page.onPageLeave(true)).should.be.true(); + + model.existingSchema = undefined; + model.newSchema = 'NewTestSchema'; + (await page.onPageLeave(true)).should.be.true(); + + model.fileFormat = 'TestExternalFileFormat'; + (await page.onPageLeave(true)).should.be.false(); + + model.fileFormat = 'NotAnExistingFileFormat'; + (await page.onPageLeave(true)).should.be.true(); + + // Existing table, but using new schema + model.table = 'TestExternalTable'; + (await page.onPageLeave(true)).should.be.true(); + + model.existingSchema = 'TestSchema'; + model.newSchema = undefined; + model.table = 'TestExternalTable'; + (await page.onPageLeave(true)).should.be.false(); + + model.table = 'NotAnExistingFileTable'; + (await page.onPageLeave(true)).should.be.true(); + + ui.databaseLoader.loading = true; + (await page.onPageLeave(false)).should.be.false(); + + ui.databaseLoader.loading = false; + ui.dataSourceLoader.loading = true; + (await page.onPageLeave(false)).should.be.false(); + + ui.dataSourceLoader.loading = false; + ui.schemaLoader.loading = true; + (await page.onPageLeave(false)).should.be.false(); + + ui.schemaLoader.loading = false; + (await page.onPageLeave(false)).should.be.true(); + }); + + it('Data Sources Test', async function () { + let dbInfo = await service.getDatabaseInfo(undefined); + let sessionInfo = await service.createDataSourceWizardSession(undefined); + model.parentFile = { + isFolder: false, + filePath: path.join('BaseDir', 'AnotherDir', 'TestFile.csv') + }; + let setupMocks = () => { + mockService.setup(s => s.getDatabaseInfo(TypeMoq.It.isAny())).returns(() => Promise.resolve(dbInfo)); + mockService.setup(s => s.createDataSourceWizardSession(TypeMoq.It.isAny())).returns(() => Promise.resolve(sessionInfo)); + mockFileNode.setup(node => node.hdfsPath).returns(() => model.parentFile.filePath); + }; + let testDataSource = (productVersion: string, dataSourceName: string, dataSourceLocation: string) => { + should(model.versionInfo.productLevel).be.equal(productVersion); + should(model.existingDataSource).be.undefined(); + should(model.newDataSource).not.be.undefined(); + should(model.newDataSource.name).be.equal(dataSourceName); + should(model.newDataSource.location).be.equal(dataSourceLocation); + should(model.newDataSource.authenticationType).be.undefined(); + should(model.newDataSource.credentialName).be.undefined(); + should(model.newDataSource.username).be.undefined(); + }; + let testNewCtp24Source = (dataSourceName: string = 'SqlStoragePool') => { + testDataSource('CTP2.4', dataSourceName, 'sqlhdfs://service-master-pool:50070/'); + }; + let testNewCtp25Source = (dataSourceName: string = 'SqlStoragePool') => { + testDataSource('CTP2.5', dataSourceName, 'sqlhdfs://nmnode-0-svc:50070/'); + }; + let testNewCtp3Source = (dataSourceName: string = 'SqlStoragePool') => { + testDataSource('CTP3.0', dataSourceName, 'sqlhdfs://controller-svc:8080/default'); + }; + + setupMocks(); + await (page).refreshPage(); + should(model.versionInfo.productLevel).be.equal('CTP3.1'); + should(model.existingDataSource).be.equal('TestSource'); + should(model.newDataSource).be.undefined(); + + sessionInfo.productLevel = 'CTP2.4'; + setupMocks(); + await (page).refreshPage(); + testNewCtp24Source(); + + sessionInfo.productLevel = 'CTP2.5'; + setupMocks(); + await (page).refreshPage(); + testNewCtp25Source(); + + dbInfo.databaseInfo.externalDataSources = []; + sessionInfo.productLevel = 'CTP3.0'; + setupMocks(); + await (page).refreshPage(); + testNewCtp3Source(); + + dbInfo.databaseInfo.externalDataSources = [{ + name: 'RandomSource', + location: 'sqlhdfs://NotARealSource:50070/' + }, { + name: 'SqlStoragePool', + location: 'sqlhdfs://NotARealSource:50070/' + }, { + name: 'SqlStoragePool1', + location: 'sqlhdfs://NotARealSource1:8080/default' + }, { + name: 'SqlStoragePool2', + location: 'sqlhdfs://NotARealSource2:8080/default' + }]; + setupMocks(); + await (page).refreshPage(); + testNewCtp3Source('SqlStoragePool3'); + + sessionInfo.productLevel = 'CTP2.4'; + setupMocks(); + await (page).refreshPage(); + testNewCtp24Source('SqlStoragePool3'); + + sessionInfo.serverMajorVersion = 1000; + sessionInfo.productLevel = 'NotARealCtpVersion'; + setupMocks(); + await (page).refreshPage(); + // Default to the latest version's data source location + testDataSource(sessionInfo.productLevel, 'SqlStoragePool3', 'sqlhdfs://controller-svc/default'); + }); + + it('Refresh Test', async function () { + let dbInfo = await service.getDatabaseInfo(undefined); + let sessionInfo = await service.createDataSourceWizardSession(undefined); + model.parentFile = { + isFolder: false, + filePath: path.join('BaseDir', 'AnotherDir', 'TestFile.csv') + }; + let setupMocks = () => { + mockService.setup(s => s.getDatabaseInfo(TypeMoq.It.isAny())).returns(() => Promise.resolve(dbInfo)); + mockService.setup(s => s.createDataSourceWizardSession(TypeMoq.It.isAny())).returns(() => Promise.resolve(sessionInfo)); + mockFileNode.setup(node => node.hdfsPath).returns(() => model.parentFile.filePath); + }; + setupMocks(); + + let testStr = 'RefreshTestStr'; + await (page).refreshPage(); + should(model.database).not.be.equal(testStr); + should(model.existingDataSource).not.be.equal(testStr); + should(model.table).not.be.equal(testStr); + should(model.existingSchema).not.be.equal(testStr); + should(model.fileFormat).not.be.equal(`FileFormat_${testStr}`); + + sessionInfo.databaseList = [{ name: testStr, hasMasterKey: false }]; + dbInfo.databaseInfo.externalDataSources[0].name = testStr; + model.parentFile = { + isFolder: false, + filePath: path.join('BaseDir', 'AnotherDir', `${testStr}.csv`) + }; + dbInfo.databaseInfo.schemaList = [testStr]; + setupMocks(); + + await (page).refreshPage(); + should(model.database).be.equal(testStr); + should(model.existingDataSource).be.equal(testStr); + should(model.table).be.equal(testStr); + should(model.existingSchema).be.equal(testStr); + should(model.fileFormat).be.equal(`FileFormat_${testStr}`); + }); + }); + + describe('Preview Page Tests', function () { + let mockPage = env.getMockedWizardPage(); + let fileNodeMock = TypeMoq.Mock.ofType(FileNode); + let model = {}; + model.proseParsingFile = fileNodeMock.object; + + fileNodeMock.setup(f => f.getFileLinesAsString(TypeMoq.It.isAny())).returns(() => Promise.resolve(service.proseTestData)); + + let page = new ProsePreviewPage(mockWizard.object, mockPage, model, undefined, service); + let ui: ProsePreviewPageUiElements = { + table: new MockTableComponent(), + loading: new MockLoadingComponent() + }; + page.setUi(ui); + + it('OnPageEnter Test', async function () { + await page.onPageEnter(); + + should(ui.loading.loading).be.false(); + + should(model.columnDelimiter).be.equal(','); + should(model.firstRow).be.equal(2); + should(model.quoteCharacter).be.equal('"'); + + should(ui.table.columns.length).be.equal(2); + should(ui.table.columns[0]).be.equal('TestId'); + should(ui.table.columns[1]).be.equal('TestStr'); + + should(ui.table.data.length).be.equal(1); + should(ui.table.data[0].length).be.equal(2); + should(ui.table.data[0][0]).be.equal('1'); + should(ui.table.data[0][1]).be.equal('abc'); + }); + + it('OnPageEnter Error Test', async function () { + let errorMockWizard = TypeMoq.Mock.ofType(TableFromFileWizard, undefined, undefined, appContext, service); + let errorFileNodeMock = TypeMoq.Mock.ofType(FileNode); + let errorModel = {}; + errorModel.proseParsingFile = errorFileNodeMock.object; + + let errorMsg = 'Expected Test Error'; + errorFileNodeMock.setup(f => f.getFileLinesAsString(TypeMoq.It.isAny())).throws(new Error(errorMsg)); + errorMockWizard.setup(w => w.showErrorMessage(TypeMoq.It.isValue(errorMsg))); + + let errorPage = new ProsePreviewPage(errorMockWizard.object, mockPage, errorModel, undefined, service); + errorMockWizard.verify((w => w.showErrorMessage(TypeMoq.It.isValue(errorMsg))), TypeMoq.Times.never()); + + errorPage.setUi({ + table: new MockTableComponent(), + loading: new MockLoadingComponent() + }); + await errorPage.onPageEnter(); + + errorMockWizard.verify((w => w.showErrorMessage(TypeMoq.It.isValue(errorMsg))), TypeMoq.Times.once()); + }); + + it('OnPageLeave Test', async function () { + (await page.onPageLeave(true)).should.be.true(); + }); + }); + + describe('Modify Columns Page Tests', function () { + let mockPage = env.getMockedWizardPage(); + let model = {}; + + let page = new ModifyColumnsPage(mockWizard.object, mockPage, model, undefined, service); + let ui: ModifyColumnsPageUiElements = { + table: new MockDeclarativeTableComponent, + loading: new MockLoadingComponent, + text: new MockTextComponent() + }; + page.setUi(ui); + + model.proseColumns = [{ + columnName: 'TestId', + dataType: 'int', + collationName: undefined, + isNullable: false + }, { + columnName: 'TestStr', + dataType: 'varchar(50)', + collationName: undefined, + isNullable: true + }]; + + it('OnPageEnter Test', async function () { + await page.onPageEnter(); + + should(ui.loading.loading).be.false(); + + should(ui.table.data.length).be.equal(2); + should(ui.table.data[0][0]).be.equal('TestId'); + should(ui.table.data[0][1]).be.equal('int'); + should(ui.table.data[0][2]).be.equal(false); + should(ui.table.data[1][0]).be.equal('TestStr'); + should(ui.table.data[1][1]).be.equal('varchar(50)'); + should(ui.table.data[1][2]).be.equal(true); + }); + + it('OnPageLeave Test', async function () { + (await page.onPageLeave(true)).should.be.true(); + }); + }); + + describe('Summary Page Tests', function () { + let mockPage = env.getMockedWizardPage(); + let model = {}; + + let page = new SummaryPage(mockWizard.object, mockPage, model, undefined, service); + let ui: SummaryPageUiElements = { + table: new MockTableComponent() + }; + page.setUi(ui); + + it('OnPageEnter Test', async function () { + model.serverConn = { + providerId: undefined, + connectionId: undefined, + serverName: 'TestServer' + }; + model.database = 'TestDb'; + model.table = 'TestTable'; + model.existingSchema = 'dbo'; + model.fileFormat = 'TestFileFormat'; + model.parentFile = { + isFolder: false, + filePath: path.join('BaseDir', 'AnotherDir', 'TestTable.csv') + }; + + mockWizard.setup(w => w.changeDoneButtonLabel(TypeMoq.It.isValue('Virtualize Data'))); + mockWizard.setup(w => w.setGenerateScriptVisibility(TypeMoq.It.isValue(true))); + + await page.onPageEnter(); + + mockWizard.verify(w => w.changeDoneButtonLabel(TypeMoq.It.isValue('Virtualize Data')), TypeMoq.Times.once()); + mockWizard.verify(w => w.setGenerateScriptVisibility(TypeMoq.It.isValue(true)), TypeMoq.Times.once()); + + should(ui.table.data[0][1]).be.equal(model.serverConn.serverName); + should(ui.table.data[1][1]).be.equal(model.database); + should(ui.table.data[2][1]).be.equal(model.table); + should(ui.table.data[3][1]).be.equal(model.existingSchema); + should(ui.table.data[4][1]).be.equal(model.fileFormat); + should(ui.table.data[5][1]).be.equal(model.parentFile.filePath); + }); + + it('OnPageLeave Test', async function () { + mockWizard.setup(w => w.changeDoneButtonLabel(TypeMoq.It.isValue('Next'))); + mockWizard.setup(w => w.setGenerateScriptVisibility(TypeMoq.It.isValue(false))); + + (await page.onPageLeave(true)).should.be.true(); + + mockWizard.verify(w => w.changeDoneButtonLabel(TypeMoq.It.isValue('Next')), TypeMoq.Times.once()); + mockWizard.verify(w => w.setGenerateScriptVisibility(TypeMoq.It.isValue(false)), TypeMoq.Times.once()); + }); + }); + + describe('Utilities Tests', function () { + it('Generate Input From Model Test', function () { + let input = TableFromFileWizard.generateInputFromModel(undefined); + should(input).be.undefined(); + + let model = { + sessionId: 'TestId', + columnDelimiter: ',', + database: 'TestDatabase', + fileFormat: 'FileFormat_TestTable', + firstRow: 1, + newDataSource: { + name: 'SqlStoragePool', + location: 'sqlhdfs://controller-svc:8080/default/', + authenticationType: undefined, + username: undefined, + credentialName: undefined + }, + newSchema: 'TestSchema', + parentFile: { + filePath: 'test/TestTable.csv', + isFolder: false + }, + proseColumns: [{ + collationName: undefined, + columnName: 'column1', + dataType: 'nvarchar(50)', + isNullable: true, + }], + quoteCharacter: '\"', + table: 'TestTable' + }; + + input = TableFromFileWizard.generateInputFromModel(model); + should(input).not.be.undefined(); + should(input.sessionId).be.equal(model.sessionId); + should(input.destDatabaseName).be.equal(model.database); + should(input.sourceServerType).be.equal(DataSourceType.SqlHDFS); + + should(input.externalTableInfoList).not.be.undefined(); + should(input.externalTableInfoList.length).be.equal(1); + + let tableInfo = input.externalTableInfoList[0]; + should(tableInfo.externalTableName).not.be.undefined(); + should(tableInfo.externalTableName.length).be.equal(2); + should(tableInfo.externalTableName[0]).be.equal(model.newSchema); + should(tableInfo.externalTableName[1]).be.equal(model.table); + + should(tableInfo.columnDefinitionList).not.be.undefined(); + should(tableInfo.columnDefinitionList.length).be.equal(1); + let columnInfo = tableInfo.columnDefinitionList[0]; + let proseInfo = model.proseColumns[0]; + should(columnInfo.collationName).be.equal(proseInfo.collationName); + should(columnInfo.columnName).be.equal(proseInfo.columnName); + should(columnInfo.dataType).be.equal(proseInfo.dataType); + should(columnInfo.isNullable).be.equal(proseInfo.isNullable); + + should(tableInfo.sourceTableLocation).not.be.undefined(); + should(tableInfo.sourceTableLocation.length).be.equal(1); + should(tableInfo.sourceTableLocation[0]).be.equal(model.parentFile.filePath); + + should(tableInfo.fileFormat).not.be.undefined(); + should(tableInfo.fileFormat.formatName).be.equal(model.fileFormat); + should(tableInfo.fileFormat.formatType).be.equal(delimitedTextFileType); + should(tableInfo.fileFormat.fieldTerminator).be.equal(model.columnDelimiter); + should(tableInfo.fileFormat.stringDelimiter).be.equal(model.quoteCharacter); + should(tableInfo.fileFormat.firstRow).be.equal(model.firstRow); + + should(input.newDataSourceName).be.equal(model.newDataSource.name); + should(input.sourceServerName).be.equal('controller-svc:8080/default/'); + should(input.existingDataSourceName).be.undefined(); + + should(input.newSchemas).not.be.undefined(); + should(input.newSchemas.length).be.equal(1); + should(input.newSchemas[0]).be.equal(model.newSchema); + }); + + it('Remove URL Path Slashes Test', function () { + let testPath = undefined; + should(stripUrlPathSlashes(testPath)).be.equal(''); + + testPath = ''; + should(stripUrlPathSlashes(testPath)).be.equal(''); + + testPath = '//////////'; + should(stripUrlPathSlashes(testPath)).be.equal(''); + + testPath = 'a/'; + should(stripUrlPathSlashes(testPath)).be.equal('a'); + + testPath = 'testPath'; + should(stripUrlPathSlashes(testPath)).be.equal('testPath'); + + testPath = '/testPath'; + should(stripUrlPathSlashes(testPath)).be.equal('testPath'); + + testPath = '/testPath/testPath2'; + should(stripUrlPathSlashes(testPath)).be.equal('testPath/testPath2'); + + testPath = 'testPath/testPath2'; + should(stripUrlPathSlashes(testPath)).be.equal('testPath/testPath2'); + + testPath = 'testPath/testPath2///'; + should(stripUrlPathSlashes(testPath)).be.equal('testPath/testPath2'); + + testPath = '/testPath/testPath2///'; + should(stripUrlPathSlashes(testPath)).be.equal('testPath/testPath2'); + + testPath = '/testPath/testPath2/testPath3/////'; + should(stripUrlPathSlashes(testPath)).be.equal('testPath/testPath2/testPath3'); + }); + }); +}); diff --git a/extensions/datavirtualization/src/test/testUtils.ts b/extensions/datavirtualization/src/test/testUtils.ts new file mode 100644 index 0000000000..4df882d5e5 --- /dev/null +++ b/extensions/datavirtualization/src/test/testUtils.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; + +export async function assertThrowsAsync(fn: Function, msg: string): Promise { + let f = () => { + // Empty + }; + try { + await fn(); + } catch (e) { + f = () => { throw e; }; + } finally { + assert.throws(f, undefined, msg); + } +} diff --git a/extensions/datavirtualization/src/test/virtualizeData.test.ts b/extensions/datavirtualization/src/test/virtualizeData.test.ts new file mode 100644 index 0000000000..7911bbd275 --- /dev/null +++ b/extensions/datavirtualization/src/test/virtualizeData.test.ts @@ -0,0 +1,375 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as TypeMoq from 'typemoq'; +import * as azdata from 'azdata'; +import * as should from 'should'; + +import { VirtualizeDataModel } from '../wizards/virtualizeData/virtualizeDataModel'; +import { CheckboxTreeNode } from '../wizards/virtualizeData/virtualizeDataTree'; +import { + MockInputBoxComponent, MockWizard, VirtualizeDataMockEnv, MockTextComponent, + MockDeclarativeTableComponent, MockDataSourceService, MockConnectionProfile +} from './stubs'; +import { CreateMasterKeyPage, MasterKeyUiElements } from '../wizards/virtualizeData/createMasterKeyPage'; +import { SummaryPage, SummaryUiElements } from '../wizards/virtualizeData/summaryPage'; +import { VDIManager } from '../wizards/virtualizeData/virtualizeDataInputManager'; +import { ColumnDefinition } from '../services/contracts'; + +describe('Wizard Setup Tests', function (): void { + it('Should set model fields after creating session.', async () => { + let mockWizard = new MockWizard(); + mockWizard.message = undefined; + let vdiManager = (new VirtualizeDataMockEnv()).getMockedVDIManager(); + let model = new VirtualizeDataModel(new MockConnectionProfile(), new MockDataSourceService(), new MockWizard(), vdiManager); + await model.createSession(); + + should(model.sessionId).be.eql('TestSessionId'); + should(model.destDatabaseList.length).be.greaterThan(0); + should(model.destDatabaseList).containEql({ name: 'TestDb', hasMasterKey: false }); + }); +}); + +describe('MasterKeyPage Tests', function (): void { + it('[MasterKeyPage Test] MasterKeyPage should create a wizard page and register content during the initialization.', async () => { + let env = new VirtualizeDataMockEnv(); + let mockDataModel = env.getMockedVirtualizeDataModel(); + let vdiManager = env.getMockedVDIManager(); + let appContext = env.getMockedAppContext(); + let apiWrapper = env.getApiWrapperMock(); + let wizardPage = env.getWizardPageMock(); + + let masterKeyPage = new CreateMasterKeyPage(mockDataModel, vdiManager, appContext); + apiWrapper.verifyAll(); + wizardPage.verifyAll(); + should(masterKeyPage).not.be.null(); + }); + + it('[MasterKeyPage Test] MasterKeyPage should get data from VirtualizeDataModel when the page is opened.', async () => { + let uiElement = new MasterKeyUiElements(); + uiElement.masterKeyPasswordInput = new MockInputBoxComponent(); + uiElement.masterKeyPasswordConfirmInput = new MockInputBoxComponent(); + + let env = new VirtualizeDataMockEnv(); + env.getMockEnvConstants().destDbMasterKeyPwd = undefined; + + let dataModel = env.getMockedVirtualizeDataModel(); + let dataModelMock = env.getVirtualizeDataModelMock(); + let vdiManager = env.getMockedVDIManager(); + let appContext = env.getMockedAppContext(); + + let masterKeyPage = new CreateMasterKeyPage(dataModel, vdiManager, appContext); + masterKeyPage.setUi(uiElement); + await masterKeyPage.updatePage(); + + dataModelMock.verify(x => x.hasMasterKey(), TypeMoq.Times.once()); + should(uiElement.masterKeyPasswordInput.enabled).be.false; + should(uiElement.masterKeyPasswordConfirmInput.enabled).be.false; + + env.getMockEnvConstants().destDbMasterKeyPwd = 'Chanel$4700'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Unit test - not actually used to authenticate")] + await masterKeyPage.updatePage(); + + should(uiElement.masterKeyPasswordInput.enabled).be.true; + should(uiElement.masterKeyPasswordConfirmInput.enabled).be.true; + }); + + it('[MasterKeyPage Test] MasterKeyPage should fail validation if page content is invalid.', async () => { + let uiElement = new MasterKeyUiElements(); + uiElement.masterKeyPasswordInput = new MockInputBoxComponent(); + uiElement.masterKeyPasswordConfirmInput = new MockInputBoxComponent(); + + let env = new VirtualizeDataMockEnv(); + let dataModel = env.getMockedVirtualizeDataModel(); + let dataModelMock = env.getVirtualizeDataModelMock(); + let vdiManager = env.getMockedVDIManager(); + let vdiManagerMock = env.getVDIManagerMock(); + let appContext = env.getMockedAppContext(); + + let masterKeyPage = new CreateMasterKeyPage(dataModel, vdiManager, appContext); + masterKeyPage.setUi(uiElement); + + uiElement.masterKeyPasswordInput.value = 'test123'; + uiElement.masterKeyPasswordConfirmInput.value = '123test'; + + should(await masterKeyPage.validate()).be.false; + dataModelMock.verify(x => x.showWizardError(TypeMoq.It.isAny()), TypeMoq.Times.once()); + dataModelMock.verify(x => x.validateInput(TypeMoq.It.isAny()), TypeMoq.Times.never()); + vdiManagerMock.verify(x => x.getVirtualizeDataInput(TypeMoq.It.isAny()), TypeMoq.Times.never()); + }); + + it('[MasterKeyPage Test] MasterKeyPage should pass validation if page content is valid.', async () => { + let uiElement = new MasterKeyUiElements(); + uiElement.masterKeyPasswordInput = new MockInputBoxComponent(); + uiElement.masterKeyPasswordConfirmInput = new MockInputBoxComponent(); + + let env = new VirtualizeDataMockEnv(); + let dataModel = env.getMockedVirtualizeDataModel(); + let dataModelMock = env.getVirtualizeDataModelMock(); + let vdiManager = env.getMockedVDIManager(); + let vdiManagerMock = env.getVDIManagerMock(); + let appContext = env.getMockedAppContext(); + + let masterKeyPage = new CreateMasterKeyPage(dataModel, vdiManager, appContext); + masterKeyPage.setUi(uiElement); + + uiElement.masterKeyPasswordInput.value = 'test123'; + uiElement.masterKeyPasswordConfirmInput.value = 'test123'; + should(await masterKeyPage.validate()).be.true; + + dataModelMock.verify(x => x.showWizardError(TypeMoq.It.isAny()), TypeMoq.Times.never()); + dataModelMock.verify(x => x.validateInput(TypeMoq.It.isAny()), TypeMoq.Times.once()); + vdiManagerMock.verify(x => x.getVirtualizeDataInput(TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); +}); + +describe('ObjectMappingPage Tests', function (): void { + it('[ObjectMappingPage Test] ObjectMappingPage should create a wizard page and register content during the initialization.', async () => { + let env = new VirtualizeDataMockEnv(); + let objectMappingPage = env.getMockedObjectMappingPage(); + let apiWrapper = env.getApiWrapperMock(); + let wizardPage = env.getWizardPageMock(); + apiWrapper.verify(x => x.createWizardPage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + wizardPage.verify(x => x.registerContent(TypeMoq.It.isAny()), TypeMoq.Times.once()); + should(objectMappingPage).not.be.undefined(); + }); + + it('[ObjectMappingPage Test] ObjectMappingPage should get data from VirtualizeDataModel when the page is opened.', async () => { + let env = new VirtualizeDataMockEnv(); + let objectMappingPage = env.getMockedObjectMappingPage(); + await objectMappingPage.updatePage(); + let omPage = objectMappingPage; + let rootNode = omPage._treeRootNode; + should(rootNode).not.be.undefined(); + let databaseNodes = await rootNode.getChildren(); + should(databaseNodes).not.be.undefined(); + should(databaseNodes.length > 0).be.true(); + + let dataModelMock = env.getVirtualizeDataModelMock(); + dataModelMock.verify(x => x.getSourceDatabases(TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce()); + }); + + it('[ObjectMappingPage Test] Database node should generate children, and mark them as checked when database node is checked before generating children.', async () => { + let env = new VirtualizeDataMockEnv(); + let objectMappingPage = env.getMockedObjectMappingPage(); + await objectMappingPage.updatePage(); + let omPage = objectMappingPage; + let rootNode: CheckboxTreeNode = omPage._treeRootNode as CheckboxTreeNode; + let databaseNodes: CheckboxTreeNode[] = await rootNode.getChildren(); + should(databaseNodes).not.be.undefined(); + should(databaseNodes.length > 0).be.true(); + + should(databaseNodes[0].hasChildren).be.false(); + await omPage.actionOnNodeCheckStatusChanged(databaseNodes[0], true); + should(databaseNodes[0].hasChildren).be.true(); + let children: CheckboxTreeNode[] = await databaseNodes[0].getChildren(); + let tableFolderNode: CheckboxTreeNode = children[0]; + let viewFolderNode: CheckboxTreeNode = children[1]; + should(tableFolderNode.checked).be.true(); + should(viewFolderNode.checked).be.true(); + should(tableFolderNode.hasChildren).be.true(); + should(viewFolderNode.hasChildren).be.true(); + let tableNodes: CheckboxTreeNode[] = await tableFolderNode.getChildren(); + tableNodes.forEach(tableNode => { + should(tableNode.checked).be.true(); + }); + let viewNodes: CheckboxTreeNode[] = await viewFolderNode.getChildren(); + viewNodes.forEach(viewNode => { + should(viewNode.checked).be.true(); + }); + }); + + it('[ObjectMappingPage Test] Column definition mapping table should be updated when table or view node is selected.', async () => { + let env = new VirtualizeDataMockEnv(); + let objectMappingPage = env.getMockedObjectMappingPage(); + await objectMappingPage.updatePage(); + let omPage = objectMappingPage; + let rootNode: CheckboxTreeNode = omPage._treeRootNode as CheckboxTreeNode; + let databaseNodes: CheckboxTreeNode[] = await rootNode.getChildren(); + should(databaseNodes).not.be.undefined(); + should(databaseNodes.length > 0).be.true(); + let folderNodes: CheckboxTreeNode[] = await databaseNodes[0].getChildren(); + should(folderNodes).not.be.undefined(); + should(folderNodes.length === 2).be.true(); + let tableFolderNodes: CheckboxTreeNode = folderNodes[0]; + let tableNodes: CheckboxTreeNode[] = await tableFolderNodes.getChildren(); + should(tableNodes).not.be.undefined(); + should(tableNodes.length > 0).be.true(); + let viewFolderNodes: CheckboxTreeNode = folderNodes[1]; + let viewNodes: CheckboxTreeNode[] = await viewFolderNodes.getChildren(); + should(viewNodes).not.be.undefined(); + should(viewNodes.length > 0).be.true(); + + let colDefTable = omPage._columnMappingTable as azdata.DeclarativeTableComponent; + colDefTable.updateProperties({ data: undefined }); + should(colDefTable.data).be.undefined(); + await omPage.actionOnNodeIsSelected(tableNodes[0]); + should(colDefTable.data).not.be.undefined(); + should(colDefTable.data.length > 0).be.true(); + colDefTable.data.forEach(row => { + should(row.length > 0).be.true(); + should(row[0] !== undefined && row[0] !== '').be.true(); + }); + + let colDefs = (await omPage._dataSourceBrowser.getColumnDefinitions((tableNodes[0]).location)) as ColumnDefinition[]; + for (let i = 0; i < colDefs.length; ++i) { + should(colDefs[i].columnName === colDefTable.data[i][0]).be.true(); + should(colDefs[i].columnName === colDefTable.data[i][1]).be.true(); + should(colDefs[i].dataType === colDefTable.data[i][2]).be.true(); + should(colDefs[i].isNullable === colDefTable.data[i][3]).be.true(); + should(colDefs[i].collationName === colDefTable.data[i][4]).be.true(); + } + + colDefTable.updateProperties({ data: undefined }); + should(colDefTable.data).be.undefined(); + await omPage.actionOnNodeIsSelected(viewNodes[0]); + should(colDefTable.data).not.be.undefined(); + should(colDefTable.data.length > 0).be.true(); + colDefTable.data.forEach(row => { + should(row.length > 0).be.true(); + should(row[0] !== undefined && row[0] !== '').be.true(); + }); + + colDefs = (await omPage._dataSourceBrowser.getColumnDefinitions((viewNodes[0]).location)) as ColumnDefinition[]; + for (let i = 0; i < colDefs.length; ++i) { + should(colDefs[i].columnName === colDefTable.data[i][0]).be.true(); + should(colDefs[i].columnName === colDefTable.data[i][1]).be.true(); + should(colDefs[i].dataType === colDefTable.data[i][2]).be.true(); + should(colDefs[i].isNullable === colDefTable.data[i][3]).be.true(); + should(colDefs[i].collationName === colDefTable.data[i][4]).be.true(); + } + }); + + it('[ObjectMappingPage Test] ObjectMappingPage should return table information for creation for checked tables', async () => { + let env = new VirtualizeDataMockEnv(); + let objectMappingPage = env.getMockedObjectMappingPage(); + await objectMappingPage.updatePage(); + let omPage = objectMappingPage; + let rootNode: CheckboxTreeNode = omPage._treeRootNode as CheckboxTreeNode; + let databaseNodes: CheckboxTreeNode[] = await rootNode.getChildren(); + + for (let i = 0; i < databaseNodes.length; ++i) { + await omPage.actionOnNodeCheckStatusChanged(databaseNodes[i], true); + } + + let inputValues = VDIManager.getEmptyInputInstance(); + omPage.getInputValues(inputValues); + should(inputValues).not.be.undefined(); + should(inputValues.externalTableInfoList).not.be.undefined(); + let tablesToBeCreated = inputValues.externalTableInfoList; + should(tablesToBeCreated.length > 0).be.true(); + }); + + it('[ObjectMappingPage Test] Database nodes and table nodes should be able to expand successfully.', async () => { + let env = new VirtualizeDataMockEnv(); + let objectMappingPage = env.getMockedObjectMappingPage(); + await objectMappingPage.updatePage(); + let omPage = objectMappingPage; + let rootNode: CheckboxTreeNode = omPage._treeRootNode as CheckboxTreeNode; + let databaseNodes: CheckboxTreeNode[] = await rootNode.getChildren(); + should(rootNode.hasChildren).be.true(); + should(databaseNodes).not.be.undefined(); + should(databaseNodes.length > 0).be.true(); + + for (let i = 0; i < databaseNodes.length; ++i) { + let tableNodes: CheckboxTreeNode[] = await databaseNodes[i].getChildren(); + should(databaseNodes[i].hasChildren).be.True(); + should(tableNodes).not.be.undefined(); + should(tableNodes.length > 0).be.true(); + } + }); + + it('[ObjectMappingPage Test] Tree view should be updated every time objectMappingPage is loaded', async () => { + let env = new VirtualizeDataMockEnv(); + let objectMappingPage = env.getMockedObjectMappingPage(); + await objectMappingPage.updatePage(); + let omPage = objectMappingPage; + + let rootNode: CheckboxTreeNode = omPage._treeRootNode as CheckboxTreeNode; + should(rootNode.hasChildren).be.false(); + let databaseNodes: CheckboxTreeNode[] = await rootNode.getChildren(); + should(rootNode.hasChildren).be.true(); + + await objectMappingPage.updatePage(); + rootNode = omPage._treeRootNode as CheckboxTreeNode; + should(rootNode.hasChildren).be.false(); + }); + + it('[ObjectMappingPage Test] ObjectMappingPage should show table help text when the page is updated', async () => { + let env = new VirtualizeDataMockEnv(); + let objectMappingPage = env.getMockedObjectMappingPage(); + await objectMappingPage.updatePage(); + let omPage = objectMappingPage; + let objectMappingWrapper = omPage._objectMappingWrapper; + should(objectMappingWrapper).not.be.undefined(); + should(objectMappingWrapper.items.length === 1).be.true(); + }); +}); + +describe('Summary Page Tests', function (): void { + it('[SummaryPage Test] SummaryPage should create a wizard page and register content during the initialization.', async () => { + let env = new VirtualizeDataMockEnv(); + let dataModel = env.getMockedVirtualizeDataModel(); + let vdiManager = env.getMockedVDIManager(); + let apiContext = env.getMockedAppContext(); + let apiWrapperMock = env.getApiWrapperMock(); + let wizardPageMock = env.getWizardPageMock(); + + let summaryPage = new SummaryPage(dataModel, vdiManager, apiContext); + wizardPageMock.verify(x => x.registerContent(TypeMoq.It.isAny()), TypeMoq.Times.once()); + apiWrapperMock.verify(x => x.createWizardPage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + should(summaryPage).not.be.null(); + }); + + it('[SummaryPage Test] SummaryPage should get data from VirtualizeDataModel when the page is opened.', async () => { + let uiElement = new SummaryUiElements(); + uiElement.destDBLabel = new MockTextComponent(); + uiElement.summaryTable = new MockDeclarativeTableComponent(); + + let env = new VirtualizeDataMockEnv(); + let dataModel = env.getMockedVirtualizeDataModel(); + let vdiManager = env.getMockedVDIManager(); + let apiContext = env.getMockedAppContext(); + let vdiManagerMock = env.getVDIManagerMock(); + + let summaryPage = new SummaryPage(dataModel, vdiManager, apiContext); + summaryPage.setUi(uiElement); + await summaryPage.updatePage(); + + vdiManagerMock.verify(x => x.getVirtualizeDataInput(TypeMoq.It.isAny()), TypeMoq.Times.once()); + + let testData = env.getMockEnvConstants(); + should(uiElement.destDBLabel.value).be.eql(testData.destDbNameSelected); + + let summaryHasKey = function (key: string): boolean { + return uiElement.summaryTable.data.some(row => row && row[0] === key); + }; + let summaryHasValue = function (value: string): boolean { + return uiElement.summaryTable.data.some(row => row && row.length > 1 && row[1] === value); + }; + should(summaryHasValue(testData.newCredentialName)).be.true; + should(summaryHasValue(testData.newDataSourceName)).be.true; + should(summaryHasValue(testData.externalTableInfoList[0].externalTableName.join('.'))).be.true; + // No value is included for the master key row, so check the row's key instead + should(summaryHasKey('Database Master Key')).be.true; + }); + + it('[SummaryPage Test] SummaryPage should register task operation on wizard submit.', async () => { + let uiElement = new SummaryUiElements(); + uiElement.destDBLabel = new MockTextComponent(); + uiElement.summaryTable = new MockDeclarativeTableComponent(); + + let env = new VirtualizeDataMockEnv(); + let dataModel = env.getMockedVirtualizeDataModel(); + let vdiManager = env.getMockedVDIManager(); + let apiContext = env.getMockedAppContext(); + let wizardMock = env.getWizardMock(); + + let summaryPage = new SummaryPage(dataModel, vdiManager, apiContext); + summaryPage.setUi(uiElement); + + should(await summaryPage.validate()).be.true; + wizardMock.verify(x => x.registerOperation(TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); +}); diff --git a/extensions/datavirtualization/src/treeNodes.ts b/extensions/datavirtualization/src/treeNodes.ts new file mode 100644 index 0000000000..f726f9f614 --- /dev/null +++ b/extensions/datavirtualization/src/treeNodes.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import { ITreeNode } from './types'; + +type TreeNodePredicate = (node: TreeNode) => boolean; + +export abstract class TreeNode implements ITreeNode { + private _parent: TreeNode = undefined; + + public get parent(): TreeNode { + return this._parent; + } + + public set parent(node: TreeNode) { + this._parent = node; + } + + public generateNodePath(): string { + let path = undefined; + if (this.parent) { + path = this.parent.generateNodePath(); + } + path = path ? `${path}/${this.nodePathValue}` : this.nodePathValue; + return path; + } + + public findNodeByPath(path: string, expandIfNeeded: boolean = false): Promise { + let condition: TreeNodePredicate = (node: TreeNode) => node.getNodeInfo().nodePath === path; + let filter: TreeNodePredicate = (node: TreeNode) => path.startsWith(node.getNodeInfo().nodePath); + return TreeNode.findNode(this, condition, filter, true); + } + + public static async findNode(node: TreeNode, condition: TreeNodePredicate, filter: TreeNodePredicate, expandIfNeeded: boolean): Promise { + if (!node) { + return undefined; + } + + if (condition(node)) { + return node; + } + + let nodeInfo = node.getNodeInfo(); + if (nodeInfo.isLeaf) { + return undefined; + } + + // TODO #659 support filtering by already expanded / not yet expanded + let children = await node.getChildren(false); + if (children) { + for (let child of children) { + if (filter && filter(child)) { + let childNode = await this.findNode(child, condition, filter, expandIfNeeded); + if (childNode) { + return childNode; + } + } + } + } + return undefined; + } + + /** + * The value to use for this node in the node path + */ + public abstract get nodePathValue(): string; + + abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise; + abstract getTreeItem(): vscode.TreeItem | Promise; + + abstract getNodeInfo(): azdata.NodeInfo; +} diff --git a/extensions/datavirtualization/src/types.d.ts b/extensions/datavirtualization/src/types.d.ts new file mode 100644 index 0000000000..56c353c1c7 --- /dev/null +++ b/extensions/datavirtualization/src/types.d.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; + +/** + * The API provided by this extension + * + * @export + * @interface IExtensionApi + */ +export interface IExtensionApi { + /** + * Gets the object explorer API that supports querying over the connections supported by this extension + * + * @returns {IObjectExplorerBrowser} + * @memberof IExtensionApi + */ + getObjectExplorerBrowser(): IObjectExplorerBrowser; +} + +/** + * A browser supporting actions over the object explorer connections provided by this extension. + * Currently this is the + * + * @export + * @interface IObjectExplorerBrowser + */ +export interface IObjectExplorerBrowser { + /** + * Gets the matching node given a context object, e.g. one from a right-click on a node in Object Explorer + * + * @param {azdata.ObjectExplorerContext} objectExplorerContext + * @returns {Promise} + */ + getNode(objectExplorerContext: azdata.ObjectExplorerContext): Promise; +} + +/** + * A tree node in the object explorer tree + * + * @export + * @interface ITreeNode + */ +export interface ITreeNode { + getNodeInfo(): azdata.NodeInfo; + getChildren(refreshChildren: boolean): ITreeNode[] | Promise; +} + +/** + * A HDFS file node. This is a leaf node in the object explorer tree, and its contents + * can be queried + * + * @export + * @interface IFileNode + * @extends {ITreeNode} + */ +export interface IFileNode extends ITreeNode { + getFileContentsAsString(maxBytes?: number): Promise; +} diff --git a/extensions/datavirtualization/src/typings/globals/istanbul/index.d.ts b/extensions/datavirtualization/src/typings/globals/istanbul/index.d.ts new file mode 100644 index 0000000000..bb742ffe0b --- /dev/null +++ b/extensions/datavirtualization/src/typings/globals/istanbul/index.d.ts @@ -0,0 +1,72 @@ +// Generated by typings +// Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/istanbul/istanbul.d.ts +declare module 'istanbul' { + namespace istanbul { + interface Istanbul { + new (options?: any): Istanbul; + Collector: Collector; + config: Config; + ContentWriter: ContentWriter; + FileWriter: FileWriter; + hook: Hook; + Instrumenter: Instrumenter; + Report: Report; + Reporter: Reporter; + Store: Store; + utils: ObjectUtils; + VERSION: string; + Writer: Writer; + } + + interface Collector { + new (options?: any): Collector; + add(coverage: any, testName?: string): void; + } + + interface Config { + } + + interface ContentWriter { + } + + interface FileWriter { + } + + interface Hook { + hookRequire(matcher: any, transformer: any, options: any): void; + unhookRequire(): void; + } + + interface Instrumenter { + new (options?: any): Instrumenter; + instrumentSync(code: string, filename: string): string; + } + + interface Report { + } + + interface Configuration { + new (obj: any, overrides: any): Configuration; + } + + interface Reporter { + new (cfg?: Configuration, dir?: string): Reporter; + add(fmt: string): void; + addAll(fmts: Array): void; + write(collector: Collector, sync: boolean, callback: Function): void; + } + + interface Store { + } + + interface ObjectUtils { + } + + interface Writer { + } + } + + var istanbul: istanbul.Istanbul; + + export = istanbul; +} diff --git a/extensions/datavirtualization/src/typings/globals/istanbul/typings.json b/extensions/datavirtualization/src/typings/globals/istanbul/typings.json new file mode 100644 index 0000000000..9c1e6b1a52 --- /dev/null +++ b/extensions/datavirtualization/src/typings/globals/istanbul/typings.json @@ -0,0 +1,8 @@ +{ + "resolution": "main", + "tree": { + "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/istanbul/istanbul.d.ts", + "raw": "registry:dt/istanbul#0.4.0+20160316155526", + "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/istanbul/istanbul.d.ts" + } +} diff --git a/extensions/datavirtualization/src/typings/markdown-it-named-headers.d.ts b/extensions/datavirtualization/src/typings/markdown-it-named-headers.d.ts new file mode 100644 index 0000000000..de9b23d0df --- /dev/null +++ b/extensions/datavirtualization/src/typings/markdown-it-named-headers.d.ts @@ -0,0 +1,5 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +declare module 'markdown-it-named-headers' { } \ No newline at end of file diff --git a/extensions/datavirtualization/src/typings/mssqlapis.d.ts b/extensions/datavirtualization/src/typings/mssqlapis.d.ts new file mode 100644 index 0000000000..f5d0f0a354 --- /dev/null +++ b/extensions/datavirtualization/src/typings/mssqlapis.d.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// This is the place for extensions to expose APIs. + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; + +/** +* The APIs provided by Mssql extension +* +* @export +* @interface MssqlExtensionApi +*/ +export interface MssqlExtensionApi { + /** + * Gets the object explorer API that supports querying over the connections supported by this extension + * + * @returns {IMssqlObjectExplorerBrowser} + * @memberof IMssqlExtensionApi + */ + getMssqlObjectExplorerBrowser(): MssqlObjectExplorerBrowser; +} + +/** + * A browser supporting actions over the object explorer connections provided by this extension. + * Currently this is the + * + * @export + * @interface MssqlObjectExplorerBrowser + */ +export interface MssqlObjectExplorerBrowser { + /** + * Gets the matching node given a context object, e.g. one from a right-click on a node in Object Explorer + * + * @param {azdata.ObjectExplorerContext} objectExplorerContext + * @returns {Promise} + */ + getNode(objectExplorerContext: azdata.ObjectExplorerContext): Promise; +} + +/** + * A tree node in the object explorer tree + * + * @export + * @interface ITreeNode + */ +export interface ITreeNode { + getNodeInfo(): azdata.NodeInfo; + getChildren(refreshChildren: boolean): ITreeNode[] | Promise; +} + +/** + * A HDFS file node. This is a leaf node in the object explorer tree, and its contents + * can be queried + * + * @export + * @interface IFileNode + * @extends {ITreeNode} + */ +export interface IFileNode extends ITreeNode { + getFileContentsAsString(maxBytes?: number): Promise; +} diff --git a/extensions/datavirtualization/src/typings/ref.d.ts b/extensions/datavirtualization/src/typings/ref.d.ts new file mode 100644 index 0000000000..641bd7ffe9 --- /dev/null +++ b/extensions/datavirtualization/src/typings/ref.d.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// +/// diff --git a/extensions/datavirtualization/src/utils.ts b/extensions/datavirtualization/src/utils.ts new file mode 100644 index 0000000000..6a0f69cf27 --- /dev/null +++ b/extensions/datavirtualization/src/utils.ts @@ -0,0 +1,232 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as childProcess from 'child_process'; +import { ExecOptions } from 'child_process'; +import * as nls from 'vscode-nls'; +import * as path from 'path'; +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; + +import * as Constants from './constants'; +import { CategoryValue } from 'azdata'; + +const localize = nls.loadMessageBundle(); + +export function getDropdownValue(dropdownValue: string | CategoryValue): string { + if (typeof (dropdownValue) === 'string') { + return dropdownValue; + } else { + return dropdownValue ? (dropdownValue).name : undefined; + } +} + +/** + * Helper to log messages to the developer console if enabled + * @param msg Message to log to the console + */ +export function logDebug(msg: any): void { + let config = vscode.workspace.getConfiguration(Constants.extensionConfigSectionName); + let logDebugInfo = config[Constants.configLogDebugInfo]; + if (logDebugInfo === true) { + let currentTime = new Date().toLocaleTimeString(); + let outputMsg = '[' + currentTime + ']: ' + msg ? msg.toString() : ''; + console.log(outputMsg); + } +} + +export function getKnoxUrl(host: string, port: string): string { + return `https://${host}:${port}/gateway`; +} + +export function getLivyUrl(serverName: string, port: string): string { + return this.getKnoxUrl(serverName, port) + '/default/livy/v1/'; +} + +export function getTemplatePath(extensionPath: string, templateName: string): string { + return path.join(extensionPath, 'resources', templateName); +} + +export function getErrorMessage(error: Error | string): string { + return (error instanceof Error) ? error.message : error; +} + +// COMMAND EXECUTION HELPERS /////////////////////////////////////////////// +export function executeBufferedCommand(cmd: string, options: ExecOptions, outputChannel?: vscode.OutputChannel): Thenable { + return new Promise((resolve, reject) => { + if (outputChannel) { + outputChannel.appendLine(` > ${cmd}`); + } + + let child = childProcess.exec(cmd, options, (err, stdout) => { + if (err) { + reject(err); + } else { + resolve(stdout); + } + }); + + // Add listeners to print stdout and stderr if an output channel was provided + if (outputChannel) { + child.stdout.on('data', data => { outputDataChunk(data, outputChannel, ' stdout: '); }); + child.stderr.on('data', data => { outputDataChunk(data, outputChannel, ' stderr: '); }); + } + }); +} + +export function executeExitCodeCommand(cmd: string, outputChannel?: vscode.OutputChannel): Thenable { + return new Promise((resolve, reject) => { + if (outputChannel) { + outputChannel.appendLine(` > ${cmd}`); + } + + let child = childProcess.spawn(cmd, [], { shell: true, detached: false }); + + // Add listeners for the process to exit + child.on('error', reject); + child.on('exit', (code: number) => { resolve(code); }); + + // Add listeners to print stdout and stderr if an output channel was provided + if (outputChannel) { + child.stdout.on('data', data => { outputDataChunk(data, outputChannel, ' stdout: '); }); + child.stderr.on('data', data => { outputDataChunk(data, outputChannel, ' stderr: '); }); + } + }); +} + +export function executeStreamedCommand(cmd: string, outputChannel?: vscode.OutputChannel): Thenable { + return new Promise((resolve, reject) => { + // Start the command + if (outputChannel) { + outputChannel.appendLine(` > ${cmd}`); + } + let child = childProcess.spawn(cmd, [], { shell: true, detached: false }); + + // Add listeners to resolve/reject the promise on exit + child.on('error', reject); + child.on('exit', (code: number) => { + if (code === 0) { + resolve(); + } else { + reject(localize('executeCommandProcessExited', 'Process exited with code {0}', code)); + } + }); + + // Add listeners to print stdout and stderr if an output channel was provided + if (outputChannel) { + child.stdout.on('data', data => { outputDataChunk(data, outputChannel, ' stdout: '); }); + child.stderr.on('data', data => { outputDataChunk(data, outputChannel, ' stderr: '); }); + } + }); +} + +export function isObjectExplorerContext(object: any): object is azdata.ObjectExplorerContext { + return 'connectionProfile' in object && 'isConnectionNode' in object; +} + +export function getUserHome(): string { + return process.env.HOME || process.env.USERPROFILE; +} + +export enum Platform { + Mac, + Linux, + Windows, + Others +} + +export function getOSPlatform(): Platform { + switch (process.platform) { + case 'win32': + return Platform.Windows; + case 'darwin': + return Platform.Mac; + case 'linux': + return Platform.Linux; + default: + return Platform.Others; + } +} + +export function getOSPlatformId(): string { + var platformId = undefined; + switch (process.platform) { + case 'win32': + platformId = 'win-x64'; + break; + case 'darwin': + platformId = 'osx'; + break; + default: + platformId = 'linux-x64'; + break; + } + return platformId; +} + +// PRIVATE HELPERS ///////////////////////////////////////////////////////// +function outputDataChunk(data: string | Buffer, outputChannel: vscode.OutputChannel, header: string): void { + data.toString().split(/\r?\n/) + .forEach(line => { + outputChannel.appendLine(header + line); + }); +} + +export function clone(obj: T): T { + if (!obj || typeof obj !== 'object') { + return obj; + } + if (obj instanceof RegExp) { + // See https://github.com/Microsoft/TypeScript/issues/10990 + return obj as any; + } + const result = (Array.isArray(obj)) ? [] : {}; + Object.keys(obj).forEach(key => { + if (obj[key] && typeof obj[key] === 'object') { + result[key] = clone(obj[key]); + } else { + result[key] = obj[key]; + } + }); + return result; +} + +export function isValidNumber(maybeNumber: any) { + return maybeNumber !== undefined + && maybeNumber !== null + && maybeNumber !== '' + && !isNaN(Number(maybeNumber.toString())); +} + +/** + * Removes the leading and trailing slashes from the pathName portion of a URL. + * @param pathName Path name portion of a URL. + * @returns Cleaned pathName string, or empty string if pathName is undefined. + */ +export function stripUrlPathSlashes(pathName: string): string { + if (!pathName) { + return ''; + } + + // Exclude empty trailing slashes + const lastCharIndex = pathName.length - 1; + if (pathName.length > 0 && pathName[lastCharIndex] === '/') { + let parseEndIndex = 0; + for (let i = lastCharIndex; i >= 0; --i) { + if (pathName[i] !== '/') { + parseEndIndex = i + 1; + break; + } + } + pathName = pathName.slice(0, parseEndIndex); + } + + // Strip leading slash + if (pathName.length > 0 && pathName[0] === '/') { + pathName = pathName.slice(1); + } + + return pathName; +} diff --git a/extensions/datavirtualization/src/wizards/tableFromFile/api/importPage.ts b/extensions/datavirtualization/src/wizards/tableFromFile/api/importPage.ts new file mode 100644 index 0000000000..43aed381bf --- /dev/null +++ b/extensions/datavirtualization/src/wizards/tableFromFile/api/importPage.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; + +import { DataSourceWizardService } from '../../../services/contracts'; +import { ImportDataModel } from './models'; +import { TableFromFileWizard } from '../tableFromFileWizard'; + +export abstract class ImportPage { + + protected constructor( + protected readonly instance: TableFromFileWizard, + protected readonly wizardPage: azdata.window.WizardPage, + protected readonly model: ImportDataModel, + protected readonly view: azdata.ModelView, + protected readonly provider: DataSourceWizardService) { } + + /** + * This method constructs all the elements of the page. + * @returns {Promise} + */ + public abstract start(): Promise; + + /** + * This method is called when the user is entering the page. + * @returns {Promise} + */ + public abstract onPageEnter(): Promise; + + /** + * This method is called when the user is leaving the page. + * @returns {Promise} + */ + public abstract onPageLeave(clickedNext: boolean): Promise; +} diff --git a/extensions/datavirtualization/src/wizards/tableFromFile/api/models.ts b/extensions/datavirtualization/src/wizards/tableFromFile/api/models.ts new file mode 100644 index 0000000000..fad58ddefc --- /dev/null +++ b/extensions/datavirtualization/src/wizards/tableFromFile/api/models.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; + +import { FileNode } from '../../../hdfsProvider'; +import { ColumnDefinition, DataSourceInstance } from '../../../services/contracts'; + +/** + * The main data model that communicates between the pages. + */ +export interface ImportDataModel { + proseColumns: ColumnDefinition[]; + proseDataPreview: string[][]; + serverConn: azdata.connection.ConnectionProfile; + sessionId: string; + allDatabases: string[]; + versionInfo: { + serverMajorVersion: number; + productLevel: string; + }; + database: string; + existingDataSource: string; + newDataSource: DataSourceInstance; + table: string; + fileFormat: string; + existingSchema: string; + newSchema: string; + parentFile: { + isFolder: boolean; + filePath: string; + }; + proseParsingFile: FileNode; + fileType: string; + columnDelimiter: string; + firstRow: number; + quoteCharacter: string; +} diff --git a/extensions/datavirtualization/src/wizards/tableFromFile/pages/fileConfigPage.ts b/extensions/datavirtualization/src/wizards/tableFromFile/pages/fileConfigPage.ts new file mode 100644 index 0000000000..af5f0a0698 --- /dev/null +++ b/extensions/datavirtualization/src/wizards/tableFromFile/pages/fileConfigPage.ts @@ -0,0 +1,548 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +import * as path from 'path'; +import * as url from 'url'; + +import { ImportDataModel } from '../api/models'; +import { ImportPage } from '../api/importPage'; +import { TableFromFileWizard } from '../tableFromFileWizard'; +import { DataSourceWizardService, DatabaseInfo } from '../../../services/contracts'; +import { getDropdownValue, getErrorMessage, stripUrlPathSlashes } from '../../../utils'; +import { ctp24Version, sql2019MajorVersion, ctp25Version, ctp3Version } from '../../../constants'; + +const localize = nls.loadMessageBundle(); + +export class FileConfigPageUiElements { + public fileTextBox: azdata.TextComponent; + public serverTextBox: azdata.TextComponent; + public databaseDropdown: azdata.DropDownComponent; + public dataSourceDropdown: azdata.DropDownComponent; + public tableNameTextBox: azdata.InputBoxComponent; + public schemaDropdown: azdata.DropDownComponent; + public databaseLoader: azdata.LoadingComponent; + public dataSourceLoader: azdata.LoadingComponent; + public schemaLoader: azdata.LoadingComponent; + public fileFormatNameTextBox: azdata.InputBoxComponent; + public refreshButton: azdata.ButtonComponent; +} + +export class FileConfigPage extends ImportPage { + private ui: FileConfigPageUiElements; + public form: azdata.FormContainer; + + private readonly noDataSourcesError = localize('tableFromFileImport.noDataSources', 'No valid external data sources were found in the specified database.'); + private readonly noSchemasError = localize('tableFromFileImport.noSchemas', 'No user schemas were found in the specified database.'); + private readonly tableExistsError = localize('tableFromFileImport.tableExists', 'The specified table name already exists under the specified schema.'); + private readonly fileFormatExistsError = localize('tableFromFileImport.fileFormatExists', 'The specified external file format name already exists.'); + + private pageSetupComplete: boolean = false; + + private existingTableSet: Set; + private existingFileFormatSet: Set; + private existingSchemaSet: Set; + + public constructor(instance: TableFromFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: DataSourceWizardService) { + super(instance, wizardPage, model, view, provider); + } + + public setUi(ui: FileConfigPageUiElements) { + this.ui = ui; + } + + async start(): Promise { + this.ui = new FileConfigPageUiElements(); + let fileNameComponent = this.createFileTextBox(); + let serverNameComponent = this.createServerTextBox(); + let databaseComponent = this.createDatabaseDropdown(); + let dataSourceComponent = this.createDataSourceDropdown(); + let tableNameComponent = this.createTableNameBox(); + let schemaComponent = this.createSchemaDropdown(); + let fileFormatNameComponent = this.createFileFormatNameBox(); + let refreshButton = this.createRefreshButton(); + + this.form = this.view.modelBuilder.formContainer() + .withFormItems([ + fileNameComponent, + serverNameComponent, + databaseComponent, + dataSourceComponent, + tableNameComponent, + schemaComponent, + fileFormatNameComponent, + refreshButton + ]).component(); + + await this.view.initializeModel(this.form); + return true; + } + + async onPageEnter(): Promise { + if (!this.pageSetupComplete) { + this.instance.clearStatusMessage(); + this.toggleInputsEnabled(false, true); + try { + this.parseFileInfo(); + + await this.createSession(); + + await this.populateDatabaseDropdown(); + await this.populateDatabaseInfo(); + } finally { + this.toggleInputsEnabled(true, true); + } + this.pageSetupComplete = true; + } + } + + async onPageLeave(clickedNext: boolean): Promise { + if (this.ui.schemaLoader.loading || + this.ui.databaseLoader.loading || + this.ui.dataSourceLoader.loading || + !this.ui.refreshButton.enabled) { + return false; + } + + if (clickedNext) { + if ((this.model.newSchema === undefined || this.model.newSchema === '') && + (this.model.existingSchema === undefined || this.model.existingSchema === '')) { + return false; + } + + if (!this.model.newDataSource && + (this.model.existingDataSource === undefined || this.model.existingDataSource === '')) { + return false; + } + + if (this.model.existingSchema && this.model.existingSchema !== '' && + this.existingTableSet && this.existingTableSet.has(this.model.existingSchema + '.' + this.model.table)) { + this.instance.showErrorMessage(this.tableExistsError); + return false; + } + + if (this.existingFileFormatSet && this.existingFileFormatSet.has(this.model.fileFormat)) { + this.instance.showErrorMessage(this.fileFormatExistsError); + return false; + } + } + + return true; + } + + private async createSession(): Promise { + try { + this.ui.serverTextBox.value = this.model.serverConn.serverName; + + if (this.model.sessionId) { + await this.provider.disposeWizardSession(this.model.sessionId); + delete this.model.sessionId; + delete this.model.allDatabases; + delete this.model.versionInfo; + } + + let sessionResponse = await this.provider.createDataSourceWizardSession(this.model.serverConn); + + this.model.sessionId = sessionResponse.sessionId; + this.model.allDatabases = sessionResponse.databaseList.map(db => db.name); + this.model.versionInfo = { + serverMajorVersion: sessionResponse.serverMajorVersion, + productLevel: sessionResponse.productLevel + }; + } catch (err) { + this.instance.showErrorMessage(getErrorMessage(err)); + } + } + + private createDatabaseDropdown(): azdata.FormComponent { + this.ui.databaseDropdown = this.view.modelBuilder.dropDown().withProps({ + values: [''], + value: undefined + }).component(); + + // Handle database changes + this.ui.databaseDropdown.onValueChanged(async (db) => { + this.model.database = getDropdownValue(this.ui.databaseDropdown.value); + + this.instance.clearStatusMessage(); + this.toggleInputsEnabled(false, false); + try { + await this.populateDatabaseInfo(); + } finally { + this.toggleInputsEnabled(true, false); + } + }); + + this.ui.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.ui.databaseDropdown).component(); + + return { + component: this.ui.databaseLoader, + title: localize('tableFromFileImport.databaseDropdownTitle', 'Database the external table will be created in') + }; + } + + private async populateDatabaseDropdown(): Promise { + let idx = -1; + let count = -1; + let dbNames = await this.model.allDatabases.map(dbName => { + count++; + if (this.model.database && dbName === this.model.database) { + idx = count; + } + + return dbName; + }); + + if (idx >= 0) { + let tmp = dbNames[0]; + dbNames[0] = dbNames[idx]; + dbNames[idx] = tmp; + } + + this.model.database = dbNames[0]; + + this.ui.databaseDropdown.updateProperties({ + values: dbNames, + value: dbNames[0] + }); + return true; + } + + private createDataSourceDropdown(): azdata.FormComponent { + this.ui.dataSourceDropdown = this.view.modelBuilder.dropDown().withProps({ + values: [''], + value: undefined + }).component(); + + this.ui.dataSourceDropdown.onValueChanged(async (db) => { + if (!this.model.newDataSource) { + this.model.existingDataSource = getDropdownValue(this.ui.dataSourceDropdown.value); + } + }); + + this.ui.dataSourceLoader = this.view.modelBuilder.loadingComponent().withItem(this.ui.dataSourceDropdown).component(); + + return { + component: this.ui.dataSourceLoader, + title: localize('tableFromFileImport.dataSourceDropdown', 'External data source for new external table') + }; + } + + private populateDataSourceDropdown(dbInfo: DatabaseInfo): boolean { + let errorCleanup = (errorMsg: string = this.noDataSourcesError) => { + this.ui.dataSourceDropdown.updateProperties({ values: [''], value: undefined }); + this.instance.showErrorMessage(errorMsg); + this.model.existingDataSource = undefined; + this.model.newDataSource = undefined; + }; + if (!dbInfo || !dbInfo.externalDataSources) { + errorCleanup(); + return false; + } + + let expectedDataSourceHost: string; + let expectedDataSourcePort: string; + let expectedDataSourcePath = ''; + let majorVersion = this.model.versionInfo.serverMajorVersion; + let productLevel = this.model.versionInfo.productLevel; + + if (majorVersion === sql2019MajorVersion && productLevel === ctp24Version) { + expectedDataSourceHost = 'service-master-pool'; + expectedDataSourcePort = '50070'; + } else if (majorVersion === sql2019MajorVersion && productLevel === ctp25Version) { + expectedDataSourceHost = 'nmnode-0-svc'; + expectedDataSourcePort = '50070'; + } else if (majorVersion === sql2019MajorVersion && productLevel === ctp3Version) { + expectedDataSourceHost = 'controller-svc'; + expectedDataSourcePort = '8080'; + expectedDataSourcePath = 'default'; + } else { // Default: SQL 2019 CTP 3.1 syntax + expectedDataSourceHost = 'controller-svc'; + expectedDataSourcePort = null; + expectedDataSourcePath = 'default'; + } + + let filteredSources = dbInfo.externalDataSources.filter(dataSource => { + if (!dataSource.location) { + return false; + } + + let locationUrl = url.parse(dataSource.location); + let pathName = stripUrlPathSlashes(locationUrl.pathname); + return locationUrl.protocol === 'sqlhdfs:' + && locationUrl.hostname === expectedDataSourceHost + && locationUrl.port === expectedDataSourcePort + && pathName === expectedDataSourcePath; + }); + if (filteredSources.length === 0) { + let sourceName = 'SqlStoragePool'; + let nameSuffix = 0; + let existingNames = new Set(dbInfo.externalDataSources.map(dataSource => dataSource.name)); + while (existingNames.has(sourceName)) { + sourceName = `SqlStoragePool${++nameSuffix}`; + } + + let storageLocation: string; + if (expectedDataSourcePort !== null) { + storageLocation = `sqlhdfs://${expectedDataSourceHost}:${expectedDataSourcePort}/${expectedDataSourcePath}`; + } else { + storageLocation = `sqlhdfs://${expectedDataSourceHost}/${expectedDataSourcePath}`; + } + this.model.newDataSource = { + name: sourceName, + location: storageLocation, + authenticationType: undefined, + username: undefined, + credentialName: undefined + }; + filteredSources.unshift(this.model.newDataSource); + } else { + this.model.newDataSource = undefined; + } + + let idx = -1; + let count = -1; + let dataSourceNames = filteredSources.map(dataSource => { + let sourceName = dataSource.name; + count++; + if ((this.model.existingDataSource && sourceName === this.model.existingDataSource) || + (this.model.newDataSource && sourceName === this.model.newDataSource.name)) { + idx = count; + } + + return sourceName; + }); + + if (idx >= 0) { + let tmp = dataSourceNames[0]; + dataSourceNames[0] = dataSourceNames[idx]; + dataSourceNames[idx] = tmp; + } + + if (this.model.newDataSource) { + this.model.existingDataSource = undefined; + } else { + this.model.existingDataSource = dataSourceNames[0]; + } + + this.ui.dataSourceDropdown.updateProperties({ + values: dataSourceNames, + value: dataSourceNames[0] + }); + + return true; + } + + private createFileTextBox(): azdata.FormComponent { + this.ui.fileTextBox = this.view.modelBuilder.text().component(); + let title = this.model.parentFile.isFolder + ? localize('tableFromFileImport.folderTextboxTitle', 'Source Folder') + : localize('tableFromFileImport.fileTextboxTitle', 'Source File'); + return { + component: this.ui.fileTextBox, + title: title + }; + } + + private createServerTextBox(): azdata.FormComponent { + this.ui.serverTextBox = this.view.modelBuilder.text().component(); + return { + component: this.ui.serverTextBox, + title: localize('tableFromFileImport.destConnTitle', 'Destination Server') + }; + } + + private parseFileInfo(): void { + let parentFilePath = this.model.parentFile.filePath; + this.ui.fileTextBox.value = parentFilePath; + + let parsingFileExtension = path.extname(this.model.proseParsingFile.hdfsPath); + if (parsingFileExtension.toLowerCase() === '.json') { + this.model.fileType = 'JSON'; + } else { + this.model.fileType = 'TXT'; + } + + let parentBaseName = path.basename(parentFilePath, parsingFileExtension); + + this.ui.tableNameTextBox.value = parentBaseName; + this.model.table = this.ui.tableNameTextBox.value; + this.ui.tableNameTextBox.validate(); + + this.ui.fileFormatNameTextBox.value = `FileFormat_${parentBaseName}`; + this.model.fileFormat = this.ui.fileFormatNameTextBox.value; + this.ui.fileFormatNameTextBox.validate(); + } + + private createTableNameBox(): azdata.FormComponent { + this.ui.tableNameTextBox = this.view.modelBuilder.inputBox() + .withValidation((name) => { + let tableName = name.value; + if (!tableName || tableName.length === 0) { + return false; + } + return true; + }).withProperties({ + required: true, + }).component(); + + this.ui.tableNameTextBox.onTextChanged((tableName) => { + this.model.table = tableName; + }); + + return { + component: this.ui.tableNameTextBox, + title: localize('tableFromFileImport.tableTextboxTitle', 'Name for new external table '), + }; + } + + private createFileFormatNameBox(): azdata.FormComponent { + this.ui.fileFormatNameTextBox = this.view.modelBuilder.inputBox() + .withValidation((name) => { + let fileFormat = name.value; + if (!fileFormat || fileFormat.length === 0) { + return false; + } + return true; + }).withProperties({ + required: true, + }).component(); + + this.ui.fileFormatNameTextBox.onTextChanged((fileFormat) => { + this.model.fileFormat = fileFormat; + }); + + return { + component: this.ui.fileFormatNameTextBox, + title: localize('tableFromFileImport.fileFormatTextboxTitle', 'Name for new table\'s external file format'), + }; + } + + private createSchemaDropdown(): azdata.FormComponent { + this.ui.schemaDropdown = this.view.modelBuilder.dropDown().withProps({ + values: [''], + value: undefined, + editable: true, + fireOnTextChange: true + }).component(); + this.ui.schemaLoader = this.view.modelBuilder.loadingComponent().withItem(this.ui.schemaDropdown).component(); + + this.ui.schemaDropdown.onValueChanged(() => { + let schema = getDropdownValue(this.ui.schemaDropdown.value); + if (this.existingSchemaSet.has(schema)) { + this.model.newSchema = undefined; + this.model.existingSchema = schema; + } else { + this.model.newSchema = schema; + this.model.existingSchema = undefined; + } + }); + + return { + component: this.ui.schemaLoader, + title: localize('tableFromFileImport.schemaTextboxTitle', 'Schema for new external table'), + }; + } + + private populateSchemaDropdown(dbInfo: DatabaseInfo): boolean { + if (!dbInfo || !dbInfo.schemaList || dbInfo.schemaList.length === 0) { + this.ui.schemaDropdown.updateProperties({ values: [''], value: undefined }); + + this.instance.showErrorMessage(this.noSchemasError); + this.model.newSchema = undefined; + this.model.existingSchema = undefined; + return false; + } + + this.model.newSchema = undefined; + if (!this.model.existingSchema) { + this.model.existingSchema = dbInfo.defaultSchema; + } + + let idx = -1; + let count = -1; + + let values = dbInfo.schemaList.map(schema => { + count++; + if (this.model.existingSchema && schema === this.model.existingSchema) { + idx = count; + } + return schema; + }); + + if (idx >= 0) { + let tmp = values[0]; + values[0] = values[idx]; + values[idx] = tmp; + } else { + // Default schema wasn't in the list, so take the first one instead + this.model.existingSchema = values[0]; + } + + this.ui.schemaDropdown.updateProperties({ + values: values, + value: values[0] + }); + + return true; + } + + private async refreshPage(): Promise { + this.pageSetupComplete = false; + await this.onPageEnter(); + } + + private createRefreshButton(): azdata.FormComponent { + this.ui.refreshButton = this.view.modelBuilder.button().withProps({ + label: localize('tableFromFileImport.refreshButtonTitle', 'Refresh') + }).component(); + + this.ui.refreshButton.onDidClick(async () => await this.refreshPage()); + + return { + component: this.ui.refreshButton, + title: undefined + }; + } + + private async populateDatabaseInfo(): Promise { + try { + let dbInfo: DatabaseInfo = undefined; + let dbInfoResponse = await this.provider.getDatabaseInfo({ sessionId: this.model.sessionId, databaseName: this.model.database }); + if (!dbInfoResponse.isSuccess) { + this.instance.showErrorMessage(dbInfoResponse.errorMessages.join('\n')); + this.existingTableSet = undefined; + this.existingFileFormatSet = undefined; + this.existingSchemaSet = undefined; + } else { + dbInfo = dbInfoResponse.databaseInfo; + this.existingTableSet = new Set(dbInfo.externalTables.map(table => table.schemaName + '.' + table.tableName)); + this.existingFileFormatSet = new Set(dbInfo.externalFileFormats); + this.existingSchemaSet = new Set(dbInfo.schemaList); + } + + let r1 = this.populateDataSourceDropdown(dbInfo); + let r2 = this.populateSchemaDropdown(dbInfo); + return r1 && r2; + } catch (err) { + this.instance.showErrorMessage(getErrorMessage(err)); + } + } + + private toggleInputsEnabled(enable: boolean, includeDbLoader: boolean) { + if (includeDbLoader) { + this.ui.databaseLoader.loading = !enable; + } + + this.ui.databaseDropdown.enabled = enable; + this.ui.refreshButton.enabled = enable; + this.ui.dataSourceDropdown.enabled = enable; + this.ui.schemaDropdown.enabled = enable; + + this.ui.dataSourceLoader.loading = !enable; + this.ui.schemaLoader.loading = !enable; + } +} diff --git a/extensions/datavirtualization/src/wizards/tableFromFile/pages/modifyColumnsPage.ts b/extensions/datavirtualization/src/wizards/tableFromFile/pages/modifyColumnsPage.ts new file mode 100644 index 0000000000..86865b03ad --- /dev/null +++ b/extensions/datavirtualization/src/wizards/tableFromFile/pages/modifyColumnsPage.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; + +import { ImportDataModel } from '../api/models'; +import { ImportPage } from '../api/importPage'; +import { TableFromFileWizard } from '../tableFromFileWizard'; +import { DataSourceWizardService, ColumnDefinition } from '../../../services/contracts'; + +const localize = nls.loadMessageBundle(); + +export class ModifyColumnsPageUiElements { + public table: azdata.DeclarativeTableComponent; + public loading: azdata.LoadingComponent; + public text: azdata.TextComponent; +} + +export class ModifyColumnsPage extends ImportPage { + private readonly categoryValues = [ + { name: 'bigint', displayName: 'bigint' }, + { name: 'binary(50)', displayName: 'binary(50)' }, + { name: 'bit', displayName: 'bit' }, + { name: 'char(10)', displayName: 'char(10)' }, + { name: 'date', displayName: 'date' }, + { name: 'datetime', displayName: 'datetime' }, + { name: 'datetime2(7)', displayName: 'datetime2(7)' }, + { name: 'datetimeoffset(7)', displayName: 'datetimeoffset(7)' }, + { name: 'decimal(18, 10)', displayName: 'decimal(18, 10)' }, + { name: 'float', displayName: 'float' }, + { name: 'geography', displayName: 'geography' }, + { name: 'geometry', displayName: 'geometry' }, + { name: 'hierarchyid', displayName: 'hierarchyid' }, + { name: 'int', displayName: 'int' }, + { name: 'money', displayName: 'money' }, + { name: 'nchar(10)', displayName: 'nchar(10)' }, + { name: 'ntext', displayName: 'ntext' }, + { name: 'numeric(18, 0)', displayName: 'numeric(18, 0)' }, + { name: 'nvarchar(50)', displayName: 'nvarchar(50)' }, + { name: 'nvarchar(MAX)', displayName: 'nvarchar(MAX)' }, + { name: 'real', displayName: 'real' }, + { name: 'smalldatetime', displayName: 'smalldatetime' }, + { name: 'smallint', displayName: 'smallint' }, + { name: 'smallmoney', displayName: 'smallmoney' }, + { name: 'sql_variant', displayName: 'sql_variant' }, + { name: 'text', displayName: 'text' }, + { name: 'time(7)', displayName: 'time(7)' }, + { name: 'timestamp', displayName: 'timestamp' }, + { name: 'tinyint', displayName: 'tinyint' }, + { name: 'uniqueidentifier', displayName: 'uniqueidentifier' }, + { name: 'varbinary(50)', displayName: 'varbinary(50)' }, + { name: 'varbinary(MAX)', displayName: 'varbinary(MAX)' }, + { name: 'varchar(50)', displayName: 'varchar(50)' }, + { name: 'varchar(MAX)', displayName: 'varchar(MAX)' } + ]; + private ui: ModifyColumnsPageUiElements; + private form: azdata.FormContainer; + + public constructor(instance: TableFromFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: DataSourceWizardService) { + super(instance, wizardPage, model, view, provider); + } + + public setUi(ui: ModifyColumnsPageUiElements) { + this.ui = ui; + } + + private static convertMetadata(column: ColumnDefinition): any[] { + return [column.columnName, column.dataType, column.isNullable]; + } + + async start(): Promise { + this.ui = new ModifyColumnsPageUiElements(); + this.ui.loading = this.view.modelBuilder.loadingComponent().component(); + this.ui.table = this.view.modelBuilder.declarativeTable().component(); + this.ui.text = this.view.modelBuilder.text().component(); + + this.ui.table.onDataChanged((e) => { + this.model.proseColumns = []; + this.ui.table.data.forEach((row) => { + this.model.proseColumns.push({ + columnName: row[0], + dataType: row[1], + isNullable: row[2], + collationName: undefined + }); + }); + }); + + this.form = this.view.modelBuilder.formContainer() + .withFormItems( + [ + { + component: this.ui.text, + title: '' + }, + { + component: this.ui.table, + title: '' + } + ], { + horizontal: false, + componentWidth: '100%' + }).component(); + + this.ui.loading.component = this.form; + await this.view.initializeModel(this.form); + return true; + } + + async onPageEnter(): Promise { + this.ui.loading.loading = true; + await this.populateTable(); + this.ui.loading.loading = false; + } + + async onPageLeave(clickedNext: boolean): Promise { + if (this.ui.loading.loading) { + return false; + } + return true; + } + + private async populateTable() { + let data: any[][] = []; + + this.model.proseColumns.forEach((column) => { + data.push(ModifyColumnsPage.convertMetadata(column)); + }); + + this.ui.table.updateProperties({ + columns: [{ + displayName: localize('tableFromFileImport.columnName', 'Column Name'), + valueType: azdata.DeclarativeDataType.string, + width: '150px', + isReadOnly: false + }, { + displayName: localize('tableFromFileImport.dataType', 'Data Type'), + valueType: azdata.DeclarativeDataType.editableCategory, + width: '150px', + isReadOnly: false, + categoryValues: this.categoryValues + }, { + displayName: localize('tableFromFileImport.allowNulls', 'Allow Nulls'), + valueType: azdata.DeclarativeDataType.boolean, + isReadOnly: false, + width: '100px' + }], + data: data + }); + } +} diff --git a/extensions/datavirtualization/src/wizards/tableFromFile/pages/prosePreviewPage.ts b/extensions/datavirtualization/src/wizards/tableFromFile/pages/prosePreviewPage.ts new file mode 100644 index 0000000000..47b3cad824 --- /dev/null +++ b/extensions/datavirtualization/src/wizards/tableFromFile/pages/prosePreviewPage.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +import * as vscode from 'vscode'; + +import { ImportDataModel } from '../api/models'; +import { ImportPage } from '../api/importPage'; +import { TableFromFileWizard } from '../tableFromFileWizard'; +import { DataSourceWizardService, ColumnDefinition, ProseDiscoveryResponse } from '../../../services/contracts'; +import { getErrorMessage } from '../../../utils'; +import { extensionConfigSectionName, configProseParsingMaxLines, proseMaxLinesDefault } from '../../../constants'; + +const localize = nls.loadMessageBundle(); + +export class ProsePreviewPageUiElements { + public table: azdata.TableComponent; + public loading: azdata.LoadingComponent; +} + +export class ProsePreviewPage extends ImportPage { + private ui: ProsePreviewPageUiElements; + private form: azdata.FormContainer; + private proseParsingComplete: Promise; + + public constructor(instance: TableFromFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: DataSourceWizardService) { + super(instance, wizardPage, model, view, provider); + + this.proseParsingComplete = this.doProseDiscovery(); + } + + public setUi(ui: ProsePreviewPageUiElements) { + this.ui = ui; + } + + async start(): Promise { + this.ui = new ProsePreviewPageUiElements(); + this.ui.table = this.view.modelBuilder.table().component(); + this.ui.loading = this.view.modelBuilder.loadingComponent().component(); + + this.form = this.view.modelBuilder.formContainer().withFormItems([ + { + component: this.ui.table, + title: localize('tableFromFileImport.prosePreviewMessage', 'This operation analyzed the input file structure to generate the preview below for up to the first 50 rows.') + } + ]).component(); + + this.ui.loading.component = this.form; + + await this.view.initializeModel(this.ui.loading); + + return true; + } + + async onPageEnter(): Promise { + if (!this.model.proseDataPreview) { + this.ui.loading.loading = true; + await this.handleProsePreview(); + this.ui.loading.loading = false; + + await this.populateTable(this.model.proseDataPreview, this.model.proseColumns); + } + } + + async onPageLeave(clickedNext: boolean): Promise { + if (this.ui.loading.loading) { + return false; + } + + if (clickedNext) { + // Should have shown an error for these already in the loading step + return this.model.proseDataPreview !== undefined && this.model.proseColumns !== undefined; + } else { + return true; + } + } + + private async doProseDiscovery(): Promise { + let maxLines = proseMaxLinesDefault; + let config = vscode.workspace.getConfiguration(extensionConfigSectionName); + if (config) { + let maxLinesConfig = config[configProseParsingMaxLines]; + if (maxLinesConfig) { + maxLines = maxLinesConfig; + } + } + + let contents = await this.model.proseParsingFile.getFileLinesAsString(maxLines); + + return this.provider.sendProseDiscoveryRequest({ + filePath: undefined, + tableName: this.model.table, + schemaName: this.model.newSchema ? this.model.newSchema : this.model.existingSchema, + fileType: this.model.fileType, + fileContents: contents + }); + } + + private async handleProsePreview() { + let result: ProseDiscoveryResponse; + try { + result = await this.proseParsingComplete; + } catch (err) { + this.instance.showErrorMessage(getErrorMessage(err)); + return; + } + + if (!result || !result.dataPreview) { + this.instance.showErrorMessage(localize('tableFromFileImport.noPreviewData', 'Failed to retrieve any data from the specified file.')); + return; + } + + if (!result.columnInfo) { + this.instance.showErrorMessage(localize('tableFromFileImport.noProseInfo', 'Failed to generate column information for the specified file.')); + return; + } + + this.model.proseDataPreview = result.dataPreview; + + this.model.proseColumns = []; + result.columnInfo.forEach((column) => { + this.model.proseColumns.push({ + columnName: column.name, + dataType: column.sqlType, + isNullable: column.isNullable, + collationName: undefined + }); + }); + + let unquoteString = (value: string): string => { + return value ? value.replace(/^"(.*)"$/, '$1') : undefined; + }; + this.model.columnDelimiter = unquoteString(result.columnDelimiter); + this.model.firstRow = result.firstRow; + this.model.quoteCharacter = unquoteString(result.quoteCharacter); + } + + private async populateTable(tableData: string[][], columns: ColumnDefinition[]) { + let columnHeaders: string[] = columns ? columns.map(c => c.columnName) : undefined; + + let rows; + const maxRows = 50; + if (tableData && tableData.length > maxRows) { + rows = tableData.slice(0, maxRows); + } else { + rows = tableData; + } + + this.ui.table.updateProperties({ + data: rows, + columns: columnHeaders, + height: 600, + width: 800 + }); + } +} diff --git a/extensions/datavirtualization/src/wizards/tableFromFile/pages/summaryPage.ts b/extensions/datavirtualization/src/wizards/tableFromFile/pages/summaryPage.ts new file mode 100644 index 0000000000..ff995c8a98 --- /dev/null +++ b/extensions/datavirtualization/src/wizards/tableFromFile/pages/summaryPage.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; + +import { ImportDataModel } from '../api/models'; +import { ImportPage } from '../api/importPage'; +import { TableFromFileWizard } from '../tableFromFileWizard'; +import { DataSourceWizardService } from '../../../services/contracts'; + +const localize = nls.loadMessageBundle(); + +export class SummaryPageUiElements { + public table: azdata.TableComponent; +} + +export class SummaryPage extends ImportPage { + private ui: SummaryPageUiElements; + private form: azdata.FormContainer; + + public constructor(instance: TableFromFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: DataSourceWizardService) { + super(instance, wizardPage, model, view, provider); + } + + public setUi(ui: SummaryPageUiElements) { + this.ui = ui; + } + + async start(): Promise { + this.ui = new SummaryPageUiElements(); + this.ui.table = this.view.modelBuilder.table().component(); + + this.form = this.view.modelBuilder.formContainer().withFormItems( + [{ + component: this.ui.table, + title: localize('tableFromFileImport.importInformation', 'Data Virtualization information') + }] + ).component(); + + await this.view.initializeModel(this.form); + return true; + } + + async onPageEnter(): Promise { + this.instance.changeDoneButtonLabel(localize('tableFromFileImport.importData', 'Virtualize Data')); + this.instance.setGenerateScriptVisibility(true); + + this.populateTable(); + } + + async onPageLeave(clickedNext: boolean): Promise { + this.instance.changeDoneButtonLabel(localize('tableFromFileImport.next', 'Next')); + this.instance.setGenerateScriptVisibility(false); + return true; + } + + private populateTable() { + let sourceTitle = this.model.parentFile.isFolder + ? localize('tableFromFileImport.summaryFolderName', 'Source Folder') + : localize('tableFromFileImport.summaryFileName', 'Source File'); + + this.ui.table.updateProperties({ + data: [ + [localize('tableFromFileImport.serverName', 'Server name'), this.model.serverConn.serverName], + [localize('tableFromFileImport.databaseName', 'Database name'), this.model.database], + [localize('tableFromFileImport.tableName', 'Table name'), this.model.table], + [localize('tableFromFileImport.tableSchema', 'Table schema'), this.model.newSchema ? this.model.newSchema : this.model.existingSchema], + [localize('tableFromFileImport.fileFormat', 'File format name'), this.model.fileFormat], + [sourceTitle, this.model.parentFile.filePath] + ], + columns: ['Object type', 'Name'], + width: 600, + height: 200 + }); + } +} diff --git a/extensions/datavirtualization/src/wizards/tableFromFile/tableFromFileWizard.ts b/extensions/datavirtualization/src/wizards/tableFromFile/tableFromFileWizard.ts new file mode 100644 index 0000000000..f687023888 --- /dev/null +++ b/extensions/datavirtualization/src/wizards/tableFromFile/tableFromFileWizard.ts @@ -0,0 +1,329 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import * as azdata from 'azdata'; +import * as path from 'path'; +import * as url from 'url'; +import * as utils from '../../utils'; + +import { ImportDataModel } from './api/models'; +import { ImportPage } from './api/importPage'; +import { FileConfigPage } from './pages/fileConfigPage'; +import { ProsePreviewPage } from './pages/prosePreviewPage'; +import { ModifyColumnsPage } from './pages/modifyColumnsPage'; +import { SummaryPage } from './pages/summaryPage'; +import { DataSourceWizardService, VirtualizeDataInput } from '../../services/contracts'; +import { HdfsFileSourceNode, FileNode } from '../../hdfsProvider'; +import { AppContext } from '../../appContext'; +import { TreeNode } from '../../treeNodes'; +import { HdfsItems, MssqlClusterItems, DataSourceType, delimitedTextFileType } from '../../constants'; + +const localize = nls.loadMessageBundle(); + +export class TableFromFileWizard { + private readonly connection: azdata.connection.ConnectionProfile; + private readonly appContext: AppContext; + private readonly provider: DataSourceWizardService; + private wizard: azdata.window.Wizard; + private model: ImportDataModel; + + constructor(connection: azdata.connection.ConnectionProfile, appContext: AppContext, provider: DataSourceWizardService) { + this.connection = connection; + this.appContext = appContext; + this.provider = provider; + } + + public async start(hdfsFileNode: HdfsFileSourceNode, ...args: any[]) { + if (!hdfsFileNode) { + vscode.window.showErrorMessage(localize('import.needFile', 'Please select a source file or folder before using this wizard.')); + return; + } + + let noCsvError = localize('tableFromFileImport.onlyCsvSupported', 'Currently only csv files are supported for this wizard.'); + let proseParsingFile: FileNode; + let parentIsFolder = false; + + let isFolder = (node: TreeNode): boolean => { + let nodeType = node.getNodeInfo().nodeType; + return nodeType === HdfsItems.Folder || nodeType === MssqlClusterItems.Folder; + }; + if (isFolder(hdfsFileNode)) { + let visibleFilesFilter = node => { + // Polybase excludes files that start with '.' or '_', so skip these + // files when trying to find a file to run prose discovery on + if (node.hdfsPath) { + let baseName = path.basename(node.hdfsPath); + return baseName.length > 0 && baseName[0] !== '.' && baseName[0] !== '_'; + } + return false; + }; + let nodeSearch = async (condition) => TreeNode.findNode(hdfsFileNode, condition, visibleFilesFilter, true); + + let nonCsvFile = await nodeSearch(node => { + return !isFolder(node) && path.extname(node.hdfsPath).toLowerCase() !== '.csv'; + }); + + if (nonCsvFile) { + vscode.window.showErrorMessage(noCsvError); + return; + } + + let csvFile = await nodeSearch(node => { + return !isFolder(node) && path.extname(node.hdfsPath).toLowerCase() === '.csv'; + }) as FileNode; + + if (!csvFile) { + vscode.window.showErrorMessage(localize('tableFromFileImport.noCsvFileFound', 'No csv files were found in the specified folder.')); + return; + } + + parentIsFolder = true; + proseParsingFile = csvFile; + } else { + if (path.extname(hdfsFileNode.hdfsPath).toLowerCase() !== '.csv') { + vscode.window.showErrorMessage(noCsvError); + return; + } + + proseParsingFile = hdfsFileNode as FileNode; + } + + this.model = { + parentFile: { + isFolder: parentIsFolder, + filePath: hdfsFileNode.hdfsPath + }, + proseParsingFile: proseParsingFile, + serverConn: this.connection + }; + + let pages: Map = new Map(); + + this.wizard = azdata.window.createWizard(localize('tableFromFileImport.wizardName', 'Virtualize Data From CSV')); + let page0 = azdata.window.createWizardPage(localize('tableFromFileImport.page0Name', 'Select the destination database for your external table')); + let page1 = azdata.window.createWizardPage(localize('tableFromFileImport.page1Name', 'Preview Data')); + let page2 = azdata.window.createWizardPage(localize('tableFromFileImport.page2Name', 'Modify Columns')); + let page3 = azdata.window.createWizardPage(localize('tableFromFileImport.page3Name', 'Summary')); + + let fileConfigPage: FileConfigPage; + page0.registerContent(async (view) => { + fileConfigPage = new FileConfigPage(this, page0, this.model, view, this.provider); + pages.set(0, fileConfigPage); + await fileConfigPage.start().then(() => { + fileConfigPage.onPageEnter(); + }); + }); + + let prosePreviewPage: ProsePreviewPage; + page1.registerContent(async (view) => { + prosePreviewPage = new ProsePreviewPage(this, page1, this.model, view, this.provider); + pages.set(1, prosePreviewPage); + await prosePreviewPage.start(); + }); + + let modifyColumnsPage: ModifyColumnsPage; + page2.registerContent(async (view) => { + modifyColumnsPage = new ModifyColumnsPage(this, page2, this.model, view, this.provider); + pages.set(2, modifyColumnsPage); + await modifyColumnsPage.start(); + }); + + let summaryPage: SummaryPage; + page3.registerContent(async (view) => { + summaryPage = new SummaryPage(this, page3, this.model, view, this.provider); + pages.set(3, summaryPage); + await summaryPage.start(); + }); + + this.wizard.onPageChanged(async info => { + let newPage = pages.get(info.newPage); + if (newPage) { + await newPage.onPageEnter(); + } + }); + + this.wizard.registerNavigationValidator(async (info) => { + let lastPage = pages.get(info.lastPage); + let newPage = pages.get(info.newPage); + + // Hit "next" on last page, so handle submit + let nextOnLastPage = !newPage && lastPage instanceof SummaryPage; + if (nextOnLastPage) { + let createSuccess = await this.handleVirtualizeData(); + if (createSuccess) { + this.showTaskComplete(); + } + return createSuccess; + } + + if (lastPage) { + let clickedNext = nextOnLastPage || info.newPage > info.lastPage; + let pageValid = await lastPage.onPageLeave(clickedNext); + if (!pageValid) { + return false; + } + } + + this.clearStatusMessage(); + return true; + }); + + let cleanupSession = async () => { + try { + if (this.model.sessionId) { + await this.provider.disposeWizardSession(this.model.sessionId); + delete this.model.sessionId; + delete this.model.allDatabases; + } + } catch (error) { + this.appContext.apiWrapper.showErrorMessage(error.toString()); + } + }; + this.wizard.cancelButton.onClick(() => { + cleanupSession(); + }); + this.wizard.doneButton.onClick(() => { + cleanupSession(); + }); + + this.wizard.generateScriptButton.hidden = true; + this.wizard.generateScriptButton.onClick(async () => { + let input = TableFromFileWizard.generateInputFromModel(this.model); + let generateScriptResponse = await this.provider.generateScript(input); + if (generateScriptResponse.isSuccess) { + let doc = await this.appContext.apiWrapper.openTextDocument({ language: 'sql', content: generateScriptResponse.script }); + await this.appContext.apiWrapper.showDocument(doc); + this.showInfoMessage( + localize('tableFromFileImport.openScriptMsg', + 'The script has opened in a document window. You can view it once the wizard is closed.')); + } else { + this.showErrorMessage(generateScriptResponse.errorMessages.join('\n')); + } + }); + + this.wizard.pages = [page0, page1, page2, page3]; + + this.wizard.open(); + } + + public setGenerateScriptVisibility(visible: boolean) { + this.wizard.generateScriptButton.hidden = !visible; + } + + public registerNavigationValidator(validator: (pageChangeInfo: azdata.window.WizardPageChangeInfo) => boolean) { + this.wizard.registerNavigationValidator(validator); + } + + public changeDoneButtonLabel(label: string) { + this.wizard.doneButton.label = label; + } + + public showErrorMessage(errorMsg: string) { + this.showStatusMessage(errorMsg, azdata.window.MessageLevel.Error); + } + + public showInfoMessage(infoMsg: string) { + this.showStatusMessage(infoMsg, azdata.window.MessageLevel.Information); + } + + private async getConnectionInfo(): Promise { + let serverConn = await azdata.connection.getCurrentConnection(); + if (serverConn) { + let credentials = await azdata.connection.getCredentials(serverConn.connectionId); + if (credentials) { + Object.assign(serverConn, credentials); + } + } + + return serverConn; + } + + private showStatusMessage(message: string, level: azdata.window.MessageLevel) { + this.wizard.message = { + text: message, + level: level + }; + } + + public clearStatusMessage() { + this.wizard.message = undefined; + } + + public static generateInputFromModel(model: ImportDataModel): VirtualizeDataInput { + if (!model) { + return undefined; + } + + let result = { + sessionId: model.sessionId, + destDatabaseName: model.database, + sourceServerType: DataSourceType.SqlHDFS, + externalTableInfoList: [{ + externalTableName: undefined, + columnDefinitionList: model.proseColumns, + sourceTableLocation: [model.parentFile.filePath], + fileFormat: { + formatName: model.fileFormat, + formatType: delimitedTextFileType, + fieldTerminator: model.columnDelimiter, + stringDelimiter: model.quoteCharacter, + firstRow: model.firstRow + } + }] + }; + + if (model.newDataSource) { + result.newDataSourceName = model.newDataSource.name; + let dataSrcUrl = url.parse(model.newDataSource.location); + result.sourceServerName = `${dataSrcUrl.host}${dataSrcUrl.pathname}`; + } else { + result.existingDataSourceName = model.existingDataSource; + } + + if (model.newSchema) { + result.newSchemas = [model.newSchema]; + result.externalTableInfoList[0].externalTableName = [model.newSchema, model.table]; + } else { + result.externalTableInfoList[0].externalTableName = [model.existingSchema, model.table]; + } + + return result; + } + + private async handleVirtualizeData(): Promise { + let errorMsg: string; + + try { + let dataInput = TableFromFileWizard.generateInputFromModel(this.model); + let createTableResponse = await this.provider.processVirtualizeDataInput(dataInput); + if (!createTableResponse.isSuccess) { + errorMsg = createTableResponse.errorMessages.join('\n'); + } + } catch (err) { + errorMsg = utils.getErrorMessage(err); + } + + if (errorMsg) { + this.showErrorMessage(errorMsg); + return false; + } + + return true; + } + + private showTaskComplete() { + this.wizard.registerOperation({ + connection: undefined, + displayName: localize('tableFromFile.taskLabel', 'Virtualize Data'), + description: undefined, + isCancelable: false, + operation: op => { + op.updateStatus(azdata.TaskStatus.Succeeded); + } + }); + } +} diff --git a/extensions/datavirtualization/src/wizards/virtualizeData/connectionDetailsPage.ts b/extensions/datavirtualization/src/wizards/virtualizeData/connectionDetailsPage.ts new file mode 100644 index 0000000000..112335e638 --- /dev/null +++ b/extensions/datavirtualization/src/wizards/virtualizeData/connectionDetailsPage.ts @@ -0,0 +1,312 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { IWizardPageWrapper } from '../wizardPageWrapper'; +import { VirtualizeDataModel } from './virtualizeDataModel'; +import { VirtualizeDataInput } from '../../services/contracts'; +import { getDropdownValue } from '../../utils'; +import { AppContext } from '../../appContext'; +import { VDIManager } from './virtualizeDataInputManager'; +import { dataSourcePrefixMapping, connectionPageInfoMapping } from '../../constants'; + +export class ConnectionDetailsPage implements IWizardPageWrapper { + + private _page: azdata.window.WizardPage; + private _modelBuilder: azdata.ModelBuilder; + private _mainContainer: azdata.FlexContainer; + + private _dataSourceNameForm: azdata.FormComponent; + private _sourceServerInfoComponentsFormGroup: azdata.FormComponentGroup; + private _credentialComponentsFormGroup: azdata.FormComponentGroup; + + private _dataSourceNameDropDown: azdata.DropDownComponent; + private _serverNameInput: azdata.InputBoxComponent; + private _databaseNameInput: azdata.InputBoxComponent; + private _existingCredDropdown: azdata.DropDownComponent; + private _credentialNameInput: azdata.InputBoxComponent; + private _usernameInput: azdata.InputBoxComponent; + private _passwordInput: azdata.InputBoxComponent; + + private readonly _createCredLabel = localize('newCredOption', '-- Create New Credential --'); + private readonly _parentLayout: azdata.FormItemLayout = { horizontal: true, componentWidth: '600px' }; + private readonly _dataSourceNameInputBoxLayout: azdata.FormItemLayout = + Object.assign({ info: localize('dataSourceHelpText', 'The name for your External Data Source.') }, this._parentLayout); + private readonly _existingCredDropdownLayout: azdata.FormItemLayout = + Object.assign({ + info: localize('credNameHelpText', + 'The name of the Database Scoped Credential used to securely store the login information for the External Data Source you are creating.') + }, this._parentLayout); + + private _currentDataSourceType: string; + private _currentDestDbName: string; + + constructor(private _dataModel: VirtualizeDataModel, private _vdiManager: VDIManager, private _appContext: AppContext) { + this._page = this._appContext.apiWrapper.createWizardPage(localize('connectionDetailsTitle', 'Create a connection to your Data Source')); + this._page.registerContent(async (modelView) => { + this._modelBuilder = modelView.modelBuilder; + this._mainContainer = this._modelBuilder.flexContainer().component(); + await modelView.initializeModel(this._mainContainer); + }); + } + + public async buildMainContainer(): Promise { + // Create data source fields first, since it preloads the database metadata + await this.buildDataSourceNameForm(); + await this.buildSourceServerInfoComponentsFormGroup(); + await this.buildCredentialComponentsFormGroup(); + const serverAndCredentialComponents: (azdata.FormComponent | azdata.FormComponentGroup)[] = []; + serverAndCredentialComponents.push(this._sourceServerInfoComponentsFormGroup); + serverAndCredentialComponents.push(this._credentialComponentsFormGroup); + + const mainFormBuilder: azdata.FormBuilder = this._modelBuilder.formContainer(); + mainFormBuilder.addFormItem(this._dataSourceNameForm, this._dataSourceNameInputBoxLayout); + mainFormBuilder.addFormItems(serverAndCredentialComponents, this._parentLayout); + this._mainContainer.clearItems(); + this._mainContainer.addItem(mainFormBuilder.component()); + } + + public async buildDataSourceNameForm(): Promise { + let destinationDB = this._vdiManager.destinationDatabaseName; + let dbInfo = await this._dataModel.loadDatabaseInfo(destinationDB); + let existingDataSources = dbInfo ? dbInfo.externalDataSources : []; + const locationPrefix = dataSourcePrefixMapping.get(this._currentDataSourceType) ?? ''; + existingDataSources = existingDataSources.filter(ds => ds.location.startsWith(locationPrefix)); + + let dataSourceInfo = existingDataSources.map(e => { + return { name: e.name, location: e.location, credName: e.credentialName }; + }); + + this._dataSourceNameDropDown = this._modelBuilder.dropDown().component(); + await this._dataSourceNameDropDown.updateProperties({ + values: [''].concat(dataSourceInfo.map(e => `${e.name} (${e.location}, ${e.credName})`)), + value: undefined, + editable: true, + height: undefined, + enabled: true, + fireOnTextChange: true + }); + + this._dataSourceNameDropDown.onValueChanged(async () => { + let dataSourceName = getDropdownValue(this._dataSourceNameDropDown.value); + let dsInfo = dataSourceInfo.find(e => dataSourceName === `${e.name} (${e.location}, ${e.credName})`); + if (dsInfo) { + await this._dataSourceNameDropDown.updateProperties({ value: dsInfo.name }); + return; + } + if (dataSourceName === '') { + await this._dataSourceNameDropDown.updateProperties({ value: undefined }); + await this.toggleServerCredInputs(true, '', this._createCredLabel, '', '', ''); + return; + } + let selectedDataSource = existingDataSources.find(ds => ds.name === this._dataSourceNameDropDown.value); + if (selectedDataSource) { + let serverName: string = selectedDataSource.location.substring(locationPrefix.length); + await this.toggleServerCredInputs(false, serverName, selectedDataSource.credentialName, + selectedDataSource.credentialName, selectedDataSource.username, ''); + return; + } + if (!this._serverNameInput.enabled) { + await this.toggleServerCredInputs(true, '', this._createCredLabel, '', '', ''); + return; + } + }); + + this._dataSourceNameForm = { + component: this._dataSourceNameDropDown, + title: localize('sourceNameInput', 'External Data Source Name'), + required: true + }; + } + + public async toggleServerCredInputs( + enable: boolean, + serverNameValue: string, + credDropDownValue: string, + credNameValue: string, + usernameValue: string, + passwordValue: string + ): Promise { + // There is a bug in recognizing required field. + // As workaround, it intentionally updates 'enabled' property first and then update 'value' + await this._serverNameInput.updateProperties({ enabled: enable }); + await this._existingCredDropdown.updateProperties({ enabled: enable }); + await this._credentialNameInput.updateProperties({ enabled: enable }); + await this._usernameInput.updateProperties({ enabled: enable }); + await this._passwordInput.updateProperties({ enabled: enable }); + + await this._serverNameInput.updateProperties({ value: serverNameValue }); + await this._existingCredDropdown.updateProperties({ value: credDropDownValue }); + await this._credentialNameInput.updateProperties({ value: credNameValue }); + await this._usernameInput.updateProperties({ value: usernameValue }); + await this._passwordInput.updateProperties({ value: passwordValue }); + } + + // Server-specific fields + public async buildSourceServerInfoComponentsFormGroup(): Promise { + let serverNameValue: string = ''; + let dbNameValue: string = ''; + + const connectionPageInfo = connectionPageInfoMapping.get(this._currentDataSourceType); + + let sourceServerInfoComponents: azdata.FormComponent[] = []; + + this._serverNameInput = this._modelBuilder.inputBox().withProps({ + value: serverNameValue + }).component(); + sourceServerInfoComponents.push({ + component: this._serverNameInput, + title: connectionPageInfo.serverNameTitle, + required: true + }); + + this._databaseNameInput = this._modelBuilder.inputBox().withProps({ + value: dbNameValue + }).component(); + sourceServerInfoComponents.push({ + component: this._databaseNameInput, + title: connectionPageInfo.databaseNameTitle, + required: connectionPageInfo.isDbRequired + }); + + this._sourceServerInfoComponentsFormGroup = { + components: sourceServerInfoComponents, + title: localize('serverFields', 'Server Connection') + }; + } + + // Credential fields + public async buildCredentialComponentsFormGroup(): Promise { + let credentialNames = this._dataModel.existingCredentials ? + this._dataModel.existingCredentials.map(cred => cred.credentialName) : []; + credentialNames.unshift(this._createCredLabel); + + let credDropDownValues: string[] = credentialNames; + let credDropDownValue: string = this._createCredLabel; + let credDropDownRequired: boolean = true; + let credNameValue: string = ''; + let credNameRequired: boolean = true; + let usernameValue: string = ''; + let usernameRequired: boolean = true; + let passwordValue: string = ''; + let passwordRequired: boolean = true; + + let credentialComponents: (azdata.FormComponent & { layout?: azdata.FormItemLayout })[] = []; + + this._existingCredDropdown = this._modelBuilder.dropDown().withProps({ + values: credDropDownValues, + value: credDropDownValue, + }).component(); + this._existingCredDropdown.onValueChanged(async (selection) => { + if (selection.selected === this._createCredLabel) { + await this.toggleCredentialInputs(true); + } else { + await this.toggleCredentialInputs(false); + await this._credentialNameInput.updateProperties({ value: '' }); + let credential = this._dataModel.existingCredentials.find(cred => cred.credentialName === selection.selected); + await this._usernameInput.updateProperties({ value: credential ? credential.username : '' }); + await this._passwordInput.updateProperties({ value: '' }); + } + }); + + credentialComponents.push({ + component: this._existingCredDropdown, + title: localize('credentialNameDropdown', 'Choose Credential'), + required: credDropDownRequired, + layout: this._existingCredDropdownLayout + }); + + this._credentialNameInput = this._modelBuilder.inputBox().withProps({ + value: credNameValue, + }).component(); + + credentialComponents.push({ + component: this._credentialNameInput, + title: localize('credentialNameInput', 'New Credential Name'), + required: credNameRequired + }); + + this._usernameInput = this._modelBuilder.inputBox().withProps({ + value: usernameValue, + }).component(); + + credentialComponents.push({ + component: this._usernameInput, + title: localize('usernameInput', 'Username'), + required: usernameRequired + }); + + this._passwordInput = this._modelBuilder.inputBox().withProps({ + value: passwordValue, + inputType: 'password' + }).component(); + + credentialComponents.push({ + component: this._passwordInput, + title: localize('passwordInput', 'Password'), + required: passwordRequired + }); + + this._credentialComponentsFormGroup = { + components: credentialComponents, + title: localize('credentialFields', 'Configure Credential') + }; + } + + public async validate(): Promise { + let inputValues = this._vdiManager.getVirtualizeDataInput(this); + return this._dataModel.validateInput(inputValues); + } + + public getPage(): azdata.window.WizardPage { + return this._page; + } + + public async updatePage(): Promise { + let newDataSourceType = this._vdiManager.sourceServerType; + let newDestDbName = this._vdiManager.destinationDatabaseName; + if ((newDataSourceType && this._currentDataSourceType !== newDataSourceType) + || (newDestDbName && this._currentDestDbName !== newDestDbName)) { + this._currentDataSourceType = newDataSourceType; + this._currentDestDbName = newDestDbName; + await this.buildMainContainer(); + } + } + + private async toggleCredentialInputs(enable: boolean): Promise { + await this._credentialNameInput.updateProperties({ enabled: enable }); + await this._usernameInput.updateProperties({ enabled: enable }); + await this._passwordInput.updateProperties({ enabled: enable }); + } + + public getInputValues(existingInput: VirtualizeDataInput): void { + if (!this._dataSourceNameDropDown) { return; } + + let isNewDataSource: boolean = this._serverNameInput ? this._serverNameInput.enabled : undefined; + let dataSourceName: string = this._dataSourceNameDropDown ? getDropdownValue(this._dataSourceNameDropDown.value) : undefined; + if (isNewDataSource) { + existingInput.newDataSourceName = dataSourceName; + let isNewCredential: boolean = this._existingCredDropdown ? + this._existingCredDropdown.value === this._createCredLabel : undefined; + if (isNewCredential) { + existingInput.newCredentialName = this._credentialNameInput ? this._credentialNameInput.value : undefined; + existingInput.sourceUsername = this._usernameInput ? this._usernameInput.value : undefined; + existingInput.sourcePassword = this._passwordInput ? this._passwordInput.value : undefined; + } else { + existingInput.existingCredentialName = this._existingCredDropdown ? + getDropdownValue(this._existingCredDropdown.value) : undefined; + } + } else { + existingInput.existingDataSourceName = dataSourceName; + existingInput.existingCredentialName = this._existingCredDropdown ? + getDropdownValue(this._existingCredDropdown.value) : undefined; + } + existingInput.sourceServerName = this._serverNameInput ? this._serverNameInput.value : undefined; + existingInput.sourceDatabaseName = this._databaseNameInput ? this._databaseNameInput.value : undefined; + } +} diff --git a/extensions/datavirtualization/src/wizards/virtualizeData/createMasterKeyPage.ts b/extensions/datavirtualization/src/wizards/virtualizeData/createMasterKeyPage.ts new file mode 100644 index 0000000000..89bb410d58 --- /dev/null +++ b/extensions/datavirtualization/src/wizards/virtualizeData/createMasterKeyPage.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { IWizardPageWrapper } from '../wizardPageWrapper'; +import { VirtualizeDataModel } from './virtualizeDataModel'; +import { VirtualizeDataInput } from '../../services/contracts'; +import { VDIManager } from './virtualizeDataInputManager'; +import { AppContext } from '../../appContext'; + +export class MasterKeyUiElements { + public masterKeyPasswordInput: azdata.InputBoxComponent; + public masterKeyPasswordConfirmInput: azdata.InputBoxComponent; +} +export class CreateMasterKeyPage implements IWizardPageWrapper { + private _page: azdata.window.WizardPage; + private _uiElements: MasterKeyUiElements; + + private readonly _masterKeyExistsMsg = localize('masterKeyExistsMsg', 'A Master Key already exists for the selected database. No action is required on this page.'); + + public constructor(private _dataModel: VirtualizeDataModel, private _vdiManager: VDIManager, private _appContext: AppContext) { + this.buildPage(); + } + + public setUi(ui: MasterKeyUiElements): void { + this._uiElements = ui; + } + + private buildPage(): void { + this._page = this._appContext.apiWrapper.createWizardPage(localize('createMasterKeyTitle', 'Create Database Master Key')); + this._page.description = localize( + 'createMasterKeyDescription', + 'A master key is required. This secures the credentials used by an External Data Source. Note that you should back up the master key by using BACKUP MASTER KEY and store the backup in a secure, off-site location.'); + + this._page.registerContent(async (modelView) => { + let ui = new MasterKeyUiElements(); + let builder = modelView.modelBuilder; + let allComponents: (azdata.FormComponent | azdata.FormComponentGroup)[] = []; + + // Master key fields + ui.masterKeyPasswordInput = builder.inputBox().withProperties({ + inputType: 'password' + }).component(); + ui.masterKeyPasswordConfirmInput = builder.inputBox().withProperties({ + inputType: 'password' + }).component(); + allComponents.push({ + components: + [ + { + component: ui.masterKeyPasswordInput, + title: localize('masterKeyPasswordInput', 'Password'), + required: true + }, + { + component: ui.masterKeyPasswordConfirmInput, + title: localize('masterKeyPasswordConfirmInput', 'Confirm Password'), + required: true + } + ], + title: localize('masterKeyPasswordLabel', 'Set the Master Key password.') + }); + + let formContainer = builder.formContainer() + .withFormItems(allComponents, + { + horizontal: true, + componentWidth: '600px' + }).component(); + + let pwdReminderText = builder.text().withProperties({ + value: localize('pwdReminderText', 'Strong passwords use a combination of alphanumeric, upper, lower, and special characters.') + }).component(); + + let flexContainer = builder.flexContainer().withLayout({ + flexFlow: 'column', + alignItems: 'stretch', + height: '100%', + width: '100%' + }).component(); + + flexContainer.addItem(formContainer, { CSSStyles: { 'padding': '0px' } }); + flexContainer.addItem(pwdReminderText, { CSSStyles: { 'padding': '10px 0 0 30px' } }); + + this.setUi(ui); + await modelView.initializeModel(flexContainer); + }); + } + + public async validate(): Promise { + if (this._uiElements.masterKeyPasswordInput.value === this._uiElements.masterKeyPasswordConfirmInput.value) { + let inputValues = this._vdiManager.getVirtualizeDataInput(this); + return this._dataModel.validateInput(inputValues); + } else { + this._dataModel.showWizardError(localize('passwordMismatchWithConfirmError', 'Password values do not match.')); + return false; + } + } + + public getPage(): azdata.window.WizardPage { + return this._page; + } + + public async updatePage(): Promise { + let hasMasterKey: boolean = await this._dataModel.hasMasterKey(); + this._uiElements.masterKeyPasswordInput.updateProperties({ enabled: !hasMasterKey, required: !hasMasterKey }); + this._uiElements.masterKeyPasswordConfirmInput.updateProperties({ enabled: !hasMasterKey, required: !hasMasterKey }); + + if (hasMasterKey) { + this._dataModel.showWizardInfo(this._masterKeyExistsMsg); + } + } + + public getInputValues(existingInput: VirtualizeDataInput): void { + existingInput.destDbMasterKeyPwd = (this._uiElements && this._uiElements.masterKeyPasswordInput) ? + this._uiElements.masterKeyPasswordInput.value : undefined; + } +} diff --git a/extensions/datavirtualization/src/wizards/virtualizeData/objectMappingPage.ts b/extensions/datavirtualization/src/wizards/virtualizeData/objectMappingPage.ts new file mode 100644 index 0000000000..0892742981 --- /dev/null +++ b/extensions/datavirtualization/src/wizards/virtualizeData/objectMappingPage.ts @@ -0,0 +1,1763 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +import * as path from 'path'; +const localize = nls.loadMessageBundle(); + +import { IWizardPageWrapper } from '../wizardPageWrapper'; +import { VirtualizeDataModel } from './virtualizeDataModel'; +import { ColumnDefinition, ExternalTableInfo, SchemaTables, FileFormat, ExecutionResult, SchemaViews } from '../../services/contracts'; +import { CheckboxTreeNode, CheckboxTreeDataProvider } from './virtualizeDataTree'; +import { VirtualizeDataInput } from '../../services/contracts'; +import { AppContext } from '../../appContext'; +import { VDIManager } from './virtualizeDataInputManager'; +import * as loc from '../../localizedConstants'; + +export class ObjectMappingPage implements IWizardPageWrapper { + private _page: azdata.window.WizardPage; + private _modelBuilder: azdata.ModelBuilder; + + // data source tree + private _dataSourceTreeContainer: azdata.FlexContainer; + private _dataSourceTableTreeSpinner: azdata.LoadingComponent; + private _dataSourceTableTree: azdata.TreeComponent; + private _treeRootNode: CheckboxTreeNode; + + // object mapping wrapper + private _objectMappingWrapperSpinner: azdata.LoadingComponent; + private _objectMappingWrapper: azdata.FlexContainer; + private _objectMappingContainer: azdata.FlexContainer; + private _tableHelpTextContainer: azdata.FlexContainer; + + // table name mapping container + private _tableNameMappingContainer: azdata.FlexContainer; + private _sourceTableNameContainer: azdata.FlexContainer; + private _sourceSchemaInputBox: azdata.InputBoxComponent; + private _sourceTableNameInputBox: azdata.InputBoxComponent; + private _destTableNameInputContainer: azdata.FlexContainer; + private _destTableSchemaDropdown: azdata.DropDownComponent; + private _destTableNameInputBox: azdata.InputBoxComponent; + + // column mapping table container + private _columnMappingTableSpinner: azdata.LoadingComponent; + private _columnMappingTableContainer: azdata.FormContainer; + private _columnMappingTable: azdata.DeclarativeTableComponent; + private _columnMappingTableHeader: azdata.DeclarativeTableColumn[]; + + // current status + private _selectedLocation: string[]; + + // dependencies + private _dataSourceBrowser: DataSourceBrowser; + private _mappingInfoCache: MappingInfoCache; + private _mappingInfoRetriever: MappingInfoRetriever; + private _dataModel: VirtualizeDataModel; + private _vdiManager: VDIManager; + private _appContext: AppContext; + + private _existingSchemaNames: string[] = []; + + constructor(dataModel: VirtualizeDataModel, vdiManager: VDIManager, appContext: AppContext) { + if (dataModel && vdiManager && appContext) { + CheckboxTreeNode.clearNodeRegistry(); + TableTreeNode.clearTableNodeCache(); + + this._dataModel = dataModel; + this._vdiManager = vdiManager; + this._appContext = appContext; + PathResolver.initialize(this._appContext); + this._dataSourceBrowser = new DataSourceBrowser(this._dataModel, this._vdiManager); + this._mappingInfoCache = new MappingInfoCache(this._vdiManager); + this._mappingInfoRetriever = new MappingInfoRetriever(this._dataSourceBrowser, this._mappingInfoCache, this._dataModel); + this._treeRootNode = undefined; + this.buildPage(); + } + } + + private async buildPage(): Promise { + this._page = this._appContext.apiWrapper.createWizardPage(localize('objectMappingTitle', 'Map your data source objects to your external table')); + + this._page.registerContent(async (modelView) => { + this._modelBuilder = modelView.modelBuilder; + + this.buildSourceTreeContainer(); + await this.buildObjectMappingWrapper(); + + let mainContainer = this._modelBuilder.flexContainer().withLayout({ + flexFlow: 'row', + alignItems: 'stretch', + width: '100%', + height: '100%' + }).component(); + mainContainer.addItem(this._dataSourceTableTreeSpinner, { + flex: '1, 0, 0%', + CSSStyles: { + 'width': '38%', + 'height': '100%', + 'resize': 'horizontal', + 'overflow': 'scroll', + 'border-right': '1px solid rgb(185, 185, 185)', + } + }); + mainContainer.addItem(this._objectMappingWrapperSpinner, { + flex: '1, 0, 0%', + CSSStyles: { + 'border-left': '1px solid rgb(185, 185, 185)', + 'margin-left': '-1px', + 'width': '100%', + 'height': '100%', + 'overflow': 'scroll' + } + }); + + let wrapperContainer = this._modelBuilder.flexContainer().withLayout({ + flexFlow: 'row', + alignItems: 'stretch', + width: '100%', + height: '100%', + }).component(); + wrapperContainer.addItem(mainContainer, { + flex: '1, 0, 0%', + CSSStyles: { + 'border-top': '2px solid rgba(0, 0, 0, 0.22)', + 'width': '100%', + 'overflow': 'auto' + } + }); + + await modelView.initializeModel(wrapperContainer); + }); + } + + private buildSourceTreeContainer(): void { + this._dataSourceTreeContainer = this._modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + alignItems: 'stretch', + width: '100%', + height: '100%' + }).component(); + + this._dataSourceTableTreeSpinner = this._modelBuilder.loadingComponent() + .withItem(this._dataSourceTreeContainer) + .withProps({ loading: false }) + .component(); + } + + private async buildObjectMappingWrapper(): Promise { + await this.buildObjectMappingContainer(); + this.buildTableHelpTextContainer(); + + this._objectMappingWrapper = this._modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + alignItems: 'stretch', + width: '100%' + }).component(); + + this._objectMappingWrapperSpinner = + this._modelBuilder.loadingComponent() + .withItem(this._objectMappingWrapper) + .withProps({ loading: false }) + .component(); + } + + private async buildObjectMappingContainer(): Promise { + this.buildTableNameMappingContainer(); + await this.buildColumnMappingTableContainer(); + + this._objectMappingContainer = this._modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + alignItems: 'stretch', + width: '100%', + height: '100%' + }).component(); + this._objectMappingContainer.addItem(this._tableNameMappingContainer, { + flex: '0', + CSSStyles: { + 'width': '100%', + 'height': '90px' + } + }); + this._objectMappingContainer.addItem(this._columnMappingTableSpinner, { + flex: '1', + CSSStyles: { + 'width': '100%' + } + }); + } + + private buildTableNameMappingContainer(): void { + this.buildSourceTableNameInputContainer(); + this.buildDestTableNameInputContainer(); + let arrowText: azdata.TextComponent = this._modelBuilder.text() + .withProps({ value: '\u279F' }) + .component(); + + this._tableNameMappingContainer = this._modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'row', + alignItems: 'stretch', + width: '100%', + height: '100%' + }) + .component(); + + this._tableNameMappingContainer.addItem(this._sourceTableNameContainer, { + flex: '1, 1, 0%', + CSSStyles: { 'width': '40%' } + }); + this._tableNameMappingContainer.addItem(arrowText, { + flex: '0', + CSSStyles: { + 'font-size': '30px', + 'align': 'center', + 'padding': '16px 7px 0px' + } + }); + this._tableNameMappingContainer.addItem(this._destTableNameInputContainer, { + flex: '1, 1, 0%', + CSSStyles: { + 'width': '60%' + } + }); + } + + private buildSourceTableNameInputContainer(): void { + this._sourceSchemaInputBox = this._modelBuilder.inputBox() + .withProps({ width: '100%', ariaLabel: loc.sourceSchemaTitle }) + .component(); + let dotText = this._modelBuilder.text() + .withProps({ value: '.' }) + .component(); + this._sourceTableNameInputBox = this._modelBuilder.inputBox() + .withProps({ width: '100%', ariaLabel: loc.sourceTableTitle }) + .component(); + + let bindingContainer: azdata.FlexContainer = this._modelBuilder.flexContainer() + .withLayout({ flexFlow: 'row', alignItems: 'center', width: '100%' }) + .component(); + bindingContainer.addItem(this._sourceSchemaInputBox, { + CSSStyles: { 'width': '30%', 'pointer-events': 'none', 'padding': '8px 0px 8px 8px' } + }); + bindingContainer.addItem(dotText, { + flex: '0, 1, 0%', + CSSStyles: { 'font-wight': '900', 'padding': '0px 3px 0px 3px' } + }); + bindingContainer.addItem(this._sourceTableNameInputBox, { + CSSStyles: { 'width': '70%', 'pointer-events': 'none', 'padding': '8px 8px 8px 0px' } + }); + + let wrapperContainer: azdata.FlexContainer = this._modelBuilder.flexContainer() + .withLayout({ flexFlow: 'row', alignItems: 'center', width: '100%' }) + .component(); + wrapperContainer.addItem(bindingContainer, { + CSSStyles: { 'width': '100%' } + }); + + let titledContainer = new TitledContainer(this._modelBuilder); + titledContainer.title = loc.sourceTableTitle; + titledContainer.setTitleMargin(0, 0, 7, 0); + titledContainer.setPadding(15, 30, 0, 30); + titledContainer.addContentContainer(wrapperContainer); + this._sourceTableNameContainer = titledContainer.flexContainer; + } + + private buildDestTableNameInputContainer(): void { + this._destTableSchemaDropdown = this._modelBuilder.dropDown() + .withProps({ + ariaLabel: loc.externalSchemaTitle, + editable: true, + width: '100%', + fireOnTextChange: true + }).component(); + this._destTableSchemaDropdown.onValueChanged(async () => { + await this.storeUserModification(); + }); + + let dotText = this._modelBuilder.text() + .withProps({ value: '.' }) + .component(); + + this._destTableNameInputBox = this._modelBuilder.inputBox() + .withProps({ width: '100%', ariaLabel: loc.externalTableTitle }) + .component(); + this._destTableNameInputBox.onTextChanged(async () => { + await this.storeUserModification(); + }); + + let destTableNameInputContainer: azdata.FlexContainer = this._modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'row', + alignItems: 'center', + width: '100%' + }) + .component(); + destTableNameInputContainer.addItem(this._destTableSchemaDropdown, { + flex: '1, 1, 0%', + CSSStyles: { + 'width': '30%', + 'padding': '8px 0px 8px 8px' + } + }); + destTableNameInputContainer.addItem(dotText, { + flex: '0, 1, 0%', + CSSStyles: { + 'font-wight': '900', + 'padding': '0px 3px 0px 3px' + } + }); + destTableNameInputContainer.addItem(this._destTableNameInputBox, { + flex: '1, 1, 0%', + CSSStyles: { + 'width': '70%', + 'padding': '8px 8px 8px 0px' + } + }); + + let wrapperContainer: azdata.FlexContainer = this._modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'row', + alignItems: 'center', + width: '100%' + }) + .component(); + wrapperContainer.addItem(destTableNameInputContainer, { + flex: '1, 1, 0%', + CSSStyles: { + 'width': '100%' + } + }); + + let titledContainer = new TitledContainer(this._modelBuilder); + titledContainer.title = loc.externalTableTitle; + titledContainer.setTitleMargin(0, 0, 7, 0); + titledContainer.setPadding(15, 30, 0, 30); + titledContainer.addContentContainer(wrapperContainer); + this._destTableNameInputContainer = titledContainer.flexContainer; + } + + private async buildColumnMappingTableContainer(): Promise { + this.buildColumnMappingTableHeader(); + + this._columnMappingTable = this._modelBuilder.declarativeTable().withProperties({ + columns: this._columnMappingTableHeader, + data: [['', '', '', false, '']], + width: '100%' + }).component(); + + this._columnMappingTableContainer = this._modelBuilder.formContainer() + .withFormItems([{ + component: this._columnMappingTable, + title: localize('externalTableMappingLabel', 'Column Mapping') + }], { + horizontal: false, + componentWidth: '100%' + }) + .withLayout({ + width: '100%' + }) + .component(); + + this._columnMappingTable.onDataChanged(async () => { + await this.storeUserModification(); + }); + + this._columnMappingTableSpinner = this._modelBuilder.loadingComponent() + .withItem(this._columnMappingTableContainer) + .withProps({ loading: false }) + .component(); + } + + private buildColumnMappingTableHeader(): void { + let columns: azdata.DeclarativeTableColumn[] = [{ + displayName: localize('mapping.SourceName', 'Source'), + valueType: azdata.DeclarativeDataType.string, + width: '28%', + isReadOnly: true, + categoryValues: undefined + }, { + displayName: localize('mapping.externalName', 'External'), + valueType: azdata.DeclarativeDataType.string, + width: '28%', + isReadOnly: true, + categoryValues: undefined + }, { + displayName: localize('mapping.sqlDataType', 'SQL Data Type'), + valueType: azdata.DeclarativeDataType.string, + width: '15%', + isReadOnly: true, + categoryValues: undefined + }, { + displayName: localize('mapping.nullable', 'Nullable'), + valueType: azdata.DeclarativeDataType.boolean, + width: '5%', + isReadOnly: true, + categoryValues: undefined + }, { + displayName: localize('mapping.collations', 'Collations'), + valueType: azdata.DeclarativeDataType.string, + width: '20%', + isReadOnly: true, + categoryValues: undefined + }]; + + this._columnMappingTableHeader = columns; + } + + private buildTableHelpTextContainer(): void { + let tableHelpText = localize('mappingTableHelpText', 'Clicking on the table name will show you the column mapping information for that table.'); + let tableHelpTextComponent = this._modelBuilder.text().withProperties({ value: tableHelpText }).component(); + + let checkboxHelpText = localize('mappingTableCheckboxHelpText', 'Clicking on the checkbox will select that table to be mapped.'); + let checkboxHelpTextComponent = this._modelBuilder.text().withProperties({ value: checkboxHelpText }).component(); + + this._tableHelpTextContainer = + this._modelBuilder.flexContainer() + .withItems([tableHelpTextComponent, checkboxHelpTextComponent], { + CSSStyles: { + 'flex': '1, 0, 0%', + 'padding': '0 30px 0 30px' + } + }) + .withLayout({ + flexFlow: 'column', + height: '100%', + width: '100%' + }) + .component(); + } + + private toggleObjectMappingWrapper(): void { + this._objectMappingWrapper.clearItems(); + if (this._selectedLocation) { + this._objectMappingWrapper.addItem(this._objectMappingContainer, { + flex: '1', + CSSStyles: { + 'width': '100%' + } + }); + } else { + this._objectMappingWrapper.addItem(this._tableHelpTextContainer, { + flex: '1', + CSSStyles: { + 'width': '100%' + } + }); + } + } + + private async storeUserModification(): Promise { + if (!this._destTableNameInputBox || !this._destTableSchemaDropdown || !this._selectedLocation) { + return; + } + + if (!this._columnMappingTable || !this._columnMappingTable.data || this._columnMappingTable.data.length === 0) { + return; + } + + let colDefResult = await this._dataSourceBrowser.getColumnDefinitions(this._selectedLocation); + if (!colDefResult || !colDefResult.isSuccess) { + return; + } + + if (colDefResult.returnValue.find(e => e.isSupported === false)) { + return; + } + + let colDefs: ColumnDefinition[] = []; + this._columnMappingTable.data.forEach(row => { + colDefs.push({ + columnName: row[MappingProperty.ColumnName], + dataType: row[MappingProperty.DataType], + isNullable: row[MappingProperty.IsNullable], + collationName: row[MappingProperty.CollationName] + }); + }); + + let tableNameWithoutSchema: string = this._destTableNameInputBox.value + || LocationHandler.getTableName(this._selectedLocation); + let desiredTableName = [`${this._destTableSchemaDropdown.value}`, tableNameWithoutSchema]; + this._mappingInfoCache.putMappingInfo(desiredTableName, this._selectedLocation, colDefs, true); + } + + public async updatePage(): Promise { + let destinationDB = this._vdiManager.destinationDatabaseName; + let dbInfo = await this._dataModel.loadDatabaseInfo(destinationDB); + this._existingSchemaNames = dbInfo ? dbInfo.schemaList : []; + + CheckboxTreeNode.clearNodeRegistry(); + TableTreeNode.clearNodeRegistry(); + this._dataSourceBrowser.clearCache(); + + this._selectedLocation = undefined; + + this.updateSourceTreeContainer(); + this.toggleObjectMappingWrapper(); + } + + private updateSourceTreeContainer(): void { + if (!this._modelBuilder || !this._dataSourceTreeContainer) { return; } + + let treeHeight: string = '800px'; + this._dataSourceTableTree = this._modelBuilder.tree().withProperties({ + withCheckbox: true, + height: treeHeight + }).component(); + + this._dataSourceTreeContainer.clearItems(); + this._dataSourceTreeContainer.addItem(this._dataSourceTableTree, { + CSSStyles: { + 'padding': '10px 0px 0px 10px', + 'height': treeHeight + } + }); + + this._treeRootNode = RootTreeNode.getInstance(this._dataSourceBrowser, this._dataSourceTableTreeSpinner); + let treeDataProvider = new CheckboxTreeDataProvider(this._treeRootNode); + let treeView = this._dataSourceTableTree.registerDataProvider(treeDataProvider); + treeView.onNodeCheckedChanged(async item => { + if (item && item.element) { + await this.actionOnNodeCheckStatusChanged(item.element, item.checked); + } + }); + treeView.onDidChangeSelection(async selectedNodes => { + if (selectedNodes && selectedNodes.selection && selectedNodes.selection.length === 1 && selectedNodes.selection[0]) { + await this.actionOnNodeIsSelected(selectedNodes.selection[0]); + } + }); + } + + private async actionOnNodeCheckStatusChanged(node: CheckboxTreeNode, checked: boolean): Promise { + if (node && checked !== undefined) { + await this.checkAndExpand(node, checked); + } + } + + private async checkAndExpand(treeNode: CheckboxTreeNode, checked: boolean): Promise { + treeNode.setCheckedState(checked); + let nodes: CheckboxTreeNode[] = [treeNode]; + let tableNodes: CheckboxTreeNode[] = []; + while (nodes && nodes.length > 0) { + let node = nodes.shift(); + node.setCheckedState(checked); + if (node instanceof TableTreeNode) { + tableNodes.push(node); + } else { + let newChildren = await node.getChildren(); + if (newChildren && newChildren.length > 0) { + nodes = newChildren.concat(nodes); + } + } + } + for (let tn of tableNodes) { + await this.loadColumnDefinitions(tn as TableTreeNode); + } + treeNode.notifyStateChanged(); + } + + private async actionOnNodeIsSelected(node: CheckboxTreeNode): Promise { + if (node) { + if (node instanceof TableTreeNode) { + let tableNode: TableTreeNode = node as TableTreeNode; + if (!this._selectedLocation) { + this._selectedLocation = tableNode.location; + this._objectMappingWrapperSpinner.loading = true; + await this.loadColumnDefinitions(tableNode); + this.toggleObjectMappingWrapper(); + this._objectMappingWrapperSpinner.loading = false; + await this.updateObjectMappingContainer(tableNode); + } else { + this._selectedLocation = tableNode.location; + this._columnMappingTableSpinner.loading = true; + await this.updateObjectMappingContainer(tableNode); + this._columnMappingTableSpinner.loading = false; + + } + } + } + } + + private async loadColumnDefinitions(tableNode: TableTreeNode): Promise> { + if (!tableNode) { + return; + } + let location: string[] = tableNode.location; + if (!location) { + return; + } + let mappingInfoResult = await this._mappingInfoRetriever.getMappingInfo(location); + if (mappingInfoResult && mappingInfoResult.isSuccess && tableNode.enabled) { + let mappingInfo = mappingInfoResult.returnValue; + let unsupportedColumn = mappingInfo && mappingInfo.columnDefinitionList && + mappingInfo.columnDefinitionList.find(colDef => colDef.isSupported === false); + if (unsupportedColumn) { + await tableNode.setCheckedState(false); + await tableNode.setEnable(false); + } + } + return mappingInfoResult; + } + + private async updateObjectMappingContainer(tableNode: TableTreeNode): Promise { + let location: string[] = tableNode.location || this._selectedLocation; + if (location && location.length >= 2) { + let mappingInfoResult = await this.loadColumnDefinitions(tableNode); + let sourceSchemaName: string = LocationHandler.getSchemaName(location); + let sourceTableName: string = LocationHandler.getTableName(location); + let destSchemaName: string = undefined; + let destTableName: string = undefined; + let enableTableNameMapping: boolean = undefined; + let colDefList: ColumnDefinition[] = undefined; + + if (mappingInfoResult && mappingInfoResult.isSuccess && mappingInfoResult.returnValue) { + let mappingInfo: TableMappingInfo = mappingInfoResult.returnValue; + destSchemaName = mappingInfo.externalTableName[0]; + destTableName = mappingInfo.externalTableName[1]; + enableTableNameMapping = true; + colDefList = mappingInfo && mappingInfo.columnDefinitionList; + + let unsupportedColumns = mappingInfo && + mappingInfo.columnDefinitionList.filter(colDef => colDef.isSupported === false).map(e => [e.columnName, e.dataType]); + if (unsupportedColumns && unsupportedColumns.length > 0) { + enableTableNameMapping = false; + this._dataModel.showWizardWarning( + localize('warning.unsupported_column_type_title', 'Unsupported Column Types found'), + localize('warning.unsupported_column_type_description', 'These column types are not supported for external tables:{0}{1}', + os.EOL, unsupportedColumns.map(e => ` * ${e[0]} (${e[1]})`).join(os.EOL)) + ); + } + } else { + destSchemaName = this._dataModel.defaultSchema; + destTableName = sourceTableName; + enableTableNameMapping = false; + colDefList = undefined; + + let errorMessages: string[] = mappingInfoResult.errorMessages; + if (!errorMessages) { + let errorMsg: string = localize('noTableError', `No table information present for '{0}'`, LocationHandler.getLocationString(location)); + errorMessages = [errorMsg]; + } + this._dataModel.showWizardError(errorMessages.join(" ")); + } + + await this.updateTableNameMappingContainer(sourceSchemaName, sourceTableName, destSchemaName, destTableName, enableTableNameMapping); + await this.updateColumnMappingContainer(colDefList); + } + } + + private async updateTableNameMappingContainer(sourceSchemaName: string, sourceTableName: string, + destSchemaName: string, destTableName: string, enabled?: boolean + ): Promise { + if (enabled === undefined) { enabled = true; } + await this._sourceSchemaInputBox.updateProperties({ + value: sourceSchemaName + }); + await this._sourceTableNameInputBox.updateProperties({ + value: sourceTableName + }); + await this._destTableSchemaDropdown.updateProperties({ + values: this._dataModel.schemaList, + // Default to the source schema if we have it (which will create it if it doesn't exist) + value: destSchemaName ? destSchemaName : this._dataModel.defaultSchema, + enabled: enabled + }); + await this._destTableNameInputBox.updateProperties({ + value: destTableName, + enabled: enabled + }); + } + + private async updateColumnMappingContainer(columnDefinitions: ColumnDefinition[]): Promise { + let colDefTableData = [['', '', '', false, '']]; + if (columnDefinitions && columnDefinitions.length > 0) { + colDefTableData = []; + columnDefinitions.forEach(def => { + colDefTableData.push([def.columnName, def.columnName, def.dataType, def.isNullable, def.collationName]); + }); + } + await this._columnMappingTable.updateProperties({ + data: colDefTableData, + width: '100%' + }); + } + + public async validate(): Promise { + await this.storeUserModification(); + await this.loadCheckedTableColDefs(); + if (!this.anythingToCreateExists()) { + this._dataModel.showWizardError(localize('noObjectSelectedMessage', 'No objects were selected.')); + return false; + } + return true; + } + + private anythingToCreateExists(): boolean { + return this._vdiManager.destDbMasterKeyPwd !== undefined || + this._vdiManager.newDataSourceName !== undefined || + (this._vdiManager.externalTableInfoList !== undefined && + this._vdiManager.externalTableInfoList.length > 0); + } + + public async loadCheckedTableColDefs(): Promise { + let checkedTableNodes: TableTreeNode[] = this.getCheckedTableNodes(); + if (!checkedTableNodes || checkedTableNodes.length === 0) { return; } + + for (let i = checkedTableNodes.length - 1; i >= 0; --i) { + await this.loadColumnDefinitions(checkedTableNodes[i]); + } + } + + public getCheckedTableNodes(): TableTreeNode[] { + if (!this._vdiManager) { return undefined; } + + let dataSourceName = this._vdiManager.dataSourceName; + if (!dataSourceName) { return undefined; } + + let sourceServerName = this._vdiManager.sourceServerName; + if (!sourceServerName) { return undefined; } + + let sourceDatabaseName = this._vdiManager.sourceDatabaseName; + + let tableNodes: TableTreeNode[] = TableTreeNode.getAllNodes(dataSourceName, sourceServerName, sourceDatabaseName); + if (!tableNodes || tableNodes.length === 0) { return undefined; } + + tableNodes = tableNodes.filter(e => e.checked); + if (!tableNodes || tableNodes.length === 0) { return undefined; } + + return tableNodes; + } + + public getPage(): azdata.window.WizardPage { + return this._page; + } + + public getInputValues(existingInput: VirtualizeDataInput): void { + existingInput.externalTableInfoList = undefined; + existingInput.newSchemas = undefined; + + let checkedTableNodes: TableTreeNode[] = this.getCheckedTableNodes(); + if (!checkedTableNodes || checkedTableNodes.length === 0) { return; } + + let checkedLocations: string[][] = checkedTableNodes.map(e => e.location); + if (!checkedLocations || checkedLocations.length === 0) { return; } + + let externalTableInfoList = []; + let newSchemaSet = new Set(); + let existingSchemaSet = new Set(this._existingSchemaNames); + checkedLocations.forEach(location => { + let externalTableInfo: TableMappingInfo = this._mappingInfoCache.getMappingInfo(location); + if (externalTableInfo) { + externalTableInfoList.push(externalTableInfo); + + if (externalTableInfo.externalTableName.length === 2) { + let schemaName = externalTableInfo.externalTableName[0]; + if (!existingSchemaSet.has(schemaName)) { + newSchemaSet.add(schemaName); + } + } + } + }); + + existingInput.newSchemas = Array.from(newSchemaSet); + existingInput.externalTableInfoList = externalTableInfoList; + } +} + +enum MappingProperty { + ColumnName = 1, + DataType = 2, + IsNullable = 3, + CollationName = 4 +} + +class RootTreeNode extends CheckboxTreeNode { + private _dataSourceBrowser: DataSourceBrowser; + private _treeSpinner: azdata.LoadingComponent; + + constructor(dataSourceBrowser: DataSourceBrowser, treeSpinner?: azdata.LoadingComponent) { + super({ isRoot: true }); + this.setDataSourceBrowser(dataSourceBrowser); + this._treeSpinner = treeSpinner || this._treeSpinner; + } + + public setDataSourceBrowser(dataSourceBrowser: DataSourceBrowser): void { + if (dataSourceBrowser) { + this._dataSourceBrowser = dataSourceBrowser || this._dataSourceBrowser; + let treeId: string = DataSourceId.getIdFromDataSourceBrowser(this._dataSourceBrowser); + super.setArgs({ treeId: treeId }); + } + } + + public async getChildren(): Promise { + if (!this.hasChildren) { + await this.setTreeLoadingState(true); + let dbNames: string[] = await this._dataSourceBrowser.getDatabaseNames(); + if (dbNames && dbNames.length > 0) { + dbNames.forEach(dbName => { + let childNode = DatabaseTreeNode.getInstance(dbName, this._dataSourceBrowser); + this.addChildNode(childNode); + }); + this.notifyStateChanged(); + } + await this.setTreeLoadingState(false); + } + this.children = this.children.sort((a, b) => a.label.localeCompare(b.label)); + return this.children; + } + + private async setTreeLoadingState(isLoading: boolean): Promise { + if (this._treeSpinner) { + await this._treeSpinner.updateProperties({ loading: isLoading }); + } + } + + public get iconPath(): string { + return ''; + } + + public static getInstance(dataSourceBrowser: DataSourceBrowser, treeSpinner: azdata.LoadingComponent): RootTreeNode { + let rootNode: RootTreeNode = undefined; + if (dataSourceBrowser) { + let treeId: string = DataSourceId.getIdFromDataSourceBrowser(dataSourceBrowser); + if (treeId) { + rootNode = CheckboxTreeNode.findNode(treeId, 'root') as RootTreeNode; + if (!rootNode) { + rootNode = new RootTreeNode(dataSourceBrowser, treeSpinner); + } + } + } + return rootNode; + } +} + +class DataSourceId { + public static getId(dataSourceName: string, sourceServerName: string, sourceDatabaseName: string): string { + let dataSourceKey = dataSourceName ? LocationHandler.peelOffBrackets(dataSourceName) : '_'; + let databaseKey = sourceDatabaseName ? LocationHandler.peelOffBrackets(sourceDatabaseName) : '_'; + let serverKey = sourceServerName ? LocationHandler.peelOffBrackets(sourceServerName) : '_'; + return `[${dataSourceKey}@${serverKey}@${databaseKey}]`; + } + + public static getIdFromDataSourceBrowser(dataSourceBrowser: DataSourceBrowser): string { + let dataSourceId: string = undefined; + if (dataSourceBrowser && dataSourceBrowser.vdiManager) { + let vdiManager = dataSourceBrowser.vdiManager; + let dataSourceName: string = vdiManager.dataSourceName; + let sourceDatabaseName: string = vdiManager.sourceDatabaseName; + let sourceServerName: string = vdiManager.sourceServerName; + dataSourceId = DataSourceId.getId(dataSourceName, sourceServerName, sourceDatabaseName); + } + return dataSourceId; + } +} + +class DatabaseTreeNode extends CheckboxTreeNode { + private _dataSourceBrowser: DataSourceBrowser; + private _databaseName: string; + + constructor(databaseName: string, dataSourceBrowser: DataSourceBrowser) { + super(); + this._databaseName = databaseName || this._databaseName; + if (this._databaseName) { + let nodeId = LocationHandler.encloseWithBrackets(this._databaseName); + super.setArgs({ nodeId: nodeId, label: this._databaseName }); + } + this.setDataSourceBrowser(dataSourceBrowser); + } + + public setDataSourceBrowser(dataSourceBrowser: DataSourceBrowser): void { + if (dataSourceBrowser) { + this._dataSourceBrowser = dataSourceBrowser || this._dataSourceBrowser; + let treeId: string = DataSourceId.getIdFromDataSourceBrowser(this._dataSourceBrowser); + super.setArgs({ treeId: treeId }); + } + } + + public async getChildren(): Promise { + if (!this.hasChildren && this._dataSourceBrowser && this._databaseName) { + let tableFolderNode = TableFolderNode.getInstance(this._databaseName, this._dataSourceBrowser); + let viewFolderNode = ViewFolderNode.getInstance(this._databaseName, this._dataSourceBrowser); + if (this.checked) { + await tableFolderNode.setCheckedState(true); + await viewFolderNode.setCheckedState(true); + } + this.addChildNode(tableFolderNode); + this.addChildNode(viewFolderNode); + this.notifyStateChanged(); + } + return this.children; + } + + public get iconPath(): string { + return PathResolver.databaseIconPath; + } + + public static getInstance(databaseName: string, dataSourceBrowser: DataSourceBrowser): DatabaseTreeNode { + let dbNode: DatabaseTreeNode = undefined; + if (databaseName && dataSourceBrowser) { + let treeId: string = DataSourceId.getIdFromDataSourceBrowser(dataSourceBrowser); + if (treeId) { + let nodeId = LocationHandler.encloseWithBrackets(databaseName); + dbNode = CheckboxTreeNode.findNode(treeId, nodeId) as DatabaseTreeNode; + if (!dbNode) { + dbNode = new DatabaseTreeNode(databaseName, dataSourceBrowser); + } + } + } + return dbNode; + } +} + +class TableFolderNode extends CheckboxTreeNode { + private _dataSourceBrowser: DataSourceBrowser; + private _databaseName: string; + + constructor(databaseName: string, dataSourceBrowser: DataSourceBrowser) { + super(); + this._databaseName = databaseName || this._databaseName; + let nodeId: string = TableFolderNode.getNodeId(this._databaseName); + let tableFolderLabel = localize('tableFolderLabel', 'Tables'); + super.setArgs({ nodeId: nodeId, label: tableFolderLabel }); + this.setDataSourceBrowser(dataSourceBrowser); + } + + public setDataSourceBrowser(dataSourceBrowser: DataSourceBrowser): void { + if (dataSourceBrowser) { + this._dataSourceBrowser = dataSourceBrowser || this._dataSourceBrowser; + let treeId: string = DataSourceId.getIdFromDataSourceBrowser(this._dataSourceBrowser); + super.setArgs({ treeId: treeId }); + } + } + + public async getChildren(): Promise { + if (!this.hasChildren && this._dataSourceBrowser) { + let result = await this._dataSourceBrowser.getTableNames(this._databaseName); + let tableNames: string[] = result.returnValue; + if (result.isSuccess && tableNames && tableNames.length > 0 && this._dataSourceBrowser.vdiManager) { + let vdiManager = this._dataSourceBrowser.vdiManager; + let dataSourceName: string = vdiManager.dataSourceName; + let sourceDatabaseName: string = this._dataSourceBrowser.vdiManager.sourceDatabaseName; + let sourceServerName: string = this._dataSourceBrowser.vdiManager.sourceServerName; + for (let tableNameWithSchema of tableNames) { + let location: string[] = LocationHandler.getLocation(`[${this._databaseName}].${tableNameWithSchema}`); + let childNode = TableTreeNode.getInstance(dataSourceName, sourceServerName, sourceDatabaseName, location); + if (childNode) { + if (this.checked) { + await childNode.setCheckedState(true); + } + this.addChildNode(childNode); + } + } + } + + if (!this.hasChildren) { + this.isLeaf = true; + await this.setCheckedState(false); + await this.setEnable(false); + } + + this.notifyStateChanged(); + } + + this.children = this.children.sort((a, b) => a.label.localeCompare(b.label)); + return this.children; + } + + public get iconPath(): { light: string; dark: string } { + return { + light: PathResolver.folderIconPath, + dark: PathResolver.folderIconDarkPath + }; + } + + private static getNodeId(databaseName: string): string { + let nodeId: string = undefined; + if (databaseName) { + let databaseId: string = LocationHandler.encloseWithBrackets(databaseName); + nodeId = `${databaseId}.[Tables]`; + } + return nodeId; + } + + public static getInstance(databaseName: string, dataSourceBrowser: DataSourceBrowser): TableFolderNode { + let tableFolderNode: TableFolderNode = undefined; + if (databaseName && dataSourceBrowser) { + let treeId: string = DataSourceId.getIdFromDataSourceBrowser(dataSourceBrowser); + let nodeId = TableFolderNode.getNodeId(databaseName); + tableFolderNode = CheckboxTreeNode.findNode(treeId, nodeId) as TableFolderNode; + if (!tableFolderNode) { + tableFolderNode = new TableFolderNode(databaseName, dataSourceBrowser); + } + } + return tableFolderNode; + } +} + +class ViewFolderNode extends CheckboxTreeNode { + private _dataSourceBrowser: DataSourceBrowser; + private _databaseName: string; + + constructor(databaseName: string, dataSourceBrowser: DataSourceBrowser) { + super(); + this._databaseName = databaseName || this._databaseName; + let nodeId: string = ViewFolderNode.getNodeId(databaseName); + let viewFolderLabel = localize('viewFolderLabel', 'Views'); + super.setArgs({ nodeId: nodeId, label: viewFolderLabel }); + this.setDataSourceBrowser(dataSourceBrowser); + } + + public setDataSourceBrowser(dataSourceBrowser: DataSourceBrowser): void { + if (dataSourceBrowser) { + this._dataSourceBrowser = dataSourceBrowser || this._dataSourceBrowser; + let treeId: string = DataSourceId.getIdFromDataSourceBrowser(this._dataSourceBrowser); + super.setArgs({ treeId: treeId }); + } + } + + public async getChildren(): Promise { + if (!this.hasChildren && this._dataSourceBrowser) { + let result = await this._dataSourceBrowser.getViewNameList(this._databaseName); + let viewNames: string[] = result.returnValue; + if (result.isSuccess && viewNames && viewNames.length > 0 && this._dataSourceBrowser.vdiManager) { + let vdiManager = this._dataSourceBrowser.vdiManager; + let dataSourceName: string = vdiManager.dataSourceName; + let sourceDatabaseName: string = this._dataSourceBrowser.vdiManager.sourceDatabaseName; + let sourceServerName: string = this._dataSourceBrowser.vdiManager.sourceServerName; + + for (let viewNameWithSchema of viewNames) { + let location: string[] = LocationHandler.getLocation(`[${this._databaseName}].${viewNameWithSchema}`); + let childNode = ViewTreeNode.getInstance(dataSourceName, sourceServerName, sourceDatabaseName, location); + if (childNode) { + if (this.checked) { + childNode.setCheckedState(true); + } + this.addChildNode(childNode); + } + } + } + + if (!this.hasChildren) { + this.isLeaf = true; + await this.setCheckedState(false); + await this.setEnable(false); + } + + this.notifyStateChanged(); + } + + this.children = this.children.sort((a, b) => a.label.localeCompare(b.label)); + return this.children; + } + + public get iconPath(): { light: string; dark: string } { + return { + light: PathResolver.folderIconPath, + dark: PathResolver.folderIconDarkPath + }; + } + + private static getNodeId(databaseName: string): string { + let nodeId: string = undefined; + if (databaseName) { + let databaseId: string = LocationHandler.encloseWithBrackets(databaseName); + nodeId = `${databaseId}.[Views]`; + } + return nodeId; + } + + public static getInstance(databaseName: string, dataSourceBrowser: DataSourceBrowser): ViewFolderNode { + let viewFolderNode: ViewFolderNode = undefined; + if (databaseName && dataSourceBrowser) { + let treeId: string = DataSourceId.getIdFromDataSourceBrowser(dataSourceBrowser); + let nodeId = ViewFolderNode.getNodeId(databaseName); + viewFolderNode = CheckboxTreeNode.findNode(treeId, nodeId) as ViewFolderNode; + if (!viewFolderNode) { + viewFolderNode = new ViewFolderNode(databaseName, dataSourceBrowser); + } + } + return viewFolderNode; + } +} + +class TableTreeNode extends CheckboxTreeNode { + private static _tableNodeCache: { [treeId: string]: TableTreeNode[] } = {}; + private _location: string[]; + + constructor(dataSourceName: string, sourceServerName: string, sourceDatabaseName: string, location: string[]) { + super(); + this._location = location || this._location; + const treeId: string = DataSourceId.getId(dataSourceName, sourceServerName, sourceDatabaseName); + const nodeId: string = LocationHandler.getLocationString(this._location); + const schema = LocationHandler.getSchemaName(this._location); + const label: string = schema ? + `${schema}.${LocationHandler.getTableName(this._location)}` : + `${LocationHandler.getTableName(this._location)}` + ; + super.setArgs({ treeId: treeId, nodeId: nodeId, label: label, maxLabelLength: 38, isLeaf: true }); + TableTreeNode.AddToCache(this); + } + + public get location(): string[] { + return this._location; + } + + public getChildren(): Promise { + return Promise.resolve([]); + } + + public get iconPath(): string { + return PathResolver.tableIconPath; + } + + public static getInstance(dataSourceName: string, sourceServerName: string, sourceDatabaseName: string, location: string[]): TableTreeNode { + let tableNode: TableTreeNode = undefined; + if (dataSourceName && sourceServerName && location && location.length > 0) { + let nodeId: string = LocationHandler.getLocationString(location); + let treeId: string = DataSourceId.getId(dataSourceName, sourceServerName, sourceDatabaseName); + tableNode = CheckboxTreeNode.findNode(treeId, nodeId) as TableTreeNode; + if (!tableNode) { + tableNode = new TableTreeNode(dataSourceName, sourceServerName, sourceDatabaseName, location); + } + } + return tableNode; + } + + public static clearTableNodeCache() { + TableTreeNode._tableNodeCache = {}; + } + + private static AddToCache(node: TableTreeNode): void { + if (node.treeId) { + if (!TableTreeNode._tableNodeCache[node.treeId]) { + TableTreeNode._tableNodeCache[node.treeId] = []; + } + TableTreeNode._tableNodeCache[node.treeId].push(node); + } + } + + public static getAllNodes(dataSourceName: string, sourceServerName: string, sourceDatabaseName: string): TableTreeNode[] { + let allNodes: TableTreeNode[] = undefined; + let treeId: string = DataSourceId.getId(dataSourceName, sourceServerName, sourceDatabaseName); + if (treeId) { + allNodes = TableTreeNode._tableNodeCache[treeId]; + } + return allNodes; + } +} + +class ViewTreeNode extends TableTreeNode { } + +class DataSourceBrowser { + private _dbNameCache: { [dataSourceId: string]: string[] }; + private _tableNameCache: { [dbOrSchemaId: string]: string[] }; + private _viewNameCache: { [dbOrSchemaId: string]: string[] }; + private _columnDefCache: { [tableOrViewId: string]: ColumnDefinition[] }; + private _currentDataSourceId: string; + + constructor(private _dataModel: VirtualizeDataModel, private _vdiManager: VDIManager) { + this.clearCache(); + } + + public clearCache(): void { + this._dbNameCache = {}; + this._tableNameCache = {}; + this._viewNameCache = {}; + this._columnDefCache = {}; + } + + public get vdiManager(): VDIManager { + return this._vdiManager; + } + + private getDataSourceId(): string { + this._currentDataSourceId = DataSourceId.getId(this._vdiManager.dataSourceName, this._vdiManager.sourceServerName, this._vdiManager.sourceDatabaseName); + return this._currentDataSourceId; + } + + private getDatabaseId(databaseName: string): string { + if (!databaseName) { + return undefined; + } + + this._currentDataSourceId = this.getDataSourceId(); + if (!this._currentDataSourceId) { + return undefined; + } + + let databaseId: string = `${this._currentDataSourceId}.${LocationHandler.encloseWithBrackets(databaseName)}`; + return databaseId; + } + + private getSchemaId(databaseName: string, schemaName: string): string { + if (!databaseName || !schemaName) { + return undefined; + } + + let databaseId: string = this.getDatabaseId(databaseName); + if (!databaseId) { + return undefined; + } + + let schemaId: string = `${databaseId}.${LocationHandler.encloseWithBrackets(schemaName)}`; + return schemaId; + } + + private getTableId(location: string[]): string { + if (!location || location.length === 0) { + return undefined; + } + let dataSourceId = this.getDataSourceId(); + if (!dataSourceId) { + return undefined; + } + + let tableNameId: string = `${dataSourceId}.${LocationHandler.getLocationString(location)}`; + return tableNameId; + } + + public async getDatabaseNames(): Promise { + let dataSourceId: string = this.getDataSourceId(); + if (!dataSourceId) { + return undefined; + } + + if (!this._dbNameCache[dataSourceId] && this._vdiManager) { + let inputValues: VirtualizeDataInput = this._vdiManager.inputUptoConnectionDetailsPage; + let databasesResponse = await this._dataModel.getSourceDatabases(inputValues); + this._dbNameCache[dataSourceId] = databasesResponse.databaseNames; + } + return this._dbNameCache[dataSourceId]; + } + + private async loadTableList(databaseName: string): Promise> { + if (!databaseName) { + return; + } + + let databaseId: string = this.getDatabaseId(databaseName); + if (!databaseId) { + return; + } + + let result: ExecutionResult = undefined; + if (!this._tableNameCache[databaseId]) { + let inputValues: VirtualizeDataInput = this._vdiManager.inputUptoConnectionDetailsPage; + let tablesResponse = await this._dataModel.getSourceTables({ + sessionId: inputValues.sessionId, + virtualizeDataInput: inputValues, + sourceDatabaseName: databaseName + }); + + if (tablesResponse && tablesResponse.isSuccess) { + let schemaTablesList: SchemaTables[] = tablesResponse.schemaTablesList; + if (schemaTablesList && schemaTablesList.length > 0) { + schemaTablesList.forEach(schemaTables => { + let schemaName: string = schemaTables.schemaName; + let tableNamesWithoutSchema: string[] = schemaTables.tableNames; + if (tableNamesWithoutSchema && tableNamesWithoutSchema.length > 0) { + let schemaId: string = `${databaseId}.[${schemaName}]`; + let tableNameWithSchemaSet: Set = new Set(this._tableNameCache[databaseId] || []); + let tableNameWithoutSchemaSet: Set = new Set(this._tableNameCache[schemaId] || []); + tableNamesWithoutSchema.forEach(tableName => { + const schemaPrefix = schemaName ? `[${schemaName}].` : ''; + tableNameWithSchemaSet.add(`${schemaPrefix}[${tableName}]`); + tableNameWithoutSchemaSet.add(`[${tableName}]`); + }); + this._tableNameCache[databaseId] = [...tableNameWithSchemaSet]; + this._tableNameCache[schemaId] = [...tableNameWithoutSchemaSet]; + } + }); + } + result = { isSuccess: true, errorMessages: undefined, returnValue: undefined }; + } else { + result = { isSuccess: false, errorMessages: tablesResponse.errorMessages, returnValue: undefined }; + } + } else { + result = { isSuccess: true, errorMessages: undefined, returnValue: undefined }; + } + + return result; + } + + private async loadViewList(databaseName: string): Promise> { + if (!databaseName) { + return; + } + + let databaseId: string = this.getDatabaseId(databaseName); + if (!databaseId) { + return; + } + + let result: ExecutionResult = undefined; + if (!this._viewNameCache[databaseId]) { + let inputValues: VirtualizeDataInput = this._vdiManager.inputUptoConnectionDetailsPage; + let viewsResponse = await this._dataModel.getSourceViewList({ + virtualizeDataInput: inputValues, + querySubject: databaseName + }); + + if (viewsResponse && viewsResponse.isSuccess) { + let schemaViewsList: SchemaViews[] = viewsResponse.returnValue; + if (schemaViewsList && schemaViewsList.length > 0) { + schemaViewsList.forEach(schemaViews => { + let schemaName: string = schemaViews.schemaName; + let viewNamesWithoutSchema: string[] = schemaViews.viewNames; + if (viewNamesWithoutSchema && viewNamesWithoutSchema.length > 0) { + let schemaId: string = `${databaseId}.[${schemaName}]`; + let viewNameWithSchemaSet: Set = new Set(this._viewNameCache[databaseId] || []); + let viewNameWithoutSchemaSet: Set = new Set(this._viewNameCache[schemaId] || []); + viewNamesWithoutSchema.forEach(viewName => { + const schemaPrefix = schemaName ? `[${schemaName}].` : ''; + viewNameWithSchemaSet.add(`${schemaPrefix}[${viewName}]`); + viewNameWithoutSchemaSet.add(`[${viewName}]`); + }); + this._viewNameCache[databaseId] = [...viewNameWithSchemaSet]; + this._viewNameCache[schemaId] = [...viewNameWithoutSchemaSet]; + } + }); + } + result = { isSuccess: true, errorMessages: undefined, returnValue: undefined }; + } else { + result = { isSuccess: false, errorMessages: viewsResponse.errorMessages, returnValue: undefined }; + } + } else { + result = { isSuccess: true, errorMessages: undefined, returnValue: undefined }; + } + + return result; + } + + public async getTableNames(databaseNameWithoutSchema: string, schemaName?: string): Promise> { + if (!databaseNameWithoutSchema) { + return undefined; + } + + let databaseIdOrSchemaId: string = schemaName ? + this.getSchemaId(databaseNameWithoutSchema, schemaName) : + this.getDatabaseId(databaseNameWithoutSchema); + if (!databaseIdOrSchemaId) { + return undefined; + } + + let result: ExecutionResult = undefined; + if (!this._tableNameCache[databaseIdOrSchemaId]) { + let loadResult = await this.loadTableList(databaseNameWithoutSchema); + if (loadResult.isSuccess) { + let tableOrViewNames: string[] = this._tableNameCache[databaseIdOrSchemaId]; + result = { isSuccess: true, errorMessages: undefined, returnValue: tableOrViewNames }; + } else { + result = { isSuccess: false, errorMessages: loadResult.errorMessages, returnValue: undefined }; + } + } else { + let tableOrViewNames: string[] = this._tableNameCache[databaseIdOrSchemaId]; + result = { isSuccess: true, errorMessages: undefined, returnValue: tableOrViewNames }; + } + return result; + } + + public async getViewNameList(databaseNameWithoutSchema: string, schemaName?: string): Promise> { + if (!databaseNameWithoutSchema) { + return undefined; + } + + let databaseIdOrSchemaId: string = schemaName ? + this.getSchemaId(databaseNameWithoutSchema, schemaName) : + this.getDatabaseId(databaseNameWithoutSchema); + if (!databaseIdOrSchemaId) { + return undefined; + } + + let result: ExecutionResult = undefined; + if (!this._viewNameCache[databaseIdOrSchemaId]) { + let loadResult = await this.loadViewList(databaseNameWithoutSchema); + if (loadResult.isSuccess) { + let tableOrViewNames: string[] = this._viewNameCache[databaseIdOrSchemaId]; + result = { isSuccess: true, errorMessages: undefined, returnValue: tableOrViewNames }; + } else { + result = { isSuccess: false, errorMessages: loadResult.errorMessages, returnValue: undefined }; + } + } else { + let tableOrViewNames: string[] = this._viewNameCache[databaseIdOrSchemaId]; + result = { isSuccess: true, errorMessages: undefined, returnValue: tableOrViewNames }; + } + return result; + } + + public async getColumnDefinitions(location: string[]): Promise> { + if (!location || location.length === 0) { + return undefined; + } + + let tableNameId: string = this.getTableId(location); + if (!tableNameId) { + return undefined; + } + + let result: ExecutionResult = undefined; + if (this._columnDefCache[tableNameId]) { + result = { isSuccess: true, errorMessages: undefined, returnValue: this._columnDefCache[tableNameId] }; + } else if (this._vdiManager) { + let inputValues: VirtualizeDataInput = this._vdiManager.inputUptoConnectionDetailsPage; + result = await this._dataModel.getSourceColumnDefinitions({ + sessionId: inputValues.sessionId, + virtualizeDataInput: inputValues, + location: location + }); + if (result.isSuccess) { + this._columnDefCache[tableNameId] = result.returnValue; + } + } else { + result = { isSuccess: false, errorMessages: undefined, returnValue: undefined }; + } + return result; + } +} + +export class LocationHandler { + public static isEnclosedWithBrackets(str: string): boolean { + let isEnclosed: boolean = undefined; + if (str) { + isEnclosed = (/^\[(.+)\]$/g).test(str); + } + return isEnclosed; + } + + public static encloseWithBrackets(str: string): string { + let result: string = undefined; + if (str) { + result = LocationHandler.isEnclosedWithBrackets(str) ? str : `[${str}]`; + } + return result; + } + + public static peelOffBrackets(locationString: string): string { + let result: string; + if (locationString) { + let location: string[] = LocationHandler.getLocation(locationString); + if (location && location.length > 0) { + result = location.join('.'); + } + } + return result; + } + + public static getLocationString(location: string[]): string { + let locationString: string = undefined; + if (location && location.length > 0) { + locationString = location.map(e => LocationHandler.encloseWithBrackets(e)).join('.'); + } + return locationString; + } + + public static isLocationStrFormat(locationString: string): boolean { + let isCorrect: boolean = undefined; + if (locationString) { + isCorrect = (/^\[.+\](\.\[.+\])*$/g).test(locationString); + } + return isCorrect; + } + + public static getLocation(locationString: string): string[] { + let location: string[] = undefined; + if (locationString) { + if (LocationHandler.isLocationStrFormat(locationString)) { + if (!locationString.includes('].[')) { + location = [locationString.substr(1, locationString.length - 2)]; + } else { + location = locationString.substr(1, locationString.length - 2).split('].['); + } + } else { + location = [locationString]; + } + } + return location; + } + + public static getSchemaName(location: string[]): string { + let schemaName: string = undefined; + if (location && location.length >= 2) { + schemaName = location[location.length - 2]; + } + return schemaName; + } + + public static getTableName(location: string[]): string { + let tableName: string = undefined; + if (location && location.length > 0) { + tableName = location[location.length - 1]; + } + return tableName; + } +} + +class MappingInfoCache { + private _mappingInfoCache: { [dataSourceId: string]: Map }; + private _currentCache: Map; + private _vdiManager: VDIManager; + + constructor(vdiManager: VDIManager) { + this._mappingInfoCache = {}; + this._vdiManager = vdiManager; + } + + private refreshCurrentCache(): void { + if (this._vdiManager) { + let dataSourceName: string = this._vdiManager.dataSourceName; + let sourceDatabaseName: string = this._vdiManager.sourceDatabaseName; + let sourceServerName: string = this._vdiManager.sourceServerName; + let dataSourceId: string = DataSourceId.getId(dataSourceName, sourceServerName, sourceDatabaseName); + if (dataSourceId) { + if (!this._mappingInfoCache[dataSourceId]) { + this._mappingInfoCache[dataSourceId] = new Map(); + } + this._currentCache = this._mappingInfoCache[dataSourceId]; + } + } + } + + public putMappingInfo(desiredTableName: string[], location: string[], columnDefinitions: ColumnDefinition[], replaceExisting?: boolean): void { + this.refreshCurrentCache(); + if (desiredTableName && desiredTableName.length >= 1 && desiredTableName.length <= 2 && + location && location.length > 0 && columnDefinitions && columnDefinitions.length > 0 && this._currentCache) { + let cacheKey: string = this.getCacheKey(location); + if (replaceExisting) { + if (this._currentCache.has(cacheKey)) { + this._currentCache.delete(cacheKey); + } + this._currentCache.set(cacheKey, new TableMappingInfo(desiredTableName, columnDefinitions, location)); + } else { + if (!this._currentCache.has(cacheKey)) { + this._currentCache.set(cacheKey, new TableMappingInfo(desiredTableName, columnDefinitions, location)); + } + } + } + } + + private getCacheKey(location: string[]): string { + return LocationHandler.getLocationString(location); + } + + public hasMappingInfo(location: string[]): boolean { + this.refreshCurrentCache(); + let key: string = this.getCacheKey(location); + return location && this._currentCache && key ? this._currentCache.has(key) : undefined; + } + + public getMappingInfo(location: string[]): TableMappingInfo { + this.refreshCurrentCache(); + let modifiedInfo: TableMappingInfo = undefined; + if (this.hasMappingInfo(location)) { + modifiedInfo = this._currentCache.get(this.getCacheKey(location)); + } + return modifiedInfo; + } +} + +class TableMappingInfo implements ExternalTableInfo { + public externalTableName: string[]; + public columnDefinitionList: ColumnDefinition[]; + public sourceTableLocation: string[]; + public fileFormat?: FileFormat; + + constructor(externalTableName: string[], columnDefinitionList: ColumnDefinition[], sourceTableLocation: string[]) { + this.externalTableName = externalTableName; + this.columnDefinitionList = columnDefinitionList; + this.sourceTableLocation = sourceTableLocation; + } +} + +class MappingInfoRetriever { + private _dataSourceBrowser: DataSourceBrowser; + private _mappingInfoCache: MappingInfoCache; + private _virtualizeDataModel: VirtualizeDataModel; + + constructor(dataSourceBrowser: DataSourceBrowser, mappingInfoCache: MappingInfoCache, virtualizeDataModel: VirtualizeDataModel) { + this._dataSourceBrowser = dataSourceBrowser; + this._mappingInfoCache = mappingInfoCache; + this._virtualizeDataModel = virtualizeDataModel; + } + + public async getMappingInfo(location: string[]): Promise> { + let result: ExecutionResult = null; + if (location && this._mappingInfoCache) { + if (this._mappingInfoCache.hasMappingInfo(location)) { + let mappingInfo: TableMappingInfo = this._mappingInfoCache.getMappingInfo(location); + result = { isSuccess: true, errorMessages: undefined, returnValue: mappingInfo }; + } else if (this._dataSourceBrowser) { + let colDefResult: ExecutionResult = await this._dataSourceBrowser.getColumnDefinitions(location); + if (colDefResult) { + if (colDefResult.isSuccess && colDefResult.returnValue && colDefResult.returnValue.length > 0) { + let tableNameWithoutSchema: string = LocationHandler.getTableName(location); + const schemaName = LocationHandler.getSchemaName(location); + let colDefs: ColumnDefinition[] = colDefResult.returnValue; + this._mappingInfoCache.putMappingInfo([schemaName, tableNameWithoutSchema], location, colDefs); + let mappingInfo: TableMappingInfo = this._mappingInfoCache.getMappingInfo(location); + result = { isSuccess: true, errorMessages: undefined, returnValue: mappingInfo }; + } else { + result = { isSuccess: false, errorMessages: colDefResult.errorMessages, returnValue: undefined }; + } + } else { + result = { isSuccess: false, errorMessages: undefined, returnValue: undefined }; + } + } + } + return result; + } +} + +class PathResolver { + private static _appContext: AppContext; + private static _absolutePaths: { [target: string]: string } = {}; + + public static initialize(appContext: AppContext): void { + PathResolver.appContext = appContext; + } + + public static set appContext(appContext: AppContext) { + PathResolver._appContext = appContext; + PathResolver.setAbsoluteIconPath('databaseIconPath', path.join('resources', 'light', 'database_OE.svg')); + PathResolver.setAbsoluteIconPath('tableIconPath', path.join('resources', 'light', 'table.svg')); + PathResolver.setAbsoluteIconPath('folderIconPath', path.join('resources', 'light', 'Folder.svg')); + PathResolver.setAbsoluteIconPath('folderIconDarkPath', path.join('resources', 'dark', 'folder_inverse.svg')); + } + + private static setAbsoluteIconPath(target: string, relativePath: string): void { + PathResolver._absolutePaths[target] = this._appContext.extensionContext.asAbsolutePath(relativePath); + } + + public static get databaseIconPath(): string { return PathResolver._absolutePaths['databaseIconPath']; } + public static get tableIconPath(): string { return PathResolver._absolutePaths['tableIconPath']; } + public static get folderIconPath(): string { return PathResolver._absolutePaths['folderIconPath']; } + public static get folderIconDarkPath(): string { return PathResolver._absolutePaths['folderIconDarkPath']; } +} + +class TitledContainer { + private _modelBuilder: azdata.ModelBuilder; + + private _titleTopMargin: number; + private _titleBottomMargin: number; + public titleFontSize: number; + public title: string; + public titleRightMargin: number; + public titleLeftMargin: number; + + private _contentContainers: azdata.FlexContainer[]; + private _fullContainer: azdata.FlexContainer; + + public topPaddingPx: number; + public rightPaddingPx: number; + public bottomPaddingPx: number; + public leftPaddingPx: number; + + private static readonly _pBeforePx: number = 16; + private static readonly _pAfterPx: number = 16; + + constructor(modelBuilder: azdata.ModelBuilder) { + this._modelBuilder = modelBuilder; + this._contentContainers = []; + + this.titleFontSize = 14; + this.setTitleMargin(0, 0, 5, 0); + this.setPadding(10, 30, 0, 30); + } + + public set titleTopMargin(px: number) { + this._titleTopMargin = px - TitledContainer._pBeforePx; + } + + public set titleBottomMargin(px: number) { + this._titleBottomMargin = px - TitledContainer._pAfterPx; + } + + public setTitleMargin(topPx: number, rightPx: number, bottomPx: number, leftPx: number) { + this.titleTopMargin = topPx; + this.titleRightMargin = rightPx; + this.titleBottomMargin = bottomPx; + this.titleLeftMargin = leftPx; + } + + public setPadding(topPx: number, rightPx: number, bottomPx: number, leftPx: number) { + this.topPaddingPx = topPx; + this.rightPaddingPx = rightPx; + this.bottomPaddingPx = bottomPx; + this.leftPaddingPx = leftPx; + } + + public addContentContainer(content: azdata.FlexContainer) { + this._contentContainers.push(content); + } + + public get flexContainer(): azdata.FlexContainer { + let titleContainer: azdata.FlexContainer = undefined; + if (this.title) { + let titleTextComponent = this._modelBuilder.text() + .withProps({ value: this.title }) + .component(); + titleContainer = this._modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'column', + alignItems: 'start', + width: '100%' + }) + .component(); + titleContainer.addItem(titleTextComponent, { + flex: '1, 1, 0%', + CSSStyles: { + 'font-size': `${this.titleFontSize}px`, + 'width': '100%', + 'margin': `${this._titleTopMargin}px ${this.titleRightMargin}px ${this._titleBottomMargin}px ${this.titleLeftMargin}px` + } + }); + } + + let bindingContainer = this._modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'column', + alignItems: 'stretch', + width: '100%' + }) + .component(); + if (titleContainer) { + bindingContainer.addItem(titleContainer, { + flex: '1, 1, 0%', + CSSStyles: { + 'width': '100%' + } + }); + } + for (let content of this._contentContainers) { + bindingContainer.addItem(content, { + flex: '1, 1, 0%', + CSSStyles: { + 'width': '100%' + } + }); + } + + if (!this._fullContainer) { + this._fullContainer = this._modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'column', + alignItems: 'stretch', + width: '100%' + }) + .component(); + } else { + this._fullContainer.clearItems(); + } + + this._fullContainer.addItem(bindingContainer, { + flex: '1, 1, 0%', + CSSStyles: { + 'padding': `${this.topPaddingPx}px ${this.rightPaddingPx}px ${this.bottomPaddingPx}px ${this.leftPaddingPx}px` + } + }); + + return this._fullContainer; + } +} diff --git a/extensions/datavirtualization/src/wizards/virtualizeData/selectDataSourcePage.ts b/extensions/datavirtualization/src/wizards/virtualizeData/selectDataSourcePage.ts new file mode 100644 index 0000000000..1ed7a4b66d --- /dev/null +++ b/extensions/datavirtualization/src/wizards/virtualizeData/selectDataSourcePage.ts @@ -0,0 +1,225 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +import * as localizedConstants from '../../localizedConstants'; +const localize = nls.loadMessageBundle(); + +import { IWizardPageWrapper } from '../wizardPageWrapper'; +import { VirtualizeDataModel } from './virtualizeDataModel'; +import { VirtualizeDataInput } from '../../services/contracts'; +import { getDropdownValue } from '../../utils'; +import { AppContext } from '../../appContext'; +import { VDIManager } from './virtualizeDataInputManager'; +import { VirtualizeDataWizard } from './virtualizeDataWizard'; +import { CreateMasterKeyPage } from './createMasterKeyPage'; + +export class SelectDataSourcePage implements IWizardPageWrapper { + private readonly SqlServerType = localizedConstants.SqlServerName; + private readonly DefaultType = localize('defaultSourceType', 'Default'); + private readonly IconsConfig: {} = {}; + + private _dataModel: VirtualizeDataModel; + private _vdiManager: VDIManager; + private _appContext: AppContext; + + private _page: azdata.window.WizardPage; + + private _modelBuilder: azdata.ModelBuilder; + private _formContainer: azdata.FormBuilder; + + private _loadingSpinner: azdata.LoadingComponent; + private _destDBDropDown: azdata.DropDownComponent; + private _selectedSourceType: string; + + private _componentsAreSetup: boolean; + private _modelInitialized: boolean; + + constructor(private _virtualizeDataWizard: VirtualizeDataWizard) { + if (this._virtualizeDataWizard) { + this._dataModel = _virtualizeDataWizard.dataModel; + this._vdiManager = _virtualizeDataWizard.vdiManager; + this._appContext = _virtualizeDataWizard.appContext; + } + + this._componentsAreSetup = false; + this._modelInitialized = false; + + this.IconsConfig[this.SqlServerType] = { + light: 'resources/light/server.svg', + dark: 'resources/dark/server_inverse.svg' + }; + this.IconsConfig[this.DefaultType] = { + light: 'resources/light/database.svg', + dark: 'resources/dark/database_inverse.svg' + }; + + this._page = azdata.window.createWizardPage(localize('selectDataSrcTitle', 'Select a Data Source')); + + this._page.registerContent(async (modelView) => { + this._modelBuilder = modelView.modelBuilder; + this._formContainer = this._modelBuilder.formContainer(); + + let parentLayout: azdata.FormItemLayout = { + horizontal: false + }; + + this._destDBDropDown = this._modelBuilder.dropDown().withProps({ + values: [], + value: '', + height: undefined, + width: undefined + }).component(); + + this._loadingSpinner = this._modelBuilder.loadingComponent() + .withItem(this._destDBDropDown) + .withProps({ loading: true }) + .component(); + + this._formContainer.addFormItem({ + component: this._loadingSpinner, + title: localize('destDBLabel', 'Select the destination database for your external table') + }, + Object.assign({ info: localize('destDBHelpText', 'The database in which to create your External Data Source.') }, + parentLayout) + ); + + await modelView.initializeModel(this._formContainer.component()); + + this._modelInitialized = true; + await this.setupPageComponents(); + }); + } + + public async setupPageComponents(): Promise { + if (!this._componentsAreSetup && this._modelInitialized && this._dataModel.configInfoResponse) { + this._componentsAreSetup = true; + + let parentLayout: azdata.FormItemLayout = { + horizontal: false + }; + + // Destination DB + let databaseList: string[] = this._dataModel.destDatabaseList.map(db => db.name).sort((a, b) => a.localeCompare(b)); + let connectedDatabase = this._dataModel.connection.databaseName; + let selectedDatabase: string; + if (connectedDatabase && databaseList.some(name => name === connectedDatabase)) { + selectedDatabase = connectedDatabase; + } else { + selectedDatabase = databaseList.length > 0 ? databaseList[0] : ''; + } + + await this._destDBDropDown.updateProperties({ + values: databaseList, + value: selectedDatabase + }); + await this.toggleCreateMasterKeyPage(getDropdownValue(this._destDBDropDown.value)); + this._destDBDropDown.onValueChanged(async (selection) => { + await this.toggleCreateMasterKeyPage(selection.selected); + }); + + await this._loadingSpinner.updateProperties({ + loading: false + }); + + // Source Type + let components: azdata.FormComponent[] = []; + let info = this._dataModel.configInfoResponse; + const cards: azdata.RadioCard[] = []; + info.supportedSourceTypes.forEach(sourceType => { + let typeName = sourceType.typeName; + let iconTypeName: string; + if (this.IconsConfig[typeName]) { + iconTypeName = typeName; + } else { + iconTypeName = this.DefaultType; + } + + let iconPath = this._appContext ? + { + light: this._appContext.extensionContext.asAbsolutePath(this.IconsConfig[iconTypeName].light), + dark: this._appContext.extensionContext.asAbsolutePath(this.IconsConfig[iconTypeName].dark) + } : undefined; + + cards.push({ + id: typeName, + descriptions: [{ textValue: typeName }], + icon: iconPath + }); + }); + + const cardGroup = this._modelBuilder.radioCardGroup().withProps({ + cards: cards, + cardWidth: '150px', + cardHeight: '160px', + iconWidth: '50px', + iconHeight: '50px' + }).component(); + + cardGroup.onSelectionChanged((e: azdata.RadioCardSelectionChangedEvent) => { + this._selectedSourceType = e.cardId; + }); + + if (cards.length > 0) { + cardGroup.selectedCardId = cards[0].id; + } + + components.push({ + component: cardGroup, + title: localize('sourceCardsLabel', 'Select your data source type') + }); + this._formContainer.addFormItems(components, parentLayout); + + this._dataModel.wizard.nextButton.enabled = true; + } + } + + public async validate(): Promise { + let inputValues = this._vdiManager.getVirtualizeDataInput(this); + if (!inputValues.sourceServerType) { + this._dataModel.showWizardError(localize('noServerTypeError', 'A data source type must be selected.')); + return false; + } + if (!inputValues.destDatabaseName) { + this._dataModel.showWizardError(localize('noDestDatabaseError', 'A destination database must be selected.')); + return false; + } + + return await this._dataModel.validateInput(inputValues); + } + + private async toggleCreateMasterKeyPage(dbSelected: string): Promise { + if (!dbSelected || !this._virtualizeDataWizard || !this._virtualizeDataWizard.wizard + || !this._virtualizeDataWizard.wizard.pages) { return; } + let databaseListWithMasterKey: string[] = this._dataModel.destDatabaseList.filter(db => db.hasMasterKey).map(db => db.name) || []; + let currentPages = this._virtualizeDataWizard.wizard.pages; + let currentWrappers = currentPages.map(p => p['owner']); + if (databaseListWithMasterKey.find(e => e === dbSelected)) { + let indexToRemove = currentWrappers.findIndex(w => w instanceof CreateMasterKeyPage); + if (indexToRemove >= 0) { + await this._virtualizeDataWizard.wizard.removePage(indexToRemove); + } + } else if (!currentWrappers.find(w => w instanceof CreateMasterKeyPage)) { + let thisWrapperIndex = currentWrappers.findIndex(w => Object.is(w, this)); + let createMasterKeyPageWrapper = this._virtualizeDataWizard.wizardPageWrappers.find(w => w instanceof CreateMasterKeyPage); + await this._virtualizeDataWizard.wizard.addPage(createMasterKeyPageWrapper.getPage(), thisWrapperIndex + 1); + } + } + + public getPage(): azdata.window.WizardPage { + return this._page; + } + + public async updatePage(): Promise { + return; + } + + public getInputValues(existingInput: VirtualizeDataInput): void { + existingInput.destDatabaseName = (this._destDBDropDown && this._destDBDropDown.value) ? + getDropdownValue(this._destDBDropDown.value) : undefined; + existingInput.sourceServerType = this._selectedSourceType; + } +} diff --git a/extensions/datavirtualization/src/wizards/virtualizeData/summaryPage.ts b/extensions/datavirtualization/src/wizards/virtualizeData/summaryPage.ts new file mode 100644 index 0000000000..9639294a2f --- /dev/null +++ b/extensions/datavirtualization/src/wizards/virtualizeData/summaryPage.ts @@ -0,0 +1,200 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { IWizardPageWrapper } from '../wizardPageWrapper'; +import { VirtualizeDataModel } from './virtualizeDataModel'; +import { VirtualizeDataInput } from '../../services/contracts'; +import { VDIManager } from './virtualizeDataInputManager'; +import { AppContext } from '../../appContext'; + +export class SummaryUiElements { + public destDBLabel: azdata.TextComponent; + public summaryTable: azdata.DeclarativeTableComponent; +} + +export class SummaryPage implements IWizardPageWrapper { + private _page: azdata.window.WizardPage; + + private _uiElements: SummaryUiElements; + + private readonly _taskLabel = localize('virtualizeTaskLabel', 'Virtualize Data'); + + constructor(private _dataModel: VirtualizeDataModel, private _vdiManager: VDIManager, private _appContext: AppContext) { + this._page = this._appContext.apiWrapper.createWizardPage(localize('summaryPageTitle', 'Summary')); + this._page.registerContent(async (modelView) => { + let ui = new SummaryUiElements(); + let builder = modelView.modelBuilder; + let components: azdata.FormComponent[] = []; + + ui.destDBLabel = builder.text().withProperties({ + value: '' + }).component(); + components.push({ + component: ui.destDBLabel, + title: localize('summaryDestDb', 'Destination Database:') + }); + + let tableData = [['', '']]; + ui.summaryTable = builder.declarativeTable() + .withProperties({ + columns: [{ + displayName: localize('summaryObjTypeLabel', 'Object type'), + valueType: azdata.DeclarativeDataType.string, + width: '300px', + isReadOnly: true + }, { + displayName: localize('summaryObjNameLabel', 'Name'), + valueType: azdata.DeclarativeDataType.string, + width: '300px', + isReadOnly: true + } + ], + data: tableData + }).component(); + components.push({ + component: ui.summaryTable, + title: localize('summaryTitle', 'The following objects will be created in the destination database:') + }); + + let form = builder.formContainer() + .withFormItems(components, { + horizontal: false + }) + .withLayout({ + width: '800px' + }).component(); + + this.setUi(ui); + await modelView.initializeModel(form); + }); + } + + public setUi(ui: SummaryUiElements): void { + this._uiElements = ui; + } + + public async validate(): Promise { + this._dataModel.wizard.registerOperation({ + connection: undefined, + displayName: this._taskLabel, + description: this._taskLabel, + isCancelable: false, + operation: op => { + op.updateStatus(azdata.TaskStatus.InProgress, localize('virtualizeTaskStart', 'Executing script...')); + + let inputValues = this._vdiManager.getVirtualizeDataInput(); + this._dataModel.processInput(inputValues).then(response => { + if (!response.isSuccess) { + op.updateStatus(azdata.TaskStatus.Failed, localize('createSourceError', 'External Table creation failed')); + if (response.errorMessages) { + this._appContext.apiWrapper.showErrorMessage(response.errorMessages.join('\n')); + } + } else { + op.updateStatus(azdata.TaskStatus.Succeeded, localize('createSourceInfo', 'External Table creation completed successfully')); + let serverName = this._dataModel.connection.serverName; + let databaseName = inputValues.destDatabaseName; + let nodePath = `${serverName}/Databases/${databaseName}/Tables`; + let username = this._dataModel.connection.userName; + SummaryPage.refreshExplorerNode(nodePath, '/', username); + } + }); + } + }); + + // Always return true, so that wizard closes. + return true; + } + + private static async refreshExplorerNode(nodePath: string, delimiter: string, username?: string): Promise { + if (!nodePath || !delimiter) { return false; } + let refreshNodePath = nodePath.split(delimiter); + if (!refreshNodePath || refreshNodePath.length === 0) { return false; } + + let isSuccess: boolean = false; + try { + let targetNodes: azdata.objectexplorer.ObjectExplorerNode[] = undefined; + let nodes = await azdata.objectexplorer.getActiveConnectionNodes(); + if (nodes && username) { + nodes = nodes.filter(n => n.label.endsWith(` - ${username})`)); + } + let currentNodePath: string = undefined; + for (let i = 0; i < refreshNodePath.length; ++i) { + if (nodes && nodes.length > 0) { + currentNodePath = currentNodePath ? `${currentNodePath}/${refreshNodePath[i]}` : refreshNodePath[i]; + let currentNodes = nodes.filter(node => node.nodePath === currentNodePath); + if (currentNodes && currentNodes.length > 0) { + targetNodes = currentNodes; + let newNodes = []; + for (let n of targetNodes) { newNodes = newNodes.concat(await n.getChildren()); } + nodes = newNodes; + } else { + nodes = undefined; + } + } else { + break; + } + } + + if (targetNodes && targetNodes.length > 0) { + for (let n of targetNodes) { await n.refresh(); } + isSuccess = true; + } + } catch { } + return isSuccess; + } + + public getPage(): azdata.window.WizardPage { + return this._page; + } + + public async updatePage(): Promise { + let summary = this._vdiManager.getVirtualizeDataInput(); + if (summary) { + await this._uiElements.destDBLabel.updateProperties({ + value: summary.destDatabaseName + }); + + let tableData = this.getTableData(summary); + await this._uiElements.summaryTable.updateProperties({ + data: tableData + }); + } + } + + private getTableData(summary: VirtualizeDataInput): string[][] { + let data = []; + if (summary.destDbMasterKeyPwd) { + let mdash = '\u2014'; + data.push([localize('summaryMasterKeyLabel', 'Database Master Key'), mdash]); + } + if (summary.newCredentialName) { + data.push([localize('summaryCredLabel', 'Database Scoped Credential'), summary.newCredentialName]); + } + if (summary.newDataSourceName) { + data.push([localize('summaryDataSrcLabel', 'External Data Source'), summary.newDataSourceName]); + } + if (summary.newSchemas) { + for (let schemaName of summary.newSchemas) { + data.push([localize('summaryNewSchemaLabel', 'Schema'), schemaName]); + } + } + if (summary.externalTableInfoList) { + let labelText: string = localize('summaryExternalTableLabel', 'External Table'); + for (let tableInfo of summary.externalTableInfoList) { + data.push([labelText, tableInfo.externalTableName.join('.')]); + } + } + + return data; + } + + public getInputValues(existingInput: VirtualizeDataInput): void { + return; + } +} diff --git a/extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataInputManager.ts b/extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataInputManager.ts new file mode 100644 index 0000000000..1a9391ab2d --- /dev/null +++ b/extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataInputManager.ts @@ -0,0 +1,171 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ConnectionDetailsPage } from "./connectionDetailsPage"; +import { CreateMasterKeyPage } from "./createMasterKeyPage"; +import { IWizardPageWrapper } from "../wizardPageWrapper"; +import { ObjectMappingPage } from "./objectMappingPage"; +import { SelectDataSourcePage } from "./selectDataSourcePage"; +import { VirtualizeDataModel } from "./virtualizeDataModel"; +import { VirtualizeDataInput } from "../../services/contracts"; + +export class VDIManager { + private _selectDataSourcePage: IWizardPageWrapper; + private _createMasterKeyPage: IWizardPageWrapper; + private _connectionDetailsPage: IWizardPageWrapper; + private _objectMappingPage: IWizardPageWrapper; + private _pages: IWizardPageWrapper[]; + + private _virtualizeDataModel: VirtualizeDataModel; + private _propertyLookUp: Map = new Map(); + + public setInputPages(inputPages: IWizardPageWrapper[]): void { + if (inputPages && inputPages.length > 0) { + this._pages = inputPages; + this.setInputPagesInOrder(); + this.setPropertyLookUp(); + } + } + + private setInputPagesInOrder(): void { + this._selectDataSourcePage = this.getSelectDataSourcePage(); + this._createMasterKeyPage = this.getCreateMasterKeyPage(); + this._connectionDetailsPage = this.getConnectionDetailsPage(); + this._objectMappingPage = this.getObjectMappingPage(); + let inputPages: IWizardPageWrapper[] = []; + [ + this._selectDataSourcePage, + this._createMasterKeyPage, + this._connectionDetailsPage, + this._objectMappingPage + ].forEach(e => { + if (e) { inputPages.push(e); } + }); + this._pages = inputPages; + } + + private setPropertyLookUp(): void { + if (this._pages && this._pages.length > 0) { + this._pages.forEach(page => { + if (page instanceof SelectDataSourcePage) { + this._propertyLookUp.set('destDatabaseName', page); + this._propertyLookUp.set('sourceServerType', page); + } else if (page instanceof CreateMasterKeyPage) { + this._propertyLookUp.set('destDbMasterKeyPwd', page); + } else if (page instanceof ConnectionDetailsPage) { + this._propertyLookUp.set('existingCredentialName', page); + this._propertyLookUp.set('newCredentialName', page); + this._propertyLookUp.set('sourceUsername', page); + this._propertyLookUp.set('sourcePassword', page); + this._propertyLookUp.set('existingDataSourceName', page); + this._propertyLookUp.set('newDataSourceName', page); + this._propertyLookUp.set('sourceServerName', page); + this._propertyLookUp.set('sourceDatabaseName', page); + } else if (page instanceof ObjectMappingPage) { + this._propertyLookUp.set('externalTableInfoList', page); + } + // No inputs set from SummaryPage + }); + } + } + + public setVirtualizeDataModel(virtualizeDataModel: VirtualizeDataModel): void { + this._virtualizeDataModel = virtualizeDataModel; + } + + public getVirtualizeDataInput(upToPage?: IWizardPageWrapper): VirtualizeDataInput { + let virtualizeDataInput: VirtualizeDataInput = VDIManager.getEmptyInputInstance(); + if (this._virtualizeDataModel && this._virtualizeDataModel.configInfoResponse) { + virtualizeDataInput.sessionId = this._virtualizeDataModel.configInfoResponse.sessionId; + } + for (let page of this._pages) { + if (page) { + page.getInputValues(virtualizeDataInput); + if (upToPage && page === upToPage) { break; } + } + } + return virtualizeDataInput; + } + + public get virtualizeDataInput(): VirtualizeDataInput { + return this.getVirtualizeDataInput(); + } + + public getPropertyValue(property: string): any { + let propertyValue: any = undefined; + if (property && this._propertyLookUp.has(property)) { + let pageInput = VDIManager.getEmptyInputInstance(); + this._propertyLookUp.get(property).getInputValues(pageInput); + if (pageInput) { + propertyValue = pageInput[property]; + } + } + return propertyValue; + } + + public get dataSourceName(): string { + return this.existingDataSourceName || this.newDataSourceName; + } + + public get existingDataSourceName(): string { + return this.getPropertyValue('existingDataSourceName'); + } + + public get newDataSourceName(): string { + return this.getPropertyValue('newDataSourceName'); + } + + public get sourceServerName(): string { + return this.getPropertyValue('sourceServerName'); + } + + public get sourceDatabaseName(): string { + return this.getPropertyValue('sourceDatabaseName'); + } + + public get destinationDatabaseName(): string { + return this.getPropertyValue('destDatabaseName'); + } + + public get sourceServerType(): string { + return this.getPropertyValue('sourceServerType'); + } + + public get externalTableInfoList(): string { + return this.getPropertyValue('externalTableInfoList'); + } + + public get destDbMasterKeyPwd(): string { + return this.getPropertyValue('destDbMasterKeyPwd'); + } + + public get inputUptoConnectionDetailsPage(): VirtualizeDataInput { + let inputValues: VirtualizeDataInput = undefined; + if (this._connectionDetailsPage) { + inputValues = this.getVirtualizeDataInput(this._connectionDetailsPage); + } + return inputValues; + } + + private getSelectDataSourcePage(): IWizardPageWrapper { + return this._pages.find(page => page instanceof SelectDataSourcePage); + } + + private getCreateMasterKeyPage(): IWizardPageWrapper { + return this._pages.find(page => page instanceof CreateMasterKeyPage); + } + + private getConnectionDetailsPage(): IWizardPageWrapper { + return this._pages.find(page => page instanceof ConnectionDetailsPage); + } + + private getObjectMappingPage(): IWizardPageWrapper { + return this._pages.find(page => page instanceof ObjectMappingPage); + } + + public static getEmptyInputInstance(): VirtualizeDataInput { + return {}; + } +} diff --git a/extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataModel.ts b/extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataModel.ts new file mode 100644 index 0000000000..5e1e7a9f90 --- /dev/null +++ b/extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataModel.ts @@ -0,0 +1,271 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import * as utils from '../../utils'; + +import { + DataSourceWizardConfigInfoResponse, DataSourceWizardService, VirtualizeDataInput, + ProcessVirtualizeDataInputResponse, + GenerateScriptResponse, + GetDatabaseInfoResponse, + DatabaseInfo, + CredentialInfo, + GetSourceDatabasesResponse, + GetSourceTablesRequestParams, + GetSourceTablesResponse, + GetSourceColumnDefinitionsRequestParams, + ColumnDefinition, + ExecutionResult, + DataSourceBrowsingParams, + SchemaViews, + DatabaseOverview +} from '../../services/contracts'; +import { VDIManager } from './virtualizeDataInputManager'; + +// Stores important state and service methods used by the Virtualize Data wizard. +export class VirtualizeDataModel { + + private _configInfoResponse: DataSourceWizardConfigInfoResponse; + private _databaseInfo: { [databaseName: string]: DatabaseInfo }; + + constructor( + private readonly _connection: azdata.connection.ConnectionProfile, + private readonly _wizardService: DataSourceWizardService, + private readonly _wizard: azdata.window.Wizard, + private readonly _vdiManager: VDIManager) { + this._databaseInfo = {}; + } + + public get connection(): azdata.connection.ConnectionProfile { + return this._connection; + } + + public get wizardService(): DataSourceWizardService { + return this._wizardService; + } + + public get wizard(): azdata.window.Wizard { + return this._wizard; + } + + public get configInfoResponse(): DataSourceWizardConfigInfoResponse { + return this._configInfoResponse; + } + + public get destDatabaseList(): DatabaseOverview[] { + return this._configInfoResponse ? (this._configInfoResponse.databaseList || []) : []; + } + + public get sessionId(): string { + return this._configInfoResponse ? this._configInfoResponse.sessionId : undefined; + } + + public get existingCredentials(): CredentialInfo[] { + let currentDbInfo = this._databaseInfo[this.selectedDestDatabaseName]; + return currentDbInfo ? currentDbInfo.existingCredentials : undefined; + } + + private get selectedDestDatabaseName(): string { + return this._vdiManager.destinationDatabaseName; + } + + public get defaultSchema(): string { + let currentDbInfo = this._databaseInfo[this.selectedDestDatabaseName]; + return currentDbInfo ? currentDbInfo.defaultSchema : undefined; + } + + public get schemaList(): string[] { + let currentDbInfo = this._databaseInfo[this.selectedDestDatabaseName]; + return currentDbInfo ? currentDbInfo.schemaList : []; + } + + public async hasMasterKey(): Promise { + let dbInfo = this._databaseInfo[this.selectedDestDatabaseName]; + if (!dbInfo) { + await this.loadDatabaseInfo(); + dbInfo = this._databaseInfo[this.selectedDestDatabaseName]; + } + return dbInfo.hasMasterKey; + } + + public showWizardError(title: string, description?: string): void { + this.showWizardMessage(title, description, azdata.window.MessageLevel.Error); + } + + public showWizardInfo(title: string, description?: string): void { + this.showWizardMessage(title, description, azdata.window.MessageLevel.Information); + } + + public showWizardWarning(title: string, description?: string): void { + this.showWizardMessage(title, description, azdata.window.MessageLevel.Warning); + } + + public showWizardMessage(title: string, description: string, msgLevel: number): void { + this._wizard.message = { + text: title, + level: msgLevel, + description: description + }; + } + + public async createSession(): Promise { + if (!this._configInfoResponse) { + try { + let credentials = await azdata.connection.getCredentials(this.connection.connectionId); + if (credentials) { + Object.assign(this.connection, credentials); + } + } catch (error) { + // swallow this as either it was integrated auth or we will fail later with login failed, + // which is a good error that makes sense to the user + } + + try { + const timeout = vscode.workspace.getConfiguration('mssql').get('query.executionTimeout'); + this.connection.options['QueryTimeout'] = timeout; + this._configInfoResponse = await this.wizardService.createDataSourceWizardSession(this.connection); + } catch (error) { + this.showWizardError(utils.getErrorMessage(error)); + this._configInfoResponse = { + sessionId: undefined, + supportedSourceTypes: [], + databaseList: [], + serverMajorVersion: -1, + productLevel: undefined + }; + } + } + } + + public async validateInput(virtualizeDataInput: VirtualizeDataInput): Promise { + try { + let response = await this._wizardService.validateVirtualizeDataInput(virtualizeDataInput); + if (!response.isValid) { + this.showWizardError(response.errorMessages.join('\n')); + } + return response.isValid; + } catch (error) { + this.showWizardError(utils.getErrorMessage(error)); + return false; + } + } + + public async getDatabaseInfo(databaseName: string): Promise { + try { + let response = await this._wizardService.getDatabaseInfo({ sessionId: this.sessionId, databaseName: databaseName }); + if (!response.isSuccess) { + this.showWizardError(response.errorMessages.join('\n')); + } + return response; + } catch (error) { + let eMessage = utils.getErrorMessage(error); + return { isSuccess: false, errorMessages: [eMessage], databaseInfo: undefined }; + } + } + + public async loadDatabaseInfo(databaseName?: string): Promise { + if (!databaseName) { + databaseName = this.selectedDestDatabaseName; + } + let databaseInfo: DatabaseInfo = this._databaseInfo[databaseName]; + if (databaseInfo === undefined) { + let response = await this.getDatabaseInfo(databaseName); + if (response.isSuccess) { + databaseInfo = response.databaseInfo; + this._databaseInfo[databaseName] = databaseInfo; + } else { + this.showWizardError(response.errorMessages.join('\n')); + } + } + return databaseInfo; + } + + public async generateScript(virtualizeDataInput: VirtualizeDataInput): Promise { + try { + let response = await this._wizardService.generateScript(virtualizeDataInput); + if (!response.isSuccess) { + this.showWizardError(response.errorMessages.join('\n')); + } + return response; + } catch (error) { + let eMessage = utils.getErrorMessage(error); + return { isSuccess: false, errorMessages: [eMessage], script: undefined }; + } + } + + public async processInput(virtualizeDataInput: VirtualizeDataInput): Promise { + try { + let response = await this._wizardService.processVirtualizeDataInput(virtualizeDataInput); + if (!response.isSuccess) { + this.showWizardError(response.errorMessages.join('\n')); + } + return response; + } catch (error) { + let eMessage = utils.getErrorMessage(error); + return { isSuccess: false, errorMessages: [eMessage] }; + } + } + + public async getSourceDatabases(virtualizeDataInput: VirtualizeDataInput): Promise { + try { + let response = await this._wizardService.getSourceDatabases(virtualizeDataInput); + if (!response.isSuccess) { + this.showWizardError(response.errorMessages.join('\n')); + } + return response; + } catch (error) { + let eMessage = utils.getErrorMessage(error); + this.showWizardError(eMessage); + return { isSuccess: false, errorMessages: [eMessage], databaseNames: undefined }; + } + } + + public async getSourceTables(requestParams: GetSourceTablesRequestParams): Promise { + try { + let response = await this._wizardService.getSourceTables(requestParams); + if (!response.isSuccess) { + this.showWizardError(response.errorMessages.join('\n')); + } + return response; + } catch (error) { + let eMessage = utils.getErrorMessage(error); + this.showWizardError(eMessage); + return { isSuccess: false, errorMessages: [eMessage], schemaTablesList: undefined }; + } + } + + public async getSourceViewList(requestParams: DataSourceBrowsingParams): Promise> { + let result: ExecutionResult = undefined; + try { + result = await this._wizardService.getSourceViewList(requestParams); + if (!result.isSuccess) { + this.showWizardError(result.errorMessages.join('\n')); + } + } catch (error) { + let eMessage = utils.getErrorMessage(error); + this.showWizardError(eMessage); + result = { isSuccess: false, errorMessages: [eMessage], returnValue: undefined }; + } + return result; + } + + public async getSourceColumnDefinitions(requestParams: GetSourceColumnDefinitionsRequestParams): Promise> { + let result: ExecutionResult = undefined; + try { + let response = await this._wizardService.getSourceColumnDefinitions(requestParams); + if (response && response.isSuccess) { + result = { isSuccess: true, errorMessages: undefined, returnValue: response.columnDefinitions }; + } else { + result = { isSuccess: false, errorMessages: response.errorMessages, returnValue: undefined }; + } + } catch (error) { + let eMessage = utils.getErrorMessage(error); + result = { isSuccess: false, errorMessages: [eMessage], returnValue: undefined }; + } + return result; + } +} diff --git a/extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataTree.ts b/extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataTree.ts new file mode 100644 index 0000000000..a917f8c72e --- /dev/null +++ b/extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataTree.ts @@ -0,0 +1,364 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; +import { Uri, ThemeIcon } from 'vscode'; + +export enum TreeCheckboxState { + Intermediate = 0, + Checked = 1, + Unchecked = 2 +} + +export interface CheckboxTreeNodeArg { + treeId?: string; + nodeId?: string; + isRoot?: boolean; + label?: string; + maxLabelLength?: number; + isLeaf?: boolean; + isChecked?: boolean; + isEnabled?: boolean; +} + +export abstract class CheckboxTreeNode implements azdata.TreeComponentItem { + + protected _onNodeChange = new vscode.EventEmitter(); + protected _onTreeChange = new vscode.EventEmitter(); + public readonly onNodeChange: vscode.Event = this._onNodeChange.event; + public readonly onTreeChange: vscode.Event = this._onTreeChange.event; + + private _nodeId: string; + public label: string; + private _isRoot: boolean; + private _isLeaf: boolean; + private _isChecked: boolean; + private _isEnabled: boolean; + private _treeId: string; + private _maxLabelLength: number; + + private _rootNode: CheckboxTreeNode; + private _parent?: CheckboxTreeNode; + private _children: CheckboxTreeNode[]; + + private static _nodeRegistry: { [treeId: string]: Map } = {}; + + constructor(treeArg?: CheckboxTreeNodeArg) { + this._isRoot = false; + this._isLeaf = false; + this._isChecked = false; + this._isEnabled = true; + this.setArgs(treeArg); + } + + public setArgs(treeArg: CheckboxTreeNodeArg): void { + if (treeArg) { + this._isRoot = treeArg.isRoot !== undefined ? treeArg.isRoot : this._isRoot; + this._treeId = treeArg.treeId || this._treeId; + this._nodeId = this._isRoot ? 'root' : (treeArg.nodeId || this._nodeId); + this.label = this._isRoot ? 'root' : (treeArg.label || this.label); + this._isLeaf = treeArg.isLeaf !== undefined ? treeArg.isLeaf : this._isLeaf; + this._isChecked = treeArg.isChecked !== undefined ? treeArg.isChecked : this._isChecked; + this._isEnabled = treeArg.isEnabled !== undefined ? treeArg.isEnabled : this._isEnabled; + this._maxLabelLength = treeArg.maxLabelLength || this._maxLabelLength; + } + CheckboxTreeNode.AddToNodeRegistry(this); + } + + public static clearNodeRegistry(): void { + CheckboxTreeNode._nodeRegistry = {}; + } + + private static AddToNodeRegistry(node: CheckboxTreeNode): void { + if (node._treeId && node._nodeId) { + if (!CheckboxTreeNode._nodeRegistry[node._treeId]) { + CheckboxTreeNode._nodeRegistry[node._treeId] = new Map(); + } + let registry = CheckboxTreeNode._nodeRegistry[node._treeId]; + if (!registry.has(node._nodeId)) { + registry.set(node._nodeId, node); + } else { + throw new Error(`tree node with id: '${node._nodeId}' already exists`); + } + } + } + + public static findNode(treeId: string, nodeId: string): CheckboxTreeNode { + let wantedNode: CheckboxTreeNode = undefined; + if (treeId && nodeId && CheckboxTreeNode._nodeRegistry[treeId] && CheckboxTreeNode._nodeRegistry[treeId].has(nodeId)) { + wantedNode = CheckboxTreeNode._nodeRegistry[treeId].get(nodeId); + } + return wantedNode; + } + + public get id(): string { + return this._nodeId; + } + + public get parent(): CheckboxTreeNode { + return this._parent; + } + + public get children(): CheckboxTreeNode[] { + return this._children; + } + + public set children(children: CheckboxTreeNode[]) { + if (children) { + this._children = children; + } + } + + public get isRoot(): boolean { + return this._isRoot; + } + + public get isLeaf(): boolean { + return this._isLeaf; + } + + public set isLeaf(isLeaf: boolean) { + if (isLeaf !== undefined) { + this._isLeaf = isLeaf; + } + } + + public get treeId(): string { + return this._treeId; + } + + public set treeId(treeId: string) { + if (treeId) { + this._treeId = treeId; + } + } + + public get checked(): boolean { + return this._isChecked; + } + + public get enabled(): boolean { + return this._isEnabled; + } + + public get hasChildren(): boolean { + return this._children !== undefined && this._children.length > 0; + } + + protected get rootNode(): CheckboxTreeNode { + if (!this._rootNode && this._treeId) { + this._rootNode = CheckboxTreeNode._nodeRegistry[this._treeId].get('root'); + } + return this._rootNode; + } + + public get collapsibleState(): vscode.TreeItemCollapsibleState { + if (!this._isLeaf) { + return vscode.TreeItemCollapsibleState.Expanded; + } else { + vscode.TreeItemCollapsibleState.None; + } + } + + public abstract get iconPath(): string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon; + + public get nodePath(): string { + return `${this.parent ? this.parent.nodePath + '-' : ''}${this.id}`; + } + + public async setCheckedState(isChecked: boolean): Promise { + let nodesToCheck: CheckboxTreeNode[] = [this]; + while (nodesToCheck && nodesToCheck.length > 0) { + let node = nodesToCheck.shift(); + if (node._isEnabled) { + node._isChecked = isChecked; + node.notifyStateChanged(); + if (node.hasChildren) { + nodesToCheck = node._children.concat(nodesToCheck); + } + if (node.parent) { + await node.parent.refreshCheckedState(); + } + } + } + this.notifyStateChanged(); + } + + public async refreshCheckedState(): Promise { + let nodeToRefresh: CheckboxTreeNode = this; + while (nodeToRefresh && nodeToRefresh.hasChildren) { + if (nodeToRefresh._children.every(c => c.checked)) { + if (!nodeToRefresh._isChecked) { + nodeToRefresh._isChecked = true; + nodeToRefresh.notifyStateChanged(); + nodeToRefresh = nodeToRefresh.parent; + } else { + nodeToRefresh = undefined; + } + } else if (nodeToRefresh._children.every(c => c.checked === false)) { + if (nodeToRefresh._isChecked !== false) { + nodeToRefresh._isChecked = false; + nodeToRefresh.notifyStateChanged(); + nodeToRefresh = nodeToRefresh.parent; + } else { + nodeToRefresh = undefined; + } + } else { + if (nodeToRefresh._isChecked !== undefined) { + nodeToRefresh._isChecked = undefined; + nodeToRefresh.notifyStateChanged(); + nodeToRefresh = nodeToRefresh.parent; + } else { + nodeToRefresh = undefined; + } + } + } + this.notifyStateChanged(); + } + + public async setEnable(isEnabled: boolean): Promise { + if (isEnabled === undefined) { + isEnabled = true; + } + + let nodesToSet: CheckboxTreeNode[] = [this]; + while (nodesToSet && nodesToSet.length > 0) { + let node = nodesToSet.shift(); + node._isEnabled = isEnabled; + node.notifyStateChanged(); + if (node.hasChildren) { + nodesToSet = node._children.concat(nodesToSet); + } + if (node.parent) { + await node.parent.refreshEnableState(); + } + } + this.notifyStateChanged(); + } + + public async refreshEnableState(): Promise { + let nodeToRefresh: CheckboxTreeNode = this; + while (nodeToRefresh && nodeToRefresh.hasChildren) { + if (nodeToRefresh._children.every(c => c._isEnabled === false)) { + if (nodeToRefresh._isEnabled !== false) { + nodeToRefresh._isEnabled = false; + nodeToRefresh.notifyStateChanged(); + nodeToRefresh = nodeToRefresh.parent; + } else { + nodeToRefresh = undefined; + } + } else { + if (!nodeToRefresh._isEnabled) { + nodeToRefresh._isEnabled = true; + nodeToRefresh.notifyStateChanged(); + nodeToRefresh = nodeToRefresh.parent; + } else { + nodeToRefresh = undefined; + } + } + } + this.notifyStateChanged(); + } + + public notifyStateChanged(): void { + this._onNodeChange.fire(); + let rootNode = this.rootNode; + if (rootNode) { + rootNode._onTreeChange.fire(this); + } + } + + public get checkboxState(): TreeCheckboxState { + if (this.checked === undefined) { + return TreeCheckboxState.Intermediate; + } else { + return this.checked ? TreeCheckboxState.Checked : TreeCheckboxState.Unchecked; + } + } + + public findNode(nodeId: string): CheckboxTreeNode { + let wantedNode: CheckboxTreeNode = undefined; + if (this.id === nodeId) { + wantedNode = this; + } else { + wantedNode = CheckboxTreeNode.findNode(this._treeId, nodeId); + } + return wantedNode; + } + + public abstract getChildren(): Promise; + + public clearChildren(): void { + if (this.children) { + this.children.forEach(child => { + child.clearChildren(); + }); + this._children = undefined; + this.notifyStateChanged(); + } + } + + public addChildNode(node: CheckboxTreeNode): void { + if (node) { + if (!this._children) { + this._children = []; + } + node._parent = this; + this._children.push(node); + } + } +} + +export class CheckboxTreeDataProvider implements azdata.TreeComponentDataProvider { + private _onDidChangeTreeData = new vscode.EventEmitter(); + constructor(private _root: CheckboxTreeNode) { + if (this._root) { + this._root.onTreeChange(node => { + this._onDidChangeTreeData.fire(node); + }); + } + } + + onDidChangeTreeData?: vscode.Event = this._onDidChangeTreeData.event; + + /** + * Get [TreeItem](#TreeItem) representation of the `element` + * + * @param element The element for which [TreeItem](#TreeItem) representation is asked for. + * @return [TreeItem](#TreeItem) representation of the element + */ + getTreeItem(element: CheckboxTreeNode): azdata.TreeComponentItem | Thenable { + let item: azdata.TreeComponentItem = {}; + item.label = element.label; + item.checked = element.checked; + item.collapsibleState = element.isLeaf ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed; + item.iconPath = element.iconPath; + item.enabled = element.enabled; + return item; + } + + /** + * Get the children of `element` or root if no element is passed. + * + * @param element The element from which the provider gets children. Can be `undefined`. + * @return Children of `element` or root if no element is passed. + */ + getChildren(element?: CheckboxTreeNode): vscode.ProviderResult { + if (element) { + return element.getChildren(); + } else { + return Promise.resolve(this._root.getChildren()); + } + } + + getParent(element?: CheckboxTreeNode): vscode.ProviderResult { + if (element) { + return Promise.resolve(element.parent); + } else { + return Promise.resolve(this._root); + } + } +} diff --git a/extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataWizard.ts b/extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataWizard.ts new file mode 100644 index 0000000000..9f4ec708bb --- /dev/null +++ b/extensions/datavirtualization/src/wizards/virtualizeData/virtualizeDataWizard.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { ApiWrapper } from '../../apiWrapper'; +import { DataSourceWizardService } from '../../services/contracts'; +import { SelectDataSourcePage } from './selectDataSourcePage'; +import { ConnectionDetailsPage } from './connectionDetailsPage'; +import { SummaryPage } from './summaryPage'; +import { ObjectMappingPage } from './objectMappingPage'; +import { IWizardPageWrapper } from '../wizardPageWrapper'; +import { VirtualizeDataModel } from './virtualizeDataModel'; +import { sqlFileExtension } from '../../constants'; +import { AppContext } from '../../appContext'; +import { CreateMasterKeyPage } from './createMasterKeyPage'; +import { getErrorMessage } from '../../utils'; +import { VDIManager } from './virtualizeDataInputManager'; + +export class VirtualizeDataWizard { + private _wizard: azdata.window.Wizard; + private _wizardPageWrappers: IWizardPageWrapper[]; + private _dataModel: VirtualizeDataModel; + private _vdiManager: VDIManager; + + constructor( + private _connection: azdata.connection.ConnectionProfile, + private _wizardService: DataSourceWizardService, + private _appContext: AppContext) { + } + + public async openWizard(): Promise { + await this.initialize(); + await this._wizard.open(); + } + + private async initialize(): Promise { + this._wizard = azdata.window.createWizard(localize('getExtDataTitle', 'Virtualize Data')); + this._wizard.nextButton.enabled = false; + + // TODO: Add placeholder loading page or spinner here + this._vdiManager = new VDIManager(); + this._dataModel = new VirtualizeDataModel(this._connection, this._wizardService, this._wizard, this._vdiManager); + await this._dataModel.createSession(); + + this._wizardPageWrappers = [ + new SelectDataSourcePage(this), + new CreateMasterKeyPage(this._dataModel, this._vdiManager, this.appContext), + new ConnectionDetailsPage(this._dataModel, this._vdiManager, this._appContext), + new ObjectMappingPage(this._dataModel, this._vdiManager, this._appContext), + new SummaryPage(this._dataModel, this._vdiManager, this._appContext) + ]; + + this._wizardPageWrappers.forEach(w => { + let page = w.getPage(); + if (page) { page['owner'] = w; } + }); + + this._vdiManager.setInputPages(this._wizardPageWrappers); + this._vdiManager.setVirtualizeDataModel(this._dataModel); + + this._wizard.pages = this._wizardPageWrappers.map(wrapper => wrapper.getPage()); + this._wizard.displayPageTitles = true; + + this._wizard.cancelButton.onClick(() => this.actionClose()); + + this._wizard.doneButton.label = localize('doneButtonLabel', 'Create'); + this._wizard.doneButton.hidden = true; + + this._wizard.generateScriptButton.onClick(async () => await this.actionGenerateScript()); + this._wizard.generateScriptButton.hidden = true; + this._wizard.generateScriptButton.enabled = false; + + this._wizard.registerNavigationValidator(async (info) => await this.actionValidateInputAndUpdateNextPage(info)); + + this._wizard.onPageChanged(info => this.actionChangePage(info)); + } + + private async actionClose(): Promise { + try { + let sessionId = this._dataModel.sessionId; + if (sessionId) { + await this._wizardService.disposeWizardSession(sessionId); + } + } catch (error) { + this.apiWrapper.showErrorMessage(error.toString()); + } + } + + private async actionGenerateScript(): Promise { + try { + // Disable the button while generating the script to prevent an issue where multiple quick + // button presses would duplicate the script. (There's no good reason to allow multiple + // scripts to be generated anyways) + this._wizard.generateScriptButton.enabled = false; + let virtualizeDataInput = this._vdiManager.virtualizeDataInput; + let response = await this._dataModel.generateScript(virtualizeDataInput); + if (response.isSuccess) { + let sqlScript: string = response.script; + let doc = await this.apiWrapper.openTextDocument({ language: sqlFileExtension, content: sqlScript }); + await this.apiWrapper.showDocument(doc); + await azdata.queryeditor.connect(doc.uri.toString(), this._dataModel.connection.connectionId); + + this._dataModel.showWizardInfo( + localize('openScriptMsg', + 'The script has opened in a document window. You can view it once the wizard is closed.')); + } else { + let eMessage = response.errorMessages.join('\n'); + this._dataModel.showWizardError(eMessage); + } + } catch (error) { + this._dataModel.showWizardError(error.toString()); + // re-enable button if an error occurred since we didn't actually generate a script + this._wizard.generateScriptButton.enabled = true; + } + } + + private actionChangePage(info: azdata.window.WizardPageChangeInfo): void { + this.toggleLastPageButtons(info.newPage === (this._wizard.pages.length - 1)); + } + + private toggleLastPageButtons(isLastPage: boolean): void { + this._wizard.doneButton.hidden = !isLastPage; + this._wizard.generateScriptButton.hidden = !isLastPage; + this._wizard.generateScriptButton.enabled = isLastPage; + } + + private async actionValidateInputAndUpdateNextPage(info: azdata.window.WizardPageChangeInfo): Promise { + this._wizard.message = undefined; + + // Skip validation for moving to a previous page + if (info.newPage < info.lastPage) { + return true; + } + + try { + let currentPageWrapper: IWizardPageWrapper = this.GetWizardPageWrapper(info.lastPage); + if (!currentPageWrapper || !(await currentPageWrapper.validate())) { return false; } + + if (!info.newPage) { return true; } + let newPageWrapper: IWizardPageWrapper = this.GetWizardPageWrapper(info.newPage); + if (!newPageWrapper) { return false; } + + await newPageWrapper.updatePage(); + return true; + } catch (error) { + this._dataModel.showWizardError(getErrorMessage(error)); + } + + return false; + } + + private GetWizardPageWrapper(pageIndex: number): IWizardPageWrapper { + if (!this._wizard || !this._wizard.pages || this._wizard.pages.length === 0 + || pageIndex < 0 || pageIndex >= this._wizard.pages.length) { return undefined; } + let wizardPage = this._wizard.pages[pageIndex]; + return wizardPage && wizardPage['owner']; + } + + private get apiWrapper(): ApiWrapper { + return this._appContext.apiWrapper; + } + + public get appContext(): AppContext { + return this._appContext; + } + + public get dataModel(): VirtualizeDataModel { + return this._dataModel; + } + + public get vdiManager(): VDIManager { + return this._vdiManager; + } + + public get wizard(): azdata.window.Wizard { + return this._wizard; + } + + public get wizardPageWrappers(): IWizardPageWrapper[] { + return this._wizardPageWrappers; + } +} diff --git a/extensions/datavirtualization/src/wizards/wizardCommands.ts b/extensions/datavirtualization/src/wizards/wizardCommands.ts new file mode 100644 index 0000000000..b5b5bb998b --- /dev/null +++ b/extensions/datavirtualization/src/wizards/wizardCommands.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { ICommandViewContext, Command, ICommandObjectExplorerContext, ICommandUnknownContext } from '../command'; +import { VirtualizeDataWizard } from './virtualizeData/virtualizeDataWizard'; +import { DataSourceWizardService } from '../services/contracts'; +import { AppContext } from '../appContext'; +import { getErrorMessage } from '../utils'; +import * as constants from '../constants'; +import { TableFromFileWizard } from './tableFromFile/tableFromFileWizard'; +import { getNodeFromMssqlProvider } from '../hdfsCommands'; +import { HdfsFileSourceNode } from '../hdfsProvider'; + +export class OpenVirtualizeDataWizardCommand extends Command { + private readonly dataWizardTask: OpenVirtualizeDataWizardTask; + + constructor(appContext: AppContext, wizardService: DataSourceWizardService) { + super(constants.virtualizeDataCommand, appContext); + this.dataWizardTask = new OpenVirtualizeDataWizardTask(appContext, wizardService); + } + + protected async preExecute(context: ICommandUnknownContext | ICommandObjectExplorerContext, args: object = {}): Promise { + return this.execute(context, args); + } + + async execute(context: ICommandUnknownContext | ICommandObjectExplorerContext, ...args: any[]): Promise { + let profile: azdata.IConnectionProfile = undefined; + if (context && context.type === constants.ObjectExplorerService && context.explorerContext) { + profile = context.explorerContext.connectionProfile; + } + this.dataWizardTask.execute(profile, args); + } +} + +export class OpenVirtualizeDataWizardTask { + constructor(private appContext: AppContext, private wizardService: DataSourceWizardService) { + } + + async execute(profile: azdata.IConnectionProfile, ...args: any[]): Promise { + try { + let connection: azdata.connection.ConnectionProfile; + if (profile) { + connection = convertIConnectionProfile(profile); + } else { + connection = await azdata.connection.getCurrentConnection(); + if (!connection) { + this.appContext.apiWrapper.showErrorMessage(localize('noConnection', 'Data Virtualization requires a connection to be selected.')); + return; + } + } + let wizard = new VirtualizeDataWizard(connection, this.wizardService, this.appContext); + await wizard.openWizard(); + } catch (error) { + this.appContext.apiWrapper.showErrorMessage(getErrorMessage(error)); + } + } +} + +export class OpenMssqlHdfsTableFromFileWizardCommand extends Command { + constructor(appContext: AppContext, private wizardService: DataSourceWizardService) { + super(constants.mssqlHdfsTableFromFileCommand, appContext); + } + + protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise { + return this.execute(context, args); + } + + async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise { + try { + let connection: azdata.connection.ConnectionProfile; + if (context && context.type === constants.ObjectExplorerService && context.explorerContext) { + connection = convertIConnectionProfile(context.explorerContext.connectionProfile); + } + + if (!connection) { + connection = await azdata.connection.getCurrentConnection(); + if (!connection) { + this.appContext.apiWrapper.showErrorMessage(localize('noConnection', 'Data Virtualization requires a connection to be selected.')); + return; + } + } + + let fileNode = await getNodeFromMssqlProvider(context, this.appContext); + let wizard = new TableFromFileWizard(connection, this.appContext, this.wizardService); + await wizard.start(fileNode); + } catch (error) { + this.appContext.apiWrapper.showErrorMessage(getErrorMessage(error)); + } + } +} + +function convertIConnectionProfile(profile: azdata.IConnectionProfile): azdata.connection.ConnectionProfile { + let connection: azdata.connection.ConnectionProfile; + if (profile) { + connection = { + providerId: profile.providerName, + connectionId: profile.id, + connectionName: profile.connectionName, + serverName: profile.serverName, + databaseName: profile.databaseName, + userName: profile.userName, + password: profile.password, + authenticationType: profile.authenticationType, + savePassword: profile.savePassword, + groupFullName: profile.groupFullName, + groupId: profile.groupId, + saveProfile: profile.saveProfile, + azureTenantId: profile.azureTenantId, + options: {} + }; + } + return connection; +} diff --git a/extensions/datavirtualization/src/wizards/wizardPageWrapper.ts b/extensions/datavirtualization/src/wizards/wizardPageWrapper.ts new file mode 100644 index 0000000000..d7fe335e5b --- /dev/null +++ b/extensions/datavirtualization/src/wizards/wizardPageWrapper.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import { VirtualizeDataInput } from '../services/contracts'; + +export interface IWizardPageWrapper { + // Returns underlying wizard page object. + getPage(): azdata.window.WizardPage; + + // Called for the current page after clicking the Wizard's Next button. + // Returns boolean indicating whether validation was successful and thus + // if page can be changed. + validate(): Promise; + + // Updates the wizard page by retrieving current info from the backing data model. + updatePage(): Promise; + + // Adds this page's input contributions to the provided data input object + getInputValues(existingInput: VirtualizeDataInput): void; +} diff --git a/extensions/datavirtualization/tsconfig.json b/extensions/datavirtualization/tsconfig.json new file mode 100644 index 0000000000..aa253b9792 --- /dev/null +++ b/extensions/datavirtualization/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../tsconfig.base.json", + "compileOnSave": true, + "compilerOptions": { + "outDir": "./out", + "lib": [ + "es6", + "dom" + ], + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "strict": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitAny": false, + "noImplicitReturns": false, + "noImplicitOverride": false + }, + "exclude": [ + "node_modules" + ] +} diff --git a/extensions/datavirtualization/tslint.json b/extensions/datavirtualization/tslint.json new file mode 100644 index 0000000000..532dbe9fa6 --- /dev/null +++ b/extensions/datavirtualization/tslint.json @@ -0,0 +1,14 @@ +{ + "linterOptions": { + "exclude": [ + "typings/*.d.ts" + ] + }, + "rules": { + "no-unused-expression": false, + "curly": true, + "class-name": true, + "semicolon": true, + "triple-equals": true + } +} diff --git a/extensions/datavirtualization/yarn.lock b/extensions/datavirtualization/yarn.lock new file mode 100644 index 0000000000..139e532cf4 --- /dev/null +++ b/extensions/datavirtualization/yarn.lock @@ -0,0 +1,1796 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" + integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== + dependencies: + "@babel/highlight" "^7.14.5" + +"@babel/compat-data@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.15.0.tgz#2dbaf8b85334796cafbb0f5793a90a2fc010b176" + integrity sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA== + +"@babel/core@^7.7.5": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.0.tgz#749e57c68778b73ad8082775561f67f5196aafa8" + integrity sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/generator" "^7.15.0" + "@babel/helper-compilation-targets" "^7.15.0" + "@babel/helper-module-transforms" "^7.15.0" + "@babel/helpers" "^7.14.8" + "@babel/parser" "^7.15.0" + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.15.0" + "@babel/types" "^7.15.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + source-map "^0.5.0" + +"@babel/generator@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.15.0.tgz#a7d0c172e0d814974bad5aa77ace543b97917f15" + integrity sha512-eKl4XdMrbpYvuB505KTta4AV9g+wWzmVBW69tX0H2NwKVKd2YJbKgyK6M8j/rgLbmHOYJn6rUklV677nOyJrEQ== + dependencies: + "@babel/types" "^7.15.0" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-compilation-targets@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.0.tgz#973df8cbd025515f3ff25db0c05efc704fa79818" + integrity sha512-h+/9t0ncd4jfZ8wsdAsoIxSa61qhBYlycXiHWqJaQBCXAhDCMbPRSMTGnZIkkmt1u4ag+UQmuqcILwqKzZ4N2A== + dependencies: + "@babel/compat-data" "^7.15.0" + "@babel/helper-validator-option" "^7.14.5" + browserslist "^4.16.6" + semver "^6.3.0" + +"@babel/helper-function-name@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz#89e2c474972f15d8e233b52ee8c480e2cfcd50c4" + integrity sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ== + dependencies: + "@babel/helper-get-function-arity" "^7.14.5" + "@babel/template" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/helper-get-function-arity@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz#25fbfa579b0937eee1f3b805ece4ce398c431815" + integrity sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-hoist-variables@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz#e0dd27c33a78e577d7c8884916a3e7ef1f7c7f8d" + integrity sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-member-expression-to-functions@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.0.tgz#0ddaf5299c8179f27f37327936553e9bba60990b" + integrity sha512-Jq8H8U2kYiafuj2xMTPQwkTBnEEdGKpT35lJEQsRRjnG0LW3neucsaMWLgKcwu3OHKNeYugfw+Z20BXBSEs2Lg== + dependencies: + "@babel/types" "^7.15.0" + +"@babel/helper-module-imports@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz#6d1a44df6a38c957aa7c312da076429f11b422f3" + integrity sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-module-transforms@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.15.0.tgz#679275581ea056373eddbe360e1419ef23783b08" + integrity sha512-RkGiW5Rer7fpXv9m1B3iHIFDZdItnO2/BLfWVW/9q7+KqQSDY5kUfQEbzdXM1MVhJGcugKV7kRrNVzNxmk7NBg== + dependencies: + "@babel/helper-module-imports" "^7.14.5" + "@babel/helper-replace-supers" "^7.15.0" + "@babel/helper-simple-access" "^7.14.8" + "@babel/helper-split-export-declaration" "^7.14.5" + "@babel/helper-validator-identifier" "^7.14.9" + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.15.0" + "@babel/types" "^7.15.0" + +"@babel/helper-optimise-call-expression@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz#f27395a8619e0665b3f0364cddb41c25d71b499c" + integrity sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-replace-supers@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.15.0.tgz#ace07708f5bf746bf2e6ba99572cce79b5d4e7f4" + integrity sha512-6O+eWrhx+HEra/uJnifCwhwMd6Bp5+ZfZeJwbqUTuqkhIT6YcRhiZCOOFChRypOIe0cV46kFrRBlm+t5vHCEaA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.15.0" + "@babel/helper-optimise-call-expression" "^7.14.5" + "@babel/traverse" "^7.15.0" + "@babel/types" "^7.15.0" + +"@babel/helper-simple-access@^7.14.8": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz#82e1fec0644a7e775c74d305f212c39f8fe73924" + integrity sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg== + dependencies: + "@babel/types" "^7.14.8" + +"@babel/helper-split-export-declaration@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz#22b23a54ef51c2b7605d851930c1976dd0bc693a" + integrity sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA== + dependencies: + "@babel/types" "^7.14.5" + +"@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.9": + version "7.14.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48" + integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g== + +"@babel/helper-validator-option@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" + integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== + +"@babel/helpers@^7.14.8": + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.15.3.tgz#c96838b752b95dcd525b4e741ed40bb1dc2a1357" + integrity sha512-HwJiz52XaS96lX+28Tnbu31VeFSQJGOeKHJeaEPQlTl7PnlhFElWPj8tUXtqFIzeN86XxXoBr+WFAyK2PPVz6g== + dependencies: + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.15.0" + "@babel/types" "^7.15.0" + +"@babel/highlight@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" + integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== + dependencies: + "@babel/helper-validator-identifier" "^7.14.5" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.14.5", "@babel/parser@^7.15.0": + version "7.15.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.3.tgz#3416d9bea748052cfcb63dbcc27368105b1ed862" + integrity sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA== + +"@babel/template@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" + integrity sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/parser" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/traverse@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.0.tgz#4cca838fd1b2a03283c1f38e141f639d60b3fc98" + integrity sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/generator" "^7.15.0" + "@babel/helper-function-name" "^7.14.5" + "@babel/helper-hoist-variables" "^7.14.5" + "@babel/helper-split-export-declaration" "^7.14.5" + "@babel/parser" "^7.15.0" + "@babel/types" "^7.15.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.15.0": + version "7.15.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.0.tgz#61af11f2286c4e9c69ca8deb5f4375a73c72dcbd" + integrity sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ== + dependencies: + "@babel/helper-validator-identifier" "^7.14.9" + to-fast-properties "^2.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@microsoft/1ds-core-js@3.2.8", "@microsoft/1ds-core-js@^3.2.3": + version "3.2.8" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-core-js/-/1ds-core-js-3.2.8.tgz#1b6b7d9bb858238c818ccf4e4b58ece7aeae5760" + integrity sha512-9o9SUAamJiTXIYwpkQDuueYt83uZfXp8zp8YFix1IwVPwC9RmE36T2CX9gXOeq1nDckOuOduYpA8qHvdh5BGfQ== + dependencies: + "@microsoft/applicationinsights-core-js" "2.8.9" + "@microsoft/applicationinsights-shims" "^2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" + +"@microsoft/1ds-post-js@^3.2.3": + version "3.2.8" + resolved "https://registry.yarnpkg.com/@microsoft/1ds-post-js/-/1ds-post-js-3.2.8.tgz#46793842cca161bf7a2a5b6053c349f429e55110" + integrity sha512-SjlRoNcXcXBH6WQD/5SkkaCHIVqldH3gDu+bI7YagrOVJ5APxwT1Duw9gm3L1FjFa9S2i81fvJ3EVSKpp9wULA== + dependencies: + "@microsoft/1ds-core-js" "3.2.8" + "@microsoft/applicationinsights-shims" "^2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" + +"@microsoft/ads-extension-telemetry@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@microsoft/ads-extension-telemetry/-/ads-extension-telemetry-1.3.2.tgz#d9cfb4bc7099df73e000b7bafa48bb748db924fe" + integrity sha512-TG1TE7FPp5rBA9zYPVjralZut8Bq/b5XCgm0kmkLyoQyn3c9ntmWXFuNQPOXmgbIemg5YY1/7DHKrfNcO/igkQ== + dependencies: + "@vscode/extension-telemetry" "^0.6.2" + +"@microsoft/ads-service-downloader@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@microsoft/ads-service-downloader/-/ads-service-downloader-1.0.4.tgz#94e13461d655d0864cbf93978247cbd1097e7863" + integrity sha512-XVJ3RW4X5mzlPYeJnwTii5/6ywVib4UqCtrvxwRWSFe214Hi8jO2zNxzcpamiTCnHm2b8wZAuWGfsvIShbf/yg== + dependencies: + async-retry "^1.2.3" + eventemitter2 "^5.0.1" + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.3" + mkdirp "1.0.4" + tar "^6.1.11" + tmp "^0.0.33" + yauzl "^2.10.0" + +"@microsoft/applicationinsights-core-js@2.8.9": + version "2.8.9" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.9.tgz#0e5d207acfae6986a6fc97249eeb6117e523bf1b" + integrity sha512-HRuIuZ6aOWezcg/G5VyFDDWGL8hDNe/ljPP01J7ImH2kRPEgbtcfPSUMjkamGMefgdq81GZsSoC/NNGTP4pp2w== + dependencies: + "@microsoft/applicationinsights-shims" "2.0.2" + "@microsoft/dynamicproto-js" "^1.1.7" + +"@microsoft/applicationinsights-shims@2.0.2", "@microsoft/applicationinsights-shims@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.2.tgz#92b36a09375e2d9cb2b4203383b05772be837085" + integrity sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg== + +"@microsoft/dynamicproto-js@^1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.7.tgz#ede48dd3f85af14ee369c805e5ed5b84222b9fe2" + integrity sha512-SK3D3aVt+5vOOccKPnGaJWB5gQ8FuKfjboUJHedMP7gu54HqSCXX5iFXhktGD8nfJb0Go30eDvs/UDoTnR2kOA== + +"@microsoft/vscodetestcover@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@microsoft/vscodetestcover/-/vscodetestcover-1.2.1.tgz#65f25132075a465a7a99688204486ee2b65ac07b" + integrity sha512-ODHGLbx+mqHCrs44Yy3OTxr6gZowqsf45s5FQYWukI6OCgJlWrhIPNgW2YyDVH8OE5ZDI7dt9pXXdhEbfGV7gA== + dependencies: + decache "^4.4.0" + glob "^7.1.2" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-hook "^3.0.0" + istanbul-lib-instrument "^4.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^3.0.6" + istanbul-reports "^3.0.0" + mocha "^7.1.1" + +"@vscode/extension-telemetry@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.6.2.tgz#b86814ee680615730da94220c2b03ea9c3c14a8e" + integrity sha512-yb/wxLuaaCRcBAZtDCjNYSisAXz3FWsSqAha5nhHcYxx2ZPdQdWuZqVXGKq0ZpHVndBWWtK6XqtpCN2/HB4S1w== + dependencies: + "@microsoft/1ds-core-js" "^3.2.3" + "@microsoft/1ds-post-js" "^3.2.3" + +agent-base@4: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== + dependencies: + es6-promisify "^5.0.0" + +agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + +ansi-colors@3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" + integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== + +ansi-regex@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1" + integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== + +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +anymatch@~3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-transform@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-2.0.0.tgz#99d9d29c7b38391e6f428d28ce136551f0b77e12" + integrity sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg== + dependencies: + default-require-extensions "^3.0.0" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array.prototype.reduce@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz#6b20b0daa9d9734dd6bc7ea66b5bbce395471eac" + integrity sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-array-method-boxes-properly "^1.0.0" + is-string "^1.0.7" + +async-retry@^1.2.3: + version "1.3.1" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.1.tgz#139f31f8ddce50c0870b0ba558a6079684aaed55" + integrity sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA== + dependencies: + retry "0.12.0" + +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserslist@^4.16.6: + version "4.16.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.7.tgz#108b0d1ef33c4af1b587c54f390e7041178e4335" + integrity sha512-7I4qVwqZltJ7j37wObBe3SoTz+nS8APaNcrBOlgoirb6/HbEU2XxW/LpUDTCngM6iauwFqmRTuOMfyKnFGY5JA== + dependencies: + caniuse-lite "^1.0.30001248" + colorette "^1.2.2" + electron-to-chromium "^1.3.793" + escalade "^3.1.1" + node-releases "^1.1.73" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsite@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +caniuse-lite@^1.0.30001248: + version "1.0.30001251" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz#6853a606ec50893115db660f82c094d18f096d85" + integrity sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A== + +chalk@^2.0.0, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chokidar@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.0.tgz#12c0714668c55800f659e262d4962a97faf554a6" + integrity sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.2.0" + optionalDependencies: + fsevents "~2.1.1" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +colorette@^1.2.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af" + integrity sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + +"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.3.2": + version "1.3.2" + resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/e3be16cffbac882ef545e4da9654a82dc010d1b7" + dependencies: + vscode-languageclient "5.2.1" + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^3.1.0: + version "3.2.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.5.tgz#c2418fbfd7a29f4d4f70ff4cea604d4b64c46407" + integrity sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg== + dependencies: + ms "^2.1.1" + +debug@^4.1.0, debug@^4.1.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + dependencies: + ms "2.1.2" + +decache@^4.4.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/decache/-/decache-4.6.0.tgz#87026bc6e696759e82d57a3841c4e251a30356e8" + integrity sha512-PppOuLiz+DFeaUvFXEYZjLxAkKiMYH/do/b/MxpDe/8AgKBi5GhZxridoVIbBq72GDbL36e4p0Ce2jTGUwwU+w== + dependencies: + callsite "^1.0.0" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +default-require-extensions@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.0.tgz#e03f93aac9b2b6443fc52e5e4a37b3ad9ad8df96" + integrity sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg== + dependencies: + strip-bom "^4.0.0" + +define-properties@^1.1.2, define-properties@^1.1.3, define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +diff@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +electron-to-chromium@^1.3.793: + version "1.3.806" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.806.tgz#21502100f11aead6c501d1cd7f2504f16c936642" + integrity sha512-AH/otJLAAecgyrYp0XK1DPiGVWcOgwPeJBOLeuFQ5l//vhQhwC9u6d+GijClqJAmsHG4XDue81ndSQPohUu0xA== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +es-abstract@^1.19.0, es-abstract@^1.20.4: + version "1.21.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.1.tgz#e6105a099967c08377830a0c9cb589d570dd86c6" + integrity sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.3" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.4" + is-array-buffer "^3.0.1" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.10" + is-weakref "^1.0.2" + object-inspect "^1.12.2" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.9" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es6-promise@^4.0.3: + version "4.2.5" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054" + integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +eventemitter2@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-5.0.1.tgz#6197a095d5fb6b57e8942f6fd7eaad63a09c9452" + integrity sha1-YZegldX7a1folC9v1+qtY6CclFI= + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@3.0.0, find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +flat@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.1.tgz#a392059cc382881ff98642f5da4dde0a959f309b" + integrity sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA== + dependencies: + is-buffer "~2.0.3" + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@~2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +glob-parent@~5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@7.1.3, glob@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + dependencies: + 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" + +glob@^7.1.3: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + 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" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbols@^1.0.0, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-proxy-agent@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" + integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== + dependencies: + agent-base "4" + debug "3.1.0" + +https-proxy-agent@^2.2.3: + version "2.2.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" + integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +internal-slot@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" + integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + side-channel "^1.0.4" + +is-array-buffer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a" + integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-typed-array "^1.1.10" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@~2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.10, is-typed-array@^1.1.9: + version "1.1.10" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" + integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +istanbul-lib-coverage@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== + +istanbul-lib-coverage@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" + integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== + +istanbul-lib-hook@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz#8f84c9434888cc6b1d0a9d7092a76d239ebf0cc6" + integrity sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ== + dependencies: + append-transform "^2.0.0" + +istanbul-lib-instrument@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" + integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== + dependencies: + "@babel/core" "^7.7.5" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.0.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + rimraf "^2.6.3" + source-map "^0.6.1" + +istanbul-reports@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" + integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json5@^2.1.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" + integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== + dependencies: + minimist "^1.2.5" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +lodash@^4.17.15: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lodash@^4.17.4: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +log-symbols@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" + integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== + dependencies: + chalk "^2.4.2" + +make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +minimatch@3.0.4, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minipass@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +mkdirp@1.0.4, mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mocha@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.2.0.tgz#01cc227b00d875ab1eed03a75106689cfed5a604" + integrity sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ== + dependencies: + ansi-colors "3.2.3" + browser-stdout "1.3.1" + chokidar "3.3.0" + debug "3.2.6" + diff "3.5.0" + escape-string-regexp "1.0.5" + find-up "3.0.0" + glob "7.1.3" + growl "1.10.5" + he "1.2.0" + js-yaml "3.13.1" + log-symbols "3.0.0" + minimatch "3.0.4" + mkdirp "0.5.5" + ms "2.1.1" + node-environment-flags "1.0.6" + object.assign "4.1.0" + strip-json-comments "2.0.1" + supports-color "6.0.0" + which "1.3.1" + wide-align "1.1.3" + yargs "13.3.2" + yargs-parser "13.1.2" + yargs-unparser "1.6.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1, ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +node-environment-flags@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088" + integrity sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw== + dependencies: + object.getownpropertydescriptors "^2.0.3" + semver "^5.7.0" + +node-releases@^1.1.73: + version "1.1.74" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.74.tgz#e5866488080ebaa70a93b91144ccde06f3c3463e" + integrity sha512-caJBVempXZPepZoZAPCWRTNxYQ+xtG/KAi4ozTA5A+nJ7IU+kLQCbqaUjb5Rwy14M9upBWiQ4NutcmW04LJSRw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +object-inspect@^1.12.2, object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + +object-keys@^1.0.11, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.getownpropertydescriptors@^2.0.3: + version "2.1.5" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz#db5a9002489b64eef903df81d6623c07e5b4b4d3" + integrity sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw== + dependencies: + array.prototype.reduce "^1.0.5" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +p-limit@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + +picomatch@^2.0.4: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +postinstall-build@^5.0.1: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7" + integrity sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg== + +readdirp@~3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839" + integrity sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ== + dependencies: + picomatch "^2.0.4" + +regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +retry@0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + +semver@^5.5.0, semver@^5.6.0, semver@^5.7.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.0.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +should-equal@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" + integrity sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA== + dependencies: + should-type "^1.4.0" + +should-format@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1" + integrity sha1-m/yPdPo5IFxT04w01xcwPidxJPE= + dependencies: + should-type "^1.3.0" + should-type-adaptors "^1.0.1" + +should-type-adaptors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a" + integrity sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA== + dependencies: + should-type "^1.3.0" + should-util "^1.0.0" + +should-type@^1.3.0, should-type@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3" + integrity sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM= + +should-util@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/should-util/-/should-util-1.0.0.tgz#c98cda374aa6b190df8ba87c9889c2b4db620063" + integrity sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM= + +should@^13.2.1: + version "13.2.3" + resolved "https://registry.yarnpkg.com/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10" + integrity sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ== + dependencies: + should-equal "^2.0.0" + should-format "^3.0.3" + should-type "^1.4.0" + should-type-adaptors "^1.0.1" + should-util "^1.0.0" + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +source-map@^0.5.0: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string.prototype.trimend@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string.prototype.trimstart@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow== + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-json-comments@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +supports-color@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a" + integrity sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg== + dependencies: + has-flag "^3.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +tar@^6.1.11: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + +typemoq@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8" + integrity sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw== + dependencies: + circular-json "^0.3.1" + lodash "^4.17.4" + postinstall-build "^5.0.1" + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +vscode-jsonrpc@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-4.0.0.tgz#a7bf74ef3254d0a0c272fab15c82128e378b3be9" + integrity sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg== + +vscode-languageclient@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-5.2.1.tgz#7cfc83a294c409f58cfa2b910a8cfeaad0397193" + integrity sha512-7jrS/9WnV0ruqPamN1nE7qCxn0phkH5LjSgSp9h6qoJGoeAKzwKz/PF6M+iGA/aklx4GLZg1prddhEPQtuXI1Q== + dependencies: + semver "^5.5.0" + vscode-languageserver-protocol "3.14.1" + +vscode-languageserver-protocol@3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.14.1.tgz#b8aab6afae2849c84a8983d39a1cf742417afe2f" + integrity sha512-IL66BLb2g20uIKog5Y2dQ0IiigW0XKrvmWiOvc0yXw80z3tMEzEnHjaGAb3ENuU7MnQqgnYJ1Cl2l9RvNgDi4g== + dependencies: + vscode-jsonrpc "^4.0.0" + vscode-languageserver-types "3.14.0" + +vscode-languageserver-types@3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz#d3b5952246d30e5241592b6dde8280e03942e743" + integrity sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A== + +vscode-nls@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.2.0.tgz#3cb6893dd9bd695244d8a024bdf746eea665cc3f" + integrity sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng== + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== + +which-typed-array@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" + integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.10" + +which@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wide-align@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@13.1.2, yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-unparser@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" + integrity sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw== + dependencies: + flat "^4.1.0" + lodash "^4.17.15" + yargs "^13.3.0" + +yargs@13.3.2, yargs@^13.3.0: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0"