* Add ui based connection dialog

* Updating connection dialog icon

* Adding recent tab

* Adding icons for connection dialog

* adding form

* feat: Update connection dialog to include account information and support for updating connections

* feat: Add support for editing connections in connection dialog

* More fixes

* adding icon and fixing form

* Adding more fields and action buttons

* feat: Fix action button length check in ConnectionInfoFormContainer

* add validation messages

* feat: Add validation messages and fix action button length check in ConnectionInfoFormContainer

* Update ConnectionInfoFormContainer to use horizontal orientation for checkbox fields

* Adding boiler plate code for connectivity

* rewriting profile

* Adding some validations and adding basic connect method

* feat: Add Azure sign-in functionality to ConnectionDialogWebViewController

* Add validation messages and fix action button length check in ConnectionInfoFormContainer

* connection dialog connect code

* chore: Rename enablePreviewFeatures configuration option to enableExperimentalFeatures

* Adding prompt free connection handling

* Fix recent connection profile name

* fix: Set connection status to error when form validation fails

* Fixing edits

* Adding code to select and focus the connection node after it gets added

* reverting back extension launch

* chore: Update connection edit label to "Edit Connection"

* Fixing icons and form component values not properly being set

* Fixing connection profile field clearing logic

* Rewriting connection password handling in ConnectionDialogWebViewController for conn string

* fix: Handle case when 'Password=' is not found in connection string

* Adding database option

* Hiding old add connection when experimental features are enabled.

* Hiding duplicate connection

* downgrading vscode types

* Adding loading icon
This commit is contained in:
Aasim Khan 2024-07-25 11:05:10 -07:00 коммит произвёл GitHub
Родитель 62294a9d86
Коммит a3c514003b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
29 изменённых файлов: 1522 добавлений и 60 удалений

Просмотреть файл

@ -220,6 +220,7 @@ async function generateReactWebviewsBundle() {
*/
entryPoints: {
tableDesigner: 'src/reactviews/pages/TableDesigner/index.tsx',
connectionDialog: 'src/reactviews/pages/ConnectionDialog/index.tsx',
},
bundle: true,
outdir: 'out/src/reactviews/assets',
@ -230,6 +231,7 @@ async function generateReactWebviewsBundle() {
'.tsx': 'tsx',
'.ts': 'ts',
'.css': 'css',
'.svg': 'dataurl'
},
tsconfig: './tsconfig.react.json',
plugins: [
@ -441,7 +443,7 @@ gulp.task('watch-tests', function () {
});
gulp.task('watch-reactviews', function () {
return gulp.watch('./src/reactviews/**/*', gulp.series('ext:compile-reactviews'))
return gulp.watch(['./src/reactviews/**/*', './typings/**/*', './src/sharedInterfaces/**/*'], gulp.series('ext:compile-reactviews'))
});
// Do a full build first so we have the latest compiled files before we start watching for more changes

Просмотреть файл

@ -434,6 +434,9 @@
<trans-unit id="mssql.editTable">
<source xml:lang="en">Edit Table</source>
</trans-unit>
<trans-unit id="mssql.editConnection">
<source xml:lang="en">Edit Connection</source>
</trans-unit>
</body>
</file>
</xliff>

3
media/addConnection.svg Normal file
Просмотреть файл

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048">
<path d="M256 1216q0-90 34-172t97-145l211-211 634 634-211 211q-63 63-145 97t-172 34q-73 0-141-22t-127-67l-327 326-90-90 326-327q-44-58-66-126t-23-142zm448 323q63 0 110-17t88-48 76-69 79-81L596 863q-41 41-80 78t-71 77-49 88-19 110q0 69 25 128t70 102 104 68 128 25zm960-835q0 89-34 171t-97 146l-212 211-633-633 211-212q63-63 146-95t171-32q72 0 141 21t127 64l327-326 90 90-326 327q44 58 66 126t23 142zm-340 354q41-40 80-77t70-78 50-89 19-110q0-69-25-128t-70-102-104-68-128-25q-63 0-110 18t-88 47-77 69-79 81l462 462zm724 734h-256v256h-128v-256h-256v-128h256v-256h128v256h256v128z" />
</svg>

После

Ширина:  |  Высота:  |  Размер: 654 B

Просмотреть файл

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048">
<path d="M256 1216q0-90 34-172t97-145l211-211 634 634-211 211q-63 63-145 97t-172 34q-73 0-141-22t-127-67l-327 326-90-90 326-327q-44-58-66-126t-23-142zm448 323q63 0 110-17t88-48 76-69 79-81L596 863q-41 41-80 78t-71 77-49 88-19 110q0 69 25 128t70 102 104 68 128 25zm960-835q0 89-34 171t-97 146l-212 211-633-633 211-212q63-63 146-95t171-32q72 0 141 21t127 64l327-326 90 90-326 327q44 58 66 126t23 142zm-340 354q41-40 80-77t70-78 50-89 19-110q0-69-25-128t-70-102-104-68-128-25q-63 0-110 18t-88 47-77 69-79 81l462 462zm724 734h-256v256h-128v-256h-256v-128h256v-256h128v256h256v128z" fill="#ffffff"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 668 B

Просмотреть файл

@ -0,0 +1,11 @@
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.75">
<path d="M10 4V5H7V4H10ZM7 9H10V8H7V9ZM7 11H10V10H7V11Z" fill="#1F1F1F" />
</g>
<path opacity="0.1" d="M3 8.474C2.6717 8.474 2.34661 8.40934 2.04329 8.2837C1.73998 8.15806 1.46438 7.97391 1.23223 7.74177C0.763392 7.27293 0.5 6.63704 0.5 5.974V3.5H5.5V5.974C5.5 6.63704 5.23661 7.27293 4.76777 7.74177C4.29893 8.21061 3.66304 8.474 3 8.474V8.474Z" fill="#1F1F1F" />
<path d="M5 3V1C5 0.867392 4.94732 0.740215 4.85355 0.646447C4.75979 0.552678 4.63261 0.5 4.5 0.5C4.36739 0.5 4.24021 0.552678 4.14645 0.646447C4.05268 0.740215 4 0.867392 4 1V3H2V1C2 0.867392 1.94732 0.740215 1.85355 0.646447C1.75979 0.552678 1.63261 0.5 1.5 0.5C1.36739 0.5 1.24021 0.552678 1.14645 0.646447C1.05268 0.740215 1 0.867392 1 1V3H0V5.974C0.000245693 6.68278 0.251437 7.36859 0.709048 7.90985C1.16666 8.45111 1.80113 8.81287 2.5 8.931V12H3.5V8.931C4.19887 8.81287 4.83334 8.45111 5.29095 7.90985C5.74856 7.36859 5.99975 6.68278 6 5.974V3H5ZM5 5.974C5 6.50443 4.78929 7.01314 4.41421 7.38821C4.03914 7.76329 3.53043 7.974 3 7.974C2.46957 7.974 1.96086 7.76329 1.58579 7.38821C1.21071 7.01314 1 6.50443 1 5.974V4H5V5.974Z" fill="#1F1F1F" />
<path opacity="0.1" d="M15 14.5H2L2.75 13H5V12.5L5.5 13H11.5L12 12.5V10.5H13L15 14.5Z" fill="#1F1F1F" />
<path d="M15.447 14.276L15 15H2L1.553 14.276L2.191 13H3.309L2.809 14H14.191L12.691 11H12V10H13L13.447 10.276L15.447 14.276Z" fill="#1F1F1F" />
<path opacity="0.1" d="M11.5 2.5V12.5H5.5V9.1C5.96914 8.72541 6.34768 8.24975 6.6074 7.7085C6.86712 7.16724 7.00132 6.57434 7 5.974V2H6V1.653C6.14967 1.55752 6.32253 1.50462 6.5 1.5H10.5C10.7652 1.5 11.0196 1.60536 11.2071 1.79289C11.3946 1.98043 11.5 2.23478 11.5 2.5Z" fill="#1F1F1F" />
<path d="M12 2.5V12.5L11.5 13H5.5L5 12.5V9.437C5.37601 9.21989 5.71383 8.94253 6 8.616V12H11V2.5C11 2.36739 10.9473 2.24021 10.8536 2.14645C10.7598 2.05268 10.6326 2 10.5 2H6V1.092C6.16013 1.03278 6.32928 1.00166 6.5 1H10.5C10.8978 1 11.2794 1.15804 11.5607 1.43934C11.842 1.72064 12 2.10218 12 2.5Z" fill="#1F1F1F" />
</svg>

После

Ширина:  |  Высота:  |  Размер: 2.0 KiB

Просмотреть файл

@ -0,0 +1,11 @@
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.75">
<path d="M10 4V5H7V4H10ZM7 9H10V8H7V9ZM7 11H10V10H7V11Z" fill="#fff" />
</g>
<path opacity="0.1" d="M3 8.474C2.6717 8.474 2.34661 8.40934 2.04329 8.2837C1.73998 8.15806 1.46438 7.97391 1.23223 7.74177C0.763392 7.27293 0.5 6.63704 0.5 5.974V3.5H5.5V5.974C5.5 6.63704 5.23661 7.27293 4.76777 7.74177C4.29893 8.21061 3.66304 8.474 3 8.474V8.474Z" fill="#fff" />
<path d="M5 3V1C5 0.867392 4.94732 0.740215 4.85355 0.646447C4.75979 0.552678 4.63261 0.5 4.5 0.5C4.36739 0.5 4.24021 0.552678 4.14645 0.646447C4.05268 0.740215 4 0.867392 4 1V3H2V1C2 0.867392 1.94732 0.740215 1.85355 0.646447C1.75979 0.552678 1.63261 0.5 1.5 0.5C1.36739 0.5 1.24021 0.552678 1.14645 0.646447C1.05268 0.740215 1 0.867392 1 1V3H0V5.974C0.000245693 6.68278 0.251437 7.36859 0.709048 7.90985C1.16666 8.45111 1.80113 8.81287 2.5 8.931V12H3.5V8.931C4.19887 8.81287 4.83334 8.45111 5.29095 7.90985C5.74856 7.36859 5.99975 6.68278 6 5.974V3H5ZM5 5.974C5 6.50443 4.78929 7.01314 4.41421 7.38821C4.03914 7.76329 3.53043 7.974 3 7.974C2.46957 7.974 1.96086 7.76329 1.58579 7.38821C1.21071 7.01314 1 6.50443 1 5.974V4H5V5.974Z" fill="#fff" />
<path opacity="0.1" d="M15 14.5H2L2.75 13H5V12.5L5.5 13H11.5L12 12.5V10.5H13L15 14.5Z" fill="#fff" />
<path d="M15.447 14.276L15 15H2L1.553 14.276L2.191 13H3.309L2.809 14H14.191L12.691 11H12V10H13L13.447 10.276L15.447 14.276Z" fill="#fff" />
<path opacity="0.1" d="M11.5 2.5V12.5H5.5V9.1C5.96914 8.72541 6.34768 8.24975 6.6074 7.7085C6.86712 7.16724 7.00132 6.57434 7 5.974V2H6V1.653C6.14967 1.55752 6.32253 1.50462 6.5 1.5H10.5C10.7652 1.5 11.0196 1.60536 11.2071 1.79289C11.3946 1.98043 11.5 2.23478 11.5 2.5Z" fill="#fff" />
<path d="M12 2.5V12.5L11.5 13H5.5L5 12.5V9.437C5.37601 9.21989 5.71383 8.94253 6 8.616V12H11V2.5C11 2.36739 10.9473 2.24021 10.8536 2.14645C10.7598 2.05268 10.6326 2 10.5 2H6V1.092C6.16013 1.03278 6.32928 1.00166 6.5 1H10.5C10.8978 1 11.2794 1.15804 11.5607 1.43934C11.842 1.72064 12 2.10218 12 2.5Z" fill="#fff" />
</svg>

После

Ширина:  |  Высота:  |  Размер: 2.0 KiB

25
media/sqlServer.svg Normal file
Просмотреть файл

@ -0,0 +1,25 @@
<svg id="b1cfe86c-f00b-4507-bce2-50d07e45fe96" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<title>Icon-databases-132</title>
<path d="
M6.84,
5.09c-3.5,
0-6.34-1-6.34-2.3V15c0,
1.26,
2.79,
2.28,
6.25,
2.3h.09c3.5,
0,
6.34-1,
6.34-2.3V2.79C13.18,
4.06,
10.34,
5.09,
6.84,
5.09Z
" fill="#0000" stroke="#000" stroke-width="0.2px" />
<path d="M13.18,2.79c0,1.27-2.84,2.3-6.34,2.3S.5,4.06.5,2.79,3.34.49,6.84.49s6.34,1,6.34,2.3" fill="#0000" stroke="#000" stroke-width="0.4px" />
<path d="M11.7,2.6c0,.81-2.18,1.46-4.86,1.46S2,3.41,2,2.6,4.16,1.14,6.84,1.14,11.7,1.8,11.7,2.6" fill="#0000" stroke="#000" stroke-width="0.4px" />
<path d="M10.74,11.1V7.72H9.81v4.14h2.46V11.1ZM3.59,9.43a1.92,1.92,0,0,1-.51-.31A.44.44,0,0,1,3,8.8a.38.38,0,0,1,.16-.31.72.72,0,0,1,.42-.11,1.67,1.67,0,0,1,1,.29V7.81a2.67,2.67,0,0,0-1-.16A1.74,1.74,0,0,0,2.38,8a1.13,1.13,0,0,0-.41.9c0,.51.32.91,1,1.21a2.9,2.9,0,0,1,.61.36.4.4,0,0,1,.16.32.38.38,0,0,1-.16.31.75.75,0,0,1-.45.12A1.6,1.6,0,0,1,2,10.77v.93a2.29,2.29,0,0,0,1.07.23,2,2,0,0,0,1.18-.32,1.1,1.1,0,0,0,.43-.92,1,1,0,0,0-.25-.7A2.42,2.42,0,0,0,3.59,9.43ZM8.79,11a2.4,2.4,0,0,0,.33-1.27,2.32,2.32,0,0,0-.25-1.1,1.81,1.81,0,0,0-.7-.75,2,2,0,0,0-1-.26,2.18,2.18,0,0,0-1.09.27,1.87,1.87,0,0,0-.73.77,2.41,2.41,0,0,0-.26,1.15,2.26,2.26,0,0,0,.24,1.05,1.83,1.83,0,0,0,.68.75,2,2,0,0,0,1,.29l.85,1H9.05l-1.2-1.11A1.81,1.81,0,0,0,8.79,11Zm-.93-.26a1,1,0,0,1-1.53,0,1.51,1.51,0,0,1-.28-1,1.48,1.48,0,0,1,.29-1,.92.92,0,0,1,.78-.37.89.89,0,0,1,.75.37,1.62,1.62,0,0,1,.27,1A1.46,1.46,0,0,1,7.86,10.77Z" fill="#000" />
<path d="M14.81,17.49l.24-.79.47-.27.81.36.52-.53V16.2l-.37-.71.22-.5.81-.29.09,0v-.73l-.1,0-.8-.24-.26-.46.35-.82-.53-.51H16.2l-.71.36L15,12l-.32-.89h-.74l0,.11-.24.79-.51.22-.87-.4-.51.53.05.1.38.74-.2.51L11.1,14v.74l.11,0,.79.24.22.51-.39.86.53.52.09-.05.74-.38.51.2.34.89h.73Zm-1.2-2.36a1.06,1.06,0,1,1,1.49-1.52,1.06,1.06,0,0,1-1.49,1.52Z" fill="#000"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.9 KiB

Просмотреть файл

@ -0,0 +1,25 @@
<svg id="b1cfe86c-f00b-4507-bce2-50d07e45fe96" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<title>Icon-databases-132</title>
<path d="
M6.84,
5.09c-3.5,
0-6.34-1-6.34-2.3V15c0,
1.26,
2.79,
2.28,
6.25,
2.3h.09c3.5,
0,
6.34-1,
6.34-2.3V2.79C13.18,
4.06,
10.34,
5.09,
6.84,
5.09Z
" fill="#0000" stroke="#fff" stroke-width="0.2px" />
<path d="M13.18,2.79c0,1.27-2.84,2.3-6.34,2.3S.5,4.06.5,2.79,3.34.49,6.84.49s6.34,1,6.34,2.3" fill="#0000" stroke="#fff" stroke-width="0.2px" />
<path d="M11.7,2.6c0,.81-2.18,1.46-4.86,1.46S2,3.41,2,2.6,4.16,1.14,6.84,1.14,11.7,1.8,11.7,2.6" fill="#0000" stroke="#fff" stroke-width="0.2px" />
<path d="M10.74,11.1V7.72H9.81v4.14h2.46V11.1ZM3.59,9.43a1.92,1.92,0,0,1-.51-.31A.44.44,0,0,1,3,8.8a.38.38,0,0,1,.16-.31.72.72,0,0,1,.42-.11,1.67,1.67,0,0,1,1,.29V7.81a2.67,2.67,0,0,0-1-.16A1.74,1.74,0,0,0,2.38,8a1.13,1.13,0,0,0-.41.9c0,.51.32.91,1,1.21a2.9,2.9,0,0,1,.61.36.4.4,0,0,1,.16.32.38.38,0,0,1-.16.31.75.75,0,0,1-.45.12A1.6,1.6,0,0,1,2,10.77v.93a2.29,2.29,0,0,0,1.07.23,2,2,0,0,0,1.18-.32,1.1,1.1,0,0,0,.43-.92,1,1,0,0,0-.25-.7A2.42,2.42,0,0,0,3.59,9.43ZM8.79,11a2.4,2.4,0,0,0,.33-1.27,2.32,2.32,0,0,0-.25-1.1,1.81,1.81,0,0,0-.7-.75,2,2,0,0,0-1-.26,2.18,2.18,0,0,0-1.09.27,1.87,1.87,0,0,0-.73.77,2.41,2.41,0,0,0-.26,1.15,2.26,2.26,0,0,0,.24,1.05,1.83,1.83,0,0,0,.68.75,2,2,0,0,0,1,.29l.85,1H9.05l-1.2-1.11A1.81,1.81,0,0,0,8.79,11Zm-.93-.26a1,1,0,0,1-1.53,0,1.51,1.51,0,0,1-.28-1,1.48,1.48,0,0,1,.29-1,.92.92,0,0,1,.78-.37.89.89,0,0,1,.75.37,1.62,1.62,0,0,1,.27,1A1.46,1.46,0,0,1,7.86,10.77Z" fill="#fff" />
<path d="M14.81,17.49l.24-.79.47-.27.81.36.52-.53V16.2l-.37-.71.22-.5.81-.29.09,0v-.73l-.1,0-.8-.24-.26-.46.35-.82-.53-.51H16.2l-.71.36L15,12l-.32-.89h-.74l0,.11-.24.79-.51.22-.87-.4-.51.53.05.1.38.74-.2.51L11.1,14v.74l.11,0,.79.24.22.51-.39.86.53.52.09-.05.74-.38.51.2.34.89h.73Zm-1.2-2.36a1.06,1.06,0,1,1,1.49-1.52,1.06,1.06,0,0,1-1.49,1.52Z" fill="#fff" stroke="black" stroke-width="0.2px" />
</svg>

После

Ширина:  |  Высота:  |  Размер: 2.0 KiB

Просмотреть файл

@ -85,7 +85,7 @@
"@types/sinon": "^10.0.12",
"@types/tmp": "0.0.28",
"@types/underscore": "1.8.3",
"@types/vscode": "1.78.1",
"@types/vscode": "1.83.1",
"@types/vscode-webview": "^1.57.5",
"@vscode/test-electron": "^2.3.9",
"@xmldom/xmldom": "0.8.4",
@ -270,7 +270,13 @@
"view/title": [
{
"command": "mssql.addObjectExplorer",
"when": "view == objectExplorer",
"when": "view == objectExplorer && !config.mssql.enableExperimentalFeatures",
"title": "%mssql.addObjectExplorer%",
"group": "navigation"
},
{
"command": "mssql.addObjectExplorer2",
"when": "view == objectExplorer && config.mssql.enableExperimentalFeatures",
"title": "%mssql.addObjectExplorer%",
"group": "navigation"
},
@ -316,6 +322,11 @@
"when": "view == objectExplorer && viewItem =~ /^(disconnectedServer|Server)$/",
"group": "MS_SQL@4"
},
{
"command": "mssql.editConnection",
"when": "view == objectExplorer && viewItem =~ /^(disconnectedServer|Server)$/",
"group": "inline"
},
{
"command": "mssql.refreshObjectExplorerNode",
"when": "view == objectExplorer && viewItem != disconnectedServer",
@ -386,6 +397,10 @@
"command": "mssql.removeObjectExplorerNode",
"when": "view == objectExplorer && viewItem =~ /^(disconnectedServer|Server)$/"
},
{
"command": "mssql.editConnection",
"when": "view == objectExplorer && viewItem =~ /^(disconnectedServer|Server)$/"
},
{
"command": "mssql.refreshObjectExplorerNode",
"when": "view == objectExplorer && viewItem != disconnectedServer"
@ -532,6 +547,12 @@
"category": "MS SQL",
"icon": "$(add)"
},
{
"command": "mssql.addObjectExplorer2",
"title": "%mssql.addObjectExplorer%",
"category": "MS SQL",
"icon": "$(add)"
},
{
"command": "mssql.objectExplorer.enableGroupBySchema",
"title": "%mssql.objectExplorer.enableGroupBySchema%",
@ -560,6 +581,12 @@
"title": "%mssql.removeObjectExplorerNode%",
"category": "MS SQL"
},
{
"command": "mssql.editConnection",
"title": "%mssql.editConnection%",
"category": "MS SQL",
"icon": "$(edit)"
},
{
"command": "mssql.refreshObjectExplorerNode",
"title": "%mssql.refreshObjectExplorerNode%",

Просмотреть файл

@ -142,5 +142,6 @@
"mssql.objectExplorer.disableGroupBySchema":"Disable Group By Schema",
"mssql.objectExplorer.expandTimeout":"The timeout in seconds for expanding a node in Object Explorer. The default value is 45 seconds.",
"mssql.newTable":"New Table",
"mssql.editTable":"Edit Table"
"mssql.editTable":"Edit Table",
"mssql.editConnection":"Edit Connection"
}

Просмотреть файл

@ -0,0 +1,563 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ReactWebViewPanelController } from "../controllers/reactWebviewController";
import { ApiStatus, AuthenticationType, ConnectionDialogWebviewState, FormComponent, FormComponentActionButton, FormComponentOptions, FormComponentType, FormEvent, FormTabs, IConnectionDialogProfile } from '../sharedInterfaces/connectionDialog';
import { IConnectionInfo } from 'vscode-mssql';
import MainController from '../controllers/mainController';
import { getConnectionDisplayName } from '../models/connectionInfo';
import { AzureController } from '../azure/azureController';
import { ObjectExplorerProvider } from '../objectExplorer/objectExplorerProvider';
export class ConnectionDialogWebViewController extends ReactWebViewPanelController<ConnectionDialogWebviewState> {
private _connectionToEditCopy: IConnectionDialogProfile | undefined;
constructor(
context: vscode.ExtensionContext,
private _mainController: MainController,
private _objectExplorerProvider: ObjectExplorerProvider,
private _connectionToEdit?: IConnectionInfo
) {
super(
context,
'Connection Dialog',
'connectionDialog.js',
'connectionDialog.css',
{
recentConnections: [],
selectedFormTab: FormTabs.Parameters,
connectionProfile: {} as IConnectionDialogProfile,
formComponents: [],
connectionStatus: ApiStatus.NotStarted,
formError: ''
},
vscode.ViewColumn.Active,
{
dark: vscode.Uri.joinPath(context.extensionUri, 'media', 'connectionDialogEditor_inverse.svg'),
light: vscode.Uri.joinPath(context.extensionUri, 'media', 'connectionDialogEditor.svg')
}
);
this.registerRpcHandlers();
this.initializeDialog().catch(err => vscode.window.showErrorMessage(err.toString()));
}
private async initializeDialog() {
await this.loadRecentConnections();
if (this._connectionToEdit) {
await this.loadConnectionToEdit();
} else {
await this.loadEmptyConnection();
}
this.state.formComponents = await this.generateFormComponents();
await this.updateItemVisibility();
this.state = this.state;
}
private async loadRecentConnections() {
const recentConnections = this._mainController.connectionManager.connectionStore.loadAllConnections(true).map(c => c.connectionCreds);
const dialogConnections = [];
for (let i = 0; i < recentConnections.length; i++) {
dialogConnections.push(await this.initializeConnectionForDialog(recentConnections[i]));
}
this.state.recentConnections = dialogConnections;
this.state = this.state;
}
private async loadConnectionToEdit() {
if (this._connectionToEdit) {
this._connectionToEditCopy = structuredClone(this._connectionToEdit);
const connection = await this.initializeConnectionForDialog(this._connectionToEdit);
this.state.connectionProfile = connection;
this.state = this.state;
}
}
private async loadEmptyConnection() {
const emptyConnection = {
authenticationType: AuthenticationType.SqlLogin,
} as IConnectionDialogProfile;
this.state.connectionProfile = emptyConnection;
}
private async initializeConnectionForDialog(connection: IConnectionInfo) {
// Load the password if it's saved
const isConnectionStringConnection = connection.connectionString !== undefined && connection.connectionString !== '';
const password = await this._mainController.connectionManager.connectionStore.lookupPassword(connection, isConnectionStringConnection);
if (!isConnectionStringConnection) {
connection.password = password;
} else {
connection.connectionString = '';
// extract password from connection string it starts after 'Password=' and ends before ';'
const passwordIndex = password.indexOf('Password=') === -1 ? password.indexOf('password=') : password.indexOf('Password=');
if (passwordIndex !== -1) {
const passwordStart = passwordIndex + 'Password='.length;
const passwordEnd = password.indexOf(';', passwordStart);
if (passwordEnd !== -1) {
connection.password = password.substring(passwordStart, passwordEnd);
}
}
}
const dialogConnection = connection as IConnectionDialogProfile;
// Set the profile name
dialogConnection.profileName = dialogConnection.profileName ?? getConnectionDisplayName(connection);
return dialogConnection;
}
private async updateItemVisibility() {
const selectedTab = this.state.selectedFormTab;
let hiddenProperties: (keyof IConnectionDialogProfile)[] = [];
if (selectedTab === FormTabs.ConnectionString) {
hiddenProperties = [
'server',
'authenticationType',
'user',
'password',
'savePassword',
'accountId',
'tenantId',
'database',
'trustServerCertificate',
'encrypt'
];
} else {
hiddenProperties = [
'connectionString'
];
if (this.state.connectionProfile.authenticationType !== AuthenticationType.SqlLogin) {
hiddenProperties.push('user', 'password', 'savePassword');
}
if (this.state.connectionProfile.authenticationType !== AuthenticationType.AzureMFA) {
hiddenProperties.push('accountId', 'tenantId');
}
if (this.state.connectionProfile.authenticationType === AuthenticationType.AzureMFA) {
// Hide tenantId if accountId has only one tenant
const tenants = await this.getTenants(this.state.connectionProfile.accountId);
if (tenants.length === 1) {
hiddenProperties.push('tenantId');
}
}
}
for (let i = 0; i < this.state.formComponents.length; i++) {
const component = this.state.formComponents[i];
if (hiddenProperties.includes(component.propertyName)) {
component.hidden = true;
} else {
component.hidden = false;
}
}
}
private getFormComponent(propertyName: keyof IConnectionDialogProfile): FormComponent | undefined {
return this.state.formComponents.find(c => c.propertyName === propertyName);
}
private async getAccounts(): Promise<FormComponentOptions[]> {
const accounts = await this._mainController.azureAccountService.getAccounts();
return accounts.map(account => {
return {
displayName: account.displayInfo.displayName,
value: account.displayInfo.userId
};
});
}
private async getTenants(accountId: string): Promise<FormComponentOptions[]> {
const account = (await this._mainController.azureAccountService.getAccounts()).find(account => account.displayInfo.userId === accountId);
if (!account) {
return [];
}
const tenants = account.properties.tenants;
if (!tenants) {
return [];
}
return tenants.map(tenant => {
return {
displayName: tenant.displayName,
value: tenant.id
};
});
}
private async generateFormComponents(): Promise<FormComponent[]> {
const result: FormComponent[] = [
{
type: FormComponentType.Input,
propertyName: 'server',
label: 'Server',
required: true,
validate: (value: string) => {
if (this.state.selectedFormTab === FormTabs.Parameters && !value) {
return {
isValid: false,
validationMessage: 'Server is required'
};
}
return {
isValid: true,
validationMessage: ''
};
}
},
{
type: FormComponentType.TextArea,
propertyName: 'connectionString',
label: 'Connection String',
required: true,
validate: (value: string) => {
if (this.state.selectedFormTab === FormTabs.ConnectionString && !value) {
return {
isValid: false,
validationMessage: 'Connection string is required'
};
}
return {
isValid: true,
validationMessage: ''
};
}
},
{
type: FormComponentType.Dropdown,
propertyName: 'authenticationType',
label: 'Authentication Type',
required: true,
options: [
{
displayName: 'SQL Login',
value: AuthenticationType.SqlLogin
},
{
displayName: 'Windows Authentication',
value: AuthenticationType.Integrated
},
{
displayName: 'Azure MFA',
value: AuthenticationType.AzureMFA
}
],
},
{
// Hidden if connection string is set or if the authentication type is not SQL Login
propertyName: 'user',
label: 'User Name',
type: FormComponentType.Input,
required: true,
validate: (value: string) => {
if (this.state.connectionProfile.authenticationType === AuthenticationType.SqlLogin && !value) {
return {
isValid: false,
validationMessage: 'User name is required'
};
}
return {
isValid: true,
validationMessage: ''
};
}
},
{
propertyName: 'password',
label: 'Password',
required: false,
type: FormComponentType.Password,
},
{
propertyName: 'savePassword',
label: 'Save Password',
required: false,
type: FormComponentType.Checkbox,
},
{
propertyName: 'accountId',
label: 'Azure Account',
required: true,
type: FormComponentType.Dropdown,
options: await this.getAccounts(),
placeholder: 'Select an account',
actionButtons: await this.getAzureActionButtons(),
validate: (value: string) => {
if (this.state.connectionProfile.authenticationType === AuthenticationType.AzureMFA && !value) {
return {
isValid: false,
validationMessage: 'Azure Account is required'
};
}
return {
isValid: true,
validationMessage: ''
};
},
},
{
propertyName: 'tenantId',
label: 'Tenant ID',
required: true,
type: FormComponentType.Dropdown,
options: [],
hidden: true,
placeholder: 'Select a tenant',
validate: (value: string) => {
if (this.state.connectionProfile.authenticationType === AuthenticationType.AzureMFA && !value) {
return {
isValid: false,
validationMessage: 'Tenant ID is required'
};
}
return {
isValid: true,
validationMessage: ''
};
}
},
{
propertyName: 'database',
label: 'Database',
required: false,
type: FormComponentType.Input,
},
{
propertyName: 'trustServerCertificate',
label: 'Trust Server Certificate',
required: false,
type: FormComponentType.Checkbox,
},
{
propertyName: 'encrypt',
label: 'Encrypt Connection',
required: false,
type: FormComponentType.Dropdown,
options: [
{
displayName: 'Optional',
value: 'Optional'
},
{
displayName: 'Mandatory',
value: 'Mandatory'
},
{
displayName: 'Strict (Requires SQL Server 2022 or Azure SQL)',
value: 'Strict'
}
],
},
{
propertyName: 'profileName',
label: 'Profile Name',
required: false,
type: FormComponentType.Input,
}
];
return result;
}
private async validateFormComponents(propertyName?: keyof IConnectionDialogProfile): Promise<number> {
let errorCount = 0;
if (propertyName) {
const component = this.getFormComponent(propertyName);
if (component && component.validate) {
component.validation = component.validate(this.state.connectionProfile[propertyName]);
if (!component.validation.isValid) {
return 1;
}
}
}
else {
this.state.formComponents.forEach(c => {
if (c.hidden) {
c.validation = {
isValid: true,
validationMessage: ''
};
return;
} else {
if (c.validate) {
c.validation = c.validate(this.state.connectionProfile[c.propertyName]);
if (!c.validation.isValid) {
errorCount++;
}
}
}
});
}
return errorCount;
}
private async getAzureActionButtons(): Promise<FormComponentActionButton[]> {
const actionButtons: FormComponentActionButton[] = [];
actionButtons.push({
label: 'Sign in',
id: 'azureSignIn',
callback: async () => {
const account = await this._mainController.azureAccountService.addAccount();
const accountsComponent = this.getFormComponent('accountId');
if (accountsComponent) {
accountsComponent.options = await this.getAccounts();
this.state.connectionProfile.accountId = account.key.id;
this.state = this.state;
await this.handleAzureMFAEdits('accountId');
}
}
});
if (this.state.connectionProfile.authenticationType === AuthenticationType.AzureMFA && this.state.connectionProfile.accountId) {
const account = (await this._mainController.azureAccountService.getAccounts()).find(account => account.displayInfo.userId === this.state.connectionProfile.accountId);
if (account) {
const session = await this._mainController.azureAccountService.getAccountSecurityToken(account, undefined);
const isTokenExpired = AzureController.isTokenInValid(session.token, session.expiresOn);
if (isTokenExpired) {
actionButtons.push({
label: 'Refresh Token',
id: 'refreshToken',
callback: async () => {
const account = (await this._mainController.azureAccountService.getAccounts()).find(account => account.displayInfo.userId === this.state.connectionProfile.accountId);
if (account) {
const session = await this._mainController.azureAccountService.getAccountSecurityToken(account, undefined);
console.log('Token refreshed', session.expiresOn);
}
}
});
}
}
}
return actionButtons;
}
private async handleAzureMFAEdits(propertyName: keyof IConnectionDialogProfile) {
const mfaComponents: (keyof IConnectionDialogProfile)[] = ['accountId', 'tenantId', 'authenticationType'];
if (mfaComponents.includes(propertyName)) {
if (this.state.connectionProfile.authenticationType !== AuthenticationType.AzureMFA) {
return;
}
const accountComponent = this.getFormComponent('accountId');
const tenantComponent = this.getFormComponent('tenantId');
let tenants: FormComponentOptions[] = [];
switch (propertyName) {
case 'accountId':
tenants = await this.getTenants(this.state.connectionProfile.accountId);
if (tenantComponent) {
tenantComponent.options = tenants;
if (tenants && tenants.length > 0) {
this.state.connectionProfile.tenantId = tenants[0].value;
}
}
accountComponent.actionButtons = await this.getAzureActionButtons();
break;
case 'tenantId':
break;
case 'authenticationType':
const firstOption = accountComponent.options[0];
if (firstOption) {
this.state.connectionProfile.accountId = firstOption.value;
}
tenants = await this.getTenants(this.state.connectionProfile.accountId);
if (tenantComponent) {
tenantComponent.options = tenants;
if (tenants && tenants.length > 0) {
this.state.connectionProfile.tenantId = tenants[0].value;
}
}
accountComponent.actionButtons = await this.getAzureActionButtons();
break;
}
}
}
private clearFormError() {
this.state.formError = '';
for (let i = 0; i < this.state.formComponents.length; i++) {
this.state.formComponents[i].validation = undefined;
}
}
private registerRpcHandlers() {
this.registerReducers({
'setFormTab': async (state, payload: {
tab: FormTabs
}) => {
this.state.selectedFormTab = payload.tab;
await this.updateItemVisibility();
return state;
},
'formAction': async (state, payload: {
event: FormEvent
}) => {
if (payload.event.isAction) {
const component = this.getFormComponent(payload.event.propertyName);
if (component && component.actionButtons) {
const actionButton = component.actionButtons.find(b => b.id === payload.event.value);
if (actionButton?.callback) {
await actionButton.callback();
}
}
} else {
(this.state.connectionProfile[payload.event.propertyName] as any) = payload.event.value;
await this.validateFormComponents(payload.event.propertyName);
await this.handleAzureMFAEdits(payload.event.propertyName);
}
await this.updateItemVisibility();
return state;
},
'loadConnection': async (state, payload: {
connection: IConnectionDialogProfile
}) => {
this._connectionToEditCopy = structuredClone(payload.connection);
this.clearFormError();
this.state.connectionProfile = payload.connection;
await this.updateItemVisibility();
await this.handleAzureMFAEdits('azureAuthType');
await this.handleAzureMFAEdits('accountId');
return state;
},
'connect': async (state) => {
this.clearFormError();
this.state.connectionStatus = ApiStatus.Loading;
this.state.formError = '';
this.state = this.state;
const notHiddenComponents = this.state.formComponents.filter(c => !c.hidden).map(c => c.propertyName);
// Set all other fields to undefined
Object.keys(this.state.connectionProfile).forEach(key => {
if (!notHiddenComponents.includes(key as keyof IConnectionDialogProfile)) {
(this.state.connectionProfile[key as keyof IConnectionDialogProfile] as any) = undefined;
}
});
const errorCount = await this.validateFormComponents();
if (errorCount > 0) {
this.state.connectionStatus = ApiStatus.Error;
return state;
}
try {
const result = await this._mainController.connectionManager.connectionUI.validateAndSaveProfileFromDialog(this.state.connectionProfile as any);
if (result?.errorMessage) {
this.state.formError = result.errorMessage;
this.state.connectionStatus = ApiStatus.Error;
return state;
}
if (this._connectionToEditCopy) {
await this._mainController.connectionManager.getUriForConnection(this._connectionToEditCopy);
await this._objectExplorerProvider.removeConnectionNodes([this._connectionToEditCopy]);
await this._mainController.connectionManager.connectionStore.removeProfile(this._connectionToEditCopy as any);
await this._objectExplorerProvider.refresh(undefined);
}
await this._mainController.connectionManager.connectionUI.saveProfile(this.state.connectionProfile as any);
const node = await this._mainController.createObjectExplorerSessionFromDialog(this.state.connectionProfile);
await this._objectExplorerProvider.refresh(undefined);
await this.loadRecentConnections();
this.state.connectionStatus = ApiStatus.Loaded;
await this._mainController.objectExplorerTree.reveal(node, { focus: true, select: true, expand: true });
await this.panel.dispose();
} catch (error) {
this.state.connectionStatus = ApiStatus.Error;
return state;
}
return state;
}
});
}
}

Просмотреть файл

@ -42,6 +42,7 @@ export const cmdManageConnectionProfiles = 'mssql.manageProfiles';
export const cmdClearPooledConnections = 'mssql.clearPooledConnections';
export const cmdRebuildIntelliSenseCache = 'mssql.rebuildIntelliSenseCache';
export const cmdAddObjectExplorer = 'mssql.addObjectExplorer';
export const cmdAddObjectExplorer2 = 'mssql.addObjectExplorer2';
export const cmdObjectExplorerNewQuery = 'mssql.objectExplorerNewQuery';
export const cmdRemoveObjectExplorerNode = 'mssql.removeObjectExplorerNode';
export const cmdRefreshObjectExplorerNode = 'mssql.refreshObjectExplorerNode';
@ -70,6 +71,7 @@ export const cmdAadAddAccount = 'mssql.addAadAccount';
export const cmdClearAzureTokenCache = 'mssql.clearAzureAccountTokenCache';
export const cmdNewTable = 'mssql.newTable';
export const cmdEditTable = 'mssql.editTable';
export const cmdEditConnection = 'mssql.editConnection';
export const piiLogging = 'piiLogging';
export const mssqlPiiLogging = 'mssql.piiLogging';
export const enableSqlAuthenticationProvider = 'mssql.enableSqlAuthenticationProvider';

Просмотреть файл

@ -33,6 +33,7 @@ import StatusView from '../views/statusView';
import VscodeWrapper from './vscodeWrapper';
import { sendActionEvent, sendErrorEvent } from '../telemetry/telemetry';
import { TelemetryActions, TelemetryViews } from '../telemetry/telemetryInterfaces';
import { ObjectExplorerUtils } from '../objectExplorer/objectExplorerUtils';
/**
* Information for a document's connection. Exported for testing purposes.
@ -53,6 +54,8 @@ export class ConnectionInfo {
*/
public connectHandler: (result: boolean, error?: any) => void;
public connectionCompleteHandler: (result: ConnectionContracts.ConnectionCompleteParams) => void;
/**
* Information about the SQL Server instance.
*/
@ -89,6 +92,7 @@ export default class ConnectionManager {
private _connectionCredentialsToServerInfoMap:
Map<IConnectionInfo, IServerInfo>;
private _uriToConnectionPromiseMap: Map<string, Deferred<boolean>>;
private _uriToConnectionCompleteParamsMap: Map<string, Deferred<ConnectionContracts.ConnectionCompleteParams>>;
private _failedUriToFirewallIpMap: Map<string, string>;
private _failedUriToSSLMap: Map<string, string>;
private _accountService: AccountService;
@ -108,7 +112,7 @@ export default class ConnectionManager {
this._connections = {};
this._connectionCredentialsToServerInfoMap = new Map<IConnectionInfo, IServerInfo>();
this._uriToConnectionPromiseMap = new Map<string, Deferred<boolean>>();
this._uriToConnectionCompleteParamsMap = new Map<string, Deferred<ConnectionContracts.ConnectionCompleteParams>>();
if (!this.client) {
this.client = SqlToolsServerClient.instance;
}
@ -399,6 +403,11 @@ export default class ConnectionManager {
promise.resolve(true);
self._uriToConnectionPromiseMap.delete(result.ownerUri);
}
const completePromise = self._uriToConnectionCompleteParamsMap.get(result.ownerUri);
if (completePromise) {
completePromise.resolve(result);
self._uriToConnectionCompleteParamsMap.delete(result.ownerUri);
}
} else {
mruConnection = undefined;
const promise = self._uriToConnectionPromiseMap.get(result.ownerUri);
@ -412,6 +421,11 @@ export default class ConnectionManager {
self._uriToConnectionPromiseMap.delete(result.ownerUri);
}
}
const completePromise = self._uriToConnectionCompleteParamsMap.get(result.ownerUri);
if (completePromise) {
completePromise.resolve(result);
self._uriToConnectionCompleteParamsMap.delete(result.ownerUri);
}
await self.handleConnectionErrors(fileUri, connection, result);
}
@ -901,6 +915,97 @@ export default class ConnectionManager {
});
}
public async connectDialog(connectionCreds: IConnectionInfo): Promise<ConnectionContracts.ConnectionCompleteParams> {
// If the connection info doesn't have server or connection string, throw an error
if (!connectionCreds.server && !connectionCreds.connectionString) {
throw new Error(LocalizedConstants.serverNameMissing);
}
if (connectionCreds.authenticationType === Constants.azureMfa) {
if (AzureController.isTokenInValid(connectionCreds.azureAccountToken, connectionCreds.expiresOn)) {
let account: IAccount;
let profile: ConnectionProfile;
if (connectionCreds.accountId) {
account = this.accountStore.getAccount(connectionCreds.accountId);
profile = new ConnectionProfile(connectionCreds);
} else {
throw new Error(LocalizedConstants.cannotConnect);
}
if (account) {
// Always set username
connectionCreds.user = account.displayInfo.displayName;
connectionCreds.email = account.displayInfo.email;
profile.user = account.displayInfo.displayName;
profile.email = account.displayInfo.email;
let azureAccountToken = await this.azureController.refreshAccessToken(account!,
this.accountStore, profile.tenantId, providerSettings.resources.databaseResource!);
if (!azureAccountToken) {
let errorMessage = LocalizedConstants.msgAccountRefreshFailed;
let refreshResult = await this.vscodeWrapper.showErrorMessage(errorMessage, LocalizedConstants.refreshTokenLabel);
if (refreshResult === LocalizedConstants.refreshTokenLabel) {
await this.azureController.populateAccountProperties(
profile, this.accountStore, providerSettings.resources.databaseResource!);
} else {
throw new Error(LocalizedConstants.cannotConnect);
}
} else {
connectionCreds.azureAccountToken = azureAccountToken.token;
connectionCreds.expiresOn = azureAccountToken.expiresOn;
}
} else {
throw new Error(LocalizedConstants.msgAccountNotFound);
}
}
}
if (connectionCreds.connectionString?.includes(ConnectionStore.CRED_PREFIX)
&& connectionCreds.connectionString?.includes('isConnectionString:true')) {
let connectionString = await this.connectionStore.lookupPassword(connectionCreds, true);
connectionCreds.connectionString = connectionString;
}
const uri = ObjectExplorerUtils.getNodeUriFromProfile(connectionCreds as IConnectionProfile);
let connectionInfo: ConnectionInfo = new ConnectionInfo();
connectionInfo.credentials = connectionCreds;
connectionInfo.connecting = true;
// Setup the handler for the connection complete notification to call
connectionInfo.connectHandler = ((connectResult, error) => {
});
this._connections[uri] = connectionInfo;
// Note: must call flavor changed before connecting, or the timer showing an animation doesn't occur
if (this.statusView) {
this.statusView.languageFlavorChanged(uri, Constants.mssqlProviderName);
this.statusView.connecting(uri, connectionCreds);
this.statusView.languageFlavorChanged(uri, Constants.mssqlProviderName);
}
// this.vscodeWrapper.logToOutputChannel(
// Utils.formatString(LocalizedConstants.msgConnecting, connectionCreds.server, fileUri)
// );
const connectionDetails = ConnectionCredentials.createConnectionDetails(connectionCreds);
let connectParams = new ConnectionContracts.ConnectParams();
connectParams.ownerUri = uri;
connectParams.connection = connectionDetails;
const connectionCompletePromise = new Deferred<ConnectionContracts.ConnectionCompleteParams>();
this._uriToConnectionCompleteParamsMap.set(connectParams.ownerUri, connectionCompletePromise);
try {
const result = await this.client.sendRequest(ConnectionContracts.ConnectionRequest.type, connectParams);
if (!result) {
// Failed to process connect request
throw new Error('Failed to connect');
}
} catch (error) {
throw new Error('Failed to connect');
}
return await connectionCompletePromise;
}
public async onCancelConnect(): Promise<void> {
const result = await this.connectionUI.promptToCancelConnection();
if (result) {
@ -1023,13 +1128,14 @@ export default class ConnectionManager {
return;
}
public async addAccount(): Promise<void> {
public async addAccount(): Promise<IAccount> {
let account = await this.connectionUI.addNewAccount();
if (account) {
this.vscodeWrapper.showInformationMessage(Utils.formatString(LocalizedConstants.accountAddedSuccessfully, account.displayInfo.displayName));
} else {
this.vscodeWrapper.showErrorMessage(LocalizedConstants.accountCouldNotBeAdded);
}
return account;
}
public async removeAccount(prompter: IPrompter): Promise<void> {

Просмотреть файл

@ -43,6 +43,7 @@ import { sendActionEvent } from '../telemetry/telemetry';
import { TelemetryActions, TelemetryViews } from '../telemetry/telemetryInterfaces';
import { TableDesignerService } from '../services/tableDesignerService';
import { TableDesignerWebViewController } from '../tableDesigner/tableDesignerWebViewController';
import { ConnectionDialogWebViewController } from '../connectionconfig/connectionDialogWebViewController';
/**
* The main controller class that initializes the extension
@ -73,6 +74,7 @@ export default class MainController implements vscode.Disposable {
public azureResourceService: AzureResourceService;
public tableDesignerService: TableDesignerService;
public configuration: vscode.WorkspaceConfiguration;
public objectExplorerTree: vscode.TreeView<TreeNodeInfo>;
/**
* The main controller constructor
@ -124,7 +126,7 @@ export default class MainController implements vscode.Disposable {
this._statusview.dispose();
}
public get isPreviewEnabled(): boolean {
public get isExperimentalEnabled(): boolean {
return this.configuration.get(Constants.configEnableExperimentalFeatures);
}
@ -383,12 +385,13 @@ export default class MainController implements vscode.Disposable {
* @param connectionCredentials Connection credentials to use for the session
* @returns True if the session was created successfully, false otherwise
*/
private async createObjectExplorerSession(connectionCredentials?: IConnectionInfo): Promise<boolean> {
public async createObjectExplorerSession(connectionCredentials?: IConnectionInfo): Promise<boolean> {
let createSessionPromise = new Deferred<TreeNodeInfo>();
const sessionId = await this._objectExplorerProvider.createSession(createSessionPromise, connectionCredentials, this._context);
if (sessionId) {
const newNode = await createSessionPromise;
if (newNode) {
console.log(newNode);
this._objectExplorerProvider.refresh(undefined);
return true;
}
@ -396,6 +399,20 @@ export default class MainController implements vscode.Disposable {
return false;
}
public async createObjectExplorerSessionFromDialog(connectionCredentials?: IConnectionInfo): Promise<TreeNodeInfo> {
let createSessionPromise = new Deferred<TreeNodeInfo>();
const sessionId = await this._objectExplorerProvider.createSession(createSessionPromise, connectionCredentials, this._context);
if (sessionId) {
const newNode = await createSessionPromise;
if (newNode) {
console.log(newNode);
this._objectExplorerProvider.refresh(undefined);
return newNode;
}
}
return undefined;
}
/**
* Initializes the Object Explorer commands
*/
@ -403,27 +420,31 @@ export default class MainController implements vscode.Disposable {
const self = this;
// Register the object explorer tree provider
this._objectExplorerProvider = new ObjectExplorerProvider(this._connectionMgr);
const treeView = vscode.window.createTreeView('objectExplorer', {
this.objectExplorerTree = vscode.window.createTreeView('objectExplorer', {
treeDataProvider: this._objectExplorerProvider,
canSelectMany: false
});
this._context.subscriptions.push(treeView);
this._context.subscriptions.push(this.objectExplorerTree);
// Sets the correct current node on any node selection
this._context.subscriptions.push(treeView.onDidChangeSelection((e: vscode.TreeViewSelectionChangeEvent<TreeNodeInfo>) => {
this._context.subscriptions.push(this.objectExplorerTree.onDidChangeSelection((e: vscode.TreeViewSelectionChangeEvent<TreeNodeInfo>) => {
if (e.selection?.length > 0) {
self._objectExplorerProvider.currentNode = e.selection[0];
}
}));
// Add Object Explorer Node
this.registerCommand(Constants.cmdAddObjectExplorer);
this._event.on(Constants.cmdAddObjectExplorer, async () => {
if (!self._objectExplorerProvider.objectExplorerExists) {
self._objectExplorerProvider.objectExplorerExists = true;
}
await self.createObjectExplorerSession();
});
// Old style Add connection when experimental features are not enabled
if (!this.isExperimentalEnabled) {
// Add Object Explorer Node
this.registerCommand(Constants.cmdAddObjectExplorer);
this._event.on(Constants.cmdAddObjectExplorer, async () => {
if (!self._objectExplorerProvider.objectExplorerExists) {
self._objectExplorerProvider.objectExplorerExists = true;
}
await self.createObjectExplorerSession();
});
}
// Object Explorer New Query
this._context.subscriptions.push(
@ -487,7 +508,30 @@ export default class MainController implements vscode.Disposable {
return this._objectExplorerProvider.refresh(undefined);
}));
if (this.isPreviewEnabled) {
if (this.isExperimentalEnabled) {
this.registerCommand(Constants.cmdAddObjectExplorer2);
this._event.on(Constants.cmdAddObjectExplorer2, async () => {
const connDialog = new ConnectionDialogWebViewController(
this._context,
this,
this._objectExplorerProvider
);
connDialog.revealToForeground();
});
this._context.subscriptions.push(
vscode.commands.registerCommand(
Constants.cmdEditConnection, async (node: TreeNodeInfo) => {
const connDialog = new ConnectionDialogWebViewController(
this._context,
this,
this._objectExplorerProvider,
node.connectionInfo,
);
connDialog.revealToForeground();
}
));
this._context.subscriptions.push(
vscode.commands.registerCommand(
Constants.cmdNewTable, async (node: TreeNodeInfo) => {
@ -1369,14 +1413,4 @@ export default class MainController implements vscode.Disposable {
public onClearAzureTokenCache(): void {
this.connectionManager.onClearTokenCache();
}
}
export function getNonce(): string {
let text: string = "";
const possible: string =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}

Просмотреть файл

@ -227,3 +227,20 @@ export function getEncryptionMode(encryption: string | boolean | undefined): Enc
}
return encryptionMode;
}
export function getConnectionDisplayName(credentials: IConnectionInfo): string {
let database = credentials.database;
const server = credentials.server;
const authType = credentials.authenticationType;
let userOrAuthType = authType;
if (authType === Constants.sqlAuthentication) {
userOrAuthType = credentials.user;
}
if (authType === Constants.azureMfa) {
userOrAuthType = credentials.email;
}
if (!database || database === '') {
database = LocalizedConstants.defaultDatabaseLabel;
}
return `${server}, ${database} (${userOrAuthType})`;
}

Просмотреть файл

@ -28,11 +28,11 @@ export class ConnectionProfile extends ConnectionCredentials implements IConnect
public savePassword: boolean;
public emptyPasswordInput: boolean;
public azureAuthType: AzureAuthType;
public azureAccountToken: string | undefined;
public expiresOn: number | undefined;
declare public azureAccountToken: string | undefined;
declare public expiresOn: number | undefined;
public accountStore: AccountStore;
public accountId: string;
public tenantId: string;
declare public accountId: string;
declare public tenantId: string;
constructor(connectionCredentials?: ConnectionCredentials) {
super();

Просмотреть файл

@ -22,6 +22,11 @@ export class ObjectExplorerProvider implements vscode.TreeDataProvider<any> {
this._objectExplorerService = new ObjectExplorerService(connectionManager, this);
}
getParent(element: TreeNodeInfo)
{
return element.parentNode;
}
refresh(nodeInfo?: TreeNodeInfo): void {
this._onDidChangeTreeData.fire(nodeInfo);
}

Просмотреть файл

@ -31,6 +31,7 @@ import { sendActionEvent } from '../telemetry/telemetry';
import { IAccount } from '../models/contracts/azure';
import * as AzureConstants from '../azure/constants';
import { TelemetryActions, TelemetryViews } from '../telemetry/telemetryInterfaces';
import { getConnectionDisplayName } from '../models/connectionInfo';
function getParentNode(node: TreeNodeType): TreeNodeInfo {
node = node.parentNode;
@ -97,11 +98,11 @@ export class ObjectExplorerService {
let node: TreeNodeInfo;
if (self._currentNode && (self._currentNode.sessionId === result.sessionId)) {
nodeLabel = !nodeLabel ? self.createNodeLabel(self._currentNode.connectionInfo) : nodeLabel;
nodeLabel = !nodeLabel ? getConnectionDisplayName(self._currentNode.connectionInfo) : nodeLabel;
node = TreeNodeInfo.fromNodeInfo(result.rootNode, result.sessionId,
undefined, self._currentNode.connectionInfo, nodeLabel, Constants.serverLabel);
} else {
nodeLabel = !nodeLabel ? self.createNodeLabel(nodeConnection) : nodeLabel;
nodeLabel = !nodeLabel ? getConnectionDisplayName(nodeConnection) : nodeLabel;
node = TreeNodeInfo.fromNodeInfo(result.rootNode, result.sessionId,
undefined, nodeConnection, nodeLabel, Constants.serverLabel);
}
@ -318,7 +319,7 @@ export class ObjectExplorerService {
let savedConnections = this._connectionManager.connectionStore.loadAllConnections();
for (const conn of savedConnections) {
let nodeLabel = conn.label === conn.connectionCreds.server ?
this.createNodeLabel(conn.connectionCreds) : conn.label;
getConnectionDisplayName(conn.connectionCreds) : conn.label;
this._nodePathToNodeLabelMap.set(conn.connectionCreds.server, nodeLabel);
let node = new TreeNodeInfo(nodeLabel,
Constants.disconnectedServerLabel,
@ -650,7 +651,7 @@ export class ObjectExplorerService {
public addDisconnectedNode(connectionCredentials: IConnectionInfo): void {
const label = (<IConnectionProfile>connectionCredentials).profileName ?
(<IConnectionProfile>connectionCredentials).profileName :
this.createNodeLabel(connectionCredentials);
getConnectionDisplayName(connectionCredentials);
const node = new TreeNodeInfo(label, Constants.disconnectedServerLabel,
vscode.TreeItemCollapsibleState.Collapsed, undefined, undefined,
Constants.disconnectedServerLabel, undefined, connectionCredentials,
@ -658,23 +659,6 @@ export class ObjectExplorerService {
this.updateNode(node);
}
private createNodeLabel(credentials: IConnectionInfo): string {
let database = credentials.database;
const server = credentials.server;
const authType = credentials.authenticationType;
let userOrAuthType = authType;
if (authType === Constants.sqlAuthentication) {
userOrAuthType = credentials.user;
}
if (authType === Constants.azureMfa) {
userOrAuthType = credentials.email;
}
if (!database || database === '') {
database = LocalizedConstants.defaultDatabaseLabel;
}
return `${server}, ${database} (${userOrAuthType})`;
}
/**
* Sends a close session request
* @param node

Просмотреть файл

@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createContext, useContext } from "react";
import { VscodeWebviewContext } from "../../common/vscodeWebViewProvider";
import { ConnectionDialogContextProps, ConnectionDialogWebviewState, FormTabs, IConnectionDialogProfile } from "../../../sharedInterfaces/connectionDialog";
const ConnectionDialogContext = createContext<ConnectionDialogContextProps | undefined>(undefined);
interface ConnectionDialogProviderProps {
children: React.ReactNode;
}
const ConnectionDialogStateProvider: React.FC<ConnectionDialogProviderProps> = ({ children }) => {
const webViewState = useContext(VscodeWebviewContext);
const connectionDialogState = webViewState?.state as ConnectionDialogWebviewState;
return <ConnectionDialogContext.Provider value={
{
state: connectionDialogState,
loadConnection: function (connection: IConnectionDialogProfile): void {
webViewState?.extensionRpc.action('loadConnection', {
connection: connection
});
},
formAction: function (event): void {
webViewState?.extensionRpc.action('formAction', {
event: event
});
},
setFormTab: function (tab: FormTabs): void {
webViewState?.extensionRpc.action('setFormTab', {
tab: tab
});
},
connect: function (): void {
webViewState?.extensionRpc.action('connect', {});
}
}
}>{children}</ConnectionDialogContext.Provider>;
};
export { ConnectionDialogContext, ConnectionDialogStateProvider };

Просмотреть файл

@ -0,0 +1,257 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { useContext, useEffect, useState } from "react";
import { ConnectionDialogContext } from "./connectionDialogStateProvider";
import { Text, Button, Checkbox, Dropdown, Field, Input, Option, Tab, TabList, makeStyles, Image, MessageBar, Textarea, webLightTheme, Spinner } from "@fluentui/react-components";
import { ApiStatus, FormComponent, FormComponentType, FormTabs, IConnectionDialogProfile } from "../../../sharedInterfaces/connectionDialog";
import { EyeRegular, EyeOffRegular } from "@fluentui/react-icons";
import './sqlServerRotation.css';
import { VscodeWebviewContext } from "../../common/vscodeWebViewProvider";
const sqlServerImage = require('../../../../media/sqlServer.svg');
const sqlServerImageDark = require('../../../../media/sqlServer_inverse.svg');
const useStyles = makeStyles({
formRoot: {
display: 'flex',
flexDirection: 'column',
height: '100%',
},
formDiv: {
padding: '10px',
maxWidth: '500px',
display: 'flex',
flexDirection: 'column',
'> *': {
margin: '5px',
}
},
formComponentDiv: {
'> *': {
margin: '5px',
}
},
formComponentActionDiv: {
display: 'flex',
flexDirection: 'row',
'> *': {
margin: '5px',
}
}
});
const FormInput = ({ value, target, type }: { value: string, target: keyof IConnectionDialogProfile, type: 'input' | 'password' | 'textarea' }) => {
const connectionDialogContext = useContext(ConnectionDialogContext);
const [inputVal, setValueVal] = useState(value);
const [showPassword, setShowPassword] = useState(false);
useEffect(() => {
console.log('value changed');
setValueVal(value);
}, [value]);
const handleChange = (data: string) => {
setValueVal(data);
};
const handleBlur = () => {
connectionDialogContext?.formAction({
propertyName: target,
isAction: false,
value: inputVal
});
};
return (
<>
{
type === 'input' &&
<Input
value={inputVal}
onChange={(_value, data) => handleChange(data.value)}
onBlur={handleBlur}
size="small"
/>
}
{
type === 'password' &&
<Input
type={showPassword ? 'text' : 'password'}
value={inputVal}
onChange={(_value, data) => handleChange(data.value)}
onBlur={handleBlur}
size="small"
contentAfter={
<Button
onClick={() => setShowPassword(!showPassword)}
icon={showPassword ? <EyeRegular /> : <EyeOffRegular />}
appearance="transparent"
size="small"
>
</Button>}
/>
}
{
type === 'textarea' &&
<Textarea
value={inputVal}
size="small"
onChange={(_value, data) => handleChange(data.value)}
onBlur={handleBlur}
/>
}
</>
);
};
export const ConnectionInfoFormContainer = () => {
const connectionDialogContext = useContext(ConnectionDialogContext);
const classes = useStyles();
const vscode = useContext(VscodeWebviewContext);
const generateFormComponent = (component: FormComponent, profile: IConnectionDialogProfile, _idx: number) => {
switch (component.type) {
case FormComponentType.Input:
return <FormInput value={profile[component.propertyName] as string ?? ''} target={component.propertyName} type='input' />;
case FormComponentType.TextArea:
return <FormInput value={profile[component.propertyName] as string ?? ''} target={component.propertyName} type='textarea' />;
case FormComponentType.Password:
return <FormInput value={profile[component.propertyName] as string ?? ''} target={component.propertyName} type='password' />;
case FormComponentType.Dropdown:
if (component.options === undefined) {
throw new Error('Dropdown component must have options');
}
return <Dropdown
size="small"
placeholder={component.placeholder ?? ''}
value={component.options.find(option => option.value === profile[component.propertyName])?.displayName ?? ''}
selectedOptions={[profile[component.propertyName] as string]}
onOptionSelect={(_event, data) => {
connectionDialogContext?.formAction({
propertyName: component.propertyName,
isAction: false,
value: data.optionValue as string
});
}}>
{
component.options?.map((option, idx) => {
return <Option key={component.propertyName + idx} value={option.value}>{option.displayName}</Option>
})
}
</Dropdown>;
case FormComponentType.Checkbox:
return <Checkbox
size="medium"
checked={profile[component.propertyName] as boolean ?? false}
onChange={(_value, data) => connectionDialogContext?.formAction({
propertyName: component.propertyName,
isAction: false,
value: data.checked
})}
/>;
}
};
if (!connectionDialogContext?.state) {
return undefined;
}
return (
<div className={classes.formRoot}>
<div style={
{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}
}>
<Image style={
{
padding: '10px',
}
}
src={vscode?.theme === webLightTheme ? sqlServerImage : sqlServerImageDark} alt='SQL Server' height={60} width={60} />
<Text size={500} style={
{
lineHeight: '60px'
}
} weight='medium'>Connect to SQL Server</Text>
</div>
<TabList selectedValue={connectionDialogContext?.state?.selectedFormTab ?? FormTabs.Parameters} onTabSelect={(_event, data) => {
connectionDialogContext?.setFormTab(data.value as FormTabs);
}}>
<Tab value={FormTabs.Parameters}>Parameters</Tab>
<Tab value={FormTabs.ConnectionString}>Connection String</Tab>
</TabList>
<div style={
{
overflow: 'auto'
}
}>
<div className={classes.formDiv}>
{
connectionDialogContext?.state.formError &&
<MessageBar intent="error">
{connectionDialogContext.state.formError}
</MessageBar>
}
{
connectionDialogContext.state.formComponents.map((component, idx) => {
if (component.hidden === true) {
return undefined;
}
return <div className={classes.formComponentDiv} key={idx}>
<Field
validationMessage={component.validation?.validationMessage ?? ''}
orientation={component.type === FormComponentType.Checkbox ? 'horizontal' : 'vertical'}
validationState={component.validation ? (component.validation.isValid ? 'none' : 'error') : 'none'}
required={component.required}
label={component.label}>
{generateFormComponent(component, connectionDialogContext.state.connectionProfile, idx)}
</Field>
{
component?.actionButtons?.length! > 0 &&
<div className={classes.formComponentActionDiv}>
{
component.actionButtons?.map((actionButton, idx) => {
return <Button shape="square" key={idx + actionButton.id} appearance='outline' style={
{
width: '120px'
}
} onClick={() => connectionDialogContext?.formAction({
propertyName: component.propertyName,
isAction: true,
value: actionButton.id
})}>{actionButton.label}</Button>
})
}
</div>
}
</div>;
})
}
<Button
appearance="primary"
disabled={connectionDialogContext.state.connectionStatus === ApiStatus.Loading}
shape="square"
onClick={(_event) => {
connectionDialogContext.connect();
}} style={
{
width: '200px',
alignSelf: 'center'
}
}
iconPosition="after"
icon={ connectionDialogContext.state.connectionStatus === ApiStatus.Loading ? <Spinner size='tiny' /> : undefined}>
Connect
</Button>
</div>
</div>
</div>
)
}

Просмотреть файл

@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Divider, makeStyles, shorthands } from "@fluentui/react-components";
import { ResizableBox } from "react-resizable";
import { MruConnectionsContainer } from "./mruConnectionsContainer";
import { ConnectionInfoFormContainer } from "./connectionInfoFormContainer";
export const useStyles = makeStyles({
root: {
flexDirection: 'row',
display: 'flex',
height: '100%',
width: '100%',
maxWidth: '100%',
maxHeight: '100%',
},
mainContainer: {
...shorthands.flex(1),
height: '100%',
},
mruContainer: {
position: 'relative',
height: '100%',
width: '325px',
padding: '20px',
},
mruPaneHandle: {
position: 'absolute',
top: '0',
left: '0',
width: '10px',
height: '100%',
cursor: 'ew-resize',
zIndex: 1,
},
});
export const ConnectionPage = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<div className={classes.mainContainer}>
<ConnectionInfoFormContainer />
</div>
<Divider style={
{
width: '5px',
height: '100%',
flex: 0
}
} vertical />
<ResizableBox
className={classes.mruContainer}
width={350}
height={Infinity}
maxConstraints={[800, Infinity]}
minConstraints={[300, Infinity]}
resizeHandles={['w']}
handle={
<div className={classes.mruPaneHandle} />
}
>
<MruConnectionsContainer />
</ResizableBox>
</div>
);
}

Просмотреть файл

@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import ReactDOM from 'react-dom/client'
import '../../index.css'
import { VscodeWebViewProvider } from '../../common/vscodeWebViewProvider'
import { ConnectionPage } from './connectionPage'
import { ConnectionDialogStateProvider } from './connectionDialogStateProvider'
ReactDOM.createRoot(document.getElementById('root')!).render(
<VscodeWebViewProvider>
<ConnectionDialogStateProvider>
<ConnectionPage />
</ConnectionDialogStateProvider>
</VscodeWebViewProvider>
)

Просмотреть файл

@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Text, Tree, TreeItem, TreeItemLayout, makeStyles, tokens } from "@fluentui/react-components"
import { ServerRegular } from "@fluentui/react-icons";
import { useContext } from "react";
import { ConnectionDialogContext } from "./connectionDialogStateProvider";
const useStyles = makeStyles({
paneTitle: {
marginTop: '12px',
marginBottom: '12px',
},
main: {
gap: "36px",
display: "flex",
flexDirection: "column",
flexWrap: "wrap",
},
card: {
width: "100%",
maxWidth: "100%",
height: "fit-content",
marginBottom: "10px"
},
horizontalCardImage: {
width: "50px",
height: "30px",
paddingRight: '0px'
},
caption: {
color: tokens.colorNeutralForeground3,
},
text: { margin: "0" },
});
export const MruConnectionsContainer = () => {
const styles = useStyles();
const connectionDialogContext = useContext(ConnectionDialogContext);
return (
<div>
<div className={styles.paneTitle}>
<Text weight="semibold" className={styles.paneTitle}>Recent Connections</Text>
</div>
<Tree >
{
connectionDialogContext?.state?.recentConnections?.map((connection, index) => {
return <TreeItem itemType='leaf' key={'mru' + index} className={styles.card} onClick={() => {
connectionDialogContext.loadConnection(connection);
}}>
<TreeItemLayout iconBefore={<ServerRegular />}>
{connection.profileName}
</TreeItemLayout>
</TreeItem>
})
}
</Tree>
</div >
)
}

Просмотреть файл

@ -0,0 +1,13 @@
/* styles.css */
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.sqlServerSvg {
animation: rotate 2s linear infinite;
}

Просмотреть файл

@ -0,0 +1,155 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscodeMssql from "vscode-mssql";
export enum FormTabs {
Parameters = 'parameter',
ConnectionString = 'connString'
}
// A Connection Profile contains all the properties of connection credentials, with additional
// optional name and details on whether password should be saved
export interface IConnectionDialogProfile extends vscodeMssql.IConnectionInfo {
profileName?: string;
savePassword?: boolean;
emptyPasswordInput?: boolean;
azureAuthType?: vscodeMssql.AzureAuthType;
}
export interface ConnectionDialogWebviewState {
selectedFormTab: FormTabs;
recentConnections: IConnectionDialogProfile[];
formComponents: FormComponent[];
connectionProfile: IConnectionDialogProfile;
connectionStatus: ApiStatus;
formError: string;
}
export enum ApiStatus {
NotStarted = 'notStarted',
Loading = 'loading',
Loaded = 'loaded',
Error = 'error',
}
export interface ConnectionDialogContextProps {
state: ConnectionDialogWebviewState;
loadConnection: (connection: IConnectionDialogProfile) => void;
formAction: (event: FormEvent) => void;
setFormTab: (tab: FormTabs) => void;
connect: () => void;
}
/**
* Describes a field in a connection dialog form.
*/
export interface FormComponent {
/**
* The type of the form component
*/
type: FormComponentType;
/**
* The property name of the form component
*/
propertyName: keyof IConnectionDialogProfile;
/**
* The label of the form component
*/
label: string;
/**
* Whether the form component is required
*/
required: boolean;
/**
* The tooltip of the form component
*/
tooltip?: string;
/**
* The options for the form component in case of a dropdown
*/
options?: FormComponentOptions[];
/**
* Whether the form component is hidden
*/
hidden?: boolean;
/**
* Action buttons for the form component
*/
actionButtons?: FormComponentActionButton[];
/**
* Placeholder text for the form component
*/
placeholder?: string;
/**
* Validation callback for the form component
*/
validate?: (value: string | boolean | number) => FormComponentValidationState;
/**
* Validation state and message for the form component
*/
validation?: FormComponentValidationState;
}
export interface FormComponentValidationState {
/**
* The validation state of the form component
*/
isValid: boolean
/**
* The validation message of the form component
*/
validationMessage: string;
}
export interface FormComponentActionButton {
label: string;
id: string;
hidden?: boolean;
callback: () => void;
}
export interface FormComponentOptions {
displayName: string;
value: string;
}
/**
* Interface for a form event
*/
export interface FormEvent {
/**
* The property name of the form component that triggered the event
*/
propertyName: keyof IConnectionDialogProfile;
/**
* Whether the event was triggered by an action button for the component
*/
isAction: boolean;
/**
* Contains the updated value of the form component that triggered the event.
* In case of isAction being true, this will contain the id of the action button that was clicked
*/
value: string | boolean;
}
/**
* Enum for the type of form component
*/
export enum FormComponentType {
Input = 'input',
Dropdown = 'dropdown',
Checkbox = 'checkbox',
Password = 'password',
Button = 'button',
TextArea = 'textarea'
}
export enum AuthenticationType {
SqlLogin = 'SqlLogin',
Integrated = 'Integrated',
AzureMFA = 'AzureMFA'
}

Просмотреть файл

@ -21,6 +21,7 @@ import { Timer } from '../models/utils';
import { ObjectExplorerUtils } from '../objectExplorer/objectExplorerUtils';
import { INameValueChoice, IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
import { CancelError } from '../utils/utils';
import { ConnectionCompleteParams } from '../models/contracts/connection';
/**
* The different tasks for managing connection profiles.
@ -495,6 +496,14 @@ export class ConnectionUI {
});
}
/**
* Validate a connection profile by connecting to it, and save it if we are successful.
*/
public async validateAndSaveProfileFromDialog(profile: IConnectionProfile): Promise<ConnectionCompleteParams> {
const result = await this.connectionManager.connectDialog(profile);
return result;
}
public async addFirewallRule(uri: string, profile: IConnectionProfile): Promise<boolean> {
if (this.connectionManager.failedUriToFirewallIpMap.has(uri)) {
// Firewall rule error

Просмотреть файл

@ -16,9 +16,10 @@
"experimentalDecorators": true,
"jsx": "react-jsx",
"rootDir": ".",
"noUnusedLocals": true
"noUnusedLocals": true,
"skipLibCheck": true
},
"exclude": [
"node_modules",
]
],
}

Просмотреть файл

@ -25,6 +25,9 @@
"allowSyntheticDefaultImports": true,
},
"include": [
"src/reactviews"
]
"src/reactviews",
"typings",
"src/sharedInterfaces",
"media"
],
}

Просмотреть файл

@ -1718,11 +1718,16 @@
resolved "https://registry.yarnpkg.com/@types/vscode-webview/-/vscode-webview-1.57.5.tgz#5b910525386c02305eb1d0772e0181c5f19c579b"
integrity sha512-iBAUYNYkz+uk1kdsq05fEcoh8gJmwT3lqqFPN7MGyjQ3HVloViMdo7ZJ8DFIP8WOK74PjOEilosqAyxV2iUFUw==
"@types/vscode@*", "@types/vscode@1.78.1":
"@types/vscode@*":
version "1.78.1"
resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.78.1.tgz#027dba038c9e4c3f8e83570e1494aab679030485"
integrity sha512-wEA+54axejHu7DhcUfnFBan1IqFD1gBDxAFz8LoX06NbNDMRJv/T6OGthOs52yZccasKfN588EyffHWABkR0fg==
"@types/vscode@1.83.1":
version "1.83.1"
resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.83.1.tgz#fe51b3d913a5c5b265a622179ae4ab6c0af7d54e"
integrity sha512-BHu51NaNKOtDf3BOonY3sKFFmZKEpRkzqkZVpSYxowLbs5JqjOQemYFob7Gs5rpxE5tiGhfpnMpcdF/oKrLg4w==
"@typescript-eslint/eslint-plugin@7.13.1":
version "7.13.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.1.tgz#cdc521c8bca38b55585cf30db787fb2abad3f9fd"