Merge remote-tracking branch 'origin/master' into ads-master-vscode-2020-05-31T19-47-47

This commit is contained in:
Anthony Dresser
2020-06-03 12:35:26 -07:00
114 changed files with 2815 additions and 2428 deletions

View File

@@ -51,16 +51,14 @@ jobs:
name: Run Unit Tests (Electron) name: Run Unit Tests (Electron)
- run: DISPLAY=:10 ./scripts/test-extensions-unit.sh - run: DISPLAY=:10 ./scripts/test-extensions-unit.sh
name: Run Extension Unit Tests (Electron) name: Run Extension Unit Tests (Electron)
# {{SQL CARBON EDIT}} Add coveralls. We merge first to get around issue where parallel builds weren't being combined correctly # {{SQL CARBON EDIT}} Add coveralls. We merge first to get around issue where parallel builds weren't being combined correctly
- run: | - run: node test/combineCoverage
mkdir .build/coverage-combined name: Combine code coverage files
cat .build/coverage-single/lcov.info ./extensions/admin-tool-ext-win/coverage/lcov.info ./extensions/agent/coverage/lcov.info ./extensions/azurecore/coverage/lcov.info ./extensions/cms/coverage/lcov.info ./extensions/dacpac/coverage/lcov.info ./extensions/schema-compare/coverage/lcov.info ./extensions/notebook/coverage/lcov.info ./extensions/resource-deployment/coverage/lcov.info ./extensions/machine-learning/coverage/lcov.info ./extensions/sql-database-projects/coverage/lcov.info > .build/coverage-combined/lcov.info
name: Merge coverage reports
- name: Upload Code Coverage - name: Upload Code Coverage
uses: coverallsapp/github-action@v1.1.1 uses: coverallsapp/github-action@v1.1.1
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: '.build/coverage-combined/lcov.info' path-to-lcov: 'test/coverage/lcov.info'
# Fails with cryptic error (e.g. https://github.com/microsoft/vscode/pull/90292/checks?check_run_id=433681926#step:13:9) # Fails with cryptic error (e.g. https://github.com/microsoft/vscode/pull/90292/checks?check_run_id=433681926#step:13:9)
# - run: DISPLAY=:10 yarn test-browser --browser chromium # - run: DISPLAY=:10 yarn test-browser --browser chromium

View File

@@ -6,10 +6,10 @@
"**/node_modules/**", "**/node_modules/**",
"**/test/**" "**/test/**"
], ],
"includePid": false,
"reports": [ "reports": [
"cobertura", "cobertura",
"lcov" "lcov",
"json"
], ],
"verbose": false, "verbose": false,
"remapOptions": { "remapOptions": {

View File

@@ -85,7 +85,7 @@
"mocha-junit-reporter": "^1.17.0", "mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",
"should": "^13.2.3", "should": "^13.2.3",
"vscodetestcover": "github:corivera/vscodetestcover#1.0.6" "vscodetestcover": "^1.0.9"
}, },
"__metadata": { "__metadata": {
"id": "41", "id": "41",

View File

@@ -935,9 +935,10 @@ vscode-nls@^3.2.1:
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4"
integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw== integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw==
"vscodetestcover@github:corivera/vscodetestcover#1.0.6": vscodetestcover@^1.0.9:
version "1.0.5" version "1.0.9"
resolved "https://codeload.github.com/corivera/vscodetestcover/tar.gz/14e0f2c46346b31bc1af2c590febeaf69a9112eb" resolved "https://registry.yarnpkg.com/vscodetestcover/-/vscodetestcover-1.0.9.tgz#0191f403dd59ba1153fc57979e281e992ce63731"
integrity sha512-8z2961KF9Tuz5XdHAC6RMV3CrzAoUcfIK7wLYjLIXD4dbHIT7ceZMhoxToW1olyi3pFnThlS4lRXtx8Q5iyMMQ==
dependencies: dependencies:
decache "^4.4.0" decache "^4.4.0"
glob "^7.1.2" glob "^7.1.2"

View File

@@ -6,10 +6,10 @@
"**/node_modules/**", "**/node_modules/**",
"**/test/**" "**/test/**"
], ],
"includePid": false,
"reports": [ "reports": [
"cobertura", "cobertura",
"lcov" "lcov",
"json"
], ],
"verbose": false, "verbose": false,
"remapOptions": { "remapOptions": {

View File

@@ -90,7 +90,7 @@
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",
"should": "^13.2.1", "should": "^13.2.1",
"typemoq": "^2.1.0", "typemoq": "^2.1.0",
"vscodetestcover": "github:corivera/vscodetestcover#1.0.6" "vscodetestcover": "^1.0.9"
}, },
"__metadata": { "__metadata": {
"id": "10", "id": "10",

View File

@@ -769,9 +769,10 @@ vscode-nls@^3.2.1:
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4"
integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw== integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw==
"vscodetestcover@github:corivera/vscodetestcover#1.0.6": vscodetestcover@^1.0.9:
version "1.0.5" version "1.0.9"
resolved "https://codeload.github.com/corivera/vscodetestcover/tar.gz/14e0f2c46346b31bc1af2c590febeaf69a9112eb" resolved "https://registry.yarnpkg.com/vscodetestcover/-/vscodetestcover-1.0.9.tgz#0191f403dd59ba1153fc57979e281e992ce63731"
integrity sha512-8z2961KF9Tuz5XdHAC6RMV3CrzAoUcfIK7wLYjLIXD4dbHIT7ceZMhoxToW1olyi3pFnThlS4lRXtx8Q5iyMMQ==
dependencies: dependencies:
decache "^4.4.0" decache "^4.4.0"
glob "^7.1.2" glob "^7.1.2"

View File

@@ -6,10 +6,10 @@
"**/node_modules/**", "**/node_modules/**",
"**/test/**" "**/test/**"
], ],
"includePid": false,
"reports": [ "reports": [
"cobertura", "cobertura",
"lcov" "lcov",
"json"
], ],
"verbose": false, "verbose": false,
"remapOptions": { "remapOptions": {

View File

@@ -146,10 +146,7 @@
{ {
"command": "azure.resource.startterminal", "command": "azure.resource.startterminal",
"title": "%azure.resource.startterminal.title%", "title": "%azure.resource.startterminal.title%",
"icon": { "icon": "$(console)"
"dark": "resources/dark/console.svg",
"light": "resources/light/console.svg"
}
}, },
{ {
"command": "azure.resource.openInAzurePortal", "command": "azure.resource.openInAzurePortal",
@@ -288,6 +285,6 @@
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",
"should": "^13.2.1", "should": "^13.2.1",
"typemoq": "^2.1.0", "typemoq": "^2.1.0",
"vscodetestcover": "github:corivera/vscodetestcover#1.0.6" "vscodetestcover": "^1.0.9"
} }
} }

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H15V15H1V1ZM2 14H14V2H2V14ZM4.00008 5.70709L4.70718 4.99999L8.24272 8.53552L7.53561 9.24263L7.53558 9.2426L4.70711 12.0711L4 11.364L6.82848 8.53549L4.00008 5.70709Z" fill="#C5C5C5"/>
</svg>

Before

Width:  |  Height:  |  Size: 339 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H15V15H1V1ZM2 14H14V2H2V14ZM4.00008 5.70709L4.70718 4.99999L8.24272 8.53552L7.53561 9.24263L7.53558 9.2426L4.70711 12.0711L4 11.364L6.82848 8.53549L4.00008 5.70709Z" fill="#424242"/>
</svg>

Before

Width:  |  Height:  |  Size: 339 B

View File

@@ -1331,9 +1331,10 @@ vscode-nls@^4.0.0:
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167"
integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw== integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw==
"vscodetestcover@github:corivera/vscodetestcover#1.0.6": vscodetestcover@^1.0.9:
version "1.0.5" version "1.0.9"
resolved "https://codeload.github.com/corivera/vscodetestcover/tar.gz/14e0f2c46346b31bc1af2c590febeaf69a9112eb" resolved "https://registry.yarnpkg.com/vscodetestcover/-/vscodetestcover-1.0.9.tgz#0191f403dd59ba1153fc57979e281e992ce63731"
integrity sha512-8z2961KF9Tuz5XdHAC6RMV3CrzAoUcfIK7wLYjLIXD4dbHIT7ceZMhoxToW1olyi3pFnThlS4lRXtx8Q5iyMMQ==
dependencies: dependencies:
decache "^4.4.0" decache "^4.4.0"
glob "^7.1.2" glob "^7.1.2"

View File

@@ -6,10 +6,10 @@
"**/node_modules/**", "**/node_modules/**",
"**/test/**" "**/test/**"
], ],
"includePid": false,
"reports": [ "reports": [
"cobertura", "cobertura",
"lcov" "lcov",
"json"
], ],
"verbose": false, "verbose": false,
"remapOptions": { "remapOptions": {

View File

@@ -657,7 +657,7 @@
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"should": "^13.2.1", "should": "^13.2.1",
"vscodetestcover": "github:corivera/vscodetestcover#1.0.6", "vscodetestcover": "^1.0.9",
"typemoq": "^2.1.0" "typemoq": "^2.1.0"
}, },
"__metadata": { "__metadata": {

View File

@@ -1088,9 +1088,10 @@ vscode-nls@^4.0.0:
resolved "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" resolved "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002"
integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw== integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw==
"vscodetestcover@github:corivera/vscodetestcover#1.0.6": vscodetestcover@^1.0.9:
version "1.0.5" version "1.0.9"
resolved "https://codeload.github.com/corivera/vscodetestcover/tar.gz/14e0f2c46346b31bc1af2c590febeaf69a9112eb" resolved "https://registry.yarnpkg.com/vscodetestcover/-/vscodetestcover-1.0.9.tgz#0191f403dd59ba1153fc57979e281e992ce63731"
integrity sha512-8z2961KF9Tuz5XdHAC6RMV3CrzAoUcfIK7wLYjLIXD4dbHIT7ceZMhoxToW1olyi3pFnThlS4lRXtx8Q5iyMMQ==
dependencies: dependencies:
decache "^4.4.0" decache "^4.4.0"
glob "^7.1.2" glob "^7.1.2"

View File

@@ -6,10 +6,10 @@
"**/node_modules/**", "**/node_modules/**",
"**/test/**" "**/test/**"
], ],
"includePid": false,
"reports": [ "reports": [
"cobertura", "cobertura",
"lcov" "lcov",
"json"
], ],
"verbose": false, "verbose": false,
"remapOptions": { "remapOptions": {

View File

@@ -5,7 +5,6 @@
"ignorePatterns": [ "ignorePatterns": [
"**/node_modules/**" "**/node_modules/**"
], ],
"includePid": false,
"reports": [ "reports": [
"cobertura" "cobertura"
], ],
@@ -14,4 +13,4 @@
"basePath": ".", "basePath": ".",
"useAbsolutePaths": true "useAbsolutePaths": true
} }
} }

View File

@@ -62,7 +62,7 @@
"mocha-junit-reporter": "^1.17.0", "mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",
"should": "^13.2.1", "should": "^13.2.1",
"vscodetestcover": "github:corivera/vscodetestcover#1.0.6" "vscodetestcover": "^1.0.9"
}, },
"__metadata": { "__metadata": {
"id": "33", "id": "33",

View File

@@ -816,9 +816,10 @@ vscode-nls@^4.0.0:
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c"
integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A== integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A==
"vscodetestcover@github:corivera/vscodetestcover#1.0.6": vscodetestcover@^1.0.9:
version "1.0.5" version "1.0.9"
resolved "https://codeload.github.com/corivera/vscodetestcover/tar.gz/14e0f2c46346b31bc1af2c590febeaf69a9112eb" resolved "https://registry.yarnpkg.com/vscodetestcover/-/vscodetestcover-1.0.9.tgz#0191f403dd59ba1153fc57979e281e992ce63731"
integrity sha512-8z2961KF9Tuz5XdHAC6RMV3CrzAoUcfIK7wLYjLIXD4dbHIT7ceZMhoxToW1olyi3pFnThlS4lRXtx8Q5iyMMQ==
dependencies: dependencies:
decache "^4.4.0" decache "^4.4.0"
glob "^7.1.2" glob "^7.1.2"

View File

@@ -0,0 +1,21 @@
{
"enabled": true,
"relativeSourcePath": "../../../",
"relativeCoverageDir": "../../coverage",
"ignorePatterns": [
"**/coverage/**",
"**/generated/**",
"**/node_modules/**",
"**/test/**"
],
"reports": [
"cobertura",
"lcov",
"json"
],
"verbose": false,
"remapOptions": {
"basePath": "../../../",
"useAbsolutePaths": true
}
}

View File

@@ -22,8 +22,7 @@
"Microsoft.azuredatastudio-postgresql" "Microsoft.azuredatastudio-postgresql"
], ],
"scripts": { "scripts": {
"vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-colorize-tests ./tsconfig.json", "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-colorize-tests ./tsconfig.json"
"postinstall": "node ./node_modules/vscode/bin/install"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "3.4.34", "@types/chai": "3.4.34",
@@ -33,6 +32,6 @@
"mocha-junit-reporter": "^1.17.0", "mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",
"ms-rest-azure": "^2.6.0", "ms-rest-azure": "^2.6.0",
"vscode": "1.1.5" "vscodetestcover": "^1.0.9"
} }
} }

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as testRunner from 'vscodetestcover';
const suite = 'Extension Integration Tests';
const mochaOptions: any = {
ui: 'tdd',
useColors: true,
timeout: 60000
};
// set relevant mocha options from the environment
if (process.env.ADS_TEST_GREP) {
mochaOptions.grep = process.env.ADS_TEST_GREP;
console.log(`setting options.grep to: ${mochaOptions.grep}`);
}
if (process.env.ADS_TEST_INVERT_GREP) {
mochaOptions.invert = parseInt(process.env.ADS_TEST_INVERT_GREP);
console.log(`setting options.invert to: ${mochaOptions.invert}`);
}
if (process.env.ADS_TEST_TIMEOUT) {
mochaOptions.timeout = parseInt(process.env.ADS_TEST_TIMEOUT);
console.log(`setting options.timeout to: ${mochaOptions.timeout}`);
}
if (process.env.ADS_TEST_RETRIES) {
mochaOptions.retries = parseInt(process.env.ADS_TEST_RETRIES);
console.log(`setting options.retries to: ${mochaOptions.retries}`);
}
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
mochaOptions.reporter = 'mocha-multi-reporters';
mochaOptions.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} ${process.platform}`,
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}
testRunner.configure(mochaOptions, { coverConfig: '../../coverConfig.json' });
export = testRunner;

View File

@@ -84,7 +84,7 @@ suite('Notebook integration test suite', function () {
} }
}); });
test('Sql NB run cells above and below test', async function () { test('Sql NB run cells above and below test @UNSTABLE@', async function () {
let notebook = await openNotebook(sqlNotebookMultipleCellsContent, sqlKernelMetadata, this.test.title + this.invocationCount++); let notebook = await openNotebook(sqlNotebookMultipleCellsContent, sqlKernelMetadata, this.test.title + this.invocationCount++);
// When running all cells above a cell, ensure that only cells preceding current cell have output // When running all cells above a cell, ensure that only cells preceding current cell have output
await runCells(notebook, true, undefined, notebook.document.cells[1]); await runCells(notebook, true, undefined, notebook.document.cells[1]);
@@ -101,13 +101,13 @@ suite('Notebook integration test suite', function () {
assert(notebook.document.cells[2].contents.outputs.length === 3, `Expected length: '3', Actual: '${notebook.document.cells[2].contents.outputs.length}'`); assert(notebook.document.cells[2].contents.outputs.length === 3, `Expected length: '3', Actual: '${notebook.document.cells[2].contents.outputs.length}'`);
}); });
test('Clear cell output - SQL notebook', async function () { test('Clear cell output - SQL notebook @UNSTABLE@', async function () {
let notebook = await openNotebook(sqlNotebookContent, sqlKernelMetadata, this.test.title + this.invocationCount++); let notebook = await openNotebook(sqlNotebookContent, sqlKernelMetadata, this.test.title + this.invocationCount++);
await runCell(notebook); await runCell(notebook);
await verifyClearOutputs(notebook); await verifyClearOutputs(notebook);
}); });
test('Clear all outputs - SQL notebook ', async function () { test('Clear all outputs - SQL notebook @UNSTABLE@', async function () {
let notebook = await openNotebook(sqlNotebookContent, sqlKernelMetadata, this.test.title + this.invocationCount++); let notebook = await openNotebook(sqlNotebookContent, sqlKernelMetadata, this.test.title + this.invocationCount++);
await runCell(notebook); await runCell(notebook);
await verifyClearAllOutputs(notebook); await verifyClearAllOutputs(notebook);

View File

@@ -1,50 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as testRunner from 'vscode/lib/testrunner';
import * as path from 'path';
const suite = 'Extension Integration Tests';
const options: any = {
ui: 'tdd',
useColors: true,
timeout: 600000
};
// set relevant mocha options from the environment
if (process.env.ADS_TEST_GREP) {
options.grep = process.env.ADS_TEST_GREP;
console.log(`setting options.grep to: ${options.grep}`);
}
if (process.env.ADS_TEST_INVERT_GREP) {
const value = parseInt(process.env.ADS_TEST_INVERT_GREP);
options.invert = Boolean(value);
console.log(`setting options.invert to: ${options.invert}`);
}
if (process.env.ADS_TEST_TIMEOUT) {
options.timeout = parseInt(process.env.ADS_TEST_TIMEOUT);
console.log(`setting options.timeout to: ${options.timeout}`);
}
if (process.env.ADS_TEST_RETRIES) {
options.retries = parseInt(process.env.ADS_TEST_RETRIES);
console.log(`setting options.retries to: ${options.retries}`);
}
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
console.log(`environment variable BUILD_ARTIFACTSTAGINGDIRECTORY is set to ${process.env.BUILD_ARTIFACTSTAGINGDIRECTORY} so configuring multiple reporters for test results.\n For this to work the ${process.env.BUILD_ARTIFACTSTAGINGDIRECTORY} must be fully qualified directory and must exist`);
options.reporter = 'mocha-multi-reporters';
options.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} Tests ${process.platform}`,
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}
testRunner.configure(options);
export = testRunner;

View File

@@ -5,4 +5,5 @@
/// <reference path='../../../../src/sql/azdata.d.ts'/> /// <reference path='../../../../src/sql/azdata.d.ts'/>
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/> /// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
/// <reference path='../../../../src/sql/azdata.test.d.ts'/> /// <reference path='../../../../src/sql/azdata.test.d.ts'/>
/// <reference path='../../../../src/vs/vscode.d.ts'/>
/// <reference types='@types/node'/> /// <reference types='@types/node'/>

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"requiredPythonPackages": [ "requiredPythonPackages": [
{ {
"name": "sqlmlutils", "name": "sqlmlutils",
"version": "1.0.0" "version": "1.0.1"
} }
], ],
"requiredRPackages": [ "requiredRPackages": [

View File

@@ -6,10 +6,10 @@
"**/node_modules/**", "**/node_modules/**",
"**/test/**" "**/test/**"
], ],
"includePid": false,
"reports": [ "reports": [
"cobertura", "cobertura",
"lcov" "lcov",
"json"
], ],
"verbose": false, "verbose": false,
"remapOptions": { "remapOptions": {

View File

@@ -148,7 +148,7 @@
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",
"should": "^13.2.1", "should": "^13.2.1",
"typemoq": "^2.1.0", "typemoq": "^2.1.0",
"vscodetestcover": "github:corivera/vscodetestcover#1.0.6" "vscodetestcover": "^1.0.9"
}, },
"__metadata": { "__metadata": {
"id": "65", "id": "65",

View File

@@ -67,13 +67,14 @@ export class SqlPythonPackageManageProvider extends SqlPackageManageProviderBase
let port = '1433'; let port = '1433';
let server = connection.serverName; let server = connection.serverName;
let database = databaseName ? `, database="${databaseName}"` : ''; let database = databaseName ? `, database="${databaseName}"` : '';
const auth = connection.userName ? `, uid="${connection.userName}", pwd="${credentials[azdata.ConnectionOptionSpecialType.password]}"` : '';
let index = connection.serverName.indexOf(','); let index = connection.serverName.indexOf(',');
if (index > 0) { if (index > 0) {
port = connection.serverName.substring(index + 1); port = connection.serverName.substring(index + 1);
server = connection.serverName.substring(0, index); server = connection.serverName.substring(0, index);
} }
let pythonConnectionParts = `server="${server}", port=${port}, uid="${connection.userName}", pwd="${credentials[azdata.ConnectionOptionSpecialType.password]}"${database})`; let pythonConnectionParts = `server="${server}", port=${port}${auth}${database})`;
let pythonCommandScript = scriptMode === ScriptMode.Install ? let pythonCommandScript = scriptMode === ScriptMode.Install ?
`pkgmanager.install(package="${packageDetails.name}", version="${packageDetails.version}")` : `pkgmanager.install(package="${packageDetails.name}", version="${packageDetails.version}")` :
`pkgmanager.uninstall(package_name="${packageDetails.name}")`; `pkgmanager.uninstall(package_name="${packageDetails.name}")`;

View File

@@ -83,7 +83,7 @@ export class PredictService {
language: 'sql', language: 'sql',
content: query content: query
}); });
await this._apiWrapper.showTextDocument(document.uri); await this._apiWrapper.executeCommand('vscode.open', document.uri);
await this._apiWrapper.connect(document.uri.toString(), connection.connectionId); await this._apiWrapper.connect(document.uri.toString(), connection.connectionId);
this._apiWrapper.runQuery(document.uri.toString(), undefined, false); this._apiWrapper.runQuery(document.uri.toString(), undefined, false);
return query; return query;

View File

@@ -132,6 +132,7 @@ describe('SQL Python Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile(); let connection = new azdata.connection.ConnectionProfile();
connection.serverName = 'serverName'; connection.serverName = 'serverName';
connection.databaseName = 'databaseName'; connection.databaseName = 'databaseName';
connection.userName = 'user';
let credentials = { [azdata.ConnectionOptionSpecialType.password]: 'password' }; let credentials = { [azdata.ConnectionOptionSpecialType.password]: 'password' };
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.apiWrapper.setup(x => x.getCredentials(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(credentials); }); testContext.apiWrapper.setup(x => x.getCredentials(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(credentials); });
@@ -173,6 +174,7 @@ describe('SQL Python Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile(); let connection = new azdata.connection.ConnectionProfile();
connection.serverName = 'serverName'; connection.serverName = 'serverName';
connection.databaseName = 'databaseName'; connection.databaseName = 'databaseName';
connection.userName = 'user';
let credentials = { [azdata.ConnectionOptionSpecialType.password]: 'password' }; let credentials = { [azdata.ConnectionOptionSpecialType.password]: 'password' };
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.apiWrapper.setup(x => x.getCredentials(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(credentials); }); testContext.apiWrapper.setup(x => x.getCredentials(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(credentials); });
@@ -213,6 +215,7 @@ describe('SQL Python Package Manager', () => {
let connection = new azdata.connection.ConnectionProfile(); let connection = new azdata.connection.ConnectionProfile();
connection.serverName = 'serverName,3433'; connection.serverName = 'serverName,3433';
connection.databaseName = 'databaseName'; connection.databaseName = 'databaseName';
connection.userName = 'user';
let credentials = { [azdata.ConnectionOptionSpecialType.password]: 'password' }; let credentials = { [azdata.ConnectionOptionSpecialType.password]: 'password' };
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.apiWrapper.setup(x => x.getCredentials(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(credentials); }); testContext.apiWrapper.setup(x => x.getCredentials(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(credentials); });
@@ -237,6 +240,88 @@ describe('SQL Python Package Manager', () => {
should.deepEqual(packagesUpdated, true); should.deepEqual(packagesUpdated, true);
}); });
it('installPackages Should not include credential for windows auth', async function (): Promise<void> {
let testContext = createContext();
let packagesUpdated = false;
let packages: nbExtensionApis.IPackageDetails[] = [
{
'name': 'a-name',
'version': '1.1.2'
},
{
'name': 'b-name',
'version': '1.1.1'
}
];
let connection = new azdata.connection.ConnectionProfile();
connection.serverName = 'serverName,3433';
connection.databaseName = 'databaseName';
let credentials = { [azdata.ConnectionOptionSpecialType.password]: 'password' };
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.apiWrapper.setup(x => x.getCredentials(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(credentials); });
testContext.processService.setup(x => x.execScripts(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((path, scripts: string[]) => {
if (path && scripts.find(x => x.indexOf('install') > 0) &&
scripts.find(x => x.indexOf('port=3433') > 0) &&
scripts.find(x => x.indexOf('server="serverName"') > 0) &&
scripts.find(x => x.indexOf('database="databaseName"') > 0) &&
scripts.find(x => x.indexOf('package="a-name"') > 0) &&
scripts.find(x => x.indexOf('version="1.1.2"') > 0) &&
scripts.find(x => x.indexOf('pwd="password"') < 0)) {
packagesUpdated = true;
}
return Promise.resolve('');
});
let provider = createProvider(testContext);
await provider.installPackages(packages, false, connection.databaseName);
should.deepEqual(packagesUpdated, true);
});
it('installPackages Should not include database if not specified', async function (): Promise<void> {
let testContext = createContext();
let packagesUpdated = false;
let packages: nbExtensionApis.IPackageDetails[] = [
{
'name': 'a-name',
'version': '1.1.2'
},
{
'name': 'b-name',
'version': '1.1.1'
}
];
let connection = new azdata.connection.ConnectionProfile();
connection.serverName = 'serverName,3433';
connection.databaseName = '';
let credentials = { [azdata.ConnectionOptionSpecialType.password]: 'password' };
testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); });
testContext.apiWrapper.setup(x => x.getCredentials(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(credentials); });
testContext.processService.setup(x => x.execScripts(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((path, scripts: string[]) => {
if (path && scripts.find(x => x.indexOf('install') > 0) &&
scripts.find(x => x.indexOf('port=3433') > 0) &&
scripts.find(x => x.indexOf('server="serverName"') > 0) &&
scripts.find(x => x.indexOf('database="databaseName"') < 0) &&
scripts.find(x => x.indexOf('package="a-name"') > 0) &&
scripts.find(x => x.indexOf('version="1.1.2"') > 0) &&
scripts.find(x => x.indexOf('pwd="password"') < 0)) {
packagesUpdated = true;
}
return Promise.resolve('');
});
let provider = createProvider(testContext);
await provider.installPackages(packages, false, connection.databaseName);
should.deepEqual(packagesUpdated, true);
});
it('installPackages Should not install any packages give empty list', async function (): Promise<void> { it('installPackages Should not install any packages give empty list', async function (): Promise<void> {
let testContext = createContext(); let testContext = createContext();
let packagesUpdated = false; let packagesUpdated = false;

View File

@@ -1,19 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import 'mocha';
import { createContext } from './utils';
import { LanguagesDialog } from '../../../views/externalLanguages/languagesDialog';
describe('External Languages Dialog', () => {
it('Should open dialog successfully ', async function (): Promise<void> {
let testContext = createContext();
let dialog = new LanguagesDialog(testContext.apiWrapper.object, '');
dialog.showDialog();
should.notEqual(dialog.addNewLanguageTab, undefined);
should.notEqual(dialog.currentLanguagesTab, undefined);
});
});

View File

@@ -1242,9 +1242,10 @@ vscode-nls@^4.0.0:
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c"
integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A== integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A==
"vscodetestcover@github:corivera/vscodetestcover#1.0.6": vscodetestcover@^1.0.9:
version "1.0.5" version "1.0.9"
resolved "https://codeload.github.com/corivera/vscodetestcover/tar.gz/14e0f2c46346b31bc1af2c590febeaf69a9112eb" resolved "https://registry.yarnpkg.com/vscodetestcover/-/vscodetestcover-1.0.9.tgz#0191f403dd59ba1153fc57979e281e992ce63731"
integrity sha512-8z2961KF9Tuz5XdHAC6RMV3CrzAoUcfIK7wLYjLIXD4dbHIT7ceZMhoxToW1olyi3pFnThlS4lRXtx8Q5iyMMQ==
dependencies: dependencies:
decache "^4.4.0" decache "^4.4.0"
glob "^7.1.2" glob "^7.1.2"

View File

@@ -6,10 +6,10 @@
"**/node_modules/**", "**/node_modules/**",
"**/test/**" "**/test/**"
], ],
"includePid": false,
"reports": [ "reports": [
"cobertura", "cobertura",
"lcov" "lcov",
"json"
], ],
"verbose": false, "verbose": false,
"remapOptions": { "remapOptions": {

View File

@@ -1110,6 +1110,6 @@
"mocha-junit-reporter": "^1.17.0", "mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",
"typemoq": "^2.1.0", "typemoq": "^2.1.0",
"vscodetestcover": "github:corivera/vscodetestcover#1.0.6" "vscodetestcover": "^1.0.9"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,10 @@
"**/node_modules/**", "**/node_modules/**",
"**/test/**" "**/test/**"
], ],
"includePid": false,
"reports": [ "reports": [
"cobertura", "cobertura",
"lcov" "lcov",
"json"
], ],
"verbose": false, "verbose": false,
"remapOptions": { "remapOptions": {

View File

@@ -339,6 +339,12 @@
"when": "false" "when": "false"
} }
], ],
"notebooks/title": [
{
"command": "notebook.command.createBook",
"group": "secondary"
}
],
"touchBar": [ "touchBar": [
{ {
"command": "notebook.command.runactivecell", "command": "notebook.command.runactivecell",
@@ -376,12 +382,12 @@
}, },
{ {
"command": "notebook.command.searchUntitledBook", "command": "notebook.command.searchUntitledBook",
"when": "view == unsavedBookTreeView && viewItem == unsavedBook && unsavedBooks", "when": "view == providedBooksView && viewItem == providedBook && providedBooks",
"group": "inline" "group": "inline"
}, },
{ {
"command": "notebook.command.saveBook", "command": "notebook.command.saveBook",
"when": "view == unsavedBookTreeView && viewItem == unsavedBook && unsavedBooks", "when": "view == providedBooksView && viewItem == providedBook && providedBooks",
"group": "inline" "group": "inline"
}, },
{ {
@@ -400,8 +406,8 @@
"group": "navigation" "group": "navigation"
}, },
{ {
"command": "notebook.command.createBook", "command": "books.sqlserver2019",
"when": "view == bookTreeView" "when": "view == providedBooksView"
}, },
{ {
"command": "notebook.command.openNotebookFolder", "command": "notebook.command.openNotebookFolder",
@@ -416,6 +422,18 @@
} }
] ]
}, },
"views": {
"notebooks": [
{
"id": "bookTreeView",
"name": "%title.SavedBooks%"
},
{
"id": "providedBooksView",
"name": "%title.ProvidedBooks%"
}
]
},
"keybindings": [ "keybindings": [
{ {
"command": "notebook.command.runactivecell", "command": "notebook.command.runactivecell",
@@ -507,27 +525,6 @@
"connectionProviderIds": [] "connectionProviderIds": []
} }
] ]
},
"viewsContainers": {
"activitybar": [
{
"id": "books-explorer",
"title": "Notebooks",
"icon": "resources/dark/JupyterBook_2.svg"
}
]
},
"views": {
"books-explorer": [
{
"id": "bookTreeView",
"name": "%title.SavedBooks%"
},
{
"id": "unsavedBookTreeView",
"name": "%title.UnsavedBooks%"
}
]
} }
}, },
"dependencies": { "dependencies": {
@@ -564,7 +561,7 @@
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",
"should": "^13.2.3", "should": "^13.2.3",
"typemoq": "^2.1.0", "typemoq": "^2.1.0",
"vscodetestcover": "github:corivera/vscodetestcover#1.0.6" "vscodetestcover": "^1.0.9"
}, },
"enableProposedApi": true "enableProposedApi": true
} }

View File

@@ -34,7 +34,7 @@
"title.trustBook": "Trust Book", "title.trustBook": "Trust Book",
"title.searchJupyterBook": "Search Book", "title.searchJupyterBook": "Search Book",
"title.SavedBooks": "Notebooks", "title.SavedBooks": "Notebooks",
"title.UnsavedBooks": "Provided Books", "title.ProvidedBooks": "Provided Books",
"title.PreviewLocalizedBook": "Get localized SQL Server 2019 guide", "title.PreviewLocalizedBook": "Get localized SQL Server 2019 guide",
"title.openJupyterBook": "Open Jupyter Book", "title.openJupyterBook": "Open Jupyter Book",
"title.closeJupyterBook": "Close Jupyter Book", "title.closeJupyterBook": "Close Jupyter Book",

View File

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>ADS_jupyterBook</title>
<path d="M4.25,24A2.251,2.251,0,0,1,2,21.75V3.86A2.257,2.257,0,0,1,3.962,1.628L16.462.019A2.251,2.251,0,0,1,19,2.25H17.5a.741.741,0,0,0-.255-.563.755.755,0,0,0-.591-.181l-12.5,1.61A.752.752,0,0,0,3.5,3.86V21.75a.738.738,0,0,0,.255.563.746.746,0,0,0,.59.181l12.5-1.61a.753.753,0,0,0,.655-.744V2.263H19V20.14a2.257,2.257,0,0,1-1.963,2.232l-12.5,1.609A2.139,2.139,0,0,1,4.25,24Z" fill="#fff"/>
<path d="M19.75,24H4.5a.75.75,0,0,1,0-1.5H19.75a.75.75,0,0,0,.75-.75V3.763a.75.75,0,0,0-.75-.75h-1.5a.75.75,0,0,1,0-1.5h1.5A2.252,2.252,0,0,1,22,3.763V21.75A2.252,2.252,0,0,1,19.75,24Z" fill="#fff"/>
<rect width="24" height="24" fill="none"/>
<path d="M7.645,10.966A1.615,1.615,0,0,1,6.022,9.351v-1.8a1.619,1.619,0,0,1,1.411-1.6l5.809-.927a1.615,1.615,0,0,1,1.845,1.6v1.8a1.62,1.62,0,0,1-1.41,1.6l-5.809.927A1.693,1.693,0,0,1,7.645,10.966ZM13.36,5.757l.119.741-5.833.93c-.08.011-.124.06-.124.119v1.8l.228.859-.118-.74,5.831-.93a.126.126,0,0,0,.124-.119v-1.8Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -41,7 +41,7 @@ export class BookTreeItem extends vscode.TreeItem {
this.collapsibleState = book.treeItemCollapsibleState; this.collapsibleState = book.treeItemCollapsibleState;
this._sections = book.page; this._sections = book.page;
if (book.isUntitled) { if (book.isUntitled) {
this.contextValue = 'unsavedBook'; this.contextValue = 'providedBook';
} else { } else {
this.contextValue = 'savedBook'; this.contextValue = 'savedBook';
} }

View File

@@ -54,7 +54,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
} }
private async initialize(workspaceFolders: vscode.WorkspaceFolder[]): Promise<void> { private async initialize(workspaceFolders: vscode.WorkspaceFolder[]): Promise<void> {
await vscode.commands.executeCommand('setContext', 'unsavedBooks', this._openAsUntitled); await vscode.commands.executeCommand('setContext', 'providedBooks', this._openAsUntitled);
await Promise.all(workspaceFolders.map(async (workspaceFolder) => { await Promise.all(workspaceFolders.map(async (workspaceFolder) => {
try { try {
await this.loadNotebooksInFolder(workspaceFolder.uri.fsPath); await this.loadNotebooksInFolder(workspaceFolder.uri.fsPath);

View File

@@ -24,21 +24,21 @@ const JUPYTER_NOTEBOOK_PROVIDER = 'jupyter';
const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', "This sample code loads the file into a data frame and shows the first 10 results."); const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', "This sample code loads the file into a data frame and shows the first 10 results.");
const noNotebookVisible = localize('noNotebookVisible', "No notebook editor is active"); const noNotebookVisible = localize('noNotebookVisible', "No notebook editor is active");
const BOOKS_VIEWID = 'bookTreeView'; const BOOKS_VIEWID = 'bookTreeView';
const READONLY_BOOKS_VIEWID = 'unsavedBookTreeView'; const PROVIDED_BOOKS_VIEWID = 'providedBooksView';
let controller: JupyterController; let controller: JupyterController;
type ChooseCellType = { label: string, id: CellType }; type ChooseCellType = { label: string, id: CellType };
export async function activate(extensionContext: vscode.ExtensionContext): Promise<IExtensionApi> { export async function activate(extensionContext: vscode.ExtensionContext): Promise<IExtensionApi> {
const createBookPath: string = path.posix.join(extensionContext.extensionPath, 'resources', 'notebooks', 'JupyterBooksCreate.ipynb'); const createBookPath: string = path.posix.join(extensionContext.extensionPath, 'resources', 'notebooks', 'JupyterBooksCreate.ipynb');
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openBook', (bookPath: string, openAsUntitled: boolean, urlToOpen?: string) => openAsUntitled ? untitledBookTreeViewProvider.openBook(bookPath, urlToOpen, true) : bookTreeViewProvider.openBook(bookPath, urlToOpen, true))); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openBook', (bookPath: string, openAsUntitled: boolean, urlToOpen?: string) => openAsUntitled ? providedBookTreeViewProvider.openBook(bookPath, urlToOpen, true) : bookTreeViewProvider.openBook(bookPath, urlToOpen, true)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebook', (resource) => bookTreeViewProvider.openNotebook(resource))); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebook', (resource) => bookTreeViewProvider.openNotebook(resource)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openUntitledNotebook', (resource) => untitledBookTreeViewProvider.openNotebookAsUntitled(resource))); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openUntitledNotebook', (resource) => providedBookTreeViewProvider.openNotebookAsUntitled(resource)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openMarkdown', (resource) => bookTreeViewProvider.openMarkdown(resource))); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openMarkdown', (resource) => bookTreeViewProvider.openMarkdown(resource)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openExternalLink', (resource) => bookTreeViewProvider.openExternalLink(resource))); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openExternalLink', (resource) => bookTreeViewProvider.openExternalLink(resource)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.saveBook', () => untitledBookTreeViewProvider.saveJupyterBooks())); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.saveBook', () => providedBookTreeViewProvider.saveJupyterBooks()));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.trustBook', (resource) => bookTreeViewProvider.trustBook(resource))); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.trustBook', (resource) => bookTreeViewProvider.trustBook(resource)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.searchBook', (item) => bookTreeViewProvider.searchJupyterBooks(item))); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.searchBook', (item) => bookTreeViewProvider.searchJupyterBooks(item)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.searchUntitledBook', () => untitledBookTreeViewProvider.searchJupyterBooks())); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.searchUntitledBook', () => providedBookTreeViewProvider.searchJupyterBooks()));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.openBook', () => bookTreeViewProvider.openNewBook())); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.openBook', () => bookTreeViewProvider.openNewBook()));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.closeBook', (book: any) => bookTreeViewProvider.closeBook(book))); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.closeBook', (book: any) => bookTreeViewProvider.closeBook(book)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.closeNotebook', (book: any) => bookTreeViewProvider.closeBook(book))); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.closeNotebook', (book: any) => bookTreeViewProvider.closeBook(book)));
@@ -114,7 +114,7 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
})); }));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.revealInBooksViewlet', (uri: vscode.Uri, shouldReveal: boolean) => bookTreeViewProvider.revealActiveDocumentInViewlet(uri, shouldReveal))); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.revealInBooksViewlet', (uri: vscode.Uri, shouldReveal: boolean) => bookTreeViewProvider.revealActiveDocumentInViewlet(uri, shouldReveal)));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.revealInUntitledBooksViewlet', (uri: vscode.Uri, shouldReveal: boolean) => untitledBookTreeViewProvider.revealActiveDocumentInViewlet(uri, shouldReveal))); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.revealInUntitledBooksViewlet', (uri: vscode.Uri, shouldReveal: boolean) => providedBookTreeViewProvider.revealActiveDocumentInViewlet(uri, shouldReveal)));
let appContext = new AppContext(extensionContext, new ApiWrapper()); let appContext = new AppContext(extensionContext, new ApiWrapper());
controller = new JupyterController(appContext); controller = new JupyterController(appContext);
@@ -126,9 +126,12 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
let workspaceFolders = vscode.workspace.workspaceFolders?.slice() ?? []; let workspaceFolders = vscode.workspace.workspaceFolders?.slice() ?? [];
const bookTreeViewProvider = new BookTreeViewProvider(appContext.apiWrapper, workspaceFolders, extensionContext, false, BOOKS_VIEWID); const bookTreeViewProvider = new BookTreeViewProvider(appContext.apiWrapper, workspaceFolders, extensionContext, false, BOOKS_VIEWID);
await bookTreeViewProvider.initialized; await bookTreeViewProvider.initialized;
const untitledBookTreeViewProvider = new BookTreeViewProvider(appContext.apiWrapper, [], extensionContext, true, READONLY_BOOKS_VIEWID); const providedBookTreeViewProvider = new BookTreeViewProvider(appContext.apiWrapper, [], extensionContext, true, PROVIDED_BOOKS_VIEWID);
await untitledBookTreeViewProvider.initialized; await providedBookTreeViewProvider.initialized;
extensionContext.subscriptions.push(vscode.window.registerTreeDataProvider(BOOKS_VIEWID, bookTreeViewProvider));
extensionContext.subscriptions.push(vscode.window.registerTreeDataProvider(PROVIDED_BOOKS_VIEWID, providedBookTreeViewProvider));
return { return {
getJupyterController() { getJupyterController() {
return controller; return controller;

View File

@@ -282,6 +282,10 @@ export class JupyterSession implements nb.ISession {
// %_do_not_call_change_endpoint is a SparkMagic command that lets users change endpoint options, // %_do_not_call_change_endpoint is a SparkMagic command that lets users change endpoint options,
// such as user/profile/host name/auth type // such as user/profile/host name/auth type
let credentials;
if (!this.isIntegratedAuth(connectionProfile)) {
credentials = await connection.getCredentials(connectionProfile.id);
}
//Update server info with bigdata endpoint - Unified Connection //Update server info with bigdata endpoint - Unified Connection
if (connectionProfile.providerName === SQL_PROVIDER) { if (connectionProfile.providerName === SQL_PROVIDER) {
const endpoints = await this.getClusterEndpoints(connectionProfile.id); const endpoints = await this.getClusterEndpoints(connectionProfile.id);
@@ -296,16 +300,16 @@ export class JupyterSession implements nb.ISession {
// as a default now we'll still fall back to root if it's empty for some reason. (but the calls below should // as a default now we'll still fall back to root if it's empty for some reason. (but the calls below should
// get the actual correct value regardless) // get the actual correct value regardless)
connectionProfile.options[USER] = connectionProfile.userName || 'root'; connectionProfile.options[USER] = connectionProfile.userName || 'root';
if (!this.isIntegratedAuth(connectionProfile)) {
try { try {
const bdcApi = <bdc.IExtension>await vscode.extensions.getExtension(bdc.constants.extensionName).activate(); const bdcApi = <bdc.IExtension>await vscode.extensions.getExtension(bdc.constants.extensionName).activate();
const controllerEndpoint = endpoints.find(ep => ep.serviceName.toLowerCase() === CONTROLLER_ENDPOINT); const controllerEndpoint = endpoints.find(ep => ep.serviceName.toLowerCase() === CONTROLLER_ENDPOINT);
const controller = bdcApi.getClusterController(controllerEndpoint.endpoint, 'basic', connectionProfile.userName, connectionProfile.password); const controller = bdcApi.getClusterController(controllerEndpoint.endpoint, 'basic', connectionProfile.userName, credentials.password);
connectionProfile.options[USER] = await controller.getKnoxUsername(connectionProfile.userName); connectionProfile.options[USER] = await controller.getKnoxUsername(connectionProfile.userName);
} catch (err) { } catch (err) {
console.log(`Unexpected error getting Knox username for Spark kernel: ${err}`); console.log(`Unexpected error getting Knox username for Spark kernel: ${err}`);
}
} }
} }
else { else {
connectionProfile.options[KNOX_ENDPOINT_PORT] = this.getKnoxPortOrDefault(connectionProfile); connectionProfile.options[KNOX_ENDPOINT_PORT] = this.getKnoxPortOrDefault(connectionProfile);
@@ -318,7 +322,7 @@ export class JupyterSession implements nb.ISession {
if (this.isIntegratedAuth(connectionProfile)) { if (this.isIntegratedAuth(connectionProfile)) {
doNotCallChangeEndpointParams = `%_do_not_call_change_endpoint --server=${server} --auth=Kerberos`; doNotCallChangeEndpointParams = `%_do_not_call_change_endpoint --server=${server} --auth=Kerberos`;
} else { } else {
const credentials = await connection.getCredentials(connectionProfile.id);
doNotCallChangeEndpointParams = `%_do_not_call_change_endpoint --username=${connectionProfile.options[USER]} --password=${credentials.password} --server=${server} --auth=Basic_Access`; doNotCallChangeEndpointParams = `%_do_not_call_change_endpoint --username=${connectionProfile.options[USER]} --password=${credentials.password} --server=${server} --auth=Basic_Access`;
} }
let future = this.sessionImpl.kernel.requestExecute({ let future = this.sessionImpl.kernel.requestExecute({

View File

@@ -1661,9 +1661,10 @@ vscode-nls@^4.0.0:
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002"
integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw== integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw==
"vscodetestcover@github:corivera/vscodetestcover#1.0.6": vscodetestcover@^1.0.9:
version "1.0.5" version "1.0.9"
resolved "https://codeload.github.com/corivera/vscodetestcover/tar.gz/14e0f2c46346b31bc1af2c590febeaf69a9112eb" resolved "https://registry.yarnpkg.com/vscodetestcover/-/vscodetestcover-1.0.9.tgz#0191f403dd59ba1153fc57979e281e992ce63731"
integrity sha512-8z2961KF9Tuz5XdHAC6RMV3CrzAoUcfIK7wLYjLIXD4dbHIT7ceZMhoxToW1olyi3pFnThlS4lRXtx8Q5iyMMQ==
dependencies: dependencies:
decache "^4.4.0" decache "^4.4.0"
glob "^7.1.2" glob "^7.1.2"

View File

@@ -6,10 +6,10 @@
"**/node_modules/**", "**/node_modules/**",
"**/test/**" "**/test/**"
], ],
"includePid": false,
"reports": [ "reports": [
"cobertura", "cobertura",
"lcov" "lcov",
"json"
], ],
"verbose": false, "verbose": false,
"remapOptions": { "remapOptions": {

View File

@@ -1,17 +0,0 @@
{
"enabled": true,
"relativeSourcePath": "..",
"relativeCoverageDir": "../../coverage",
"ignorePatterns": [
"**/node_modules/**"
],
"includePid": false,
"reports": [
"cobertura"
],
"verbose": false,
"remapOptions": {
"basePath": ".",
"useAbsolutePaths": true
}
}

View File

@@ -362,6 +362,6 @@
"mocha-junit-reporter": "^1.17.0", "mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",
"typemoq": "^2.1.0", "typemoq": "^2.1.0",
"vscodetestcover": "github:corivera/vscodetestcover#1.0.6" "vscodetestcover": "^1.0.9"
} }
} }

View File

@@ -42,7 +42,7 @@ export interface DialogDeploymentProvider extends DeploymentProviderBase {
} }
export interface BdcWizardDeploymentProvider extends DeploymentProviderBase { export interface BdcWizardDeploymentProvider extends DeploymentProviderBase {
bdcWizard: WizardInfo; bdcWizard: BdcWizardInfo;
} }
export interface NotebookWizardDeploymentProvider extends DeploymentProviderBase { export interface NotebookWizardDeploymentProvider extends DeploymentProviderBase {
@@ -50,7 +50,7 @@ export interface NotebookWizardDeploymentProvider extends DeploymentProviderBase
} }
export interface NotebookDeploymentProvider extends DeploymentProviderBase { export interface NotebookDeploymentProvider extends DeploymentProviderBase {
notebook: string | NotebookInfo; notebook: string | NotebookPathInfo;
} }
export interface WebPageDeploymentProvider extends DeploymentProviderBase { export interface WebPageDeploymentProvider extends DeploymentProviderBase {
@@ -100,31 +100,32 @@ export interface DeploymentProviderBase {
export type DeploymentProvider = DialogDeploymentProvider | BdcWizardDeploymentProvider | NotebookWizardDeploymentProvider | NotebookDeploymentProvider | WebPageDeploymentProvider | DownloadDeploymentProvider | CommandDeploymentProvider; export type DeploymentProvider = DialogDeploymentProvider | BdcWizardDeploymentProvider | NotebookWizardDeploymentProvider | NotebookDeploymentProvider | WebPageDeploymentProvider | DownloadDeploymentProvider | CommandDeploymentProvider;
export interface WizardInfo { export interface BdcWizardInfo {
notebook: string | NotebookInfo; notebook: string | NotebookPathInfo;
type: BdcDeploymentType; type: BdcDeploymentType;
} }
export interface NotebookWizardInfo extends WizardInfoBase { export interface NotebookWizardInfo extends WizardInfoBase {
notebook: string | NotebookInfo; notebook: string | NotebookPathInfo;
runNotebook?: boolean;
codeCellInsertionPosition?: number;
pages: NotebookWizardPageInfo[]
} }
export interface WizardInfoBase extends SharedFieldAttributes { export interface WizardInfoBase extends FieldInfoBase {
taskName?: string; taskName?: string;
type?: DeploymentType; type?: DeploymentType;
runNotebook?: boolean;
actionText?: string; actionText?: string;
title: string; title: string;
pages: NotebookWizardPageInfo[]; pages: PageInfoBase[];
summaryPage: NotebookWizardPageInfo; isSummaryPageAutoGenerated?: boolean
generateSummaryPage: boolean;
} }
export interface NotebookWizardPageInfo extends PageInfoBase { export interface NotebookWizardPageInfo extends PageInfoBase {
description?: string; description?: string;
} }
export interface NotebookBasedDialogInfo extends DialogInfoBase { export interface NotebookBasedDialogInfo extends DialogInfoBase {
notebook: string | NotebookInfo; notebook: string | NotebookPathInfo;
runNotebook?: boolean; runNotebook?: boolean;
taskName?: string; taskName?: string;
} }
@@ -153,18 +154,39 @@ export interface DialogInfoBase {
export interface DialogTabInfo extends PageInfoBase { export interface DialogTabInfo extends PageInfoBase {
} }
export interface PageInfoBase extends SharedFieldAttributes { export interface PageInfoBase extends FieldInfoBase {
title: string; title: string;
isSummaryPage?: boolean; isSummaryPage?: boolean;
sections: SectionInfo[]; sections: SectionInfo[];
} }
export interface SharedFieldAttributes { export interface TextCSSStyles {
fontStyle?: FontStyle | undefined;
fontWeight?: FontWeight | undefined;
color?: string;
[key: string]: string | undefined;
}
export type ComponentCSSStyles = {
[key: string]: string;
};
export interface OptionsInfo {
values: string[] | azdata.CategoryValue[],
defaultValue: string,
optionsType?: OptionsType
}
export interface FieldInfoBase {
labelWidth?: string; labelWidth?: string;
inputWidth?: string; inputWidth?: string;
labelPosition?: LabelPosition; // Default value is top labelPosition?: LabelPosition; // Default value is top
fieldWidth?: string;
fieldHeight?: string;
fieldAlignItems?: azdata.AlignItemsType;
} }
export interface SectionInfo extends SharedFieldAttributes { export interface SectionInfo extends FieldInfoBase {
title?: string; title?: string;
fields?: FieldInfo[]; // Use this if the dialog is not wide. All fields will be displayed in one column, label will be placed on top of the input component. fields?: FieldInfo[]; // Use this if the dialog is not wide. All fields will be displayed in one column, label will be placed on top of the input component.
rows?: RowInfo[]; // Use this for wide dialog or wizard. label will be placed to the left of the input component. rows?: RowInfo[]; // Use this for wide dialog or wizard. label will be placed to the left of the input component.
@@ -174,7 +196,8 @@ export interface SectionInfo extends SharedFieldAttributes {
} }
export interface RowInfo { export interface RowInfo {
fields: FieldInfo[]; cssStyles?: ComponentCSSStyles;
items: FieldInfo[] | RowInfo[];
} }
export interface SubFieldInfo { export interface SubFieldInfo {
@@ -182,7 +205,7 @@ export interface SubFieldInfo {
variableName?: string; variableName?: string;
} }
export interface FieldInfo extends SubFieldInfo, SharedFieldAttributes { export interface FieldInfo extends SubFieldInfo, FieldInfoBase {
subFields?: SubFieldInfo[]; subFields?: SubFieldInfo[];
type: FieldType; type: FieldType;
defaultValue?: string; defaultValue?: string;
@@ -194,22 +217,23 @@ export interface FieldInfo extends SubFieldInfo, SharedFieldAttributes {
min?: number; min?: number;
max?: number; max?: number;
required?: boolean; required?: boolean;
options?: string[] | azdata.CategoryValue[]; options?: string[] | azdata.CategoryValue[] | OptionsInfo;
placeHolder?: string; placeHolder?: string;
userName?: string; // needed for sql server's password complexity requirement check, password can not include the login name. userName?: string; // needed for sql server's password complexity requirement check, password can not include the login name.
description?: string; description?: string;
fontStyle?: FontStyle; labelCSSStyles?: TextCSSStyles;
labelFontWeight?: FontWeight; fontWeight?: FontWeight;
textFontWeight?: FontWeight;
links?: azdata.LinkArea[]; links?: azdata.LinkArea[];
editable?: boolean; // for editable drop-down, editable?: boolean; // for editable drop-down,
enabled?: boolean; enabled?: boolean;
isEvaluated?: boolean;
} }
export interface KubeClusterContextFieldInfo extends FieldInfo { export interface KubeClusterContextFieldInfo extends FieldInfo {
configFileVariableName?: string; configFileVariableName?: string;
} }
export interface AzureAccountFieldInfo extends AzureLocationsFieldInfo { export interface AzureAccountFieldInfo extends AzureLocationsFieldInfo {
displaySubscriptionVariableName?: string;
subscriptionVariableName?: string; subscriptionVariableName?: string;
resourceGroupVariableName?: string; resourceGroupVariableName?: string;
} }
@@ -242,16 +266,20 @@ export enum FieldType {
SQLPassword = 'sql_password', SQLPassword = 'sql_password',
Password = 'password', Password = 'password',
Options = 'options', Options = 'options',
RadioOptions = 'radio_options',
ReadonlyText = 'readonly_text', ReadonlyText = 'readonly_text',
Checkbox = 'checkbox', Checkbox = 'checkbox',
AzureAccount = 'azure_account', AzureAccount = 'azure_account',
AzureLocations = 'azure_locations', AzureLocations = 'azure_locations',
FilePicker = 'file_picker', FilePicker = 'file_picker',
KubeClusterContextPicker = 'kube_cluster_context_picker' KubeClusterContextPicker = 'kube_cluster_context_picker',
} }
export interface NotebookInfo { export enum OptionsType {
Dropdown = 'dropdown',
Radio = 'radio'
}
export interface NotebookPathInfo {
win32: string; win32: string;
darwin: string; darwin: string;
linux: string; linux: string;

View File

@@ -9,7 +9,7 @@ import * as path from 'path';
import { isString } from 'util'; import { isString } from 'util';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { NotebookInfo } from '../interfaces'; import { NotebookPathInfo } from '../interfaces';
import { getDateTimeString, getErrorMessage } from '../utils'; import { getDateTimeString, getErrorMessage } from '../utils';
import { IPlatformService } from './platformService'; import { IPlatformService } from './platformService';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
@@ -33,11 +33,13 @@ export interface NotebookExecutionResult {
} }
export interface INotebookService { export interface INotebookService {
launchNotebook(notebook: string | NotebookInfo): Thenable<azdata.nb.NotebookEditor>; launchNotebook(notebook: string | NotebookPathInfo): Promise<azdata.nb.NotebookEditor>;
launchNotebookWithContent(title: string, content: string): Thenable<azdata.nb.NotebookEditor>; launchNotebookWithEdits(notebook: string | NotebookPathInfo, cellStatements: string[], insertionPosition?: number): Promise<void>;
getNotebook(notebook: string | NotebookInfo): Promise<Notebook>; launchNotebookWithContent(title: string, content: string): Promise<azdata.nb.NotebookEditor>;
getNotebook(notebook: string | NotebookPathInfo): Promise<Notebook>;
getNotebookPath(notebook: string | NotebookPathInfo): string;
executeNotebook(notebook: any, env?: NodeJS.ProcessEnv): Promise<NotebookExecutionResult>; executeNotebook(notebook: any, env?: NodeJS.ProcessEnv): Promise<NotebookExecutionResult>;
backgroundExecuteNotebook(taskName: string | undefined, notebookInfo: string | NotebookInfo, tempNoteBookPrefix: string, platformService: IPlatformService): void; backgroundExecuteNotebook(taskName: string | undefined, notebookInfo: string | NotebookPathInfo | Notebook, tempNotebookPrefix: string, platformService: IPlatformService, env?: NodeJS.ProcessEnv): void;
} }
export class NotebookService implements INotebookService { export class NotebookService implements INotebookService {
@@ -48,9 +50,26 @@ export class NotebookService implements INotebookService {
* Launch notebook with file path * Launch notebook with file path
* @param notebook the path of the notebook * @param notebook the path of the notebook
*/ */
launchNotebook(notebook: string | NotebookInfo): Thenable<azdata.nb.NotebookEditor> { async launchNotebook(notebook: string | NotebookPathInfo): Promise<azdata.nb.NotebookEditor> {
return this.getNotebookFullPath(notebook).then(notebookPath => { const notebookPath = await this.getNotebookFullPath(notebook);
return this.showNotebookAsUntitled(notebookPath); return await this.showNotebookAsUntitled(notebookPath);
}
/**
* Inserts cell code given by {@param cellStatements} in an existing notebook given by {@param notebook} file path at the location
* {@param insertionPosition} and then launches the edited notebook.
*
* @param notebook - the path to notebook that needs to be launched
* @param cellStatements - array of statements to be inserted in a cell
* @param insertionPosition - the position at which cells are inserted. Default is a new cell at the beginning of the notebook.
*/
async launchNotebookWithEdits(notebook: string, cellStatements: string[], insertionPosition: number = 0): Promise<void> {
const openedNotebook = await this.launchNotebook(notebook);
await openedNotebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => {
editBuilder.insertCell({
cell_type: 'code',
source: cellStatements
}, insertionPosition);
}); });
} }
@@ -59,9 +78,9 @@ export class NotebookService implements INotebookService {
* @param title the title of the notebook * @param title the title of the notebook
* @param content the notebook content * @param content the notebook content
*/ */
launchNotebookWithContent(title: string, content: string): Thenable<azdata.nb.NotebookEditor> { async launchNotebookWithContent(title: string, content: string): Promise<azdata.nb.NotebookEditor> {
const uri: vscode.Uri = vscode.Uri.parse(`untitled:${title}`); const uri: vscode.Uri = vscode.Uri.parse(`untitled:${this.findNextUntitledEditorName(title)}`);
return azdata.nb.showNotebookDocument(uri, { return await azdata.nb.showNotebookDocument(uri, {
connectionProfile: undefined, connectionProfile: undefined,
preview: false, preview: false,
initialContent: content, initialContent: content,
@@ -70,7 +89,7 @@ export class NotebookService implements INotebookService {
} }
async getNotebook(notebook: string | NotebookInfo): Promise<Notebook> { async getNotebook(notebook: string | NotebookPathInfo): Promise<Notebook> {
const notebookPath = await this.getNotebookFullPath(notebook); const notebookPath = await this.getNotebookFullPath(notebook);
return <Notebook>JSON.parse(await this.platformService.readTextFile(notebookPath)); return <Notebook>JSON.parse(await this.platformService.readTextFile(notebookPath));
} }
@@ -83,7 +102,7 @@ export class NotebookService implements INotebookService {
const outputFullPath = path.join(workingDirectory, `output-${fileName}`); const outputFullPath = path.join(workingDirectory, `output-${fileName}`);
const additionalEnvironmentVariables: NodeJS.ProcessEnv = env || {}; const additionalEnvironmentVariables: NodeJS.ProcessEnv = env || {};
// Set the azdata eula // Set the azdata eula
// Scenarios using the executeNotebook feature already have the EULA acceptted by the user before executing this. // Scenarios using the executeNotebook feature already have the EULA accepted by the user before executing this.
additionalEnvironmentVariables['ACCEPT_EULA'] = 'yes'; additionalEnvironmentVariables['ACCEPT_EULA'] = 'yes';
try { try {
await this.platformService.saveTextFile(content, notebookFullPath); await this.platformService.saveTextFile(content, notebookFullPath);
@@ -109,15 +128,17 @@ export class NotebookService implements INotebookService {
} }
} }
public backgroundExecuteNotebook(taskName: string | undefined, notebookInfo: string | NotebookInfo, tempNotebookPrefix: string, platformService: IPlatformService): void { backgroundExecuteNotebook(taskName: string = 'Executing notebook', notebookInfo: string | NotebookPathInfo | Notebook, tempNotebookPrefix: string, platformService: IPlatformService, env?: NodeJS.ProcessEnv): void {
azdata.tasks.startBackgroundOperation({ azdata.tasks.startBackgroundOperation({
displayName: taskName!, displayName: taskName,
description: taskName!, description: taskName,
isCancelable: false, isCancelable: false,
operation: async op => { operation: async op => {
op.updateStatus(azdata.TaskStatus.InProgress); op.updateStatus(azdata.TaskStatus.InProgress);
const notebook = await this.getNotebook(notebookInfo); const notebook = (typeof notebookInfo === 'object' && 'cells' in notebookInfo)
const result = await this.executeNotebook(notebook); ? <Notebook>notebookInfo
: await this.getNotebook(notebookInfo);
const result = await this.executeNotebook(notebook, env);
if (result.succeeded) { if (result.succeeded) {
op.updateStatus(azdata.TaskStatus.Succeeded); op.updateStatus(azdata.TaskStatus.Succeeded);
} else { } else {
@@ -129,7 +150,7 @@ export class NotebookService implements INotebookService {
platformService.logToOutputChannel(taskFailedMessage); platformService.logToOutputChannel(taskFailedMessage);
if (selectedOption === viewErrorDetail) { if (selectedOption === viewErrorDetail) {
try { try {
this.launchNotebookWithContent(`${tempNotebookPrefix}-${getDateTimeString()}`, result.outputNotebook); await this.launchNotebookWithContent(`${tempNotebookPrefix}-${getDateTimeString()}`, result.outputNotebook);
} catch (error) { } catch (error) {
const launchNotebookError = localize('resourceDeployment.FailedToOpenNotebook', "An error occurred launching the output notebook. {1}{2}.", EOL, getErrorMessage(error)); const launchNotebookError = localize('resourceDeployment.FailedToOpenNotebook', "An error occurred launching the output notebook. {1}{2}.", EOL, getErrorMessage(error));
platformService.logToOutputChannel(launchNotebookError); platformService.logToOutputChannel(launchNotebookError);
@@ -146,7 +167,7 @@ export class NotebookService implements INotebookService {
}); });
} }
async getNotebookFullPath(notebook: string | NotebookInfo): Promise<string> { async getNotebookFullPath(notebook: string | NotebookPathInfo): Promise<string> {
const notebookPath = this.getNotebookPath(notebook); const notebookPath = this.getNotebookPath(notebook);
let notebookExists = await this.platformService.fileExists(notebookPath); let notebookExists = await this.platformService.fileExists(notebookPath);
if (notebookExists) { if (notebookExists) {
@@ -168,7 +189,7 @@ export class NotebookService implements INotebookService {
* get the notebook path for current platform * get the notebook path for current platform
* @param notebook the notebook path * @param notebook the notebook path
*/ */
getNotebookPath(notebook: string | NotebookInfo): string { getNotebookPath(notebook: string | NotebookPathInfo): string {
let notebookPath; let notebookPath;
if (notebook && !isString(notebook)) { if (notebook && !isString(notebook)) {
const platform = this.platformService.platform(); const platform = this.platformService.platform();
@@ -199,17 +220,16 @@ export class NotebookService implements INotebookService {
return title; return title;
} }
showNotebookAsUntitled(notebookPath: string): Thenable<azdata.nb.NotebookEditor> { async showNotebookAsUntitled(notebookPath: string): Promise<azdata.nb.NotebookEditor> {
let targetFileName: string = this.findNextUntitledEditorName(notebookPath); let targetFileName: string = this.findNextUntitledEditorName(notebookPath);
const untitledFileName: vscode.Uri = vscode.Uri.parse(`untitled:${targetFileName}`); const untitledFileName: vscode.Uri = vscode.Uri.parse(`untitled:${targetFileName}`);
return vscode.workspace.openTextDocument(notebookPath).then((document) => { const document = await vscode.workspace.openTextDocument(notebookPath);
let initialContent = document.getText(); let initialContent = document.getText();
return azdata.nb.showNotebookDocument(untitledFileName, { return await azdata.nb.showNotebookDocument(untitledFileName, {
connectionProfile: undefined, connectionProfile: undefined,
preview: false, preview: false,
initialContent: initialContent, initialContent: initialContent,
initialDirtyState: false initialDirtyState: false
});
}); });
} }
} }

View File

@@ -124,7 +124,8 @@ export class PlatformService implements IPlatformService {
} }
isNotebookNameUsed(title: string): boolean { isNotebookNameUsed(title: string): boolean {
return (azdata.nb.notebookDocuments.findIndex(doc => doc.isUntitled && doc.fileName === title) > -1); return (azdata.nb.notebookDocuments.findIndex(doc => doc.isUntitled && doc.fileName === title) > -1)
&& (vscode.workspace.textDocuments.findIndex(doc => doc.isUntitled && doc.fileName === title) > -1);
} }
async makeDirectory(path: string): Promise<void> { async makeDirectory(path: string): Promise<void> {

View File

@@ -13,7 +13,7 @@ import * as nls from 'vscode-nls';
import { INotebookService } from './notebookService'; import { INotebookService } from './notebookService';
import { IPlatformService } from './platformService'; import { IPlatformService } from './platformService';
import { IToolsService } from './toolsService'; import { IToolsService } from './toolsService';
import { ResourceType, ResourceTypeOption, NotebookInfo, DeploymentProvider, instanceOfWizardDeploymentProvider, instanceOfDialogDeploymentProvider, instanceOfNotebookDeploymentProvider, instanceOfDownloadDeploymentProvider, instanceOfWebPageDeploymentProvider, instanceOfCommandDeploymentProvider, instanceOfNotebookBasedDialogInfo, instanceOfNotebookWizardDeploymentProvider } from '../interfaces'; import { ResourceType, ResourceTypeOption, NotebookPathInfo, DeploymentProvider, instanceOfWizardDeploymentProvider, instanceOfDialogDeploymentProvider, instanceOfNotebookDeploymentProvider, instanceOfDownloadDeploymentProvider, instanceOfWebPageDeploymentProvider, instanceOfCommandDeploymentProvider, instanceOfNotebookBasedDialogInfo, instanceOfNotebookWizardDeploymentProvider } from '../interfaces';
import { DeployClusterWizard } from '../ui/deployClusterWizard/deployClusterWizard'; import { DeployClusterWizard } from '../ui/deployClusterWizard/deployClusterWizard';
import { DeploymentInputDialog } from '../ui/deploymentInputDialog'; import { DeploymentInputDialog } from '../ui/deploymentInputDialog';
@@ -77,7 +77,7 @@ export class ResourceTypeService implements IResourceTypeService {
}); });
} }
private updateNotebookPath(objWithNotebookProperty: { notebook: string | NotebookInfo } | undefined, extensionPath: string): void { private updateNotebookPath(objWithNotebookProperty: { notebook: string | NotebookPathInfo } | undefined, extensionPath: string): void {
if (objWithNotebookProperty && objWithNotebookProperty.notebook) { if (objWithNotebookProperty && objWithNotebookProperty.notebook) {
if (typeof objWithNotebookProperty.notebook === 'string') { if (typeof objWithNotebookProperty.notebook === 'string') {
objWithNotebookProperty.notebook = path.join(extensionPath, objWithNotebookProperty.notebook); objWithNotebookProperty.notebook = path.join(extensionPath, objWithNotebookProperty.notebook);
@@ -239,10 +239,10 @@ export class ResourceTypeService implements IResourceTypeService {
public startDeployment(provider: DeploymentProvider): void { public startDeployment(provider: DeploymentProvider): void {
const self = this; const self = this;
if (instanceOfWizardDeploymentProvider(provider)) { if (instanceOfWizardDeploymentProvider(provider)) {
const wizard = new DeployClusterWizard(provider.bdcWizard, new KubeService(), new AzdataService(this.platformService), this.notebookService); const wizard = new DeployClusterWizard(provider.bdcWizard, new KubeService(), new AzdataService(this.platformService), this.notebookService, this.toolsService);
wizard.open(); wizard.open();
} else if (instanceOfNotebookWizardDeploymentProvider(provider)) { } else if (instanceOfNotebookWizardDeploymentProvider(provider)) {
const wizard = new NotebookWizard(provider.notebookWizard, this.notebookService, this.platformService); const wizard = new NotebookWizard(provider.notebookWizard, this.notebookService, this.platformService, this.toolsService);
wizard.open(); wizard.open();
} else if (instanceOfDialogDeploymentProvider(provider)) { } else if (instanceOfDialogDeploymentProvider(provider)) {
const dialog = new DeploymentInputDialog(this.notebookService, this.platformService, provider.dialog); const dialog = new DeploymentInputDialog(this.notebookService, this.platformService, provider.dialog);

View File

@@ -11,10 +11,12 @@ import { IPlatformService } from './platformService';
export interface IToolsService { export interface IToolsService {
getToolByName(toolName: string): ITool | undefined; getToolByName(toolName: string): ITool | undefined;
toolsForCurrentProvider: ITool[];
} }
export class ToolsService implements IToolsService { export class ToolsService implements IToolsService {
private supportedTools: Map<string, ITool>; private supportedTools: Map<string, ITool>;
private currentTools: ITool[] = [];
constructor(private _platformService: IPlatformService) { constructor(private _platformService: IPlatformService) {
this.supportedTools = new Map<string, ITool>( this.supportedTools = new Map<string, ITool>(
@@ -30,4 +32,12 @@ export class ToolsService implements IToolsService {
getToolByName(toolName: string): ITool | undefined { getToolByName(toolName: string): ITool | undefined {
return this.supportedTools.get(toolName); return this.supportedTools.get(toolName);
} }
get toolsForCurrentProvider(): ITool[] {
return this.currentTools;
}
set toolsForCurrentProvider(tools: ITool[]) {
this.currentTools = tools;
}
} }

View File

@@ -7,7 +7,7 @@ import * as TypeMoq from 'typemoq';
import 'mocha'; import 'mocha';
import { NotebookService } from '../services/notebookService'; import { NotebookService } from '../services/notebookService';
import assert = require('assert'); import assert = require('assert');
import { NotebookInfo } from '../interfaces'; import { NotebookPathInfo } from '../interfaces';
import { IPlatformService } from '../services/platformService'; import { IPlatformService } from '../services/platformService';
suite('Notebook Service Tests', function (): void { suite('Notebook Service Tests', function (): void {
@@ -35,7 +35,7 @@ suite('Notebook Service Tests', function (): void {
const notebookDarwin = 'test-notebook-darwin.ipynb'; const notebookDarwin = 'test-notebook-darwin.ipynb';
const notebookLinux = 'test-notebook-linux.ipynb'; const notebookLinux = 'test-notebook-linux.ipynb';
const notebookInput: NotebookInfo = { const notebookInput: NotebookPathInfo = {
darwin: notebookDarwin, darwin: notebookDarwin,
win32: notebookWin32, win32: notebookWin32,
linux: notebookLinux linux: notebookLinux

View File

@@ -4,28 +4,33 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as fs from 'fs';
import { SummaryPage } from './pages/summaryPage';
import { WizardBase } from '../wizardBase';
import * as nls from 'vscode-nls';
import { WizardInfo, BdcDeploymentType } from '../../interfaces';
import { WizardPageBase } from '../wizardPageBase';
import { AzureSettingsPage } from './pages/azureSettingsPage';
import { ClusterSettingsPage } from './pages/clusterSettingsPage';
import { ServiceSettingsPage } from './pages/serviceSettingsPage';
import { TargetClusterContextPage } from './pages/targetClusterPage';
import { IKubeService } from '../../services/kubeService';
import { IAzdataService } from '../../services/azdataService';
import { DeploymentProfilePage } from './pages/deploymentProfilePage';
import { INotebookService } from '../../services/notebookService';
import { DeployClusterWizardModel, AuthenticationMode } from './deployClusterWizardModel';
import * as VariableNames from './constants';
import * as os from 'os'; import * as os from 'os';
import { join } from 'path'; import { join } from 'path';
import * as fs from 'fs'; import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { BdcDeploymentType, BdcWizardInfo } from '../../interfaces';
import { IAzdataService } from '../../services/azdataService';
import { IKubeService } from '../../services/kubeService';
import { INotebookService } from '../../services/notebookService';
import { IToolsService } from '../../services/toolsService';
import { getErrorMessage } from '../../utils';
import { InputComponents } from '../modelViewUtils';
import { WizardBase } from '../wizardBase';
import { WizardPageBase } from '../wizardPageBase';
import * as VariableNames from './constants';
import { AuthenticationMode, DeployClusterWizardModel } from './deployClusterWizardModel';
import { AzureSettingsPage } from './pages/azureSettingsPage';
import { ClusterSettingsPage } from './pages/clusterSettingsPage';
import { DeploymentProfilePage } from './pages/deploymentProfilePage';
import { ServiceSettingsPage } from './pages/serviceSettingsPage';
import { SummaryPage } from './pages/summaryPage';
import { TargetClusterContextPage } from './pages/targetClusterPage';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
export class DeployClusterWizard extends WizardBase<DeployClusterWizard, DeployClusterWizardModel> { export class DeployClusterWizard extends WizardBase<DeployClusterWizard, WizardPageBase<DeployClusterWizard>, DeployClusterWizardModel> {
private _inputComponents: InputComponents = {};
private _saveConfigButton: azdata.window.Button; private _saveConfigButton: azdata.window.Button;
public get kubeService(): IKubeService { public get kubeService(): IKubeService {
@@ -40,6 +45,10 @@ export class DeployClusterWizard extends WizardBase<DeployClusterWizard, DeployC
return this._notebookService; return this._notebookService;
} }
public get inputComponents(): InputComponents {
return this._inputComponents;
}
public showCustomButtons(): void { public showCustomButtons(): void {
this._saveConfigButton.hidden = false; this._saveConfigButton.hidden = false;
} }
@@ -48,7 +57,7 @@ export class DeployClusterWizard extends WizardBase<DeployClusterWizard, DeployC
this._saveConfigButton.hidden = true; this._saveConfigButton.hidden = true;
} }
constructor(private wizardInfo: WizardInfo, private _kubeService: IKubeService, private _azdataService: IAzdataService, private _notebookService: INotebookService) { constructor(private wizardInfo: BdcWizardInfo, private _kubeService: IKubeService, private _azdataService: IAzdataService, private _notebookService: INotebookService, private _toolsService: IToolsService) {
super(DeployClusterWizard.getTitle(wizardInfo.type), new DeployClusterWizardModel(wizardInfo.type)); super(DeployClusterWizard.getTitle(wizardInfo.type), new DeployClusterWizardModel(wizardInfo.type));
this._saveConfigButton = azdata.window.createButton(localize('deployCluster.SaveConfigFiles', "Save config files"), 'left'); this._saveConfigButton = azdata.window.createButton(localize('deployCluster.SaveConfigFiles', "Save config files"), 'left');
this._saveConfigButton.hidden = true; this._saveConfigButton.hidden = true;
@@ -69,8 +78,8 @@ export class DeployClusterWizard extends WizardBase<DeployClusterWizard, DeployC
protected onCancel(): void { protected onCancel(): void {
} }
protected onOk(): void { protected async onOk(): Promise<void> {
this.scriptToNotebook(); await this.scriptToNotebook();
} }
private getPages(): WizardPageBase<DeployClusterWizard>[] { private getPages(): WizardPageBase<DeployClusterWizard>[] {
@@ -135,19 +144,15 @@ export class DeployClusterWizard extends WizardBase<DeployClusterWizard, DeployC
} }
} }
private scriptToNotebook(): void { private async scriptToNotebook(): Promise<void> {
this.setEnvironmentVariables(process.env); this.setEnvironmentVariables(process.env);
this.notebookService.launchNotebook(this.wizardInfo.notebook).then((notebook: azdata.nb.NotebookEditor) => { const variableValueStatements = this.model.getCodeCellContentForNotebook(this._toolsService.toolsForCurrentProvider);
notebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => { const insertionPosition = 5; // Cell number 5 is the position where the python variable setting statements need to be inserted in this.wizardInfo.notebook.
// 5 is the position after the 'Set variables' cell in the deployment notebooks try {
editBuilder.insertCell({ await this.notebookService.launchNotebookWithEdits(this.wizardInfo.notebook, variableValueStatements, insertionPosition);
cell_type: 'code', } catch (error) {
source: this.model.getCodeCellContentForNotebook() vscode.window.showErrorMessage(getErrorMessage(error));
}, 5); }
});
}, (error) => {
vscode.window.showErrorMessage(error);
});
} }
private setEnvironmentVariables(env: NodeJS.ProcessEnv): void { private setEnvironmentVariables(env: NodeJS.ProcessEnv): void {

View File

@@ -4,13 +4,13 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { EOL } from 'os'; import { EOL } from 'os';
import { delimiter } from 'path'; import { delimiter } from 'path';
import { BdcDeploymentType } from '../../interfaces'; import { BdcDeploymentType, ITool } from '../../interfaces';
import { BigDataClusterDeploymentProfile, DataResource, HdfsResource, SqlServerMasterResource } from '../../services/bigDataClusterDeploymentProfile'; import { BigDataClusterDeploymentProfile, DataResource, HdfsResource, SqlServerMasterResource } from '../../services/bigDataClusterDeploymentProfile';
import { KubeCtlToolName } from '../../services/tools/kubeCtlTool'; import { KubeCtlToolName } from '../../services/tools/kubeCtlTool';
import { getRuntimeBinaryPathEnvironmentVariableName } from '../../utils'; import { getRuntimeBinaryPathEnvironmentVariableName, setEnvironmentVariablesForInstallPaths } from '../../utils';
import { Model } from '../model'; import { Model } from '../model';
import * as VariableNames from './constants';
import { ToolsInstallPath } from './../../constants'; import { ToolsInstallPath } from './../../constants';
import * as VariableNames from './constants';
export class DeployClusterWizardModel extends Model { export class DeployClusterWizardModel extends Model {
constructor(public deploymentTarget: BdcDeploymentType) { constructor(public deploymentTarget: BdcDeploymentType) {
@@ -138,7 +138,7 @@ export class DeployClusterWizardModel extends Model {
return targetDeploymentProfile; return targetDeploymentProfile;
} }
public getCodeCellContentForNotebook(): string[] { public getCodeCellContentForNotebook(tools: ITool[]): string[] {
const profile = this.createTargetProfile(); const profile = this.createTargetProfile();
const statements: string[] = []; const statements: string[] = [];
if (this.deploymentTarget === BdcDeploymentType.NewAKS) { if (this.deploymentTarget === BdcDeploymentType.NewAKS) {
@@ -166,16 +166,13 @@ export class DeployClusterWizardModel extends Model {
statements.push(`os.environ["DOCKER_PASSWORD"] = os.environ["${VariableNames.DockerPassword_VariableName}"]`); statements.push(`os.environ["DOCKER_PASSWORD"] = os.environ["${VariableNames.DockerPassword_VariableName}"]`);
} }
const kubeCtlEnvVarName: string = getRuntimeBinaryPathEnvironmentVariableName(KubeCtlToolName); const kubeCtlEnvVarName: string = getRuntimeBinaryPathEnvironmentVariableName(KubeCtlToolName);
statements.push(`os.environ["${kubeCtlEnvVarName}"] = "${this.escapeForNotebookCodeCell(process.env[kubeCtlEnvVarName]!)}"`); const env: NodeJS.ProcessEnv = {};
statements.push(`os.environ["PATH"] = os.environ["PATH"] + "${delimiter}" + "${this.escapeForNotebookCodeCell(process.env[ToolsInstallPath]!)}"`); setEnvironmentVariablesForInstallPaths(tools, env);
statements.push(`os.environ["${kubeCtlEnvVarName}"] = "${this.escapeForNotebookCodeCell(env[kubeCtlEnvVarName]!)}"`);
statements.push(`os.environ["PATH"] = os.environ["PATH"] + "${delimiter}" + "${this.escapeForNotebookCodeCell(env[ToolsInstallPath]!)}"`);
statements.push(`print('Variables have been set successfully.')`); statements.push(`print('Variables have been set successfully.')`);
return statements.map(line => line + EOL); return statements.map(line => line + EOL);
} }
private escapeForNotebookCodeCell(original: string): string {
// Escape the \ character for the code cell string value
return original && original.replace(/\\/g, '\\\\');
}
} }
export enum AuthenticationMode { export enum AuthenticationMode {

View File

@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { FieldType, LabelPosition, SectionInfo } from '../../../interfaces'; import { FieldType, LabelPosition, SectionInfo } from '../../../interfaces';
import { createSection, getDropdownComponent, InputComponents, InputComponent, setModelValues, Validator } from '../../modelViewUtils'; import { createSection, getDropdownComponent, InputComponentInfo, InputComponents, setModelValues, Validator } from '../../modelViewUtils';
import { WizardPageBase } from '../../wizardPageBase'; import { WizardPageBase } from '../../wizardPageBase';
import { AksName_VariableName, Location_VariableName, ResourceGroup_VariableName, SubscriptionId_VariableName, VMCount_VariableName, VMSize_VariableName } from '../constants'; import { AksName_VariableName, Location_VariableName, ResourceGroup_VariableName, SubscriptionId_VariableName, VMCount_VariableName, VMSize_VariableName } from '../constants';
import { DeployClusterWizard } from '../deployClusterWizard'; import { DeployClusterWizard } from '../deployClusterWizard';
@@ -29,7 +29,7 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
labelPosition: LabelPosition.Left, labelPosition: LabelPosition.Left,
spaceBetweenFields: '5px', spaceBetweenFields: '5px',
rows: [{ rows: [{
fields: [{ items: [{
type: FieldType.Text, type: FieldType.Text,
label: localize('deployCluster.SubscriptionField', "Subscription id"), label: localize('deployCluster.SubscriptionField', "Subscription id"),
required: false, required: false,
@@ -38,9 +38,7 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
description: localize('deployCluster.SubscriptionDescription', "The default subscription will be used if you leave this field blank.") description: localize('deployCluster.SubscriptionDescription', "The default subscription will be used if you leave this field blank.")
}, { }, {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: '', label: '{0}',
labelWidth: '0px',
defaultValue: localize('deployCluster.SubscriptionHelpText', "{0}"),
links: [ links: [
{ {
text: localize('deployCluster.SubscriptionHelpLink', "View available Azure subscriptions"), text: localize('deployCluster.SubscriptionHelpLink', "View available Azure subscriptions"),
@@ -49,7 +47,7 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
] ]
}] }]
}, { }, {
fields: [{ items: [{
type: FieldType.DateTimeText, type: FieldType.DateTimeText,
label: localize('deployCluster.ResourceGroupName', "New resource group name"), label: localize('deployCluster.ResourceGroupName', "New resource group name"),
required: true, required: true,
@@ -57,7 +55,7 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
defaultValue: 'mssql-' defaultValue: 'mssql-'
}] }]
}, { }, {
fields: [{ items: [{
type: FieldType.Options, type: FieldType.Options,
label: localize('deployCluster.Location', "Location"), label: localize('deployCluster.Location', "Location"),
required: true, required: true,
@@ -79,9 +77,7 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
] ]
}, { }, {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: '', label: '{0}',
labelWidth: '0px',
defaultValue: localize('deployCluster.LocationHelpText', "{0}"),
links: [ links: [
{ {
text: localize('deployCluster.AzureLocationHelpLink', "View available Azure locations"), text: localize('deployCluster.AzureLocationHelpLink', "View available Azure locations"),
@@ -90,7 +86,7 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
] ]
}] }]
}, { }, {
fields: [{ items: [{
type: FieldType.DateTimeText, type: FieldType.DateTimeText,
label: localize('deployCluster.AksName', "AKS cluster name"), label: localize('deployCluster.AksName', "AKS cluster name"),
required: true, required: true,
@@ -98,7 +94,7 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
defaultValue: 'mssql-', defaultValue: 'mssql-',
}] }]
}, { }, {
fields: [ items: [
{ {
type: FieldType.Number, type: FieldType.Number,
label: localize('deployCluster.VMCount', "VM count"), label: localize('deployCluster.VMCount', "VM count"),
@@ -110,7 +106,7 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
} }
] ]
}, { }, {
fields: [{ items: [{
type: FieldType.Text, type: FieldType.Text,
label: localize('deployCluster.VMSize', "VM size"), label: localize('deployCluster.VMSize', "VM size"),
required: true, required: true,
@@ -118,9 +114,7 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
defaultValue: 'Standard_E8s_v3' defaultValue: 'Standard_E8s_v3'
}, { }, {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: '', label: '{0}',
labelWidth: '0px',
defaultValue: localize('deployCluster.VMSizeHelpText', "{0}"),
links: [ links: [
{ {
text: localize('deployCluster.VMSizeHelpLink', "View available VM sizes"), text: localize('deployCluster.VMSizeHelpLink', "View available VM sizes"),
@@ -137,13 +131,14 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
onNewDisposableCreated: (disposable: vscode.Disposable): void => { onNewDisposableCreated: (disposable: vscode.Disposable): void => {
self.wizard.registerDisposable(disposable); self.wizard.registerDisposable(disposable);
}, },
onNewInputComponentCreated: (name: string, component: InputComponent): void => { onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => {
self.inputComponents[name] = { component: component }; this.inputComponents[name] = { component: inputComponentInfo.component };
}, },
onNewValidatorCreated: (validator: Validator): void => { onNewValidatorCreated: (validator: Validator): void => {
self.validators.push(validator); self.validators.push(validator);
}, },
container: this.wizard.wizardObject container: this.wizard.wizardObject,
inputComponents: this.wizard.inputComponents
}); });
const formBuilder = view.modelBuilder.formContainer().withFormItems( const formBuilder = view.modelBuilder.formContainer().withFormItems(
[{ [{
@@ -184,5 +179,6 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
return true; return true;
}); });
setModelValues(this.inputComponents, this.wizard.model); setModelValues(this.inputComponents, this.wizard.model);
Object.assign(this.wizard.inputComponents, this.inputComponents);
} }
} }

View File

@@ -4,14 +4,14 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import { EOL } from 'os';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { DeployClusterWizard } from '../deployClusterWizard'; import { FieldType, LabelPosition, SectionInfo } from '../../../interfaces';
import { SectionInfo, FieldType, LabelPosition } from '../../../interfaces'; import { createSection, getInputBoxComponent, getInvalidSQLPasswordMessage, getPasswordMismatchMessage, InputComponentInfo, InputComponents, isValidSQLPassword, setModelValues, Validator } from '../../modelViewUtils';
import { createSection, InputComponents, setModelValues, Validator, getInputBoxComponent, isValidSQLPassword, getInvalidSQLPasswordMessage, getPasswordMismatchMessage, InputComponent } from '../../modelViewUtils';
import { WizardPageBase } from '../../wizardPageBase'; import { WizardPageBase } from '../../wizardPageBase';
import * as VariableNames from '../constants'; import * as VariableNames from '../constants';
import { EOL } from 'os'; import { DeployClusterWizard } from '../deployClusterWizard';
import { AuthenticationMode } from '../deployClusterWizardModel'; import { AuthenticationMode } from '../deployClusterWizardModel';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
@@ -174,7 +174,7 @@ export class ClusterSettingsPage extends WizardPageBase<DeployClusterWizard> {
variableName: VariableNames.DomainServiceAccountPassword_VariableName variableName: VariableNames.DomainServiceAccountPassword_VariableName
}, { }, {
type: FieldType.Text, type: FieldType.Text,
label: localize('deployCluster.AppOwers', "App owners"), label: localize('deployCluster.AppOwners', "App owners"),
required: false, required: false,
variableName: VariableNames.AppOwners_VariableName, variableName: VariableNames.AppOwners_VariableName,
placeHolder: localize('deployCluster.AppOwnersPlaceHolder', "Use comma to separate the values."), placeHolder: localize('deployCluster.AppOwnersPlaceHolder', "Use comma to separate the values."),
@@ -193,12 +193,13 @@ export class ClusterSettingsPage extends WizardPageBase<DeployClusterWizard> {
const basicSettingsGroup = createSection({ const basicSettingsGroup = createSection({
view: view, view: view,
container: self.wizard.wizardObject, container: self.wizard.wizardObject,
inputComponents: this.wizard.inputComponents,
sectionInfo: basicSection, sectionInfo: basicSection,
onNewDisposableCreated: (disposable: vscode.Disposable): void => { onNewDisposableCreated: (disposable: vscode.Disposable): void => {
self.wizard.registerDisposable(disposable); self.wizard.registerDisposable(disposable);
}, },
onNewInputComponentCreated: (name: string, component: InputComponent): void => { onNewInputComponentCreated: (name: string, inputComponent: InputComponentInfo): void => {
self.inputComponents[name] = { component: component }; self.inputComponents[name] = { component: inputComponent.component };
}, },
onNewValidatorCreated: (validator: Validator): void => { onNewValidatorCreated: (validator: Validator): void => {
self.validators.push(validator); self.validators.push(validator);
@@ -207,12 +208,13 @@ export class ClusterSettingsPage extends WizardPageBase<DeployClusterWizard> {
const activeDirectorySettingsGroup = createSection({ const activeDirectorySettingsGroup = createSection({
view: view, view: view,
container: self.wizard.wizardObject, container: self.wizard.wizardObject,
inputComponents: this.wizard.inputComponents,
sectionInfo: activeDirectorySection, sectionInfo: activeDirectorySection,
onNewDisposableCreated: (disposable: vscode.Disposable): void => { onNewDisposableCreated: (disposable: vscode.Disposable): void => {
self.wizard.registerDisposable(disposable); self.wizard.registerDisposable(disposable);
}, },
onNewInputComponentCreated: (name: string, component: InputComponent): void => { onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => {
self.inputComponents[name] = { component: component }; this.inputComponents[name] = { component: inputComponentInfo.component };
}, },
onNewValidatorCreated: (validator: Validator): void => { onNewValidatorCreated: (validator: Validator): void => {
self.validators.push(validator); self.validators.push(validator);
@@ -221,12 +223,13 @@ export class ClusterSettingsPage extends WizardPageBase<DeployClusterWizard> {
const dockerSettingsGroup = createSection({ const dockerSettingsGroup = createSection({
view: view, view: view,
container: self.wizard.wizardObject, container: self.wizard.wizardObject,
inputComponents: this.wizard.inputComponents,
sectionInfo: dockerSection, sectionInfo: dockerSection,
onNewDisposableCreated: (disposable: vscode.Disposable): void => { onNewDisposableCreated: (disposable: vscode.Disposable): void => {
self.wizard.registerDisposable(disposable); self.wizard.registerDisposable(disposable);
}, },
onNewInputComponentCreated: (name: string, component: InputComponent): void => { onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => {
self.inputComponents[name] = { component: component }; this.inputComponents[name] = { component: inputComponentInfo.component };
}, },
onNewValidatorCreated: (validator: Validator): void => { onNewValidatorCreated: (validator: Validator): void => {
self.validators.push(validator); self.validators.push(validator);
@@ -266,6 +269,7 @@ export class ClusterSettingsPage extends WizardPageBase<DeployClusterWizard> {
public onLeave() { public onLeave() {
setModelValues(this.inputComponents, this.wizard.model); setModelValues(this.inputComponents, this.wizard.model);
Object.assign(this.wizard.inputComponents, this.inputComponents);
if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) { if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) {
const variableDNSPrefixMapping: { [s: string]: string } = {}; const variableDNSPrefixMapping: { [s: string]: string } = {};
variableDNSPrefixMapping[VariableNames.AppServiceProxyDNSName_VariableName] = 'bdc-appproxy'; variableDNSPrefixMapping[VariableNames.AppServiceProxyDNSName_VariableName] = 'bdc-appproxy';

View File

@@ -5,11 +5,11 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { DeployClusterWizard } from '../deployClusterWizard'; import { FieldType, SectionInfo } from '../../../interfaces';
import { SectionInfo, FieldType } from '../../../interfaces'; import { createFlexContainer, createGroupContainer, createLabel, createNumberInput, createSection, createTextInput, getCheckboxComponent, getDropdownComponent, getInputBoxComponent, InputComponentInfo, InputComponents, setModelValues, Validator } from '../../modelViewUtils';
import { Validator, InputComponents, InputComponent, createSection, createGroupContainer, createLabel, createFlexContainer, createTextInput, createNumberInput, setModelValues, getInputBoxComponent, getCheckboxComponent, getDropdownComponent } from '../../modelViewUtils';
import { WizardPageBase } from '../../wizardPageBase'; import { WizardPageBase } from '../../wizardPageBase';
import * as VariableNames from '../constants'; import * as VariableNames from '../constants';
import { DeployClusterWizard } from '../deployClusterWizard';
import { AuthenticationMode } from '../deployClusterWizardModel'; import { AuthenticationMode } from '../deployClusterWizardModel';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
@@ -59,7 +59,7 @@ export class ServiceSettingsPage extends WizardPageBase<DeployClusterWizard> {
inputWidth: NumberInputWidth, inputWidth: NumberInputWidth,
spaceBetweenFields: '40px', spaceBetweenFields: '40px',
rows: [{ rows: [{
fields: [{ items: [{
type: FieldType.Options, type: FieldType.Options,
label: localize('deployCluster.MasterSqlServerInstances', "SQL Server master instances"), label: localize('deployCluster.MasterSqlServerInstances', "SQL Server master instances"),
options: ['1', '3', '4', '5', '6', '7', '8', '9'], options: ['1', '3', '4', '5', '6', '7', '8', '9'],
@@ -75,7 +75,7 @@ export class ServiceSettingsPage extends WizardPageBase<DeployClusterWizard> {
variableName: VariableNames.ComputePoolScale_VariableName, variableName: VariableNames.ComputePoolScale_VariableName,
}] }]
}, { }, {
fields: [{ items: [{
type: FieldType.Number, type: FieldType.Number,
label: localize('deployCluster.DataPoolInstances', "Data pool instances"), label: localize('deployCluster.DataPoolInstances', "Data pool instances"),
min: 1, min: 1,
@@ -93,7 +93,7 @@ export class ServiceSettingsPage extends WizardPageBase<DeployClusterWizard> {
variableName: VariableNames.SparkPoolScale_VariableName variableName: VariableNames.SparkPoolScale_VariableName
}] }]
}, { }, {
fields: [ items: [
{ {
type: FieldType.Number, type: FieldType.Number,
label: localize('deployCluster.StoragePoolInstances', "Storage pool (HDFS) instances"), label: localize('deployCluster.StoragePoolInstances', "Storage pool (HDFS) instances"),
@@ -119,12 +119,13 @@ export class ServiceSettingsPage extends WizardPageBase<DeployClusterWizard> {
return createSection({ return createSection({
view: view, view: view,
container: this.wizard.wizardObject, container: this.wizard.wizardObject,
inputComponents: this.inputComponents,
sectionInfo: sectionInfo, sectionInfo: sectionInfo,
onNewDisposableCreated: (disposable: vscode.Disposable): void => { onNewDisposableCreated: (disposable: vscode.Disposable): void => {
this.wizard.registerDisposable(disposable); this.wizard.registerDisposable(disposable);
}, },
onNewInputComponentCreated: (name: string, component: InputComponent): void => { onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => {
this.inputComponents[name] = { component: component }; this.inputComponents[name] = { component: inputComponentInfo.component };
}, },
onNewValidatorCreated: (validator: Validator): void => { onNewValidatorCreated: (validator: Validator): void => {
} }
@@ -400,6 +401,7 @@ export class ServiceSettingsPage extends WizardPageBase<DeployClusterWizard> {
public onLeave(): void { public onLeave(): void {
setModelValues(this.inputComponents, this.wizard.model); setModelValues(this.inputComponents, this.wizard.model);
Object.assign(this.wizard.inputComponents, this.inputComponents);
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true; return true;
}); });

View File

@@ -43,18 +43,17 @@ export class SummaryPage extends WizardPageBase<DeployClusterWizard> {
title: localize('deployCluster.DeploymentTarget', "Deployment target"), title: localize('deployCluster.DeploymentTarget', "Deployment target"),
rows: [ rows: [
{ {
fields: [ items: [
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.Kubeconfig', "Kube config"), label: localize('deployCluster.Kubeconfig', "Kube config"),
defaultValue: this.wizard.model.getStringValue(VariableNames.KubeConfigPath_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.KubeConfigPath_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}, },
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.ClusterContext', "Cluster context"), label: localize('deployCluster.ClusterContext', "Cluster context"),
defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterContext_VariableName), labelCSSStyles: { fontWeight: FontWeight.Bold }
labelFontWeight: FontWeight.Bold
}] }]
} }
] ]
@@ -68,33 +67,33 @@ export class SummaryPage extends WizardPageBase<DeployClusterWizard> {
rows: [ rows: [
{ {
fields: [ items: [
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.DeploymentProfile', "Deployment profile"), label: localize('deployCluster.DeploymentProfile', "Deployment profile"),
defaultValue: this.wizard.model.getStringValue(VariableNames.DeploymentProfile_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.DeploymentProfile_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}, },
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.ClusterName', "Cluster name"), label: localize('deployCluster.ClusterName', "Cluster name"),
defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterName_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterName_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}] }]
}, { }, {
fields: [ items: [
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.ControllerUsername', "Controller username"), label: localize('deployCluster.ControllerUsername', "Controller username"),
defaultValue: this.wizard.model.getStringValue(VariableNames.AdminUserName_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.AdminUserName_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}, { }, {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.AuthenticationMode', "Authentication mode"), label: localize('deployCluster.AuthenticationMode', "Authentication mode"),
defaultValue: this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory ? defaultValue: this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory ?
localize('deployCluster.AuthenticationMode.ActiveDirectory', "Active Directory") : localize('deployCluster.AuthenticationMode.ActiveDirectory', "Active Directory") :
localize('deployCluster.AuthenticationMode.Basic', "Basic"), localize('deployCluster.AuthenticationMode.Basic', "Basic"),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
} }
] ]
} }
@@ -103,72 +102,72 @@ export class SummaryPage extends WizardPageBase<DeployClusterWizard> {
if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) { if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) {
clusterSectionInfo.rows!.push({ clusterSectionInfo.rows!.push({
fields: [ items: [
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.OuDistinguishedName', "Organizational unit"), label: localize('deployCluster.OuDistinguishedName', "Organizational unit"),
defaultValue: this.wizard.model.getStringValue(VariableNames.OrganizationalUnitDistinguishedName_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.OrganizationalUnitDistinguishedName_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}, },
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.DomainControllerFQDNs', "Domain controller FQDNs"), label: localize('deployCluster.DomainControllerFQDNs', "Domain controller FQDNs"),
defaultValue: this.wizard.model.getStringValue(VariableNames.DomainControllerFQDNs_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.DomainControllerFQDNs_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}] }]
}); });
clusterSectionInfo.rows!.push({ clusterSectionInfo.rows!.push({
fields: [ items: [
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.DomainDNSIPAddresses', "Domain DNS IP addresses"), label: localize('deployCluster.DomainDNSIPAddresses', "Domain DNS IP addresses"),
defaultValue: this.wizard.model.getStringValue(VariableNames.DomainDNSIPAddresses_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.DomainDNSIPAddresses_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}, },
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.DomainDNSName', "Domain DNS name"), label: localize('deployCluster.DomainDNSName', "Domain DNS name"),
defaultValue: this.wizard.model.getStringValue(VariableNames.DomainDNSName_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.DomainDNSName_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}] }]
}); });
clusterSectionInfo.rows!.push({ clusterSectionInfo.rows!.push({
fields: [ items: [
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.ClusterAdmins', "Cluster admin group"), label: localize('deployCluster.ClusterAdmins', "Cluster admin group"),
defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterAdmins_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterAdmins_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}, },
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.ClusterUsers', "Cluster users"), label: localize('deployCluster.ClusterUsers', "Cluster users"),
defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterUsers_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterUsers_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}] }]
}); });
clusterSectionInfo.rows!.push({ clusterSectionInfo.rows!.push({
fields: [ items: [
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.AppOwers', "App owners"), label: localize('deployCluster.AppOwners', "App owners"),
defaultValue: this.wizard.model.getStringValue(VariableNames.AppOwners_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.AppOwners_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}, },
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.AppReaders', "App readers"), label: localize('deployCluster.AppReaders', "App readers"),
defaultValue: this.wizard.model.getStringValue(VariableNames.AppReaders_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.AppReaders_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}] }]
}); });
clusterSectionInfo.rows!.push({ clusterSectionInfo.rows!.push({
fields: [ items: [
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.DomainServiceAccountUserName', "Service account username"), label: localize('deployCluster.DomainServiceAccountUserName', "Service account username"),
defaultValue: this.wizard.model.getStringValue(VariableNames.DomainServiceAccountUserName_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.DomainServiceAccountUserName_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}] }]
}); });
} }
@@ -179,45 +178,45 @@ export class SummaryPage extends WizardPageBase<DeployClusterWizard> {
inputWidth: '200px', inputWidth: '200px',
title: localize('deployCluster.AzureSettings', "Azure settings"), title: localize('deployCluster.AzureSettings', "Azure settings"),
rows: [{ rows: [{
fields: [ items: [
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.SubscriptionId', "Subscription id"), label: localize('deployCluster.SubscriptionId', "Subscription id"),
defaultValue: this.wizard.model.getStringValue(VariableNames.SubscriptionId_VariableName) || localize('deployCluster.DefaultSubscription', "Default Azure Subscription"), defaultValue: this.wizard.model.getStringValue(VariableNames.SubscriptionId_VariableName) || localize('deployCluster.DefaultSubscription', "Default Azure Subscription"),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}, { }, {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.ResourceGroup', "Resource group"), label: localize('deployCluster.ResourceGroup', "Resource group"),
defaultValue: this.wizard.model.getStringValue(VariableNames.ResourceGroup_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.ResourceGroup_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
} }
] ]
}, { }, {
fields: [ items: [
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.Location', "Location"), label: localize('deployCluster.Location', "Location"),
defaultValue: this.wizard.model.getStringValue(VariableNames.Location_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.Location_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}, { }, {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.AksClusterName', "AKS cluster name"), label: localize('deployCluster.AksClusterName', "AKS cluster name"),
defaultValue: this.wizard.model.getStringValue(VariableNames.AksName_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.AksName_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
} }
] ]
}, { }, {
fields: [ items: [
{ {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.VMSize', "VM size"), label: localize('deployCluster.VMSize', "VM size"),
defaultValue: this.wizard.model.getStringValue(VariableNames.VMSize_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.VMSize_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}, { }, {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.VMCount', "VM count"), label: localize('deployCluster.VMCount', "VM count"),
defaultValue: this.wizard.model.getStringValue(VariableNames.VMCount_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.VMCount_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
} }
] ]
} }
@@ -231,35 +230,35 @@ export class SummaryPage extends WizardPageBase<DeployClusterWizard> {
title: localize('deployCluster.ScaleSettings', "Scale settings"), title: localize('deployCluster.ScaleSettings', "Scale settings"),
rows: [ rows: [
{ {
fields: [{ items: [{
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.MasterSqlServerInstances', "SQL Server master instances"), label: localize('deployCluster.MasterSqlServerInstances', "SQL Server master instances"),
defaultValue: this.wizard.model.getStringValue(VariableNames.SQLServerScale_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.SQLServerScale_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}, { }, {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.ComputePoolInstances', "Compute pool instances"), label: localize('deployCluster.ComputePoolInstances', "Compute pool instances"),
defaultValue: this.wizard.model.getStringValue(VariableNames.ComputePoolScale_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.ComputePoolScale_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}] }]
}, { }, {
fields: [{ items: [{
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.DataPoolInstances', "Data pool instances"), label: localize('deployCluster.DataPoolInstances', "Data pool instances"),
defaultValue: this.wizard.model.getStringValue(VariableNames.DataPoolScale_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.DataPoolScale_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}, { }, {
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.SparkPoolInstances', "Spark pool instances"), label: localize('deployCluster.SparkPoolInstances', "Spark pool instances"),
defaultValue: this.wizard.model.getStringValue(VariableNames.SparkPoolScale_VariableName), defaultValue: this.wizard.model.getStringValue(VariableNames.SparkPoolScale_VariableName),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}] }]
}, { }, {
fields: [{ items: [{
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: localize('deployCluster.StoragePoolInstances', "Storage pool (HDFS) instances"), label: localize('deployCluster.StoragePoolInstances', "Storage pool (HDFS) instances"),
defaultValue: `${this.wizard.model.getStringValue(VariableNames.HDFSPoolScale_VariableName)} ${this.wizard.model.getBooleanValue(VariableNames.IncludeSpark_VariableName) ? localize('deployCluster.WithSpark', "(Spark included)") : ''}`, defaultValue: `${this.wizard.model.getStringValue(VariableNames.HDFSPoolScale_VariableName)} ${this.wizard.model.getBooleanValue(VariableNames.IncludeSpark_VariableName) ? localize('deployCluster.WithSpark', "(Spark included)") : ''}`,
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}] }]
} }
] ]
@@ -270,6 +269,7 @@ export class SummaryPage extends WizardPageBase<DeployClusterWizard> {
title: '', title: '',
component: createSection({ component: createSection({
container: this.wizard.wizardObject, container: this.wizard.wizardObject,
inputComponents: this.wizard.inputComponents,
sectionInfo: sectionInfo, sectionInfo: sectionInfo,
view: this.view, view: this.view,
onNewDisposableCreated: () => { }, onNewDisposableCreated: () => { },
@@ -398,7 +398,7 @@ export class SummaryPage extends WizardPageBase<DeployClusterWizard> {
private createEndpointRow(name: string, dnsVariableName: string, portVariableName: string): azdata.FlexContainer { private createEndpointRow(name: string, dnsVariableName: string, portVariableName: string): azdata.FlexContainer {
const items = []; const items = [];
items.push(createLabel(this.view, { text: name, width: '150px', fontWeight: FontWeight.Bold })); items.push(createLabel(this.view, { text: name, width: '150px', cssStyles: { fontWeight: FontWeight.Bold } }));
if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) { if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) {
items.push(createLabel(this.view, { items.push(createLabel(this.view, {
text: this.wizard.model.getStringValue(dnsVariableName)!, width: '200px' text: this.wizard.model.getStringValue(dnsVariableName)!, width: '200px'

View File

@@ -4,15 +4,15 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import { EOL } from 'os';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { DialogBase } from './dialogBase';
import { INotebookService } from '../services/notebookService';
import { DialogInfo, instanceOfNotebookBasedDialogInfo, NotebookBasedDialogInfo } from '../interfaces'; import { DialogInfo, instanceOfNotebookBasedDialogInfo, NotebookBasedDialogInfo } from '../interfaces';
import { Validator, initializeDialog, InputComponents, setModelValues, InputValueTransformer, InputComponent } from './modelViewUtils'; import { INotebookService } from '../services/notebookService';
import { Model } from './model';
import { EOL } from 'os';
import { IPlatformService } from '../services/platformService'; import { IPlatformService } from '../services/platformService';
import { DialogBase } from './dialogBase';
import { Model } from './model';
import { initializeDialog, InputComponentInfo, InputComponents, setModelValues, Validator } from './modelViewUtils';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
@@ -42,11 +42,12 @@ export class DeploymentInputDialog extends DialogBase {
initializeDialog({ initializeDialog({
dialogInfo: this.dialogInfo, dialogInfo: this.dialogInfo,
container: this._dialogObject, container: this._dialogObject,
inputComponents: this.inputComponents,
onNewDisposableCreated: (disposable: vscode.Disposable): void => { onNewDisposableCreated: (disposable: vscode.Disposable): void => {
this._toDispose.push(disposable); this._toDispose.push(disposable);
}, },
onNewInputComponentCreated: (name: string, component: InputComponent, inputValueTransformer?: InputValueTransformer): void => { onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => {
this.inputComponents[name] = { component: component, inputValueTransformer: inputValueTransformer }; this.inputComponents[name] = inputComponentInfo;
}, },
onNewValidatorCreated: (validator: Validator): void => { onNewValidatorCreated: (validator: Validator): void => {
validators.push(validator); validators.push(validator);

View File

@@ -2,7 +2,14 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { NoteBookEnvironmentVariablePrefix } from '../interfaces'; import { EOL } from 'os';
import { ITool, NoteBookEnvironmentVariablePrefix } from '../interfaces';
import { setEnvironmentVariablesForInstallPaths, getRuntimeBinaryPathEnvironmentVariableName } from '../utils';
import { ToolsInstallPath } from '../constants';
import { delimiter } from 'path';
const NotebookEnvironmentVariablePrefixRegex = new RegExp(`^${NoteBookEnvironmentVariablePrefix}`);
export class Model { export class Model {
private propValueObject: { [s: string]: string | undefined } = {}; private propValueObject: { [s: string]: string | undefined } = {};
@@ -32,13 +39,57 @@ export class Model {
return value === undefined ? defaultValue : value === 'true'; return value === undefined ? defaultValue : value === 'true';
} }
public setEnvironmentVariables(): void { /**
Object.keys(this.propValueObject).filter(propertyName => propertyName.startsWith(NoteBookEnvironmentVariablePrefix)).forEach(propertyName => { * Returns python code statements for setting variables starting with {@see NoteBookEnvironmentVariablePrefix} as python variables.
const value = this.getStringValue(propertyName); * The prefix {@see NoteBookEnvironmentVariablePrefix} is removed and variable name changed to all lowercase to arrive at python variable name.
if (value !== undefined && value !== '') { * The statements returned are escaped for use in cell of a python notebook.
process.env[propertyName] = value; *
} * @param tools - optional set of tools for which variable value setting statements need to be generated;
process.env[propertyName] = value === undefined ? '' : value; * @param inputFilter - optional parameter to filter out setting of specific variable names. Every variable for which this function returns false is not included
* in the emitted code.
*/
public getCodeCellContentForNotebook(tools: ITool[] = [], inputFilter: (varName: string) => boolean = () => true): string[] {
const statements: string[] = Object.keys(this.propValueObject)
.filter(propertyName => propertyName.startsWith(NoteBookEnvironmentVariablePrefix) && inputFilter(propertyName))
.map(propertyName => {
const value = this.escapeForNotebookCodeCell(this.getStringValue(propertyName, ''));
const varName = propertyName.replace(NotebookEnvironmentVariablePrefixRegex, '').toLocaleLowerCase();
return `${varName} = '${value}'${EOL}`;
});
statements.push(`print('Variables have been set successfully.')${EOL}`);
const env: NodeJS.ProcessEnv = {};
setEnvironmentVariablesForInstallPaths(tools, env);
tools.forEach(tool => {
const envVarName: string = getRuntimeBinaryPathEnvironmentVariableName(tool.name);
statements.push(`os.environ["${envVarName}"] = "${this.escapeForNotebookCodeCell(env[envVarName]!)}"${EOL}`);
}); });
if (env[ToolsInstallPath]) {
statements.push(`os.environ["PATH"] = os.environ["PATH"] + "${delimiter}" + "${this.escapeForNotebookCodeCell(env[ToolsInstallPath])}"${EOL}`);
}
statements.push(`print('Environment Variables for tools have been set successfully.')${EOL}`);
return statements;
}
protected escapeForNotebookCodeCell(original?: string): string | undefined {
// Escape the \ character for the code cell string value
return original && original.replace(/\\/g, '\\\\');
}
/**
* Sets the environment variable for each model variable that starts with {@see NoteBookEnvironmentVariablePrefix} in the
* current process.
*
* @param env - env variable object in which the environment variables are populated. Default: process.env
* @param inputFilter - an optional filter to further restrict the variables that are set into the env object.
* Every variable for which this function returns false is not included does not get the env variable set.
* Default all variable meeting prefix requirements are set.
*/
public setEnvironmentVariables(env: NodeJS.ProcessEnv = process.env, inputFilter: (varName: string) => boolean = () => true): void {
Object.keys(this.propValueObject)
.filter(propertyName => propertyName.startsWith(NoteBookEnvironmentVariablePrefix) && inputFilter(propertyName))
.forEach(propertyName => {
const value = this.getStringValue(propertyName);
env[propertyName] = value === undefined ? '' : value;
});
} }
} }

View File

@@ -9,19 +9,28 @@ import * as vscode from 'vscode';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import * as azurecore from '../../../azurecore/src/azurecore'; import * as azurecore from '../../../azurecore/src/azurecore';
import { azureResource } from '../../../azurecore/src/azureResource/azure-resource'; import { azureResource } from '../../../azurecore/src/azureResource/azure-resource';
import { AzureAccountFieldInfo, AzureLocationsFieldInfo, DialogInfoBase, FieldInfo, FieldType, FontStyle, FontWeight, LabelPosition, PageInfoBase, SectionInfo, KubeClusterContextFieldInfo } from '../interfaces'; import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles } from '../interfaces';
import * as loc from '../localizedConstants'; import * as loc from '../localizedConstants';
import { getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../services/kubeService'; import { getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../services/kubeService';
import { getDateTimeString, getErrorMessage } from '../utils'; import { assert, getDateTimeString, getErrorMessage } from '../utils';
import { WizardInfoBase } from './../interfaces'; import { WizardInfoBase } from './../interfaces';
import { Model } from './model'; import { Model } from './model';
import { RadioGroupLoadingComponentBuilder } from './radioGroupLoadingComponentBuilder';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
export type Validator = () => { valid: boolean, message: string }; export type Validator = () => { valid: boolean, message: string };
export type InputValueTransformer = (inputValue: string) => string; export type InputValueTransformer = (inputValue: string) => string;
export type InputComponent = azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent | azdata.RadioButtonComponent; export type InputComponent = azdata.TextComponent | azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent | RadioGroupLoadingComponentBuilder;
export type InputComponents = { [s: string]: { component: InputComponent; inputValueTransformer?: InputValueTransformer } }; export type InputComponentInfo = {
component: InputComponent;
inputValueTransformer?: InputValueTransformer;
isPassword?: boolean
};
export type InputComponents = {
[s: string]: InputComponentInfo
};
export function getInputBoxComponent(name: string, inputComponents: InputComponents): azdata.InputBoxComponent { export function getInputBoxComponent(name: string, inputComponents: InputComponents): azdata.InputBoxComponent {
return <azdata.InputBoxComponent>inputComponents[name].component; return <azdata.InputBoxComponent>inputComponents[name].component;
@@ -39,40 +48,42 @@ export function getTextComponent(name: string, inputComponents: InputComponents)
return <azdata.TextComponent>inputComponents[name].component; return <azdata.TextComponent>inputComponents[name].component;
} }
export const DefaultInputComponentWidth = '400px'; export const DefaultInputWidth = '400px';
export const DefaultLabelComponentWidth = '200px'; export const DefaultLabelWidth = '200px';
export const DefaultFieldAlignItems = undefined;
export const DefaultFieldWidth = undefined;
export const DefaultFieldHeight = undefined;
export interface DialogContext extends CreateContext { export interface DialogContext extends ContextBase {
dialogInfo: DialogInfoBase; dialogInfo: DialogInfoBase;
container: azdata.window.Dialog; container: azdata.window.Dialog;
} }
export interface WizardPageContext extends CreateContext { export interface WizardPageContext extends ContextBase {
wizardInfo: WizardInfoBase; wizardInfo: WizardInfoBase;
pageInfo: PageInfoBase; pageInfo: PageInfoBase;
page: azdata.window.WizardPage; page: azdata.window.WizardPage;
container: azdata.window.Wizard; container: azdata.window.Wizard;
} }
export interface SectionContext extends CreateContext { export interface SectionContext extends ContextBase {
sectionInfo: SectionInfo; sectionInfo: SectionInfo;
view: azdata.ModelView; view: azdata.ModelView;
} }
interface FieldContext extends CreateContext { export interface FieldContext extends ContextBase {
fieldInfo: FieldInfo; fieldInfo: FieldInfo;
components: azdata.Component[]; components: azdata.Component[];
view: azdata.ModelView; view: azdata.ModelView;
} }
interface FilePickerInputs { export interface FilePickerInputs {
input: azdata.InputBoxComponent; input: azdata.InputBoxComponent;
browseButton: azdata.ButtonComponent; browseButton: azdata.ButtonComponent;
} }
interface ReadOnlyFieldInputs {
interface RadioOptionsInputs { label: azdata.TextComponent;
optionsList: azdata.DivContainer; text?: azdata.TextComponent;
loader: azdata.LoadingComponent;
} }
interface KubeClusterContextFieldContext extends FieldContext { interface KubeClusterContextFieldContext extends FieldContext {
@@ -87,11 +98,12 @@ interface AzureAccountFieldContext extends FieldContext {
fieldInfo: AzureAccountFieldInfo; fieldInfo: AzureAccountFieldInfo;
} }
interface CreateContext { interface ContextBase {
container: azdata.window.Dialog | azdata.window.Wizard; container: azdata.window.Dialog | azdata.window.Wizard;
inputComponents: InputComponents;
onNewValidatorCreated: (validator: Validator) => void; onNewValidatorCreated: (validator: Validator) => void;
onNewDisposableCreated: (disposable: vscode.Disposable) => void; onNewDisposableCreated: (disposable: vscode.Disposable) => void;
onNewInputComponentCreated: (name: string, component: InputComponent, inputValueTransformer?: InputValueTransformer) => void; onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo) => void;
} }
export function createTextInput(view: azdata.ModelView, inputInfo: { defaultValue?: string, ariaLabel: string, required?: boolean, placeHolder?: string, width?: string, enabled?: boolean }): azdata.InputBoxComponent { export function createTextInput(view: azdata.ModelView, inputInfo: { defaultValue?: string, ariaLabel: string, required?: boolean, placeHolder?: string, width?: string, enabled?: boolean }): azdata.InputBoxComponent {
@@ -106,12 +118,20 @@ export function createTextInput(view: azdata.ModelView, inputInfo: { defaultValu
}).component(); }).component();
} }
export function createLabel(view: azdata.ModelView, info: { text: string, description?: string, required?: boolean, width?: string, fontStyle?: FontStyle, fontWeight?: FontWeight, links?: azdata.LinkArea[] }): azdata.TextComponent { export function createLabel(view: azdata.ModelView, info: { text: string, description?: string, required?: boolean, width?: string, links?: azdata.LinkArea[], cssStyles?: TextCSSStyles }): azdata.TextComponent {
let cssStyles: { [key: string]: string } = {};
if (info.cssStyles !== undefined) {
cssStyles = Object.assign(cssStyles, info.cssStyles, { 'font-style': info.cssStyles.fontStyle || 'normal', 'font-weight': info.cssStyles.fontWeight || 'normal' });
if (info.cssStyles.color !== undefined) {
cssStyles['color'] = info.cssStyles.color;
}
}
const text = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ const text = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: info.text, value: info.text,
description: info.description, description: info.description,
requiredIndicator: info.required, requiredIndicator: info.required,
CSSStyles: { 'font-style': info.fontStyle || 'normal', 'font-weight': info.fontWeight || 'normal' }, CSSStyles: cssStyles,
links: info.links links: info.links
}).component(); }).component();
text.width = info.width; text.width = info.width;
@@ -157,8 +177,11 @@ export function initializeDialog(dialogContext: DialogContext): void {
const tab = azdata.window.createTab(tabInfo.title); const tab = azdata.window.createTab(tabInfo.title);
tab.registerContent((view: azdata.ModelView) => { tab.registerContent((view: azdata.ModelView) => {
const sections = tabInfo.sections.map(sectionInfo => { const sections = tabInfo.sections.map(sectionInfo => {
sectionInfo.inputWidth = sectionInfo.inputWidth || tabInfo.inputWidth || DefaultInputComponentWidth; sectionInfo.inputWidth = sectionInfo.inputWidth || tabInfo.inputWidth || DefaultInputWidth;
sectionInfo.labelWidth = sectionInfo.labelWidth || tabInfo.labelWidth || DefaultLabelComponentWidth; sectionInfo.labelWidth = sectionInfo.labelWidth || tabInfo.labelWidth || DefaultLabelWidth;
sectionInfo.fieldAlignItems = sectionInfo.fieldAlignItems || tabInfo.fieldAlignItems || DefaultFieldAlignItems;
sectionInfo.fieldWidth = sectionInfo.fieldWidth || tabInfo.fieldWidth || DefaultFieldWidth;
sectionInfo.fieldHeight = sectionInfo.fieldHeight || tabInfo.fieldHeight || DefaultFieldHeight;
sectionInfo.labelPosition = sectionInfo.labelPosition || tabInfo.labelPosition; sectionInfo.labelPosition = sectionInfo.labelPosition || tabInfo.labelPosition;
return createSection({ return createSection({
sectionInfo: sectionInfo, sectionInfo: sectionInfo,
@@ -166,7 +189,8 @@ export function initializeDialog(dialogContext: DialogContext): void {
onNewDisposableCreated: dialogContext.onNewDisposableCreated, onNewDisposableCreated: dialogContext.onNewDisposableCreated,
onNewInputComponentCreated: dialogContext.onNewInputComponentCreated, onNewInputComponentCreated: dialogContext.onNewInputComponentCreated,
onNewValidatorCreated: dialogContext.onNewValidatorCreated, onNewValidatorCreated: dialogContext.onNewValidatorCreated,
container: dialogContext.container container: dialogContext.container,
inputComponents: dialogContext.inputComponents
}); });
}); });
const formBuilder = view.modelBuilder.formContainer().withFormItems( const formBuilder = view.modelBuilder.formContainer().withFormItems(
@@ -189,12 +213,16 @@ export function initializeDialog(dialogContext: DialogContext): void {
export function initializeWizardPage(context: WizardPageContext): void { export function initializeWizardPage(context: WizardPageContext): void {
context.page.registerContent((view: azdata.ModelView) => { context.page.registerContent((view: azdata.ModelView) => {
const sections = context.pageInfo.sections.map(sectionInfo => { const sections = context.pageInfo.sections.map(sectionInfo => {
sectionInfo.inputWidth = sectionInfo.inputWidth || context.pageInfo.inputWidth || context.wizardInfo.inputWidth || DefaultInputComponentWidth; sectionInfo.inputWidth = sectionInfo.inputWidth || context.pageInfo.inputWidth || context.wizardInfo.inputWidth || DefaultInputWidth;
sectionInfo.labelWidth = sectionInfo.labelWidth || context.pageInfo.labelWidth || context.wizardInfo.labelWidth || DefaultLabelComponentWidth; sectionInfo.labelWidth = sectionInfo.labelWidth || context.pageInfo.labelWidth || context.wizardInfo.labelWidth || DefaultLabelWidth;
sectionInfo.fieldAlignItems = sectionInfo.fieldAlignItems || context.pageInfo.fieldAlignItems || DefaultFieldAlignItems;
sectionInfo.fieldWidth = sectionInfo.fieldWidth || context.pageInfo.fieldWidth || context.wizardInfo.fieldWidth || DefaultFieldWidth;
sectionInfo.fieldHeight = sectionInfo.fieldHeight || context.pageInfo.fieldHeight || context.wizardInfo.fieldHeight || DefaultFieldHeight;
sectionInfo.labelPosition = sectionInfo.labelPosition || context.pageInfo.labelPosition || context.wizardInfo.labelPosition; sectionInfo.labelPosition = sectionInfo.labelPosition || context.pageInfo.labelPosition || context.wizardInfo.labelPosition;
return createSection({ return createSection({
view: view, view: view,
container: context.container, container: context.container,
inputComponents: context.inputComponents,
onNewDisposableCreated: context.onNewDisposableCreated, onNewDisposableCreated: context.onNewDisposableCreated,
onNewInputComponentCreated: context.onNewInputComponentCreated, onNewInputComponentCreated: context.onNewInputComponentCreated,
onNewValidatorCreated: context.onNewValidatorCreated, onNewValidatorCreated: context.onNewValidatorCreated,
@@ -215,16 +243,16 @@ export function initializeWizardPage(context: WizardPageContext): void {
export function createSection(context: SectionContext): azdata.GroupContainer { export function createSection(context: SectionContext): azdata.GroupContainer {
const components: azdata.Component[] = []; const components: azdata.Component[] = [];
context.sectionInfo.inputWidth = context.sectionInfo.inputWidth || DefaultInputComponentWidth; context.sectionInfo.inputWidth = context.sectionInfo.inputWidth || DefaultInputWidth;
context.sectionInfo.labelWidth = context.sectionInfo.labelWidth || DefaultLabelComponentWidth; context.sectionInfo.labelWidth = context.sectionInfo.labelWidth || DefaultLabelWidth;
context.sectionInfo.fieldAlignItems = context.sectionInfo.fieldAlignItems || DefaultFieldAlignItems;
context.sectionInfo.fieldWidth = context.sectionInfo.fieldWidth || DefaultFieldWidth;
context.sectionInfo.fieldHeight = context.sectionInfo.fieldHeight || DefaultFieldHeight;
if (context.sectionInfo.fields) { if (context.sectionInfo.fields) {
processFields(context.sectionInfo.fields, components, context); processFields(context.sectionInfo.fields, components, context);
} else if (context.sectionInfo.rows) { } else if (context.sectionInfo.rows) {
context.sectionInfo.rows.forEach(rowInfo => { context.sectionInfo.rows.forEach(rowInfo => {
const rowItems: azdata.Component[] = []; components.push(processRow(rowInfo, context));
processFields(rowInfo.fields, rowItems, context, context.sectionInfo.spaceBetweenFields || '50px');
const row = createFlexContainer(context.view, rowItems);
components.push(row);
}); });
} }
@@ -235,11 +263,26 @@ export function createSection(context: SectionContext): azdata.GroupContainer {
}); });
} }
function processRow(rowInfo: RowInfo, context: SectionContext): azdata.Component {
const items: azdata.Component[] = [];
if ('items' in rowInfo.items[0]) { // rowInfo.items is RowInfo[]
const rowItems = rowInfo.items as RowInfo[];
items.push(...rowItems.map(rowInfo => processRow(rowInfo, context)));
} else { // rowInfo.items is FieldInfo[]
const fieldItems = rowInfo.items as FieldInfo[];
processFields(fieldItems, items, context, context.sectionInfo.spaceBetweenFields === undefined ? '50px' : context.sectionInfo.spaceBetweenFields);
}
return createFlexContainer(context.view, items, true, context.sectionInfo.fieldWidth, context.sectionInfo.fieldHeight, context.sectionInfo.fieldAlignItems, rowInfo.cssStyles);
}
function processFields(fieldInfoArray: FieldInfo[], components: azdata.Component[], context: SectionContext, spaceBetweenFields?: string): void { function processFields(fieldInfoArray: FieldInfo[], components: azdata.Component[], context: SectionContext, spaceBetweenFields?: string): void {
for (let i = 0; i < fieldInfoArray.length; i++) { for (let i = 0; i < fieldInfoArray.length; i++) {
const fieldInfo = fieldInfoArray[i]; const fieldInfo = fieldInfoArray[i];
fieldInfo.labelWidth = fieldInfo.labelWidth || context.sectionInfo.labelWidth; fieldInfo.labelWidth = fieldInfo.labelWidth || context.sectionInfo.labelWidth;
fieldInfo.inputWidth = fieldInfo.inputWidth || context.sectionInfo.inputWidth; fieldInfo.inputWidth = fieldInfo.inputWidth || context.sectionInfo.inputWidth;
fieldInfo.fieldAlignItems = fieldInfo.fieldAlignItems || context.sectionInfo.fieldAlignItems;
fieldInfo.fieldWidth = fieldInfo.fieldWidth || context.sectionInfo.fieldWidth;
fieldInfo.fieldHeight = fieldInfo.fieldHeight || context.sectionInfo.fieldHeight;
fieldInfo.labelPosition = fieldInfo.labelPosition === undefined ? context.sectionInfo.labelPosition : fieldInfo.labelPosition; fieldInfo.labelPosition = fieldInfo.labelPosition === undefined ? context.sectionInfo.labelPosition : fieldInfo.labelPosition;
processField({ processField({
view: context.view, view: context.view,
@@ -248,6 +291,7 @@ function processFields(fieldInfoArray: FieldInfo[], components: azdata.Component
onNewValidatorCreated: context.onNewValidatorCreated, onNewValidatorCreated: context.onNewValidatorCreated,
fieldInfo: fieldInfo, fieldInfo: fieldInfo,
container: context.container, container: context.container,
inputComponents: context.inputComponents,
components: components components: components
}); });
if (spaceBetweenFields && i < fieldInfoArray.length - 1) { if (spaceBetweenFields && i < fieldInfoArray.length - 1) {
@@ -256,24 +300,37 @@ function processFields(fieldInfoArray: FieldInfo[], components: azdata.Component
} }
} }
export function createFlexContainer(view: azdata.ModelView, items: azdata.Component[], rowLayout: boolean = true): azdata.FlexContainer { export function createFlexContainer(view: azdata.ModelView, items: azdata.Component[], rowLayout: boolean = true, width?: string | number, height?: string | number, alignItems?: azdata.AlignItemsType, cssStyles?: ComponentCSSStyles): azdata.FlexContainer {
const flexFlow = rowLayout ? 'row' : 'column'; const flexFlow = rowLayout ? 'row' : 'column';
const alignItems = rowLayout ? 'center' : undefined; alignItems = alignItems || (rowLayout ? 'center' : undefined);
const itemsStyle = rowLayout ? { CSSStyles: { 'margin-right': '5px' } } : {}; const itemsStyle = rowLayout ? { CSSStyles: { 'margin-right': '5px', } } : {};
return view.modelBuilder.flexContainer().withItems(items, itemsStyle).withLayout({ flexFlow: flexFlow, alignItems: alignItems }).component(); const flexLayout: azdata.FlexLayout = { flexFlow: flexFlow };
if (height) {
flexLayout.height = height;
}
if (width) {
flexLayout.width = width;
}
if (alignItems) {
flexLayout.alignItems = alignItems;
}
return view.modelBuilder.flexContainer().withItems(items, itemsStyle).withLayout(flexLayout).withProperties<azdata.ComponentProperties>({ CSSStyles: cssStyles || {} }).component();
} }
export function createGroupContainer(view: azdata.ModelView, items: azdata.Component[], layout: azdata.GroupLayout): azdata.GroupContainer { export function createGroupContainer(view: azdata.ModelView, items: azdata.Component[], layout: azdata.GroupLayout): azdata.GroupContainer {
return view.modelBuilder.groupContainer().withItems(items).withLayout(layout).component(); return view.modelBuilder.groupContainer().withItems(items).withLayout(layout).component();
} }
function addLabelInputPairToContainer(view: azdata.ModelView, components: azdata.Component[], label: azdata.Component, input: azdata.Component, labelPosition?: LabelPosition, additionalComponents?: azdata.Component[]) { function addLabelInputPairToContainer(view: azdata.ModelView, components: azdata.Component[], label: azdata.Component, input: azdata.Component | undefined, fieldInfo: FieldInfo, additionalComponents?: azdata.Component[]) {
const inputs = [label, input]; const inputs: azdata.Component[] = [label];
if (input !== undefined) {
inputs.push(input);
}
if (additionalComponents && additionalComponents.length > 0) { if (additionalComponents && additionalComponents.length > 0) {
inputs.push(...additionalComponents); inputs.push(...additionalComponents);
} }
if (labelPosition && labelPosition === LabelPosition.Left) { if (fieldInfo.labelPosition === LabelPosition.Left) {
const row = createFlexContainer(view, inputs); const row = createFlexContainer(view, inputs, true, fieldInfo.fieldWidth, fieldInfo.fieldHeight, fieldInfo.fieldAlignItems);
components.push(row); components.push(row);
} else { } else {
components.push(...inputs); components.push(...inputs);
@@ -285,9 +342,6 @@ function processField(context: FieldContext): void {
case FieldType.Options: case FieldType.Options:
processOptionsTypeField(context); processOptionsTypeField(context);
break; break;
case FieldType.RadioOptions:
processRadioOptionsTypeField(context);
break;
case FieldType.DateTimeText: case FieldType.DateTimeText:
processDateTimeTextField(context); processDateTimeTextField(context);
break; break;
@@ -325,21 +379,42 @@ function processField(context: FieldContext): void {
} }
function processOptionsTypeField(context: FieldContext): void { function processOptionsTypeField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); assert(context.fieldInfo.options !== undefined, `FieldInfo.options must be defined for FieldType:${FieldType.Options}`);
if (Array.isArray(context.fieldInfo.options)) {
context.fieldInfo.options = <OptionsInfo>{
values: context.fieldInfo.options,
defaultValue: context.fieldInfo.defaultValue,
optionsType: OptionsType.Dropdown
};
}
assert(typeof context.fieldInfo.options === 'object', `FieldInfo.options must be an object if it is not an array`);
assert('optionsType' in context.fieldInfo.options, `When FieldInfo.options is an object it must have 'optionsType' property`);
if (context.fieldInfo.options.optionsType === OptionsType.Radio) {
processRadioOptionsTypeField(context);
} else {
assert(context.fieldInfo.options.optionsType === OptionsType.Dropdown, `When optionsType is not ${OptionsType.Radio} then it must be ${OptionsType.Dropdown}`);
processDropdownOptionsTypeField(context);
}
}
function processDropdownOptionsTypeField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const options = context.fieldInfo.options as OptionsInfo;
const dropdown = createDropdown(context.view, { const dropdown = createDropdown(context.view, {
values: context.fieldInfo.options, values: options.values,
defaultValue: context.fieldInfo.defaultValue, defaultValue: options.defaultValue,
width: context.fieldInfo.inputWidth, width: context.fieldInfo.inputWidth,
editable: context.fieldInfo.editable, editable: context.fieldInfo.editable,
required: context.fieldInfo.required, required: context.fieldInfo.required,
label: context.fieldInfo.label label: context.fieldInfo.label
}); });
context.onNewInputComponentCreated(context.fieldInfo.variableName!, dropdown); dropdown.fireOnTextChange = true;
addLabelInputPairToContainer(context.view, context.components, label, dropdown, context.fieldInfo.labelPosition); context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: dropdown });
addLabelInputPairToContainer(context.view, context.components, label, dropdown, context.fieldInfo);
} }
function processDateTimeTextField(context: FieldContext): void { function processDateTimeTextField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const defaultValue = context.fieldInfo.defaultValue + getDateTimeString(); const defaultValue = context.fieldInfo.defaultValue + getDateTimeString();
const input = context.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({ const input = context.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
value: defaultValue, value: defaultValue,
@@ -349,12 +424,12 @@ function processDateTimeTextField(context: FieldContext): void {
placeHolder: context.fieldInfo.placeHolder placeHolder: context.fieldInfo.placeHolder
}).component(); }).component();
input.width = context.fieldInfo.inputWidth; input.width = context.fieldInfo.inputWidth;
context.onNewInputComponentCreated(context.fieldInfo.variableName!, input); context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input });
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo.labelPosition); addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo);
} }
function processNumberField(context: FieldContext): void { function processNumberField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const input = createNumberInput(context.view, { const input = createNumberInput(context.view, {
defaultValue: context.fieldInfo.defaultValue, defaultValue: context.fieldInfo.defaultValue,
ariaLabel: context.fieldInfo.label, ariaLabel: context.fieldInfo.label,
@@ -364,12 +439,12 @@ function processNumberField(context: FieldContext): void {
width: context.fieldInfo.inputWidth, width: context.fieldInfo.inputWidth,
placeHolder: context.fieldInfo.placeHolder placeHolder: context.fieldInfo.placeHolder
}); });
context.onNewInputComponentCreated(context.fieldInfo.variableName!, input); context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input });
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo.labelPosition); addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo);
} }
function processTextField(context: FieldContext): void { function processTextField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const input = createTextInput(context.view, { const input = createTextInput(context.view, {
defaultValue: context.fieldInfo.defaultValue, defaultValue: context.fieldInfo.defaultValue,
ariaLabel: context.fieldInfo.label, ariaLabel: context.fieldInfo.label,
@@ -378,8 +453,8 @@ function processTextField(context: FieldContext): void {
width: context.fieldInfo.inputWidth, width: context.fieldInfo.inputWidth,
enabled: context.fieldInfo.enabled enabled: context.fieldInfo.enabled
}); });
context.onNewInputComponentCreated(context.fieldInfo.variableName!, input); context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input });
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo.labelPosition); addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo);
if (context.fieldInfo.textValidationRequired) { if (context.fieldInfo.textValidationRequired) {
let validationRegex: RegExp = new RegExp(context.fieldInfo.textValidationRegex!); let validationRegex: RegExp = new RegExp(context.fieldInfo.textValidationRegex!);
@@ -399,12 +474,11 @@ function processTextField(context: FieldContext): void {
return { valid: inputIsValid, message: context.fieldInfo.textValidationDescription! }; return { valid: inputIsValid, message: context.fieldInfo.textValidationDescription! };
}; };
context.onNewValidatorCreated(inputValidator); context.onNewValidatorCreated(inputValidator);
} }
} }
function processPasswordField(context: FieldContext): void { function processPasswordField(context: FieldContext): void {
const passwordLabel = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); const passwordLabel = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const passwordInput = context.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({ const passwordInput = context.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: context.fieldInfo.label, ariaLabel: context.fieldInfo.label,
inputType: 'password', inputType: 'password',
@@ -412,8 +486,8 @@ function processPasswordField(context: FieldContext): void {
placeHolder: context.fieldInfo.placeHolder, placeHolder: context.fieldInfo.placeHolder,
width: context.fieldInfo.inputWidth width: context.fieldInfo.inputWidth
}).component(); }).component();
context.onNewInputComponentCreated(context.fieldInfo.variableName!, passwordInput); context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: passwordInput, isPassword: true });
addLabelInputPairToContainer(context.view, context.components, passwordLabel, passwordInput, context.fieldInfo.labelPosition); addLabelInputPairToContainer(context.view, context.components, passwordLabel, passwordInput, context.fieldInfo);
if (context.fieldInfo.type === FieldType.SQLPassword) { if (context.fieldInfo.type === FieldType.SQLPassword) {
const invalidPasswordMessage = getInvalidSQLPasswordMessage(context.fieldInfo.label); const invalidPasswordMessage = getInvalidSQLPasswordMessage(context.fieldInfo.label);
@@ -430,7 +504,7 @@ function processPasswordField(context: FieldContext): void {
if (context.fieldInfo.confirmationRequired) { if (context.fieldInfo.confirmationRequired) {
const passwordNotMatchMessage = getPasswordMismatchMessage(context.fieldInfo.label); const passwordNotMatchMessage = getPasswordMismatchMessage(context.fieldInfo.label);
const confirmPasswordLabel = createLabel(context.view, { text: context.fieldInfo.confirmationLabel!, required: true, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); const confirmPasswordLabel = createLabel(context.view, { text: context.fieldInfo.confirmationLabel!, required: true, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const confirmPasswordInput = context.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({ const confirmPasswordInput = context.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: context.fieldInfo.confirmationLabel, ariaLabel: context.fieldInfo.confirmationLabel,
inputType: 'password', inputType: 'password',
@@ -438,7 +512,7 @@ function processPasswordField(context: FieldContext): void {
width: context.fieldInfo.inputWidth width: context.fieldInfo.inputWidth
}).component(); }).component();
addLabelInputPairToContainer(context.view, context.components, confirmPasswordLabel, confirmPasswordInput, context.fieldInfo.labelPosition); addLabelInputPairToContainer(context.view, context.components, confirmPasswordLabel, confirmPasswordInput, context.fieldInfo);
context.onNewValidatorCreated((): { valid: boolean, message: string } => { context.onNewValidatorCreated((): { valid: boolean, message: string } => {
const passwordMatches = passwordInput.value === confirmPasswordInput.value; const passwordMatches = passwordInput.value === confirmPasswordInput.value;
return { valid: passwordMatches, message: passwordNotMatchMessage }; return { valid: passwordMatches, message: passwordNotMatchMessage };
@@ -459,34 +533,85 @@ function processPasswordField(context: FieldContext): void {
} }
} }
function processReadonlyTextField(context: FieldContext): void { function processReadonlyTextField(context: FieldContext, allowEvaluation: boolean = true): ReadOnlyFieldInputs {
let defaultValue = context.fieldInfo.defaultValue || ''; if ((context.fieldInfo.links?.length ?? 0) > 0) {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: false, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); return processHyperlinkedTextField(context);
const text = createLabel(context.view, { text: defaultValue, description: '', required: false, width: context.fieldInfo.inputWidth, fontWeight: context.fieldInfo.textFontWeight, fontStyle: context.fieldInfo.fontStyle, links: context.fieldInfo.links }); } else if (context.fieldInfo.isEvaluated && allowEvaluation) {
addLabelInputPairToContainer(context.view, context.components, label, text, context.fieldInfo.labelPosition); return processEvaluatedTextField(context);
}
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: false, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const text = context.fieldInfo.defaultValue !== undefined
? createLabel(context.view, { text: context.fieldInfo.defaultValue, description: '', required: false, width: context.fieldInfo.inputWidth })
: undefined;
addLabelInputPairToContainer(context.view, context.components, label, text, context.fieldInfo);
return { label: label, text: text };
}
/**
* creates a text component that has text that contains hyperlinks. The context.fieldInfo.label contains {0},{1} ...
* placeholder(s) where contents of link array object are placed with that portion interpolated as a clickable link.
*
* @param context - the FieldContext object using which the field gets created
*/
function processHyperlinkedTextField(context: FieldContext): ReadOnlyFieldInputs {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: false, width: context.fieldInfo.labelWidth, links: context.fieldInfo.links, cssStyles: context.fieldInfo.labelCSSStyles });
context.components.push(label);
return { label: label };
}
function processEvaluatedTextField(context: FieldContext): ReadOnlyFieldInputs {
const readOnlyField = processReadonlyTextField(context, false /*allowEvaluation*/);
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, {
component: readOnlyField.text!,
inputValueTransformer: () => {
readOnlyField.text!.value = substituteVariableValues(context.inputComponents, context.fieldInfo.defaultValue);
return readOnlyField.text?.value!;
}
});
return readOnlyField;
}
/**
* Returns a string that interpolates all variable names in the {@param inputValue} string de-marked as $(VariableName)
* substituted with their corresponding values.
*
* Only variables in the current model starting with {@see NoteBookEnvironmentVariablePrefix} are replaced.
*
* @param inputValue
* @param inputComponents
*/
function substituteVariableValues(inputComponents: InputComponents, inputValue?: string): string | undefined {
Object.keys(inputComponents)
.filter(key => key.startsWith(NoteBookEnvironmentVariablePrefix))
.forEach(key => {
const value = getInputComponentValue(inputComponents, key) ?? '<undefined>';
const re: RegExp = new RegExp(`\\\$\\\(${key}\\\)`, 'gi');
inputValue = inputValue?.replace(re, value);
});
return inputValue;
} }
function processCheckboxField(context: FieldContext): void { function processCheckboxField(context: FieldContext): void {
const checkbox = createCheckbox(context.view, { initialValue: context.fieldInfo.defaultValue! === 'true', label: context.fieldInfo.label, required: context.fieldInfo.required }); const checkbox = createCheckbox(context.view, { initialValue: context.fieldInfo.defaultValue! === 'true', label: context.fieldInfo.label, required: context.fieldInfo.required });
context.components.push(checkbox); context.components.push(checkbox);
context.onNewInputComponentCreated(context.fieldInfo.variableName!, checkbox); context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: checkbox });
} }
/** /**
* A File Picker field consists of a text field and a browse button that allows a user to pick a file system file. * A File Picker field consists of a text field and a browse button that allows a user to pick a file system file.
* @param context The context to use to create the field * @param context The context to use to create the field
*/ */
function processFilePickerField(context: FieldContext, defaultFilePath?: string): FilePickerInputs { function processFilePickerField(context: FieldContext): FilePickerInputs {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const input = createTextInput(context.view, { const input = createTextInput(context.view, {
defaultValue: defaultFilePath || context.fieldInfo.defaultValue || '', defaultValue: context.fieldInfo.defaultValue || '',
ariaLabel: context.fieldInfo.label, ariaLabel: context.fieldInfo.label,
required: context.fieldInfo.required, required: context.fieldInfo.required,
placeHolder: context.fieldInfo.placeHolder, placeHolder: context.fieldInfo.placeHolder,
width: context.fieldInfo.inputWidth, width: context.fieldInfo.inputWidth,
enabled: context.fieldInfo.enabled enabled: context.fieldInfo.enabled
}); });
context.onNewInputComponentCreated(context.fieldInfo.variableName!, input); context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input });
input.enabled = false; input.enabled = false;
const browseFileButton = context.view!.modelBuilder.button().withProperties({ label: loc.browse }).component(); const browseFileButton = context.view!.modelBuilder.button().withProperties({ label: loc.browse }).component();
context.onNewDisposableCreated(browseFileButton.onDidClick(async () => { context.onNewDisposableCreated(browseFileButton.onDidClick(async () => {
@@ -506,42 +631,22 @@ function processFilePickerField(context: FieldContext, defaultFilePath?: string)
let fileUri = fileUris[0]; let fileUri = fileUris[0];
input.value = fileUri.fsPath; input.value = fileUri.fsPath;
})); }));
addLabelInputPairToContainer(context.view, context.components, label, input, LabelPosition.Left, [browseFileButton]); context.fieldInfo.labelPosition = LabelPosition.Left;
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo, [browseFileButton]);
return { input: input, browseButton: browseFileButton }; return { input: input, browseButton: browseFileButton };
} }
/** /**
* An Kube Config Cluster picker field consists of a file system filee picker and radio button selector for cluster contexts defined in the config filed picked using the file picker. * This function returns a method that reads the cluster context from the {@param file}. This method then returns the cluster contexts
* @param context The context to use to create the field * read as an OptionsInfo object asynchronously.
*
* @param file - the file from which to fetch the cluster contexts
*/ */
async function processKubeConfigClusterPickerField(context: KubeClusterContextFieldContext): Promise<void> { function getClusterContexts(file: string): (() => Promise<OptionsInfo>) {
const kubeConfigFilePathVariableName = context.fieldInfo.configFileVariableName || 'AZDATA_NB_VAR_KUBECONFIG_PATH'; return async () => {
const filePickerContext: FieldContext = {
container: context.container,
components: context.components,
view: context.view,
onNewValidatorCreated: context.onNewValidatorCreated,
onNewDisposableCreated: context.onNewDisposableCreated,
onNewInputComponentCreated: context.onNewInputComponentCreated,
fieldInfo: {
label: loc.kubeConfigFilePath,
type: FieldType.FilePicker,
labelWidth: context.fieldInfo.labelWidth,
variableName: kubeConfigFilePathVariableName,
required: true
}
};
const filePicker = processFilePickerField(filePickerContext, getDefaultKubeConfigPath());
context.fieldInfo.subFields = context.fieldInfo.subFields || [];
context.fieldInfo.subFields!.push({
label: filePickerContext.fieldInfo.label,
variableName: kubeConfigFilePathVariableName
});
context.onNewInputComponentCreated(kubeConfigFilePathVariableName, filePicker.input);
const getClusterContexts = async () => {
try { try {
let currentClusterContext = ''; let currentClusterContext = '';
const clusterContexts: string[] = (await getKubeConfigClusterContexts(filePicker.input.value!)).map(kubeClusterContext => { const clusterContexts: string[] = (await getKubeConfigClusterContexts(file)).map(kubeClusterContext => {
if (kubeClusterContext.isCurrentContext) { if (kubeClusterContext.isCurrentContext) {
currentClusterContext = kubeClusterContext.name; currentClusterContext = kubeClusterContext.name;
} }
@@ -551,76 +656,78 @@ async function processKubeConfigClusterPickerField(context: KubeClusterContextFi
throw Error(loc.clusterContextNotFound); throw Error(loc.clusterContextNotFound);
} }
return { values: clusterContexts, defaultValue: currentClusterContext }; return { values: clusterContexts, defaultValue: currentClusterContext };
} catch (e) { }
throw Error(localize('kubeConfigClusterPicker.errorLoadingClusters', "An error ocurred while loading or parsing the config file:{0}, error is:{1}", filePicker.input.value, getErrorMessage(e))); catch (e) {
throw Error(localize('getClusterContexts.errorFetchingClusters', "An error ocurred while loading or parsing the config file:{0}, error is:{1}", file, getErrorMessage(e)));
} }
}; };
createRadioOptions(context, getClusterContexts)
.then(clusterContextOptions => {
filePicker.input.onTextChanged(async () => {
await loadOrReloadRadioOptions(context, clusterContextOptions.optionsList, clusterContextOptions.loader, getClusterContexts);
});
}).catch(error => {
console.log(`failed to create radio options, Error: ${error}`);
});
} }
async function processRadioOptionsTypeField(context: FieldContext): Promise<RadioOptionsInputs> { /**
* A Kube Config Cluster picker field consists of a file system file picker and radio button selector for cluster contexts defined in the config filed picked using the file picker.
* @param context The context to use to create the field
*/
async function processKubeConfigClusterPickerField(context: KubeClusterContextFieldContext): Promise<void> {
const kubeConfigFilePathVariableName = context.fieldInfo.configFileVariableName || 'AZDATA_NB_VAR_KUBECONFIG_PATH';
const filePickerContext: FieldContext = {
container: context.container,
inputComponents: context.inputComponents,
components: context.components,
view: context.view,
onNewValidatorCreated: context.onNewValidatorCreated,
onNewDisposableCreated: context.onNewDisposableCreated,
onNewInputComponentCreated: context.onNewInputComponentCreated,
fieldInfo: {
label: loc.kubeConfigFilePath,
type: FieldType.FilePicker,
defaultValue: getDefaultKubeConfigPath(),
inputWidth: context.fieldInfo.inputWidth,
labelWidth: context.fieldInfo.labelWidth,
variableName: kubeConfigFilePathVariableName,
required: true
}
};
const filePicker = processFilePickerField(filePickerContext);
context.fieldInfo.subFields = context.fieldInfo.subFields || [];
context.fieldInfo.subFields.push({
label: filePickerContext.fieldInfo.label,
variableName: kubeConfigFilePathVariableName
});
const radioOptionsGroup = await createRadioOptions(context, getClusterContexts(filePicker.input.value!));
context.onNewDisposableCreated(filePicker.input.onTextChanged(async () =>
await radioOptionsGroup.loadOptions(getClusterContexts(filePicker.input.value!))
));
}
async function processRadioOptionsTypeField(context: FieldContext): Promise<RadioGroupLoadingComponentBuilder> {
return await createRadioOptions(context); return await createRadioOptions(context);
} }
async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: (() => Promise<{ values: string[] | azdata.CategoryValue[], defaultValue: string }>))
: Promise<RadioOptionsInputs> { async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: (() => Promise<OptionsInfo>))
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); : Promise<RadioGroupLoadingComponentBuilder> {
const optionsList = context.view!.modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: false }).component(); if (context.fieldInfo.fieldAlignItems === undefined) {
const radioOptionsLoadingComponent = context.view!.modelBuilder.loadingComponent().withItem(optionsList).component(); context.fieldInfo.fieldAlignItems = 'flex-start'; // by default align the items to the top.
addLabelInputPairToContainer(context.view, context.components, label, radioOptionsLoadingComponent, LabelPosition.Left); }
await loadOrReloadRadioOptions(context, optionsList, radioOptionsLoadingComponent, getRadioButtonInfo); const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
return { optionsList: optionsList, loader: radioOptionsLoadingComponent }; const radioGroupLoadingComponentBuilder = new RadioGroupLoadingComponentBuilder(context.view, context.onNewDisposableCreated);
context.fieldInfo.labelPosition = LabelPosition.Left;
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: radioGroupLoadingComponentBuilder });
addLabelInputPairToContainer(context.view, context.components, label, radioGroupLoadingComponentBuilder.component(), context.fieldInfo);
const options = context.fieldInfo.options as OptionsInfo;
await radioGroupLoadingComponentBuilder.loadOptions(
getRadioButtonInfo || options); // wait for the radioGroup to be fully initialized
return radioGroupLoadingComponentBuilder;
} }
async function loadOrReloadRadioOptions(context: FieldContext, optionsList: azdata.DivContainer, radioOptionsLoadingComponent: azdata.LoadingComponent, getRadioButtonInfo: (() => Promise<{ values: string[] | azdata.CategoryValue[]; defaultValue: string; }>) | undefined): Promise<void> {
radioOptionsLoadingComponent.loading = true;
optionsList.clearItems();
let options: (string[] | azdata.CategoryValue[]) = context.fieldInfo.options!;
let defaultValue: string = context.fieldInfo.defaultValue!;
try {
if (getRadioButtonInfo) {
const radioButtonsInfo = await getRadioButtonInfo();
options = radioButtonsInfo.values;
defaultValue = radioButtonsInfo.defaultValue;
}
options.forEach((op: string | azdata.CategoryValue) => {
const option: azdata.CategoryValue = (typeof op === 'string') ? { name: op, displayName: op } : op as azdata.CategoryValue;
const radioOption = context.view!.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
label: option.displayName,
checked: option.displayName === defaultValue,
name: option.name,
}).component();
if (radioOption.checked) {
context.onNewInputComponentCreated(context.fieldInfo.variableName!, radioOption);
}
context.onNewDisposableCreated(radioOption.onDidClick(() => {
// reset checked status of all remaining radioButtons
optionsList.items.filter(otherOption => otherOption !== radioOption).forEach(otherOption => (otherOption as azdata.RadioButtonComponent).checked = false);
context.onNewInputComponentCreated(context.fieldInfo.variableName!, radioOption!);
}));
optionsList.addItem(radioOption);
});
}
catch (e) {
const errorLoadingRadioOptionsLabel = context.view!.modelBuilder.text().withProperties({ value: getErrorMessage(e) }).component();
optionsList.addItem(errorLoadingRadioOptionsLabel);
}
radioOptionsLoadingComponent.loading = false;
}
/** /**
* An Azure Account field consists of 3 separate dropdown fields - Account, Subscription and Resource Group * An Azure Account field consists of 3 separate dropdown fields - Account, Subscription and Resource Group
* @param context The context to use to create the field * @param context The context to use to create the field
*/ */
function processAzureAccountField(context: AzureAccountFieldContext): void { async function processAzureAccountField(context: AzureAccountFieldContext): Promise<void> {
context.fieldInfo.subFields = []; context.fieldInfo.subFields = [];
const accountValueToAccountMap = new Map<string, azdata.Account>(); const accountValueToAccountMap = new Map<string, azdata.Account>();
const subscriptionValueToSubscriptionMap = new Map<string, azureResource.AzureResourceSubscription>(); const subscriptionValueToSubscriptionMap = new Map<string, azureResource.AzureResourceSubscription>();
@@ -628,11 +735,12 @@ function processAzureAccountField(context: AzureAccountFieldContext): void {
const subscriptionDropdown = createAzureSubscriptionDropdown(context, subscriptionValueToSubscriptionMap); const subscriptionDropdown = createAzureSubscriptionDropdown(context, subscriptionValueToSubscriptionMap);
const resourceGroupDropdown = createAzureResourceGroupsDropdown(context, accountDropdown, accountValueToAccountMap, subscriptionDropdown, subscriptionValueToSubscriptionMap); const resourceGroupDropdown = createAzureResourceGroupsDropdown(context, accountDropdown, accountValueToAccountMap, subscriptionDropdown, subscriptionValueToSubscriptionMap);
const locationDropdown = context.fieldInfo.locations && processAzureLocationsField(context); const locationDropdown = context.fieldInfo.locations && processAzureLocationsField(context);
accountDropdown.onValueChanged(selectedItem => { accountDropdown.onValueChanged(async selectedItem => {
const selectedAccount = accountValueToAccountMap.get(selectedItem.selected)!; const selectedAccount = accountValueToAccountMap.get(selectedItem.selected)!;
handleSelectedAccountChanged(context, selectedAccount, subscriptionDropdown, subscriptionValueToSubscriptionMap, resourceGroupDropdown, locationDropdown); await handleSelectedAccountChanged(context, selectedAccount, subscriptionDropdown, subscriptionValueToSubscriptionMap, resourceGroupDropdown, locationDropdown);
}); });
azdata.accounts.getAllAccounts().then((accounts: azdata.Account[]) => { try {
const accounts = await azdata.accounts.getAllAccounts();
// Append a blank value for the "default" option if the field isn't required, context will clear all the dropdowns when selected // Append a blank value for the "default" option if the field isn't required, context will clear all the dropdowns when selected
const dropdownValues = context.fieldInfo.required ? [] : ['']; const dropdownValues = context.fieldInfo.required ? [] : [''];
accountDropdown.values = dropdownValues.concat(accounts.map(account => { accountDropdown.values = dropdownValues.concat(accounts.map(account => {
@@ -641,8 +749,10 @@ function processAzureAccountField(context: AzureAccountFieldContext): void {
return displayName; return displayName;
})); }));
const selectedAccount = accountDropdown.value ? accountValueToAccountMap.get(accountDropdown.value.toString()) : undefined; const selectedAccount = accountDropdown.value ? accountValueToAccountMap.get(accountDropdown.value.toString()) : undefined;
handleSelectedAccountChanged(context, selectedAccount, subscriptionDropdown, subscriptionValueToSubscriptionMap, resourceGroupDropdown, locationDropdown); await handleSelectedAccountChanged(context, selectedAccount, subscriptionDropdown, subscriptionValueToSubscriptionMap, resourceGroupDropdown, locationDropdown);
}, (err: any) => console.log(`Unexpected error fetching accounts: ${err}`)); } catch (error) {
vscode.window.showErrorMessage(localize('azure.accounts.unexpectedAccountsError', 'Unexpected error fetching accounts: ${0}', getErrorMessage(error)));
}
} }
function createAzureAccountDropdown(context: AzureAccountFieldContext): azdata.DropDownComponent { function createAzureAccountDropdown(context: AzureAccountFieldContext): azdata.DropDownComponent {
@@ -651,7 +761,7 @@ function createAzureAccountDropdown(context: AzureAccountFieldContext): azdata.D
description: context.fieldInfo.description, description: context.fieldInfo.description,
required: context.fieldInfo.required, required: context.fieldInfo.required,
width: context.fieldInfo.labelWidth, width: context.fieldInfo.labelWidth,
fontWeight: context.fieldInfo.labelFontWeight cssStyles: context.fieldInfo.labelCSSStyles
}); });
const accountDropdown = createDropdown(context.view, { const accountDropdown = createDropdown(context.view, {
width: context.fieldInfo.inputWidth, width: context.fieldInfo.inputWidth,
@@ -659,8 +769,9 @@ function createAzureAccountDropdown(context: AzureAccountFieldContext): azdata.D
required: context.fieldInfo.required, required: context.fieldInfo.required,
label: loc.account label: loc.account
}); });
context.onNewInputComponentCreated(context.fieldInfo.variableName!, accountDropdown); accountDropdown.fireOnTextChange = true;
addLabelInputPairToContainer(context.view, context.components, label, accountDropdown, context.fieldInfo.labelPosition); context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: accountDropdown });
addLabelInputPairToContainer(context.view, context.components, label, accountDropdown, context.fieldInfo);
return accountDropdown; return accountDropdown;
} }
@@ -671,36 +782,48 @@ function createAzureSubscriptionDropdown(
text: loc.subscription, text: loc.subscription,
required: context.fieldInfo.required, required: context.fieldInfo.required,
width: context.fieldInfo.labelWidth, width: context.fieldInfo.labelWidth,
fontWeight: context.fieldInfo.labelFontWeight cssStyles: context.fieldInfo.labelCSSStyles
}); });
const subscriptionDropdown = createDropdown(context.view, { const subscriptionDropdown = createDropdown(context.view, {
defaultValue: (context.fieldInfo.required) ? undefined : '',
width: context.fieldInfo.inputWidth, width: context.fieldInfo.inputWidth,
editable: false, editable: false,
required: context.fieldInfo.required, required: context.fieldInfo.required,
label: loc.subscription label: loc.subscription
}); });
subscriptionDropdown.fireOnTextChange = true;
context.fieldInfo.subFields!.push({ context.fieldInfo.subFields!.push({
label: label.value!, label: label.value!,
variableName: context.fieldInfo.subscriptionVariableName variableName: context.fieldInfo.subscriptionVariableName
}); });
context.onNewInputComponentCreated(context.fieldInfo.subscriptionVariableName!, subscriptionDropdown, (inputValue: string) => { context.onNewInputComponentCreated(context.fieldInfo.subscriptionVariableName!, {
return subscriptionValueToSubscriptionMap.get(inputValue)?.id || inputValue; component: subscriptionDropdown,
inputValueTransformer: (inputValue: string) => {
return subscriptionValueToSubscriptionMap.get(inputValue)?.id || inputValue;
}
}); });
addLabelInputPairToContainer(context.view, context.components, label, subscriptionDropdown, context.fieldInfo.labelPosition); if (context.fieldInfo.displaySubscriptionVariableName) {
context.fieldInfo.subFields!.push({
label: label.value!,
variableName: context.fieldInfo.displaySubscriptionVariableName
});
context.onNewInputComponentCreated(context.fieldInfo.displaySubscriptionVariableName, { component: subscriptionDropdown });
}
addLabelInputPairToContainer(context.view, context.components, label, subscriptionDropdown, context.fieldInfo);
return subscriptionDropdown; return subscriptionDropdown;
} }
function handleSelectedAccountChanged( async function handleSelectedAccountChanged(
context: AzureAccountFieldContext, context: AzureAccountFieldContext,
selectedAccount: azdata.Account | undefined, selectedAccount: azdata.Account | undefined,
subscriptionDropdown: azdata.DropDownComponent, subscriptionDropdown: azdata.DropDownComponent,
subscriptionValueToSubscriptionMap: Map<string, azureResource.AzureResourceSubscription>, subscriptionValueToSubscriptionMap: Map<string, azureResource.AzureResourceSubscription>,
resourceGroupDropdown: azdata.DropDownComponent, resourceGroupDropdown: azdata.DropDownComponent,
locationDropdown?: azdata.DropDownComponent locationDropdown?: azdata.DropDownComponent
): void { ): Promise<void> {
subscriptionValueToSubscriptionMap.clear(); subscriptionValueToSubscriptionMap.clear();
subscriptionDropdown.values = []; subscriptionDropdown.values = [];
handleSelectedSubscriptionChanged(context, selectedAccount, undefined, resourceGroupDropdown); await handleSelectedSubscriptionChanged(context, selectedAccount, undefined, resourceGroupDropdown);
if (!selectedAccount) { if (!selectedAccount) {
subscriptionDropdown.values = ['']; subscriptionDropdown.values = [''];
if (locationDropdown) { if (locationDropdown) {
@@ -715,7 +838,8 @@ function handleSelectedAccountChanged(
} }
} }
vscode.commands.executeCommand<azurecore.GetSubscriptionsResult>('azure.accounts.getSubscriptions', selectedAccount, true /*ignoreErrors*/).then(response => { try {
const response = await vscode.commands.executeCommand<azurecore.GetSubscriptionsResult>('azure.accounts.getSubscriptions', selectedAccount, true /*ignoreErrors*/);
if (!response) { if (!response) {
return; return;
} }
@@ -732,8 +856,10 @@ function handleSelectedAccountChanged(
return displayName; return displayName;
}).sort((a: string, b: string) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase())); }).sort((a: string, b: string) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()));
const selectedSubscription = subscriptionDropdown.values.length > 0 ? subscriptionValueToSubscriptionMap.get(subscriptionDropdown.values[0]) : undefined; const selectedSubscription = subscriptionDropdown.values.length > 0 ? subscriptionValueToSubscriptionMap.get(subscriptionDropdown.values[0]) : undefined;
handleSelectedSubscriptionChanged(context, selectedAccount, selectedSubscription, resourceGroupDropdown); await handleSelectedSubscriptionChanged(context, selectedAccount, selectedSubscription, resourceGroupDropdown);
}, err => { vscode.window.showErrorMessage(localize('azure.accounts.unexpectedSubscriptionsError', "Unexpected error fetching subscriptions for account {0} ({1}): {2}", selectedAccount?.displayInfo.displayName, selectedAccount?.key.accountId, err.message)); }); } catch (error) {
vscode.window.showErrorMessage(localize('azure.accounts.unexpectedSubscriptionsError', "Unexpected error fetching subscriptions for account {0} ({1}): {2}", selectedAccount?.displayInfo.displayName, selectedAccount?.key.accountId, getErrorMessage(error)));
}
} }
function createAzureResourceGroupsDropdown( function createAzureResourceGroupsDropdown(
@@ -746,35 +872,42 @@ function createAzureResourceGroupsDropdown(
text: loc.resourceGroup, text: loc.resourceGroup,
required: context.fieldInfo.required, required: context.fieldInfo.required,
width: context.fieldInfo.labelWidth, width: context.fieldInfo.labelWidth,
fontWeight: context.fieldInfo.labelFontWeight cssStyles: context.fieldInfo.labelCSSStyles
}); });
const resourceGroupDropdown = createDropdown(context.view, { const resourceGroupDropdown = createDropdown(context.view, {
defaultValue: (context.fieldInfo.required) ? undefined : '',
width: context.fieldInfo.inputWidth, width: context.fieldInfo.inputWidth,
editable: false, editable: false,
required: context.fieldInfo.required, required: context.fieldInfo.required,
label: loc.resourceGroup label: loc.resourceGroup
}); });
resourceGroupDropdown.fireOnTextChange = true;
context.fieldInfo.subFields!.push({ context.fieldInfo.subFields!.push({
label: label.value!, label: label.value!,
variableName: context.fieldInfo.resourceGroupVariableName variableName: context.fieldInfo.resourceGroupVariableName
}); });
context.onNewInputComponentCreated(context.fieldInfo.resourceGroupVariableName!, resourceGroupDropdown); const rgValueChangedEmitter = new vscode.EventEmitter<void>();
addLabelInputPairToContainer(context.view, context.components, label, resourceGroupDropdown, context.fieldInfo.labelPosition); resourceGroupDropdown.onValueChanged(() => rgValueChangedEmitter.fire());
subscriptionDropdown.onValueChanged(selectedItem => { context.onNewInputComponentCreated(context.fieldInfo.resourceGroupVariableName!, { component: resourceGroupDropdown });
addLabelInputPairToContainer(context.view, context.components, label, resourceGroupDropdown, context.fieldInfo);
subscriptionDropdown.onValueChanged(async selectedItem => {
const selectedAccount = !accountDropdown || !accountDropdown.value ? undefined : accountValueToAccountMap.get(accountDropdown.value.toString()); const selectedAccount = !accountDropdown || !accountDropdown.value ? undefined : accountValueToAccountMap.get(accountDropdown.value.toString());
const selectedSubscription = subscriptionValueToSubscriptionMap.get(selectedItem.selected); const selectedSubscription = subscriptionValueToSubscriptionMap.get(selectedItem.selected);
handleSelectedSubscriptionChanged(context, selectedAccount, selectedSubscription, resourceGroupDropdown); await handleSelectedSubscriptionChanged(context, selectedAccount, selectedSubscription, resourceGroupDropdown);
rgValueChangedEmitter.fire();
}); });
return resourceGroupDropdown; return resourceGroupDropdown;
} }
function handleSelectedSubscriptionChanged(context: AzureAccountFieldContext, selectedAccount: azdata.Account | undefined, selectedSubscription: azureResource.AzureResourceSubscription | undefined, resourceGroupDropdown: azdata.DropDownComponent): void { async function handleSelectedSubscriptionChanged(context: AzureAccountFieldContext, selectedAccount: azdata.Account | undefined, selectedSubscription: azureResource.AzureResourceSubscription | undefined, resourceGroupDropdown: azdata.DropDownComponent): Promise<void> {
resourceGroupDropdown.values = ['']; resourceGroupDropdown.values = [''];
if (!selectedAccount || !selectedSubscription) { if (!selectedAccount || !selectedSubscription) {
// Don't need to execute command if we don't have both an account and subscription selected // Don't need to execute command if we don't have both an account and subscription selected
return; return;
} }
vscode.commands.executeCommand<azurecore.GetResourceGroupsResult>('azure.accounts.getResourceGroups', selectedAccount, selectedSubscription, true /*ignoreErrors*/).then(response => { try {
const response = await vscode.commands.executeCommand<azurecore.GetResourceGroupsResult>('azure.accounts.getResourceGroups', selectedAccount, selectedSubscription, true /*ignoreErrors*/);
//.then(response => {
if (!response) { if (!response) {
return; return;
} }
@@ -788,7 +921,9 @@ function handleSelectedSubscriptionChanged(context: AzureAccountFieldContext, se
resourceGroupDropdown.values = (response.resourceGroups.length !== 0) resourceGroupDropdown.values = (response.resourceGroups.length !== 0)
? response.resourceGroups.map(resourceGroup => resourceGroup.name).sort((a: string, b: string) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase())) ? response.resourceGroups.map(resourceGroup => resourceGroup.name).sort((a: string, b: string) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()))
: ['']; : [''];
}, err => { vscode.window.showErrorMessage(localize('azure.accounts.unexpectedResourceGroupsError', "Unexpected error fetching resource groups for subscription {0} ({1}): {2}", selectedSubscription?.name, selectedSubscription?.id, err.message)); }); } catch (error) {
vscode.window.showErrorMessage(localize('azure.accounts.unexpectedResourceGroupsError', "Unexpected error fetching resource groups for subscription {0} ({1}): {2}", selectedSubscription?.name, selectedSubscription?.id, getErrorMessage(error)));
}
} }
/** /**
@@ -809,7 +944,7 @@ function processAzureLocationsField(context: AzureLocationsFieldContext): azdata
text: context.fieldInfo.label || loc.location, text: context.fieldInfo.label || loc.location,
required: context.fieldInfo.required, required: context.fieldInfo.required,
width: context.fieldInfo.labelWidth, width: context.fieldInfo.labelWidth,
fontWeight: context.fieldInfo.labelFontWeight cssStyles: context.fieldInfo.labelCSSStyles
}); });
const locationDropdown = createDropdown(context.view, { const locationDropdown = createDropdown(context.view, {
width: context.fieldInfo.inputWidth, width: context.fieldInfo.inputWidth,
@@ -818,14 +953,18 @@ function processAzureLocationsField(context: AzureLocationsFieldContext): azdata
label: loc.location, label: loc.location,
values: context.fieldInfo.locations values: context.fieldInfo.locations
}); });
locationDropdown.fireOnTextChange = true;
context.fieldInfo.subFields = context.fieldInfo.subFields || []; context.fieldInfo.subFields = context.fieldInfo.subFields || [];
if (context.fieldInfo.locationVariableName) { if (context.fieldInfo.locationVariableName) {
context.fieldInfo.subFields!.push({ context.fieldInfo.subFields!.push({
label: label.value!, label: label.value!,
variableName: context.fieldInfo.locationVariableName variableName: context.fieldInfo.locationVariableName
}); });
context.onNewInputComponentCreated(context.fieldInfo.locationVariableName, locationDropdown, (inputValue: string) => { context.onNewInputComponentCreated(context.fieldInfo.locationVariableName, {
return knownAzureLocationNameMappings.get(inputValue) || inputValue; component: locationDropdown,
inputValueTransformer: (inputValue: string) => {
return knownAzureLocationNameMappings.get(inputValue) || inputValue;
}
}); });
} }
if (context.fieldInfo.displayLocationVariableName) { if (context.fieldInfo.displayLocationVariableName) {
@@ -833,12 +972,15 @@ function processAzureLocationsField(context: AzureLocationsFieldContext): azdata
label: label.value!, label: label.value!,
variableName: context.fieldInfo.displayLocationVariableName variableName: context.fieldInfo.displayLocationVariableName
}); });
context.onNewInputComponentCreated(context.fieldInfo.displayLocationVariableName, locationDropdown); context.onNewInputComponentCreated(context.fieldInfo.displayLocationVariableName, { component: locationDropdown });
} }
context.onNewInputComponentCreated(context.fieldInfo.variableName!, locationDropdown, (inputValue: string) => { context.onNewInputComponentCreated(context.fieldInfo.variableName!, {
return knownAzureLocationNameMappings.get(inputValue) || inputValue; component: locationDropdown,
inputValueTransformer: (inputValue: string) => {
return knownAzureLocationNameMappings.get(inputValue) || inputValue;
}
}); });
addLabelInputPairToContainer(context.view, context.components, label, locationDropdown, context.fieldInfo.labelPosition); addLabelInputPairToContainer(context.view, context.components, label, locationDropdown, context.fieldInfo);
return locationDropdown; return locationDropdown;
} }
@@ -871,31 +1013,38 @@ export function getPasswordMismatchMessage(fieldName: string): string {
export function setModelValues(inputComponents: InputComponents, model: Model): void { export function setModelValues(inputComponents: InputComponents, model: Model): void {
Object.keys(inputComponents).forEach(key => { Object.keys(inputComponents).forEach(key => {
let value; const value = getInputComponentValue(inputComponents, key);
const input = inputComponents[key].component;
if ('name' in input && 'checked' in input) { //RadioButtonComponent
value = input.name;
} else if ('checked' in input) { // CheckBoxComponent
value = input.checked ? 'true' : 'false';
} else if ('value' in input) { // InputBoxComponent or DropDownComponent
const inputValue = input.value;
if (typeof inputValue === 'string' || typeof inputValue === 'undefined') {
value = inputValue;
} else {
value = inputValue.name;
}
} else {
throw new Error(`Unknown input type with ID ${input.id}`);
}
const inputValueTransformer = inputComponents[key].inputValueTransformer;
if (inputValueTransformer) {
value = inputValueTransformer(value || '');
}
model.setPropertyValue(key, value); model.setPropertyValue(key, value);
}); });
} }
function getInputComponentValue(inputComponents: InputComponents, key: string): string | undefined {
const input = inputComponents[key].component;
if (input === undefined) {
return undefined;
}
let value;
if (input instanceof RadioGroupLoadingComponentBuilder) {
value = input.value;
} else if ('checked' in input) { // CheckBoxComponent
value = input.checked ? 'true' : 'false';
} else if ('value' in input) { // InputBoxComponent or DropDownComponent
const inputValue = input.value;
if (typeof inputValue === 'string' || typeof inputValue === 'undefined') {
value = inputValue;
} else {
value = inputValue.name;
}
} else {
throw new Error(`Unknown input type with ID ${input.id}`);
}
const inputValueTransformer = inputComponents[key].inputValueTransformer;
if (inputValueTransformer) {
value = inputValueTransformer(value || '');
}
return value;
}
export function isInputBoxEmpty(input: azdata.InputBoxComponent): boolean { export function isInputBoxEmpty(input: azdata.InputBoxComponent): boolean {
return input.value === undefined || input.value === ''; return input.value === undefined || input.value === '';
} }

View File

@@ -4,18 +4,20 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { INotebookService } from '../../services/notebookService'; import { INotebookService, Notebook } from '../../services/notebookService';
import { IToolsService } from '../../services/toolsService';
import { Model } from '../model'; import { Model } from '../model';
import { InputComponents, setModelValues } from '../modelViewUtils';
import { WizardBase } from '../wizardBase'; import { WizardBase } from '../wizardBase';
import { WizardPageBase } from '../wizardPageBase';
import { DeploymentType, NotebookWizardInfo } from './../../interfaces'; import { DeploymentType, NotebookWizardInfo } from './../../interfaces';
import { IPlatformService } from './../../services/platformService'; import { IPlatformService } from './../../services/platformService';
import { NotebookWizardAutoSummaryPage } from './notebookWizardAutoSummaryPage';
import { NotebookWizardPage } from './notebookWizardPage'; import { NotebookWizardPage } from './notebookWizardPage';
import { NotebookWizardSummaryPage } from './notebookWizardSummaryPage';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
export class NotebookWizard extends WizardBase<NotebookWizard, Model> { export class NotebookWizard extends WizardBase<NotebookWizard, NotebookWizardPage, Model> {
private _inputComponents: InputComponents = {};
public get notebookService(): INotebookService { public get notebookService(): INotebookService {
return this._notebookService; return this._notebookService;
@@ -29,8 +31,15 @@ export class NotebookWizard extends WizardBase<NotebookWizard, Model> {
return this._wizardInfo; return this._wizardInfo;
} }
constructor(private _wizardInfo: NotebookWizardInfo, private _notebookService: INotebookService, private _platformService: IPlatformService) { public get inputComponents(): InputComponents {
return this._inputComponents;
}
constructor(private _wizardInfo: NotebookWizardInfo, private _notebookService: INotebookService, private _platformService: IPlatformService, private _toolsService: IToolsService) {
super(_wizardInfo.title, new Model()); super(_wizardInfo.title, new Model());
if (this._wizardInfo.codeCellInsertionPosition === undefined) {
this._wizardInfo.codeCellInsertionPosition = 0;
}
this.wizardObject.doneButton.label = _wizardInfo.actionText || this.wizardObject.doneButton.label; this.wizardObject.doneButton.label = _wizardInfo.actionText || this.wizardObject.doneButton.label;
} }
@@ -41,31 +50,63 @@ export class NotebookWizard extends WizardBase<NotebookWizard, Model> {
protected initialize(): void { protected initialize(): void {
this.setPages(this.getPages()); this.setPages(this.getPages());
this.wizardObject.generateScriptButton.hidden = true; this.wizardObject.generateScriptButton.hidden = true;
this.wizardInfo.actionText = this.wizardInfo.actionText || localize('deployCluster.ScriptToNotebook', "Script to Notebook"); this.wizardInfo.actionText = this.wizardInfo.actionText || localize('notebookWizard.ScriptToNotebook', "Script to Notebook");
this.wizardObject.doneButton.label = this.wizardInfo.actionText; this.wizardObject.doneButton.label = this.wizardInfo.actionText;
} }
protected onCancel(): void { protected onCancel(): void {
} }
protected onOk(): void { protected async onOk(): Promise<void> {
this.model.setEnvironmentVariables(); setModelValues(this.inputComponents, this.model);
if (this.wizardInfo.runNotebook) { const env: NodeJS.ProcessEnv = {};
this.notebookService.backgroundExecuteNotebook(this.wizardInfo.taskName, this.wizardInfo.notebook, 'deploy', this.platformService); this.model.setEnvironmentVariables(env, (varName) => {
} else { const isPassword = !!this.inputComponents[varName]?.isPassword;
this.notebookService.launchNotebook(this.wizardInfo.notebook).then(() => { }, (error) => { return isPassword;
vscode.window.showErrorMessage(error); });
}); const notebook: Notebook = await this.notebookService.getNotebook(this.wizardInfo.notebook);
// generate python code statements for all variables captured by the wizard
const statements = this.model.getCodeCellContentForNotebook(
this._toolsService.toolsForCurrentProvider,
(varName) => {
const isPassword = !!this.inputComponents[varName]?.isPassword;
return !isPassword;
}
);
// insert generated code statements into the notebook.
notebook.cells.splice(
this.wizardInfo.codeCellInsertionPosition ?? 0,
0,
{
cell_type: 'code',
source: statements,
metadata: {},
outputs: [],
execution_count: 0
}
);
try {
if (this.wizardInfo.runNotebook) {
this.notebookService.backgroundExecuteNotebook(this.wizardInfo.taskName, notebook, 'deploy', this.platformService, env);
} else {
Object.assign(process.env, env);
const notebookPath = this.notebookService.getNotebookPath(this.wizardInfo.notebook);
await this.notebookService.launchNotebookWithContent(notebookPath, JSON.stringify(notebook, undefined, 4));
}
} catch (error) {
vscode.window.showErrorMessage(error);
} }
} }
private getPages(): WizardPageBase<NotebookWizard>[] { private getPages(): NotebookWizardPage[] {
const pages: WizardPageBase<NotebookWizard>[] = []; const pages: NotebookWizardPage[] = [];
for (let pageIndex: number = 0; pageIndex < this.wizardInfo.pages.length; pageIndex++) { for (let pageIndex: number = 0; pageIndex < this.wizardInfo.pages.length; pageIndex++) {
pages.push(new NotebookWizardPage(this, pageIndex)); if (this.wizardInfo.pages[pageIndex].isSummaryPage && this.wizardInfo.isSummaryPageAutoGenerated) {
} // If we are auto-generating the summary page
if (this.wizardInfo.generateSummaryPage) { pages.push(new NotebookWizardAutoSummaryPage(this, pageIndex));
pages.push(new NotebookWizardSummaryPage(this)); } else {
pages.push(new NotebookWizardPage(this, pageIndex));
}
} }
return pages; return pages;
} }

View File

@@ -6,19 +6,23 @@ import * as azdata from 'azdata';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { SubFieldInfo, FieldType, FontWeight, LabelPosition, SectionInfo } from '../../interfaces'; import { SubFieldInfo, FieldType, FontWeight, LabelPosition, SectionInfo } from '../../interfaces';
import { createSection, DefaultInputComponentWidth, DefaultLabelComponentWidth } from '../modelViewUtils'; import { createSection, DefaultInputWidth, DefaultLabelWidth, DefaultFieldAlignItems, DefaultFieldWidth, DefaultFieldHeight } from '../modelViewUtils';
import { WizardPageBase } from '../wizardPageBase';
import { NotebookWizard } from './notebookWizard'; import { NotebookWizard } from './notebookWizard';
import { NotebookWizardPage } from './notebookWizardPage';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
export class NotebookWizardSummaryPage extends WizardPageBase<NotebookWizard> { export class NotebookWizardAutoSummaryPage extends NotebookWizardPage {
private formItems: azdata.FormComponent[] = []; private formItems: azdata.FormComponent[] = [];
private form!: azdata.FormBuilder; private form!: azdata.FormBuilder;
private view!: azdata.ModelView; private view!: azdata.ModelView;
constructor(wizard: NotebookWizard) { constructor(wizard: NotebookWizard, _pageIndex: number) {
super(localize('notebookWizard.summaryPageTitle', "Review your configuration"), '', wizard); super(wizard,
_pageIndex,
wizard.wizardInfo.pages[_pageIndex].title || localize('notebookWizard.autoSummaryPageTitle', "Review your configuration"),
wizard.wizardInfo.pages[_pageIndex].description || ''
);
} }
public initialize(): void { public initialize(): void {
@@ -29,25 +33,31 @@ export class NotebookWizardSummaryPage extends WizardPageBase<NotebookWizard> {
}); });
} }
public onLeave() { public onLeave(): void {
this.wizard.wizardObject.message = { text: '' }; this.wizard.wizardObject.message = { text: '' };
} }
public onEnter() { public onEnter(): void {
this.formItems.forEach(item => { this.formItems.forEach(item => {
this.form!.removeFormItem(item); this.form!.removeFormItem(item);
}); });
this.formItems = []; this.formItems = [];
const inputWidth = this.wizard.wizardInfo.inputWidth || (this.wizard.wizardInfo.summaryPage && this.wizard.wizardInfo.summaryPage.inputWidth) || DefaultInputComponentWidth; const fieldWidth = this.pageInfo.fieldWidth || this.wizard.wizardInfo.fieldWidth || DefaultFieldWidth;
const labelWidth = this.wizard.wizardInfo.labelWidth || (this.wizard.wizardInfo.summaryPage && this.wizard.wizardInfo.summaryPage.labelWidth) || DefaultLabelComponentWidth; const fieldHeight = this.pageInfo.fieldHeight || this.wizard.wizardInfo.fieldHeight || DefaultFieldHeight;
const labelPosition = this.wizard.wizardInfo.labelPosition || (this.wizard.wizardInfo.summaryPage && this.wizard.wizardInfo.summaryPage.labelPosition) || LabelPosition.Left; const fieldAlignItems = this.pageInfo.fieldAlignItems || this.wizard.wizardInfo.fieldAlignItems || DefaultFieldAlignItems;
const labelWidth = this.pageInfo.labelWidth || this.wizard.wizardInfo.labelWidth || DefaultLabelWidth;
const labelPosition = this.pageInfo.labelPosition || this.wizard.wizardInfo.labelPosition || LabelPosition.Left;
const inputWidth = this.pageInfo.inputWidth || this.wizard.wizardInfo.inputWidth || DefaultInputWidth;
this.wizard.wizardInfo.pages.forEach(pageInfo => { this.wizard.wizardInfo.pages.filter((undefined, index) => index < this._pageIndex).forEach(pageInfo => {
const summarySectionInfo: SectionInfo = { const summarySectionInfo: SectionInfo = {
labelPosition: labelPosition, labelPosition: labelPosition,
labelWidth: labelWidth, labelWidth: labelWidth,
inputWidth: inputWidth, inputWidth: inputWidth,
fieldWidth: fieldWidth,
fieldHeight: fieldHeight,
fieldAlignItems: fieldAlignItems,
title: '', title: '',
rows: [] rows: []
}; };
@@ -68,6 +78,7 @@ export class NotebookWizardSummaryPage extends WizardPageBase<NotebookWizard> {
title: pageInfo.title, title: pageInfo.title,
component: createSection({ component: createSection({
container: this.wizard.wizardObject, container: this.wizard.wizardObject,
inputComponents: this.wizard.inputComponents,
sectionInfo: summarySectionInfo, sectionInfo: summarySectionInfo,
view: this.view, view: this.view,
onNewDisposableCreated: () => { }, onNewDisposableCreated: () => { },
@@ -84,11 +95,11 @@ export class NotebookWizardSummaryPage extends WizardPageBase<NotebookWizard> {
private addSummaryForVariable(summarySectionInfo: SectionInfo, fieldInfo: SubFieldInfo) { private addSummaryForVariable(summarySectionInfo: SectionInfo, fieldInfo: SubFieldInfo) {
summarySectionInfo!.rows!.push({ summarySectionInfo!.rows!.push({
fields: [{ items: [{
type: FieldType.ReadonlyText, type: FieldType.ReadonlyText,
label: fieldInfo.label, label: fieldInfo.label,
defaultValue: this.wizard.model.getStringValue(fieldInfo.variableName!), defaultValue: this.wizard.model.getStringValue(fieldInfo.variableName!),
labelFontWeight: FontWeight.Bold labelCSSStyles: { fontWeight: FontWeight.Bold }
}] }]
}); });
} }

View File

@@ -7,58 +7,73 @@ import { EOL } from 'os';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { NotebookWizardPageInfo } from '../../interfaces'; import { NotebookWizardPageInfo } from '../../interfaces';
import { initializeWizardPage, InputComponents, InputComponent, setModelValues, Validator } from '../modelViewUtils'; import { initializeWizardPage, InputComponentInfo, setModelValues, Validator } from '../modelViewUtils';
import { WizardPageBase } from '../wizardPageBase'; import { WizardPageBase } from '../wizardPageBase';
import { NotebookWizard } from './notebookWizard'; import { NotebookWizard } from './notebookWizard';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
export class NotebookWizardPage extends WizardPageBase<NotebookWizard> { export class NotebookWizardPage extends WizardPageBase<NotebookWizard> {
private inputComponents: InputComponents = {};
protected get pageInfo(): NotebookWizardPageInfo { protected get pageInfo(): NotebookWizardPageInfo {
return this.wizard.wizardInfo.pages[this._pageIndex]; return this.wizard.wizardInfo.pages[this._pageIndex];
} }
constructor(wizard: NotebookWizard, private _pageIndex: number) { constructor(
super(wizard.wizardInfo.pages[_pageIndex].title, wizard.wizardInfo.pages[_pageIndex].description || '', wizard); wizard: NotebookWizard,
protected _pageIndex: number,
title?: string,
description?: string
) {
super(
wizard.wizardInfo.pages[_pageIndex].title || title || '',
wizard.wizardInfo.pages[_pageIndex].description || description || '',
wizard
);
} }
public initialize(): void { public initialize(): void {
const self = this;
initializeWizardPage({ initializeWizardPage({
container: this.wizard.wizardObject, container: this.wizard.wizardObject,
inputComponents: this.wizard.inputComponents,
wizardInfo: this.wizard.wizardInfo, wizardInfo: this.wizard.wizardInfo,
pageInfo: this.pageInfo, pageInfo: this.pageInfo,
page: this.pageObject, page: this.pageObject,
onNewDisposableCreated: (disposable: vscode.Disposable): void => { onNewDisposableCreated: (disposable: vscode.Disposable): void => {
self.wizard.registerDisposable(disposable); this.wizard.registerDisposable(disposable);
}, },
onNewInputComponentCreated: (name: string, component: InputComponent): void => { onNewInputComponentCreated: (
self.inputComponents[name] = { component: component }; name: string,
inputComponentInfo: InputComponentInfo
): void => {
if (name) {
this.wizard.inputComponents[name] = inputComponentInfo;
}
}, },
onNewValidatorCreated: (validator: Validator): void => { onNewValidatorCreated: (validator: Validator): void => {
self.validators.push(validator); this.validators.push(validator);
} },
}); });
} }
public onLeave(): void {
public onLeave() {
setModelValues(this.inputComponents, this.wizard.model);
// The following callback registration clears previous navigation validators. // The following callback registration clears previous navigation validators.
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true; return true;
}); });
} }
public onEnter() { public onEnter(): void {
if (this.pageInfo.isSummaryPage) {
setModelValues(this.wizard.inputComponents, this.wizard.model);
}
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
this.wizard.wizardObject.message = { text: '' }; this.wizard.wizardObject.message = { text: '' };
if (pcInfo.newPage > pcInfo.lastPage) { if (pcInfo.newPage > pcInfo.lastPage) {
const messages: string[] = []; const messages: string[] = [];
this.validators.forEach(validator => { this.validators.forEach((validator) => {
const result = validator(); const result = validator();
if (!result.valid) { if (!result.valid) {
messages.push(result.message); messages.push(result.message);
@@ -67,9 +82,15 @@ export class NotebookWizardPage extends WizardPageBase<NotebookWizard> {
if (messages.length > 0) { if (messages.length > 0) {
this.wizard.wizardObject.message = { this.wizard.wizardObject.message = {
text: messages.length === 1 ? messages[0] : localize('wizardPage.ValidationError', "There are some errors on this page, click 'Show Details' to view the errors."), text:
messages.length === 1
? messages[0]
: localize(
"wizardPage.ValidationError",
"There are some errors on this page, click 'Show Details' to view the errors."
),
description: messages.length === 1 ? undefined : messages.join(EOL), description: messages.length === 1 ? undefined : messages.join(EOL),
level: azdata.window.MessageLevel.Error level: azdata.window.MessageLevel.Error,
}; };
} }
return messages.length === 0; return messages.length === 0;

View File

@@ -0,0 +1,82 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { OptionsInfo } from '../interfaces';
import { getErrorMessage } from '../utils';
export class RadioGroupLoadingComponentBuilder implements azdata.ComponentBuilder<azdata.LoadingComponent> {
private _optionsDivContainer!: azdata.DivContainer;
private _optionsLoadingBuilder: azdata.LoadingComponentBuilder;
private _onValueChangedEmitter: vscode.EventEmitter<void> = new vscode.EventEmitter();
private _currentRadioOption!: azdata.RadioButtonComponent;
constructor(private _view: azdata.ModelView, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void) {
this._optionsDivContainer = this._view!.modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: false }).component();
this._optionsLoadingBuilder = this._view!.modelBuilder.loadingComponent().withItem(this._optionsDivContainer);
}
component(): azdata.LoadingComponent {
return this._optionsLoadingBuilder.component();
}
withProperties<U>(properties: U): azdata.ComponentBuilder<azdata.LoadingComponent> {
return this._optionsLoadingBuilder.withProperties(properties);
}
withValidation(validation: (component: azdata.LoadingComponent) => boolean): azdata.ComponentBuilder<azdata.LoadingComponent> {
return this._optionsLoadingBuilder.withValidation(validation);
}
async loadOptions(optionsInfo: OptionsInfo | (() => Promise<OptionsInfo>)): Promise<void> {
if (typeof optionsInfo !== 'object') {
optionsInfo = await optionsInfo();
}
this.component().loading = true;
this._optionsDivContainer.clearItems();
let options: (string[] | azdata.CategoryValue[]) = optionsInfo.values!;
let defaultValue: string = optionsInfo.defaultValue!;
try {
options.forEach((op: string | azdata.CategoryValue) => {
const option: azdata.CategoryValue = (typeof op === 'string')
? { name: op, displayName: op }
: op as azdata.CategoryValue;
const radioOption = this._view!.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
label: option.displayName,
checked: option.displayName === defaultValue,
name: option.name,
}).component();
if (radioOption.checked) {
this._currentRadioOption = radioOption;
this._onValueChangedEmitter.fire();
}
this._onNewDisposableCreated(radioOption.onDidClick(() => {
this._optionsDivContainer.items
.filter(otherOption => otherOption !== radioOption)
.forEach(otherOption => (otherOption as azdata.RadioButtonComponent).checked = false);
this._currentRadioOption = radioOption;
this._onValueChangedEmitter.fire();
}));
this._optionsDivContainer.addItem(radioOption);
});
}
catch (e) {
const errorLoadingRadioOptionsLabel = this._view!.modelBuilder.text().withProperties({ value: getErrorMessage(e), CSSStyles: { 'color': 'Red' } }).component();
this._optionsDivContainer.addItem(errorLoadingRadioOptionsLabel);
}
this.component().loading = false;
}
get value(): string | undefined {
return this._currentRadioOption?.label;
}
get checked(): azdata.RadioButtonComponent {
return this._currentRadioOption;
}
get onValueChanged(): vscode.Event<void> {
return this._onValueChangedEmitter.event;
}
}

View File

@@ -8,7 +8,7 @@ import * as nls from 'vscode-nls';
import { AgreementInfo, DeploymentProvider, ITool, ResourceType, ToolStatus } from '../interfaces'; import { AgreementInfo, DeploymentProvider, ITool, ResourceType, ToolStatus } from '../interfaces';
import { IResourceTypeService } from '../services/resourceTypeService'; import { IResourceTypeService } from '../services/resourceTypeService';
import { IToolsService } from '../services/toolsService'; import { IToolsService } from '../services/toolsService';
import { getErrorMessage, setEnvironmentVariablesForInstallPaths } from '../utils'; import { getErrorMessage } from '../utils';
import { DialogBase } from './dialogBase'; import { DialogBase } from './dialogBase';
import { createFlexContainer } from './modelViewUtils'; import { createFlexContainer } from './modelViewUtils';
@@ -337,7 +337,7 @@ export class ResourceTypePickerDialog extends DialogBase {
} }
protected onComplete(): void { protected onComplete(): void {
setEnvironmentVariablesForInstallPaths(this._tools); this.toolsService.toolsForCurrentProvider = this._tools;
this.resourceTypeService.startDeployment(this.getCurrentProvider()); this.resourceTypeService.startDeployment(this.getCurrentProvider());
} }

View File

@@ -10,9 +10,9 @@ import { WizardPageBase } from './wizardPageBase';
import { Model } from './model'; import { Model } from './model';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
export abstract class WizardBase<T, M extends Model> { export abstract class WizardBase<T, P extends WizardPageBase<T>, M extends Model> {
private customButtons: azdata.window.Button[] = []; private customButtons: azdata.window.Button[] = [];
private pages: WizardPageBase<T>[] = []; public pages: P[] = [];
public wizardObject: azdata.window.Wizard; public wizardObject: azdata.window.Wizard;
public toDispose: vscode.Disposable[] = []; public toDispose: vscode.Disposable[] = [];
@@ -34,8 +34,8 @@ export abstract class WizardBase<T, M extends Model> {
newPage.onEnter(); newPage.onEnter();
})); }));
this.toDispose.push(this.wizardObject.doneButton.onClick(() => { this.toDispose.push(this.wizardObject.doneButton.onClick(async () => {
this.onOk(); await this.onOk();
this.dispose(); this.dispose();
})); }));
this.toDispose.push(this.wizardObject.cancelButton.onClick(() => { this.toDispose.push(this.wizardObject.cancelButton.onClick(() => {
@@ -52,14 +52,14 @@ export abstract class WizardBase<T, M extends Model> {
} }
protected abstract initialize(): void; protected abstract initialize(): void;
protected abstract onOk(): void; protected abstract async onOk(): Promise<void>;
protected abstract onCancel(): void; protected abstract onCancel(): void;
public addButton(button: azdata.window.Button) { public addButton(button: azdata.window.Button) {
this.customButtons.push(button); this.customButtons.push(button);
} }
protected setPages(pages: WizardPageBase<T>[]) { protected setPages(pages: P[]) {
this.wizardObject!.pages = pages.map(p => p.pageObject); this.wizardObject!.pages = pages.map(p => p.pageObject);
this.pages = pages; this.pages = pages;
this.pages.forEach((page) => { this.pages.forEach((page) => {

View File

@@ -2,9 +2,9 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { ITool, NoteBookEnvironmentVariablePrefix } from './interfaces';
import * as path from 'path'; import * as path from 'path';
import { ToolsInstallPath } from './constants'; import { ToolsInstallPath } from './constants';
import { ITool, NoteBookEnvironmentVariablePrefix } from './interfaces';
export function getErrorMessage(error: any): string { export function getErrorMessage(error: any): string {
return (error instanceof Error) return (error instanceof Error)
@@ -21,7 +21,7 @@ export function getRuntimeBinaryPathEnvironmentVariableName(toolName: string): s
return `${NoteBookEnvironmentVariablePrefix}${toolName.toUpperCase().replace(/ |-/g, '_')}`; return `${NoteBookEnvironmentVariablePrefix}${toolName.toUpperCase().replace(/ |-/g, '_')}`;
} }
export function setEnvironmentVariablesForInstallPaths(tools: ITool[]): void { export function setEnvironmentVariablesForInstallPaths(tools: ITool[], env: NodeJS.ProcessEnv = process.env): void {
// Use Set class to make sure the collection only contains unique values. // Use Set class to make sure the collection only contains unique values.
let installationPaths: Set<string> = new Set<string>(); let installationPaths: Set<string> = new Set<string>();
tools.forEach(t => { tools.forEach(t => {
@@ -30,12 +30,18 @@ export function setEnvironmentVariablesForInstallPaths(tools: ITool[]): void {
// construct an env variable name with NoteBookEnvironmentVariablePrefix prefix // construct an env variable name with NoteBookEnvironmentVariablePrefix prefix
// and tool.name as suffix, making sure of using all uppercase characters and only _ as separator // and tool.name as suffix, making sure of using all uppercase characters and only _ as separator
const envVarName = getRuntimeBinaryPathEnvironmentVariableName(t.name); const envVarName = getRuntimeBinaryPathEnvironmentVariableName(t.name);
process.env[envVarName] = t.installationPathOrAdditionalInformation; env[envVarName] = t.installationPathOrAdditionalInformation;
installationPaths.add(path.dirname(t.installationPathOrAdditionalInformation)); installationPaths.add(path.dirname(t.installationPathOrAdditionalInformation));
} }
}); });
if (installationPaths.size > 0) { if (installationPaths.size > 0) {
const envVarToolsInstallationPath: string = [...installationPaths.values()].join(path.delimiter); const envVarToolsInstallationPath: string = [...installationPaths.values()].join(path.delimiter);
process.env[ToolsInstallPath] = envVarToolsInstallationPath; env[ToolsInstallPath] = envVarToolsInstallationPath;
}
}
export function assert(condition: boolean, message?: string): asserts condition {
if (!condition) {
throw new Error(message);
} }
} }

View File

@@ -766,9 +766,10 @@ vscode-nls@^4.0.0:
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c"
integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A== integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A==
"vscodetestcover@github:corivera/vscodetestcover#1.0.6": vscodetestcover@^1.0.9:
version "1.0.5" version "1.0.9"
resolved "https://codeload.github.com/corivera/vscodetestcover/tar.gz/14e0f2c46346b31bc1af2c590febeaf69a9112eb" resolved "https://registry.yarnpkg.com/vscodetestcover/-/vscodetestcover-1.0.9.tgz#0191f403dd59ba1153fc57979e281e992ce63731"
integrity sha512-8z2961KF9Tuz5XdHAC6RMV3CrzAoUcfIK7wLYjLIXD4dbHIT7ceZMhoxToW1olyi3pFnThlS4lRXtx8Q5iyMMQ==
dependencies: dependencies:
decache "^4.4.0" decache "^4.4.0"
glob "^7.1.2" glob "^7.1.2"

View File

@@ -6,10 +6,10 @@
"**/node_modules/**", "**/node_modules/**",
"**/test/**" "**/test/**"
], ],
"includePid": false,
"reports": [ "reports": [
"cobertura", "cobertura",
"lcov" "lcov",
"json"
], ],
"verbose": false, "verbose": false,
"remapOptions": { "remapOptions": {

View File

@@ -75,7 +75,7 @@
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",
"should": "^13.2.1", "should": "^13.2.1",
"typemoq": "^2.1.0", "typemoq": "^2.1.0",
"vscodetestcover": "github:corivera/vscodetestcover#1.0.6" "vscodetestcover": "^1.0.9"
}, },
"__metadata": { "__metadata": {
"id": "37", "id": "37",

View File

@@ -808,9 +808,10 @@ vscode-nls@^4.0.0:
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c"
integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A== integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A==
"vscodetestcover@github:corivera/vscodetestcover#1.0.6": vscodetestcover@^1.0.9:
version "1.0.5" version "1.0.9"
resolved "https://codeload.github.com/corivera/vscodetestcover/tar.gz/14e0f2c46346b31bc1af2c590febeaf69a9112eb" resolved "https://registry.yarnpkg.com/vscodetestcover/-/vscodetestcover-1.0.9.tgz#0191f403dd59ba1153fc57979e281e992ce63731"
integrity sha512-8z2961KF9Tuz5XdHAC6RMV3CrzAoUcfIK7wLYjLIXD4dbHIT7ceZMhoxToW1olyi3pFnThlS4lRXtx8Q5iyMMQ==
dependencies: dependencies:
decache "^4.4.0" decache "^4.4.0"
glob "^7.1.2" glob "^7.1.2"

View File

@@ -6,10 +6,10 @@
"**/node_modules/**", "**/node_modules/**",
"**/test/**" "**/test/**"
], ],
"includePid": false,
"reports": [ "reports": [
"cobertura", "cobertura",
"lcov" "lcov",
"json"
], ],
"verbose": false, "verbose": false,
"remapOptions": { "remapOptions": {

View File

@@ -269,6 +269,6 @@
"tslint": "^5.8.0", "tslint": "^5.8.0",
"typemoq": "^2.1.0", "typemoq": "^2.1.0",
"typescript": "^2.6.1", "typescript": "^2.6.1",
"vscodetestcover": "github:corivera/vscodetestcover#1.0.6" "vscodetestcover": "^1.0.9"
} }
} }

View File

@@ -141,7 +141,7 @@ export class Project {
return fileEntry; return fileEntry;
} }
private createProjectEntry(relativePath: string, entryType: EntryType): ProjectEntry { public createProjectEntry(relativePath: string, entryType: EntryType): ProjectEntry {
return new ProjectEntry(Uri.file(path.join(this.projectFolderPath, relativePath)), relativePath, entryType); return new ProjectEntry(Uri.file(path.join(this.projectFolderPath, relativePath)), relativePath, entryType);
} }

View File

@@ -22,7 +22,7 @@ export class FolderNode extends BaseProjectTreeItem {
} }
public get children(): BaseProjectTreeItem[] { public get children(): BaseProjectTreeItem[] {
return Object.values(this.fileChildren).sort(); return Object.values(this.fileChildren).sort(sortFileFolderNodes);
} }
public get treeItem(): vscode.TreeItem { public get treeItem(): vscode.TreeItem {
@@ -64,6 +64,23 @@ export class FileNode extends BaseProjectTreeItem {
} }
} }
/**
* Compares two folder/file tree nodes so that folders come before files, then alphabetically
* @param a a folder or file tree node
* @param b another folder or file tree node
*/
export function sortFileFolderNodes(a: (FolderNode | FileNode), b: (FolderNode | FileNode)): number {
if (a instanceof FolderNode && !(b instanceof FolderNode)) {
return -1;
}
else if (!(a instanceof FolderNode) && b instanceof FolderNode) {
return 1;
}
else {
return a.uri.fsPath.localeCompare(b.uri.fsPath);
}
}
/** /**
* Converts a full filesystem URI to a project-relative URI that's compatible with the project tree * Converts a full filesystem URI to a project-relative URI that's compatible with the project tree
*/ */
@@ -75,8 +92,7 @@ function fsPathToProjectUri(fileSystemUri: vscode.Uri, projectNode: ProjectRootT
localUri = fileSystemUri.fsPath.substring(projBaseDir.length); localUri = fileSystemUri.fsPath.substring(projBaseDir.length);
} }
else { else {
vscode.window.showErrorMessage('Project pointing to file outside of directory'); throw new Error(`Project (${projBaseDir}) pointing to file outside of directory (${fileSystemUri.fsPath})`);
throw new Error('Project pointing to file outside of directory');
} }
return vscode.Uri.file(path.join(projectNode.uri.path, localUri)); return vscode.Uri.file(path.join(projectNode.uri.path, localUri));

View File

@@ -32,14 +32,7 @@ export class ProjectRootTreeItem extends BaseProjectTreeItem {
const output: BaseProjectTreeItem[] = []; const output: BaseProjectTreeItem[] = [];
output.push(this.dataSourceNode); output.push(this.dataSourceNode);
// sort children so that folders come first, then alphabetical return output.concat(Object.values(this.fileChildren).sort(fileTree.sortFileFolderNodes));
const sortedChildren = Object.values(this.fileChildren).sort((a: (fileTree.FolderNode | fileTree.FileNode), b: (fileTree.FolderNode | fileTree.FileNode)) => {
if (a instanceof fileTree.FolderNode && !(b instanceof fileTree.FolderNode)) { return -1; }
else if (!(a instanceof fileTree.FolderNode) && b instanceof fileTree.FolderNode) { return 1; }
else { return a.uri.fsPath.localeCompare(b.uri.fsPath); }
});
return output.concat(sortedChildren);
} }
public get treeItem(): vscode.TreeItem { public get treeItem(): vscode.TreeItem {
@@ -53,6 +46,10 @@ export class ProjectRootTreeItem extends BaseProjectTreeItem {
for (const entry of this.project.files) { for (const entry of this.project.files) {
const parentNode = this.getEntryParentNode(entry); const parentNode = this.getEntryParentNode(entry);
if (Object.keys(parentNode.fileChildren).includes(path.basename(entry.fsUri.path))) {
continue; // ignore duplicate entries
}
let newNode: fileTree.FolderNode | fileTree.FileNode; let newNode: fileTree.FolderNode | fileTree.FileNode;
switch (entry.type) { switch (entry.type) {

View File

@@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as vscode from 'vscode';
import * as os from 'os';
import * as path from 'path';
import { Project, EntryType } from '../models/project';
import { FolderNode, FileNode, sortFileFolderNodes } from '../models/tree/fileFolderTreeItem';
import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
describe('Project Tree tests', function (): void {
it('Should correctly order tree nodes by type, then by name', async function (): Promise<void> {
const root = os.platform() === 'win32' ? 'Z:\\' : '/';
const parent = new ProjectRootTreeItem(new Project(vscode.Uri.file(`${root}Fake.sqlproj`).fsPath));
let inputNodes: (FileNode | FolderNode)[] = [
new FileNode(vscode.Uri.file(`${root}C`), parent),
new FileNode(vscode.Uri.file(`${root}D`), parent),
new FolderNode(vscode.Uri.file(`${root}Z`), parent),
new FolderNode(vscode.Uri.file(`${root}X`), parent),
new FileNode(vscode.Uri.file(`${root}B`), parent),
new FileNode(vscode.Uri.file(`${root}A`), parent),
new FolderNode(vscode.Uri.file(`${root}W`), parent),
new FolderNode(vscode.Uri.file(`${root}Y`), parent)
];
inputNodes = inputNodes.sort(sortFileFolderNodes);
const expectedNodes: (FileNode | FolderNode)[] = [
new FolderNode(vscode.Uri.file(`${root}W`), parent),
new FolderNode(vscode.Uri.file(`${root}X`), parent),
new FolderNode(vscode.Uri.file(`${root}Y`), parent),
new FolderNode(vscode.Uri.file(`${root}Z`), parent),
new FileNode(vscode.Uri.file(`${root}A`), parent),
new FileNode(vscode.Uri.file(`${root}B`), parent),
new FileNode(vscode.Uri.file(`${root}C`), parent),
new FileNode(vscode.Uri.file(`${root}D`), parent)
];
should(inputNodes.map(n => n.uri.path)).deepEqual(expectedNodes.map(n => n.uri.path));
});
it('Should build tree from Project file correctly', async function (): Promise<void> {
const root = os.platform() === 'win32' ? 'Z:\\' : '/';
const proj = new Project(vscode.Uri.file(`${root}TestProj.sqlproj`).fsPath);
// nested entries before explicit top-level folder entry
// also, ordering of files/folders at all levels
proj.files.push(proj.createProjectEntry(path.join('someFolder', 'bNestedTest.sql'), EntryType.File));
proj.files.push(proj.createProjectEntry(path.join('someFolder', 'bNestedFolder'), EntryType.Folder));
proj.files.push(proj.createProjectEntry(path.join('someFolder', 'aNestedTest.sql'), EntryType.File));
proj.files.push(proj.createProjectEntry(path.join('someFolder', 'aNestedFolder'), EntryType.Folder));
proj.files.push(proj.createProjectEntry('someFolder', EntryType.Folder));
// duplicate files
proj.files.push(proj.createProjectEntry('duplicate.sql', EntryType.File));
proj.files.push(proj.createProjectEntry('duplicate.sql', EntryType.File));
// duplicate folders
proj.files.push(proj.createProjectEntry('duplicateFolder', EntryType.Folder));
proj.files.push(proj.createProjectEntry('duplicateFolder', EntryType.Folder));
const tree = new ProjectRootTreeItem(proj);
should(tree.children.map(x => x.uri.path)).deepEqual([
'/TestProj.sqlproj/Data Sources',
'/TestProj.sqlproj/duplicateFolder',
'/TestProj.sqlproj/someFolder',
'/TestProj.sqlproj/duplicate.sql']);
should(tree.children.find(x => x.uri.path === '/TestProj.sqlproj/someFolder')?.children.map(y => y.uri.path)).deepEqual([
'/TestProj.sqlproj/someFolder/aNestedFolder',
'/TestProj.sqlproj/someFolder/bNestedFolder',
'/TestProj.sqlproj/someFolder/aNestedTest.sql',
'/TestProj.sqlproj/someFolder/bNestedTest.sql']);
});
});

View File

@@ -901,9 +901,10 @@ vscode-nls@^3.2.1:
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4"
integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw== integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw==
"vscodetestcover@github:corivera/vscodetestcover#1.0.6": vscodetestcover@^1.0.9:
version "1.0.5" version "1.0.9"
resolved "https://codeload.github.com/corivera/vscodetestcover/tar.gz/14e0f2c46346b31bc1af2c590febeaf69a9112eb" resolved "https://registry.yarnpkg.com/vscodetestcover/-/vscodetestcover-1.0.9.tgz#0191f403dd59ba1153fc57979e281e992ce63731"
integrity sha512-8z2961KF9Tuz5XdHAC6RMV3CrzAoUcfIK7wLYjLIXD4dbHIT7ceZMhoxToW1olyi3pFnThlS4lRXtx8Q5iyMMQ==
dependencies: dependencies:
decache "^4.4.0" decache "^4.4.0"
glob "^7.1.2" glob "^7.1.2"

View File

@@ -10,7 +10,7 @@
"aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e", "aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e",
"engines": { "engines": {
"vscode": "*", "vscode": "*",
"azdata": ">1.10.0" "azdata": ">=1.19.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -23,6 +23,257 @@
], ],
"contributes": { "contributes": {
"resourceDeploymentTypes": [ "resourceDeploymentTypes": [
{
"name": "test-wizard",
"displayName": "%resource.type.wizard.display.name%",
"description": "%resource.type.wizard.description%",
"platforms": "*",
"icon": {
"light": "./images/book.svg",
"dark": "./images/book_inverse.svg"
},
"providers": [
{
"notebookWizard": {
"notebook": "%deployment-notebook-1%",
"type": "new-arc-control-plane",
"runNotebook": false,
"codeCellInsertionPosition": 1,
"actionText": "%deploy.wizard.action%",
"title": "%wizard.new.wizard.title%",
"name": "wizard.new.wizard",
"labelPosition": "left",
"generateSummaryPage": false,
"pages": [
{
"title": "%wizard.select.cluster.title%",
"sections": [
{
"fields": [
{
"type": "kube_cluster_context_picker",
"label": "%wizard.kube.cluster.context%",
"required": true,
"inputWidth": "350px",
"variableName": "AZDATA_NB_VAR_CLUSTER_CONTEXT",
"configFileVariableName": "AZDATA_NB_VAR_CONFIG_FILE"
}
]
}
]
},
{
"title": "%wizard.cluster.config.profile.title%",
"sections": [
{
"fields": [
{
"type": "readonly_text",
"label": "%wizard.project.details.description%",
"labelWidth": "600px"
},
{
"type": "options",
"label": "%wizard.cluster.config.profile%",
"required": true,
"variableName": "AZDATA_NB_VAR_PROFILE",
"editable": false,
"options": {
"values":[
"aks-dev-test",
"aks-dev-test-ha",
"aks-dev-test",
"aks-private-preview",
"kubeadm-dev-test",
"kubeadm-private-preview"
],
"defaultValue": "aks-dev-test",
"optionsType": "radio"
}
},
{
"label": "%wizard.dropdown.options.field%",
"variableName": "AZDATA_NB_VAR_DROPDOWN_OPTIONS",
"type": "options",
"options": {
"values": ["1","2","3"],
"defaultValue": "2",
"optionsType": "dropdown"
}
}
]
}
]
},
{
"title": "%wizard.data.controller.create.summary.title%",
"isSummaryPage": true,
"fieldHeight": "16px",
"sections": [
{
"title": "",
"collapsible": false,
"fieldWidth": "200px",
"fieldHeight": "12px",
"spaceBetweenFields": 0,
"rows": [
{
"items": [
{
"items": [
{
"label": "%wizard.summary.data.controller%",
"type": "readonly_text",
"enabled": true,
"labelWidth": "185px"
}
]
},
{
"items": [
{
"label": "%wizard.summary.estimated.cost.per.month%",
"type": "readonly_text",
"enabled": true,
"labelWidth": "190px",
"labelCSSStyles": {
"fontWeight": "Bold"
}
}
]
}
]
},
{
"items": [
{
"items": [
{
"label": "%wizard.summary.by.contoso%",
"type": "readonly_text",
"labelWidth": "185px"
}
]
},
{
"items": [
{
"label": "%wizard.summary.free%",
"type": "readonly_text",
"enabled": true,
"defaultValue": "",
"labelWidth": "100px"
}
]
}
]
},
{
"items": [
{
"items": [
{
"label": "{0}",
"type": "readonly_text",
"enabled": true,
"labelCSSStyles": { "color": "#0078D4" },
"labelWidth": "67px",
"links": [
{
"text": "%wizard.summary.terms.of.use%",
"url": "https://aka.ms/eula-azdata-en"
}
]
},
{
"label": "|",
"type": "readonly_text",
"enabled": true,
"defaultValue": "",
"labelWidth": "4px",
"fieldWidth": "6px"
},
{
"label": "{0}",
"type": "readonly_text",
"enabled": true,
"labelCSSStyles": { "color": "#0078D4" },
"labelWidth": "102px",
"links": [
{
"text": "%wizard.summary.terms.privacy.policy%",
"url": "https://go.contoso.com/fwlink/?LinkId=853010"
}
]
}
]
}
]
}
]
},
{
"title": "%wizard.summary.terms%",
"fieldHeight": "88px",
"fields":[
{
"label": "%wizard.summary.terms.description%",
"type": "readonly_text",
"enabled": true,
"labelWidth": "750px"
}
]
},
{
"title": "%wizard.summary.kubernetes%",
"fields":[
{
"label": "%wizard.summary.kube.config.file.path%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_CONFIG_FILE)"
},
{
"label": "%wizard.summary.cluster.context%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_CLUSTER_CONTEXT)"
},
{
"label": "%wizard.summary.profile%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_PROFILE)"
}
]
}
]
}
]
},
"requiredTools": [
{
"name": "kubectl"
}
],
"when": true
}
],
"agreement": {
"template": "%wizard.data.controller.agreement%",
"links": [
{
"text": "%contoso.agreement.privacy.statement%",
"url": "https://go.contoso.com/fwlink/?LinkId=853010"
},
{
"text": "%wizard.agreement.contosoCmd.eula%",
"url": "https://aka.ms/eula-contosoCmd-en"
}
]
}
},
{ {
"name": "x-data-service", "name": "x-data-service",
"displayName": "%resource-type-display-name%", "displayName": "%resource-type-display-name%",
@@ -81,6 +332,14 @@
"defaultValue": "", "defaultValue": "",
"required": true "required": true
}, },
{
"type": "kube_cluster_context_picker",
"label": "%kube.cluster.context%",
"required": true,
"inputWidth": "350px",
"variableName": "AZDATA_NB_VAR_CLUSTER_CONTEXT",
"configFileVariableName": "AZDATA_NB_VAR_CONFIG_FILE"
},
{ {
"label": "%number-field%", "label": "%number-field%",
"variableName": "AZDATA_NB_VAR_NUMBER", "variableName": "AZDATA_NB_VAR_NUMBER",
@@ -159,7 +418,7 @@
"links": [ "links": [
{ {
"text": "%agreement-1-name%", "text": "%agreement-1-name%",
"url": "https://www.microsoft.com" "url": "https://www.contoso.com"
}, },
{ {
"text": "%agreement-2-name%", "text": "%agreement-2-name%",

View File

@@ -12,9 +12,41 @@
"deployment-notebook-2": "./notebooks/deploy-x-data-service-2.ipynb", "deployment-notebook-2": "./notebooks/deploy-x-data-service-2.ipynb",
"text-field": "text field", "text-field": "text field",
"password-field": "password field", "password-field": "password field",
"kube.cluster.context": "Kube cluster context",
"number-field": "numeric field", "number-field": "numeric field",
"confirm-password": "confirm password", "confirm-password": "confirm password",
"agreement": "I accept {0} and {1}.", "agreement": "I accept {0} and {1}.",
"agreement-1-name": "Agreement 1", "agreement-1-name": "Agreement 1",
"agreement-2-name": "Agreement 2" "agreement-2-name": "Agreement 2",
"resource.type.wizard.display.name": "Test controller",
"resource.type.wizard.description": "Creates a Test controller",
"wizard.new.wizard.title": "Create Test controller",
"wizard.cluster.environment.title": "What is your target existing Kubernetes cluster environment?",
"wizard.select.cluster.title": "Select from installed existing Kubernetes clusters",
"wizard.kube.cluster.context": "Cluster context",
"wizard.cluster.config.profile.title": "Choose the config profile",
"wizard.cluster.config.profile": "Config profile",
"wizard.dropdown.options.field": "dropdown field",
"wizard.project.details.title": "Project details",
"wizard.project.details.description": "Project details for Contoso corporation",
"wizard.data.controller.create.summary.title": "Review your configuration",
"wizard.summary.data.controller": "Test controller",
"wizard.summary.estimated.cost.per.month": "Estimated cost per month",
"wizard.summary.by.contoso" : "by Contoso",
"wizard.summary.free" : "Free",
"wizard.summary.terms.of.use" : "Terms of use",
"wizard.summary.terms.privacy.policy" : "Privacy policy",
"wizard.summary.terms" : "Terms",
"wizard.summary.terms.description": "By clicking 'Script to notebook', I (a) agree to the legal terms and privacy statement(s) associated with the doing business with Contoso.",
"wizard.summary.kubernetes": "Kubernetes",
"wizard.summary.kube.config.file.path": "Kube config file path",
"wizard.summary.cluster.context": "Cluster context",
"wizard.summary.profile": "Config profile",
"wizard.data.controller.agreement": "I accept {0} and {1}.",
"contoso.agreement.privacy.statement":"contoso Privacy Statement",
"wizard.agreement.contosoCmd.eula":"contoso cmd license terms",
"deploy.wizard.action":"Script to notebook"
} }

View File

@@ -47,7 +47,7 @@ if "%SKIP_PYTHON_INSTALL_TEST%" == "1" (
) )
call %INTEGRATION_TEST_ELECTRON_PATH% --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 ^ call %INTEGRATION_TEST_ELECTRON_PATH% --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 ^
--extensionDevelopmentPath=%~dp0\..\extensions\integration-tests --extensionTestsPath=%~dp0\..\extensions\integration-tests\out\tests --disable-telemetry --disable-crash-reporter --disable-updates -nogpu --extensionDevelopmentPath=%~dp0\..\extensions\integration-tests --extensionTestsPath=%~dp0\..\extensions\integration-tests\out\test --disable-telemetry --disable-crash-reporter --disable-updates -nogpu
rmdir /s /q %VSCODEUSERDATADIR% rmdir /s /q %VSCODEUSERDATADIR%
rmdir /s /q %VSCODEEXTENSIONSDIR% rmdir /s /q %VSCODEEXTENSIONSDIR%

View File

@@ -91,10 +91,10 @@ REM echo *** starting mssql tests ***
REM echo ****************************************** REM echo ******************************************
REM call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\mssql --extensionTestsPath=%~dp0\..\extensions\mssql\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu REM call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\mssql --extensionTestsPath=%~dp0\..\extensions\mssql\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu
echo ******************************************** REM echo ********************************************
echo *** starting sql-database-projects tests *** REM echo *** starting sql-database-projects tests ***
echo ******************************************** REM echo ********************************************
call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\sql-database-projects --extensionTestsPath=%~dp0\..\extensions\sql-database-projects\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu REM call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\sql-database-projects --extensionTestsPath=%~dp0\..\extensions\sql-database-projects\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu
if %errorlevel% neq 0 exit /b %errorlevel% if %errorlevel% neq 0 exit /b %errorlevel%

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Artboard 10</title>
<g>
<path d="M4,6h7V3H4ZM5,4h5V5H5Z"/>
<path d="M3,0a1.732,1.732,0,0,0-.742.168,2.256,2.256,0,0,0-1.09,1.09A1.735,1.735,0,0,0,1,2V14a1.947,1.947,0,0,0,.156.777,2.018,2.018,0,0,0,1.067,1.067A1.947,1.947,0,0,0,3,16H14V0ZM3,15a.972.972,0,0,1-.391-.078,1.023,1.023,0,0,1-.531-.531,1.019,1.019,0,0,1,0-.782,1.024,1.024,0,0,1,.215-.316,1.012,1.012,0,0,1,.316-.215A.972.972,0,0,1,3,13H13v2Zm10-3H3a1.836,1.836,0,0,0-.523.074A2.194,2.194,0,0,0,2,12.281V2a.8.8,0,0,1,.09-.359,1.223,1.223,0,0,1,.23-.321,1.246,1.246,0,0,1,.321-.23A.792.792,0,0,1,3,1H13Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 684 B

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M4 6H11V3H4V6ZM5 4H10V5H5V4Z" fill="white"/>
<path d="M2.99998 0C2.74326 0.000315985 2.48981 0.0577002 2.25798 0.168C1.77794 0.392083 1.39207 0.777956 1.16798 1.258C1.05778 1.48986 1.0004 1.74328 0.999984 2V14C0.998165 14.267 1.05127 14.5314 1.15598 14.777C1.36029 15.2573 1.74272 15.6397 2.22298 15.844C2.46855 15.9487 2.73303 16.0018 2.99998 16H14V0H2.99998ZM2.99998 15C2.86566 15.0013 2.73253 14.9747 2.60898 14.922C2.37107 14.8187 2.18127 14.6289 2.07798 14.391C2.02649 14.2671 1.99998 14.1342 1.99998 14C1.99998 13.8658 2.02649 13.7329 2.07798 13.609C2.12918 13.4911 2.20212 13.3839 2.29298 13.293C2.38371 13.2019 2.49095 13.1289 2.60898 13.078C2.73253 13.0253 2.86566 12.9987 2.99998 13H13V15H2.99998ZM13 12H2.99998C2.823 11.9993 2.64684 12.0243 2.47698 12.074C2.31034 12.1238 2.15022 12.1933 1.99998 12.281V2C2.00151 1.87494 2.03233 1.75199 2.08998 1.641C2.14876 1.52225 2.22643 1.41383 2.31998 1.32C2.41408 1.22676 2.52244 1.14912 2.64098 1.09C2.75186 1.03207 2.87489 1.00123 2.99998 1H13V12Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -545,6 +545,13 @@ Includes non-masked style declarations. */
background-image: url("database_colored.svg"); background-image: url("database_colored.svg");
} }
.book.codicon {
-webkit-mask-image: url("book_image.svg");
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: 50% 50%;
}
.small { .small {
width: 16px; width: 16px;
height: 16px; height: 16px;

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Activity Bar */
.monaco-workbench .activitybar .monaco-action-bar .action-label.book {
background-color: rgba(255, 255, 255, 0.4);
}
/* Activity Bar */
.monaco-workbench .activitybar .monaco-action-bar .checked .action-label.book {
background-color: rgb(255, 255, 255);
}

View File

@@ -46,6 +46,9 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr
import { NotebookThemingContribution } from 'sql/workbench/contrib/notebook/browser/notebookThemingContribution'; import { NotebookThemingContribution } from 'sql/workbench/contrib/notebook/browser/notebookThemingContribution';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode'; import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode';
import { NotebookExplorerViewletViewsContribution, OpenNotebookExplorerViewletAction } from 'sql/workbench/contrib/notebook/browser/notebookExplorer/notebookExplorerViewlet';
import 'vs/css!./media/notebook.contribution';
Registry.as<IEditorInputFactoryRegistry>(EditorInputFactoryExtensions.EditorInputFactories) Registry.as<IEditorInputFactoryRegistry>(EditorInputFactoryExtensions.EditorInputFactories)
.registerEditorInputFactory(FileNotebookInput.ID, FileNoteBookEditorInputFactory); .registerEditorInputFactory(FileNotebookInput.ID, FileNoteBookEditorInputFactory);
@@ -348,3 +351,16 @@ registerComponentType({
selector: MimeRendererComponent.SELECTOR selector: MimeRendererComponent.SELECTOR
}); });
registerCellComponent(TextCellComponent); registerCellComponent(TextCellComponent);
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(NotebookExplorerViewletViewsContribution, LifecyclePhase.Starting);
const registry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionsExtensions.WorkbenchActions);
registry.registerWorkbenchAction(
SyncActionDescriptor.create(
OpenNotebookExplorerViewletAction,
OpenNotebookExplorerViewletAction.ID,
OpenNotebookExplorerViewletAction.LABEL,
{ primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_B }),
'View: Show Notebook Explorer',
localize('notebookExplorer.view', "View")
);

Some files were not shown because too many files have changed in this diff Show More