Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
887f4e8985 | ||
|
|
c91c4b01f9 | ||
|
|
5f198dba08 | ||
|
|
67f9a7f5e4 | ||
|
|
62404721ed | ||
|
|
6d37329e74 | ||
|
|
0c316d3225 | ||
|
|
131644477d | ||
|
|
b964dd0895 | ||
|
|
7dd32ed44b | ||
|
|
0b6aedfc93 | ||
|
|
b692088c94 | ||
|
|
160ab8d0ae | ||
|
|
a599cb436a | ||
|
|
294aa81298 | ||
|
|
ddc4b3dd6e | ||
|
|
4c5bf3ad2b | ||
|
|
88c33214c6 | ||
|
|
393be65aa6 | ||
|
|
9ce9a1598f | ||
|
|
d9079fe18e | ||
|
|
3cde070d3b | ||
|
|
b2a5f65a77 | ||
|
|
69dff5a2cb | ||
|
|
40e0d5cfbf | ||
|
|
5a0100757f | ||
|
|
f9fe88898d | ||
|
|
a2d6955f79 | ||
|
|
8fa247145e | ||
|
|
04bb65dcf7 | ||
|
|
e4884c7835 | ||
|
|
8b9ce3e8de | ||
|
|
327a5f5fae | ||
|
|
50b971477b | ||
|
|
07c7eea2df | ||
|
|
42135d3e53 | ||
|
|
d74e5e6457 | ||
|
|
a2c7377134 | ||
|
|
0e54393d5a | ||
|
|
8cf8cefc92 | ||
|
|
098c40e9ac | ||
|
|
80c1c4c6c8 | ||
|
|
ef8afab7e8 | ||
|
|
84e0e08aec | ||
|
|
2fce771214 | ||
|
|
15929e8cf2 | ||
|
|
f1c8ec141a | ||
|
|
a62393e0ed | ||
|
|
a6defd9b62 | ||
|
|
374212beaa | ||
|
|
5132e62045 | ||
|
|
9504ede1f3 | ||
|
|
afb6e6b5ba | ||
|
|
60b2b92803 | ||
|
|
6113311fda | ||
|
|
ecac6201d0 | ||
|
|
90d8c37f91 | ||
|
|
c43085beab | ||
|
|
d9c383b2ef | ||
|
|
100938b0e5 | ||
|
|
83a6ee0a22 | ||
|
|
0dab7f02ed | ||
|
|
0e6f2eb1cd | ||
|
|
9a371f8998 | ||
|
|
8a7bbd1795 | ||
|
|
d1fef24723 | ||
|
|
3ddc5e7846 | ||
|
|
b439ea45ec | ||
|
|
5680785f86 |
@@ -19,7 +19,12 @@ Linux DEB | https://go.microsoft.com/fwlink/?linkid=2050157
|
||||
|
||||
Go to our [download page](https://aka.ms/azuredatastudio) for more specific instructions.
|
||||
|
||||
Try out the latest insiders build from `master` at https://github.com/Microsoft/azuredatastudio/releases.
|
||||
Try out the latest insiders build from `master`:
|
||||
- [Windows User Installer - **Insiders build**](https://azuredatastudio-update.azurewebsites.net/latest/win32-x64-user/insider)
|
||||
- [Windows System Installer - **Insiders build**](https://azuredatastudio-update.azurewebsites.net/latest/win32-x64/insider)
|
||||
- [Windows ZIP - **Insiders build**](https://azuredatastudio-update.azurewebsites.net/latest/win32-x64-archive/insider)
|
||||
- [macOS ZIP - **Insiders build**](https://azuredatastudio-update.azurewebsites.net/latest/darwin/insider)
|
||||
- [Linux TAR.GZ - **Insiders build**](https://azuredatastudio-update.azurewebsites.net/latest/linux-x64/insider)
|
||||
|
||||
See the [change log](https://github.com/Microsoft/azuredatastudio/blob/master/CHANGELOG.md) for additional details of what's in this release.
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ const sqlBuiltInExtensions = [
|
||||
'import',
|
||||
'profiler'
|
||||
];
|
||||
var azureExtensions = [ 'azurecore'];
|
||||
var azureExtensions = [ 'azurecore', 'mssql'];
|
||||
|
||||
const vscodeEntryPoints = _.flatten([
|
||||
buildfile.entrypoint('vs/workbench/workbench.main'),
|
||||
@@ -280,13 +280,12 @@ function packageBuiltInExtensions() {
|
||||
});
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
function packageAzureCoreTask(platform, arch) {
|
||||
function packageExtensionTask(extensionName, platform, arch) {
|
||||
var destination = path.join(path.dirname(root), 'azuredatastudio') + (platform ? '-' + platform : '') + (arch ? '-' + arch : '');
|
||||
if (platform === 'darwin') {
|
||||
destination = path.join(destination, 'Azure Data Studio.app', 'Contents', 'Resources', 'app', 'extensions', 'azurecore');
|
||||
destination = path.join(destination, 'Azure Data Studio.app', 'Contents', 'Resources', 'app', 'extensions', extensionName);
|
||||
} else {
|
||||
destination = path.join(destination, 'resources', 'app', 'extensions', 'azurecore');
|
||||
destination = path.join(destination, 'resources', 'app', 'extensions', extensionName);
|
||||
}
|
||||
|
||||
platform = platform || process.platform;
|
||||
@@ -299,7 +298,7 @@ function packageAzureCoreTask(platform, arch) {
|
||||
const extensionName = path.basename(extensionPath);
|
||||
return { name: extensionName, path: extensionPath };
|
||||
})
|
||||
.filter(({ name }) => azureExtensions.indexOf(name) > -1);
|
||||
.filter(({ name }) => extensionName === name);
|
||||
|
||||
const localExtensions = es.merge(...localExtensionDescriptions.map(extension => {
|
||||
return ext.fromLocal(extension.path);
|
||||
@@ -500,9 +499,13 @@ function packageTask(platform, arch, opts) {
|
||||
const buildRoot = path.dirname(root);
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
gulp.task('vscode-win32-x64-azurecore', ['optimize-vscode'], packageAzureCoreTask('win32', 'x64'));
|
||||
gulp.task('vscode-darwin-azurecore', ['optimize-vscode'], packageAzureCoreTask('darwin'));
|
||||
gulp.task('vscode-linux-x64-azurecore', ['optimize-vscode'], packageAzureCoreTask('linux', 'x64'));
|
||||
gulp.task('vscode-win32-x64-azurecore', ['optimize-vscode'], packageExtensionTask('azurecore', 'win32', 'x64'));
|
||||
gulp.task('vscode-darwin-azurecore', ['optimize-vscode'], packageExtensionTask('azurecore', 'darwin'));
|
||||
gulp.task('vscode-linux-x64-azurecore', ['optimize-vscode'], packageExtensionTask('azurecore', 'linux', 'x64'));
|
||||
|
||||
gulp.task('vscode-win32-x64-mssql', ['vscode-linux-x64-azurecore', 'optimize-vscode'], packageExtensionTask('mssql', 'win32', 'x64'));
|
||||
gulp.task('vscode-darwin-mssql', ['vscode-linux-x64-azurecore', 'optimize-vscode'], packageExtensionTask('mssql', 'darwin'));
|
||||
gulp.task('vscode-linux-x64-mssql', ['vscode-linux-x64-azurecore', 'optimize-vscode'], packageExtensionTask('mssql', 'linux', 'x64'));
|
||||
|
||||
gulp.task('clean-vscode-win32-ia32', util.rimraf(path.join(buildRoot, 'azuredatastudio-win32-ia32')));
|
||||
gulp.task('clean-vscode-win32-x64', util.rimraf(path.join(buildRoot, 'azuredatastudio-win32-x64')));
|
||||
@@ -512,10 +515,10 @@ gulp.task('clean-vscode-linux-x64', util.rimraf(path.join(buildRoot, 'azuredatas
|
||||
gulp.task('clean-vscode-linux-arm', util.rimraf(path.join(buildRoot, 'azuredatastudio-linux-arm')));
|
||||
|
||||
gulp.task('vscode-win32-ia32', ['optimize-vscode', 'clean-vscode-win32-ia32'], packageTask('win32', 'ia32'));
|
||||
gulp.task('vscode-win32-x64', ['vscode-win32-x64-azurecore', 'optimize-vscode', 'clean-vscode-win32-x64'], packageTask('win32', 'x64'));
|
||||
gulp.task('vscode-darwin', ['vscode-darwin-azurecore', 'optimize-vscode', 'clean-vscode-darwin'], packageTask('darwin'));
|
||||
gulp.task('vscode-win32-x64', ['vscode-win32-x64-azurecore', 'vscode-win32-x64-mssql', 'optimize-vscode', 'clean-vscode-win32-x64'], packageTask('win32', 'x64'));
|
||||
gulp.task('vscode-darwin', ['vscode-darwin-azurecore', 'vscode-darwin-mssql', 'optimize-vscode', 'clean-vscode-darwin'], packageTask('darwin'));
|
||||
gulp.task('vscode-linux-ia32', ['optimize-vscode', 'clean-vscode-linux-ia32'], packageTask('linux', 'ia32'));
|
||||
gulp.task('vscode-linux-x64', ['vscode-linux-x64-azurecore', 'optimize-vscode', 'clean-vscode-linux-x64'], packageTask('linux', 'x64'));
|
||||
gulp.task('vscode-linux-x64', ['vscode-linux-x64-azurecore', 'vscode-linux-x64-mssql', 'optimize-vscode', 'clean-vscode-linux-x64'], packageTask('linux', 'x64'));
|
||||
gulp.task('vscode-linux-arm', ['optimize-vscode', 'clean-vscode-linux-arm'], packageTask('linux', 'arm'));
|
||||
|
||||
gulp.task('vscode-win32-ia32-min', ['minify-vscode', 'clean-vscode-win32-ia32'], packageTask('win32', 'ia32', { minified: true }));
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"icon": "images/sqlserver.png",
|
||||
"aiKey": "AIF-5574968e-856d-40d2-af67-c89a14e76412",
|
||||
"engines": {
|
||||
"vscode": "0.10.x"
|
||||
"vscode": "0.10.x"
|
||||
},
|
||||
"activationEvents": [
|
||||
"*"
|
||||
@@ -45,7 +45,7 @@
|
||||
"vscode-nls": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha-junit-reporter": "^1.17.0",
|
||||
"mocha-multi-reporters": "^1.1.7"
|
||||
}
|
||||
"mocha-junit-reporter": "^1.17.0",
|
||||
"mocha-multi-reporters": "^1.1.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,11 +25,11 @@ debug@^2.2.0:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
||||
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
ms "^2.1.1"
|
||||
|
||||
is-buffer@~1.1.1:
|
||||
version "1.1.6"
|
||||
@@ -37,9 +37,9 @@ is-buffer@~1.1.1:
|
||||
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
|
||||
|
||||
lodash@^4.16.4:
|
||||
version "4.17.10"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
|
||||
integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==
|
||||
version "4.17.11"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
||||
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
||||
|
||||
md5@^2.1.0:
|
||||
version "2.2.1"
|
||||
@@ -63,9 +63,9 @@ mkdirp@~0.5.1:
|
||||
minimist "0.0.8"
|
||||
|
||||
mocha-junit-reporter@^1.17.0:
|
||||
version "1.17.0"
|
||||
resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.17.0.tgz#2e5149ed40fc5d2e3ca71e42db5ab1fec9c6d85c"
|
||||
integrity sha1-LlFJ7UD8XS48px5C21qx/snG2Fw=
|
||||
version "1.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.18.0.tgz#9209a3fba30025ae3ae5e6bfe7f9c5bc3c2e8ee2"
|
||||
integrity sha512-y3XuqKa2+HRYtg0wYyhW/XsLm2Ps+pqf9HaTAt7+MVUAKFJaNAHOrNseTZo9KCxjfIbxUWwckP5qCDDPUmjSWA==
|
||||
dependencies:
|
||||
debug "^2.2.0"
|
||||
md5 "^2.1.0"
|
||||
@@ -86,6 +86,11 @@ ms@2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||
|
||||
ms@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
|
||||
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
|
||||
|
||||
strip-ansi@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
|
||||
@@ -94,9 +99,9 @@ strip-ansi@^4.0.0:
|
||||
ansi-regex "^3.0.0"
|
||||
|
||||
vscode-nls@^3.2.1:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.2.tgz#3817eca5b985c2393de325197cf4e15eb2aa5350"
|
||||
integrity sha512-/Ur1+tgazwd51+ncRyoy0UIu4dvMdVXS9XMUULQlZIBoNGEwOhwEx9x+hHWoUjldMrOQ32t2CGKo0u6D4R6/hg==
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4"
|
||||
integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw==
|
||||
|
||||
xml@^1.0.0:
|
||||
version "1.0.1"
|
||||
|
||||
@@ -122,6 +122,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"dataExplorer": {
|
||||
"azureResource": [
|
||||
{
|
||||
"id": "azureResourceExplorer",
|
||||
"name": "%azure.resource.explorer.title%"
|
||||
}
|
||||
]
|
||||
},
|
||||
"menus": {
|
||||
"view/title": [
|
||||
{
|
||||
@@ -166,7 +174,7 @@
|
||||
"@types/node": "^8.0.24",
|
||||
"mocha": "^5.2.0",
|
||||
"should": "^13.2.1",
|
||||
"vscode": "^1.1.26",
|
||||
"typemoq": "^2.1.0"
|
||||
"typemoq": "^2.1.0",
|
||||
"vscode": "^1.1.26"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,8 @@ This experience is currently in its initial preview. Please report issues and fe
|
||||
* This wizard requires an active connection to a SQL Server instance to start.
|
||||
* This wizard only works on .txt and .csv files.
|
||||
|
||||
### How do I start the Import Flat File wizard?
|
||||
* The main entry point for the wizard is to right click a database in the Object Explorer, and click **Import wizard**.
|
||||
* If a user is connected to a SQL Server instance, the user can also press **Ctrl**+**I** to start the wizard.
|
||||
## How do I start the Flat File Import wizard?
|
||||
* In Azure Data Studio, press **Ctrl**+**I** to start the wizard.
|
||||
|
||||
### Why would I use the Import Flat File wizard?
|
||||
This wizard was created to improve the current import experience leveraging an intelligent framework known as Program Synthesis using Examples ([PROSE](https://microsoft.github.io/prose/)). For a user without specialized domain knowledge, importing data can often be a complex, error prone, and tedious task. This wizard streamlines the import process as simple as selecting an input file and unique table name, and the PROSE framework handles the rest.
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as nls from 'vscode-nls';
|
||||
import * as sqlops from 'sqlops';
|
||||
import { SelectOperationPage } from './pages/selectOperationpage';
|
||||
import { DeployConfigPage } from './pages/deployConfigPage';
|
||||
import { DeployPlanPage } from './pages/deployPlanPage';
|
||||
import { DeployActionPage } from './pages/deployActionPage';
|
||||
import { DacFxSummaryPage } from './pages/dacFxSummaryPage';
|
||||
import { ExportConfigPage } from './pages/exportConfigPage';
|
||||
@@ -38,10 +39,17 @@ export enum Operation {
|
||||
export enum DeployOperationPath {
|
||||
selectOperation,
|
||||
deployOptions,
|
||||
deployPlan,
|
||||
deployAction,
|
||||
summary
|
||||
}
|
||||
|
||||
export enum DeployNewOperationPath {
|
||||
selectOperation,
|
||||
deployOptions,
|
||||
summary
|
||||
}
|
||||
|
||||
export enum ExtractOperationPath {
|
||||
selectOperation,
|
||||
options,
|
||||
@@ -87,6 +95,7 @@ export class DataTierApplicationWizard {
|
||||
this.wizard = sqlops.window.modelviewdialog.createWizard('Data-tier Application Wizard');
|
||||
let selectOperationWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.selectOperationPageName', 'Select an Operation'));
|
||||
let deployConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.deployConfigPageName', 'Select Deploy Dacpac Settings'));
|
||||
let deployPlanWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.deployPlanPage', 'Review the deploy plan'));
|
||||
let deployActionWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.deployActionPageName', 'Select Action'));
|
||||
let summaryWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.summaryPageName', 'Summary'));
|
||||
let extractConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.extractConfigPageName', 'Select Extract Dacpac Settings'));
|
||||
@@ -95,6 +104,7 @@ export class DataTierApplicationWizard {
|
||||
|
||||
this.pages.set('selectOperation', new Page(selectOperationWizardPage));
|
||||
this.pages.set('deployConfig', new Page(deployConfigWizardPage));
|
||||
this.pages.set('deployPlan', new Page(deployPlanWizardPage));
|
||||
this.pages.set('deployAction', new Page(deployActionWizardPage));
|
||||
this.pages.set('extractConfig', new Page(extractConfigWizardPage));
|
||||
this.pages.set('importConfig', new Page(importConfigWizardPage));
|
||||
@@ -116,6 +126,12 @@ export class DataTierApplicationWizard {
|
||||
await deployConfigDacFxPage.start();
|
||||
});
|
||||
|
||||
deployPlanWizardPage.registerContent(async (view) => {
|
||||
let deployPlanDacFxPage = new DeployPlanPage(this, deployPlanWizardPage, this.model, view);
|
||||
this.pages.get('deployPlan').dacFxPage = deployPlanDacFxPage;
|
||||
await deployPlanDacFxPage.start();
|
||||
});
|
||||
|
||||
deployActionWizardPage.registerContent(async (view) => {
|
||||
let deployActionDacFxPage = new DeployActionPage(this, deployActionWizardPage, this.model, view);
|
||||
this.pages.get('deployAction').dacFxPage = deployActionDacFxPage;
|
||||
@@ -166,7 +182,7 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
});
|
||||
|
||||
this.wizard.pages = [selectOperationWizardPage, deployConfigWizardPage, deployActionWizardPage, summaryWizardPage];
|
||||
this.wizard.pages = [selectOperationWizardPage, deployConfigWizardPage, deployPlanWizardPage, deployActionWizardPage, summaryWizardPage];
|
||||
this.wizard.generateScriptButton.hidden = true;
|
||||
this.wizard.generateScriptButton.onClick(async () => await this.generateDeployScript());
|
||||
this.wizard.doneButton.onClick(async () => await this.executeOperation());
|
||||
@@ -323,10 +339,12 @@ export class DataTierApplicationWizard {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ((this.selectedOperation === Operation.deploy || this.selectedOperation === Operation.generateDeployScript) && idx === DeployOperationPath.deployAction) {
|
||||
page = this.pages.get('deployAction');
|
||||
} else if (this.isSummaryPage(idx)) {
|
||||
page = this.pages.get('summary');
|
||||
} else if ((this.selectedOperation === Operation.deploy || this.selectedOperation === Operation.generateDeployScript) && idx === DeployOperationPath.deployPlan) {
|
||||
page = this.pages.get('deployPlan');
|
||||
} else if ((this.selectedOperation === Operation.deploy || this.selectedOperation === Operation.generateDeployScript) && idx === DeployOperationPath.deployAction) {
|
||||
page = this.pages.get('deployAction');
|
||||
}
|
||||
|
||||
return page;
|
||||
@@ -336,9 +354,24 @@ export class DataTierApplicationWizard {
|
||||
return this.selectedOperation === Operation.import && idx === ImportOperationPath.summary
|
||||
|| this.selectedOperation === Operation.export && idx === ExportOperationPath.summary
|
||||
|| this.selectedOperation === Operation.extract && idx === ExtractOperationPath.summary
|
||||
|| this.selectedOperation === Operation.deploy && !this.model.upgradeExisting && idx === DeployNewOperationPath.summary
|
||||
|| (this.selectedOperation === Operation.deploy || this.selectedOperation === Operation.generateDeployScript) && idx === DeployOperationPath.summary;
|
||||
}
|
||||
|
||||
public async generateDeployPlan(): Promise<string> {
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.generateDeployPlan(this.model.filePath, this.model.database, ownerUri, sqlops.TaskExecutionMode.execute);
|
||||
|
||||
if (!result || !result.success) {
|
||||
vscode.window.showErrorMessage(
|
||||
localize('alertData.deployPlanErrorMessage', "Generating deploy plan failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
|
||||
}
|
||||
|
||||
return result.report;
|
||||
}
|
||||
|
||||
private static async getService(providerName: string): Promise<sqlops.DacFxServicesProvider> {
|
||||
let service = sqlops.dataprotocol.getProvider<sqlops.DacFxServicesProvider>(providerName, sqlops.DataProviderType.DacFxServicesProvider);
|
||||
return service;
|
||||
|
||||
@@ -131,7 +131,15 @@ export class DacFxSummaryPage extends BasePage {
|
||||
|
||||
this.table.updateProperties({
|
||||
data: data,
|
||||
columns: ['Setting', 'Value'],
|
||||
columns: [
|
||||
{
|
||||
value: localize('dacfx.settingColumn', 'Setting'),
|
||||
cssClass: 'align-with-header'
|
||||
},
|
||||
{
|
||||
value: localize('dacfx.valueColumn', 'Value'),
|
||||
cssClass: 'align-with-header'
|
||||
}],
|
||||
width: 700,
|
||||
height: 200
|
||||
});
|
||||
|
||||
@@ -54,6 +54,8 @@ export class DeployActionPage extends DacFxConfigPage {
|
||||
}
|
||||
|
||||
async onPageEnter(): Promise<boolean> {
|
||||
// generate script file path in case the database changed since last time the page was entered
|
||||
this.setDefaultScriptFilePath();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -120,11 +122,7 @@ export class DeployActionPage extends DacFxConfigPage {
|
||||
this.createFileBrowserParts();
|
||||
|
||||
//default filepath
|
||||
let now = new Date();
|
||||
let datetime = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + '-' + now.getHours() + '-' + now.getMinutes();
|
||||
this.fileTextBox.value= path.join(os.homedir(), this.model.database + '_UpgradeDACScript_' + datetime + '.sql');
|
||||
this.model.scriptFilePath = this.fileTextBox.value;
|
||||
|
||||
this.setDefaultScriptFilePath();
|
||||
this.fileButton.onDidClick(async (click) => {
|
||||
let fileUri = await vscode.window.showSaveDialog(
|
||||
{
|
||||
@@ -151,15 +149,15 @@ export class DeployActionPage extends DacFxConfigPage {
|
||||
return {
|
||||
title: '',
|
||||
components: [
|
||||
{
|
||||
title: localize('dacfx.generatedScriptLocation','Deployment Script Location'),
|
||||
component: this.fileTextBox,
|
||||
layout: {
|
||||
horizontal: true,
|
||||
componentWidth: 400
|
||||
},
|
||||
actions: [this.fileButton]
|
||||
},],
|
||||
{
|
||||
title: localize('dacfx.generatedScriptLocation', 'Deployment Script Location'),
|
||||
component: this.fileTextBox,
|
||||
layout: {
|
||||
horizontal: true,
|
||||
componentWidth: 400
|
||||
},
|
||||
actions: [this.fileButton]
|
||||
},],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -168,6 +166,13 @@ export class DeployActionPage extends DacFxConfigPage {
|
||||
this.fileButton.enabled = enable;
|
||||
}
|
||||
|
||||
private setDefaultScriptFilePath(): void {
|
||||
let now = new Date();
|
||||
let datetime = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + '-' + now.getHours() + '-' + now.getMinutes();
|
||||
this.fileTextBox.value = path.join(os.homedir(), this.model.database + '_UpgradeDACScript_' + datetime + '.sql');
|
||||
this.model.scriptFilePath = this.fileTextBox.value;
|
||||
}
|
||||
|
||||
public setupNavigationValidator() {
|
||||
this.instance.registerNavigationValidator(() => {
|
||||
return true;
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { DacFxDataModel } from '../api/models';
|
||||
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
|
||||
import { DataTierApplicationWizard, DeployOperationPath, Operation } from '../dataTierApplicationWizard';
|
||||
import { DacFxConfigPage } from '../api/dacFxConfigPage';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
@@ -122,6 +122,12 @@ export class DeployConfigPage extends DacFxConfigPage {
|
||||
this.formBuilder.removeFormItem(this.databaseComponent);
|
||||
this.formBuilder.addFormItem(this.databaseDropdownComponent, { horizontal: true, componentWidth: 400 });
|
||||
this.model.database = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
|
||||
|
||||
// add deploy plan and generate script pages
|
||||
let deployPlanPage = this.instance.pages.get('deployPlan');
|
||||
this.instance.wizard.addPage(deployPlanPage.wizardPage, DeployOperationPath.deployPlan);
|
||||
let deployActionPage = this.instance.pages.get('deployAction');
|
||||
this.instance.wizard.addPage(deployActionPage.wizardPage, DeployOperationPath.deployAction);
|
||||
});
|
||||
|
||||
newRadioButton.onDidClick(() => {
|
||||
@@ -129,6 +135,11 @@ export class DeployConfigPage extends DacFxConfigPage {
|
||||
this.formBuilder.removeFormItem(this.databaseDropdownComponent);
|
||||
this.formBuilder.addFormItem(this.databaseComponent, { horizontal: true, componentWidth: 400 });
|
||||
this.model.database = this.databaseTextBox.value;
|
||||
this.instance.setDoneButton(Operation.deploy);
|
||||
|
||||
// remove deploy plan and generate script pages
|
||||
this.instance.wizard.removePage(DeployOperationPath.deployAction);
|
||||
this.instance.wizard.removePage(DeployOperationPath.deployPlan);
|
||||
});
|
||||
|
||||
//Initialize with upgrade existing true
|
||||
|
||||
296
extensions/import/src/wizard/pages/deployPlanPage.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as parser from 'htmlparser2';
|
||||
import { DacFxDataModel } from '../api/models';
|
||||
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
|
||||
import { DacFxConfigPage } from '../api/dacFxConfigPage';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export enum deployPlanXml {
|
||||
AlertElement = 'Alert',
|
||||
OperationElement = 'Operation',
|
||||
ItemElement = 'Item',
|
||||
NameAttribute = 'Name',
|
||||
ValueAttribute = 'Value',
|
||||
TypeAttribute = 'Type',
|
||||
IdAttribute = 'Id',
|
||||
DataIssueAttribute = 'DataIssue'
|
||||
}
|
||||
|
||||
export class TableObject {
|
||||
operation: string;
|
||||
object: string;
|
||||
type: string;
|
||||
dataloss: boolean;
|
||||
}
|
||||
|
||||
export class DeployPlanResult {
|
||||
columnData: Array<Array<string>>;
|
||||
dataLossAlerts: Set<string>;
|
||||
}
|
||||
|
||||
export class DeployPlanPage extends DacFxConfigPage {
|
||||
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
|
||||
protected readonly instance: DataTierApplicationWizard;
|
||||
protected readonly model: DacFxDataModel;
|
||||
protected readonly view: sqlops.ModelView;
|
||||
private formBuilder: sqlops.FormBuilder;
|
||||
private form: sqlops.FormContainer;
|
||||
private table: sqlops.TableComponent;
|
||||
private loader: sqlops.LoadingComponent;
|
||||
private dataLossCheckbox: sqlops.CheckBoxComponent;
|
||||
private dataLossText: sqlops.TextComponent;
|
||||
private dataLossComponentGroup: sqlops.FormComponentGroup;
|
||||
private noDataLossTextComponent: sqlops.FormComponent;
|
||||
|
||||
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
|
||||
super(instance, wizardPage, model, view);
|
||||
}
|
||||
|
||||
async start(): Promise<boolean> {
|
||||
this.table = this.view.modelBuilder.table().component();
|
||||
this.loader = this.view.modelBuilder.loadingComponent().withItem(this.table).component();
|
||||
this.dataLossComponentGroup = await this.createDataLossComponents();
|
||||
this.noDataLossTextComponent = await this.createNoDataLossText();
|
||||
|
||||
this.formBuilder = this.view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
[
|
||||
{
|
||||
component: this.loader,
|
||||
title: ''
|
||||
},
|
||||
this.dataLossComponentGroup
|
||||
], {
|
||||
horizontal: true,
|
||||
});
|
||||
this.form = this.formBuilder.component();
|
||||
await this.view.initializeModel(this.form);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async onPageEnter(): Promise<boolean> {
|
||||
// reset checkbox settings
|
||||
this.formBuilder.addFormItem(this.dataLossComponentGroup, { horizontal: true, componentWidth: 400 });
|
||||
this.dataLossCheckbox.checked = false;
|
||||
this.dataLossCheckbox.enabled = false;
|
||||
this.formBuilder.removeFormItem(this.noDataLossTextComponent);
|
||||
|
||||
this.loader.loading = true;
|
||||
this.table.data = [];
|
||||
await this.populateTable();
|
||||
this.loader.loading = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private async populateTable() {
|
||||
let report = await this.instance.generateDeployPlan();
|
||||
let result = this.parseXml(report);
|
||||
|
||||
this.table.updateProperties({
|
||||
data: this.getColumnData(result),
|
||||
columns: this.getTableColumns(result.dataLossAlerts.size > 0),
|
||||
width: 875,
|
||||
height: 300
|
||||
});
|
||||
|
||||
if (result.dataLossAlerts.size > 0) {
|
||||
// update message to list how many operations could result in data loss
|
||||
this.dataLossText.updateProperties({
|
||||
value: localize('dacfx.dataLossTextWithCount', '{0} of the deploy actions listed may result in data loss. Please ensure you have a backup or snapshot available in the event of an issue with the deployment.', result.dataLossAlerts.size)
|
||||
});
|
||||
this.dataLossCheckbox.enabled = true;
|
||||
} else {
|
||||
// check checkbox to enable Next button and remove checkbox because there won't be any possible data loss
|
||||
this.dataLossCheckbox.checked = true;
|
||||
this.formBuilder.removeFormItem(this.dataLossComponentGroup);
|
||||
this.formBuilder.addFormItem(this.noDataLossTextComponent, { componentWidth: 300, horizontal: true });
|
||||
}
|
||||
}
|
||||
|
||||
private async createDataLossCheckbox(): Promise<sqlops.FormComponent> {
|
||||
this.dataLossCheckbox = this.view.modelBuilder.checkBox()
|
||||
.withValidation(component => component.checked === true)
|
||||
.withProperties({
|
||||
label: localize('dacFx.dataLossCheckbox', 'Proceed despite possible data loss'),
|
||||
}).component();
|
||||
|
||||
return {
|
||||
component: this.dataLossCheckbox,
|
||||
title: '',
|
||||
required: true
|
||||
};
|
||||
}
|
||||
|
||||
private async createNoDataLossText(): Promise<sqlops.FormComponent> {
|
||||
let noDataLossText = this.view.modelBuilder.text()
|
||||
.withProperties({
|
||||
value: localize('dacfx.noDataLossText', 'No data loss will occur from the listed deploy actions.')
|
||||
}).component();
|
||||
|
||||
return {
|
||||
title: '',
|
||||
component: noDataLossText
|
||||
};
|
||||
}
|
||||
|
||||
private async createDataLossComponents(): Promise<sqlops.FormComponentGroup> {
|
||||
let dataLossComponent = await this.createDataLossCheckbox();
|
||||
this.dataLossText = this.view.modelBuilder.text()
|
||||
.withProperties({
|
||||
value: localize('dacfx.dataLossText', 'The deploy actions may result in data loss. Please ensure you have a backup or snapshot available in the event of an issue with the deployment.')
|
||||
}).component();
|
||||
|
||||
return {
|
||||
title: '',
|
||||
components: [
|
||||
{
|
||||
component: this.dataLossText,
|
||||
layout: {
|
||||
componentWidth: 400,
|
||||
horizontal: true
|
||||
},
|
||||
title: ''
|
||||
},
|
||||
dataLossComponent
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
private getColumnData(result: DeployPlanResult): Array<Array<string>> {
|
||||
// remove data loss column data if there aren't any alerts
|
||||
let columnData = result.columnData;
|
||||
if (result.dataLossAlerts.size === 0) {
|
||||
columnData.forEach(entry => {
|
||||
entry.shift();
|
||||
});
|
||||
}
|
||||
return columnData;
|
||||
}
|
||||
|
||||
private getTableColumns(dataloss: boolean): sqlops.TableColumn[] {
|
||||
let columns: sqlops.TableColumn[] = [
|
||||
{
|
||||
value: localize('dacfx.operationColumn', 'Operation'),
|
||||
width: 75,
|
||||
cssClass: 'align-with-header',
|
||||
toolTip: localize('dacfx.operationTooltip', 'Operation(Create, Alter, Delete) that will occur during deployment')
|
||||
},
|
||||
{
|
||||
value: localize('dacfx.typeColumn', 'Type'),
|
||||
width: 100,
|
||||
cssClass: 'align-with-header',
|
||||
toolTip: localize('dacfx.typeTooltip', 'Type of object that will be affected by deployment')
|
||||
},
|
||||
{
|
||||
value: localize('dacfx.objectColumn', 'Object'),
|
||||
width: 300,
|
||||
cssClass: 'align-with-header',
|
||||
toolTip: localize('dacfx.objecTooltip', 'Name of object that will be affected by deployment')
|
||||
}];
|
||||
|
||||
if (dataloss) {
|
||||
columns.unshift(
|
||||
{
|
||||
value: localize('dacfx.dataLossColumn', 'Data Loss'),
|
||||
width: 50,
|
||||
cssClass: 'center-align',
|
||||
toolTip: localize('dacfx.dataLossTooltip', 'Operations that may result in data loss are marked with a warning sign')
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
private parseXml(report: string): DeployPlanResult {
|
||||
let operations = new Array<TableObject>();
|
||||
let dataLossAlerts = new Set<string>();
|
||||
|
||||
let currentOperation = '';
|
||||
let dataIssueAlert = false;
|
||||
let currentReportSection: deployPlanXml;
|
||||
let currentTableObj: TableObject;
|
||||
let p = new parser.Parser({
|
||||
onopentagname(name) {
|
||||
if (name === deployPlanXml.AlertElement) {
|
||||
currentReportSection = deployPlanXml.AlertElement;
|
||||
} else if (name === deployPlanXml.OperationElement) {
|
||||
currentReportSection = deployPlanXml.OperationElement;
|
||||
} else if (name === deployPlanXml.ItemElement) {
|
||||
currentTableObj = new TableObject();
|
||||
}
|
||||
},
|
||||
onattribute: function (name, value) {
|
||||
if (currentReportSection === deployPlanXml.AlertElement) {
|
||||
switch (name) {
|
||||
case deployPlanXml.NameAttribute: {
|
||||
// only care about showing data loss alerts
|
||||
if (value === deployPlanXml.DataIssueAttribute) {
|
||||
dataIssueAlert = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case deployPlanXml.IdAttribute: {
|
||||
if (dataIssueAlert) {
|
||||
dataLossAlerts.add(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (currentReportSection === deployPlanXml.OperationElement) {
|
||||
switch (name) {
|
||||
case deployPlanXml.NameAttribute: {
|
||||
currentOperation = value;
|
||||
break;
|
||||
}
|
||||
case deployPlanXml.ValueAttribute: {
|
||||
currentTableObj.object = value;
|
||||
break;
|
||||
}
|
||||
case deployPlanXml.TypeAttribute: {
|
||||
currentTableObj.type = value;
|
||||
break;
|
||||
}
|
||||
case deployPlanXml.IdAttribute: {
|
||||
if (dataLossAlerts.has(value)) {
|
||||
currentTableObj.dataloss = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onclosetag: function (name) {
|
||||
if (name === deployPlanXml.ItemElement) {
|
||||
currentTableObj.operation = currentOperation;
|
||||
operations.push(currentTableObj);
|
||||
}
|
||||
}
|
||||
}, { xmlMode: true, decodeEntities: true });
|
||||
p.parseChunk(report);
|
||||
|
||||
let data = new Array<Array<string>>();
|
||||
operations.forEach(operation => {
|
||||
let isDataLoss = operation.dataloss ? '⚠️' : '';
|
||||
data.push([isDataLoss, operation.operation, operation.type, operation.object]);
|
||||
});
|
||||
|
||||
return {
|
||||
columnData: data,
|
||||
dataLossAlerts: dataLossAlerts
|
||||
};
|
||||
}
|
||||
|
||||
public setupNavigationValidator() {
|
||||
this.instance.registerNavigationValidator(() => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { DacFxDataModel } from '../api/models';
|
||||
import { DataTierApplicationWizard, Operation } from '../dataTierApplicationWizard';
|
||||
import { DataTierApplicationWizard, Operation, DeployOperationPath, ExtractOperationPath, ImportOperationPath, ExportOperationPath } from '../dataTierApplicationWizard';
|
||||
import { BasePage } from '../api/basePage';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
@@ -73,10 +73,12 @@ export class SelectOperationPage extends BasePage {
|
||||
|
||||
//add deploy pages
|
||||
let configPage = this.instance.pages.get('deployConfig');
|
||||
this.instance.wizard.addPage(configPage.wizardPage, 1);
|
||||
this.instance.wizard.addPage(configPage.wizardPage, DeployOperationPath.deployOptions);
|
||||
let deployPlanPage = this.instance.pages.get('deployPlan');
|
||||
this.instance.wizard.addPage(deployPlanPage.wizardPage, DeployOperationPath.deployPlan);
|
||||
let actionPage = this.instance.pages.get('deployAction');
|
||||
this.instance.wizard.addPage(actionPage.wizardPage, 2);
|
||||
this.addSummaryPage(3);
|
||||
this.instance.wizard.addPage(actionPage.wizardPage, DeployOperationPath.deployAction);
|
||||
this.addSummaryPage(DeployOperationPath.summary);
|
||||
|
||||
// change button text and operation
|
||||
this.instance.setDoneButton(Operation.deploy);
|
||||
@@ -100,8 +102,8 @@ export class SelectOperationPage extends BasePage {
|
||||
|
||||
// add the extract page
|
||||
let page = this.instance.pages.get('extractConfig');
|
||||
this.instance.wizard.addPage(page.wizardPage, 1);
|
||||
this.addSummaryPage(2);
|
||||
this.instance.wizard.addPage(page.wizardPage, ExtractOperationPath.options);
|
||||
this.addSummaryPage(ExtractOperationPath.summary);
|
||||
|
||||
// change button text and operation
|
||||
this.instance.setDoneButton(Operation.extract);
|
||||
@@ -125,8 +127,8 @@ export class SelectOperationPage extends BasePage {
|
||||
|
||||
// add the import page
|
||||
let page = this.instance.pages.get('importConfig');
|
||||
this.instance.wizard.addPage(page.wizardPage, 1);
|
||||
this.addSummaryPage(2);
|
||||
this.instance.wizard.addPage(page.wizardPage, ImportOperationPath.options);
|
||||
this.addSummaryPage(ImportOperationPath.summary);
|
||||
|
||||
// change button text and operation
|
||||
this.instance.setDoneButton(Operation.import);
|
||||
@@ -150,8 +152,8 @@ export class SelectOperationPage extends BasePage {
|
||||
|
||||
// add the export pages
|
||||
let page = this.instance.pages.get('exportConfig');
|
||||
this.instance.wizard.addPage(page.wizardPage, 1);
|
||||
this.addSummaryPage(2);
|
||||
this.instance.wizard.addPage(page.wizardPage, ExportOperationPath.options);
|
||||
this.addSummaryPage(ExportOperationPath.summary);
|
||||
|
||||
// change button text and operation
|
||||
this.instance.setDoneButton(Operation.export);
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
agent-base@4, agent-base@^4.1.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
|
||||
integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
|
||||
dependencies:
|
||||
es6-promisify "^5.0.0"
|
||||
|
||||
applicationinsights@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.1.tgz#53446b830fe8d5d619eee2a278b31d3d25030927"
|
||||
integrity sha1-U0Rrgw/o1dYZ7uKieLMdPSUDCSc=
|
||||
dependencies:
|
||||
diagnostic-channel "0.2.0"
|
||||
diagnostic-channel-publishers "0.2.1"
|
||||
@@ -19,10 +21,12 @@ applicationinsights@1.0.1:
|
||||
base64-js@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978"
|
||||
integrity sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=
|
||||
|
||||
bl@^1.0.0:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c"
|
||||
integrity sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==
|
||||
dependencies:
|
||||
readable-stream "^2.3.5"
|
||||
safe-buffer "^5.1.1"
|
||||
@@ -30,10 +34,12 @@ bl@^1.0.0:
|
||||
buffer-alloc-unsafe@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
|
||||
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
|
||||
|
||||
buffer-alloc@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
|
||||
integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
|
||||
dependencies:
|
||||
buffer-alloc-unsafe "^1.1.0"
|
||||
buffer-fill "^1.0.0"
|
||||
@@ -41,14 +47,17 @@ buffer-alloc@^1.2.0:
|
||||
buffer-crc32@~0.2.3:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
|
||||
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
|
||||
|
||||
buffer-fill@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
|
||||
integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
|
||||
|
||||
buffer@^3.0.1:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-3.6.0.tgz#a72c936f77b96bf52f5f7e7b467180628551defb"
|
||||
integrity sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=
|
||||
dependencies:
|
||||
base64-js "0.0.8"
|
||||
ieee754 "^1.1.4"
|
||||
@@ -57,12 +66,14 @@ buffer@^3.0.1:
|
||||
commander@~2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
|
||||
integrity sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=
|
||||
dependencies:
|
||||
graceful-readlink ">= 1.0.0"
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
|
||||
|
||||
"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#0.2.15":
|
||||
version "0.2.15"
|
||||
@@ -73,18 +84,21 @@ core-util-is@~1.0.0:
|
||||
debug@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@^3.1.0:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
||||
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1"
|
||||
integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==
|
||||
dependencies:
|
||||
file-type "^5.2.0"
|
||||
is-stream "^1.1.0"
|
||||
@@ -93,6 +107,7 @@ decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1:
|
||||
decompress-tarbz2@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b"
|
||||
integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==
|
||||
dependencies:
|
||||
decompress-tar "^4.1.0"
|
||||
file-type "^6.1.0"
|
||||
@@ -103,6 +118,7 @@ decompress-tarbz2@^4.0.0:
|
||||
decompress-targz@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee"
|
||||
integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==
|
||||
dependencies:
|
||||
decompress-tar "^4.1.1"
|
||||
file-type "^5.2.0"
|
||||
@@ -111,6 +127,7 @@ decompress-targz@^4.0.0:
|
||||
decompress-unzip@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69"
|
||||
integrity sha1-3qrM39FK6vhVePczroIQ+bSEj2k=
|
||||
dependencies:
|
||||
file-type "^3.8.0"
|
||||
get-stream "^2.2.0"
|
||||
@@ -120,6 +137,7 @@ decompress-unzip@^4.0.1:
|
||||
decompress@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.0.tgz#7aedd85427e5a92dacfe55674a7c505e96d01f9d"
|
||||
integrity sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=
|
||||
dependencies:
|
||||
decompress-tar "^4.0.0"
|
||||
decompress-tarbz2 "^4.0.0"
|
||||
@@ -133,58 +151,70 @@ decompress@^4.2.0:
|
||||
diagnostic-channel-publishers@0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3"
|
||||
integrity sha1-ji1geottef6IC1SLxYzGvrKIxPM=
|
||||
|
||||
diagnostic-channel@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17"
|
||||
integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=
|
||||
dependencies:
|
||||
semver "^5.3.0"
|
||||
|
||||
end-of-stream@^1.0.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
|
||||
integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==
|
||||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
es6-promise@^4.0.3:
|
||||
version "4.2.5"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054"
|
||||
integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==
|
||||
|
||||
es6-promisify@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
|
||||
integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
|
||||
dependencies:
|
||||
es6-promise "^4.0.3"
|
||||
|
||||
eventemitter2@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-5.0.1.tgz#6197a095d5fb6b57e8942f6fd7eaad63a09c9452"
|
||||
integrity sha1-YZegldX7a1folC9v1+qtY6CclFI=
|
||||
|
||||
fd-slicer@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
|
||||
integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
file-type@^3.8.0:
|
||||
version "3.9.0"
|
||||
resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9"
|
||||
integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek=
|
||||
|
||||
file-type@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6"
|
||||
integrity sha1-LdvqfHP/42No365J3DOMBYwritY=
|
||||
|
||||
file-type@^6.1.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919"
|
||||
integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==
|
||||
|
||||
fs-constants@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
|
||||
|
||||
get-stream@^2.2.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de"
|
||||
integrity sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=
|
||||
dependencies:
|
||||
object-assign "^4.0.1"
|
||||
pinkie-promise "^2.0.0"
|
||||
@@ -192,14 +222,17 @@ get-stream@^2.2.0:
|
||||
graceful-fs@^4.1.10:
|
||||
version "4.1.15"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
|
||||
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
|
||||
|
||||
"graceful-readlink@>= 1.0.0":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
||||
integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
|
||||
|
||||
http-proxy-agent@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
|
||||
integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==
|
||||
dependencies:
|
||||
agent-base "4"
|
||||
debug "3.1.0"
|
||||
@@ -207,6 +240,7 @@ http-proxy-agent@^2.1.0:
|
||||
https-proxy-agent@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
|
||||
integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==
|
||||
dependencies:
|
||||
agent-base "^4.1.0"
|
||||
debug "^3.1.0"
|
||||
@@ -214,94 +248,115 @@ https-proxy-agent@^2.2.1:
|
||||
ieee754@^1.1.4:
|
||||
version "1.1.12"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b"
|
||||
integrity sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==
|
||||
|
||||
inherits@~2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||
|
||||
is-natural-number@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8"
|
||||
integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=
|
||||
|
||||
is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
|
||||
|
||||
isarray@^1.0.0, isarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
|
||||
|
||||
make-dir@^1.0.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
|
||||
integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==
|
||||
dependencies:
|
||||
pify "^3.0.0"
|
||||
|
||||
minimist@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
|
||||
|
||||
mkdirp@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
||||
integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
|
||||
dependencies:
|
||||
minimist "0.0.8"
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||
|
||||
ms@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
|
||||
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
|
||||
|
||||
object-assign@^4.0.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
||||
|
||||
once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
opener@^1.4.3:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed"
|
||||
integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==
|
||||
|
||||
os-tmpdir@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
|
||||
|
||||
pend@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
||||
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
|
||||
|
||||
pify@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
|
||||
|
||||
pify@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
|
||||
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
|
||||
|
||||
pinkie-promise@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
|
||||
integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o=
|
||||
dependencies:
|
||||
pinkie "^2.0.0"
|
||||
|
||||
pinkie@^2.0.0:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
|
||||
integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
|
||||
|
||||
process-nextick-args@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
|
||||
integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
|
||||
|
||||
readable-stream@^2.3.0, readable-stream@^2.3.5:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
||||
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.3"
|
||||
@@ -314,16 +369,19 @@ readable-stream@^2.3.0, readable-stream@^2.3.5:
|
||||
safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
seek-bzip@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc"
|
||||
integrity sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=
|
||||
dependencies:
|
||||
commander "~2.8.1"
|
||||
|
||||
semver@^5.3.0:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
|
||||
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
|
||||
|
||||
"service-downloader@github:anthonydresser/service-downloader#0.1.5":
|
||||
version "0.1.5"
|
||||
@@ -339,18 +397,21 @@ semver@^5.3.0:
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
strip-dirs@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5"
|
||||
integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==
|
||||
dependencies:
|
||||
is-natural-number "^4.0.1"
|
||||
|
||||
tar-stream@^1.5.2:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
|
||||
integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
|
||||
dependencies:
|
||||
bl "^1.0.0"
|
||||
buffer-alloc "^1.2.0"
|
||||
@@ -363,20 +424,24 @@ tar-stream@^1.5.2:
|
||||
through@^2.3.6:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
|
||||
|
||||
tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
|
||||
dependencies:
|
||||
os-tmpdir "~1.0.2"
|
||||
|
||||
to-buffer@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80"
|
||||
integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==
|
||||
|
||||
unbzip2-stream@^1.0.9:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.1.tgz#7854da51622a7e63624221196357803b552966a1"
|
||||
integrity sha512-fIZnvdjblYs7Cru/xC6tCPVhz7JkYcVQQkePwMLyQELzYTds2Xn8QefPVnvdVhhZqubxNA1cASXEH5wcK0Bucw==
|
||||
dependencies:
|
||||
buffer "^3.0.1"
|
||||
through "^2.3.6"
|
||||
@@ -384,26 +449,31 @@ unbzip2-stream@^1.0.9:
|
||||
util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
||||
vscode-extension-telemetry@0.0.18:
|
||||
version "0.0.18"
|
||||
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.0.18.tgz#602ba20d8c71453aa34533a291e7638f6e5c0327"
|
||||
integrity sha512-Vw3Sr+dZwl+c6PlsUwrTtCOJkgrmvS3OUVDQGcmpXWAgq9xGq6as0K4pUx+aGqTjzLAESmWSrs6HlJm6J6Khcg==
|
||||
dependencies:
|
||||
applicationinsights "1.0.1"
|
||||
|
||||
vscode-jsonrpc@3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-3.5.0.tgz#87239d9e166b2d7352245b8a813597804c1d63aa"
|
||||
integrity sha1-hyOdnhZrLXNSJFuKgTWXgEwdY6o=
|
||||
|
||||
vscode-languageclient@3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-3.5.1.tgz#c78e582459c24e58f88020dfa34065e976186a98"
|
||||
integrity sha512-GTQ+hSq/o4c/y6GYmyP9XNrVoIu0NFZ67KltSkqN+tO0eUNDIlrVNX+3DJzzyLhSsrctuGzuYWm3t87mNAcBmQ==
|
||||
dependencies:
|
||||
vscode-languageserver-protocol "3.5.1"
|
||||
|
||||
vscode-languageserver-protocol@3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.5.1.tgz#5144a3a9eeccbd83fe2745bd4ed75fad6cc45f0d"
|
||||
integrity sha512-1fPDIwsAv1difCV+8daOrJEGunClNJWqnUHq/ncWrjhitKWXgGmRCjlwZ3gDUTt54yRcvXz1PXJDaRNvNH6pYA==
|
||||
dependencies:
|
||||
vscode-jsonrpc "3.5.0"
|
||||
vscode-languageserver-types "3.5.0"
|
||||
@@ -411,22 +481,27 @@ vscode-languageserver-protocol@3.5.1:
|
||||
vscode-languageserver-types@3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.5.0.tgz#e48d79962f0b8e02de955e3f524908e2b19c0374"
|
||||
integrity sha1-5I15li8LjgLelV4/UkkI4rGcA3Q=
|
||||
|
||||
vscode-nls@^3.2.1:
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4"
|
||||
integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw==
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||
|
||||
xtend@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
||||
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
|
||||
|
||||
yauzl@^2.4.2:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
|
||||
integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
|
||||
dependencies:
|
||||
buffer-crc32 "~0.2.3"
|
||||
fd-slicer "~1.1.0"
|
||||
@@ -434,3 +509,4 @@ yauzl@^2.4.2:
|
||||
zone.js@0.7.6:
|
||||
version "0.7.6"
|
||||
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009"
|
||||
integrity sha1-+7w50+AmHQmG8boGMG6zrrDSIAk=
|
||||
|
||||
@@ -18,13 +18,21 @@
|
||||
"update-grammar": "node ../../build/npm/update-grammar.js Microsoft/vscode-mssql syntaxes/SQL.plist ./syntaxes/sql.tmLanguage.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.0",
|
||||
"clipboardy": "^1.2.3",
|
||||
"dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#0.2.15",
|
||||
"error-ex": "^1.3.2",
|
||||
"figures": "^2.0.0",
|
||||
"opener": "^1.4.3",
|
||||
"request-promise": "^4.2.2",
|
||||
"service-downloader": "github:anthonydresser/service-downloader#0.1.5",
|
||||
"vscode-extension-telemetry": "^0.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"stream-meter": "^1.0.4",
|
||||
"uri-js": "^4.2.2",
|
||||
"vscode-extension-telemetry": "^0.0.15",
|
||||
"vscode-nls": "^4.0.0",
|
||||
"webhdfs": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
@@ -37,6 +45,80 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"command": "mssqlCluster.uploadFiles",
|
||||
"title": "%mssqlCluster.uploadFiles%"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.mkdir",
|
||||
"title": "%mssqlCluster.mkdir%"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.deleteFiles",
|
||||
"title": "%mssqlCluster.deleteFiles%"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.previewFile",
|
||||
"title": "%mssqlCluster.previewFile%"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.saveFile",
|
||||
"title": "%mssqlCluster.saveFile%"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.copyPath",
|
||||
"title": "%mssqlCluster.copyPath%"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.task.newNotebook",
|
||||
"title": "%notebook.command.new%",
|
||||
"icon": {
|
||||
"dark": "resources/dark/new_notebook_inverse.svg",
|
||||
"light": "resources/light/new_notebook.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.task.openNotebook",
|
||||
"title": "%notebook.command.open%",
|
||||
"icon": {
|
||||
"dark": "resources/dark/open_notebook_inverse.svg",
|
||||
"light": "resources/light/open_notebook.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.livy.cmd.submitSparkJob",
|
||||
"title": "%title.submitSparkJob%"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.livy.task.submitSparkJob",
|
||||
"title": "%title.newSparkJob%",
|
||||
"icon": {
|
||||
"dark": "resources/dark/new_spark_job_inverse.svg",
|
||||
"light": "resources/light/new_spark_job.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.livy.task.openSparkHistory",
|
||||
"title": "%title.openSparkHistory%",
|
||||
"icon": {
|
||||
"dark": "resources/dark/new_spark_job_inverse.svg",
|
||||
"light": "resources/light/new_spark_job.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.livy.task.openYarnHistory",
|
||||
"title": "%title.openYarnHistory%",
|
||||
"icon": {
|
||||
"dark": "resources/light/hadoop.svg",
|
||||
"light": "resources/light/hadoop.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.livy.cmd.submitFileToSparkJob",
|
||||
"title": "%title.submitSparkJob%"
|
||||
}
|
||||
],
|
||||
"outputChannels": [
|
||||
"MSSQL"
|
||||
],
|
||||
@@ -126,6 +208,92 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "mssqlCluster.uploadFiles",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.mkdir",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.deleteFiles",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.previewFile",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.saveFile",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.copyPath",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.task.newNotebook",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.task.openNotebook",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.livy.cmd.submitFileToSparkJob",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.livy.task.submitSparkJob",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"objectExplorer/item/context": [
|
||||
{
|
||||
"command": "mssqlCluster.uploadFiles",
|
||||
"when": "nodeType=~/^mssqlCluster/ && nodeType != mssqlCluster:message && nodeType != mssqlCluster:file",
|
||||
"group": "1mssqlCluster@1"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.mkdir",
|
||||
"when": "nodeType=~/^mssqlCluster/ && nodeType != mssqlCluster:message && nodeType != mssqlCluster:file",
|
||||
"group": "1mssqlCluster@1"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.saveFile",
|
||||
"when": "nodeType == mssqlCluster:file",
|
||||
"group": "1mssqlCluster@1"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.previewFile",
|
||||
"when": "nodeType == mssqlCluster:file",
|
||||
"group": "1mssqlCluster@2"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.copyPath",
|
||||
"when": "nodeType=~/^mssqlCluster/ && nodeType != mssqlCluster:connection && nodeType != mssqlCluster:message",
|
||||
"group": "1mssqlCluster@3"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.deleteFiles",
|
||||
"when": "nodeType=~/^mssqlCluster/ && viewItem != mssqlCluster:connection && nodeType != mssqlCluster:message",
|
||||
"group": "1mssqlCluster@4"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.livy.cmd.submitSparkJob",
|
||||
"when": "connectionProvider == MSSQL && nodeType == dataservices",
|
||||
"group": "1root@1"
|
||||
},
|
||||
{
|
||||
"command": "mssqlCluster.livy.cmd.submitFileToSparkJob",
|
||||
"when": "nodeType == mssqlCluster:file && nodeSubType == mssqlCluster:spark",
|
||||
"group": "1mssqlCluster@6"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dashboard": {
|
||||
"provider": "MSSQL",
|
||||
"flavors": [
|
||||
@@ -221,6 +389,34 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"dashboard.tabs": [
|
||||
{
|
||||
"id": "mssql-big-data-cluster",
|
||||
"description": "tab.bigDataClusterDescription",
|
||||
"provider": "MSSQL",
|
||||
"title": "%title.bigDataCluster%",
|
||||
"when": "connectionProvider == 'MSSQL' && mssql:iscluster",
|
||||
"container": {
|
||||
"grid-container": [
|
||||
{
|
||||
"name": "%title.tasks%",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"colspan": 2,
|
||||
"widget": {
|
||||
"tasks-widget": [
|
||||
"mssqlCluster.task.newNotebook",
|
||||
"mssqlCluster.task.openNotebook",
|
||||
"mssqlCluster.livy.task.submitSparkJob",
|
||||
"mssqlCluster.livy.task.openSparkHistory",
|
||||
"mssqlCluster.livy.task.openYarnHistory"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"connectionProvider": {
|
||||
"providerId": "MSSQL",
|
||||
"displayName": "Microsoft SQL Server",
|
||||
|
||||
@@ -4,5 +4,25 @@
|
||||
"json.schemas.fileMatch.desc": "An array of file patterns to match against when resolving JSON files to schemas.",
|
||||
"json.schemas.fileMatch.item.desc": "A file pattern that can contain '*' to match against when resolving JSON files to schemas.",
|
||||
"json.schemas.schema.desc": "The schema definition for the given URL. The schema only needs to be provided to avoid accesses to the schema URL.",
|
||||
"json.format.enable.desc": "Enable/disable default JSON formatter (requires restart)"
|
||||
"json.format.enable.desc": "Enable/disable default JSON formatter (requires restart)",
|
||||
|
||||
"mssqlCluster.uploadFiles": "Upload files",
|
||||
"mssqlCluster.mkdir": "New directory",
|
||||
"mssqlCluster.deleteFiles": "Delete",
|
||||
"mssqlCluster.previewFile": "Preview",
|
||||
"mssqlCluster.saveFile": "Save",
|
||||
"mssqlCluster.copyPath": "Copy Path",
|
||||
|
||||
"notebook.command.new": "New Notebook",
|
||||
"notebook.command.open": "Open Notebook",
|
||||
|
||||
"tab.bigDataClusterDescription": "Tasks and information about your SQL Server Big Data Cluster",
|
||||
"title.bigDataCluster": "SQL Server Big Data Cluster",
|
||||
"title.submitSparkJob": "Submit Spark Job",
|
||||
"title.newSparkJob": "New Spark Job",
|
||||
"title.openSparkHistory": "View Spark History",
|
||||
"title.openYarnHistory": "View Yarn History",
|
||||
"title.tasks": "Tasks",
|
||||
"title.installPackages": "Install Packages",
|
||||
"title.configurePython": "Configure Python for Notebooks"
|
||||
}
|
||||
1
extensions/mssql/resources/dark/cluster_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>cluster_inverse</title><path class="cls-1" d="M14,7a1.94,1.94,0,0,1,.78.16,2,2,0,0,1,1.07,1.07,2,2,0,0,1,0,1.55,2,2,0,0,1-1.07,1.07,2,2,0,0,1-1.51,0,2.05,2.05,0,0,1-1.05-1,1.88,1.88,0,0,1-.2-.72L10.84,9a3,3,0,0,1-.56,1,3,3,0,0,1-.87.7L9.86,12H10a1.94,1.94,0,0,1,.78.16,2,2,0,0,1,1.07,1.07,2,2,0,0,1,0,1.55,2,2,0,0,1-1.07,1.07,2,2,0,0,1-1.55,0,2,2,0,0,1-1.07-1.07A2,2,0,0,1,8.25,13a2,2,0,0,1,.67-.72L8.46,11l-.23,0H8a3,3,0,0,1-1.36-.32,3,3,0,0,1-1.07-.9L4,10.58a2,2,0,0,1-.11,1.2,2,2,0,0,1-1.07,1.07A1.94,1.94,0,0,1,2,13a1.94,1.94,0,0,1-.78-.16A2,2,0,0,1,.16,11.78a2,2,0,0,1,0-1.55A2,2,0,0,1,1.22,9.16,1.94,1.94,0,0,1,2,9a2,2,0,0,1,.83.18,2,2,0,0,1,.68.51l1.63-.81A3,3,0,0,1,5.2,6.93,2.91,2.91,0,0,1,5.77,6L4.82,4.82A2,2,0,0,1,4,5a1.94,1.94,0,0,1-.78-.16A2,2,0,0,1,2.16,3.78a2,2,0,0,1,0-1.55A2,2,0,0,1,3.22,1.16a2,2,0,0,1,1.55,0A2,2,0,0,1,5.84,2.22,1.94,1.94,0,0,1,6,3a1.94,1.94,0,0,1-.4,1.2l.94,1.18a3.24,3.24,0,0,1,.71-.28A2.94,2.94,0,0,1,8,5a3,3,0,0,1,1.23.26l1.28-1.92a2,2,0,0,1-.37-.62A2,2,0,0,1,10,2a1.94,1.94,0,0,1,.16-.78A2,2,0,0,1,11.22.16a2,2,0,0,1,1.55,0,2,2,0,0,1,1.07,1.07A1.94,1.94,0,0,1,14,2a1.94,1.94,0,0,1-.16.78,2,2,0,0,1-1.07,1.07A1.94,1.94,0,0,1,12,4a2.06,2.06,0,0,1-.66-.11L10.05,5.82A3,3,0,0,1,11,8l1.17.2a2,2,0,0,1,.74-.86,2.14,2.14,0,0,1,.52-.24A1.92,1.92,0,0,1,14,7ZM2,12a1,1,0,0,0,.39-.08,1,1,0,0,0,.53-.53,1,1,0,0,0,0-.78,1,1,0,0,0-.53-.53,1,1,0,0,0-.78,0,1,1,0,0,0-.53.53,1,1,0,0,0,0,.78,1,1,0,0,0,.53.53A1,1,0,0,0,2,12ZM3,3a1,1,0,0,0,.08.39,1,1,0,0,0,.53.53,1,1,0,0,0,.78,0,1,1,0,0,0,.53-.53,1,1,0,0,0,0-.78,1,1,0,0,0-.53-.53,1,1,0,0,0-.78,0,1,1,0,0,0-.53.53A1,1,0,0,0,3,3Zm5,7a1.94,1.94,0,0,0,.78-.16A2,2,0,0,0,9.84,8.78a2,2,0,0,0,0-1.55A2,2,0,0,0,8.78,6.16a2,2,0,0,0-1.55,0A2,2,0,0,0,6.16,7.22a2,2,0,0,0,0,1.55A2,2,0,0,0,7.22,9.84,1.94,1.94,0,0,0,8,10Zm3,4a1,1,0,0,0-.08-.39,1,1,0,0,0-.53-.53,1,1,0,0,0-.78,0,1,1,0,0,0-.53.53,1,1,0,0,0,0,.78,1,1,0,0,0,.53.53,1,1,0,0,0,.78,0,1,1,0,0,0,.53-.53A1,1,0,0,0,11,14ZM12,1a1,1,0,0,0-.39.08,1,1,0,0,0-.53.53,1,1,0,0,0,0,.78,1,1,0,0,0,.53.53,1,1,0,0,0,.78,0,1,1,0,0,0,.53-.53,1,1,0,0,0,0-.78,1,1,0,0,0-.53-.53A1,1,0,0,0,12,1Zm2,9a1,1,0,0,0,.39-.08,1,1,0,0,0,.53-.53,1,1,0,0,0,0-.78,1,1,0,0,0-.53-.53,1,1,0,0,0-.78,0,1,1,0,0,0-.53.53,1,1,0,0,0,0,.78,1,1,0,0,0,.53.53A1,1,0,0,0,14,10Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
extensions/mssql/resources/dark/new_notebook_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#388a34;}</style></defs><title>new_notebook_inverse</title><path class="cls-1" d="M11.87,1.24V.33H9.13A3.78,3.78,0,0,0,7.92.52a3.48,3.48,0,0,0-1.07.58A3.6,3.6,0,0,0,5.78.52,3.78,3.78,0,0,0,4.57.33H1.83v.91H0V13.1H9.67v-.91H7a4,4,0,0,1,.47-.39A2.39,2.39,0,0,1,8,11.52a2.2,2.2,0,0,1,.53-.18,2.93,2.93,0,0,1,.61-.06h2.74V2.15h.91V9h.91V1.24Zm-9.13,0H4.57a3,3,0,0,1,1,.17,2.58,2.58,0,0,1,.85.49v8.93a3.94,3.94,0,0,0-.88-.35,3.73,3.73,0,0,0-.94-.12H2.74Zm-1.82,11v-10h.91v9.13H4.57a2.93,2.93,0,0,1,.61.06,2.55,2.55,0,0,1,.53.18,2.68,2.68,0,0,1,.49.28,3.29,3.29,0,0,1,.46.39Zm8.21-1.83a3.73,3.73,0,0,0-.94.12,4.22,4.22,0,0,0-.89.35V1.9a2.74,2.74,0,0,1,.86-.49,2.91,2.91,0,0,1,1-.17H11v9.12ZM12.87,10v2.2h-2.2v.91h3V10Z"/><polygon class="cls-2" points="16 12.19 16 13.13 13.8 13.13 13.8 15.33 12.87 15.33 12.87 13.13 10.67 13.13 10.67 12.19 12.87 12.19 12.87 9.99 13.8 9.99 13.8 12.19 16 12.19"/><path class="cls-2" d="M13.8,12.19V10h-.93v2.2h-2.2v.94h2.2v2.2h.93v-2.2H16v-.94Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#0095d7;}</style></defs><title>open_notebook_inverse</title><path class="cls-1" d="M12.55,4.21l-.08-.11h-.56l-.69.06a1.54,1.54,0,0,0-.23.29v8.69H9.18a3.32,3.32,0,0,0-.93.13,3.34,3.34,0,0,0-.87.34V4.76a2.88,2.88,0,0,1,.43-.31A5.58,5.58,0,0,1,8.29,3.3a2.63,2.63,0,0,0-.3.09A3.62,3.62,0,0,0,6.93,4a3.68,3.68,0,0,0-1.07-.57A3.58,3.58,0,0,0,4.67,3.2H2v.9H.15V15.85H13.72V5.48ZM2.86,4.1H4.67a2.61,2.61,0,0,1,1,.17,2.32,2.32,0,0,1,.86.49v8.85a3.27,3.27,0,0,0-.88-.34,3.22,3.22,0,0,0-.93-.13H2.86ZM1,15V5H2v9H4.67a3.94,3.94,0,0,1,.61.06,3.2,3.2,0,0,1,.52.18,4.19,4.19,0,0,1,.49.29,2.28,2.28,0,0,1,.45.39ZM12.8,15H7.11a2.7,2.7,0,0,1,.47-.39A2.83,2.83,0,0,1,8,14.28a3.42,3.42,0,0,1,.54-.18A3.81,3.81,0,0,1,9.18,14h2.73V5h.89Z"/><polygon class="cls-2" points="13.2 3.56 13.2 3.58 13.19 3.57 13.2 3.56"/><path class="cls-2" d="M13.19,3.57h0v0Z"/><polygon class="cls-2" points="13.2 3.56 13.2 3.58 13.19 3.57 13.2 3.56"/><polygon class="cls-2" points="14.21 1.65 14.19 1.65 14.19 1.63 14.21 1.65"/><path class="cls-2" d="M15.91,2.1,14.2,3.81l-.38.38-.62-.61v0l1-1H12.79a3.35,3.35,0,0,0-1.09.26h0a3.94,3.94,0,0,0-.86.52l-.24.21s0,0,0,0a3.3,3.3,0,0,0-.51.67,3.1,3.1,0,0,0-.26.47A3.41,3.41,0,0,0,9.5,6.11H8.6a4.68,4.68,0,0,1,.16-1.19A4.74,4.74,0,0,1,9,4.26a2.21,2.21,0,0,1,.2-.41,4.66,4.66,0,0,1,.36-.51c.1-.13.22-.26.34-.39a4.14,4.14,0,0,1,.66-.53,1.19,1.19,0,0,1,.23-.16,2.79,2.79,0,0,1,.34-.18l.31-.13.42-.14a4.32,4.32,0,0,1,1.19-.16h1.15l-1-1L13.82,0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
extensions/mssql/resources/light/cluster.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>cluster</title><path d="M14,7a1.94,1.94,0,0,1,.78.16,2,2,0,0,1,1.07,1.07,2,2,0,0,1,0,1.55,2,2,0,0,1-1.07,1.07,2,2,0,0,1-1.51,0,2.05,2.05,0,0,1-1.05-1,1.88,1.88,0,0,1-.2-.72L10.84,9a3,3,0,0,1-.56,1,3,3,0,0,1-.87.7L9.86,12H10a1.94,1.94,0,0,1,.78.16,2,2,0,0,1,1.07,1.07,2,2,0,0,1,0,1.55,2,2,0,0,1-1.07,1.07,2,2,0,0,1-1.55,0,2,2,0,0,1-1.07-1.07A2,2,0,0,1,8.25,13a2,2,0,0,1,.67-.72L8.46,11l-.23,0H8a3,3,0,0,1-1.36-.32,3,3,0,0,1-1.07-.9L4,10.58a2,2,0,0,1-.11,1.2,2,2,0,0,1-1.07,1.07A1.94,1.94,0,0,1,2,13a1.94,1.94,0,0,1-.78-.16A2,2,0,0,1,.16,11.78a2,2,0,0,1,0-1.55A2,2,0,0,1,1.22,9.16,1.94,1.94,0,0,1,2,9a2,2,0,0,1,.83.18,2,2,0,0,1,.68.51l1.63-.81A3,3,0,0,1,5.2,6.93,2.91,2.91,0,0,1,5.77,6L4.82,4.82A2,2,0,0,1,4,5a1.94,1.94,0,0,1-.78-.16A2,2,0,0,1,2.16,3.78a2,2,0,0,1,0-1.55A2,2,0,0,1,3.22,1.16a2,2,0,0,1,1.55,0A2,2,0,0,1,5.84,2.22,1.94,1.94,0,0,1,6,3a1.94,1.94,0,0,1-.4,1.2l.94,1.18a3.24,3.24,0,0,1,.71-.28A2.94,2.94,0,0,1,8,5a3,3,0,0,1,1.23.26l1.28-1.92a2,2,0,0,1-.37-.62A2,2,0,0,1,10,2a1.94,1.94,0,0,1,.16-.78A2,2,0,0,1,11.22.16a2,2,0,0,1,1.55,0,2,2,0,0,1,1.07,1.07A1.94,1.94,0,0,1,14,2a1.94,1.94,0,0,1-.16.78,2,2,0,0,1-1.07,1.07A1.94,1.94,0,0,1,12,4a2.06,2.06,0,0,1-.66-.11L10.05,5.82A3,3,0,0,1,11,8l1.17.2a2,2,0,0,1,.74-.86,2.14,2.14,0,0,1,.52-.24A1.92,1.92,0,0,1,14,7ZM2,12a1,1,0,0,0,.39-.08,1,1,0,0,0,.53-.53,1,1,0,0,0,0-.78,1,1,0,0,0-.53-.53,1,1,0,0,0-.78,0,1,1,0,0,0-.53.53,1,1,0,0,0,0,.78,1,1,0,0,0,.53.53A1,1,0,0,0,2,12ZM3,3a1,1,0,0,0,.08.39,1,1,0,0,0,.53.53,1,1,0,0,0,.78,0,1,1,0,0,0,.53-.53,1,1,0,0,0,0-.78,1,1,0,0,0-.53-.53,1,1,0,0,0-.78,0,1,1,0,0,0-.53.53A1,1,0,0,0,3,3Zm5,7a1.94,1.94,0,0,0,.78-.16A2,2,0,0,0,9.84,8.78a2,2,0,0,0,0-1.55A2,2,0,0,0,8.78,6.16a2,2,0,0,0-1.55,0A2,2,0,0,0,6.16,7.22a2,2,0,0,0,0,1.55A2,2,0,0,0,7.22,9.84,1.94,1.94,0,0,0,8,10Zm3,4a1,1,0,0,0-.08-.39,1,1,0,0,0-.53-.53,1,1,0,0,0-.78,0,1,1,0,0,0-.53.53,1,1,0,0,0,0,.78,1,1,0,0,0,.53.53,1,1,0,0,0,.78,0,1,1,0,0,0,.53-.53A1,1,0,0,0,11,14ZM12,1a1,1,0,0,0-.39.08,1,1,0,0,0-.53.53,1,1,0,0,0,0,.78,1,1,0,0,0,.53.53,1,1,0,0,0,.78,0,1,1,0,0,0,.53-.53,1,1,0,0,0,0-.78,1,1,0,0,0-.53-.53A1,1,0,0,0,12,1Zm2,9a1,1,0,0,0,.39-.08,1,1,0,0,0,.53-.53,1,1,0,0,0,0-.78,1,1,0,0,0-.53-.53,1,1,0,0,0-.78,0,1,1,0,0,0-.53.53,1,1,0,0,0,0,.78,1,1,0,0,0,.53.53A1,1,0,0,0,14,10Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
1
extensions/mssql/resources/light/hadoop.svg
Normal file
|
After Width: | Height: | Size: 38 KiB |
1
extensions/mssql/resources/light/new_notebook.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#388a34;}</style></defs><title>new_notebook</title><path d="M11.86,1.24V.33H9.13A3.78,3.78,0,0,0,7.91.52a3.48,3.48,0,0,0-1.07.58A3.6,3.6,0,0,0,5.78.52,3.78,3.78,0,0,0,4.57.33H1.83v.91H0V13.1H9.66v-.91H7a4,4,0,0,1,.47-.39A2.39,2.39,0,0,1,8,11.52a2.2,2.2,0,0,1,.53-.18,2.93,2.93,0,0,1,.61-.06h2.74V2.15h.91V9h.91V1.24Zm-9.13,0H4.57a3,3,0,0,1,1,.17,2.58,2.58,0,0,1,.85.49v8.93a3.94,3.94,0,0,0-.88-.35,3.73,3.73,0,0,0-.94-.12H2.73Zm-1.82,11v-10h.91v9.13H4.57a2.93,2.93,0,0,1,.61.06,2.55,2.55,0,0,1,.53.18,2.68,2.68,0,0,1,.49.28,3.29,3.29,0,0,1,.46.39Zm8.21-1.83a3.73,3.73,0,0,0-.94.12,4.22,4.22,0,0,0-.89.35V1.9a2.74,2.74,0,0,1,.86-.49,2.91,2.91,0,0,1,1-.17h1.82v9.12ZM12.86,10v2.2h-2.2v.91h3V10Z"/><polygon class="cls-1" points="15.99 12.19 15.99 13.13 13.79 13.13 13.79 15.33 12.87 15.33 12.87 13.13 10.66 13.13 10.66 12.19 12.87 12.19 12.87 9.99 13.79 9.99 13.79 12.19 15.99 12.19"/><path class="cls-1" d="M13.79,12.19V10h-.93v2.2h-2.2v.94h2.2v2.2h.93v-2.2H16v-.94Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
extensions/mssql/resources/light/new_spark_job.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1,.cls-2{fill:none;}.cls-1{clip-rule:evenodd;}.cls-3{clip-path:url(#clip-path);}.cls-4{fill:#e25a1c;}.cls-5{clip-path:url(#clip-path-2);}.cls-6{fill:#3c3a3e;}.cls-7{clip-path:url(#clip-path-3);}.cls-8{clip-path:url(#clip-path-4);}.cls-9{clip-path:url(#clip-path-5);}.cls-10{clip-path:url(#clip-path-6);}.cls-11{clip-path:url(#clip-path-7);}</style><clipPath id="clip-path"><path class="cls-1" d="M14.58,6.89l0-.06L14,5.7a.07.07,0,0,1,0-.09l.95-1.11a.1.1,0,0,0,0,0l-.28.07-1.15.3a.05.05,0,0,1-.07,0l-.65-1.09a.15.15,0,0,0,0-.05l-.05.29-.18,1s0,.07,0,.11,0,0-.05.06l-1.35.43-.06,0L12.14,6l0,0-.69.45a.07.07,0,0,1-.08,0l-.83-.37a.85.85,0,0,1-.32-.23.43.43,0,0,1,.1-.68,1.23,1.23,0,0,1,.28-.13l1.33-.42A.08.08,0,0,0,12,4.62l.18-1a1.78,1.78,0,0,1,.14-.54.9.9,0,0,1,.12-.18.39.39,0,0,1,.61,0,1.15,1.15,0,0,1,.16.21l.61,1a.07.07,0,0,0,.09,0l1.48-.39a.7.7,0,0,1,.31,0,.3.3,0,0,1,.25.44.84.84,0,0,1-.16.26l-1,1.21a.07.07,0,0,0,0,.09l.62,1.17a.65.65,0,0,1,.09.3.48.48,0,0,1-.42.48.87.87,0,0,1-.39,0l-.93-.28a.05.05,0,0,1,0-.05c0-.22-.07-.44-.11-.65a.14.14,0,0,1,0,0l1.07.29"/></clipPath><clipPath id="clip-path-2"><path class="cls-1" d="M14,10.07h-.84a.08.08,0,0,1-.08,0l-1-1.51,0-.06-.21,1.6h-.73l0-.21.21-1.63.21-1.56a.07.07,0,0,1,0,0l.76-.49h0l-.23,1.74h0l1.2-1.33,0,.18c0,.17.06.33.09.5a.08.08,0,0,1,0,.08l-.77.8,0,0,0,0L13.95,10l0,0h0"/></clipPath><clipPath id="clip-path-3"><path class="cls-1" d="M3.39,9.86l-.06.47-.08.61s0,0,0,0H2.59l0-.29.13-1c.05-.39.09-.77.16-1.16A1.81,1.81,0,0,1,4.29,7.1a1.42,1.42,0,0,1,1.11.18A1.24,1.24,0,0,1,6,8.22a1.66,1.66,0,0,1-.55,1.43,1.7,1.7,0,0,1-.95.47,1.4,1.4,0,0,1-1-.23Zm1.93-1.4a1.71,1.71,0,0,0,0-.22.75.75,0,0,0-.91-.49,1,1,0,0,0-.8.9A.73.73,0,0,0,4,9.42a.86.86,0,0,0,.76-.09A1,1,0,0,0,5.32,8.46Z"/></clipPath><clipPath id="clip-path-4"><path class="cls-1" d="M3.06,6.64l-.66.49L2.3,7a.51.51,0,0,0-.38-.24.43.43,0,0,0-.36.14.25.25,0,0,0,0,.33c.09.12.19.23.29.33l.5.53a1.12,1.12,0,0,1,.3.57,1.16,1.16,0,0,1-.13.75,1.43,1.43,0,0,1-1.08.76,1.42,1.42,0,0,1-.63,0,.93.93,0,0,1-.59-.52c0-.09-.08-.19-.12-.28l.72-.38L.81,9a2.14,2.14,0,0,0,.12.24.49.49,0,0,0,.64.18.7.7,0,0,0,.17-.11.37.37,0,0,0,.07-.51,2.49,2.49,0,0,0-.23-.28c-.2-.22-.4-.43-.59-.65a1,1,0,0,1-.25-.53.91.91,0,0,1,.13-.62A1.34,1.34,0,0,1,2.13,6a1,1,0,0,1,.76.4l.17.23"/></clipPath><clipPath id="clip-path-5"><path class="cls-1" d="M8.4,9.14l-.11.81a.05.05,0,0,1,0,0A1.45,1.45,0,0,1,6.56,9.7a1.31,1.31,0,0,1-.33-1A1.8,1.8,0,0,1,7.79,7.08,1.33,1.33,0,0,1,9,7.52a1.24,1.24,0,0,1,.31.9c0,.22,0,.44-.07.67s-.08.63-.12.94v0H8.48l0-.21c0-.36.1-.72.14-1.09a1.16,1.16,0,0,0-.09-.66A.64.64,0,0,0,8,7.74a1,1,0,0,0-1.09.79.75.75,0,0,0,.3.81A.82.82,0,0,0,8,9.4a1,1,0,0,0,.37-.26"/></clipPath><clipPath id="clip-path-6"><path class="cls-1" d="M11.15,7.14l-.09.68h-.41a.26.26,0,0,0-.24.17.71.71,0,0,0,0,.12l-.2,1.56-.05.39H9.45l0-.29.13-1c0-.29.07-.58.12-.87a.94.94,0,0,1,.84-.75h.57"/></clipPath><clipPath id="clip-path-7"><path class="cls-2" d="M14.28,9.68v.38h-.06V9.68h-.11V9.62h.27v.06Zm.5.38V9.69h0l-.11.37h0l-.11-.37h0v.37h-.06V9.62h.09l.1.34.1-.34h.09v.44Z"/></clipPath></defs><title>new_spark_job</title><g class="cls-3"><rect class="cls-4" x="5.65" y="-1.69" width="14.7" height="13.77"/></g><g class="cls-5"><rect class="cls-6" x="6.67" y="1.7" width="11.72" height="12.78"/></g><g class="cls-7"><rect class="cls-6" x="-1.83" y="2.64" width="12.23" height="12.75"/></g><g class="cls-8"><rect class="cls-6" x="-4.35" y="1.59" width="11.83" height="12.99"/></g><g class="cls-9"><rect class="cls-6" x="1.82" y="2.64" width="11.93" height="11.91"/></g><g class="cls-10"><rect class="cls-6" x="5.03" y="2.72" width="10.53" height="11.76"/></g><g class="cls-11"><rect class="cls-6" x="9.7" y="5.2" width="9.55" height="9.27"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
1
extensions/mssql/resources/light/open_notebook.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#00539c;}</style></defs><title>open_notebook</title><path d="M12.4,4.21l-.08-.11h-.56l-.69.06a1.54,1.54,0,0,0-.23.29v8.69H9a3.32,3.32,0,0,0-.93.13,3.34,3.34,0,0,0-.87.34V4.76a2.88,2.88,0,0,1,.43-.31A5.58,5.58,0,0,1,8.14,3.3a2.63,2.63,0,0,0-.3.09A3.62,3.62,0,0,0,6.78,4a3.68,3.68,0,0,0-1.07-.57A3.58,3.58,0,0,0,4.52,3.2H1.81v.9H0V15.85H13.57V5.48ZM2.71,4.1H4.52a2.61,2.61,0,0,1,1,.17,2.32,2.32,0,0,1,.86.49v8.85a3.27,3.27,0,0,0-.88-.34,3.22,3.22,0,0,0-.93-.13H2.71ZM.9,15V5h.91v9H4.52a3.94,3.94,0,0,1,.61.06,3.2,3.2,0,0,1,.52.18,4.19,4.19,0,0,1,.49.29,2.28,2.28,0,0,1,.45.39Zm11.75,0H7a2.7,2.7,0,0,1,.47-.39,2.83,2.83,0,0,1,.47-.29,3.42,3.42,0,0,1,.54-.18A3.81,3.81,0,0,1,9,14h2.73V5h.89Z"/><polygon class="cls-1" points="13.05 3.56 13.05 3.58 13.04 3.57 13.05 3.56"/><path class="cls-1" d="M13,3.57h0v0Z"/><polygon class="cls-1" points="13.05 3.56 13.05 3.58 13.04 3.57 13.05 3.56"/><polygon class="cls-1" points="14.06 1.65 14.04 1.65 14.04 1.63 14.06 1.65"/><path class="cls-1" d="M15.76,2.1,14,3.81l-.38.38L13,3.58v0l1-1H12.64a3.35,3.35,0,0,0-1.09.26h0a3.94,3.94,0,0,0-.86.52l-.24.21s0,0,0,0a3.3,3.3,0,0,0-.51.67,3.1,3.1,0,0,0-.26.47,3.41,3.41,0,0,0-.27,1.39h-.9a4.68,4.68,0,0,1,.16-1.19,4.74,4.74,0,0,1,.25-.66,2.21,2.21,0,0,1,.2-.41,4.66,4.66,0,0,1,.36-.51c.1-.13.22-.26.34-.39a4.14,4.14,0,0,1,.66-.53,1.19,1.19,0,0,1,.23-.16A2.79,2.79,0,0,1,11,2.08l.31-.13.42-.14a4.32,4.32,0,0,1,1.19-.16h1.15l-1-1L13.67,0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
65
extensions/mssql/src/api/mssqlapis.d.ts
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// This is the place for extensions to expose APIs.
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
/**
|
||||
* The APIs provided by Mssql extension
|
||||
*
|
||||
* @export
|
||||
* @interface MssqlExtensionApi
|
||||
*/
|
||||
export interface MssqlExtensionApi {
|
||||
/**
|
||||
* Gets the object explorer API that supports querying over the connections supported by this extension
|
||||
*
|
||||
* @returns {IMssqlObjectExplorerBrowser}
|
||||
* @memberof IMssqlExtensionApi
|
||||
*/
|
||||
getMssqlObjectExplorerBrowser(): MssqlObjectExplorerBrowser;
|
||||
}
|
||||
|
||||
/**
|
||||
* A browser supporting actions over the object explorer connections provided by this extension.
|
||||
* Currently this is the
|
||||
*
|
||||
* @export
|
||||
* @interface MssqlObjectExplorerBrowser
|
||||
*/
|
||||
export interface MssqlObjectExplorerBrowser {
|
||||
/**
|
||||
* Gets the matching node given a context object, e.g. one from a right-click on a node in Object Explorer
|
||||
*
|
||||
* @param {sqlops.ObjectExplorerContext} objectExplorerContext
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
getNode<T extends ITreeNode>(objectExplorerContext: sqlops.ObjectExplorerContext): Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A tree node in the object explorer tree
|
||||
*
|
||||
* @export
|
||||
* @interface ITreeNode
|
||||
*/
|
||||
export interface ITreeNode {
|
||||
getNodeInfo(): sqlops.NodeInfo;
|
||||
getChildren(refreshChildren: boolean): ITreeNode[] | Promise<ITreeNode[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A HDFS file node. This is a leaf node in the object explorer tree, and its contents
|
||||
* can be queried
|
||||
*
|
||||
* @export
|
||||
* @interface IFileNode
|
||||
* @extends {ITreeNode}
|
||||
*/
|
||||
export interface IFileNode extends ITreeNode {
|
||||
getFileContentsAsString(maxBytes?: number): Promise<string>;
|
||||
}
|
||||
119
extensions/mssql/src/apiWrapper.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as sqlops from 'sqlops';
|
||||
|
||||
/**
|
||||
* Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into
|
||||
* this API from our code
|
||||
*
|
||||
* @export
|
||||
* @class ApiWrapper
|
||||
*/
|
||||
export class ApiWrapper {
|
||||
// Data APIs
|
||||
public registerConnectionProvider(provider: sqlops.ConnectionProvider): vscode.Disposable {
|
||||
return sqlops.dataprotocol.registerConnectionProvider(provider);
|
||||
}
|
||||
|
||||
public registerObjectExplorerNodeProvider(provider: sqlops.ObjectExplorerNodeProvider): vscode.Disposable {
|
||||
return sqlops.dataprotocol.registerObjectExplorerNodeProvider(provider);
|
||||
}
|
||||
|
||||
public registerTaskServicesProvider(provider: sqlops.TaskServicesProvider): vscode.Disposable {
|
||||
return sqlops.dataprotocol.registerTaskServicesProvider(provider);
|
||||
}
|
||||
|
||||
public registerFileBrowserProvider(provider: sqlops.FileBrowserProvider): vscode.Disposable {
|
||||
return sqlops.dataprotocol.registerFileBrowserProvider(provider);
|
||||
}
|
||||
|
||||
public createDialog(title: string): sqlops.window.modelviewdialog.Dialog {
|
||||
return sqlops.window.modelviewdialog.createDialog(title);
|
||||
}
|
||||
|
||||
public openDialog(dialog: sqlops.window.modelviewdialog.Dialog): void {
|
||||
return sqlops.window.modelviewdialog.openDialog(dialog);
|
||||
}
|
||||
|
||||
public closeDialog(dialog: sqlops.window.modelviewdialog.Dialog): void {
|
||||
return sqlops.window.modelviewdialog.closeDialog(dialog);
|
||||
}
|
||||
|
||||
public registerTaskHandler(taskId: string, handler: (profile: sqlops.IConnectionProfile) => void): void {
|
||||
sqlops.tasks.registerTask(taskId, handler);
|
||||
}
|
||||
|
||||
public startBackgroundOperation(operationInfo: sqlops.BackgroundOperationInfo): void {
|
||||
sqlops.tasks.startBackgroundOperation(operationInfo);
|
||||
}
|
||||
|
||||
public getActiveConnections(): Thenable<sqlops.connection.Connection[]> {
|
||||
return sqlops.connection.getActiveConnections();
|
||||
}
|
||||
|
||||
// VSCode APIs
|
||||
public executeCommand(command: string, ...rest: any[]): Thenable<any> {
|
||||
return vscode.commands.executeCommand(command, ...rest);
|
||||
}
|
||||
|
||||
public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable {
|
||||
return vscode.commands.registerCommand(command, callback, thisArg);
|
||||
}
|
||||
|
||||
public showErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
return vscode.window.showErrorMessage(message, ...items);
|
||||
}
|
||||
|
||||
public showWarningMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
return vscode.window.showWarningMessage(message, ...items);
|
||||
}
|
||||
|
||||
public showInformationMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
return vscode.window.showInformationMessage(message, ...items);
|
||||
}
|
||||
|
||||
public showOpenDialog(options: vscode.OpenDialogOptions): Thenable<vscode.Uri[] | undefined> {
|
||||
return vscode.window.showOpenDialog(options);
|
||||
}
|
||||
|
||||
public showSaveDialog(options: vscode.SaveDialogOptions): Thenable<vscode.Uri> {
|
||||
return vscode.window.showSaveDialog(options);
|
||||
}
|
||||
|
||||
public openTextDocument(uri: vscode.Uri): Thenable<vscode.TextDocument>;
|
||||
public openTextDocument(options: { language?: string; content?: string; }): Thenable<vscode.TextDocument>;
|
||||
public openTextDocument(uriOrOptions): Thenable<vscode.TextDocument> {
|
||||
return vscode.workspace.openTextDocument(uriOrOptions);
|
||||
}
|
||||
|
||||
public showTextDocument(document: vscode.TextDocument, column?: vscode.ViewColumn, preserveFocus?: boolean, preview?: boolean): Thenable<vscode.TextEditor> {
|
||||
let options: vscode.TextDocumentShowOptions = {
|
||||
viewColumn: column,
|
||||
preserveFocus: preserveFocus,
|
||||
preview: preview
|
||||
};
|
||||
return vscode.window.showTextDocument(document, options);
|
||||
}
|
||||
|
||||
public get workspaceFolders(): vscode.WorkspaceFolder[] {
|
||||
return vscode.workspace.workspaceFolders;
|
||||
}
|
||||
|
||||
public createStatusBarItem(alignment?: vscode.StatusBarAlignment, priority?: number): vscode.StatusBarItem {
|
||||
return vscode.window.createStatusBarItem(alignment, priority);
|
||||
}
|
||||
|
||||
public createOutputChannel(name: string): vscode.OutputChannel {
|
||||
return vscode.window.createOutputChannel(name);
|
||||
}
|
||||
|
||||
public createTab(title: string): sqlops.window.modelviewdialog.DialogTab {
|
||||
return sqlops.window.modelviewdialog.createTab(title);
|
||||
}
|
||||
}
|
||||
28
extensions/mssql/src/appContext.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { ApiWrapper } from './apiWrapper';
|
||||
|
||||
/**
|
||||
* Global context for the application
|
||||
*/
|
||||
export class AppContext {
|
||||
|
||||
private serviceMap: Map<string, any> = new Map();
|
||||
constructor(public readonly extensionContext: vscode.ExtensionContext, public readonly apiWrapper: ApiWrapper) {
|
||||
this.apiWrapper = apiWrapper || new ApiWrapper();
|
||||
}
|
||||
|
||||
public getService<T>(serviceName: string): T {
|
||||
return this.serviceMap.get(serviceName) as T;
|
||||
}
|
||||
|
||||
public registerService<T>(serviceName: string, service: T): void {
|
||||
this.serviceMap.set(serviceName, service);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
|
||||
"version": "1.5.0-alpha.68",
|
||||
"version": "1.5.0-alpha.71",
|
||||
"downloadFileNames": {
|
||||
"Windows_86": "win-x86-netcoreapp2.2.zip",
|
||||
"Windows_64": "win-x64-netcoreapp2.2.zip",
|
||||
|
||||
@@ -10,3 +10,65 @@ export const serviceCrashMessage = 'SQL Tools Service component exited unexpecte
|
||||
export const serviceCrashButton = 'View Known Issues';
|
||||
export const serviceCrashLink = 'https://github.com/Microsoft/vscode-mssql/wiki/SqlToolsService-Known-Issues';
|
||||
export const extensionConfigSectionName = 'mssql';
|
||||
|
||||
// DATA PROTOCOL VALUES ///////////////////////////////////////////////////////////
|
||||
export const mssqlClusterProviderName = 'mssqlCluster';
|
||||
export const hadoopKnoxEndpointName = 'Knox';
|
||||
export const protocolVersion = '1.0';
|
||||
export const hostPropName = 'host';
|
||||
export const userPropName = 'user';
|
||||
export const knoxPortPropName = 'knoxport';
|
||||
export const passwordPropName = 'password';
|
||||
export const groupIdPropName = 'groupId';
|
||||
export const defaultKnoxPort = '30443';
|
||||
export const groupIdName = 'groupId';
|
||||
export const sqlProviderName = 'MSSQL';
|
||||
export const dataService = 'Data Services';
|
||||
|
||||
export const hdfsHost = 'host';
|
||||
export const hdfsUser = 'user';
|
||||
export const UNTITLED_SCHEMA = 'untitled';
|
||||
|
||||
export const hadoopConnectionTimeoutSeconds = 15;
|
||||
export const hdfsRootPath = '/';
|
||||
|
||||
export const clusterEndpointsProperty = 'clusterEndpoints';
|
||||
export const isBigDataClusterProperty = 'isBigDataCluster';
|
||||
|
||||
// SERVICE NAMES //////////////////////////////////////////////////////////
|
||||
export const ObjectExplorerService = 'objectexplorer';
|
||||
export const objectExplorerPrefix: string = 'objectexplorer://';
|
||||
export const ViewType = 'view';
|
||||
|
||||
export enum BuiltInCommands {
|
||||
SetContext = 'setContext'
|
||||
}
|
||||
|
||||
export enum CommandContext {
|
||||
WizardServiceEnabled = 'wizardservice:enabled'
|
||||
}
|
||||
|
||||
export enum MssqlClusterItems {
|
||||
Connection = 'mssqlCluster:connection',
|
||||
Folder = 'mssqlCluster:folder',
|
||||
File = 'mssqlCluster:file',
|
||||
Error = 'mssqlCluster:error'
|
||||
}
|
||||
|
||||
export enum MssqlClusterItemsSubType {
|
||||
Spark = 'mssqlCluster:spark'
|
||||
}
|
||||
|
||||
// SPARK JOB SUBMISSION //////////////////////////////////////////////////////////
|
||||
export const mssqlClusterNewNotebookTask = 'mssqlCluster.task.newNotebook';
|
||||
export const mssqlClusterOpenNotebookTask = 'mssqlCluster.task.openNotebook';
|
||||
export const mssqlClusterLivySubmitSparkJobCommand = 'mssqlCluster.livy.cmd.submitSparkJob';
|
||||
export const mssqlClusterLivySubmitSparkJobFromFileCommand = 'mssqlCluster.livy.cmd.submitFileToSparkJob';
|
||||
export const mssqlClusterLivySubmitSparkJobTask = 'mssqlCluster.livy.task.submitSparkJob';
|
||||
export const mssqlClusterLivyOpenSparkHistory = 'mssqlCluster.livy.task.openSparkHistory';
|
||||
export const mssqlClusterLivyOpenYarnHistory = 'mssqlCluster.livy.task.openYarnHistory';
|
||||
export const mssqlClusterLivySubmitPath = '/gateway/default/livy/v1/batches';
|
||||
export const mssqlClusterLivyTimeInMSForCheckYarnApp = 1000;
|
||||
export const mssqlClusterLivyRetryTimesForCheckYarnApp = 20;
|
||||
export const mssqlClusterSparkJobFileSelectorButtonWidth = '30px';
|
||||
export const mssqlClusterSparkJobFileSelectorButtonHeight = '30px';
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
|
||||
import * as sqlops from 'sqlops';
|
||||
|
||||
import * as types from './types';
|
||||
import * as Constants from './constants';
|
||||
|
||||
export enum BuiltInCommands {
|
||||
SetContext = 'setContext',
|
||||
@@ -14,7 +15,9 @@ export enum BuiltInCommands {
|
||||
|
||||
export enum ContextKeys {
|
||||
ISCLOUD = 'mssql:iscloud',
|
||||
EDITIONID = 'mssql:engineedition'
|
||||
EDITIONID = 'mssql:engineedition',
|
||||
ISCLUSTER = 'mssql:iscluster',
|
||||
SERVERMAJORVERSION = 'mssql:servermajorversion'
|
||||
}
|
||||
|
||||
const isCloudEditions = [
|
||||
@@ -37,6 +40,8 @@ export default class ContextProvider {
|
||||
public onDashboardOpen(e: sqlops.DashboardDocument): void {
|
||||
let iscloud: boolean;
|
||||
let edition: number;
|
||||
let isCluster: boolean = false;
|
||||
let serverMajorVersion: number;
|
||||
if (e.profile.providerName.toLowerCase() === 'mssql' && !types.isUndefinedOrNull(e.serverInfo) && !types.isUndefinedOrNull(e.serverInfo.engineEditionId)) {
|
||||
if (isCloudEditions.some(i => i === e.serverInfo.engineEditionId)) {
|
||||
iscloud = true;
|
||||
@@ -45,6 +50,14 @@ export default class ContextProvider {
|
||||
}
|
||||
|
||||
edition = e.serverInfo.engineEditionId;
|
||||
|
||||
if (!types.isUndefinedOrNull(e.serverInfo.options)) {
|
||||
let isBigDataCluster = e.serverInfo.options[Constants.isBigDataClusterProperty];
|
||||
if (isBigDataCluster) {
|
||||
isCluster = isBigDataCluster;
|
||||
}
|
||||
}
|
||||
serverMajorVersion = e.serverInfo.serverMajorVersion;
|
||||
}
|
||||
|
||||
if (iscloud === true || iscloud === false) {
|
||||
@@ -54,6 +67,14 @@ export default class ContextProvider {
|
||||
if (!types.isUndefinedOrNull(edition)) {
|
||||
setCommandContext(ContextKeys.EDITIONID, edition);
|
||||
}
|
||||
|
||||
if (!types.isUndefinedOrNull(isCluster)) {
|
||||
setCommandContext(ContextKeys.ISCLUSTER, isCluster);
|
||||
}
|
||||
|
||||
if (!types.isUndefinedOrNull(serverMajorVersion)) {
|
||||
setCommandContext(ContextKeys.SERVERMAJORVERSION, serverMajorVersion);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
@@ -339,6 +339,13 @@ export interface GenerateDeployScriptParams {
|
||||
taskExecutionMode: TaskExecutionMode;
|
||||
}
|
||||
|
||||
export interface GenerateDeployPlanParams {
|
||||
packageFilePath: string;
|
||||
databaseName: string;
|
||||
ownerUri: string;
|
||||
taskExecutionMode: TaskExecutionMode;
|
||||
}
|
||||
|
||||
export namespace ExportRequest {
|
||||
export const type = new RequestType<ExportParams, sqlops.DacFxResult, void, void>('dacfx/export');
|
||||
}
|
||||
@@ -359,4 +366,7 @@ export namespace GenerateDeployScriptRequest {
|
||||
export const type = new RequestType<GenerateDeployScriptParams, sqlops.DacFxResult, void, void>('dacfx/generateDeploymentScript');
|
||||
}
|
||||
|
||||
export namespace GenerateDeployPlanRequest {
|
||||
export const type = new RequestType<GenerateDeployPlanParams, sqlops.GenerateDeployPlanResult, void, void>('dacfx/generateDeployPlan');
|
||||
}
|
||||
// ------------------------------- < DacFx > ------------------------------------
|
||||
3
extensions/mssql/src/escapeException.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
export default require('error-ex')('EscapeException');
|
||||
@@ -113,7 +113,20 @@ export class DacFxServicesFeature extends SqlOpsFeature<undefined> {
|
||||
return r;
|
||||
},
|
||||
e => {
|
||||
client.logFailedRequest(contracts.DeployRequest.type, e);
|
||||
client.logFailedRequest(contracts.GenerateDeployScriptRequest.type, e);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
let generateDeployPlan = (packageFilePath: string, targetDatabaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.GenerateDeployPlanResult> => {
|
||||
let params: contracts.GenerateDeployPlanParams = { packageFilePath: packageFilePath, databaseName: targetDatabaseName, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode };
|
||||
return client.sendRequest(contracts.GenerateDeployPlanRequest.type, params).then(
|
||||
r => {
|
||||
return r;
|
||||
},
|
||||
e => {
|
||||
client.logFailedRequest(contracts.GenerateDeployPlanRequest.type, e);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
);
|
||||
@@ -125,7 +138,8 @@ export class DacFxServicesFeature extends SqlOpsFeature<undefined> {
|
||||
importBacpac,
|
||||
extractDacpac,
|
||||
deployDacpac,
|
||||
generateDeployScript
|
||||
generateDeployScript,
|
||||
generateDeployPlan
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
27
extensions/mssql/src/localizedConstants.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
// HDFS Constants //////////////////////////////////////////////////////////
|
||||
export const msgMissingNodeContext = localize('msgMissingNodeContext', 'Node Command called without any node passed');
|
||||
|
||||
// Spark Job Submission Constants //////////////////////////////////////////
|
||||
export const sparkLocalFileDestinationHint = localize('sparkJobSubmission_LocalFileDestinationHint', 'Local file will be uploaded to HDFS. ');
|
||||
export const sparkJobSubmissionEndMessage = localize('sparkJobSubmission_SubmissionEndMessage', '.......................... Submit Spark Job End ............................');
|
||||
export function sparkJobSubmissionPrepareUploadingFile(localPath: string, clusterFolder: string): string { return localize('sparkJobSubmission_PrepareUploadingFile', 'Uploading file from local {0} to HDFS folder: {1}', localPath, clusterFolder); }
|
||||
export const sparkJobSubmissionUploadingFileSucceeded = localize('sparkJobSubmission_UploadingFileSucceeded', 'Upload file to cluster Succeeded!');
|
||||
export function sparkJobSubmissionUploadingFileFailed(err: string): string { return localize('sparkJobSubmission_UploadingFileFailed', 'Upload file to cluster Failed. {0}', err); }
|
||||
export function sparkJobSubmissionPrepareSubmitJob(jobName: string): string { return localize('sparkJobSubmission_PrepareSubmitJob', 'Submitting job {0} ... ', jobName); }
|
||||
export const sparkJobSubmissionSparkJobHasBeenSubmitted = localize('sparkJobSubmission_SubmitJobFinished', 'The Spark Job has been submitted.');
|
||||
export function sparkJobSubmissionSubmitJobFailed(err: string): string { return localize('sparkJobSubmission_SubmitJobFailed', 'Spark Job Submission Failed. {0} ', err); }
|
||||
export function sparkJobSubmissionYarnUIMessage(yarnUIURL: string): string { return localize('sparkJobSubmission_YarnUIMessage', 'YarnUI Url: {0} ', yarnUIURL); }
|
||||
export function sparkJobSubmissionSparkHistoryLinkMessage(sparkHistoryLink: string): string { return localize('sparkJobSubmission_SparkHistoryLinkMessage', 'Spark History Url: {0} ', sparkHistoryLink); }
|
||||
export function sparkJobSubmissionGetApplicationIdFailed(err: string): string { return localize('sparkJobSubmission_GetApplicationIdFailed', 'Get Application Id Failed. {0}', err); }
|
||||
export function sparkJobSubmissionLocalFileNotExisted(path: string): string { return localize('sparkJobSubmission_LocalFileNotExisted', 'Local file {0} does not existed. ', path); }
|
||||
export const sparkJobSubmissionNoSqlBigDataClusterFound = localize('sparkJobSubmission_NoSqlBigDataClusterFound','No Sql Server Big Data Cluster found.');
|
||||
@@ -5,7 +5,12 @@
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { SqlOpsDataClient, ClientOptions } from 'dataprotocol-client';
|
||||
import { IConfig, ServerProvider, Events } from 'service-downloader';
|
||||
import { ServerOptions, TransportKind } from 'vscode-languageclient';
|
||||
@@ -17,12 +22,25 @@ import { AzureResourceProvider } from './resourceProvider/resourceProvider';
|
||||
import * as Utils from './utils';
|
||||
import { Telemetry, LanguageClientErrorHandler } from './telemetry';
|
||||
import { TelemetryFeature, AgentServicesFeature, DacFxServicesFeature } from './features';
|
||||
import { AppContext } from './appContext';
|
||||
import { ApiWrapper } from './apiWrapper';
|
||||
import { UploadFilesCommand, MkDirCommand, SaveFileCommand, PreviewFileCommand, CopyPathCommand, DeleteFilesCommand } from './objectExplorerNodeProvider/hdfsCommands';
|
||||
import { IPrompter } from './prompts/question';
|
||||
import CodeAdapter from './prompts/adapter';
|
||||
import { MssqlExtensionApi, MssqlObjectExplorerBrowser } from './api/mssqlapis';
|
||||
import { OpenSparkJobSubmissionDialogCommand, OpenSparkJobSubmissionDialogFromFileCommand, OpenSparkJobSubmissionDialogTask } from './sparkFeature/dialog/dialogCommands';
|
||||
import { OpenSparkYarnHistoryTask } from './sparkFeature/historyTask';
|
||||
import { MssqlObjectExplorerNodeProvider, mssqlOutputChannel } from './objectExplorerNodeProvider/objectExplorerNodeProvider';
|
||||
|
||||
const baseConfig = require('./config.json');
|
||||
const outputChannel = vscode.window.createOutputChannel(Constants.serviceName);
|
||||
const statusView = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
|
||||
const jupyterNotebookProviderId = 'jupyter';
|
||||
const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', 'This sample code loads the file into a data frame and shows the first 10 results.');
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
let untitledCounter = 0;
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext): Promise<MssqlExtensionApi> {
|
||||
// lets make sure we support this platform first
|
||||
let supported = await Utils.verifyPlatform();
|
||||
|
||||
@@ -61,6 +79,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
outputChannel: new CustomOutputChannel()
|
||||
};
|
||||
|
||||
let prompter: IPrompter = new CodeAdapter();
|
||||
let appContext = new AppContext(context, new ApiWrapper());
|
||||
|
||||
const installationStart = Date.now();
|
||||
serverdownloader.getOrDownloadServer().then(e => {
|
||||
const installationComplete = Date.now();
|
||||
@@ -85,6 +106,11 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
languageClient.start();
|
||||
credentialsStore.start();
|
||||
resourceProvider.start();
|
||||
|
||||
let nodeProvider = new MssqlObjectExplorerNodeProvider(appContext);
|
||||
sqlops.dataprotocol.registerObjectExplorerNodeProvider(nodeProvider);
|
||||
activateSparkFeatures(appContext);
|
||||
activateNotebookTask(appContext);
|
||||
}, e => {
|
||||
Telemetry.sendTelemetryEvent('ServiceInitializingFailed');
|
||||
vscode.window.showErrorMessage('Failed to start Sql tools service');
|
||||
@@ -94,7 +120,112 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(contextProvider);
|
||||
context.subscriptions.push(credentialsStore);
|
||||
context.subscriptions.push(resourceProvider);
|
||||
context.subscriptions.push(new UploadFilesCommand(prompter, appContext));
|
||||
context.subscriptions.push(new MkDirCommand(prompter, appContext));
|
||||
context.subscriptions.push(new SaveFileCommand(prompter, appContext));
|
||||
context.subscriptions.push(new PreviewFileCommand(prompter, appContext));
|
||||
context.subscriptions.push(new CopyPathCommand(appContext));
|
||||
context.subscriptions.push(new DeleteFilesCommand(prompter, appContext));
|
||||
context.subscriptions.push({ dispose: () => languageClient.stop() });
|
||||
|
||||
let api: MssqlExtensionApi = {
|
||||
getMssqlObjectExplorerBrowser(): MssqlObjectExplorerBrowser {
|
||||
return {
|
||||
getNode: (context: sqlops.ObjectExplorerContext) => {
|
||||
let oeProvider = appContext.getService<MssqlObjectExplorerNodeProvider>(Constants.ObjectExplorerService);
|
||||
return <any>oeProvider.findSqlClusterNodeByContext(context);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
return api;
|
||||
}
|
||||
|
||||
function activateSparkFeatures(appContext: AppContext): void {
|
||||
let extensionContext = appContext.extensionContext;
|
||||
let apiWrapper = appContext.apiWrapper;
|
||||
let outputChannel: vscode.OutputChannel = mssqlOutputChannel;
|
||||
extensionContext.subscriptions.push(new OpenSparkJobSubmissionDialogCommand(appContext, outputChannel));
|
||||
extensionContext.subscriptions.push(new OpenSparkJobSubmissionDialogFromFileCommand(appContext, outputChannel));
|
||||
apiWrapper.registerTaskHandler(Constants.mssqlClusterLivySubmitSparkJobTask, (profile: sqlops.IConnectionProfile) => {
|
||||
new OpenSparkJobSubmissionDialogTask(appContext, outputChannel).execute(profile);
|
||||
});
|
||||
apiWrapper.registerTaskHandler(Constants.mssqlClusterLivyOpenSparkHistory, (profile: sqlops.IConnectionProfile) => {
|
||||
new OpenSparkYarnHistoryTask(appContext).execute(profile, true);
|
||||
});
|
||||
apiWrapper.registerTaskHandler(Constants.mssqlClusterLivyOpenYarnHistory, (profile: sqlops.IConnectionProfile) => {
|
||||
new OpenSparkYarnHistoryTask(appContext).execute(profile, false);
|
||||
});
|
||||
}
|
||||
|
||||
function activateNotebookTask(appContext: AppContext): void {
|
||||
let apiWrapper = appContext.apiWrapper;
|
||||
apiWrapper.registerTaskHandler(Constants.mssqlClusterNewNotebookTask, (profile: sqlops.IConnectionProfile) => {
|
||||
return saveProfileAndCreateNotebook(profile);
|
||||
});
|
||||
apiWrapper.registerTaskHandler(Constants.mssqlClusterOpenNotebookTask, (profile: sqlops.IConnectionProfile) => {
|
||||
return handleOpenNotebookTask(profile);
|
||||
});
|
||||
}
|
||||
|
||||
function saveProfileAndCreateNotebook(profile: sqlops.IConnectionProfile): Promise<void> {
|
||||
return handleNewNotebookTask(undefined, profile);
|
||||
}
|
||||
|
||||
async function handleNewNotebookTask(oeContext?: sqlops.ObjectExplorerContext, profile?: sqlops.IConnectionProfile): Promise<void> {
|
||||
// Ensure we get a unique ID for the notebook. For now we're using a different prefix to the built-in untitled files
|
||||
// to handle this. We should look into improving this in the future
|
||||
let untitledUri = vscode.Uri.parse(`untitled:Notebook-${untitledCounter++}`);
|
||||
let editor = await sqlops.nb.showNotebookDocument(untitledUri, {
|
||||
connectionId: profile.id,
|
||||
providerId: jupyterNotebookProviderId,
|
||||
preview: false,
|
||||
defaultKernel: {
|
||||
name: 'pyspark3kernel',
|
||||
display_name: 'PySpark3',
|
||||
language: 'python'
|
||||
}
|
||||
});
|
||||
if (oeContext && oeContext.nodeInfo && oeContext.nodeInfo.nodePath) {
|
||||
// Get the file path after '/HDFS'
|
||||
let hdfsPath: string = oeContext.nodeInfo.nodePath.substring(oeContext.nodeInfo.nodePath.indexOf('/HDFS') + '/HDFS'.length);
|
||||
if (hdfsPath.length > 0) {
|
||||
let analyzeCommand = "#" + msgSampleCodeDataFrame + os.EOL + "df = (spark.read.option(\"inferSchema\", \"true\")"
|
||||
+ os.EOL + ".option(\"header\", \"true\")" + os.EOL + ".csv('{0}'))" + os.EOL + "df.show(10)";
|
||||
editor.edit(editBuilder => {
|
||||
editBuilder.replace(0, {
|
||||
cell_type: 'code',
|
||||
source: analyzeCommand.replace('{0}', hdfsPath)
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOpenNotebookTask(profile: sqlops.IConnectionProfile): Promise<void> {
|
||||
let notebookFileTypeName = localize('notebookFileType', 'Notebooks');
|
||||
let filter = {};
|
||||
filter[notebookFileTypeName] = 'ipynb';
|
||||
let uris = await vscode.window.showOpenDialog({
|
||||
filters: filter,
|
||||
canSelectFiles: true,
|
||||
canSelectMany: false
|
||||
});
|
||||
if (uris && uris.length > 0) {
|
||||
let fileUri = uris[0];
|
||||
// Verify this is a .ipynb file since this isn't actually filtered on Mac/Linux
|
||||
if (path.extname(fileUri.fsPath) !== '.ipynb') {
|
||||
// in the future might want additional supported types
|
||||
vscode.window.showErrorMessage(localize('unsupportedFileType', 'Only .ipynb Notebooks are supported'));
|
||||
} else {
|
||||
await sqlops.nb.showNotebookDocument(fileUri, {
|
||||
connectionId: profile.id,
|
||||
providerId: jupyterNotebookProviderId,
|
||||
preview: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateServerOptions(executablePath: string): ServerOptions {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Transform } from 'stream';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class CancelableStream extends Transform {
|
||||
constructor(private cancelationToken: vscode.CancellationTokenSource) {
|
||||
super();
|
||||
}
|
||||
|
||||
public _transform(chunk: any, encoding: string, callback: Function): void {
|
||||
if (this.cancelationToken && this.cancelationToken.token.isCancellationRequested) {
|
||||
callback(new Error(localize('streamCanceled', 'Stream operation canceled by the user')));
|
||||
} else {
|
||||
this.push(chunk);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
176
extensions/mssql/src/objectExplorerNodeProvider/command.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import * as vscode from 'vscode';
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { ApiWrapper } from '../apiWrapper';
|
||||
import { TreeNode } from './treeNodes';
|
||||
import { QuestionTypes, IPrompter, IQuestion } from '../prompts/question';
|
||||
import * as utils from '../utils';
|
||||
import * as constants from '../constants';
|
||||
import { AppContext } from '../appContext';
|
||||
|
||||
export interface ICommandContextParsingOptions {
|
||||
editor: boolean;
|
||||
uri: boolean;
|
||||
}
|
||||
|
||||
export interface ICommandBaseContext {
|
||||
command: string;
|
||||
editor?: vscode.TextEditor;
|
||||
uri?: vscode.Uri;
|
||||
}
|
||||
|
||||
export interface ICommandUnknownContext extends ICommandBaseContext {
|
||||
type: 'unknown';
|
||||
}
|
||||
|
||||
export interface ICommandUriContext extends ICommandBaseContext {
|
||||
type: 'uri';
|
||||
}
|
||||
|
||||
export interface ICommandViewContext extends ICommandBaseContext {
|
||||
type: 'view';
|
||||
node: TreeNode;
|
||||
}
|
||||
|
||||
export interface ICommandObjectExplorerContext extends ICommandBaseContext {
|
||||
type: 'objectexplorer';
|
||||
explorerContext: sqlops.ObjectExplorerContext;
|
||||
}
|
||||
|
||||
export type CommandContext = ICommandObjectExplorerContext | ICommandViewContext | ICommandUriContext | ICommandUnknownContext;
|
||||
|
||||
function isTextEditor(editor: any): editor is vscode.TextEditor {
|
||||
if (editor === undefined) { return false; }
|
||||
|
||||
return editor.id !== undefined && ((editor as vscode.TextEditor).edit !== undefined || (editor as vscode.TextEditor).document !== undefined);
|
||||
}
|
||||
|
||||
export abstract class Command extends vscode.Disposable {
|
||||
|
||||
|
||||
protected readonly contextParsingOptions: ICommandContextParsingOptions = { editor: false, uri: false };
|
||||
|
||||
private disposable: vscode.Disposable;
|
||||
|
||||
constructor(command: string | string[], protected appContext: AppContext) {
|
||||
super(() => this.dispose());
|
||||
|
||||
if (typeof command === 'string') {
|
||||
this.disposable = this.apiWrapper.registerCommand(command, (...args: any[]) => this._execute(command, ...args), this);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const subscriptions = command.map(cmd => this.apiWrapper.registerCommand(cmd, (...args: any[]) => this._execute(cmd, ...args), this));
|
||||
this.disposable = vscode.Disposable.from(...subscriptions);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposable && this.disposable.dispose();
|
||||
}
|
||||
|
||||
protected get apiWrapper(): ApiWrapper {
|
||||
return this.appContext.apiWrapper;
|
||||
}
|
||||
|
||||
protected async preExecute(...args: any[]): Promise<any> {
|
||||
return this.execute(...args);
|
||||
}
|
||||
|
||||
abstract execute(...args: any[]): any;
|
||||
|
||||
protected _execute(command: string, ...args: any[]): any {
|
||||
// TODO consider using Telemetry.trackEvent(command);
|
||||
|
||||
const [context, rest] = Command.parseContext(command, this.contextParsingOptions, ...args);
|
||||
return this.preExecute(context, ...rest);
|
||||
}
|
||||
|
||||
private static parseContext(command: string, options: ICommandContextParsingOptions, ...args: any[]): [CommandContext, any[]] {
|
||||
let editor: vscode.TextEditor | undefined = undefined;
|
||||
|
||||
let firstArg = args[0];
|
||||
if (options.editor && (firstArg === undefined || isTextEditor(firstArg))) {
|
||||
editor = firstArg;
|
||||
args = args.slice(1);
|
||||
firstArg = args[0];
|
||||
}
|
||||
|
||||
if (options.uri && (firstArg === undefined || firstArg instanceof vscode.Uri)) {
|
||||
const [uri, ...rest] = args as [vscode.Uri, any];
|
||||
return [{ command: command, type: 'uri', editor: editor, uri: uri }, rest];
|
||||
}
|
||||
|
||||
if (firstArg instanceof TreeNode) {
|
||||
const [node, ...rest] = args as [TreeNode, any];
|
||||
return [{ command: command, type: constants.ViewType, node: node }, rest];
|
||||
}
|
||||
|
||||
if (firstArg && utils.isObjectExplorerContext(firstArg)) {
|
||||
const [explorerContext, ...rest] = args as [sqlops.ObjectExplorerContext, any];
|
||||
return [{ command: command, type: constants.ObjectExplorerService, explorerContext: explorerContext }, rest];
|
||||
}
|
||||
|
||||
return [{ command: command, type: 'unknown', editor: editor }, args];
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class ProgressCommand extends Command {
|
||||
static progressId = 0;
|
||||
constructor(private command: string, protected prompter: IPrompter, appContext: AppContext) {
|
||||
super(command, appContext);
|
||||
}
|
||||
|
||||
protected async executeWithProgress(
|
||||
execution: (cancelToken: vscode.CancellationTokenSource) => Promise<void>,
|
||||
label: string,
|
||||
isCancelable: boolean = false,
|
||||
onCanceled?: () => void
|
||||
): Promise<void> {
|
||||
let disposables: vscode.Disposable[] = [];
|
||||
const tokenSource = new vscode.CancellationTokenSource();
|
||||
const statusBarItem = this.apiWrapper.createStatusBarItem(vscode.StatusBarAlignment.Left);
|
||||
disposables.push(vscode.Disposable.from(statusBarItem));
|
||||
statusBarItem.text = localize('progress', '$(sync~spin) {0}...', label);
|
||||
if (isCancelable) {
|
||||
const cancelCommandId = `cancelProgress${ProgressCommand.progressId++}`;
|
||||
disposables.push(this.apiWrapper.registerCommand(cancelCommandId, async () => {
|
||||
if (await this.confirmCancel()) {
|
||||
tokenSource.cancel();
|
||||
}
|
||||
}));
|
||||
statusBarItem.tooltip = localize('cancelTooltip', 'Cancel');
|
||||
statusBarItem.command = cancelCommandId;
|
||||
}
|
||||
statusBarItem.show();
|
||||
|
||||
try {
|
||||
await execution(tokenSource);
|
||||
} catch (error) {
|
||||
if (isCancelable && onCanceled && tokenSource.token.isCancellationRequested) {
|
||||
// The error can be assumed to be due to cancelation occurring. Do the callback
|
||||
onCanceled();
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
private async confirmCancel(): Promise<boolean> {
|
||||
return await this.prompter.promptSingle<boolean>(<IQuestion>{
|
||||
type: QuestionTypes.confirm,
|
||||
message: localize('cancel', 'Cancel operation?'),
|
||||
default: true
|
||||
});
|
||||
}
|
||||
}
|
||||
114
extensions/mssql/src/objectExplorerNodeProvider/connection.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import * as constants from '../constants';
|
||||
import { IFileSource, IHdfsOptions, IRequestParams, FileSourceFactory } from './fileSources';
|
||||
|
||||
export class SqlClusterConnection {
|
||||
private _connection: sqlops.connection.Connection;
|
||||
private _profile: sqlops.IConnectionProfile;
|
||||
private _host: string;
|
||||
private _port: string;
|
||||
private _user: string;
|
||||
private _password: string;
|
||||
|
||||
constructor(connectionInfo: sqlops.connection.Connection | sqlops.IConnectionProfile) {
|
||||
this.validate(connectionInfo);
|
||||
|
||||
if ('id' in connectionInfo) {
|
||||
this._profile = connectionInfo;
|
||||
this._connection = this.toConnection(this._profile);
|
||||
} else {
|
||||
this._connection = connectionInfo;
|
||||
this._profile = this.toConnectionProfile(this._connection);
|
||||
}
|
||||
this._host = this._connection.options[constants.hostPropName];
|
||||
this._port = this._connection.options[constants.knoxPortPropName];
|
||||
this._user = this._connection.options[constants.userPropName];
|
||||
this._password = this._connection.options[constants.passwordPropName];
|
||||
}
|
||||
|
||||
public get connection(): sqlops.connection.Connection { return this._connection; }
|
||||
public get profile(): sqlops.IConnectionProfile { return this._profile; }
|
||||
public get host(): string { return this._host; }
|
||||
public get port(): string { return this._port || constants.defaultKnoxPort; }
|
||||
public get user(): string { return this._user; }
|
||||
public get password(): string { return this._password; }
|
||||
|
||||
public isMatch(connection: SqlClusterConnection | sqlops.ConnectionInfo): boolean {
|
||||
if (!connection) { return false; }
|
||||
let options1 = connection instanceof SqlClusterConnection ?
|
||||
connection._connection.options : connection.options;
|
||||
let options2 = this._connection.options;
|
||||
return [constants.hostPropName, constants.knoxPortPropName, constants.userPropName]
|
||||
.every(e => options1[e] === options2[e]);
|
||||
}
|
||||
|
||||
public createHdfsFileSource(): IFileSource {
|
||||
let options: IHdfsOptions = {
|
||||
protocol: 'https',
|
||||
host: this.host,
|
||||
port: this.port,
|
||||
user: this.user,
|
||||
path: 'gateway/default/webhdfs/v1',
|
||||
requestParams: {
|
||||
auth: {
|
||||
user: this.user,
|
||||
pass: this.password
|
||||
}
|
||||
}
|
||||
};
|
||||
return FileSourceFactory.instance.createHdfsFileSource(options);
|
||||
}
|
||||
|
||||
private validate(connectionInfo: sqlops.ConnectionInfo): void {
|
||||
if (!connectionInfo) {
|
||||
throw new Error(localize('connectionInfoUndefined', 'ConnectionInfo is undefined.'));
|
||||
}
|
||||
if (!connectionInfo.options) {
|
||||
throw new Error(localize('connectionInfoOptionsUndefined', 'ConnectionInfo.options is undefined.'));
|
||||
}
|
||||
let missingProperties: string[] = this.getMissingProperties(connectionInfo);
|
||||
if (missingProperties && missingProperties.length > 0) {
|
||||
throw new Error(localize('connectionInfoOptionsMissingProperties',
|
||||
'Some missing properties in connectionInfo.options: {0}',
|
||||
missingProperties.join(', ')));
|
||||
}
|
||||
}
|
||||
|
||||
private getMissingProperties(connectionInfo: sqlops.ConnectionInfo): string[] {
|
||||
if (!connectionInfo || !connectionInfo.options) { return undefined; }
|
||||
return [
|
||||
constants.hostPropName, constants.knoxPortPropName,
|
||||
constants.userPropName, constants.passwordPropName
|
||||
].filter(e => connectionInfo.options[e] === undefined);
|
||||
}
|
||||
|
||||
private toConnection(connProfile: sqlops.IConnectionProfile): sqlops.connection.Connection {
|
||||
let connection: sqlops.connection.Connection = Object.assign(connProfile,
|
||||
{ connectionId: this._profile.id });
|
||||
return connection;
|
||||
}
|
||||
|
||||
private toConnectionProfile(connectionInfo: sqlops.connection.Connection): sqlops.IConnectionProfile {
|
||||
let options = connectionInfo.options;
|
||||
let connProfile: sqlops.IConnectionProfile = Object.assign(<sqlops.IConnectionProfile>{},
|
||||
connectionInfo,
|
||||
{
|
||||
serverName: `${options[constants.hostPropName]},${options[constants.knoxPortPropName]}`,
|
||||
userName: options[constants.userPropName],
|
||||
password: options[constants.passwordPropName],
|
||||
id: connectionInfo.connectionId,
|
||||
}
|
||||
);
|
||||
return connProfile;
|
||||
}
|
||||
}
|
||||
371
extensions/mssql/src/objectExplorerNodeProvider/fileSources.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import * as fspath from 'path';
|
||||
import * as webhdfs from 'webhdfs';
|
||||
import * as fs from 'fs';
|
||||
import * as meter from 'stream-meter';
|
||||
import * as bytes from 'bytes';
|
||||
import * as https from 'https';
|
||||
import * as readline from 'readline';
|
||||
import * as os from 'os';
|
||||
|
||||
import * as constants from '../constants';
|
||||
import * as utils from '../utils';
|
||||
|
||||
export function joinHdfsPath(parent: string, child: string): string {
|
||||
if (parent === constants.hdfsRootPath) {
|
||||
return `/${child}`;
|
||||
}
|
||||
return `${parent}/${child}`;
|
||||
}
|
||||
|
||||
export interface IFile {
|
||||
path: string;
|
||||
isDirectory: boolean;
|
||||
}
|
||||
|
||||
export class File implements IFile {
|
||||
constructor(public path: string, public isDirectory: boolean) {
|
||||
|
||||
}
|
||||
|
||||
public static createPath(path: string, fileName: string): string {
|
||||
return joinHdfsPath(path, fileName);
|
||||
}
|
||||
|
||||
public static createChild(parent: IFile, fileName: string, isDirectory: boolean): IFile {
|
||||
return new File(File.createPath(parent.path, fileName), isDirectory);
|
||||
}
|
||||
|
||||
public static createFile(parent: IFile, fileName: string): File {
|
||||
return File.createChild(parent, fileName, false);
|
||||
}
|
||||
|
||||
public static createDirectory(parent: IFile, fileName: string): IFile {
|
||||
return File.createChild(parent, fileName, true);
|
||||
}
|
||||
|
||||
public static getBasename(file: IFile): string {
|
||||
return fspath.basename(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IFileSource {
|
||||
|
||||
enumerateFiles(path: string): Promise<IFile[]>;
|
||||
mkdir(dirName: string, remoteBasePath: string): Promise<void>;
|
||||
createReadStream(path: string): fs.ReadStream;
|
||||
readFile(path: string, maxBytes?: number): Promise<Buffer>;
|
||||
readFileLines(path: string, maxLines: number): Promise<Buffer>;
|
||||
writeFile(localFile: IFile, remoteDir: string): Promise<string>;
|
||||
delete(path: string, recursive?: boolean): Promise<void>;
|
||||
exists(path: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface IHttpAuthentication {
|
||||
user: string;
|
||||
pass: string;
|
||||
}
|
||||
export interface IHdfsOptions {
|
||||
host?: string;
|
||||
port?: string;
|
||||
protocol?: string;
|
||||
user?: string;
|
||||
path?: string;
|
||||
requestParams?: IRequestParams;
|
||||
}
|
||||
|
||||
export interface IRequestParams {
|
||||
auth?: IHttpAuthentication;
|
||||
/**
|
||||
* Timeout in milliseconds to wait for response
|
||||
*/
|
||||
timeout?: number;
|
||||
agent?: https.Agent;
|
||||
}
|
||||
|
||||
export interface IHdfsFileStatus {
|
||||
type: 'FILE' | 'DIRECTORY';
|
||||
pathSuffix: string;
|
||||
}
|
||||
|
||||
export interface IHdfsClient {
|
||||
readdir(path: string, callback: (err: Error, files: any[]) => void): void;
|
||||
|
||||
/**
|
||||
* Create readable stream for given path
|
||||
*
|
||||
* @method createReadStream
|
||||
* @fires Request#data
|
||||
* @fires WebHDFS#finish
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {Object} [opts]
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
createReadStream (path: string, opts?: object): fs.ReadStream;
|
||||
|
||||
/**
|
||||
* Create writable stream for given path
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* var WebHDFS = require('webhdfs');
|
||||
* var hdfs = WebHDFS.createClient();
|
||||
*
|
||||
* var localFileStream = fs.createReadStream('/path/to/local/file');
|
||||
* var remoteFileStream = hdfs.createWriteStream('/path/to/remote/file');
|
||||
*
|
||||
* localFileStream.pipe(remoteFileStream);
|
||||
*
|
||||
* remoteFileStream.on('error', function onError (err) {
|
||||
* // Do something with the error
|
||||
* });
|
||||
*
|
||||
* remoteFileStream.on('finish', function onFinish () {
|
||||
* // Upload is done
|
||||
* });
|
||||
*
|
||||
* @method createWriteStream
|
||||
* @fires WebHDFS#finish
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {Boolean} [append] If set to true then append data to the file
|
||||
* @param {Object} [opts]
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
createWriteStream(path: string, append?: boolean, opts?: object): fs.WriteStream;
|
||||
|
||||
/**
|
||||
* Make new directory
|
||||
*
|
||||
* @method mkdir
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {String} [mode=0777]
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
mkdir (path: string, callback: Function): void;
|
||||
mkdir (path: string, mode: string, callback: Function): void;
|
||||
|
||||
/**
|
||||
* Delete directory or file path
|
||||
*
|
||||
* @method unlink
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {Boolean} [recursive=false]
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
rmdir (path: string, recursive: boolean, callback: Function): void;
|
||||
|
||||
/**
|
||||
* Check file existence
|
||||
* Wraps stat method
|
||||
*
|
||||
* @method stat
|
||||
* @see WebHDFS.stat
|
||||
*
|
||||
* @param {String} path
|
||||
* @param {Function} callback
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
exists (path: string, callback: Function): boolean;
|
||||
}
|
||||
|
||||
export class FileSourceFactory {
|
||||
private static _instance: FileSourceFactory;
|
||||
|
||||
public static get instance(): FileSourceFactory {
|
||||
if (!FileSourceFactory._instance) {
|
||||
FileSourceFactory._instance = new FileSourceFactory();
|
||||
}
|
||||
return FileSourceFactory._instance;
|
||||
}
|
||||
|
||||
public createHdfsFileSource(options: IHdfsOptions): IFileSource {
|
||||
options = options && options.host ? FileSourceFactory.removePortFromHost(options) : options;
|
||||
let requestParams: IRequestParams = options.requestParams ? options.requestParams : {};
|
||||
if (requestParams.auth) {
|
||||
// TODO Remove handling of unsigned cert once we have real certs in our Knox service
|
||||
let agentOptions = {
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
path: constants.hdfsRootPath,
|
||||
rejectUnauthorized: false
|
||||
};
|
||||
let agent = new https.Agent(agentOptions);
|
||||
requestParams['agent'] = agent;
|
||||
}
|
||||
return new HdfsFileSource(webhdfs.createClient(options, requestParams));
|
||||
}
|
||||
|
||||
// remove port from host when port is specified after a comma or colon
|
||||
private static removePortFromHost(options: IHdfsOptions): IHdfsOptions {
|
||||
// determine whether the host has either a ',' or ':' in it
|
||||
options = this.setHostAndPort(options, ',');
|
||||
options = this.setHostAndPort(options, ':');
|
||||
return options;
|
||||
}
|
||||
|
||||
// set port and host correctly after we've identified that a delimiter exists in the host name
|
||||
private static setHostAndPort(options: IHdfsOptions, delimeter: string): IHdfsOptions {
|
||||
let optionsHost: string = options.host;
|
||||
if (options.host.indexOf(delimeter) > -1) {
|
||||
options.host = options.host.slice(0, options.host.indexOf(delimeter));
|
||||
options.port = optionsHost.replace(options.host + delimeter, '');
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
export class HdfsFileSource implements IFileSource {
|
||||
constructor(private client: IHdfsClient) {
|
||||
}
|
||||
|
||||
public enumerateFiles(path: string): Promise<IFile[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.readdir(path, (error, files) => {
|
||||
if (error) {
|
||||
reject(error.message);
|
||||
} else {
|
||||
let hdfsFiles: IFile[] = files.map(file => {
|
||||
let hdfsFile = <IHdfsFileStatus> file;
|
||||
return new File(File.createPath(path, hdfsFile.pathSuffix), hdfsFile.type === 'DIRECTORY');
|
||||
});
|
||||
resolve(hdfsFiles);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public mkdir(dirName: string, remoteBasePath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let remotePath = joinHdfsPath(remoteBasePath, dirName);
|
||||
this.client.mkdir(remotePath, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public createReadStream(path: string): fs.ReadStream {
|
||||
return this.client.createReadStream(path);
|
||||
}
|
||||
|
||||
public readFile(path: string, maxBytes?: number): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let remoteFileStream = this.client.createReadStream(path);
|
||||
if (maxBytes) {
|
||||
remoteFileStream = remoteFileStream.pipe(meter(maxBytes));
|
||||
}
|
||||
let data = [];
|
||||
let error = undefined;
|
||||
remoteFileStream.on('error', (err) => {
|
||||
error = err.toString();
|
||||
if (error.includes('Stream exceeded specified max')) {
|
||||
error = `File exceeds max size of ${bytes(maxBytes)}`;
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
|
||||
remoteFileStream.on('data', (chunk) => {
|
||||
data.push(chunk);
|
||||
});
|
||||
|
||||
remoteFileStream.once('finish', () => {
|
||||
if (!error) {
|
||||
resolve(Buffer.concat(data));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public readFileLines(path: string, maxLines: number): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let lineReader = readline.createInterface({
|
||||
input: this.client.createReadStream(path)
|
||||
});
|
||||
|
||||
let lineCount = 0;
|
||||
let lineData: string[] = [];
|
||||
let errorMsg = undefined;
|
||||
lineReader.on('line', (line: string) => {
|
||||
lineCount++;
|
||||
lineData.push(line);
|
||||
if (lineCount >= maxLines) {
|
||||
resolve(Buffer.from(lineData.join(os.EOL)));
|
||||
lineReader.close();
|
||||
}
|
||||
})
|
||||
.on('error', (err) => {
|
||||
errorMsg = utils.getErrorMessage(err);
|
||||
reject(errorMsg);
|
||||
})
|
||||
.on('close', () => {
|
||||
if (!errorMsg) {
|
||||
resolve(Buffer.from(lineData.join(os.EOL)));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public writeFile(localFile: IFile, remoteDirPath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let fileName = fspath.basename(localFile.path);
|
||||
let remotePath = joinHdfsPath(remoteDirPath, fileName);
|
||||
|
||||
let writeStream = this.client.createWriteStream(remotePath);
|
||||
|
||||
let readStream = fs.createReadStream(localFile.path);
|
||||
readStream.pipe(writeStream);
|
||||
|
||||
let error: string | Error = undefined;
|
||||
|
||||
// API always calls finish, so catch error then handle exit in the finish event
|
||||
writeStream.on('error', (err => {
|
||||
error = err;
|
||||
reject(error);
|
||||
}));
|
||||
writeStream.on('finish', (location) => {
|
||||
if (!error) {
|
||||
resolve(location);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public delete(path: string, recursive: boolean = false): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.rmdir(path, recursive, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public exists(path: string): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.exists(path, (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
363
extensions/mssql/src/objectExplorerNodeProvider/hdfsCommands.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as fs from 'fs';
|
||||
import * as fspath from 'path';
|
||||
import * as clipboardy from 'clipboardy';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { ApiWrapper } from '../apiWrapper';
|
||||
import { Command, ICommandViewContext, ProgressCommand, ICommandObjectExplorerContext } from './command';
|
||||
import { IHdfsOptions, HdfsFileSource, File, IFile, joinHdfsPath, FileSourceFactory } from './fileSources';
|
||||
import { HdfsProvider, FolderNode, FileNode, HdfsFileSourceNode } from './hdfsProvider';
|
||||
import { IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
|
||||
import * as constants from '../constants';
|
||||
import * as LocalizedConstants from '../localizedConstants';
|
||||
import * as utils from '../utils';
|
||||
import { SqlClusterConnection } from './connection';
|
||||
import { AppContext } from '../appContext';
|
||||
import { TreeNode } from './treeNodes';
|
||||
import { MssqlObjectExplorerNodeProvider } from './objectExplorerNodeProvider';
|
||||
|
||||
function getSaveableUri(apiWrapper: ApiWrapper, fileName: string, isPreview?: boolean): vscode.Uri {
|
||||
let root = utils.getUserHome();
|
||||
let workspaceFolders = apiWrapper.workspaceFolders;
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
root = workspaceFolders[0].uri.fsPath;
|
||||
}
|
||||
// Cannot preview with a file path that already exists, so keep looking for a valid path that does not exist
|
||||
if (isPreview) {
|
||||
let fileNum = 1;
|
||||
let fileNameWithoutExtension = fspath.parse(fileName).name;
|
||||
let fileExtension = fspath.parse(fileName).ext;
|
||||
while (fs.existsSync(fspath.join(root, fileName))) {
|
||||
fileName = `${fileNameWithoutExtension}-${fileNum}${fileExtension}`;
|
||||
fileNum++;
|
||||
}
|
||||
}
|
||||
return vscode.Uri.file(fspath.join(root, fileName));
|
||||
}
|
||||
|
||||
export async function getNode<T extends TreeNode>(context: ICommandViewContext | ICommandObjectExplorerContext, appContext: AppContext): Promise<T> {
|
||||
let node: T = undefined;
|
||||
if (context && context.type === constants.ViewType && context.node) {
|
||||
node = context.node as T;
|
||||
} else if (context && context.type === constants.ObjectExplorerService) {
|
||||
let oeNodeProvider = appContext.getService<MssqlObjectExplorerNodeProvider>(constants.ObjectExplorerService);
|
||||
if (oeNodeProvider) {
|
||||
node = await oeNodeProvider.findSqlClusterNodeByContext<T>(context);
|
||||
}
|
||||
} else {
|
||||
throw new Error(LocalizedConstants.msgMissingNodeContext);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
export class UploadFilesCommand extends ProgressCommand {
|
||||
|
||||
constructor(prompter: IPrompter, appContext: AppContext) {
|
||||
super('mssqlCluster.uploadFiles', prompter, appContext);
|
||||
}
|
||||
|
||||
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
|
||||
return this.execute(context, args);
|
||||
}
|
||||
|
||||
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
|
||||
try {
|
||||
let folderNode = await getNode<FolderNode>(context, this.appContext);
|
||||
const allFilesFilter = localize('allFiles', 'All Files');
|
||||
let filter = {};
|
||||
filter[allFilesFilter] = '*';
|
||||
if (folderNode) {
|
||||
let options: vscode.OpenDialogOptions = {
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
openLabel: localize('lblUploadFiles', 'Upload'),
|
||||
filters: filter
|
||||
};
|
||||
let fileUris: vscode.Uri[] = await this.apiWrapper.showOpenDialog(options);
|
||||
if (fileUris) {
|
||||
let files: IFile[] = fileUris.map(uri => uri.fsPath).map(this.mapPathsToFiles());
|
||||
await this.executeWithProgress(
|
||||
async (cancelToken: vscode.CancellationTokenSource) => this.writeFiles(files, folderNode, cancelToken),
|
||||
localize('uploading', 'Uploading files to HDFS'), true,
|
||||
() => this.apiWrapper.showInformationMessage(localize('uploadCanceled', 'Upload operation was canceled')));
|
||||
if (context.type === constants.ObjectExplorerService) {
|
||||
let objectExplorerNode = await sqlops.objectexplorer.getNode(context.explorerContext.connectionProfile.id, folderNode.getNodeInfo().nodePath);
|
||||
await objectExplorerNode.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.apiWrapper.showErrorMessage(localize('uploadError', 'Error uploading files: {0}', utils.getErrorMessage(err)));
|
||||
}
|
||||
}
|
||||
|
||||
private mapPathsToFiles(): (value: string, index: number, array: string[]) => File {
|
||||
return (path: string) => {
|
||||
let isDir = fs.lstatSync(path).isDirectory();
|
||||
return new File(path, isDir);
|
||||
};
|
||||
}
|
||||
|
||||
private async writeFiles(files: IFile[], folderNode: FolderNode, cancelToken: vscode.CancellationTokenSource): Promise<void> {
|
||||
for (let file of files) {
|
||||
if (cancelToken.token.isCancellationRequested) {
|
||||
// Throw here so that all recursion is ended
|
||||
throw new Error('Upload canceled');
|
||||
}
|
||||
if (file.isDirectory) {
|
||||
let dirName = fspath.basename(file.path);
|
||||
let subFolder = await folderNode.mkdir(dirName);
|
||||
let children: IFile[] = fs.readdirSync(file.path)
|
||||
.map(childFileName => joinHdfsPath(file.path, childFileName))
|
||||
.map(this.mapPathsToFiles());
|
||||
this.writeFiles(children, subFolder, cancelToken);
|
||||
} else {
|
||||
await folderNode.writeFile(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export class MkDirCommand extends ProgressCommand {
|
||||
|
||||
constructor(prompter: IPrompter, appContext: AppContext) {
|
||||
super('mssqlCluster.mkdir', prompter, appContext);
|
||||
}
|
||||
|
||||
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
|
||||
return this.execute(context, args);
|
||||
}
|
||||
|
||||
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
|
||||
try {
|
||||
let folderNode = await getNode<FolderNode>(context, this.appContext);
|
||||
|
||||
if (folderNode) {
|
||||
let fileName: string = await this.getDirName();
|
||||
if (fileName && fileName.length > 0) {
|
||||
await this.executeWithProgress(
|
||||
async (cancelToken: vscode.CancellationTokenSource) => this.mkDir(fileName, folderNode, cancelToken),
|
||||
localize('makingDir', 'Creating directory'), true,
|
||||
() => this.apiWrapper.showInformationMessage(localize('mkdirCanceled', 'Operation was canceled')));
|
||||
if (context.type === constants.ObjectExplorerService) {
|
||||
let objectExplorerNode = await sqlops.objectexplorer.getNode(context.explorerContext.connectionProfile.id, folderNode.getNodeInfo().nodePath);
|
||||
await objectExplorerNode.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.apiWrapper.showErrorMessage(localize('uploadError', 'Error uploading files: {0}', utils.getErrorMessage(err)));
|
||||
}
|
||||
}
|
||||
|
||||
private async getDirName(): Promise<string> {
|
||||
return await this.prompter.promptSingle(<IQuestion>{
|
||||
type: QuestionTypes.input,
|
||||
name: 'enterDirName',
|
||||
message: localize('enterDirName', 'Enter directory name'),
|
||||
default: ''
|
||||
}).then(confirmed => <string>confirmed);
|
||||
}
|
||||
|
||||
private async mkDir(fileName, folderNode: FolderNode, cancelToken: vscode.CancellationTokenSource): Promise<void> {
|
||||
let subFolder = await folderNode.mkdir(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteFilesCommand extends Command {
|
||||
|
||||
constructor(private prompter: IPrompter, appContext: AppContext) {
|
||||
super('mssqlCluster.deleteFiles', appContext);
|
||||
}
|
||||
|
||||
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
|
||||
return this.execute(context, args);
|
||||
}
|
||||
|
||||
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
|
||||
try {
|
||||
let node = await getNode<TreeNode>(context, this.appContext);
|
||||
if (node) {
|
||||
// TODO ideally would let node define if it's deletable
|
||||
// TODO also, would like to change this to getNodeInfo as OE is the primary use case now
|
||||
let treeItem = await node.getTreeItem();
|
||||
let oeNodeToRefresh: sqlops.objectexplorer.ObjectExplorerNode = undefined;
|
||||
if (context.type === constants.ObjectExplorerService) {
|
||||
let oeNodeToDelete = await sqlops.objectexplorer.getNode(context.explorerContext.connectionProfile.id, node.getNodeInfo().nodePath);
|
||||
oeNodeToRefresh = await oeNodeToDelete.getParent();
|
||||
}
|
||||
switch (treeItem.contextValue) {
|
||||
case constants.MssqlClusterItems.Folder:
|
||||
await this.deleteFolder(<FolderNode>node);
|
||||
break;
|
||||
case constants.MssqlClusterItems.File:
|
||||
await this.deleteFile(<FileNode>node);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (oeNodeToRefresh) {
|
||||
await oeNodeToRefresh.refresh();
|
||||
}
|
||||
} else {
|
||||
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
|
||||
}
|
||||
} catch (err) {
|
||||
this.apiWrapper.showErrorMessage(localize('deleteError', 'Error deleting files {0}', utils.getErrorMessage(err)));
|
||||
}
|
||||
}
|
||||
|
||||
private async confirmDelete(deleteMsg: string): Promise<boolean> {
|
||||
return await this.prompter.promptSingle(<IQuestion>{
|
||||
type: QuestionTypes.confirm,
|
||||
message: deleteMsg,
|
||||
default: false
|
||||
}).then(confirmed => <boolean>confirmed);
|
||||
}
|
||||
|
||||
private async deleteFolder(node: FolderNode): Promise<void> {
|
||||
if (node) {
|
||||
let confirmed = await this.confirmDelete(localize('msgDeleteFolder', 'Are you sure you want to delete this folder and its contents?'));
|
||||
if (confirmed) {
|
||||
// TODO prompt for recursive delete if non-empty?
|
||||
await node.delete(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteFile(node: FileNode): Promise<void> {
|
||||
if (node) {
|
||||
let confirmed = await this.confirmDelete(localize('msgDeleteFile', 'Are you sure you want to delete this file?'));
|
||||
if (confirmed) {
|
||||
await node.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SaveFileCommand extends ProgressCommand {
|
||||
|
||||
constructor(prompter: IPrompter, appContext: AppContext) {
|
||||
super('mssqlCluster.saveFile', prompter, appContext);
|
||||
}
|
||||
|
||||
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
|
||||
return this.execute(context, args);
|
||||
}
|
||||
|
||||
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
|
||||
try {
|
||||
let fileNode = await getNode<FileNode>(context, this.appContext);
|
||||
if (fileNode) {
|
||||
let defaultUri = getSaveableUri(this.apiWrapper, fspath.basename(fileNode.hdfsPath));
|
||||
let fileUri: vscode.Uri = await this.apiWrapper.showSaveDialog({
|
||||
defaultUri: defaultUri
|
||||
});
|
||||
if (fileUri) {
|
||||
await this.executeWithProgress(
|
||||
async (cancelToken: vscode.CancellationTokenSource) => this.doSaveAndOpen(fileUri, fileNode, cancelToken),
|
||||
localize('saving', 'Saving HDFS Files'), true,
|
||||
() => this.apiWrapper.showInformationMessage(localize('saveCanceled', 'Save operation was canceled')));
|
||||
}
|
||||
} else {
|
||||
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
|
||||
}
|
||||
} catch (err) {
|
||||
this.apiWrapper.showErrorMessage(localize('saveError', 'Error saving file: {0}', utils.getErrorMessage(err)));
|
||||
}
|
||||
}
|
||||
|
||||
private async doSaveAndOpen(fileUri: vscode.Uri, fileNode: FileNode, cancelToken: vscode.CancellationTokenSource): Promise<void> {
|
||||
await fileNode.writeFileContentsToDisk(fileUri.fsPath, cancelToken);
|
||||
await this.apiWrapper.executeCommand('vscode.open', fileUri);
|
||||
}
|
||||
}
|
||||
|
||||
export class PreviewFileCommand extends ProgressCommand {
|
||||
public static readonly DefaultMaxSize = 30 * 1024 * 1024;
|
||||
|
||||
constructor(prompter: IPrompter, appContext: AppContext) {
|
||||
super('mssqlCluster.previewFile', prompter, appContext);
|
||||
}
|
||||
|
||||
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
|
||||
return this.execute(context, args);
|
||||
}
|
||||
|
||||
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
|
||||
try {
|
||||
let fileNode = await getNode<FileNode>(context, this.appContext);
|
||||
if (fileNode) {
|
||||
await this.executeWithProgress(
|
||||
async (cancelToken: vscode.CancellationTokenSource) => {
|
||||
let contents = await fileNode.getFileContentsAsString(PreviewFileCommand.DefaultMaxSize);
|
||||
let doc = await this.openTextDocument(fspath.basename(fileNode.hdfsPath));
|
||||
let editor = await this.apiWrapper.showTextDocument(doc, vscode.ViewColumn.Active, false);
|
||||
await editor.edit(edit => {
|
||||
edit.insert(new vscode.Position(0, 0), contents);
|
||||
});
|
||||
},
|
||||
localize('previewing', 'Generating preview'),
|
||||
false);
|
||||
} else {
|
||||
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
|
||||
}
|
||||
} catch (err) {
|
||||
this.apiWrapper.showErrorMessage(localize('previewError', 'Error previewing file: {0}', utils.getErrorMessage(err)));
|
||||
}
|
||||
}
|
||||
|
||||
private async openTextDocument(fileName: string): Promise<vscode.TextDocument> {
|
||||
let docUri: vscode.Uri = getSaveableUri(this.apiWrapper, fileName, true);
|
||||
if (docUri) {
|
||||
docUri = docUri.with({ scheme: constants.UNTITLED_SCHEMA });
|
||||
return await this.apiWrapper.openTextDocument(docUri);
|
||||
} else {
|
||||
// Can't reliably create a filename to save as so just use untitled
|
||||
let language = fspath.extname(fileName);
|
||||
if (language && language.length > 0) {
|
||||
// trim the '.'
|
||||
language = language.substring(1);
|
||||
}
|
||||
return await this.apiWrapper.openTextDocument({
|
||||
language: language
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyPathCommand extends Command {
|
||||
public static readonly DefaultMaxSize = 30 * 1024 * 1024;
|
||||
|
||||
constructor(appContext: AppContext) {
|
||||
super('mssqlCluster.copyPath', appContext);
|
||||
}
|
||||
|
||||
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
|
||||
return this.execute(context, args);
|
||||
}
|
||||
|
||||
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
|
||||
try {
|
||||
let node = await getNode<HdfsFileSourceNode>(context, this.appContext);
|
||||
if (node) {
|
||||
let path = node.hdfsPath;
|
||||
clipboardy.writeSync(path);
|
||||
} else {
|
||||
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
|
||||
}
|
||||
} catch (err) {
|
||||
this.apiWrapper.showErrorMessage(localize('copyPathError', 'Error copying path: {0}', utils.getErrorMessage(err)));
|
||||
}
|
||||
}
|
||||
}
|
||||
366
extensions/mssql/src/objectExplorerNodeProvider/hdfsProvider.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as vscode from 'vscode';
|
||||
import * as fspath from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { ApiWrapper } from '../apiWrapper';
|
||||
import * as Constants from '../constants';
|
||||
import { IFileSource, IHdfsOptions, HdfsFileSource, IFile, File, FileSourceFactory } from './fileSources';
|
||||
import { CancelableStream } from './cancelableStream';
|
||||
import { TreeNode } from './treeNodes';
|
||||
import * as utils from '../utils';
|
||||
import { IFileNode } from './types';
|
||||
|
||||
export interface ITreeChangeHandler {
|
||||
notifyNodeChanged(node: TreeNode): void;
|
||||
}
|
||||
export class TreeDataContext {
|
||||
|
||||
constructor(public extensionContext: vscode.ExtensionContext, public changeHandler: ITreeChangeHandler) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class HdfsProvider implements vscode.TreeDataProvider<TreeNode>, ITreeChangeHandler {
|
||||
static readonly NoConnectionsMessage = 'No connections added';
|
||||
static readonly ConnectionsLabel = 'Connections';
|
||||
|
||||
private connections: ConnectionNode[];
|
||||
private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode>();
|
||||
private context: TreeDataContext;
|
||||
|
||||
constructor(extensionContext: vscode.ExtensionContext, private vscodeApi: ApiWrapper) {
|
||||
this.connections = [];
|
||||
this.context = new TreeDataContext(extensionContext, this);
|
||||
}
|
||||
|
||||
public get onDidChangeTreeData(): vscode.Event<TreeNode> {
|
||||
return this._onDidChangeTreeData.event;
|
||||
}
|
||||
|
||||
getTreeItem(element: TreeNode): vscode.TreeItem | Thenable<vscode.TreeItem> {
|
||||
return element.getTreeItem();
|
||||
}
|
||||
|
||||
getChildren(element?: TreeNode): vscode.ProviderResult<TreeNode[]> {
|
||||
if (element) {
|
||||
return element.getChildren(false);
|
||||
} else {
|
||||
return this.connections.length > 0 ? this.connections : [ErrorNode.create(HdfsProvider.NoConnectionsMessage, element)];
|
||||
}
|
||||
}
|
||||
|
||||
addConnection(displayName: string, fileSource: IFileSource): void {
|
||||
if (!this.connections.find(c => c.getDisplayName() === displayName)) {
|
||||
this.connections.push(new ConnectionNode(this.context, displayName, fileSource));
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
}
|
||||
|
||||
addHdfsConnection(options: IHdfsOptions): void {
|
||||
let displayName = `${options.user}@${options.host}:${options.port}`;
|
||||
let fileSource = FileSourceFactory.instance.createHdfsFileSource(options);
|
||||
this.addConnection(displayName, fileSource);
|
||||
}
|
||||
|
||||
notifyNodeChanged(node: TreeNode): void {
|
||||
this._onDidChangeTreeData.fire(node);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class HdfsFileSourceNode extends TreeNode {
|
||||
constructor(protected context: TreeDataContext, protected _path: string, protected fileSource: IFileSource) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get hdfsPath(): string {
|
||||
return this._path;
|
||||
}
|
||||
|
||||
public get nodePathValue(): string {
|
||||
return this.getDisplayName();
|
||||
}
|
||||
|
||||
getDisplayName(): string {
|
||||
return fspath.basename(this._path);
|
||||
}
|
||||
|
||||
public async delete(recursive: boolean = false): Promise<void> {
|
||||
await this.fileSource.delete(this.hdfsPath, recursive);
|
||||
// Notify parent should be updated. If at top, will return undefined which will refresh whole tree
|
||||
(<HdfsFileSourceNode>this.parent).onChildRemoved();
|
||||
this.context.changeHandler.notifyNodeChanged(this.parent);
|
||||
}
|
||||
public abstract onChildRemoved(): void;
|
||||
}
|
||||
|
||||
export class FolderNode extends HdfsFileSourceNode {
|
||||
private children: TreeNode[];
|
||||
protected _nodeType: string;
|
||||
constructor(context: TreeDataContext, path: string, fileSource: IFileSource, nodeType?: string) {
|
||||
super(context, path, fileSource);
|
||||
this._nodeType = nodeType ? nodeType : Constants.MssqlClusterItems.Folder;
|
||||
}
|
||||
|
||||
private ensureChildrenExist(): void {
|
||||
if (!this.children) {
|
||||
this.children = [];
|
||||
}
|
||||
}
|
||||
|
||||
public onChildRemoved(): void {
|
||||
this.children = undefined;
|
||||
}
|
||||
|
||||
async getChildren(refreshChildren: boolean): Promise<TreeNode[]> {
|
||||
if (refreshChildren || !this.children) {
|
||||
this.ensureChildrenExist();
|
||||
try {
|
||||
let files: IFile[] = await this.fileSource.enumerateFiles(this._path);
|
||||
if (files) {
|
||||
// Note: for now, assuming HDFS-provided sorting is sufficient
|
||||
this.children = files.map((file) => {
|
||||
let node: TreeNode = file.isDirectory ? new FolderNode(this.context, file.path, this.fileSource)
|
||||
: new FileNode(this.context, file.path, this.fileSource);
|
||||
node.parent = this;
|
||||
return node;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.children = [ErrorNode.create(localize('errorExpanding', 'Error: {0}', utils.getErrorMessage(error)), this)];
|
||||
}
|
||||
}
|
||||
return this.children;
|
||||
}
|
||||
|
||||
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
|
||||
let item = new vscode.TreeItem(this.getDisplayName(), vscode.TreeItemCollapsibleState.Collapsed);
|
||||
// For now, folder always looks the same. We're using SQL icons to differentiate remote vs local files
|
||||
item.iconPath = {
|
||||
dark: this.context.extensionContext.asAbsolutePath('resources/light/Folder.svg'),
|
||||
light: this.context.extensionContext.asAbsolutePath('resources/light/Folder.svg')
|
||||
};
|
||||
item.contextValue = this._nodeType;
|
||||
return item;
|
||||
}
|
||||
|
||||
getNodeInfo(): sqlops.NodeInfo {
|
||||
// TODO handle error message case by returning it in the OE API
|
||||
// TODO support better mapping of node type
|
||||
let nodeInfo: sqlops.NodeInfo = {
|
||||
label: this.getDisplayName(),
|
||||
isLeaf: false,
|
||||
errorMessage: undefined,
|
||||
metadata: undefined,
|
||||
nodePath: this.generateNodePath(),
|
||||
nodeStatus: undefined,
|
||||
nodeType: this._nodeType,
|
||||
nodeSubType: undefined,
|
||||
iconType: 'Folder'
|
||||
};
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
public async writeFile(localFile: IFile): Promise<FileNode> {
|
||||
return this.runChildAddAction<FileNode>(() => this.writeFileAsync(localFile));
|
||||
}
|
||||
|
||||
private async writeFileAsync(localFile: IFile): Promise<FileNode> {
|
||||
await this.fileSource.writeFile(localFile, this._path);
|
||||
let fileNode = new FileNode(this.context, File.createPath(this._path, File.getBasename(localFile)), this.fileSource);
|
||||
return fileNode;
|
||||
}
|
||||
|
||||
public async mkdir(name: string): Promise<FolderNode> {
|
||||
return this.runChildAddAction<FolderNode>(() => this.mkdirAsync(name));
|
||||
}
|
||||
|
||||
private async mkdirAsync(name: string): Promise<FolderNode> {
|
||||
await this.fileSource.mkdir(name, this._path);
|
||||
let subDir = new FolderNode(this.context, File.createPath(this._path, name), this.fileSource);
|
||||
return subDir;
|
||||
}
|
||||
|
||||
private async runChildAddAction<T extends TreeNode>(action: () => Promise<T>): Promise<T> {
|
||||
let node = await action();
|
||||
await this.getChildren(true);
|
||||
if (this.children) {
|
||||
// Find the child matching the node. This is necessary
|
||||
// since writing can add duplicates.
|
||||
node = this.children.find(n => n.nodePathValue === node.nodePathValue) as T;
|
||||
this.context.changeHandler.notifyNodeChanged(this);
|
||||
} else {
|
||||
// Failed to retrieve children from server so something went wrong
|
||||
node = undefined;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectionNode extends FolderNode {
|
||||
|
||||
constructor(context: TreeDataContext, private displayName: string, fileSource: IFileSource) {
|
||||
super(context, '/', fileSource, Constants.MssqlClusterItems.Connection);
|
||||
}
|
||||
|
||||
getDisplayName(): string {
|
||||
return this.displayName;
|
||||
}
|
||||
|
||||
public async delete(): Promise<void> {
|
||||
throw new Error(localize('errDeleteConnectionNode', 'Cannot delete a connection. Only subfolders and files can be deleted.'));
|
||||
}
|
||||
|
||||
async getTreeItem(): Promise<vscode.TreeItem> {
|
||||
let item = await super.getTreeItem();
|
||||
item.contextValue = this._nodeType;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
export class FileNode extends HdfsFileSourceNode implements IFileNode {
|
||||
|
||||
constructor(context: TreeDataContext, path: string, fileSource: IFileSource) {
|
||||
super(context, path, fileSource);
|
||||
}
|
||||
|
||||
public onChildRemoved(): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
|
||||
let item = new vscode.TreeItem(this.getDisplayName(), vscode.TreeItemCollapsibleState.None);
|
||||
item.iconPath = {
|
||||
dark: this.context.extensionContext.asAbsolutePath('resources/dark/file_inverse.svg'),
|
||||
light: this.context.extensionContext.asAbsolutePath('resources/light/file.svg')
|
||||
};
|
||||
item.contextValue = Constants.MssqlClusterItems.File;
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
getNodeInfo(): sqlops.NodeInfo {
|
||||
// TODO improve node type handling so it's not tied to SQL Server types
|
||||
let nodeInfo: sqlops.NodeInfo = {
|
||||
label: this.getDisplayName(),
|
||||
isLeaf: true,
|
||||
errorMessage: undefined,
|
||||
metadata: undefined,
|
||||
nodePath: this.generateNodePath(),
|
||||
nodeStatus: undefined,
|
||||
nodeType: Constants.MssqlClusterItems.File,
|
||||
nodeSubType: this.getSubType(),
|
||||
iconType: 'FileGroupFile'
|
||||
};
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
public async getFileContentsAsString(maxBytes?: number): Promise<string> {
|
||||
let contents: Buffer = await this.fileSource.readFile(this.hdfsPath, maxBytes);
|
||||
return contents ? contents.toString('utf8') : '';
|
||||
}
|
||||
|
||||
public async getFileLinesAsString(maxLines: number): Promise<string> {
|
||||
let contents: Buffer = await this.fileSource.readFileLines(this.hdfsPath, maxLines);
|
||||
return contents ? contents.toString('utf8') : '';
|
||||
}
|
||||
|
||||
public writeFileContentsToDisk(localPath: string, cancelToken?: vscode.CancellationTokenSource): Promise<vscode.Uri> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let readStream: fs.ReadStream = this.fileSource.createReadStream(this.hdfsPath);
|
||||
let writeStream = fs.createWriteStream(localPath, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
let cancelable = new CancelableStream(cancelToken);
|
||||
cancelable.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
readStream.pipe(cancelable).pipe(writeStream);
|
||||
|
||||
let error: string | Error = undefined;
|
||||
|
||||
writeStream.on('error', (err) => {
|
||||
error = err;
|
||||
reject(error);
|
||||
});
|
||||
writeStream.on('finish', (location) => {
|
||||
if (!error) {
|
||||
resolve(vscode.Uri.file(localPath));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getSubType(): string {
|
||||
if (this.getDisplayName().toLowerCase().endsWith('.jar') || this.getDisplayName().toLowerCase().endsWith('.py')) {
|
||||
return Constants.MssqlClusterItemsSubType.Spark;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorNode extends TreeNode {
|
||||
static messageNum: number = 0;
|
||||
|
||||
private _nodePathValue: string;
|
||||
constructor(private message: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
public static create(message: string, parent: TreeNode): ErrorNode {
|
||||
let node = new ErrorNode(message);
|
||||
node.parent = parent;
|
||||
return node;
|
||||
}
|
||||
|
||||
private ensureNodePathValue(): void {
|
||||
if (!this._nodePathValue) {
|
||||
this._nodePathValue = `message_${ErrorNode.messageNum++}`;
|
||||
}
|
||||
}
|
||||
|
||||
public get nodePathValue(): string {
|
||||
this.ensureNodePathValue();
|
||||
return this._nodePathValue;
|
||||
}
|
||||
|
||||
public getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
public getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
|
||||
let item = new vscode.TreeItem(this.message, vscode.TreeItemCollapsibleState.None);
|
||||
item.contextValue = Constants.MssqlClusterItems.Error;
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
getNodeInfo(): sqlops.NodeInfo {
|
||||
let nodeInfo: sqlops.NodeInfo = {
|
||||
label: this.message,
|
||||
isLeaf: false,
|
||||
errorMessage: undefined,
|
||||
metadata: undefined,
|
||||
nodePath: this.generateNodePath(),
|
||||
nodeStatus: undefined,
|
||||
nodeType: Constants.MssqlClusterItems.Error,
|
||||
nodeSubType: undefined,
|
||||
iconType: 'MessageType'
|
||||
};
|
||||
return nodeInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { ProviderBase } from './providerBase';
|
||||
import { SqlClusterConnection } from './connection';
|
||||
import * as utils from '../utils';
|
||||
import { TreeNode } from './treeNodes';
|
||||
import { ConnectionNode, TreeDataContext, ITreeChangeHandler } from './hdfsProvider';
|
||||
import { IFileSource } from './fileSources';
|
||||
import { AppContext } from '../appContext';
|
||||
import * as constants from '../constants';
|
||||
import * as SqlClusterLookUp from '../sqlClusterLookUp';
|
||||
import { ICommandObjectExplorerContext } from './command';
|
||||
|
||||
export const mssqlOutputChannel = vscode.window.createOutputChannel(constants.providerId);
|
||||
|
||||
export class MssqlObjectExplorerNodeProvider extends ProviderBase implements sqlops.ObjectExplorerNodeProvider, ITreeChangeHandler {
|
||||
public readonly supportedProviderId: string = constants.providerId;
|
||||
private sessionMap: Map<string, SqlClusterSession>;
|
||||
private expandCompleteEmitter = new vscode.EventEmitter<sqlops.ObjectExplorerExpandInfo>();
|
||||
|
||||
constructor(private appContext: AppContext) {
|
||||
super();
|
||||
this.sessionMap = new Map<string, SqlClusterSession>();
|
||||
this.appContext.registerService<MssqlObjectExplorerNodeProvider>(constants.ObjectExplorerService, this);
|
||||
}
|
||||
|
||||
handleSessionOpen(session: sqlops.ObjectExplorerSession): Thenable<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!session) {
|
||||
reject('handleSessionOpen requires a session object to be passed');
|
||||
} else {
|
||||
resolve(this.doSessionOpen(session));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async doSessionOpen(session: sqlops.ObjectExplorerSession): Promise<boolean> {
|
||||
if (!session || !session.sessionId) { return false; }
|
||||
|
||||
let sqlConnProfile = await sqlops.objectexplorer.getSessionConnectionProfile(session.sessionId);
|
||||
if (!sqlConnProfile) { return false; }
|
||||
|
||||
let clusterConnInfo = await SqlClusterLookUp.getSqlClusterConnection(sqlConnProfile);
|
||||
if (!clusterConnInfo) { return false; }
|
||||
|
||||
let clusterConnection = new SqlClusterConnection(clusterConnInfo);
|
||||
let clusterSession = new SqlClusterSession(clusterConnection, session, sqlConnProfile, this.appContext, this);
|
||||
this.sessionMap.set(session.sessionId, clusterSession);
|
||||
return true;
|
||||
}
|
||||
|
||||
expandNode(nodeInfo: sqlops.ExpandNodeInfo, isRefresh: boolean = false): Thenable<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!nodeInfo) {
|
||||
reject('expandNode requires a nodeInfo object to be passed');
|
||||
} else {
|
||||
resolve(this.doExpandNode(nodeInfo, isRefresh));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async doExpandNode(nodeInfo: sqlops.ExpandNodeInfo, isRefresh: boolean = false): Promise<boolean> {
|
||||
let session = this.sessionMap.get(nodeInfo.sessionId);
|
||||
let response = {
|
||||
sessionId: nodeInfo.sessionId,
|
||||
nodePath: nodeInfo.nodePath,
|
||||
errorMessage: undefined,
|
||||
nodes: []
|
||||
};
|
||||
|
||||
if (!session) {
|
||||
// This is not an error case. Just fire reponse with empty nodes for example: request from standalone SQL instance
|
||||
this.expandCompleteEmitter.fire(response);
|
||||
return false;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
|
||||
// Running after promise resolution as we need the Ops Studio-side map to have been updated
|
||||
// Intentionally not awaiting or catching errors.
|
||||
// Any failure in startExpansion should be emitted in the expand complete result
|
||||
// We want this to be async and ideally return true before it completes
|
||||
this.startExpansion(session, nodeInfo, isRefresh);
|
||||
}, 10);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async startExpansion(session: SqlClusterSession, nodeInfo: sqlops.ExpandNodeInfo, isRefresh: boolean = false): Promise<void> {
|
||||
let expandResult: sqlops.ObjectExplorerExpandInfo = {
|
||||
sessionId: session.sessionId,
|
||||
nodePath: nodeInfo.nodePath,
|
||||
errorMessage: undefined,
|
||||
nodes: []
|
||||
};
|
||||
try {
|
||||
let node = await session.rootNode.findNodeByPath(nodeInfo.nodePath, true);
|
||||
if (node) {
|
||||
expandResult.errorMessage = node.getNodeInfo().errorMessage;
|
||||
let children = await node.getChildren(true);
|
||||
if (children) {
|
||||
expandResult.nodes = children.map(c => c.getNodeInfo());
|
||||
// There is only child returned when failure happens
|
||||
if (children.length === 1) {
|
||||
let child = children[0].getNodeInfo();
|
||||
if (child && child.nodeType === constants.MssqlClusterItems.Error) {
|
||||
expandResult.errorMessage = child.label;
|
||||
expandResult.nodes = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
expandResult.errorMessage = utils.getErrorMessage(error);
|
||||
}
|
||||
this.expandCompleteEmitter.fire(expandResult);
|
||||
}
|
||||
|
||||
refreshNode(nodeInfo: sqlops.ExpandNodeInfo): Thenable<boolean> {
|
||||
// TODO #3815 implement properly
|
||||
return this.expandNode(nodeInfo, true);
|
||||
}
|
||||
|
||||
handleSessionClose(closeSessionInfo: sqlops.ObjectExplorerCloseSessionInfo): void {
|
||||
this.sessionMap.delete(closeSessionInfo.sessionId);
|
||||
}
|
||||
|
||||
findNodes(findNodesInfo: sqlops.FindNodesInfo): Thenable<sqlops.ObjectExplorerFindNodesResponse> {
|
||||
// TODO #3814 implement
|
||||
let response: sqlops.ObjectExplorerFindNodesResponse = {
|
||||
nodes: []
|
||||
};
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
registerOnExpandCompleted(handler: (response: sqlops.ObjectExplorerExpandInfo) => any): void {
|
||||
this.expandCompleteEmitter.event(handler);
|
||||
}
|
||||
|
||||
notifyNodeChanged(node: TreeNode): void {
|
||||
this.notifyNodeChangesAsync(node);
|
||||
}
|
||||
|
||||
private async notifyNodeChangesAsync(node: TreeNode): Promise<void> {
|
||||
try {
|
||||
let session = this.getSqlClusterSessionForNode(node);
|
||||
if (!session) {
|
||||
this.appContext.apiWrapper.showErrorMessage(localize('sessionNotFound', 'Session for node {0} does not exist', node.nodePathValue));
|
||||
} else {
|
||||
let nodeInfo = node.getNodeInfo();
|
||||
let expandInfo: sqlops.ExpandNodeInfo = {
|
||||
nodePath: nodeInfo.nodePath,
|
||||
sessionId: session.sessionId
|
||||
};
|
||||
await this.refreshNode(expandInfo);
|
||||
}
|
||||
} catch (err) {
|
||||
mssqlOutputChannel.appendLine(localize('notifyError', 'Error notifying of node change: {0}', err));
|
||||
}
|
||||
}
|
||||
|
||||
private getSqlClusterSessionForNode(node: TreeNode): SqlClusterSession {
|
||||
let sqlClusterSession: SqlClusterSession = undefined;
|
||||
while (node !== undefined) {
|
||||
if (node instanceof DataServicesNode) {
|
||||
sqlClusterSession = node.session;
|
||||
break;
|
||||
} else {
|
||||
node = node.parent;
|
||||
}
|
||||
}
|
||||
return sqlClusterSession;
|
||||
}
|
||||
|
||||
async findSqlClusterNodeByContext<T extends TreeNode>(context: ICommandObjectExplorerContext | sqlops.ObjectExplorerContext): Promise<T> {
|
||||
let node: T = undefined;
|
||||
let explorerContext = 'explorerContext' in context ? context.explorerContext : context;
|
||||
let sqlConnProfile = explorerContext.connectionProfile;
|
||||
let session = this.findSqlClusterSessionBySqlConnProfile(sqlConnProfile);
|
||||
if (session) {
|
||||
if (explorerContext.isConnectionNode) {
|
||||
// Note: ideally fix so we verify T matches RootNode and go from there
|
||||
node = <T><any>session.rootNode;
|
||||
} else {
|
||||
// Find the node under the session
|
||||
node = <T><any>await session.rootNode.findNodeByPath(explorerContext.nodeInfo.nodePath, true);
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
public findSqlClusterSessionBySqlConnProfile(connectionProfile: sqlops.IConnectionProfile): SqlClusterSession {
|
||||
for (let session of this.sessionMap.values()) {
|
||||
if (session.isMatchedSqlConnection(connectionProfile)) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class SqlClusterSession {
|
||||
private _rootNode: SqlClusterRootNode;
|
||||
|
||||
constructor(
|
||||
private _sqlClusterConnection: SqlClusterConnection,
|
||||
private _sqlSession: sqlops.ObjectExplorerSession,
|
||||
private _sqlConnectionProfile: sqlops.IConnectionProfile,
|
||||
private _appContext: AppContext,
|
||||
private _changeHandler: ITreeChangeHandler
|
||||
) {
|
||||
this._rootNode = new SqlClusterRootNode(this,
|
||||
new TreeDataContext(this._appContext.extensionContext, this._changeHandler),
|
||||
this._sqlSession.rootNode.nodePath);
|
||||
}
|
||||
|
||||
public get sqlClusterConnection(): SqlClusterConnection { return this._sqlClusterConnection; }
|
||||
public get sqlSession(): sqlops.ObjectExplorerSession { return this._sqlSession; }
|
||||
public get sqlConnectionProfile(): sqlops.IConnectionProfile { return this._sqlConnectionProfile; }
|
||||
public get sessionId(): string { return this._sqlSession.sessionId; }
|
||||
public get rootNode(): SqlClusterRootNode { return this._rootNode; }
|
||||
|
||||
public isMatchedSqlConnection(sqlConnProfile: sqlops.IConnectionProfile): boolean {
|
||||
return this._sqlConnectionProfile.id === sqlConnProfile.id;
|
||||
}
|
||||
}
|
||||
|
||||
class SqlClusterRootNode extends TreeNode {
|
||||
private _children: TreeNode[];
|
||||
constructor(
|
||||
private _session: SqlClusterSession,
|
||||
private _treeDataContext: TreeDataContext,
|
||||
private _nodePathValue: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get session(): SqlClusterSession {
|
||||
return this._session;
|
||||
}
|
||||
|
||||
public get nodePathValue(): string {
|
||||
return this._nodePathValue;
|
||||
}
|
||||
|
||||
public getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
|
||||
if (refreshChildren || !this._children) {
|
||||
this._children = [];
|
||||
let dataServicesNode = new DataServicesNode(this._session, this._treeDataContext, this._nodePathValue);
|
||||
dataServicesNode.parent = this;
|
||||
this._children.push(dataServicesNode);
|
||||
}
|
||||
return this._children;
|
||||
}
|
||||
|
||||
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
|
||||
throw new Error('Not intended for use in a file explorer view.');
|
||||
}
|
||||
|
||||
getNodeInfo(): sqlops.NodeInfo {
|
||||
let nodeInfo: sqlops.NodeInfo = {
|
||||
label: localize('rootLabel', 'Root'),
|
||||
isLeaf: false,
|
||||
errorMessage: undefined,
|
||||
metadata: undefined,
|
||||
nodePath: this.generateNodePath(),
|
||||
nodeStatus: undefined,
|
||||
nodeType: 'sqlCluster:root',
|
||||
nodeSubType: undefined,
|
||||
iconType: 'folder'
|
||||
};
|
||||
return nodeInfo;
|
||||
}
|
||||
}
|
||||
|
||||
class DataServicesNode extends TreeNode {
|
||||
private _children: TreeNode[];
|
||||
constructor(private _session: SqlClusterSession, private _context: TreeDataContext, private _nodePath: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get session(): SqlClusterSession {
|
||||
return this._session;
|
||||
}
|
||||
|
||||
public get nodePathValue(): string {
|
||||
return this._nodePath;
|
||||
}
|
||||
|
||||
public getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
|
||||
if (refreshChildren || !this._children) {
|
||||
this._children = [];
|
||||
let fileSource: IFileSource = this.session.sqlClusterConnection.createHdfsFileSource();
|
||||
let hdfsNode = new ConnectionNode(this._context, localize('hdfsFolder', 'HDFS'), fileSource);
|
||||
hdfsNode.parent = this;
|
||||
this._children.push(hdfsNode);
|
||||
}
|
||||
return this._children;
|
||||
}
|
||||
|
||||
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
|
||||
throw new Error('Not intended for use in a file explorer view.');
|
||||
}
|
||||
|
||||
getNodeInfo(): sqlops.NodeInfo {
|
||||
let nodeInfo: sqlops.NodeInfo = {
|
||||
label: localize('dataServicesLabel', 'Data Services'),
|
||||
isLeaf: false,
|
||||
errorMessage: undefined,
|
||||
metadata: undefined,
|
||||
nodePath: this.generateNodePath(),
|
||||
nodeStatus: undefined,
|
||||
nodeType: 'dataservices',
|
||||
nodeSubType: undefined,
|
||||
iconType: 'folder'
|
||||
};
|
||||
return nodeInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as constants from '../constants';
|
||||
|
||||
export abstract class ProviderBase {
|
||||
public readonly providerId: string = constants.mssqlClusterProviderName;
|
||||
public handle: number;
|
||||
}
|
||||
78
extensions/mssql/src/objectExplorerNodeProvider/treeNodes.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as vscode from 'vscode';
|
||||
import { ITreeNode } from './types';
|
||||
|
||||
type TreeNodePredicate = (node: TreeNode) => boolean;
|
||||
|
||||
export abstract class TreeNode implements ITreeNode {
|
||||
private _parent: TreeNode = undefined;
|
||||
|
||||
public get parent(): TreeNode {
|
||||
return this._parent;
|
||||
}
|
||||
|
||||
public set parent(node: TreeNode) {
|
||||
this._parent = node;
|
||||
}
|
||||
|
||||
public generateNodePath(): string {
|
||||
let path = undefined;
|
||||
if (this.parent) {
|
||||
path = this.parent.generateNodePath();
|
||||
}
|
||||
path = path ? `${path}/${this.nodePathValue}` : this.nodePathValue;
|
||||
return path;
|
||||
}
|
||||
|
||||
public findNodeByPath(path: string, expandIfNeeded: boolean = false): Promise<TreeNode> {
|
||||
let condition: TreeNodePredicate = (node: TreeNode) => node.getNodeInfo().nodePath === path || node.getNodeInfo().nodePath.startsWith(path);
|
||||
let filter: TreeNodePredicate = (node: TreeNode) => path.startsWith(node.getNodeInfo().nodePath);
|
||||
return TreeNode.findNode(this, condition, filter, true);
|
||||
}
|
||||
|
||||
public static async findNode(node: TreeNode, condition: TreeNodePredicate, filter: TreeNodePredicate, expandIfNeeded: boolean): Promise<TreeNode> {
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (condition(node)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
let nodeInfo = node.getNodeInfo();
|
||||
if (nodeInfo.isLeaf) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// TODO #3813 support filtering by already expanded / not yet expanded
|
||||
let children = await node.getChildren(false);
|
||||
if (children) {
|
||||
for (let child of children) {
|
||||
if (filter && filter(child)) {
|
||||
let childNode = await this.findNode(child, condition, filter, expandIfNeeded);
|
||||
if (childNode) {
|
||||
return childNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The value to use for this node in the node path
|
||||
*/
|
||||
public abstract get nodePathValue(): string;
|
||||
|
||||
abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]>;
|
||||
abstract getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem>;
|
||||
|
||||
abstract getNodeInfo(): sqlops.NodeInfo;
|
||||
}
|
||||
30
extensions/mssql/src/objectExplorerNodeProvider/types.d.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as sqlops from 'sqlops';
|
||||
|
||||
/**
|
||||
* A tree node in the object explorer tree
|
||||
*
|
||||
* @export
|
||||
* @interface ITreeNode
|
||||
*/
|
||||
export interface ITreeNode {
|
||||
getNodeInfo(): sqlops.NodeInfo;
|
||||
getChildren(refreshChildren: boolean): ITreeNode[] | Promise<ITreeNode[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A HDFS file node. This is a leaf node in the object explorer tree, and its contents
|
||||
* can be queried
|
||||
*
|
||||
* @export
|
||||
* @interface IFileNode
|
||||
* @extends {ITreeNode}
|
||||
*/
|
||||
export interface IFileNode extends ITreeNode {
|
||||
getFileContentsAsString(maxBytes?: number): Promise<string>;
|
||||
}
|
||||
111
extensions/mssql/src/prompts/adapter.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import {window, OutputChannel } from 'vscode';
|
||||
import * as nodeUtil from 'util';
|
||||
import PromptFactory from './factory';
|
||||
import EscapeException from '../escapeException';
|
||||
import { IQuestion, IPrompter, IPromptCallback } from './question';
|
||||
|
||||
// Supports simple pattern for prompting for user input and acting on this
|
||||
export default class CodeAdapter implements IPrompter {
|
||||
|
||||
private outChannel: OutputChannel;
|
||||
private outBuffer: string = '';
|
||||
private messageLevelFormatters = {};
|
||||
constructor() {
|
||||
// TODO Decide whether output channel logging should be saved here?
|
||||
this.outChannel = window.createOutputChannel('test');
|
||||
// this.outChannel.clear();
|
||||
}
|
||||
|
||||
public logError(message: any): void {
|
||||
let line = `error: ${message.message}\n Code - ${message.code}`;
|
||||
|
||||
this.outBuffer += `${line}\n`;
|
||||
this.outChannel.appendLine(line);
|
||||
}
|
||||
|
||||
private formatMessage(message: any): string {
|
||||
const prefix = `${message.level}: (${message.id}) `;
|
||||
return `${prefix}${message.message}`;
|
||||
}
|
||||
|
||||
public clearLog(): void {
|
||||
this.outChannel.clear();
|
||||
}
|
||||
|
||||
public showLog(): void {
|
||||
this.outChannel.show();
|
||||
}
|
||||
|
||||
// TODO define question interface
|
||||
private fixQuestion(question: any): any {
|
||||
if (question.type === 'checkbox' && Array.isArray(question.choices)) {
|
||||
// For some reason when there's a choice of checkboxes, they aren't formatted properly
|
||||
// Not sure where the issue is
|
||||
question.choices = question.choices.map(item => {
|
||||
if (typeof (item) === 'string') {
|
||||
return { checked: false, name: item, value: item };
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public promptSingle<T>(question: IQuestion, ignoreFocusOut?: boolean): Promise<T> {
|
||||
let questions: IQuestion[] = [question];
|
||||
return this.prompt(questions, ignoreFocusOut).then( (answers: {[key: string]: T}) => {
|
||||
if (answers) {
|
||||
let response: T = answers[question.name];
|
||||
return response || undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public prompt<T>(questions: IQuestion[], ignoreFocusOut?: boolean): Promise<{[key: string]: T}> {
|
||||
let answers: {[key: string]: T} = {};
|
||||
|
||||
// Collapse multiple questions into a set of prompt steps
|
||||
let promptResult: Promise<{[key: string]: T}> = questions.reduce((promise: Promise<{[key: string]: T}>, question: IQuestion) => {
|
||||
this.fixQuestion(question);
|
||||
|
||||
return promise.then(() => {
|
||||
return PromptFactory.createPrompt(question, ignoreFocusOut);
|
||||
}).then(prompt => {
|
||||
if (!question.shouldPrompt || question.shouldPrompt(answers) === true) {
|
||||
return prompt.render().then(result => {
|
||||
answers[question.name] = result;
|
||||
|
||||
if (question.onAnswered) {
|
||||
question.onAnswered(result);
|
||||
}
|
||||
return answers;
|
||||
});
|
||||
}
|
||||
return answers;
|
||||
});
|
||||
}, Promise.resolve());
|
||||
|
||||
return promptResult.catch(err => {
|
||||
if (err instanceof EscapeException || err instanceof TypeError) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
window.showErrorMessage(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to make it possible to prompt using callback pattern. Generally Promise is a preferred flow
|
||||
public promptCallback(questions: IQuestion[], callback: IPromptCallback): void {
|
||||
// Collapse multiple questions into a set of prompt steps
|
||||
this.prompt(questions).then(answers => {
|
||||
if (callback) {
|
||||
callback(answers);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
52
extensions/mssql/src/prompts/checkbox.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import { window } from 'vscode';
|
||||
import Prompt from './prompt';
|
||||
import EscapeException from '../escapeException';
|
||||
|
||||
const figures = require('figures');
|
||||
|
||||
export default class CheckboxPrompt extends Prompt {
|
||||
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
super(question, ignoreFocusOut);
|
||||
}
|
||||
|
||||
public render(): any {
|
||||
let choices = this._question.choices.reduce((result, choice) => {
|
||||
let choiceName = choice.name || choice;
|
||||
result[`${choice.checked === true ? figures.radioOn : figures.radioOff} ${choiceName}`] = choice;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
let options = this.defaultQuickPickOptions;
|
||||
options.placeHolder = this._question.message;
|
||||
|
||||
let quickPickOptions = Object.keys(choices);
|
||||
quickPickOptions.push(figures.tick);
|
||||
|
||||
return window.showQuickPick(quickPickOptions, options)
|
||||
.then(result => {
|
||||
if (result === undefined) {
|
||||
throw new EscapeException();
|
||||
}
|
||||
|
||||
if (result !== figures.tick) {
|
||||
choices[result].checked = !choices[result].checked;
|
||||
|
||||
return this.render();
|
||||
}
|
||||
|
||||
return this._question.choices.reduce((result2, choice) => {
|
||||
if (choice.checked === true) {
|
||||
result2.push(choice.value);
|
||||
}
|
||||
|
||||
return result2;
|
||||
}, []);
|
||||
});
|
||||
}
|
||||
}
|
||||
36
extensions/mssql/src/prompts/confirm.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { window } from 'vscode';
|
||||
import Prompt from './prompt';
|
||||
import EscapeException from '../escapeException';
|
||||
|
||||
export default class ConfirmPrompt extends Prompt {
|
||||
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
super(question, ignoreFocusOut);
|
||||
}
|
||||
|
||||
public render(): any {
|
||||
let choices: { [id: string]: boolean } = {};
|
||||
choices[localize('msgYes', 'Yes')] = true;
|
||||
choices[localize('msgNo', 'No')] = false;
|
||||
|
||||
let options = this.defaultQuickPickOptions;
|
||||
options.placeHolder = this._question.message;
|
||||
|
||||
return window.showQuickPick(Object.keys(choices), options)
|
||||
.then(result => {
|
||||
if (result === undefined) {
|
||||
throw new EscapeException();
|
||||
}
|
||||
|
||||
return choices[result] || false;
|
||||
});
|
||||
}
|
||||
}
|
||||
78
extensions/mssql/src/prompts/expand.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import vscode = require('vscode');
|
||||
import Prompt from './prompt';
|
||||
import EscapeException from '../escapeException';
|
||||
import { INameValueChoice } from './question';
|
||||
|
||||
const figures = require('figures');
|
||||
|
||||
export default class ExpandPrompt extends Prompt {
|
||||
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
super(question, ignoreFocusOut);
|
||||
}
|
||||
|
||||
public render(): any {
|
||||
// label indicates this is a quickpick item. Otherwise it's a name-value pair
|
||||
if (this._question.choices[0].label) {
|
||||
return this.renderQuickPick(this._question.choices);
|
||||
} else {
|
||||
return this.renderNameValueChoice(this._question.choices);
|
||||
}
|
||||
}
|
||||
|
||||
private renderQuickPick(choices: vscode.QuickPickItem[]): any {
|
||||
let options = this.defaultQuickPickOptions;
|
||||
options.placeHolder = this._question.message;
|
||||
|
||||
return vscode.window.showQuickPick(choices, options)
|
||||
.then(result => {
|
||||
if (result === undefined) {
|
||||
throw new EscapeException();
|
||||
}
|
||||
|
||||
return this.validateAndReturn(result || false);
|
||||
});
|
||||
}
|
||||
private renderNameValueChoice(choices: INameValueChoice[]): any {
|
||||
const choiceMap = this._question.choices.reduce((result, choice) => {
|
||||
result[choice.name] = choice.value;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
let options = this.defaultQuickPickOptions;
|
||||
options.placeHolder = this._question.message;
|
||||
|
||||
return vscode.window.showQuickPick(Object.keys(choiceMap), options)
|
||||
.then(result => {
|
||||
if (result === undefined) {
|
||||
throw new EscapeException();
|
||||
}
|
||||
|
||||
// Note: cannot be used with 0 or false responses
|
||||
let returnVal = choiceMap[result] || false;
|
||||
return this.validateAndReturn(returnVal);
|
||||
});
|
||||
}
|
||||
|
||||
private validateAndReturn(value: any): any {
|
||||
if (!this.validate(value)) {
|
||||
return this.render();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private validate(value: any): boolean {
|
||||
const validationError = this._question.validate ? this._question.validate(value || '') : undefined;
|
||||
|
||||
if (validationError) {
|
||||
this._question.message = `${figures.warning} ${validationError}`;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
35
extensions/mssql/src/prompts/factory.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import Prompt from './prompt';
|
||||
import InputPrompt from './input';
|
||||
import PasswordPrompt from './password';
|
||||
import ListPrompt from './list';
|
||||
import ConfirmPrompt from './confirm';
|
||||
import CheckboxPrompt from './checkbox';
|
||||
import ExpandPrompt from './expand';
|
||||
|
||||
export default class PromptFactory {
|
||||
|
||||
public static createPrompt(question: any, ignoreFocusOut?: boolean): Prompt {
|
||||
switch (question.type || 'input') {
|
||||
case 'string':
|
||||
case 'input':
|
||||
return new InputPrompt(question, ignoreFocusOut);
|
||||
case 'password':
|
||||
return new PasswordPrompt(question, ignoreFocusOut);
|
||||
case 'list':
|
||||
return new ListPrompt(question, ignoreFocusOut);
|
||||
case 'confirm':
|
||||
return new ConfirmPrompt(question, ignoreFocusOut);
|
||||
case 'checkbox':
|
||||
return new CheckboxPrompt(question, ignoreFocusOut);
|
||||
case 'expand':
|
||||
return new ExpandPrompt(question, ignoreFocusOut);
|
||||
default:
|
||||
throw new Error(`Could not find a prompt for question type ${question.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
extensions/mssql/src/prompts/input.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import { window, InputBoxOptions } from 'vscode';
|
||||
import Prompt from './prompt';
|
||||
import EscapeException from '../escapeException';
|
||||
|
||||
const figures = require('figures');
|
||||
|
||||
export default class InputPrompt extends Prompt {
|
||||
|
||||
protected _options: InputBoxOptions;
|
||||
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
super(question, ignoreFocusOut);
|
||||
|
||||
this._options = this.defaultInputBoxOptions;
|
||||
this._options.prompt = this._question.message;
|
||||
}
|
||||
|
||||
// Helper for callers to know the right type to get from the type factory
|
||||
public static get promptType(): string { return 'input'; }
|
||||
|
||||
public render(): any {
|
||||
// Prefer default over the placeHolder, if specified
|
||||
let placeHolder = this._question.default ? this._question.default : this._question.placeHolder;
|
||||
|
||||
if (this._question.default instanceof Error) {
|
||||
placeHolder = this._question.default.message;
|
||||
this._question.default = undefined;
|
||||
}
|
||||
|
||||
this._options.placeHolder = placeHolder;
|
||||
|
||||
return window.showInputBox(this._options)
|
||||
.then(result => {
|
||||
if (result === undefined) {
|
||||
throw new EscapeException();
|
||||
}
|
||||
|
||||
if (result === '') {
|
||||
// Use the default value, if defined
|
||||
result = this._question.default || '';
|
||||
}
|
||||
|
||||
const validationError = this._question.validate ? this._question.validate(result || '') : undefined;
|
||||
|
||||
if (validationError) {
|
||||
this._question.default = new Error(`${figures.warning} ${validationError}`);
|
||||
|
||||
return this.render();
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
33
extensions/mssql/src/prompts/list.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import { window } from 'vscode';
|
||||
import Prompt from './prompt';
|
||||
import EscapeException from '../escapeException';
|
||||
|
||||
export default class ListPrompt extends Prompt {
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
super(question, ignoreFocusOut);
|
||||
}
|
||||
|
||||
public render(): any {
|
||||
const choices = this._question.choices.reduce((result, choice) => {
|
||||
result[choice.name] = choice.value;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
let options = this.defaultQuickPickOptions;
|
||||
options.placeHolder = this._question.message;
|
||||
|
||||
return window.showQuickPick(Object.keys(choices), options)
|
||||
.then(result => {
|
||||
if (result === undefined) {
|
||||
throw new EscapeException();
|
||||
}
|
||||
|
||||
return choices[result];
|
||||
});
|
||||
}
|
||||
}
|
||||
15
extensions/mssql/src/prompts/password.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import InputPrompt from './input';
|
||||
|
||||
export default class PasswordPrompt extends InputPrompt {
|
||||
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
super(question, ignoreFocusOut);
|
||||
|
||||
this._options.password = true;
|
||||
}
|
||||
}
|
||||
70
extensions/mssql/src/prompts/progressIndicator.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import {window, StatusBarItem, StatusBarAlignment} from 'vscode';
|
||||
|
||||
export default class ProgressIndicator {
|
||||
|
||||
private _statusBarItem: StatusBarItem;
|
||||
|
||||
constructor() {
|
||||
this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left);
|
||||
}
|
||||
|
||||
private _tasks: string[] = [];
|
||||
public beginTask(task: string): void {
|
||||
this._tasks.push(task);
|
||||
this.displayProgressIndicator();
|
||||
}
|
||||
|
||||
public endTask(task: string): void {
|
||||
if (this._tasks.length > 0) {
|
||||
this._tasks.pop();
|
||||
}
|
||||
|
||||
this.setMessage();
|
||||
}
|
||||
|
||||
private setMessage(): void {
|
||||
if (this._tasks.length === 0) {
|
||||
this._statusBarItem.text = '';
|
||||
this.hideProgressIndicator();
|
||||
return;
|
||||
}
|
||||
|
||||
this._statusBarItem.text = this._tasks[this._tasks.length - 1];
|
||||
this._statusBarItem.show();
|
||||
}
|
||||
|
||||
private _interval: any;
|
||||
private displayProgressIndicator(): void {
|
||||
this.setMessage();
|
||||
this.hideProgressIndicator();
|
||||
this._interval = setInterval(() => this.onDisplayProgressIndicator(), 100);
|
||||
}
|
||||
private hideProgressIndicator(): void {
|
||||
if (this._interval) {
|
||||
clearInterval(this._interval);
|
||||
this._interval = undefined;
|
||||
}
|
||||
this.ProgressCounter = 0;
|
||||
}
|
||||
|
||||
private ProgressText = ['|', '/', '-', '\\', '|', '/', '-', '\\'];
|
||||
private ProgressCounter = 0;
|
||||
private onDisplayProgressIndicator(): void {
|
||||
if (this._tasks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let txt = this.ProgressText[this.ProgressCounter];
|
||||
this._statusBarItem.text = this._tasks[this._tasks.length - 1] + ' ' + txt;
|
||||
this.ProgressCounter++;
|
||||
|
||||
if (this.ProgressCounter >= this.ProgressText.length - 1) {
|
||||
this.ProgressCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
extensions/mssql/src/prompts/prompt.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
|
||||
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
|
||||
|
||||
import { InputBoxOptions, QuickPickOptions } from 'vscode';
|
||||
|
||||
abstract class Prompt {
|
||||
|
||||
protected _question: any;
|
||||
protected _ignoreFocusOut?: boolean;
|
||||
|
||||
constructor(question: any, ignoreFocusOut?: boolean) {
|
||||
this._question = question;
|
||||
this._ignoreFocusOut = ignoreFocusOut ? ignoreFocusOut : false;
|
||||
}
|
||||
|
||||
public abstract render(): any;
|
||||
|
||||
protected get defaultQuickPickOptions(): QuickPickOptions {
|
||||
return {
|
||||
ignoreFocusOut: this._ignoreFocusOut
|
||||
};
|
||||
}
|
||||
|
||||
protected get defaultInputBoxOptions(): InputBoxOptions {
|
||||
return {
|
||||
ignoreFocusOut: this._ignoreFocusOut
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default Prompt;
|
||||
68
extensions/mssql/src/prompts/question.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import vscode = require('vscode');
|
||||
|
||||
export class QuestionTypes {
|
||||
public static get input(): string { return 'input'; }
|
||||
public static get password(): string { return 'password'; }
|
||||
public static get list(): string { return 'list'; }
|
||||
public static get confirm(): string { return 'confirm'; }
|
||||
public static get checkbox(): string { return 'checkbox'; }
|
||||
public static get expand(): string { return 'expand'; }
|
||||
}
|
||||
|
||||
// Question interface to clarify how to use the prompt feature
|
||||
// based on Bower Question format: https://github.com/bower/bower/blob/89069784bb46bfd6639b4a75e98a0d7399a8c2cb/packages/bower-logger/README.md
|
||||
export interface IQuestion {
|
||||
// Type of question (see QuestionTypes)
|
||||
type: string;
|
||||
// Name of the question for disambiguation
|
||||
name: string;
|
||||
// Message to display to the user
|
||||
message: string;
|
||||
// Optional placeHolder to give more detailed information to the user
|
||||
placeHolder?: any;
|
||||
// Optional default value - this will be used instead of placeHolder
|
||||
default?: any;
|
||||
// optional set of choices to be used. Can be QuickPickItems or a simple name-value pair
|
||||
choices?: Array<vscode.QuickPickItem | INameValueChoice>;
|
||||
// Optional validation function that returns an error string if validation fails
|
||||
validate?: (value: any) => string;
|
||||
// Optional pre-prompt function. Takes in set of answers so far, and returns true if prompt should occur
|
||||
shouldPrompt?: (answers: { [id: string]: any }) => boolean;
|
||||
// Optional action to take on the question being answered
|
||||
onAnswered?: (value: any) => void;
|
||||
// Optional set of options to support matching choices.
|
||||
matchOptions?: vscode.QuickPickOptions;
|
||||
}
|
||||
|
||||
// Pair used to display simple choices to the user
|
||||
export interface INameValueChoice {
|
||||
name: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
// Generic object that can be used to define a set of questions and handle the result
|
||||
export interface IQuestionHandler {
|
||||
// Set of questions to be answered
|
||||
questions: IQuestion[];
|
||||
// Optional callback, since questions may handle themselves
|
||||
callback?: IPromptCallback;
|
||||
}
|
||||
|
||||
export interface IPrompter {
|
||||
promptSingle<T>(question: IQuestion, ignoreFocusOut?: boolean): Promise<T>;
|
||||
/**
|
||||
* Prompts for multiple questions
|
||||
*
|
||||
* @returns {[questionId: string]: T} Map of question IDs to results, or undefined if
|
||||
* the user canceled the question session
|
||||
*/
|
||||
prompt<T>(questions: IQuestion[], ignoreFocusOut?: boolean): Promise<{ [questionId: string]: any }>;
|
||||
promptCallback(questions: IQuestion[], callback: IPromptCallback): void;
|
||||
}
|
||||
|
||||
export interface IPromptCallback {
|
||||
(answers: { [id: string]: any }): void;
|
||||
}
|
||||
@@ -4,15 +4,12 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as path from 'path';
|
||||
import * as sqlops from 'sqlops';
|
||||
import { IConfig, ServerProvider } from 'service-downloader';
|
||||
import { SqlOpsDataClient, SqlOpsFeature, ClientOptions } from 'dataprotocol-client';
|
||||
import { ServerCapabilities, ClientCapabilities, RPCMessageType, ServerOptions, TransportKind } from 'vscode-languageclient';
|
||||
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import { Disposable } from 'vscode';
|
||||
|
||||
import { CreateFirewallRuleRequest, HandleFirewallRuleRequest, CreateFirewallRuleParams, HandleFirewallRuleParams } from './contracts';
|
||||
import * as Constants from './constants';
|
||||
import * as Utils from '../utils';
|
||||
|
||||
140
extensions/mssql/src/sparkFeature/dialog/dialogCommands.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as vscode from 'vscode';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { ICommandViewContext, Command, ICommandObjectExplorerContext, ICommandUnknownContext } from '../../objectExplorerNodeProvider/command';
|
||||
import { SparkJobSubmissionDialog } from './sparkJobSubmission/sparkJobSubmissionDialog';
|
||||
import { AppContext } from '../../appContext';
|
||||
import { getErrorMessage } from '../../utils';
|
||||
import * as constants from '../../constants';
|
||||
import { HdfsFileSourceNode } from '../../objectExplorerNodeProvider/hdfsProvider';
|
||||
import { getNode } from '../../objectExplorerNodeProvider/hdfsCommands';
|
||||
import * as LocalizedConstants from '../../localizedConstants';
|
||||
import * as SqlClusterLookUp from '../../sqlClusterLookUp';
|
||||
import { SqlClusterConnection } from '../../objectExplorerNodeProvider/connection';
|
||||
|
||||
export class OpenSparkJobSubmissionDialogCommand extends Command {
|
||||
constructor(appContext: AppContext, private outputChannel: vscode.OutputChannel) {
|
||||
super(constants.mssqlClusterLivySubmitSparkJobCommand, appContext);
|
||||
}
|
||||
|
||||
protected async preExecute(context: ICommandUnknownContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
|
||||
return this.execute(context, args);
|
||||
}
|
||||
|
||||
async execute(context: ICommandUnknownContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
|
||||
try {
|
||||
let sqlClusterConnection: SqlClusterConnection = undefined;
|
||||
if (context.type === constants.ObjectExplorerService) {
|
||||
sqlClusterConnection = SqlClusterLookUp.findSqlClusterConnection(context, this.appContext);
|
||||
}
|
||||
if (!sqlClusterConnection) {
|
||||
sqlClusterConnection = await this.selectConnection();
|
||||
}
|
||||
|
||||
let dialog = new SparkJobSubmissionDialog(sqlClusterConnection, this.appContext, this.outputChannel);
|
||||
await dialog.openDialog();
|
||||
} catch (error) {
|
||||
this.appContext.apiWrapper.showErrorMessage(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
private async selectConnection(): Promise<SqlClusterConnection> {
|
||||
let connectionList: sqlops.connection.Connection[] = await this.apiWrapper.getActiveConnections();
|
||||
let displayList: string[] = new Array();
|
||||
let connectionMap: Map<string, sqlops.connection.Connection> = new Map();
|
||||
if (connectionList && connectionList.length > 0) {
|
||||
connectionList.forEach(conn => {
|
||||
if (conn.providerName === constants.sqlProviderName) {
|
||||
displayList.push(conn.options.host);
|
||||
connectionMap.set(conn.options.host, conn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let selectedHost: string = await vscode.window.showQuickPick(displayList, {
|
||||
placeHolder:
|
||||
localize('sparkJobSubmission_PleaseSelectSqlWithCluster',
|
||||
'Please select SQL Server with Big Data Cluster. ')
|
||||
});
|
||||
let errorMsg = localize('sparkJobSubmission_NoSqlSelected', 'No Sql Server is selected.');
|
||||
if (!selectedHost) { throw new Error(errorMsg); }
|
||||
|
||||
let sqlConnection = connectionMap.get(selectedHost);
|
||||
if (!sqlConnection) { throw new Error(errorMsg); }
|
||||
|
||||
let sqlClusterConnection = await SqlClusterLookUp.getSqlClusterConnection(sqlConnection);
|
||||
if (!sqlClusterConnection) {
|
||||
throw new Error(LocalizedConstants.sparkJobSubmissionNoSqlBigDataClusterFound);
|
||||
}
|
||||
|
||||
return new SqlClusterConnection(sqlClusterConnection);
|
||||
}
|
||||
}
|
||||
|
||||
// Open the submission dialog for a specific file path.
|
||||
export class OpenSparkJobSubmissionDialogFromFileCommand extends Command {
|
||||
constructor(appContext: AppContext, private outputChannel: vscode.OutputChannel) {
|
||||
super(constants.mssqlClusterLivySubmitSparkJobFromFileCommand, appContext);
|
||||
}
|
||||
|
||||
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
|
||||
return this.execute(context, args);
|
||||
}
|
||||
|
||||
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
|
||||
let path: string = undefined;
|
||||
try {
|
||||
let node = await getNode<HdfsFileSourceNode>(context, this.appContext);
|
||||
if (node && node.hdfsPath) {
|
||||
path = node.hdfsPath;
|
||||
} else {
|
||||
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
this.apiWrapper.showErrorMessage(localize('sparkJobSubmission_GetFilePathFromSelectedNodeFailed', 'Error Get File Path: {0}', err));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let sqlClusterConnection: SqlClusterConnection = undefined;
|
||||
if (context.type === constants.ObjectExplorerService) {
|
||||
sqlClusterConnection = await SqlClusterLookUp.findSqlClusterConnection(context, this.appContext);
|
||||
}
|
||||
if (!sqlClusterConnection) {
|
||||
throw new Error(LocalizedConstants.sparkJobSubmissionNoSqlBigDataClusterFound);
|
||||
}
|
||||
let dialog = new SparkJobSubmissionDialog(sqlClusterConnection, this.appContext, this.outputChannel);
|
||||
await dialog.openDialog(path);
|
||||
} catch (error) {
|
||||
this.appContext.apiWrapper.showErrorMessage(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenSparkJobSubmissionDialogTask {
|
||||
constructor(private appContext: AppContext, private outputChannel: vscode.OutputChannel) {
|
||||
}
|
||||
|
||||
async execute(profile: sqlops.IConnectionProfile, ...args: any[]): Promise<void> {
|
||||
try {
|
||||
let sqlClusterConnection = SqlClusterLookUp.findSqlClusterConnection(profile, this.appContext);
|
||||
if (!sqlClusterConnection) {
|
||||
throw new Error(LocalizedConstants.sparkJobSubmissionNoSqlBigDataClusterFound);
|
||||
}
|
||||
let dialog = new SparkJobSubmissionDialog(sqlClusterConnection, this.appContext, this.outputChannel);
|
||||
await dialog.openDialog();
|
||||
} catch (error) {
|
||||
this.appContext.apiWrapper.showErrorMessage(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { SparkJobSubmissionModel } from './sparkJobSubmissionModel';
|
||||
import { AppContext } from '../../../appContext';
|
||||
import { ApiWrapper } from '../../../apiWrapper';
|
||||
|
||||
export class SparkAdvancedTab {
|
||||
private _tab: sqlops.window.modelviewdialog.DialogTab;
|
||||
public get tab(): sqlops.window.modelviewdialog.DialogTab { return this._tab; }
|
||||
|
||||
private _referenceFilesInputBox: sqlops.InputBoxComponent;
|
||||
private _referenceJARFilesInputBox: sqlops.InputBoxComponent;
|
||||
private _referencePyFilesInputBox: sqlops.InputBoxComponent;
|
||||
|
||||
private get apiWrapper(): ApiWrapper {
|
||||
return this.appContext.apiWrapper;
|
||||
}
|
||||
|
||||
constructor(private appContext: AppContext) {
|
||||
this._tab = this.apiWrapper.createTab(localize('sparkJobSubmission_AdvancedTabName', 'ADVANCED'));
|
||||
|
||||
this._tab.registerContent(async (modelView) => {
|
||||
let builder = modelView.modelBuilder;
|
||||
let parentLayout: sqlops.FormItemLayout = {
|
||||
horizontal: false,
|
||||
componentWidth: '400px'
|
||||
};
|
||||
|
||||
let formContainer = builder.formContainer();
|
||||
|
||||
this._referenceJARFilesInputBox = builder.inputBox().component();
|
||||
formContainer.addFormItem({
|
||||
component: this._referenceJARFilesInputBox,
|
||||
title: localize('sparkJobSubmission_ReferenceJarList', 'Reference Jars')
|
||||
},
|
||||
Object.assign(
|
||||
{
|
||||
info: localize('sparkJobSubmission_ReferenceJarListToolTip',
|
||||
'Jars to be placed in executor working directory. The Jar path needs to be an HDFS Path. Multiple paths should be split by semicolon (;)')
|
||||
},
|
||||
parentLayout));
|
||||
|
||||
this._referencePyFilesInputBox = builder.inputBox().component();
|
||||
formContainer.addFormItem({
|
||||
component: this._referencePyFilesInputBox,
|
||||
title: localize('sparkJobSubmission_ReferencePyList', 'Reference py Files')
|
||||
},
|
||||
Object.assign(
|
||||
{
|
||||
info: localize('sparkJobSubmission_ReferencePyListTooltip',
|
||||
'Py Files to be placed in executor working directory. The file path needs to be an HDFS Path. Multiple paths should be split by semicolon(;)')
|
||||
},
|
||||
parentLayout));
|
||||
|
||||
this._referenceFilesInputBox = builder.inputBox().component();
|
||||
formContainer.addFormItem({
|
||||
component: this._referenceFilesInputBox,
|
||||
title: localize('sparkJobSubmission_ReferenceFilesList', 'Reference Files')
|
||||
},
|
||||
Object.assign({
|
||||
info: localize('sparkJobSubmission_ReferenceFilesListTooltip',
|
||||
'Files to be placed in executor working directory. The file path needs to be an HDFS Path. Multiple paths should be split by semicolon(;)')
|
||||
}, parentLayout));
|
||||
|
||||
await modelView.initializeModel(formContainer.component());
|
||||
});
|
||||
}
|
||||
|
||||
public getInputValues(): string[] {
|
||||
return [this._referenceJARFilesInputBox.value, this._referencePyFilesInputBox.value, this._referenceFilesInputBox.value];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as fspath from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as vscode from 'vscode';
|
||||
import * as utils from '../../../utils';
|
||||
import * as LocalizedConstants from '../../../localizedConstants';
|
||||
import * as constants from '../../../constants';
|
||||
|
||||
import { AppContext } from '../../../appContext';
|
||||
import { ApiWrapper } from '../../../apiWrapper';
|
||||
import { SparkJobSubmissionModel } from './sparkJobSubmissionModel';
|
||||
import { SparkFileSource } from './sparkJobSubmissionService';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class SparkConfigurationTab {
|
||||
private _tab: sqlops.window.modelviewdialog.DialogTab;
|
||||
public get tab(): sqlops.window.modelviewdialog.DialogTab { return this._tab; }
|
||||
|
||||
private _jobNameInputBox: sqlops.InputBoxComponent;
|
||||
private _sparkContextLabel: sqlops.TextComponent;
|
||||
private _fileSourceDropDown: sqlops.DropDownComponent;
|
||||
private _sparkSourceFileInputBox: sqlops.InputBoxComponent;
|
||||
private _filePickerButton: sqlops.ButtonComponent;
|
||||
private _sourceFlexContainer: sqlops.FlexContainer;
|
||||
private _sourceFlexContainerWithHint: sqlops.FlexContainer;
|
||||
private _localUploadDestinationLabel: sqlops.TextComponent;
|
||||
private _mainClassInputBox: sqlops.InputBoxComponent;
|
||||
private _argumentsInputBox: sqlops.InputBoxComponent;
|
||||
|
||||
private get apiWrapper(): ApiWrapper {
|
||||
return this.appContext.apiWrapper;
|
||||
}
|
||||
|
||||
// If path is specified, means the default source setting for this tab is HDFS file, otherwise, it would be local file.
|
||||
constructor(private _dataModel: SparkJobSubmissionModel, private appContext: AppContext, private _path?: string) {
|
||||
this._tab = this.apiWrapper.createTab(localize('sparkJobSubmission_GeneralTabName', 'GENERAL'));
|
||||
|
||||
this._tab.registerContent(async (modelView) => {
|
||||
let builder = modelView.modelBuilder;
|
||||
let parentLayout: sqlops.FormItemLayout = {
|
||||
horizontal: false,
|
||||
componentWidth: '400px'
|
||||
};
|
||||
|
||||
let formContainer = builder.formContainer();
|
||||
|
||||
this._jobNameInputBox = builder.inputBox().withProperties({
|
||||
placeHolder: localize('sparkJobSubmission_JobNamePlaceHolder', 'Enter a name ...'),
|
||||
value: (this._path) ? fspath.basename(this._path) : ''
|
||||
}).component();
|
||||
|
||||
formContainer.addFormItem({
|
||||
component: this._jobNameInputBox,
|
||||
title: localize('sparkJobSubmission_JobName', 'Job Name'),
|
||||
required: true
|
||||
}, parentLayout);
|
||||
|
||||
this._sparkContextLabel = builder.text().withProperties({
|
||||
value: this._dataModel.getSparkClusterUrl()
|
||||
}).component();
|
||||
formContainer.addFormItem({
|
||||
component: this._sparkContextLabel,
|
||||
title: localize('sparkJobSubmission_SparkCluster', 'Spark Cluster')
|
||||
}, parentLayout);
|
||||
|
||||
this._fileSourceDropDown = builder.dropDown().withProperties<sqlops.DropDownProperties>({
|
||||
values: [SparkFileSource.Local.toString(), SparkFileSource.HDFS.toString()],
|
||||
value: (this._path) ? SparkFileSource.HDFS.toString() : SparkFileSource.Local.toString()
|
||||
}).component();
|
||||
|
||||
this._fileSourceDropDown.onValueChanged(selection => {
|
||||
let isLocal = selection.selected === SparkFileSource.Local.toString();
|
||||
// Disable browser button for cloud source.
|
||||
if (this._filePickerButton) {
|
||||
this._filePickerButton.updateProperties({
|
||||
enabled: isLocal,
|
||||
required: isLocal
|
||||
});
|
||||
}
|
||||
|
||||
// Clear the path When switching source.
|
||||
if (this._sparkSourceFileInputBox) {
|
||||
this._sparkSourceFileInputBox.value = '';
|
||||
}
|
||||
|
||||
if (this._localUploadDestinationLabel) {
|
||||
if (isLocal) {
|
||||
this._localUploadDestinationLabel.value = LocalizedConstants.sparkLocalFileDestinationHint;
|
||||
} else {
|
||||
this._localUploadDestinationLabel.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._sparkSourceFileInputBox = builder.inputBox().withProperties({
|
||||
required: true,
|
||||
placeHolder: localize('sparkJobSubmission_FilePathPlaceHolder', 'Path to a .jar or .py file'),
|
||||
value: (this._path) ? this._path : ''
|
||||
}).component();
|
||||
this._sparkSourceFileInputBox.onTextChanged(text => {
|
||||
if (this._fileSourceDropDown.value === SparkFileSource.Local.toString()) {
|
||||
this._dataModel.updateModelByLocalPath(text);
|
||||
if (this._localUploadDestinationLabel) {
|
||||
if (text) {
|
||||
this._localUploadDestinationLabel.value = localize('sparkJobSubmission_LocalFileDestinationHintWithPath',
|
||||
'The selected local file will be uploaded to HDFS: {0}', this._dataModel.hdfsSubmitFilePath);
|
||||
} else {
|
||||
this._localUploadDestinationLabel.value = LocalizedConstants.sparkLocalFileDestinationHint;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._dataModel.hdfsSubmitFilePath = text;
|
||||
}
|
||||
|
||||
// main class disable/enable is according to whether it's jar file.
|
||||
let isJarFile = this._dataModel.isJarFile();
|
||||
this._mainClassInputBox.updateProperties({ enabled: isJarFile, required: isJarFile });
|
||||
if (!isJarFile) {
|
||||
// Clear main class for py file.
|
||||
this._mainClassInputBox.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
this._filePickerButton = builder.button().withProperties({
|
||||
required: (this._path) ? false : true,
|
||||
enabled: (this._path) ? false : true,
|
||||
label: '•••',
|
||||
width: constants.mssqlClusterSparkJobFileSelectorButtonWidth,
|
||||
height: constants.mssqlClusterSparkJobFileSelectorButtonHeight
|
||||
}).component();
|
||||
this._filePickerButton.onDidClick(() => this.onSelectFile());
|
||||
|
||||
this._sourceFlexContainer = builder.flexContainer().component();
|
||||
this._sourceFlexContainer.addItem(this._fileSourceDropDown, { flex: '0 0 auto', CSSStyles: { 'minWidth': '75px', 'marginBottom': '5px', 'paddingRight': '3px' } });
|
||||
this._sourceFlexContainer.addItem(this._sparkSourceFileInputBox, { flex: '1 1 auto', CSSStyles: { 'marginBottom': '5px', 'paddingRight': '3px' } });
|
||||
// Do not add margin for file picker button as the label forces it to have 5px margin
|
||||
this._sourceFlexContainer.addItem(this._filePickerButton, { flex: '0 0 auto' });
|
||||
this._sourceFlexContainer.setLayout({
|
||||
flexFlow: 'row',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignContent: 'stretch'
|
||||
});
|
||||
|
||||
this._localUploadDestinationLabel = builder.text().withProperties({
|
||||
value: (this._path) ? '' : LocalizedConstants.sparkLocalFileDestinationHint
|
||||
}).component();
|
||||
this._sourceFlexContainerWithHint = builder.flexContainer().component();
|
||||
this._sourceFlexContainerWithHint.addItem(this._sourceFlexContainer, { flex: '0 0 auto' });
|
||||
this._sourceFlexContainerWithHint.addItem(this._localUploadDestinationLabel, { flex: '1 1 auto' });
|
||||
this._sourceFlexContainerWithHint.setLayout({
|
||||
flexFlow: 'column',
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'stretch',
|
||||
alignContent: 'stretch'
|
||||
});
|
||||
|
||||
formContainer.addFormItem({
|
||||
component: this._sourceFlexContainerWithHint,
|
||||
title: localize('sparkJobSubmission_MainFilePath', 'JAR/py File'),
|
||||
required: true
|
||||
}, parentLayout);
|
||||
|
||||
this._mainClassInputBox = builder.inputBox().component();
|
||||
formContainer.addFormItem({
|
||||
component: this._mainClassInputBox,
|
||||
title: localize('sparkJobSubmission_MainClass', 'Main Class'),
|
||||
required: true
|
||||
}, parentLayout);
|
||||
|
||||
this._argumentsInputBox = builder.inputBox().component();
|
||||
formContainer.addFormItem({
|
||||
component: this._argumentsInputBox,
|
||||
title: localize('sparkJobSubmission_Arguments', 'Arguments')
|
||||
},
|
||||
Object.assign(
|
||||
{ info: localize('sparkJobSubmission_ArgumentsTooltip', 'Command line arguments used in your main class, multiple arguments should be split by space.') },
|
||||
parentLayout));
|
||||
|
||||
await modelView.initializeModel(formContainer.component());
|
||||
});
|
||||
}
|
||||
|
||||
public async validate(): Promise<boolean> {
|
||||
if (!this._jobNameInputBox.value) {
|
||||
this._dataModel.showDialogError(localize('sparkJobSubmission_NotSpecifyJobName', 'Property Job Name is not specified.'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._fileSourceDropDown.value === SparkFileSource.Local.toString()) {
|
||||
if (this._sparkSourceFileInputBox.value) {
|
||||
this._dataModel.isMainSourceFromLocal = true;
|
||||
this._dataModel.updateModelByLocalPath(this._sparkSourceFileInputBox.value);
|
||||
} else {
|
||||
this._dataModel.showDialogError(localize('sparkJobSubmission_NotSpecifyJARPYPath', 'Property JAR/py File is not specified.'));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (this._sparkSourceFileInputBox.value) {
|
||||
this._dataModel.isMainSourceFromLocal = false;
|
||||
this._dataModel.hdfsSubmitFilePath = this._sparkSourceFileInputBox.value;
|
||||
} else {
|
||||
this._dataModel.showDialogError(localize('sparkJobSubmission_NotSpecifyJARPYPath', 'Property JAR/py File is not specified.'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._dataModel.isJarFile() && !this._mainClassInputBox.value) {
|
||||
this._dataModel.showDialogError(localize('sparkJobSubmission_NotSpecifyMainClass', 'Property Main Class is not specified.'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. For local file Source check whether they existed.
|
||||
if (this._dataModel.isMainSourceFromLocal) {
|
||||
if (!fs.existsSync(this._dataModel.localFileSourcePath)) {
|
||||
this._dataModel.showDialogError(LocalizedConstants.sparkJobSubmissionLocalFileNotExisted(this._dataModel.localFileSourcePath));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// 2. Check HDFS file existed for HDFS source.
|
||||
try {
|
||||
let isFileExisted = await this._dataModel.isClusterFileExisted(this._dataModel.hdfsSubmitFilePath);
|
||||
if (!isFileExisted) {
|
||||
this._dataModel.showDialogError(localize('sparkJobSubmission_HDFSFileNotExistedWithPath', '{0} does not exist in Cluster or exception thrown. ', this._dataModel.hdfsSubmitFilePath));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
this._dataModel.showDialogError(localize('sparkJobSubmission_HDFSFileNotExisted', 'The specified HDFS file does not exist. '));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async onSelectFile(): Promise<void> {
|
||||
let filePath = await this.pickFile();
|
||||
if (filePath) {
|
||||
this._sparkSourceFileInputBox.value = filePath;
|
||||
}
|
||||
}
|
||||
|
||||
public getInputValues(): string[] {
|
||||
return [this._jobNameInputBox.value, this._mainClassInputBox.value, this._argumentsInputBox.value];
|
||||
}
|
||||
|
||||
public async pickFile(): Promise<string> {
|
||||
try {
|
||||
let filter = { 'JAR/py files': ['jar', 'py'] };
|
||||
let options: vscode.OpenDialogOptions = {
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
openLabel: localize('sparkSelectLocalFile', 'Select'),
|
||||
filters: filter
|
||||
};
|
||||
|
||||
let fileUris: vscode.Uri[] = await this.apiWrapper.showOpenDialog(options);
|
||||
if (fileUris && fileUris[0]) {
|
||||
return fileUris[0].fsPath;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
} catch (err) {
|
||||
this.apiWrapper.showErrorMessage(localize('sparkJobSubmission_SelectFileError', 'Error in locating the file due to Error: {0}', utils.getErrorMessage(err)));
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as utils from '../../../utils';
|
||||
import * as LocalizedConstants from '../../../localizedConstants';
|
||||
|
||||
import { AppContext } from '../../../appContext';
|
||||
import { ApiWrapper } from '../../../apiWrapper';
|
||||
import { SparkJobSubmissionModel } from './sparkJobSubmissionModel';
|
||||
import { SparkConfigurationTab } from './sparkConfigurationTab';
|
||||
import { SparkJobSubmissionInput } from './sparkJobSubmissionService';
|
||||
import { SparkAdvancedTab } from './sparkAdvancedTab';
|
||||
import { SqlClusterConnection } from '../../../objectExplorerNodeProvider/connection';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class SparkJobSubmissionDialog {
|
||||
private _dialog: sqlops.window.modelviewdialog.Dialog;
|
||||
private _dataModel: SparkJobSubmissionModel;
|
||||
private _sparkConfigTab: SparkConfigurationTab;
|
||||
private _sparkAdvancedTab: SparkAdvancedTab;
|
||||
private get apiWrapper(): ApiWrapper {
|
||||
return this.appContext.apiWrapper;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private sqlClusterConnection: SqlClusterConnection,
|
||||
private appContext: AppContext,
|
||||
private outputChannel: vscode.OutputChannel) {
|
||||
if (!this.sqlClusterConnection || !this.appContext || !this.outputChannel) {
|
||||
throw new Error(localize('sparkJobSubmission_SparkJobSubmissionDialogInitializeError',
|
||||
'Parameters for SparkJobSubmissionDialog is illegal'));
|
||||
}
|
||||
}
|
||||
|
||||
public async openDialog(path?: string): Promise<void> {
|
||||
this._dialog = this.apiWrapper.createDialog(localize('sparkJobSubmission_DialogTitleNewJob', 'New Job'));
|
||||
|
||||
this._dataModel = new SparkJobSubmissionModel(this.sqlClusterConnection, this._dialog, this.appContext);
|
||||
|
||||
this._sparkConfigTab = new SparkConfigurationTab(this._dataModel, this.appContext, path);
|
||||
this._sparkAdvancedTab = new SparkAdvancedTab(this.appContext);
|
||||
|
||||
this._dialog.content = [this._sparkConfigTab.tab, this._sparkAdvancedTab.tab];
|
||||
|
||||
this._dialog.cancelButton.label = localize('sparkJobSubmission_DialogCancelButton', 'Cancel');
|
||||
|
||||
this._dialog.okButton.label = localize('sparkJobSubmission_DialogSubmitButton', 'Submit');
|
||||
this._dialog.okButton.onClick(() => this.onClickOk());
|
||||
|
||||
this._dialog.registerCloseValidator(() => this.handleValidate());
|
||||
|
||||
await this.apiWrapper.openDialog(this._dialog);
|
||||
}
|
||||
|
||||
private onClickOk(): void {
|
||||
let jobName = localize('sparkJobSubmission_SubmitSparkJob', '{0} Spark Job Submission:',
|
||||
this._sparkConfigTab.getInputValues()[0]);
|
||||
this.apiWrapper.startBackgroundOperation(
|
||||
{
|
||||
connection: this.sqlClusterConnection.connection,
|
||||
displayName: jobName,
|
||||
description: jobName,
|
||||
isCancelable: false,
|
||||
operation: op => {
|
||||
this.onSubmit(op);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async onSubmit(op: sqlops.BackgroundOperation): Promise<void> {
|
||||
try {
|
||||
this.outputChannel.show();
|
||||
let msg = localize('sparkJobSubmission_SubmissionStartMessage',
|
||||
'.......................... Submit Spark Job Start ..........................');
|
||||
this.outputChannel.appendLine(msg);
|
||||
// 1. Upload local file to HDFS for local source.
|
||||
if (this._dataModel.isMainSourceFromLocal) {
|
||||
try {
|
||||
this.outputChannel.appendLine(this.addInfoTag(LocalizedConstants.sparkJobSubmissionPrepareUploadingFile(this._dataModel.localFileSourcePath, this._dataModel.hdfsFolderDestinationPath)));
|
||||
op.updateStatus(sqlops.TaskStatus.InProgress, LocalizedConstants.sparkJobSubmissionPrepareUploadingFile(this._dataModel.localFileSourcePath, this._dataModel.hdfsFolderDestinationPath));
|
||||
await this._dataModel.uploadFile(this._dataModel.localFileSourcePath, this._dataModel.hdfsFolderDestinationPath);
|
||||
vscode.window.showInformationMessage(LocalizedConstants.sparkJobSubmissionUploadingFileSucceeded);
|
||||
this.outputChannel.appendLine(this.addInfoTag(LocalizedConstants.sparkJobSubmissionUploadingFileSucceeded));
|
||||
op.updateStatus(sqlops.TaskStatus.InProgress, LocalizedConstants.sparkJobSubmissionUploadingFileSucceeded);
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(LocalizedConstants.sparkJobSubmissionUploadingFileFailed(utils.getErrorMessage(error)));
|
||||
this.outputChannel.appendLine(this.addErrorTag(LocalizedConstants.sparkJobSubmissionUploadingFileFailed(utils.getErrorMessage(error))));
|
||||
op.updateStatus(sqlops.TaskStatus.Failed, LocalizedConstants.sparkJobSubmissionUploadingFileFailed(utils.getErrorMessage(error)));
|
||||
this.outputChannel.appendLine(LocalizedConstants.sparkJobSubmissionEndMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Submit job to cluster.
|
||||
let submissionSettings: SparkJobSubmissionInput = this.getSubmissionInput();
|
||||
this.outputChannel.appendLine(this.addInfoTag(LocalizedConstants.sparkJobSubmissionPrepareSubmitJob(submissionSettings.jobName)));
|
||||
op.updateStatus(sqlops.TaskStatus.InProgress, LocalizedConstants.sparkJobSubmissionPrepareSubmitJob(submissionSettings.jobName));
|
||||
let livyBatchId = await this._dataModel.submitBatchJobByLivy(submissionSettings);
|
||||
vscode.window.showInformationMessage(LocalizedConstants.sparkJobSubmissionSparkJobHasBeenSubmitted);
|
||||
this.outputChannel.appendLine(this.addInfoTag(LocalizedConstants.sparkJobSubmissionSparkJobHasBeenSubmitted));
|
||||
op.updateStatus(sqlops.TaskStatus.InProgress, LocalizedConstants.sparkJobSubmissionSparkJobHasBeenSubmitted);
|
||||
|
||||
// 3. Get SparkHistory/YarnUI Url.
|
||||
try {
|
||||
let appId = await this._dataModel.getApplicationID(submissionSettings, livyBatchId);
|
||||
|
||||
let sparkHistoryUrl = this._dataModel.generateSparkHistoryUIUrl(submissionSettings, appId);
|
||||
vscode.window.showInformationMessage(LocalizedConstants.sparkJobSubmissionSparkHistoryLinkMessage(sparkHistoryUrl));
|
||||
this.outputChannel.appendLine(this.addInfoTag(LocalizedConstants.sparkJobSubmissionSparkHistoryLinkMessage(sparkHistoryUrl)));
|
||||
op.updateStatus(sqlops.TaskStatus.Succeeded, LocalizedConstants.sparkJobSubmissionSparkHistoryLinkMessage(sparkHistoryUrl));
|
||||
|
||||
/*
|
||||
// Spark Tracking URl is not working now.
|
||||
let sparkTrackingUrl = this._dataModel.generateSparkTrackingUIUrl(submissionSettings, appId);
|
||||
vscode.window.showInformationMessage(LocalizedConstants.sparkJobSubmissionTrackingLinkMessage(sparkTrackingUrl));
|
||||
this.outputChannel.appendLine(this.addInfoTag(LocalizedConstants.sparkJobSubmissionTrackingLinkMessage(sparkTrackingUrl)));
|
||||
op.updateStatus(sqlops.TaskStatus.Succeeded, LocalizedConstants.sparkJobSubmissionTrackingLinkMessage(sparkTrackingUrl));
|
||||
*/
|
||||
|
||||
let yarnUIUrl = this._dataModel.generateYarnUIUrl(submissionSettings, appId);
|
||||
vscode.window.showInformationMessage(LocalizedConstants.sparkJobSubmissionYarnUIMessage(yarnUIUrl));
|
||||
this.outputChannel.appendLine(this.addInfoTag(LocalizedConstants.sparkJobSubmissionYarnUIMessage(yarnUIUrl)));
|
||||
op.updateStatus(sqlops.TaskStatus.Succeeded, LocalizedConstants.sparkJobSubmissionYarnUIMessage(yarnUIUrl));
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(LocalizedConstants.sparkJobSubmissionGetApplicationIdFailed(utils.getErrorMessage(error)));
|
||||
this.outputChannel.appendLine(this.addErrorTag(LocalizedConstants.sparkJobSubmissionGetApplicationIdFailed(utils.getErrorMessage(error))));
|
||||
op.updateStatus(sqlops.TaskStatus.Failed, LocalizedConstants.sparkJobSubmissionGetApplicationIdFailed(utils.getErrorMessage(error)));
|
||||
this.outputChannel.appendLine(LocalizedConstants.sparkJobSubmissionEndMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
this.outputChannel.appendLine(LocalizedConstants.sparkJobSubmissionEndMessage);
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(LocalizedConstants.sparkJobSubmissionSubmitJobFailed(utils.getErrorMessage(error)));
|
||||
this.outputChannel.appendLine(this.addErrorTag(LocalizedConstants.sparkJobSubmissionSubmitJobFailed(utils.getErrorMessage(error))));
|
||||
op.updateStatus(sqlops.TaskStatus.Failed, LocalizedConstants.sparkJobSubmissionSubmitJobFailed(utils.getErrorMessage(error)));
|
||||
this.outputChannel.appendLine(LocalizedConstants.sparkJobSubmissionEndMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleValidate(): Promise<boolean> {
|
||||
return this._sparkConfigTab.validate();
|
||||
}
|
||||
|
||||
private getSubmissionInput(): SparkJobSubmissionInput {
|
||||
let generalConfig = this._sparkConfigTab.getInputValues();
|
||||
let advancedConfig = this._sparkAdvancedTab.getInputValues();
|
||||
return new SparkJobSubmissionInput(generalConfig[0], this._dataModel.hdfsSubmitFilePath, generalConfig[1], generalConfig[2],
|
||||
advancedConfig[0], advancedConfig[1], advancedConfig[2]);
|
||||
}
|
||||
|
||||
private addInfoTag(info: string): string {
|
||||
return `[Info] ${info}`;
|
||||
}
|
||||
|
||||
private addErrorTag(error: string): string {
|
||||
return `[Error] ${error}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
import * as fs from 'fs';
|
||||
import * as fspath from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
import * as constants from '../../../constants';
|
||||
import { SqlClusterConnection } from '../../../objectExplorerNodeProvider/connection';
|
||||
import * as LocalizedConstants from '../../../localizedConstants';
|
||||
import * as utils from '../../../utils';
|
||||
import { SparkJobSubmissionService, SparkJobSubmissionInput, LivyLogResponse } from './sparkJobSubmissionService';
|
||||
import { AppContext } from '../../../appContext';
|
||||
import { IFileSource, File, joinHdfsPath } from '../../../objectExplorerNodeProvider/fileSources';
|
||||
|
||||
|
||||
// Stores important state and service methods used by the Spark Job Submission Dialog.
|
||||
export class SparkJobSubmissionModel {
|
||||
private _dialogService: SparkJobSubmissionService;
|
||||
private _guidForClusterFolder: string;
|
||||
public get guidForClusterFolder(): string { return this._guidForClusterFolder; }
|
||||
|
||||
// Whether the file is from local or HDFS
|
||||
public isMainSourceFromLocal: boolean;
|
||||
|
||||
// indicate the final path to be submitted within HDFS
|
||||
public hdfsSubmitFilePath: string;
|
||||
|
||||
// local file uploading related path: source; destinationFolder
|
||||
public localFileSourcePath: string;
|
||||
public hdfsFolderDestinationPath: string;
|
||||
|
||||
constructor(
|
||||
private readonly _sqlClusterConnection: SqlClusterConnection,
|
||||
private readonly _dialog: sqlops.window.modelviewdialog.Dialog,
|
||||
private readonly _appContext: AppContext,
|
||||
requestService?: (args: any) => any) {
|
||||
|
||||
if (!this._sqlClusterConnection || !this._dialog || !this._appContext) {
|
||||
throw new Error(localize('sparkJobSubmission_SparkJobSubmissionModelInitializeError',
|
||||
'Parameters for SparkJobSubmissionModel is illegal'));
|
||||
}
|
||||
|
||||
this._dialogService = new SparkJobSubmissionService(requestService);
|
||||
this._guidForClusterFolder = utils.generateGuid();
|
||||
}
|
||||
|
||||
public get connection(): SqlClusterConnection { return this._sqlClusterConnection; }
|
||||
public get dialogService(): SparkJobSubmissionService { return this._dialogService; }
|
||||
public get dialog(): sqlops.window.modelviewdialog.Dialog { return this._dialog; }
|
||||
|
||||
public isJarFile(): boolean {
|
||||
if (this.hdfsSubmitFilePath) {
|
||||
return this.hdfsSubmitFilePath.toLowerCase().endsWith('jar');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public showDialogError(message: string): void {
|
||||
let errorLevel = sqlops.window.modelviewdialog.MessageLevel ? sqlops.window.modelviewdialog.MessageLevel : 0;
|
||||
this._dialog.message = {
|
||||
text: message,
|
||||
level: <sqlops.window.modelviewdialog.MessageLevel>errorLevel
|
||||
};
|
||||
}
|
||||
|
||||
public showDialogInfo(message: string): void {
|
||||
let infoLevel = sqlops.window.modelviewdialog.MessageLevel ? sqlops.window.modelviewdialog.MessageLevel.Information : 2;
|
||||
this._dialog.message = {
|
||||
text: message,
|
||||
level: infoLevel
|
||||
};
|
||||
}
|
||||
|
||||
public getSparkClusterUrl(): string {
|
||||
if (this._sqlClusterConnection && this._sqlClusterConnection.host && this._sqlClusterConnection.port) {
|
||||
return `https://${this._sqlClusterConnection.host}:${this._sqlClusterConnection.port}`;
|
||||
}
|
||||
|
||||
// Only for safety check, Won't happen with correct Model initialize.
|
||||
return '';
|
||||
}
|
||||
|
||||
public async submitBatchJobByLivy(submissionArgs: SparkJobSubmissionInput): Promise<string> {
|
||||
try {
|
||||
if (!submissionArgs) {
|
||||
return Promise.reject(localize('sparkJobSubmission_submissionArgsIsInvalid', 'submissionArgs is invalid. '));
|
||||
}
|
||||
|
||||
submissionArgs.setSparkClusterInfo(this._sqlClusterConnection);
|
||||
let livyBatchId = await this._dialogService.submitBatchJob(submissionArgs);
|
||||
return livyBatchId;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async getApplicationID(submissionArgs: SparkJobSubmissionInput, livyBatchId: string, retryTime?: number): Promise<string> {
|
||||
// TODO: whether set timeout as 15000ms
|
||||
try {
|
||||
if (!submissionArgs) {
|
||||
return Promise.reject(localize('sparkJobSubmission_submissionArgsIsInvalid', 'submissionArgs is invalid. '));
|
||||
}
|
||||
|
||||
if (!utils.isValidNumber(livyBatchId)) {
|
||||
return Promise.reject(new Error(localize('sparkJobSubmission_LivyBatchIdIsInvalid', 'livyBatchId is invalid. ')));
|
||||
}
|
||||
|
||||
if (!retryTime) {
|
||||
retryTime = constants.mssqlClusterLivyRetryTimesForCheckYarnApp;
|
||||
}
|
||||
|
||||
submissionArgs.setSparkClusterInfo(this._sqlClusterConnection);
|
||||
let response: LivyLogResponse = undefined;
|
||||
let timeOutCount: number = 0;
|
||||
do {
|
||||
timeOutCount++;
|
||||
await this.sleep(constants.mssqlClusterLivyTimeInMSForCheckYarnApp);
|
||||
response = await this._dialogService.getYarnAppId(submissionArgs, livyBatchId);
|
||||
} while (response.appId === '' && timeOutCount < retryTime);
|
||||
|
||||
if (response.appId === '') {
|
||||
return Promise.reject(localize('sparkJobSubmission_GetApplicationIdTimeOut', 'Get Application Id time out. {0}[Log] {1}', os.EOL, response.log));
|
||||
} else {
|
||||
return response.appId;
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadFile(localFilePath: string, hdfsFolderPath: string): Promise<void> {
|
||||
try {
|
||||
if (!localFilePath || !hdfsFolderPath) {
|
||||
return Promise.reject(localize('sparkJobSubmission_localFileOrFolderNotSpecified.', 'Property localFilePath or hdfsFolderPath is not specified. '));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(localFilePath)) {
|
||||
return Promise.reject(LocalizedConstants.sparkJobSubmissionLocalFileNotExisted(localFilePath));
|
||||
}
|
||||
|
||||
let fileSource: IFileSource = this._sqlClusterConnection.createHdfsFileSource();
|
||||
await fileSource.writeFile(new File(localFilePath, false), hdfsFolderPath);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async isClusterFileExisted(path: string): Promise<boolean> {
|
||||
try {
|
||||
if (!path) {
|
||||
return Promise.reject(localize('sparkJobSubmission_PathNotSpecified.', 'Property Path is not specified. '));
|
||||
}
|
||||
|
||||
let fileSource: IFileSource = this._sqlClusterConnection.createHdfsFileSource();
|
||||
return await fileSource.exists(path);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
public updateModelByLocalPath(localPath: string): void {
|
||||
if (localPath) {
|
||||
this.localFileSourcePath = localPath;
|
||||
this.hdfsFolderDestinationPath = this.generateDestinationFolder();
|
||||
let fileName = fspath.basename(localPath);
|
||||
this.hdfsSubmitFilePath = joinHdfsPath(this.hdfsFolderDestinationPath, fileName);
|
||||
} else {
|
||||
this.hdfsSubmitFilePath = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Example path: /SparkSubmission/2018/08/21/b682a6c4-1954-401e-8542-9c573d69d9c0/default_artifact.jar
|
||||
private generateDestinationFolder(): string {
|
||||
let day = new Date();
|
||||
return `/SparkSubmission/${day.getUTCFullYear()}/${day.getUTCMonth() + 1}/${day.getUTCDate()}/${this._guidForClusterFolder}`;
|
||||
}
|
||||
|
||||
// Example: https://host:30443/gateway/default/yarn/cluster/app/application_1532646201938_0057
|
||||
public generateYarnUIUrl(submissionArgs: SparkJobSubmissionInput, appId: string): string {
|
||||
return `https://${submissionArgs.host}:${submissionArgs.port}/gateway/default/yarn/cluster/app/${appId}`;
|
||||
}
|
||||
|
||||
// Example: https://host:30443/gateway/default/yarn/proxy/application_1532646201938_0411
|
||||
public generateSparkTrackingUIUrl(submissionArgs: SparkJobSubmissionInput, appId: string): string {
|
||||
return `https://${submissionArgs.host}:${submissionArgs.port}/gateway/default/yarn/proxy/${appId}`;
|
||||
}
|
||||
|
||||
// Example: https://host:30443/gateway/default/sparkhistory/history/application_1532646201938_0057/1
|
||||
public generateSparkHistoryUIUrl(submissionArgs: SparkJobSubmissionInput, appId: string): string {
|
||||
return `https://${submissionArgs.host}:${submissionArgs.port}/gateway/default/sparkhistory/history/${appId}/1`;
|
||||
}
|
||||
|
||||
private async sleep(ms: number): Promise<{}> {
|
||||
// tslint:disable-next-line no-string-based-set-timeout
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as os from 'os';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
import * as constants from '../../../constants';
|
||||
import { SqlClusterConnection } from '../../../objectExplorerNodeProvider/connection';
|
||||
import * as utils from '../../../utils';
|
||||
|
||||
export class SparkJobSubmissionService {
|
||||
private _requestPromise: (args: any) => any;
|
||||
|
||||
constructor(
|
||||
requestService?: (args: any) => any) {
|
||||
if (requestService) {
|
||||
// this is to fake the request service for test.
|
||||
this._requestPromise = requestService;
|
||||
} else {
|
||||
this._requestPromise = require('request-promise');
|
||||
}
|
||||
}
|
||||
|
||||
public async submitBatchJob(submissionArgs: SparkJobSubmissionInput): Promise<string> {
|
||||
try {
|
||||
let livyUrl: string = `https://${submissionArgs.host}:${submissionArgs.port}${submissionArgs.livyPath}/`;
|
||||
let options = {
|
||||
uri: livyUrl,
|
||||
method: 'POST',
|
||||
json: true,
|
||||
// TODO, change it back after service's authentication changed.
|
||||
rejectUnauthorized: false,
|
||||
body: {
|
||||
file: submissionArgs.sparkFile,
|
||||
proxyUser: submissionArgs.user,
|
||||
className: submissionArgs.mainClass,
|
||||
name: submissionArgs.jobName
|
||||
},
|
||||
// authentication headers
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + new Buffer(submissionArgs.user + ':' + submissionArgs.password).toString('base64')
|
||||
}
|
||||
};
|
||||
|
||||
// Set arguments
|
||||
if (submissionArgs.jobArguments && submissionArgs.jobArguments.trim()) {
|
||||
let argsList = submissionArgs.jobArguments.split(' ');
|
||||
if (argsList.length > 0) {
|
||||
options.body['args'] = argsList;
|
||||
}
|
||||
}
|
||||
|
||||
// Set jars files
|
||||
if (submissionArgs.jarFileList && submissionArgs.jarFileList.trim()) {
|
||||
let jarList = submissionArgs.jarFileList.split(';');
|
||||
if (jarList.length > 0) {
|
||||
options.body['jars'] = jarList;
|
||||
}
|
||||
}
|
||||
|
||||
// Set py files
|
||||
if (submissionArgs.pyFileList && submissionArgs.pyFileList.trim()) {
|
||||
let pyList = submissionArgs.pyFileList.split(';');
|
||||
if (pyList.length > 0) {
|
||||
options.body['pyFiles'] = pyList;
|
||||
}
|
||||
}
|
||||
|
||||
// Set other files
|
||||
if (submissionArgs.otherFileList && submissionArgs.otherFileList.trim()) {
|
||||
let otherList = submissionArgs.otherFileList.split(';');
|
||||
if (otherList.length > 0) {
|
||||
options.body['files'] = otherList;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this._requestPromise(options);
|
||||
if (response && utils.isValidNumber(response.id)) {
|
||||
return response.id;
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(localize('sparkJobSubmission_LivyNoBatchIdReturned',
|
||||
'No Spark job batch id is returned from response.{0}[Error] {1}', os.EOL, JSON.stringify(response))));
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async getYarnAppId(submissionArgs: SparkJobSubmissionInput, livyBatchId: string): Promise<LivyLogResponse> {
|
||||
try {
|
||||
let livyUrl = `https://${submissionArgs.host}:${submissionArgs.port}${submissionArgs.livyPath}/${livyBatchId}/log`;
|
||||
let options = {
|
||||
uri: livyUrl,
|
||||
method: 'GET',
|
||||
json: true,
|
||||
rejectUnauthorized: false,
|
||||
// authentication headers
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + new Buffer(submissionArgs.user + ':' + submissionArgs.password).toString('base64')
|
||||
}
|
||||
};
|
||||
|
||||
const response = await this._requestPromise(options);
|
||||
if (response && response.log) {
|
||||
return this.extractYarnAppIdFromLog(response.log);
|
||||
}
|
||||
|
||||
return Promise.reject(localize('sparkJobSubmission_LivyNoLogReturned',
|
||||
'No log is returned within response.{0}[Error] {1}', os.EOL, JSON.stringify(response)));
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extractYarnAppIdFromLog(log: any): LivyLogResponse {
|
||||
let logForPrint = log;
|
||||
if (Array.isArray(log)) {
|
||||
logForPrint = log.join(os.EOL);
|
||||
}
|
||||
|
||||
// eg: '18/08/23 11:02:50 INFO yarn.Client: Application report for application_1532646201938_0182 (state: ACCEPTED)'
|
||||
for (let entry of log) {
|
||||
if (entry.indexOf('Application report for') >= 0 && entry.indexOf('(state: ACCEPTED)') >= 0) {
|
||||
let tokens = entry.split(' ');
|
||||
for (let token of tokens) {
|
||||
if (token.startsWith('application_')) {
|
||||
return new LivyLogResponse(logForPrint, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new LivyLogResponse(logForPrint, '');
|
||||
}
|
||||
}
|
||||
|
||||
export class SparkJobSubmissionInput {
|
||||
public setSparkClusterInfo(sqlClusterConnection: SqlClusterConnection): void {
|
||||
this._host = sqlClusterConnection.host;
|
||||
this._port = sqlClusterConnection.port;
|
||||
this._livyPath = constants.mssqlClusterLivySubmitPath;
|
||||
this._user = sqlClusterConnection.user;
|
||||
this._passWord = sqlClusterConnection.password;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _jobName: string,
|
||||
private readonly _sparkFile: string,
|
||||
private readonly _mainClass: string,
|
||||
private readonly _arguments: string,
|
||||
private readonly _jarFileList: string,
|
||||
private readonly _pyFileList: string,
|
||||
private readonly _otherFileList: string,
|
||||
private _host?: string,
|
||||
private _port?: string,
|
||||
private _livyPath?: string,
|
||||
private _user?: string,
|
||||
private _passWord?: string) {
|
||||
}
|
||||
|
||||
public get jobName(): string { return this._jobName; }
|
||||
public get sparkFile(): string { return this._sparkFile; }
|
||||
public get mainClass(): string { return this._mainClass; }
|
||||
public get jobArguments(): string { return this._arguments; }
|
||||
public get jarFileList(): string { return this._jarFileList; }
|
||||
public get otherFileList(): string { return this._otherFileList; }
|
||||
public get pyFileList(): string { return this._pyFileList; }
|
||||
public get host(): string { return this._host; }
|
||||
public get port(): string { return this._port; }
|
||||
public get livyPath(): string { return this._livyPath; }
|
||||
public get user(): string { return this._user; }
|
||||
public get password(): string { return this._passWord; }
|
||||
}
|
||||
|
||||
export enum SparkFileSource {
|
||||
HDFS = <any>'HDFS',
|
||||
Local = <any>'Local'
|
||||
}
|
||||
|
||||
export class LivyLogResponse {
|
||||
constructor(public log: string, public appId: string) { }
|
||||
}
|
||||
45
extensions/mssql/src/sparkFeature/historyTask.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as vscode from 'vscode';
|
||||
import { AppContext } from '../appContext';
|
||||
import { getErrorMessage } from '../utils';
|
||||
import * as SqlClusterLookUp from '../sqlClusterLookUp';
|
||||
|
||||
export class OpenSparkYarnHistoryTask {
|
||||
constructor(private appContext: AppContext) {
|
||||
}
|
||||
|
||||
async execute(sqlConnProfile: sqlops.IConnectionProfile, isSpark: boolean): Promise<void> {
|
||||
try {
|
||||
let sqlClusterConnection = SqlClusterLookUp.findSqlClusterConnection(sqlConnProfile, this.appContext);
|
||||
if (!sqlClusterConnection)
|
||||
{
|
||||
let name = isSpark? 'Spark' : 'Yarn';
|
||||
this.appContext.apiWrapper.showErrorMessage(`Please connect to the Spark cluster before View ${name} History.`);
|
||||
return;
|
||||
}
|
||||
if (isSpark) {
|
||||
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(this.generateSparkHistoryUrl(sqlClusterConnection.host, sqlClusterConnection.port)));
|
||||
}
|
||||
else {
|
||||
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(this.generateYarnHistoryUrl(sqlClusterConnection.host, sqlClusterConnection.port)));
|
||||
}
|
||||
} catch (error) {
|
||||
this.appContext.apiWrapper.showErrorMessage(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
private generateSparkHistoryUrl(host: string, port: string): string {
|
||||
return `https://${host}:${port}/gateway/default/sparkhistory/`;
|
||||
}
|
||||
|
||||
private generateYarnHistoryUrl(host: string, port: string): string {
|
||||
return `https://${host}:${port}/gateway/default/yarn/cluster/apps`;
|
||||
}
|
||||
}
|
||||
220
extensions/mssql/src/sparkFeature/sparkUtils.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as childProcess from 'child_process';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as path from 'path';
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as vscode from 'vscode';
|
||||
import * as which from 'which';
|
||||
import * as Constants from '../constants';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export function getDropdownValue(dropdownValue: string | sqlops.CategoryValue): string {
|
||||
if (typeof(dropdownValue) === 'string') {
|
||||
return <string>dropdownValue;
|
||||
} else {
|
||||
return dropdownValue ? (<sqlops.CategoryValue>dropdownValue).name : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getServerAddressFromName(connection: sqlops.ConnectionInfo | string): string {
|
||||
// Strip TDS port number from the server URI
|
||||
if ((<sqlops.ConnectionInfo>connection).options && (<sqlops.ConnectionInfo>connection).options['host']) {
|
||||
return (<sqlops.ConnectionInfo>connection).options['host'].split(',')[0].split(':')[0];
|
||||
} else if ((<sqlops.ConnectionInfo>connection).options && (<sqlops.ConnectionInfo>connection).options['server']) {
|
||||
return (<sqlops.ConnectionInfo>connection).options['server'].split(',')[0].split(':')[0];
|
||||
} else {
|
||||
return (<string>connection).split(',')[0].split(':')[0];
|
||||
}
|
||||
}
|
||||
|
||||
export function getKnoxUrl(host: string, port: string): string {
|
||||
return `https://${host}:${port}/gateway`;
|
||||
}
|
||||
|
||||
export function getLivyUrl(serverName: string, port: string): string {
|
||||
return this.getKnoxUrl(serverName, port) + '/default/livy/v1/';
|
||||
}
|
||||
|
||||
export function getTemplatePath(extensionPath: string, templateName: string): string {
|
||||
return path.join(extensionPath, 'resources', templateName);
|
||||
}
|
||||
export function shellWhichResolving(cmd: string): Promise<string> {
|
||||
return new Promise<string>(resolve => {
|
||||
which(cmd, (err, foundPath) => {
|
||||
if (err) {
|
||||
resolve(undefined);
|
||||
} else {
|
||||
// NOTE: Using realpath b/c some system installs are symlinked from */bin
|
||||
resolve(fs.realpathSync(foundPath));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function mkDir(dirPath: string, outputChannel?: vscode.OutputChannel): Promise<void> {
|
||||
if (!await fs.exists(dirPath)) {
|
||||
if (outputChannel) {
|
||||
outputChannel.appendLine(localize('mkdirOutputMsg', '... Creating {0}', dirPath));
|
||||
}
|
||||
await fs.ensureDir(dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: Error | string): string {
|
||||
return (error instanceof Error) ? error.message : error;
|
||||
}
|
||||
|
||||
// COMMAND EXECUTION HELPERS ///////////////////////////////////////////////
|
||||
export function executeBufferedCommand(cmd: string, options: childProcess.ExecOptions, outputChannel?: vscode.OutputChannel): Thenable<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (outputChannel) {
|
||||
outputChannel.appendLine(` > ${cmd}`);
|
||||
}
|
||||
|
||||
let child = childProcess.exec(cmd, options, (err, stdout) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
|
||||
// Add listeners to print stdout and stderr if an output channel was provided
|
||||
if (outputChannel) {
|
||||
child.stdout.on('data', data => { outputDataChunk(data, outputChannel, ' stdout: '); });
|
||||
child.stderr.on('data', data => { outputDataChunk(data, outputChannel, ' stderr: '); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function executeExitCodeCommand(cmd: string, outputChannel?: vscode.OutputChannel): Thenable<number> {
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
if (outputChannel) {
|
||||
outputChannel.appendLine(` > ${cmd}`);
|
||||
}
|
||||
|
||||
let child = childProcess.spawn(cmd, [], { shell: true, detached: false });
|
||||
|
||||
// Add listeners for the process to exit
|
||||
child.on('error', reject);
|
||||
child.on('exit', (code: number) => { resolve(code); });
|
||||
|
||||
// Add listeners to print stdout and stderr if an output channel was provided
|
||||
if (outputChannel) {
|
||||
child.stdout.on('data', data => { outputDataChunk(data, outputChannel, ' stdout: '); });
|
||||
child.stderr.on('data', data => { outputDataChunk(data, outputChannel, ' stderr: '); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function executeStreamedCommand(cmd: string, outputChannel?: vscode.OutputChannel): Thenable<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// Start the command
|
||||
if (outputChannel) {
|
||||
outputChannel.appendLine(` > ${cmd}`);
|
||||
}
|
||||
let child = childProcess.spawn(cmd, [], { shell: true, detached: false });
|
||||
|
||||
// Add listeners to resolve/reject the promise on exit
|
||||
child.on('error', reject);
|
||||
child.on('exit', (code: number) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(localize('executeCommandProcessExited', 'Process exited with code {0}', code));
|
||||
}
|
||||
});
|
||||
|
||||
// Add listeners to print stdout and stderr if an output channel was provided
|
||||
if (outputChannel) {
|
||||
child.stdout.on('data', data => { outputDataChunk(data, outputChannel, ' stdout: '); });
|
||||
child.stderr.on('data', data => { outputDataChunk(data, outputChannel, ' stderr: '); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function isObjectExplorerContext(object: any): object is sqlops.ObjectExplorerContext {
|
||||
return 'connectionProfile' in object && 'isConnectionNode' in object;
|
||||
}
|
||||
|
||||
export function getUserHome(): string {
|
||||
return process.env.HOME || process.env.USERPROFILE;
|
||||
}
|
||||
|
||||
export enum Platform {
|
||||
Mac,
|
||||
Linux,
|
||||
Windows,
|
||||
Others
|
||||
}
|
||||
|
||||
export function getOSPlatform(): Platform {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return Platform.Windows;
|
||||
case 'darwin':
|
||||
return Platform.Mac;
|
||||
case 'linux':
|
||||
return Platform.Linux;
|
||||
default:
|
||||
return Platform.Others;
|
||||
}
|
||||
}
|
||||
|
||||
export function getOSPlatformId(): string {
|
||||
var platformId = undefined;
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
platformId = 'win-x64';
|
||||
break;
|
||||
case 'darwin':
|
||||
platformId = 'osx';
|
||||
break;
|
||||
default:
|
||||
platformId = 'linux-x64';
|
||||
break;
|
||||
}
|
||||
return platformId;
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////////
|
||||
function outputDataChunk(data: string | Buffer, outputChannel: vscode.OutputChannel, header: string): void {
|
||||
data.toString().split(/\r?\n/)
|
||||
.forEach(line => {
|
||||
outputChannel.appendLine(header + line);
|
||||
});
|
||||
}
|
||||
|
||||
export function clone<T>(obj: T): T {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
if (obj instanceof RegExp) {
|
||||
// See https://github.com/Microsoft/TypeScript/issues/10990
|
||||
return obj as any;
|
||||
}
|
||||
const result = (Array.isArray(obj)) ? <any>[] : <any>{};
|
||||
Object.keys(obj).forEach(key => {
|
||||
if (obj[key] && typeof obj[key] === 'object') {
|
||||
result[key] = clone(obj[key]);
|
||||
} else {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function isValidNumber(maybeNumber: any) {
|
||||
return maybeNumber !== undefined
|
||||
&& maybeNumber !== null
|
||||
&& maybeNumber !== ''
|
||||
&& !isNaN(Number(maybeNumber.toString()));
|
||||
}
|
||||
146
extensions/mssql/src/sqlClusterLookUp.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as constants from './constants';
|
||||
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
|
||||
import { AppContext } from './appContext';
|
||||
import { SqlClusterConnection } from './objectExplorerNodeProvider/connection';
|
||||
import { ICommandObjectExplorerContext } from './objectExplorerNodeProvider/command';
|
||||
import { MssqlObjectExplorerNodeProvider } from './objectExplorerNodeProvider/objectExplorerNodeProvider';
|
||||
|
||||
|
||||
export function findSqlClusterConnection(
|
||||
obj: ICommandObjectExplorerContext | sqlops.IConnectionProfile,
|
||||
appContext: AppContext) : SqlClusterConnection {
|
||||
|
||||
if (!obj || !appContext) { return undefined; }
|
||||
|
||||
let sqlConnProfile: sqlops.IConnectionProfile;
|
||||
if ('type' in obj && obj.type === constants.ObjectExplorerService
|
||||
&& 'explorerContext' in obj && obj.explorerContext && obj.explorerContext.connectionProfile) {
|
||||
sqlConnProfile = obj.explorerContext.connectionProfile;
|
||||
} else if ('options' in obj) {
|
||||
sqlConnProfile = obj;
|
||||
}
|
||||
|
||||
let sqlClusterConnection: SqlClusterConnection = undefined;
|
||||
if (sqlConnProfile) {
|
||||
sqlClusterConnection = findSqlClusterConnectionBySqlConnProfile(sqlConnProfile, appContext);
|
||||
}
|
||||
return sqlClusterConnection;
|
||||
}
|
||||
|
||||
function findSqlClusterConnectionBySqlConnProfile(sqlConnProfile: sqlops.IConnectionProfile, appContext: AppContext): SqlClusterConnection {
|
||||
if (!sqlConnProfile || !appContext) { return undefined; }
|
||||
|
||||
let sqlOeNodeProvider = appContext.getService<MssqlObjectExplorerNodeProvider>(constants.ObjectExplorerService);
|
||||
if (!sqlOeNodeProvider) { return undefined; }
|
||||
|
||||
let sqlClusterSession = sqlOeNodeProvider.findSqlClusterSessionBySqlConnProfile(sqlConnProfile);
|
||||
if (!sqlClusterSession) { return undefined; }
|
||||
|
||||
return sqlClusterSession.sqlClusterConnection;
|
||||
}
|
||||
|
||||
export async function getSqlClusterConnection(
|
||||
obj: sqlops.IConnectionProfile | sqlops.connection.Connection | ICommandObjectExplorerContext): Promise<ConnectionParam> {
|
||||
|
||||
if (!obj) { return undefined; }
|
||||
|
||||
let sqlClusterConnInfo: ConnectionParam = undefined;
|
||||
if ('providerName' in obj) {
|
||||
if (obj.providerName === constants.mssqlClusterProviderName) {
|
||||
sqlClusterConnInfo = 'id' in obj ? connProfileToConnectionParam(obj) : connToConnectionParam(obj);
|
||||
} else {
|
||||
sqlClusterConnInfo = await createSqlClusterConnInfo(obj);
|
||||
}
|
||||
} else {
|
||||
sqlClusterConnInfo = await createSqlClusterConnInfo(obj.explorerContext.connectionProfile);
|
||||
}
|
||||
|
||||
return sqlClusterConnInfo;
|
||||
}
|
||||
|
||||
async function createSqlClusterConnInfo(sqlConnInfo: sqlops.IConnectionProfile | sqlops.connection.Connection): Promise<ConnectionParam> {
|
||||
if (!sqlConnInfo) { return undefined; }
|
||||
|
||||
let connectionId: string = 'id' in sqlConnInfo ? sqlConnInfo.id : sqlConnInfo.connectionId;
|
||||
if (!connectionId) { return undefined; }
|
||||
|
||||
let serverInfo = await sqlops.connection.getServerInfo(connectionId);
|
||||
if (!serverInfo || !serverInfo.options) { return undefined; }
|
||||
|
||||
let endpoints: IEndpoint[] = serverInfo.options[constants.clusterEndpointsProperty];
|
||||
if (!endpoints || endpoints.length === 0) { return undefined; }
|
||||
|
||||
let index = endpoints.findIndex(ep => ep.serviceName === constants.hadoopKnoxEndpointName);
|
||||
if (index < 0) { return undefined; }
|
||||
|
||||
let credentials = await sqlops.connection.getCredentials(connectionId);
|
||||
if (!credentials) { return undefined; }
|
||||
|
||||
let clusterConnInfo = <ConnectionParam>{
|
||||
providerName: constants.mssqlClusterProviderName,
|
||||
connectionId: UUID.generateUuid(),
|
||||
options: {}
|
||||
};
|
||||
|
||||
clusterConnInfo.options[constants.hostPropName] = endpoints[index].ipAddress;
|
||||
clusterConnInfo.options[constants.knoxPortPropName] = endpoints[index].port;
|
||||
clusterConnInfo.options[constants.userPropName] = 'root'; //should be the same user as sql master
|
||||
clusterConnInfo.options[constants.passwordPropName] = credentials.password;
|
||||
clusterConnInfo = connToConnectionParam(clusterConnInfo);
|
||||
|
||||
return clusterConnInfo;
|
||||
}
|
||||
|
||||
function connProfileToConnectionParam(connectionProfile: sqlops.IConnectionProfile): ConnectionParam {
|
||||
let result = Object.assign(connectionProfile, { connectionId: connectionProfile.id });
|
||||
return <ConnectionParam>result;
|
||||
}
|
||||
|
||||
function connToConnectionParam(connection: sqlops.connection.Connection): ConnectionParam {
|
||||
let connectionId = connection.connectionId;
|
||||
let options = connection.options;
|
||||
let result = Object.assign(connection,
|
||||
{
|
||||
serverName: `${options[constants.hostPropName]},${options[constants.knoxPortPropName]}`,
|
||||
userName: options[constants.userPropName],
|
||||
password: options[constants.passwordPropName],
|
||||
id: connectionId,
|
||||
}
|
||||
);
|
||||
return <ConnectionParam>result;
|
||||
}
|
||||
|
||||
interface IEndpoint {
|
||||
serviceName: string;
|
||||
ipAddress: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
class ConnectionParam implements sqlops.connection.Connection, sqlops.IConnectionProfile, sqlops.ConnectionInfo
|
||||
{
|
||||
public connectionName: string;
|
||||
public serverName: string;
|
||||
public databaseName: string;
|
||||
public userName: string;
|
||||
public password: string;
|
||||
public authenticationType: string;
|
||||
public savePassword: boolean;
|
||||
public groupFullName: string;
|
||||
public groupId: string;
|
||||
public saveProfile: boolean;
|
||||
public id: string;
|
||||
public azureTenantId?: string;
|
||||
|
||||
public providerName: string;
|
||||
public connectionId: string;
|
||||
|
||||
public options: { [name: string]: any; };
|
||||
}
|
||||
3
extensions/mssql/src/typings/refs.d.ts
vendored
@@ -4,4 +4,5 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/sql/sqlops.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/sql/sqlops.proposed.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
|
||||
@@ -4,16 +4,19 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import * as os from 'os';
|
||||
import {workspace, WorkspaceConfiguration} from 'vscode';
|
||||
import * as findRemoveSync from 'find-remove';
|
||||
import * as constants from './constants';
|
||||
|
||||
const configTracingLevel = 'tracingLevel';
|
||||
const configLogRetentionMinutes = 'logRetentionMinutes';
|
||||
const configLogFilesRemovalLimit = 'logFilesRemovalLimit';
|
||||
const extensionConfigSectionName = 'mssql';
|
||||
const configLogDebugInfo = 'logDebugInfo';
|
||||
|
||||
// The function is a duplicate of \src\paths.js. IT would be better to import path.js but it doesn't
|
||||
// work for now because the extension is running in different process.
|
||||
@@ -27,56 +30,53 @@ export function getAppDataPath() {
|
||||
}
|
||||
}
|
||||
|
||||
export function removeOldLogFiles(prefix: string) : JSON {
|
||||
return findRemoveSync(getDefaultLogDir(), {prefix: `${prefix}_`, age: {seconds: getConfigLogRetentionSeconds()}, limit: getConfigLogFilesRemovalLimit()});
|
||||
export function removeOldLogFiles(prefix: string): JSON {
|
||||
return findRemoveSync(getDefaultLogDir(), { prefix: `${prefix}_`, age: { seconds: getConfigLogRetentionSeconds() }, limit: getConfigLogFilesRemovalLimit() });
|
||||
}
|
||||
|
||||
export function getConfiguration(config: string = extensionConfigSectionName) : WorkspaceConfiguration {
|
||||
return workspace.getConfiguration(extensionConfigSectionName);
|
||||
export function getConfiguration(config: string = extensionConfigSectionName): vscode.WorkspaceConfiguration {
|
||||
return vscode.workspace.getConfiguration(extensionConfigSectionName);
|
||||
}
|
||||
|
||||
export function getConfigLogFilesRemovalLimit() : number {
|
||||
export function getConfigLogFilesRemovalLimit(): number {
|
||||
let config = getConfiguration();
|
||||
if (config) {
|
||||
return Number((config[configLogFilesRemovalLimit]).toFixed(0));
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getConfigLogRetentionSeconds() : number {
|
||||
export function getConfigLogRetentionSeconds(): number {
|
||||
let config = getConfiguration();
|
||||
if (config) {
|
||||
return Number((config[configLogRetentionMinutes] * 60).toFixed(0));
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getConfigTracingLevel() : string {
|
||||
export function getConfigTracingLevel(): string {
|
||||
let config = getConfiguration();
|
||||
if (config) {
|
||||
return config[configTracingLevel];
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultLogDir() : string {
|
||||
return path.join(process.env['VSCODE_LOGS'], '..', '..','mssql');
|
||||
export function getDefaultLogDir(): string {
|
||||
return path.join(process.env['VSCODE_LOGS'], '..', '..', 'mssql');
|
||||
}
|
||||
|
||||
export function getDefaultLogFile(prefix: string, pid: number) : string {
|
||||
export function getDefaultLogFile(prefix: string, pid: number): string {
|
||||
return path.join(getDefaultLogDir(), `${prefix}_${pid}.log`);
|
||||
}
|
||||
|
||||
export function getCommonLaunchArgsAndCleanupOldLogFiles(prefix: string, executablePath: string) : string [] {
|
||||
export function getCommonLaunchArgsAndCleanupOldLogFiles(prefix: string, executablePath: string): string[] {
|
||||
let launchArgs = [];
|
||||
launchArgs.push('--log-file');
|
||||
let logFile = getDefaultLogFile(prefix, process.pid);
|
||||
@@ -169,3 +169,64 @@ export function verifyPlatform(): Thenable<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: Error | string): string {
|
||||
return (error instanceof Error) ? error.message : error;
|
||||
}
|
||||
|
||||
export function isObjectExplorerContext(object: any): object is sqlops.ObjectExplorerContext {
|
||||
return 'connectionProfile' in object && 'isConnectionNode' in object;
|
||||
}
|
||||
|
||||
export function getUserHome(): string {
|
||||
return process.env.HOME || process.env.USERPROFILE;
|
||||
}
|
||||
|
||||
export async function getClusterEndpoint(profileId: string, serviceName: string): Promise<IEndpoint> {
|
||||
|
||||
let serverInfo: sqlops.ServerInfo = await sqlops.connection.getServerInfo(profileId);
|
||||
if (!serverInfo || !serverInfo.options) {
|
||||
return undefined;
|
||||
}
|
||||
let endpoints: IEndpoint[] = serverInfo.options[constants.clusterEndpointsProperty];
|
||||
if (!endpoints || endpoints.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
let index = endpoints.findIndex(ep => ep.serviceName === serviceName);
|
||||
if (index === -1) {
|
||||
return undefined;
|
||||
}
|
||||
let clusterEndpoint: IEndpoint = {
|
||||
serviceName: endpoints[index].serviceName,
|
||||
ipAddress: endpoints[index].ipAddress,
|
||||
port: endpoints[index].port
|
||||
};
|
||||
return clusterEndpoint;
|
||||
}
|
||||
|
||||
interface IEndpoint {
|
||||
serviceName: string;
|
||||
ipAddress: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export function isValidNumber(maybeNumber: any) {
|
||||
return maybeNumber !== undefined
|
||||
&& maybeNumber !== null
|
||||
&& maybeNumber !== ''
|
||||
&& !isNaN(Number(maybeNumber.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to log messages to the developer console if enabled
|
||||
* @param msg Message to log to the console
|
||||
*/
|
||||
export function logDebug(msg: any): void {
|
||||
let config = vscode.workspace.getConfiguration(extensionConfigSectionName);
|
||||
let logDebugInfo = config[configLogDebugInfo];
|
||||
if (logDebugInfo === true) {
|
||||
let currentTime = new Date().toLocaleTimeString();
|
||||
let outputMsg = '[' + currentTime + ']: ' + msg ? msg.toString() : '';
|
||||
console.log(outputMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,19 +17,28 @@
|
||||
"type": "object",
|
||||
"title": "%notebook.configuration.title%",
|
||||
"properties": {
|
||||
"notebook.enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "%notebook.enabled.description%"
|
||||
},
|
||||
"notebook.pythonPath": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "%notebook.pythonPath.description%"
|
||||
},
|
||||
"notebook.sqlKernelEnabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "%notebook.sqlKernelEnabled.description%"
|
||||
},
|
||||
"notebook.overrideEditorTheming": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "%notebook.overrideEditorTheming.description%"
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "notebook.command.analyzeNotebook",
|
||||
"title": "%notebook.analyzeJupyterNotebook%"
|
||||
},
|
||||
{
|
||||
"command": "notebook.command.new",
|
||||
"title": "%notebook.command.new%",
|
||||
@@ -45,25 +54,76 @@
|
||||
"dark": "resources/dark/open_notebook_inverse.svg",
|
||||
"light": "resources/light/open_notebook.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "notebook.command.runactivecell",
|
||||
"title": "%notebook.command.runactivecell%"
|
||||
},
|
||||
{
|
||||
"command": "notebook.command.addcode",
|
||||
"title": "%notebook.command.addcode%"
|
||||
},
|
||||
{
|
||||
"command": "notebook.command.addtext",
|
||||
"title": "%notebook.command.addtext%"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "notebook.command.new",
|
||||
"when": "config.notebook.enabled"
|
||||
"command": "notebook.command.analyzeNotebook"
|
||||
},
|
||||
{
|
||||
"command": "notebook.command.open",
|
||||
"when": "config.notebook.enabled"
|
||||
"command": "notebook.command.new"
|
||||
},
|
||||
{
|
||||
"command": "notebook.command.open"
|
||||
},
|
||||
{
|
||||
"command": "notebook.command.runactivecell",
|
||||
"when": "notebookEditorVisible"
|
||||
},
|
||||
{
|
||||
"command": "notebook.command.addcode",
|
||||
"when": "notebookEditorVisible"
|
||||
},
|
||||
{
|
||||
"command": "notebook.command.addtext",
|
||||
"when": "notebookEditorVisible"
|
||||
}
|
||||
],
|
||||
"objectExplorer/item/context": [
|
||||
{
|
||||
"command": "notebook.command.new",
|
||||
"when": "connectionProvider == MSSQL && nodeType == dataservices",
|
||||
"group": "1root@1"
|
||||
},
|
||||
{
|
||||
"command": "notebook.command.analyzeNotebook",
|
||||
"when": "nodeType=~/^mssqlCluster/ && nodeLabel=~/[^\\s]+(\\.(csv|tsv|txt))$/ && nodeType == mssqlCluster:file",
|
||||
"group": "1notebook@1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"keybindings": [
|
||||
{
|
||||
"command": "notebook.command.new",
|
||||
"key": "Ctrl+Shift+N",
|
||||
"when": "config.notebook.enabled"
|
||||
"key": "Ctrl+Shift+N"
|
||||
},
|
||||
{
|
||||
"command": "notebook.command.runactivecell",
|
||||
"key": "F5",
|
||||
"when": "notebookEditorVisible"
|
||||
},
|
||||
{
|
||||
"command": "notebook.command.addcode",
|
||||
"key": "Ctrl+Shift+C",
|
||||
"when": "notebookEditorVisible"
|
||||
},
|
||||
{
|
||||
"command": "notebook.command.addtext",
|
||||
"key": "Ctrl+Shift+T",
|
||||
"when": "notebookEditorVisible"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -73,4 +133,4 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "8.0.33"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,13 @@
|
||||
"displayName": "Notebook Core Extensions",
|
||||
"description": "Defines the Data-procotol based Notebook contribution and many Notebook commands and contributions.",
|
||||
"notebook.configuration.title": "Notebook configuration",
|
||||
"notebook.enabled.description": "Enable viewing notebook files using built-in notebook editor.",
|
||||
"notebook.pythonPath.description": "Local path to python installation used by Notebooks.",
|
||||
"notebook.sqlKernelEnabled.description": "Enable SQL kernel in notebook editor (Preview). Requires reloading this window to take effect",
|
||||
"notebook.overrideEditorTheming.description": "Override editor default settings in the Notebook editor. Settings include background color, current line color and border",
|
||||
"notebook.command.new": "New Notebook",
|
||||
"notebook.command.open": "Open Notebook"
|
||||
"notebook.command.open": "Open Notebook",
|
||||
"notebook.analyzeJupyterNotebook": "Analyze in Notebook",
|
||||
"notebook.command.runactivecell": "Run Cell",
|
||||
"notebook.command.addcode": "Add Code Cell",
|
||||
"notebook.command.addtext": "Add Text Cell"
|
||||
}
|
||||
@@ -7,27 +7,56 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as sqlops from 'sqlops';
|
||||
import * as os from 'os';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
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 noNotebookVisible = localize('noNotebookVisible', 'No notebook editor is active');
|
||||
|
||||
let counter = 0;
|
||||
|
||||
export function activate(extensionContext: vscode.ExtensionContext) {
|
||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.new', () => {
|
||||
let title = `Untitled-${counter++}`;
|
||||
let untitledUri = vscode.Uri.parse(`untitled:${title}`);
|
||||
sqlops.nb.showNotebookDocument(untitledUri).then(success => {
|
||||
|
||||
}, (err: Error) => {
|
||||
vscode.window.showErrorMessage(err.message);
|
||||
});
|
||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.new', (connectionId?: string) => {
|
||||
newNotebook(connectionId);
|
||||
}));
|
||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.open', () => {
|
||||
openNotebook();
|
||||
}));
|
||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.runactivecell', () => {
|
||||
runActiveCell();
|
||||
}));
|
||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.addcode', () => {
|
||||
addCell('code');
|
||||
}));
|
||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.addtext', () => {
|
||||
addCell('markdown');
|
||||
}));
|
||||
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.analyzeNotebook', (explorerContext: sqlops.ObjectExplorerContext) => {
|
||||
analyzeNotebook(explorerContext);
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
function newNotebook(connectionId: string) {
|
||||
let title = `Untitled-${counter++}`;
|
||||
let untitledUri = vscode.Uri.parse(`untitled:${title}`);
|
||||
let options: sqlops.nb.NotebookShowOptions = connectionId ? {
|
||||
viewColumn: null,
|
||||
preserveFocus: true,
|
||||
preview: null,
|
||||
providerId: null,
|
||||
connectionId: connectionId,
|
||||
defaultKernel: null
|
||||
} : null;
|
||||
sqlops.nb.showNotebookDocument(untitledUri, options).then(success => {
|
||||
|
||||
}, (err: Error) => {
|
||||
vscode.window.showErrorMessage(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
async function openNotebook(): Promise<void> {
|
||||
try {
|
||||
let filter = {};
|
||||
@@ -45,6 +74,71 @@ async function openNotebook(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function runActiveCell(): Promise<void> {
|
||||
try {
|
||||
let notebook = sqlops.nb.activeNotebookEditor;
|
||||
if (notebook) {
|
||||
await notebook.runCell();
|
||||
} else {
|
||||
throw new Error(noNotebookVisible);
|
||||
}
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function addCell(cellType: sqlops.nb.CellType): Promise<void> {
|
||||
try {
|
||||
let notebook = sqlops.nb.activeNotebookEditor;
|
||||
if (notebook) {
|
||||
await notebook.edit((editBuilder: sqlops.nb.NotebookEditorEdit) => {
|
||||
// TODO should prompt and handle cell placement
|
||||
editBuilder.insertCell({
|
||||
cell_type: cellType,
|
||||
source: ''
|
||||
});
|
||||
});
|
||||
} else {
|
||||
throw new Error(noNotebookVisible);
|
||||
}
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function analyzeNotebook(oeContext?: sqlops.ObjectExplorerContext): Promise<void> {
|
||||
// Ensure we get a unique ID for the notebook. For now we're using a different prefix to the built-in untitled files
|
||||
// to handle this. We should look into improving this in the future
|
||||
let untitledUri = vscode.Uri.parse(`untitled:Notebook-${counter++}`);
|
||||
|
||||
let editor = await sqlops.nb.showNotebookDocument(untitledUri, {
|
||||
connectionId: oeContext ? oeContext.connectionProfile.id : '',
|
||||
providerId: JUPYTER_NOTEBOOK_PROVIDER,
|
||||
preview: false,
|
||||
defaultKernel: {
|
||||
name: 'pyspark3kernel',
|
||||
display_name: 'PySpark3',
|
||||
language: 'python'
|
||||
}
|
||||
});
|
||||
if (oeContext && oeContext.nodeInfo && oeContext.nodeInfo.nodePath) {
|
||||
// Get the file path after '/HDFS'
|
||||
let hdfsPath: string = oeContext.nodeInfo.nodePath.substring(oeContext.nodeInfo.nodePath.indexOf('/HDFS') + '/HDFS'.length);
|
||||
if (hdfsPath.length > 0) {
|
||||
let analyzeCommand = "#" + msgSampleCodeDataFrame + os.EOL + "df = (spark.read.option(\"inferSchema\", \"true\")"
|
||||
+ os.EOL + ".option(\"header\", \"true\")" + os.EOL + ".csv('{0}'))" + os.EOL + "df.show(10)";
|
||||
|
||||
editor.edit(editBuilder => {
|
||||
editBuilder.replace(0, {
|
||||
cell_type: 'code',
|
||||
source: analyzeCommand.replace('{0}', hdfsPath)
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
export function deactivate() {
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"Microsoft.mssql"
|
||||
],
|
||||
"contributes": {
|
||||
"commands": [{
|
||||
"commands": [
|
||||
{
|
||||
"command": "profiler.newProfiler",
|
||||
"title": "Launch Profiler",
|
||||
"category": "Profiler"
|
||||
@@ -48,23 +49,27 @@
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [{
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "profiler.start",
|
||||
"when": "False"
|
||||
},
|
||||
{
|
||||
"command": "profiler.stop",
|
||||
"when": "False"
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"command": "profiler.openCreateSessionDialog",
|
||||
"when": "False"
|
||||
}
|
||||
],
|
||||
"objectExplorer/item/context": [{
|
||||
"command": "profiler.newProfiler",
|
||||
"when": "connectionProvider == MSSQL && nodeType && nodeType == Server",
|
||||
"group": "profiler"
|
||||
}]
|
||||
"objectExplorer/item/context": [
|
||||
{
|
||||
"command": "profiler.newProfiler",
|
||||
"when": "connectionProvider == MSSQL && nodeType && nodeType == Server",
|
||||
"group": "profiler"
|
||||
}
|
||||
]
|
||||
},
|
||||
"outputChannels": [
|
||||
"sqlprofiler"
|
||||
@@ -76,4 +81,4 @@
|
||||
"devDependencies": {
|
||||
"vscode": "1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "azuredatastudio",
|
||||
"version": "1.4.3",
|
||||
"version": "1.4.5",
|
||||
"distro": "8c3e97e3425cc9814496472ab73e076de2ba99ee",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
@@ -50,7 +50,7 @@
|
||||
"gc-signals": "^0.0.1",
|
||||
"getmac": "1.4.1",
|
||||
"graceful-fs": "4.1.11",
|
||||
"html-query-plan": "git://github.com/anthonydresser/html-query-plan.git#2.4",
|
||||
"html-query-plan": "git://github.com/anthonydresser/html-query-plan.git#2.6",
|
||||
"http-proxy-agent": "^2.1.0",
|
||||
"https-proxy-agent": "^2.2.1",
|
||||
"iconv-lite": "0.4.23",
|
||||
|
||||
4003
samples/extensionSamples/package-lock.json
generated
5555
samples/serverReports/package-lock.json
generated
6226
samples/sp_whoIsActive/package-lock.json
generated
405
samples/sqlservices/package-lock.json
generated
@@ -57,30 +57,11 @@
|
||||
"json-schema-traverse": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"align-text": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
|
||||
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
|
||||
"requires": {
|
||||
"kind-of": "^3.0.2",
|
||||
"longest": "^1.0.1",
|
||||
"repeat-string": "^1.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"kind-of": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
|
||||
"requires": {
|
||||
"is-buffer": "^1.1.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"amdefine": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
|
||||
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU="
|
||||
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-colors": {
|
||||
"version": "1.1.0",
|
||||
@@ -311,12 +292,6 @@
|
||||
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=",
|
||||
"dev": true
|
||||
},
|
||||
"assert-plus": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
|
||||
"integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=",
|
||||
"dev": true
|
||||
},
|
||||
"assign-symbols": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
|
||||
@@ -324,9 +299,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"async": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
||||
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
|
||||
"integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.10"
|
||||
}
|
||||
},
|
||||
"async-done": {
|
||||
"version": "1.2.4",
|
||||
@@ -367,12 +345,6 @@
|
||||
"integrity": "sha512-SuiKH8vbsOyCALjA/+EINmt/Kdl+TQPrtFgW7XZZcwtryFu9e5kQoX3bjCW6mIvGH1fbeAZZuvwGR5IlBRznGw==",
|
||||
"dev": true
|
||||
},
|
||||
"aws-sign2": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
|
||||
"integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=",
|
||||
"dev": true
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz",
|
||||
@@ -465,15 +437,6 @@
|
||||
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
|
||||
"dev": true
|
||||
},
|
||||
"boom": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
|
||||
"integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"hoek": "2.x.x"
|
||||
}
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
@@ -577,22 +540,6 @@
|
||||
"integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=",
|
||||
"dev": true
|
||||
},
|
||||
"caseless": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz",
|
||||
"integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=",
|
||||
"dev": true
|
||||
},
|
||||
"center-align": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
|
||||
"integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"align-text": "^0.1.3",
|
||||
"lazy-cache": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||
@@ -901,15 +848,6 @@
|
||||
"which": "^1.2.9"
|
||||
}
|
||||
},
|
||||
"cryptiles": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
|
||||
"integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"boom": "2.x.x"
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css/-/css-2.2.1.tgz",
|
||||
@@ -1041,7 +979,8 @@
|
||||
"decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
|
||||
"dev": true
|
||||
},
|
||||
"decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
@@ -1745,17 +1684,6 @@
|
||||
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
|
||||
"dev": true
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
|
||||
"integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.5",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"fragment-cache": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
|
||||
@@ -2719,21 +2647,6 @@
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
},
|
||||
"generate-function": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz",
|
||||
"integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=",
|
||||
"dev": true
|
||||
},
|
||||
"generate-object-property": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
|
||||
"integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-property": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
|
||||
@@ -3057,70 +2970,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"gulp-remote-src": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/gulp-remote-src/-/gulp-remote-src-0.4.3.tgz",
|
||||
"integrity": "sha1-VyjP1kNDPdSEXd7wlp8PlxoqtKE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"event-stream": "~3.3.4",
|
||||
"node.extend": "~1.1.2",
|
||||
"request": "~2.79.0",
|
||||
"through2": "~2.0.3",
|
||||
"vinyl": "~2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"clone": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
|
||||
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
|
||||
"dev": true
|
||||
},
|
||||
"request": {
|
||||
"version": "2.79.0",
|
||||
"resolved": "http://registry.npmjs.org/request/-/request-2.79.0.tgz",
|
||||
"integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"aws-sign2": "~0.6.0",
|
||||
"aws4": "^1.2.1",
|
||||
"caseless": "~0.11.0",
|
||||
"combined-stream": "~1.0.5",
|
||||
"extend": "~3.0.0",
|
||||
"forever-agent": "~0.6.1",
|
||||
"form-data": "~2.1.1",
|
||||
"har-validator": "~2.0.6",
|
||||
"hawk": "~3.1.3",
|
||||
"http-signature": "~1.1.0",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "~2.1.7",
|
||||
"oauth-sign": "~0.8.1",
|
||||
"qs": "~6.3.0",
|
||||
"stringstream": "~0.0.4",
|
||||
"tough-cookie": "~2.3.0",
|
||||
"tunnel-agent": "~0.4.1",
|
||||
"uuid": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"vinyl": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.0.2.tgz",
|
||||
"integrity": "sha1-CjcT2NTpIhxY8QyhbAEWyeJe2nw=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"clone": "^1.0.0",
|
||||
"clone-buffer": "^1.0.0",
|
||||
"clone-stats": "^1.0.0",
|
||||
"cloneable-readable": "^1.0.0",
|
||||
"is-stream": "^1.1.0",
|
||||
"remove-trailing-separator": "^1.0.1",
|
||||
"replace-ext": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gulp-remote-src-vscode": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/gulp-remote-src-vscode/-/gulp-remote-src-vscode-0.5.0.tgz",
|
||||
@@ -4092,23 +3941,20 @@
|
||||
}
|
||||
},
|
||||
"handlebars": {
|
||||
"version": "4.0.11",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz",
|
||||
"integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.0.tgz",
|
||||
"integrity": "sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w==",
|
||||
"requires": {
|
||||
"async": "^1.4.0",
|
||||
"async": "^2.5.0",
|
||||
"optimist": "^0.6.1",
|
||||
"source-map": "^0.4.4",
|
||||
"uglify-js": "^2.6"
|
||||
"source-map": "^0.6.1",
|
||||
"uglify-js": "^3.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
|
||||
"integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
|
||||
"requires": {
|
||||
"amdefine": ">=0.0.4"
|
||||
}
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4118,18 +3964,6 @@
|
||||
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
|
||||
"dev": true
|
||||
},
|
||||
"har-validator": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz",
|
||||
"integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^1.1.1",
|
||||
"commander": "^2.9.0",
|
||||
"is-my-json-valid": "^2.12.4",
|
||||
"pinkie-promise": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"has-ansi": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
|
||||
@@ -4192,30 +4026,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"hawk": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
|
||||
"integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"boom": "2.x.x",
|
||||
"cryptiles": "2.x.x",
|
||||
"hoek": "2.x.x",
|
||||
"sntp": "1.x.x"
|
||||
}
|
||||
},
|
||||
"he": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
|
||||
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
|
||||
"dev": true
|
||||
},
|
||||
"hoek": {
|
||||
"version": "2.16.3",
|
||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
|
||||
"integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
|
||||
"dev": true
|
||||
},
|
||||
"homedir-polyfill": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz",
|
||||
@@ -4245,17 +4061,6 @@
|
||||
"readable-stream": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
|
||||
"integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"assert-plus": "^0.2.0",
|
||||
"jsprim": "^1.2.2",
|
||||
"sshpk": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
@@ -4333,7 +4138,8 @@
|
||||
"is-buffer": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
|
||||
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
|
||||
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
|
||||
"dev": true
|
||||
},
|
||||
"is-builtin-module": {
|
||||
"version": "1.0.0",
|
||||
@@ -4409,25 +4215,6 @@
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"is-my-ip-valid": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz",
|
||||
"integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-my-json-valid": {
|
||||
"version": "2.17.2",
|
||||
"resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz",
|
||||
"integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"generate-function": "^2.0.0",
|
||||
"generate-object-property": "^1.1.0",
|
||||
"is-my-ip-valid": "^1.0.0",
|
||||
"jsonpointer": "^4.0.0",
|
||||
"xtend": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"is-negated-glob": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz",
|
||||
@@ -4528,12 +4315,6 @@
|
||||
"integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
|
||||
"dev": true
|
||||
},
|
||||
"is-property": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||
"integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=",
|
||||
"dev": true
|
||||
},
|
||||
"is-relative": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
|
||||
@@ -4654,12 +4435,6 @@
|
||||
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
|
||||
"dev": true
|
||||
},
|
||||
"jsonpointer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz",
|
||||
"integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=",
|
||||
"dev": true
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
@@ -4702,12 +4477,6 @@
|
||||
"es6-weak-map": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"lazy-cache": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
|
||||
"integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=",
|
||||
"optional": true
|
||||
},
|
||||
"lazystream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz",
|
||||
@@ -4776,8 +4545,7 @@
|
||||
"lodash": {
|
||||
"version": "4.17.10",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
|
||||
"integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg=="
|
||||
},
|
||||
"lodash._basecopy": {
|
||||
"version": "3.0.1",
|
||||
@@ -4904,11 +4672,6 @@
|
||||
"lodash.escape": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"longest": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
|
||||
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz",
|
||||
@@ -5831,12 +5594,6 @@
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
|
||||
"dev": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz",
|
||||
"integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=",
|
||||
"dev": true
|
||||
},
|
||||
"querystringify": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz",
|
||||
@@ -6002,7 +5759,8 @@
|
||||
"repeat-string": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
|
||||
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
|
||||
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
|
||||
"dev": true
|
||||
},
|
||||
"replace-ext": {
|
||||
"version": "1.0.0",
|
||||
@@ -6104,9 +5862,9 @@
|
||||
"integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "0.4.0",
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "1.0.6",
|
||||
"mime-types": "2.1.18"
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"har-validator": {
|
||||
@@ -6143,9 +5901,9 @@
|
||||
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0",
|
||||
"jsprim": "1.4.1",
|
||||
"sshpk": "1.14.1"
|
||||
"assert-plus": "^1.0.0",
|
||||
"jsprim": "^1.2.2",
|
||||
"sshpk": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
@@ -6169,7 +5927,7 @@
|
||||
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.1"
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6232,15 +5990,6 @@
|
||||
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
|
||||
"dev": true
|
||||
},
|
||||
"right-align": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
|
||||
"integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"align-text": "^0.1.1"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
|
||||
@@ -6450,19 +6199,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sntp": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
|
||||
"integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"hoek": "2.x.x"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
|
||||
"dev": true
|
||||
},
|
||||
"source-map-resolve": {
|
||||
"version": "0.5.1",
|
||||
@@ -6571,7 +6312,7 @@
|
||||
"gulp-chmod": "^2.0.0",
|
||||
"gulp-filter": "^5.0.1",
|
||||
"gulp-gunzip": "1.0.0",
|
||||
"gulp-remote-src": "^0.4.3",
|
||||
"gulp-remote-src-vscode": "^0.5.0",
|
||||
"gulp-symdest": "^1.1.0",
|
||||
"gulp-untar": "^0.0.6",
|
||||
"gulp-vinyl-zip": "^2.1.0",
|
||||
@@ -6994,12 +6735,6 @@
|
||||
"integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM=",
|
||||
"dev": true
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
|
||||
"integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=",
|
||||
"dev": true
|
||||
},
|
||||
"tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
@@ -7036,59 +6771,29 @@
|
||||
"dev": true
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "2.8.29",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
|
||||
"integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
|
||||
"version": "3.4.9",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
|
||||
"integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"source-map": "~0.5.1",
|
||||
"uglify-to-browserify": "~1.0.0",
|
||||
"yargs": "~3.10.0"
|
||||
"commander": "~2.17.1",
|
||||
"source-map": "~0.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"camelcase": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
|
||||
"integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
|
||||
"commander": {
|
||||
"version": "2.17.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
|
||||
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
|
||||
"optional": true
|
||||
},
|
||||
"cliui": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
|
||||
"integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"center-align": "^0.1.1",
|
||||
"right-align": "^0.1.1",
|
||||
"wordwrap": "0.0.2"
|
||||
}
|
||||
},
|
||||
"wordwrap": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
|
||||
"integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=",
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"optional": true
|
||||
},
|
||||
"yargs": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
|
||||
"integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"camelcase": "^1.0.2",
|
||||
"cliui": "^2.1.0",
|
||||
"decamelize": "^1.0.0",
|
||||
"window-size": "0.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"uglify-to-browserify": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz",
|
||||
"integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=",
|
||||
"optional": true
|
||||
},
|
||||
"unc-path-regex": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
|
||||
@@ -7180,9 +6885,9 @@
|
||||
}
|
||||
},
|
||||
"universalify": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
|
||||
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
|
||||
},
|
||||
"unset-value": {
|
||||
"version": "1.0.0",
|
||||
@@ -7519,9 +7224,9 @@
|
||||
}
|
||||
},
|
||||
"vscode-nls": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-3.2.2.tgz",
|
||||
"integrity": "sha512-/Ur1+tgazwd51+ncRyoy0UIu4dvMdVXS9XMUULQlZIBoNGEwOhwEx9x+hHWoUjldMrOQ32t2CGKo0u6D4R6/hg=="
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-3.2.5.tgz",
|
||||
"integrity": "sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw=="
|
||||
},
|
||||
"vso-node-api": {
|
||||
"version": "6.5.0",
|
||||
@@ -7549,12 +7254,6 @@
|
||||
"integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=",
|
||||
"dev": true
|
||||
},
|
||||
"window-size": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
|
||||
"integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=",
|
||||
"optional": true
|
||||
},
|
||||
"wordwrap": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
|
||||
|
||||
@@ -430,7 +430,6 @@ export abstract class Modal extends Disposable implements IThemable {
|
||||
* Hides the modal and removes key listeners
|
||||
*/
|
||||
protected hide() {
|
||||
this._footerButtons.forEach(button => button.applyStyles());
|
||||
this._modalShowingContext.get().pop();
|
||||
this._builder.offDOM();
|
||||
if (this._focusedElementBeforeOpen) {
|
||||
|
||||
@@ -221,6 +221,7 @@ export class ScrollableSplitView extends HeightMap implements IDisposable {
|
||||
|
||||
this.state = State.Busy;
|
||||
|
||||
let currentIndex = index;
|
||||
for (let i = 0; i < views.length; i++) {
|
||||
let size: number | Sizing;
|
||||
if (Array.isArray(sizes)) {
|
||||
@@ -267,9 +268,9 @@ export class ScrollableSplitView extends HeightMap implements IDisposable {
|
||||
}
|
||||
|
||||
const item: IViewItem = { onAdd, onRemove, view, container, size: viewSize, layout, disposable, height: viewSize, top: 0, width: 0 };
|
||||
this.viewItems.splice(index, 0, item);
|
||||
this.viewItems.splice(currentIndex, 0, item);
|
||||
|
||||
this.onInsertItems(new ArrayIterator([item]), index > 0 ? this.viewItems[index - 1].view.id : undefined);
|
||||
this.onInsertItems(new ArrayIterator([item]), currentIndex > 0 ? this.viewItems[currentIndex - 1].view.id : undefined);
|
||||
|
||||
// Add sash
|
||||
if (this.options.enableResizing && this.viewItems.length > 1) {
|
||||
@@ -296,10 +297,11 @@ export class ScrollableSplitView extends HeightMap implements IDisposable {
|
||||
const disposable = combinedDisposable([onStartDisposable, onChangeDisposable, onEndDisposable, onDidResetDisposable, sash]);
|
||||
const sashItem: ISashItem = { sash, disposable };
|
||||
|
||||
this.sashItems.splice(index - 1, 0, sashItem);
|
||||
this.sashItems.splice(currentIndex - 1, 0, sashItem);
|
||||
}
|
||||
|
||||
container.appendChild(view.element);
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
let highPriorityIndex: number | undefined;
|
||||
@@ -314,6 +316,14 @@ export class ScrollableSplitView extends HeightMap implements IDisposable {
|
||||
if (!types.isArray(sizes) && sizes.type === 'distribute') {
|
||||
this.distributeViewSizes();
|
||||
}
|
||||
|
||||
// Re-render the views. Set lastRenderTop and lastRenderHeight to undefined since
|
||||
// this isn't actually scrolling up or down
|
||||
let scrollTop = this.lastRenderTop;
|
||||
let viewHeight = this.lastRenderHeight;
|
||||
this.lastRenderTop = undefined;
|
||||
this.lastRenderHeight = undefined;
|
||||
this.render(scrollTop, viewHeight);
|
||||
}
|
||||
|
||||
addView(view: IView, size: number | Sizing, index = this.viewItems.length): void {
|
||||
|
||||
@@ -32,3 +32,9 @@
|
||||
-webkit-mask: url('icons/search_inverse.svg') no-repeat 50% 50%;
|
||||
-webkit-mask-size: 25px 25px;
|
||||
}
|
||||
|
||||
/* Activity Bar - Data Explorer */
|
||||
.monaco-workbench > .activitybar .monaco-action-bar .action-label.dataExplorer {
|
||||
-webkit-mask: url('icons/server_page_inverse.svg') no-repeat 50% 50%;
|
||||
-webkit-mask-size: 25px 25px;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@
|
||||
background: url("database_inverse.svg") center center no-repeat;
|
||||
}
|
||||
|
||||
.vs .icon.error,
|
||||
.vs-dark .icon.error,
|
||||
.hc-black .icon.error {
|
||||
background: url("globalerror_red.svg") center center no-repeat;
|
||||
}
|
||||
|
||||
.vs .icon.file {
|
||||
background: url("file.svg") center center no-repeat;
|
||||
}
|
||||
|
||||
1
src/sql/media/icons/globalerror_red.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#d02e00;}</style></defs><title>globalerror_red</title><path class="cls-1" d="M8,0a7.92,7.92,0,0,1,4,1.09A8.15,8.15,0,0,1,14.91,4a8,8,0,0,1,.81,1.91,8,8,0,0,1-.81,6.16A8.15,8.15,0,0,1,12,14.92a8,8,0,0,1-8.07,0,8.15,8.15,0,0,1-2.87-2.87A8,8,0,0,1,1.09,4,8.15,8.15,0,0,1,4,1.11,7.92,7.92,0,0,1,8,0ZM8,15a6.88,6.88,0,0,0,1.86-.25,7,7,0,0,0,4.89-4.89,7.07,7.07,0,0,0,0-3.73A7,7,0,0,0,9.86,1.27a7.07,7.07,0,0,0-3.73,0A7,7,0,0,0,1.25,6.15a7.07,7.07,0,0,0,0,3.73,7,7,0,0,0,4.89,4.89A6.88,6.88,0,0,0,8,15Zm3.46-9.76L8.71,8l2.75,2.76-.7.7L8,8.73,5.24,11.48l-.7-.7L7.29,8,4.54,5.26l.7-.7L8,7.31l2.76-2.75Z"/></svg>
|
||||
|
After Width: | Height: | Size: 721 B |
@@ -13,7 +13,7 @@ import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput';
|
||||
import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
import { IQueryEditorOptions } from 'sql/parts/query/common/queryEditorService';
|
||||
import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput';
|
||||
import { NotebookInput, NotebookInputModel, NotebookInputValidator } from 'sql/parts/notebook/notebookInput';
|
||||
import { NotebookInput, NotebookInputModel } from 'sql/parts/notebook/notebookInput';
|
||||
import { DEFAULT_NOTEBOOK_PROVIDER, INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { getProvidersForFileName, getStandardKernelsForProvider } from 'sql/parts/notebook/notebookUtils';
|
||||
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
|
||||
@@ -56,9 +56,8 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti
|
||||
}
|
||||
|
||||
//Notebook
|
||||
let notebookValidator = instantiationService.createInstance(NotebookInputValidator);
|
||||
uri = getNotebookEditorUri(input, instantiationService);
|
||||
if (uri && notebookValidator.isNotebookEnabled()) {
|
||||
if (uri) {
|
||||
return withService<INotebookService, NotebookInput>(instantiationService, INotebookService, notebookService => {
|
||||
let fileName: string = 'untitled';
|
||||
let providerIds: string[] = [DEFAULT_NOTEBOOK_PROVIDER];
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!sql/media/actionBarLabel';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor } from 'vs/workbench/browser/viewlet';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { VIEWLET_ID } from 'sql/parts/dataExplorer/common/dataExplorerExtensionPoint';
|
||||
import { DataExplorerViewlet, DataExplorerViewletViewsContribution } from 'sql/parts/dataExplorer/viewlet/dataExplorerViewlet';
|
||||
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
|
||||
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { OpenConnectionsViewletAction } from 'sql/parts/objectExplorer/common/registeredServer.contribution';
|
||||
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
|
||||
|
||||
// Data Explorer Viewlet
|
||||
const viewletDescriptor = new ViewletDescriptor(
|
||||
DataExplorerViewlet,
|
||||
VIEWLET_ID,
|
||||
localize('workbench.dataExplorer', 'Data Explorer'),
|
||||
'dataExplorer',
|
||||
0
|
||||
);
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
Registry.as<ViewletRegistry>(ViewletExtensions.Viewlets).registerViewlet(viewletDescriptor);
|
||||
Registry.as<ViewletRegistry>(ViewletExtensions.Viewlets).setDefaultViewletId(VIEWLET_ID);
|
||||
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
|
||||
workbenchRegistry.registerWorkbenchContribution(DataExplorerViewletViewsContribution, LifecyclePhase.Starting);
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
registry.registerWorkbenchAction(
|
||||
new SyncActionDescriptor(
|
||||
OpenConnectionsViewletAction,
|
||||
OpenConnectionsViewletAction.ID,
|
||||
OpenConnectionsViewletAction.LABEL,
|
||||
{ primary: KeyMod.CtrlCmd | KeyCode.Shift | KeyCode.KEY_C }),
|
||||
'View: Show Servers',
|
||||
localize('registeredServers.view', "View")
|
||||
);
|
||||
|
||||
let configurationRegistry = <IConfigurationRegistry>Registry.as(Extensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': 'databaseConnections',
|
||||
'order': 0,
|
||||
'title': localize('databaseConnections', 'Database Connections'),
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'datasource.connections': {
|
||||
'description': localize('datasource.connections', 'data source connections'),
|
||||
'type': 'array'
|
||||
},
|
||||
'datasource.connectionGroups': {
|
||||
'description': localize('datasource.connectionGroups', 'data source groups'),
|
||||
'type': 'array'
|
||||
}
|
||||
}
|
||||
});
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': 'startupConfig',
|
||||
'title': localize('startupConfig', 'Startup Configuration'),
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'startup.alwaysShowServersView': {
|
||||
'type': 'boolean',
|
||||
'description': localize('startup.alwaysShowServersView', 'True for the Servers view to be shown on launch of Azure Data Studio default; false if the last opened view should be shown'),
|
||||
'default': true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
166
src/sql/parts/dataExplorer/common/dataExplorerExtensionPoint.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { forEach } from 'vs/base/common/collections';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IViewContainersRegistry, ViewContainer, Extensions as ViewContainerExtensions, ICustomViewDescriptor, ViewsRegistry } from 'vs/workbench/common/views';
|
||||
import { IExtensionPoint, ExtensionsRegistry, ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { CustomTreeViewPanel, CustomTreeViewer } from 'vs/workbench/browser/parts/views/customView';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { viewsContainersExtensionPoint } from 'vs/workbench/api/browser/viewsContainersExtensionPoint';
|
||||
|
||||
export const DataExplorerViewlet = {
|
||||
DataExplorer: 'dataExplorer'
|
||||
};
|
||||
export const VIEWLET_ID = 'workbench.view.dataExplorer';
|
||||
export const VIEW_CONTAINER: ViewContainer = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID);
|
||||
|
||||
|
||||
|
||||
interface IUserFriendlyViewDescriptor {
|
||||
id: string;
|
||||
name: string;
|
||||
when?: string;
|
||||
}
|
||||
|
||||
const viewDescriptor: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
description: localize('vscode.extension.contributes.view.id', 'Identifier of the view. Use this to register a data provider through `vscode.window.registerTreeDataProviderForView` API. Also to trigger activating your extension by registering `onView:${id}` event to `activationEvents`.'),
|
||||
type: 'string'
|
||||
},
|
||||
name: {
|
||||
description: localize('vscode.extension.contributes.view.name', 'The human-readable name of the view. Will be shown'),
|
||||
type: 'string'
|
||||
},
|
||||
when: {
|
||||
description: localize('vscode.extension.contributes.view.when', 'Condition which must be true to show this view'),
|
||||
type: 'string'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const dataExplorerContribution: IJSONSchema = {
|
||||
description: localize('extension.contributes.dataExplorer', "Contributes views to the editor"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'dataExplorer': {
|
||||
description: localize('extension.dataExplorer', "Contributes views to Data Explorer container in the Activity bar"),
|
||||
type: 'array',
|
||||
items: viewDescriptor,
|
||||
default: []
|
||||
}
|
||||
},
|
||||
additionalProperties: {
|
||||
description: localize('dataExplorer.contributed', "Contributes views to contributed views container"),
|
||||
type: 'array',
|
||||
items: viewDescriptor,
|
||||
default: []
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const dataExplorerExtensionPoint: IExtensionPoint<{ [loc: string]: IUserFriendlyViewDescriptor[] }> = ExtensionsRegistry.registerExtensionPoint<{ [loc: string]: IUserFriendlyViewDescriptor[] }>('dataExplorer', [viewsContainersExtensionPoint], dataExplorerContribution);
|
||||
|
||||
class DataExplorerContainerExtensionHandler implements IWorkbenchContribution {
|
||||
|
||||
private viewContainersRegistry: IViewContainersRegistry;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private instantiationService: IInstantiationService
|
||||
) {
|
||||
this.viewContainersRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
|
||||
this.handleAndRegisterCustomViews();
|
||||
}
|
||||
|
||||
private handleAndRegisterCustomViews() {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
return;
|
||||
}
|
||||
dataExplorerExtensionPoint.setHandler(extensions => {
|
||||
for (let extension of extensions) {
|
||||
const { value, collector } = extension;
|
||||
|
||||
forEach(value, entry => {
|
||||
if (!this.isValidViewDescriptors(entry.value, collector)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let container = this.viewContainersRegistry.get(VIEWLET_ID);
|
||||
if (!container) {
|
||||
collector.warn(localize('ViewsContainerDoesnotExist', "View container '{0}' does not exist and all views registered to it will be added to 'Data Explorer'.", entry.key));
|
||||
container = this.viewContainersRegistry.get(VIEWLET_ID);
|
||||
}
|
||||
const registeredViews = ViewsRegistry.getViews(container);
|
||||
const viewIds = [];
|
||||
const viewDescriptors = coalesce(entry.value.map(item => {
|
||||
// validate
|
||||
if (viewIds.indexOf(item.id) !== -1) {
|
||||
collector.error(localize('duplicateView1', "Cannot register multiple views with same id `{0}` in the view container `{1}`", item.id, container.id));
|
||||
return null;
|
||||
}
|
||||
if (registeredViews.some(v => v.id === item.id)) {
|
||||
collector.error(localize('duplicateView2', "A view with id `{0}` is already registered in the view container `{1}`", item.id, container.id));
|
||||
return null;
|
||||
}
|
||||
|
||||
const viewDescriptor = <ICustomViewDescriptor>{
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
ctor: CustomTreeViewPanel,
|
||||
container,
|
||||
when: ContextKeyExpr.deserialize(item.when),
|
||||
canToggleVisibility: true,
|
||||
collapsed: this.showCollapsed(container),
|
||||
treeViewer: this.instantiationService.createInstance(CustomTreeViewer, item.id, container)
|
||||
};
|
||||
|
||||
viewIds.push(viewDescriptor.id);
|
||||
return viewDescriptor;
|
||||
}));
|
||||
ViewsRegistry.registerViews(viewDescriptors);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private isValidViewDescriptors(viewDescriptors: IUserFriendlyViewDescriptor[], collector: ExtensionMessageCollector): boolean {
|
||||
if (!Array.isArray(viewDescriptors)) {
|
||||
collector.error(localize('requirearray', "views must be an array"));
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let descriptor of viewDescriptors) {
|
||||
if (typeof descriptor.id !== 'string') {
|
||||
collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'id'));
|
||||
return false;
|
||||
}
|
||||
if (typeof descriptor.name !== 'string') {
|
||||
collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'name'));
|
||||
return false;
|
||||
}
|
||||
if (descriptor.when && typeof descriptor.when !== 'string') {
|
||||
collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private showCollapsed(container: ViewContainer): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
|
||||
workbenchRegistry.registerWorkbenchContribution(DataExplorerContainerExtensionHandler, LifecyclePhase.Starting);
|
||||
@@ -0,0 +1,173 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IExtensionTipsService, IExtensionManagementServerService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/common/extensions';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { Builder } from 'vs/base/browser/builder';
|
||||
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { ServerTreeView } from 'sql/parts/objectExplorer/viewlet/serverTreeView';
|
||||
import { ClearSearchAction, ActiveConnectionsFilterAction,
|
||||
AddServerAction, AddServerGroupAction } from 'sql/parts/objectExplorer/viewlet/connectionTreeAction';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
|
||||
|
||||
export class ConnectionViewletPanel extends ViewletPanel {
|
||||
|
||||
private _root: HTMLElement;
|
||||
private _searchBox: InputBox;
|
||||
private _toDisposeViewlet: IDisposable[] = [];
|
||||
private _serverTreeView: ServerTreeView;
|
||||
private _clearSearchAction: ClearSearchAction;
|
||||
private _addServerAction: IAction;
|
||||
private _addServerGroupAction: IAction;
|
||||
private _activeConnectionsFilterAction: ActiveConnectionsFilterAction;
|
||||
|
||||
constructor(
|
||||
private options: IViewletViewOptions,
|
||||
@INotificationService protected notificationService: INotificationService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IInstantiationService protected instantiationService: IInstantiationService,
|
||||
@IThemeService private themeService: IThemeService,
|
||||
@IExtensionsWorkbenchService protected extensionsWorkbenchService: IExtensionsWorkbenchService,
|
||||
@IExtensionTipsService protected tipsService: IExtensionTipsService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
|
||||
@IExtensionManagementServerService protected extensionManagementServerService: IExtensionManagementServerService,
|
||||
@IObjectExplorerService private objectExplorerService: IObjectExplorerService
|
||||
) {
|
||||
super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService);
|
||||
this._clearSearchAction = this.instantiationService.createInstance(ClearSearchAction, ClearSearchAction.ID, ClearSearchAction.LABEL, this);
|
||||
this._addServerAction = this.instantiationService.createInstance(AddServerAction,
|
||||
AddServerAction.ID,
|
||||
AddServerAction.LABEL);
|
||||
this._addServerGroupAction = this.instantiationService.createInstance(AddServerGroupAction,
|
||||
AddServerGroupAction.ID,
|
||||
AddServerGroupAction.LABEL);
|
||||
this._serverTreeView = this.instantiationService.createInstance(ServerTreeView);
|
||||
this._activeConnectionsFilterAction = this._serverTreeView.activeConnectionsFilterAction;
|
||||
|
||||
this.objectExplorerService.registerServerTreeView(this._serverTreeView);
|
||||
}
|
||||
|
||||
protected renderHeader(container: HTMLElement): void {
|
||||
super.renderHeader(container);
|
||||
}
|
||||
|
||||
renderHeaderTitle(container: HTMLElement): void {
|
||||
super.renderHeaderTitle(container, this.options.title);
|
||||
}
|
||||
|
||||
renderBody(container: HTMLElement): void {
|
||||
let parentBuilder = new Builder(container);
|
||||
parentBuilder.div({ class: 'server-explorer-viewlet' }, (viewletContainer) => {
|
||||
viewletContainer.div({ class: 'search-box' }, (searchBoxContainer) => {
|
||||
let searchServerString = localize('Search server names', 'Search server names');
|
||||
this._searchBox = new InputBox(
|
||||
searchBoxContainer.getHTMLElement(),
|
||||
null,
|
||||
{
|
||||
placeholder: searchServerString,
|
||||
actions: [this._clearSearchAction],
|
||||
ariaLabel: searchServerString
|
||||
}
|
||||
);
|
||||
|
||||
this._searchBox.onDidChange(() => {
|
||||
this.search(this._searchBox.value);
|
||||
});
|
||||
|
||||
// Theme styler
|
||||
this._toDisposeViewlet.push(attachInputBoxStyler(this._searchBox, this.themeService));
|
||||
|
||||
});
|
||||
viewletContainer.div({ Class: 'object-explorer-view' }, (viewContainer) => {
|
||||
this._serverTreeView.renderBody(viewContainer.getHTMLElement()).then(() => {
|
||||
Promise.resolve(null);
|
||||
}, error => {
|
||||
console.warn('render registered servers: ' + error);
|
||||
Promise.resolve(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
this._root = container;
|
||||
}
|
||||
|
||||
layoutBody(size: number): void {
|
||||
this._searchBox.layout();
|
||||
this._serverTreeView.layout(size); // account for search box
|
||||
DOM.toggleClass(this._root, 'narrow', this._root.clientWidth < 300);
|
||||
}
|
||||
|
||||
show(): void {
|
||||
}
|
||||
|
||||
select(): void {
|
||||
}
|
||||
|
||||
showPrevious(): void {
|
||||
}
|
||||
|
||||
showPreviousPage(): void {
|
||||
}
|
||||
|
||||
showNext(): void {
|
||||
}
|
||||
|
||||
showNextPage(): void {
|
||||
}
|
||||
|
||||
count(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public getActions(): IAction[] {
|
||||
return [
|
||||
this._addServerAction,
|
||||
this._addServerGroupAction,
|
||||
this._activeConnectionsFilterAction
|
||||
];
|
||||
}
|
||||
|
||||
public clearSearch() {
|
||||
this._serverTreeView.refreshTree();
|
||||
this._searchBox.value = '';
|
||||
this._clearSearchAction.enabled = false;
|
||||
this._searchBox.focus();
|
||||
}
|
||||
|
||||
public search(value: string): void {
|
||||
if (value) {
|
||||
this._clearSearchAction.enabled = true;
|
||||
this._serverTreeView.searchTree(value);
|
||||
} else {
|
||||
this.clearSearch();
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._serverTreeView.dispose();
|
||||
super.dispose();
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
super.focus();
|
||||
}
|
||||
}
|
||||
146
src/sql/parts/dataExplorer/viewlet/dataExplorerViewlet.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { IViewlet } from 'vs/workbench/common/viewlet';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { append, $, addClass, toggleClass, Dimension } from 'vs/base/browser/dom';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { ViewContainerViewlet, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IPartService } from 'vs/workbench/services/part/common/partService';
|
||||
import { IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/views';
|
||||
import { ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { VIEWLET_ID, VIEW_CONTAINER } from 'sql/parts/dataExplorer/common/dataExplorerExtensionPoint';
|
||||
import { ConnectionViewletPanel } from 'sql/parts/dataExplorer/objectExplorer/connectionViewlet/connectionViewletPanel';
|
||||
import { Extensions as ViewContainerExtensions, ViewsRegistry, IViewDescriptor } from 'vs/workbench/common/views';
|
||||
|
||||
export class DataExplorerViewletViewsContribution implements IWorkbenchContribution {
|
||||
|
||||
constructor() {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
this.registerViews();
|
||||
}
|
||||
}
|
||||
|
||||
private registerViews(): void {
|
||||
let viewDescriptors = [];
|
||||
viewDescriptors.push(this.createObjectExplorerViewDescriptor());
|
||||
ViewsRegistry.registerViews(viewDescriptors);
|
||||
}
|
||||
|
||||
private createObjectExplorerViewDescriptor(): IViewDescriptor {
|
||||
return {
|
||||
id: 'dataExplorer.servers',
|
||||
name: localize('dataExplorer.servers', "Servers"),
|
||||
container: VIEW_CONTAINER,
|
||||
ctor: ConnectionViewletPanel,
|
||||
weight: 100,
|
||||
canToggleVisibility: true,
|
||||
order: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class DataExplorerViewlet extends ViewContainerViewlet {
|
||||
private root: HTMLElement;
|
||||
|
||||
private dataSourcesBox: HTMLElement;
|
||||
private primaryActions: IAction[];
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
@IPartService partService: IPartService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IViewletService private viewletService: IViewletService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IWorkspaceContextService contextService: IWorkspaceContextService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IExtensionService extensionService: IExtensionService
|
||||
) {
|
||||
super(VIEWLET_ID, `${VIEWLET_ID}.state`, true, partService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService);
|
||||
this.disposables.push(this.viewletService.onDidViewletOpen(this.onViewletOpen, this, this.disposables));
|
||||
}
|
||||
|
||||
create(parent: HTMLElement): TPromise<void> {
|
||||
addClass(parent, 'dataExplorer-viewlet');
|
||||
this.root = parent;
|
||||
|
||||
this.dataSourcesBox = append(this.root, $('.dataSources'));
|
||||
|
||||
return super.create(this.dataSourcesBox);
|
||||
}
|
||||
|
||||
public updateStyles(): void {
|
||||
super.updateStyles();
|
||||
}
|
||||
|
||||
setVisible(visible: boolean): TPromise<void> {
|
||||
const isVisibilityChanged = this.isVisible() !== visible;
|
||||
return super.setVisible(visible).then(() => {
|
||||
if (isVisibilityChanged) {
|
||||
if (visible) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
}
|
||||
|
||||
layout(dimension: Dimension): void {
|
||||
toggleClass(this.root, 'narrow', dimension.width <= 300);
|
||||
super.layout(new Dimension(dimension.width, dimension.height));
|
||||
}
|
||||
|
||||
getOptimalWidth(): number {
|
||||
return 400;
|
||||
}
|
||||
|
||||
getActions(): IAction[] {
|
||||
if (!this.primaryActions) {
|
||||
this.primaryActions = [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
getSecondaryActions(): IAction[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected onDidAddViews(added: IAddedViewDescriptorRef[]): ViewletPanel[] {
|
||||
const addedViews = super.onDidAddViews(added);
|
||||
TPromise.join(addedViews);
|
||||
return addedViews;
|
||||
}
|
||||
|
||||
protected createView(viewDescriptor: IViewDescriptor, options: IViewletViewOptions): ViewletPanel {
|
||||
return this.instantiationService.createInstance(viewDescriptor.ctor, options) as ViewletPanel;
|
||||
}
|
||||
|
||||
private onViewletOpen(viewlet: IViewlet): void {
|
||||
if (!viewlet || viewlet.getId() === VIEWLET_ID) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables = dispose(this.disposables);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,12 @@ import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorIn
|
||||
import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
|
||||
import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor';
|
||||
import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
|
||||
import { EditorOptions } from 'vs/workbench/common/editor';
|
||||
@@ -25,6 +24,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { Configuration } from 'vs/editor/browser/config/configuration';
|
||||
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
|
||||
/**
|
||||
* Extension of TextResourceEditor that is always readonly rather than only with non UntitledInputs
|
||||
@@ -34,17 +34,23 @@ export class QueryTextEditor extends BaseTextEditor {
|
||||
public static ID = 'modelview.editors.textEditor';
|
||||
private _dimension: DOM.Dimension;
|
||||
private _config: editorCommon.IConfiguration;
|
||||
private _minHeight: number;
|
||||
private _minHeight: number = 0;
|
||||
private _maxHeight: number = 4000;
|
||||
private _selected: boolean;
|
||||
private _hideLineNumbers: boolean;
|
||||
private _editorWorkspaceConfig;
|
||||
private _scrollbarHeight: number;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@ITextResourceConfigurationService configurationService: ITextResourceConfigurationService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IModeService modeService: IModeService,
|
||||
@ITextFileService textFileService: ITextFileService,
|
||||
@IEditorGroupsService editorGroupService: IEditorGroupsService,
|
||||
@IEditorService protected editorService: IEditorService,
|
||||
@IWorkspaceConfigurationService private workspaceConfigurationService: IWorkspaceConfigurationService
|
||||
|
||||
) {
|
||||
super(
|
||||
@@ -62,8 +68,6 @@ export class QueryTextEditor extends BaseTextEditor {
|
||||
options.inDiffEditor = true;
|
||||
options.scrollBeyondLastLine = false;
|
||||
options.folding = false;
|
||||
options.renderWhitespace = 'none';
|
||||
options.wordWrap = 'on';
|
||||
options.renderIndentGuides = false;
|
||||
options.rulers = [];
|
||||
options.glyphMargin = true;
|
||||
@@ -73,6 +77,12 @@ export class QueryTextEditor extends BaseTextEditor {
|
||||
options.overviewRulerLanes = 0;
|
||||
options.overviewRulerBorder = false;
|
||||
options.hideCursorInOverviewRuler = true;
|
||||
if (!this._selected) {
|
||||
options.renderLineHighlight = 'none';
|
||||
}
|
||||
if (this._hideLineNumbers) {
|
||||
options.lineNumbers = 'off';
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -114,10 +124,11 @@ export class QueryTextEditor extends BaseTextEditor {
|
||||
return editorWidget.getScrollHeight();
|
||||
}
|
||||
|
||||
public setHeightToScrollHeight(): void {
|
||||
public setHeightToScrollHeight(configChanged?: boolean): void {
|
||||
let editorWidget = this.getControl() as ICodeEditor;
|
||||
if (!this._config) {
|
||||
this._config = new Configuration(undefined, editorWidget.getDomNode());
|
||||
this._scrollbarHeight = this._config.editor.viewInfo.scrollbar.horizontalScrollbarSize;
|
||||
}
|
||||
let editorWidgetModel = editorWidget.getModel();
|
||||
let lineCount = editorWidgetModel.getLineCount();
|
||||
@@ -127,17 +138,58 @@ export class QueryTextEditor extends BaseTextEditor {
|
||||
// number of lines that wrap). Finally, viewportColumn is calculated on editor resizing automatically; we can use it to ensure
|
||||
// that the viewportColumn will always be greater than any character's column in an editor.
|
||||
let numberWrappedLines = 0;
|
||||
for (let line = 1; line <= lineCount; line++) {
|
||||
if (editorWidgetModel.getLineMaxColumn(line) >= this._config.editor.layoutInfo.viewportColumn - 1) {
|
||||
numberWrappedLines += Math.ceil(editorWidgetModel.getLineMaxColumn(line) / this._config.editor.layoutInfo.viewportColumn);
|
||||
let shouldAddHorizontalScrollbarHeight = false;
|
||||
if (!this._editorWorkspaceConfig || configChanged) {
|
||||
this._editorWorkspaceConfig = this.workspaceConfigurationService.getValue('editor');
|
||||
}
|
||||
let wordWrapEnabled: boolean = this._editorWorkspaceConfig && this._editorWorkspaceConfig['wordWrap'] && this._editorWorkspaceConfig['wordWrap'] === 'on' ? true : false;
|
||||
if (wordWrapEnabled) {
|
||||
for (let line = 1; line <= lineCount; line++) {
|
||||
// 4 columns is equivalent to the viewport column width and the edge of the editor
|
||||
if (editorWidgetModel.getLineMaxColumn(line) >= this._config.editor.layoutInfo.viewportColumn + 4) {
|
||||
numberWrappedLines += Math.ceil(editorWidgetModel.getLineMaxColumn(line) / this._config.editor.layoutInfo.viewportColumn);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let line = 1; line <= lineCount; line++) {
|
||||
// The horizontal scrollbar always appears 1 column past the viewport column when word wrap is disabled
|
||||
if (editorWidgetModel.getLineMaxColumn(line) >= this._config.editor.layoutInfo.viewportColumn + 1) {
|
||||
shouldAddHorizontalScrollbarHeight = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let editorHeightUsingLines = this._config.editor.lineHeight * (lineCount + numberWrappedLines);
|
||||
let editorHeightUsingMinHeight = Math.max(editorHeightUsingLines, this._minHeight);
|
||||
let editorHeightUsingMinHeight = Math.max(Math.min(editorHeightUsingLines, this._maxHeight), this._minHeight);
|
||||
editorHeightUsingMinHeight = shouldAddHorizontalScrollbarHeight ? editorHeightUsingMinHeight + this._scrollbarHeight : editorHeightUsingMinHeight;
|
||||
this.setHeight(editorHeightUsingMinHeight);
|
||||
}
|
||||
|
||||
public setMinimumHeight(height: number) : void {
|
||||
this._minHeight = height;
|
||||
}
|
||||
|
||||
public setMaximumHeight(height: number) : void {
|
||||
this._maxHeight = height;
|
||||
}
|
||||
|
||||
public toggleEditorSelected(selected: boolean): void {
|
||||
this._selected = selected;
|
||||
this.refreshEditorConfguration();
|
||||
}
|
||||
|
||||
public set hideLineNumbers(value: boolean) {
|
||||
this._hideLineNumbers = value;
|
||||
this.refreshEditorConfguration();
|
||||
}
|
||||
|
||||
private refreshEditorConfguration(configuration = this.configurationService.getValue<IEditorConfiguration>(this.getResource())): void {
|
||||
if (!this.getControl()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editorConfiguration = this.computeConfiguration(configuration);
|
||||
let editorSettingsToApply = editorConfiguration;
|
||||
this.getControl().updateOptions(editorSettingsToApply);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./table';
|
||||
|
||||
import {
|
||||
Component, Input, Inject, ChangeDetectorRef, forwardRef, ViewChild, ElementRef, OnDestroy, AfterViewInit
|
||||
} from '@angular/core';
|
||||
@@ -53,7 +55,10 @@ export default class TableComponent extends ComponentBase implements IComponent,
|
||||
return <Slick.Column<any>>{
|
||||
name: col.value,
|
||||
id: col.value,
|
||||
field: col.value
|
||||
field: col.value,
|
||||
width: col.width,
|
||||
cssClass: col.cssClass,
|
||||
toolTip: col.toolTip
|
||||
};
|
||||
} else {
|
||||
return <Slick.Column<any>>{
|
||||
|
||||
14
src/sql/parts/modelComponents/table.css
Normal 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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.center-align
|
||||
{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-with-header
|
||||
{
|
||||
padding-left:3px !important;
|
||||
}
|
||||
@@ -5,14 +5,12 @@
|
||||
|
||||
import { ElementRef } from '@angular/core';
|
||||
|
||||
import { nb } from 'sqlops';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
|
||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { CellContext, CellActionBase } from 'sql/parts/notebook/cellViews/codeActions';
|
||||
@@ -21,9 +19,12 @@ import { ToggleMoreWidgetAction } from 'sql/parts/dashboard/common/actions';
|
||||
import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
|
||||
import { CellModel } from 'sql/parts/notebook/models/cell';
|
||||
|
||||
export const HIDDEN_CLASS ='actionhidden';
|
||||
|
||||
export class CellToggleMoreActions {
|
||||
private _actions: Action[] = [];
|
||||
private _actions: CellActionBase[] = [];
|
||||
private _moreActions: ActionBar;
|
||||
private _moreActionsElement: HTMLElement;
|
||||
constructor(
|
||||
@IInstantiationService private instantiationService: IInstantiationService) {
|
||||
this._actions.push(
|
||||
@@ -36,19 +37,26 @@ export class CellToggleMoreActions {
|
||||
);
|
||||
}
|
||||
|
||||
public toggle(showIcon: boolean, elementRef: ElementRef, model: NotebookModel, cellModel: ICellModel) {
|
||||
public onInit(elementRef: ElementRef, model: NotebookModel, cellModel: ICellModel) {
|
||||
let context = new CellContext(model,cellModel);
|
||||
let moreActionsElement = <HTMLElement>elementRef.nativeElement;
|
||||
if (showIcon) {
|
||||
if (moreActionsElement.childNodes.length > 0) {
|
||||
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||
}
|
||||
this._moreActions = new ActionBar(moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
|
||||
this._moreActions.context = { target: moreActionsElement };
|
||||
this._moreActions.push(this.instantiationService.createInstance(ToggleMoreWidgetAction, this._actions, context), { icon: showIcon, label: false });
|
||||
this._moreActionsElement = <HTMLElement>elementRef.nativeElement;
|
||||
if (this._moreActionsElement.childNodes.length > 0) {
|
||||
this._moreActionsElement.removeChild(this._moreActionsElement.childNodes[0]);
|
||||
}
|
||||
else if (moreActionsElement.childNodes.length > 0) {
|
||||
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||
this._moreActions = new ActionBar(this._moreActionsElement, { orientation: ActionsOrientation.VERTICAL, isMenu: true });
|
||||
this._moreActions.context = { target: this._moreActionsElement };
|
||||
let validActions = this._actions.filter(a => a.canRun(context));
|
||||
this._moreActions.push(this.instantiationService.createInstance(ToggleMoreWidgetAction, validActions, context), { icon: true, label: false });
|
||||
}
|
||||
|
||||
public toggleVisible(visible: boolean): void {
|
||||
if (!this._moreActionsElement) {
|
||||
return;
|
||||
}
|
||||
if (visible) {
|
||||
DOM.addClass(this._moreActionsElement, HIDDEN_CLASS);
|
||||
} else {
|
||||
DOM.removeClass(this._moreActionsElement, HIDDEN_CLASS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +69,7 @@ export class AddCellFromContextAction extends CellActionBase {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
runCellAction(context: CellContext): Promise<void> {
|
||||
doRun(context: CellContext): Promise<void> {
|
||||
try {
|
||||
let model = context.model;
|
||||
let index = model.cells.findIndex((cell) => cell.id === context.cell.id);
|
||||
@@ -88,7 +96,7 @@ export class DeleteCellAction extends CellActionBase {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
runCellAction(context: CellContext): Promise<void> {
|
||||
doRun(context: CellContext): Promise<void> {
|
||||
try {
|
||||
context.model.deleteCell(context.cell);
|
||||
} catch (error) {
|
||||
@@ -110,9 +118,17 @@ export class ClearCellOutputAction extends CellActionBase {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
runCellAction(context: CellContext): Promise<void> {
|
||||
public canRun(context: CellContext): boolean {
|
||||
return context.cell && context.cell.cellType === CellTypes.Code;
|
||||
}
|
||||
|
||||
|
||||
doRun(context: CellContext): Promise<void> {
|
||||
try {
|
||||
(context.model.activeCell as CellModel).clearOutputs();
|
||||
let cell = context.cell || context.model.activeCell;
|
||||
if (cell) {
|
||||
(cell as CellModel).clearOutputs();
|
||||
}
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: row">
|
||||
<div #toolbar class="toolbar" style="flex: 0 0 auto; display: flex; flex-flow:column; width: 40px; min-height: 40px; padding-top: 10px; orientation: portrait">
|
||||
<div style="width: 100%; height: 100%; display: flex; flex-flow: row" (mouseover)="hover=true" (mouseleave)="hover=false">
|
||||
<div #toolbar class="toolbar">
|
||||
</div>
|
||||
<div #editor class="editor" style="flex: 1 1 auto; overflow: hidden;">
|
||||
</div>
|
||||
|
||||
@@ -30,8 +30,13 @@ import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Emitter, debounceEvent } from 'vs/base/common/event';
|
||||
import { CellTypes } from 'sql/parts/notebook/models/contracts';
|
||||
import { OVERRIDE_EDITOR_THEMING_SETTING } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
|
||||
export const CODE_SELECTOR: string = 'code-component';
|
||||
const MARKDOWN_CLASS = 'markdown';
|
||||
|
||||
@Component({
|
||||
selector: CODE_SELECTOR,
|
||||
@@ -41,8 +46,18 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
@ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef;
|
||||
@ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef;
|
||||
@ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef;
|
||||
@Input() cellModel: ICellModel;
|
||||
@Input() hideVerticalToolbar: boolean = false;
|
||||
|
||||
public get cellModel(): ICellModel {
|
||||
return this._cellModel;
|
||||
}
|
||||
|
||||
@Input() public set cellModel(value: ICellModel) {
|
||||
this._cellModel = value;
|
||||
if (this.toolbarElement && value && value.cellType === CellTypes.Markdown) {
|
||||
let nativeToolbar = <HTMLElement> this.toolbarElement.nativeElement;
|
||||
DOM.addClass(nativeToolbar, MARKDOWN_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
@Output() public onContentChanged = new EventEmitter<void>();
|
||||
|
||||
@@ -54,15 +69,25 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
this._activeCellId = value;
|
||||
}
|
||||
|
||||
@Input() set hover(value: boolean) {
|
||||
this.cellModel.hover = value;
|
||||
if (!this.isActive()) {
|
||||
// Only make a change if we're not active, since this has priority
|
||||
this.toggleMoreActionsButton(this.cellModel.hover);
|
||||
}
|
||||
}
|
||||
|
||||
protected _actionBar: Taskbar;
|
||||
private readonly _minimumHeight = 30;
|
||||
private readonly _maximumHeight = 4000;
|
||||
private _cellModel: ICellModel;
|
||||
private _editor: QueryTextEditor;
|
||||
private _editorInput: UntitledEditorInput;
|
||||
private _editorModel: ITextModel;
|
||||
private _uri: string;
|
||||
private _model: NotebookModel;
|
||||
private _activeCellId: string;
|
||||
private _cellToggleMoreActions: CellToggleMoreActions;
|
||||
private _layoutEmitter = new Emitter<void>();
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
||||
@@ -74,17 +99,19 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
|
||||
@Inject(IContextViewService) private contextViewService: IContextViewService,
|
||||
@Inject(INotificationService) private notificationService: INotificationService,
|
||||
@Inject(IConfigurationService) private _configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
|
||||
debounceEvent(this._layoutEmitter.event, (l, e) => e, 250, /*leading=*/false)
|
||||
(() => this.layout());
|
||||
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||
this.updateTheme(this.themeService.getColorTheme());
|
||||
if (!this.hideVerticalToolbar) {
|
||||
this.initActionBar();
|
||||
}
|
||||
this.initActionBar();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||
@@ -93,11 +120,10 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
for (let propName in changes) {
|
||||
if (propName === 'activeCellId') {
|
||||
let changedProp = changes[propName];
|
||||
if (this.cellModel.id === changedProp.currentValue) {
|
||||
this._cellToggleMoreActions.toggle(true, this.moreActionsElementRef, this.model, this.cellModel);
|
||||
}
|
||||
else {
|
||||
this._cellToggleMoreActions.toggle(false, this.moreActionsElementRef, this.model, this.cellModel);
|
||||
let isActive = this.cellModel.id === changedProp.currentValue;
|
||||
this.toggleMoreActionsButton(isActive);
|
||||
if (this._editor) {
|
||||
this._editor.toggleEditorSelected(isActive);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -107,10 +133,14 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
ngAfterContentInit(): void {
|
||||
this.createEditor();
|
||||
this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, e => {
|
||||
this.layout();
|
||||
this._layoutEmitter.fire();
|
||||
}));
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this._layoutEmitter.fire();
|
||||
}
|
||||
|
||||
get model(): NotebookModel {
|
||||
return this._model;
|
||||
}
|
||||
@@ -125,6 +155,7 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
this._editor.create(this.codeElement.nativeElement);
|
||||
this._editor.setVisible(true);
|
||||
this._editor.setMinimumHeight(this._minimumHeight);
|
||||
this._editor.setMaximumHeight(this._maximumHeight);
|
||||
let uri = this.createUri();
|
||||
this._editorInput = instantiationService.createInstance(UntitledEditorInput, uri, false, this.cellModel.language, '', '');
|
||||
this._editor.setInput(this._editorInput, undefined);
|
||||
@@ -133,6 +164,12 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
this._editorModel = model.textEditorModel;
|
||||
this._modelService.updateModel(this._editorModel, this.cellModel.source);
|
||||
});
|
||||
let isActive = this.cellModel.id === this._activeCellId;
|
||||
this._editor.toggleEditorSelected(isActive);
|
||||
|
||||
// For markdown cells, don't show line numbers unless we're using editor defaults
|
||||
let overrideEditorSetting = this._configurationService.getValue<boolean>(OVERRIDE_EDITOR_THEMING_SETTING);
|
||||
this._editor.hideLineNumbers = (overrideEditorSetting && this.cellModel.cellType === CellTypes.Markdown);
|
||||
|
||||
this._register(this._editor);
|
||||
this._register(this._editorInput);
|
||||
@@ -140,7 +177,15 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
this._editor.setHeightToScrollHeight();
|
||||
this.cellModel.source = this._editorModel.getValue();
|
||||
this.onContentChanged.emit();
|
||||
// TODO see if there's a better way to handle reassessing size.
|
||||
setTimeout(() => this._layoutEmitter.fire(), 250);
|
||||
}));
|
||||
this._register(this._configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('editor.wordWrap')) {
|
||||
this._editor.setHeightToScrollHeight(true);
|
||||
}
|
||||
}));
|
||||
this._register(this.model.layoutChanged(() => this._layoutEmitter.fire, this));
|
||||
this.layout();
|
||||
}
|
||||
|
||||
@@ -153,7 +198,7 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
|
||||
protected initActionBar() {
|
||||
let context = new CellContext(this.model, this.cellModel);
|
||||
let runCellAction = this._instantiationService.createInstance(RunCellAction);
|
||||
let runCellAction = this._instantiationService.createInstance(RunCellAction, context);
|
||||
|
||||
let taskbar = <HTMLElement>this.toolbarElement.nativeElement;
|
||||
this._actionBar = new Taskbar(taskbar, this.contextMenuService);
|
||||
@@ -161,6 +206,8 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
this._actionBar.setContent([
|
||||
{ action: runCellAction }
|
||||
]);
|
||||
|
||||
this._cellToggleMoreActions.onInit(this.moreActionsElementRef, this.model, this.cellModel);
|
||||
}
|
||||
|
||||
|
||||
@@ -200,4 +247,12 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
this._editor.getContainer().scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
protected isActive() {
|
||||
return this.cellModel && this.cellModel.id === this.activeCellId;
|
||||
}
|
||||
|
||||
protected toggleMoreActionsButton(isActiveOrHovered: boolean) {
|
||||
this._cellToggleMoreActions.toggleVisible(!isActiveOrHovered);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,22 @@ code-component {
|
||||
|
||||
code-component .toolbar {
|
||||
border-right-width: 1px;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-flow:column;
|
||||
width: 40px;
|
||||
min-height: 40px;
|
||||
orientation: portrait
|
||||
}
|
||||
|
||||
code-component .toolbar.markdown {
|
||||
display: none;
|
||||
}
|
||||
|
||||
code-component .toolbar .carbon-taskbar {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
code-component .toolbarIconRun {
|
||||
@@ -24,17 +40,26 @@ code-component .toolbarIconRun {
|
||||
background-image: url('../media/dark/execute_cell_inverse.svg');
|
||||
}
|
||||
|
||||
code-component .toolbarIconRunError {
|
||||
height: 20px;
|
||||
background-image: url('../media/light/execute_cell_error.svg');
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
code-component .toolbarIconStop {
|
||||
height: 20px;
|
||||
background-image: url('../media/light/stop_cell.svg');
|
||||
background-image: url('../media/light/stop_cell_solidanimation.svg');
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.vs-dark code-component .toolbarIconStop,
|
||||
.hc-black code-component .toolbarIconStop {
|
||||
background-image: url('../media/dark/stop_cell_inverse.svg');
|
||||
background-image: url('../media/dark/stop_cell_solidanimation_inverse.svg');
|
||||
}
|
||||
|
||||
code-component .editor {
|
||||
padding: 5px 0px 5px 0px
|
||||
}
|
||||
/* overview ruler */
|
||||
code-component .monaco-editor .decorationsOverviewRuler {
|
||||
visibility: hidden;
|
||||
@@ -45,6 +70,21 @@ code-component .carbon-taskbar .icon {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .icon.hideIcon {
|
||||
width: 0px;
|
||||
padding-left: 0px;
|
||||
padding-top: 6px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .icon.hideIcon.execCountTen {
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar .icon.hideIcon.execCountHundred {
|
||||
margin-left: -6px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated .actions-container
|
||||
{
|
||||
|
||||
@@ -8,14 +8,17 @@ import { nb } from 'sqlops';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { localize } from 'vs/nls';
|
||||
import { CellType } from 'sql/parts/notebook/models/contracts';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
|
||||
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||
import { getErrorMessage } from 'sql/parts/notebook/notebookUtils';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { ICellModel, FutureInternal } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { ToggleableAction } from 'sql/parts/notebook/notebookActions';
|
||||
import { ICellModel, CellExecutionState } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { MultiStateAction, IMultiStateData, IActionStateData } from 'sql/parts/notebook/notebookActions';
|
||||
|
||||
let notebookMoreActionMsg = localize('notebook.failed', "Please select active cell and try again");
|
||||
const emptyExecutionCountLabel = '[ ]';
|
||||
|
||||
function hasModelAndCell(context: CellContext, notificationService: INotificationService): boolean {
|
||||
if (!context || !context.model) {
|
||||
@@ -46,92 +49,83 @@ export abstract class CellActionBase extends Action {
|
||||
super(id, label, icon);
|
||||
}
|
||||
|
||||
public canRun(context: CellContext): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public run(context: CellContext): TPromise<boolean> {
|
||||
if (hasModelAndCell(context, this.notificationService)) {
|
||||
return TPromise.wrap(this.runCellAction(context).then(() => true));
|
||||
return TPromise.wrap(this.doRun(context).then(() => true));
|
||||
}
|
||||
return TPromise.as(true);
|
||||
}
|
||||
|
||||
abstract runCellAction(context: CellContext): Promise<void>;
|
||||
abstract doRun(context: CellContext): Promise<void>;
|
||||
}
|
||||
|
||||
export class RunCellAction extends ToggleableAction {
|
||||
export class RunCellAction extends MultiStateAction<CellExecutionState> {
|
||||
public static ID = 'notebook.runCell';
|
||||
public static LABEL = 'Run cell';
|
||||
|
||||
constructor(@INotificationService private notificationService: INotificationService) {
|
||||
super(RunCellAction.ID, {
|
||||
shouldToggleTooltip: true,
|
||||
toggleOnLabel: localize('runCell', 'Run cell'),
|
||||
toggleOnClass: 'toolbarIconRun',
|
||||
toggleOffLabel: localize('stopCell', 'Cancel execution'),
|
||||
toggleOffClass: 'toolbarIconStop',
|
||||
isOn: true
|
||||
});
|
||||
private _executionChangedDisposable: IDisposable;
|
||||
private _context: CellContext;
|
||||
constructor(context: CellContext, @INotificationService private notificationService: INotificationService) {
|
||||
super(RunCellAction.ID, new IMultiStateData<CellExecutionState>([
|
||||
{ key: CellExecutionState.Hidden, value: { label: emptyExecutionCountLabel, className: '', tooltip: '', hideIcon: true }},
|
||||
{ key: CellExecutionState.Stopped, value: { label: '', className: 'toolbarIconRun', tooltip: localize('runCell', 'Run cell') }},
|
||||
{ key: CellExecutionState.Running, value: { label: '', className: 'toolbarIconStop', tooltip: localize('stopCell', 'Cancel execution') }},
|
||||
{ key: CellExecutionState.Error, value: { label: '', className: 'toolbarIconRunError', tooltip: localize('errorRunCell', 'Error on last run. Click to run again') }},
|
||||
], CellExecutionState.Hidden ));
|
||||
this.ensureContextIsUpdated(context);
|
||||
}
|
||||
|
||||
public run(context: CellContext): TPromise<boolean> {
|
||||
if (hasModelAndCell(context, this.notificationService)) {
|
||||
return TPromise.wrap(this.runCellAction(context).then(() => true));
|
||||
public run(context?: CellContext): TPromise<boolean> {
|
||||
return TPromise.wrap(this.doRun(context).then(() => true));
|
||||
}
|
||||
|
||||
public async doRun(context: CellContext): Promise<void> {
|
||||
this.ensureContextIsUpdated(context);
|
||||
if (!this._context) {
|
||||
// TODO should we error?
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this._context.cell.runCell(this.notificationService);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
this.notificationService.error(message);
|
||||
}
|
||||
return TPromise.as(true);
|
||||
}
|
||||
|
||||
public async runCellAction(context: CellContext): Promise<void> {
|
||||
try {
|
||||
let model = context.model;
|
||||
let cell = context.cell;
|
||||
let kernel = await this.getOrStartKernel(model);
|
||||
if (!kernel) {
|
||||
return;
|
||||
private ensureContextIsUpdated(context: CellContext) {
|
||||
if (context && context !== this._context) {
|
||||
if (this._executionChangedDisposable) {
|
||||
this._executionChangedDisposable.dispose();
|
||||
}
|
||||
// If cell is currently running and user clicks the stop/cancel button, call kernel.interrupt()
|
||||
// This matches the same behavior as JupyterLab
|
||||
if (cell.future && cell.future.inProgress) {
|
||||
cell.future.inProgress = false;
|
||||
await kernel.interrupt();
|
||||
} else {
|
||||
// TODO update source based on editor component contents
|
||||
let content = cell.source;
|
||||
if (content) {
|
||||
this.toggle(false);
|
||||
let future = await kernel.requestExecute({
|
||||
code: content,
|
||||
stop_on_error: true
|
||||
}, false);
|
||||
cell.setFuture(future as FutureInternal);
|
||||
// For now, await future completion. Later we should just track and handle cancellation based on model notifications
|
||||
let reply = await future.done;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
this.notificationService.error(message);
|
||||
} finally {
|
||||
this.toggle(true);
|
||||
this._context = context;
|
||||
this.updateStateAndExecutionCount(context.cell.executionState);
|
||||
this._executionChangedDisposable = this._context.cell.onExecutionStateChange((state) => {
|
||||
this.updateStateAndExecutionCount(state);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getOrStartKernel(model: NotebookModel): Promise<nb.IKernel> {
|
||||
let clientSession = model && model.clientSession;
|
||||
if (!clientSession) {
|
||||
this.notificationService.error(localize('notebookNotReady', 'The session for this notebook is not yet ready'));
|
||||
return undefined;
|
||||
} else if (!clientSession.isReady || clientSession.status === 'dead') {
|
||||
this.notificationService.info(localize('sessionNotReady', 'The session for this notebook will start momentarily'));
|
||||
await clientSession.kernelChangeCompleted;
|
||||
}
|
||||
if (!clientSession.kernel) {
|
||||
let defaultKernel = model && model.defaultKernel && model.defaultKernel.name;
|
||||
if (!defaultKernel) {
|
||||
this.notificationService.error(localize('noDefaultKernel', 'No kernel is available for this notebook'));
|
||||
return undefined;
|
||||
}
|
||||
await clientSession.changeKernel({
|
||||
name: defaultKernel
|
||||
});
|
||||
}
|
||||
return clientSession.kernel;
|
||||
}
|
||||
private updateStateAndExecutionCount(state: CellExecutionState) {
|
||||
let label = emptyExecutionCountLabel;
|
||||
let className = '';
|
||||
if (!types.isUndefinedOrNull(this._context.cell.executionCount)) {
|
||||
label = `[${this._context.cell.executionCount}]`;
|
||||
// Heuristic to try and align correctly independent of execution count length. Moving left margin
|
||||
// back by a few px seems to make things "work" OK, but isn't a super clean solution
|
||||
if (label.length === 4) {
|
||||
className = 'execCountTen';
|
||||
} else if (label.length > 4) {
|
||||
className = 'execCountHundred';
|
||||
}
|
||||
}
|
||||
this.states.updateStateData(CellExecutionState.Hidden, (data) => {
|
||||
data.label = label;
|
||||
data.className = className;
|
||||
});
|
||||
this.updateState(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||
<div class="notebook-code" style="flex: 0 0 auto;">
|
||||
<div style="width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||
<div style="flex: 0 0 auto;">
|
||||
<code-component [cellModel]="cellModel" [model]="model" [activeCellId]="activeCellId"></code-component>
|
||||
</div>
|
||||
<div style="flex: 0 0 auto; width: 100%; height: 100%; display: block">
|
||||
|
||||