New UI based add connection (#17939)
* 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:
Родитель
62294a9d86
Коммит
a3c514003b
|
@ -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>
|
||||
|
|
|
@ -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 |
|
@ -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 |
31
package.json
31
package.json
|
@ -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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче