mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 18:46:43 -05:00
Compare commits
219 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
995095f696 | ||
|
|
37727b7911 | ||
|
|
20b24a89e7 | ||
|
|
1bc2057a83 | ||
|
|
0423480148 | ||
|
|
3419dd5498 | ||
|
|
f212ddc253 | ||
|
|
83ef008306 | ||
|
|
166045b3c5 | ||
|
|
abea315abb | ||
|
|
4b87a0869f | ||
|
|
b17ae9a24e | ||
|
|
75772d6f74 | ||
|
|
22c13aaf7e | ||
|
|
614a130811 | ||
|
|
de83a33075 | ||
|
|
676f3d3acd | ||
|
|
06bace1fcf | ||
|
|
86aa24e198 | ||
|
|
63f9be6b5f | ||
|
|
467a34e878 | ||
|
|
67829af0c5 | ||
|
|
ac5ff2ec7f | ||
|
|
ddd80b982c | ||
|
|
3c4ffd2a9c | ||
|
|
49fa56369c | ||
|
|
1944813c4a | ||
|
|
71d9c91551 | ||
|
|
350034cbb1 | ||
|
|
c027ce4f00 | ||
|
|
e99b4a7bed | ||
|
|
814dfba338 | ||
|
|
6907c8edab | ||
|
|
cce8a961ac | ||
|
|
3a89731036 | ||
|
|
bcc73dfbcf | ||
|
|
395dfd6c52 | ||
|
|
dcf17cc08b | ||
|
|
52a642f351 | ||
|
|
b390052c86 | ||
|
|
9ac180d772 | ||
|
|
41915bda8d | ||
|
|
86a5cb27b7 | ||
|
|
661e7c361d | ||
|
|
a0656d6f8d | ||
|
|
9b43ee6287 | ||
|
|
0e84dd240b | ||
|
|
2f49513bae | ||
|
|
8f5dc1526a | ||
|
|
1c0259f4c5 | ||
|
|
15fa03876d | ||
|
|
f6e39a7211 | ||
|
|
667c12abdc | ||
|
|
72b8d96dbc | ||
|
|
d98ac86bd9 | ||
|
|
927e435aea | ||
|
|
90b33b256d | ||
|
|
dd3fe7b613 | ||
|
|
09d5a8b88b | ||
|
|
db845754c6 | ||
|
|
5bf1f640e8 | ||
|
|
23999951d0 | ||
|
|
ac8f950147 | ||
|
|
feb92b96da | ||
|
|
c3a00c2cc6 | ||
|
|
c16aee760f | ||
|
|
f9ea6430ee | ||
|
|
ff6f30505c | ||
|
|
8677ffc68c | ||
|
|
14cf6add73 | ||
|
|
8651db1e7e | ||
|
|
fa150fbe67 | ||
|
|
db8d06d628 | ||
|
|
79cb27898d | ||
|
|
45bdf2ed95 | ||
|
|
26bb3ca033 | ||
|
|
b3891ff68b | ||
|
|
028568971f | ||
|
|
a04c2f3ba6 | ||
|
|
e79a80590a | ||
|
|
d828a1f042 | ||
|
|
1a84db089a | ||
|
|
300bce4070 | ||
|
|
db0464d07c | ||
|
|
52fbdca2ed | ||
|
|
728aeded01 | ||
|
|
42a8680738 | ||
|
|
ed26938dc8 | ||
|
|
5be2e01632 | ||
|
|
d059032dee | ||
|
|
dd0fa50d32 | ||
|
|
d4f2214055 | ||
|
|
82b363fe90 | ||
|
|
c5c1f183c3 | ||
|
|
c903cd87bf | ||
|
|
e280205340 | ||
|
|
0b5b8c9261 | ||
|
|
0316d9ac57 | ||
|
|
07d798c949 | ||
|
|
a96caf82c3 | ||
|
|
f986b8cf78 | ||
|
|
5f61becd36 | ||
|
|
068649cba4 | ||
|
|
58d4dda1e8 | ||
|
|
3cff682ae4 | ||
|
|
d27b376964 | ||
|
|
1108560780 | ||
|
|
04dff9cdf2 | ||
|
|
9e9fac2991 | ||
|
|
7d4fa0aa9b | ||
|
|
a3eb9d29a9 | ||
|
|
b089d880fc | ||
|
|
bd8346a1b2 | ||
|
|
94b697340d | ||
|
|
462d4137d5 | ||
|
|
a394a2146a | ||
|
|
2161f323c3 | ||
|
|
5810623e55 | ||
|
|
7a0a56df2f | ||
|
|
e6bfbe7fa7 | ||
|
|
8671b6aa5d | ||
|
|
09cce42233 | ||
|
|
9e3bfea922 | ||
|
|
c9fcf4434a | ||
|
|
591a0ca9ee | ||
|
|
a44b196a6f | ||
|
|
431da155a7 | ||
|
|
b6c80557ba | ||
|
|
c39c20cd4b | ||
|
|
dd9d247950 | ||
|
|
452b7d744e | ||
|
|
8ef9b1cc1f | ||
|
|
1d42431914 | ||
|
|
1ec42a082e | ||
|
|
bcae0d2b02 | ||
|
|
140a1fbc3e | ||
|
|
b69572a7c9 | ||
|
|
f37fb0b1d5 | ||
|
|
8a5b737fda | ||
|
|
569be8eb24 | ||
|
|
df51f35be0 | ||
|
|
5d1b328866 | ||
|
|
e7fb44b3a2 | ||
|
|
6c2e713a92 | ||
|
|
81a1b1a55a | ||
|
|
72be8045ec | ||
|
|
f75665d488 | ||
|
|
4af333d362 | ||
|
|
c8d17f1ee3 | ||
|
|
fae0600a46 | ||
|
|
16943c68c6 | ||
|
|
ab6d11596e | ||
|
|
bc9719d5a8 | ||
|
|
67eebf7d14 | ||
|
|
558d70b427 | ||
|
|
c329a2524b | ||
|
|
30054e632c | ||
|
|
1d09dbf591 | ||
|
|
11f236ade1 | ||
|
|
f314a9e1e6 | ||
|
|
cf7c506d75 | ||
|
|
c90933eb1b | ||
|
|
bc1fc8527c | ||
|
|
57446faa1e | ||
|
|
786d526e9c | ||
|
|
c7303803ba | ||
|
|
0b8b6064ed | ||
|
|
72a3aa1207 | ||
|
|
59ad601664 | ||
|
|
0b00533f99 | ||
|
|
2d6074706a | ||
|
|
9aaa78e020 | ||
|
|
385800553c | ||
|
|
d8f8abb644 | ||
|
|
e3e1ae92b2 | ||
|
|
f88e17a294 | ||
|
|
60f556428f | ||
|
|
e9964a4dfd | ||
|
|
598aaed500 | ||
|
|
7f38e87ef3 | ||
|
|
7b06194199 | ||
|
|
60925aa3a9 | ||
|
|
2abc11a1c7 | ||
|
|
94a777b23f | ||
|
|
dcb6cddab4 | ||
|
|
4575f2bbe6 | ||
|
|
9adffbb950 | ||
|
|
725cd9f82f | ||
|
|
1903388b6b | ||
|
|
ae6494f3e4 | ||
|
|
1f630b9767 | ||
|
|
7496aea1b0 | ||
|
|
f28bdf0d62 | ||
|
|
1aaf80c3ab | ||
|
|
c2de462955 | ||
|
|
f5e737c760 | ||
|
|
63536eba9f | ||
|
|
9dcb7d4351 | ||
|
|
133958ea47 | ||
|
|
965f2a90c7 | ||
|
|
fcbb51ad6b | ||
|
|
95dc01558f | ||
|
|
15b3d80e52 | ||
|
|
b46afde818 | ||
|
|
a926f87965 | ||
|
|
496fe0afa5 | ||
|
|
d2a525bbbe | ||
|
|
e82f49d390 | ||
|
|
a5231ec0e5 | ||
|
|
2df67c4f78 | ||
|
|
81ed7123c6 | ||
|
|
4ab0f729e1 | ||
|
|
95d22a03ae | ||
|
|
94feb1a80d | ||
|
|
254ecc4123 | ||
|
|
515b0794b0 | ||
|
|
dc8788b77f | ||
|
|
147ee53e35 | ||
|
|
e7884b8b61 |
13
.github/CODEOWNERS
vendored
13
.github/CODEOWNERS
vendored
@@ -2,10 +2,15 @@
|
||||
# Each line is a file pattern followed by one or more owners.
|
||||
# Syntax can be found here: https://docs.github.com/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
|
||||
|
||||
/src/sql/*.d.ts @alanrenmsft @Charles-Gagnon @ranasaria
|
||||
/extensions/resource-deployment/ @ranasaria
|
||||
/extensions/arc/ @ranasaria
|
||||
/extensions/azdata/ @ranasaria
|
||||
/extensions/admin-tool-ext-win @Charles-Gagnon
|
||||
/extensions/arc/ @Charles-Gagnon
|
||||
/extensions/azdata/ @Charles-Gagnon
|
||||
/extensions/big-data-cluster/ @Charles-Gagnon
|
||||
/extensions/dacpac/ @kisantia
|
||||
/extensions/query-history/ @Charles-Gagnon
|
||||
/extensions/resource-deployment/ @Charles-Gagnon
|
||||
/extensions/schema-compare/ @kisantia
|
||||
/extensions/sql-database-projects/ @Benjin @kisantia
|
||||
/extensions/mssql/config.json @Charles-Gagnon @alanrenmsft @kburtram
|
||||
|
||||
/src/sql/*.d.ts @alanrenmsft @Charles-Gagnon
|
||||
|
||||
27
.github/label-actions.yml
vendored
27
.github/label-actions.yml
vendored
@@ -1,2 +1,27 @@
|
||||
Needs Logs:
|
||||
comment: "We need more info to debug your particular issue. If you could attach your logs to the issue (ensure no private data is in them), it would help us fix the issue much faster.\n\nTo find your logs:\n\n- Open command palette (Click **View** -> **Command Palette**)\n- Run the command: **`Developer: Open Logs Folder`**\n\nThis will open the log file locally. Please zip up this folder and attach it to the issue."
|
||||
comment: "We need more info to debug your particular issue. If you could attach your logs to the issue (ensure no private data is in them), it would help us fix the issue much faster.
|
||||
|
||||
|
||||
There are two types of logs to collect:
|
||||
|
||||
|
||||
**Console Logs**
|
||||
|
||||
|
||||
- Open Developer Tools (Help -> Toggle Developer Tools)
|
||||
|
||||
- Click the **Console** tab
|
||||
|
||||
- Click in the log area and select all text (CTRL+A)
|
||||
|
||||
- Save this text into a file named console.log and attach it to this issue.
|
||||
|
||||
|
||||
**Application Logs**
|
||||
|
||||
|
||||
- Open command palette (Click **View** -> **Command Palette**)
|
||||
|
||||
- Run the command: **`Developer: Open Logs Folder`**
|
||||
|
||||
- This will open the log folder locally. Please zip up this folder and attach it to the issue."
|
||||
|
||||
4
.yarnrc
4
.yarnrc
@@ -1,3 +1,3 @@
|
||||
disturl "https://atom.io/download/electron"
|
||||
target "9.3.0"
|
||||
disturl "https://electronjs.org/headers"
|
||||
target "9.4.3"
|
||||
runtime "electron"
|
||||
|
||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
||||
# Change Log
|
||||
|
||||
## Version 1.25.2
|
||||
* Release date: January 22, 2021
|
||||
* Release status: General Availability
|
||||
* Fixes https://github.com/microsoft/azuredatastudio/issues/13899
|
||||
|
||||
## Version 1.25.1
|
||||
* Release date: December 10, 2020
|
||||
* Release status: General Availability
|
||||
* Fixes https://github.com/microsoft/azuredatastudio/issues/13751
|
||||
|
||||
## Version 1.25.0
|
||||
* Release date: December 8, 2020
|
||||
* Release status: General Availability
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.6",
|
||||
"@actions/github": "^2.1.1",
|
||||
"axios": "^0.19.2",
|
||||
"axios": "^0.21.1",
|
||||
"ts-node": "^8.6.2",
|
||||
"typescript": "^3.8.3"
|
||||
}
|
||||
|
||||
@@ -144,12 +144,12 @@ atob-lite@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696"
|
||||
integrity sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=
|
||||
|
||||
axios@^0.19.2:
|
||||
version "0.19.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
|
||||
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
|
||||
axios@^0.21.1:
|
||||
version "0.21.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
|
||||
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
|
||||
dependencies:
|
||||
follow-redirects "1.5.10"
|
||||
follow-redirects "^1.10.0"
|
||||
|
||||
before-after-hook@^2.0.0:
|
||||
version "2.1.0"
|
||||
@@ -177,13 +177,6 @@ cross-spawn@^6.0.0:
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
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"
|
||||
|
||||
deprecation@^2.0.0, deprecation@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
|
||||
@@ -214,12 +207,10 @@ execa@^1.0.0:
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
follow-redirects@1.5.10:
|
||||
version "1.5.10"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
|
||||
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
|
||||
dependencies:
|
||||
debug "=3.1.0"
|
||||
follow-redirects@^1.10.0:
|
||||
version "1.13.1"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
|
||||
integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
|
||||
|
||||
get-stream@^4.0.0:
|
||||
version "4.1.0"
|
||||
@@ -275,11 +266,6 @@ make-error@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||
|
||||
nice-try@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||
|
||||
@@ -8,6 +8,7 @@ steps:
|
||||
versionSpec: "1.x"
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: Restore Cache - Node Modules # {{SQL CARBON EDIT}}
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
@@ -19,6 +20,7 @@ steps:
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: Save Cache - Node Modules # {{SQL CARBON EDIT}}
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
|
||||
@@ -12,6 +12,7 @@ steps:
|
||||
displayName: Prepare cache flag
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: Restore Cache - Compiled Files
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .build/commit, .build/quality'
|
||||
targetfolder: '.build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min'
|
||||
@@ -49,7 +50,7 @@ steps:
|
||||
password $(github-distro-mixin-password)
|
||||
EOF
|
||||
|
||||
git config user.email "andresse@microsoft.com"
|
||||
git config user.email "sqltools@service.microsoft.com"
|
||||
git config user.name "AzureDataStudio"
|
||||
displayName: Prepare tooling
|
||||
|
||||
@@ -61,6 +62,7 @@ steps:
|
||||
displayName: Merge distro
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: Restore Cache - Node Modules
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
@@ -75,6 +77,7 @@ steps:
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: Save Cache - Node Modules
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
@@ -199,7 +202,7 @@ steps:
|
||||
testResultsFiles: 'test-results.xml'
|
||||
searchFolder: '$(Build.SourcesDirectory)'
|
||||
continueOnError: true
|
||||
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
|
||||
condition: and(succeededOrFailed(), eq(variables['RUN_TESTS'], 'true'))
|
||||
|
||||
- task: PublishCodeCoverageResults@1
|
||||
displayName: 'Publish code coverage from $(Build.SourcesDirectory)/.build/coverage/cobertura-coverage.xml'
|
||||
|
||||
@@ -28,7 +28,7 @@ steps:
|
||||
password $(github-distro-mixin-password)
|
||||
EOF
|
||||
|
||||
git config user.email "andresse@microsoft.com"
|
||||
git config user.email "sqltools@service.microsoft.com"
|
||||
git config user.name "AzureDataStudio"
|
||||
|
||||
git remote add distro "https://github.com/$(VSCODE_MIXIN_REPO).git"
|
||||
|
||||
@@ -22,7 +22,7 @@ steps:
|
||||
password $(github-distro-mixin-password)
|
||||
EOF
|
||||
|
||||
git config user.email "andresse@microsoft.com"
|
||||
git config user.email "sqltools@service.microsoft.com"
|
||||
git config user.name "AzureDataStudio"
|
||||
displayName: Prepare tooling
|
||||
|
||||
@@ -34,6 +34,7 @@ steps:
|
||||
displayName: Merge distro
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: Restore Cache - Node Modules
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
@@ -48,6 +49,7 @@ steps:
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: Save Cache - Node Modules
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
|
||||
@@ -17,6 +17,7 @@ steps:
|
||||
versionSpec: "1.x"
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: Restore Cache - Node Modules # {{SQL CARBON EDIT}}
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
@@ -28,6 +29,7 @@ steps:
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: Save Cache - Node Modules # {{SQL CARBON EDIT}}
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
|
||||
@@ -9,6 +9,7 @@ steps:
|
||||
displayName: Prepare cache flag
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: Restore Cache - Compiled Files
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .build/commit, .build/quality'
|
||||
targetfolder: '.build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min'
|
||||
@@ -45,7 +46,7 @@ steps:
|
||||
password $(github-distro-mixin-password)
|
||||
EOF
|
||||
|
||||
git config user.email "andresse@microsoft.com"
|
||||
git config user.email "sqltools@service.microsoft.com"
|
||||
git config user.name "AzureDataStudio"
|
||||
displayName: Prepare tooling
|
||||
|
||||
@@ -57,6 +58,7 @@ steps:
|
||||
displayName: Merge distro
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: Restore Cache - Node Modules
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
@@ -71,6 +73,7 @@ steps:
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: Save Cache - Node Modules
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
@@ -149,11 +152,17 @@ steps:
|
||||
continueOnError: true
|
||||
condition: and(succeeded(), eq(variables['RUN_UNSTABLE_TESTS'], 'true'))
|
||||
|
||||
- script: |
|
||||
- bash: |
|
||||
set -e
|
||||
mkdir -p $(Build.ArtifactStagingDirectory)/logs/linux-x64
|
||||
cd /tmp
|
||||
tar -czvf $(Build.ArtifactStagingDirectory)/logs/linux-x64/logs-linux-x64.tar.gz adsuser*
|
||||
for folder in adsuser*/
|
||||
do
|
||||
folder=${folder%/}
|
||||
# Only archive directories we want for debugging purposes
|
||||
tar -czvf $(Build.ArtifactStagingDirectory)/logs/linux-x64/$folder.tar.gz $folder/User $folder/logs
|
||||
done
|
||||
|
||||
displayName: Archive Logs
|
||||
continueOnError: true
|
||||
condition: succeededOrFailed()
|
||||
@@ -226,7 +235,7 @@ steps:
|
||||
testResultsFiles: '*.xml'
|
||||
searchFolder: '$(Build.ArtifactStagingDirectory)/test-results'
|
||||
continueOnError: true
|
||||
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
|
||||
condition: and(succeededOrFailed(), eq(variables['RUN_TESTS'], 'true'))
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact: drop'
|
||||
|
||||
@@ -6,6 +6,7 @@ steps:
|
||||
displayName: Prepare cache flag
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: Restore Cache - Compiled Files
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .build/commit, .build/quality'
|
||||
targetfolder: '.build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min'
|
||||
@@ -36,7 +37,7 @@ steps:
|
||||
password $(github-distro-mixin-password)
|
||||
EOF
|
||||
|
||||
git config user.email "andresse@microsoft.com"
|
||||
git config user.email "sqltools@service.microsoft.com"
|
||||
git config user.name "AzureDataStudio"
|
||||
displayName: Prepare tooling
|
||||
condition: and(succeeded(), ne(variables['CacheRestored-Compilation'], 'true'))
|
||||
@@ -50,6 +51,7 @@ steps:
|
||||
condition: and(succeeded(), ne(variables['CacheRestored-Compilation'], 'true'))
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: Restore Cache - Node Modules
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
@@ -62,6 +64,7 @@ steps:
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: Save Cache - Node Modules
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
@@ -123,6 +126,7 @@ steps:
|
||||
displayName: 'Publish Artifact: drop'
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: Save Cache - Compiled Files
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .build/commit, .build/quality'
|
||||
targetfolder: '.build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min'
|
||||
|
||||
@@ -6,6 +6,7 @@ steps:
|
||||
displayName: Prepare cache flag
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: Restore Cache - Compiled Files
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .build/commit, .build/quality'
|
||||
targetfolder: '.build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min'
|
||||
@@ -42,7 +43,7 @@ steps:
|
||||
password $(github-distro-mixin-password)
|
||||
EOF
|
||||
|
||||
git config user.email "andresse@microsoft.com"
|
||||
git config user.email "sqltools@service.microsoft.com"
|
||||
git config user.name "AzureDataStudio"
|
||||
displayName: Prepare tooling
|
||||
|
||||
@@ -54,6 +55,7 @@ steps:
|
||||
displayName: Merge distro
|
||||
|
||||
# - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
# displayName: Restore Cache - Node Modules
|
||||
# inputs:
|
||||
# keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock'
|
||||
# targetfolder: '**/node_modules, !**/node_modules/**/node_modules'
|
||||
@@ -66,6 +68,7 @@ steps:
|
||||
# condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
|
||||
# - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
# displayName: Save Cache - Node Modules
|
||||
# inputs:
|
||||
# keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock'
|
||||
# targetfolder: '**/node_modules, !**/node_modules/**/node_modules'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="ESRP" value="https://microsoft.pkgs.visualstudio.com/_packaging/ESRP/nuget/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
</configuration>
|
||||
|
||||
@@ -13,6 +13,7 @@ steps:
|
||||
addToPath: true
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: Restore Cache - Node Modules # {{SQL CARBON EDIT}}
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
@@ -26,6 +27,7 @@ steps:
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: Save Cache - Node Modules # {{SQL CARBON EDIT}}
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
|
||||
@@ -6,6 +6,7 @@ steps:
|
||||
displayName: Prepare cache flag
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: Restore Cache - Compiled Files
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .build/commit, .build/quality'
|
||||
targetfolder: '.build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min'
|
||||
@@ -44,7 +45,7 @@ steps:
|
||||
$ErrorActionPreference = "Stop"
|
||||
"machine github.com`nlogin azuredatastudio`npassword $(github-distro-mixin-password)" | Out-File "$env:USERPROFILE\_netrc" -Encoding ASCII
|
||||
|
||||
exec { git config user.email "andresse@microsoft.com" }
|
||||
exec { git config user.email "sqltools@service.microsoft.com" }
|
||||
exec { git config user.name "AzureDataStudio" }
|
||||
displayName: Prepare tooling
|
||||
|
||||
@@ -55,6 +56,7 @@ steps:
|
||||
displayName: Merge distro
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1
|
||||
displayName: Restore Cache - Node Modules
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
@@ -71,6 +73,7 @@ steps:
|
||||
condition: and(succeeded(), ne(variables['CacheRestored'], 'true'))
|
||||
|
||||
- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1
|
||||
displayName: Save Cache - Node Modules
|
||||
inputs:
|
||||
keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock'
|
||||
targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules'
|
||||
@@ -135,13 +138,13 @@ steps:
|
||||
displayName: Run integration tests (Electron)
|
||||
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
|
||||
|
||||
- powershell: |
|
||||
. build/azure-pipelines/win32/exec.ps1
|
||||
$ErrorActionPreference = "Stop"
|
||||
exec { .\scripts\test-unstable.bat --build --tfs }
|
||||
continueOnError: true
|
||||
condition: and(succeeded(), eq(variables['RUN_UNSTABLE_TESTS'], 'true'))
|
||||
displayName: Run unstable tests
|
||||
# - powershell: |
|
||||
# . build/azure-pipelines/win32/exec.ps1
|
||||
# $ErrorActionPreference = "Stop"
|
||||
# exec { .\scripts\test-unstable.bat --build --tfs }
|
||||
# continueOnError: true
|
||||
# condition: and(succeeded(), eq(variables['RUN_UNSTABLE_TESTS'], 'true'))
|
||||
# displayName: Run unstable tests
|
||||
|
||||
- task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1
|
||||
displayName: 'Sign out code'
|
||||
@@ -290,7 +293,7 @@ steps:
|
||||
searchFolder: '$(Build.SourcesDirectory)'
|
||||
failTaskOnFailedTests: true
|
||||
continueOnError: true
|
||||
condition: and(succeeded(), eq(variables['RUN_UNSTABLE_TESTS'], 'true'))
|
||||
condition: and(succeededOrFailed(), eq(variables['RUN_UNSTABLE_TESTS'], 'true'))
|
||||
|
||||
- task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0
|
||||
displayName: 'Component Detection'
|
||||
|
||||
@@ -99,4 +99,4 @@ steps:
|
||||
mergeTestResults: true
|
||||
failTaskOnFailedTests: true
|
||||
continueOnError: true
|
||||
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
|
||||
condition: and(succeededOrFailed(), eq(variables['RUN_TESTS'], 'true'))
|
||||
|
||||
@@ -98,7 +98,7 @@ const indentationFilter = [
|
||||
// {{SQL CARBON EDIT}}
|
||||
'!**/*.gif',
|
||||
'!build/actions/**/*.js',
|
||||
'!**/*.{xlf,docx,sql,vsix,bacpac,ipynb,jpg}',
|
||||
'!**/*.{xlf,lcl,docx,sql,vsix,bacpac,ipynb,jpg}',
|
||||
'!extensions/mssql/sqltoolsservice/**',
|
||||
'!extensions/import/flatfileimportservice/**',
|
||||
'!extensions/admin-tool-ext-win/ssmsmin/**',
|
||||
|
||||
@@ -223,9 +223,10 @@ const externalExtensions = [
|
||||
'profiler',
|
||||
'query-history',
|
||||
'schema-compare',
|
||||
'server-report',
|
||||
'sql-assessment',
|
||||
'sql-database-projects',
|
||||
'sql-migration',
|
||||
'sql-migration'
|
||||
];
|
||||
// extensions that require a rebuild since they have native parts
|
||||
const rebuildExtensions = [
|
||||
|
||||
@@ -257,9 +257,10 @@ const externalExtensions = [
|
||||
'profiler',
|
||||
'query-history',
|
||||
'schema-compare',
|
||||
'server-report',
|
||||
'sql-assessment',
|
||||
'sql-database-projects',
|
||||
'sql-migration',
|
||||
'sql-migration'
|
||||
];
|
||||
|
||||
// extensions that require a rebuild since they have native parts
|
||||
|
||||
@@ -1860,9 +1860,9 @@ inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
|
||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||
|
||||
ini@~1.3.0:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
|
||||
integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
|
||||
|
||||
is-absolute@^1.0.0:
|
||||
version "1.0.0"
|
||||
|
||||
@@ -60,12 +60,12 @@
|
||||
"git": {
|
||||
"name": "electron",
|
||||
"repositoryUrl": "https://github.com/electron/electron",
|
||||
"commitHash": "fb03807cd21915ddc3aa2521ba4f5ba14597bd7e"
|
||||
"commitHash": "ca82414364002efa665ffa7427e267adf76ed1f3"
|
||||
}
|
||||
},
|
||||
"isOnlyProductionDependency": true,
|
||||
"license": "MIT",
|
||||
"version": "9.3.0"
|
||||
"version": "9.4.3"
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"ads-extension-telemetry": "^1.0.0",
|
||||
"@microsoft/ads-extension-telemetry": "^1.1.3",
|
||||
"service-downloader": "0.2.1",
|
||||
"vscode-nls": "^3.2.1"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import AdsTelemetryReporter from 'ads-extension-telemetry';
|
||||
import AdsTelemetryReporter from '@microsoft/ads-extension-telemetry';
|
||||
|
||||
import * as Utils from './utils';
|
||||
|
||||
|
||||
@@ -182,6 +182,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
|
||||
integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
|
||||
|
||||
"@microsoft/ads-extension-telemetry@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@microsoft/ads-extension-telemetry/-/ads-extension-telemetry-1.1.3.tgz#54160eefa21f2a536622b0617f3c3f2018cf9c87"
|
||||
integrity sha512-+h6hM9oOA6Zj/N0nCGPzRgydR0YHiHpNJoNlv6a/ziWXO3RYSbQX+3U/PpT3gEA6+8RwByf6RVICo7uIGBy1LQ==
|
||||
dependencies:
|
||||
vscode-extension-telemetry "^0.1.6"
|
||||
|
||||
"@types/mocha@^5.2.5":
|
||||
version "5.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea"
|
||||
@@ -192,13 +199,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.7.tgz#01e4ea724d9e3bd50d90c11fd5980ba317d8fa11"
|
||||
integrity sha512-E6Zn0rffhgd130zbCbAr/JdXfXkoOUFAKNs/rF8qnafSJ8KYaA/j3oz7dcwal+lYjLA7xvdd5J4wdYpCTlP8+w==
|
||||
|
||||
ads-extension-telemetry@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ads-extension-telemetry/-/ads-extension-telemetry-1.0.0.tgz#840b363a6ad958447819b9bc59fdad3e49de31a9"
|
||||
integrity sha512-ouxZVECe4tsO0ek0dLdnAZEz1Lrytv1uLbbGZhRbZsHITsUYNjnkKnA471uWh0Dj80s+orvv49/j3/tNBDP/SQ==
|
||||
dependencies:
|
||||
vscode-extension-telemetry "0.1.2"
|
||||
|
||||
agent-base@4, agent-base@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
|
||||
@@ -225,15 +225,15 @@ append-transform@^2.0.0:
|
||||
dependencies:
|
||||
default-require-extensions "^3.0.0"
|
||||
|
||||
applicationinsights@1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.4.0.tgz#e17e436427b6e273291055181e29832cca978644"
|
||||
integrity sha512-TV8MYb0Kw9uE2cdu4V/UvTKdOABkX2+Fga9iDz0zqV7FLrNXfmAugWZmmdTx4JoynYkln3d5CUHY3oVSUEbfFw==
|
||||
applicationinsights@1.7.4:
|
||||
version "1.7.4"
|
||||
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.7.4.tgz#e7d96435594d893b00cf49f70a5927105dbb8749"
|
||||
integrity sha512-XFLsNlcanpjFhHNvVWEfcm6hr7lu9znnb6Le1Lk5RE03YUV9X2B2n2MfM4kJZRrUdV+C0hdHxvWyv+vWoLfY7A==
|
||||
dependencies:
|
||||
cls-hooked "^4.2.2"
|
||||
continuation-local-storage "^3.2.1"
|
||||
diagnostic-channel "0.2.0"
|
||||
diagnostic-channel-publishers "^0.3.2"
|
||||
diagnostic-channel-publishers "^0.3.3"
|
||||
|
||||
async-hook-jl@^1.7.6:
|
||||
version "1.7.6"
|
||||
@@ -397,7 +397,7 @@ default-require-extensions@^3.0.0:
|
||||
dependencies:
|
||||
strip-bom "^4.0.0"
|
||||
|
||||
diagnostic-channel-publishers@^0.3.2:
|
||||
diagnostic-channel-publishers@^0.3.3:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.3.5.tgz#a84a05fd6cc1d7619fdd17791c17e540119a7536"
|
||||
integrity sha512-AOIjw4T7Nxl0G2BoBPhkQ6i7T4bUd9+xvdYizwvG7vVAM1dvr+SDrcUudlmzwH0kbEwdR2V1EcnKT0wAeYLQNQ==
|
||||
@@ -974,12 +974,12 @@ to-fast-properties@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
|
||||
integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
|
||||
|
||||
vscode-extension-telemetry@0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.2.tgz#049207f5453930888ff68ca925b07bab08f2c955"
|
||||
integrity sha512-FSbaZKlIH3VKvBJsKw7v5bESWHXzltji2rtjaJeJglpQH4tfClzwHMzlMXUZGiblV++djEzb1gW8mb5E+wxFsg==
|
||||
vscode-extension-telemetry@^0.1.6:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.6.tgz#048b70c93243413036a8315cda493b8e7342980c"
|
||||
integrity sha512-rbzSg7k4NnsCdF4Lz0gI4jl3JLXR0hnlmfFgsY8CSDYhXgdoIxcre8jw5rjkobY0xhSDhbG7xCjP8zxskySJ/g==
|
||||
dependencies:
|
||||
applicationinsights "1.4.0"
|
||||
applicationinsights "1.7.4"
|
||||
|
||||
vscode-nls@^3.2.1:
|
||||
version "3.2.5"
|
||||
|
||||
@@ -30,9 +30,9 @@ export abstract class AgentDialog<T extends IAgentDialogData> {
|
||||
return this.model.dialogMode;
|
||||
}
|
||||
|
||||
protected abstract async updateModel(): Promise<void>;
|
||||
protected abstract updateModel(): Promise<void>;
|
||||
|
||||
protected abstract async initializeDialog(dialog: azdata.window.Dialog): Promise<void>;
|
||||
protected abstract initializeDialog(dialog: azdata.window.Dialog): Promise<void>;
|
||||
|
||||
public async openDialog(dialogName?: string) {
|
||||
if (!this._isOpen) {
|
||||
|
||||
10
extensions/arc/images/gear.svg
Normal file
10
extensions/arc/images/gear.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg id="a2f0dd32-c564-48d6-97d7-86323bfba35b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
|
||||
<defs>
|
||||
<linearGradient id="b863127b-2eb8-42a1-a46b-989a6a8d258c" x1="9" y1="18" x2="9" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#32bedd" />
|
||||
<stop offset="0.576" stop-color="#32ceef" />
|
||||
<stop offset="1" stop-color="#32d4f5" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M18,9.972V7.92l-.288-.108-2.2-.72-.576-1.4,1.116-2.376-1.44-1.44-.288.144L12.276,3.06l-1.4-.576L9.972,0H7.92L7.812.288l-.72,2.2-1.4.576L3.348,1.944l-1.44,1.44.144.288L3.1,5.724l-.576,1.4L0,8.028V10.08l.288.108,2.2.72.576,1.4L1.944,14.688l1.44,1.44.288-.144L5.724,14.94l1.4.576L8.028,18H10.08l.108-.288.72-2.2,1.4-.576,2.376,1.116,1.44-1.44-.144-.288L14.94,12.276l.576-1.4ZM9,12.95A3.95,3.95,0,1,1,12.95,9,3.947,3.947,0,0,1,9,12.95Z" fill="url(#b863127b-2eb8-42a1-a46b-989a6a8d258c)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 909 B |
3
extensions/arc/images/reset.svg
Normal file
3
extensions/arc/images/reset.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 5H2V0H3V3.3L3.8 2.4L4.6 1.7L5.4 1L6.3 0.5C6.59362 0.297586 6.94345 0.192635 7.3 0.2L8.5 0L9.9 0.2L11.3 0.8L12.4 1.6C12.7233 1.95924 12.9927 2.36344 13.2 2.8C13.459 3.20359 13.6609 3.64107 13.8 4.1C13.9226 4.55696 13.9897 5.027 14 5.5C13.9637 6.21504 13.8291 6.92168 13.6 7.6C13.2924 8.22655 12.8874 8.80035 12.4 9.3L5.9 15.9L5.1 15.1L11.7 8.6C12.0904 8.19804 12.3964 7.72203 12.6 7.2C12.8748 6.67624 13.0125 6.09136 13 5.5C13.0218 4.90769 12.8836 4.32046 12.6 3.8C12.4219 3.23995 12.072 2.75016 11.6 2.4C11.2498 1.928 10.76 1.57815 10.2 1.4C9.67954 1.11642 9.09231 0.978247 8.5 1C7.9834 0.981133 7.4696 1.08389 7 1.3L5.8 2L4.8 2.9L3.7 4H7V5Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 775 B |
@@ -2,7 +2,8 @@
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
"display_name": "Python 3",
|
||||
"language": "python"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
"display_name": "Python 3",
|
||||
"language": "python"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
"name": "arc",
|
||||
"displayName": "%arc.displayName%",
|
||||
"description": "%arc.description%",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.3",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/extension.png",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": ">=1.25.0"
|
||||
"azdata": ">=1.26.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onCommand:arc.connectToController",
|
||||
@@ -189,7 +189,9 @@
|
||||
"editable": false,
|
||||
"options": {
|
||||
"source": {
|
||||
"providerId": "arc.controller.config.profiles"
|
||||
"providerId": "arc.controller.config.profiles",
|
||||
"loadingText": "%arc.data.controller.cluster.config.profile.loading%",
|
||||
"loadingCompletedText": "%arc.data.controller.cluster.config.profile.loadingcompleted%"
|
||||
},
|
||||
"defaultValue": "azure-arc-aks-default-storage",
|
||||
"optionsType": "radio"
|
||||
@@ -290,11 +292,13 @@
|
||||
"target": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
|
||||
"value": "direct"
|
||||
},
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$",
|
||||
"description": "%arc.data.controller.spclientid.validation.description%"
|
||||
}]
|
||||
"validations": [
|
||||
{
|
||||
"type": "regex_match",
|
||||
"regex": "^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$",
|
||||
"description": "%arc.data.controller.spclientid.validation.description%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.data.controller.spclientsecret%",
|
||||
@@ -320,11 +324,13 @@
|
||||
"providerId": "subscription-id-to-tenant-id",
|
||||
"triggerField": "AZDATA_NB_VAR_ARC_SUBSCRIPTION"
|
||||
},
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$",
|
||||
"description": "%arc.data.controller.sptenantid.validation.description%"
|
||||
}]
|
||||
"validations": [
|
||||
{
|
||||
"type": "regex_match",
|
||||
"regex": "^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$",
|
||||
"description": "%arc.data.controller.sptenantid.validation.description%"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -344,11 +350,13 @@
|
||||
{
|
||||
"type": "text",
|
||||
"label": "%arc.data.controller.namespace%",
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$",
|
||||
"description": "%arc.data.controller.namespace.validation.description%"
|
||||
}],
|
||||
"validations": [
|
||||
{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$",
|
||||
"description": "%arc.data.controller.namespace.validation.description%"
|
||||
}
|
||||
],
|
||||
"defaultValue": "arc",
|
||||
"required": true,
|
||||
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE"
|
||||
@@ -356,11 +364,13 @@
|
||||
{
|
||||
"type": "text",
|
||||
"label": "%arc.data.controller.name%",
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z0-9]([-.a-z0-9]{0,251}[a-z0-9])?$",
|
||||
"description": "%arc.data.controller.name.validation.description%"
|
||||
}],
|
||||
"validations": [
|
||||
{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z0-9]([-.a-z0-9]{0,251}[a-z0-9])?$",
|
||||
"description": "%arc.data.controller.name.validation.description%"
|
||||
}
|
||||
],
|
||||
"defaultValue": "arc-dc",
|
||||
"required": true,
|
||||
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME"
|
||||
@@ -608,205 +618,13 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.2.0"
|
||||
"version": "20.3.0"
|
||||
}
|
||||
],
|
||||
"when": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "arc.sql",
|
||||
"displayName": "%resource.type.arc.sql.display.name%",
|
||||
"description": "%resource.type.arc.sql.description%",
|
||||
"platforms": "*",
|
||||
"icon": "./images/miaa.svg",
|
||||
"tags": [
|
||||
"Hybrid",
|
||||
"SQL Server"
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"notebookWizard": {
|
||||
"notebook": "./notebooks/arcDeployment/deploy.sql.existing.arc.ipynb",
|
||||
"doneAction": {
|
||||
"label": "%deploy.done.action%"
|
||||
},
|
||||
"scriptAction": {
|
||||
"label": "%deploy.script.action%"
|
||||
},
|
||||
"codeCellInsertionPosition": 5,
|
||||
"title": "%arc.sql.wizard.title%",
|
||||
"name": "arc.sql.wizard",
|
||||
"labelPosition": "left",
|
||||
"generateSummaryPage": false,
|
||||
"pages": [
|
||||
{
|
||||
"title": "%arc.sql.wizard.page1.title%",
|
||||
"labelWidth": "175px",
|
||||
"inputWidth": "280px",
|
||||
"sections": [
|
||||
{
|
||||
"title": "%arc.sql.connection.settings.section.title%",
|
||||
"fields": [
|
||||
{
|
||||
"label": "%arc.controller%",
|
||||
"variableName": "",
|
||||
"type": "options",
|
||||
"editable": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"source": {
|
||||
"providerId": "arc.controllers",
|
||||
"variableNames": {
|
||||
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
|
||||
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME",
|
||||
"password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
|
||||
}
|
||||
},
|
||||
"optionsType": "dropdown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "%arc.sql.instance.name%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_INSTANCE_NAME",
|
||||
"type": "text",
|
||||
"defaultValue": "sqlinstance1",
|
||||
"required": true,
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z]([-a-z0-9]{0,11}[a-z0-9])?$",
|
||||
"description": "%arc.sql.invalid.instance.name%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.sql.username%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_USERNAME",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^(?!sa$)",
|
||||
"description": "%arc.sql.invalid.username%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.password%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_PASSWORD",
|
||||
"type": "sql_password",
|
||||
"userName": "sa",
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%arc.confirm.password%",
|
||||
"defaultValue": "",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "%arc.sql.instance.settings.section.title%",
|
||||
"fields": [
|
||||
{
|
||||
"label": "%arc.storage-class.data.label%",
|
||||
"description": "%arc.sql.storage-class.data.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA",
|
||||
"type": "kube_storage_class",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "%arc.storage-class.logs.label%",
|
||||
"description": "%arc.sql.storage-class.logs.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS",
|
||||
"type": "kube_storage_class",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "%arc.cores-request.label%",
|
||||
"description": "%arc.sql.cores-request.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_CORES_REQUEST",
|
||||
"type": "number",
|
||||
"min": 1,
|
||||
"required": false,
|
||||
"validations": [
|
||||
{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_SQL_CORES_LIMIT",
|
||||
"description": "%requested.cores.less.than.or.equal.to.cores.limit%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.cores-limit.label%",
|
||||
"description": "%arc.sql.cores-limit.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_CORES_LIMIT",
|
||||
"type": "number",
|
||||
"min": 1,
|
||||
"required": false,
|
||||
"validations": [
|
||||
{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_SQL_CORES_REQUEST",
|
||||
"description": "%cores.limit.greater.than.or.equal.to.requested.cores%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.memory-request.label%",
|
||||
"description": "%arc.sql.memory-request.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_MEMORY_REQUEST",
|
||||
"type": "number",
|
||||
"min": 2,
|
||||
"required": false,
|
||||
"validations": [{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_SQL_MEMORY_LIMIT",
|
||||
"description": "%requested.memory.less.than.or.equal.to.memory.limit%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.memory-limit.label%",
|
||||
"description": "%arc.sql.memory-limit.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_MEMORY_LIMIT",
|
||||
"type": "number",
|
||||
"min": 2,
|
||||
"required": false,
|
||||
"validations": [{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_SQL_MEMORY_REQUEST",
|
||||
"description": "%memory.limit.greater.than.or.equal.to.requested.memory%"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"requiredTools": [
|
||||
{
|
||||
"name": "kubectl"
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.2.0"
|
||||
}
|
||||
],
|
||||
"when": "true"
|
||||
}
|
||||
],
|
||||
"agreement": {
|
||||
"template": "%arc.agreement%",
|
||||
"links": [
|
||||
{
|
||||
"text": "%microsoft.agreement.privacy.statement%",
|
||||
"url": "https://go.microsoft.com/fwlink/?LinkId=853010"
|
||||
},
|
||||
{
|
||||
"text": "%arc.agreement.sql.terms.conditions%",
|
||||
"url": "https://go.microsoft.com/fwlink/?linkid=2045708"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "arc.postgres",
|
||||
"displayName": "%resource.type.arc.postgres.display.name%",
|
||||
@@ -853,6 +671,8 @@
|
||||
"variableNames": {
|
||||
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
|
||||
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME",
|
||||
"kubeConfig": "AZDATA_NB_VAR_CONTROLLER_KUBECONFIG",
|
||||
"clusterContext": "AZDATA_NB_VAR_CONTROLLER_KUBECTL_CONTEXT",
|
||||
"password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
|
||||
}
|
||||
},
|
||||
@@ -864,11 +684,13 @@
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME",
|
||||
"type": "text",
|
||||
"description": "%arc.postgres.server.group.name.validation.description%",
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z]([-a-z0-9]{0,10}[a-z0-9])?$",
|
||||
"description": "%arc.postgres.server.group.name.validation.description%"
|
||||
}],
|
||||
"validations": [
|
||||
{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z]([-a-z0-9]{0,9}[a-z0-9])?$",
|
||||
"description": "%arc.postgres.server.group.name.validation.description%"
|
||||
}
|
||||
],
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
@@ -885,10 +707,12 @@
|
||||
"description": "%arc.postgres.server.group.workers.description%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_WORKERS",
|
||||
"type": "number",
|
||||
"validations": [{
|
||||
"type": "is_integer",
|
||||
"description": "%should.be.integer%"
|
||||
}],
|
||||
"validations": [
|
||||
{
|
||||
"type": "is_integer",
|
||||
"description": "%should.be.integer%"
|
||||
}
|
||||
],
|
||||
"defaultValue": "0",
|
||||
"min": 0
|
||||
},
|
||||
@@ -896,10 +720,12 @@
|
||||
"label": "%arc.postgres.server.group.port%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PORT",
|
||||
"type": "number",
|
||||
"validations": [{
|
||||
"type": "is_integer",
|
||||
"description": "%should.be.integer%"
|
||||
}],
|
||||
"validations": [
|
||||
{
|
||||
"type": "is_integer",
|
||||
"description": "%should.be.integer%"
|
||||
}
|
||||
],
|
||||
"defaultValue": "5432",
|
||||
"min": 1,
|
||||
"max": 65535
|
||||
@@ -981,11 +807,13 @@
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST",
|
||||
"type": "number",
|
||||
"min": 1,
|
||||
"validations": [{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT",
|
||||
"description": "%requested.cores.less.than.or.equal.to.cores.limit%"
|
||||
}]
|
||||
"validations": [
|
||||
{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT",
|
||||
"description": "%requested.cores.less.than.or.equal.to.cores.limit%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.postgres.server.group.cores.limit.label%",
|
||||
@@ -993,11 +821,13 @@
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT",
|
||||
"type": "number",
|
||||
"min": 1,
|
||||
"validations": [{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST",
|
||||
"description": "%cores.limit.greater.than.or.equal.to.requested.cores%"
|
||||
}]
|
||||
"validations": [
|
||||
{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST",
|
||||
"description": "%cores.limit.greater.than.or.equal.to.requested.cores%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.postgres.server.group.memory.request.label%",
|
||||
@@ -1005,11 +835,13 @@
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST",
|
||||
"type": "number",
|
||||
"min": 0.25,
|
||||
"validations": [{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT",
|
||||
"description": "%requested.memory.less.than.or.equal.to.memory.limit%"
|
||||
}]
|
||||
"validations": [
|
||||
{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT",
|
||||
"description": "%requested.memory.less.than.or.equal.to.memory.limit%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.postgres.server.group.memory.limit.label%",
|
||||
@@ -1017,11 +849,13 @@
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT",
|
||||
"type": "number",
|
||||
"min": 0.25,
|
||||
"validations": [{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST",
|
||||
"description": "%memory.limit.greater.than.or.equal.to.requested.memory%"
|
||||
}]
|
||||
"validations": [
|
||||
{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST",
|
||||
"description": "%memory.limit.greater.than.or.equal.to.requested.memory%"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1035,12 +869,224 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.2.0"
|
||||
"version": "20.3.0"
|
||||
}
|
||||
],
|
||||
"when": "true"
|
||||
}
|
||||
],
|
||||
"agreements": [
|
||||
{
|
||||
"template": "%arc.agreement%",
|
||||
"links": [
|
||||
{
|
||||
"text": "%microsoft.agreement.privacy.statement%",
|
||||
"url": "https://go.microsoft.com/fwlink/?LinkId=853010"
|
||||
},
|
||||
{
|
||||
"text": "%arc.agreement.postgres.terms.conditions%",
|
||||
"url": "https://go.microsoft.com/fwlink/?linkid=2045708"
|
||||
}
|
||||
],
|
||||
"when": "true"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"resourceDeploymentSubTypes": [
|
||||
{
|
||||
"resourceName": "azure-sql-mi",
|
||||
"options": [
|
||||
{
|
||||
"name": "mi-type",
|
||||
"values": [
|
||||
{
|
||||
"name": "arc-mi",
|
||||
"displayName": "%resource.type.arc.sql.display.name%"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Hybrid",
|
||||
"SQL Server"
|
||||
],
|
||||
"provider": {
|
||||
"notebookWizard": {
|
||||
"notebook": "./notebooks/arcDeployment/deploy.sql.existing.arc.ipynb",
|
||||
"doneAction": {
|
||||
"label": "%deploy.done.action%"
|
||||
},
|
||||
"scriptAction": {
|
||||
"label": "%deploy.script.action%"
|
||||
},
|
||||
"codeCellInsertionPosition": 5,
|
||||
"title": "%arc.sql.wizard.title%",
|
||||
"name": "arc.sql.wizard",
|
||||
"labelPosition": "left",
|
||||
"generateSummaryPage": false,
|
||||
"pages": [
|
||||
{
|
||||
"title": "%arc.sql.wizard.page1.title%",
|
||||
"labelWidth": "175px",
|
||||
"inputWidth": "280px",
|
||||
"sections": [
|
||||
{
|
||||
"title": "%arc.sql.connection.settings.section.title%",
|
||||
"fields": [
|
||||
{
|
||||
"label": "%arc.controller%",
|
||||
"variableName": "",
|
||||
"type": "options",
|
||||
"editable": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"source": {
|
||||
"providerId": "arc.controllers",
|
||||
"variableNames": {
|
||||
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
|
||||
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME",
|
||||
"kubeConfig": "AZDATA_NB_VAR_CONTROLLER_KUBECONFIG",
|
||||
"clusterContext": "AZDATA_NB_VAR_CONTROLLER_KUBECTL_CONTEXT",
|
||||
"password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
|
||||
}
|
||||
},
|
||||
"optionsType": "dropdown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "%arc.sql.instance.name%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_INSTANCE_NAME",
|
||||
"type": "text",
|
||||
"defaultValue": "sqlinstance1",
|
||||
"required": true,
|
||||
"validations": [
|
||||
{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z]([-a-z0-9]{0,11}[a-z0-9])?$",
|
||||
"description": "%arc.sql.invalid.instance.name%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.sql.username%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_USERNAME",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"validations": [
|
||||
{
|
||||
"type": "regex_match",
|
||||
"regex": "^(?!sa$)",
|
||||
"description": "%arc.sql.invalid.username%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.password%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_PASSWORD",
|
||||
"type": "sql_password",
|
||||
"userName": "sa",
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%arc.confirm.password%",
|
||||
"defaultValue": "",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "%arc.sql.instance.settings.section.title%",
|
||||
"fields": [
|
||||
{
|
||||
"label": "%arc.storage-class.data.label%",
|
||||
"description": "%arc.sql.storage-class.data.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA",
|
||||
"type": "kube_storage_class",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "%arc.storage-class.logs.label%",
|
||||
"description": "%arc.sql.storage-class.logs.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS",
|
||||
"type": "kube_storage_class",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "%arc.cores-request.label%",
|
||||
"description": "%arc.sql.cores-request.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_CORES_REQUEST",
|
||||
"type": "number",
|
||||
"min": 1,
|
||||
"required": false,
|
||||
"validations": [
|
||||
{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_SQL_CORES_LIMIT",
|
||||
"description": "%requested.cores.less.than.or.equal.to.cores.limit%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.cores-limit.label%",
|
||||
"description": "%arc.sql.cores-limit.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_CORES_LIMIT",
|
||||
"type": "number",
|
||||
"min": 1,
|
||||
"required": false,
|
||||
"validations": [
|
||||
{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_SQL_CORES_REQUEST",
|
||||
"description": "%cores.limit.greater.than.or.equal.to.requested.cores%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.memory-request.label%",
|
||||
"description": "%arc.sql.memory-request.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_MEMORY_REQUEST",
|
||||
"type": "number",
|
||||
"min": 2,
|
||||
"required": false,
|
||||
"validations": [
|
||||
{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_SQL_MEMORY_LIMIT",
|
||||
"description": "%requested.memory.less.than.or.equal.to.memory.limit%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.memory-limit.label%",
|
||||
"description": "%arc.sql.memory-limit.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_MEMORY_LIMIT",
|
||||
"type": "number",
|
||||
"min": 2,
|
||||
"required": false,
|
||||
"validations": [
|
||||
{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_SQL_MEMORY_REQUEST",
|
||||
"description": "%memory.limit.greater.than.or.equal.to.requested.memory%"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"requiredTools": [
|
||||
{
|
||||
"name": "kubectl"
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.3.0"
|
||||
}
|
||||
],
|
||||
"when": "mi-type=arc-mi"
|
||||
},
|
||||
"agreement": {
|
||||
"template": "%arc.agreement%",
|
||||
"links": [
|
||||
@@ -1049,10 +1095,21 @@
|
||||
"url": "https://go.microsoft.com/fwlink/?LinkId=853010"
|
||||
},
|
||||
{
|
||||
"text": "%arc.agreement.postgres.terms.conditions%",
|
||||
"text": "%arc.agreement.sql.terms.conditions%",
|
||||
"url": "https://go.microsoft.com/fwlink/?linkid=2045708"
|
||||
}
|
||||
]
|
||||
],
|
||||
"when": "mi-type=arc-mi"
|
||||
},
|
||||
"helpText": {
|
||||
"template": "%arc.agreement.sql.help.text%",
|
||||
"links": [
|
||||
{
|
||||
"text": "%arc.agreement.sql.help.text.learn.more%",
|
||||
"url": "https://go.microsoft.com/fwlink/?linkid=2141849"
|
||||
}
|
||||
],
|
||||
"when": "mi-type=arc-mi"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1070,6 +1127,7 @@
|
||||
"@types/sinon": "^9.0.4",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/yamljs": "^0.2.31",
|
||||
"azdata-test": "^1.1.1",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-junit-reporter": "^1.17.0",
|
||||
"mocha-multi-reporters": "^1.1.7",
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
"arc.data.controller.kube.cluster.context": "Cluster context",
|
||||
"arc.data.controller.cluster.config.profile.title": "Choose the config profile",
|
||||
"arc.data.controller.cluster.config.profile": "Config profile",
|
||||
"arc.data.controller.cluster.config.profile.loading": "Loading config profiles",
|
||||
"arc.data.controller.cluster.config.profile.loadingcompleted": "Loading config profiles complete",
|
||||
"arc.data.controller.create.azureconfig.title": "Azure and Connectivity Configuration",
|
||||
"arc.data.controller.connectivitymode.description": "Select the connectivity mode for the controller.",
|
||||
"arc.data.controller.create.controllerconfig.title": "Controller Configuration",
|
||||
@@ -39,12 +41,12 @@
|
||||
"arc.data.controller.connectivitymode": "Connectivity Mode",
|
||||
"arc.data.controller.direct": "Direct",
|
||||
"arc.data.controller.indirect": "Indirect",
|
||||
"arc.data.controller.serviceprincipal.description": "When deploying a controller in direct connected mode a Service Principal is required for uploading metrics to Azure. {0} about how to create this Service Principal and assign it the correct roles.",
|
||||
"arc.data.controller.serviceprincipal.description": "When deploying a controller in direct connected mode a Service Principal is required for connecting to Azure. {0} about how to create this Service Principal and assign it the correct roles.",
|
||||
"arc.data.controller.spclientid": "Service Principal Client ID",
|
||||
"arc.data.controller.spclientid.description": "The Application (client) ID of the created Service Principal",
|
||||
"arc.data.controller.spclientid.validation.description": "The client ID must be a GUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"arc.data.controller.spclientid.description": "The Client (application) ID of the created Service Principal",
|
||||
"arc.data.controller.spclientid.validation.description": "The Client ID must be a GUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"arc.data.controller.spclientsecret": "Service Principal Client Secret",
|
||||
"arc.data.controller.spclientsecret.description": "The password generated during creation of the Service Principal",
|
||||
"arc.data.controller.spclientsecret.description": "The secret (password) of the Service Principal",
|
||||
"arc.data.controller.sptenantid": "Service Principal Tenant ID",
|
||||
"arc.data.controller.sptenantid.description": "The Tenant ID of the Service Principal. This must be the same as the Tenant ID of the subscription selected to create this controller for.",
|
||||
"arc.data.controller.sptenantid.validation.description": "The tenant ID must be a GUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
@@ -123,7 +125,7 @@
|
||||
"arc.postgres.settings.resource.title": "Resource settings",
|
||||
"arc.postgres.settings.storage.title": "Storage settings",
|
||||
"arc.postgres.server.group.name": "Server group name",
|
||||
"arc.postgres.server.group.name.validation.description": "Server group name must consist of lower case alphanumeric characters or '-', start with a letter, end with an alphanumeric character, and be 12 characters or fewer in length.",
|
||||
"arc.postgres.server.group.name.validation.description": "Server group name must consist of lower case alphanumeric characters or '-', start with a letter, end with an alphanumeric character, and be 11 characters or fewer in length.",
|
||||
"arc.postgres.server.group.workers.label": "Number of workers",
|
||||
"arc.postgres.server.group.workers.description": "The number of worker nodes to provision in a sharded cluster, or zero (the default) for single-node Postgres.",
|
||||
"arc.postgres.server.group.port": "Port",
|
||||
@@ -151,5 +153,7 @@
|
||||
"requested.cores.less.than.or.equal.to.cores.limit": "Requested cores must be less than or equal to cores limit",
|
||||
"cores.limit.greater.than.or.equal.to.requested.cores": "Cores limit must be greater than or equal to requested cores",
|
||||
"requested.memory.less.than.or.equal.to.memory.limit": "Requested memory must be less than or equal to memory limit",
|
||||
"memory.limit.greater.than.or.equal.to.requested.memory": "Memory limit must be greater than or equal to requested memory"
|
||||
"memory.limit.greater.than.or.equal.to.requested.memory": "Memory limit must be greater than or equal to requested memory",
|
||||
"arc.agreement.sql.help.text": "Azure Arc enabled Managed Instance provides SQL Server access and feature compatibility that can be deployed on the infrastructure of your choice. While this service is in preview, it has some feature limitations compared to SQL Managed Instance on Azure. {0}",
|
||||
"arc.agreement.sql.help.text.learn.more": "Learn more"
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Deferred } from './promise';
|
||||
|
||||
const enum Status {
|
||||
notStarted,
|
||||
inProgress,
|
||||
done
|
||||
}
|
||||
|
||||
interface State<T> {
|
||||
entry?: T,
|
||||
error?: Error,
|
||||
status: Status,
|
||||
id: number,
|
||||
pendingOperation: Deferred<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of Cache Manager which ensures that only one call to populate cache miss is pending at a given time.
|
||||
* All remaining calls for retrieval are awaited until the one in progress finishes and then all awaited calls are resolved with the value
|
||||
* from the cache.
|
||||
*/
|
||||
export class CacheManager<K, T> {
|
||||
private _cache = new Map<K, State<T>>();
|
||||
private _id = 0;
|
||||
|
||||
public async getCacheEntry(key: K, retrieveEntry: (key: K) => Promise<T>): Promise<T> {
|
||||
const cacheHit: State<T> | undefined = this._cache.get(key);
|
||||
// each branch either throws or returns the password.
|
||||
if (cacheHit === undefined) {
|
||||
// populate a new state entry and add it to the cache
|
||||
const state: State<T> = {
|
||||
status: Status.notStarted,
|
||||
id: this._id++,
|
||||
pendingOperation: new Deferred<void>()
|
||||
};
|
||||
this._cache.set(key, state);
|
||||
// now that we have the state entry initialized, retry to fetch the cacheEntry
|
||||
let returnValue: T = await this.getCacheEntry(key, retrieveEntry);
|
||||
await state.pendingOperation;
|
||||
return returnValue!;
|
||||
} else {
|
||||
switch (cacheHit.status) {
|
||||
case Status.notStarted: {
|
||||
cacheHit.status = Status.inProgress;
|
||||
// retrieve and populate the missed cache hit.
|
||||
try {
|
||||
cacheHit.entry = await retrieveEntry(key);
|
||||
} catch (error) {
|
||||
cacheHit.error = error;
|
||||
} finally {
|
||||
cacheHit.status = Status.done;
|
||||
// we do not reject here even in error case because we do not want our awaits on pendingOperation to throw
|
||||
// We track our own error state and when all done we throw if an error had happened. This results
|
||||
// in the rejection of the promised returned by this method.
|
||||
cacheHit.pendingOperation.resolve();
|
||||
}
|
||||
return await this.getCacheEntry(key, retrieveEntry);
|
||||
}
|
||||
|
||||
case Status.inProgress: {
|
||||
await cacheHit.pendingOperation;
|
||||
return await this.getCacheEntry(key, retrieveEntry);
|
||||
}
|
||||
|
||||
case Status.done: {
|
||||
if (cacheHit.error !== undefined) {
|
||||
await cacheHit.pendingOperation;
|
||||
throw cacheHit.error;
|
||||
}
|
||||
else {
|
||||
await cacheHit.pendingOperation;
|
||||
return cacheHit.entry!;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,11 @@ export interface KubeClusterContext {
|
||||
isCurrentContext: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the cluster context defined in the {@see configFile}
|
||||
*
|
||||
* @param configFile
|
||||
*/
|
||||
export function getKubeConfigClusterContexts(configFile: string): Promise<KubeClusterContext[]> {
|
||||
const config: any = yamljs.load(configFile);
|
||||
const rawContexts = <any[]>config['contexts'];
|
||||
@@ -33,6 +38,38 @@ export function getKubeConfigClusterContexts(configFile: string): Promise<KubeCl
|
||||
return Promise.resolve(contexts);
|
||||
}
|
||||
|
||||
/**
|
||||
* searches for {@see previousClusterContext} in the array of {@see clusterContexts}.
|
||||
* if {@see previousClusterContext} was truthy and it was found in {@see clusterContexts}
|
||||
* then it returns {@see previousClusterContext}
|
||||
* else it returns the current cluster context from {@see clusterContexts} unless throwIfNotFound was set on input in which case an error is thrown instead.
|
||||
* else it returns the current cluster context from {@see clusterContexts}
|
||||
*
|
||||
*
|
||||
* @param clusterContexts
|
||||
* @param previousClusterContext
|
||||
* @param throwIfNotFound
|
||||
*/
|
||||
export function getCurrentClusterContext(clusterContexts: KubeClusterContext[], previousClusterContext?: string, throwIfNotFound: boolean = false): string {
|
||||
if (previousClusterContext) {
|
||||
if (clusterContexts.find(c => c.name === previousClusterContext)) { // if previous cluster context value is found in clusters then return that value
|
||||
return previousClusterContext;
|
||||
} else {
|
||||
if (throwIfNotFound) {
|
||||
throw new Error(loc.clusterContextNotFound(previousClusterContext));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if not previousClusterContext or throwIfNotFound was false when previousCLusterContext was not found in the clusterContexts
|
||||
const currentClusterContext = clusterContexts.find(c => c.isCurrentContext)?.name;
|
||||
throwUnless(currentClusterContext !== undefined, loc.noCurrentClusterContext);
|
||||
return currentClusterContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the default kube config file path
|
||||
*/
|
||||
export function getDefaultKubeConfigPath(): string {
|
||||
return path.join(os.homedir(), '.kube', 'config');
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
export class Deferred<T> {
|
||||
promise: Promise<T>;
|
||||
resolve!: (value?: T | PromiseLike<T>) => void;
|
||||
resolve!: (value: T | PromiseLike<T>) => void;
|
||||
reject!: (reason?: any) => void;
|
||||
constructor() {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
|
||||
@@ -109,7 +109,7 @@ export function getDatabaseStateDisplayText(state: string): string {
|
||||
* @returns Promise resolving to the user's input if it passed validation,
|
||||
* or undefined if the input box was closed for any other reason
|
||||
*/
|
||||
async function promptInputBox(title: string, options: vscode.InputBoxOptions): Promise<string> {
|
||||
async function promptInputBox(title: string, options: vscode.InputBoxOptions): Promise<string | undefined> {
|
||||
const inputBox = vscode.window.createInputBox();
|
||||
inputBox.title = title;
|
||||
inputBox.prompt = options.prompt;
|
||||
@@ -198,12 +198,16 @@ export function getErrorMessage(error: any, useMessageWithLink: boolean = false)
|
||||
|
||||
/**
|
||||
* Parses an address into its separate ip and port values. Address must be in the form <ip>:<port>
|
||||
* or <ip>,<port>
|
||||
* @param address The address to parse
|
||||
*/
|
||||
export function parseIpAndPort(address: string): { ip: string, port: string } {
|
||||
const sections = address.split(':');
|
||||
let sections = address.split(':');
|
||||
if (sections.length !== 2) {
|
||||
throw new Error(`Invalid address format for ${address}. Address must be in the form <ip>:<port>`);
|
||||
sections = address.split(',');
|
||||
if (sections.length !== 2) {
|
||||
throw new Error(`Invalid address format for ${address}. Address must be in the form <ip>:<port> or <ip>,<port>`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
ip: sections[0],
|
||||
@@ -295,3 +299,35 @@ export async function tryExecuteAction<T>(action: () => T | PromiseLike<T>): Pro
|
||||
}
|
||||
return { result, error };
|
||||
}
|
||||
|
||||
function decorate(decorator: (fn: Function, key: string) => Function): Function {
|
||||
return (_target: any, key: string, descriptor: any) => {
|
||||
let fnKey: string | null = null;
|
||||
let fn: Function | null = null;
|
||||
|
||||
if (typeof descriptor.value === 'function') {
|
||||
fnKey = 'value';
|
||||
fn = descriptor.value;
|
||||
} else if (typeof descriptor.get === 'function') {
|
||||
fnKey = 'get';
|
||||
fn = descriptor.get;
|
||||
}
|
||||
|
||||
if (!fn || !fnKey) {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
|
||||
descriptor[fnKey] = decorator(fn, key);
|
||||
};
|
||||
}
|
||||
|
||||
export function debounce(delay: number): Function {
|
||||
return decorate((fn, key) => {
|
||||
const timerKey = `$debounce$${key}`;
|
||||
|
||||
return function (this: any, ...args: any[]) {
|
||||
clearTimeout(this[timerKey]);
|
||||
this[timerKey] = setTimeout(() => fn.apply(this, args), delay);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export class IconPathHelper {
|
||||
public static properties: IconPath;
|
||||
public static networking: IconPath;
|
||||
public static refresh: IconPath;
|
||||
public static reset: IconPath;
|
||||
public static support: IconPath;
|
||||
public static wrench: IconPath;
|
||||
public static miaa: IconPath;
|
||||
@@ -44,6 +45,7 @@ export class IconPathHelper {
|
||||
public static discard: IconPath;
|
||||
public static fail: IconPath;
|
||||
public static information: IconPath;
|
||||
public static gear: IconPath;
|
||||
|
||||
public static setExtensionContext(context: vscode.ExtensionContext) {
|
||||
IconPathHelper.context = context;
|
||||
@@ -95,6 +97,10 @@ export class IconPathHelper {
|
||||
light: context.asAbsolutePath('images/refresh.svg'),
|
||||
dark: context.asAbsolutePath('images/refresh.svg')
|
||||
};
|
||||
IconPathHelper.reset = {
|
||||
light: context.asAbsolutePath('images/reset.svg'),
|
||||
dark: context.asAbsolutePath('images/reset.svg')
|
||||
};
|
||||
IconPathHelper.support = {
|
||||
light: context.asAbsolutePath('images/support.svg'),
|
||||
dark: context.asAbsolutePath('images/support.svg')
|
||||
@@ -135,6 +141,10 @@ export class IconPathHelper {
|
||||
light: context.asAbsolutePath('images/information.svg'),
|
||||
dark: context.asAbsolutePath('images/information.svg'),
|
||||
};
|
||||
IconPathHelper.gear = {
|
||||
light: context.asAbsolutePath('images/gear.svg'),
|
||||
dark: context.asAbsolutePath('images/gear.svg'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,14 +28,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<arc.IE
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('arc.connectToController', async () => {
|
||||
const nodes = await treeDataProvider.getChildren();
|
||||
if (nodes.length > 0) {
|
||||
const response = await vscode.window.showErrorMessage(loc.onlyOneControllerSupported, loc.yes, loc.no);
|
||||
if (response !== loc.yes) {
|
||||
return;
|
||||
}
|
||||
await treeDataProvider.removeController(nodes[0] as ControllerTreeNode);
|
||||
}
|
||||
const dialog = new ConnectToControllerDialog(treeDataProvider);
|
||||
dialog.showDialog();
|
||||
const model = await dialog.waitForClose();
|
||||
|
||||
@@ -23,12 +23,15 @@ export const properties = localize('arc.properties', "Properties");
|
||||
export const settings = localize('arc.settings', "Settings");
|
||||
export const security = localize('arc.security', "Security");
|
||||
export const computeAndStorage = localize('arc.computeAndStorage', "Compute + Storage");
|
||||
export const nodeParameters = localize('arc.nodeParameters', "Node Parameters");
|
||||
export const compute = localize('arc.compute', "Compute");
|
||||
export const backup = localize('arc.backup', "Backup");
|
||||
export const newSupportRequest = localize('arc.newSupportRequest', "New support request");
|
||||
export const diagnoseAndSolveProblems = localize('arc.diagnoseAndSolveProblems', "Diagnose and solve problems");
|
||||
export const supportAndTroubleshooting = localize('arc.supportAndTroubleshooting', "Support + troubleshooting");
|
||||
export const resourceHealth = localize('arc.resourceHealth', "Resource health");
|
||||
export const parameterName = localize('arc.parameterName', "Parameter Name");
|
||||
export const value = localize('arc.value', "Value");
|
||||
|
||||
export const newInstance = localize('arc.createNew', "New Instance");
|
||||
export const deleteText = localize('arc.delete', "Delete");
|
||||
@@ -61,16 +64,20 @@ export const yes = localize('arc.yes', "Yes");
|
||||
export const no = localize('arc.no', "No");
|
||||
export const feedback = localize('arc.feedback', "Feedback");
|
||||
export const selectConnectionString = localize('arc.selectConnectionString', "Select from available client connection strings below.");
|
||||
export const addingWokerNodes = localize('arc.addingWokerNodes', "adding worker nodes");
|
||||
export const addingWorkerNodes = localize('arc.addingWorkerNodes', "adding worker nodes");
|
||||
export const workerNodesDescription = localize('arc.workerNodesDescription', "Expand your server group and scale your database by adding worker nodes.");
|
||||
export const postgresConfigurationInformation = localize('arc.postgres.configurationInformation', "You can configure the number of CPU cores and storage size that will apply to both worker nodes and coordinator node. Each worker node will have the same configuration. Adjust the number of CPU cores and memory settings for your server group.");
|
||||
export const workerNodesInformation = localize('arc.workerNodeInformation', "In preview it is not possible to reduce the number of worker nodes. Please refer to documentation linked above for more information.");
|
||||
export const vCores = localize('arc.vCores', "vCores");
|
||||
export const ram = localize('arc.ram', "RAM");
|
||||
export const refresh = localize('arc.refresh', "Refresh");
|
||||
export const resetAllToDefault = localize('arc.resetAllToDefault', "Reset all to default");
|
||||
export const resetToDefault = localize('arc.resetToDefault', "Reset to default");
|
||||
export const troubleshoot = localize('arc.troubleshoot', "Troubleshoot");
|
||||
export const clickTheNewSupportRequestButton = localize('arc.clickTheNewSupportRequestButton', "Click the new support request button to file a support request in the Azure Portal.");
|
||||
export const running = localize('arc.running', "Running");
|
||||
export const ready = localize('arc.ready', "Ready");
|
||||
export const notReady = localize('arc.notReady', "Not Ready");
|
||||
export const pending = localize('arc.pending', "Pending");
|
||||
export const failed = localize('arc.failed', "Failed");
|
||||
export const unknown = localize('arc.unknown', "Unknown");
|
||||
@@ -79,19 +86,27 @@ export const indirect = localize('arc.indirect', "Indirect");
|
||||
export const loading = localize('arc.loading', "Loading...");
|
||||
export const refreshToEnterCredentials = localize('arc.refreshToEnterCredentials', "Refresh node to enter credentials");
|
||||
export const noInstancesAvailable = localize('arc.noInstancesAvailable', "No instances available");
|
||||
export const connectToServer = localize('arc.connecToServer', "Connect to Server");
|
||||
export const connectToController = localize('arc.connectToController', "Connect to Existing Controller");
|
||||
export function connectToSql(name: string): string { return localize('arc.connectToSql', "Connect to SQL managed instance - Azure Arc ({0})", name); }
|
||||
export function connectToMSSql(name: string): string { return localize('arc.connectToMSSql', "Connect to SQL managed instance - Azure Arc ({0})", name); }
|
||||
export function connectToPGSql(name: string): string { return localize('arc.connectToPGSql', "Connect to PostgreSQL Hyperscale - Azure Arc ({0})", name); }
|
||||
export const passwordToController = localize('arc.passwordToController', "Provide Password to Controller");
|
||||
export const controllerUrl = localize('arc.controllerUrl', "Controller URL");
|
||||
export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint");
|
||||
export const controllerName = localize('arc.controllerName', "Name");
|
||||
export const controllerKubeConfig = localize('arc.controllerKubeConfig', "Kube Config File Path");
|
||||
export const controllerClusterContext = localize('arc.controllerClusterContext', "Cluster Context");
|
||||
export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc");
|
||||
export const postgresProviderName = localize('arc.postgresProviderName', "PGSQL");
|
||||
export const miaaProviderName = localize('arc.miaaProviderName', "MSSQL");
|
||||
export const username = localize('arc.username', "Username");
|
||||
export const password = localize('arc.password', "Password");
|
||||
export const rememberPassword = localize('arc.rememberPassword', "Remember Password");
|
||||
export const connect = localize('arc.connect', "Connect");
|
||||
export const cancel = localize('arc.cancel', "Cancel");
|
||||
export const ok = localize('arc.ok', "Ok");
|
||||
export const on = localize('arc.on', "On");
|
||||
export const off = localize('arc.off', "Off");
|
||||
export const notConfigured = localize('arc.notConfigured', "Not Configured");
|
||||
|
||||
// Database States - see https://docs.microsoft.com/sql/relational-databases/databases/database-states
|
||||
@@ -120,6 +135,10 @@ export const databaseName = localize('arc.databaseName', "Database name");
|
||||
export const enterNewPassword = localize('arc.enterNewPassword', "Enter a new password");
|
||||
export const confirmNewPassword = localize('arc.confirmNewPassword', "Confirm the new password");
|
||||
export const learnAboutPostgresClients = localize('arc.learnAboutPostgresClients', "Learn more about Azure PostgreSQL Hyperscale client interfaces");
|
||||
export const nodeParametersDescription = localize('arc.nodeParametersDescription', " These server parameters of the Coordinator node and the Worker nodes can be set to custom (non-default) values. Search to find parameters.");
|
||||
export const learnAboutNodeParameters = localize('arc.learnAboutNodeParameters', "Learn more about database engine settings for Azure Arc enabled PostgreSQL Hyperscale");
|
||||
export const noNodeParametersFound = localize('arc.noNodeParametersFound', "No worker server parameters found...");
|
||||
export const searchToFilter = localize('arc.searchToFilter', "Search to filter items...");
|
||||
export const scalingCompute = localize('arc.scalingCompute', "scaling compute vCores and memory.");
|
||||
export const postgresComputeAndStorageDescriptionPartOne = localize('arc.postgresComputeAndStorageDescriptionPartOne', "You can scale your Azure Arc enabled");
|
||||
export const miaaComputeAndStorageDescriptionPartOne = localize('arc.miaaComputeAndStorageDescriptionPartOne', "You can scale your Azure SQL managed instance - Azure Arc by");
|
||||
@@ -151,9 +170,15 @@ export const details = localize('arc.details', "Details");
|
||||
export const lastUpdated = localize('arc.lastUpdated', "Last updated");
|
||||
export const noExternalEndpoint = localize('arc.noExternalEndpoint', "No External Endpoint has been configured so this information isn't available.");
|
||||
export const podsReady = localize('arc.podsReady', "pods ready");
|
||||
export const connectToPostgresDescription = localize('arc.connectToPostgresDescription', "A connection to the server is required to show and set database engine settings, which will require the PostgreSQL Extension to be installed.");
|
||||
export const postgresExtension = localize('arc.postgresExtension', "microsoft.azuredatastudio-postgresql");
|
||||
|
||||
export function rangeSetting(min: string, max: string): string { return localize('arc.rangeSetting', "Value is expected to be in the range {0} - {1}", min, max); }
|
||||
export function allowedValue(value: string): string { return localize('arc.allowedValue', "Value is expected to be {0}", value); }
|
||||
export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", name); }
|
||||
export function deletingInstance(name: string): string { return localize('arc.deletingInstance', "Deleting instance '{0}'...", name); }
|
||||
export function installingExtension(name: string): string { return localize('arc.installingExtension', "Installing extension '{0}'...", name); }
|
||||
export function extensionInstalled(name: string): string { return localize('arc.extensionInstalled', "Extension '{0}' has been installed.", name); }
|
||||
export function updatingInstance(name: string): string { return localize('arc.updatingInstance', "Updating instance '{0}'...", name); }
|
||||
export function instanceDeleted(name: string): string { return localize('arc.instanceDeleted', "Instance '{0}' deleted", name); }
|
||||
export function instanceUpdated(name: string): string { return localize('arc.instanceUpdated', "Instance '{0}' updated", name); }
|
||||
@@ -174,35 +199,46 @@ export function updated(when: string): string { return localize('arc.updated', "
|
||||
export function validationMin(min: number): string { return localize('arc.validationMin', "Value must be greater than or equal to {0}.", min); }
|
||||
|
||||
// Errors
|
||||
export const connectionRequired = localize('arc.connectionRequired', "A connection is required to show all properties. Click refresh to re-enter connection information");
|
||||
export const pgConnectionRequired = localize('arc.pgConnectionRequired', "A connection is required to show and set database engine settings.");
|
||||
export const miaaConnectionRequired = localize('arc.miaaConnectionRequired', "A connection is required to list the databases on this instance.");
|
||||
export const couldNotFindControllerRegistration = localize('arc.couldNotFindControllerRegistration', "Could not find controller registration.");
|
||||
export function outOfRange(min: string, max: string): string { return localize('arc.outOfRange', "The number must be in range {0} - {1}", min, max); }
|
||||
export function refreshFailed(error: any): string { return localize('arc.refreshFailed', "Refresh failed. {0}", getErrorMessage(error)); }
|
||||
export function resetFailed(error: any): string { return localize('arc.resetFailed', "Reset failed. {0}", getErrorMessage(error)); }
|
||||
export function openDashboardFailed(error: any): string { return localize('arc.openDashboardFailed', "Error opening dashboard. {0}", getErrorMessage(error)); }
|
||||
export function instanceDeletionFailed(name: string, error: any): string { return localize('arc.instanceDeletionFailed', "Failed to delete instance {0}. {1}", name, getErrorMessage(error)); }
|
||||
export function instanceUpdateFailed(name: string, error: any): string { return localize('arc.instanceUpdateFailed', "Failed to update instance {0}. {1}", name, getErrorMessage(error)); }
|
||||
export function pageDiscardFailed(error: any): string { return localize('arc.pageDiscardFailed', "Failed to discard user input. {0}", getErrorMessage(error)); }
|
||||
export function databaseCreationFailed(name: string, error: any): string { return localize('arc.databaseCreationFailed', "Failed to create database {0}. {1}", name, getErrorMessage(error)); }
|
||||
export function connectToControllerFailed(url: string, error: any): string { return localize('arc.connectToControllerFailed', "Could not connect to controller {0}. {1}", url, getErrorMessage(error)); }
|
||||
export function connectToSqlFailed(serverName: string, error: any): string { return localize('arc.connectToSqlFailed', "Could not connect to SQL managed instance - Azure Arc Instance {0}. {1}", serverName, getErrorMessage(error)); }
|
||||
export function connectToMSSqlFailed(serverName: string, error: any): string { return localize('arc.connectToMSSqlFailed', "Could not connect to SQL managed instance - Azure Arc Instance {0}. {1}", serverName, getErrorMessage(error)); }
|
||||
export function connectToPGSqlFailed(serverName: string, error: any): string { return localize('arc.connectToPGSqlFailed', "Could not connect to PostgreSQL Hyperscale - Azure Arc Instance {0}. {1}", serverName, getErrorMessage(error)); }
|
||||
export function missingExtension(extensionName: string): string { return localize('arc.missingExtension', "The {0} extension is required to view engine settings. Do you wish to install it now?", extensionName); }
|
||||
export function extensionInstallationFailed(extensionName: string): string { return localize('arc.extensionInstallationFailed', "Failed to install extension {0}.", extensionName); }
|
||||
export function fetchConfigFailed(name: string, error: any): string { return localize('arc.fetchConfigFailed', "An unexpected error occurred retrieving the config for '{0}'. {1}", name, getErrorMessage(error)); }
|
||||
export function fetchEndpointsFailed(name: string, error: any): string { return localize('arc.fetchEndpointsFailed', "An unexpected error occurred retrieving the endpoints for '{0}'. {1}", name, getErrorMessage(error)); }
|
||||
export function fetchRegistrationsFailed(name: string, error: any): string { return localize('arc.fetchRegistrationsFailed', "An unexpected error occurred retrieving the registrations for '{0}'. {1}", name, getErrorMessage(error)); }
|
||||
export function fetchDatabasesFailed(name: string, error: any): string { return localize('arc.fetchDatabasesFailed', "An unexpected error occurred retrieving the databases for '{0}'. {1}", name, getErrorMessage(error)); }
|
||||
export function fetchEngineSettingsFailed(name: string, error: any): string { return localize('arc.fetchEngineSettingsFailed', "An unexpected error occurred retrieving the engine settings for '{0}'. {1}", name, getErrorMessage(error)); }
|
||||
export function instanceDeletionWarning(name: string): string { return localize('arc.instanceDeletionWarning', "Warning! Deleting an instance is permanent and cannot be undone. To delete the instance '{0}' type the name '{0}' below to proceed.", name); }
|
||||
export function invalidInstanceDeletionName(name: string): string { return localize('arc.invalidInstanceDeletionName', "The value '{0}' does not match the instance name. Try again or press escape to exit", name); }
|
||||
export function couldNotFindAzureResource(name: string): string { return localize('arc.couldNotFindAzureResource', "Could not find Azure resource for {0}", name); }
|
||||
export function passwordResetFailed(error: any): string { return localize('arc.passwordResetFailed', "Failed to reset password. {0}", getErrorMessage(error)); }
|
||||
export function errorConnectingToController(error: any): string { return localize('arc.errorConnectingToController', "Error connecting to controller. {0}", getErrorMessage(error, true)); }
|
||||
export function passwordAcquisitionFailed(error: any): string { return localize('arc.passwordAcquisitionFailed', "Failed to acquire password. {0}", getErrorMessage(error)); }
|
||||
export const invalidPassword = localize('arc.invalidPassword', "The password did not work, try again.");
|
||||
export const loginFailed = localize('arc.loginFailed', "Error logging into controller - wrong username or password");
|
||||
export function errorVerifyingPassword(error: any): string { return localize('arc.errorVerifyingPassword', "Error encountered while verifying password. {0}", getErrorMessage(error)); }
|
||||
export const onlyOneControllerSupported = localize('arc.onlyOneControllerSupported', "Only one controller connection is currently supported at this time. Do you wish to remove the existing connection and add a new one?");
|
||||
export const noControllersConnected = localize('noControllersConnected', "No Azure Arc controllers are currently connected. Please run the command: 'Connect to Existing Azure Arc Controller' and then try again");
|
||||
export const variableValueFetchForUnsupportedVariable = (variableName: string) => localize('getVariableValue.unknownVariableName', "Attempt to get variable value for unknown variable:{0}", variableName);
|
||||
export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName);
|
||||
export const noControllerInfoFound = (name: string) => localize('noControllerInfoFound', "Controller Info could not be found with name: {0}", name);
|
||||
export const noPasswordFound = (controllerName: string) => localize('noPasswordFound', "Password could not be retrieved for controller: {0} and user did not provide a password. Please retry later.", controllerName);
|
||||
export const clusterContextNotFound = (clusterContext: string) => localize('clusterContextNotFound', "Cluster Context with name: {0} not found in the Kube config file", clusterContext);
|
||||
export const noCurrentClusterContext = localize('noCurrentClusterContext', "No current cluster context was found in the kube config file");
|
||||
export const browse = localize('filePicker.browse', "Browse");
|
||||
export const select = localize('button.label', "Select");
|
||||
export const noContextFound = (configFile: string) => localize('noContextFound', "No 'contexts' found in the config file: {0}", configFile);
|
||||
export const noCurrentContextFound = (configFile: string) => localize('noCurrentContextFound', "No context is marked as 'current-context' in the config file: {0}", configFile);
|
||||
export const noNameInContext = (configFile: string) => localize('noNameInContext', "No name field was found in a cluster context in the config file: {0}", configFile);
|
||||
export const userCancelledError = localize('userCancelledError', "User cancelled the dialog");
|
||||
export const userCancelledError = localize('arc.userCancelledError', "User cancelled the dialog");
|
||||
export const clusterContextConfigNoLongerValid = (configFile: string, clusterContext: string, error: any) => localize('clusterContextConfigNoLongerValid', "The cluster context information specified by config file: {0} and cluster context: {1} is no longer valid. Error is:\n\t{2}\n Do you want to update this information?", configFile, clusterContext, getErrorMessage(error));
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ControllerInfo, ResourceType } from 'arc';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as vscode from 'vscode';
|
||||
import { UserCancelledError } from '../common/api';
|
||||
import { getCurrentClusterContext, getKubeConfigClusterContexts } from '../common/kubeUtils';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog';
|
||||
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||
@@ -50,19 +51,42 @@ export class ControllerModel {
|
||||
this._onInfoUpdated.fire(this._info);
|
||||
}
|
||||
|
||||
public get azdataAdditionalEnvVars(): azdataExt.AdditionalEnvVars {
|
||||
return {
|
||||
'KUBECONFIG': this.info.kubeConfigFilePath,
|
||||
'KUBECTL_CONTEXT': this.info.kubeClusterContext
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls azdata login to set the context to this controller
|
||||
* Calls azdata login to set the context to this controller and acquires a login session to prevent other
|
||||
* calls from changing the context while commands for this session are being executed.
|
||||
* @param promptReconnect
|
||||
*/
|
||||
public async azdataLogin(promptReconnect: boolean = false): Promise<void> {
|
||||
// We haven't gotten our password yet or we want to prompt for a reconnect
|
||||
if (!this._password || promptReconnect) {
|
||||
public async acquireAzdataSession(promptReconnect: boolean = false): Promise<azdataExt.AzdataSession> {
|
||||
let promptForValidClusterContext: boolean = false;
|
||||
try {
|
||||
const contexts = await getKubeConfigClusterContexts(this.info.kubeConfigFilePath);
|
||||
getCurrentClusterContext(contexts, this.info.kubeClusterContext, true); // this throws if this.info.kubeClusterContext is not found in 'contexts'
|
||||
} catch (error) {
|
||||
const response = await vscode.window.showErrorMessage(loc.clusterContextConfigNoLongerValid(this.info.kubeConfigFilePath, this.info.kubeClusterContext, error), loc.yes, loc.no);
|
||||
if (response === loc.yes) {
|
||||
promptForValidClusterContext = true;
|
||||
} else {
|
||||
if (!promptReconnect) { //throw unless we are required to prompt for reconnect anyways
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We haven't gotten our password yet or we want to prompt for a reconnect or we want to prompt to reacquire valid cluster context or any and all of these.
|
||||
if (!this._password || promptReconnect || promptForValidClusterContext) {
|
||||
this._password = '';
|
||||
if (this.info.rememberPassword) {
|
||||
// It should be in the credentials store, get it from there
|
||||
this._password = await this.treeDataProvider.getPassword(this.info);
|
||||
}
|
||||
if (promptReconnect || !this._password) {
|
||||
if (promptReconnect || !this._password || promptForValidClusterContext) {
|
||||
// No password yet or we want to re-prompt for credentials so prompt for it from the user
|
||||
const dialog = new ConnectToControllerDialog(this.treeDataProvider);
|
||||
dialog.showDialog(this.info, this._password);
|
||||
@@ -70,13 +94,14 @@ export class ControllerModel {
|
||||
if (model) {
|
||||
await this.treeDataProvider.addOrUpdateController(model.controllerModel, model.password, false);
|
||||
this._password = model.password;
|
||||
this._info = model.controllerModel.info;
|
||||
} else {
|
||||
throw new UserCancelledError(loc.userCancelledError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this._azdataApi.azdata.login(this.info.url, this.info.username, this._password);
|
||||
return this._azdataApi.azdata.acquireSession(this.info.url, this.info.username, this._password, this.azdataAdditionalEnvVars);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,62 +116,66 @@ export class ControllerModel {
|
||||
}
|
||||
}
|
||||
public async refresh(showErrors: boolean = true, promptReconnect: boolean = false): Promise<void> {
|
||||
await this.azdataLogin(promptReconnect);
|
||||
const session = await this.acquireAzdataSession(promptReconnect);
|
||||
const newRegistrations: Registration[] = [];
|
||||
await Promise.all([
|
||||
this._azdataApi.azdata.arc.dc.config.show().then(result => {
|
||||
this._controllerConfig = result.result;
|
||||
this.configLastUpdated = new Date();
|
||||
this._onConfigUpdated.fire(this._controllerConfig);
|
||||
}).catch(err => {
|
||||
// If an error occurs show a message so the user knows something failed but still
|
||||
// fire the event so callers can know to update (e.g. so dashboards don't show the
|
||||
// loading icon forever)
|
||||
if (showErrors) {
|
||||
vscode.window.showErrorMessage(loc.fetchConfigFailed(this.info.name, err));
|
||||
}
|
||||
this._onConfigUpdated.fire(this._controllerConfig);
|
||||
throw err;
|
||||
}),
|
||||
this._azdataApi.azdata.arc.dc.endpoint.list().then(result => {
|
||||
this._endpoints = result.result;
|
||||
this.endpointsLastUpdated = new Date();
|
||||
this._onEndpointsUpdated.fire(this._endpoints);
|
||||
}).catch(err => {
|
||||
// If an error occurs show a message so the user knows something failed but still
|
||||
// fire the event so callers can know to update (e.g. so dashboards don't show the
|
||||
// loading icon forever)
|
||||
if (showErrors) {
|
||||
vscode.window.showErrorMessage(loc.fetchEndpointsFailed(this.info.name, err));
|
||||
}
|
||||
this._onEndpointsUpdated.fire(this._endpoints);
|
||||
throw err;
|
||||
}),
|
||||
Promise.all([
|
||||
this._azdataApi.azdata.arc.postgres.server.list().then(result => {
|
||||
newRegistrations.push(...result.result.map(r => {
|
||||
return {
|
||||
instanceName: r.name,
|
||||
state: r.state,
|
||||
instanceType: ResourceType.postgresInstances
|
||||
};
|
||||
}));
|
||||
try {
|
||||
await Promise.all([
|
||||
this._azdataApi.azdata.arc.dc.config.show(this.azdataAdditionalEnvVars, session).then(result => {
|
||||
this._controllerConfig = result.result;
|
||||
this.configLastUpdated = new Date();
|
||||
this._onConfigUpdated.fire(this._controllerConfig);
|
||||
}).catch(err => {
|
||||
// If an error occurs show a message so the user knows something failed but still
|
||||
// fire the event so callers hooking into this can handle the error (e.g. so dashboards don't show the
|
||||
// loading icon forever)
|
||||
if (showErrors) {
|
||||
vscode.window.showErrorMessage(loc.fetchConfigFailed(this.info.name, err));
|
||||
}
|
||||
this._onConfigUpdated.fire(this._controllerConfig);
|
||||
throw err;
|
||||
}),
|
||||
this._azdataApi.azdata.arc.sql.mi.list().then(result => {
|
||||
newRegistrations.push(...result.result.map(r => {
|
||||
return {
|
||||
instanceName: r.name,
|
||||
state: r.state,
|
||||
instanceType: ResourceType.sqlManagedInstances
|
||||
};
|
||||
}));
|
||||
this._azdataApi.azdata.arc.dc.endpoint.list(this.azdataAdditionalEnvVars, session).then(result => {
|
||||
this._endpoints = result.result;
|
||||
this.endpointsLastUpdated = new Date();
|
||||
this._onEndpointsUpdated.fire(this._endpoints);
|
||||
}).catch(err => {
|
||||
// If an error occurs show a message so the user knows something failed but still
|
||||
// fire the event so callers can know to update (e.g. so dashboards don't show the
|
||||
// loading icon forever)
|
||||
if (showErrors) {
|
||||
vscode.window.showErrorMessage(loc.fetchEndpointsFailed(this.info.name, err));
|
||||
}
|
||||
this._onEndpointsUpdated.fire(this._endpoints);
|
||||
throw err;
|
||||
}),
|
||||
Promise.all([
|
||||
this._azdataApi.azdata.arc.postgres.server.list(this.azdataAdditionalEnvVars, session).then(result => {
|
||||
newRegistrations.push(...result.result.map(r => {
|
||||
return {
|
||||
instanceName: r.name,
|
||||
state: r.state,
|
||||
instanceType: ResourceType.postgresInstances
|
||||
};
|
||||
}));
|
||||
}),
|
||||
this._azdataApi.azdata.arc.sql.mi.list(this.azdataAdditionalEnvVars, session).then(result => {
|
||||
newRegistrations.push(...result.result.map(r => {
|
||||
return {
|
||||
instanceName: r.name,
|
||||
state: r.state,
|
||||
instanceType: ResourceType.sqlManagedInstances
|
||||
};
|
||||
}));
|
||||
})
|
||||
]).then(() => {
|
||||
this._registrations = newRegistrations;
|
||||
this.registrationsLastUpdated = new Date();
|
||||
this._onRegistrationsUpdated.fire(this._registrations);
|
||||
})
|
||||
]).then(() => {
|
||||
this._registrations = newRegistrations;
|
||||
this.registrationsLastUpdated = new Date();
|
||||
this._onRegistrationsUpdated.fire(this._registrations);
|
||||
})
|
||||
]);
|
||||
]);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public get endpoints(): azdataExt.DcEndpointListResult[] {
|
||||
|
||||
@@ -9,10 +9,9 @@ import * as azdataExt from 'azdata-ext';
|
||||
import * as vscode from 'vscode';
|
||||
import { UserCancelledError } from '../common/api';
|
||||
import { Deferred } from '../common/promise';
|
||||
import { createCredentialId, parseIpAndPort } from '../common/utils';
|
||||
import { credentialNamespace } from '../constants';
|
||||
import { parseIpAndPort } from '../common/utils';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { ConnectToSqlDialog } from '../ui/dialogs/connectSqlDialog';
|
||||
import { ConnectToMiaaSqlDialog } from '../ui/dialogs/connectMiaaDialog';
|
||||
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||
import { ControllerModel, Registration } from './controllerModel';
|
||||
import { ResourceModel } from './resourceModel';
|
||||
@@ -23,23 +22,19 @@ export class MiaaModel extends ResourceModel {
|
||||
|
||||
private _config: azdataExt.SqlMiShowResult | undefined;
|
||||
private _databases: DatabaseModel[] = [];
|
||||
// The saved connection information
|
||||
private _connectionProfile: azdata.IConnectionProfile | undefined = undefined;
|
||||
// The ID of the active connection used to query the server
|
||||
private _activeConnectionId: string | undefined = undefined;
|
||||
|
||||
private readonly _onConfigUpdated = new vscode.EventEmitter<azdataExt.SqlMiShowResult | undefined>();
|
||||
private readonly _onDatabasesUpdated = new vscode.EventEmitter<DatabaseModel[]>();
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
public onConfigUpdated = this._onConfigUpdated.event;
|
||||
public onDatabasesUpdated = this._onDatabasesUpdated.event;
|
||||
public configLastUpdated?: Date;
|
||||
public databasesLastUpdated?: Date;
|
||||
public configLastUpdated: Date | undefined;
|
||||
public databasesLastUpdated: Date | undefined;
|
||||
|
||||
private _refreshPromise: Deferred<void> | undefined = undefined;
|
||||
|
||||
constructor(private _controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
|
||||
super(_miaaInfo, registration);
|
||||
constructor(_controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
|
||||
super(_controllerModel, _miaaInfo, registration);
|
||||
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
}
|
||||
|
||||
@@ -76,10 +71,11 @@ export class MiaaModel extends ResourceModel {
|
||||
return this._refreshPromise.promise;
|
||||
}
|
||||
this._refreshPromise = new Deferred();
|
||||
let session: azdataExt.AzdataSession | undefined = undefined;
|
||||
try {
|
||||
await this._controllerModel.azdataLogin();
|
||||
session = await this.controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
const result = await this._azdataApi.azdata.arc.sql.mi.show(this.info.name);
|
||||
const result = await this._azdataApi.azdata.arc.sql.mi.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, session);
|
||||
this._config = result.result;
|
||||
this.configLastUpdated = new Date();
|
||||
this._onConfigUpdated.fire(this._config);
|
||||
@@ -95,22 +91,16 @@ export class MiaaModel extends ResourceModel {
|
||||
|
||||
// If we have an external endpoint configured then fetch the databases now
|
||||
if (this._config.status.externalEndpoint) {
|
||||
this.getDatabases().catch(err => {
|
||||
// If an error occurs show a message so the user knows something failed but still
|
||||
// fire the event so callers can know to update (e.g. so dashboards don't show the
|
||||
// loading icon forever)
|
||||
if (err instanceof UserCancelledError) {
|
||||
vscode.window.showWarningMessage(loc.connectionRequired);
|
||||
} else {
|
||||
vscode.window.showErrorMessage(loc.fetchDatabasesFailed(this.info.name, err));
|
||||
}
|
||||
this.databasesLastUpdated = new Date();
|
||||
this.getDatabases(false).catch(_err => {
|
||||
// If an error occurs still fire the event so callers can know to
|
||||
// update (e.g. so dashboards don't show the loading icon forever)
|
||||
|
||||
this.databasesLastUpdated = undefined;
|
||||
this._onDatabasesUpdated.fire(this._databases);
|
||||
throw err;
|
||||
});
|
||||
} else {
|
||||
// Otherwise just fire the event so dashboards can update appropriately
|
||||
this.databasesLastUpdated = new Date();
|
||||
this.databasesLastUpdated = undefined;
|
||||
this._onDatabasesUpdated.fire(this._databases);
|
||||
}
|
||||
|
||||
@@ -119,52 +109,47 @@ export class MiaaModel extends ResourceModel {
|
||||
this._refreshPromise.reject(err);
|
||||
throw err;
|
||||
} finally {
|
||||
session?.dispose();
|
||||
this._refreshPromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async getDatabases(): Promise<void> {
|
||||
await this.getConnectionProfile();
|
||||
if (this._connectionProfile) {
|
||||
// We haven't connected yet so do so now and then store the ID for the active connection
|
||||
if (!this._activeConnectionId) {
|
||||
const result = await azdata.connection.connect(this._connectionProfile, false, false);
|
||||
if (!result.connected) {
|
||||
throw new Error(result.errorMessage);
|
||||
}
|
||||
this._activeConnectionId = result.connectionId;
|
||||
}
|
||||
|
||||
const provider = azdata.dataprotocol.getProvider<azdata.MetadataProvider>(this._connectionProfile.providerName, azdata.DataProviderType.MetadataProvider);
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this._activeConnectionId);
|
||||
const databases = await provider.getDatabases(ownerUri);
|
||||
if (!databases) {
|
||||
throw new Error('Could not fetch databases');
|
||||
}
|
||||
if (databases.length > 0 && typeof (databases[0]) === 'object') {
|
||||
this._databases = (<azdata.DatabaseInfo[]>databases).map(db => { return { name: db.options['name'], status: db.options['state'] }; });
|
||||
} else {
|
||||
this._databases = (<string[]>databases).map(db => { return { name: db, status: '-' }; });
|
||||
}
|
||||
this.databasesLastUpdated = new Date();
|
||||
this._onDatabasesUpdated.fire(this._databases);
|
||||
public async getDatabases(promptForConnection: boolean = true): Promise<void> {
|
||||
if (!this._connectionProfile) {
|
||||
await this.getConnectionProfile(promptForConnection);
|
||||
}
|
||||
|
||||
// We haven't connected yet so do so now and then store the ID for the active connection
|
||||
if (!this._activeConnectionId) {
|
||||
const result = await azdata.connection.connect(this._connectionProfile!, false, false);
|
||||
if (!result.connected) {
|
||||
throw new Error(result.errorMessage);
|
||||
}
|
||||
this._activeConnectionId = result.connectionId;
|
||||
}
|
||||
|
||||
const provider = azdata.dataprotocol.getProvider<azdata.MetadataProvider>(this._connectionProfile!.providerName, azdata.DataProviderType.MetadataProvider);
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this._activeConnectionId);
|
||||
const databases = await provider.getDatabases(ownerUri);
|
||||
if (!databases) {
|
||||
throw new Error('Could not fetch databases');
|
||||
}
|
||||
if (databases.length > 0 && typeof (databases[0]) === 'object') {
|
||||
this._databases = (<azdata.DatabaseInfo[]>databases).map(db => { return { name: db.options['name'], status: db.options['state'] }; });
|
||||
} else {
|
||||
this._databases = (<string[]>databases).map(db => { return { name: db, status: '-' }; });
|
||||
}
|
||||
this.databasesLastUpdated = new Date();
|
||||
this._onDatabasesUpdated.fire(this._databases);
|
||||
}
|
||||
/**
|
||||
* Loads the saved connection profile associated with this model. Will prompt for one if
|
||||
* we don't have one or can't find it (it was deleted)
|
||||
*/
|
||||
private async getConnectionProfile(): Promise<void> {
|
||||
if (this._connectionProfile) {
|
||||
return;
|
||||
}
|
||||
|
||||
protected createConnectionProfile(): azdata.IConnectionProfile {
|
||||
const ipAndPort = parseIpAndPort(this.config?.status.externalEndpoint || '');
|
||||
let connectionProfile: azdata.IConnectionProfile | undefined = {
|
||||
return {
|
||||
serverName: `${ipAndPort.ip},${ipAndPort.port}`,
|
||||
databaseName: '',
|
||||
authenticationType: 'SqlLogin',
|
||||
providerName: 'MSSQL',
|
||||
providerName: loc.miaaProviderName,
|
||||
connectionName: '',
|
||||
userName: this._miaaInfo.userName || '',
|
||||
password: '',
|
||||
@@ -175,48 +160,23 @@ export class MiaaModel extends ResourceModel {
|
||||
groupId: undefined,
|
||||
options: {}
|
||||
};
|
||||
}
|
||||
|
||||
// If we have the ID stored then try to retrieve the password from previous connections
|
||||
if (this.info.connectionId) {
|
||||
try {
|
||||
const credentialProvider = await azdata.credentials.getProvider(credentialNamespace);
|
||||
const credentials = await credentialProvider.readCredential(createCredentialId(this._controllerModel.info.id, this.info.resourceType, this.info.name));
|
||||
if (credentials.password) {
|
||||
// Try to connect to verify credentials are still valid
|
||||
connectionProfile.password = credentials.password;
|
||||
// If we don't have a username for some reason then just continue on and we'll prompt for the username below
|
||||
if (connectionProfile.userName) {
|
||||
const result = await azdata.connection.connect(connectionProfile, false, false);
|
||||
if (!result.connected) {
|
||||
vscode.window.showErrorMessage(loc.connectToSqlFailed(connectionProfile.serverName, result.errorMessage));
|
||||
const connectToSqlDialog = new ConnectToSqlDialog(this._controllerModel, this);
|
||||
connectToSqlDialog.showDialog(connectionProfile);
|
||||
connectionProfile = await connectToSqlDialog.waitForClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Unexpected error fetching password for MIAA instance ${err}`);
|
||||
// ignore - something happened fetching the password so just reprompt
|
||||
}
|
||||
}
|
||||
protected async promptForConnection(connectionProfile: azdata.IConnectionProfile): Promise<void> {
|
||||
const connectToSqlDialog = new ConnectToMiaaSqlDialog(this.controllerModel, this);
|
||||
connectToSqlDialog.showDialog(loc.connectToMSSql(this.info.name), connectionProfile);
|
||||
let profileFromDialog = await connectToSqlDialog.waitForClose();
|
||||
|
||||
if (!connectionProfile?.userName || !connectionProfile?.password) {
|
||||
// Need to prompt user for password since we don't have one stored
|
||||
const connectToSqlDialog = new ConnectToSqlDialog(this._controllerModel, this);
|
||||
connectToSqlDialog.showDialog(connectionProfile);
|
||||
connectionProfile = await connectToSqlDialog.waitForClose();
|
||||
}
|
||||
|
||||
if (connectionProfile) {
|
||||
this.updateConnectionProfile(connectionProfile);
|
||||
if (profileFromDialog) {
|
||||
this.updateConnectionProfile(profileFromDialog);
|
||||
} else {
|
||||
throw new UserCancelledError();
|
||||
}
|
||||
}
|
||||
|
||||
private async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise<void> {
|
||||
protected async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise<void> {
|
||||
this._connectionProfile = connectionProfile;
|
||||
this._activeConnectionId = connectionProfile.id;
|
||||
this.info.connectionId = connectionProfile.id;
|
||||
this._miaaInfo.userName = connectionProfile.userName;
|
||||
await this._treeDataProvider.saveControllers();
|
||||
|
||||
@@ -3,27 +3,45 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ResourceInfo } from 'arc';
|
||||
import { PGResourceInfo } from 'arc';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { ConnectToPGSqlDialog } from '../ui/dialogs/connectPGDialog';
|
||||
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||
import { ControllerModel, Registration } from './controllerModel';
|
||||
import { parseIpAndPort } from '../common/utils';
|
||||
import { UserCancelledError } from '../common/api';
|
||||
import { ResourceModel } from './resourceModel';
|
||||
import { Deferred } from '../common/promise';
|
||||
import { parseIpAndPort } from '../common/utils';
|
||||
|
||||
export type EngineSettingsModel = {
|
||||
parameterName: string | undefined,
|
||||
value: string | undefined,
|
||||
description: string | undefined,
|
||||
min: string | undefined,
|
||||
max: string | undefined,
|
||||
options: string | undefined,
|
||||
type: string | undefined
|
||||
};
|
||||
|
||||
export class PostgresModel extends ResourceModel {
|
||||
private _config?: azdataExt.PostgresServerShowResult;
|
||||
public _engineSettings: EngineSettingsModel[] = [];
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
private readonly _onConfigUpdated = new vscode.EventEmitter<azdataExt.PostgresServerShowResult>();
|
||||
public readonly _onEngineSettingsUpdated = new vscode.EventEmitter<EngineSettingsModel[]>();
|
||||
public onConfigUpdated = this._onConfigUpdated.event;
|
||||
public onEngineSettingsUpdated = this._onEngineSettingsUpdated.event;
|
||||
public configLastUpdated?: Date;
|
||||
public engineSettingsLastUpdated?: Date;
|
||||
|
||||
private _refreshPromise?: Deferred<void>;
|
||||
|
||||
constructor(private _controllerModel: ControllerModel, info: ResourceInfo, registration: Registration) {
|
||||
super(info, registration);
|
||||
constructor(_controllerModel: ControllerModel, private _pgInfo: PGResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
|
||||
super(_controllerModel, _pgInfo, registration);
|
||||
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
}
|
||||
|
||||
@@ -89,10 +107,10 @@ export class PostgresModel extends ResourceModel {
|
||||
return this._refreshPromise.promise;
|
||||
}
|
||||
this._refreshPromise = new Deferred();
|
||||
|
||||
let session: azdataExt.AzdataSession | undefined = undefined;
|
||||
try {
|
||||
await this._controllerModel.azdataLogin();
|
||||
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name)).result;
|
||||
session = await this.controllerModel.acquireAzdataSession();
|
||||
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, session)).result;
|
||||
this.configLastUpdated = new Date();
|
||||
this._onConfigUpdated.fire(this._config);
|
||||
this._refreshPromise.resolve();
|
||||
@@ -100,7 +118,100 @@ export class PostgresModel extends ResourceModel {
|
||||
this._refreshPromise.reject(err);
|
||||
throw err;
|
||||
} finally {
|
||||
session?.dispose();
|
||||
this._refreshPromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async getEngineSettings(): Promise<void> {
|
||||
if (!this._connectionProfile) {
|
||||
await this.getConnectionProfile();
|
||||
}
|
||||
|
||||
// We haven't connected yet so do so now and then store the ID for the active connection
|
||||
if (!this._activeConnectionId) {
|
||||
const result = await azdata.connection.connect(this._connectionProfile!, false, false);
|
||||
if (!result.connected) {
|
||||
throw new Error(result.errorMessage);
|
||||
}
|
||||
this._activeConnectionId = result.connectionId;
|
||||
}
|
||||
|
||||
const provider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(this._connectionProfile!.providerName, azdata.DataProviderType.QueryProvider);
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this._activeConnectionId);
|
||||
|
||||
const engineSettings = await provider.runQueryAndReturn(ownerUri, 'select name, setting, short_desc,min_val, max_val, enumvals, vartype from pg_settings');
|
||||
if (!engineSettings) {
|
||||
throw new Error('Could not fetch engine settings');
|
||||
}
|
||||
|
||||
const skippedEngineSettings: String[] = [
|
||||
'archive_command', 'archive_timeout', 'log_directory', 'log_file_mode', 'log_filename', 'restore_command',
|
||||
'shared_preload_libraries', 'synchronous_commit', 'ssl', 'unix_socket_permissions', 'wal_level'
|
||||
];
|
||||
|
||||
this._engineSettings = [];
|
||||
|
||||
engineSettings.rows.forEach(row => {
|
||||
let rowValues = row.map(c => c.displayValue);
|
||||
let name = rowValues.shift();
|
||||
if (!skippedEngineSettings.includes(name!)) {
|
||||
let result: EngineSettingsModel = {
|
||||
parameterName: name,
|
||||
value: rowValues.shift(),
|
||||
description: rowValues.shift(),
|
||||
min: rowValues.shift(),
|
||||
max: rowValues.shift(),
|
||||
options: rowValues.shift(),
|
||||
type: rowValues.shift()
|
||||
};
|
||||
|
||||
this._engineSettings.push(result);
|
||||
}
|
||||
});
|
||||
|
||||
this.engineSettingsLastUpdated = new Date();
|
||||
this._onEngineSettingsUpdated.fire(this._engineSettings);
|
||||
}
|
||||
|
||||
protected createConnectionProfile(): azdata.IConnectionProfile {
|
||||
const ipAndPort = parseIpAndPort(this.config?.status.externalEndpoint || '');
|
||||
return {
|
||||
serverName: `${ipAndPort.ip},${ipAndPort.port}`,
|
||||
databaseName: '',
|
||||
authenticationType: 'SqlLogin',
|
||||
providerName: loc.postgresProviderName,
|
||||
connectionName: '',
|
||||
userName: this._pgInfo.userName || '',
|
||||
password: '',
|
||||
savePassword: true,
|
||||
groupFullName: undefined,
|
||||
saveProfile: true,
|
||||
id: '',
|
||||
groupId: undefined,
|
||||
options: {
|
||||
host: `${ipAndPort.ip}`,
|
||||
port: `${ipAndPort.port}`,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected async promptForConnection(connectionProfile: azdata.IConnectionProfile): Promise<void> {
|
||||
const connectToSqlDialog = new ConnectToPGSqlDialog(this.controllerModel, this);
|
||||
connectToSqlDialog.showDialog(loc.connectToPGSql(this.info.name), connectionProfile);
|
||||
let profileFromDialog = await connectToSqlDialog.waitForClose();
|
||||
|
||||
if (profileFromDialog) {
|
||||
this.updateConnectionProfile(profileFromDialog);
|
||||
} else {
|
||||
throw new UserCancelledError();
|
||||
}
|
||||
}
|
||||
|
||||
protected async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise<void> {
|
||||
this._connectionProfile = connectionProfile;
|
||||
this.info.connectionId = connectionProfile.id;
|
||||
this._pgInfo.userName = connectionProfile.userName;
|
||||
await this._treeDataProvider.saveControllers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,23 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ResourceInfo } from 'arc';
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { Registration } from './controllerModel';
|
||||
import { ControllerModel, Registration } from './controllerModel';
|
||||
import { createCredentialId } from '../common/utils';
|
||||
import { credentialNamespace } from '../constants';
|
||||
|
||||
export abstract class ResourceModel {
|
||||
|
||||
private readonly _onRegistrationUpdated = new vscode.EventEmitter<Registration>();
|
||||
public onRegistrationUpdated = this._onRegistrationUpdated.event;
|
||||
|
||||
constructor(public info: ResourceInfo, private _registration: Registration) { }
|
||||
// The saved connection information
|
||||
protected _connectionProfile: azdata.IConnectionProfile | undefined = undefined;
|
||||
// The ID of the active connection used to query the server
|
||||
protected _activeConnectionId: string | undefined = undefined;
|
||||
|
||||
constructor(public readonly controllerModel: ControllerModel, public info: ResourceInfo, private _registration: Registration) { }
|
||||
|
||||
public get registration(): Registration {
|
||||
return this._registration;
|
||||
@@ -23,5 +31,57 @@ export abstract class ResourceModel {
|
||||
this._onRegistrationUpdated.fire(this._registration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the saved connection profile associated with this model. Will prompt for one if
|
||||
* we don't have one or can't find it (it was deleted)
|
||||
*/
|
||||
protected async getConnectionProfile(promptForConnection: boolean = true): Promise<void> {
|
||||
let connectionProfile: azdata.IConnectionProfile | undefined = this.createConnectionProfile();
|
||||
|
||||
// If we have the ID stored then try to retrieve the password from previous connections
|
||||
if (this.info.connectionId) {
|
||||
try {
|
||||
const credentialProvider = await azdata.credentials.getProvider(credentialNamespace);
|
||||
const credentials = await credentialProvider.readCredential(createCredentialId(this.controllerModel.info.id, this.info.resourceType, this.info.name));
|
||||
if (credentials.password) {
|
||||
// Try to connect to verify credentials are still valid
|
||||
connectionProfile.password = credentials.password;
|
||||
// If we don't have a username for some reason then just continue on and we'll prompt for the username below
|
||||
if (connectionProfile.userName) {
|
||||
const result = await azdata.connection.connect(connectionProfile, false, false);
|
||||
if (!result.connected) {
|
||||
if (promptForConnection) {
|
||||
await this.promptForConnection(connectionProfile);
|
||||
} else {
|
||||
throw new Error(result.errorMessage);
|
||||
}
|
||||
} else {
|
||||
this.updateConnectionProfile(connectionProfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Unexpected error fetching password for instance ${err}`);
|
||||
// ignore - something happened fetching the password so just reprompt
|
||||
}
|
||||
}
|
||||
|
||||
if (!connectionProfile?.userName || !connectionProfile?.password) {
|
||||
if (promptForConnection) {
|
||||
// Need to prompt user for password since we don't have one stored
|
||||
await this.promptForConnection(connectionProfile);
|
||||
} else {
|
||||
throw new Error('Missing username/password for connection profile');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public abstract refresh(): Promise<void>;
|
||||
|
||||
protected abstract createConnectionProfile(): azdata.IConnectionProfile;
|
||||
|
||||
protected abstract promptForConnection(connectionProfile: azdata.IConnectionProfile): Promise<void>;
|
||||
|
||||
protected abstract updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import * as arc from 'arc';
|
||||
import * as azdata from 'azdata';
|
||||
import * as rd from 'resource-deployment';
|
||||
import { getControllerPassword, getRegisteredDataControllers, reacquireControllerPassword } from '../common/api';
|
||||
import { CacheManager } from '../common/cacheManager';
|
||||
import { throwUnless } from '../common/utils';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||
@@ -16,11 +15,10 @@ import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||
* Class that provides options sources for an Arc Data Controller
|
||||
*/
|
||||
export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourceProvider {
|
||||
private _cacheManager = new CacheManager<string, string>();
|
||||
readonly id = 'arc.controllers';
|
||||
constructor(private _treeProvider: AzureArcTreeDataProvider) { }
|
||||
|
||||
async getOptions(): Promise<string[] | azdata.CategoryValue[]> {
|
||||
public async getOptions(): Promise<string[] | azdata.CategoryValue[]> {
|
||||
const controllers = await getRegisteredDataControllers(this._treeProvider);
|
||||
throwUnless(controllers !== undefined && controllers.length !== 0, loc.noControllersConnected);
|
||||
return controllers.map(ci => {
|
||||
@@ -28,24 +26,19 @@ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourcePro
|
||||
});
|
||||
}
|
||||
|
||||
private async retrieveVariable(key: string): Promise<string> {
|
||||
const [variableName, controllerLabel] = JSON.parse(key);
|
||||
public async getVariableValue(variableName: string, controllerLabel: string): Promise<string> {
|
||||
const controller = (await getRegisteredDataControllers(this._treeProvider)).find(ci => ci.label === controllerLabel);
|
||||
throwUnless(controller !== undefined, loc.noControllerInfoFound(controllerLabel));
|
||||
switch (variableName) {
|
||||
case 'endpoint': return controller.info.url;
|
||||
case 'username': return controller.info.username;
|
||||
case 'kubeConfig': return controller.info.kubeConfigFilePath;
|
||||
case 'clusterContext': return controller.info.kubeClusterContext;
|
||||
case 'password': return this.getPassword(controller);
|
||||
default: throw new Error(loc.variableValueFetchForUnsupportedVariable(variableName));
|
||||
}
|
||||
}
|
||||
|
||||
getVariableValue(variableName: string, controllerLabel: string): Promise<string> {
|
||||
// capture 'this' in an arrow function object
|
||||
const retrieveVariable = (key: string) => this.retrieveVariable(key);
|
||||
return this._cacheManager.getCacheEntry(JSON.stringify([variableName, controllerLabel]), retrieveVariable);
|
||||
}
|
||||
|
||||
private async getPassword(controller: arc.DataController): Promise<string> {
|
||||
let password = await getControllerPassword(this._treeProvider, controller.info);
|
||||
if (!password) {
|
||||
@@ -55,10 +48,12 @@ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourcePro
|
||||
return password;
|
||||
}
|
||||
|
||||
getIsPassword(variableName: string): boolean {
|
||||
public getIsPassword(variableName: string): boolean {
|
||||
switch (variableName) {
|
||||
case 'endpoint': return false;
|
||||
case 'username': return false;
|
||||
case 'kubeConfig': return false;
|
||||
case 'clusterContext': return false;
|
||||
case 'password': return true;
|
||||
default: throw new Error(loc.isPasswordFetchForUnsupportedVariable(variableName));
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('KubeUtils', function (): void {
|
||||
});
|
||||
it('throws error when unable to load config file', async () => {
|
||||
const error = new Error('unknown error accessing file');
|
||||
sinon.stub(yamljs, 'load').throws(error); //erroring config file load
|
||||
sinon.stub(yamljs, 'load').throws(error); // simulate an error thrown from config file load
|
||||
((await tryExecuteAction(() => getKubeConfigClusterContexts(configFile))).error).should.equal(error, `test: getKubeConfigClusterContexts failed`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Deferred } from '../../common/promise';
|
||||
|
||||
describe('Deferred', () => {
|
||||
it('Then should be called upon resolution', function (done): void {
|
||||
const deferred = new Deferred();
|
||||
const deferred = new Deferred<void>();
|
||||
deferred.then(() => {
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -50,7 +50,8 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
|
||||
workers?: number
|
||||
},
|
||||
_engineVersion?: string,
|
||||
_additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
|
||||
_additionalEnvVars?: azdataExt.AdditionalEnvVars
|
||||
): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
|
||||
}
|
||||
},
|
||||
sql: {
|
||||
@@ -74,9 +75,12 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
|
||||
getPath(): Promise<string> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
login(_endpoint: string, _username: string, _password: string): Promise<azdataExt.AzdataOutput<any>> {
|
||||
login(_endpoint: string, _username: string, _password: string): Promise<azdataExt.AzdataOutput<void>> {
|
||||
return <any>undefined;
|
||||
}
|
||||
acquireSession(_endpoint: string, _username: string, _password: string): Promise<azdataExt.AzdataSession> {
|
||||
return Promise.resolve({ dispose: () => { } });
|
||||
}
|
||||
version(): Promise<azdataExt.AzdataOutput<string>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider
|
||||
export class FakeControllerModel extends ControllerModel {
|
||||
|
||||
constructor(treeDataProvider?: AzureArcTreeDataProvider, info?: Partial<ControllerInfo>, password?: string) {
|
||||
const _info: ControllerInfo = Object.assign({ id: uuid(), url: '', name: '', username: '', rememberPassword: false, resources: [] }, info);
|
||||
const _info: ControllerInfo = Object.assign({ id: uuid(), url: '', kubeConfigFilePath: '', kubeClusterContext: '', name: '', username: '', rememberPassword: false, resources: [] }, info);
|
||||
super(treeDataProvider!, _info, password);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
export class FakeRadioButton implements azdata.RadioButtonComponent {
|
||||
|
||||
private _onDidClickEmitter = new vscode.EventEmitter<any>();
|
||||
|
||||
onDidClick = this._onDidClickEmitter.event;
|
||||
|
||||
constructor(props: azdata.RadioButtonProperties) {
|
||||
this.label = props.label;
|
||||
this.value = props.value;
|
||||
this.checked = props.checked;
|
||||
this.enabled = props.enabled;
|
||||
}
|
||||
|
||||
//#region RadioButtonProperties implementation
|
||||
label?: string;
|
||||
value?: string;
|
||||
checked?: boolean;
|
||||
//#endregion
|
||||
|
||||
click() {
|
||||
this.checked = true;
|
||||
this._onDidClickEmitter.fire(this);
|
||||
}
|
||||
//#region Component Implementation
|
||||
id: string = '';
|
||||
updateProperties(_properties: { [key: string]: any; }): Thenable<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
updateProperty(_key: string, _value: any): Thenable<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
updateCssStyles(_cssStyles: { [key: string]: string; }): Thenable<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
onValidityChanged: vscode.Event<boolean> = <vscode.Event<boolean>>{};
|
||||
valid: boolean = false;
|
||||
validate(): Thenable<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
focus(): Thenable<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
ariaHidden?: boolean | undefined;
|
||||
//#endregion
|
||||
|
||||
//#region ComponentProperties Implementation
|
||||
height?: number | string;
|
||||
width?: number | string;
|
||||
/**
|
||||
* The position CSS property. Empty by default.
|
||||
* This is particularly useful if laying out components inside a FlexContainer and
|
||||
* the size of the component is meant to be a fixed size. In this case the position must be
|
||||
* set to 'absolute', with the parent FlexContainer having 'relative' position.
|
||||
* Without this the component will fail to correctly size itself
|
||||
*/
|
||||
position?: azdata.PositionType;
|
||||
/**
|
||||
* Whether the component is enabled in the DOM
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Corresponds to the display CSS property for the element
|
||||
*/
|
||||
display?: azdata.DisplayType;
|
||||
/**
|
||||
* Corresponds to the aria-label accessibility attribute for this component
|
||||
*/
|
||||
ariaLabel?: string;
|
||||
/**
|
||||
* Corresponds to the role accessibility attribute for this component
|
||||
*/
|
||||
ariaRole?: string;
|
||||
/**
|
||||
* Corresponds to the aria-selected accessibility attribute for this component
|
||||
*/
|
||||
ariaSelected?: boolean;
|
||||
/**
|
||||
* Matches the CSS style key and its available values.
|
||||
*/
|
||||
CSSStyles?: { [key: string]: string };
|
||||
//#endregion
|
||||
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import * as TypeMoq from 'typemoq';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../../localizedConstants';
|
||||
import * as kubeUtils from '../../common/kubeUtils';
|
||||
import { UserCancelledError } from '../../common/api';
|
||||
import { ControllerModel } from '../../models/controllerModel';
|
||||
import { ConnectToControllerDialog } from '../../ui/dialogs/connectControllerDialog';
|
||||
@@ -34,13 +35,15 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
beforeEach(function (): void {
|
||||
sinon.stub(ConnectToControllerDialog.prototype, 'showDialog');
|
||||
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').resolves([{ name: 'currentCluster', isCurrentContext: true }]);
|
||||
sinon.stub(vscode.window, 'showErrorMessage').resolves(<any>loc.yes);
|
||||
});
|
||||
|
||||
it('Rejected with expected error when user cancels', async function (): Promise<void> {
|
||||
// Returning an undefined model here indicates that the dialog closed without clicking "Ok" - usually through the user clicking "Cancel"
|
||||
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(undefined));
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||
await should(model.azdataLogin()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||
await should(model.acquireAzdataSession()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
|
||||
});
|
||||
|
||||
it('Reads password from cred store', async function (): Promise<void> {
|
||||
@@ -55,13 +58,13 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
|
||||
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||
|
||||
await model.azdataLogin();
|
||||
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
|
||||
await model.acquireAzdataSession();
|
||||
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
it('Prompt for password when not in cred store', async function (): Promise<void> {
|
||||
@@ -76,18 +79,18 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
|
||||
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
||||
|
||||
// Set up dialog to return new model with our password
|
||||
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
||||
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
||||
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
|
||||
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||
|
||||
await model.azdataLogin();
|
||||
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
|
||||
await model.acquireAzdataSession();
|
||||
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
it('Prompt for password when rememberPassword is true but prompt reconnect is true', async function (): Promise<void> {
|
||||
@@ -101,19 +104,19 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
|
||||
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
||||
|
||||
// Set up dialog to return new model with our new password from the reprompt
|
||||
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
||||
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
||||
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
|
||||
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||
|
||||
await model.azdataLogin(true);
|
||||
await model.acquireAzdataSession(true);
|
||||
should(waitForCloseStub.called).be.true('waitForClose should have been called');
|
||||
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
|
||||
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
it('Prompt for password when we already have a password but prompt reconnect is true', async function (): Promise<void> {
|
||||
@@ -127,20 +130,20 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
|
||||
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
||||
|
||||
// Set up dialog to return new model with our new password from the reprompt
|
||||
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
||||
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
||||
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
|
||||
|
||||
// Set up original model with a password
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 'originalPassword');
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 'originalPassword');
|
||||
|
||||
await model.azdataLogin(true);
|
||||
await model.acquireAzdataSession(true);
|
||||
should(waitForCloseStub.called).be.true('waitForClose should have been called');
|
||||
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
|
||||
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
it('Model values are updated correctly when modified during reconnect', async function (): Promise<void> {
|
||||
@@ -155,7 +158,7 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
|
||||
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
||||
|
||||
@@ -166,6 +169,8 @@ describe('ControllerModel', function (): void {
|
||||
{
|
||||
id: uuid(),
|
||||
url: '127.0.0.1',
|
||||
kubeConfigFilePath: '/path/to/.kube/config',
|
||||
kubeClusterContext: 'currentCluster',
|
||||
username: 'admin',
|
||||
name: 'arc',
|
||||
rememberPassword: false,
|
||||
@@ -178,6 +183,8 @@ describe('ControllerModel', function (): void {
|
||||
const newInfo: ControllerInfo = {
|
||||
id: model.info.id, // The ID stays the same since we're just re-entering information for the same model
|
||||
url: 'newUrl',
|
||||
kubeConfigFilePath: '/path/to/.kube/config',
|
||||
kubeClusterContext: 'currentCluster',
|
||||
username: 'newUser',
|
||||
name: 'newName',
|
||||
rememberPassword: true,
|
||||
@@ -192,10 +199,11 @@ describe('ControllerModel', function (): void {
|
||||
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(
|
||||
{ controllerModel: newModel, password: newPassword }));
|
||||
|
||||
await model.azdataLogin(true);
|
||||
await model.acquireAzdataSession(true);
|
||||
should(waitForCloseStub.called).be.true('waitForClose should have been called');
|
||||
should((await treeDataProvider.getChildren()).length).equal(1, 'Tree Data provider should still only have 1 node');
|
||||
should(model.info).deepEqual(newInfo, 'Model info should have been updated');
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -3,66 +3,8 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export function createModelViewMock() {
|
||||
const mockModelBuilder = TypeMoq.Mock.ofType<azdata.ModelBuilder>();
|
||||
const mockTextBuilder = setupMockComponentBuilder<azdata.TextComponent, azdata.TextComponentProperties>();
|
||||
const mockInputBoxBuilder = setupMockComponentBuilder<azdata.InputBoxComponent, azdata.InputBoxProperties>();
|
||||
const mockRadioButtonBuilder = setupMockComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>();
|
||||
const mockDivBuilder = setupMockContainerBuilder<azdata.DivContainer, azdata.DivContainerProperties, azdata.DivBuilder>();
|
||||
const mockLoadingBuilder = setupMockLoadingBuilder();
|
||||
mockModelBuilder.setup(b => b.loadingComponent()).returns(() => mockLoadingBuilder.object);
|
||||
mockModelBuilder.setup(b => b.text()).returns(() => mockTextBuilder.object);
|
||||
mockModelBuilder.setup(b => b.inputBox()).returns(() => mockInputBoxBuilder.object);
|
||||
mockModelBuilder.setup(b => b.radioButton()).returns(() => mockRadioButtonBuilder.object);
|
||||
mockModelBuilder.setup(b => b.divContainer()).returns(() => mockDivBuilder.object);
|
||||
const mockModelView = TypeMoq.Mock.ofType<azdata.ModelView>();
|
||||
mockModelView.setup(mv => mv.modelBuilder).returns(() => mockModelBuilder.object);
|
||||
return { mockModelView, mockModelBuilder, mockTextBuilder, mockInputBoxBuilder, mockRadioButtonBuilder, mockDivBuilder };
|
||||
}
|
||||
|
||||
function setupMockLoadingBuilder(
|
||||
loadingBuilderGetter?: (item: azdata.Component) => azdata.LoadingComponentBuilder,
|
||||
mockLoadingBuilder?: TypeMoq.IMock<azdata.LoadingComponentBuilder>
|
||||
): TypeMoq.IMock<azdata.LoadingComponentBuilder> {
|
||||
mockLoadingBuilder = mockLoadingBuilder ?? setupMockComponentBuilder<azdata.LoadingComponent, azdata.LoadingComponentProperties, azdata.LoadingComponentBuilder>();
|
||||
let item: azdata.Component;
|
||||
mockLoadingBuilder.setup(b => b.withItem(TypeMoq.It.isAny())).callback((_item) => item = _item).returns(() => loadingBuilderGetter ? loadingBuilderGetter(item) : mockLoadingBuilder!.object);
|
||||
return mockLoadingBuilder;
|
||||
}
|
||||
|
||||
export function setupMockComponentBuilder<T extends azdata.Component, P extends azdata.ComponentProperties, B extends azdata.ComponentBuilder<T, P> = azdata.ComponentBuilder<T, P>>(
|
||||
componentGetter?: (props: P) => T,
|
||||
mockComponentBuilder?: TypeMoq.IMock<B>,
|
||||
): TypeMoq.IMock<B> {
|
||||
mockComponentBuilder = mockComponentBuilder ?? TypeMoq.Mock.ofType<B>();
|
||||
const returnComponent = TypeMoq.Mock.ofType<T>();
|
||||
// Need to setup 'then' for when a mocked object is resolved otherwise the test will hang : https://github.com/florinn/typemoq/issues/66
|
||||
returnComponent.setup((x: any) => x.then).returns(() => { });
|
||||
let compProps: P;
|
||||
mockComponentBuilder.setup(b => b.withProperties(TypeMoq.It.isAny())).callback((props: P) => compProps = props).returns(() => mockComponentBuilder!.object);
|
||||
mockComponentBuilder.setup(b => b.component()).returns(() => {
|
||||
return componentGetter ? componentGetter(compProps) : Object.assign<T, P>(Object.assign({}, returnComponent.object), compProps);
|
||||
});
|
||||
|
||||
// For now just have these be passthrough - can hook up additional functionality later if needed
|
||||
mockComponentBuilder.setup(b => b.withValidation(TypeMoq.It.isAny())).returns(() => mockComponentBuilder!.object);
|
||||
return mockComponentBuilder;
|
||||
}
|
||||
|
||||
export function setupMockContainerBuilder<T extends azdata.Container<any, any>, P extends azdata.ComponentProperties, B extends azdata.ContainerBuilder<T, any, any, any> = azdata.ContainerBuilder<T, any, any, any>>(
|
||||
mockContainerBuilder?: TypeMoq.IMock<B>
|
||||
): TypeMoq.IMock<B> {
|
||||
mockContainerBuilder = mockContainerBuilder ?? setupMockComponentBuilder<T, P, B>();
|
||||
// For now just have these be passthrough - can hook up additional functionality later if needed
|
||||
mockContainerBuilder.setup(b => b.withItems(TypeMoq.It.isAny(), undefined)).returns(() => mockContainerBuilder!.object);
|
||||
mockContainerBuilder.setup(b => b.withLayout(TypeMoq.It.isAny())).returns(() => mockContainerBuilder!.object);
|
||||
return mockContainerBuilder;
|
||||
}
|
||||
|
||||
export class MockInputBox implements vscode.InputBox {
|
||||
private _value: string = '';
|
||||
public get value(): string {
|
||||
@@ -77,17 +19,17 @@ export class MockInputBox implements vscode.InputBox {
|
||||
placeholder: string | undefined;
|
||||
password: boolean = false;
|
||||
private _onDidChangeValueCallback: ((e: string) => any) | undefined = undefined;
|
||||
onDidChangeValue: vscode.Event<string> = (listener) => {
|
||||
onDidChangeValue: vscode.Event<string> = (listener: (value: string) => void) => {
|
||||
this._onDidChangeValueCallback = listener;
|
||||
return new vscode.Disposable(() => { });
|
||||
};
|
||||
private _onDidAcceptCallback: ((e: void) => any) | undefined = undefined;
|
||||
public onDidAccept: vscode.Event<void> = (listener) => {
|
||||
public onDidAccept: vscode.Event<void> = (listener: () => void) => {
|
||||
this._onDidAcceptCallback = listener;
|
||||
return new vscode.Disposable(() => { });
|
||||
};
|
||||
buttons: readonly vscode.QuickInputButton[] = [];
|
||||
onDidTriggerButton: vscode.Event<vscode.QuickInputButton> = (_) => { return new vscode.Disposable(() => { }); };
|
||||
onDidTriggerButton: vscode.Event<vscode.QuickInputButton> = () => { return new vscode.Disposable(() => { }); };
|
||||
prompt: string | undefined;
|
||||
validationMessage: string | undefined;
|
||||
title: string | undefined;
|
||||
@@ -104,7 +46,7 @@ export class MockInputBox implements vscode.InputBox {
|
||||
}
|
||||
}
|
||||
private _onDidHideCallback: ((e: void) => any) | undefined = undefined;
|
||||
onDidHide: vscode.Event<void> = (listener) => {
|
||||
onDidHide: vscode.Event<void> = (listener: () => void) => {
|
||||
this._onDidHideCallback = listener;
|
||||
return new vscode.Disposable(() => { });
|
||||
};
|
||||
|
||||
50
extensions/arc/src/test/ui/components/filePicker.test.ts
Normal file
50
extensions/arc/src/test/ui/components/filePicker.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 should from 'should';
|
||||
import * as sinon from 'sinon';
|
||||
import * as vscode from 'vscode';
|
||||
import { Deferred } from '../../../common/promise';
|
||||
import { FilePicker } from '../../../ui/components/filePicker';
|
||||
import { createModelViewMock } from 'azdata-test/out/mocks/modelView/modelViewMock';
|
||||
import { StubButton } from 'azdata-test/out/stubs/modelView/stubButton';
|
||||
|
||||
let filePicker: FilePicker;
|
||||
const initialPath = path.join('path', 'to', '.kube','config');
|
||||
const newFileUri = vscode.Uri.file(path.join('path', 'to', 'new', '.kube', 'config'));
|
||||
describe('filePicker', function (): void {
|
||||
beforeEach(async () => {
|
||||
const { modelBuilderMock } = createModelViewMock();
|
||||
filePicker = new FilePicker(modelBuilderMock.object, initialPath, (_disposable) => { });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('browse Button chooses new FilePath', async () => {
|
||||
should(filePicker.filePathInputBox.value).should.not.be.undefined();
|
||||
filePicker.value!.should.equal(initialPath);
|
||||
filePicker.component().items.length.should.equal(2, 'Filepicker container should have two components');
|
||||
const deferred = new Deferred<void>();
|
||||
sinon.stub(vscode.window, 'showOpenDialog').callsFake(async (_options) => {
|
||||
deferred.resolve();
|
||||
return [newFileUri];
|
||||
});
|
||||
(filePicker.filePickerButton as StubButton).click();
|
||||
await deferred;
|
||||
filePicker.value!.should.equal(newFileUri.fsPath);
|
||||
});
|
||||
|
||||
describe('getters and setters', async () => {
|
||||
it('component getter', () => {
|
||||
should(filePicker.component()).not.be.undefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -7,41 +7,25 @@ import * as azdata from 'azdata';
|
||||
import * as should from 'should';
|
||||
import { getErrorMessage } from '../../../common/utils';
|
||||
import { RadioOptionsGroup, RadioOptionsInfo } from '../../../ui/components/radioOptionsGroup';
|
||||
import { FakeRadioButton } from '../../mocks/fakeRadioButton';
|
||||
import { setupMockComponentBuilder, createModelViewMock } from '../../stubs';
|
||||
import { createModelViewMock } from 'azdata-test/out/mocks/modelView/modelViewMock';
|
||||
import { StubRadioButton } from 'azdata-test/out/stubs/modelView/stubRadioButton';
|
||||
|
||||
|
||||
const loadingError = new Error('Error loading options');
|
||||
const radioOptionsInfo = <RadioOptionsInfo>{
|
||||
const radioOptionsInfo: RadioOptionsInfo = {
|
||||
values: [
|
||||
'value1',
|
||||
'value2'
|
||||
],
|
||||
defaultValue: 'value2'
|
||||
};
|
||||
const divItems: azdata.Component[] = [];
|
||||
let radioOptionsGroup: RadioOptionsGroup;
|
||||
|
||||
let radioOptionsGroup: RadioOptionsGroup;
|
||||
|
||||
describe('radioOptionsGroup', function (): void {
|
||||
beforeEach(async () => {
|
||||
const { mockModelView, mockRadioButtonBuilder, mockDivBuilder } = createModelViewMock();
|
||||
mockRadioButtonBuilder.reset(); // reset any previous mock so that we can set our own.
|
||||
setupMockComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>(
|
||||
(props) => new FakeRadioButton(props),
|
||||
mockRadioButtonBuilder,
|
||||
);
|
||||
mockDivBuilder.reset(); // reset previous setups so new setups we are about to create will replace the setups instead creating a recording chain
|
||||
// create new setups for the DivContainer with custom behavior
|
||||
setupMockComponentBuilder<azdata.DivContainer, azdata.DivContainerProperties, azdata.DivBuilder>(
|
||||
() => <azdata.DivContainer>{
|
||||
addItem: (item) => { divItems.push(item); },
|
||||
clearItems: () => { divItems.length = 0; },
|
||||
get items() { return divItems; },
|
||||
},
|
||||
mockDivBuilder
|
||||
);
|
||||
radioOptionsGroup = new RadioOptionsGroup(mockModelView.object, (_disposable) => { });
|
||||
const { modelBuilderMock } = createModelViewMock();
|
||||
radioOptionsGroup = new RadioOptionsGroup(modelBuilderMock.object, (_disposable) => { });
|
||||
await radioOptionsGroup.load(async () => radioOptionsInfo);
|
||||
});
|
||||
|
||||
@@ -55,34 +39,40 @@ describe('radioOptionsGroup', function (): void {
|
||||
|
||||
it('onClick', async () => {
|
||||
// click the radioButton corresponding to 'value1'
|
||||
(divItems as FakeRadioButton[]).filter(r => r.value === 'value1').pop()!.click();
|
||||
((radioOptionsGroup.items as azdata.RadioButtonComponent[]).find(r => r.value === 'value1') as StubRadioButton).click();
|
||||
radioOptionsGroup.value!.should.equal('value1', 'radio options group should correspond to the radioButton that we clicked');
|
||||
// verify all the radioButtons created in the group
|
||||
verifyRadioGroup();
|
||||
});
|
||||
|
||||
it('load throws', async () => {
|
||||
radioOptionsGroup.load(() => { throw loadingError; });
|
||||
await radioOptionsGroup.load(() => { throw loadingError; });
|
||||
//in error case radioButtons array wont hold radioButtons but holds a TextComponent with value equal to error string
|
||||
divItems.length.should.equal(1, 'There is should be only one element in the divContainer when loading error happens');
|
||||
const label = divItems[0] as azdata.TextComponent;
|
||||
radioOptionsGroup.items.length.should.equal(1, 'There is should be only one element in the divContainer when loading error happens');
|
||||
const label = radioOptionsGroup.items[0] as azdata.TextComponent;
|
||||
should(label.value).not.be.undefined();
|
||||
label.value!.should.deepEqual(getErrorMessage(loadingError));
|
||||
should(label.CSSStyles).not.be.undefined();
|
||||
should(label.CSSStyles!.color).not.be.undefined();
|
||||
label.CSSStyles!.color.should.equal('Red');
|
||||
});
|
||||
|
||||
describe('getters and setters', async () => {
|
||||
it(`component getter`, () => {
|
||||
should(radioOptionsGroup.component()).not.be.undefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function verifyRadioGroup() {
|
||||
const radioButtons = divItems as FakeRadioButton[];
|
||||
radioButtons.length.should.equal(radioOptionsInfo.values!.length);
|
||||
const radioButtons = radioOptionsGroup.items as azdata.RadioButtonComponent[];
|
||||
radioButtons.length.should.equal(radioOptionsInfo.values!.length, 'Unexpected number of radio buttons');
|
||||
radioButtons.forEach(rb => {
|
||||
should(rb.label).not.be.undefined();
|
||||
should(rb.value).not.be.undefined();
|
||||
should(rb.enabled).not.be.undefined();
|
||||
rb.label!.should.equal(rb.value);
|
||||
rb.enabled!.should.be.true();
|
||||
should(rb.label).not.equal(undefined, 'Radio Button label should not be undefined');
|
||||
should(rb.value).not.equal(undefined, 'Radio button value should not be undefined');
|
||||
should(rb.enabled).not.equal(undefined, 'Enabled should not be undefined');
|
||||
rb.label!.should.equal(rb.value, 'Radio button label did not match');
|
||||
rb.enabled!.should.be.true('Radio button should be enabled');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('ConnectControllerDialog', function (): void {
|
||||
it('validate returns false if controller refresh fails', async function (): Promise<void> {
|
||||
sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.reject('Controller refresh failed'));
|
||||
const connectControllerDialog = new ConnectToControllerDialog(undefined!);
|
||||
const info = { id: uuid(), url: 'https://127.0.0.1:30080', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
|
||||
const info = { id: uuid(), url: 'https://127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
|
||||
connectControllerDialog.showDialog(info, 'pwd');
|
||||
await connectControllerDialog.isInitialized;
|
||||
const validateResult = await connectControllerDialog.validate();
|
||||
@@ -41,36 +41,36 @@ describe('ConnectControllerDialog', function (): void {
|
||||
|
||||
it('validate replaces http with https', async function (): Promise<void> {
|
||||
await validateConnectControllerDialog(
|
||||
{ id: uuid(), url: 'http://127.0.0.1:30081', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||
'https://127.0.0.1:30081');
|
||||
});
|
||||
|
||||
it('validate appends https if missing', async function (): Promise<void> {
|
||||
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1:30080', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||
'https://127.0.0.1:30080');
|
||||
});
|
||||
|
||||
it('validate appends default port if missing', async function (): Promise<void> {
|
||||
await validateConnectControllerDialog({ id: uuid(), url: 'https://127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||
await validateConnectControllerDialog({ id: uuid(), url: 'https://127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||
'https://127.0.0.1:30080');
|
||||
});
|
||||
|
||||
it('validate appends both port and https if missing', async function (): Promise<void> {
|
||||
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||
'https://127.0.0.1:30080');
|
||||
});
|
||||
|
||||
for (const name of ['', undefined]) {
|
||||
it.skip(`validate display name gets set to arc instance name for user chosen name of:${name}`, async function (): Promise<void> {
|
||||
await validateConnectControllerDialog(
|
||||
{ id: uuid(), url: 'http://127.0.0.1:30081', name: name!, username: 'sa', rememberPassword: true, resources: [] },
|
||||
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: name!, username: 'sa', rememberPassword: true, resources: [] },
|
||||
'https://127.0.0.1:30081');
|
||||
});
|
||||
}
|
||||
|
||||
it.skip(`validate display name gets set to default data controller name for user chosen name of:'' and instanceName in explicably returned as undefined from the controller endpoint`, async function (): Promise<void> {
|
||||
await validateConnectControllerDialog(
|
||||
{ id: uuid(), url: 'http://127.0.0.1:30081', name: '', username: 'sa', rememberPassword: true, resources: [] },
|
||||
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: '', username: 'sa', rememberPassword: true, resources: [] },
|
||||
'https://127.0.0.1:30081',
|
||||
undefined);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as sinon from 'sinon';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as kubeUtils from '../../../common/kubeUtils';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { MiaaModel } from '../../../models/miaaModel';
|
||||
import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
|
||||
@@ -53,7 +54,7 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
treeDataProvider['_loading'] = false;
|
||||
let children = await treeDataProvider.getChildren();
|
||||
should(children.length).equal(0, 'There initially shouldn\'t be any children');
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
should(children.length).equal(1, 'Controller node should be added correctly');
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
@@ -64,12 +65,12 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
treeDataProvider['_loading'] = false;
|
||||
let children = await treeDataProvider.getChildren();
|
||||
should(children.length).equal(0, 'There initially shouldn\'t be any children');
|
||||
const originalInfo: ControllerInfo = { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
|
||||
const originalInfo: ControllerInfo = { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
|
||||
const controllerModel = new ControllerModel(treeDataProvider, originalInfo);
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
should(children.length).equal(1, 'Controller node should be added correctly');
|
||||
should((<ControllerTreeNode>children[0]).model.info).deepEqual(originalInfo);
|
||||
const newInfo = { id: originalInfo.id, url: '1.1.1.1', name: 'new-name', username: 'admin', rememberPassword: false, resources: [] };
|
||||
const newInfo = { id: originalInfo.id, url: '1.1.1.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'new-name', username: 'admin', rememberPassword: false, resources: [] };
|
||||
const controllerModel2 = new ControllerModel(treeDataProvider, newInfo);
|
||||
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
||||
should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
|
||||
@@ -102,21 +103,22 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi);
|
||||
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object);
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'mypassword');
|
||||
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').resolves([{ name: 'currentCluster', isCurrentContext: true }]);
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'mypassword');
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
const controllerNode = treeDataProvider.getControllerNode(controllerModel);
|
||||
const children = await treeDataProvider.getChildren(controllerNode);
|
||||
should(children.filter(c => c.label === fakeAzdataApi.postgresInstances[0].name).length).equal(1, 'Should have a Postgres child');
|
||||
should(children.filter(c => c.label === fakeAzdataApi.miaaInstances[0].name).length).equal(1, 'Should have a MIAA child');
|
||||
should(children.length).equal(2, 'Should have excatly 2 children');
|
||||
should(children.length).equal(2, 'Should have exactly 2 children');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeController', function (): void {
|
||||
it('removing a controller should work as expected', async function (): Promise<void> {
|
||||
treeDataProvider['_loading'] = false;
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
const controllerModel2 = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.2', name: 'my-arc', username: 'cloudsa', rememberPassword: true, resources: [] });
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
const controllerModel2 = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.2', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'cloudsa', rememberPassword: true, resources: [] });
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
||||
const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren());
|
||||
@@ -133,20 +135,20 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
|
||||
describe('openResourceDashboard', function (): void {
|
||||
it('Opening dashboard for nonexistent controller node throws', async function (): Promise<void> {
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
|
||||
await should(openDashboardPromise).be.rejected();
|
||||
});
|
||||
|
||||
it('Opening dashboard for nonexistent resource throws', async function (): Promise<void> {
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
|
||||
await should(openDashboardPromise).be.rejected();
|
||||
});
|
||||
|
||||
it('Opening dashboard for existing resource node succeeds', async function (): Promise<void> {
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||
const miaaModel = new MiaaModel(controllerModel, { name: 'miaa-1', resourceType: ResourceType.sqlManagedInstances }, undefined!, treeDataProvider);
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
const controllerNode = treeDataProvider.getControllerNode(controllerModel)!;
|
||||
|
||||
6
extensions/arc/src/typings/arc.d.ts
vendored
6
extensions/arc/src/typings/arc.d.ts
vendored
@@ -23,6 +23,10 @@ declare module 'arc' {
|
||||
userName?: string
|
||||
};
|
||||
|
||||
export type PGResourceInfo = ResourceInfo & {
|
||||
userName?: string
|
||||
};
|
||||
|
||||
export type ResourceInfo = {
|
||||
name: string,
|
||||
resourceType: ResourceType | string,
|
||||
@@ -31,6 +35,8 @@ declare module 'arc' {
|
||||
|
||||
export type ControllerInfo = {
|
||||
id: string,
|
||||
kubeConfigFilePath: string,
|
||||
kubeClusterContext: string
|
||||
url: string,
|
||||
name: string,
|
||||
username: string,
|
||||
|
||||
@@ -24,5 +24,5 @@ export abstract class Dashboard {
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
protected abstract async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]>;
|
||||
protected abstract registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]>;
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ export abstract class DashboardPage extends InitializingComponent {
|
||||
title: this.title,
|
||||
id: this.id,
|
||||
icon: this.icon,
|
||||
content: this.container,
|
||||
toolbar: this.toolbarContainer
|
||||
// Get toolbar first since things in the container might depend on these being created
|
||||
toolbar: this.toolbarContainer,
|
||||
content: this.container
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
77
extensions/arc/src/ui/components/filePicker.ts
Normal file
77
extensions/arc/src/ui/components/filePicker.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../../localizedConstants';
|
||||
|
||||
export interface RadioOptionsInfo {
|
||||
values?: string[],
|
||||
defaultValue: string
|
||||
}
|
||||
|
||||
export class FilePicker {
|
||||
private _flexContainer: azdata.FlexContainer;
|
||||
public readonly filePathInputBox: azdata.InputBoxComponent;
|
||||
public readonly filePickerButton: azdata.ButtonComponent;
|
||||
constructor(
|
||||
modelBuilder: azdata.ModelBuilder,
|
||||
initialPath: string, onNewDisposableCreated: (disposable: vscode.Disposable) => void
|
||||
) {
|
||||
const buttonWidth = 80;
|
||||
this.filePathInputBox = modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
value: initialPath,
|
||||
width: 350
|
||||
}).component();
|
||||
|
||||
this.filePickerButton = modelBuilder.button()
|
||||
.withProperties<azdata.ButtonProperties>({
|
||||
label: loc.browse,
|
||||
width: buttonWidth
|
||||
}).component();
|
||||
onNewDisposableCreated(this.filePickerButton.onDidClick(async () => {
|
||||
const fileUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
defaultUri: this.filePathInputBox.value ? vscode.Uri.file(path.dirname(this.filePathInputBox.value)) : undefined,
|
||||
openLabel: loc.select,
|
||||
filters: undefined /* file type filters */
|
||||
});
|
||||
|
||||
if (!fileUris || fileUris.length === 0) {
|
||||
return; // This can happen when a user cancels out. We don't throw and the user just won't be able to move on until they select something.
|
||||
}
|
||||
const fileUri = fileUris[0]; //we allow the user to select only one file in the dialog
|
||||
this.filePathInputBox.value = fileUri.fsPath;
|
||||
}));
|
||||
this._flexContainer = createFlexContainer(modelBuilder, [this.filePathInputBox, this.filePickerButton]);
|
||||
}
|
||||
|
||||
component(): azdata.FlexContainer {
|
||||
return this._flexContainer;
|
||||
}
|
||||
|
||||
get onTextChanged() {
|
||||
return this.filePathInputBox.onTextChanged;
|
||||
}
|
||||
|
||||
get value(): string | undefined {
|
||||
return this.filePathInputBox?.value;
|
||||
}
|
||||
|
||||
get items(): azdata.Component[] {
|
||||
return this._flexContainer.items;
|
||||
}
|
||||
}
|
||||
|
||||
function createFlexContainer(modelBuilder: azdata.ModelBuilder, items: azdata.Component[], rowLayout: boolean = true, width?: string | number, height?: string | number, alignItems?: azdata.AlignItemsType, cssStyles?: { [key: string]: string }): azdata.FlexContainer {
|
||||
const flexFlow = rowLayout ? 'row' : 'column';
|
||||
alignItems = alignItems || (rowLayout ? 'center' : undefined);
|
||||
const itemsStyle = rowLayout ? { CSSStyles: { 'margin-right': '5px', } } : {};
|
||||
const flexLayout: azdata.FlexLayout = { flexFlow: flexFlow, height: height, width: width, alignItems: alignItems };
|
||||
return modelBuilder.flexContainer().withItems(items, itemsStyle).withLayout(flexLayout).withProperties<azdata.ComponentProperties>({ CSSStyles: cssStyles || {} }).component();
|
||||
}
|
||||
@@ -17,12 +17,9 @@ export class RadioOptionsGroup {
|
||||
private _loadingBuilder: azdata.LoadingComponentBuilder;
|
||||
private _currentRadioOption!: azdata.RadioButtonComponent;
|
||||
|
||||
constructor(private _view: azdata.ModelView, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) {
|
||||
const divBuilder = this._view.modelBuilder.divContainer();
|
||||
const divBuilderWithProperties = divBuilder.withProperties<azdata.DivContainerProperties>({ clickable: false });
|
||||
this._divContainer = divBuilderWithProperties.component();
|
||||
const loadingComponentBuilder = this._view.modelBuilder.loadingComponent();
|
||||
this._loadingBuilder = loadingComponentBuilder.withItem(this._divContainer);
|
||||
constructor(private _modelBuilder: azdata.ModelBuilder, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) {
|
||||
this._divContainer = this._modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: false }).component();
|
||||
this._loadingBuilder = this._modelBuilder.loadingComponent().withItem(this._divContainer);
|
||||
}
|
||||
|
||||
public component(): azdata.LoadingComponent {
|
||||
@@ -37,7 +34,7 @@ export class RadioOptionsGroup {
|
||||
const options = optionsInfo.values!;
|
||||
let defaultValue: string = optionsInfo.defaultValue!;
|
||||
options.forEach((option: string) => {
|
||||
const radioOption = this._view!.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
|
||||
const radioOption = this._modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
|
||||
label: option,
|
||||
checked: option === defaultValue,
|
||||
name: this._groupName,
|
||||
@@ -60,7 +57,7 @@ export class RadioOptionsGroup {
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
const errorLabel = this._view!.modelBuilder.text().withProperties({ value: getErrorMessage(e), CSSStyles: { 'color': 'Red' } }).component();
|
||||
const errorLabel = this._modelBuilder.text().withProperties({ value: getErrorMessage(e), CSSStyles: { 'color': 'Red' } }).component();
|
||||
this._divContainer.addItem(errorLabel);
|
||||
}
|
||||
this.component().loading = false;
|
||||
@@ -69,4 +66,8 @@ export class RadioOptionsGroup {
|
||||
get value(): string | undefined {
|
||||
return this._currentRadioOption?.value;
|
||||
}
|
||||
|
||||
get items(): azdata.Component[] {
|
||||
return this._divContainer.items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
|
||||
|
||||
this.disposables.push(
|
||||
newInstance.onDidClick(async () => {
|
||||
await vscode.commands.executeCommand('azdata.resource.deploy', 'arc.sql', ['arc.sql', 'arc.postgres']);
|
||||
await vscode.commands.executeCommand('azdata.resource.deploy', 'azure-sql-mi', ['azure-sql-mi', 'arc.postgres'], { 'azure-sql-mi': { 'mi-type': ['arc-mi'] } });
|
||||
}));
|
||||
|
||||
// Refresh
|
||||
@@ -179,7 +179,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
|
||||
const config = this._controllerModel.controllerConfig;
|
||||
if (config) {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(
|
||||
`https://portal.azure.com/#resource/subscriptions/${config.spec.settings.azure.subscription}/resourceGroups/${config.spec.settings.azure.resourceGroup}/providers/Microsoft.AzureData/${ResourceType.dataControllers}/${config.metadata.name}`));
|
||||
`https://portal.azure.com/#resource/subscriptions/${config.spec.settings.azure.subscription}/resourceGroups/${config.spec.settings.azure.resourceGroup}/providers/Microsoft.AzureArcData/${ResourceType.dataControllers}/${config.metadata.name}`));
|
||||
} else {
|
||||
vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration);
|
||||
}
|
||||
|
||||
@@ -129,12 +129,16 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
let session: azdataExt.AzdataSession | undefined = undefined;
|
||||
try {
|
||||
session = await this._miaaModel.controllerModel.acquireAzdataSession();
|
||||
await this._azdataApi.azdata.arc.sql.mi.edit(
|
||||
this._miaaModel.info.name, this.saveArgs);
|
||||
this._miaaModel.info.name, this.saveArgs, this._miaaModel.controllerModel.azdataAdditionalEnvVars, session);
|
||||
} catch (err) {
|
||||
this.saveButton!.enabled = true;
|
||||
throw err;
|
||||
} finally {
|
||||
session?.dispose();
|
||||
}
|
||||
|
||||
await this._miaaModel.refresh();
|
||||
|
||||
@@ -14,13 +14,13 @@ import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { MiaaModel } from '../../../models/miaaModel';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { ResourceType } from 'arc';
|
||||
import { UserCancelledError } from '../../../common/api';
|
||||
|
||||
export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
|
||||
private _propertiesLoading!: azdata.LoadingComponent;
|
||||
private _kibanaLoading!: azdata.LoadingComponent;
|
||||
private _grafanaLoading!: azdata.LoadingComponent;
|
||||
private _databasesTableLoading!: azdata.LoadingComponent;
|
||||
|
||||
private _propertiesContainer!: azdata.PropertiesContainerComponent;
|
||||
private _kibanaLink!: azdata.HyperlinkComponent;
|
||||
@@ -29,6 +29,11 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
private _databasesMessage!: azdata.TextComponent;
|
||||
private _openInAzurePortalButton!: azdata.ButtonComponent;
|
||||
|
||||
private _databasesContainer!: azdata.DivContainer;
|
||||
private _connectToServerLoading!: azdata.LoadingComponent;
|
||||
private _connectToServerButton!: azdata.ButtonComponent;
|
||||
private _databasesTableLoading!: azdata.LoadingComponent;
|
||||
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
private readonly _azurecoreApi: azurecore.IExtension;
|
||||
|
||||
@@ -84,6 +89,28 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
this._grafanaLink = this.modelView.modelBuilder.hyperlink().component();
|
||||
this._grafanaLoading = this.modelView.modelBuilder.loadingComponent().component();
|
||||
|
||||
this._databasesContainer = this.modelView.modelBuilder.divContainer().component();
|
||||
|
||||
const connectToServerText = this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.miaaConnectionRequired
|
||||
}).component();
|
||||
|
||||
this._connectToServerButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.connectToServer,
|
||||
enabled: false,
|
||||
CSSStyles: { 'max-width': '125px', 'margin-left': '40%' }
|
||||
}).component();
|
||||
|
||||
const connectToServerContainer = this.modelView.modelBuilder.divContainer().component();
|
||||
|
||||
|
||||
connectToServerContainer.addItem(connectToServerText, { CSSStyles: { 'text-align': 'center', 'margin-top': '20px' } });
|
||||
connectToServerContainer.addItem(this._connectToServerButton);
|
||||
|
||||
this._connectToServerLoading = this.modelView.modelBuilder.loadingComponent().withItem(connectToServerContainer).component();
|
||||
|
||||
this._databasesContainer.addItem(this._connectToServerLoading, { CSSStyles: { 'margin-top': '20px' } });
|
||||
|
||||
this._databasesTableLoading = this.modelView.modelBuilder.loadingComponent().component();
|
||||
this._databasesTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
|
||||
width: '100%',
|
||||
@@ -180,7 +207,18 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
|
||||
// Databases
|
||||
rootContainer.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: loc.databases, CSSStyles: titleCSS }).component());
|
||||
rootContainer.addItem(this._databasesTableLoading, { CSSStyles: { 'margin-bottom': '20px' } });
|
||||
this.disposables.push(
|
||||
this._connectToServerButton!.onDidClick(async () => {
|
||||
this._connectToServerButton!.enabled = false;
|
||||
this._databasesTableLoading!.loading = true;
|
||||
try {
|
||||
await this.callGetDatabases();
|
||||
} catch {
|
||||
this._connectToServerButton!.enabled = true;
|
||||
}
|
||||
})
|
||||
);
|
||||
rootContainer.addItem(this._databasesContainer);
|
||||
rootContainer.addItem(this._databasesMessage);
|
||||
|
||||
this.initialized = true;
|
||||
@@ -205,8 +243,14 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
title: loc.deletingInstance(this._miaaModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
(_progress, _token) => {
|
||||
return this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name);
|
||||
async (_progress, _token) => {
|
||||
const session = await this._controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
return await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name, this._controllerModel.azdataAdditionalEnvVars, session);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
await this._controllerModel.refreshTreeNode();
|
||||
@@ -251,7 +295,7 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
const config = this._controllerModel.controllerConfig;
|
||||
if (config) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(
|
||||
`https://portal.azure.com/#resource/subscriptions/${config.spec.settings.azure.subscription}/resourceGroups/${config.spec.settings.azure.resourceGroup}/providers/Microsoft.AzureData/${ResourceType.sqlManagedInstances}/${this._miaaModel.info.name}`));
|
||||
`https://portal.azure.com/#resource/subscriptions/${config.spec.settings.azure.subscription}/resourceGroups/${config.spec.settings.azure.resourceGroup}/providers/Microsoft.AzureArcData/${ResourceType.sqlManagedInstances}/${this._miaaModel.info.name}`));
|
||||
} else {
|
||||
vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration);
|
||||
}
|
||||
@@ -277,6 +321,19 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
).component();
|
||||
}
|
||||
|
||||
private async callGetDatabases(): Promise<void> {
|
||||
try {
|
||||
await this._miaaModel.getDatabases();
|
||||
} catch (error) {
|
||||
if (error instanceof UserCancelledError) {
|
||||
vscode.window.showWarningMessage(loc.miaaConnectionRequired);
|
||||
} else {
|
||||
vscode.window.showErrorMessage(loc.fetchDatabasesFailed(this._miaaModel.info.name, error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private handleRegistrationsUpdated(): void {
|
||||
const config = this._controllerModel.controllerConfig;
|
||||
if (this._openInAzurePortalButton) {
|
||||
@@ -295,6 +352,9 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
this._instanceProperties.externalEndpoint = this._miaaModel.config.status.externalEndpoint || loc.notConfigured;
|
||||
this._instanceProperties.vCores = this._miaaModel.config.spec.limits?.vcores?.toString() || '';
|
||||
this._databasesMessage.value = !this._miaaModel.config.status.externalEndpoint ? loc.noExternalEndpoint : '';
|
||||
if (!this._miaaModel.config.status.externalEndpoint) {
|
||||
this._databasesContainer.removeItem(this._connectToServerLoading);
|
||||
}
|
||||
}
|
||||
|
||||
this.refreshDisplayedProperties();
|
||||
@@ -306,7 +366,20 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
this._instanceProperties.miaaAdmin = this._miaaModel.username || this._instanceProperties.miaaAdmin;
|
||||
this.refreshDisplayedProperties();
|
||||
this._databasesTable.data = this._miaaModel.databases.map(d => [d.name, getDatabaseStateDisplayText(d.status)]);
|
||||
this._databasesTableLoading.loading = !this._miaaModel.databasesLastUpdated;
|
||||
this._databasesTableLoading.loading = false;
|
||||
|
||||
if (this._miaaModel.databasesLastUpdated) {
|
||||
// We successfully connected so now can remove the button and replace it with the actual databases table
|
||||
this._databasesContainer.removeItem(this._connectToServerLoading);
|
||||
this._databasesContainer.addItem(this._databasesTableLoading, { CSSStyles: { 'margin-bottom': '20px' } });
|
||||
} else {
|
||||
// If we don't have an endpoint then there's no point in showing the connect button - but the logic
|
||||
// to display text informing the user of this is already handled by the handleMiaaConfigUpdated
|
||||
if (this._miaaModel?.config?.status.externalEndpoint) {
|
||||
this._connectToServerLoading.loading = false;
|
||||
this._connectToServerButton.enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private refreshDisplayedProperties(): void {
|
||||
|
||||
@@ -76,7 +76,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
}).component();
|
||||
|
||||
const workerNodeslink = this.modelView.modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({
|
||||
label: loc.addingWokerNodes,
|
||||
label: loc.addingWorkerNodes,
|
||||
url: 'https://docs.microsoft.com/azure/azure-arc/data/scale-up-down-postgresql-hyperscale-server-group-using-cli',
|
||||
CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
@@ -155,16 +155,23 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
let session: azdataExt.AzdataSession | undefined = undefined;
|
||||
try {
|
||||
session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
this.saveArgs,
|
||||
this._postgresModel.engineVersion);
|
||||
this._postgresModel.engineVersion,
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session
|
||||
);
|
||||
} catch (err) {
|
||||
// If an error occurs while editing the instance then re-enable the save button since
|
||||
// the edit wasn't successfully applied
|
||||
this.saveButton!.enabled = true;
|
||||
throw err;
|
||||
} finally {
|
||||
session?.dispose();
|
||||
}
|
||||
await this._postgresModel.refresh();
|
||||
}
|
||||
@@ -334,8 +341,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
const information = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
iconPath: IconPathHelper.information,
|
||||
title: loc.workerNodesInformation,
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
width: '15px',
|
||||
height: '15px',
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
@@ -427,8 +434,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
const information = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
iconPath: IconPathHelper.information,
|
||||
title: loc.postgresConfigurationInformation,
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
width: '15px',
|
||||
height: '15px',
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import { Dashboard } from '../../components/dashboard';
|
||||
import { PostgresDiagnoseAndSolveProblemsPage } from './postgresDiagnoseAndSolveProblemsPage';
|
||||
import { PostgresSupportRequestPage } from './postgresSupportRequestPage';
|
||||
import { PostgresComputeAndStoragePage } from './postgresComputeAndStoragePage';
|
||||
import { PostgresParametersPage } from './postgresParametersPage';
|
||||
import { PostgresPropertiesPage } from './postgresPropertiesPage';
|
||||
|
||||
export class PostgresDashboard extends Dashboard {
|
||||
constructor(private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
@@ -32,8 +34,8 @@ export class PostgresDashboard extends Dashboard {
|
||||
const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel);
|
||||
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel);
|
||||
const computeAndStoragePage = new PostgresComputeAndStoragePage(modelView, this._postgresModel);
|
||||
// TODO: Removed properties page while investigating bug where refreshed values don't appear in UI
|
||||
// const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel);
|
||||
const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel);
|
||||
const parametersPage = new PostgresParametersPage(modelView, this._postgresModel);
|
||||
const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, this._postgresModel);
|
||||
const supportRequestPage = new PostgresSupportRequestPage(modelView, this._controllerModel, this._postgresModel);
|
||||
|
||||
@@ -42,8 +44,10 @@ export class PostgresDashboard extends Dashboard {
|
||||
{
|
||||
title: loc.settings,
|
||||
tabs: [
|
||||
propertiesPage.tab,
|
||||
connectionStringsPage.tab,
|
||||
computeAndStoragePage.tab
|
||||
computeAndStoragePage.tab,
|
||||
parametersPage.tab
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,16 +7,23 @@ import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { IconPathHelper, cssStyles, iconSize } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
import { promptAndConfirmPassword, promptForInstanceDeletion } from '../../../common/utils';
|
||||
import { ResourceType } from 'arc';
|
||||
|
||||
export type PodStatusModel = {
|
||||
podName: azdata.Component,
|
||||
type: string,
|
||||
status: string
|
||||
};
|
||||
|
||||
export class PostgresOverviewPage extends DashboardPage {
|
||||
|
||||
private propertiesLoading!: azdata.LoadingComponent;
|
||||
private serverGroupNodesLoading!: azdata.LoadingComponent;
|
||||
private kibanaLoading!: azdata.LoadingComponent;
|
||||
private grafanaLoading!: azdata.LoadingComponent;
|
||||
|
||||
@@ -24,6 +31,9 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
private kibanaLink!: azdata.HyperlinkComponent;
|
||||
private grafanaLink!: azdata.HyperlinkComponent;
|
||||
|
||||
private podStatusTable!: azdata.DeclarativeTableComponent;
|
||||
private podStatusData: PodStatusModel[] = [];
|
||||
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
@@ -132,8 +142,63 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
[loc.kibanaDashboard, this.kibanaLoading, loc.kibanaDashboardDescription],
|
||||
[loc.grafanaDashboard, this.grafanaLoading, loc.grafanaDashboardDescription]]
|
||||
}).component();
|
||||
|
||||
content.addItem(endpointsTable);
|
||||
|
||||
// Server Group Nodes
|
||||
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.serverGroupNodes,
|
||||
CSSStyles: titleCSS
|
||||
}).component());
|
||||
|
||||
this.podStatusTable = this.modelView.modelBuilder.declarativeTable().withProps({
|
||||
width: '100%',
|
||||
columns: [
|
||||
{
|
||||
displayName: loc.name,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: '35%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: {
|
||||
...cssStyles.tableRow,
|
||||
'overflow': 'hidden',
|
||||
'text-overflow': 'ellipsis',
|
||||
'white-space': 'nowrap',
|
||||
'max-width': '0'
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: loc.type,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '35%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.status,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '30%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
}
|
||||
],
|
||||
data: [this.podStatusData.map(p => [p.podName, p.type, p.status])]
|
||||
}).component();
|
||||
|
||||
|
||||
|
||||
this.serverGroupNodesLoading = this.modelView.modelBuilder.loadingComponent()
|
||||
.withItem(this.podStatusTable)
|
||||
.withProperties<azdata.LoadingComponentProperties>({
|
||||
loading: !this._postgresModel.configLastUpdated
|
||||
}).component();
|
||||
|
||||
this.refreshServerNodes();
|
||||
|
||||
content.addItem(this.serverGroupNodesLoading, { CSSStyles: cssStyles.text });
|
||||
|
||||
this.initialized = true;
|
||||
return root;
|
||||
}
|
||||
@@ -151,14 +216,21 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
try {
|
||||
const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : '');
|
||||
if (password) {
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{
|
||||
adminPassword: true,
|
||||
noWait: true
|
||||
},
|
||||
this._postgresModel.engineVersion,
|
||||
{ 'AZDATA_PASSWORD': password });
|
||||
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{
|
||||
adminPassword: true,
|
||||
noWait: true
|
||||
},
|
||||
this._postgresModel.engineVersion,
|
||||
Object.assign({ 'AZDATA_PASSWORD': password }, this._controllerModel.azdataAdditionalEnvVars),
|
||||
session
|
||||
);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
vscode.window.showInformationMessage(loc.passwordReset);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -185,8 +257,14 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
title: loc.deletingInstance(this._postgresModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
(_progress, _token) => {
|
||||
return this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name);
|
||||
async (_progress, _token) => {
|
||||
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
return await this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name, this._controllerModel.azdataAdditionalEnvVars, session);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
await this._controllerModel.refreshTreeNode();
|
||||
@@ -210,6 +288,7 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
refreshButton.enabled = false;
|
||||
try {
|
||||
this.propertiesLoading!.loading = true;
|
||||
this.serverGroupNodesLoading!.loading = true;
|
||||
this.kibanaLoading!.loading = true;
|
||||
this.grafanaLoading!.loading = true;
|
||||
|
||||
@@ -236,7 +315,7 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
const azure = this._controllerModel.controllerConfig?.spec.settings.azure;
|
||||
if (azure) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(
|
||||
`https://portal.azure.com/#resource/subscriptions/${azure.subscription}/resourceGroups/${azure.resourceGroup}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${this._postgresModel.info.name}`));
|
||||
`https://portal.azure.com/#resource/subscriptions/${azure.subscription}/resourceGroups/${azure.resourceGroup}/providers/Microsoft.AzureArcData/${ResourceType.postgresInstances}/${this._postgresModel.info.name}`));
|
||||
} else {
|
||||
vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration);
|
||||
}
|
||||
@@ -268,6 +347,54 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
];
|
||||
}
|
||||
|
||||
private getPodStatus(): PodStatusModel[] {
|
||||
let podModels: PodStatusModel[] = [];
|
||||
const podStatus = this._postgresModel.config?.status.podsStatus;
|
||||
|
||||
podStatus?.forEach(p => {
|
||||
// If a condition of the pod has a status of False, pod is not Ready
|
||||
const status = p.conditions.find(c => c.status === 'False') ? loc.notReady : loc.ready;
|
||||
|
||||
const podLabelContainer = this.modelView.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: { 'alignItems': 'center', 'height': '15px' }
|
||||
}).component();
|
||||
|
||||
const imageComponent = this.modelView.modelBuilder.image().withProps({
|
||||
iconPath: IconPathHelper.postgres,
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
iconHeight: '15px',
|
||||
iconWidth: '15px'
|
||||
}).component();
|
||||
|
||||
let podLabel = this.modelView.modelBuilder.text().withProps({
|
||||
value: p.name,
|
||||
}).component();
|
||||
|
||||
if (p.role.toUpperCase() === loc.worker.toUpperCase()) {
|
||||
podLabelContainer.addItem(imageComponent, { CSSStyles: { 'margin-left': '15px', 'margin-right': '0px' } });
|
||||
podLabelContainer.addItem(podLabel);
|
||||
let pod: PodStatusModel = {
|
||||
podName: podLabelContainer,
|
||||
type: loc.worker,
|
||||
status: status
|
||||
};
|
||||
podModels.push(pod);
|
||||
} else {
|
||||
podLabelContainer.addItem(imageComponent, { CSSStyles: { 'margin-right': '0px' } });
|
||||
podLabelContainer.addItem(podLabel);
|
||||
let pod: PodStatusModel = {
|
||||
podName: podLabelContainer,
|
||||
type: loc.coordinator,
|
||||
status: status
|
||||
};
|
||||
podModels.unshift(pod);
|
||||
}
|
||||
});
|
||||
|
||||
return podModels;
|
||||
}
|
||||
|
||||
private refreshDashboardLinks(): void {
|
||||
if (this._postgresModel.config) {
|
||||
const kibanaUrl = this._postgresModel.config.status.logSearchDashboard ?? '';
|
||||
@@ -282,6 +409,14 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
}
|
||||
}
|
||||
|
||||
private refreshServerNodes(): void {
|
||||
if (this._postgresModel.config) {
|
||||
this.podStatusData = this.getPodStatus();
|
||||
this.podStatusTable.data = this.podStatusData.map(p => [p.podName, p.type, p.status]);
|
||||
this.serverGroupNodesLoading.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private handleRegistrationsUpdated() {
|
||||
this.properties!.propertyItems = this.getProperties();
|
||||
this.propertiesLoading!.loading = false;
|
||||
@@ -291,5 +426,6 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
this.properties!.propertyItems = this.getProperties();
|
||||
this.propertiesLoading!.loading = false;
|
||||
this.refreshDashboardLinks();
|
||||
this.refreshServerNodes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,588 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 azdataExt from 'azdata-ext';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { UserCancelledError } from '../../../common/api';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { EngineSettingsModel, PostgresModel } from '../../../models/postgresModel';
|
||||
import { debounce } from '../../../common/utils';
|
||||
|
||||
export type ParametersModel = {
|
||||
parameterName: string,
|
||||
valueContainer: azdata.FlexContainer,
|
||||
description: string,
|
||||
resetButton: azdata.ButtonComponent
|
||||
};
|
||||
|
||||
export class PostgresParametersPage extends DashboardPage {
|
||||
private searchBox!: azdata.InputBoxComponent;
|
||||
private parametersTable!: azdata.DeclarativeTableComponent;
|
||||
private parameterContainer?: azdata.DivContainer;
|
||||
private _parametersTableLoading!: azdata.LoadingComponent;
|
||||
|
||||
private discardButton!: azdata.ButtonComponent;
|
||||
private saveButton!: azdata.ButtonComponent;
|
||||
private resetAllButton!: azdata.ButtonComponent;
|
||||
private connectToServerButton?: azdata.ButtonComponent;
|
||||
|
||||
private _parameters: ParametersModel[] = [];
|
||||
private parameterUpdates: Map<string, string> = new Map();
|
||||
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
|
||||
this.initializeConnectButton();
|
||||
this.initializeSearchBox();
|
||||
|
||||
this.disposables.push(
|
||||
this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())),
|
||||
this._postgresModel.onEngineSettingsUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshParametersTable()))
|
||||
);
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
return loc.nodeParameters;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-node-parameters';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.gear;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
const root = this.modelView.modelBuilder.divContainer().component();
|
||||
const content = this.modelView.modelBuilder.divContainer().component();
|
||||
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.nodeParameters,
|
||||
CSSStyles: { ...cssStyles.title }
|
||||
}).component());
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.nodeParametersDescription,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component());
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.hyperlink().withProps({
|
||||
label: loc.learnAboutNodeParameters,
|
||||
url: 'https://docs.microsoft.com/azure/azure-arc/data/configure-server-parameters-postgresql-hyperscale'
|
||||
}).component(), { CSSStyles: { 'margin-bottom': '20px' } });
|
||||
|
||||
content.addItem(this.searchBox!, { CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'margin-bottom': '20px' } });
|
||||
|
||||
this.parametersTable = this.modelView.modelBuilder.declarativeTable().withProps({
|
||||
width: '100%',
|
||||
columns: [
|
||||
{
|
||||
displayName: loc.parameterName,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '20%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.value,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: false,
|
||||
width: '20%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.description,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '50%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: {
|
||||
...cssStyles.tableRow
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: loc.resetToDefault,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: false,
|
||||
width: '10%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
}
|
||||
],
|
||||
data: []
|
||||
}).component();
|
||||
|
||||
this._parametersTableLoading = this.modelView.modelBuilder.loadingComponent().component();
|
||||
|
||||
this.parameterContainer = this.modelView.modelBuilder.divContainer().component();
|
||||
this.selectComponent();
|
||||
|
||||
content.addItem(this.parameterContainer);
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
// Save Edits
|
||||
this.saveButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.saveText,
|
||||
iconPath: IconPathHelper.save,
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
let engineSettings: string[] = [];
|
||||
this.disposables.push(
|
||||
this.saveButton.onDidClick(async () => {
|
||||
this.saveButton!.enabled = false;
|
||||
try {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.updatingInstance(this._postgresModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
try {
|
||||
this.parameterUpdates!.forEach((value, key) => {
|
||||
engineSettings.push(`${key}="${value}"`);
|
||||
});
|
||||
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: engineSettings.toString() },
|
||||
this._postgresModel.engineVersion,
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
} catch (err) {
|
||||
// If an error occurs while editing the instance then re-enable the save button since
|
||||
// the edit wasn't successfully applied
|
||||
this.saveButton!.enabled = true;
|
||||
throw err;
|
||||
}
|
||||
await this._postgresModel.refresh();
|
||||
}
|
||||
);
|
||||
|
||||
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
|
||||
|
||||
engineSettings = [];
|
||||
this.parameterUpdates!.clear();
|
||||
this.discardButton!.enabled = false;
|
||||
this.resetAllButton!.enabled = true;
|
||||
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Discard
|
||||
this.discardButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.discardText,
|
||||
iconPath: IconPathHelper.discard,
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.discardButton.onDidClick(async () => {
|
||||
this.discardButton!.enabled = false;
|
||||
try {
|
||||
this.refreshParametersTable();
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.pageDiscardFailed(error));
|
||||
} finally {
|
||||
this.saveButton!.enabled = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Reset all
|
||||
this.resetAllButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.resetAllToDefault,
|
||||
iconPath: IconPathHelper.reset,
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.resetAllButton.onDidClick(async () => {
|
||||
this.resetAllButton!.enabled = false;
|
||||
this.discardButton!.enabled = false;
|
||||
this.saveButton!.enabled = false;
|
||||
try {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.updatingInstance(this._postgresModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
//all
|
||||
// azdata arc postgres server edit -n <server group name> -e '' -re
|
||||
let session: azdataExt.AzdataSession | undefined = undefined;
|
||||
try {
|
||||
session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: `''`, replaceEngineSettings: true },
|
||||
this._postgresModel.engineVersion,
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session);
|
||||
} catch (err) {
|
||||
// If an error occurs while resetting the instance then re-enable the reset button since
|
||||
// the edit wasn't successfully applied
|
||||
if (this.parameterUpdates.size > 0) {
|
||||
this.discardButton!.enabled = true;
|
||||
this.saveButton!.enabled = true;
|
||||
}
|
||||
this.resetAllButton!.enabled = true;
|
||||
throw err;
|
||||
} finally {
|
||||
session?.dispose();
|
||||
}
|
||||
await this._postgresModel.refresh();
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
|
||||
this.parameterUpdates!.clear();
|
||||
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.resetFailed(error));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
|
||||
{ component: this.saveButton },
|
||||
{ component: this.discardButton },
|
||||
{ component: this.resetAllButton }
|
||||
]).component();
|
||||
}
|
||||
|
||||
private initializeConnectButton(): void {
|
||||
this.connectToServerButton = this.modelView.modelBuilder.button().withProps({
|
||||
label: loc.connectToServer,
|
||||
enabled: false,
|
||||
CSSStyles: { 'max-width': '125px' }
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.connectToServerButton!.onDidClick(async () => {
|
||||
this.connectToServerButton!.enabled = false;
|
||||
if (!vscode.extensions.getExtension(loc.postgresExtension)) {
|
||||
const response = await vscode.window.showErrorMessage(loc.missingExtension('PostgreSQL'), loc.yes, loc.no);
|
||||
if (response !== loc.yes) {
|
||||
this.connectToServerButton!.enabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.installingExtension(loc.postgresExtension),
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
try {
|
||||
await vscode.commands.executeCommand('workbench.extensions.installExtension', loc.postgresExtension);
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(loc.extensionInstallationFailed(loc.postgresExtension));
|
||||
this.connectToServerButton!.enabled = true;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
);
|
||||
vscode.window.showInformationMessage(loc.extensionInstalled(loc.postgresExtension));
|
||||
}
|
||||
|
||||
this._parametersTableLoading!.loading = true;
|
||||
await this.callGetEngineSettings().finally(() => this._parametersTableLoading!.loading = false);
|
||||
this.searchBox!.enabled = true;
|
||||
this.resetAllButton!.enabled = true;
|
||||
this.parameterContainer!.clearItems();
|
||||
this.parameterContainer!.addItem(this.parametersTable);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private selectComponent(): void {
|
||||
if (!this._postgresModel.engineSettingsLastUpdated) {
|
||||
this.parameterContainer!.addItem(this.modelView.modelBuilder.text().withProps({
|
||||
value: loc.connectToPostgresDescription,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component());
|
||||
this.parameterContainer!.addItem(this.connectToServerButton!, { CSSStyles: { 'max-width': '125px' } });
|
||||
this.parameterContainer!.addItem(this._parametersTableLoading!);
|
||||
} else {
|
||||
this.searchBox!.enabled = true;
|
||||
this.resetAllButton!.enabled = true;
|
||||
this.parameterContainer!.addItem(this.parametersTable!);
|
||||
this.refreshParametersTable();
|
||||
}
|
||||
}
|
||||
|
||||
private async callGetEngineSettings(): Promise<void> {
|
||||
try {
|
||||
await this._postgresModel.getEngineSettings();
|
||||
} catch (error) {
|
||||
if (error instanceof UserCancelledError) {
|
||||
vscode.window.showWarningMessage(loc.pgConnectionRequired);
|
||||
} else {
|
||||
vscode.window.showErrorMessage(loc.fetchEngineSettingsFailed(this._postgresModel.info.name, error));
|
||||
}
|
||||
this.connectToServerButton!.enabled = true;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private initializeSearchBox(): void {
|
||||
this.searchBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
readOnly: false,
|
||||
enabled: false,
|
||||
placeHolder: loc.searchToFilter
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.searchBox.onTextChanged(() => {
|
||||
this.onSearchFilter();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
private onSearchFilter(): void {
|
||||
if (!this.searchBox!.value) {
|
||||
this.parametersTable.setFilter(undefined);
|
||||
} else {
|
||||
this.filterParameters(this.searchBox!.value);
|
||||
}
|
||||
}
|
||||
|
||||
private filterParameters(search: string): void {
|
||||
const filteredRowIndexes: number[] = [];
|
||||
this.parametersTable.data?.forEach((row, index) => {
|
||||
if (row[0].toUpperCase()?.search(search.toUpperCase()) !== -1 || row[2].toUpperCase()?.search(search.toUpperCase()) !== -1) {
|
||||
filteredRowIndexes.push(index);
|
||||
}
|
||||
});
|
||||
this.parametersTable.setFilter(filteredRowIndexes);
|
||||
}
|
||||
|
||||
private handleOnTextChanged(component: azdata.InputBoxComponent, currentValue: string | undefined): boolean {
|
||||
if (!component.valid) {
|
||||
// If invalid value return false and enable discard button
|
||||
this.discardButton!.enabled = true;
|
||||
return false;
|
||||
} else if (component.value === currentValue) {
|
||||
return false;
|
||||
} else {
|
||||
/* If a valid value has been entered into the input box, enable save and discard buttons
|
||||
so that user could choose to either edit instance or clear all inputs
|
||||
return true */
|
||||
this.saveButton!.enabled = true;
|
||||
this.discardButton!.enabled = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private createParameterComponents(engineSetting: EngineSettingsModel): ParametersModel {
|
||||
|
||||
// Container to hold input component and information bubble
|
||||
const valueContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
|
||||
|
||||
if (engineSetting.type === 'enum') {
|
||||
// If type is enum, component should be drop down menu
|
||||
let options = engineSetting.options?.slice(1, -1).split(',');
|
||||
let values: string[] = [];
|
||||
options!.forEach(option => {
|
||||
values.push(option.slice(option.indexOf('"') + 1, -1));
|
||||
});
|
||||
|
||||
let valueBox = this.modelView.modelBuilder.dropDown().withProps({
|
||||
values: values,
|
||||
value: engineSetting.value,
|
||||
width: '150px'
|
||||
}).component();
|
||||
valueContainer.addItem(valueBox);
|
||||
|
||||
this.disposables.push(
|
||||
valueBox.onValueChanged(() => {
|
||||
if (engineSetting.value !== String(valueBox.value)) {
|
||||
this.parameterUpdates!.set(engineSetting.parameterName!, String(valueBox.value));
|
||||
this.saveButton!.enabled = true;
|
||||
this.discardButton!.enabled = true;
|
||||
} else if (this.parameterUpdates!.has(engineSetting.parameterName!)) {
|
||||
this.parameterUpdates!.delete(engineSetting.parameterName!);
|
||||
}
|
||||
})
|
||||
);
|
||||
} else if (engineSetting.type === 'bool') {
|
||||
// If type is bool, component should be checkbox to turn on or off
|
||||
let valueBox = this.modelView.modelBuilder.checkBox().withProps({
|
||||
label: loc.on,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
valueContainer.addItem(valueBox);
|
||||
|
||||
if (engineSetting.value === 'on') {
|
||||
valueBox.checked = true;
|
||||
} else {
|
||||
valueBox.checked = false;
|
||||
}
|
||||
|
||||
this.disposables.push(
|
||||
valueBox.onChanged(() => {
|
||||
if (valueBox.checked && engineSetting.value === 'off') {
|
||||
this.parameterUpdates!.set(engineSetting.parameterName!, loc.on);
|
||||
this.saveButton!.enabled = true;
|
||||
this.discardButton!.enabled = true;
|
||||
} else if (!valueBox.checked && engineSetting.value === 'on') {
|
||||
this.parameterUpdates!.set(engineSetting.parameterName!, loc.off);
|
||||
this.saveButton!.enabled = true;
|
||||
this.discardButton!.enabled = true;
|
||||
} else if (this.parameterUpdates!.has(engineSetting.parameterName!)) {
|
||||
this.parameterUpdates!.delete(engineSetting.parameterName!);
|
||||
}
|
||||
})
|
||||
);
|
||||
} else if (engineSetting.type === 'string') {
|
||||
// If type is string, component should be text inputbox
|
||||
let valueBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: engineSetting.value,
|
||||
width: '150px'
|
||||
}).component();
|
||||
valueContainer.addItem(valueBox);
|
||||
|
||||
this.disposables.push(
|
||||
valueBox.onTextChanged(() => {
|
||||
if ((this.handleOnTextChanged(valueBox, engineSetting.value))) {
|
||||
this.parameterUpdates!.set(engineSetting.parameterName!, `"${valueBox.value!}"`);
|
||||
} else if (this.parameterUpdates!.has(engineSetting.parameterName!)) {
|
||||
this.parameterUpdates!.delete(engineSetting.parameterName!);
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// If type is real or interger, component should be inputbox set to inputType of number. Max and min values also set.
|
||||
let valueBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
required: true,
|
||||
readOnly: false,
|
||||
min: parseInt(engineSetting.min!),
|
||||
max: parseInt(engineSetting.max!),
|
||||
validationErrorMessage: loc.outOfRange(engineSetting.min!, engineSetting.max!),
|
||||
inputType: 'number',
|
||||
value: engineSetting.value,
|
||||
width: '150px'
|
||||
}).component();
|
||||
|
||||
valueContainer.addItem(valueBox, { CSSStyles: { 'margin-right': '0px' } });
|
||||
|
||||
this.disposables.push(
|
||||
valueBox.onTextChanged(() => {
|
||||
if ((this.handleOnTextChanged(valueBox, engineSetting.value))) {
|
||||
this.parameterUpdates!.set(engineSetting.parameterName!, valueBox.value!);
|
||||
} else if (this.parameterUpdates!.has(engineSetting.parameterName!)) {
|
||||
this.parameterUpdates!.delete(engineSetting.parameterName!);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Information bubble title to show allowed values
|
||||
let information = this.modelView.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.information,
|
||||
width: '15px',
|
||||
height: '15px',
|
||||
enabled: false,
|
||||
title: loc.allowedValue(loc.rangeSetting(engineSetting.min!, engineSetting.max!))
|
||||
}).component();
|
||||
valueContainer.addItem(information, { CSSStyles: { 'margin-left': '5px' } });
|
||||
}
|
||||
|
||||
// Can reset individual parameter
|
||||
const resetParameterButton = this.modelView.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.reset,
|
||||
title: loc.resetToDefault,
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
enabled: true
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
resetParameterButton.onDidClick(async () => {
|
||||
try {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.updatingInstance(this._postgresModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: engineSetting.parameterName + '=' },
|
||||
this._postgresModel.engineVersion,
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
await this._postgresModel.refresh();
|
||||
}
|
||||
);
|
||||
|
||||
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let parameter: ParametersModel = {
|
||||
parameterName: engineSetting.parameterName!,
|
||||
valueContainer: valueContainer,
|
||||
description: engineSetting.description!,
|
||||
resetButton: resetParameterButton
|
||||
};
|
||||
|
||||
return parameter;
|
||||
}
|
||||
|
||||
private refreshParametersTable(): void {
|
||||
this._parameters = this._postgresModel._engineSettings.map(engineSetting => this.createParameterComponents(engineSetting));
|
||||
this.parametersTable.data = this._parameters.map(p => [p.parameterName, p.valueContainer, p.description, p.resetButton]);
|
||||
}
|
||||
|
||||
private async handleServiceUpdated(): Promise<void> {
|
||||
if (this._postgresModel.configLastUpdated && !this._postgresModel.engineSettingsLastUpdated) {
|
||||
this.connectToServerButton!.enabled = true;
|
||||
this._parametersTableLoading!.loading = false;
|
||||
} else if (this._postgresModel.engineSettingsLastUpdated) {
|
||||
await this.callGetEngineSettings();
|
||||
this.discardButton!.enabled = false;
|
||||
this.saveButton!.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,11 @@ import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { KeyValueContainer, KeyValue, InputKeyValue, TextKeyValue } from '../../components/keyValueContainer';
|
||||
import { KeyValueContainer, KeyValue, InputKeyValue, TextKeyValue, LinkKeyValue } from '../../components/keyValueContainer';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
import { ControllerDashboard } from '../controller/controllerDashboard';
|
||||
|
||||
export class PostgresPropertiesPage extends DashboardPage {
|
||||
private loading?: azdata.LoadingComponent;
|
||||
@@ -49,6 +50,7 @@ export class PostgresPropertiesPage extends DashboardPage {
|
||||
}).component());
|
||||
|
||||
this.keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, this.getProperties());
|
||||
this.keyValueContainer.container.updateCssStyles({ 'max-width': '750px' });
|
||||
this.disposables.push(this.keyValueContainer);
|
||||
|
||||
this.loading = this.modelView.modelBuilder.loadingComponent()
|
||||
@@ -93,12 +95,13 @@ export class PostgresPropertiesPage extends DashboardPage {
|
||||
private getProperties(): KeyValue[] {
|
||||
const endpoint = this._postgresModel.endpoint;
|
||||
const status = this._postgresModel.config?.status;
|
||||
const controllerDashboard = new ControllerDashboard(this._controllerModel);
|
||||
|
||||
return [
|
||||
new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, endpoint ? `postgresql://postgres@${endpoint.ip}:${endpoint.port}` : ''),
|
||||
new InputKeyValue(this.modelView.modelBuilder, loc.postgresAdminUsername, 'postgres'),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.status, status ? `${status.state} (${status.readyPods} ${loc.podsReady})` : loc.unknown),
|
||||
// TODO: Make this a LinkKeyValue that opens the controller dashboard
|
||||
new LinkKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? '', () => controllerDashboard.showDashboard()),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.scaleConfiguration ?? ''),
|
||||
new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.engineVersion ?? ''),
|
||||
|
||||
@@ -55,7 +55,7 @@ export class PostgresSupportRequestPage extends DashboardPage {
|
||||
const azure = this._controllerModel.controllerConfig?.spec.settings.azure;
|
||||
if (azure) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(
|
||||
`https://portal.azure.com/#resource/subscriptions/${azure.subscription}/resourceGroups/${azure.resourceGroup}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${this._postgresModel.info.name}/supportrequest`));
|
||||
`https://portal.azure.com/#resource/subscriptions/${azure.subscription}/resourceGroups/${azure.resourceGroup}/providers/Microsoft.AzureArcData/${ResourceType.postgresInstances}/${this._postgresModel.info.name}/supportrequest`));
|
||||
} else {
|
||||
vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration);
|
||||
}
|
||||
|
||||
@@ -14,23 +14,43 @@ import { ControllerModel } from '../../models/controllerModel';
|
||||
import { InitializingComponent } from '../components/initializingComponent';
|
||||
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
|
||||
import { getErrorMessage } from '../../common/utils';
|
||||
import { RadioOptionsGroup } from '../components/radioOptionsGroup';
|
||||
import { getCurrentClusterContext, getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../../common/kubeUtils';
|
||||
import { FilePicker } from '../components/filePicker';
|
||||
|
||||
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
|
||||
|
||||
abstract class ControllerDialogBase extends InitializingComponent {
|
||||
protected _toDispose: vscode.Disposable[] = [];
|
||||
protected modelBuilder!: azdata.ModelBuilder;
|
||||
protected dialog: azdata.window.Dialog;
|
||||
|
||||
protected urlInputBox!: azdata.InputBoxComponent;
|
||||
protected kubeConfigInputBox!: FilePicker;
|
||||
protected clusterContextRadioGroup!: RadioOptionsGroup;
|
||||
protected nameInputBox!: azdata.InputBoxComponent;
|
||||
protected usernameInputBox!: azdata.InputBoxComponent;
|
||||
protected passwordInputBox!: azdata.InputBoxComponent;
|
||||
|
||||
protected dispose(): void {
|
||||
this._toDispose.forEach(disposable => disposable.dispose());
|
||||
this._toDispose.length = 0; // clear the _toDispose array
|
||||
}
|
||||
|
||||
protected getComponents(): (azdata.FormComponent<azdata.Component> & { layout?: azdata.FormItemLayout | undefined; })[] {
|
||||
return [
|
||||
{
|
||||
component: this.urlInputBox,
|
||||
title: loc.controllerUrl,
|
||||
required: true
|
||||
}, {
|
||||
component: this.kubeConfigInputBox.component(),
|
||||
title: loc.controllerKubeConfig,
|
||||
required: true
|
||||
}, {
|
||||
component: this.clusterContextRadioGroup.component(),
|
||||
title: loc.controllerClusterContext,
|
||||
required: true
|
||||
}, {
|
||||
component: this.nameInputBox,
|
||||
title: loc.controllerName,
|
||||
@@ -48,7 +68,7 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
}
|
||||
|
||||
protected abstract fieldToFocusOn(): azdata.Component;
|
||||
protected readonlyFields(): azdata.InputBoxComponent[] { return []; }
|
||||
protected readonlyFields(): azdata.Component[] { return []; }
|
||||
|
||||
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
|
||||
this.urlInputBox = this.modelBuilder.inputBox()
|
||||
@@ -57,6 +77,18 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
// If we have a model then we're editing an existing connection so don't let them modify the URL
|
||||
readOnly: !!controllerInfo
|
||||
}).component();
|
||||
this.kubeConfigInputBox = new FilePicker(
|
||||
this.modelBuilder,
|
||||
controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath(),
|
||||
(disposable) => this._toDispose.push(disposable)
|
||||
);
|
||||
this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
value: controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath()
|
||||
}).component();
|
||||
this.clusterContextRadioGroup = new RadioOptionsGroup(this.modelBuilder, (disposable) => this._toDispose.push(disposable));
|
||||
this.loadRadioGroup(controllerInfo?.kubeClusterContext);
|
||||
this._toDispose.push(this.kubeConfigInputBox.onTextChanged(() => this.loadRadioGroup(controllerInfo?.kubeClusterContext)));
|
||||
this.nameInputBox = this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
value: controllerInfo?.name
|
||||
@@ -81,10 +113,20 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
this.dialog = azdata.window.createModelViewDialog(title);
|
||||
}
|
||||
|
||||
private loadRadioGroup(previousClusterContext?: string): void {
|
||||
this.clusterContextRadioGroup.load(async () => {
|
||||
const clusters = await getKubeConfigClusterContexts(this.kubeConfigInputBox.value!);
|
||||
return {
|
||||
values: clusters.map(c => c.name),
|
||||
defaultValue: getCurrentClusterContext(clusters, previousClusterContext, false),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public showDialog(controllerInfo?: ControllerInfo, password: string | undefined = undefined): azdata.window.Dialog {
|
||||
this.id = controllerInfo?.id ?? uuid();
|
||||
this.resources = controllerInfo?.resources ?? [];
|
||||
this.dialog.cancelButton.onClick(() => this.handleCancel());
|
||||
this._toDispose.push(this.dialog.cancelButton.onClick(() => this.handleCancel()));
|
||||
this.dialog.registerContent(async (view) => {
|
||||
this.modelBuilder = view.modelBuilder;
|
||||
this.initializeFields(controllerInfo, password);
|
||||
@@ -96,18 +138,24 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
}]).withLayout({ width: '100%' }).component();
|
||||
await view.initializeModel(formModel);
|
||||
await this.fieldToFocusOn().focus();
|
||||
this.readonlyFields().forEach(f => f.readOnly = true);
|
||||
this.readonlyFields().forEach(f => f.enabled = false);
|
||||
this.initialized = true;
|
||||
});
|
||||
|
||||
this.dialog.registerCloseValidator(async () => await this.validate());
|
||||
this.dialog.registerCloseValidator(async () => {
|
||||
const isValidated = await this.validate();
|
||||
if (isValidated) {
|
||||
this.dispose();
|
||||
}
|
||||
return isValidated;
|
||||
});
|
||||
this.dialog.okButton.label = loc.connect;
|
||||
this.dialog.cancelButton.label = loc.cancel;
|
||||
azdata.window.openDialog(this.dialog);
|
||||
return this.dialog;
|
||||
}
|
||||
|
||||
public abstract async validate(): Promise<boolean>;
|
||||
public abstract validate(): Promise<boolean>;
|
||||
|
||||
private handleCancel(): void {
|
||||
this.completionPromise.resolve(undefined);
|
||||
@@ -116,6 +164,19 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
public waitForClose(): Promise<ConnectToControllerDialogModel | undefined> {
|
||||
return this.completionPromise.promise;
|
||||
}
|
||||
|
||||
protected getControllerInfo(url: string, rememberPassword: boolean = false): ControllerInfo {
|
||||
return {
|
||||
id: this.id,
|
||||
url: url,
|
||||
kubeConfigFilePath: this.kubeConfigInputBox.value!,
|
||||
kubeClusterContext: this.clusterContextRadioGroup.value!,
|
||||
name: this.nameInputBox.value ?? '',
|
||||
username: this.usernameInputBox.value!,
|
||||
rememberPassword: rememberPassword,
|
||||
resources: this.resources
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectToControllerDialog extends ControllerDialogBase {
|
||||
@@ -164,14 +225,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
|
||||
if (!/.*:\d*$/.test(url)) {
|
||||
url = `${url}:30080`;
|
||||
}
|
||||
const controllerInfo: ControllerInfo = {
|
||||
id: this.id,
|
||||
url: url,
|
||||
name: this.nameInputBox.value ?? '',
|
||||
username: this.usernameInputBox.value,
|
||||
rememberPassword: this.rememberPwCheckBox.checked ?? false,
|
||||
resources: this.resources
|
||||
};
|
||||
const controllerInfo: ControllerInfo = this.getControllerInfo(url, !!this.rememberPwCheckBox.checked);
|
||||
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
||||
try {
|
||||
// Validate that we can connect to the controller, this also populates the controllerRegistration from the connection response.
|
||||
@@ -199,9 +253,11 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
|
||||
return this.passwordInputBox;
|
||||
}
|
||||
|
||||
protected readonlyFields() {
|
||||
protected readonlyFields(): azdata.Component[] {
|
||||
return [
|
||||
this.urlInputBox,
|
||||
...this.kubeConfigInputBox.items,
|
||||
...this.clusterContextRadioGroup.items,
|
||||
this.nameInputBox,
|
||||
this.usernameInputBox
|
||||
];
|
||||
@@ -213,11 +269,19 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
|
||||
}
|
||||
const azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
try {
|
||||
await azdataApi.azdata.login(this.urlInputBox.value!, this.usernameInputBox.value!, this.passwordInputBox.value);
|
||||
await azdataApi.azdata.login(
|
||||
this.urlInputBox.value!,
|
||||
this.usernameInputBox.value!,
|
||||
this.passwordInputBox.value,
|
||||
{
|
||||
'KUBECONFIG': this.kubeConfigInputBox.value!,
|
||||
'KUBECTL_CONTEXT': this.clusterContextRadioGroup.value!
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
if (getErrorMessage(e).match(/Wrong username or password/i)) {
|
||||
this.dialog.message = {
|
||||
text: loc.invalidPassword,
|
||||
text: loc.loginFailed,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false;
|
||||
@@ -229,14 +293,7 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const controllerInfo: ControllerInfo = {
|
||||
id: this.id,
|
||||
url: this.urlInputBox.value!,
|
||||
name: this.nameInputBox.value!,
|
||||
username: this.usernameInputBox.value!,
|
||||
rememberPassword: false,
|
||||
resources: []
|
||||
};
|
||||
const controllerInfo: ControllerInfo = this.getControllerInfo(this.urlInputBox.value!, false);
|
||||
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
||||
this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
|
||||
return true;
|
||||
@@ -248,3 +305,5 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
24
extensions/arc/src/ui/dialogs/connectMiaaDialog.ts
Normal file
24
extensions/arc/src/ui/dialogs/connectMiaaDialog.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ControllerModel } from '../../models/controllerModel';
|
||||
import { MiaaModel } from '../../models/miaaModel';
|
||||
import { ConnectToSqlDialog } from './connectSqlDialog';
|
||||
import * as loc from '../../localizedConstants';
|
||||
|
||||
export class ConnectToMiaaSqlDialog extends ConnectToSqlDialog {
|
||||
|
||||
constructor(_controllerModel: ControllerModel, _miaaModel: MiaaModel) {
|
||||
super(_controllerModel, _miaaModel);
|
||||
}
|
||||
|
||||
protected get providerName(): string {
|
||||
return 'MSSQL';
|
||||
}
|
||||
|
||||
protected connectionFailedMessage(error: any): string {
|
||||
return loc.connectToMSSqlFailed(this.serverNameInputBox.value!, error);
|
||||
}
|
||||
}
|
||||
24
extensions/arc/src/ui/dialogs/connectPGDialog.ts
Normal file
24
extensions/arc/src/ui/dialogs/connectPGDialog.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ControllerModel } from '../../models/controllerModel';
|
||||
import { PostgresModel } from '../../models/postgresModel';
|
||||
import { ConnectToSqlDialog } from './connectSqlDialog';
|
||||
import * as loc from '../../localizedConstants';
|
||||
|
||||
export class ConnectToPGSqlDialog extends ConnectToSqlDialog {
|
||||
|
||||
constructor(_controllerModel: ControllerModel, _postgresModel: PostgresModel) {
|
||||
super(_controllerModel, _postgresModel);
|
||||
}
|
||||
|
||||
protected get providerName(): string {
|
||||
return 'PGSQL';
|
||||
}
|
||||
|
||||
protected connectionFailedMessage(error: any): string {
|
||||
return loc.connectToPGSqlFailed(this.serverNameInputBox.value!, error);
|
||||
}
|
||||
}
|
||||
@@ -6,29 +6,30 @@
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { Deferred } from '../../common/promise';
|
||||
import * as loc from '../../localizedConstants';
|
||||
import { createCredentialId } from '../../common/utils';
|
||||
import { credentialNamespace } from '../../constants';
|
||||
import * as loc from '../../localizedConstants';
|
||||
import { ControllerModel } from '../../models/controllerModel';
|
||||
import { MiaaModel } from '../../models/miaaModel';
|
||||
import { InitializingComponent } from '../components/initializingComponent';
|
||||
import { ResourceModel } from '../../models/resourceModel';
|
||||
import { ControllerModel } from '../../models/controllerModel';
|
||||
|
||||
export class ConnectToSqlDialog extends InitializingComponent {
|
||||
private modelBuilder!: azdata.ModelBuilder;
|
||||
export abstract class ConnectToSqlDialog extends InitializingComponent {
|
||||
protected modelBuilder!: azdata.ModelBuilder;
|
||||
|
||||
private serverNameInputBox!: azdata.InputBoxComponent;
|
||||
private usernameInputBox!: azdata.InputBoxComponent;
|
||||
private passwordInputBox!: azdata.InputBoxComponent;
|
||||
private rememberPwCheckBox!: azdata.CheckBoxComponent;
|
||||
protected serverNameInputBox!: azdata.InputBoxComponent;
|
||||
protected usernameInputBox!: azdata.InputBoxComponent;
|
||||
protected passwordInputBox!: azdata.InputBoxComponent;
|
||||
protected rememberPwCheckBox!: azdata.CheckBoxComponent;
|
||||
private options: { [name: string]: any } = {};
|
||||
|
||||
private _completionPromise = new Deferred<azdata.IConnectionProfile | undefined>();
|
||||
protected _completionPromise = new Deferred<azdata.IConnectionProfile | undefined>();
|
||||
|
||||
constructor(private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
|
||||
constructor(private _controllerModel: ControllerModel, protected _model: ResourceModel) {
|
||||
super();
|
||||
}
|
||||
|
||||
public showDialog(connectionProfile?: azdata.IConnectionProfile): azdata.window.Dialog {
|
||||
const dialog = azdata.window.createModelViewDialog(loc.connectToSql(this._miaaModel.info.name));
|
||||
public showDialog(dialogTitle: string, connectionProfile?: azdata.IConnectionProfile): azdata.window.Dialog {
|
||||
const dialog = azdata.window.createModelViewDialog(dialogTitle);
|
||||
dialog.cancelButton.onClick(() => this.handleCancel());
|
||||
dialog.registerContent(async view => {
|
||||
this.modelBuilder = view.modelBuilder;
|
||||
@@ -84,6 +85,7 @@ export class ConnectToSqlDialog extends InitializingComponent {
|
||||
dialog.registerCloseValidator(async () => await this.validate());
|
||||
dialog.okButton.label = loc.connect;
|
||||
dialog.cancelButton.label = loc.cancel;
|
||||
this.options = connectionProfile?.options!;
|
||||
azdata.window.openDialog(dialog);
|
||||
return dialog;
|
||||
}
|
||||
@@ -96,7 +98,7 @@ export class ConnectToSqlDialog extends InitializingComponent {
|
||||
serverName: this.serverNameInputBox.value,
|
||||
databaseName: '',
|
||||
authenticationType: 'SqlLogin',
|
||||
providerName: 'MSSQL',
|
||||
providerName: this.providerName,
|
||||
connectionName: '',
|
||||
userName: this.usernameInputBox.value,
|
||||
password: this.passwordInputBox.value,
|
||||
@@ -105,26 +107,30 @@ export class ConnectToSqlDialog extends InitializingComponent {
|
||||
saveProfile: true,
|
||||
id: '',
|
||||
groupId: undefined,
|
||||
options: {}
|
||||
options: this.options
|
||||
};
|
||||
const result = await azdata.connection.connect(connectionProfile, false, false);
|
||||
if (result.connected) {
|
||||
connectionProfile.id = result.connectionId;
|
||||
const credentialProvider = await azdata.credentials.getProvider(credentialNamespace);
|
||||
if (connectionProfile.savePassword) {
|
||||
await credentialProvider.saveCredential(createCredentialId(this._controllerModel.info.id, this._miaaModel.info.resourceType, this._miaaModel.info.name), connectionProfile.password);
|
||||
await credentialProvider.saveCredential(createCredentialId(this._controllerModel.info.id, this._model.info.resourceType, this._model.info.name), connectionProfile.password);
|
||||
} else {
|
||||
await credentialProvider.deleteCredential(createCredentialId(this._controllerModel.info.id, this._miaaModel.info.resourceType, this._miaaModel.info.name));
|
||||
await credentialProvider.deleteCredential(createCredentialId(this._controllerModel.info.id, this._model.info.resourceType, this._model.info.name));
|
||||
}
|
||||
this._completionPromise.resolve(connectionProfile);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
vscode.window.showErrorMessage(loc.connectToSqlFailed(this.serverNameInputBox.value, result.errorMessage));
|
||||
vscode.window.showErrorMessage(this.connectionFailedMessage(result.errorMessage));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract get providerName(): string;
|
||||
|
||||
protected abstract connectionFailedMessage(error: any): string;
|
||||
|
||||
private handleCancel(): void {
|
||||
this._completionPromise.resolve(undefined);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ControllerModel } from '../../models/controllerModel';
|
||||
import { ControllerTreeNode } from './controllerTreeNode';
|
||||
import { TreeNode } from './treeNode';
|
||||
|
||||
const mementoToken = 'arcControllers';
|
||||
const mementoToken = 'arcDataControllers';
|
||||
|
||||
/**
|
||||
* The TreeDataProvider for the Azure Arc view, which displays a list of registered
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { MiaaResourceInfo, ResourceInfo, ResourceType } from 'arc';
|
||||
import { MiaaResourceInfo, PGResourceInfo, ResourceInfo, ResourceType } from 'arc';
|
||||
import * as vscode from 'vscode';
|
||||
import { UserCancelledError } from '../../common/api';
|
||||
import * as loc from '../../localizedConstants';
|
||||
@@ -102,7 +102,11 @@ export class ControllerTreeNode extends TreeNode {
|
||||
|
||||
switch (registration.instanceType) {
|
||||
case ResourceType.postgresInstances:
|
||||
const postgresModel = new PostgresModel(this.model, resourceInfo, registration);
|
||||
// Fill in the username too if we already have it
|
||||
(resourceInfo as PGResourceInfo).userName = (this.model.info.resources.find(info =>
|
||||
info.name === resourceInfo.name &&
|
||||
info.resourceType === resourceInfo.resourceType) as PGResourceInfo)?.userName;
|
||||
const postgresModel = new PostgresModel(this.model, resourceInfo, registration, this._treeDataProvider);
|
||||
node = new PostgresTreeNode(postgresModel, this.model, this._context);
|
||||
break;
|
||||
case ResourceType.sqlManagedInstances:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"extends": "../shared.tsconfig.json",
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"outDir": "./out",
|
||||
"lib": [
|
||||
"es6",
|
||||
|
||||
@@ -275,6 +275,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.31.tgz#b1a620b115c96db7b3bfdf0cf54aee0c57139245"
|
||||
integrity sha512-QcJ5ZczaXAqbVD3o8mw/mEBhRvO5UAdTtbvgwL/OgoWubvNBh6/MxLBAigtcgIFaq3shon9m3POIxQaLQt4fxQ==
|
||||
|
||||
agent-base@4, 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"
|
||||
|
||||
ajv@^6.5.5:
|
||||
version "6.12.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7"
|
||||
@@ -338,6 +345,16 @@ aws4@^1.8.0:
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
|
||||
integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
|
||||
|
||||
azdata-test@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/azdata-test/-/azdata-test-1.1.1.tgz#0feb13ec01397841f7bbce0ad25a41e4a3afb5b5"
|
||||
integrity sha512-+jZ5/4Orqt5Q2MLPqmV+iQjGYxG5FK4pC3UqTOPdSzlbs0JQhOXckC8Hmw8ZOS0N6Cn7mNDCpfaqEU9coTyVsw==
|
||||
dependencies:
|
||||
http-proxy-agent "^2.1.0"
|
||||
https-proxy-agent "^2.2.4"
|
||||
rimraf "^2.6.3"
|
||||
typemoq "^2.1.0"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
@@ -510,6 +527,18 @@ ecc-jsbn@~0.1.1:
|
||||
jsbn "~0.1.0"
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
es6-promise@^4.0.3:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
|
||||
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
@@ -647,6 +676,14 @@ html-escaper@^2.0.0:
|
||||
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"
|
||||
|
||||
http-signature@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||
@@ -656,6 +693,14 @@ http-signature@~1.2.0:
|
||||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
https-proxy-agent@^2.2.4:
|
||||
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"
|
||||
@@ -1204,7 +1249,7 @@ type-detect@4.0.8, type-detect@^4.0.8:
|
||||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
|
||||
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
|
||||
|
||||
typemoq@2.1.0:
|
||||
typemoq@2.1.0, 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==
|
||||
|
||||
@@ -338,11 +338,13 @@
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%vm_password_confirm%",
|
||||
"required": true,
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[\\W_])[A-Za-z\\d\\W_]{12,123}$",
|
||||
"description": "%vm_password_validation_error_message%"
|
||||
}]
|
||||
"validations": [
|
||||
{
|
||||
"type": "regex_match",
|
||||
"regex": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[\\W_])[A-Za-z\\d\\W_]{12,123}$",
|
||||
"description": "%vm_password_validation_error_message%"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -580,19 +582,22 @@
|
||||
"when": "type=azure-multi-device"
|
||||
}
|
||||
],
|
||||
"agreement": {
|
||||
"template": "%edge-agreement%",
|
||||
"links": [
|
||||
{
|
||||
"text": "%microsoft-privacy-statement%",
|
||||
"url": "https://go.microsoft.com/fwlink/?LinkId=853010"
|
||||
},
|
||||
{
|
||||
"text": "%edge-eula%",
|
||||
"url": "https://go.microsoft.com/fwlink/?linkid=2128283"
|
||||
}
|
||||
]
|
||||
}
|
||||
"agreements": [
|
||||
{
|
||||
"template": "%edge-agreement%",
|
||||
"links": [
|
||||
{
|
||||
"text": "%microsoft-privacy-statement%",
|
||||
"url": "https://go.microsoft.com/fwlink/?LinkId=853010"
|
||||
},
|
||||
{
|
||||
"text": "%edge-eula%",
|
||||
"url": "https://go.microsoft.com/fwlink/?linkid=2128283"
|
||||
}
|
||||
],
|
||||
"when": "true"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
"name": "azdata",
|
||||
"displayName": "%azdata.displayName%",
|
||||
"description": "%azdata.description%",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.3",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/extension.png",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": ">=1.25.0"
|
||||
"azdata": ">=1.26.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"*"
|
||||
|
||||
@@ -12,7 +12,7 @@ import * as constants from './constants';
|
||||
import * as loc from './localizedConstants';
|
||||
import { AzdataToolService } from './services/azdataToolService';
|
||||
|
||||
function throwIfNoAzdataOrEulaNotAccepted(azdata: IAzdataTool | undefined, eulaAccepted: boolean): asserts azdata {
|
||||
export function throwIfNoAzdataOrEulaNotAccepted(azdata: IAzdataTool | undefined, eulaAccepted: boolean): asserts azdata {
|
||||
throwIfNoAzdata(azdata);
|
||||
if (!eulaAccepted) {
|
||||
Logger.log(loc.eulaNotAccepted);
|
||||
@@ -45,47 +45,57 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
return {
|
||||
arc: {
|
||||
dc: {
|
||||
create: async (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string) => {
|
||||
create: async (
|
||||
namespace: string,
|
||||
name: string,
|
||||
connectivityMode: string,
|
||||
resourceGroup: string,
|
||||
location: string,
|
||||
subscription: string,
|
||||
profileName?: string,
|
||||
storageClass?: string,
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass);
|
||||
return azdataToolService.localAzdata.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars, session);
|
||||
},
|
||||
endpoint: {
|
||||
list: async () => {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.dc.endpoint.list();
|
||||
return azdataToolService.localAzdata.arc.dc.endpoint.list(additionalEnvVars, session);
|
||||
}
|
||||
},
|
||||
config: {
|
||||
list: async () => {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.dc.config.list();
|
||||
return azdataToolService.localAzdata.arc.dc.config.list(additionalEnvVars, session);
|
||||
},
|
||||
show: async () => {
|
||||
show: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.dc.config.show();
|
||||
return azdataToolService.localAzdata.arc.dc.config.show(additionalEnvVars, session);
|
||||
}
|
||||
}
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
delete: async (name: string) => {
|
||||
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.delete(name);
|
||||
return azdataToolService.localAzdata.arc.postgres.server.delete(name, additionalEnvVars, session);
|
||||
},
|
||||
list: async () => {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.list();
|
||||
return azdataToolService.localAzdata.arc.postgres.server.list(additionalEnvVars, session);
|
||||
},
|
||||
show: async (name: string) => {
|
||||
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.show(name);
|
||||
return azdataToolService.localAzdata.arc.postgres.server.show(name, additionalEnvVars, session);
|
||||
},
|
||||
edit: async (
|
||||
name: string,
|
||||
@@ -103,29 +113,30 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
workers?: number;
|
||||
},
|
||||
engineVersion?: string,
|
||||
additionalEnvVars?: { [key: string]: string; }) => {
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, engineVersion, additionalEnvVars);
|
||||
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, engineVersion, additionalEnvVars, session);
|
||||
}
|
||||
}
|
||||
},
|
||||
sql: {
|
||||
mi: {
|
||||
delete: async (name: string) => {
|
||||
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.delete(name);
|
||||
return azdataToolService.localAzdata.arc.sql.mi.delete(name, additionalEnvVars, session);
|
||||
},
|
||||
list: async () => {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.list();
|
||||
return azdataToolService.localAzdata.arc.sql.mi.list(additionalEnvVars, session);
|
||||
},
|
||||
show: async (name: string) => {
|
||||
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.show(name);
|
||||
return azdataToolService.localAzdata.arc.sql.mi.show(name, additionalEnvVars, session);
|
||||
},
|
||||
edit: async (
|
||||
name: string,
|
||||
@@ -135,10 +146,13 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
memoryLimit?: string;
|
||||
memoryRequest?: string;
|
||||
noWait?: boolean;
|
||||
}) => {
|
||||
},
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession
|
||||
) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.edit(name, args);
|
||||
return azdataToolService.localAzdata.arc.sql.mi.edit(name, args, additionalEnvVars, session);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,9 +162,13 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
throwIfNoAzdata(azdataToolService.localAzdata);
|
||||
return azdataToolService.localAzdata.getPath();
|
||||
},
|
||||
login: async (endpoint: string, username: string, password: string) => {
|
||||
login: async (endpoint: string, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.login(endpoint, username, password);
|
||||
return azdataToolService.localAzdata.login(endpoint, username, password, additionalEnvVars);
|
||||
},
|
||||
acquireSession: async (endpoint: string, username: string, password: string, additionEnvVars?: azdataExt.AdditionalEnvVars) => {
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata?.acquireSession(endpoint, username, password, additionEnvVars);
|
||||
},
|
||||
getSemVersion: async () => {
|
||||
await localAzdataDiscovered;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getPlatformDownloadLink, getPlatformReleaseVersion } from './azdataRele
|
||||
import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess';
|
||||
import { HttpClient } from './common/httpClient';
|
||||
import Logger from './common/logger';
|
||||
import { Deferred } from './common/promise';
|
||||
import { getErrorMessage, NoAzdataError, searchForCmd } from './common/utils';
|
||||
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataInstallKey, azdataUpdateKey, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
|
||||
import * as loc from './localizedConstants';
|
||||
@@ -31,7 +32,20 @@ export interface IAzdataTool extends azdataExt.IAzdataApi {
|
||||
* @param args The args to pass to azdata
|
||||
* @param parseResult A function used to parse out the raw result into the desired shape
|
||||
*/
|
||||
executeCommand<R>(args: string[], additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<R>>
|
||||
executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<R>>
|
||||
}
|
||||
|
||||
class AzdataSession implements azdataExt.AzdataSession {
|
||||
|
||||
private _session = new Deferred<void>();
|
||||
|
||||
public sessionEnded(): Promise<void> {
|
||||
return this._session.promise;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._session.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,6 +54,10 @@ export interface IAzdataTool extends azdataExt.IAzdataApi {
|
||||
export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
|
||||
private _semVersion: SemVer;
|
||||
private _currentSession: azdataExt.AzdataSession | undefined = undefined;
|
||||
private _currentlyExecutingCommands: Deferred<void>[] = [];
|
||||
private _queuedCommands: { deferred: Deferred<void>, session?: azdataExt.AzdataSession }[] = [];
|
||||
|
||||
constructor(private _path: string, version: string) {
|
||||
this._semVersion = new SemVer(version);
|
||||
}
|
||||
@@ -62,7 +80,17 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
|
||||
public arc = {
|
||||
dc: {
|
||||
create: (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
create: (
|
||||
namespace: string,
|
||||
name: string,
|
||||
connectivityMode: string,
|
||||
resourceGroup: string,
|
||||
location: string,
|
||||
subscription: string,
|
||||
profileName?: string,
|
||||
storageClass?: string,
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
const args = ['arc', 'dc', 'create',
|
||||
'--namespace', namespace,
|
||||
'--name', name,
|
||||
@@ -76,32 +104,32 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
if (storageClass) {
|
||||
args.push('--storage-class', storageClass);
|
||||
}
|
||||
return this.executeCommand<void>(args);
|
||||
return this.executeCommand<void>(args, additionalEnvVars, session);
|
||||
},
|
||||
endpoint: {
|
||||
list: (): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list']);
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list'], additionalEnvVars, session);
|
||||
}
|
||||
},
|
||||
config: {
|
||||
list: (): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list']);
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list'], additionalEnvVars, session);
|
||||
},
|
||||
show: (): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => {
|
||||
return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show']);
|
||||
show: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => {
|
||||
return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show'], additionalEnvVars, session);
|
||||
}
|
||||
}
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
delete: (name: string): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name, '--force']);
|
||||
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name, '--force'], additionalEnvVars, session);
|
||||
},
|
||||
list: (): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list']);
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list'], additionalEnvVars, session);
|
||||
},
|
||||
show: (name: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> => {
|
||||
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name]);
|
||||
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> => {
|
||||
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name], additionalEnvVars, session);
|
||||
},
|
||||
edit: (
|
||||
name: string,
|
||||
@@ -119,7 +147,8 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
workers?: number
|
||||
},
|
||||
engineVersion?: string,
|
||||
additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name];
|
||||
if (args.adminPassword) { argsArray.push('--admin-password'); }
|
||||
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
|
||||
@@ -133,20 +162,20 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); }
|
||||
if (args.workers) { argsArray.push('--workers', args.workers.toString()); }
|
||||
if (engineVersion) { argsArray.push('--engine-version', engineVersion); }
|
||||
return this.executeCommand<void>(argsArray, additionalEnvVars);
|
||||
return this.executeCommand<void>(argsArray, additionalEnvVars, session);
|
||||
}
|
||||
}
|
||||
},
|
||||
sql: {
|
||||
mi: {
|
||||
delete: (name: string): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name]);
|
||||
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name], additionalEnvVars, session);
|
||||
},
|
||||
list: (): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.SqlMiListResult[]>(['arc', 'sql', 'mi', 'list']);
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.SqlMiListResult[]>(['arc', 'sql', 'mi', 'list'], additionalEnvVars, session);
|
||||
},
|
||||
show: (name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> => {
|
||||
return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name]);
|
||||
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> => {
|
||||
return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name], additionalEnvVars, session);
|
||||
},
|
||||
edit: (
|
||||
name: string,
|
||||
@@ -156,21 +185,69 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
memoryLimit?: string,
|
||||
memoryRequest?: string,
|
||||
noWait?: boolean,
|
||||
}): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
},
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession
|
||||
): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
const argsArray = ['arc', 'sql', 'mi', 'edit', '-n', name];
|
||||
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
|
||||
if (args.coresRequest) { argsArray.push('--cores-request', args.coresRequest); }
|
||||
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
|
||||
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
|
||||
if (args.noWait) { argsArray.push('--no-wait'); }
|
||||
return this.executeCommand<void>(argsArray);
|
||||
return this.executeCommand<void>(argsArray, additionalEnvVars, session);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public login(endpoint: string, username: string, password: string): Promise<azdataExt.AzdataOutput<void>> {
|
||||
return this.executeCommand<void>(['login', '-e', endpoint, '-u', username], { 'AZDATA_PASSWORD': password });
|
||||
public async login(endpoint: string, username: string, password: string, additionalEnvVars: azdataExt.AdditionalEnvVars = {}): Promise<azdataExt.AzdataOutput<void>> {
|
||||
// Since login changes the context we want to wait until all currently executing commands are finished before this is executed
|
||||
while (this._currentlyExecutingCommands.length > 0) {
|
||||
await this._currentlyExecutingCommands[0];
|
||||
}
|
||||
// Logins need to be done outside the session aware logic so call impl directly
|
||||
return this.executeCommandImpl<void>(['login', '-e', endpoint, '-u', username], Object.assign({}, additionalEnvVars, { 'AZDATA_PASSWORD': password }));
|
||||
}
|
||||
|
||||
public async acquireSession(endpoint: string, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataSession> {
|
||||
const session = new AzdataSession();
|
||||
session.sessionEnded().then(async () => {
|
||||
// Wait for all commands running for this session to end
|
||||
while (this._currentlyExecutingCommands.length > 0) {
|
||||
await this._currentlyExecutingCommands[0].promise;
|
||||
}
|
||||
this._currentSession = undefined;
|
||||
// Start our next command now that we're all done with this session
|
||||
// TODO: Should we check if the command has a session that hasn't started? That should never happen..
|
||||
// TODO: Look into kicking off multiple commands
|
||||
this._queuedCommands.shift()?.deferred.resolve();
|
||||
});
|
||||
|
||||
// We're not in a session or waiting on anything so just set the current session right now
|
||||
if (!this._currentSession && this._queuedCommands.length === 0) {
|
||||
this._currentSession = session;
|
||||
} else {
|
||||
// We're in a session or another command is executing so add this to the end of the queued commands and wait our turn
|
||||
const deferred = new Deferred<void>();
|
||||
deferred.promise.then(() => {
|
||||
this._currentSession = session;
|
||||
// We've started a new session so look at all our queued commands and start
|
||||
// the ones for this session now.
|
||||
this._queuedCommands = this._queuedCommands.filter(c => {
|
||||
if (c.session === this._currentSession) {
|
||||
c.deferred.resolve();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
this._queuedCommands.push({ deferred, session: undefined });
|
||||
await deferred.promise;
|
||||
}
|
||||
|
||||
await this.login(endpoint, username, password, additionalEnvVars);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,7 +265,33 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
};
|
||||
}
|
||||
|
||||
public async executeCommand<R>(args: string[], additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<R>> {
|
||||
public async executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<R>> {
|
||||
if (this._currentSession && this._currentSession !== session) {
|
||||
const deferred = new Deferred<void>();
|
||||
this._queuedCommands.push({ deferred, session: session });
|
||||
await deferred.promise;
|
||||
}
|
||||
const executingDeferred = new Deferred<void>();
|
||||
this._currentlyExecutingCommands.push(executingDeferred);
|
||||
try {
|
||||
return await this.executeCommandImpl<R>(args, additionalEnvVars);
|
||||
}
|
||||
finally {
|
||||
this._currentlyExecutingCommands = this._currentlyExecutingCommands.filter(c => c !== executingDeferred);
|
||||
executingDeferred.resolve();
|
||||
// If there isn't an active session and we still have queued commands then we have to manually kick off the next one
|
||||
if (this._queuedCommands.length > 0 && !this._currentSession) {
|
||||
this._queuedCommands.shift()?.deferred.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the specified azdata command. This is NOT session-aware so should only be used for calls that don't care about a session
|
||||
* @param args The args to pass to azdata
|
||||
* @param additionalEnvVars Additional environment variables to set for this execution
|
||||
*/
|
||||
private async executeCommandImpl<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<R>> {
|
||||
try {
|
||||
const output = JSON.parse((await executeAzdataCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars)).stdout);
|
||||
return {
|
||||
@@ -609,7 +712,7 @@ async function discoverLatestStableAzdataVersionDarwin(): Promise<SemVer> {
|
||||
return new SemVer(azdataPackageVersionInfo.versions.stable);
|
||||
}
|
||||
|
||||
async function executeAzdataCommand(command: string, args: string[], additionalEnvVars: { [key: string]: string } = {}): Promise<ProcessOutput> {
|
||||
async function executeAzdataCommand(command: string, args: string[], additionalEnvVars: azdataExt.AdditionalEnvVars = {}): Promise<ProcessOutput> {
|
||||
additionalEnvVars = Object.assign(additionalEnvVars, { 'ACCEPT_EULA': 'yes' });
|
||||
const debug = vscode.workspace.getConfiguration(azdataConfigSection).get(debugConfigKey);
|
||||
if (debug) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AdditionalEnvVars } from 'azdata-ext';
|
||||
import * as cp from 'child_process';
|
||||
import * as sudo from 'sudo-prompt';
|
||||
import * as loc from '../localizedConstants';
|
||||
@@ -47,7 +48,7 @@ export type ProcessOutput = { stdout: string, stderr: string };
|
||||
* @param args Optional args to pass, every arg and arg value must be a separate item in the array
|
||||
* @param additionalEnvVars Additional environment variables to add to the process environment
|
||||
*/
|
||||
export async function executeCommand(command: string, args: string[], additionalEnvVars?: { [key: string]: string },): Promise<ProcessOutput> {
|
||||
export async function executeCommand(command: string, args: string[], additionalEnvVars?: AdditionalEnvVars): Promise<ProcessOutput> {
|
||||
return new Promise((resolve, reject) => {
|
||||
Logger.log(loc.executingCommand(command, args));
|
||||
const stdoutBuffers: Buffer[] = [];
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
export class Deferred<T> {
|
||||
promise: Promise<T>;
|
||||
resolve!: (value?: T | PromiseLike<T>) => void;
|
||||
resolve!: (value: T | PromiseLike<T>) => void;
|
||||
reject!: (reason?: any) => void;
|
||||
constructor() {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
|
||||
@@ -22,7 +22,7 @@ export class NoAzdataError extends Error implements azdataExt.ErrorWithLink {
|
||||
*/
|
||||
export function searchForCmd(exe: string): Promise<string> {
|
||||
// Note : This is separated out to allow for easy test stubbing
|
||||
return new Promise<string>((resolve, reject) => which(exe, (err, path) => err ? reject(err) : resolve(path)));
|
||||
return new Promise<string>((resolve, reject) => which(exe, (err, path) => err ? reject(err) : resolve(path || '')));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,9 +18,7 @@ export class AzdataToolService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the localAzdata that was last saved
|
||||
*
|
||||
* @param memento The memento that stores the localAzdata object
|
||||
* Sets the localAzdata object to be used for azdata operations
|
||||
*/
|
||||
set localAzdata(azdata: IAzdataTool | undefined) {
|
||||
this._localAzdata = azdata;
|
||||
|
||||
@@ -3,45 +3,126 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as childProcess from '../common/childProcess';
|
||||
import * as sinon from 'sinon';
|
||||
import * as should from 'should';
|
||||
import * as vscode from 'vscode';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import { getExtensionApi } from '../api';
|
||||
import { getExtensionApi, throwIfNoAzdataOrEulaNotAccepted } from '../api';
|
||||
import { AzdataToolService } from '../services/azdataToolService';
|
||||
import { assertRejected } from './testUtils';
|
||||
import { AzdataTool, IAzdataTool, AzdataDeployOption } from '../azdata';
|
||||
|
||||
describe('api', function (): void {
|
||||
afterEach(function (): void {
|
||||
sinon.restore();
|
||||
});
|
||||
describe('throwIfNoAzdataOrEulaNotAccepted', function (): void {
|
||||
it('throws when no azdata', function (): void {
|
||||
should(() => throwIfNoAzdataOrEulaNotAccepted(undefined, false)).throw();
|
||||
});
|
||||
it('throws when EULA not accepted', function (): void {
|
||||
should(() => throwIfNoAzdataOrEulaNotAccepted(TypeMoq.Mock.ofType<IAzdataTool>().object, false)).throw();
|
||||
});
|
||||
it('passes with AzdataTool and EULA accepted', function (): void {
|
||||
throwIfNoAzdataOrEulaNotAccepted(TypeMoq.Mock.ofType<IAzdataTool>().object, true);
|
||||
});
|
||||
});
|
||||
describe('getExtensionApi', function (): void {
|
||||
it('throws when no azdata', async function(): Promise<void> {
|
||||
it('throws when no azdata', async function (): Promise<void> {
|
||||
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
|
||||
const azdataToolService = new AzdataToolService();
|
||||
const api = getExtensionApi(mementoMock.object, azdataToolService, Promise.resolve(undefined));
|
||||
await assertRejected(api.isEulaAccepted(), 'isEulaAccepted');
|
||||
|
||||
await assertRejected(api.azdata.getPath(), 'getPath');
|
||||
await assertRejected(api.azdata.getSemVersion(), 'getSemVersion');
|
||||
await assertRejected(api.azdata.login('', '', ''), 'login');
|
||||
await assertRejected(api.azdata.version(), 'version');
|
||||
|
||||
await assertRejected(api.azdata.arc.dc.create('', '', '', '', '', ''), 'arc dc create');
|
||||
|
||||
await assertRejected(api.azdata.arc.dc.config.list(), 'arc dc config list');
|
||||
await assertRejected(api.azdata.arc.dc.config.show(), 'arc dc config show');
|
||||
|
||||
await assertRejected(api.azdata.arc.dc.endpoint.list(), 'arc dc endpoint list');
|
||||
|
||||
await assertRejected(api.azdata.arc.sql.mi.list(), 'arc sql mi list');
|
||||
await assertRejected(api.azdata.arc.sql.mi.delete(''), 'arc sql mi delete');
|
||||
await assertRejected(api.azdata.arc.sql.mi.show(''), 'arc sql mi show');
|
||||
|
||||
await assertRejected(api.azdata.arc.postgres.server.list(), 'arc sql postgres server list');
|
||||
await assertRejected(api.azdata.arc.postgres.server.delete(''), 'arc sql postgres server delete');
|
||||
await assertRejected(api.azdata.arc.postgres.server.show(''), 'arc sql postgres server show');
|
||||
await assertRejected(api.azdata.arc.postgres.server.edit('', { }), 'arc sql postgres server edit');
|
||||
await assertApiCalls(api, assertRejected);
|
||||
});
|
||||
|
||||
it('throws when EULA not accepted', async function (): Promise<void> {
|
||||
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
|
||||
mementoMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => false);
|
||||
const azdataToolService = new AzdataToolService();
|
||||
// Not using a mock here because it'll hang when resolving mocked objects
|
||||
const api = getExtensionApi(mementoMock.object, azdataToolService, Promise.resolve(new AzdataTool('', '1.0.0')));
|
||||
should(await api.isEulaAccepted()).be.false('EULA should not be accepted');
|
||||
await assertApiCalls(api, assertRejected);
|
||||
});
|
||||
|
||||
it('succeed when azdata present and EULA accepted', async function (): Promise<void> {
|
||||
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
|
||||
mementoMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => true);
|
||||
const azdataTool = new AzdataTool('', '1.0.0');
|
||||
const azdataToolService = new AzdataToolService();
|
||||
azdataToolService.localAzdata = azdataTool;
|
||||
// Not using a mock here because it'll hang when resolving mocked objects
|
||||
const api = getExtensionApi(mementoMock.object, azdataToolService, Promise.resolve(azdataTool));
|
||||
should(await api.isEulaAccepted()).be.true('EULA should be accepted');
|
||||
sinon.stub(childProcess, 'executeCommand').callsFake(async (_command, args) => {
|
||||
// Version needs to be valid so it can be parsed correctly
|
||||
if (args[0] === '--version') {
|
||||
return { stdout: `1.0.0`, stderr: '' };
|
||||
}
|
||||
console.log(args[0]);
|
||||
return { stdout: `{ }`, stderr: '' };
|
||||
});
|
||||
await assertApiCalls(api, async (promise, message) => {
|
||||
try {
|
||||
await promise;
|
||||
} catch (err) {
|
||||
throw new Error(`API call to ${message} should have succeeded. ${err}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('promptForEula', async function (): Promise<void> {
|
||||
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
|
||||
mementoMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => true);
|
||||
const azdataToolService = new AzdataToolService();
|
||||
// Not using a mock here because it'll hang when resolving mocked objects
|
||||
const api = getExtensionApi(mementoMock.object, azdataToolService, Promise.resolve(new AzdataTool('', '1.0.0')));
|
||||
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
|
||||
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => AzdataDeployOption.dontPrompt);
|
||||
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
|
||||
const showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage');
|
||||
should(await api.promptForEula()).be.false();
|
||||
should(showErrorMessageStub.called).be.true('User should have been prompted to accept');
|
||||
});
|
||||
|
||||
/**
|
||||
* Asserts that calls to the Azdata API behave as expected
|
||||
* @param api The API object to test the calls with
|
||||
* @param assertCallback The function to assert that the results are as expected
|
||||
*/
|
||||
async function assertApiCalls(api: azdataExt.IExtension, assertCallback: (promise: Promise<any>, message: string) => Promise<void>): Promise<void> {
|
||||
await assertCallback(api.azdata.getPath(), 'getPath');
|
||||
await assertCallback(api.azdata.getSemVersion(), 'getSemVersion');
|
||||
await assertCallback(api.azdata.login('', '', ''), 'login');
|
||||
await assertCallback((async () => {
|
||||
let session: azdataExt.AzdataSession | undefined;
|
||||
try {
|
||||
session = await api.azdata.acquireSession('', '', '');
|
||||
} finally {
|
||||
session?.dispose();
|
||||
}
|
||||
})(), 'acquireSession');
|
||||
await assertCallback(api.azdata.version(), 'version');
|
||||
|
||||
await assertCallback(api.azdata.arc.dc.create('', '', '', '', '', ''), 'arc dc create');
|
||||
|
||||
await assertCallback(api.azdata.arc.dc.config.list(), 'arc dc config list');
|
||||
await assertCallback(api.azdata.arc.dc.config.show(), 'arc dc config show');
|
||||
|
||||
await assertCallback(api.azdata.arc.dc.endpoint.list(), 'arc dc endpoint list');
|
||||
|
||||
await assertCallback(api.azdata.arc.sql.mi.list(), 'arc sql mi list');
|
||||
await assertCallback(api.azdata.arc.sql.mi.delete(''), 'arc sql mi delete');
|
||||
await assertCallback(api.azdata.arc.sql.mi.show(''), 'arc sql mi show');
|
||||
await assertCallback(api.azdata.arc.sql.mi.edit('', { }), 'arc sql mi edit');
|
||||
await assertCallback(api.azdata.arc.postgres.server.list(), 'arc sql postgres server list');
|
||||
await assertCallback(api.azdata.arc.postgres.server.delete(''), 'arc sql postgres server delete');
|
||||
await assertCallback(api.azdata.arc.postgres.server.show(''), 'arc sql postgres server show');
|
||||
await assertCallback(api.azdata.arc.postgres.server.edit('', {}), 'arc sql postgres server edit');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as should from 'should';
|
||||
import * as sinon from 'sinon';
|
||||
import * as vscode from 'vscode';
|
||||
@@ -16,6 +17,7 @@ import * as fs from 'fs';
|
||||
import { AzdataReleaseInfo } from '../azdataReleaseInfo';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import { eulaAccepted } from '../constants';
|
||||
import { sleep } from './testUtils';
|
||||
|
||||
const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', '0.0.0');
|
||||
const currentAzdataMock = new azdata.AzdataTool('/path/to/azdata', '9999.999.999');
|
||||
@@ -170,18 +172,6 @@ describe('azdata', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
it('login', async function (): Promise<void> {
|
||||
const endpoint = 'myEndpoint';
|
||||
const username = 'myUsername';
|
||||
const password = 'myPassword';
|
||||
await azdataTool.login(endpoint, username, password);
|
||||
verifyExecuteCommandCalledWithArgs(['login', endpoint, username]);
|
||||
});
|
||||
it('version', async function (): Promise<void> {
|
||||
executeCommandStub.resolves({ stdout: '1.0.0', stderr: '' });
|
||||
await azdataTool.version();
|
||||
verifyExecuteCommandCalledWithArgs(['--version']);
|
||||
});
|
||||
it('general error throws', async function (): Promise<void> {
|
||||
const err = new Error();
|
||||
executeCommandStub.throws(err);
|
||||
@@ -228,12 +218,136 @@ describe('azdata', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('login', async function (): Promise<void> {
|
||||
const endpoint = 'myEndpoint';
|
||||
const username = 'myUsername';
|
||||
const password = 'myPassword';
|
||||
await azdataTool.login(endpoint, username, password);
|
||||
verifyExecuteCommandCalledWithArgs(['login', endpoint, username]);
|
||||
});
|
||||
|
||||
describe('acquireSession', function (): void {
|
||||
it('calls login', async function (): Promise<void> {
|
||||
const endpoint = 'myEndpoint';
|
||||
const username = 'myUsername';
|
||||
const password = 'myPassword';
|
||||
const session = await azdataTool.acquireSession(endpoint, username, password);
|
||||
session.dispose();
|
||||
verifyExecuteCommandCalledWithArgs(['login', endpoint, username]);
|
||||
});
|
||||
|
||||
it('command executed under current session completes', async function (): Promise<void> {
|
||||
const session = await azdataTool.acquireSession('', '', '');
|
||||
try {
|
||||
await azdataTool.arc.dc.config.show(undefined, session);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
verifyExecuteCommandCalledWithArgs(['login'], 0);
|
||||
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
|
||||
});
|
||||
it('multiple commands executed under current session completes', async function (): Promise<void> {
|
||||
const session = await azdataTool.acquireSession('', '', '');
|
||||
try {
|
||||
// Kick off multiple commands at the same time and then ensure that they both complete
|
||||
await Promise.all([
|
||||
azdataTool.arc.dc.config.show(undefined, session),
|
||||
azdataTool.arc.sql.mi.list(undefined, session)
|
||||
]);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
verifyExecuteCommandCalledWithArgs(['login'], 0);
|
||||
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
|
||||
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 2);
|
||||
});
|
||||
it('command executed without session context is queued up until session is closed', async function (): Promise<void> {
|
||||
const session = await azdataTool.acquireSession('', '', '');
|
||||
let nonSessionCommand: Promise<any> | undefined = undefined;
|
||||
try {
|
||||
// Start one command in the current session
|
||||
await azdataTool.arc.dc.config.show(undefined, session);
|
||||
// Verify that the command isn't executed until after the session is disposed
|
||||
let isFulfilled = false;
|
||||
nonSessionCommand = azdataTool.arc.sql.mi.list().then(() => isFulfilled = true);
|
||||
await sleep(2000);
|
||||
should(isFulfilled).equal(false, 'The command should not be completed yet');
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
await nonSessionCommand;
|
||||
verifyExecuteCommandCalledWithArgs(['login'], 0);
|
||||
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
|
||||
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 2);
|
||||
});
|
||||
it('multiple commands executed without session context are queued up until session is closed', async function (): Promise<void> {
|
||||
const session = await azdataTool.acquireSession('', '', '');
|
||||
let nonSessionCommand1: Promise<any> | undefined = undefined;
|
||||
let nonSessionCommand2: Promise<any> | undefined = undefined;
|
||||
try {
|
||||
// Start one command in the current session
|
||||
await azdataTool.arc.dc.config.show(undefined, session);
|
||||
// Verify that neither command is completed until the session is closed
|
||||
let isFulfilled = false;
|
||||
nonSessionCommand1 = azdataTool.arc.sql.mi.list().then(() => isFulfilled = true);
|
||||
nonSessionCommand2 = azdataTool.arc.postgres.server.list().then(() => isFulfilled = true);
|
||||
await sleep(2000);
|
||||
should(isFulfilled).equal(false, 'The commands should not be completed yet');
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
await Promise.all([nonSessionCommand1, nonSessionCommand2]);
|
||||
verifyExecuteCommandCalledWithArgs(['login'], 0);
|
||||
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
|
||||
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 2);
|
||||
verifyExecuteCommandCalledWithArgs(['arc', 'postgres', 'server', 'list'], 3);
|
||||
});
|
||||
it('attempting to acquire a second session while a first is still active queues the second session', async function (): Promise<void> {
|
||||
const firstSession = await azdataTool.acquireSession('', '', '');
|
||||
let sessionPromise: Promise<azdataExt.AzdataSession> | undefined = undefined;
|
||||
let secondSessionCommand: Promise<any> | undefined = undefined;
|
||||
try {
|
||||
try {
|
||||
// Start one command in the current session
|
||||
await azdataTool.arc.dc.config.show(undefined, firstSession);
|
||||
// Verify that none of the commands for the second session are completed before the first is disposed
|
||||
let isFulfilled = false;
|
||||
sessionPromise = azdataTool.acquireSession('', '', '');
|
||||
sessionPromise.then(session => {
|
||||
isFulfilled = true;
|
||||
secondSessionCommand = azdataTool.arc.sql.mi.list(undefined, session).then(() => isFulfilled = true);
|
||||
});
|
||||
await sleep(2000);
|
||||
should(isFulfilled).equal(false, 'The commands should not be completed yet');
|
||||
} finally {
|
||||
firstSession.dispose();
|
||||
}
|
||||
} finally {
|
||||
(await sessionPromise)?.dispose();
|
||||
}
|
||||
should(secondSessionCommand).not.equal(undefined, 'The second command should have been queued already');
|
||||
await secondSessionCommand!;
|
||||
|
||||
|
||||
verifyExecuteCommandCalledWithArgs(['login'], 0);
|
||||
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
|
||||
verifyExecuteCommandCalledWithArgs(['login'], 2);
|
||||
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 3);
|
||||
});
|
||||
});
|
||||
|
||||
it('version', async function (): Promise<void> {
|
||||
executeCommandStub.resolves({ stdout: '1.0.0', stderr: '' });
|
||||
await azdataTool.version();
|
||||
verifyExecuteCommandCalledWithArgs(['--version']);
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies that the specified args were included in the call to executeCommand
|
||||
* @param args The args to check were included in the execute command call
|
||||
*/
|
||||
function verifyExecuteCommandCalledWithArgs(args: string[]): void {
|
||||
const commandArgs = executeCommandStub.args[0][1] as string[];
|
||||
function verifyExecuteCommandCalledWithArgs(args: string[], callIndex = 0): void {
|
||||
const commandArgs = executeCommandStub.args[callIndex][1] as string[];
|
||||
args.forEach(arg => should(commandArgs).containEql(arg));
|
||||
}
|
||||
|
||||
@@ -469,8 +583,8 @@ describe('azdata', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('promptForEula', function(): void {
|
||||
it('skipped because of config', async function(): Promise<void> {
|
||||
describe('promptForEula', function (): void {
|
||||
it('skipped because of config', async function (): Promise<void> {
|
||||
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
|
||||
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.dontPrompt);
|
||||
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
|
||||
@@ -479,7 +593,7 @@ describe('azdata', function () {
|
||||
should(result).be.false();
|
||||
});
|
||||
|
||||
it('always prompt if user requested', async function(): Promise<void> {
|
||||
it('always prompt if user requested', async function (): Promise<void> {
|
||||
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
|
||||
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.dontPrompt);
|
||||
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
|
||||
@@ -490,7 +604,7 @@ describe('azdata', function () {
|
||||
should(showInformationMessage.calledOnce).be.true('showInformationMessage should have been called to prompt user');
|
||||
});
|
||||
|
||||
it('prompt if config set to do so', async function(): Promise<void> {
|
||||
it('prompt if config set to do so', async function (): Promise<void> {
|
||||
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
|
||||
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.prompt);
|
||||
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
|
||||
@@ -501,7 +615,7 @@ describe('azdata', function () {
|
||||
should(showInformationMessage.calledOnce).be.true('showInformationMessage should have been called to prompt user');
|
||||
});
|
||||
|
||||
it('update config if user chooses not to prompt', async function(): Promise<void> {
|
||||
it('update config if user chooses not to prompt', async function (): Promise<void> {
|
||||
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
|
||||
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.prompt);
|
||||
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
|
||||
@@ -513,7 +627,7 @@ describe('azdata', function () {
|
||||
should(showInformationMessage.calledOnce).be.true('showInformationMessage should have been called to prompt user');
|
||||
});
|
||||
|
||||
it('user accepted EULA', async function(): Promise<void> {
|
||||
it('user accepted EULA', async function (): Promise<void> {
|
||||
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
|
||||
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.prompt);
|
||||
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
|
||||
@@ -525,7 +639,7 @@ describe('azdata', function () {
|
||||
should(showInformationMessage.calledOnce).be.true('showInformationMessage should have been called to prompt user');
|
||||
});
|
||||
|
||||
it('user accepted EULA - require user action', async function(): Promise<void> {
|
||||
it('user accepted EULA - require user action', async function (): Promise<void> {
|
||||
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
|
||||
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.prompt);
|
||||
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
|
||||
@@ -538,7 +652,7 @@ describe('azdata', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEulaAccepted', function(): void {
|
||||
describe('isEulaAccepted', function (): void {
|
||||
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
|
||||
mementoMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => true);
|
||||
should(azdata.isEulaAccepted(mementoMock.object)).be.true();
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('HttpClient', function (): void {
|
||||
it('errors on write stream error', async function (): Promise<void> {
|
||||
const downloadFolder = os.tmpdir();
|
||||
const mockWriteStream = new PassThrough();
|
||||
const deferredPromise = new Deferred();
|
||||
const deferredPromise = new Deferred<void>();
|
||||
sinon.stub(fs, 'createWriteStream').callsFake(() => {
|
||||
deferredPromise.resolve();
|
||||
return <any>mockWriteStream;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Deferred } from '../../common/promise';
|
||||
describe('DeferredPromise', function (): void {
|
||||
|
||||
it('Resolves correctly', async function(): Promise<void> {
|
||||
const deferred = new Deferred();
|
||||
const deferred = new Deferred<void>();
|
||||
deferred.resolve();
|
||||
await should(deferred.promise).be.resolved();
|
||||
});
|
||||
@@ -21,7 +21,7 @@ describe('DeferredPromise', function (): void {
|
||||
});
|
||||
|
||||
it('Chains then correctly', function(done): void {
|
||||
const deferred = new Deferred();
|
||||
const deferred = new Deferred<void>();
|
||||
deferred.then( () => {
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as should from 'should';
|
||||
import { searchForCmd as searchForExe } from '../../common/utils';
|
||||
import { NoAzdataError, searchForCmd as searchForExe } from '../../common/utils';
|
||||
|
||||
describe('utils', function () {
|
||||
describe('searchForExe', function (): void {
|
||||
@@ -14,4 +14,13 @@ describe('utils', function () {
|
||||
await should(searchForExe('someFakeExe')).be.rejected();
|
||||
});
|
||||
});
|
||||
|
||||
describe('NoAzdataError', function (): void {
|
||||
it('error contains message with and without links', function (): void {
|
||||
const error = new NoAzdataError();
|
||||
should(error.message).not.be.empty();
|
||||
should(error.messageWithLink).not.be.empty();
|
||||
should(error.message).not.equal(error.messageWithLink, 'Messages should not be equal');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,3 +18,7 @@ export async function assertRejected(promise: Promise<any>, message: string): Pr
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
export async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
|
||||
55
extensions/azdata/src/typings/azdata-ext.d.ts
vendored
55
extensions/azdata/src/typings/azdata-ext.d.ts
vendored
@@ -5,6 +5,7 @@
|
||||
|
||||
declare module 'azdata-ext' {
|
||||
import { SemVer } from 'semver';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
/**
|
||||
* Covers defining what the azdata extension exports to other extensions
|
||||
@@ -16,6 +17,8 @@ declare module 'azdata-ext' {
|
||||
name = 'Microsoft.azdata'
|
||||
}
|
||||
|
||||
export type AdditionalEnvVars = { [key: string]: string };
|
||||
|
||||
export interface ErrorWithLink extends Error {
|
||||
messageWithLink: string;
|
||||
}
|
||||
@@ -219,6 +222,17 @@ declare module 'azdata-ext' {
|
||||
state: string, // "Ready"
|
||||
logSearchDashboard: string, // https://127.0.0.1:30777/kibana/app/kibana#/discover?_a=(query:(language:kuery,query:'custom_resource_name:pg1'))
|
||||
metricsDashboard: string, // https://127.0.0.1:30777/grafana/d/40q72HnGk/sql-managed-instance-metrics?var-hostname=pg1
|
||||
podsStatus: {
|
||||
conditions: {
|
||||
lastTransitionTime: string, // "2020-08-19T17:05:39Z"
|
||||
message?: string, // "containers with unready status: [fluentbit postgres telegraf]"
|
||||
reason?: string, // "ContainersNotReady"
|
||||
status: string, // "True"
|
||||
type: string // "Ready"
|
||||
}[],
|
||||
name: string, // "pg-instancew-0",
|
||||
role: string // "worker"
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,23 +244,25 @@ declare module 'azdata-ext' {
|
||||
code?: number
|
||||
}
|
||||
|
||||
export interface AzdataSession extends vscode.Disposable { }
|
||||
|
||||
export interface IAzdataApi {
|
||||
arc: {
|
||||
dc: {
|
||||
create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string): Promise<AzdataOutput<void>>,
|
||||
create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>,
|
||||
endpoint: {
|
||||
list(): Promise<AzdataOutput<DcEndpointListResult[]>>
|
||||
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcEndpointListResult[]>>
|
||||
},
|
||||
config: {
|
||||
list(): Promise<AzdataOutput<DcConfigListResult[]>>,
|
||||
show(): Promise<AzdataOutput<DcConfigShowResult>>
|
||||
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcConfigListResult[]>>,
|
||||
show(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcConfigShowResult>>
|
||||
}
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
delete(name: string): Promise<AzdataOutput<void>>,
|
||||
list(): Promise<AzdataOutput<PostgresServerListResult[]>>,
|
||||
show(name: string): Promise<AzdataOutput<PostgresServerShowResult>>,
|
||||
delete(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>,
|
||||
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<PostgresServerListResult[]>>,
|
||||
show(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<PostgresServerShowResult>>,
|
||||
edit(
|
||||
name: string,
|
||||
args: {
|
||||
@@ -263,14 +279,16 @@ declare module 'azdata-ext' {
|
||||
workers?: number
|
||||
},
|
||||
engineVersion?: string,
|
||||
additionalEnvVars?: { [key: string]: string }): Promise<AzdataOutput<void>>
|
||||
additionalEnvVars?: AdditionalEnvVars,
|
||||
session?: AzdataSession
|
||||
): Promise<AzdataOutput<void>>
|
||||
}
|
||||
},
|
||||
sql: {
|
||||
mi: {
|
||||
delete(name: string): Promise<AzdataOutput<void>>,
|
||||
list(): Promise<AzdataOutput<SqlMiListResult[]>>,
|
||||
show(name: string): Promise<AzdataOutput<SqlMiShowResult>>,
|
||||
delete(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>,
|
||||
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<SqlMiListResult[]>>,
|
||||
show(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<SqlMiShowResult>>,
|
||||
edit(
|
||||
name: string,
|
||||
args: {
|
||||
@@ -279,13 +297,24 @@ declare module 'azdata-ext' {
|
||||
memoryLimit?: string,
|
||||
memoryRequest?: string,
|
||||
noWait?: boolean,
|
||||
}
|
||||
},
|
||||
additionalEnvVars?: AdditionalEnvVars,
|
||||
session?: AzdataSession
|
||||
): Promise<AzdataOutput<void>>
|
||||
}
|
||||
}
|
||||
},
|
||||
getPath(): Promise<string>,
|
||||
login(endpoint: string, username: string, password: string): Promise<AzdataOutput<any>>,
|
||||
login(endpoint: string, username: string, password: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<void>>,
|
||||
/**
|
||||
* Acquires a session for the specified controller, which will log in to the specified controller and then block all other commands
|
||||
* that are not part of the original session from executing until the session is released (disposed).
|
||||
* @param endpoint
|
||||
* @param username
|
||||
* @param password
|
||||
* @param additionalEnvVars
|
||||
*/
|
||||
acquireSession(endpoint: string, username: string, password: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataSession>,
|
||||
/**
|
||||
* The semVersion corresponding to this installation of azdata. version() method should have been run
|
||||
* before fetching this value to ensure that correct value is returned. This is almost always correct unless
|
||||
|
||||
@@ -321,9 +321,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/arm-resourcegraph": "^2.0.0",
|
||||
"@azure/arm-storage": "^15.1.0",
|
||||
"@azure/arm-subscriptions": "1.0.0",
|
||||
"axios": "^0.19.2",
|
||||
"axios": "^0.21.1",
|
||||
"qs": "^6.9.1",
|
||||
"vscode-nls": "^4.0.0",
|
||||
"ws": "^7.2.0"
|
||||
|
||||
@@ -214,7 +214,7 @@ export abstract class AzureAuth implements vscode.Disposable {
|
||||
|
||||
|
||||
|
||||
protected abstract async login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse, authComplete: Deferred<void, Error> }>;
|
||||
protected abstract login(tenant: Tenant, resource: Resource): Promise<{ response: OAuthTokenResponse, authComplete: Deferred<void, Error> }>;
|
||||
|
||||
/**
|
||||
* Refreshes a token, if a refreshToken is passed in then we use that. If it is not passed in then we will prompt the user for consent.
|
||||
|
||||
@@ -74,6 +74,11 @@ interface Settings {
|
||||
*/
|
||||
graphResource?: Resource;
|
||||
|
||||
/**
|
||||
* Information that describes the MS graph resource
|
||||
*/
|
||||
msGraphResource?: Resource;
|
||||
|
||||
/**
|
||||
* Information that describes the Azure resource management resource
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,17 @@ import { AzureResource } from 'azdata';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const enum SettingIds {
|
||||
marm = 'marm',
|
||||
graph = 'graph',
|
||||
msgraph = 'msgraph',
|
||||
arm = 'arm',
|
||||
sql = 'sql',
|
||||
ossrdbms = 'ossrdbms',
|
||||
vault = 'vault',
|
||||
ado = 'ado'
|
||||
}
|
||||
|
||||
const publicAzureSettings: ProviderSettings = {
|
||||
configKey: 'enablePublicCloud',
|
||||
metadata: {
|
||||
@@ -18,37 +29,42 @@ const publicAzureSettings: ProviderSettings = {
|
||||
host: 'https://login.microsoftonline.com/',
|
||||
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
||||
microsoftResource: {
|
||||
id: 'marm',
|
||||
id: SettingIds.marm,
|
||||
endpoint: 'https://management.core.windows.net/',
|
||||
azureResourceId: AzureResource.MicrosoftResourceManagement
|
||||
},
|
||||
graphResource: {
|
||||
id: 'graph',
|
||||
endpoint: 'https://graph.microsoft.com',
|
||||
id: SettingIds.graph,
|
||||
endpoint: 'https://graph.windows.net',
|
||||
azureResourceId: AzureResource.Graph
|
||||
},
|
||||
msGraphResource: {
|
||||
id: SettingIds.msgraph,
|
||||
endpoint: 'https://graph.microsoft.com/',
|
||||
azureResourceId: AzureResource.MsGraph
|
||||
},
|
||||
armResource: {
|
||||
id: 'arm',
|
||||
id: SettingIds.arm,
|
||||
endpoint: 'https://management.azure.com',
|
||||
azureResourceId: AzureResource.ResourceManagement
|
||||
},
|
||||
sqlResource: {
|
||||
id: 'sql',
|
||||
id: SettingIds.sql,
|
||||
endpoint: 'https://database.windows.net/',
|
||||
azureResourceId: AzureResource.Sql
|
||||
},
|
||||
ossRdbmsResource: {
|
||||
id: 'ossrdbms',
|
||||
id: SettingIds.ossrdbms,
|
||||
endpoint: 'https://ossrdbms-aad.database.windows.net',
|
||||
azureResourceId: AzureResource.OssRdbms
|
||||
},
|
||||
azureKeyVaultResource: {
|
||||
id: 'vault',
|
||||
id: SettingIds.vault,
|
||||
endpoint: 'https://vault.azure.net',
|
||||
azureResourceId: AzureResource.AzureKeyVault
|
||||
},
|
||||
azureDevOpsResource: {
|
||||
id: 'ado',
|
||||
id: SettingIds.ado,
|
||||
endpoint: '499b84ac-1321-427f-aa17-267ca6975798',
|
||||
azureResourceId: AzureResource.AzureDevOps,
|
||||
},
|
||||
@@ -72,32 +88,32 @@ const usGovAzureSettings: ProviderSettings = {
|
||||
host: 'https://login.microsoftonline.us/',
|
||||
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
||||
microsoftResource: {
|
||||
id: 'marm',
|
||||
id: SettingIds.marm,
|
||||
endpoint: 'https://management.core.usgovcloudapi.net/',
|
||||
azureResourceId: AzureResource.MicrosoftResourceManagement
|
||||
},
|
||||
graphResource: {
|
||||
id: 'graph',
|
||||
id: SettingIds.graph,
|
||||
endpoint: 'https://graph.windows.net',
|
||||
azureResourceId: AzureResource.Graph
|
||||
},
|
||||
armResource: {
|
||||
id: 'arm',
|
||||
id: SettingIds.arm,
|
||||
endpoint: 'https://management.usgovcloudapi.net',
|
||||
azureResourceId: AzureResource.ResourceManagement
|
||||
},
|
||||
sqlResource: {
|
||||
id: 'sql',
|
||||
id: SettingIds.sql,
|
||||
endpoint: 'https://database.usgovcloudapi.net/',
|
||||
azureResourceId: AzureResource.Sql
|
||||
},
|
||||
ossRdbmsResource: {
|
||||
id: 'ossrdbms',
|
||||
id: SettingIds.ossrdbms,
|
||||
endpoint: 'https://ossrdbms-aad.database.usgovcloudapi.net',
|
||||
azureResourceId: AzureResource.OssRdbms
|
||||
},
|
||||
azureKeyVaultResource: {
|
||||
id: 'vault',
|
||||
id: SettingIds.vault,
|
||||
endpoint: 'https://vault.usgovcloudapi.net',
|
||||
azureResourceId: AzureResource.AzureKeyVault
|
||||
},
|
||||
@@ -120,32 +136,32 @@ const usNatAzureSettings: ProviderSettings = {
|
||||
host: 'https://login.microsoftonline.eaglex.ic.gov/',
|
||||
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
||||
microsoftResource: {
|
||||
id: 'marm',
|
||||
id: SettingIds.marm,
|
||||
endpoint: 'https://management.azure.eaglex.ic.gov/',
|
||||
azureResourceId: AzureResource.MicrosoftResourceManagement
|
||||
},
|
||||
graphResource: {
|
||||
id: 'graph',
|
||||
id: SettingIds.graph,
|
||||
endpoint: 'https://graph.eaglex.ic.gov',
|
||||
azureResourceId: AzureResource.Graph
|
||||
},
|
||||
armResource: {
|
||||
id: 'arm',
|
||||
id: SettingIds.arm,
|
||||
endpoint: 'https://management.core.eaglex.ic.gov/',
|
||||
azureResourceId: AzureResource.ResourceManagement
|
||||
},
|
||||
sqlResource: {
|
||||
id: 'sql',
|
||||
id: SettingIds.sql,
|
||||
endpoint: 'https://database.cloudapi.eaglex.ic.gov/',
|
||||
azureResourceId: AzureResource.Sql
|
||||
},
|
||||
ossRdbmsResource: {
|
||||
id: 'ossrdbms',
|
||||
id: SettingIds.ossrdbms,
|
||||
endpoint: 'https://ossrdbms-aad.database.cloudapi.eaglex.ic.gov',
|
||||
azureResourceId: AzureResource.OssRdbms
|
||||
},
|
||||
azureKeyVaultResource: {
|
||||
id: 'vault',
|
||||
id: SettingIds.vault,
|
||||
endpoint: 'https://vault.cloudapi.eaglex.ic.gov',
|
||||
azureResourceId: AzureResource.AzureKeyVault
|
||||
},
|
||||
@@ -169,16 +185,24 @@ const germanyAzureSettings: ProviderSettings = {
|
||||
host: 'https://login.microsoftazure.de/',
|
||||
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
||||
graphResource: {
|
||||
id: 'https://graph.cloudapi.de/',
|
||||
endpoint: 'https://graph.cloudapi.de'
|
||||
id: SettingIds.graph,
|
||||
endpoint: 'https://graph.cloudapi.de',
|
||||
azureResourceId: AzureResource.Graph
|
||||
},
|
||||
msGraphResource: {
|
||||
id: SettingIds.msgraph,
|
||||
endpoint: 'https://graph.microsoft.de',
|
||||
azureResourceId: AzureResource.MsGraph
|
||||
},
|
||||
armResource: {
|
||||
id: 'https://management.core.cloudapi.de/',
|
||||
endpoint: 'https://management.microsoftazure.de'
|
||||
id: SettingIds.arm,
|
||||
endpoint: 'https://management.microsoftazure.de',
|
||||
azureResourceId: AzureResource.ResourceManagement
|
||||
},
|
||||
azureKeyVaultResource: {
|
||||
id: 'https://vault.microsoftazure.de',
|
||||
endpoint: 'https://vault.microsoftazure.de'
|
||||
id: SettingIds.vault,
|
||||
endpoint: 'https://vault.microsoftazure.de',
|
||||
azureResourceId: AzureResource.AzureKeyVault
|
||||
},
|
||||
redirectUri: 'https://vscode-redirect.azurewebsites.net/'
|
||||
}
|
||||
@@ -194,16 +218,24 @@ const chinaAzureSettings: ProviderSettings = {
|
||||
host: 'https://login.chinacloudapi.cn/',
|
||||
clientId: 'a69788c6-1d43-44ed-9ca3-b83e194da255',
|
||||
graphResource: {
|
||||
id: 'https://graph.chinacloudapi.cn/',
|
||||
endpoint: 'https://graph.chinacloudapi.cn'
|
||||
id: SettingIds.graph,
|
||||
endpoint: 'https://graph.chinacloudapi.cn',
|
||||
azureResourceId: AzureResource.Graph
|
||||
},
|
||||
msGraphResource: {
|
||||
id: SettingIds.msgraph,
|
||||
endpoint: 'https://microsoftgraph.chinacloudapi.cn',
|
||||
azureResourceId: AzureResource.MsGraph
|
||||
},
|
||||
armResource: {
|
||||
id: 'https://management.core.chinacloudapi.cn/',
|
||||
endpoint: 'https://managemement.chinacloudapi.net'
|
||||
id: SettingIds.arm,
|
||||
endpoint: 'https://managemement.chinacloudapi.net',
|
||||
azureResourceId: AzureResource.ResourceManagement
|
||||
},
|
||||
azureKeyVaultResource: {
|
||||
id: 'https://vault.azure.cn',
|
||||
endpoint: 'https://vault.azure.cn'
|
||||
id: SettingIds.vault,
|
||||
endpoint: 'https://vault.azure.cn',
|
||||
azureResourceId: AzureResource.AzureKeyVault
|
||||
},
|
||||
redirectUri: 'https://vscode-redirect.azurewebsites.net/'
|
||||
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
declare module 'azureResource' {
|
||||
import { TreeDataProvider } from 'vscode';
|
||||
import { DataProvider, Account, TreeItem } from 'azdata';
|
||||
import { FileShareItem, ListContainerItem } from '@azure/arm-storage/esm/models';
|
||||
export namespace azureResource {
|
||||
|
||||
/**
|
||||
* AzureCore core extension supports following resource types of Azure Resource Graph.
|
||||
* To add more resources, please refer this guide: https://docs.microsoft.com/en-us/azure/governance/resource-graph/reference/supported-tables-resources
|
||||
*/
|
||||
export const enum AzureResourceType {
|
||||
resourceGroup = 'microsoft.resources/subscriptions/resourcegroups',
|
||||
sqlServer = 'microsoft.sql/servers',
|
||||
@@ -77,10 +80,8 @@ declare module 'azureResource' {
|
||||
fullName: string;
|
||||
defaultDatabaseName: string;
|
||||
}
|
||||
export interface BlobContainer extends ListContainerItem {
|
||||
}
|
||||
export interface BlobContainer extends AzureResource { }
|
||||
|
||||
export interface FileShare extends FileShareItem {
|
||||
}
|
||||
export interface FileShare extends AzureResource { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,20 +53,32 @@ export class ConnectionDialogTreeProvider implements vscode.TreeDataProvider<Tre
|
||||
return [AzureResourceMessageTreeNode.create(localize('azure.resource.tree.treeProvider.loadingLabel', "Loading ..."), undefined)];
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.accounts && this.accounts.length > 0) {
|
||||
const accountNodes: FlatAccountTreeNode[] = [];
|
||||
for (const account of this.accounts) {
|
||||
if (this.accounts && this.accounts.length > 0) {
|
||||
const accountNodes: FlatAccountTreeNode[] = [];
|
||||
const errorMessages: string[] = [];
|
||||
// We are doing sequential account loading to avoid the Azure request throttling
|
||||
for (const account of this.accounts) {
|
||||
try {
|
||||
const accountNode = new FlatAccountTreeNode(account, this.appContext, this);
|
||||
await accountNode.updateLabel();
|
||||
accountNodes.push(accountNode);
|
||||
}
|
||||
return accountNodes;
|
||||
} else {
|
||||
return [new AzureResourceAccountNotSignedInTreeNode()];
|
||||
catch (error) {
|
||||
errorMessages.push(AzureResourceErrorMessageUtil.getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), undefined)];
|
||||
if (errorMessages.length > 0) {
|
||||
const showAccountsAction = localize('azure.resource.tree.treeProvider.openAccountsDialog', "Show Azure accounts");
|
||||
vscode.window.showErrorMessage(localize('azure.resource.tree.treeProvider.accountLoadError', "Failed to load some Azure accounts. {0}", errorMessages.join(',')), showAccountsAction).then(result => {
|
||||
if (result === showAccountsAction) {
|
||||
vscode.commands.executeCommand('azure.resource.signin');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return accountNodes;
|
||||
} else {
|
||||
return [new AzureResourceAccountNotSignedInTreeNode()];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ async function getSubscriptionInfo(account: AzureAccount, subscriptionService: I
|
||||
subscriptions.push(...(await subscriptionService.getSubscriptions(account, new TokenCredentials(token.token, token.tokenType), tenant.id) || <azureResource.AzureResourceSubscription[]>[]));
|
||||
}
|
||||
} catch (error) {
|
||||
throw new AzureResourceCredentialError(localize('azure.resource.tree.accountTreeNode.credentialError', "Failed to get credential for account {0}. Please refresh the account.", account.key.accountId), error);
|
||||
throw new AzureResourceCredentialError(localize('azure.resource.tree.accountTreeNode.credentialError', "Failed to get credential for account {0}. Please go to the accounts dialog and refresh the account.", account.key.accountId), error);
|
||||
}
|
||||
const total = subscriptions.length;
|
||||
let selected = total;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user