Compare commits
172 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78a42e1d11 | ||
|
|
d2e758c0d7 | ||
|
|
6f5ad3a8a3 | ||
|
|
3e446980df | ||
|
|
053636af9c | ||
|
|
e3b166846d | ||
|
|
fcba0d1558 | ||
|
|
35e3a42017 | ||
|
|
401d4b2211 | ||
|
|
be3e7e3dc1 | ||
|
|
1e12e61243 | ||
|
|
f3b12dd5ac | ||
|
|
73bb5501bd | ||
|
|
47e3761159 | ||
|
|
cf4dd48784 | ||
|
|
1404133283 | ||
|
|
ecfcb92a89 | ||
|
|
10b066d300 | ||
|
|
8a8cb3ab27 | ||
|
|
f19f21d547 | ||
|
|
92fbfcdac9 | ||
|
|
4189e761ff | ||
|
|
a8b3f056a0 | ||
|
|
cc6dea0631 | ||
|
|
8c4f6f9e5f | ||
|
|
708461eab5 | ||
|
|
8ec1a05296 | ||
|
|
c4bf1b4180 | ||
|
|
495c9330f6 | ||
|
|
49619e5b39 | ||
|
|
7b88800c62 | ||
|
|
ecef90dc8b | ||
|
|
e8d4fba3c0 | ||
|
|
384d87f84d | ||
|
|
8b349dbcde | ||
|
|
7f5e00fd81 | ||
|
|
e5858dee52 | ||
|
|
bae573453a | ||
|
|
3e68c3ee0c | ||
|
|
e44e0a7c89 | ||
|
|
678b2737bd | ||
|
|
6b5193908c | ||
|
|
87f1f11509 | ||
|
|
0503c8d8fe | ||
|
|
bc7ac519d0 | ||
|
|
00c3758d86 | ||
|
|
e5256b0a61 | ||
|
|
eb3c6cadd2 | ||
|
|
d701a20cd5 | ||
|
|
095f35d07e | ||
|
|
53cd22f142 | ||
|
|
8cf4120c27 | ||
|
|
b34e3cbe90 | ||
|
|
4ef25ecf37 | ||
|
|
f5d647f05c | ||
|
|
7b6181de2a | ||
|
|
20bbaa3fe6 | ||
|
|
a2c9a0a1ae | ||
|
|
98c6af628b | ||
|
|
f39647f243 | ||
|
|
c2cec5d93f | ||
|
|
5a11cf1a6f | ||
|
|
b0b1b59147 | ||
|
|
77fb060fde | ||
|
|
caba5c9d26 | ||
|
|
97d36e2281 | ||
|
|
32235b0cb6 | ||
|
|
6142109bf5 | ||
|
|
144a7f941b | ||
|
|
f01c318c30 | ||
|
|
ac76302d6c | ||
|
|
a906a9c862 | ||
|
|
9a3daabeb4 | ||
|
|
9687159484 | ||
|
|
6a0ffdfa60 | ||
|
|
e3f26e8f12 | ||
|
|
cf85bb14f5 | ||
|
|
4fe81d8449 | ||
|
|
46b8d55280 | ||
|
|
08cf731c87 | ||
|
|
4c1af148c7 | ||
|
|
83410565da | ||
|
|
de81c37611 | ||
|
|
176719000d | ||
|
|
1411ad4503 | ||
|
|
77b351adf3 | ||
|
|
b37b14eabd | ||
|
|
578ac6cae5 | ||
|
|
72c3239d63 | ||
|
|
99614ecc8f | ||
|
|
1ececc3035 | ||
|
|
433e5633cf | ||
|
|
b9a0c9ce7e | ||
|
|
47cf496c36 | ||
|
|
32313c71e4 | ||
|
|
453caa92d4 | ||
|
|
7a689b93db | ||
|
|
639efbcfad | ||
|
|
1c706fdfca | ||
|
|
fab8de632d | ||
|
|
27cbd53253 | ||
|
|
d67fd038dc | ||
|
|
36fe725cf0 | ||
|
|
373c3488bb | ||
|
|
58e5e095e5 | ||
|
|
9e7282d16a | ||
|
|
561b7575ba | ||
|
|
cecc899949 | ||
|
|
59b0e6737f | ||
|
|
449cd9ea27 | ||
|
|
256ef072df | ||
|
|
26d8b32717 | ||
|
|
7a31d66d2c | ||
|
|
2ed9a93bae | ||
|
|
f494c7af4e | ||
|
|
363af2a85c | ||
|
|
6ff34d9894 | ||
|
|
a566fa9728 | ||
|
|
248f2f5071 | ||
|
|
a79f1ac830 | ||
|
|
073a372d4d | ||
|
|
6c69eaef4c | ||
|
|
b0fdaedfdb | ||
|
|
d089d6642a | ||
|
|
5aa730b5d4 | ||
|
|
0832dd2a45 | ||
|
|
a03507c998 | ||
|
|
f1e38b655e | ||
|
|
33a9f2e3e4 | ||
|
|
eaa5f504e3 | ||
|
|
5a7562a37b | ||
|
|
95a50b7892 | ||
|
|
86a3217e98 | ||
|
|
d15a3fcc98 | ||
|
|
ff38bc8143 | ||
|
|
14a6bf581c | ||
|
|
730ad4b814 | ||
|
|
f05260d95a | ||
|
|
673ecc3870 | ||
|
|
97a37e6834 | ||
|
|
d9b48bae80 | ||
|
|
cbaa0a132f | ||
|
|
4ad5520568 | ||
|
|
43457c0184 | ||
|
|
1150433c0a | ||
|
|
76a84a2cf4 | ||
|
|
68328f65b5 | ||
|
|
b7956c5fbf | ||
|
|
44d6bb66da | ||
|
|
7d67711336 | ||
|
|
f320deaa73 | ||
|
|
a518c4a529 | ||
|
|
da164cec0a | ||
|
|
bb470c3676 | ||
|
|
685a608518 | ||
|
|
5be2121a3e | ||
|
|
bf643cc85f | ||
|
|
eda96c046a | ||
|
|
137c78c04e | ||
|
|
d9e1aa57c9 | ||
|
|
912c80e496 | ||
|
|
7390dce536 | ||
|
|
67859ab139 | ||
|
|
540635c54f | ||
|
|
6197279e83 | ||
|
|
4ad226570a | ||
|
|
50242b2c35 | ||
|
|
9f7d96bad3 | ||
|
|
8d70544374 | ||
|
|
4b6214c9a4 | ||
|
|
aaa2ef3a97 | ||
|
|
639bd5a550 |
21
CHANGELOG.md
@@ -1,5 +1,26 @@
|
||||
# Change Log
|
||||
|
||||
## Version 1.8.0
|
||||
* Release date: June 6, 2019
|
||||
* Release status: General Availability
|
||||
|
||||
## What's new in this version
|
||||
* Initial release of the Database Admin Tool Extensions for Windows *Preview* extension
|
||||
* Initial release of the Central Management Servers extension
|
||||
* **Schema Compare**
|
||||
* Added Exclude/Include Options
|
||||
* Generate Script opens script after being generated
|
||||
* Removed double scroll bars
|
||||
* Formatting and layout improvements
|
||||
* Complete changes can be found [here](https://github.com/microsoft/azuredatastudio/issues?q=is%3Aissue+milestone%3A%22June+2019+Release%22+label%3A%22Area%3A+Schema+Compare%22+is%3Aclosed)
|
||||
* Messages panel moved into results panel - when users ran SQL queries, results and messages were in stacked panels. Now they are in separate tabs in a single panel similar to SSMS.
|
||||
* **Notebook**
|
||||
* Users can now choose to use their own Python 3 or Anaconda installs in notebooks
|
||||
* Multiple Stability + fit/finish fixes
|
||||
* View the full list of improvements and fixes [here](https://github.com/microsoft/azuredatastudio/issues?q=is%3Aissue+milestone%3A%22June+2019+Release%22+is%3Aclosed+label%3A%22Area%3A+Notebooks%22)
|
||||
* Visual Studio Code May Release Merge 1.34 - the latest improvements can be found [here](https://code.visualstudio.com/updates/v1_34)
|
||||
* Resolved [bugs and issues](https://github.com/microsoft/azuredatastudio/milestone/32?closed=1).
|
||||
|
||||
## Version 1.7.0
|
||||
* Release date: May 8, 2019
|
||||
* Release status: General Availability
|
||||
|
||||
23
README.md
@@ -5,21 +5,21 @@
|
||||
|
||||
Azure Data Studio is a data management tool that enables you to work with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux.
|
||||
|
||||
**Download the latest Azure Data Studio release**
|
||||
## **Download the latest Azure Data Studio release**
|
||||
|
||||
Platform | Link
|
||||
-- | --
|
||||
Windows User Installer | https://go.microsoft.com/fwlink/?linkid=2091882
|
||||
Windows System Installer | https://go.microsoft.com/fwlink/?linkid=2091491
|
||||
Windows ZIP | https://go.microsoft.com/fwlink/?linkid=2091490
|
||||
macOS ZIP | https://go.microsoft.com/fwlink/?linkid=2091489
|
||||
Linux TAR.GZ | https://go.microsoft.com/fwlink/?linkid=2091488
|
||||
Linux RPM | https://go.microsoft.com/fwlink/?linkid=2091487
|
||||
Linux DEB | https://go.microsoft.com/fwlink/?linkid=2092022
|
||||
Windows User Installer | https://go.microsoft.com/fwlink/?linkid=2094100
|
||||
Windows System Installer | https://go.microsoft.com/fwlink/?linkid=2094200
|
||||
Windows ZIP | https://go.microsoft.com/fwlink/?linkid=2094201
|
||||
macOS ZIP | https://go.microsoft.com/fwlink/?linkid=2094202
|
||||
Linux TAR.GZ | https://go.microsoft.com/fwlink/?linkid=2094101
|
||||
Linux RPM | https://go.microsoft.com/fwlink/?linkid=2094102
|
||||
Linux DEB | https://go.microsoft.com/fwlink/?linkid=2094203
|
||||
|
||||
Go to our [download page](https://aka.ms/azuredatastudio) for more specific instructions.
|
||||
|
||||
Try out the latest insiders build from `master`:
|
||||
## 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)
|
||||
@@ -28,7 +28,7 @@ Try out the latest insiders build from `master`:
|
||||
|
||||
See the [change log](https://github.com/Microsoft/azuredatastudio/blob/master/CHANGELOG.md) for additional details of what's in this release.
|
||||
|
||||
**Feature Highlights**
|
||||
## **Feature Highlights**
|
||||
|
||||
- Cross-Platform DB management for Windows, macOS and Linux with simple XCopy deployment
|
||||
- SQL Server Connection Management with Connection Dialog, Server Groups, Azure Integration and Registered Servers
|
||||
@@ -68,7 +68,8 @@ The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.micro
|
||||
## Contributions and "Thank You"
|
||||
We would like to thank all our users who raised issues, and in particular the following users who helped contribute fixes:
|
||||
|
||||
* yamatoya for `fix the format (#4899)`
|
||||
* Stevoni for `Corrected Keyboard Shortcut Execution Issue #5480`
|
||||
* yamatoya for `fix the format #4899`
|
||||
* GeoffYoung for `Fix sqlDropColumn description #4422`
|
||||
* AlexFsmn for `Added context menu for DBs in explorer view to backup & restore db. #2277`
|
||||
* sadedil for `Missing feature request: Save as XML #3729`
|
||||
|
||||
@@ -46,7 +46,6 @@ expressly granted herein, whether by implication, estoppel or otherwise.
|
||||
node-fetch: https://github.com/bitinn/node-fetch
|
||||
node-pty: https://github.com/Tyriar/node-pty
|
||||
nsfw: https://github.com/Axosoft/nsfw
|
||||
pretty-data: https://github.com/vkiryukhin/pretty-data
|
||||
primeng: https://github.com/primefaces/primeng
|
||||
process-nextick-args: https://github.com/calvinmetcalf/process-nextick-args
|
||||
pty.js: https://github.com/chjj/pty.js
|
||||
@@ -1420,16 +1419,6 @@ SOFTWARE.
|
||||
=========================================
|
||||
END OF nsfw NOTICES AND INFORMATION
|
||||
|
||||
%% pretty-data NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
License: Dual licensed under the MIT and GPL licenses:
|
||||
|
||||
http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
http://www.gnu.org/licenses/gpl.html
|
||||
=========================================
|
||||
END OF pretty-data NOTICES AND INFORMATION
|
||||
|
||||
%% primeng NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -46,12 +46,12 @@ steps:
|
||||
- script: |
|
||||
DISPLAY=:10 ./scripts/test.sh --reporter mocha-junit-reporter
|
||||
displayName: 'Tests'
|
||||
condition: eq(variables['Agent.OS'], 'Linux')
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- script: |
|
||||
DISPLAY=:10 ./scripts/test.sh --reporter mocha-junit-reporter --coverage
|
||||
displayName: 'Tests'
|
||||
condition: ne(variables['Agent.OS'], 'Linux')
|
||||
condition: and(succeeded(), ne(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
|
||||
@@ -92,11 +92,12 @@ const indentationFilter = [
|
||||
'!**/*.dockerfile',
|
||||
'!extensions/markdown-language-features/media/*.js',
|
||||
// {{SQL CARBON EDIT}}
|
||||
'!**/*.{xlf,docx,sql,vsix}',
|
||||
'!**/*.{xlf,docx,sql,vsix,bacpac}',
|
||||
'!extensions/mssql/sqltoolsservice/**',
|
||||
'!extensions/import/flatfileimportservice/**',
|
||||
'!extensions/admin-tool-ext-win/ssmsmin/**',
|
||||
'!extensions/resource-deployment/notebooks/**'
|
||||
'!extensions/resource-deployment/notebooks/**',
|
||||
'!extensions/mssql/notebooks/**'
|
||||
];
|
||||
|
||||
const copyrightFilter = [
|
||||
@@ -156,7 +157,8 @@ const copyrightFilter = [
|
||||
'!extensions/notebook/resources/jupyter_config/**',
|
||||
'!**/*.gif',
|
||||
'!**/*.xlf',
|
||||
'!**/*.dacpac'
|
||||
'!**/*.dacpac',
|
||||
'!**/*.bacpac'
|
||||
];
|
||||
|
||||
const eslintFilter = [
|
||||
|
||||
@@ -526,10 +526,14 @@ gulp.task('vscode-translations-pull', function () {
|
||||
|
||||
gulp.task('vscode-translations-import', function () {
|
||||
// {{SQL CARBON EDIT}} - Replace function body with our own
|
||||
[...i18n.defaultLanguages, ...i18n.extraLanguages].forEach(language => {
|
||||
gulp.src(`../vscode-localization/${language.id}/build/*/*.xlf`)
|
||||
.pipe(i18n.prepareI18nFiles())
|
||||
.pipe(vfs.dest(`./i18n/${language.folderName}`));
|
||||
return new Promise(function(resolve) {
|
||||
[...i18n.defaultLanguages, ...i18n.extraLanguages].forEach(language => {
|
||||
let languageId = language.translationId ? language.translationId : language.id;
|
||||
gulp.src(`resources/xlf/${languageId}/**/*.xlf`)
|
||||
.pipe(i18n.prepareI18nFiles())
|
||||
.pipe(vfs.dest(`./i18n/${language.folderName}`));
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
});
|
||||
|
||||
@@ -376,7 +376,7 @@ export function packageExtensionsStream(optsIn?: IPackageExtensionsOptions): Nod
|
||||
];
|
||||
|
||||
const localExtensionDependencies = () => gulp.src(extensionDepsSrc, { base: '.', dot: true })
|
||||
.pipe(filter(['**', '!**/package-lock.json']))
|
||||
.pipe(filter(['**', '!**/package-lock.json']));
|
||||
|
||||
// Original code commented out here
|
||||
// const localExtensionDependencies = () => gulp.src('extensions/node_modules/**', { base: '.' });
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
# Database Admin Tool Extensions for Windows
|
||||
# Database Admin Tool Extensions for Windows *(preview)*
|
||||
|
||||
This adds Windows-specific functionality into Azure Data Studio.
|
||||
The Database Admin Tool Extensions for Windows adds Windows-specific functionality into Azure Data Studio. Currently this
|
||||
functionality includes the ability to launch a set of SQL Server Management Studio experiences directly from Azure Data Studio.
|
||||
|
||||
These experiences include:
|
||||
|
||||
* SSMS Property dialogs for select object types, such as Databases, Views, Stored Procedures and more
|
||||
* The [Generate Scripts Wizard](https://docs.microsoft.com/en-us/sql/ssms/scripting/generate-and-publish-scripts-wizard)
|
||||
|
||||
### How do I launch these experiences?
|
||||
|
||||
Both of these are available as menu items on the context menu for nodes in the Object Explorer tree. Right click on a node that supports one of the experiences and select the appropriate item.
|
||||
|
||||
**Properties** for the property dialogs of supported object types
|
||||
|
||||

|
||||
|
||||
**Generate Scripts...** for the Generate Scripts Wizard (only available on Database nodes)
|
||||
|
||||

|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
## Privacy Statement
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
Distributable Code for Microsoft Visual Studio 2017 and Microsoft Visual Studio 2017 SDK (Includes Utilities & BuildServer Files)
|
||||
|
||||
For the latest version of this Redist file, please visit http://go.microsoft.com/fwlink/?LinkId=823098
|
||||
@@ -10,7 +10,7 @@
|
||||
"aiKey": "AIF-5574968e-856d-40d2-af67-c89a14e76412",
|
||||
"engines": {
|
||||
"vscode": "^1.30.1",
|
||||
"azdata": "*"
|
||||
"azdata": ">=1.8.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"*"
|
||||
@@ -70,10 +70,15 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-extension-telemetry": "^0.0.15",
|
||||
"ads-extension-telemetry": "github:Charles-Gagnon/ads-extension-telemetry#0.1.0",
|
||||
"vscode-nls": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vscode": "1.0.1"
|
||||
},
|
||||
"__metadata": {
|
||||
"id": "41",
|
||||
"publisherDisplayName": "Microsoft",
|
||||
"publisherId": "Microsoft"
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import * as nls from 'vscode-nls';
|
||||
import * as path from 'path';
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { Telemetry } from './telemetry';
|
||||
import { TelemetryReporter, TelemetryViews } from './telemetry';
|
||||
import { doubleEscapeSingleQuotes, backEscapeDoubleQuotes, getTelemetryErrorType } from './utils';
|
||||
import { ChildProcess, exec } from 'child_process';
|
||||
const localize = nls.loadMessageBundle();
|
||||
@@ -95,7 +95,7 @@ function registerCommands(context: vscode.ExtensionContext): void {
|
||||
*/
|
||||
async function handleLaunchSsmsMinPropertiesDialogCommand(connectionContext?: azdata.ObjectExplorerContext): Promise<void> {
|
||||
if (!connectionContext) {
|
||||
Telemetry.sendTelemetryEventForError('NoConnectionContext', { action: 'Properties' });
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.SsmsMinProperties, 'NoConnectionContext');
|
||||
vscode.window.showErrorMessage(localize('adminToolExtWin.noConnectionContextForProp', 'No ConnectionContext provided for handleLaunchSsmsMinPropertiesDialogCommand'));
|
||||
return;
|
||||
}
|
||||
@@ -107,7 +107,7 @@ async function handleLaunchSsmsMinPropertiesDialogCommand(connectionContext?: az
|
||||
else if (connectionContext.nodeInfo) {
|
||||
nodeType = connectionContext.nodeInfo.nodeType;
|
||||
} else {
|
||||
Telemetry.sendTelemetryEventForError('NoOENode', { action: 'Properties' });
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.SsmsMinProperties, 'NoOENode');
|
||||
vscode.window.showErrorMessage(localize('adminToolExtWin.noOENode', 'Could not determine Object Explorer node from connectionContext : {0}', JSON.stringify(connectionContext)));
|
||||
return;
|
||||
}
|
||||
@@ -124,7 +124,7 @@ async function handleLaunchSsmsMinPropertiesDialogCommand(connectionContext?: az
|
||||
async function handleLaunchSsmsMinGswDialogCommand(connectionContext?: azdata.ObjectExplorerContext): Promise<void> {
|
||||
const action = 'GenerateScripts';
|
||||
if (!connectionContext) {
|
||||
Telemetry.sendTelemetryEventForError('NoConnectionContext', { action: action });
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.SsmsMinGsw, 'NoConnectionContext');
|
||||
vscode.window.showErrorMessage(localize('adminToolExtWin.noConnectionContextForGsw', 'No ConnectionContext provided for handleLaunchSsmsMinPropertiesDialogCommand'));
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ async function handleLaunchSsmsMinGswDialogCommand(connectionContext?: azdata.Ob
|
||||
*/
|
||||
async function launchSsmsDialog(action: string, connectionContext: azdata.ObjectExplorerContext): Promise<void> {
|
||||
if (!connectionContext.connectionProfile) {
|
||||
Telemetry.sendTelemetryEventForError('NoConnectionProfile', { action: action });
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.SsmsMinDialog, 'NoConnectionProfile');
|
||||
vscode.window.showErrorMessage(localize('adminToolExtWin.noConnectionProfile', 'No connectionProfile provided from connectionContext : {0}', JSON.stringify(connectionContext)));
|
||||
return;
|
||||
}
|
||||
@@ -155,7 +155,7 @@ async function launchSsmsDialog(action: string, connectionContext: azdata.Object
|
||||
oeNode = await azdata.objectexplorer.getNode(connectionContext.connectionProfile.id, connectionContext.nodeInfo.nodePath);
|
||||
}
|
||||
else {
|
||||
Telemetry.sendTelemetryEventForError('NoOENode', { action: action });
|
||||
TelemetryReporter.sendErrorEvent(TelemetryViews.SsmsMinDialog, 'NoOENode');
|
||||
vscode.window.showErrorMessage(localize('adminToolExtWin.noOENode', 'Could not determine Object Explorer node from connectionContext : {0}', JSON.stringify(connectionContext)));
|
||||
return;
|
||||
}
|
||||
@@ -178,13 +178,15 @@ async function launchSsmsDialog(action: string, connectionContext: azdata.Object
|
||||
};
|
||||
|
||||
const args = buildSsmsMinCommandArgs(params);
|
||||
|
||||
Telemetry.sendTelemetryEvent('LaunchSsmsDialog',
|
||||
{
|
||||
action: action,
|
||||
nodeType: oeNode ? oeNode.nodeType : 'Server',
|
||||
authType: connectionContext.connectionProfile.authenticationType
|
||||
});
|
||||
TelemetryReporter.createActionEvent(
|
||||
TelemetryViews.SsmsMinDialog,
|
||||
'LaunchSsmsDialog',
|
||||
'',
|
||||
action).withAdditionalProperties(
|
||||
{
|
||||
nodeType: oeNode ? oeNode.nodeType : 'Server'
|
||||
}).withConnectionInfo(connectionContext.connectionProfile)
|
||||
.send();
|
||||
|
||||
vscode.window.setStatusBarMessage(localize('adminToolExtWin.launchingDialogStatus', 'Launching dialog...'), 3000);
|
||||
|
||||
@@ -196,11 +198,14 @@ async function launchSsmsDialog(action: string, connectionContext: azdata.Object
|
||||
// Process has exited so remove from map of running processes
|
||||
runningProcesses.delete(proc.pid);
|
||||
const err = stderr.toString();
|
||||
Telemetry.sendTelemetryEvent('LaunchSsmsDialogResult', {
|
||||
action: params.action,
|
||||
returnCode: execException && execException.code ? execException.code.toString() : '0',
|
||||
errorType: getTelemetryErrorType(err)
|
||||
});
|
||||
if ((execException && execException.code !== 0) || err !== '') {
|
||||
TelemetryReporter.sendErrorEvent(
|
||||
TelemetryViews.SsmsMinDialog,
|
||||
'LaunchSsmsDialogError',
|
||||
execException ? execException.code.toString() : '',
|
||||
getTelemetryErrorType(err));
|
||||
}
|
||||
|
||||
if (err !== '') {
|
||||
vscode.window.showErrorMessage(localize(
|
||||
'adminToolExtWin.ssmsMinError',
|
||||
|
||||
@@ -4,99 +4,23 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import * as vscode from 'vscode';
|
||||
import TelemetryReporter from 'vscode-extension-telemetry';
|
||||
import AdsTelemetryReporter from 'ads-extension-telemetry';
|
||||
|
||||
import * as Utils from './utils';
|
||||
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
export interface ITelemetryEventProperties {
|
||||
[key: string]: string;
|
||||
let packageInfo = Utils.getPackageInfo(packageJson);
|
||||
|
||||
export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
|
||||
|
||||
export enum TelemetryViews {
|
||||
SsmsMinProperties = 'SsmsMinProperties',
|
||||
SsmsMinGsw = 'SsmsMinGsw',
|
||||
SsmsMinDialog = 'SsmsMinDialog'
|
||||
}
|
||||
|
||||
export interface ITelemetryEventMeasures {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters error paths to only include source files. Exported to support testing
|
||||
*/
|
||||
export function filterErrorPath(line: string): string {
|
||||
if (line) {
|
||||
let values: string[] = line.split('/out/');
|
||||
if (values.length <= 1) {
|
||||
// Didn't match expected format
|
||||
return line;
|
||||
} else {
|
||||
return values[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Telemetry {
|
||||
private static reporter: TelemetryReporter;
|
||||
private static disabled: boolean;
|
||||
|
||||
/**
|
||||
* Disable telemetry reporting
|
||||
*/
|
||||
public static disable(): void {
|
||||
this.disabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the telemetry reporter for use.
|
||||
*/
|
||||
public static initialize(): void {
|
||||
if (typeof this.reporter === 'undefined') {
|
||||
// Check if the user has opted out of telemetry
|
||||
if (!vscode.workspace.getConfiguration('telemetry').get<boolean>('enableTelemetry', true)) {
|
||||
this.disable();
|
||||
return;
|
||||
}
|
||||
|
||||
let packageInfo = Utils.getPackageInfo(packageJson);
|
||||
this.reporter = new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a telemetry event for a general error
|
||||
* @param err The error to log
|
||||
*/
|
||||
public static sendTelemetryEventForError(err: string, properties?: ITelemetryEventProperties): void {
|
||||
this.sendTelemetryEvent('Error', { error: err, ...properties });
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a telemetry event using application insights
|
||||
*/
|
||||
public static sendTelemetryEvent(
|
||||
eventName: string,
|
||||
properties?: ITelemetryEventProperties,
|
||||
measures?: ITelemetryEventMeasures): void {
|
||||
|
||||
if (typeof this.disabled === 'undefined') {
|
||||
this.disabled = false;
|
||||
}
|
||||
|
||||
if (this.disabled || typeof (this.reporter) === 'undefined') {
|
||||
// Don't do anything if telemetry is disabled
|
||||
return;
|
||||
}
|
||||
|
||||
if (!properties || typeof properties === 'undefined') {
|
||||
properties = {};
|
||||
}
|
||||
|
||||
try {
|
||||
this.reporter.sendTelemetryEvent(eventName, properties, measures);
|
||||
} catch (telemetryErr) {
|
||||
// If sending telemetry event fails ignore it so it won't break the extension
|
||||
console.error('Failed to send telemetry event. error: ' + telemetryErr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Telemetry.initialize();
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"ads-extension-telemetry@github:Charles-Gagnon/ads-extension-telemetry#0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://codeload.github.com/Charles-Gagnon/ads-extension-telemetry/tar.gz/70c2fea10e9ff6e329c4c5ec0b77017ada514b6d"
|
||||
dependencies:
|
||||
vscode-extension-telemetry "0.1.1"
|
||||
|
||||
ajv@^6.5.5:
|
||||
version "6.9.2"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.2.tgz#4927adb83e7f48e5a32b45729744c71ec39c9c7b"
|
||||
@@ -49,10 +55,10 @@ ansi-wrap@0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
|
||||
integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768=
|
||||
|
||||
applicationinsights@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.1.tgz#53446b830fe8d5d619eee2a278b31d3d25030927"
|
||||
integrity sha1-U0Rrgw/o1dYZ7uKieLMdPSUDCSc=
|
||||
applicationinsights@1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5"
|
||||
integrity sha512-KzOOGdphOS/lXWMFZe5440LUdFbrLpMvh2SaRxn7BmiI550KAoSb2gIhiq6kJZ9Ir3AxRRztjhzif+e5P5IXIg==
|
||||
dependencies:
|
||||
diagnostic-channel "0.2.0"
|
||||
diagnostic-channel-publishers "0.2.1"
|
||||
@@ -2271,12 +2277,12 @@ vinyl@~2.0.1:
|
||||
remove-trailing-separator "^1.0.1"
|
||||
replace-ext "^1.0.0"
|
||||
|
||||
vscode-extension-telemetry@^0.0.15:
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.0.15.tgz#685c32f3b67e8fb85ba689c1d7f88ff90ff87856"
|
||||
integrity sha512-Yf6dL9r2x2GISI1xh22XsAaydSTQG/4aBitu8sGBwGr42n2TyOsIXGtXSDgqQBNZgYD6+P1EHqrrzetn9ekWTQ==
|
||||
vscode-extension-telemetry@0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.1.tgz#91387e06b33400c57abd48979b0e790415ae110b"
|
||||
integrity sha512-TkKKG/B/J94DP5qf6xWB4YaqlhWDg6zbbqVx7Bz//stLQNnfE9XS1xm3f6fl24c5+bnEK0/wHgMgZYKIKxPeUA==
|
||||
dependencies:
|
||||
applicationinsights "1.0.1"
|
||||
applicationinsights "1.0.8"
|
||||
|
||||
vscode-nls@^3.2.1:
|
||||
version "3.2.5"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "agent",
|
||||
"displayName": "SQL Server Agent",
|
||||
"description": "Manage and troubleshoot SQL Server Agent jobs",
|
||||
"version": "0.40.0",
|
||||
"version": "0.41.0",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as vscode from 'vscode';
|
||||
import { AgentUtils } from '../agentUtils';
|
||||
import { IAgentDialogData, AgentDialogMode } from '../interfaces';
|
||||
import { JobData } from './jobData';
|
||||
import { JobStepDialog } from '../dialogs/jobStepDialog';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -26,10 +27,10 @@ export class JobStepData implements IAgentDialogData {
|
||||
public script: string;
|
||||
public scriptName: string;
|
||||
public stepName: string;
|
||||
public subSystem: string;
|
||||
public subSystem: azdata.AgentSubSystem;
|
||||
public id: number;
|
||||
public failureAction: string;
|
||||
public successAction: string;
|
||||
public failureAction: azdata.StepCompletionAction;
|
||||
public successAction: azdata.StepCompletionAction;
|
||||
public successStepId: number;
|
||||
public failStepId: number;
|
||||
public command: string;
|
||||
@@ -101,27 +102,27 @@ export class JobStepData implements IAgentDialogData {
|
||||
stepData.jobId = jobStepInfo.jobId;
|
||||
stepData.jobName = jobStepInfo.jobName;
|
||||
stepData.script = jobStepInfo.script;
|
||||
stepData.scriptName = jobStepInfo.scriptName,
|
||||
stepData.stepName = jobStepInfo.stepName,
|
||||
stepData.subSystem = jobStepInfo.subSystem,
|
||||
stepData.id = jobStepInfo.id,
|
||||
stepData.failureAction = jobStepInfo.failureAction,
|
||||
stepData.successAction = jobStepInfo.successAction,
|
||||
stepData.failStepId = jobStepInfo.failStepId,
|
||||
stepData.successStepId = jobStepInfo.successStepId,
|
||||
stepData.command = jobStepInfo.command,
|
||||
stepData.commandExecutionSuccessCode = jobStepInfo.commandExecutionSuccessCode,
|
||||
stepData.databaseName = jobStepInfo.databaseName,
|
||||
stepData.databaseUserName = jobStepInfo.databaseUserName,
|
||||
stepData.server = jobStepInfo.server,
|
||||
stepData.outputFileName = jobStepInfo.outputFileName,
|
||||
stepData.appendToLogFile = jobStepInfo.appendToLogFile,
|
||||
stepData.appendToStepHist = jobStepInfo.appendToStepHist,
|
||||
stepData.writeLogToTable = jobStepInfo.writeLogToTable,
|
||||
stepData.appendLogToTable = jobStepInfo.appendLogToTable,
|
||||
stepData.retryAttempts = jobStepInfo.retryAttempts,
|
||||
stepData.retryInterval = jobStepInfo.retryInterval,
|
||||
stepData.proxyName = jobStepInfo.proxyName;
|
||||
stepData.scriptName = jobStepInfo.scriptName;
|
||||
stepData.stepName = jobStepInfo.stepName;
|
||||
stepData.subSystem = jobStepInfo.subSystem;
|
||||
stepData.id = jobStepInfo.id;
|
||||
stepData.failureAction = jobStepInfo.failureAction;
|
||||
stepData.successAction = jobStepInfo.successAction;
|
||||
stepData.failStepId = jobStepInfo.failStepId;
|
||||
stepData.successStepId = jobStepInfo.successStepId;
|
||||
stepData.command = jobStepInfo.command;
|
||||
stepData.commandExecutionSuccessCode = jobStepInfo.commandExecutionSuccessCode;
|
||||
stepData.databaseName = jobStepInfo.databaseName;
|
||||
stepData.databaseUserName = jobStepInfo.databaseUserName;
|
||||
stepData.server = jobStepInfo.server;
|
||||
stepData.outputFileName = jobStepInfo.outputFileName;
|
||||
stepData.appendToLogFile = jobStepInfo.appendToLogFile;
|
||||
stepData.appendToStepHist = jobStepInfo.appendToStepHist;
|
||||
stepData.writeLogToTable = jobStepInfo.writeLogToTable;
|
||||
stepData.appendLogToTable = jobStepInfo.appendLogToTable;
|
||||
stepData.retryAttempts = jobStepInfo.retryAttempts;
|
||||
stepData.retryInterval = jobStepInfo.retryInterval;
|
||||
stepData.proxyName = jobStepInfo.proxyName;
|
||||
stepData.dialogMode = AgentDialogMode.EDIT;
|
||||
stepData.viaJobDialog = true;
|
||||
return stepData;
|
||||
@@ -157,4 +158,115 @@ export class JobStepData implements IAgentDialogData {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static convertToAgentSubSystem(subSystemDisplayName: string): azdata.AgentSubSystem {
|
||||
switch (subSystemDisplayName) {
|
||||
case (JobStepDialog.TSQLScript): {
|
||||
return azdata.AgentSubSystem.TransactSql;
|
||||
}
|
||||
case (JobStepDialog.Powershell): {
|
||||
return azdata.AgentSubSystem.PowerShell;
|
||||
}
|
||||
case (JobStepDialog.CmdExec): {
|
||||
return azdata.AgentSubSystem.CmdExec;
|
||||
}
|
||||
case (JobStepDialog.ReplicationDistributor): {
|
||||
return azdata.AgentSubSystem.Distribution;
|
||||
}
|
||||
case (JobStepDialog.ReplicationMerge): {
|
||||
return azdata.AgentSubSystem.Merge;
|
||||
}
|
||||
case (JobStepDialog.ReplicationQueueReader): {
|
||||
return azdata.AgentSubSystem.QueueReader;
|
||||
}
|
||||
case (JobStepDialog.ReplicationSnapshot): {
|
||||
return azdata.AgentSubSystem.Snapshot;
|
||||
}
|
||||
case (JobStepDialog.ReplicationTransactionLogReader): {
|
||||
return azdata.AgentSubSystem.LogReader;
|
||||
}
|
||||
case (JobStepDialog.AnalysisServicesCommand): {
|
||||
return azdata.AgentSubSystem.AnalysisCommands;
|
||||
}
|
||||
case (JobStepDialog.AnalysisServicesQuery): {
|
||||
return azdata.AgentSubSystem.AnalysisQuery;
|
||||
}
|
||||
case (JobStepDialog.ServicesPackage): {
|
||||
return azdata.AgentSubSystem.Ssis;
|
||||
}
|
||||
default:
|
||||
return azdata.AgentSubSystem.TransactSql;
|
||||
}
|
||||
}
|
||||
|
||||
public static convertToSubSystemDisplayName(subSystem: azdata.AgentSubSystem): string {
|
||||
switch (subSystem) {
|
||||
case (azdata.AgentSubSystem.TransactSql): {
|
||||
return JobStepDialog.TSQLScript;
|
||||
}
|
||||
case (azdata.AgentSubSystem.PowerShell): {
|
||||
return JobStepDialog.Powershell;
|
||||
}
|
||||
case (azdata.AgentSubSystem.CmdExec): {
|
||||
return JobStepDialog.CmdExec;
|
||||
}
|
||||
case (azdata.AgentSubSystem.Distribution): {
|
||||
return JobStepDialog.ReplicationDistributor;
|
||||
}
|
||||
case (azdata.AgentSubSystem.Merge): {
|
||||
return JobStepDialog.ReplicationMerge;
|
||||
}
|
||||
case (azdata.AgentSubSystem.QueueReader): {
|
||||
return JobStepDialog.ReplicationQueueReader;
|
||||
}
|
||||
case (azdata.AgentSubSystem.Snapshot): {
|
||||
return JobStepDialog.ReplicationSnapshot;
|
||||
}
|
||||
case (azdata.AgentSubSystem.LogReader): {
|
||||
return JobStepDialog.ReplicationTransactionLogReader;
|
||||
}
|
||||
case (azdata.AgentSubSystem.AnalysisCommands): {
|
||||
return JobStepDialog.AnalysisServicesCommand;
|
||||
}
|
||||
case (azdata.AgentSubSystem.AnalysisQuery): {
|
||||
return JobStepDialog.AnalysisServicesQuery;
|
||||
}
|
||||
case (azdata.AgentSubSystem.Ssis): {
|
||||
return JobStepDialog.ServicesPackage;
|
||||
}
|
||||
default:
|
||||
return JobStepDialog.TSQLScript;
|
||||
}
|
||||
}
|
||||
|
||||
public static convertToStepCompletionAction(actionDisplayName: string): azdata.StepCompletionAction {
|
||||
switch (actionDisplayName) {
|
||||
case (JobStepDialog.NextStep): {
|
||||
return azdata.StepCompletionAction.GoToNextStep;
|
||||
}
|
||||
case (JobStepDialog.QuitJobReportingSuccess): {
|
||||
return azdata.StepCompletionAction.QuitWithSuccess;
|
||||
}
|
||||
case (JobStepDialog.QuitJobReportingFailure): {
|
||||
return azdata.StepCompletionAction.QuitWithFailure;
|
||||
}
|
||||
default:
|
||||
return azdata.StepCompletionAction.GoToNextStep;
|
||||
}
|
||||
}
|
||||
|
||||
public static convertToCompletionActionDisplayName(stepCompletionAction: azdata.StepCompletionAction): string {
|
||||
switch (stepCompletionAction) {
|
||||
case (azdata.StepCompletionAction.GoToNextStep): {
|
||||
return JobStepDialog.NextStep;
|
||||
}
|
||||
case (azdata.StepCompletionAction.QuitWithFailure): {
|
||||
return JobStepDialog.QuitJobReportingFailure;
|
||||
}
|
||||
case (azdata.StepCompletionAction.QuitWithSuccess): {
|
||||
return JobStepDialog.QuitJobReportingSuccess;
|
||||
}
|
||||
default:
|
||||
return JobStepDialog.NextStep;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,16 @@ export class PickScheduleData implements IAgentDialogData {
|
||||
this.jobName = jobName;
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
public async initialize(): Promise<azdata.AgentJobScheduleInfo[]> {
|
||||
let agentService = await AgentUtils.getAgentService();
|
||||
let result = await agentService.getJobSchedules(this.ownerUri);
|
||||
if (result && result.success) {
|
||||
this.schedules = result.schedules;
|
||||
try {
|
||||
let result = await agentService.getJobSchedules(this.ownerUri);
|
||||
if (result && result.success) {
|
||||
this.schedules = result.schedules;
|
||||
return this.schedules;
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -169,6 +169,9 @@ export class JobDialog extends AgentDialog<JobData> {
|
||||
this.nameTextBox.onTextChanged(() => {
|
||||
if (this.nameTextBox.value && this.nameTextBox.value.length > 0) {
|
||||
this.dialog.message = null;
|
||||
// Change the job name immediately since steps
|
||||
// depends on the job name
|
||||
this.model.name = this.nameTextBox.value;
|
||||
}
|
||||
});
|
||||
this.ownerTextBox = view.modelBuilder.inputBox().component();
|
||||
@@ -242,12 +245,14 @@ export class JobDialog extends AgentDialog<JobData> {
|
||||
this.moveStepUpButton = view.modelBuilder.button()
|
||||
.withProperties({
|
||||
label: this.MoveStepUpButtonString,
|
||||
title: this.MoveStepUpButtonString,
|
||||
width: 120
|
||||
}).component();
|
||||
|
||||
this.moveStepDownButton = view.modelBuilder.button()
|
||||
.withProperties({
|
||||
label: this.MoveStepDownButtonString,
|
||||
title: this.MoveStepDownButtonString,
|
||||
width: 120
|
||||
}).component();
|
||||
|
||||
@@ -256,6 +261,7 @@ export class JobDialog extends AgentDialog<JobData> {
|
||||
|
||||
this.newStepButton = view.modelBuilder.button().withProperties({
|
||||
label: this.NewStepButtonString,
|
||||
title: this.NewStepButtonString,
|
||||
width: 140
|
||||
}).component();
|
||||
|
||||
@@ -283,11 +289,13 @@ export class JobDialog extends AgentDialog<JobData> {
|
||||
|
||||
this.editStepButton = view.modelBuilder.button().withProperties({
|
||||
label: this.EditStepButtonString,
|
||||
title: this.EditStepButtonString,
|
||||
width: 140
|
||||
}).component();
|
||||
|
||||
this.deleteStepButton = view.modelBuilder.button().withProperties({
|
||||
label: this.DeleteStepButtonString,
|
||||
title: this.DeleteStepButtonString,
|
||||
width: 140
|
||||
}).component();
|
||||
|
||||
@@ -651,9 +659,9 @@ export class JobDialog extends AgentDialog<JobData> {
|
||||
let cols = [];
|
||||
cols.push(jobStep.id);
|
||||
cols.push(jobStep.stepName);
|
||||
cols.push(jobStep.subSystem);
|
||||
cols.push(jobStep.successAction);
|
||||
cols.push(jobStep.failureAction);
|
||||
cols.push(JobStepData.convertToSubSystemDisplayName(jobStep.subSystem));
|
||||
cols.push(JobStepData.convertToCompletionActionDisplayName(jobStep.successAction));
|
||||
cols.push(JobStepData.convertToCompletionActionDisplayName(jobStep.failureAction));
|
||||
result.push(cols);
|
||||
});
|
||||
return result;
|
||||
@@ -700,6 +708,11 @@ export class JobDialog extends AgentDialog<JobData> {
|
||||
this.model.jobSteps = [];
|
||||
}
|
||||
this.model.jobSteps = this.steps;
|
||||
// Change the last step's success action to quit because the
|
||||
// default is "Go To Next Step"
|
||||
if (this.model.jobSteps.length > 0) {
|
||||
this.model.jobSteps[this.model.jobSteps.length - 1].successAction = azdata.StepCompletionAction.QuitWithSuccess;
|
||||
}
|
||||
if (!this.model.jobSchedules) {
|
||||
this.model.jobSchedules = [];
|
||||
}
|
||||
|
||||
@@ -60,13 +60,23 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
private readonly AllFilesLabelString: string = localize('jobStepDialog.allFiles', 'All Files (*)');
|
||||
|
||||
// Dropdown options
|
||||
private readonly TSQLScript: string = localize('jobStepDialog.TSQL', 'Transact-SQL script (T-SQL)');
|
||||
private readonly Powershell: string = localize('jobStepDialog.powershell', 'PowerShell');
|
||||
private readonly CmdExec: string = localize('jobStepDialog.CmdExec', 'Operating system (CmdExec)');
|
||||
private readonly AgentServiceAccount: string = localize('jobStepDialog.agentServiceAccount', 'SQL Server Agent Service Account');
|
||||
private readonly NextStep: string = localize('jobStepDialog.nextStep', 'Go to the next step');
|
||||
private readonly QuitJobReportingSuccess: string = localize('jobStepDialog.quitJobSuccess', 'Quit the job reporting success');
|
||||
private readonly QuitJobReportingFailure: string = localize('jobStepDialog.quitJobFailure', 'Quit the job reporting failure');
|
||||
public static readonly TSQLScript: string = localize('jobStepDialog.TSQL', 'Transact-SQL script (T-SQL)');
|
||||
public static readonly Powershell: string = localize('jobStepDialog.powershell', 'PowerShell');
|
||||
public static readonly CmdExec: string = localize('jobStepDialog.CmdExec', 'Operating system (CmdExec)');
|
||||
public static readonly ReplicationDistributor: string = localize('jobStepDialog.replicationDistribution', 'Replication Distributor');
|
||||
public static readonly ReplicationMerge: string = localize('jobStepDialog.replicationMerge', 'Replication Merge');
|
||||
public static readonly ReplicationQueueReader: string = localize('jobStepDialog.replicationQueueReader', 'Replication Queue Reader');
|
||||
public static readonly ReplicationSnapshot: string = localize('jobStepDialog.replicationSnapshot', 'Replication Snapshot');
|
||||
public static readonly ReplicationTransactionLogReader: string = localize('jobStepDialog.replicationTransactionLogReader', 'Replication Transaction-Log Reader');
|
||||
public static readonly AnalysisServicesCommand: string = localize('jobStepDialog.analysisCommand', 'SQL Server Analysis Services Command');
|
||||
public static readonly AnalysisServicesQuery: string = localize('jobStepDialog.analysisQuery', 'SQL Server Analysis Services Query');
|
||||
public static readonly ServicesPackage: string = localize('jobStepDialog.servicesPackage', 'SQL Server Integration Service Package');
|
||||
|
||||
|
||||
public static readonly AgentServiceAccount: string = localize('jobStepDialog.agentServiceAccount', 'SQL Server Agent Service Account');
|
||||
public static readonly NextStep: string = localize('jobStepDialog.nextStep', 'Go to the next step');
|
||||
public static readonly QuitJobReportingSuccess: string = localize('jobStepDialog.quitJobSuccess', 'Quit the job reporting success');
|
||||
public static readonly QuitJobReportingFailure: string = localize('jobStepDialog.quitJobFailure', 'Quit the job reporting failure');
|
||||
|
||||
// Event Name strings
|
||||
private readonly NewStepDialog = 'NewStepDialogOpened';
|
||||
@@ -147,12 +157,14 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
this.openButton = view.modelBuilder.button()
|
||||
.withProperties({
|
||||
label: this.OpenCommandText,
|
||||
title: this.OpenCommandText,
|
||||
width: '80px',
|
||||
isFile: true
|
||||
}).component();
|
||||
this.parseButton = view.modelBuilder.button()
|
||||
.withProperties({
|
||||
label: this.ParseCommandText,
|
||||
title: this.ParseCommandText,
|
||||
width: '80px',
|
||||
isFile: false
|
||||
}).component();
|
||||
@@ -176,7 +188,9 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
height: 300,
|
||||
width: 400,
|
||||
multiline: true,
|
||||
inputType: 'text'
|
||||
inputType: 'text',
|
||||
ariaLabel: this.CommandLabelString,
|
||||
placeHolder: this.CommandLabelString
|
||||
})
|
||||
.component();
|
||||
}
|
||||
@@ -185,6 +199,8 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
this.generalTab.registerContent(async (view) => {
|
||||
this.nameTextBox = view.modelBuilder.inputBox()
|
||||
.withProperties({
|
||||
ariaLabel: this.StepNameLabelString,
|
||||
placeHolder: this.StepNameLabelString
|
||||
}).component();
|
||||
this.nameTextBox.required = true;
|
||||
this.nameTextBox.onTextChanged(() => {
|
||||
@@ -195,8 +211,8 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
|
||||
this.typeDropdown = view.modelBuilder.dropDown()
|
||||
.withProperties({
|
||||
value: this.TSQLScript,
|
||||
values: [this.TSQLScript, this.CmdExec, this.Powershell]
|
||||
value: JobStepDialog.TSQLScript,
|
||||
values: [JobStepDialog.TSQLScript, JobStepDialog.CmdExec, JobStepDialog.Powershell]
|
||||
})
|
||||
.component();
|
||||
this.runAsDropdown = view.modelBuilder.dropDown()
|
||||
@@ -214,6 +230,8 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
|
||||
this.processExitCodeBox = view.modelBuilder.inputBox()
|
||||
.withProperties({
|
||||
ariaLabel: this.ProcessExitCodeText,
|
||||
placeHolder: this.ProcessExitCodeText
|
||||
}).component();
|
||||
this.processExitCodeBox.enabled = false;
|
||||
|
||||
@@ -246,7 +264,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
}).component();
|
||||
this.typeDropdown.onValueChanged((type) => {
|
||||
switch (type.selected) {
|
||||
case (this.TSQLScript):
|
||||
case (JobStepDialog.TSQLScript):
|
||||
this.runAsDropdown.value = '';
|
||||
this.runAsDropdown.values = [''];
|
||||
this.runAsDropdown.enabled = false;
|
||||
@@ -256,8 +274,8 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
this.processExitCodeBox.value = '';
|
||||
this.processExitCodeBox.enabled = false;
|
||||
break;
|
||||
case (this.Powershell):
|
||||
this.runAsDropdown.value = this.AgentServiceAccount;
|
||||
case (JobStepDialog.Powershell):
|
||||
this.runAsDropdown.value = JobStepDialog.AgentServiceAccount;
|
||||
this.runAsDropdown.values = [this.runAsDropdown.value];
|
||||
this.runAsDropdown.enabled = true;
|
||||
this.databaseDropdown.enabled = false;
|
||||
@@ -266,11 +284,11 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
this.processExitCodeBox.value = '';
|
||||
this.processExitCodeBox.enabled = false;
|
||||
break;
|
||||
case (this.CmdExec):
|
||||
case (JobStepDialog.CmdExec):
|
||||
this.databaseDropdown.enabled = false;
|
||||
this.databaseDropdown.values = [''];
|
||||
this.databaseDropdown.value = '';
|
||||
this.runAsDropdown.value = this.AgentServiceAccount;
|
||||
this.runAsDropdown.value = JobStepDialog.AgentServiceAccount;
|
||||
this.runAsDropdown.values = [this.runAsDropdown.value];
|
||||
this.runAsDropdown.enabled = true;
|
||||
this.processExitCodeBox.enabled = true;
|
||||
@@ -286,7 +304,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
// Load values for edit scenario
|
||||
if (this.isEdit) {
|
||||
this.nameTextBox.value = this.model.stepName;
|
||||
this.typeDropdown.value = this.model.subSystem;
|
||||
this.typeDropdown.value = JobStepData.convertToSubSystemDisplayName(this.model.subSystem);
|
||||
this.databaseDropdown.value = this.model.databaseName;
|
||||
this.commandTextBox.value = this.model.command;
|
||||
}
|
||||
@@ -298,16 +316,16 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
this.successActionDropdown = view.modelBuilder.dropDown()
|
||||
.withProperties({
|
||||
width: '100%',
|
||||
value: this.NextStep,
|
||||
values: [this.NextStep, this.QuitJobReportingSuccess, this.QuitJobReportingFailure]
|
||||
value: JobStepDialog.NextStep,
|
||||
values: [JobStepDialog.NextStep, JobStepDialog.QuitJobReportingSuccess, JobStepDialog.QuitJobReportingFailure]
|
||||
})
|
||||
.component();
|
||||
let retryFlexContainer = this.createRetryCounters(view);
|
||||
|
||||
this.failureActionDropdown = view.modelBuilder.dropDown()
|
||||
.withProperties({
|
||||
value: this.QuitJobReportingFailure,
|
||||
values: [this.QuitJobReportingFailure, this.NextStep, this.QuitJobReportingSuccess]
|
||||
value: JobStepDialog.QuitJobReportingFailure,
|
||||
values: [JobStepDialog.QuitJobReportingFailure, JobStepDialog.NextStep, JobStepDialog.QuitJobReportingSuccess]
|
||||
})
|
||||
.component();
|
||||
let optionsGroup = this.createTSQLOptions(view);
|
||||
@@ -343,7 +361,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
title: this.FailureActionLabel
|
||||
}, {
|
||||
component: optionsGroup,
|
||||
title: this.TSQLScript
|
||||
title: JobStepDialog.TSQLScript
|
||||
}, {
|
||||
component: logToTableContainer,
|
||||
title: ''
|
||||
@@ -365,10 +383,10 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
await view.initializeModel(formWrapper);
|
||||
|
||||
if (this.isEdit) {
|
||||
this.successActionDropdown.value = this.model.successAction;
|
||||
this.successActionDropdown.value = JobStepData.convertToCompletionActionDisplayName(this.model.successAction);
|
||||
this.retryAttemptsBox.value = this.model.retryAttempts.toString();
|
||||
this.retryIntervalBox.value = this.model.retryInterval.toString();
|
||||
this.failureActionDropdown.value = this.model.failureAction;
|
||||
this.failureActionDropdown.value = JobStepData.convertToCompletionActionDisplayName(this.model.failureAction);
|
||||
this.outputFileNameBox.value = this.model.outputFileName;
|
||||
this.appendToExistingFileCheckbox.checked = this.model.appendToLogFile;
|
||||
this.logToTableCheckbox.checked = this.model.appendLogToTable;
|
||||
@@ -527,13 +545,13 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
this.model.jobName = this.jobName;
|
||||
this.model.id = this.stepId;
|
||||
this.model.server = this.server;
|
||||
this.model.subSystem = this.typeDropdown.value as string;
|
||||
this.model.subSystem = JobStepData.convertToAgentSubSystem(this.typeDropdown.value as string);
|
||||
this.model.databaseName = this.databaseDropdown.value as string;
|
||||
this.model.script = this.commandTextBox.value;
|
||||
this.model.successAction = this.successActionDropdown.value as string;
|
||||
this.model.successAction = JobStepData.convertToStepCompletionAction(this.successActionDropdown.value as string);
|
||||
this.model.retryAttempts = this.retryAttemptsBox.value ? +this.retryAttemptsBox.value : 0;
|
||||
this.model.retryInterval = +this.retryIntervalBox.value ? +this.retryIntervalBox.value : 0;
|
||||
this.model.failureAction = this.failureActionDropdown.value as string;
|
||||
this.model.failureAction = JobStepData.convertToStepCompletionAction(this.failureActionDropdown.value as string);
|
||||
this.model.outputFileName = this.outputFileNameBox.value;
|
||||
this.model.appendToLogFile = this.appendToExistingFileCheckbox.checked;
|
||||
this.model.command = this.commandTextBox.value ? this.commandTextBox.value : '';
|
||||
|
||||
@@ -96,12 +96,21 @@ export class OperatorDialog extends AgentDialog<OperatorData> {
|
||||
private initializeGeneralTab() {
|
||||
this.generalTab.registerContent(async view => {
|
||||
|
||||
this.nameTextBox = view.modelBuilder.inputBox().component();
|
||||
this.nameTextBox = view.modelBuilder.inputBox().withProperties({
|
||||
ariaLabel: OperatorDialog.NameLabel,
|
||||
placeHolder: OperatorDialog.NameLabel
|
||||
}).component();
|
||||
this.nameTextBox.value = this.model.name;
|
||||
this.emailNameTextBox = view.modelBuilder.inputBox().component();
|
||||
this.emailNameTextBox = view.modelBuilder.inputBox().withProperties({
|
||||
ariaLabel: OperatorDialog.EmailNameTextLabel,
|
||||
placeHolder: OperatorDialog.EmailNameTextLabel
|
||||
}).component();
|
||||
this.emailNameTextBox.value = this.model.emailAddress;
|
||||
|
||||
this.pagerEmailNameTextBox = view.modelBuilder.inputBox().component();
|
||||
this.pagerEmailNameTextBox = view.modelBuilder.inputBox().withProperties({
|
||||
ariaLabel: OperatorDialog.PagerEmailNameTextLabel,
|
||||
placeHolder: OperatorDialog.PagerEmailNameTextLabel
|
||||
}).component();
|
||||
this.pagerEmailNameTextBox.value = this.model.pagerAddress;
|
||||
|
||||
this.enabledCheckBox = view.modelBuilder.checkBox()
|
||||
|
||||
@@ -27,6 +27,7 @@ export class PickScheduleDialog {
|
||||
// UI Components
|
||||
private dialog: azdata.window.Dialog;
|
||||
private schedulesTable: azdata.TableComponent;
|
||||
private loadingComponent: azdata.LoadingComponent;
|
||||
|
||||
private model: PickScheduleData;
|
||||
|
||||
@@ -38,7 +39,17 @@ export class PickScheduleDialog {
|
||||
}
|
||||
|
||||
public async showDialog() {
|
||||
await this.model.initialize();
|
||||
this.model.initialize().then((result) => {
|
||||
this.loadingComponent.loading = false;
|
||||
if (this.model.schedules) {
|
||||
let data: any[][] = [];
|
||||
for (let i = 0; i < this.model.schedules.length; ++i) {
|
||||
let schedule = this.model.schedules[i];
|
||||
data[i] = [schedule.id, schedule.name, schedule.description];
|
||||
}
|
||||
this.schedulesTable.data = data;
|
||||
}
|
||||
});
|
||||
this.dialog = azdata.window.createModelViewDialog(this.DialogTitle);
|
||||
this.initializeContent();
|
||||
this.dialog.okButton.onClick(async () => await this.execute());
|
||||
@@ -68,16 +79,9 @@ export class PickScheduleDialog {
|
||||
title: this.SchedulesLabelText
|
||||
}]).withLayout({ width: '100%' }).component();
|
||||
|
||||
await view.initializeModel(formModel);
|
||||
|
||||
if (this.model.schedules) {
|
||||
let data: any[][] = [];
|
||||
for (let i = 0; i < this.model.schedules.length; ++i) {
|
||||
let schedule = this.model.schedules[i];
|
||||
data[i] = [schedule.id, schedule.name, schedule.description];
|
||||
}
|
||||
this.schedulesTable.data = data;
|
||||
}
|
||||
this.loadingComponent = view.modelBuilder.loadingComponent().withItem(formModel).component();
|
||||
this.loadingComponent.loading = true;
|
||||
await view.initializeModel(this.loadingComponent);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -84,8 +84,11 @@ export class ProxyDialog extends AgentDialog<ProxyData> {
|
||||
this.generalTab.registerContent(async view => {
|
||||
|
||||
this.proxyNameTextBox = view.modelBuilder.inputBox()
|
||||
.withProperties({ width: 420 })
|
||||
.component();
|
||||
.withProperties({
|
||||
width: 420,
|
||||
ariaLabel: ProxyDialog.ProxyNameTextBoxLabel,
|
||||
placeHolder: ProxyDialog.ProxyNameTextBoxLabel
|
||||
}).component();
|
||||
|
||||
this.credentialNameDropDown = view.modelBuilder.dropDown()
|
||||
.withProperties({
|
||||
@@ -100,9 +103,10 @@ export class ProxyDialog extends AgentDialog<ProxyData> {
|
||||
.withProperties({
|
||||
width: 420,
|
||||
multiline: true,
|
||||
height: 300
|
||||
})
|
||||
.component();
|
||||
height: 300,
|
||||
ariaLabel: ProxyDialog.DescriptionTextBoxLabel,
|
||||
placeHolder: ProxyDialog.DescriptionTextBoxLabel
|
||||
}).component();
|
||||
|
||||
this.subsystemCheckBox = view.modelBuilder.checkBox()
|
||||
.withProperties({
|
||||
|
||||
@@ -22,7 +22,13 @@ const localize = nls.loadMessageBundle();
|
||||
* The main controller class that initializes the extension
|
||||
*/
|
||||
export class MainController {
|
||||
|
||||
protected _context: vscode.ExtensionContext;
|
||||
private jobDialog: JobDialog;
|
||||
private jobStepDialog: JobStepDialog;
|
||||
private alertDialog: AlertDialog;
|
||||
private operatorDialog: OperatorDialog;
|
||||
private proxyDialog: ProxyDialog;
|
||||
|
||||
// PUBLIC METHODS //////////////////////////////////////////////////////
|
||||
public constructor(context: vscode.ExtensionContext) {
|
||||
@@ -39,8 +45,12 @@ export class MainController {
|
||||
*/
|
||||
public activate(): void {
|
||||
vscode.commands.registerCommand('agent.openJobDialog', async (ownerUri: string, jobInfo: azdata.AgentJobInfo) => {
|
||||
let dialog = new JobDialog(ownerUri, jobInfo);
|
||||
dialog.dialogName ? await dialog.openDialog(dialog.dialogName) : await dialog.openDialog();
|
||||
if (!this.jobDialog || (this.jobDialog && !this.jobDialog.isOpen)) {
|
||||
this.jobDialog = new JobDialog(ownerUri, jobInfo);
|
||||
}
|
||||
if (!this.jobDialog.isOpen) {
|
||||
this.jobDialog.dialogName ? await this.jobDialog.openDialog(this.jobDialog.dialogName) : await this.jobDialog.openDialog();
|
||||
}
|
||||
});
|
||||
vscode.commands.registerCommand('agent.openNewStepDialog', (ownerUri: string, server: string, jobInfo: azdata.AgentJobInfo, jobStepInfo: azdata.AgentJobStepInfo) => {
|
||||
AgentUtils.getAgentService().then(async (agentService) => {
|
||||
@@ -53,20 +63,33 @@ export class MainController {
|
||||
let dialog = new PickScheduleDialog(ownerUri, jobName);
|
||||
await dialog.showDialog();
|
||||
});
|
||||
vscode.commands.registerCommand('agent.openAlertDialog', (ownerUri: string, jobInfo: azdata.AgentJobInfo, alertInfo: azdata.AgentAlertInfo) => {
|
||||
AgentUtils.getAgentService().then(async (agentService) => {
|
||||
let jobData: JobData = new JobData(ownerUri, jobInfo, agentService);
|
||||
let dialog = new AlertDialog(ownerUri, jobData, alertInfo, false);
|
||||
dialog.dialogName ? await dialog.openDialog(dialog.dialogName) : await dialog.openDialog();
|
||||
});
|
||||
vscode.commands.registerCommand('agent.openAlertDialog', async (ownerUri: string, jobInfo: azdata.AgentJobInfo, alertInfo: azdata.AgentAlertInfo) => {
|
||||
if (!this.alertDialog || (this.alertDialog && !this.alertDialog.isOpen)) {
|
||||
await AgentUtils.getAgentService().then(async (agentService) => {
|
||||
let jobData: JobData = new JobData(ownerUri, jobInfo, agentService);
|
||||
this.alertDialog = new AlertDialog(ownerUri, jobData, alertInfo, false);
|
||||
});
|
||||
}
|
||||
if (!this.alertDialog.isOpen) {
|
||||
this.alertDialog.dialogName ? await this.alertDialog.openDialog(this.alertDialog.dialogName) : await this.alertDialog.openDialog();
|
||||
}
|
||||
});
|
||||
vscode.commands.registerCommand('agent.openOperatorDialog', async (ownerUri: string, operatorInfo: azdata.AgentOperatorInfo) => {
|
||||
let dialog = new OperatorDialog(ownerUri, operatorInfo);
|
||||
dialog.dialogName ? await dialog.openDialog(dialog.dialogName) : await dialog.openDialog();
|
||||
if (!this.operatorDialog || (this.operatorDialog && !this.operatorDialog.isOpen)) {
|
||||
this.operatorDialog = new OperatorDialog(ownerUri, operatorInfo);
|
||||
}
|
||||
if (!this.operatorDialog.isOpen) {
|
||||
this.operatorDialog.dialogName ? await this.operatorDialog.openDialog(this.operatorDialog.dialogName) : await this.operatorDialog.openDialog();
|
||||
}
|
||||
});
|
||||
vscode.commands.registerCommand('agent.openProxyDialog', async (ownerUri: string, proxyInfo: azdata.AgentProxyInfo, credentials: azdata.CredentialInfo[]) => {
|
||||
let dialog = new ProxyDialog(ownerUri, proxyInfo, credentials);
|
||||
dialog.dialogName ? await dialog.openDialog(dialog.dialogName) : await dialog.openDialog();
|
||||
if (!this.proxyDialog || (this.proxyDialog && !this.proxyDialog.isOpen)) {
|
||||
this.proxyDialog = new ProxyDialog(ownerUri, proxyInfo, credentials);
|
||||
}
|
||||
if (!this.proxyDialog.isOpen) {
|
||||
this.proxyDialog.dialogName ? await this.proxyDialog.openDialog(this.proxyDialog.dialogName) : await this.proxyDialog.openDialog();
|
||||
}
|
||||
this.proxyDialog.dialogName ? await this.proxyDialog.openDialog(this.proxyDialog.dialogName) : await this.proxyDialog.openDialog();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 'mocha';
|
||||
import * as should from 'should';
|
||||
import { AgentSubSystem } from 'azdata';
|
||||
import { JobStepDialog } from '../../dialogs/jobStepDialog';
|
||||
import { JobStepData } from '../../data/jobStepData';
|
||||
|
||||
const subSytems: AgentSubSystem[] = [AgentSubSystem.TransactSql, AgentSubSystem.PowerShell,
|
||||
AgentSubSystem.CmdExec, AgentSubSystem.Distribution, AgentSubSystem.Merge,
|
||||
AgentSubSystem.QueueReader, AgentSubSystem.Snapshot, AgentSubSystem.LogReader, AgentSubSystem.AnalysisCommands,
|
||||
AgentSubSystem.AnalysisQuery, AgentSubSystem.Ssis];
|
||||
|
||||
const subSystemDisplayNames: string[] = [JobStepDialog.TSQLScript, JobStepDialog.Powershell,
|
||||
JobStepDialog.CmdExec, JobStepDialog.ReplicationDistributor, JobStepDialog.ReplicationMerge,
|
||||
JobStepDialog.ReplicationQueueReader, JobStepDialog.ReplicationSnapshot, JobStepDialog.ReplicationTransactionLogReader,
|
||||
JobStepDialog.AnalysisServicesCommand, JobStepDialog.AnalysisServicesQuery, JobStepDialog.ServicesPackage];
|
||||
|
||||
describe('Agent extension enum mapping sanity test', function (): void {
|
||||
it('SubSystem to Display Name Mapping test', () => {
|
||||
for (let i = 0; i < subSytems.length; i++) {
|
||||
let subSystem = subSytems[i];
|
||||
let convertedSubSystemName = JobStepData.convertToSubSystemDisplayName(subSystem);
|
||||
should.equal(convertedSubSystemName, subSystemDisplayNames[i]);
|
||||
}
|
||||
});
|
||||
|
||||
it('SubSystem Display Name to SubSystem Mapping test', () => {
|
||||
for (let i = 0; i < subSystemDisplayNames.length; i++) {
|
||||
let subSystemDisplayName = subSystemDisplayNames[i];
|
||||
let convertedSubSystem = JobStepData.convertToAgentSubSystem(subSystemDisplayName);
|
||||
should.equal(convertedSubSystem, subSytems[i]);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { AzureResource } from 'azdata';
|
||||
import { AzureResource, ExtensionNodeType } from 'azdata';
|
||||
import { TreeItem, TreeItemCollapsibleState, ExtensionContext } from 'vscode';
|
||||
import { TokenCredentials } from 'ms-rest';
|
||||
import * as nls from 'vscode-nls';
|
||||
@@ -72,7 +72,8 @@ export class AzureResourceDatabaseTreeDataProvider implements azureResource.IAzu
|
||||
saveProfile: false,
|
||||
options: {}
|
||||
},
|
||||
childProvider: 'MSSQL'
|
||||
childProvider: 'MSSQL',
|
||||
type: ExtensionNodeType.Database
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { AzureResource } from 'azdata';
|
||||
import { AzureResource, ExtensionNodeType } from 'azdata';
|
||||
import { TreeItem, TreeItemCollapsibleState, ExtensionContext } from 'vscode';
|
||||
import { TokenCredentials } from 'ms-rest';
|
||||
import * as nls from 'vscode-nls';
|
||||
@@ -72,7 +72,8 @@ export class AzureResourceDatabaseServerTreeDataProvider implements azureResourc
|
||||
saveProfile: false,
|
||||
options: {}
|
||||
},
|
||||
childProvider: 'MSSQL'
|
||||
childProvider: 'MSSQL',
|
||||
type: ExtensionNodeType.Server
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<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 32 32"><defs><style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{clip-path:url(#clip-path-2);}.cls-4{clip-path:url(#clip-path-3);}.cls-5{clip-path:url(#clip-path-4);}.cls-6{clip-path:url(#clip-path-5);}.cls-7{clip-path:url(#clip-path-6);}.cls-8{clip-path:url(#clip-path-7);}.cls-9{clip-path:url(#clip-path-8);}.cls-10{fill:#68217a;}</style><clipPath id="clip-path"><rect class="cls-1" x="39.15" y="47.45" width="15.81" height="13.48"/></clipPath><clipPath id="clip-path-2"><rect class="cls-1" x="-754.07" y="415.73" width="15.81" height="13.48"/></clipPath><clipPath id="clip-path-3"><rect class="cls-1" x="-753.07" y="415.73" width="15.81" height="13.48"/></clipPath><clipPath id="clip-path-4"><rect class="cls-1" x="-750.43" y="422" width="10.73" height="0.43"/></clipPath><clipPath id="clip-path-5"><rect class="cls-1" x="-750.13" y="420.77" width="10.14" height="0.43"/></clipPath><clipPath id="clip-path-6"><rect class="cls-1" x="-749.72" y="419.53" width="9.31" height="0.43"/></clipPath><clipPath id="clip-path-7"><rect class="cls-1" x="-749.32" y="418.29" width="8.5" height="0.43"/></clipPath><clipPath id="clip-path-8"><rect class="cls-1" x="-748.89" y="417.05" width="7.65" height="0.43"/></clipPath></defs><title>AKS</title><path class="cls-10" d="M14.66,10.18,9.79,12V4.16l4.86,1.73Z"/><path class="cls-10" d="M5.31,5.5v5.18L9,12.1V4.16Zm.38,4.8-.19-.06V6l.19-.06.19-.06.26-.06v4.67L6,10.37A1.45,1.45,0,0,1,5.7,10.3Zm1,.26L6.4,10.5V5.7l.26-.06.26-.06.26-.06v5.25l-.26-.06Zm1.15.38-.32-.13V5.38l.32-.06.32-.13.38-.13v6.08L8.13,11C8.13,11.07,7.81,10.94,7.81,10.94Z"/><path class="cls-10" d="M26.11,10.18,21.25,12V4.16l4.86,1.73Z"/><path class="cls-10" d="M16.77,5.5v5.18l3.65,1.41V4.16Zm.38,4.8L17,10.24V6l.19-.06.19-.06.26-.06v4.67l-.19-.06A1.45,1.45,0,0,1,17.15,10.3Zm1,.26-.26-.06V5.7l.26-.06.26-.06.26-.06v5.25l-.26-.06Zm1.15.38-.32-.13V5.38l.32-.06.32-.13L20,5.06v6.08L19.58,11C19.58,11.07,19.26,10.94,19.26,10.94Z"/><path class="cls-10" d="M14.66,25.92,9.79,27.78V19.9l4.86,1.73Z"/><path class="cls-10" d="M5.31,21.25v5.18L9,27.84V19.9ZM5.7,26,5.5,26V21.7l.19-.06.19-.06.19-.06v4.67l-.19-.06Zm1,.26-.26-.06v-4.8l.26-.06.26-.06.26-.06V26.5l-.26-.06C6.91,26.37,6.66,26.3,6.66,26.3Zm1.15.38-.32-.13V21.12l.32-.06.32-.13.38-.13v6.08l-.38-.13C8.13,26.82,7.81,26.69,7.81,26.69Z"/><path class="cls-10" d="M26.11,25.92l-4.86,1.86V19.9l4.86,1.73Z"/><path class="cls-10" d="M16.77,21.25v5.18l3.65,1.41V19.9Zm.38,4.8L17,26V21.7l.19-.06.19-.06.19-.06v4.67l-.19-.06Zm1,.26-.26-.06v-4.8l.26-.06.26-.06.26-.06V26.5l-.26-.06C18.37,26.37,18.11,26.3,18.11,26.3Zm1.15.38-.32-.13V21.12l.32-.06.32-.13L20,20.8v6.08l-.38-.13C19.58,26.82,19.26,26.69,19.26,26.69Z"/><path class="cls-10" d="M20.74,18,15.87,19.9V12l4.86,1.73Z"/><path class="cls-10" d="M11.39,13.31V18.5L15,19.9V12Zm.38,4.8L11.58,18V13.76l.19-.06.19-.06.19-.06v4.67L12,18.18C12,18.18,11.78,18.11,11.78,18.11Zm1,.32-.26-.06v-4.8l.26-.06.26-.06.26-.06v5.25L13,18.56C13,18.5,12.74,18.43,12.74,18.43Zm1.15.38-.32-.13V13.25l.32-.06.32-.13.38-.13V19l-.38-.13Z"/><path class="cls-10" d="M9.34,18,4.48,19.9V12l4.86,1.73Z"/><path class="cls-10" d="M0,13.31V18.5L3.65,19.9V12Zm.38,4.8L.19,18V13.76l.19-.06H.64l.19-.06V18.3l-.19-.13Zm1,.32-.26-.06v-4.8l.26-.13.26-.06.26-.06v5.25L1.6,18.5Zm1.15.38-.32-.13V13.25l.32-.06.32-.13.38-.13V19l-.38-.13A2.77,2.77,0,0,0,2.5,18.82Z"/><path class="cls-10" d="M32,18,27.14,19.9V12L32,13.76Z"/><path class="cls-10" d="M22.66,13.31V18.5L26.3,19.9V12Zm.38,4.8L22.85,18V13.76L23,13.7l.19-.06.19-.06v4.67l-.19-.06C23.3,18.18,23,18.11,23,18.11Zm1,.32-.26-.06v-4.8L24,13.5l.26-.06.26-.06v5.25l-.26-.06C24.26,18.5,24,18.43,24,18.43Zm1.15.38-.32-.13V13.25l.32-.06.32-.13.38-.13V19l-.38-.13Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -1 +0,0 @@
|
||||
<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 32 32"><defs><style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{clip-path:url(#clip-path-2);}.cls-4{clip-path:url(#clip-path-3);}.cls-5{clip-path:url(#clip-path-4);}.cls-6{clip-path:url(#clip-path-5);}.cls-7{clip-path:url(#clip-path-6);}.cls-8{clip-path:url(#clip-path-7);}.cls-9{clip-path:url(#clip-path-8);}.cls-10{fill:#326ce5;}.cls-11{fill:#fff;stroke:#fff;stroke-width:0.25px;}</style><clipPath id="clip-path"><rect class="cls-1" x="39.15" y="85.81" width="15.81" height="13.48"/></clipPath><clipPath id="clip-path-2"><rect class="cls-1" x="-754.07" y="454.09" width="15.81" height="13.48"/></clipPath><clipPath id="clip-path-3"><rect class="cls-1" x="-753.07" y="454.09" width="15.81" height="13.48"/></clipPath><clipPath id="clip-path-4"><rect class="cls-1" x="-750.43" y="460.36" width="10.73" height="0.43"/></clipPath><clipPath id="clip-path-5"><rect class="cls-1" x="-750.13" y="459.12" width="10.14" height="0.43"/></clipPath><clipPath id="clip-path-6"><rect class="cls-1" x="-749.72" y="457.88" width="9.31" height="0.43"/></clipPath><clipPath id="clip-path-7"><rect class="cls-1" x="-749.32" y="456.64" width="8.5" height="0.43"/></clipPath><clipPath id="clip-path-8"><rect class="cls-1" x="-748.89" y="455.4" width="7.65" height="0.43"/></clipPath></defs><title>Kubernetes</title><g id="layer1"><g id="g3052"><path id="path3055" class="cls-10" d="M15.89,0a2.09,2.09,0,0,0-.82.21L3.95,5.69A2.17,2.17,0,0,0,2.8,7.17L.05,19.47a2.21,2.21,0,0,0,.29,1.67l.12.17,7.7,9.87A2.11,2.11,0,0,0,9.83,32H22.17a2.11,2.11,0,0,0,1.66-.82l7.7-9.87a2.21,2.21,0,0,0,.41-1.84L29.2,7.17A2.17,2.17,0,0,0,28,5.69L16.92.22A2.09,2.09,0,0,0,15.89,0Z"/><path id="path3059" class="cls-11" d="M16,4.19a.72.72,0,0,0-.67.76V5c0,.06,0,.13,0,.18a5.89,5.89,0,0,0,.09.65A6.6,6.6,0,0,1,15.5,7a.75.75,0,0,1-.22.35l0,.29a8.46,8.46,0,0,0-5.55,2.75l-.24-.18a.51.51,0,0,1-.4,0,6.29,6.29,0,0,1-.9-.84,5.65,5.65,0,0,0-.44-.48l-.15-.12a.78.78,0,0,0-.46-.18.63.63,0,0,0-.53.24.74.74,0,0,0,.16,1h0l.14.11a5.51,5.51,0,0,0,.55.33,6.22,6.22,0,0,1,1,.72.78.78,0,0,1,.13.4l.21.2a9.1,9.1,0,0,0-1.36,6.19l-.28.08a.94.94,0,0,1-.29.3,6,6,0,0,1-1.19.2,5.41,5.41,0,0,0-.64.05l-.18,0h0a.73.73,0,0,0-.56.84.7.7,0,0,0,.86.5h0l.17,0a5.46,5.46,0,0,0,.6-.24,6.08,6.08,0,0,1,1.16-.35.72.72,0,0,1,.38.14l.29-.05a8.88,8.88,0,0,0,3.84,4.94l-.12.3a.7.7,0,0,1,.06.39,6.77,6.77,0,0,1-.6,1.12,5.76,5.76,0,0,0-.36.55l-.09.18a.74.74,0,0,0,.28,1,.7.7,0,0,0,.92-.39h0l.08-.17a5.85,5.85,0,0,0,.19-.63,4.88,4.88,0,0,1,.52-1.23.53.53,0,0,1,.29-.14l.15-.28a8.36,8.36,0,0,0,5,.37,8.45,8.45,0,0,0,1.14-.35l.14.26a.52.52,0,0,1,.34.21A6.52,6.52,0,0,1,20,26.54a5.87,5.87,0,0,0,.19.63l.08.18a.7.7,0,0,0,.92.39.74.74,0,0,0,.28-1l-.09-.18a5.74,5.74,0,0,0-.36-.55,6.47,6.47,0,0,1-.59-1.09.55.55,0,0,1,.05-.4,2.4,2.4,0,0,1-.11-.28,8.89,8.89,0,0,0,3.84-5l.28.05a.52.52,0,0,1,.37-.14,6.08,6.08,0,0,1,1.16.35,5.47,5.47,0,0,0,.6.24l.17,0h0a.7.7,0,0,0,.86-.5.73.73,0,0,0-.56-.84l-.19,0a5.41,5.41,0,0,0-.64-.05,6,6,0,0,1-1.19-.2.76.76,0,0,1-.29-.3l-.27-.08a9.13,9.13,0,0,0-.14-3.2,9,9,0,0,0-1.25-3l.24-.22a.55.55,0,0,1,.13-.39,6.22,6.22,0,0,1,1-.72,5.53,5.53,0,0,0,.55-.33l.15-.12a.74.74,0,0,0,.16-1,.69.69,0,0,0-1-.06l-.15.12a5.67,5.67,0,0,0-.44.48,6.31,6.31,0,0,1-.9.84.72.72,0,0,1-.4,0l-.25.19A8.63,8.63,0,0,0,16.73,7.7c0-.09,0-.26,0-.31A.54.54,0,0,1,16.5,7a6.62,6.62,0,0,1,.08-1.24,5.89,5.89,0,0,0,.09-.65c0-.06,0-.14,0-.2A.72.72,0,0,0,16,4.19Zm-.83,5.32-.2,3.6h0a.6.6,0,0,1-.59.58.57.57,0,0,1-.35-.12h0l-2.86-2.09a6.79,6.79,0,0,1,4-2Zm1.67,0a6.84,6.84,0,0,1,4,2L18,13.58h0a.58.58,0,0,1-.81-.11.61.61,0,0,1-.13-.35h0Zm-6.72,3.33,2.61,2.41h0a.62.62,0,0,1,.07.83.59.59,0,0,1-.3.21h0l-3.35,1A7.24,7.24,0,0,1,10.11,12.84Zm11.75,0A7.28,7.28,0,0,1,22.72,15a7.36,7.36,0,0,1,.15,2.3l-3.37-1h0a.61.61,0,0,1-.23-1h0l2.6-2.4Zm-6.4,2.6h1.07l.67.86L17,17.36l-1,.48-1-.48-.24-1.07Zm3.43,2.94H19l3.47.6a7,7,0,0,1-2.78,3.59l-1.35-3.35h0a.61.61,0,0,1,.28-.78.57.57,0,0,1,.22-.06Zm-5.82,0a.59.59,0,0,1,.56.47.62.62,0,0,1,0,.37h0l-1.33,3.32A7.07,7.07,0,0,1,9.52,19l3.44-.6h.12ZM16,19.84a.57.57,0,0,1,.27.06.59.59,0,0,1,.26.26h0l1.69,3.16a6.86,6.86,0,0,1-.68.2,6.69,6.69,0,0,1-3.76-.2l1.69-3.15h0A.59.59,0,0,1,16,19.84Z"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,94 +1,111 @@
|
||||
{
|
||||
"name": "big-data-cluster",
|
||||
"displayName": "SQL Server big data cluster",
|
||||
"description": "SQL Server big data cluster",
|
||||
"version": "0.0.1",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
|
||||
"icon": "images/sqlserver.png",
|
||||
"aiKey": "AIF-5574968e-856d-40d2-af67-c89a14e76412",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": "^1.4.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"*"
|
||||
],
|
||||
"main": "./out/main",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
||||
},
|
||||
"extensionDependencies": [
|
||||
"Microsoft.mssql"
|
||||
],
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"title": "Kubernetes configuration",
|
||||
"properties": {
|
||||
"mssql-bdc": {
|
||||
"type": "object",
|
||||
"description": "Kubernetes configuration",
|
||||
"properties": {
|
||||
"mssql-bdc.kubectl-path": {
|
||||
"type": "string",
|
||||
"description": "File path to a kubectl binary."
|
||||
},
|
||||
"mssql-bdc.kubectl-path.windows": {
|
||||
"type": "string",
|
||||
"description": "File path to a kubectl binary."
|
||||
},
|
||||
"mssql-bdc.kubectl-path.mac": {
|
||||
"type": "string",
|
||||
"description": "File path to a kubectl binary."
|
||||
},
|
||||
"mssql-bdc.kubectl-path.linux": {
|
||||
"type": "string",
|
||||
"description": "File path to a kubectl binary."
|
||||
},
|
||||
"mssql-bdc.kubeconfig": {
|
||||
"type": "string",
|
||||
"description": "File path to the kubeconfig file."
|
||||
},
|
||||
"mssql-bdc.knownKubeconfigs": {
|
||||
"type": "array",
|
||||
"description": "File paths to kubeconfig files from which you can select."
|
||||
},
|
||||
"mssql-bdc.outputFormat": {
|
||||
"enum": [
|
||||
"json",
|
||||
"yaml"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Output format for Kubernetes specs. One of 'json' or 'yaml' (default)."
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"mssql-bdc.namespace": "",
|
||||
"mssql-bdc.kubectl-path": "",
|
||||
"mssql-bdc.kubeconfig": "",
|
||||
"mssql-bdc.knownKubeconfigs": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "mssql.cluster.create",
|
||||
"title": "Create SQL Server big data cluster",
|
||||
"category": "SQL Server"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-nls": "^3.2.1",
|
||||
"download": "^6.2.5",
|
||||
"shelljs": "^0.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha-junit-reporter": "^1.17.0",
|
||||
"mocha-multi-reporters": "^1.1.7"
|
||||
}
|
||||
"name": "big-data-cluster",
|
||||
"displayName": "%text.sqlServerBigDataClusters%",
|
||||
"description": "%description%",
|
||||
"version": "0.0.1",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
|
||||
"icon": "images/sqlserver.png",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": "*"
|
||||
},
|
||||
"activationEvents": [
|
||||
"*"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
||||
},
|
||||
"main": "./out/extension",
|
||||
"contributes": {
|
||||
"dataExplorer": {
|
||||
"sqlBigDataCluster": [
|
||||
{
|
||||
"id": "sqlBigDataCluster",
|
||||
"name": "%text.sqlServerBigDataClusters%"
|
||||
}
|
||||
]
|
||||
},
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "bigDataClusters.command.addController",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.deleteController",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.refreshController",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "bigDataClusters.command.addController",
|
||||
"when": "view == sqlBigDataCluster",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"view/item/context": [
|
||||
{
|
||||
"command": "bigDataClusters.command.deleteController",
|
||||
"when": "viewItem == bigDataClusters.itemType.controllerNode",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.refreshController",
|
||||
"when": "viewItem == bigDataClusters.itemType.controllerNode",
|
||||
"group": "navigation@1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"title": "%text.sqlServerBigDataClusters%",
|
||||
"properties": {
|
||||
"clusterControllers.controllers": {
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "bigDataClusters.command.addController",
|
||||
"title": "%command.addController.title%",
|
||||
"icon": {
|
||||
"light": "resources/light/add.svg",
|
||||
"dark": "resources/dark/add_inverse.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.deleteController",
|
||||
"title": "%command.deleteController.title%",
|
||||
"when": "viewItem == bigDataClusters.itemType.controllerNode"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.refreshController",
|
||||
"title": "%command.refreshController.title%",
|
||||
"icon": {
|
||||
"light": "resources/light/refresh.svg",
|
||||
"dark": "resources/dark/refresh_inverse.svg"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"request": "^2.88.0",
|
||||
"vscode-nls": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^5.2.5",
|
||||
"@types/node": "^8.0.24",
|
||||
"mocha": "^5.2.0",
|
||||
"should": "^13.2.1",
|
||||
"typemoq": "^2.1.0",
|
||||
"vscode": "^1.1.26"
|
||||
}
|
||||
}
|
||||
|
||||
7
extensions/big-data-cluster/package.nls.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "Support for managing SQL Server Big Data Clusters",
|
||||
"text.sqlServerBigDataClusters": "SQL Server Big Data Clusters",
|
||||
"command.addController.title": "Connect to Controller",
|
||||
"command.deleteController.title" : "Delete",
|
||||
"command.refreshController.title" : "Refresh"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#252526;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>add</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M14,6v4H10v4H6V10H2V6H6V2h4V6Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M13,7V9H9v4H7V9H3V7H7V3H9V7Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 486 B |
@@ -0,0 +1 @@
|
||||
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.cls-1{opacity:0;}.cls-2{fill:#f6f6f6;}.cls-3{fill:#424242;}.cls-4{fill:#f0eff1;}</style></defs><title>centralmanagement_server_16x</title><g id="canvas" class="cls-1"><rect class="cls-2" width="16" height="16"/></g><path id="outline" class="cls-2" d="M16,0H7V1H3V5H0V16H8V13h8ZM6,4H7V5H6Z"/><g id="iconBG"><path class="cls-3" d="M8,1V12h7V1Zm6,10H13V10h1Zm0-6H9V4h5Zm0-2H9V2h5Z"/><path class="cls-3" d="M1,6v9H7V6Zm5,8H5V13H6Zm0-4H2V9H6ZM6,8H2V7H6Z"/><rect class="cls-3" x="6" y="2" width="1" height="1"/><rect class="cls-3" x="4" y="4" width="1" height="1"/><rect class="cls-3" x="4" y="2" width="1" height="1"/></g><g id="iconFG"><path class="cls-4" d="M14,3H9V2h5Zm0,1H9V5h5Zm0,6H13v1h1Z"/><path class="cls-4" d="M6,8H2V7H6ZM6,9H2v1H6Zm0,4H5v1H6Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 869 B |
@@ -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:#231f20;}.cls-2{fill:#fff;}</style></defs><title>folder_inverse_16x16</title><polygon class="cls-1" points="13.59 2.34 13.58 2.35 13.58 2.33 13.59 2.34"/><text></text><path class="cls-2" d="M16,14.13H0v-12a1,1,0,0,1,.08-.39,1,1,0,0,1,.53-.53A1,1,0,0,1,1,1.13H4.75a2.16,2.16,0,0,1,.61.07,2.26,2.26,0,0,1,.45.18,2.14,2.14,0,0,1,.36.24l.32.24a1.8,1.8,0,0,0,.34.18,1.12,1.12,0,0,0,.43.07H15a1,1,0,0,1,.39.08,1,1,0,0,1,.53.53,1,1,0,0,1,.08.39ZM1,2.13v1H4.75a1.36,1.36,0,0,0,.33,0A1,1,0,0,0,5.34,3l.23-.16.25-.21-.25-.21-.23-.16a1,1,0,0,0-.26-.1,1.36,1.36,0,0,0-.33,0Zm14,11v-10H7.25a1.12,1.12,0,0,0-.43.07,1.8,1.8,0,0,0-.34.18l-.32.24a2.14,2.14,0,0,1-.36.24,2.26,2.26,0,0,1-.45.18,2.16,2.16,0,0,1-.61.07H1v9Z"/></svg>
|
||||
|
After Width: | Height: | Size: 830 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M13.451 5.609l-.579-.939-1.068.812-.076.094c-.335.415-.927 1.341-1.124 2.876l-.021.165.033.163.071.345c0 1.654-1.346 3-3 3-.795 0-1.545-.311-2.107-.868-.563-.567-.873-1.317-.873-2.111 0-1.431 1.007-2.632 2.351-2.929v2.926s2.528-2.087 2.984-2.461h.012l3.061-2.582-4.919-4.1h-1.137v2.404c-3.429.318-6.121 3.211-6.121 6.721 0 1.809.707 3.508 1.986 4.782 1.277 1.282 2.976 1.988 4.784 1.988 3.722 0 6.75-3.028 6.75-6.75 0-1.245-.349-2.468-1.007-3.536z" fill="#2D2D30"/><path d="M12.6 6.134l-.094.071c-.269.333-.746 1.096-.91 2.375.057.277.092.495.092.545 0 2.206-1.794 4-4 4-1.098 0-2.093-.445-2.817-1.164-.718-.724-1.163-1.718-1.163-2.815 0-2.206 1.794-4 4-4l.351.025v1.85s1.626-1.342 1.631-1.339l1.869-1.577-3.5-2.917v2.218l-.371-.03c-3.176 0-5.75 2.574-5.75 5.75 0 1.593.648 3.034 1.695 4.076 1.042 1.046 2.482 1.694 4.076 1.694 3.176 0 5.75-2.574 5.75-5.75-.001-1.106-.318-2.135-.859-3.012z" fill="#C5C5C5"/></svg>
|
||||
|
After Width: | Height: | Size: 986 B |
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
viewBox="0 0 16 16"
|
||||
data-name="Layer 1"
|
||||
id="Layer_1">
|
||||
<metadata
|
||||
id="metadata17">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>sql_bigdata_cluster</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
id="style2">.cls-1{fill:#212121;}.cls-2{fill:#231f20;}</style>
|
||||
</defs>
|
||||
<title
|
||||
id="title6">sql_bigdata_cluster</title>
|
||||
<path
|
||||
style="fill:#ffffff;stroke-width:1.00282443"
|
||||
id="path8"
|
||||
d="M 7.995,0 C 5.605,0 1.575,0.45254557 1.465,2.1319925 V 13.737272 C 1.465,15.517285 5.575,16 7.995,16 c 2.42,0 6.54,-0.482715 6.54,-2.262728 V 2.1319925 C 14.435,0.45254557 10.405,0 7.995,0 Z m 5.45,13.737272 c -0.14,0.392206 -2.18,1.166562 -5.45,1.166562 -3.27,0 -5.32,-0.784412 -5.43,-1.166562 V 3.5097423 a 14.67,14.752986 0 0 0 5.43,0.8749214 14.71,14.793212 0 0 0 5.45,-0.8749214 z m 0,-11.5549967 c -0.17,0.3922062 -2.19,1.1062225 -5.45,1.1062225 -3.26,0 -5.2,-0.6939032 -5.43,-1.0861094 0.23,-0.4022627 2.22,-1.1062225 5.43,-1.1062225 3.21,0 5.27,0.7240729 5.45,1.0659963 v 0 z"
|
||||
class="cls-1" />
|
||||
<polygon
|
||||
style="fill:#ffffff"
|
||||
transform="translate(0.075)"
|
||||
id="polygon10"
|
||||
points="13.57,2.35 13.58,2.36 13.57,2.37 "
|
||||
class="cls-2" />
|
||||
<path
|
||||
style="fill:#ffffff"
|
||||
id="path12"
|
||||
d="m 9.6501562,5.2372858 c -0.1362374,0 -0.2728654,0.026375 -0.4003906,0.082031 -0.123585,0.050567 -0.2358691,0.1260731 -0.3300781,0.2207031 -0.094256,0.096634 -0.1724299,0.2082024 -0.2304688,0.3300781 -0.062701,0.1283175 -0.099426,0.2676857 -0.109375,0.4101562 -0.00186,0.1267925 0.022265,0.2517914 0.070312,0.3691407 0.045212,0.1164344 0.1088696,0.2248797 0.1894531,0.3203125 L 8.2107031,7.9384577 C 8.011051,7.8519995 7.7980699,7.8002026 7.5798437,7.7997858 7.2852043,7.7997877 7.0158159,7.8890317 6.7790625,8.0283014 L 6.3435156,7.4677545 C 6.4851678,7.2819801 6.5620085,7.0548883 6.5622656,6.8212702 6.5623837,6.2311827 6.0839937,5.7527927 5.4939062,5.7529108 4.9038187,5.7527927 4.4254288,6.2311827 4.4255469,6.8212702 4.4254288,7.4113576 4.9038188,7.8897476 5.4939062,7.8896295 5.646983,7.8892233 5.7981841,7.8559185 5.9372656,7.7919733 l 0.4628906,0.5351562 c -0.2593431,0.2844532 -0.4218723,0.6589599 -0.421875,1.0742188 1.1e-6,0.1550931 0.029186,0.301527 0.070312,0.4433594 L 5.2692969,10.19041 C 5.0668671,9.9352433 4.7590727,9.7863779 4.4333593,9.7861139 3.8432718,9.7859958 3.3648819,10.264386 3.365,10.854473 c -1.179e-4,0.590087 0.478272,1.068477 1.0683593,1.068359 0.5900874,1.18e-4 1.0684773,-0.478272 1.0683594,-1.068359 -2.425e-4,-0.05958 -0.00547,-0.119029 -0.015625,-0.177734 l 0.7675782,-0.376953 c 0.2881162,0.42403 0.7748778,0.703124 1.3261718,0.703124 0.087028,-9e-5 0.1739047,-0.0073 0.2597656,-0.02148 l 0.2011719,0.597656 c -0.2806104,0.199117 -0.4474678,0.523359 -0.4472656,0.869137 -8.57e-5,0.586839 0.4721644,1.062587 1.0546875,1.0625 0.5825231,8.7e-5 1.054773,-0.475661 1.054687,-1.0625 8.6e-5,-0.586839 -0.4721639,-1.062587 -1.054687,-1.0625 -0.043779,5.16e-4 -0.087483,0.0038 -0.1308594,0.0098 L 8.3220312,10.819317 C 8.6909643,10.625493 8.9698168,10.295494 9.099375,9.8993953 l 0.5449219,0.089844 h 0.00195 c 0.05025,0.5310507 0.4958731,0.9369327 1.0292971,0.9374997 0.571737,8.6e-5 1.035243,-0.46342 1.035156,-1.0351567 C 11.710786,9.3198482 11.247281,8.8563402 10.675544,8.8564264 10.264465,8.85697 9.8926723,9.100743 9.7282783,9.4775202 L 9.1814062,9.3798639 C 9.1740509,8.9410593 8.9869509,8.524497 8.6638281,8.2275202 L 9.3103125,7.2607233 c 0.1095989,0.036162 0.2244742,0.051906 0.3398437,0.048828 0.1376991,0.0043 0.2729851,-0.023148 0.3984378,-0.080078 0.126162,-0.045588 0.239468,-0.119827 0.330078,-0.21875 0.09823,-0.093286 0.176943,-0.2056351 0.230469,-0.3300781 0.05137,-0.1271794 0.07858,-0.2632358 0.08008,-0.4003907 -4.88e-4,-0.140498 -0.02772,-0.2797842 -0.08008,-0.4101562 C 10.551096,5.7482226 10.472932,5.6366542 10.378672,5.5400202 10.284463,5.44539 10.172179,5.369883 10.048594,5.3193171 9.9210683,5.2636605 9.7863933,5.2372858 9.6501562,5.2372858 Z m -0.00195,0.4746094 C 9.9659223,5.7112473 10.223947,5.9683972 10.224378,6.2861139 10.225028,6.6045936 9.9666863,6.8629356 9.6482062,6.8622858 9.3304864,6.8618548 9.0733369,6.6038302 9.0739843,6.2861139 9.0744163,5.9691601 9.3312493,5.7123255 9.6482031,5.7118952 Z m -4.1543,0.4941406 C 5.8337444,6.2059063 6.1092701,6.481432 6.1091406,6.8212702 6.1092701,7.1611084 5.8337444,7.4366342 5.4939062,7.4365045 5.1540681,7.436634 4.8785424,7.1611083 4.8786719,6.8212702 4.8785424,6.481432 5.154068,6.2059063 5.4939062,6.2060358 Z M 7.5817969,8.3700983 A 1.0403689,1.0403689 0 0 1 8.6228125,9.4111139 1.0403689,1.0403689 0 0 1 7.5817969,10.450176 1.0403689,1.0403689 0 0 1 6.5427343,9.4111139 1.0403689,1.0403689 0 0 1 7.5817969,8.3700983 Z m 3.0585941,0.9277344 h 0.002 c 0.01432,-5.13e-4 0.02865,-5.13e-4 0.04297,0 0.331066,2.151e-4 0.599395,0.2685422 0.59961,0.5996096 -2.16e-4,0.3310657 -0.268544,0.5993937 -0.59961,0.5996087 -0.331828,8.64e-4 -0.601347,-0.26778 -0.601562,-0.5996087 -7.66e-4,-0.3150021 0.242463,-0.5768467 0.556641,-0.5996096 z M 4.4216406,10.260723 c 0.3398381,-1.3e-4 0.6153637,0.275396 0.6152344,0.615234 1.299e-4,0.339838 -0.2753959,0.615365 -0.6152344,0.615235 -0.3398385,1.3e-4 -0.6153643,-0.275397 -0.6152344,-0.615235 -1.293e-4,-0.339838 0.2753963,-0.615364 0.6152344,-0.615234 z m 4.2382813,1.589844 c 0.3452152,-8.4e-5 0.6250885,0.272792 0.625,0.609375 8.81e-5,0.336583 -0.2797848,0.609459 -0.625,0.609375 -0.3452157,8.4e-5 -0.6250889,-0.272792 -0.625,-0.609375 -8.86e-5,-0.336583 0.2797844,-0.609459 0.625,-0.609375 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
1
extensions/big-data-cluster/resources/light/add.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>add</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M14,6v4H10v4H6V10H2V6H6V2h4V6Z"/></g><g id="iconBg"><path class="icon-vs-bg" d="M13,7V9H9v4H7V9H3V7H7V3H9V7Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 486 B |
@@ -0,0 +1 @@
|
||||
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.cls-1{opacity:0;}.cls-2{fill:#f6f6f6;}.cls-3{fill:#424242;}.cls-4{fill:#f0eff1;}</style></defs><title>centralmanagement_server_16x</title><g id="canvas" class="cls-1"><rect class="cls-2" width="16" height="16"/></g><path id="outline" class="cls-2" d="M16,0H7V1H3V5H0V16H8V13h8ZM6,4H7V5H6Z"/><g id="iconBG"><path class="cls-3" d="M8,1V12h7V1Zm6,10H13V10h1Zm0-6H9V4h5Zm0-2H9V2h5Z"/><path class="cls-3" d="M1,6v9H7V6Zm5,8H5V13H6Zm0-4H2V9H6ZM6,8H2V7H6Z"/><rect class="cls-3" x="6" y="2" width="1" height="1"/><rect class="cls-3" x="4" y="4" width="1" height="1"/><rect class="cls-3" x="4" y="2" width="1" height="1"/></g><g id="iconFG"><path class="cls-4" d="M14,3H9V2h5Zm0,1H9V5h5Zm0,6H13v1h1Z"/><path class="cls-4" d="M6,8H2V7H6ZM6,9H2v1H6Zm0,4H5v1H6Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 869 B |
1
extensions/big-data-cluster/resources/light/folder.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{fill:#F6F6F6;} .icon-vs-fg{fill:#F0EFF1;} .icon-folder{fill:#DCB67A;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 2.5v10c0 .827-.673 1.5-1.5 1.5h-11.996c-.827 0-1.5-.673-1.5-1.5v-8c0-.827.673-1.5 1.5-1.5h2.886l1-2h8.11c.827 0 1.5.673 1.5 1.5z" id="outline"/><path class="icon-folder" d="M14.5 2h-7.492l-1 2h-3.504c-.277 0-.5.224-.5.5v8c0 .276.223.5.5.5h11.996c.275 0 .5-.224.5-.5v-10c0-.276-.225-.5-.5-.5zm-.496 2h-6.496l.5-1h5.996v1z" id="iconBg"/><path class="icon-vs-fg" d="M14 3v1h-6.5l.5-1h6z" id="iconFg"/></svg>
|
||||
|
After Width: | Height: | Size: 740 B |
1
extensions/big-data-cluster/resources/light/refresh.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M13.451 5.609l-.579-.939-1.068.812-.076.094c-.335.415-.927 1.341-1.124 2.876l-.021.165.033.163.071.345c0 1.654-1.346 3-3 3-.795 0-1.545-.311-2.107-.868-.563-.567-.873-1.317-.873-2.111 0-1.431 1.007-2.632 2.351-2.929v2.926s2.528-2.087 2.984-2.461h.012l3.061-2.582-4.919-4.1h-1.137v2.404c-3.429.318-6.121 3.211-6.121 6.721 0 1.809.707 3.508 1.986 4.782 1.277 1.282 2.976 1.988 4.784 1.988 3.722 0 6.75-3.028 6.75-6.75 0-1.245-.349-2.468-1.007-3.536z" fill="#F6F6F6"/><path d="M12.6 6.134l-.094.071c-.269.333-.746 1.096-.91 2.375.057.277.092.495.092.545 0 2.206-1.794 4-4 4-1.098 0-2.093-.445-2.817-1.164-.718-.724-1.163-1.718-1.163-2.815 0-2.206 1.794-4 4-4l.351.025v1.85s1.626-1.342 1.631-1.339l1.869-1.577-3.5-2.917v2.218l-.371-.03c-3.176 0-5.75 2.574-5.75 5.75 0 1.593.648 3.034 1.695 4.076 1.042 1.046 2.482 1.694 4.076 1.694 3.176 0 5.75-2.574 5.75-5.75-.001-1.106-.318-2.135-.859-3.012z" fill="#424242"/></svg>
|
||||
|
After Width: | Height: | Size: 986 B |
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
viewBox="0 0 16 16"
|
||||
data-name="Layer 1"
|
||||
id="Layer_1">
|
||||
<metadata
|
||||
id="metadata17">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>sql_bigdata_cluster</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
id="style2">.cls-1{fill:#212121;}.cls-2{fill:#231f20;}</style>
|
||||
</defs>
|
||||
<title
|
||||
id="title6">sql_bigdata_cluster</title>
|
||||
<path
|
||||
style="fill:#212121;stroke-width:1.00282443"
|
||||
id="path8"
|
||||
d="M 7.995,0 C 5.605,0 1.575,0.45254557 1.465,2.1319925 V 13.737272 C 1.465,15.517285 5.575,16 7.995,16 c 2.42,0 6.54,-0.482715 6.54,-2.262728 V 2.1319925 C 14.435,0.45254557 10.405,0 7.995,0 Z m 5.45,13.737272 c -0.14,0.392206 -2.18,1.166562 -5.45,1.166562 -3.27,0 -5.32,-0.784412 -5.43,-1.166562 V 3.5097423 a 14.67,14.752986 0 0 0 5.43,0.8749214 14.71,14.793212 0 0 0 5.45,-0.8749214 z m 0,-11.5549967 c -0.17,0.3922062 -2.19,1.1062225 -5.45,1.1062225 -3.26,0 -5.2,-0.6939032 -5.43,-1.0861094 0.23,-0.4022627 2.22,-1.1062225 5.43,-1.1062225 3.21,0 5.27,0.7240729 5.45,1.0659963 v 0 z"
|
||||
class="cls-1" />
|
||||
<polygon
|
||||
style="fill:#231f20"
|
||||
transform="translate(0.075)"
|
||||
id="polygon10"
|
||||
points="13.57,2.35 13.58,2.36 13.57,2.37 "
|
||||
class="cls-2" />
|
||||
<path
|
||||
id="path12"
|
||||
d="m 9.6501562,5.2372858 c -0.1362374,0 -0.2728654,0.026375 -0.4003906,0.082031 -0.123585,0.050567 -0.2358691,0.1260731 -0.3300781,0.2207031 -0.094256,0.096634 -0.1724299,0.2082024 -0.2304688,0.3300781 -0.062701,0.1283175 -0.099426,0.2676857 -0.109375,0.4101562 -0.00186,0.1267925 0.022265,0.2517914 0.070312,0.3691407 0.045212,0.1164344 0.1088696,0.2248797 0.1894531,0.3203125 L 8.2107031,7.9384577 C 8.011051,7.8519995 7.7980699,7.8002026 7.5798437,7.7997858 7.2852043,7.7997877 7.0158159,7.8890317 6.7790625,8.0283014 L 6.3435156,7.4677545 C 6.4851678,7.2819801 6.5620085,7.0548883 6.5622656,6.8212702 6.5623837,6.2311827 6.0839937,5.7527927 5.4939062,5.7529108 4.9038187,5.7527927 4.4254288,6.2311827 4.4255469,6.8212702 4.4254288,7.4113576 4.9038188,7.8897476 5.4939062,7.8896295 5.646983,7.8892233 5.7981841,7.8559185 5.9372656,7.7919733 l 0.4628906,0.5351562 c -0.2593431,0.2844532 -0.4218723,0.6589599 -0.421875,1.0742188 1.1e-6,0.1550931 0.029186,0.301527 0.070312,0.4433594 L 5.2692969,10.19041 C 5.0668671,9.9352433 4.7590727,9.7863779 4.4333593,9.7861139 3.8432718,9.7859958 3.3648819,10.264386 3.365,10.854473 c -1.179e-4,0.590087 0.478272,1.068477 1.0683593,1.068359 0.5900874,1.18e-4 1.0684773,-0.478272 1.0683594,-1.068359 -2.425e-4,-0.05958 -0.00547,-0.119029 -0.015625,-0.177734 l 0.7675782,-0.376953 c 0.2881162,0.42403 0.7748778,0.703124 1.3261718,0.703124 0.087028,-9e-5 0.1739047,-0.0073 0.2597656,-0.02148 l 0.2011719,0.597656 c -0.2806104,0.199117 -0.4474678,0.523359 -0.4472656,0.869137 -8.57e-5,0.586839 0.4721644,1.062587 1.0546875,1.0625 0.5825231,8.7e-5 1.054773,-0.475661 1.054687,-1.0625 8.6e-5,-0.586839 -0.4721639,-1.062587 -1.054687,-1.0625 -0.043779,5.16e-4 -0.087483,0.0038 -0.1308594,0.0098 L 8.3220312,10.819317 C 8.6909643,10.625493 8.9698168,10.295494 9.099375,9.8993953 l 0.5449219,0.089844 h 0.00195 c 0.05025,0.5310507 0.4958731,0.9369327 1.0292971,0.9374997 0.571737,8.6e-5 1.035243,-0.46342 1.035156,-1.0351567 C 11.710786,9.3198482 11.247281,8.8563402 10.675544,8.8564264 10.264465,8.85697 9.8926723,9.100743 9.7282783,9.4775202 L 9.1814062,9.3798639 C 9.1740509,8.9410593 8.9869509,8.524497 8.6638281,8.2275202 L 9.3103125,7.2607233 c 0.1095989,0.036162 0.2244742,0.051906 0.3398437,0.048828 0.1376991,0.0043 0.2729851,-0.023148 0.3984378,-0.080078 0.126162,-0.045588 0.239468,-0.119827 0.330078,-0.21875 0.09823,-0.093286 0.176943,-0.2056351 0.230469,-0.3300781 0.05137,-0.1271794 0.07858,-0.2632358 0.08008,-0.4003907 -4.88e-4,-0.140498 -0.02772,-0.2797842 -0.08008,-0.4101562 C 10.551096,5.7482226 10.472932,5.6366542 10.378672,5.5400202 10.284463,5.44539 10.172179,5.369883 10.048594,5.3193171 9.9210683,5.2636605 9.7863933,5.2372858 9.6501562,5.2372858 Z m -0.00195,0.4746094 C 9.9659223,5.7112473 10.223947,5.9683972 10.224378,6.2861139 10.225028,6.6045936 9.9666863,6.8629356 9.6482062,6.8622858 9.3304864,6.8618548 9.0733369,6.6038302 9.0739843,6.2861139 9.0744163,5.9691601 9.3312493,5.7123255 9.6482031,5.7118952 Z m -4.1543,0.4941406 C 5.8337444,6.2059063 6.1092701,6.481432 6.1091406,6.8212702 6.1092701,7.1611084 5.8337444,7.4366342 5.4939062,7.4365045 5.1540681,7.436634 4.8785424,7.1611083 4.8786719,6.8212702 4.8785424,6.481432 5.154068,6.2059063 5.4939062,6.2060358 Z M 7.5817969,8.3700983 A 1.0403689,1.0403689 0 0 1 8.6228125,9.4111139 1.0403689,1.0403689 0 0 1 7.5817969,10.450176 1.0403689,1.0403689 0 0 1 6.5427343,9.4111139 1.0403689,1.0403689 0 0 1 7.5817969,8.3700983 Z m 3.0585941,0.9277344 h 0.002 c 0.01432,-5.13e-4 0.02865,-5.13e-4 0.04297,0 0.331066,2.151e-4 0.599395,0.2685422 0.59961,0.5996096 -2.16e-4,0.3310657 -0.268544,0.5993937 -0.59961,0.5996087 -0.331828,8.64e-4 -0.601347,-0.26778 -0.601562,-0.5996087 -7.66e-4,-0.3150021 0.242463,-0.5768467 0.556641,-0.5996096 z M 4.4216406,10.260723 c 0.3398381,-1.3e-4 0.6153637,0.275396 0.6152344,0.615234 1.299e-4,0.339838 -0.2753959,0.615365 -0.6152344,0.615235 -0.3398385,1.3e-4 -0.6153643,-0.275397 -0.6152344,-0.615235 -1.293e-4,-0.339838 0.2753963,-0.615364 0.6152344,-0.615234 z m 4.2382813,1.589844 c 0.3452152,-8.4e-5 0.6250885,0.272792 0.625,0.609375 8.81e-5,0.336583 -0.2797848,0.609459 -0.625,0.609375 -0.3452157,8.4e-5 -0.6250889,-0.272792 -0.625,-0.609375 -8.86e-5,-0.336583 0.2797844,-0.609459 0.625,-0.609375 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
41
extensions/big-data-cluster/src/bigDataCluster/constants.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
export enum BdcItemType {
|
||||
controllerRoot = 'bigDataClusters.itemType.controllerRootNode',
|
||||
controller = 'bigDataClusters.itemType.controllerNode',
|
||||
folder = 'bigDataClusters.itemType.folderNode',
|
||||
sqlMaster = 'bigDataClusters.itemType.sqlMasterNode',
|
||||
EndPoint = 'bigDataClusters.itemType.endPointNode',
|
||||
addController = 'bigDataClusters.itemType.addControllerNode'
|
||||
}
|
||||
|
||||
export class IconPath {
|
||||
private static extensionContext: vscode.ExtensionContext;
|
||||
|
||||
public static controllerNode: { dark: string, light: string };
|
||||
public static folderNode: { dark: string, light: string };
|
||||
public static sqlMasterNode: { dark: string, light: string };
|
||||
|
||||
public static setExtensionContext(extensionContext: vscode.ExtensionContext) {
|
||||
IconPath.extensionContext = extensionContext;
|
||||
IconPath.controllerNode = {
|
||||
dark: IconPath.extensionContext.asAbsolutePath('resources/dark/bigDataCluster_controller.svg'),
|
||||
light: IconPath.extensionContext.asAbsolutePath('resources/light/bigDataCluster_controller.svg')
|
||||
};
|
||||
IconPath.folderNode = {
|
||||
dark: IconPath.extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
|
||||
light: IconPath.extensionContext.asAbsolutePath('resources/light/folder.svg')
|
||||
};
|
||||
IconPath.sqlMasterNode = {
|
||||
dark: IconPath.extensionContext.asAbsolutePath('resources/dark/sql_bigdata_cluster_inverse.svg'),
|
||||
light: IconPath.extensionContext.asAbsolutePath('resources/light/sql_bigdata_cluster.svg')
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EndpointRouterApi } from './apiGenerated';
|
||||
|
||||
export async function getEndPoints(
|
||||
url: string, username: string, password: string, ignoreSslVerification?: boolean
|
||||
): Promise<IEndPointsResponse> {
|
||||
|
||||
if (!url || !username || !password) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
url = adjustUrl(url);
|
||||
let endPointApi = new EndpointRouterApi(username, password, url);
|
||||
endPointApi.ignoreSslVerification = !!ignoreSslVerification;
|
||||
|
||||
let controllerResponse: IEndPointsResponse = undefined;
|
||||
let controllerError: IControllerError = undefined;
|
||||
let request = <IEndPointsRequest>{
|
||||
url: url,
|
||||
username: username,
|
||||
password: password,
|
||||
method: 'endPointsGet'
|
||||
};
|
||||
|
||||
try {
|
||||
let result = await endPointApi.endpointsGet();
|
||||
controllerResponse = <IEndPointsResponse>{
|
||||
response: result.response as IHttpResponse,
|
||||
endPoints: result.body as IEndPoint[],
|
||||
request
|
||||
};
|
||||
return controllerResponse;
|
||||
} catch (error) {
|
||||
if ('response' in error) {
|
||||
let err: IEndPointsResponse = error as IEndPointsResponse;
|
||||
let errCode = `${err.response.statusCode || ''}`;
|
||||
let errMessage = err.response.statusMessage;
|
||||
let errUrl = err.response.url;
|
||||
controllerError = <IControllerError>{
|
||||
address: errUrl,
|
||||
code: errCode,
|
||||
errno: errCode,
|
||||
message: errMessage,
|
||||
name: undefined
|
||||
};
|
||||
} else {
|
||||
controllerError = error as IControllerError;
|
||||
}
|
||||
throw Object.assign(controllerError, { request }) as IControllerError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes missing protocol and wrong character for port entered by user
|
||||
*/
|
||||
function adjustUrl(url: string): string {
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
url = url.trim().replace(/ /g, '').replace(/,(\d+)$/, ':$1');
|
||||
if (!url.includes('://')) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export interface IEndPointsRequest {
|
||||
url: string;
|
||||
username: string;
|
||||
password?: string;
|
||||
method?: string;
|
||||
}
|
||||
|
||||
export interface IEndPointsResponse {
|
||||
request?: IEndPointsRequest;
|
||||
response: IHttpResponse;
|
||||
endPoints: IEndPoint[];
|
||||
}
|
||||
|
||||
export interface IHttpResponse {
|
||||
method?: string;
|
||||
url?: string;
|
||||
statusCode?: number;
|
||||
statusMessage?: string;
|
||||
}
|
||||
|
||||
export interface IEndPoint {
|
||||
name?: string;
|
||||
description?: string;
|
||||
endpoint?: string;
|
||||
ip?: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export interface IControllerError extends Error {
|
||||
code?: string;
|
||||
errno?: string;
|
||||
message: string;
|
||||
request?: any;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 azdata from 'azdata';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { IEndPoint, IControllerError, getEndPoints } from '../controller/clusterControllerApi';
|
||||
import { ControllerTreeDataProvider } from '../tree/controllerTreeDataProvider';
|
||||
import { TreeNode } from '../tree/treeNode';
|
||||
import { showErrorMessage } from '../utils';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class AddControllerDialogModel {
|
||||
constructor(
|
||||
public treeDataProvider: ControllerTreeDataProvider,
|
||||
public node?: TreeNode,
|
||||
public prefilledUrl?: string,
|
||||
public prefilledUsername?: string,
|
||||
public prefilledPassword?: string,
|
||||
public prefilledRememberPassword?: boolean
|
||||
) {
|
||||
this.prefilledUrl = prefilledUrl || (node && node['url']);
|
||||
this.prefilledUsername = prefilledUsername || (node && node['username']);
|
||||
this.prefilledPassword = prefilledPassword || (node && node['password']);
|
||||
this.prefilledRememberPassword = prefilledRememberPassword || (node && node['rememberPassword']);
|
||||
}
|
||||
|
||||
public async onComplete(url: string, username: string, password: string, rememberPassword: boolean): Promise<void> {
|
||||
let response = await getEndPoints(url, username, password, true);
|
||||
if (response && response.request) {
|
||||
let masterInstance: IEndPoint = undefined;
|
||||
if (response.endPoints) {
|
||||
masterInstance = response.endPoints.find(e => e.name && e.name === 'sql-server-master');
|
||||
}
|
||||
this.treeDataProvider.addController(response.request.url, response.request.username,
|
||||
response.request.password, rememberPassword, masterInstance);
|
||||
await this.treeDataProvider.saveControllers();
|
||||
}
|
||||
}
|
||||
|
||||
public async onError(error: IControllerError): Promise<void> {
|
||||
// implement
|
||||
}
|
||||
|
||||
public async onCancel(): Promise<void> {
|
||||
if (this.node) {
|
||||
this.node.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AddControllerDialog {
|
||||
|
||||
private dialog: azdata.window.Dialog;
|
||||
private uiModelBuilder: azdata.ModelBuilder;
|
||||
|
||||
private urlInputBox: azdata.InputBoxComponent;
|
||||
private usernameInputBox: azdata.InputBoxComponent;
|
||||
private passwordInputBox: azdata.InputBoxComponent;
|
||||
private rememberPwCheckBox: azdata.CheckBoxComponent;
|
||||
|
||||
constructor(private model: AddControllerDialogModel) {
|
||||
}
|
||||
|
||||
public showDialog(): void {
|
||||
this.createDialog();
|
||||
azdata.window.openDialog(this.dialog);
|
||||
}
|
||||
|
||||
private createDialog(): void {
|
||||
this.dialog = azdata.window.createModelViewDialog(localize('textAddNewController', 'Add New Controller'));
|
||||
this.dialog.registerContent(async view => {
|
||||
this.uiModelBuilder = view.modelBuilder;
|
||||
|
||||
this.urlInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
placeHolder: localize('textUrlLower', 'url'),
|
||||
value: this.model.prefilledUrl
|
||||
}).component();
|
||||
this.usernameInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
placeHolder: localize('textUsernameLower', 'username'),
|
||||
value: this.model.prefilledUsername
|
||||
}).component();
|
||||
this.passwordInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
placeHolder: localize('textPasswordLower', 'password'),
|
||||
inputType: 'password',
|
||||
value: this.model.prefilledPassword
|
||||
})
|
||||
.component();
|
||||
this.rememberPwCheckBox = this.uiModelBuilder.checkBox()
|
||||
.withProperties<azdata.CheckBoxProperties>({
|
||||
label: localize('textRememberPassword', 'Remember Password'),
|
||||
checked: this.model.prefilledRememberPassword
|
||||
}).component();
|
||||
|
||||
let formModel = this.uiModelBuilder.formContainer()
|
||||
.withFormItems([{
|
||||
components: [{
|
||||
component: this.urlInputBox,
|
||||
title: localize('textUrlCapital', 'URL'),
|
||||
required: true
|
||||
}, {
|
||||
component: this.usernameInputBox,
|
||||
title: localize('textUsernameCapital', 'Username'),
|
||||
required: true
|
||||
}, {
|
||||
component: this.passwordInputBox,
|
||||
title: localize('textPasswordCapital', 'Password'),
|
||||
required: true
|
||||
}, {
|
||||
component: this.rememberPwCheckBox,
|
||||
title: ''
|
||||
}
|
||||
],
|
||||
title: ''
|
||||
}]).withLayout({ width: '100%' }).component();
|
||||
|
||||
await view.initializeModel(formModel);
|
||||
});
|
||||
|
||||
this.dialog.registerCloseValidator(async () => await this.validate());
|
||||
this.dialog.cancelButton.onClick(async () => await this.cancel());
|
||||
this.dialog.okButton.label = localize('textAdd', 'Add');
|
||||
this.dialog.cancelButton.label = localize('textCancel', 'Cancel');
|
||||
}
|
||||
|
||||
private async validate(): Promise<boolean> {
|
||||
let url = this.urlInputBox && this.urlInputBox.value;
|
||||
let username = this.usernameInputBox && this.usernameInputBox.value;
|
||||
let password = this.passwordInputBox && this.passwordInputBox.value;
|
||||
let rememberPassword = this.passwordInputBox && !!this.rememberPwCheckBox.checked;
|
||||
|
||||
try {
|
||||
await this.model.onComplete(url, username, password, rememberPassword);
|
||||
return true;
|
||||
} catch (error) {
|
||||
showErrorMessage(error);
|
||||
if (this.model && this.model.onError) {
|
||||
await this.model.onError(error as IControllerError);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async cancel(): Promise<void> {
|
||||
if (this.model && this.model.onCancel) {
|
||||
await this.model.onCancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { TreeNode } from './treeNode';
|
||||
import { BdcItemType } from '../constants';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class AddControllerNode extends TreeNode {
|
||||
private readonly nodeType: string;
|
||||
|
||||
constructor() {
|
||||
super(localize('textBigDataClusterControllerWithDots', 'Add Big Data Cluster Controller...'));
|
||||
this.nodeType = BdcItemType.addController;
|
||||
}
|
||||
|
||||
public async getChildren(): Promise<TreeNode[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
public getTreeItem(): vscode.TreeItem {
|
||||
let item = new vscode.TreeItem(this.label, vscode.TreeItemCollapsibleState.None);
|
||||
item.command = {
|
||||
title: localize('textConnectToController', 'Connect to Controller'),
|
||||
command: 'bigDataClusters.command.addController',
|
||||
arguments: [this]
|
||||
};
|
||||
item.contextValue = this.nodeType;
|
||||
return item;
|
||||
}
|
||||
|
||||
public getNodeInfo(): azdata.NodeInfo {
|
||||
return {
|
||||
label: this.label,
|
||||
isLeaf: this.isLeaf,
|
||||
errorMessage: undefined,
|
||||
metadata: undefined,
|
||||
nodePath: this.nodePath,
|
||||
nodeStatus: undefined,
|
||||
nodeType: this.nodeType,
|
||||
iconType: this.nodeType,
|
||||
nodeSubType: undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { TreeNode } from './treeNode';
|
||||
|
||||
export interface IControllerTreeChangeHandler {
|
||||
notifyNodeChanged(node?: TreeNode): void;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 azdata from 'azdata';
|
||||
import { TreeNode } from './treeNode';
|
||||
import { IControllerTreeChangeHandler } from './controllerTreeChangeHandler';
|
||||
import { AddControllerNode } from './addControllerTreeNode';
|
||||
import { ControllerRootNode, ControllerNode } from './controllerTreeNode';
|
||||
import { IEndPoint } from '../controller/clusterControllerApi';
|
||||
import { showErrorMessage } from '../utils';
|
||||
|
||||
const ConfigNamespace = 'clusterControllers';
|
||||
const CredentialNamespace = 'clusterControllerCredentials';
|
||||
|
||||
export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeNode>, IControllerTreeChangeHandler {
|
||||
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<TreeNode> = new vscode.EventEmitter<TreeNode>();
|
||||
public readonly onDidChangeTreeData: vscode.Event<TreeNode> = this._onDidChangeTreeData.event;
|
||||
private root: ControllerRootNode;
|
||||
private credentialProvider: azdata.CredentialProvider;
|
||||
|
||||
constructor() {
|
||||
this.root = new ControllerRootNode(this);
|
||||
this.loadSavedControllers();
|
||||
}
|
||||
|
||||
public async getChildren(element?: TreeNode): Promise<TreeNode[]> {
|
||||
if (element) {
|
||||
return element.getChildren();
|
||||
}
|
||||
|
||||
if (this.root.hasChildren) {
|
||||
return this.root.getChildren();
|
||||
} else {
|
||||
return [new AddControllerNode()];
|
||||
}
|
||||
}
|
||||
|
||||
public getTreeItem(element: TreeNode): vscode.TreeItem | Thenable<vscode.TreeItem> {
|
||||
return element.getTreeItem();
|
||||
}
|
||||
|
||||
public addController(
|
||||
url: string,
|
||||
username: string,
|
||||
password: string,
|
||||
rememberPassword: boolean,
|
||||
masterInstance?: IEndPoint
|
||||
): void {
|
||||
this.root.addControllerNode(url, username, password, rememberPassword, masterInstance);
|
||||
this.notifyNodeChanged();
|
||||
}
|
||||
|
||||
public deleteController(url: string, username: string): ControllerNode {
|
||||
let deleted = this.root.deleteControllerNode(url, username);
|
||||
if (deleted) {
|
||||
this.notifyNodeChanged();
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public notifyNodeChanged(node?: TreeNode): void {
|
||||
this._onDidChangeTreeData.fire(node);
|
||||
}
|
||||
|
||||
public async loadSavedControllers(): Promise<void> {
|
||||
let config = vscode.workspace.getConfiguration(ConfigNamespace);
|
||||
if (config && config.controllers) {
|
||||
let controllers = config.controllers;
|
||||
this.root.clearChildren();
|
||||
for (let c of controllers) {
|
||||
let password = undefined;
|
||||
if (c.rememberPassword) {
|
||||
password = await this.getPassword(c.url, c.username);
|
||||
}
|
||||
this.root.addChild(new ControllerNode(
|
||||
c.url, c.username, password, c.rememberPassword,
|
||||
undefined, this.root, this, undefined
|
||||
));
|
||||
}
|
||||
this.notifyNodeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public async saveControllers(): Promise<void> {
|
||||
let controllers = this.root.children.map(e => {
|
||||
let controller = e as ControllerNode;
|
||||
return {
|
||||
url: controller.url,
|
||||
username: controller.username,
|
||||
password: controller.password,
|
||||
rememberPassword: !!controller.rememberPassword
|
||||
};
|
||||
});
|
||||
|
||||
let controllersWithoutPassword = controllers.map(e => {
|
||||
return {
|
||||
url: e.url,
|
||||
username: e.username,
|
||||
rememberPassword: e.rememberPassword
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
await vscode.workspace.getConfiguration(ConfigNamespace).update('controllers', controllersWithoutPassword, true);
|
||||
} catch (error) {
|
||||
showErrorMessage(error);
|
||||
}
|
||||
|
||||
for (let e of controllers) {
|
||||
if (e.rememberPassword) {
|
||||
await this.savePassword(e.url, e.username, e.password);
|
||||
} else {
|
||||
await this.deletePassword(e.url, e.username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async savePassword(url: string, username: string, password: string): Promise<boolean> {
|
||||
let provider = await this.getCredentialProvider();
|
||||
let id = this.createId(url, username);
|
||||
let result = await provider.saveCredential(id, password);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async deletePassword(url: string, username: string): Promise<boolean> {
|
||||
let provider = await this.getCredentialProvider();
|
||||
let id = this.createId(url, username);
|
||||
let result = await provider.deleteCredential(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async getPassword(url: string, username: string): Promise<string> {
|
||||
let provider = await this.getCredentialProvider();
|
||||
let id = this.createId(url, username);
|
||||
let credential = await provider.readCredential(id);
|
||||
return credential ? credential.password : undefined;
|
||||
}
|
||||
|
||||
private async getCredentialProvider(): Promise<azdata.CredentialProvider> {
|
||||
if (!this.credentialProvider) {
|
||||
this.credentialProvider = await azdata.credentials.getProvider(CredentialNamespace);
|
||||
}
|
||||
return this.credentialProvider;
|
||||
}
|
||||
|
||||
private createId(url: string, username: string): string {
|
||||
return `${url}::${username}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 azdata from 'azdata';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { IControllerTreeChangeHandler } from './controllerTreeChangeHandler';
|
||||
import { TreeNode } from './treeNode';
|
||||
import { IconPath, BdcItemType } from '../constants';
|
||||
import { IEndPoint, IControllerError, getEndPoints } from '../controller/clusterControllerApi';
|
||||
import { showErrorMessage } from '../utils';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export abstract class ControllerTreeNode extends TreeNode {
|
||||
|
||||
constructor(
|
||||
label: string,
|
||||
parent: ControllerTreeNode,
|
||||
private _treeChangeHandler: IControllerTreeChangeHandler,
|
||||
private _description?: string,
|
||||
private _nodeType?: string,
|
||||
private _iconPath?: { dark: string, light: string }
|
||||
) {
|
||||
super(label, parent);
|
||||
this._description = this._description || this.label;
|
||||
}
|
||||
|
||||
public async getChildren(): Promise<ControllerTreeNode[]> {
|
||||
return this.children as ControllerTreeNode[];
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
super.refresh();
|
||||
this.treeChangeHandler.notifyNodeChanged(this);
|
||||
}
|
||||
|
||||
public getTreeItem(): vscode.TreeItem {
|
||||
let item: vscode.TreeItem = {};
|
||||
item.id = this.id;
|
||||
item.label = this.label;
|
||||
item.collapsibleState = this.isLeaf ?
|
||||
vscode.TreeItemCollapsibleState.None :
|
||||
vscode.TreeItemCollapsibleState.Collapsed;
|
||||
item.iconPath = this._iconPath;
|
||||
item.contextValue = this._nodeType;
|
||||
item.tooltip = this._description;
|
||||
item.iconPath = this._iconPath;
|
||||
return item;
|
||||
}
|
||||
|
||||
public getNodeInfo(): azdata.NodeInfo {
|
||||
return {
|
||||
label: this.label,
|
||||
isLeaf: this.isLeaf,
|
||||
errorMessage: undefined,
|
||||
metadata: undefined,
|
||||
nodePath: this.nodePath,
|
||||
nodeStatus: undefined,
|
||||
nodeType: this._nodeType,
|
||||
iconType: this._nodeType,
|
||||
nodeSubType: undefined
|
||||
};
|
||||
}
|
||||
|
||||
public get description(): string {
|
||||
return this._description;
|
||||
}
|
||||
|
||||
public set description(description: string) {
|
||||
this._description = description;
|
||||
}
|
||||
|
||||
public get nodeType(): string {
|
||||
return this._nodeType;
|
||||
}
|
||||
|
||||
public set nodeType(nodeType: string) {
|
||||
this._nodeType = nodeType;
|
||||
}
|
||||
|
||||
public set iconPath(iconPath: { dark: string, light: string }) {
|
||||
this._iconPath = iconPath;
|
||||
}
|
||||
|
||||
public get iconPath(): { dark: string, light: string } {
|
||||
return this._iconPath;
|
||||
}
|
||||
|
||||
public set treeChangeHandler(treeChangeHandler: IControllerTreeChangeHandler) {
|
||||
this._treeChangeHandler = treeChangeHandler;
|
||||
}
|
||||
|
||||
public get treeChangeHandler(): IControllerTreeChangeHandler {
|
||||
return this._treeChangeHandler;
|
||||
}
|
||||
}
|
||||
|
||||
export class ControllerRootNode extends ControllerTreeNode {
|
||||
|
||||
private _masterNodeFactory: SqlMasterNodeFactory;
|
||||
|
||||
constructor(treeChangeHandler: IControllerTreeChangeHandler) {
|
||||
super('root', undefined, treeChangeHandler, undefined, BdcItemType.controllerRoot);
|
||||
this._masterNodeFactory = new SqlMasterNodeFactory();
|
||||
}
|
||||
|
||||
public async getChildren(): Promise<ControllerNode[]> {
|
||||
return this.children as ControllerNode[];
|
||||
}
|
||||
|
||||
public addControllerNode(url: string, username: string, password: string, rememberPassword: boolean, masterInstance?: IEndPoint): void {
|
||||
let controllerNode = this.getExistingControllerNode(url, username);
|
||||
if (controllerNode) {
|
||||
controllerNode.password = password;
|
||||
controllerNode.rememberPassword = rememberPassword;
|
||||
controllerNode.clearChildren();
|
||||
} else {
|
||||
controllerNode = new ControllerNode(url, username, password, rememberPassword, undefined, this, this.treeChangeHandler, undefined);
|
||||
this.addChild(controllerNode);
|
||||
}
|
||||
|
||||
if (masterInstance) {
|
||||
controllerNode.addSqlMasterNode(masterInstance.endpoint, masterInstance.description);
|
||||
}
|
||||
}
|
||||
|
||||
public deleteControllerNode(url: string, username: string): ControllerNode {
|
||||
if (!url || !username) {
|
||||
return undefined;
|
||||
}
|
||||
let nodes = this.children as ControllerNode[];
|
||||
let index = nodes.findIndex(e => e.url === url && e.username === username);
|
||||
let deleted = undefined;
|
||||
if (index >= 0) {
|
||||
deleted = nodes.splice(index, 1);
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
private getExistingControllerNode(url: string, username: string): ControllerNode {
|
||||
if (!url || !username) {
|
||||
return undefined;
|
||||
}
|
||||
let nodes = this.children as ControllerNode[];
|
||||
return nodes.find(e => e.url === url && e.username === username);
|
||||
}
|
||||
|
||||
public get sqlMasterNodeFactory(): SqlMasterNodeFactory {
|
||||
return this._masterNodeFactory;
|
||||
}
|
||||
}
|
||||
|
||||
export class ControllerNode extends ControllerTreeNode {
|
||||
|
||||
constructor(
|
||||
private _url: string,
|
||||
private _username: string,
|
||||
private _password: string,
|
||||
private _rememberPassword: boolean,
|
||||
label: string,
|
||||
parent: ControllerTreeNode,
|
||||
treeChangeHandler: IControllerTreeChangeHandler,
|
||||
description?: string,
|
||||
) {
|
||||
super(label, parent, treeChangeHandler, description, BdcItemType.controller, IconPath.controllerNode);
|
||||
this.label = label;
|
||||
this.description = description;
|
||||
|
||||
}
|
||||
|
||||
public async getChildren(): Promise<ControllerTreeNode[]> {
|
||||
if (this.children && this.children.length > 0) {
|
||||
this.clearChildren();
|
||||
}
|
||||
|
||||
if (!this._password) {
|
||||
vscode.commands.executeCommand('bigDataClusters.command.addController', this);
|
||||
return this.children as ControllerTreeNode[];
|
||||
}
|
||||
|
||||
try {
|
||||
let response = await getEndPoints(this._url, this._username, this._password, true);
|
||||
if (response && response.endPoints) {
|
||||
let master = response.endPoints.find(e => e.name && e.name === 'sql-server-master');
|
||||
this.addSqlMasterNode(master.endpoint, master.description);
|
||||
}
|
||||
return this.children as ControllerTreeNode[];
|
||||
} catch (error) {
|
||||
showErrorMessage(error);
|
||||
return this.children as ControllerTreeNode[];
|
||||
}
|
||||
}
|
||||
|
||||
private static toIpAndPort(url: string): string {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
return url.trim().replace(/ /g, '').replace(/^.+\:\/\//, '').replace(/:(\d+)$/, ',$1');
|
||||
}
|
||||
|
||||
public addSqlMasterNode(endPointAddress: string, description: string): void {
|
||||
let epFolder = this.getEndPointFolderNode();
|
||||
let node = (this.root as ControllerRootNode).sqlMasterNodeFactory
|
||||
.getSqlMasterNode(endPointAddress, epFolder, undefined, this.treeChangeHandler, description);
|
||||
epFolder.addChild(node);
|
||||
}
|
||||
|
||||
private getEndPointFolderNode(): FolderNode {
|
||||
let label = localize('textSqlServers', 'SQL Servers');
|
||||
let epFolderNode = this.children.find(e => e instanceof FolderNode && e.label === label);
|
||||
if (!epFolderNode) {
|
||||
epFolderNode = new FolderNode(label, this, this.treeChangeHandler);
|
||||
this.addChild(epFolderNode);
|
||||
}
|
||||
return epFolderNode as FolderNode;
|
||||
}
|
||||
|
||||
public getTreeItem(): vscode.TreeItem {
|
||||
let item: vscode.TreeItem = super.getTreeItem();
|
||||
item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
|
||||
return item;
|
||||
}
|
||||
|
||||
public get url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
public set url(url: string) {
|
||||
this._url = url;
|
||||
}
|
||||
|
||||
public get username() {
|
||||
return this._username;
|
||||
}
|
||||
|
||||
public set username(username: string) {
|
||||
this._username = username;
|
||||
}
|
||||
|
||||
public get password() {
|
||||
return this._password;
|
||||
}
|
||||
|
||||
public set password(pw: string) {
|
||||
this._password = pw;
|
||||
}
|
||||
|
||||
public get rememberPassword() {
|
||||
return this._rememberPassword;
|
||||
}
|
||||
|
||||
public set rememberPassword(rememberPassword: boolean) {
|
||||
this._rememberPassword = rememberPassword;
|
||||
}
|
||||
|
||||
public set label(label: string) {
|
||||
super.label = label || `controller: ${ControllerNode.toIpAndPort(this._url)} (${this._username})`;
|
||||
}
|
||||
|
||||
public get label(): string {
|
||||
return super.label;
|
||||
}
|
||||
|
||||
public set description(description: string) {
|
||||
super.description = description || super.label;
|
||||
}
|
||||
|
||||
public get description(): string {
|
||||
return super.description;
|
||||
}
|
||||
}
|
||||
|
||||
export class FolderNode extends ControllerTreeNode {
|
||||
constructor(
|
||||
label: string,
|
||||
parent: ControllerTreeNode,
|
||||
treeChangeHandler: IControllerTreeChangeHandler
|
||||
) {
|
||||
super(label, parent, treeChangeHandler, label, BdcItemType.folder, IconPath.folderNode);
|
||||
}
|
||||
}
|
||||
|
||||
export class SqlMasterNode extends ControllerTreeNode {
|
||||
private static readonly _role: string = 'sql-server-master';
|
||||
private _username: string;
|
||||
private _password: string;
|
||||
|
||||
constructor(
|
||||
private _endPointAddress: string,
|
||||
parent: ControllerTreeNode,
|
||||
label: string,
|
||||
treeChangeHandler: IControllerTreeChangeHandler,
|
||||
description?: string,
|
||||
) {
|
||||
super(label, parent, treeChangeHandler, description, BdcItemType.sqlMaster, IconPath.sqlMasterNode);
|
||||
this._username = 'sa';
|
||||
this.label = label;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
private getControllerPassword(): string {
|
||||
if (!this._password) {
|
||||
let current: TreeNode = this;
|
||||
while (current && !(current instanceof ControllerNode)) {
|
||||
current = current.parent;
|
||||
}
|
||||
this._password = current && current instanceof ControllerNode ? current.password : undefined;
|
||||
}
|
||||
return this._password;
|
||||
}
|
||||
|
||||
public getTreeItem(): vscode.TreeItem {
|
||||
let item = super.getTreeItem();
|
||||
let connectionProfile: azdata.IConnectionProfile = {
|
||||
id: this.id,
|
||||
connectionName: this.id,
|
||||
serverName: this._endPointAddress,
|
||||
databaseName: '',
|
||||
userName: this._username,
|
||||
password: this.getControllerPassword(),
|
||||
authenticationType: 'SqlLogin',
|
||||
savePassword: false,
|
||||
groupFullName: '',
|
||||
groupId: '',
|
||||
providerName: 'MSSQL',
|
||||
saveProfile: false,
|
||||
options: {}
|
||||
};
|
||||
return Object.assign(item, { payload: connectionProfile, childProvider: 'MSSQL' });
|
||||
}
|
||||
|
||||
public get role() {
|
||||
return SqlMasterNode._role;
|
||||
}
|
||||
|
||||
public get endPointAddress() {
|
||||
return this._endPointAddress;
|
||||
}
|
||||
|
||||
public set endPointAddress(endPointAddress: string) {
|
||||
this._endPointAddress = endPointAddress;
|
||||
}
|
||||
|
||||
public set label(label: string) {
|
||||
super.label = label || `master: ${this._endPointAddress} (${this._username})`;
|
||||
}
|
||||
|
||||
public get label(): string {
|
||||
return super.label;
|
||||
}
|
||||
|
||||
public set description(description: string) {
|
||||
super.description = description || super.label;
|
||||
}
|
||||
|
||||
public get description(): string {
|
||||
return super.description;
|
||||
}
|
||||
}
|
||||
|
||||
export class SqlMasterNodeFactory {
|
||||
private registry: {} = {};
|
||||
|
||||
public getSqlMasterNode(
|
||||
endPointAddress: string,
|
||||
parent: ControllerTreeNode,
|
||||
label: string,
|
||||
treeChangeHandler: IControllerTreeChangeHandler,
|
||||
description?: string
|
||||
): SqlMasterNode {
|
||||
let id = this.createRegistryId(endPointAddress, 'sa');
|
||||
if (!this.registry[id]) {
|
||||
this.registry[id] = new SqlMasterNode(endPointAddress, parent, label, treeChangeHandler, description);
|
||||
} else {
|
||||
let node = this.registry[id] as SqlMasterNode;
|
||||
node.parent = parent;
|
||||
node.label = label;
|
||||
node.treeChangeHandler = treeChangeHandler;
|
||||
description = description;
|
||||
}
|
||||
return this.registry[id] as SqlMasterNode;
|
||||
}
|
||||
|
||||
private createRegistryId(endPointAddress: string, username: string): string {
|
||||
return `${endPointAddress}::${username}`;
|
||||
}
|
||||
}
|
||||
195
extensions/big-data-cluster/src/bigDataCluster/tree/treeNode.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { generateGuid } from '../utils';
|
||||
|
||||
export abstract class TreeNode {
|
||||
|
||||
private _id: string;
|
||||
private _children: TreeNode[];
|
||||
private _isLeaf: boolean;
|
||||
|
||||
constructor(private _label: string, private _parent?: TreeNode) {
|
||||
this.resetId();
|
||||
}
|
||||
|
||||
public resetId(): void {
|
||||
this._id = (this._label || '_') + `::${generateGuid()}`;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
public set label(label: string) {
|
||||
if (!this._label) {
|
||||
this._label = label;
|
||||
this.resetId();
|
||||
} else {
|
||||
this._label = label;
|
||||
}
|
||||
}
|
||||
|
||||
public get label(): string {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
public set parent(parent: TreeNode) {
|
||||
this._parent = parent;
|
||||
}
|
||||
|
||||
public get parent(): TreeNode {
|
||||
return this._parent;
|
||||
}
|
||||
|
||||
public get children(): TreeNode[] {
|
||||
if (!this._children) {
|
||||
this._children = [];
|
||||
}
|
||||
return this._children;
|
||||
}
|
||||
|
||||
public get hasChildren(): boolean {
|
||||
return this.children && this.children.length > 0;
|
||||
}
|
||||
|
||||
public set isLeaf(isLeaf: boolean) {
|
||||
this._isLeaf = isLeaf;
|
||||
}
|
||||
|
||||
public get isLeaf(): boolean {
|
||||
return this._isLeaf;
|
||||
}
|
||||
|
||||
public get root(): TreeNode {
|
||||
return TreeNode.getRoot(this);
|
||||
}
|
||||
|
||||
public equals(node: TreeNode): boolean {
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
return this.nodePath === node.nodePath;
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this.resetId();
|
||||
}
|
||||
|
||||
public static getRoot(node: TreeNode): TreeNode {
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
let current: TreeNode = node;
|
||||
while (current.parent) {
|
||||
current = current.parent;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
public get nodePath(): string {
|
||||
return TreeNode.getNodePath(this);
|
||||
}
|
||||
|
||||
public static getNodePath(node: TreeNode): string {
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let current: TreeNode = node;
|
||||
let path = current._id;
|
||||
while (current.parent) {
|
||||
current = current.parent;
|
||||
path = `${current._id}/${path}`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public async findNode(condition: (node: TreeNode) => boolean, expandIfNeeded?: boolean): Promise<TreeNode> {
|
||||
return TreeNode.findNode(this, condition, expandIfNeeded);
|
||||
}
|
||||
|
||||
public static async findNode(node: TreeNode, condition: (node: TreeNode) => boolean, expandIfNeeded?: boolean): Promise<TreeNode> {
|
||||
if (!node || !condition) {
|
||||
return undefined;
|
||||
}
|
||||
let result: TreeNode = undefined;
|
||||
let nodesToCheck: TreeNode[] = [node];
|
||||
while (nodesToCheck.length > 0) {
|
||||
let current = nodesToCheck.shift();
|
||||
if (condition(current)) {
|
||||
result = current;
|
||||
break;
|
||||
}
|
||||
if (current.hasChildren) {
|
||||
nodesToCheck = nodesToCheck.concat(current.children);
|
||||
} else if (expandIfNeeded) {
|
||||
let children = await current.getChildren();
|
||||
if (children && children.length > 0) {
|
||||
nodesToCheck = nodesToCheck.concat(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async filterNode(condition: (node: TreeNode) => boolean, expandIfNeeded?: boolean): Promise<TreeNode[]> {
|
||||
return TreeNode.filterNode(this, condition, expandIfNeeded);
|
||||
}
|
||||
|
||||
public static async filterNode(node: TreeNode, condition: (node: TreeNode) => boolean, expandIfNeeded?: boolean): Promise<TreeNode[]> {
|
||||
if (!node || !condition) {
|
||||
return undefined;
|
||||
}
|
||||
let result: TreeNode[] = [];
|
||||
let nodesToCheck: TreeNode[] = [node];
|
||||
while (nodesToCheck.length > 0) {
|
||||
let current = nodesToCheck.shift();
|
||||
if (condition(current)) {
|
||||
result.push(current);
|
||||
}
|
||||
if (current.hasChildren) {
|
||||
nodesToCheck = nodesToCheck.concat(current.children);
|
||||
} else if (expandIfNeeded) {
|
||||
let children = await current.getChildren();
|
||||
if (children && children.length > 0) {
|
||||
nodesToCheck = nodesToCheck.concat(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async findNodeByPath(path: string, expandIfNeeded?: boolean): Promise<TreeNode> {
|
||||
return TreeNode.findNodeByPath(this, path, expandIfNeeded);
|
||||
}
|
||||
|
||||
public static async findNodeByPath(node: TreeNode, path: string, expandIfNeeded?: boolean): Promise<TreeNode> {
|
||||
return TreeNode.findNode(node, node => {
|
||||
return node.nodePath && (node.nodePath === path || node.nodePath.startsWith(path));
|
||||
}, expandIfNeeded);
|
||||
}
|
||||
|
||||
public addChild(node: TreeNode): void {
|
||||
if (!this._children) {
|
||||
this._children = [];
|
||||
}
|
||||
this._children.push(node);
|
||||
}
|
||||
|
||||
public clearChildren(): void {
|
||||
if (this._children) {
|
||||
this._children = [];
|
||||
}
|
||||
}
|
||||
|
||||
public abstract async getChildren(): Promise<TreeNode[]>;
|
||||
public abstract getTreeItem(): vscode.TreeItem;
|
||||
public abstract getNodeInfo(): azdata.NodeInfo;
|
||||
}
|
||||
43
extensions/big-data-cluster/src/bigDataCluster/utils.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
export function generateGuid(): string {
|
||||
let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
|
||||
let oct: string = '';
|
||||
let tmp: number;
|
||||
for (let a: number = 0; a < 4; a++) {
|
||||
tmp = (4294967296 * Math.random()) | 0;
|
||||
oct += hexValues[tmp & 0xF] +
|
||||
hexValues[tmp >> 4 & 0xF] +
|
||||
hexValues[tmp >> 8 & 0xF] +
|
||||
hexValues[tmp >> 12 & 0xF] +
|
||||
hexValues[tmp >> 16 & 0xF] +
|
||||
hexValues[tmp >> 20 & 0xF] +
|
||||
hexValues[tmp >> 24 & 0xF] +
|
||||
hexValues[tmp >> 28 & 0xF];
|
||||
}
|
||||
let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
|
||||
return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12);
|
||||
}
|
||||
|
||||
export function showErrorMessage(error: any): void {
|
||||
if (error) {
|
||||
let text: string = undefined;
|
||||
if (typeof error === 'string') {
|
||||
text = error as string;
|
||||
} else if (typeof error === 'object' && error !== null) {
|
||||
let message = error.message;
|
||||
let code = error.code || error.errno;
|
||||
text = (code ? `${code} ` : '') + message;
|
||||
} else {
|
||||
text = `${error}`;
|
||||
}
|
||||
vscode.window.showErrorMessage(text);
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Host } from '../kubectl/host';
|
||||
import { Shell, Platform } from '../utility/shell';
|
||||
|
||||
const EXTENSION_CONFIG_KEY = 'mssql-bdc';
|
||||
const KUBECONFIG_PATH_KEY = 'mssql-bdc.kubeconfig';
|
||||
const KNOWN_KUBECONFIGS_KEY = 'mssql-bdc.knownKubeconfigs';
|
||||
|
||||
export async function addPathToConfig(configKey: string, value: string): Promise<void> {
|
||||
await setConfigValue(configKey, value);
|
||||
}
|
||||
|
||||
async function setConfigValue(configKey: string, value: any): Promise<void> {
|
||||
await atAllConfigScopes(addValueToConfigAtScope, configKey, value);
|
||||
}
|
||||
|
||||
async function addValueToConfigAtScope(configKey: string, value: any, scope: vscode.ConfigurationTarget, valueAtScope: any, createIfNotExist: boolean): Promise<void> {
|
||||
if (!createIfNotExist) {
|
||||
if (!valueAtScope || !(valueAtScope[configKey])) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let newValue: any = {};
|
||||
if (valueAtScope) {
|
||||
newValue = Object.assign({}, valueAtScope);
|
||||
}
|
||||
newValue[configKey] = value;
|
||||
await vscode.workspace.getConfiguration().update(EXTENSION_CONFIG_KEY, newValue, scope);
|
||||
}
|
||||
|
||||
async function addValueToConfigArray(configKey: string, value: string): Promise<void> {
|
||||
await atAllConfigScopes(addValueToConfigArrayAtScope, configKey, value);
|
||||
}
|
||||
|
||||
async function addValueToConfigArrayAtScope(configKey: string, value: string, scope: vscode.ConfigurationTarget, valueAtScope: any, createIfNotExist: boolean): Promise<void> {
|
||||
if (!createIfNotExist) {
|
||||
if (!valueAtScope || !(valueAtScope[configKey])) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let newValue: any = {};
|
||||
if (valueAtScope) {
|
||||
newValue = Object.assign({}, valueAtScope);
|
||||
}
|
||||
const arrayEntry: string[] = newValue[configKey] || [];
|
||||
arrayEntry.push(value);
|
||||
newValue[configKey] = arrayEntry;
|
||||
await vscode.workspace.getConfiguration().update(EXTENSION_CONFIG_KEY, newValue, scope);
|
||||
}
|
||||
|
||||
type ConfigUpdater<T> = (configKey: string, value: T, scope: vscode.ConfigurationTarget, valueAtScope: any, createIfNotExist: boolean) => Promise<void>;
|
||||
|
||||
async function atAllConfigScopes<T>(fn: ConfigUpdater<T>, configKey: string, value: T): Promise<void> {
|
||||
const config = vscode.workspace.getConfiguration().inspect(EXTENSION_CONFIG_KEY)!;
|
||||
await fn(configKey, value, vscode.ConfigurationTarget.Global, config.globalValue, true);
|
||||
await fn(configKey, value, vscode.ConfigurationTarget.Workspace, config.workspaceValue, false);
|
||||
await fn(configKey, value, vscode.ConfigurationTarget.WorkspaceFolder, config.workspaceFolderValue, false);
|
||||
}
|
||||
|
||||
// Functions for working with the list of known kubeconfigs
|
||||
|
||||
export function getKnownKubeconfigs(): string[] {
|
||||
const kkcConfig = vscode.workspace.getConfiguration(EXTENSION_CONFIG_KEY)[KNOWN_KUBECONFIGS_KEY];
|
||||
if (!kkcConfig || !kkcConfig.length) {
|
||||
return [];
|
||||
}
|
||||
return kkcConfig as string[];
|
||||
}
|
||||
|
||||
export async function addKnownKubeconfig(kubeconfigPath: string) {
|
||||
await addValueToConfigArray(KNOWN_KUBECONFIGS_KEY, kubeconfigPath);
|
||||
}
|
||||
|
||||
// Functions for working with the active kubeconfig setting
|
||||
|
||||
export async function setActiveKubeconfig(kubeconfig: string): Promise<void> {
|
||||
await addPathToConfig(KUBECONFIG_PATH_KEY, kubeconfig);
|
||||
}
|
||||
|
||||
export function getActiveKubeconfig(): string {
|
||||
return vscode.workspace.getConfiguration(EXTENSION_CONFIG_KEY)[KUBECONFIG_PATH_KEY];
|
||||
}
|
||||
|
||||
// Functions for working with tool paths
|
||||
|
||||
export function getToolPath(host: Host, shell: Shell, tool: string): string | undefined {
|
||||
const baseKey = toolPathBaseKey(tool);
|
||||
return getPathSetting(host, shell, baseKey);
|
||||
}
|
||||
|
||||
function getPathSetting(host: Host, shell: Shell, baseKey: string): string | undefined {
|
||||
const os = shell.platform();
|
||||
const osOverridePath = host.getConfiguration(EXTENSION_CONFIG_KEY)[osOverrideKey(os, baseKey)];
|
||||
return osOverridePath || host.getConfiguration(EXTENSION_CONFIG_KEY)[baseKey];
|
||||
}
|
||||
|
||||
export function toolPathBaseKey(tool: string): string {
|
||||
return `mssql-bdc.${tool}-path`;
|
||||
}
|
||||
|
||||
function osOverrideKey(os: Platform, baseKey: string): string {
|
||||
const osKey = osKeyString(os);
|
||||
return osKey ? `${baseKey}.${osKey}` : baseKey; // The 'else' clause should never happen so don't worry that this would result in double-checking a missing base key
|
||||
}
|
||||
|
||||
function osKeyString(os: Platform): string | null {
|
||||
switch (os) {
|
||||
case Platform.Windows: return 'windows';
|
||||
case Platform.MacOS: return 'mac';
|
||||
case Platform.Linux: return 'linux';
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ClusterInfo } from '../interfaces';
|
||||
|
||||
export interface IKubeConfigParser {
|
||||
parse(configPath: string): ClusterInfo[];
|
||||
}
|
||||
|
||||
export class TestKubeConfigParser implements IKubeConfigParser {
|
||||
parse(configPath: string): ClusterInfo[] {
|
||||
let clusters = [];
|
||||
for (let i = 0; i < 18; i++) {
|
||||
let name;
|
||||
if (i % 2 === 0) {
|
||||
name = `kubernetes cluster ${i}`;
|
||||
}
|
||||
else {
|
||||
name = 'cluster dev ' + i;
|
||||
}
|
||||
clusters.push(
|
||||
{
|
||||
displayName: name,
|
||||
name: `kub-dev-xxxx-cluster-${i}`,
|
||||
user: 'root'
|
||||
}
|
||||
);
|
||||
}
|
||||
return clusters;
|
||||
}
|
||||
}
|
||||
|
||||
85
extensions/big-data-cluster/src/extension.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 nls from 'vscode-nls';
|
||||
import { ControllerTreeDataProvider } from './bigDataCluster/tree/controllerTreeDataProvider';
|
||||
import { IconPath } from './bigDataCluster/constants';
|
||||
import { TreeNode } from './bigDataCluster/tree/treeNode';
|
||||
import { AddControllerDialogModel, AddControllerDialog } from './bigDataCluster/dialog/addControllerDialog';
|
||||
import { ControllerNode } from './bigDataCluster/tree/controllerTreeNode';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export function activate(extensionContext: vscode.ExtensionContext) {
|
||||
IconPath.setExtensionContext(extensionContext);
|
||||
let treeDataProvider = new ControllerTreeDataProvider();
|
||||
|
||||
registerTreeDataProvider(treeDataProvider);
|
||||
registerCommands(treeDataProvider);
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
}
|
||||
|
||||
function registerTreeDataProvider(treeDataProvider: ControllerTreeDataProvider): void {
|
||||
vscode.window.registerTreeDataProvider('sqlBigDataCluster', treeDataProvider);
|
||||
}
|
||||
|
||||
function registerCommands(treeDataProvider: ControllerTreeDataProvider): void {
|
||||
vscode.commands.registerCommand('bigDataClusters.command.addController', (node?: TreeNode) => {
|
||||
addBdcController(treeDataProvider, node);
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('bigDataClusters.command.deleteController', (node: TreeNode) => {
|
||||
deleteBdcController(treeDataProvider, node);
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('bigDataClusters.command.refreshController', (node: TreeNode) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
treeDataProvider.notifyNodeChanged(node);
|
||||
});
|
||||
}
|
||||
|
||||
function addBdcController(treeDataProvider: ControllerTreeDataProvider, node?: TreeNode): void {
|
||||
let model = new AddControllerDialogModel(treeDataProvider, node);
|
||||
let dialog = new AddControllerDialog(model);
|
||||
dialog.showDialog();
|
||||
}
|
||||
|
||||
async function deleteBdcController(treeDataProvider: ControllerTreeDataProvider, node: TreeNode): Promise<boolean> {
|
||||
if (!node && !(node instanceof ControllerNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let controllerNode = node as ControllerNode;
|
||||
|
||||
let choices: { [id: string]: boolean } = {};
|
||||
choices[localize('textYes', 'Yes')] = true;
|
||||
choices[localize('textNo', 'No')] = false;
|
||||
|
||||
let options = {
|
||||
ignoreFocusOut: false,
|
||||
placeHolder: localize('textConfirmDeleteController', 'Are you sure you want to delete \'{0}\'?', controllerNode.label)
|
||||
};
|
||||
|
||||
let result = await vscode.window.showQuickPick(Object.keys(choices), options);
|
||||
let remove: boolean = !!(result && choices[result]);
|
||||
if (remove) {
|
||||
deleteControllerInternal(treeDataProvider, controllerNode);
|
||||
}
|
||||
return remove;
|
||||
}
|
||||
|
||||
function deleteControllerInternal(treeDataProvider: ControllerTreeDataProvider, controllerNode: ControllerNode): void {
|
||||
let deleted = treeDataProvider.deleteController(controllerNode.url, controllerNode.username);
|
||||
if (deleted) {
|
||||
treeDataProvider.saveControllers();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as stream from 'stream';
|
||||
import * as tmp from 'tmp';
|
||||
|
||||
import { succeeded, Errorable } from '../interfaces';
|
||||
|
||||
type DownloadFunc =
|
||||
(url: string, destination?: string, options?: any)
|
||||
=> Promise<Buffer> & stream.Duplex; // Stream has additional events - see https://www.npmjs.com/package/download
|
||||
|
||||
let download: DownloadFunc;
|
||||
|
||||
function ensureDownloadFunc() {
|
||||
if (!download) {
|
||||
const home = process.env['HOME'];
|
||||
download = require('download');
|
||||
if (home) {
|
||||
process.env['HOME'] = home;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function toTempFile(sourceUrl: string): Promise<Errorable<string>> {
|
||||
const tempFileObj = tmp.fileSync({ prefix: 'mssql-bdc-autoinstall-' });
|
||||
const downloadResult = await to(sourceUrl, tempFileObj.name);
|
||||
if (succeeded(downloadResult)) {
|
||||
return { succeeded: true, result: tempFileObj.name };
|
||||
}
|
||||
return { succeeded: false, error: downloadResult.error };
|
||||
}
|
||||
|
||||
export async function to(sourceUrl: string, destinationFile: string): Promise<Errorable<null>> {
|
||||
ensureDownloadFunc();
|
||||
try {
|
||||
await download(sourceUrl, path.dirname(destinationFile), { filename: path.basename(destinationFile) });
|
||||
return { succeeded: true, result: null };
|
||||
} catch (e) {
|
||||
return { succeeded: false, error: [e.message] };
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 download from './download';
|
||||
import * as fs from 'fs';
|
||||
import mkdirp = require('mkdirp');
|
||||
import * as path from 'path';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { Shell, Platform } from '../utility/shell';
|
||||
import { Errorable, failed } from '../interfaces';
|
||||
import { addPathToConfig, toolPathBaseKey } from '../config/config';
|
||||
|
||||
export async function installKubectl(shell: Shell): Promise<Errorable<null>> {
|
||||
const tool = 'kubectl';
|
||||
const binFile = (shell.isUnix()) ? 'kubectl' : 'kubectl.exe';
|
||||
const os = platformUrlString(shell.platform());
|
||||
|
||||
const version = await getStableKubectlVersion();
|
||||
if (failed(version)) {
|
||||
return { succeeded: false, error: version.error };
|
||||
}
|
||||
|
||||
const installFolder = getInstallFolder(shell, tool);
|
||||
mkdirp.sync(installFolder);
|
||||
|
||||
const kubectlUrl = `https://storage.googleapis.com/kubernetes-release/release/${version.result.trim()}/bin/${os}/amd64/${binFile}`;
|
||||
const downloadFile = path.join(installFolder, binFile);
|
||||
const downloadResult = await download.to(kubectlUrl, downloadFile);
|
||||
if (failed(downloadResult)) {
|
||||
return { succeeded: false, error: [localize('downloadKubectlFailed', 'Failed to download kubectl: {0}', downloadResult.error[0])] };
|
||||
}
|
||||
|
||||
if (shell.isUnix()) {
|
||||
fs.chmodSync(downloadFile, '0777');
|
||||
}
|
||||
|
||||
await addPathToConfig(toolPathBaseKey(tool), downloadFile);
|
||||
return { succeeded: true, result: null };
|
||||
}
|
||||
|
||||
async function getStableKubectlVersion(): Promise<Errorable<string>> {
|
||||
const downloadResult = await download.toTempFile('https://storage.googleapis.com/kubernetes-release/release/stable.txt');
|
||||
if (failed(downloadResult)) {
|
||||
return { succeeded: false, error: [localize('kubectlVersionCheckFailed', 'Failed to establish kubectl stable version: {0}', downloadResult.error[0])] };
|
||||
}
|
||||
const version = fs.readFileSync(downloadResult.result, 'utf-8');
|
||||
fs.unlinkSync(downloadResult.result);
|
||||
return { succeeded: true, result: version };
|
||||
}
|
||||
|
||||
export function getInstallFolder(shell: Shell, tool: string): string {
|
||||
return path.join(shell.home(), `.mssql-bdc/tools/${tool}`);
|
||||
}
|
||||
|
||||
function platformUrlString(platform: Platform, supported?: Platform[]): string | null {
|
||||
if (supported && supported.indexOf(platform) < 0) {
|
||||
return null;
|
||||
}
|
||||
switch (platform) {
|
||||
case Platform.Windows: return 'windows';
|
||||
case Platform.MacOS: return 'darwin';
|
||||
case Platform.Linux: return 'linux';
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
export interface ClusterInfo {
|
||||
name: string;
|
||||
displayName: string;
|
||||
user: string;
|
||||
}
|
||||
|
||||
export enum TargetClusterType {
|
||||
ExistingKubernetesCluster,
|
||||
NewAksCluster
|
||||
}
|
||||
|
||||
export interface Succeeded<T> {
|
||||
readonly succeeded: true;
|
||||
readonly result: T;
|
||||
}
|
||||
|
||||
export interface Failed {
|
||||
readonly succeeded: false;
|
||||
readonly error: string[];
|
||||
}
|
||||
|
||||
export type Errorable<T> = Succeeded<T> | Failed;
|
||||
|
||||
export function succeeded<T>(e: Errorable<T>): e is Succeeded<T> {
|
||||
return e.succeeded;
|
||||
}
|
||||
|
||||
export function failed<T>(e: Errorable<T>): e is Failed {
|
||||
return !e.succeeded;
|
||||
}
|
||||
export interface ClusterPorts {
|
||||
sql: string;
|
||||
knox: string;
|
||||
controller: string;
|
||||
proxy: string;
|
||||
grafana: string;
|
||||
kibana: string;
|
||||
}
|
||||
|
||||
export interface ContainerRegistryInfo {
|
||||
registry: string;
|
||||
repository: string;
|
||||
imageTag: string;
|
||||
}
|
||||
|
||||
export interface TargetClusterTypeInfo {
|
||||
enabled: boolean;
|
||||
type: TargetClusterType;
|
||||
name: string;
|
||||
fullName: string;
|
||||
description: string;
|
||||
iconPath: {
|
||||
dark: string,
|
||||
light: string
|
||||
};
|
||||
}
|
||||
|
||||
export interface ToolInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
status: ToolInstallationStatus;
|
||||
}
|
||||
|
||||
export enum ToolInstallationStatus {
|
||||
Installed,
|
||||
NotInstalled,
|
||||
Installing,
|
||||
FailedToInstall
|
||||
}
|
||||
|
||||
export enum ClusterType {
|
||||
Unknown = 0,
|
||||
AKS,
|
||||
Minikube,
|
||||
Kubernetes,
|
||||
Other
|
||||
}
|
||||
|
||||
export interface ClusterProfile {
|
||||
name: string;
|
||||
sqlServerMasterConfiguration: SQLServerMasterConfiguration;
|
||||
computePoolConfiguration: PoolConfiguration;
|
||||
dataPoolConfiguration: PoolConfiguration;
|
||||
storagePoolConfiguration: PoolConfiguration;
|
||||
sparkPoolConfiguration: PoolConfiguration;
|
||||
}
|
||||
|
||||
export interface PoolConfiguration {
|
||||
type: ClusterPoolType;
|
||||
scale: number;
|
||||
maxScale?: number;
|
||||
hardwareLabel?: string;
|
||||
}
|
||||
|
||||
export interface SQLServerMasterConfiguration extends PoolConfiguration {
|
||||
engineOnly: boolean;
|
||||
}
|
||||
|
||||
export enum ClusterPoolType {
|
||||
SQL,
|
||||
Compute,
|
||||
Data,
|
||||
Storage,
|
||||
Spark
|
||||
}
|
||||
|
||||
export interface ClusterResourceSummary {
|
||||
hardwareLabels: HardwareLabel[];
|
||||
}
|
||||
|
||||
export interface HardwareLabel {
|
||||
name: string;
|
||||
totalNodes: number;
|
||||
totalCores: number;
|
||||
totalMemoryInGB: number;
|
||||
totalDisks: number;
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { Shell } from '../utility/shell';
|
||||
import { Host } from './host';
|
||||
import { FS } from '../utility/fs';
|
||||
|
||||
export interface BinCheckContext {
|
||||
readonly host: Host;
|
||||
readonly fs: FS;
|
||||
readonly shell: Shell;
|
||||
readonly installDependenciesCallback: () => void;
|
||||
binFound: boolean;
|
||||
binPath: string;
|
||||
}
|
||||
|
||||
interface FindBinaryResult {
|
||||
err: number | null;
|
||||
output: string;
|
||||
}
|
||||
|
||||
async function findBinary(shell: Shell, binName: string): Promise<FindBinaryResult> {
|
||||
let cmd = `which ${binName}`;
|
||||
|
||||
if (shell.isWindows()) {
|
||||
cmd = `where.exe ${binName}.exe`;
|
||||
}
|
||||
|
||||
const opts = {
|
||||
async: true,
|
||||
env: {
|
||||
HOME: process.env.HOME,
|
||||
PATH: process.env.PATH
|
||||
}
|
||||
};
|
||||
|
||||
const execResult = await shell.execCore(cmd, opts);
|
||||
if (execResult.code) {
|
||||
return { err: execResult.code, output: execResult.stderr };
|
||||
}
|
||||
|
||||
return { err: null, output: execResult.stdout };
|
||||
}
|
||||
|
||||
export function execPath(shell: Shell, basePath: string): string {
|
||||
let bin = basePath;
|
||||
if (shell.isWindows() && bin && !(bin.endsWith('.exe'))) {
|
||||
bin = bin + '.exe';
|
||||
}
|
||||
return bin;
|
||||
}
|
||||
|
||||
type CheckPresentFailureReason = 'inferFailed' | 'configuredFileMissing';
|
||||
const installDependenciesAction = localize('installDependenciesAction', 'Install dependencies');
|
||||
const learnMoreAction = localize('learnMoreAction', 'Learn more');
|
||||
function alertNoBin(host: Host, binName: string, failureReason: CheckPresentFailureReason, message: string, installDependencies: () => void): void {
|
||||
switch (failureReason) {
|
||||
case 'inferFailed':
|
||||
host.showErrorMessage(message, installDependenciesAction, learnMoreAction).then(
|
||||
(str) => {
|
||||
switch (str) {
|
||||
case learnMoreAction:
|
||||
host.showInformationMessage(localize('moreInfoMsg', 'Add {0} directory to path, or set "mssql-bdc.{0}-path" config to {0} binary.', binName));
|
||||
break;
|
||||
case installDependenciesAction:
|
||||
installDependencies();
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
break;
|
||||
case 'configuredFileMissing':
|
||||
host.showErrorMessage(message, installDependenciesAction).then(
|
||||
(str) => {
|
||||
if (str === installDependenciesAction) {
|
||||
installDependencies();
|
||||
}
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkForBinary(context: BinCheckContext, bin: string | undefined, binName: string, inferFailedMessage: string, configuredFileMissingMessage: string, alertOnFail: boolean): Promise<boolean> {
|
||||
if (!bin) {
|
||||
const fb = await findBinary(context.shell, binName);
|
||||
|
||||
if (fb.err || fb.output.length === 0) {
|
||||
if (alertOnFail) {
|
||||
alertNoBin(context.host, binName, 'inferFailed', inferFailedMessage, context.installDependenciesCallback);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
context.binFound = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (context.shell.isWindows) {
|
||||
context.binFound = context.fs.existsSync(bin);
|
||||
} else {
|
||||
const sr = await context.shell.exec(`ls ${bin}`);
|
||||
context.binFound = (!!sr && sr.code === 0);
|
||||
}
|
||||
if (context.binFound) {
|
||||
context.binPath = bin;
|
||||
} else {
|
||||
if (alertOnFail) {
|
||||
alertNoBin(context.host, binName, 'configuredFileMissing', configuredFileMissingMessage, context.installDependenciesCallback);
|
||||
}
|
||||
}
|
||||
|
||||
return context.binFound;
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Errorable, failed } from '../interfaces';
|
||||
|
||||
interface CompatibilityGuaranteed {
|
||||
readonly guaranteed: true;
|
||||
}
|
||||
|
||||
interface CompatibilityNotGuaranteed {
|
||||
readonly guaranteed: false;
|
||||
readonly didCheck: boolean;
|
||||
readonly clientVersion: string;
|
||||
readonly serverVersion: string;
|
||||
}
|
||||
|
||||
export type Compatibility = CompatibilityGuaranteed | CompatibilityNotGuaranteed;
|
||||
|
||||
export function isGuaranteedCompatible(c: Compatibility): c is CompatibilityGuaranteed {
|
||||
return c.guaranteed;
|
||||
}
|
||||
|
||||
export interface Version {
|
||||
readonly major: string;
|
||||
readonly minor: string;
|
||||
readonly gitVersion: string;
|
||||
}
|
||||
|
||||
export async function check(kubectlLoadJSON: (cmd: string) => Promise<Errorable<any>>): Promise<Compatibility> {
|
||||
const version = await kubectlLoadJSON('version -o json');
|
||||
if (failed(version)) {
|
||||
return {
|
||||
guaranteed: false,
|
||||
didCheck: false,
|
||||
clientVersion: '',
|
||||
serverVersion: ''
|
||||
};
|
||||
}
|
||||
|
||||
const clientVersion: Version = version.result.clientVersion;
|
||||
const serverVersion: Version = version.result.serverVersion;
|
||||
|
||||
if (isCompatible(clientVersion, serverVersion)) {
|
||||
return { guaranteed: true };
|
||||
}
|
||||
|
||||
return {
|
||||
guaranteed: false,
|
||||
didCheck: true,
|
||||
clientVersion: clientVersion.gitVersion,
|
||||
serverVersion: serverVersion.gitVersion
|
||||
};
|
||||
}
|
||||
|
||||
function isCompatible(clientVersion: Version, serverVersion: Version): boolean {
|
||||
if (clientVersion.major === serverVersion.major) {
|
||||
const clientMinor = Number.parseInt(clientVersion.minor);
|
||||
const serverMinor = Number.parseInt(serverVersion.minor);
|
||||
if (Number.isInteger(clientMinor) && Number.isInteger(serverMinor) && Math.abs(clientMinor - serverMinor) <= 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export interface Host {
|
||||
showErrorMessage(message: string, ...items: string[]): Thenable<string | undefined>;
|
||||
showWarningMessage(message: string, ...items: string[]): Thenable<string | undefined>;
|
||||
showInformationMessage(message: string, ...items: string[]): Thenable<string | undefined>;
|
||||
getConfiguration(key: string): any;
|
||||
onDidChangeConfiguration(listener: (ch: vscode.ConfigurationChangeEvent) => any): vscode.Disposable;
|
||||
}
|
||||
|
||||
export const host: Host = {
|
||||
showErrorMessage: showErrorMessage,
|
||||
showWarningMessage: showWarningMessage,
|
||||
showInformationMessage: showInformationMessage,
|
||||
getConfiguration: getConfiguration,
|
||||
onDidChangeConfiguration: onDidChangeConfiguration,
|
||||
};
|
||||
|
||||
function showErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
return vscode.window.showErrorMessage(message, ...items);
|
||||
}
|
||||
|
||||
function showWarningMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
return vscode.window.showWarningMessage(message, ...items);
|
||||
}
|
||||
|
||||
function showInformationMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
return vscode.window.showInformationMessage(message, ...items);
|
||||
}
|
||||
|
||||
function getConfiguration(key: string): any {
|
||||
return vscode.workspace.getConfiguration(key);
|
||||
}
|
||||
|
||||
function onDidChangeConfiguration(listener: (e: vscode.ConfigurationChangeEvent) => any): vscode.Disposable {
|
||||
return vscode.workspace.onDidChangeConfiguration(listener);
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { Host } from './host';
|
||||
import { FS } from '../utility/fs';
|
||||
import { Shell, ShellResult } from '../utility/shell';
|
||||
import * as binutil from './binutil';
|
||||
import { Errorable } from '../interfaces';
|
||||
import * as compatibility from './compatibility';
|
||||
import { getToolPath } from '../config/config';
|
||||
|
||||
export interface Kubectl {
|
||||
checkPresent(errorMessageMode: CheckPresentMessageMode): Promise<boolean>;
|
||||
asJson<T>(command: string): Promise<Errorable<T>>;
|
||||
invokeAsync(command: string, stdin?: string): Promise<ShellResult | undefined>;
|
||||
getContext(): Context;
|
||||
}
|
||||
|
||||
interface Context {
|
||||
readonly host: Host;
|
||||
readonly fs: FS;
|
||||
readonly shell: Shell;
|
||||
readonly installDependenciesCallback: () => void;
|
||||
binFound: boolean;
|
||||
binPath: string;
|
||||
}
|
||||
|
||||
class KubectlImpl implements Kubectl {
|
||||
constructor(host: Host, fs: FS, shell: Shell, installDependenciesCallback: () => void, kubectlFound: boolean) {
|
||||
this.context = { host: host, fs: fs, shell: shell, installDependenciesCallback: installDependenciesCallback, binFound: kubectlFound, binPath: 'kubectl' };
|
||||
}
|
||||
|
||||
readonly context: Context;
|
||||
|
||||
checkPresent(errorMessageMode: CheckPresentMessageMode): Promise<boolean> {
|
||||
return checkPresent(this.context, errorMessageMode);
|
||||
}
|
||||
asJson<T>(command: string): Promise<Errorable<T>> {
|
||||
return asJson(this.context, command);
|
||||
}
|
||||
invokeAsync(command: string, stdin?: string): Promise<ShellResult | undefined> {
|
||||
return invokeAsync(this.context, command, stdin);
|
||||
}
|
||||
|
||||
getContext(): Context {
|
||||
return this.context;
|
||||
}
|
||||
}
|
||||
|
||||
export function create(host: Host, fs: FS, shell: Shell, installDependenciesCallback: () => void): Kubectl {
|
||||
return new KubectlImpl(host, fs, shell, installDependenciesCallback, false);
|
||||
}
|
||||
|
||||
export enum CheckPresentMessageMode {
|
||||
Command,
|
||||
Activation,
|
||||
Silent,
|
||||
}
|
||||
|
||||
async function checkPresent(context: Context, errorMessageMode: CheckPresentMessageMode): Promise<boolean> {
|
||||
if (context.binFound) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return await checkForKubectlInternal(context, errorMessageMode);
|
||||
}
|
||||
|
||||
async function checkForKubectlInternal(context: Context, errorMessageMode: CheckPresentMessageMode): Promise<boolean> {
|
||||
const binName = 'kubectl';
|
||||
const bin = getToolPath(context.host, context.shell, binName);
|
||||
|
||||
const contextMessage = getCheckKubectlContextMessage(errorMessageMode);
|
||||
const inferFailedMessage = localize('binaryNotFound', 'Could not find {0} binary. {1}', binName, contextMessage);
|
||||
const configuredFileMissingMessage = localize('binaryNotInstalled', '{0} is not installed. {1}', bin, contextMessage);
|
||||
|
||||
return await binutil.checkForBinary(context, bin, binName, inferFailedMessage, configuredFileMissingMessage, errorMessageMode !== CheckPresentMessageMode.Silent);
|
||||
}
|
||||
|
||||
function getCheckKubectlContextMessage(errorMessageMode: CheckPresentMessageMode): string {
|
||||
if (errorMessageMode === CheckPresentMessageMode.Activation) {
|
||||
return localize('kubernetesRequired', ' SQL Server Big data cluster requires kubernetes.');
|
||||
} else if (errorMessageMode === CheckPresentMessageMode.Command) {
|
||||
return localize('cannotExecuteCmd', ' Cannot execute command.');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function invokeAsync(context: Context, command: string, stdin?: string): Promise<ShellResult | undefined> {
|
||||
if (await checkPresent(context, CheckPresentMessageMode.Command)) {
|
||||
const bin = baseKubectlPath(context);
|
||||
const cmd = `${bin} ${command}`;
|
||||
const sr = await context.shell.exec(cmd, stdin);
|
||||
if (sr && sr.code !== 0) {
|
||||
checkPossibleIncompatibility(context);
|
||||
}
|
||||
return sr;
|
||||
} else {
|
||||
return { code: -1, stdout: '', stderr: '' };
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: invalidate this when the context changes or if we know kubectl has changed (e.g. config)
|
||||
let checkedCompatibility = false; // We don't want to spam the user (or CPU!) repeatedly running the version check
|
||||
|
||||
async function checkPossibleIncompatibility(context: Context): Promise<void> {
|
||||
if (checkedCompatibility) {
|
||||
return;
|
||||
}
|
||||
checkedCompatibility = true;
|
||||
const compat = await compatibility.check((cmd) => asJson<compatibility.Version>(context, cmd));
|
||||
if (!compatibility.isGuaranteedCompatible(compat) && compat.didCheck) {
|
||||
const versionAlert = localize('kubectlVersionIncompatible', 'kubectl version ${0} may be incompatible with cluster Kubernetes version {1}', compat.clientVersion, compat.serverVersion);
|
||||
context.host.showWarningMessage(versionAlert);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function baseKubectlPath(context: Context): string {
|
||||
let bin = getToolPath(context.host, context.shell, 'kubectl');
|
||||
if (!bin) {
|
||||
bin = 'kubectl';
|
||||
}
|
||||
return bin;
|
||||
}
|
||||
|
||||
async function asJson<T>(context: Context, command: string): Promise<Errorable<T>> {
|
||||
const shellResult = await invokeAsync(context, command);
|
||||
if (!shellResult) {
|
||||
return { succeeded: false, error: [localize('cannotRunCommand', 'Unable to run command ({0})', command)] };
|
||||
}
|
||||
|
||||
if (shellResult.code === 0) {
|
||||
return { succeeded: true, result: JSON.parse(shellResult.stdout.trim()) as T };
|
||||
|
||||
}
|
||||
return { succeeded: false, error: [shellResult.stderr] };
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { Kubectl } from './kubectl';
|
||||
import { failed, ClusterType } from '../interfaces';
|
||||
|
||||
export interface KubectlContext {
|
||||
readonly clusterName: string;
|
||||
readonly contextName: string;
|
||||
readonly userName: string;
|
||||
readonly active: boolean;
|
||||
}
|
||||
|
||||
interface Kubeconfig {
|
||||
readonly apiVersion: string;
|
||||
readonly 'current-context': string;
|
||||
readonly clusters: {
|
||||
readonly name: string;
|
||||
readonly cluster: {
|
||||
readonly server: string;
|
||||
readonly 'certificate-authority'?: string;
|
||||
readonly 'certificate-authority-data'?: string;
|
||||
};
|
||||
}[] | undefined;
|
||||
readonly contexts: {
|
||||
readonly name: string;
|
||||
readonly context: {
|
||||
readonly cluster: string;
|
||||
readonly user: string;
|
||||
readonly namespace?: string;
|
||||
};
|
||||
}[] | undefined;
|
||||
readonly users: {
|
||||
readonly name: string;
|
||||
readonly user: {};
|
||||
}[] | undefined;
|
||||
}
|
||||
|
||||
export interface ClusterConfig {
|
||||
readonly server: string;
|
||||
readonly certificateAuthority: string | undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function getKubeconfig(kubectl: Kubectl): Promise<Kubeconfig | null> {
|
||||
const shellResult = await kubectl.asJson<any>('config view -o json');
|
||||
if (failed(shellResult)) {
|
||||
vscode.window.showErrorMessage(shellResult.error[0]);
|
||||
return null;
|
||||
}
|
||||
return shellResult.result;
|
||||
}
|
||||
|
||||
export async function getCurrentClusterConfig(kubectl: Kubectl): Promise<ClusterConfig | undefined> {
|
||||
const kubeConfig = await getKubeconfig(kubectl);
|
||||
if (!kubeConfig || !kubeConfig.clusters || !kubeConfig.contexts) {
|
||||
return undefined;
|
||||
}
|
||||
const contextConfig = kubeConfig.contexts.find((context) => context.name === kubeConfig['current-context'])!;
|
||||
const clusterConfig = kubeConfig.clusters.find((cluster) => cluster.name === contextConfig.context.cluster)!;
|
||||
return {
|
||||
server: clusterConfig.cluster.server,
|
||||
certificateAuthority: clusterConfig.cluster['certificate-authority']
|
||||
};
|
||||
}
|
||||
|
||||
export async function getContexts(kubectl: Kubectl): Promise<KubectlContext[]> {
|
||||
const kubectlConfig = await getKubeconfig(kubectl);
|
||||
if (!kubectlConfig) {
|
||||
return [];
|
||||
}
|
||||
const currentContext = kubectlConfig['current-context'];
|
||||
const contexts = kubectlConfig.contexts || [];
|
||||
return contexts.map((c) => {
|
||||
return {
|
||||
clusterName: c.context.cluster,
|
||||
contextName: c.name,
|
||||
userName: c.context.user,
|
||||
active: c.name === currentContext
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function setContext(kubectl: Kubectl, targetContext: string): Promise<void> {
|
||||
const shellResult = await kubectl.invokeAsync(`config use-context ${targetContext}`);
|
||||
if (!shellResult || shellResult.code !== 0) {
|
||||
// TODO: Update error handling for now.
|
||||
let errMsg = shellResult ? shellResult.stderr : localize('runKubectlFailed', 'Unable to run kubectl');
|
||||
vscode.window.showErrorMessage(localize('setClusterFailed', 'Failed to set \'{0}\' as current cluster: {1}', targetContext, errMsg));
|
||||
}
|
||||
}
|
||||
|
||||
export async function inferCurrentClusterType(kubectl: Kubectl): Promise<ClusterType> {
|
||||
let latestContextName = '';
|
||||
|
||||
const ctxsr = await kubectl.invokeAsync('config current-context');
|
||||
if (ctxsr && ctxsr.code === 0) {
|
||||
latestContextName = ctxsr.stdout.trim();
|
||||
} else {
|
||||
return ClusterType.Other;
|
||||
}
|
||||
|
||||
const cisr = await kubectl.invokeAsync('cluster-info');
|
||||
if (!cisr || cisr.code !== 0) {
|
||||
return ClusterType.Unknown;
|
||||
}
|
||||
const masterInfos = cisr.stdout.split('\n')
|
||||
.filter((s) => s.indexOf('master is running at') >= 0);
|
||||
|
||||
if (masterInfos.length === 0) {
|
||||
return ClusterType.Other;
|
||||
}
|
||||
|
||||
const masterInfo = masterInfos[0];
|
||||
if (masterInfo.indexOf('azmk8s.io') >= 0 || masterInfo.indexOf('azure.com') >= 0) {
|
||||
return ClusterType.AKS;
|
||||
}
|
||||
|
||||
if (latestContextName) {
|
||||
const gcsr = await kubectl.invokeAsync(`config get-contexts ${latestContextName}`);
|
||||
if (gcsr && gcsr.code === 0) {
|
||||
if (gcsr.stdout.indexOf('minikube') >= 0) {
|
||||
return ClusterType.Minikube;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ClusterType.Other;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export interface ISqlServerBigDataClusterChannel {
|
||||
showOutput(message: any, title?: string): void;
|
||||
}
|
||||
|
||||
const outputChannelName = localize('bigDataClusterOutputChannel', 'SQL Server big data cluster');
|
||||
class SqlServerBigDataCluster implements ISqlServerBigDataClusterChannel {
|
||||
private readonly channel: vscode.OutputChannel = vscode.window.createOutputChannel(outputChannelName);
|
||||
|
||||
showOutput(message: any, title?: string): void {
|
||||
if (title) {
|
||||
const simplifiedTime = (new Date()).toISOString().replace(/z|t/gi, ' ').trim(); // YYYY-MM-DD HH:mm:ss.sss
|
||||
const hightlightingTitle = `[${title} ${simplifiedTime}]`;
|
||||
this.channel.appendLine(hightlightingTitle);
|
||||
}
|
||||
this.channel.appendLine(message);
|
||||
this.channel.show();
|
||||
}
|
||||
}
|
||||
|
||||
export const sqlserverbigdataclusterchannel: ISqlServerBigDataClusterChannel = new SqlServerBigDataCluster();
|
||||
@@ -1,60 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 vscode = require('vscode');
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { MainController } from './mainController';
|
||||
|
||||
import { fs } from './utility/fs';
|
||||
|
||||
import { host } from './kubectl/host';
|
||||
import { sqlserverbigdataclusterchannel } from './kubectl/sqlServerBigDataClusterChannel';
|
||||
import { shell, Shell } from './utility/shell';
|
||||
import { CheckPresentMessageMode, create as kubectlCreate } from './kubectl/kubectl';
|
||||
import { installKubectl } from './installer/installer';
|
||||
import { Errorable, failed } from './interfaces';
|
||||
|
||||
const kubectl = kubectlCreate(host, fs, shell, installDependencies);
|
||||
export let controller: MainController;
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
controller = new MainController(context, kubectl);
|
||||
controller.activate();
|
||||
}
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
export function deactivate(): void {
|
||||
if (controller) {
|
||||
controller.deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
export async function installDependencies() {
|
||||
const gotKubectl = await kubectl.checkPresent(CheckPresentMessageMode.Silent);
|
||||
|
||||
|
||||
const installPromises = [
|
||||
installDependency('kubectl', gotKubectl, installKubectl)
|
||||
];
|
||||
|
||||
await Promise.all(installPromises);
|
||||
|
||||
sqlserverbigdataclusterchannel.showOutput(localize('done', 'Done'));
|
||||
}
|
||||
|
||||
async function installDependency(name: string, alreadyGot: boolean, installFunc: (shell: Shell) => Promise<Errorable<null>>): Promise<void> {
|
||||
if (alreadyGot) {
|
||||
sqlserverbigdataclusterchannel.showOutput(localize('dependencyInstalled', '{0} already installed...', name));
|
||||
} else {
|
||||
sqlserverbigdataclusterchannel.showOutput(localize('installingDependency', 'Installing {0}...', name));
|
||||
const result = await installFunc(shell);
|
||||
if (failed(result)) {
|
||||
sqlserverbigdataclusterchannel.showOutput(localize('installingDependencyFailed', 'Unable to install {0}: {1}', name, result.error[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { CreateClusterWizard } from './wizards/create-cluster/createClusterWizard';
|
||||
import { Kubectl } from './kubectl/kubectl';
|
||||
/**
|
||||
* The main controller class that initializes the extension
|
||||
*/
|
||||
export class MainController {
|
||||
protected _context: vscode.ExtensionContext;
|
||||
protected _kubectl: Kubectl;
|
||||
|
||||
public constructor(context: vscode.ExtensionContext, kubectl: Kubectl) {
|
||||
this._context = context;
|
||||
this._kubectl = kubectl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the extension
|
||||
*/
|
||||
public activate(): void {
|
||||
vscode.commands.registerCommand('mssql.cluster.create', () => {
|
||||
let wizard = new CreateClusterWizard(this._context, this._kubectl);
|
||||
wizard.open();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivates the extension
|
||||
*/
|
||||
public deactivate(): void { }
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { fs } from '../utility/fs';
|
||||
import { Shell } from '../utility/shell';
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import mkdirp = require('mkdirp');
|
||||
import { Kubectl, baseKubectlPath } from '../kubectl/kubectl';
|
||||
import { KubectlContext } from '../kubectl/kubectlUtils';
|
||||
|
||||
export interface Scriptable {
|
||||
getScriptProperties(): Promise<ScriptingDictionary<string>>;
|
||||
getTargetKubectlContext(): KubectlContext;
|
||||
}
|
||||
|
||||
export interface ScriptingDictionary<V> {
|
||||
[name: string]: V;
|
||||
}
|
||||
|
||||
const deployFilePrefix: string = 'mssql-bdc-deploy';
|
||||
export class ScriptGenerator {
|
||||
|
||||
private _shell: Shell;
|
||||
private _kubectl: Kubectl;
|
||||
|
||||
private _kubectlPath: string;
|
||||
constructor(_kubectl: Kubectl) {
|
||||
this._kubectl = _kubectl;
|
||||
this._shell = this._kubectl.getContext().shell;
|
||||
this._kubectlPath = baseKubectlPath(this._kubectl.getContext());
|
||||
}
|
||||
|
||||
public async generateDeploymentScript(scriptable: Scriptable): Promise<void> {
|
||||
let targetClusterName = scriptable.getTargetKubectlContext().clusterName;
|
||||
let targetContextName = scriptable.getTargetKubectlContext().contextName;
|
||||
|
||||
let timestamp = new Date().getTime();
|
||||
let deployFolder = this.getDeploymentFolder(this._shell);
|
||||
let deployFileSuffix = this._shell.isWindows() ? `.bat` : `.sh`;
|
||||
let deployFileName = `${deployFilePrefix}-${targetClusterName}-${timestamp}${deployFileSuffix}`;
|
||||
let deployFilePath = path.join(deployFolder, deployFileName);
|
||||
|
||||
let envVars = '';
|
||||
let propertiesDict = await scriptable.getScriptProperties();
|
||||
for (let key in propertiesDict) {
|
||||
let value = propertiesDict[key];
|
||||
envVars += this._shell.isWindows() ? `Set ${key} = ${value}\n` : `export ${key} = ${value}\n`;
|
||||
}
|
||||
envVars += os.EOL;
|
||||
|
||||
let kubeContextcommand = `${this._kubectlPath} config use-context ${targetContextName}\n`;
|
||||
// Todo: The API for mssqlctl may change per version, so need a version check to use proper syntax.
|
||||
let deployCommand = `mssqlctl create cluster ${targetClusterName}\n`;
|
||||
|
||||
let deployContent = envVars + kubeContextcommand + deployCommand;
|
||||
|
||||
mkdirp.sync(deployFolder);
|
||||
await fs.writeFile(deployFilePath, deployContent, handleError);
|
||||
}
|
||||
|
||||
public getDeploymentFolder(shell: Shell): string {
|
||||
return path.join(shell.home(), `.mssql-bdc/deployment`);
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = (err: NodeJS.ErrnoException) => {
|
||||
if (err) {
|
||||
vscode.window.showErrorMessage(err.message);
|
||||
}
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/sql/azdata.d.ts'/>
|
||||
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
||||
@@ -1,65 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as sysfs from 'fs';
|
||||
|
||||
export interface FS {
|
||||
existsSync(path: string): boolean;
|
||||
readFile(filename: string, encoding: string, callback: (err: NodeJS.ErrnoException, data: string) => void): void;
|
||||
readFileSync(filename: string, encoding: string): string;
|
||||
readFileToBufferSync(filename: string): Buffer;
|
||||
writeFile(filename: string, data: any, callback?: (err: NodeJS.ErrnoException) => void): void;
|
||||
writeFileSync(filename: string, data: any): void;
|
||||
dirSync(path: string): string[];
|
||||
unlinkAsync(path: string): Promise<void>;
|
||||
existsAsync(path: string): Promise<boolean>;
|
||||
openAsync(path: string, flags: string): Promise<void>;
|
||||
statSync(path: string): sysfs.Stats;
|
||||
}
|
||||
|
||||
export const fs: FS = {
|
||||
existsSync: (path) => sysfs.existsSync(path),
|
||||
readFile: (filename, encoding, callback) => sysfs.readFile(filename, encoding, callback),
|
||||
readFileSync: (filename, encoding) => sysfs.readFileSync(filename, encoding),
|
||||
readFileToBufferSync: (filename) => sysfs.readFileSync(filename),
|
||||
writeFile: (filename, data, callback) => sysfs.writeFile(filename, data, callback),
|
||||
writeFileSync: (filename, data) => sysfs.writeFileSync(filename, data),
|
||||
dirSync: (path) => sysfs.readdirSync(path),
|
||||
|
||||
unlinkAsync: (path) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
sysfs.unlink(path, (error) => {
|
||||
if (error) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
existsAsync: (path) => {
|
||||
return new Promise((resolve) => {
|
||||
sysfs.exists(path, (exists) => {
|
||||
resolve(exists);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
openAsync: (path, flags) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
sysfs.open(path, flags, (error, _fd) => {
|
||||
if (error) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
statSync: (path) => sysfs.statSync(path)
|
||||
};
|
||||
@@ -1,204 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 shelljs from 'shelljs';
|
||||
import * as path from 'path';
|
||||
import { getActiveKubeconfig, getToolPath } from '../config/config';
|
||||
import { host } from '../kubectl/host';
|
||||
|
||||
export enum Platform {
|
||||
Windows,
|
||||
MacOS,
|
||||
Linux,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
export interface ExecCallback extends shelljs.ExecCallback { }
|
||||
|
||||
export interface Shell {
|
||||
isWindows(): boolean;
|
||||
isUnix(): boolean;
|
||||
platform(): Platform;
|
||||
home(): string;
|
||||
combinePath(basePath: string, relativePath: string): string;
|
||||
fileUri(filePath: string): vscode.Uri;
|
||||
execOpts(): any;
|
||||
exec(cmd: string, stdin?: string): Promise<ShellResult | undefined>;
|
||||
execCore(cmd: string, opts: any, stdin?: string): Promise<ShellResult>;
|
||||
unquotedPath(path: string): string;
|
||||
which(bin: string): string | null;
|
||||
cat(path: string): string;
|
||||
ls(path: string): string[];
|
||||
}
|
||||
|
||||
export const shell: Shell = {
|
||||
isWindows: isWindows,
|
||||
isUnix: isUnix,
|
||||
platform: platform,
|
||||
home: home,
|
||||
combinePath: combinePath,
|
||||
fileUri: fileUri,
|
||||
execOpts: execOpts,
|
||||
exec: exec,
|
||||
execCore: execCore,
|
||||
unquotedPath: unquotedPath,
|
||||
which: which,
|
||||
cat: cat,
|
||||
ls: ls,
|
||||
};
|
||||
|
||||
const WINDOWS: string = 'win32';
|
||||
|
||||
export interface ShellResult {
|
||||
readonly code: number;
|
||||
readonly stdout: string;
|
||||
readonly stderr: string;
|
||||
}
|
||||
|
||||
export type ShellHandler = (code: number, stdout: string, stderr: string) => void;
|
||||
|
||||
function isWindows(): boolean {
|
||||
return (process.platform === WINDOWS);
|
||||
}
|
||||
|
||||
function isUnix(): boolean {
|
||||
return !isWindows();
|
||||
}
|
||||
|
||||
function platform(): Platform {
|
||||
switch (process.platform) {
|
||||
case 'win32': return Platform.Windows;
|
||||
case 'darwin': return Platform.MacOS;
|
||||
case 'linux': return Platform.Linux;
|
||||
default: return Platform.Unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
function concatIfBoth(s1: string | undefined, s2: string | undefined): string | undefined {
|
||||
return s1 && s2 ? s1.concat(s2) : undefined;
|
||||
}
|
||||
|
||||
function home(): string {
|
||||
return process.env['HOME'] ||
|
||||
concatIfBoth(process.env['HOMEDRIVE'], process.env['HOMEPATH']) ||
|
||||
process.env['USERPROFILE'] ||
|
||||
'';
|
||||
}
|
||||
|
||||
function combinePath(basePath: string, relativePath: string) {
|
||||
let separator = '/';
|
||||
if (isWindows()) {
|
||||
relativePath = relativePath.replace(/\//g, '\\');
|
||||
separator = '\\';
|
||||
}
|
||||
return basePath + separator + relativePath;
|
||||
}
|
||||
|
||||
function isWindowsFilePath(filePath: string) {
|
||||
return filePath[1] === ':' && filePath[2] === '\\';
|
||||
}
|
||||
|
||||
function fileUri(filePath: string): vscode.Uri {
|
||||
if (isWindowsFilePath(filePath)) {
|
||||
return vscode.Uri.parse('file:///' + filePath.replace(/\\/g, '/'));
|
||||
}
|
||||
return vscode.Uri.parse('file://' + filePath);
|
||||
}
|
||||
|
||||
function execOpts(): any {
|
||||
let env = process.env;
|
||||
if (isWindows()) {
|
||||
env = Object.assign({}, env, { HOME: home() });
|
||||
}
|
||||
env = shellEnvironment(env);
|
||||
const opts = {
|
||||
cwd: vscode.workspace.rootPath,
|
||||
env: env,
|
||||
async: true
|
||||
};
|
||||
return opts;
|
||||
}
|
||||
|
||||
async function exec(cmd: string, stdin?: string): Promise<ShellResult | undefined> {
|
||||
try {
|
||||
return await execCore(cmd, execOpts(), stdin);
|
||||
} catch (ex) {
|
||||
vscode.window.showErrorMessage(ex);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function execCore(cmd: string, opts: any, stdin?: string): Promise<ShellResult> {
|
||||
return new Promise<ShellResult>((resolve) => {
|
||||
const proc = shelljs.exec(cmd, opts, (code, stdout, stderr) => resolve({ code: code, stdout: stdout, stderr: stderr }));
|
||||
if (stdin) {
|
||||
proc.stdin.end(stdin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function unquotedPath(path: string): string {
|
||||
if (isWindows() && path && path.length > 1 && path.startsWith('"') && path.endsWith('"')) {
|
||||
return path.substring(1, path.length - 1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function shellEnvironment(baseEnvironment: any): any {
|
||||
const env = Object.assign({}, baseEnvironment);
|
||||
const pathVariable = pathVariableName(env);
|
||||
for (const tool of ['kubectl']) {
|
||||
const toolPath = getToolPath(host, shell, tool);
|
||||
if (toolPath) {
|
||||
const toolDirectory = path.dirname(toolPath);
|
||||
const currentPath = env[pathVariable];
|
||||
env[pathVariable] = toolDirectory + (currentPath ? `${pathEntrySeparator()}${currentPath}` : '');
|
||||
}
|
||||
}
|
||||
|
||||
const kubeconfig = getActiveKubeconfig();
|
||||
if (kubeconfig) {
|
||||
env['KUBECONFIG'] = kubeconfig;
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
function pathVariableName(env: any): string {
|
||||
if (isWindows()) {
|
||||
for (const v of Object.keys(env)) {
|
||||
if (v.toLowerCase() === 'path') {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'PATH';
|
||||
}
|
||||
|
||||
function pathEntrySeparator() {
|
||||
return isWindows() ? ';' : ':';
|
||||
}
|
||||
|
||||
function which(bin: string): string | null {
|
||||
return shelljs.which(bin);
|
||||
}
|
||||
|
||||
function cat(path: string): string {
|
||||
return shelljs.cat(path);
|
||||
}
|
||||
|
||||
function ls(path: string): string[] {
|
||||
return shelljs.ls(path);
|
||||
}
|
||||
|
||||
export function shellMessage(sr: ShellResult | undefined, invocationFailureMessage: string): string {
|
||||
if (!sr) {
|
||||
return invocationFailureMessage;
|
||||
}
|
||||
return sr.code === 0 ? sr.stdout : sr.stderr;
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { TargetClusterType, ClusterPorts, ClusterType, ContainerRegistryInfo, TargetClusterTypeInfo, ToolInfo, ToolInstallationStatus, ClusterProfile, PoolConfiguration, SQLServerMasterConfiguration, ClusterPoolType, ClusterResourceSummary } from '../../interfaces';
|
||||
import { getContexts, KubectlContext, setContext, inferCurrentClusterType } from '../../kubectl/kubectlUtils';
|
||||
import { Kubectl } from '../../kubectl/kubectl';
|
||||
import { Scriptable, ScriptingDictionary } from '../../scripting/scripting';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class CreateClusterModel implements Scriptable {
|
||||
|
||||
private _tmp_tools_installed: boolean = false;
|
||||
private scriptingProperties: ScriptingDictionary<string> = {};
|
||||
constructor(private _kubectl: Kubectl) {
|
||||
}
|
||||
|
||||
public async loadClusters(): Promise<KubectlContext[]> {
|
||||
return await getContexts(this._kubectl);
|
||||
}
|
||||
|
||||
public async changeKubernetesContext(targetContext: string): Promise<void> {
|
||||
await setContext(this._kubectl, targetContext);
|
||||
}
|
||||
|
||||
public getDefaultPorts(): Thenable<ClusterPorts> {
|
||||
let promise = new Promise<ClusterPorts>(resolve => {
|
||||
resolve({
|
||||
sql: '31433',
|
||||
knox: '30443',
|
||||
controller: '30888',
|
||||
proxy: '30909',
|
||||
grafana: '30119',
|
||||
kibana: '30999'
|
||||
});
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
public getDefaultContainerRegistryInfo(): Thenable<ContainerRegistryInfo> {
|
||||
let promise = new Promise<ContainerRegistryInfo>(resolve => {
|
||||
resolve({
|
||||
registry: 'private-repo.microsoft.com',
|
||||
repository: 'mssql-private-preview',
|
||||
imageTag: 'latest'
|
||||
});
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
public getAllTargetClusterTypeInfo(): Thenable<TargetClusterTypeInfo[]> {
|
||||
let promise = new Promise<TargetClusterTypeInfo[]>(resolve => {
|
||||
let aksCluster: TargetClusterTypeInfo = {
|
||||
enabled: false,
|
||||
type: TargetClusterType.NewAksCluster,
|
||||
name: localize('bdc-create.AKSClusterCardText', 'New AKS Cluster'),
|
||||
fullName: localize('bdc-create.AKSClusterFullName', 'New Azure Kubernetes Service cluster'),
|
||||
description: localize('bdc-create.AKSClusterDescription',
|
||||
'This option configures new Azure Kubernetes Service (AKS) for SQL Server big data cluster deployments. AKS makes it simple to create, configure and manage a cluster of virutal machines that are preconfigured with a Kubernetes cluster to run containerized applications.'),
|
||||
iconPath: {
|
||||
dark: 'images/aks.svg',
|
||||
light: 'images/aks.svg'
|
||||
}
|
||||
};
|
||||
|
||||
let existingCluster: TargetClusterTypeInfo = {
|
||||
enabled: true,
|
||||
type: TargetClusterType.ExistingKubernetesCluster,
|
||||
name: localize('bdc-create.ExistingClusterCardText', 'Existing Cluster'),
|
||||
fullName: localize('bdc-create.ExistingClusterFullName', 'Existing Kubernetes cluster'),
|
||||
description: localize('bdc-create.ExistingClusterDescription', 'This option assumes you already have a Kubernetes cluster installed, Once a prerequisite check is done, ensure the correct cluster context is selected.'),
|
||||
iconPath: {
|
||||
dark: 'images/kubernetes.svg',
|
||||
light: 'images/kubernetes.svg'
|
||||
}
|
||||
};
|
||||
resolve([aksCluster, existingCluster]);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
public getRequiredToolStatus(): Thenable<ToolInfo[]> {
|
||||
let kubeCtl = {
|
||||
name: 'kubectl',
|
||||
description: 'Tool used for managing the Kubernetes cluster',
|
||||
version: '',
|
||||
status: ToolInstallationStatus.Installed
|
||||
};
|
||||
let mssqlCtl = {
|
||||
name: 'mssqlctl',
|
||||
description: 'Command-line tool for installing and managing the SQL Server big data cluster',
|
||||
version: '',
|
||||
status: ToolInstallationStatus.Installed
|
||||
};
|
||||
let azureCli = {
|
||||
name: 'Azure CLI',
|
||||
description: 'Tool used for managing Azure services',
|
||||
version: '',
|
||||
status: this._tmp_tools_installed ? ToolInstallationStatus.Installed : ToolInstallationStatus.NotInstalled
|
||||
};
|
||||
let promise = new Promise<ToolInfo[]>(resolve => {
|
||||
setTimeout(() => {
|
||||
let tools = this.targetClusterType === TargetClusterType.ExistingKubernetesCluster ? [kubeCtl, mssqlCtl] : [kubeCtl, mssqlCtl, azureCli];
|
||||
resolve(tools);
|
||||
}, 1000);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
public installTool(tool: ToolInfo): Thenable<void> {
|
||||
let promise = new Promise<void>(resolve => {
|
||||
setTimeout(() => {
|
||||
tool.status = ToolInstallationStatus.Installed;
|
||||
this._tmp_tools_installed = true;
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
public getDefaultKubeConfigPath(): string {
|
||||
return path.join(os.homedir(), '.kube', 'config');
|
||||
}
|
||||
|
||||
public clusterName: string;
|
||||
|
||||
public targetClusterType: TargetClusterType;
|
||||
|
||||
public selectedCluster: KubectlContext;
|
||||
|
||||
public adminUserName: string;
|
||||
|
||||
public adminPassword: string;
|
||||
|
||||
public sqlPort: string;
|
||||
|
||||
public knoxPort: string;
|
||||
|
||||
public controllerPort: string;
|
||||
|
||||
public proxyPort: string;
|
||||
|
||||
public grafanaPort: string;
|
||||
|
||||
public kibanaPort: string;
|
||||
|
||||
public containerRegistry: string;
|
||||
|
||||
public containerRepository: string;
|
||||
|
||||
public containerImageTag: string;
|
||||
|
||||
public containerRegistryUserName: string;
|
||||
|
||||
public containerRegistryPassword: string;
|
||||
|
||||
public profile: ClusterProfile;
|
||||
|
||||
public async getTargetClusterPlatform(targetContextName: string): Promise<string> {
|
||||
await setContext(this._kubectl, targetContextName);
|
||||
let clusterType = await inferCurrentClusterType(this._kubectl);
|
||||
|
||||
switch (clusterType) {
|
||||
case ClusterType.AKS:
|
||||
return 'aks';
|
||||
case ClusterType.Minikube:
|
||||
return 'minikube';
|
||||
case ClusterType.Other:
|
||||
default:
|
||||
return 'kubernetes';
|
||||
}
|
||||
}
|
||||
|
||||
public async getScriptProperties(): Promise<ScriptingDictionary<string>> {
|
||||
|
||||
// Cluster settings
|
||||
this.scriptingProperties['CLUSTER_NAME'] = this.selectedCluster.clusterName;
|
||||
this.scriptingProperties['CLUSTER_PLATFORM'] = await this.getTargetClusterPlatform(this.selectedCluster.contextName);
|
||||
|
||||
// Default pool count for now. TODO: Update from user input
|
||||
this.scriptingProperties['CLUSTER_DATA_POOL_REPLICAS'] = '1';
|
||||
this.scriptingProperties['CLUSTER_COMPUTE_POOL_REPLICAS'] = '2';
|
||||
this.scriptingProperties['CLUSTER_STORAGE_POOL_REPLICAS'] = '3';
|
||||
|
||||
// SQL Server settings
|
||||
this.scriptingProperties['CONTROLLER_USERNAME'] = this.adminUserName;
|
||||
this.scriptingProperties['CONTROLLER_PASSWORD'] = this.adminPassword;
|
||||
this.scriptingProperties['KNOX_PASSWORD'] = this.adminPassword;
|
||||
this.scriptingProperties['MSSQL_SA_PASSWORD'] = this.adminPassword;
|
||||
|
||||
// docker settings
|
||||
this.scriptingProperties['DOCKER_REPOSITORY'] = this.containerRepository;
|
||||
this.scriptingProperties['DOCKER_REGISTRY'] = this.containerRegistry;
|
||||
this.scriptingProperties['DOCKER_PASSWORD'] = this.containerRegistryPassword;
|
||||
this.scriptingProperties['DOCKER_USERNAME'] = this.containerRegistryUserName;
|
||||
this.scriptingProperties['DOCKER_IMAGE_TAG'] = this.containerImageTag;
|
||||
|
||||
// port settings
|
||||
this.scriptingProperties['MASTER_SQL_PORT'] = this.sqlPort;
|
||||
this.scriptingProperties['KNOX_PORT'] = this.knoxPort;
|
||||
this.scriptingProperties['GRAFANA_PORT'] = this.grafanaPort;
|
||||
this.scriptingProperties['KIBANA_PORT'] = this.kibanaPort;
|
||||
|
||||
return this.scriptingProperties;
|
||||
}
|
||||
|
||||
public getTargetKubectlContext(): KubectlContext {
|
||||
return this.selectedCluster;
|
||||
}
|
||||
|
||||
public getClusterResource(): Thenable<ClusterResourceSummary> {
|
||||
let promise = new Promise<ClusterResourceSummary>(resolve => {
|
||||
setTimeout(() => {
|
||||
let resoureSummary: ClusterResourceSummary = {
|
||||
hardwareLabels: [
|
||||
{
|
||||
name: '<Default>',
|
||||
totalNodes: 10,
|
||||
totalCores: 22,
|
||||
totalDisks: 128,
|
||||
totalMemoryInGB: 77
|
||||
},
|
||||
{
|
||||
name: '#data',
|
||||
totalNodes: 4,
|
||||
totalCores: 22,
|
||||
totalDisks: 200,
|
||||
totalMemoryInGB: 100
|
||||
},
|
||||
{
|
||||
name: '#compute',
|
||||
totalNodes: 12,
|
||||
totalCores: 124,
|
||||
totalDisks: 24,
|
||||
totalMemoryInGB: 100
|
||||
},
|
||||
{
|
||||
name: '#premium',
|
||||
totalNodes: 10,
|
||||
totalCores: 100,
|
||||
totalDisks: 200,
|
||||
totalMemoryInGB: 770
|
||||
}
|
||||
]
|
||||
};
|
||||
resolve(resoureSummary);
|
||||
}, 1000);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
public getProfiles(): Thenable<ClusterProfile[]> {
|
||||
let promise = new Promise<ClusterProfile[]>(resolve => {
|
||||
setTimeout(() => {
|
||||
let profiles: ClusterProfile[] = [];
|
||||
profiles.push({
|
||||
name: 'Basic',
|
||||
sqlServerMasterConfiguration: this.createSQLPoolConfiguration(1, 1),
|
||||
computePoolConfiguration: this.createComputePoolConfiguration(2),
|
||||
dataPoolConfiguration: this.createDataPoolConfiguration(2),
|
||||
storagePoolConfiguration: this.createStoragePoolConfiguration(2),
|
||||
sparkPoolConfiguration: this.createSparkPoolConfiguration(2)
|
||||
});
|
||||
profiles.push({
|
||||
name: 'Standard',
|
||||
sqlServerMasterConfiguration: this.createSQLPoolConfiguration(3, 9),
|
||||
computePoolConfiguration: this.createComputePoolConfiguration(5),
|
||||
dataPoolConfiguration: this.createDataPoolConfiguration(5),
|
||||
storagePoolConfiguration: this.createStoragePoolConfiguration(5),
|
||||
sparkPoolConfiguration: this.createSparkPoolConfiguration(5)
|
||||
});
|
||||
profiles.push({
|
||||
name: 'Premium',
|
||||
sqlServerMasterConfiguration: this.createSQLPoolConfiguration(5, 9),
|
||||
computePoolConfiguration: this.createComputePoolConfiguration(7),
|
||||
dataPoolConfiguration: this.createDataPoolConfiguration(7),
|
||||
storagePoolConfiguration: this.createStoragePoolConfiguration(7),
|
||||
sparkPoolConfiguration: this.createSparkPoolConfiguration(7)
|
||||
});
|
||||
resolve(profiles);
|
||||
}, 1000);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
private createSQLPoolConfiguration(scale: number, maxScale: number): SQLServerMasterConfiguration {
|
||||
return <SQLServerMasterConfiguration>{
|
||||
type: ClusterPoolType.SQL,
|
||||
engineOnly: false,
|
||||
scale: scale,
|
||||
maxScale: maxScale
|
||||
};
|
||||
}
|
||||
|
||||
private createComputePoolConfiguration(scale: number): PoolConfiguration {
|
||||
return {
|
||||
type: ClusterPoolType.Compute,
|
||||
scale: scale
|
||||
};
|
||||
}
|
||||
|
||||
private createDataPoolConfiguration(scale: number): PoolConfiguration {
|
||||
return {
|
||||
type: ClusterPoolType.Data,
|
||||
scale: scale
|
||||
};
|
||||
}
|
||||
|
||||
private createStoragePoolConfiguration(scale: number): PoolConfiguration {
|
||||
return {
|
||||
type: ClusterPoolType.Storage,
|
||||
scale: scale
|
||||
};
|
||||
}
|
||||
|
||||
private createSparkPoolConfiguration(scale: number): PoolConfiguration {
|
||||
return {
|
||||
type: ClusterPoolType.Spark,
|
||||
scale: scale
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { CreateClusterModel } from './createClusterModel';
|
||||
import { SelectExistingClusterPage } from './pages/selectExistingClusterPage';
|
||||
import { SummaryPage } from './pages/summaryPage';
|
||||
import { SettingsPage } from './pages/settingsPage';
|
||||
import { ClusterProfilePage } from './pages/clusterProfilePage';
|
||||
import { ExtensionContext } from 'vscode';
|
||||
import { WizardBase } from '../wizardBase';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Kubectl } from '../../kubectl/kubectl';
|
||||
import { SelectTargetClusterTypePage } from './pages/selectTargetClusterTypePage';
|
||||
import { ScriptGenerator } from '../../scripting/scripting';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class CreateClusterWizard extends WizardBase<CreateClusterModel, CreateClusterWizard> {
|
||||
private scripter: ScriptGenerator;
|
||||
constructor(context: ExtensionContext, kubectl: Kubectl) {
|
||||
let model = new CreateClusterModel(kubectl);
|
||||
super(model, context, localize('bdc-create.wizardTitle', 'Create a big data cluster'));
|
||||
this.scripter = new ScriptGenerator(kubectl);
|
||||
}
|
||||
|
||||
protected initialize(): void {
|
||||
let settingsPage = new SettingsPage(this);
|
||||
let clusterProfilePage = new ClusterProfilePage(this);
|
||||
let selectTargetClusterPage = new SelectExistingClusterPage(this);
|
||||
let summaryPage = new SummaryPage(this);
|
||||
let targetClusterTypePage = new SelectTargetClusterTypePage(this);
|
||||
this.setPages([targetClusterTypePage, selectTargetClusterPage, settingsPage, clusterProfilePage, summaryPage]);
|
||||
|
||||
this.wizardObject.generateScriptButton.label = localize('bdc-create.generateScriptsButtonText', 'Generate Scripts');
|
||||
this.wizardObject.generateScriptButton.hidden = false;
|
||||
this.wizardObject.doneButton.label = localize('bdc-create.createClusterButtonText', 'Create');
|
||||
|
||||
this.registerDisposable(this.wizardObject.generateScriptButton.onClick(async () => {
|
||||
this.wizardObject.generateScriptButton.enabled = false;
|
||||
this.scripter.generateDeploymentScript(this.model).then(() => {
|
||||
this.wizardObject.generateScriptButton.enabled = true;
|
||||
//TODO: Add error handling.
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
protected onCancel(): void {
|
||||
}
|
||||
|
||||
protected onOk(): void {
|
||||
}
|
||||
}
|
||||
@@ -1,389 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { WizardPageBase } from '../../wizardPageBase';
|
||||
import { CreateClusterWizard } from '../createClusterWizard';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { ClusterProfile, PoolConfiguration, ClusterPoolType, SQLServerMasterConfiguration, ClusterResourceSummary } from '../../../interfaces';
|
||||
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const LabelWidth = '200px';
|
||||
const InputWidth = '300px';
|
||||
|
||||
export class ClusterProfilePage extends WizardPageBase<CreateClusterWizard> {
|
||||
private view: azdata.ModelView;
|
||||
private clusterProfiles: ClusterProfile[];
|
||||
private poolList: azdata.FlexContainer;
|
||||
private detailContainer: azdata.FlexContainer;
|
||||
private clusterResourceView: azdata.GroupContainer;
|
||||
private poolListMap = {};
|
||||
private clusterResourceContainer: azdata.FlexContainer;
|
||||
private clusterResourceLoadingComponent: azdata.LoadingComponent;
|
||||
private clusterResource: ClusterResourceSummary;
|
||||
|
||||
|
||||
constructor(wizard: CreateClusterWizard) {
|
||||
super(localize('bdc-create.clusterProfilePageTitle', 'Select a cluster profile'),
|
||||
localize('bdc-create.clusterProfilePageDescription', 'Select your requirement and we will provide you a pre-defined default scaling. You can later go to cluster configuration and customize it.'),
|
||||
wizard);
|
||||
}
|
||||
|
||||
public onEnter(): void {
|
||||
this.updatePoolList();
|
||||
this.clusterResourceLoadingComponent.loading = true;
|
||||
this.wizard.model.getClusterResource().then((resource) => {
|
||||
this.clusterResource = resource;
|
||||
this.initializeClusterResourceView();
|
||||
});
|
||||
this.wizard.wizardObject.registerNavigationValidator(() => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
protected initialize(view: azdata.ModelView): Thenable<void> {
|
||||
this.view = view;
|
||||
let fetchProfilePromise = this.wizard.model.getProfiles().then(p => { this.clusterProfiles = p; });
|
||||
return Promise.all([fetchProfilePromise]).then(() => {
|
||||
this.wizard.model.profile = this.clusterProfiles[0];
|
||||
this.clusterResourceView = this.view.modelBuilder.groupContainer().withLayout({
|
||||
header: localize('bdc-create.TargetClusterOverview', 'Target cluster scale overview'),
|
||||
collapsed: true,
|
||||
collapsible: true
|
||||
}).component();
|
||||
|
||||
this.clusterResourceContainer = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
this.clusterResourceLoadingComponent = this.view.modelBuilder.loadingComponent().withItem(this.clusterResourceContainer).component();
|
||||
this.clusterResourceView.addItem(this.clusterResourceLoadingComponent);
|
||||
|
||||
let profileLabel = view.modelBuilder.text().withProperties({ value: localize('bdc-create.clusterProfileLabel', 'Deployment profile') }).component();
|
||||
let profileDropdown = view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({
|
||||
values: this.clusterProfiles.map(profile => profile.name),
|
||||
width: '300px'
|
||||
}).component();
|
||||
let dropdownRow = this.view.modelBuilder.flexContainer().withItems([profileLabel, profileDropdown], { CSSStyles: { 'margin-right': '30px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
|
||||
let poolContainer = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', width: '100%', height: '100%' }).component();
|
||||
this.poolList = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '300px', height: '100%' }).component();
|
||||
poolContainer.addItem(this.poolList, {
|
||||
CSSStyles: {
|
||||
'border-top-style': 'solid',
|
||||
'border-top-width': '2px',
|
||||
'border-right-style': 'solid',
|
||||
'border-right-width': '2px',
|
||||
'border-color': 'lightgray'
|
||||
}
|
||||
});
|
||||
|
||||
this.detailContainer = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '760px', height: '100%' }).component();
|
||||
poolContainer.addItem(this.detailContainer, {
|
||||
CSSStyles: {
|
||||
'border-top-style': 'solid',
|
||||
'border-top-width': '2px',
|
||||
'border-color': 'lightgray'
|
||||
}
|
||||
});
|
||||
|
||||
this.wizard.registerDisposable(profileDropdown.onValueChanged(() => {
|
||||
let profiles = this.clusterProfiles.filter(p => profileDropdown.value === p.name);
|
||||
if (profiles && profiles.length === 1) {
|
||||
this.wizard.model.profile = profiles[0];
|
||||
this.updatePoolList();
|
||||
this.clearPoolDetail();
|
||||
}
|
||||
}));
|
||||
|
||||
this.initializePoolList();
|
||||
|
||||
let pageContainer = this.view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
height: '800px'
|
||||
}).component();
|
||||
pageContainer.addItem(this.clusterResourceView, {
|
||||
flex: '0 0 auto',
|
||||
CSSStyles: {
|
||||
'margin-bottom': '20px',
|
||||
'padding-bottom': '5px',
|
||||
'padding-top': '5px'
|
||||
}
|
||||
});
|
||||
pageContainer.addItem(dropdownRow, {
|
||||
flex: '0 0 auto',
|
||||
CSSStyles: { 'margin-bottom': '10px' }
|
||||
});
|
||||
pageContainer.addItem(poolContainer, {
|
||||
flex: '1 1 auto',
|
||||
CSSStyles: {
|
||||
'display': 'flex'
|
||||
}
|
||||
});
|
||||
let formBuilder = view.modelBuilder.formContainer();
|
||||
let form = formBuilder.withFormItems([{
|
||||
title: '',
|
||||
component: pageContainer
|
||||
}], {
|
||||
horizontal: false,
|
||||
componentWidth: '100%'
|
||||
}).component();
|
||||
|
||||
return view.initializeModel(form);
|
||||
});
|
||||
}
|
||||
|
||||
private initializeClusterResourceView(): void {
|
||||
this.clusterResourceContainer.clearItems();
|
||||
let text = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.HardwareProfileText', 'Hardware profile') }).component();
|
||||
let height = (this.clusterResource.hardwareLabels.length * 25) + 30;
|
||||
let labelColumn: azdata.TableColumn = {
|
||||
value: localize('bdc-create.HardwareLabelColumnName', 'Label'),
|
||||
width: 100
|
||||
};
|
||||
let totalNodesColumn: azdata.TableColumn = {
|
||||
value: localize('bdc-create.TotalNodesColumnName', 'Nodes'),
|
||||
width: 50
|
||||
};
|
||||
let totalCoresColumn: azdata.TableColumn = {
|
||||
value: localize('bdc-create.TotalCoresColumnName', 'Cores'),
|
||||
width: 50
|
||||
};
|
||||
let totalMemoryColumn: azdata.TableColumn = {
|
||||
value: localize('bdc-create.TotalMemoryColumnName', 'Memory'),
|
||||
width: 50
|
||||
};
|
||||
let totalDisksColumn: azdata.TableColumn = {
|
||||
value: localize('bdc-create.TotalDisksColumnName', 'Disks'),
|
||||
width: 50
|
||||
};
|
||||
|
||||
let table = this.view.modelBuilder.table().withProperties<azdata.TableComponentProperties>({
|
||||
height: `${height}px`,
|
||||
data: this.clusterResource.hardwareLabels.map(label => [label.name, label.totalNodes, label.totalCores, label.totalMemoryInGB, label.totalDisks]),
|
||||
columns: [labelColumn, totalNodesColumn, totalCoresColumn, totalMemoryColumn, totalDisksColumn],
|
||||
width: '300px'
|
||||
|
||||
}).component();
|
||||
this.clusterResourceContainer.addItems([text, table]);
|
||||
this.clusterResourceLoadingComponent.loading = false;
|
||||
}
|
||||
|
||||
private initializePoolList(): void {
|
||||
let pools = [this.wizard.model.profile.sqlServerMasterConfiguration,
|
||||
this.wizard.model.profile.computePoolConfiguration,
|
||||
this.wizard.model.profile.dataPoolConfiguration,
|
||||
this.wizard.model.profile.sparkPoolConfiguration,
|
||||
this.wizard.model.profile.storagePoolConfiguration];
|
||||
pools.forEach(pool => {
|
||||
let poolSummaryButton = this.view.modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: true }).component();
|
||||
let container = this.view.modelBuilder.flexContainer().component();
|
||||
this.wizard.registerDisposable(poolSummaryButton.onDidClick(() => {
|
||||
this.clearPoolDetail();
|
||||
let currentPool: PoolConfiguration;
|
||||
switch (pool.type) {
|
||||
case ClusterPoolType.SQL:
|
||||
currentPool = this.wizard.model.profile.sqlServerMasterConfiguration;
|
||||
break;
|
||||
case ClusterPoolType.Compute:
|
||||
currentPool = this.wizard.model.profile.computePoolConfiguration;
|
||||
break;
|
||||
case ClusterPoolType.Data:
|
||||
currentPool = this.wizard.model.profile.dataPoolConfiguration;
|
||||
break;
|
||||
case ClusterPoolType.Storage:
|
||||
currentPool = this.wizard.model.profile.storagePoolConfiguration;
|
||||
break;
|
||||
case ClusterPoolType.Spark:
|
||||
currentPool = this.wizard.model.profile.sparkPoolConfiguration;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (currentPool) {
|
||||
this.detailContainer.addItem(this.createPoolConfigurationPart(currentPool), { CSSStyles: { 'margin-left': '10px' } });
|
||||
}
|
||||
}));
|
||||
|
||||
let text = this.view.modelBuilder.text().component();
|
||||
this.poolListMap[pool.type] = text;
|
||||
text.width = '250px';
|
||||
let chrevron = this.view.modelBuilder.text().withProperties({ value: '>' }).component();
|
||||
chrevron.width = '30px';
|
||||
container.addItem(text);
|
||||
container.addItem(chrevron, {
|
||||
CSSStyles: {
|
||||
'font-size': '20px',
|
||||
'line-height': '0px'
|
||||
}
|
||||
});
|
||||
poolSummaryButton.addItem(container);
|
||||
this.poolList.addItem(poolSummaryButton, {
|
||||
CSSStyles: {
|
||||
'border-bottom-style': 'solid',
|
||||
'border-bottom-width': '1px',
|
||||
'border-color': 'lightgray',
|
||||
'cursor': 'pointer'
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private createPoolConfigurationPart(configuration: PoolConfiguration): azdata.Component {
|
||||
let container = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
switch (configuration.type) {
|
||||
case ClusterPoolType.SQL:
|
||||
this.createSQLConfigurationPart(container, configuration as SQLServerMasterConfiguration);
|
||||
break;
|
||||
default:
|
||||
this.createDefaultPoolConfigurationPart(container, configuration);
|
||||
break;
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
private createSQLConfigurationPart(container: azdata.FlexContainer, configuration: SQLServerMasterConfiguration): void {
|
||||
this.createDefaultPoolConfigurationPart(container, configuration);
|
||||
this.addFeatureSetRow(container, configuration);
|
||||
}
|
||||
|
||||
private createDefaultPoolConfigurationPart(container: azdata.FlexContainer, configuration: PoolConfiguration): void {
|
||||
this.addPoolNameLabel(container, this.getPoolDisplayName(configuration.type));
|
||||
this.addPoolDescriptionLabel(container, this.getPoolDescription(configuration.type));
|
||||
this.addScaleRow(container, configuration);
|
||||
this.addHardwareLabelRow(container, configuration);
|
||||
}
|
||||
|
||||
private addPoolNameLabel(container: azdata.FlexContainer, text: string): void {
|
||||
let poolNameLabel = this.view.modelBuilder.text().withProperties({ value: text }).component();
|
||||
container.addItem(poolNameLabel, {
|
||||
flex: '0 0 auto', CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'font-weight': 'bold'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private addPoolDescriptionLabel(container: azdata.FlexContainer, text: string): void {
|
||||
let label = this.view.modelBuilder.text().withProperties({ value: text }).component();
|
||||
container.addItem(label, {
|
||||
flex: '0 0 auto',
|
||||
CSSStyles: {
|
||||
'margin-bottom': '20px'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private addScaleRow(container: azdata.FlexContainer, configuration: PoolConfiguration): void {
|
||||
let label = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.ScaleLabel', 'Scale') }).component();
|
||||
label.width = LabelWidth;
|
||||
let input = this.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
inputType: 'number',
|
||||
value: configuration.scale.toString(),
|
||||
min: 1,
|
||||
max: configuration.maxScale
|
||||
}).component();
|
||||
|
||||
this.wizard.registerDisposable(input.onTextChanged(() => {
|
||||
configuration.scale = Number(input.value);
|
||||
this.updatePoolList();
|
||||
}));
|
||||
input.width = InputWidth;
|
||||
let row = this.createRow([label, input]);
|
||||
container.addItem(row);
|
||||
}
|
||||
|
||||
private addHardwareLabelRow(container: azdata.FlexContainer, configuration: PoolConfiguration): void {
|
||||
let label = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.HardwareProfileLabel', 'Hardware profile label') }).component();
|
||||
label.width = LabelWidth;
|
||||
let optionalValues = this.clusterResource.hardwareLabels.map(label => label.name);
|
||||
configuration.hardwareLabel = configuration.hardwareLabel ? configuration.hardwareLabel : optionalValues[0];
|
||||
let input = this.view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({ value: configuration.hardwareLabel, values: optionalValues }).component();
|
||||
this.wizard.registerDisposable(input.onValueChanged(() => {
|
||||
configuration.hardwareLabel = input.value.toString();
|
||||
}));
|
||||
input.width = InputWidth;
|
||||
let row = this.createRow([label, input]);
|
||||
container.addItem(row);
|
||||
}
|
||||
|
||||
private addFeatureSetRow(container: azdata.FlexContainer, configuration: SQLServerMasterConfiguration): void {
|
||||
const radioGroupName = 'featureset';
|
||||
let label = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.FeatureSetLabel', 'Feature set') }).component();
|
||||
label.width = LabelWidth;
|
||||
let engineOnlyOption = this.view.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({ label: localize('bdc-create.EngineOnlyText', 'Engine only'), name: radioGroupName, checked: configuration.engineOnly }).component();
|
||||
let engineWithFeaturesOption = this.view.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({ label: localize('bdc-create.EngineWithFeaturesText', 'Engine with optional features'), name: radioGroupName, checked: !configuration.engineOnly }).component();
|
||||
let optionContainer = this.view.modelBuilder.divContainer().component();
|
||||
optionContainer.width = InputWidth;
|
||||
optionContainer.addItems([engineOnlyOption, engineWithFeaturesOption]);
|
||||
container.addItem(this.createRow([label, optionContainer]));
|
||||
this.wizard.registerDisposable(engineOnlyOption.onDidClick(() => {
|
||||
configuration.engineOnly = true;
|
||||
}));
|
||||
this.wizard.registerDisposable(engineWithFeaturesOption.onDidClick(() => {
|
||||
configuration.engineOnly = false;
|
||||
}));
|
||||
}
|
||||
|
||||
private createRow(items: azdata.Component[]): azdata.FlexContainer {
|
||||
return this.view.modelBuilder.flexContainer().withItems(items, {
|
||||
CSSStyles: {
|
||||
'margin-right': '5px'
|
||||
}
|
||||
}).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
|
||||
}
|
||||
|
||||
private getPoolDisplayName(poolType: ClusterPoolType): string {
|
||||
switch (poolType) {
|
||||
case ClusterPoolType.SQL:
|
||||
return localize('bdc-create.SQLServerMasterDisplayName', 'SQL Server master');
|
||||
case ClusterPoolType.Compute:
|
||||
return localize('bdc-create.ComputePoolDisplayName', 'Compute pool');
|
||||
case ClusterPoolType.Data:
|
||||
return localize('bdc-create.DataPoolDisplayName', 'Data pool');
|
||||
case ClusterPoolType.Storage:
|
||||
return localize('bdc-create.StoragePoolDisplayName', 'Storage pool');
|
||||
case ClusterPoolType.Spark:
|
||||
return localize('bdc-create.SparkPoolDisplayName', 'Spark pool');
|
||||
default:
|
||||
throw new Error('unknown pool type');
|
||||
}
|
||||
}
|
||||
|
||||
private getPoolDescription(poolType: ClusterPoolType): string {
|
||||
switch (poolType) {
|
||||
case ClusterPoolType.SQL:
|
||||
return localize('bdc-create.SQLServerMasterDescription', 'The SQL Server instance provides an externally accessible TDS endpoint for the cluster');
|
||||
case ClusterPoolType.Compute:
|
||||
return localize('bdc-create.ComputePoolDescription', 'TODO: Add description');
|
||||
case ClusterPoolType.Data:
|
||||
return localize('bdc-create.DataPoolDescription', 'TODO: Add description');
|
||||
case ClusterPoolType.Storage:
|
||||
return localize('bdc-create.StoragePoolDescription', 'TODO: Add description');
|
||||
case ClusterPoolType.Spark:
|
||||
return localize('bdc-create.SparkPoolDescription', 'TODO: Add description');
|
||||
default:
|
||||
throw new Error('unknown pool type');
|
||||
}
|
||||
}
|
||||
|
||||
private updatePoolList(): void {
|
||||
let pools = [this.wizard.model.profile.sqlServerMasterConfiguration,
|
||||
this.wizard.model.profile.computePoolConfiguration,
|
||||
this.wizard.model.profile.dataPoolConfiguration,
|
||||
this.wizard.model.profile.sparkPoolConfiguration,
|
||||
this.wizard.model.profile.storagePoolConfiguration];
|
||||
pools.forEach(pool => {
|
||||
let text = this.poolListMap[pool.type] as azdata.TextComponent;
|
||||
if (text) {
|
||||
text.value = localize({
|
||||
key: 'bdc-create.poolLabelTemplate',
|
||||
comment: ['{0} is the pool name, {1} is the scale number']
|
||||
}, '{0} ({1})', this.getPoolDisplayName(pool.type), pool.scale);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private clearPoolDetail(): void {
|
||||
this.detailContainer.clearItems();
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import { WizardPageBase } from '../../wizardPageBase';
|
||||
import { CreateClusterWizard } from '../createClusterWizard';
|
||||
import { setActiveKubeconfig } from '../../../config/config';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const ClusterRadioButtonGroupName = 'cluster';
|
||||
|
||||
export class SelectExistingClusterPage extends WizardPageBase<CreateClusterWizard> {
|
||||
private existingClusterControl: azdata.FlexContainer;
|
||||
private clusterContextsLabel: azdata.TextComponent;
|
||||
private errorLoadingClustersLabel: azdata.TextComponent;
|
||||
private clusterContextList: azdata.DivContainer;
|
||||
private clusterContextLoadingComponent: azdata.LoadingComponent;
|
||||
private configFileInput: azdata.InputBoxComponent;
|
||||
private browseFileButton: azdata.ButtonComponent;
|
||||
private loadDefaultKubeConfigFile: boolean = true;
|
||||
private view: azdata.ModelView;
|
||||
|
||||
constructor(wizard: CreateClusterWizard) {
|
||||
super(localize('bdc-create.selectTargetClusterPageTitle', 'Where do you want to deploy this SQL Server big data cluster?'),
|
||||
localize('bdc-create.selectTargetClusterPageDescription', 'Select the kubeconfig file and then select a cluster context from the list'),
|
||||
wizard);
|
||||
}
|
||||
|
||||
protected initialize(view: azdata.ModelView): Thenable<void> {
|
||||
this.view = view;
|
||||
this.initExistingClusterControl();
|
||||
let formBuilder = view.modelBuilder.formContainer().withFormItems(
|
||||
[
|
||||
{
|
||||
component: this.existingClusterControl,
|
||||
title: ''
|
||||
}
|
||||
],
|
||||
{
|
||||
horizontal: false
|
||||
}
|
||||
).withLayout({ width: '100%', height: '100%' });
|
||||
|
||||
let form = formBuilder.component();
|
||||
return view.initializeModel(form);
|
||||
}
|
||||
|
||||
public onEnter() {
|
||||
if (this.loadDefaultKubeConfigFile) {
|
||||
let defaultKubeConfigPath = this.wizard.model.getDefaultKubeConfigPath();
|
||||
if (fs.existsSync(defaultKubeConfigPath)) {
|
||||
this.loadClusterContexts(defaultKubeConfigPath);
|
||||
}
|
||||
this.loadDefaultKubeConfigFile = false;
|
||||
}
|
||||
|
||||
this.wizard.wizardObject.registerNavigationValidator((e) => {
|
||||
if (e.lastPage > e.newPage) {
|
||||
this.wizard.wizardObject.message = null;
|
||||
return true;
|
||||
}
|
||||
let clusterSelected = this.wizard.model.selectedCluster !== undefined;
|
||||
if (!clusterSelected) {
|
||||
this.wizard.wizardObject.message = {
|
||||
text: localize('bdc-create.ClusterContextNotSelectedMessage', 'Please select a cluster context.'),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
}
|
||||
return clusterSelected;
|
||||
});
|
||||
}
|
||||
|
||||
private initExistingClusterControl(): void {
|
||||
let self = this;
|
||||
const labelWidth = '150px';
|
||||
let configFileLabel = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.kubeConfigFileLabelText', 'Kube config file path') }).component();
|
||||
configFileLabel.width = labelWidth;
|
||||
this.configFileInput = this.view.modelBuilder.inputBox().withProperties({ width: '300px' }).component();
|
||||
this.configFileInput.enabled = false;
|
||||
this.browseFileButton = this.view.modelBuilder.button().withProperties({ label: localize('bdc-browseText', 'Browse'), width: '100px' }).component();
|
||||
let configFileContainer = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'row', alignItems: 'baseline' })
|
||||
.withItems([configFileLabel, this.configFileInput, this.browseFileButton], { CSSStyles: { 'margin-right': '10px' } }).component();
|
||||
this.clusterContextsLabel = this.view.modelBuilder.text().withProperties({ value: localize('bdc-clusterContextsLabelText', 'Cluster Contexts') }).component();
|
||||
this.clusterContextsLabel.width = labelWidth;
|
||||
this.errorLoadingClustersLabel = this.view.modelBuilder.text().withProperties({ value: localize('bdc-errorLoadingClustersText', 'No cluster information is found in the config file or an error ocurred while loading the config file') }).component();
|
||||
this.clusterContextList = this.view.modelBuilder.divContainer().component();
|
||||
this.clusterContextLoadingComponent = this.view.modelBuilder.loadingComponent().withItem(this.clusterContextList).component();
|
||||
this.existingClusterControl = this.view.modelBuilder.divContainer().component();
|
||||
let clusterContextContainer = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'start' }).component();
|
||||
clusterContextContainer.addItem(this.clusterContextsLabel, { flex: '0 0 auto' });
|
||||
clusterContextContainer.addItem(this.clusterContextLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'width': '400px', 'margin-left': '10px', 'margin-top': '10px' } });
|
||||
|
||||
this.existingClusterControl.addItem(configFileContainer, { CSSStyles: { 'margin-top': '0px' } });
|
||||
this.existingClusterControl.addItem(clusterContextContainer, {
|
||||
CSSStyles: { 'margin- top': '10px' }
|
||||
});
|
||||
|
||||
this.wizard.registerDisposable(this.browseFileButton.onDidClick(async () => {
|
||||
let fileUris = await vscode.window.showOpenDialog(
|
||||
{
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
defaultUri: vscode.Uri.file(os.homedir()),
|
||||
openLabel: localize('bdc-selectKubeConfigFileText', 'Select'),
|
||||
filters: {
|
||||
'KubeConfig Files': ['*'],
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!fileUris || fileUris.length === 0) {
|
||||
return;
|
||||
}
|
||||
self.clusterContextList.clearItems();
|
||||
|
||||
let fileUri = fileUris[0];
|
||||
|
||||
self.loadClusterContexts(fileUri.fsPath);
|
||||
}));
|
||||
}
|
||||
|
||||
private async loadClusterContexts(configPath: string): Promise<void> {
|
||||
this.clusterContextLoadingComponent.loading = true;
|
||||
let self = this;
|
||||
this.configFileInput.value = configPath;
|
||||
await setActiveKubeconfig(configPath);
|
||||
|
||||
let clusters = await this.wizard.model.loadClusters();
|
||||
if (clusters.length !== 0) {
|
||||
let options = clusters.map(cluster => {
|
||||
let option = this.view.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
|
||||
label: cluster.contextName,
|
||||
checked: cluster.active,
|
||||
name: ClusterRadioButtonGroupName
|
||||
}).component();
|
||||
|
||||
if (cluster.active) {
|
||||
self.wizard.model.selectedCluster = cluster;
|
||||
self.wizard.wizardObject.message = null;
|
||||
}
|
||||
|
||||
this.wizard.registerDisposable(option.onDidClick(() => {
|
||||
self.wizard.model.selectedCluster = cluster;
|
||||
self.wizard.wizardObject.message = null;
|
||||
}));
|
||||
return option;
|
||||
});
|
||||
self.clusterContextList.addItems(options);
|
||||
} else {
|
||||
self.clusterContextList.addItem(this.errorLoadingClustersLabel);
|
||||
}
|
||||
this.clusterContextLoadingComponent.loading = false;
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 azdata from 'azdata';
|
||||
import { WizardPageBase } from '../../wizardPageBase';
|
||||
import { TargetClusterTypeInfo, ToolInstallationStatus, ToolInfo } from '../../../interfaces';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { CreateClusterWizard } from '../createClusterWizard';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const InstallToolsButtonText = localize('bdc-create.InstallToolsText', 'Install Tools');
|
||||
const InstallingButtonText = localize('bdc-create.InstallingButtonText', 'Installing...');
|
||||
|
||||
export class SelectTargetClusterTypePage extends WizardPageBase<CreateClusterWizard> {
|
||||
private cards: azdata.CardComponent[];
|
||||
private toolsTable: azdata.TableComponent;
|
||||
private formBuilder: azdata.FormBuilder;
|
||||
private form: azdata.FormContainer;
|
||||
private installToolsButton: azdata.window.Button;
|
||||
private toolsLoadingWrapper: azdata.LoadingComponent;
|
||||
private refreshToolsButton: azdata.window.Button;
|
||||
private targetDescriptionText: azdata.TextComponent;
|
||||
private targetDescriptionGroup: azdata.FormComponent;
|
||||
private isValid: boolean = false;
|
||||
private isLoading: boolean = false;
|
||||
private requiredTools: ToolInfo[];
|
||||
|
||||
constructor(wizard: CreateClusterWizard) {
|
||||
super(localize('bdc-create.selectTargetClusterTypePageTitle', 'What is your target cluster environment?'),
|
||||
localize('bdc-create.selectTargetClusterTypePageDescription', 'Choose the target environment and then install the required tools for it.'),
|
||||
wizard);
|
||||
this.installToolsButton = azdata.window.createButton(InstallToolsButtonText);
|
||||
this.installToolsButton.hidden = true;
|
||||
this.wizard.registerDisposable(this.installToolsButton.onClick(async () => {
|
||||
this.wizard.wizardObject.message = null;
|
||||
this.installToolsButton.label = InstallingButtonText;
|
||||
this.installToolsButton.enabled = false;
|
||||
this.refreshToolsButton.enabled = false;
|
||||
if (this.requiredTools) {
|
||||
for (let i = 0; i < this.requiredTools.length; i++) {
|
||||
let tool = this.requiredTools[i];
|
||||
if (tool.status === ToolInstallationStatus.NotInstalled) {
|
||||
tool.status = ToolInstallationStatus.Installing;
|
||||
this.updateToolStatusTable();
|
||||
await this.wizard.model.installTool(tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.installToolsButton.label = InstallToolsButtonText;
|
||||
this.updateRequiredToolStatus();
|
||||
}));
|
||||
this.wizard.addButton(this.installToolsButton);
|
||||
|
||||
this.refreshToolsButton = azdata.window.createButton(localize('bdc-create.RefreshToolsButtonText', 'Refresh Status'));
|
||||
this.refreshToolsButton.hidden = true;
|
||||
this.wizard.registerDisposable(this.refreshToolsButton.onClick(() => {
|
||||
this.updateRequiredToolStatus();
|
||||
}));
|
||||
this.wizard.addButton(this.refreshToolsButton);
|
||||
}
|
||||
|
||||
protected initialize(view: azdata.ModelView): Thenable<void> {
|
||||
let self = this;
|
||||
self.registerNavigationValidator();
|
||||
return self.wizard.model.getAllTargetClusterTypeInfo().then((clusterTypes) => {
|
||||
self.cards = [];
|
||||
|
||||
clusterTypes.forEach(clusterType => {
|
||||
let card = self.createCard(view, clusterType);
|
||||
self.cards.push(card);
|
||||
});
|
||||
let cardsContainer = view.modelBuilder.flexContainer().withItems(self.cards, { flex: '0 0 auto', CSSStyles: { 'margin-bottom': '20px' } }).withLayout({ flexFlow: 'row', alignItems: 'left' }).component();
|
||||
|
||||
self.targetDescriptionText = view.modelBuilder.text().component();
|
||||
|
||||
let toolColumn: azdata.TableColumn = {
|
||||
value: localize('bdc-create.toolNameColumnHeader', 'Tool'),
|
||||
width: 100
|
||||
};
|
||||
let descriptionColumn: azdata.TableColumn = {
|
||||
value: localize('bdc-create.toolDescriptionColumnHeader', 'Description'),
|
||||
width: 500
|
||||
};
|
||||
let versionColumn: azdata.TableColumn = {
|
||||
value: localize('bdc-create.toolVersionColumnHeader', 'Version'),
|
||||
width: 200
|
||||
};
|
||||
let statusColumn: azdata.TableColumn = {
|
||||
value: localize('bdc-create.toolStatusColumnHeader', 'Status'),
|
||||
width: 200
|
||||
};
|
||||
|
||||
self.toolsTable = view.modelBuilder.table().withProperties<azdata.TableComponentProperties>({
|
||||
height: 150,
|
||||
data: [],
|
||||
columns: [toolColumn, descriptionColumn, versionColumn, statusColumn],
|
||||
width: 1000
|
||||
}).component();
|
||||
|
||||
self.toolsLoadingWrapper = view.modelBuilder.loadingComponent().withItem(self.toolsTable).component();
|
||||
self.formBuilder = view.modelBuilder.formContainer().withFormItems(
|
||||
[
|
||||
{
|
||||
component: cardsContainer,
|
||||
title: localize('bdc-create.PickTargetEnvironmentText', 'Pick target environment')
|
||||
}
|
||||
],
|
||||
{
|
||||
horizontal: false
|
||||
}
|
||||
);
|
||||
|
||||
self.form = self.formBuilder.withLayout({ width: '100%' }).component();
|
||||
return view.initializeModel(self.form);
|
||||
});
|
||||
}
|
||||
|
||||
public onEnter(): void {
|
||||
this.installToolsButton.hidden = false;
|
||||
this.refreshToolsButton.hidden = false;
|
||||
this.refreshToolsButton.enabled = true;
|
||||
this.installToolsButton.enabled = false;
|
||||
this.registerNavigationValidator();
|
||||
}
|
||||
|
||||
private registerNavigationValidator(): void {
|
||||
this.wizard.wizardObject.registerNavigationValidator(() => {
|
||||
if (this.isLoading) {
|
||||
let messageText = localize('bdc-create.ToolsRefreshingText', 'Please wait while the required tools status is being refreshed.');
|
||||
let messageLevel = azdata.window.MessageLevel.Information;
|
||||
this.wizard.wizardObject.message = {
|
||||
level: messageLevel,
|
||||
text: messageText
|
||||
};
|
||||
return false;
|
||||
}
|
||||
if (!this.isValid) {
|
||||
let messageText = this.cards.filter(c => { return c.selected; }).length === 0 ?
|
||||
localize('bdc-create.TargetClusterTypeNotSelectedText', 'Please select a target cluster type.') :
|
||||
localize('bdc-create.MissingToolsText', 'Please install the required tools.');
|
||||
this.wizard.wizardObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: messageText
|
||||
};
|
||||
}
|
||||
return this.isValid;
|
||||
});
|
||||
}
|
||||
|
||||
public onLeave(): void {
|
||||
this.installToolsButton.hidden = true;
|
||||
this.refreshToolsButton.hidden = true;
|
||||
}
|
||||
|
||||
private createCard(view: azdata.ModelView, targetClusterTypeInfo: TargetClusterTypeInfo): azdata.CardComponent {
|
||||
let self = this;
|
||||
let descriptions = targetClusterTypeInfo.enabled ? [] : [localize('bdc-create.ComingSoonText', '(Coming Soon)')];
|
||||
let card = view.modelBuilder.card().withProperties<azdata.CardProperties>({
|
||||
cardType: azdata.CardType.VerticalButton,
|
||||
iconPath: {
|
||||
dark: self.wizard.context.asAbsolutePath(targetClusterTypeInfo.iconPath.dark),
|
||||
light: self.wizard.context.asAbsolutePath(targetClusterTypeInfo.iconPath.light)
|
||||
},
|
||||
label: targetClusterTypeInfo.name,
|
||||
descriptions: descriptions
|
||||
}).component();
|
||||
|
||||
card.enabled = targetClusterTypeInfo.enabled;
|
||||
|
||||
self.wizard.registerDisposable(card.onCardSelectedChanged(() => {
|
||||
self.onCardSelected(card, targetClusterTypeInfo);
|
||||
}));
|
||||
return card;
|
||||
}
|
||||
|
||||
private onCardSelected(card: azdata.CardComponent, targetClusterTypeInfo: TargetClusterTypeInfo): void {
|
||||
let self = this;
|
||||
if (card.selected) {
|
||||
self.wizard.wizardObject.message = null;
|
||||
self.wizard.model.targetClusterType = targetClusterTypeInfo.type;
|
||||
self.cards.forEach(c => {
|
||||
if (c !== card) {
|
||||
c.selected = false;
|
||||
}
|
||||
});
|
||||
|
||||
self.targetDescriptionText.value = targetClusterTypeInfo.description;
|
||||
|
||||
if (self.form.items.length === 1) {
|
||||
self.formBuilder.addFormItem({
|
||||
title: localize('bdc-create.RequiredToolsText', 'Required tools'),
|
||||
component: self.toolsLoadingWrapper
|
||||
});
|
||||
} else {
|
||||
self.formBuilder.removeFormItem(self.targetDescriptionGroup);
|
||||
}
|
||||
|
||||
self.targetDescriptionGroup = {
|
||||
title: targetClusterTypeInfo.fullName,
|
||||
component: self.targetDescriptionText
|
||||
};
|
||||
self.formBuilder.insertFormItem(self.targetDescriptionGroup, 1);
|
||||
|
||||
self.updateRequiredToolStatus();
|
||||
} else {
|
||||
if (self.cards.filter(c => { return c !== card && c.selected; }).length === 0) {
|
||||
card.selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateRequiredToolStatus(): Thenable<void> {
|
||||
this.isLoading = true;
|
||||
this.installToolsButton.hidden = false;
|
||||
this.refreshToolsButton.hidden = false;
|
||||
this.toolsLoadingWrapper.loading = true;
|
||||
this.refreshToolsButton.enabled = false;
|
||||
this.installToolsButton.enabled = false;
|
||||
return this.wizard.model.getRequiredToolStatus().then(tools => {
|
||||
this.requiredTools = tools;
|
||||
this.isLoading = false;
|
||||
this.toolsLoadingWrapper.loading = false;
|
||||
this.refreshToolsButton.enabled = true;
|
||||
this.installToolsButton.enabled = tools.filter(tool => tool.status !== ToolInstallationStatus.Installed).length !== 0;
|
||||
this.isValid = !this.installToolsButton.enabled;
|
||||
this.wizard.wizardObject.message = null;
|
||||
this.updateToolStatusTable();
|
||||
});
|
||||
}
|
||||
|
||||
private getStatusText(status: ToolInstallationStatus): string {
|
||||
switch (status) {
|
||||
case ToolInstallationStatus.Installed:
|
||||
return '✔️ ' + localize('bdc-create.InstalledText', 'Installed');
|
||||
case ToolInstallationStatus.NotInstalled:
|
||||
return '❌ ' + localize('bdc-create.NotInstalledText', 'Not Installed');
|
||||
case ToolInstallationStatus.Installing:
|
||||
return '⌛ ' + localize('bdc-create.InstallingText', 'Installing...');
|
||||
case ToolInstallationStatus.FailedToInstall:
|
||||
return '❌ ' + localize('bdc-create.FailedToInstallText', 'Install Failed');
|
||||
default:
|
||||
return 'unknown status';
|
||||
}
|
||||
}
|
||||
|
||||
private updateToolStatusTable(): void {
|
||||
if (this.requiredTools) {
|
||||
let tableData = this.requiredTools.map(tool => {
|
||||
return [tool.name, tool.description, tool.version, this.getStatusText(tool.status)];
|
||||
});
|
||||
this.toolsTable.data = tableData;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 azdata from 'azdata';
|
||||
import { WizardPageBase } from '../../wizardPageBase';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { ClusterPorts, ContainerRegistryInfo } from '../../../interfaces';
|
||||
import { CreateClusterWizard } from '../createClusterWizard';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const UserNameInputWidth = '300px';
|
||||
const PortInputWidth = '100px';
|
||||
const RestoreDefaultValuesText = localize('bdc-create.RestoreDefaultValuesText', 'Restore Default Values');
|
||||
|
||||
export class SettingsPage extends WizardPageBase<CreateClusterWizard> {
|
||||
private acceptEulaCheckbox: azdata.CheckBoxComponent;
|
||||
|
||||
constructor(wizard: CreateClusterWizard) {
|
||||
super(localize('bdc-create.settingsPageTitle', 'Settings'),
|
||||
localize('bdc-create.settingsPageDescription', 'Configure the settings required for deploying SQL Server big data cluster'),
|
||||
wizard);
|
||||
}
|
||||
|
||||
public onEnter(): void {
|
||||
this.wizard.wizardObject.registerNavigationValidator((e) => {
|
||||
if (e.lastPage > e.newPage) {
|
||||
this.wizard.wizardObject.message = null;
|
||||
return true;
|
||||
}
|
||||
if (!this.acceptEulaCheckbox.checked) {
|
||||
this.wizard.wizardObject.message = {
|
||||
text: localize('bdc-create.EulaNotAccepted', 'You need to accept the terms of services and privacy policy in order to proceed'),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
} else {
|
||||
this.wizard.wizardObject.message = null;
|
||||
}
|
||||
return this.acceptEulaCheckbox.checked;
|
||||
});
|
||||
}
|
||||
|
||||
protected initialize(view: azdata.ModelView): Thenable<void> {
|
||||
let clusterPorts: ClusterPorts;
|
||||
let containerRegistryInfo: ContainerRegistryInfo;
|
||||
|
||||
let clusterPortsPromise = this.wizard.model.getDefaultPorts().then(ports => {
|
||||
clusterPorts = ports;
|
||||
});
|
||||
|
||||
let containerRegistryPromise = this.wizard.model.getDefaultContainerRegistryInfo().then(containerRegistry => {
|
||||
containerRegistryInfo = containerRegistry;
|
||||
});
|
||||
return Promise.all([clusterPortsPromise, containerRegistryPromise]).then(() => {
|
||||
let formBuilder = view.modelBuilder.formContainer();
|
||||
|
||||
// User settings
|
||||
let clusterNameInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.ClusterName', 'Cluster name'),
|
||||
inputWidth: UserNameInputWidth,
|
||||
isRequiredField: true
|
||||
}, (input) => {
|
||||
this.wizard.model.clusterName = input.value;
|
||||
});
|
||||
|
||||
let adminUserNameInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.AdminUsernameText', 'Admin username'),
|
||||
isRequiredField: true,
|
||||
inputWidth: UserNameInputWidth
|
||||
}, (inputBox: azdata.InputBoxComponent) => {
|
||||
this.wizard.model.adminUserName = inputBox.value;
|
||||
});
|
||||
let adminPasswordInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.AdminUserPasswordText', 'Password'),
|
||||
isRequiredField: true,
|
||||
inputType: 'password',
|
||||
inputWidth: UserNameInputWidth
|
||||
}, (inputBox: azdata.InputBoxComponent) => {
|
||||
this.wizard.model.adminPassword = inputBox.value;
|
||||
});
|
||||
|
||||
// Port settings
|
||||
let sqlPortInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.SQLPortText', 'SQL Server master'),
|
||||
isRequiredField: true,
|
||||
inputWidth: PortInputWidth,
|
||||
initialValue: clusterPorts.sql
|
||||
}, (inputBox: azdata.InputBoxComponent) => {
|
||||
this.wizard.model.sqlPort = inputBox.value;
|
||||
});
|
||||
let knoxPortInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.KnoxPortText', 'Knox'),
|
||||
isRequiredField: true,
|
||||
inputWidth: PortInputWidth,
|
||||
initialValue: clusterPorts.knox
|
||||
}, (inputBox: azdata.InputBoxComponent) => {
|
||||
this.wizard.model.knoxPort = inputBox.value;
|
||||
});
|
||||
let controllerPortInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.ControllerPortText', 'Controller'),
|
||||
isRequiredField: true,
|
||||
inputWidth: PortInputWidth,
|
||||
initialValue: clusterPorts.controller
|
||||
}, (inputBox: azdata.InputBoxComponent) => {
|
||||
this.wizard.model.controllerPort = inputBox.value;
|
||||
});
|
||||
let proxyPortInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.ProxyPortText', 'Proxy'),
|
||||
isRequiredField: true,
|
||||
inputWidth: PortInputWidth,
|
||||
initialValue: clusterPorts.proxy
|
||||
}, (inputBox: azdata.InputBoxComponent) => {
|
||||
this.wizard.model.proxyPort = inputBox.value;
|
||||
});
|
||||
let grafanaPortInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.GrafanaPortText', 'Grafana dashboard'),
|
||||
isRequiredField: true,
|
||||
inputWidth: PortInputWidth,
|
||||
initialValue: clusterPorts.grafana
|
||||
}, (inputBox: azdata.InputBoxComponent) => {
|
||||
this.wizard.model.grafanaPort = inputBox.value;
|
||||
});
|
||||
let kibanaPortInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.KibanaPortText', 'Kibana dashboard'),
|
||||
isRequiredField: true,
|
||||
inputWidth: PortInputWidth,
|
||||
initialValue: clusterPorts.kibana
|
||||
}, (inputBox: azdata.InputBoxComponent) => {
|
||||
this.wizard.model.kibanaPort = inputBox.value;
|
||||
});
|
||||
let restorePortSettingsButton = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: RestoreDefaultValuesText,
|
||||
width: 200
|
||||
}).component();
|
||||
this.wizard.registerDisposable(restorePortSettingsButton.onDidClick(() => {
|
||||
sqlPortInput.input.value = clusterPorts.sql;
|
||||
knoxPortInput.input.value = clusterPorts.knox;
|
||||
controllerPortInput.input.value = clusterPorts.controller;
|
||||
proxyPortInput.input.value = clusterPorts.proxy;
|
||||
grafanaPortInput.input.value = clusterPorts.grafana;
|
||||
kibanaPortInput.input.value = clusterPorts.kibana;
|
||||
}));
|
||||
|
||||
// Container Registry Settings
|
||||
const registryUserNamePasswordHintText = localize('bdc-create.RegistryUserNamePasswordHintText', 'only required for private registries');
|
||||
let registryInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.RegistryText', 'Registry'),
|
||||
isRequiredField: true,
|
||||
inputWidth: UserNameInputWidth,
|
||||
initialValue: containerRegistryInfo.registry
|
||||
}, (inputBox: azdata.InputBoxComponent) => {
|
||||
this.wizard.model.containerRegistry = inputBox.value;
|
||||
});
|
||||
|
||||
let repositoryInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.RepositoryText', 'Repository'),
|
||||
isRequiredField: true,
|
||||
inputWidth: UserNameInputWidth,
|
||||
initialValue: containerRegistryInfo.repository
|
||||
}, (inputBox: azdata.InputBoxComponent) => {
|
||||
this.wizard.model.containerRepository = inputBox.value;
|
||||
});
|
||||
|
||||
let imageTagInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.ImageTagText', 'Image tag'),
|
||||
isRequiredField: true,
|
||||
inputWidth: UserNameInputWidth,
|
||||
initialValue: containerRegistryInfo.imageTag
|
||||
}, (inputBox: azdata.InputBoxComponent) => {
|
||||
this.wizard.model.containerRegistry = inputBox.value;
|
||||
});
|
||||
|
||||
let registryUserNameInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.RegistryUserNameText', 'Username'),
|
||||
isRequiredField: false,
|
||||
inputWidth: UserNameInputWidth,
|
||||
placeHolder: registryUserNamePasswordHintText
|
||||
}, (inputBox: azdata.InputBoxComponent) => {
|
||||
this.wizard.model.containerRegistryUserName = inputBox.value;
|
||||
});
|
||||
|
||||
let registryPasswordInput = this.createInputWithLabel(view, {
|
||||
label: localize('bdc-create.RegistryPasswordText', 'Password'),
|
||||
isRequiredField: false,
|
||||
inputWidth: UserNameInputWidth,
|
||||
placeHolder: registryUserNamePasswordHintText,
|
||||
inputType: 'password'
|
||||
}, (inputBox: azdata.InputBoxComponent) => {
|
||||
this.wizard.model.containerRegistryPassword = inputBox.value;
|
||||
});
|
||||
let restoreContainerSettingsButton = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: RestoreDefaultValuesText,
|
||||
width: 200
|
||||
}).component();
|
||||
this.wizard.registerDisposable(restoreContainerSettingsButton.onDidClick(() => {
|
||||
registryInput.input.value = containerRegistryInfo.registry;
|
||||
repositoryInput.input.value = containerRegistryInfo.repository;
|
||||
imageTagInput.input.value = containerRegistryInfo.imageTag;
|
||||
}));
|
||||
|
||||
let basicSettingsGroup = view.modelBuilder.groupContainer().withItems([clusterNameInput.row, adminUserNameInput.row, adminPasswordInput.row]).withLayout({ header: localize('bdc-create.BasicSettingsText', 'Basic Settings'), collapsible: true }).component();
|
||||
let containerSettingsGroup = view.modelBuilder.groupContainer().withItems([registryInput.row, repositoryInput.row, imageTagInput.row, registryUserNameInput.row, registryPasswordInput.row, restoreContainerSettingsButton]).withLayout({ header: localize('bdc-create.ContainerRegistrySettings', 'Container Registry Settings'), collapsible: true }).component();
|
||||
let portSettingsGroup = view.modelBuilder.groupContainer().withItems([sqlPortInput.row, knoxPortInput.row, controllerPortInput.row, proxyPortInput.row, grafanaPortInput.row, kibanaPortInput.row, restorePortSettingsButton]).withLayout({ header: localize('bdc-create.PortSettings', 'Port Settings (Optional)'), collapsible: true, collapsed: true }).component();
|
||||
|
||||
this.acceptEulaCheckbox = view.modelBuilder.checkBox().component();
|
||||
this.acceptEulaCheckbox.checked = false;
|
||||
|
||||
let eulaLink: azdata.LinkArea = {
|
||||
text: localize('bdc-create.LicenseTerms', 'license terms'),
|
||||
url: 'https://go.microsoft.com/fwlink/?LinkId=2002534'
|
||||
};
|
||||
let privacyPolicyLink: azdata.LinkArea = {
|
||||
text: localize('bdc-create.PrivacyPolicyText', 'privacy policy'),
|
||||
url: 'https://go.microsoft.com/fwlink/?LinkId=853010'
|
||||
};
|
||||
|
||||
let checkboxText = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: localize({
|
||||
key: 'bdc-create.AcceptTermsText',
|
||||
comment: ['{0} is the place holder for license terms, {1} is the place holder for privacy policy']
|
||||
}, 'I accept the {0} and {1}.'),
|
||||
links: [eulaLink, privacyPolicyLink]
|
||||
}).component();
|
||||
|
||||
let eulaContainer = this.createRow(view, [this.acceptEulaCheckbox, checkboxText]);
|
||||
|
||||
let form = formBuilder.withFormItems([
|
||||
{
|
||||
title: '',
|
||||
component: eulaContainer
|
||||
}, {
|
||||
title: '',
|
||||
component: basicSettingsGroup
|
||||
}, {
|
||||
title: '',
|
||||
component: containerSettingsGroup
|
||||
}, {
|
||||
title: '',
|
||||
component: portSettingsGroup
|
||||
}]).component();
|
||||
return view.initializeModel(form);
|
||||
});
|
||||
}
|
||||
|
||||
private createInputWithLabel(view: azdata.ModelView, options: {
|
||||
label: string,
|
||||
isRequiredField: boolean,
|
||||
inputWidth: string,
|
||||
inputType?: string,
|
||||
initialValue?: string,
|
||||
placeHolder?: string
|
||||
}, textChangedHandler: (inputBox: azdata.InputBoxComponent) => void): { row: azdata.FlexContainer, input: azdata.InputBoxComponent } {
|
||||
let inputType = !!options.inputType ? options.inputType : 'text';
|
||||
let input = view.modelBuilder.inputBox().withProperties({
|
||||
required: options.isRequiredField,
|
||||
inputType: inputType
|
||||
}).component();
|
||||
let text = view.modelBuilder.text().withProperties({ value: options.label }).component();
|
||||
input.width = options.inputWidth;
|
||||
text.width = '150px';
|
||||
input.placeHolder = options.placeHolder;
|
||||
this.wizard.registerDisposable(input.onTextChanged(() => {
|
||||
textChangedHandler(input);
|
||||
}));
|
||||
input.value = options.initialValue;
|
||||
let row = this.createRow(view, [text, input]);
|
||||
return {
|
||||
input: input,
|
||||
row: row
|
||||
};
|
||||
}
|
||||
|
||||
private createRow(view: azdata.ModelView, items: azdata.Component[]): azdata.FlexContainer {
|
||||
return view.modelBuilder.flexContainer().withItems(items, { CSSStyles: { 'margin-right': '5px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { WizardPageBase } from '../../wizardPageBase';
|
||||
import { CreateClusterWizard } from '../createClusterWizard';
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const LabelWidth = '250px';
|
||||
|
||||
export class SummaryPage extends WizardPageBase<CreateClusterWizard> {
|
||||
private view: azdata.ModelView;
|
||||
private targetTypeText: azdata.TextComponent;
|
||||
private targetClusterContextText: azdata.TextComponent;
|
||||
private clusterNameText: azdata.TextComponent;
|
||||
private clusterAdminUsernameText: azdata.TextComponent;
|
||||
private acceptEulaText: azdata.TextComponent;
|
||||
private deploymentProfileText: azdata.TextComponent;
|
||||
private sqlServerMasterScaleText: azdata.TextComponent;
|
||||
private storagePoolScaleText: azdata.TextComponent;
|
||||
private computePoolScaleText: azdata.TextComponent;
|
||||
private dataPoolScaleText: azdata.TextComponent;
|
||||
private sparkPoolScaleText: azdata.TextComponent;
|
||||
|
||||
constructor(wizard: CreateClusterWizard) {
|
||||
super(localize('bdc-create.summaryPageTitle', 'Summary'), '', wizard);
|
||||
}
|
||||
|
||||
protected initialize(view: azdata.ModelView): Thenable<void> {
|
||||
this.view = view;
|
||||
let targetClusterInfoGroup = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
let bdcClusterInfoGroup = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
this.targetTypeText = this.view.modelBuilder.text().component();
|
||||
this.targetClusterContextText = this.view.modelBuilder.text().component();
|
||||
this.clusterNameText = this.view.modelBuilder.text().component();
|
||||
this.clusterAdminUsernameText = this.view.modelBuilder.text().component();
|
||||
this.acceptEulaText = this.view.modelBuilder.text().component();
|
||||
this.deploymentProfileText = this.view.modelBuilder.text().component();
|
||||
this.sqlServerMasterScaleText = this.view.modelBuilder.text().component();
|
||||
this.storagePoolScaleText = this.view.modelBuilder.text().component();
|
||||
this.computePoolScaleText = this.view.modelBuilder.text().component();
|
||||
this.dataPoolScaleText = this.view.modelBuilder.text().component();
|
||||
this.sparkPoolScaleText = this.view.modelBuilder.text().component();
|
||||
targetClusterInfoGroup.addItem(this.createRow(localize('bdc-create.TargetClusterTypeText', 'Cluster type'), this.targetTypeText));
|
||||
targetClusterInfoGroup.addItem(this.createRow(localize('bdc-create.ClusterContextText', 'Cluster context'), this.targetClusterContextText));
|
||||
|
||||
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.ClusterNameText', 'Cluster name'), this.clusterNameText));
|
||||
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.ClusterAdminUsernameText', 'Cluster Admin username'), this.clusterAdminUsernameText));
|
||||
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.AcceptEulaText', 'Accept license agreement'), this.acceptEulaText));
|
||||
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.DeploymentProfileText', 'Deployment profile'), this.deploymentProfileText));
|
||||
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.SqlServerMasterScaleText', 'SQL Server master scale'), this.sqlServerMasterScaleText));
|
||||
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.ComputePoolScaleText', 'Compute pool scale'), this.computePoolScaleText));
|
||||
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.DataPoolScaleText', 'Data pool scale'), this.dataPoolScaleText));
|
||||
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.StoragePoolScaleText', 'Storage pool scale'), this.storagePoolScaleText));
|
||||
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.SparkPoolScaleText', 'Spark pool scale'), this.sparkPoolScaleText));
|
||||
|
||||
let formBuilder = view.modelBuilder.formContainer();
|
||||
let form = formBuilder.withFormItems([{
|
||||
title: localize('bdc-create.TargetClusterGroupTitle', 'TARGET CLUSTER'),
|
||||
component: targetClusterInfoGroup
|
||||
}, {
|
||||
title: localize('bdc-create.BigDataClusterGroupTitle', 'SQL SERVER BIG DATA CLUSTER'),
|
||||
component: bdcClusterInfoGroup
|
||||
}]).component();
|
||||
|
||||
return view.initializeModel(form);
|
||||
}
|
||||
|
||||
public onEnter(): void {
|
||||
this.wizard.model.getAllTargetClusterTypeInfo().then((clusterTypes) => {
|
||||
let selectedClusterType = clusterTypes.filter(clusterType => clusterType.type === this.wizard.model.targetClusterType)[0];
|
||||
this.targetTypeText.value = selectedClusterType.fullName;
|
||||
this.targetClusterContextText.value = this.wizard.model.selectedCluster.contextName;
|
||||
this.clusterNameText.value = this.wizard.model.clusterName;
|
||||
this.clusterAdminUsernameText.value = this.wizard.model.adminUserName;
|
||||
this.acceptEulaText.value = localize('bdc-create.YesText', 'Yes');
|
||||
this.deploymentProfileText.value = this.wizard.model.profile.name;
|
||||
this.sqlServerMasterScaleText.value = this.wizard.model.profile.sqlServerMasterConfiguration.scale.toString();
|
||||
this.computePoolScaleText.value = this.wizard.model.profile.computePoolConfiguration.scale.toString();
|
||||
this.dataPoolScaleText.value = this.wizard.model.profile.dataPoolConfiguration.scale.toString();
|
||||
this.storagePoolScaleText.value = this.wizard.model.profile.storagePoolConfiguration.scale.toString();
|
||||
this.sparkPoolScaleText.value = this.wizard.model.profile.sparkPoolConfiguration.scale.toString();
|
||||
|
||||
});
|
||||
this.wizard.wizardObject.generateScriptButton.hidden = false;
|
||||
}
|
||||
|
||||
public onLeave(): void {
|
||||
this.wizard.wizardObject.generateScriptButton.hidden = true;
|
||||
}
|
||||
|
||||
private createRow(label: string, textComponent: azdata.TextComponent): azdata.FlexContainer {
|
||||
let row = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'baseline' }).component();
|
||||
let labelComponent = this.view.modelBuilder.text().withProperties({ value: label }).component();
|
||||
labelComponent.width = LabelWidth;
|
||||
textComponent.width = LabelWidth;
|
||||
row.addItems([labelComponent, textComponent]);
|
||||
return row;
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 azdata from 'azdata';
|
||||
import { ExtensionContext, Disposable } from 'vscode';
|
||||
import { WizardPageBase } from './wizardPageBase';
|
||||
|
||||
export abstract class WizardBase<T, W> {
|
||||
|
||||
public wizardObject: azdata.window.Wizard;
|
||||
private customButtons: azdata.window.Button[];
|
||||
private pages: WizardPageBase<W>[];
|
||||
|
||||
private toDispose: Disposable[] = [];
|
||||
|
||||
constructor(public model: T, public context: ExtensionContext, private title: string) {
|
||||
this.customButtons = [];
|
||||
}
|
||||
|
||||
public open(): Thenable<void> {
|
||||
this.wizardObject = azdata.window.createWizard(this.title);
|
||||
this.initialize();
|
||||
this.wizardObject.customButtons = this.customButtons;
|
||||
this.toDispose.push(this.wizardObject.onPageChanged((e) => {
|
||||
let previousPage = this.pages[e.lastPage];
|
||||
let newPage = this.pages[e.newPage];
|
||||
previousPage.onLeave();
|
||||
newPage.onEnter();
|
||||
}));
|
||||
|
||||
this.toDispose.push(this.wizardObject.doneButton.onClick(() => {
|
||||
this.onOk();
|
||||
this.dispose();
|
||||
}));
|
||||
this.toDispose.push(this.wizardObject.cancelButton.onClick(() => {
|
||||
this.onCancel();
|
||||
this.dispose();
|
||||
}));
|
||||
|
||||
return this.wizardObject.open().then(() => {
|
||||
if (this.pages && this.pages.length > 0) {
|
||||
this.pages[0].onEnter();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
protected abstract initialize(): void;
|
||||
protected abstract onOk(): void;
|
||||
protected abstract onCancel(): void;
|
||||
|
||||
public addButton(button: azdata.window.Button) {
|
||||
this.customButtons.push(button);
|
||||
}
|
||||
|
||||
protected setPages(pages: WizardPageBase<W>[]) {
|
||||
this.wizardObject.pages = pages.map(p => p.pageObject);
|
||||
this.pages = pages;
|
||||
}
|
||||
|
||||
private dispose() {
|
||||
this.toDispose.forEach((disposable: Disposable) => {
|
||||
try {
|
||||
disposable.dispose();
|
||||
}
|
||||
catch{ }
|
||||
});
|
||||
}
|
||||
|
||||
public registerDisposable(disposable: Disposable): void {
|
||||
this.toDispose.push(disposable);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 azdata from 'azdata';
|
||||
|
||||
export abstract class WizardPageBase<T> {
|
||||
private _page: azdata.window.WizardPage;
|
||||
|
||||
public get pageObject(): azdata.window.WizardPage {
|
||||
return this._page;
|
||||
}
|
||||
|
||||
public get wizard(): T {
|
||||
return this._wizard;
|
||||
}
|
||||
|
||||
constructor(title: string, description: string, private _wizard: T) {
|
||||
this._page = azdata.window.createWizardPage(title);
|
||||
this._page.description = description;
|
||||
this._page.registerContent((view: azdata.ModelView) => {
|
||||
return this.initialize(view);
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract initialize(view: azdata.ModelView): Thenable<void>;
|
||||
|
||||
public onEnter(): void { }
|
||||
|
||||
public onLeave(): void { }
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
{
|
||||
"extends": "../shared.tsconfig.json",
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"outDir": "./out",
|
||||
"strict": false,
|
||||
"alwaysStrict": false,
|
||||
"noImplicitAny": false,
|
||||
"noImplicitReturns": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false
|
||||
"lib": [
|
||||
"es6",
|
||||
"es2015.promise"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"declaration": false,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "cms",
|
||||
"displayName": "%cms.displayName%",
|
||||
"description": "%cms.description%",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
|
||||
@@ -14,6 +14,7 @@
|
||||
"activationEvents": [
|
||||
"*"
|
||||
],
|
||||
"forceReload": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
||||
@@ -93,10 +94,6 @@
|
||||
{
|
||||
"displayName": "%cms.connectionOptions.authType.categoryValues.integrated%",
|
||||
"name": "Integrated"
|
||||
},
|
||||
{
|
||||
"displayName": "%cms.connectionOptions.authType.categoryValues.azureMFA%",
|
||||
"name": "AzureMFA"
|
||||
}
|
||||
],
|
||||
"isRequired": true,
|
||||
@@ -632,5 +629,10 @@
|
||||
"should": "^13.2.1",
|
||||
"vscode": "^1.1.26",
|
||||
"typemoq": "^2.1.0"
|
||||
},
|
||||
"__metadata": {
|
||||
"id": "40",
|
||||
"publisherDisplayName": "Microsoft",
|
||||
"publisherId": "Microsoft"
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
"cms.resource.addServerGroup.title": "New Server Group...",
|
||||
"cms.resource.registerCmsServer.title": "Add Central Management Server",
|
||||
"cms.resource.deleteCmsServer.title": "Delete",
|
||||
|
||||
"cms.configuration.title": "MSSQL configuration",
|
||||
"cms.query.displayBitAsNumber": "Should BIT columns be displayed as numbers (1 or 0)? If false, BIT columns will be displayed as 'true' or 'false'",
|
||||
"cms.format.alignColumnDefinitionsInColumns": "Should column definitions be aligned?",
|
||||
@@ -70,7 +69,7 @@
|
||||
"cms.connectionOptions.columnEncryptionSetting.displayName": "Column encryption",
|
||||
"cms.connectionOptions.columnEncryptionSetting.description": "Default column encryption setting for all the commands on the connection",
|
||||
"cms.connectionOptions.encrypt.displayName": "Encrypt",
|
||||
"cms.connectionOptions.encrypt.description": "When true, SQL Server uses SSL encryption for all data sent between the client and server if the servers has a certificate installed",
|
||||
"cms.connectionOptions.encrypt.description": "When true, SQL Server uses SSL encryption for all data sent between the client and server if the server has a certificate installed",
|
||||
"cms.connectionOptions.persistSecurityInfo.displayName": "Persist security info",
|
||||
"cms.connectionOptions.persistSecurityInfo.description": "When false, security-sensitive information, such as the password, is not returned as part of the connection",
|
||||
"cms.connectionOptions.trustServerCertificate.displayName": "Trust server certificate",
|
||||
|
||||
@@ -18,40 +18,37 @@ const localize = nls.loadMessageBundle();
|
||||
|
||||
export function registerCmsServerCommand(appContext: AppContext, tree: CmsResourceTreeProvider): void {
|
||||
// Create a CMS Server
|
||||
appContext.apiWrapper.registerCommand('cms.resource.registerCmsServer', async (node?: TreeNode) => {
|
||||
appContext.apiWrapper.registerCommand('cms.resource.registerCmsServer', async (node?: TreeNode, connectionProfile?: azdata.IConnectionProfile) => {
|
||||
if (node && !(node instanceof CmsResourceEmptyTreeNode)) {
|
||||
return;
|
||||
}
|
||||
await appContext.cmsUtils.connection.then(async (connection) => {
|
||||
if (connection && connection.options) {
|
||||
let registeredCmsServerName = connection.options.registeredServerName ?
|
||||
connection.options.registeredServerName : connection.options.server;
|
||||
// check if a CMS with the same name is registered or not
|
||||
let cachedServers = appContext.cmsUtils.registeredCmsServers;
|
||||
let serverExists: boolean = false;
|
||||
if (cachedServers) {
|
||||
serverExists = cachedServers.some((server) => {
|
||||
return server.name === registeredCmsServerName;
|
||||
});
|
||||
}
|
||||
if (!serverExists) {
|
||||
// remove any group ID if user selects a connection from
|
||||
// recent connection list
|
||||
connection.options.groupId = null;
|
||||
let registeredCmsServerDescription = connection.options.registeredServerDescription;
|
||||
// remove server description from connection uri
|
||||
connection.options.registeredCmsServerDescription = null;
|
||||
let ownerUri = await azdata.connection.getUriForConnection(connection.connectionId);
|
||||
appContext.cmsUtils.cacheRegisteredCmsServer(registeredCmsServerName, registeredCmsServerDescription, ownerUri, connection);
|
||||
tree.notifyNodeChanged(undefined);
|
||||
} else {
|
||||
// error out for same server name
|
||||
let errorText = localize('cms.errors.sameCmsServerName', 'Central Management Server Group already has a Registered Server with the name {0}', registeredCmsServerName);
|
||||
appContext.apiWrapper.showErrorMessage(errorText);
|
||||
return;
|
||||
}
|
||||
let connection = await appContext.cmsUtils.makeConnection(connectionProfile);
|
||||
if (connection && connection.options) {
|
||||
let registeredCmsServerName = connection.options.registeredServerName ?
|
||||
connection.options.registeredServerName : connection.options.server;
|
||||
// check if a CMS with the same name is registered or not
|
||||
let cachedServers = appContext.cmsUtils.registeredCmsServers;
|
||||
let serverExists: boolean = false;
|
||||
if (cachedServers) {
|
||||
serverExists = cachedServers.some((server) => {
|
||||
return server.name === registeredCmsServerName;
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!serverExists) {
|
||||
// remove any group ID if user selects a connection from
|
||||
// recent connection list
|
||||
connection.options.groupId = null;
|
||||
let registeredCmsServerDescription = connection.options.registeredServerDescription;
|
||||
let ownerUri = await azdata.connection.getUriForConnection(connection.connectionId);
|
||||
appContext.cmsUtils.cacheRegisteredCmsServer(registeredCmsServerName, registeredCmsServerDescription, ownerUri, connection);
|
||||
tree.notifyNodeChanged(undefined);
|
||||
} else {
|
||||
// error out for same server name
|
||||
let errorText = localize('cms.errors.sameCmsServerName', 'Central Management Server Group already has a Registered Server with the name {0}', registeredCmsServerName);
|
||||
appContext.apiWrapper.showErrorMessage(errorText);
|
||||
throw new Error(errorText);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,7 +58,7 @@ export function deleteCmsServerCommand(appContext: AppContext, tree: CmsResource
|
||||
if (!(node instanceof CmsResourceTreeNode)) {
|
||||
return;
|
||||
}
|
||||
await appContext.cmsUtils.deleteCmsServer(node.name);
|
||||
await appContext.cmsUtils.deleteCmsServer(node.name, node.connection);
|
||||
tree.isSystemInitialized = false;
|
||||
tree.notifyNodeChanged(undefined);
|
||||
});
|
||||
@@ -76,16 +73,8 @@ export function addRegisteredServerCommand(appContext: AppContext, tree: CmsReso
|
||||
let relativePath = node instanceof CmsResourceTreeNode ? '' : node.relativePath;
|
||||
let serverName = node instanceof CmsResourceTreeNode ? node.connection.options.registeredServerName === ''
|
||||
? node.connection.options.server : node.connection.options.registeredServerName : null;
|
||||
await appContext.cmsUtils.addRegisteredServer(relativePath, node.ownerUri, serverName).then((result) => {
|
||||
if (result) {
|
||||
tree.notifyNodeChanged(node);
|
||||
}
|
||||
}, (error) => {
|
||||
// error out
|
||||
let errorText = localize('cms.errors.addRegisterServerFail', 'Could not add the Registered Server {0}', error);
|
||||
appContext.apiWrapper.showErrorMessage(errorText);
|
||||
return;
|
||||
});
|
||||
await appContext.cmsUtils.addRegisteredServer(relativePath, node.ownerUri, serverName);
|
||||
tree.notifyNodeChanged(node);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,18 +84,14 @@ export function deleteRegisteredServerCommand(appContext: AppContext, tree: CmsR
|
||||
if (!(node instanceof RegisteredServerTreeNode)) {
|
||||
return;
|
||||
}
|
||||
appContext.apiWrapper.showWarningMessage(
|
||||
let result = await appContext.apiWrapper.showWarningMessage(
|
||||
`${localize('cms.confirmDeleteServer', 'Are you sure you want to delete')} ${node.name}?`,
|
||||
localize('cms.yes', 'Yes'),
|
||||
localize('cms.no', 'No')).then((result) => {
|
||||
if (result && result === localize('cms.yes', 'Yes')) {
|
||||
appContext.cmsUtils.removeRegisteredServer(node.name, node.relativePath, node.ownerUri).then((result) => {
|
||||
if (result) {
|
||||
tree.notifyNodeChanged(node.parent);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
localize('cms.no', 'No'));
|
||||
if (result && result === localize('cms.yes', 'Yes')) {
|
||||
await appContext.cmsUtils.removeRegisteredServer(node.name, node.relativePath, node.ownerUri);
|
||||
tree.notifyNodeChanged(node.parent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,22 +136,19 @@ export function addServerGroupCommand(appContext: AppContext, tree: CmsResourceT
|
||||
dialog.content = [mainTab];
|
||||
azdata.window.openDialog(dialog);
|
||||
let groupExists = false;
|
||||
dialog.okButton.onClick(() => {
|
||||
dialog.okButton.onClick(async () => {
|
||||
let path = node instanceof ServerGroupTreeNode ? node.relativePath : '';
|
||||
if (node.serverGroupNodes.some(node => node.name === serverGroupName)) {
|
||||
groupExists = true;
|
||||
}
|
||||
if (!groupExists) {
|
||||
appContext.cmsUtils.addServerGroup(serverGroupName, serverDescription, path, node.ownerUri).then((result) => {
|
||||
if (result) {
|
||||
tree.notifyNodeChanged(node);
|
||||
}
|
||||
});
|
||||
await appContext.cmsUtils.addServerGroup(serverGroupName, serverDescription, path, node.ownerUri);
|
||||
tree.notifyNodeChanged(node);
|
||||
} else {
|
||||
// error out for same server group
|
||||
let errorText = localize('cms.errors.sameServerGroupName', '{0} already has a Server Group with the name {1}', node.name, serverGroupName);
|
||||
const errorText = localize('cms.errors.sameServerGroupName', '{0} already has a Server Group with the name {1}', node.name, serverGroupName);
|
||||
appContext.apiWrapper.showErrorMessage(errorText);
|
||||
return;
|
||||
throw new Error(errorText);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -178,18 +160,14 @@ export function deleteServerGroupCommand(appContext: AppContext, tree: CmsResour
|
||||
if (!(node instanceof ServerGroupTreeNode)) {
|
||||
return;
|
||||
}
|
||||
appContext.apiWrapper.showWarningMessage(
|
||||
let result = await appContext.apiWrapper.showWarningMessage(
|
||||
`${localize('cms.confirmDeleteGroup', 'Are you sure you want to delete')} ${node.name}?`,
|
||||
localize('cms.yes', 'Yes'),
|
||||
localize('cms.no', 'No')).then((result) => {
|
||||
if (result && result === localize('cms.yes', 'Yes')) {
|
||||
appContext.cmsUtils.removeServerGroup(node.name, node.relativePath, node.ownerUri).then((result) => {
|
||||
if (result) {
|
||||
tree.notifyNodeChanged(node.parent);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
localize('cms.no', 'No'));
|
||||
if (result && result === localize('cms.yes', 'Yes')) {
|
||||
await appContext.cmsUtils.removeServerGroup(node.name, node.relativePath, node.ownerUri);
|
||||
tree.notifyNodeChanged(node.parent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
'use strict';
|
||||
import * as azdata from 'azdata';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { TreeItemCollapsibleState } from 'vscode';
|
||||
import { TreeItemCollapsibleState, TreeItem } from 'vscode';
|
||||
import { AppContext } from '../../appContext';
|
||||
import { TreeNode } from '../treeNode';
|
||||
import { CmsResourceTreeNodeBase } from './baseTreeNodes';
|
||||
@@ -38,48 +38,52 @@ export class CmsResourceTreeNode extends CmsResourceTreeNodeBase {
|
||||
try {
|
||||
let nodes: CmsResourceTreeNodeBase[] = [];
|
||||
if (!this.ownerUri) {
|
||||
this._ownerUri = await this.appContext.cmsUtils.getUriForConnection(this.connection);
|
||||
// Set back password to get ownerUri
|
||||
if (this.connection.options.authenticationType === 'SqlLogin' && this.connection.options.savePassword === true) {
|
||||
this.connection.options.password = await this.appContext.cmsUtils.getPassword(this.connection.options.user);
|
||||
}
|
||||
}
|
||||
return this.appContext.cmsUtils.createCmsServer(this.connection, this.name, this.description).then((result) => {
|
||||
if (result) {
|
||||
if (result.registeredServersList) {
|
||||
result.registeredServersList.forEach((registeredServer) => {
|
||||
nodes.push(new RegisteredServerTreeNode(
|
||||
registeredServer.name,
|
||||
registeredServer.description,
|
||||
registeredServer.serverName,
|
||||
registeredServer.relativePath,
|
||||
this.ownerUri,
|
||||
this.appContext,
|
||||
this.treeChangeHandler, this));
|
||||
});
|
||||
}
|
||||
if (result.registeredServerGroups) {
|
||||
if (result.registeredServerGroups) {
|
||||
this._serverGroupNodes = [];
|
||||
result.registeredServerGroups.forEach((serverGroup) => {
|
||||
let serverGroupNode = new ServerGroupTreeNode(
|
||||
serverGroup.name,
|
||||
serverGroup.description,
|
||||
serverGroup.relativePath,
|
||||
this.ownerUri,
|
||||
this.appContext,
|
||||
this.treeChangeHandler, this);
|
||||
nodes.push(serverGroupNode);
|
||||
this._serverGroupNodes.push(serverGroupNode);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (nodes.length > 0) {
|
||||
return nodes.sort((node1, node2) => node1.name > node2.name ? 1 : -1);
|
||||
} else {
|
||||
return [CmsResourceMessageTreeNode.create(CmsResourceTreeNode.noResourcesLabel, undefined)];
|
||||
}
|
||||
// cache new connection is different from old one
|
||||
if (this.appContext.cmsUtils.didConnectionChange(this._connection, result.connection)) {
|
||||
this._connection = result.connection;
|
||||
this._ownerUri = result.ownerUri;
|
||||
this.appContext.cmsUtils.cacheRegisteredCmsServer(this.name, this.description, this.ownerUri, this.connection);
|
||||
}
|
||||
if (result.listRegisteredServersResult.registeredServersList) {
|
||||
result.listRegisteredServersResult.registeredServersList.forEach((registeredServer) => {
|
||||
nodes.push(new RegisteredServerTreeNode(
|
||||
registeredServer.name,
|
||||
registeredServer.description,
|
||||
registeredServer.serverName,
|
||||
registeredServer.relativePath,
|
||||
this.ownerUri,
|
||||
this.appContext,
|
||||
this.treeChangeHandler, this));
|
||||
});
|
||||
}
|
||||
if (result.listRegisteredServersResult.registeredServerGroups) {
|
||||
this._serverGroupNodes = [];
|
||||
result.listRegisteredServersResult.registeredServerGroups.forEach((serverGroup) => {
|
||||
let serverGroupNode = new ServerGroupTreeNode(
|
||||
serverGroup.name,
|
||||
serverGroup.description,
|
||||
serverGroup.relativePath,
|
||||
this.ownerUri,
|
||||
this.appContext,
|
||||
this.treeChangeHandler, this);
|
||||
nodes.push(serverGroupNode);
|
||||
this._serverGroupNodes.push(serverGroupNode);
|
||||
});
|
||||
}
|
||||
if (nodes.length > 0) {
|
||||
return nodes.sort((node1, node2) => node1.name > node2.name ? 1 : -1);
|
||||
} else {
|
||||
return [CmsResourceMessageTreeNode.create(CmsResourceTreeNode.noResourcesLabel, undefined)];
|
||||
}
|
||||
}, (error) => {
|
||||
let errorText = localize('cms.errors.expandCmsFail', 'The Central Management Server {0} could not be found or is offline', this.name);
|
||||
this.appContext.apiWrapper.showErrorMessage(errorText);
|
||||
return [];
|
||||
this.treeChangeHandler.notifyNodeChanged(undefined);
|
||||
throw error;
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
@@ -91,6 +95,7 @@ export class CmsResourceTreeNode extends CmsResourceTreeNodeBase {
|
||||
item.contextValue = CmsResourceItemType.cmsNodeContainer;
|
||||
item.id = this._id;
|
||||
item.tooltip = this.description;
|
||||
item.type = azdata.ExtensionNodeType.Server;
|
||||
item.iconPath = {
|
||||
dark: this.appContext.extensionContext.asAbsolutePath('resources/light/centralmanagement_server.svg'),
|
||||
light: this.appContext.extensionContext.asAbsolutePath('resources/light/centralmanagement_server.svg')
|
||||
|
||||
@@ -55,6 +55,7 @@ export class RegisteredServerTreeNode extends CmsResourceTreeNodeBase {
|
||||
collapsibleState: TreeItemCollapsibleState.Collapsed,
|
||||
label: this.name ? this.name : this.serverName,
|
||||
childProvider: 'MSSQL',
|
||||
type: azdata.ExtensionNodeType.Server,
|
||||
iconPath: {
|
||||
dark: this.appContext.extensionContext.asAbsolutePath('resources/light/regserverserver.svg'),
|
||||
light: this.appContext.extensionContext.asAbsolutePath('resources/light/regserverserver.svg')
|
||||
|
||||
@@ -14,6 +14,7 @@ import { CmsResourceEmptyTreeNode } from './cmsResourceEmptyTreeNode';
|
||||
import { ICmsResourceTreeChangeHandler } from './treeChangeHandler';
|
||||
import { CmsResourceMessageTreeNode } from '../messageTreeNode';
|
||||
import { CmsResourceTreeNode } from './cmsResourceTreeNode';
|
||||
import { ICmsResourceNodeInfo } from './baseTreeNodes';
|
||||
|
||||
export class CmsResourceTreeProvider implements TreeDataProvider<TreeNode>, ICmsResourceTreeChangeHandler {
|
||||
|
||||
@@ -27,7 +28,8 @@ export class CmsResourceTreeProvider implements TreeDataProvider<TreeNode>, ICms
|
||||
|
||||
public async getChildren(element?: TreeNode): Promise<TreeNode[]> {
|
||||
if (element) {
|
||||
return element.getChildren(true);
|
||||
let children = await element.getChildren(true);
|
||||
return children;
|
||||
}
|
||||
|
||||
if (!this.isSystemInitialized) {
|
||||
@@ -42,11 +44,11 @@ export class CmsResourceTreeProvider implements TreeDataProvider<TreeNode>, ICms
|
||||
servers.push(new CmsResourceTreeNode(
|
||||
server.name,
|
||||
server.description,
|
||||
undefined,
|
||||
server.ownerUri,
|
||||
server.connection,
|
||||
this._appContext, this, null));
|
||||
this.appContext.cmsUtils.cacheRegisteredCmsServer(server.name, server.description,
|
||||
undefined, server.connection);
|
||||
server.ownerUri, server.connection);
|
||||
});
|
||||
return servers;
|
||||
}
|
||||
@@ -62,13 +64,6 @@ export class CmsResourceTreeProvider implements TreeDataProvider<TreeNode>, ICms
|
||||
let registeredCmsServers = this.appContext.cmsUtils.registeredCmsServers;
|
||||
if (registeredCmsServers && registeredCmsServers.length > 0) {
|
||||
this.isSystemInitialized = true;
|
||||
// save the CMS Servers for future use
|
||||
let toSaveCmsServers = JSON.parse(JSON.stringify(registeredCmsServers));
|
||||
toSaveCmsServers.forEach(server => {
|
||||
server.ownerUri = undefined,
|
||||
server.connection.options.password = '';
|
||||
});
|
||||
await this._appContext.cmsUtils.setConfiguration(toSaveCmsServers);
|
||||
return registeredCmsServers.map((server) => {
|
||||
return new CmsResourceTreeNode(
|
||||
server.name,
|
||||
|
||||
@@ -14,6 +14,14 @@ import { ICmsResourceNodeInfo } from './cmsResource/tree/baseTreeNodes';
|
||||
const localize = nls.loadMessageBundle();
|
||||
const cmsProvider: string = 'MSSQL-CMS';
|
||||
const mssqlProvider: string = 'MSSQL';
|
||||
const CredentialNamespace = 'cmsCredentials';
|
||||
const sqlLoginAuthType: string = 'SqlLogin';
|
||||
|
||||
export interface CreateCmsResult {
|
||||
listRegisteredServersResult: mssql.ListRegisteredServersResult;
|
||||
connection: azdata.connection.Connection;
|
||||
ownerUri: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into
|
||||
@@ -24,8 +32,21 @@ const mssqlProvider: string = 'MSSQL';
|
||||
*/
|
||||
export class CmsUtils {
|
||||
|
||||
private _credentialProvider: azdata.CredentialProvider;
|
||||
private _cmsService: mssql.CmsService;
|
||||
private _registeredCmsServers: ICmsResourceNodeInfo[];
|
||||
private _registeredCmsServers: ICmsResourceNodeInfo[] = [];
|
||||
|
||||
public async savePassword(username: string, password: string): Promise<boolean> {
|
||||
let provider = await this.credentialProvider();
|
||||
let result = await provider.saveCredential(username, password);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getPassword(username: string): Promise<string> {
|
||||
let provider = await this.credentialProvider();
|
||||
let credential = await provider.readCredential(username);
|
||||
return credential ? credential.password : undefined;
|
||||
}
|
||||
|
||||
public showErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
|
||||
return vscode.window.showErrorMessage(message, ...items);
|
||||
@@ -53,9 +74,10 @@ export class CmsUtils {
|
||||
let ownerUri = await azdata.connection.getUriForConnection(connection.connectionId);
|
||||
if (!ownerUri) {
|
||||
// Make a connection if it's not already connected
|
||||
await azdata.connection.connect(Utils.toConnectionProfile(connection), false, false).then(async (result) => {
|
||||
let result = await azdata.connection.connect(Utils.toConnectionProfile(connection), false, false);
|
||||
if (result) {
|
||||
ownerUri = await azdata.connection.getUriForConnection(result.connectionId);
|
||||
});
|
||||
}
|
||||
}
|
||||
return ownerUri;
|
||||
}
|
||||
@@ -70,59 +92,79 @@ export class CmsUtils {
|
||||
}
|
||||
|
||||
public async getRegisteredServers(ownerUri: string, relativePath: string): Promise<mssql.ListRegisteredServersResult> {
|
||||
return this.getCmsService().then((service) => {
|
||||
return service.getRegisteredServers(ownerUri, relativePath).then((result) => {
|
||||
if (result && result.registeredServersList && result.registeredServersList) {
|
||||
return result;
|
||||
}
|
||||
});
|
||||
});
|
||||
const cmsService = await this.getCmsService();
|
||||
const result = await cmsService.getRegisteredServers(ownerUri, relativePath);
|
||||
if (result && result.registeredServersList && result.registeredServersList) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public async createCmsServer(connection: azdata.connection.Connection,
|
||||
name: string, description: string): Promise<mssql.ListRegisteredServersResult> {
|
||||
name: string, description: string): Promise<CreateCmsResult> {
|
||||
let provider = await this.getCmsService();
|
||||
connection.providerName = connection.providerName === cmsProvider ? mssqlProvider : connection.providerName;
|
||||
let ownerUri = await azdata.connection.getUriForConnection(connection.connectionId);
|
||||
if (!ownerUri) {
|
||||
// Make a connection if it's not already connected
|
||||
await azdata.connection.connect(Utils.toConnectionProfile(connection), false, false).then(async (result) => {
|
||||
ownerUri = await azdata.connection.getUriForConnection(result.connectionId);
|
||||
});
|
||||
}
|
||||
return provider.createCmsServer(name, description, connection, ownerUri).then((result) => {
|
||||
if (result) {
|
||||
return Promise.resolve(result);
|
||||
} else {
|
||||
return Promise.reject(null);
|
||||
let initialConnectionProfile = this.getConnectionProfile(connection);
|
||||
let result = await azdata.connection.connect(initialConnectionProfile, false, false);
|
||||
ownerUri = await azdata.connection.getUriForConnection(result.connectionId);
|
||||
// If the ownerUri is still undefined, then open a connection dialog with the connection
|
||||
if (!ownerUri) {
|
||||
let result = await this.makeConnection(initialConnectionProfile);
|
||||
if (result) {
|
||||
ownerUri = await azdata.connection.getUriForConnection(result.connectionId);
|
||||
connection = result;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let result = await provider.createCmsServer(name, description, connection, ownerUri);
|
||||
const createCmsResult: CreateCmsResult = {
|
||||
listRegisteredServersResult: result,
|
||||
connection: connection,
|
||||
ownerUri: ownerUri
|
||||
};
|
||||
return createCmsResult;
|
||||
}
|
||||
|
||||
public async deleteCmsServer(cmsServer: any): Promise<void> {
|
||||
public async deleteCmsServer(cmsServerName: string, connection: azdata.connection.Connection): Promise<void> {
|
||||
let config = this.getConfiguration();
|
||||
if (config && config.servers) {
|
||||
let newServers = config.servers.filter((cachedServer) => {
|
||||
return cachedServer.name !== cmsServer;
|
||||
return cachedServer.name !== cmsServerName;
|
||||
});
|
||||
await this.setConfiguration(newServers);
|
||||
this._registeredCmsServers = this._registeredCmsServers.filter((cachedServer) => {
|
||||
return cachedServer.name !== cmsServer;
|
||||
return cachedServer.name !== cmsServerName;
|
||||
});
|
||||
}
|
||||
if (connection.options.authenticationType === sqlLoginAuthType && connection.options.savePassword) {
|
||||
this._credentialProvider.deleteCredential(connection.options.user);
|
||||
}
|
||||
}
|
||||
|
||||
public cacheRegisteredCmsServer(name: string, description: string, ownerUri: string, connection: azdata.connection.Connection): void {
|
||||
if (!this._registeredCmsServers) {
|
||||
this._registeredCmsServers = [];
|
||||
}
|
||||
public async cacheRegisteredCmsServer(name: string, description: string, ownerUri: string, connection: azdata.connection.Connection): Promise<void> {
|
||||
let cmsServerNode: ICmsResourceNodeInfo = {
|
||||
name: name,
|
||||
description: description,
|
||||
connection: connection,
|
||||
ownerUri: ownerUri
|
||||
};
|
||||
|
||||
// update a server if a server with same name exists
|
||||
this._registeredCmsServers = this._registeredCmsServers.filter((server) => {
|
||||
return server.name !== name;
|
||||
});
|
||||
this._registeredCmsServers.push(cmsServerNode);
|
||||
|
||||
// save the CMS Servers for future use
|
||||
let toSaveCmsServers: ICmsResourceNodeInfo[] = this._registeredCmsServers.map(server => Object.assign({}, server));
|
||||
toSaveCmsServers.forEach(server => {
|
||||
server.ownerUri = undefined;
|
||||
// don't save password in config
|
||||
server.connection.options.password = '';
|
||||
});
|
||||
await this.setConfiguration(toSaveCmsServers);
|
||||
}
|
||||
|
||||
public async addRegisteredServer(relativePath: string, ownerUri: string,
|
||||
@@ -147,45 +189,37 @@ export class CmsUtils {
|
||||
authTypeChanged: true
|
||||
}
|
||||
};
|
||||
return this.openConnectionDialog([cmsProvider], initialProfile, { saveConnection: false }).then(async (connection) => {
|
||||
if (connection && connection.options) {
|
||||
if (connection.options.server === parentServerName) {
|
||||
// error out for same server registration
|
||||
let errorText = localize('cms.errors.sameServerUnderCms', 'You cannot add a shared registered server with the same name as the Configuration Server');
|
||||
this.showErrorMessage(errorText);
|
||||
return false;
|
||||
} else {
|
||||
let registeredServerName = connection.options.registeredServerName === '' ? connection.options.server : connection.options.registeredServerName;
|
||||
let result = await provider.addRegisteredServer(ownerUri, relativePath, registeredServerName, connection.options.registeredServerDescription, connection);
|
||||
if (result) {
|
||||
return Promise.resolve(result);
|
||||
} else {
|
||||
return Promise.reject(registeredServerName);
|
||||
}
|
||||
}
|
||||
let connection = await this.openConnectionDialog([cmsProvider], initialProfile, { saveConnection: false });
|
||||
if (connection && connection.options) {
|
||||
if (connection.options.server === parentServerName) {
|
||||
// error out for same server registration
|
||||
let errorText = localize('cms.errors.sameServerUnderCms', 'You cannot add a shared registered server with the same name as the Configuration Server');
|
||||
this.showErrorMessage(errorText);
|
||||
throw new Error(errorText);
|
||||
} else {
|
||||
let registeredServerName = connection.options.registeredServerName === '' ? connection.options.server : connection.options.registeredServerName;
|
||||
let result = await provider.addRegisteredServer(ownerUri, relativePath, registeredServerName, connection.options.registeredServerDescription, connection);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async removeRegisteredServer(registeredServerName: string, relativePath: string, ownerUri: string): Promise<boolean> {
|
||||
let provider = await this.getCmsService();
|
||||
return provider.removeRegisteredServer(ownerUri, relativePath, registeredServerName).then((result) => {
|
||||
return result;
|
||||
});
|
||||
let result = await provider.removeRegisteredServer(ownerUri, relativePath, registeredServerName);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async addServerGroup(groupName: string, groupDescription: string, relativePath: string, ownerUri: string): Promise<boolean> {
|
||||
let provider = await this.getCmsService();
|
||||
return provider.addServerGroup(ownerUri, relativePath, groupName, groupDescription).then((result) => {
|
||||
return result;
|
||||
});
|
||||
let result = await provider.addServerGroup(ownerUri, relativePath, groupName, groupDescription);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async removeServerGroup(groupName: string, relativePath: string, ownerUri: string): Promise<boolean> {
|
||||
let provider = await this.getCmsService();
|
||||
return provider.removeServerGroup(ownerUri, relativePath, groupName).then((result) => {
|
||||
return result;
|
||||
});
|
||||
let result = await provider.removeServerGroup(ownerUri, relativePath, groupName);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Getters
|
||||
@@ -193,15 +227,68 @@ export class CmsUtils {
|
||||
return this._registeredCmsServers;
|
||||
}
|
||||
|
||||
public get connection(): Thenable<azdata.connection.Connection> {
|
||||
return this.openConnectionDialog([cmsProvider], undefined, { saveConnection: false }).then((connection) => {
|
||||
if (connection) {
|
||||
// remove group ID from connection if a user chose connection
|
||||
// from the recent connections list
|
||||
connection.options['groupId'] = null;
|
||||
connection.providerName = mssqlProvider;
|
||||
return connection;
|
||||
}
|
||||
});
|
||||
public async credentialProvider(): Promise<azdata.CredentialProvider> {
|
||||
if (!this._credentialProvider) {
|
||||
this._credentialProvider = await azdata.credentials.getProvider(CredentialNamespace);
|
||||
}
|
||||
return this._credentialProvider;
|
||||
}
|
||||
|
||||
public async makeConnection(initialConnectionProfile?: azdata.IConnectionProfile): Promise<azdata.connection.Connection> {
|
||||
if (!initialConnectionProfile) {
|
||||
initialConnectionProfile = {
|
||||
connectionName: undefined,
|
||||
serverName: undefined,
|
||||
databaseName: undefined,
|
||||
userName: undefined,
|
||||
password: undefined,
|
||||
authenticationType: undefined,
|
||||
savePassword: undefined,
|
||||
groupFullName: undefined,
|
||||
groupId: undefined,
|
||||
providerName: undefined,
|
||||
saveProfile: undefined,
|
||||
id: undefined,
|
||||
options: {}
|
||||
};
|
||||
}
|
||||
let connection = await this.openConnectionDialog([cmsProvider], initialConnectionProfile, { saveConnection: false });
|
||||
if (connection) {
|
||||
// remove group ID from connection if a user chose connection
|
||||
// from the recent connections list
|
||||
connection.options['groupId'] = null;
|
||||
connection.providerName = mssqlProvider;
|
||||
if (connection.options.savePassword) {
|
||||
await this.savePassword(connection.options.user, connection.options.password);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
||||
// Static Functions
|
||||
|
||||
public getConnectionProfile(connection: azdata.connection.Connection): azdata.IConnectionProfile {
|
||||
let connectionProfile: azdata.IConnectionProfile = {
|
||||
connectionName: connection.options.connectionName,
|
||||
serverName: connection.options.server,
|
||||
databaseName: undefined,
|
||||
userName: connection.options.user,
|
||||
password: connection.options.password,
|
||||
authenticationType: connection.options.authenticationType,
|
||||
savePassword: connection.options.savePassword,
|
||||
groupFullName: undefined,
|
||||
groupId: undefined,
|
||||
providerName: connection.providerName,
|
||||
saveProfile: false,
|
||||
id: connection.connectionId,
|
||||
options: connection.options
|
||||
};
|
||||
return connectionProfile;
|
||||
}
|
||||
|
||||
public didConnectionChange(connectionA: azdata.connection.Connection, connectionB: azdata.connection.Connection): boolean {
|
||||
return (connectionA !== connectionB) || ((connectionA.connectionId === connectionB.connectionId) &&
|
||||
(connectionA.options.savePassword !== connectionA.options.savePassword));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,6 +32,7 @@ describe('ServerGroupTreeNode.info', function(): void {
|
||||
beforeEach(() => {
|
||||
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
|
||||
mockCmsUtils = TypeMoq.Mock.ofType<CmsUtils>();
|
||||
mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object, mockCmsUtils.object);
|
||||
mockTreeChangeHandler = TypeMoq.Mock.ofType<ICmsResourceTreeChangeHandler>();
|
||||
mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<cmsResource.ICmsResourceTreeDataProvider>();
|
||||
@@ -47,7 +48,6 @@ describe('ServerGroupTreeNode.info', function(): void {
|
||||
|
||||
const treeNode = new ServerGroupTreeNode('test', 'test', 'test_path', 'test_ownerUri', mockAppContext, mockTreeChangeHandler.object, null);
|
||||
|
||||
should(treeNode.nodePathValue).equal('cms_serverGroup_test');
|
||||
should(treeNode.relativePath).equal('test_path');
|
||||
|
||||
const treeItem = await treeNode.getTreeItem();
|
||||
|
||||
@@ -31,6 +31,7 @@ describe('RegisteredServerTreeNode.info', function(): void {
|
||||
beforeEach(() => {
|
||||
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
|
||||
mockCmsUtils = TypeMoq.Mock.ofType<CmsUtils>();
|
||||
mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object, mockCmsUtils.object);
|
||||
mockTreeChangeHandler = TypeMoq.Mock.ofType<ICmsResourceTreeChangeHandler>();
|
||||
mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<cmsResource.ICmsResourceTreeDataProvider>();
|
||||
@@ -46,7 +47,6 @@ describe('RegisteredServerTreeNode.info', function(): void {
|
||||
|
||||
const treeNode = new RegisteredServerTreeNode('test', 'test', 'test_server', 'test_path', 'test_ownerUri', mockAppContext, mockTreeChangeHandler.object, null);
|
||||
|
||||
should(treeNode.nodePathValue).equal('cms_registeredServer_test');
|
||||
should(treeNode.relativePath).equal('test_path');
|
||||
|
||||
const treeItem = await treeNode.getTreeItem();
|
||||
|
||||
@@ -31,6 +31,7 @@ describe('ServerGroupTreeNode.info', function(): void {
|
||||
beforeEach(() => {
|
||||
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
|
||||
mockCmsUtils = TypeMoq.Mock.ofType<CmsUtils>();
|
||||
mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object, mockCmsUtils.object);
|
||||
mockTreeChangeHandler = TypeMoq.Mock.ofType<ICmsResourceTreeChangeHandler>();
|
||||
mockResourceTreeDataProvider1 = TypeMoq.Mock.ofType<cmsResource.ICmsResourceTreeDataProvider>();
|
||||
@@ -46,7 +47,6 @@ describe('ServerGroupTreeNode.info', function(): void {
|
||||
|
||||
const treeNode = new ServerGroupTreeNode('test', 'test', 'test_path', 'test_ownerUri', mockAppContext, mockTreeChangeHandler.object, null);
|
||||
|
||||
should(treeNode.nodePathValue).equal('cms_serverGroup_test');
|
||||
should(treeNode.relativePath).equal('test_path');
|
||||
|
||||
const treeItem = await treeNode.getTreeItem();
|
||||
|
||||
@@ -27,6 +27,7 @@ describe('CmsResourceTreeProvider.getChildren', function (): void {
|
||||
beforeEach(() => {
|
||||
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||
mockApiWrapper = TypeMoq.Mock.ofType<ApiWrapper>();
|
||||
mockCmsUtils = TypeMoq.Mock.ofType<CmsUtils>();
|
||||
mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object, mockCmsUtils.object);
|
||||
});
|
||||
|
||||
@@ -43,7 +44,7 @@ describe('CmsResourceTreeProvider.getChildren', function (): void {
|
||||
const treeProvider = new CmsResourceTreeProvider(mockAppContext);
|
||||
treeProvider.isSystemInitialized = true;
|
||||
should.equal(true, treeProvider.isSystemInitialized);
|
||||
mockCmsUtils.setup(x => x.registeredCmsServers).returns(null);
|
||||
mockCmsUtils.setup(x => x.registeredCmsServers).returns(() => []);
|
||||
const children = await treeProvider.getChildren(undefined);
|
||||
should.equal(children[0] instanceof CmsResourceEmptyTreeNode, true);
|
||||
});
|
||||
@@ -60,6 +61,6 @@ describe('CmsResourceTreeProvider.getChildren', function (): void {
|
||||
}];
|
||||
});
|
||||
const children = await treeProvider.getChildren(undefined);
|
||||
should.equal(children[0] instanceof CmsResourceTreeNode, true);
|
||||
should.equal(children[0] !== null, true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "dacpac",
|
||||
"displayName": "SQL Server Dacpac",
|
||||
"description": "SQL Server Dacpac for Azure Data Studio.",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"engines": {
|
||||
|
||||
@@ -46,7 +46,7 @@ export abstract class BasePage {
|
||||
public abstract setupNavigationValidator();
|
||||
|
||||
protected async getServerValues(): Promise<{ connection, displayName, name }[]> {
|
||||
let cons = await azdata.connection.getActiveConnections();
|
||||
let cons = await azdata.connection.getConnections(/* activeConnectionsOnly */ true);
|
||||
// This user has no active connections ABORT MISSION
|
||||
if (!cons || cons.length === 0) {
|
||||
return undefined;
|
||||
@@ -67,19 +67,14 @@ export abstract class BasePage {
|
||||
}
|
||||
}
|
||||
|
||||
let db = c.options.databaseDisplayName;
|
||||
let usr = c.options.user;
|
||||
let srv = c.options.server;
|
||||
|
||||
if (!db) {
|
||||
db = localize('basePage.defaultDb', '<default>');
|
||||
}
|
||||
|
||||
if (!usr) {
|
||||
usr = localize('basePage.defaultUser', 'default');
|
||||
}
|
||||
|
||||
let finalName = `${srv}, ${db} (${usr})`;
|
||||
let finalName = `${srv} (${usr})`;
|
||||
return {
|
||||
connection: c,
|
||||
displayName: finalName,
|
||||
|
||||
@@ -96,9 +96,7 @@ export abstract class DacFxConfigPage extends BasePage {
|
||||
}
|
||||
|
||||
protected async createDatabaseDropdown(): Promise<azdata.FormComponent> {
|
||||
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
|
||||
required: true
|
||||
}).component();
|
||||
this.databaseDropdown = this.view.modelBuilder.dropDown().component();
|
||||
|
||||
// Handle database changes
|
||||
this.databaseDropdown.onValueChanged(async () => {
|
||||
@@ -107,7 +105,9 @@ export abstract class DacFxConfigPage extends BasePage {
|
||||
this.model.filePath = this.fileTextBox.value;
|
||||
});
|
||||
|
||||
this.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.databaseDropdown).component();
|
||||
this.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.databaseDropdown).withProperties({
|
||||
required: true
|
||||
}).component();
|
||||
|
||||
return {
|
||||
component: this.databaseLoader,
|
||||
@@ -125,9 +125,13 @@ export abstract class DacFxConfigPage extends BasePage {
|
||||
}
|
||||
|
||||
let values = await this.getDatabaseValues();
|
||||
this.model.database = values[0].name;
|
||||
this.model.filePath = this.generateFilePathFromDatabaseAndTimestamp();
|
||||
this.fileTextBox.value = this.model.filePath;
|
||||
|
||||
// only update values and regenerate filepath if this is the first time and database isn't set yet
|
||||
if (this.model.database !== values[0].name) {
|
||||
this.model.database = values[0].name;
|
||||
this.model.filePath = this.generateFilePathFromDatabaseAndTimestamp();
|
||||
this.fileTextBox.value = this.model.filePath;
|
||||
}
|
||||
|
||||
this.databaseDropdown.updateProperties({
|
||||
values: values
|
||||
@@ -163,6 +167,6 @@ export abstract class DacFxConfigPage extends BasePage {
|
||||
}
|
||||
|
||||
interface ConnectionDropdownValue extends azdata.CategoryValue {
|
||||
connection: azdata.connection.Connection;
|
||||
connection: azdata.connection.ConnectionProfile;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,11 @@ import * as azdata from 'azdata';
|
||||
* Data model to communicate between DacFx pages
|
||||
*/
|
||||
export interface DacFxDataModel {
|
||||
server: azdata.connection.Connection;
|
||||
server: azdata.connection.ConnectionProfile;
|
||||
database: string;
|
||||
serverName: string;
|
||||
serverId: string;
|
||||
filePath: string;
|
||||
version: string;
|
||||
upgradeExisting: boolean;
|
||||
scriptFilePath: string;
|
||||
generateScriptAndDeploy: boolean;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import * as azdata from 'azdata';
|
||||
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';
|
||||
import { ExtractConfigPage } from './pages/extractConfigPage';
|
||||
@@ -18,7 +17,7 @@ import { DacFxDataModel } from './api/models';
|
||||
import { BasePage } from './api/basePage';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const msSqlProvider = 'MSSQL';
|
||||
class Page {
|
||||
wizardPage: azdata.window.WizardPage;
|
||||
dacFxPage: BasePage;
|
||||
@@ -40,7 +39,6 @@ export enum DeployOperationPath {
|
||||
selectOperation,
|
||||
deployOptions,
|
||||
deployPlan,
|
||||
deployAction,
|
||||
summary
|
||||
}
|
||||
|
||||
@@ -88,9 +86,9 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
this.connection = await azdata.connection.getCurrentConnection();
|
||||
if (!this.connection) {
|
||||
if (!this.connection || (profile && this.connection.connectionId !== profile.id)) {
|
||||
// @TODO: remove cast once azdata update complete - karlb 3/1/2019
|
||||
this.connection = <azdata.connection.ConnectionProfile><any>await azdata.connection.openConnectionDialog();
|
||||
this.connection = <azdata.connection.ConnectionProfile><any>await azdata.connection.openConnectionDialog(undefined, profile);
|
||||
|
||||
// don't open the wizard if connection dialog is cancelled
|
||||
if (!this.connection) {
|
||||
@@ -104,7 +102,6 @@ export class DataTierApplicationWizard {
|
||||
let selectOperationWizardPage = azdata.window.createWizardPage(localize('dacFx.selectOperationPageName', 'Select an Operation'));
|
||||
let deployConfigWizardPage = azdata.window.createWizardPage(localize('dacFx.deployConfigPageName', 'Select Deploy Dacpac Settings'));
|
||||
let deployPlanWizardPage = azdata.window.createWizardPage(localize('dacFx.deployPlanPage', 'Review the deploy plan'));
|
||||
let deployActionWizardPage = azdata.window.createWizardPage(localize('dacFx.deployActionPageName', 'Select Action'));
|
||||
let summaryWizardPage = azdata.window.createWizardPage(localize('dacFx.summaryPageName', 'Summary'));
|
||||
let extractConfigWizardPage = azdata.window.createWizardPage(localize('dacFx.extractConfigPageName', 'Select Extract Dacpac Settings'));
|
||||
let importConfigWizardPage = azdata.window.createWizardPage(localize('dacFx.importConfigPageName', 'Select Import Bacpac Settings'));
|
||||
@@ -113,7 +110,6 @@ 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));
|
||||
this.pages.set('exportConfig', new Page(exportConfigWizardPage));
|
||||
@@ -140,12 +136,6 @@ export class DataTierApplicationWizard {
|
||||
await deployPlanDacFxPage.start();
|
||||
});
|
||||
|
||||
deployActionWizardPage.registerContent(async (view) => {
|
||||
let deployActionDacFxPage = new DeployActionPage(this, deployActionWizardPage, this.model, view);
|
||||
this.pages.get('deployAction').dacFxPage = deployActionDacFxPage;
|
||||
await deployActionDacFxPage.start();
|
||||
});
|
||||
|
||||
extractConfigWizardPage.registerContent(async (view) => {
|
||||
let extractConfigDacFxPage = new ExtractConfigPage(this, extractConfigWizardPage, this.model, view);
|
||||
this.pages.get('extractConfig').dacFxPage = extractConfigDacFxPage;
|
||||
@@ -190,7 +180,7 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
});
|
||||
|
||||
this.wizard.pages = [selectOperationWizardPage, deployConfigWizardPage, deployPlanWizardPage, deployActionWizardPage, summaryWizardPage];
|
||||
this.wizard.pages = [selectOperationWizardPage, deployConfigWizardPage, deployPlanWizardPage, summaryWizardPage];
|
||||
this.wizard.generateScriptButton.hidden = true;
|
||||
this.wizard.generateScriptButton.onClick(async () => await this.generateDeployScript());
|
||||
this.wizard.doneButton.onClick(async () => await this.executeOperation());
|
||||
@@ -262,10 +252,10 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private async deploy() {
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
|
||||
const service = await DataTierApplicationWizard.getService(msSqlProvider);
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.deployDacpac(this.model.filePath, this.model.database, this.model.upgradeExisting, ownerUri, azdata.TaskExecutionMode.execute);
|
||||
const result = await service.deployDacpac(this.model.filePath, this.model.database, this.model.upgradeExisting, ownerUri, azdata.TaskExecutionMode.execute);
|
||||
if (!result || !result.success) {
|
||||
vscode.window.showErrorMessage(
|
||||
localize('alertData.deployErrorMessage', "Deploy failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
|
||||
@@ -273,10 +263,10 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private async extract() {
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
|
||||
const service = await DataTierApplicationWizard.getService(msSqlProvider);
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.extractDacpac(this.model.database, this.model.filePath, this.model.database, this.model.version, ownerUri, azdata.TaskExecutionMode.execute);
|
||||
const result = await service.extractDacpac(this.model.database, this.model.filePath, this.model.database, this.model.version, ownerUri, azdata.TaskExecutionMode.execute);
|
||||
if (!result || !result.success) {
|
||||
vscode.window.showErrorMessage(
|
||||
localize('alertData.extractErrorMessage', "Extract failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
|
||||
@@ -284,10 +274,10 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private async export() {
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
|
||||
const service = await DataTierApplicationWizard.getService(msSqlProvider);
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.exportBacpac(this.model.database, this.model.filePath, ownerUri, azdata.TaskExecutionMode.execute);
|
||||
const result = await service.exportBacpac(this.model.database, this.model.filePath, ownerUri, azdata.TaskExecutionMode.execute);
|
||||
if (!result || !result.success) {
|
||||
vscode.window.showErrorMessage(
|
||||
localize('alertData.exportErrorMessage', "Export failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
|
||||
@@ -295,10 +285,10 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private async import() {
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
|
||||
const service = await DataTierApplicationWizard.getService(msSqlProvider);
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.importBacpac(this.model.filePath, this.model.database, ownerUri, azdata.TaskExecutionMode.execute);
|
||||
const result = await service.importBacpac(this.model.filePath, this.model.database, ownerUri, azdata.TaskExecutionMode.execute);
|
||||
if (!result || !result.success) {
|
||||
vscode.window.showErrorMessage(
|
||||
localize('alertData.importErrorMessage', "Import failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
|
||||
@@ -306,19 +296,15 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private async generateDeployScript() {
|
||||
if (!this.model.scriptFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
|
||||
const service = await DataTierApplicationWizard.getService(msSqlProvider);
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
|
||||
this.wizard.message = {
|
||||
text: localize('dacfx.scriptGeneratingMessage', 'You can view the status of script generation in the Task History once the wizard is closed'),
|
||||
text: localize('dacfx.scriptGeneratingMessage', 'You can view the status of script generation in the Tasks View once the wizard is closed. The generated script will open when complete.'),
|
||||
level: azdata.window.MessageLevel.Information,
|
||||
description: ''
|
||||
};
|
||||
|
||||
let result = await service.generateDeployScript(this.model.filePath, this.model.database, this.model.scriptFilePath, ownerUri, azdata.TaskExecutionMode.execute);
|
||||
const result = await service.generateDeployScript(this.model.filePath, this.model.database, ownerUri, azdata.TaskExecutionMode.script);
|
||||
if (!result || !result.success) {
|
||||
vscode.window.showErrorMessage(
|
||||
localize('alertData.deployErrorMessage', "Deploy failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
|
||||
@@ -351,8 +337,6 @@ export class DataTierApplicationWizard {
|
||||
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;
|
||||
@@ -367,10 +351,10 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
public async generateDeployPlan(): Promise<string> {
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
|
||||
const service = await DataTierApplicationWizard.getService(msSqlProvider);
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.generateDeployPlan(this.model.filePath, this.model.database, ownerUri, azdata.TaskExecutionMode.execute);
|
||||
const result = await service.generateDeployPlan(this.model.filePath, this.model.database, ownerUri, azdata.TaskExecutionMode.execute);
|
||||
|
||||
if (!result || !result.success) {
|
||||
vscode.window.showErrorMessage(
|
||||
@@ -381,7 +365,7 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private static async getService(providerName: string): Promise<azdata.DacFxServicesProvider> {
|
||||
let service = azdata.dataprotocol.getProvider<azdata.DacFxServicesProvider>(providerName, azdata.DataProviderType.DacFxServicesProvider);
|
||||
const service = azdata.dataprotocol.getProvider<azdata.DacFxServicesProvider>(providerName, azdata.DataProviderType.DacFxServicesProvider);
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export class DacFxSummaryPage extends BasePage {
|
||||
async onPageEnter(): Promise<boolean> {
|
||||
this.populateTable();
|
||||
this.loader.loading = false;
|
||||
if (this.model.upgradeExisting && this.model.generateScriptAndDeploy) {
|
||||
if (this.model.upgradeExisting && this.instance.selectedOperation === Operation.deploy) {
|
||||
this.instance.wizard.generateScriptButton.hidden = false;
|
||||
}
|
||||
return true;
|
||||
@@ -76,10 +76,6 @@ export class DacFxSummaryPage extends BasePage {
|
||||
let sourceServer = localize('dacfx.sourceServerName', 'Source Server');
|
||||
let sourceDatabase = localize('dacfx.sourceDatabaseName', 'Source Database');
|
||||
let fileLocation = localize('dacfx.fileLocation', 'File Location');
|
||||
let scriptLocation = localize('dacfx.scriptLocation', 'Deployment Script Location');
|
||||
let action = localize('dacfx.action', 'Action');
|
||||
let deploy = localize('dacfx.deploy', 'Deploy');
|
||||
let generateScript = localize('dacfx.generateScript', 'Generate Deployment Script');
|
||||
|
||||
switch (this.instance.selectedOperation) {
|
||||
case Operation.deploy: {
|
||||
@@ -87,13 +83,6 @@ export class DacFxSummaryPage extends BasePage {
|
||||
[targetServer, this.model.serverName],
|
||||
[fileLocation, this.model.filePath],
|
||||
[targetDatabase, this.model.database]];
|
||||
if (this.model.generateScriptAndDeploy) {
|
||||
data[3] = [scriptLocation, this.model.scriptFilePath];
|
||||
data[4] = [action, generateScript + ', ' + deploy];
|
||||
}
|
||||
else {
|
||||
data[3] = [action, deploy];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Operation.extract: {
|
||||
@@ -122,9 +111,7 @@ export class DacFxSummaryPage extends BasePage {
|
||||
data = [
|
||||
[targetServer, this.model.serverName],
|
||||
[fileLocation, this.model.filePath],
|
||||
[targetDatabase, this.model.database],
|
||||
[scriptLocation, this.model.scriptFilePath],
|
||||
[action, generateScript]];
|
||||
[targetDatabase, this.model.database]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 azdata from 'azdata';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { DacFxDataModel } from '../api/models';
|
||||
import { DataTierApplicationWizard, Operation } from '../dataTierApplicationWizard';
|
||||
import { DacFxConfigPage } from '../api/dacFxConfigPage';
|
||||
import { sanitizeStringForFilename } from '../api/utils';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class DeployActionPage extends DacFxConfigPage {
|
||||
|
||||
protected readonly wizardPage: azdata.window.WizardPage;
|
||||
protected readonly instance: DataTierApplicationWizard;
|
||||
protected readonly model: DacFxDataModel;
|
||||
protected readonly view: azdata.ModelView;
|
||||
private deployRadioButton: azdata.RadioButtonComponent;
|
||||
private deployScriptRadioButton: azdata.RadioButtonComponent;
|
||||
private scriptRadioButton: azdata.RadioButtonComponent;
|
||||
private form: azdata.FormContainer;
|
||||
|
||||
public constructor(instance: DataTierApplicationWizard, wizardPage: azdata.window.WizardPage, model: DacFxDataModel, view: azdata.ModelView) {
|
||||
super(instance, wizardPage, model, view);
|
||||
}
|
||||
|
||||
async start(): Promise<boolean> {
|
||||
let deployComponent = await this.createDeployRadioButton();
|
||||
let deployScriptComponent = await this.createDeployScriptRadioButton();
|
||||
let scriptComponent = await this.createScriptRadioButton();
|
||||
let fileBrowserComponent = await this.createFileBrowser();
|
||||
|
||||
this.form = this.view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
[
|
||||
deployComponent,
|
||||
scriptComponent,
|
||||
deployScriptComponent,
|
||||
fileBrowserComponent
|
||||
]).component();
|
||||
await this.view.initializeModel(this.form);
|
||||
|
||||
//default have the first radio button checked
|
||||
this.deployRadioButton.checked = true;
|
||||
this.toggleFileBrowser(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async onPageEnter(): Promise<boolean> {
|
||||
// generate script file path in case the database changed since last time the page was entered
|
||||
this.setDefaultScriptFilePath();
|
||||
return true;
|
||||
}
|
||||
|
||||
private async createDeployRadioButton(): Promise<azdata.FormComponent> {
|
||||
this.deployRadioButton = this.view.modelBuilder.radioButton()
|
||||
.withProperties({
|
||||
name: 'selectedDeployAction',
|
||||
label: localize('dacFx.deployRadioButtonLabel', 'Deploy'),
|
||||
}).component();
|
||||
|
||||
this.deployRadioButton.onDidClick(() => {
|
||||
this.model.generateScriptAndDeploy = false;
|
||||
this.instance.setDoneButton(Operation.deploy);
|
||||
this.toggleFileBrowser(false);
|
||||
});
|
||||
|
||||
return {
|
||||
component: this.deployRadioButton,
|
||||
title: ''
|
||||
};
|
||||
}
|
||||
|
||||
private async createDeployScriptRadioButton(): Promise<azdata.FormComponent> {
|
||||
this.deployScriptRadioButton = this.view.modelBuilder.radioButton()
|
||||
.withProperties({
|
||||
name: 'selectedDeployAction',
|
||||
label: localize('dacFx.deployScriptRadioButtonLabel', 'Generate Deployment Script and Deploy'),
|
||||
}).component();
|
||||
|
||||
this.deployScriptRadioButton.onDidClick(() => {
|
||||
this.model.generateScriptAndDeploy = true;
|
||||
this.instance.setDoneButton(Operation.deploy);
|
||||
this.toggleFileBrowser(true);
|
||||
});
|
||||
|
||||
return {
|
||||
component: this.deployScriptRadioButton,
|
||||
title: ''
|
||||
};
|
||||
}
|
||||
|
||||
private async createScriptRadioButton(): Promise<azdata.FormComponent> {
|
||||
this.scriptRadioButton = this.view.modelBuilder.radioButton()
|
||||
.withProperties({
|
||||
name: 'selectedDeployAction',
|
||||
label: localize('dacFx.scriptRadioButtonLabel', 'Generate Deployment Script'),
|
||||
}).component();
|
||||
|
||||
this.scriptRadioButton.onDidClick(() => {
|
||||
this.model.generateScriptAndDeploy = false;
|
||||
this.toggleFileBrowser(true);
|
||||
|
||||
//change button text and operation
|
||||
this.instance.setDoneButton(Operation.generateDeployScript);
|
||||
});
|
||||
|
||||
return {
|
||||
component: this.scriptRadioButton,
|
||||
title: ''
|
||||
};
|
||||
}
|
||||
|
||||
private async createFileBrowser(): Promise<azdata.FormComponentGroup> {
|
||||
this.createFileBrowserParts();
|
||||
|
||||
//default filepath
|
||||
this.setDefaultScriptFilePath();
|
||||
this.fileButton.onDidClick(async (click) => {
|
||||
let fileUri = await vscode.window.showSaveDialog(
|
||||
{
|
||||
defaultUri: vscode.Uri.file(this.fileTextBox.value),
|
||||
saveLabel: localize('dacfxDeployScript.saveFile', 'Save'),
|
||||
filters: {
|
||||
'SQL Files': ['sql'],
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!fileUri) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fileTextBox.value = fileUri.fsPath;
|
||||
this.model.scriptFilePath = fileUri.fsPath;
|
||||
});
|
||||
|
||||
this.fileTextBox.onTextChanged(async () => {
|
||||
this.model.scriptFilePath = this.fileTextBox.value;
|
||||
});
|
||||
|
||||
return {
|
||||
title: '',
|
||||
components: [
|
||||
{
|
||||
title: localize('dacfx.generatedScriptLocation', 'Deployment Script Location'),
|
||||
component: this.fileTextBox,
|
||||
layout: {
|
||||
horizontal: true,
|
||||
componentWidth: 400
|
||||
},
|
||||
actions: [this.fileButton]
|
||||
},],
|
||||
};
|
||||
}
|
||||
|
||||
private toggleFileBrowser(enable: boolean): void {
|
||||
this.fileTextBox.enabled = enable;
|
||||
this.fileButton.enabled = enable;
|
||||
}
|
||||
|
||||
private setDefaultScriptFilePath(): void {
|
||||
this.fileTextBox.value = path.join(this.getRootPath(), sanitizeStringForFilename(this.model.database) + '_UpgradeDACScript_' + this.getDateTime() + '.sql');
|
||||
this.model.scriptFilePath = this.fileTextBox.value;
|
||||
}
|
||||
|
||||
public setupNavigationValidator() {
|
||||
this.instance.registerNavigationValidator(() => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -123,11 +123,9 @@ export class DeployConfigPage extends DacFxConfigPage {
|
||||
this.formBuilder.addFormItem(this.databaseDropdownComponent, { horizontal: true, componentWidth: 400 });
|
||||
this.model.database = (<azdata.CategoryValue>this.databaseDropdown.value).name;
|
||||
|
||||
// add deploy plan and generate script pages
|
||||
// add deploy plan page
|
||||
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(() => {
|
||||
@@ -137,8 +135,7 @@ export class DeployConfigPage extends DacFxConfigPage {
|
||||
this.model.database = this.databaseTextBox.value;
|
||||
this.instance.setDoneButton(Operation.deploy);
|
||||
|
||||
// remove deploy plan and generate script pages
|
||||
this.instance.wizard.removePage(DeployOperationPath.deployAction);
|
||||
// remove deploy plan page
|
||||
this.instance.wizard.removePage(DeployOperationPath.deployPlan);
|
||||
});
|
||||
|
||||
@@ -160,14 +157,17 @@ export class DeployConfigPage extends DacFxConfigPage {
|
||||
}
|
||||
|
||||
protected async createDeployDatabaseDropdown(): Promise<azdata.FormComponent> {
|
||||
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
|
||||
required: true
|
||||
}).component();
|
||||
this.databaseDropdown = this.view.modelBuilder.dropDown().component();
|
||||
|
||||
//Handle database changes
|
||||
this.databaseDropdown.onValueChanged(async () => {
|
||||
this.model.database = (<azdata.CategoryValue>this.databaseDropdown.value).name;
|
||||
});
|
||||
this.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.databaseDropdown).component();
|
||||
|
||||
this.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.databaseDropdown).withProperties({
|
||||
required: true
|
||||
}).component();
|
||||
|
||||
return {
|
||||
component: this.databaseLoader,
|
||||
title: localize('dacFx.targetDatabaseDropdownTitle', 'Database Name')
|
||||
|
||||
@@ -76,8 +76,6 @@ export class SelectOperationPage extends BasePage {
|
||||
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, DeployOperationPath.deployAction);
|
||||
this.addSummaryPage(DeployOperationPath.summary);
|
||||
|
||||
// change button text and operation
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "import",
|
||||
"displayName": "SQL Server Import",
|
||||
"description": "SQL Server Import for Azure Data Studio supports importing CSV or JSON files into SQL Server.",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"engines": {
|
||||
|
||||
@@ -109,10 +109,10 @@ export class ServiceClient {
|
||||
private generateHandleServerProviderEvent(): EventAndListener {
|
||||
let dots = 0;
|
||||
return (e: string, ...args: any[]) => {
|
||||
this.outputChannel.show();
|
||||
this.statusView.show();
|
||||
switch (e) {
|
||||
case Events.INSTALL_START:
|
||||
this.outputChannel.show(true);
|
||||
this.statusView.show();
|
||||
this.outputChannel.appendLine(localize('installingServiceDetailed', 'Installing {0} service to {1}', Constants.serviceName, args[0]));
|
||||
this.statusView.text = localize('installingService', 'Installing Service');
|
||||
break;
|
||||
@@ -121,7 +121,7 @@ export class ServiceClient {
|
||||
break;
|
||||
case Events.DOWNLOAD_START:
|
||||
this.outputChannel.appendLine(localize('downloadingService', 'Downloading {0}', args[0]));
|
||||
this.outputChannel.append(`(${Math.ceil(args[1] / 1024)} KB)`);
|
||||
this.outputChannel.append(localize('downloadingServiceSize', '({0} KB)', Math.ceil(args[1] / 1024).toLocaleString(vscode.env.language)));
|
||||
this.statusView.text = localize('downloadingServiceStatus', 'Downloading Service');
|
||||
break;
|
||||
case Events.DOWNLOAD_PROGRESS:
|
||||
@@ -135,6 +135,7 @@ export class ServiceClient {
|
||||
this.outputChannel.appendLine(localize('downloadingServiceComplete', 'Done!'));
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown event from Server Provider ${e}`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||