From 82d0b6c9f01440b0e22dd915967ce2fab18e1874 Mon Sep 17 00:00:00 2001 From: ranasaria <41588310+ranasaria@users.noreply.github.com> Date: Wed, 26 Sep 2018 10:32:12 -0700 Subject: [PATCH 01/11] Bumping toolservice version to 36 --- extensions/mssql/src/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/mssql/src/config.json b/extensions/mssql/src/config.json index c12fe99b36..6785710704 100644 --- a/extensions/mssql/src/config.json +++ b/extensions/mssql/src/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "1.5.0-alpha.34", + "version": "1.5.0-alpha.36", "downloadFileNames": { "Windows_86": "win-x86-netcoreapp2.1.zip", "Windows_64": "win-x64-netcoreapp2.1.zip", From 933aa88dc7592fe71b415e71cf6135129f5721a6 Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Mon, 8 Oct 2018 17:24:40 -0700 Subject: [PATCH 02/11] Account for Horizontal Scrolling in Grid (#2774) * implement horizontal scroll login in the grid plugin * remove commented code * formatting --- .../plugins/mousewheelTableScroll.plugin.ts | 85 ++++++++++++++----- 1 file changed, 63 insertions(+), 22 deletions(-) diff --git a/src/sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin.ts b/src/sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin.ts index f45bd29b99..0e363938c0 100644 --- a/src/sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/mousewheelTableScroll.plugin.ts @@ -6,6 +6,7 @@ 'use strict'; import * as DOM from 'vs/base/browser/dom'; +import * as Platform from 'vs/base/common/platform'; import { StandardMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { mixin } from 'vs/base/common/objects'; @@ -29,7 +30,7 @@ export class MouseWheelSupport implements Slick.Plugin { private _disposables: IDisposable[] = []; constructor(options: IMouseWheelSupportOptions = {}) { - this.options = mixin(options, defaultOptions); + this.options = mixin(options, defaultOptions, false); } public init(grid: Slick.Grid): void { @@ -43,29 +44,69 @@ export class MouseWheelSupport implements Slick.Plugin { this._disposables.push(DOM.addDisposableListener(this.viewport, 'DOMMouseScroll', onMouseWheel)); } - private _onMouseWheel(event: StandardMouseWheelEvent) { - const scrollHeight = this.canvas.clientHeight; - const height = this.viewport.clientHeight; - const scrollDown = Math.sign(event.deltaY) === -1; - if (scrollDown) { - if ((this.viewport.scrollTop - (event.deltaY * this.options.scrollSpeed)) + height > scrollHeight) { - this.viewport.scrollTop = scrollHeight - height; - this.viewport.dispatchEvent(new Event('scroll')); - } else { - this.viewport.scrollTop = this.viewport.scrollTop - (event.deltaY * this.options.scrollSpeed); - this.viewport.dispatchEvent(new Event('scroll')); - event.stopPropagation(); - event.preventDefault(); + private _onMouseWheel(e: StandardMouseWheelEvent) { + if (e.deltaY || e.deltaX) { + let deltaY = e.deltaY * this.options.scrollSpeed; + let deltaX = e.deltaX * this.options.scrollSpeed; + const scrollHeight = this.canvas.clientHeight; + const scrollWidth = this.canvas.clientWidth; + const height = this.viewport.clientHeight; + const width = this.viewport.clientWidth; + + // Convert vertical scrolling to horizontal if shift is held, this + // is handled at a higher level on Mac + const shiftConvert = !Platform.isMacintosh && e.browserEvent && e.browserEvent.shiftKey; + if (shiftConvert && !deltaX) { + deltaX = deltaY; + deltaY = 0; } - } else { - if ((this.viewport.scrollTop - (event.deltaY * this.options.scrollSpeed)) < 0) { - this.viewport.scrollTop = 0; - this.viewport.dispatchEvent(new Event('scroll')); + + // scroll down + if (deltaY < 0) { + if ((this.viewport.scrollTop - deltaY) + height > scrollHeight) { + this.viewport.scrollTop = scrollHeight - height; + this.viewport.dispatchEvent(new Event('scroll')); + } else { + this.viewport.scrollTop = this.viewport.scrollTop - deltaY; + this.viewport.dispatchEvent(new Event('scroll')); + event.stopPropagation(); + event.preventDefault(); + } + // scroll up } else { - this.viewport.scrollTop = this.viewport.scrollTop - (event.deltaY * this.options.scrollSpeed); - this.viewport.dispatchEvent(new Event('scroll')); - event.stopPropagation(); - event.preventDefault(); + if ((this.viewport.scrollTop - deltaY) < 0) { + this.viewport.scrollTop = 0; + this.viewport.dispatchEvent(new Event('scroll')); + } else { + this.viewport.scrollTop = this.viewport.scrollTop - deltaY; + this.viewport.dispatchEvent(new Event('scroll')); + event.stopPropagation(); + event.preventDefault(); + } + } + + // scroll left + if (deltaX < 0) { + if ((this.viewport.scrollLeft - deltaX) + width > scrollWidth) { + this.viewport.scrollLeft = scrollWidth - width; + this.viewport.dispatchEvent(new Event('scroll')); + } else { + this.viewport.scrollLeft = this.viewport.scrollLeft - deltaX; + this.viewport.dispatchEvent(new Event('scroll')); + event.stopPropagation(); + event.preventDefault(); + } + // scroll left + } else { + if ((this.viewport.scrollLeft - deltaX) < 0) { + this.viewport.scrollLeft = 0; + this.viewport.dispatchEvent(new Event('scroll')); + } else { + this.viewport.scrollLeft = this.viewport.scrollLeft - deltaX; + this.viewport.dispatchEvent(new Event('scroll')); + event.stopPropagation(); + event.preventDefault(); + } } } } From 7aa2ee08bf3af8bb2a096fcf0865909b29da81c0 Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Mon, 8 Oct 2018 17:30:08 -0700 Subject: [PATCH 03/11] properly reset to handle maximized grids (#2786) --- src/sql/parts/query/editor/gridPanel.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sql/parts/query/editor/gridPanel.ts b/src/sql/parts/query/editor/gridPanel.ts index 12bd059aa5..f6ba04b042 100644 --- a/src/sql/parts/query/editor/gridPanel.ts +++ b/src/sql/parts/query/editor/gridPanel.ts @@ -242,6 +242,7 @@ export class GridPanel extends ViewletPanel { } dispose(this.tables); this.tables = []; + this.maximizedGrid = undefined; this.maximumBodySize = this.tables.reduce((p, c) => { return p + c.maximumSize; From c800e70ec128f698fb3c3f20eb1fea0f64a95f3b Mon Sep 17 00:00:00 2001 From: Aditya Bist Date: Tue, 9 Oct 2018 10:28:55 -0700 Subject: [PATCH 04/11] Agent - Step Actions (#2779) * fixed right click context menu bug in jobs view * added stepInfo and edit job WIP * show jobs in job edit * added schedule description on select schedule * fetch schedules during history and show in edit job * added alerts to job histories and show in edit * made history calls async * filter menus now close when esc is pressed * fixed bug where clicking on error row wouldnt populate job details * added functionality to delete steps in a job * added real time adding steps in edit job --- extensions/agent/src/data/jobStepData.ts | 55 ++++++++++++++----- extensions/agent/src/dialogs/agentDialog.ts | 6 +- extensions/agent/src/dialogs/jobDialog.ts | 24 ++++++-- extensions/agent/src/dialogs/jobStepDialog.ts | 30 +++++----- .../agent/src/dialogs/pickScheduleDialog.ts | 1 - extensions/agent/src/interfaces.ts | 2 +- extensions/agent/src/mainController.ts | 11 ++-- .../parts/jobManagement/common/interfaces.ts | 2 + .../parts/jobManagement/common/jobActions.ts | 43 +++++++++++++++ .../common/jobManagementService.ts | 36 ++++++++---- .../views/jobHistory.component.ts | 1 - .../views/jobStepsView.component.ts | 6 +- .../jobManagement/views/jobStepsViewTree.ts | 10 ---- .../jobManagement/views/jobsView.component.ts | 19 ++++++- src/sql/sqlops.d.ts | 6 +- .../workbench/api/node/extHostDataProtocol.ts | 7 +++ .../api/node/mainThreadDataProtocol.ts | 3 + .../workbench/api/node/sqlExtHost.protocol.ts | 5 ++ 18 files changed, 190 insertions(+), 77 deletions(-) diff --git a/extensions/agent/src/data/jobStepData.ts b/extensions/agent/src/data/jobStepData.ts index e11ec015c8..79efe4bd39 100644 --- a/extensions/agent/src/data/jobStepData.ts +++ b/extensions/agent/src/data/jobStepData.ts @@ -6,37 +6,47 @@ import { AgentUtils } from '../agentUtils'; import { IAgentDialogData, AgentDialogMode } from '../interfaces'; +import { JobData } from './jobData'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); export class JobStepData implements IAgentDialogData { + + // Error Messages + private readonly CreateStepErrorMessage_JobNameIsEmpty = localize('stepData.jobNameRequired', 'Job name must be provided'); + private readonly CreateStepErrorMessage_StepNameIsEmpty = localize('stepData.stepNameRequired', 'Step name must be provided'); + public dialogMode: AgentDialogMode = AgentDialogMode.CREATE; public ownerUri: string; - public jobId: string; // + public jobId: string; public jobName: string; - public script: string; // + public script: string; public scriptName: string; - public stepName: string; // - public subSystem: string; // + public stepName: string; + public subSystem: string; public id: number; - public failureAction: string; // - public successAction: string; // + public failureAction: string; + public successAction: string; public failStepId: number; public successStepId: number; public command: string; public commandExecutionSuccessCode: number; - public databaseName: string; // + public databaseName: string; public databaseUserName: string; public server: string; - public outputFileName: string; // + public outputFileName: string; public appendToLogFile: boolean; public appendToStepHist: boolean; public writeLogToTable: boolean; public appendLogToTable: boolean; - public retryAttempts: number; // - public retryInterval: number; // + public retryAttempts: number; + public retryInterval: number; public proxyName: string; - constructor(ownerUri:string) { + constructor(ownerUri:string, jobModel?: JobData) { this.ownerUri = ownerUri; + this.jobName = jobModel.name; } public async initialize() { @@ -51,7 +61,7 @@ export class JobStepData implements IAgentDialogData { scriptName: this.scriptName, stepName: this.stepName, subSystem: this.subSystem, - id: 1, + id: this.id, failureAction: this.failureAction, successAction: this.successAction, failStepId: this.failStepId, @@ -70,7 +80,26 @@ export class JobStepData implements IAgentDialogData { retryInterval: this.retryInterval, proxyName: this.proxyName }).then(result => { - console.info(result); + if (result && result.success) { + console.info(result); + } }); } + + public validate(): { valid: boolean, errorMessages: string[] } { + let validationErrors: string[] = []; + + if (!(this.stepName && this.stepName.trim())) { + validationErrors.push(this.CreateStepErrorMessage_StepNameIsEmpty); + } + + if (!(this.jobName && this.jobName.trim())) { + validationErrors.push(this.CreateStepErrorMessage_JobNameIsEmpty); + } + + return { + valid: validationErrors.length === 0, + errorMessages: validationErrors + }; + } } \ No newline at end of file diff --git a/extensions/agent/src/dialogs/agentDialog.ts b/extensions/agent/src/dialogs/agentDialog.ts index fa2c1cea85..63d5d240d2 100644 --- a/extensions/agent/src/dialogs/agentDialog.ts +++ b/extensions/agent/src/dialogs/agentDialog.ts @@ -49,10 +49,8 @@ export abstract class AgentDialog { protected async execute() { this.updateModel(); - let success = await this.model.save(); - if (success) { - this._onSuccess.fire(this.model); - } + await this.model.save(); + this._onSuccess.fire(this.model); } protected async cancel() { diff --git a/extensions/agent/src/dialogs/jobDialog.ts b/extensions/agent/src/dialogs/jobDialog.ts index ab86a4f699..8bd76509ee 100644 --- a/extensions/agent/src/dialogs/jobDialog.ts +++ b/extensions/agent/src/dialogs/jobDialog.ts @@ -10,6 +10,7 @@ import { JobStepDialog } from './jobStepDialog'; import { PickScheduleDialog } from './pickScheduleDialog'; import { AlertDialog } from './alertDialog'; import { AgentDialog } from './agentDialog'; +import { AgentUtils } from '../agentUtils'; const localize = nls.loadMessageBundle(); @@ -234,10 +235,14 @@ export class JobDialog extends AgentDialog { width: 80 }).component(); + let stepDialog = new JobStepDialog(this.model.ownerUri, '' , data.length + 1, this.model); + stepDialog.onSuccess((step) => { + this.model.jobSteps.push(step); + this.stepsTable.data = this.convertStepsToData(this.model.jobSteps); + }); this.newStepButton.onDidClick((e)=>{ if (this.nameTextBox.value && this.nameTextBox.value.length > 0) { - let stepDialog = new JobStepDialog(this.model.ownerUri, this.nameTextBox.value, '' , 1, this.model); - stepDialog.openNewStepDialog(); + stepDialog.openDialog(); } else { this.dialog.message = { text: this.BlankJobNameErrorText }; } @@ -273,9 +278,17 @@ export class JobDialog extends AgentDialog { }); this.deleteStepButton.onDidClick((e) => { - // implement delete steps - - + AgentUtils.getAgentService().then((agentService) => { + let steps = this.model.jobSteps ? this.model.jobSteps : []; + agentService.deleteJobStep(this.ownerUri, stepData).then((result) => { + if (result && result.success) { + delete steps[rowNumber]; + this.model.jobSteps = steps; + let data = this.convertStepsToData(steps); + this.stepsTable.data = data; + } + }); + }); }); } }); @@ -529,7 +542,6 @@ export class JobDialog extends AgentDialog { let result = []; alerts.forEach(alert => { let cols = []; - console.log(alert); cols.push(alert.name); cols.push(alert.isEnabled); cols.push(alert.alertType.toString()); diff --git a/extensions/agent/src/dialogs/jobStepDialog.ts b/extensions/agent/src/dialogs/jobStepDialog.ts index a7be6423ab..737851d448 100644 --- a/extensions/agent/src/dialogs/jobStepDialog.ts +++ b/extensions/agent/src/dialogs/jobStepDialog.ts @@ -9,11 +9,12 @@ import * as vscode from 'vscode'; import { JobStepData } from '../data/jobStepData'; import { AgentUtils } from '../agentUtils'; import { JobData } from '../data/jobData'; +import { AgentDialog } from './agentDialog'; const path = require('path'); const localize = nls.loadMessageBundle(); -export class JobStepDialog { +export class JobStepDialog extends AgentDialog { // TODO: localize // Top level @@ -67,7 +68,6 @@ export class JobStepDialog { // UI Components // Dialogs - private dialog: sqlops.window.modelviewdialog.Dialog; private fileBrowserDialog: sqlops.window.modelviewdialog.Dialog; // Dialog tabs @@ -105,35 +105,27 @@ export class JobStepDialog { private fileBrowserTree: sqlops.FileBrowserTreeComponent; private jobModel: JobData; - private model: JobStepData; - private ownerUri: string; private jobName: string; private server: string; private stepId: number; constructor( ownerUri: string, - jobName: string, server: string, stepId: number, jobModel?: JobData ) { - this.model = new JobStepData(ownerUri); + super(ownerUri, new JobStepData(ownerUri, jobModel), 'New Step'); this.stepId = stepId; - this.ownerUri = ownerUri; - this.jobName = jobName; + this.jobName = jobModel.name; this.server = server; this.jobModel = jobModel; } private initializeUIComponents() { - this.dialog = sqlops.window.modelviewdialog.createDialog(this.DialogTitle); this.generalTab = sqlops.window.modelviewdialog.createTab(this.GeneralTabText); this.advancedTab = sqlops.window.modelviewdialog.createTab(this.AdvancedTabText); this.dialog.content = [this.generalTab, this.advancedTab]; - this.dialog.okButton.onClick(async () => await this.execute()); - this.dialog.okButton.label = this.OkButtonText; - this.dialog.cancelButton.label = this.CancelButtonText; } private createCommands(view, queryProvider: sqlops.QueryProvider) { @@ -478,7 +470,7 @@ export class JobStepDialog { return outputFileForm; } - protected execute() { + protected updateModel() { this.model.stepName = this.nameTextBox.value; if (!this.model.stepName || this.model.stepName.length === 0) { this.dialog.message = this.dialog.message = { text: this.BlankStepNameErrorText }; @@ -499,12 +491,20 @@ export class JobStepDialog { this.model.appendToLogFile = this.appendToExistingFileCheckbox.checked; } - public async openNewStepDialog() { + public async initializeDialog() { let databases = await AgentUtils.getDatabases(this.ownerUri); let queryProvider = await AgentUtils.getQueryProvider(); this.initializeUIComponents(); this.createGeneralTab(databases, queryProvider); this.createAdvancedTab(); - sqlops.window.modelviewdialog.openDialog(this.dialog); + this.dialog.registerCloseValidator(() => { + this.updateModel(); + let validationResult = this.model.validate(); + if (!validationResult.valid) { + // TODO: Show Error Messages + console.error(validationResult.errorMessages.join(',')); + } + return validationResult.valid; + }); } } \ No newline at end of file diff --git a/extensions/agent/src/dialogs/pickScheduleDialog.ts b/extensions/agent/src/dialogs/pickScheduleDialog.ts index 76e17e94a3..5fcd1ac1d1 100644 --- a/extensions/agent/src/dialogs/pickScheduleDialog.ts +++ b/extensions/agent/src/dialogs/pickScheduleDialog.ts @@ -74,7 +74,6 @@ export class PickScheduleDialog { let data: any[][] = []; for (let i = 0; i < this.model.schedules.length; ++i) { let schedule = this.model.schedules[i]; - console.log(schedule); data[i] = [ schedule.id, schedule.name, schedule.description ]; } this.schedulesTable.data = data; diff --git a/extensions/agent/src/interfaces.ts b/extensions/agent/src/interfaces.ts index 1ea6f4dc05..0e86394477 100644 --- a/extensions/agent/src/interfaces.ts +++ b/extensions/agent/src/interfaces.ts @@ -13,5 +13,5 @@ export enum AgentDialogMode { export interface IAgentDialogData { dialogMode: AgentDialogMode; initialize(): void; - save(): void; + save(): Promise; } diff --git a/extensions/agent/src/mainController.ts b/extensions/agent/src/mainController.ts index e0d1eedb6a..2797e4512c 100644 --- a/extensions/agent/src/mainController.ts +++ b/extensions/agent/src/mainController.ts @@ -40,9 +40,9 @@ export class MainController { let dialog = new JobDialog(ownerUri, jobInfo); dialog.openDialog(); }); - vscode.commands.registerCommand('agent.openNewStepDialog', (ownerUri: string, jobId: string, server: string, stepId: number) => { - let dialog = new JobStepDialog(ownerUri, jobId, server, stepId); - dialog.openNewStepDialog(); + vscode.commands.registerCommand('agent.openNewStepDialog', (ownerUri: string, server: string, stepId: number) => { + let dialog = new JobStepDialog(ownerUri, server, stepId); + dialog.openDialog(); }); vscode.commands.registerCommand('agent.openPickScheduleDialog', (ownerUri: string) => { let dialog = new PickScheduleDialog(ownerUri); @@ -57,9 +57,8 @@ export class MainController { dialog.openDialog(); }); vscode.commands.registerCommand('agent.openProxyDialog', (ownerUri: string, proxyInfo: sqlops.AgentProxyInfo, credentials: sqlops.CredentialInfo[]) => { - //@TODO: reenable create proxy after snapping July release (7/14/18) - // let dialog = new ProxyDialog(ownerUri, proxyInfo, credentials); - // dialog.openDialog(); + let dialog = new ProxyDialog(ownerUri, proxyInfo, credentials); + dialog.openDialog(); MainController.showNotYetImplemented(); }); } diff --git a/src/sql/parts/jobManagement/common/interfaces.ts b/src/sql/parts/jobManagement/common/interfaces.ts index f347da987e..2074d9454a 100644 --- a/src/sql/parts/jobManagement/common/interfaces.ts +++ b/src/sql/parts/jobManagement/common/interfaces.ts @@ -25,6 +25,8 @@ export interface IJobManagementService { getJobHistory(connectionUri: string, jobID: string): Thenable; deleteJob(connectionUri: string, job: sqlops.AgentJobInfo): Thenable; + deleteJobStep(connectionUri: string, step: sqlops.AgentJobStepInfo): Thenable; + getAlerts(connectionUri: string): Thenable; deleteAlert(connectionUri: string, alert: sqlops.AgentAlertInfo): Thenable; diff --git a/src/sql/parts/jobManagement/common/jobActions.ts b/src/sql/parts/jobManagement/common/jobActions.ts index fbd8b21317..ef48595e69 100644 --- a/src/sql/parts/jobManagement/common/jobActions.ts +++ b/src/sql/parts/jobManagement/common/jobActions.ts @@ -26,6 +26,7 @@ export enum JobActions { export interface IJobActionInfo { ownerUri: string; targetObject: any; + jobHistoryComponent?: JobHistoryComponent; } // Job actions @@ -230,6 +231,48 @@ export class NewStepAction extends Action { } } +export class DeleteStepAction extends Action { + public static ID = 'jobaction.deleteStep'; + public static LABEL = nls.localize('jobaction.deleteStep', "Delete Step"); + + constructor( + @INotificationService private _notificationService: INotificationService, + @IJobManagementService private _jobService: IJobManagementService, + @IInstantiationService private instantationService: IInstantiationService + ) { + super(DeleteStepAction.ID, DeleteStepAction.LABEL); + } + + public run(actionInfo: IJobActionInfo): TPromise { + let self = this; + let step = actionInfo.targetObject as sqlops.AgentJobStepInfo; + let refreshAction = this.instantationService.createInstance(JobsRefreshAction); + self._notificationService.prompt( + Severity.Info, + nls.localize('jobaction.deleteStepConfirm,', "Are you sure you'd like to delete the step '{0}'?", step.stepName), + [{ + label: DeleteStepAction.LABEL, + run: () => { + self._jobService.deleteJobStep(actionInfo.ownerUri, actionInfo.targetObject).then(result => { + if (!result || !result.success) { + let errorMessage = nls.localize("jobaction.failedToDeleteStep", "Could not delete step '{0}'.\nError: {1}", + step.stepName, result.errorMessage ? result.errorMessage : 'Unknown error'); + self._notificationService.error(errorMessage); + } else { + refreshAction.run(actionInfo.jobHistoryComponent); + } + }); + } + }, { + label: DeleteAlertAction.CancelLabel, + run: () => { } + }] + ); + return TPromise.as(true); + } +} + + // Alert Actions export class NewAlertAction extends Action { diff --git a/src/sql/parts/jobManagement/common/jobManagementService.ts b/src/sql/parts/jobManagement/common/jobManagementService.ts index 401c487a5f..eaa4ceea2e 100644 --- a/src/sql/parts/jobManagement/common/jobManagementService.ts +++ b/src/sql/parts/jobManagement/common/jobManagementService.ts @@ -30,6 +30,7 @@ export class JobManagementService implements IJobManagementService { this._onDidChange.fire(void 0); } + // Jobs public getJobs(connectionUri: string): Thenable { return this._runAction(connectionUri, (runner) => { return runner.getJobs(connectionUri); @@ -42,6 +43,27 @@ export class JobManagementService implements IJobManagementService { }); } + public getJobHistory(connectionUri: string, jobID: string): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.getJobHistory(connectionUri, jobID); + }); + } + + public jobAction(connectionUri: string, jobName: string, action: string): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.jobAction(connectionUri, jobName, action); + }); + } + + // Steps + public deleteJobStep(connectionUri: string, stepInfo: sqlops.AgentJobStepInfo): Thenable { + return this._runAction(connectionUri, (runner) => { + return runner.deleteJobStep(connectionUri, stepInfo); + }); + } + + + // Alerts public getAlerts(connectionUri: string): Thenable { return this._runAction(connectionUri, (runner) => { return runner.getAlerts(connectionUri); @@ -54,6 +76,7 @@ export class JobManagementService implements IJobManagementService { }); } + // Operators public getOperators(connectionUri: string): Thenable { return this._runAction(connectionUri, (runner) => { return runner.getOperators(connectionUri); @@ -66,6 +89,7 @@ export class JobManagementService implements IJobManagementService { }); } + // Proxies public getProxies(connectionUri: string): Thenable { return this._runAction(connectionUri, (runner) => { return runner.getProxies(connectionUri); @@ -84,18 +108,6 @@ export class JobManagementService implements IJobManagementService { }); } - public getJobHistory(connectionUri: string, jobID: string): Thenable { - return this._runAction(connectionUri, (runner) => { - return runner.getJobHistory(connectionUri, jobID); - }); - } - - public jobAction(connectionUri: string, jobName: string, action: string): Thenable { - return this._runAction(connectionUri, (runner) => { - return runner.jobAction(connectionUri, jobName, action); - }); - } - private _runAction(uri: string, action: (handler: sqlops.AgentServicesProvider) => Thenable): Thenable { let providerId: string = this._connectionService.getProviderIdFromUri(uri); diff --git a/src/sql/parts/jobManagement/views/jobHistory.component.ts b/src/sql/parts/jobManagement/views/jobHistory.component.ts index fc5971f574..7fe8d1e5ab 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.component.ts +++ b/src/sql/parts/jobManagement/views/jobHistory.component.ts @@ -74,7 +74,6 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { @Inject(forwardRef(() => CommonServiceInterface)) commonService: CommonServiceInterface, @Inject(forwardRef(() => AgentViewComponent)) private _agentViewComponent: AgentViewComponent, @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService, - @Inject(INotificationService) private _notificationService: INotificationService, @Inject(IInstantiationService) private instantiationService: IInstantiationService, @Inject(IContextMenuService) private contextMenuService: IContextMenuService, @Inject(IJobManagementService) private _jobManagementService: IJobManagementService, diff --git a/src/sql/parts/jobManagement/views/jobStepsView.component.ts b/src/sql/parts/jobManagement/views/jobStepsView.component.ts index 495e62abe8..7a92babbc0 100644 --- a/src/sql/parts/jobManagement/views/jobStepsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobStepsView.component.ts @@ -20,7 +20,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TabChild } from 'sql/base/browser/ui/panel/tab.component'; -import * as dom from 'vs/base/browser/dom'; +import { IJobManagementService } from 'sql/parts/jobManagement/common/interfaces'; export const JOBSTEPSVIEW_SELECTOR: string = 'jobstepsview-component'; @@ -36,7 +36,7 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit, private _treeDataSource = new JobStepsViewDataSource(); private _treeRenderer = new JobStepsViewRenderer(); private _treeFilter = new JobStepsViewFilter(); - private static _pageSize = 1024; + private _pageSize = 1024; @ViewChild('table') private _tableContainer: ElementRef; @@ -66,7 +66,7 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit, }, { verticalScrollMode: ScrollbarVisibility.Visible }); this._register(attachListStyler(this._tree, this.themeService)); } - this._tree.layout(500); + this._tree.layout(this._pageSize); this._tree.setInput(new JobStepsViewModel()); $('jobstepsview-component .steps-tree .monaco-tree').attr('tabIndex', '-1'); $('jobstepsview-component .steps-tree .monaco-tree-row').attr('tabIndex', '0'); diff --git a/src/sql/parts/jobManagement/views/jobStepsViewTree.ts b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts index 49c021f4cd..ac03318b61 100644 --- a/src/sql/parts/jobManagement/views/jobStepsViewTree.ts +++ b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts @@ -10,7 +10,6 @@ import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults'; import { Promise, TPromise } from 'vs/base/common/winjs.base'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { generateUuid } from 'vs/base/common/uuid'; -import { AgentJobHistoryInfo } from 'sqlops'; import { JobManagementUtilities } from 'sql/parts/jobManagement/common/jobManagementUtilities'; export class JobStepsViewRow { @@ -27,7 +26,6 @@ export class JobStepsViewModel { } export class JobStepsViewController extends TreeDefaults.DefaultController { - private _jobHistories: AgentJobHistoryInfo[]; protected onLeftClick(tree: tree.ITree, element: JobStepsViewRow, event: IMouseEvent, origin: string = 'mouse'): boolean { return true; @@ -37,14 +35,6 @@ export class JobStepsViewController extends TreeDefaults.DefaultController { return true; } - public set jobHistories(value: AgentJobHistoryInfo[]) { - this._jobHistories = value; - } - - public get jobHistories(): AgentJobHistoryInfo[] { - return this._jobHistories; - } - } export class JobStepsViewDataSource implements tree.IDataSource { diff --git a/src/sql/parts/jobManagement/views/jobsView.component.ts b/src/sql/parts/jobManagement/views/jobsView.component.ts index 6625051cad..a6646460ca 100644 --- a/src/sql/parts/jobManagement/views/jobsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobsView.component.ts @@ -584,7 +584,12 @@ export class JobsViewComponent extends JobManagementView implements OnInit { self.jobHistories[job.jobId] = result.jobs; self._jobCacheObject.setJobHistory(job.jobId, result.jobs); let jobHistories = self._jobCacheObject.getJobHistory(job.jobId); - let previousRuns = jobHistories.slice(jobHistories.length - 5, jobHistories.length); + let previousRuns: sqlops.AgentJobHistoryInfo[]; + if (jobHistories.length >= 5) { + previousRuns = jobHistories.slice(jobHistories.length - 5, jobHistories.length); + } else { + previousRuns = jobHistories; + } self.createJobChart(job.jobId, previousRuns); if (self._agentViewComponent.expanded.has(job.jobId)) { let lastJobHistory = jobHistories[result.jobs.length - 1]; @@ -645,12 +650,22 @@ export class JobsViewComponent extends JobManagementView implements OnInit { maxDuration = maxDuration === 0 ? 1 : maxDuration; let maxBarHeight: number = 24; let chartHeights = []; + let zeroDurationJobCount = 0; for (let i = 0; i < jobHistories.length; i++) { let duration = jobHistories[i].runDuration; let chartHeight = (maxBarHeight * JobManagementUtilities.convertDurationToSeconds(duration)) / maxDuration; chartHeights.push(`${chartHeight}px`); + if (chartHeight === 0) { + zeroDurationJobCount++; + } + } + // if the durations are all 0 secs, show minimal chart + // instead of nothing + if (zeroDurationJobCount === jobHistories.length) { + return ['5px', '5px', '5px', '5px', '5px']; + } else { + return chartHeights; } - return chartHeights; } private expandJobs(start: boolean): void { diff --git a/src/sql/sqlops.d.ts b/src/sql/sqlops.d.ts index 401cc534e4..a34e97b8dc 100644 --- a/src/sql/sqlops.d.ts +++ b/src/sql/sqlops.d.ts @@ -1537,9 +1537,9 @@ declare module 'sqlops' { getJobDefaults(ownerUri: string): Thenable; // Job Step management methods - createJobStep(ownerUri: string, jobInfo: AgentJobStepInfo): Thenable; - updateJobStep(ownerUri: string, originalJobStepName: string, jobInfo: AgentJobStepInfo): Thenable; - deleteJobStep(ownerUri: string, jobInfo: AgentJobStepInfo): Thenable; + createJobStep(ownerUri: string, stepInfo: AgentJobStepInfo): Thenable; + updateJobStep(ownerUri: string, originalJobStepName: string, stepInfo: AgentJobStepInfo): Thenable; + deleteJobStep(ownerUri: string, stepInfo: AgentJobStepInfo): Thenable; // Alert management methods getAlerts(ownerUri: string): Thenable; diff --git a/src/sql/workbench/api/node/extHostDataProtocol.ts b/src/sql/workbench/api/node/extHostDataProtocol.ts index 95bd9d950a..0d430c383e 100644 --- a/src/sql/workbench/api/node/extHostDataProtocol.ts +++ b/src/sql/workbench/api/node/extHostDataProtocol.ts @@ -596,6 +596,13 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { throw this._resolveProvider(handle).deleteJob(ownerUri, job); } + /** + * Deletes a job step + */ + $deleteJobStep(handle: number, ownerUri: string, step: sqlops.AgentJobStepInfo): Thenable { + throw this._resolveProvider(handle).deleteJobStep(ownerUri, step); + } + /** * Get Agent Alerts list */ diff --git a/src/sql/workbench/api/node/mainThreadDataProtocol.ts b/src/sql/workbench/api/node/mainThreadDataProtocol.ts index ba8fa88f2e..bed01823a2 100644 --- a/src/sql/workbench/api/node/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/node/mainThreadDataProtocol.ts @@ -359,6 +359,9 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape { deleteJob(connectionUri: string, jobInfo: sqlops.AgentJobInfo): Thenable { return self._proxy.$deleteJob(handle, connectionUri, jobInfo); }, + deleteJobStep(connectionUri: string, stepInfo: sqlops.AgentJobStepInfo): Thenable { + return self._proxy.$deleteJobStep(handle, connectionUri, stepInfo); + }, getAlerts(connectionUri: string): Thenable { return self._proxy.$getAlerts(handle, connectionUri); }, diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 77ce5b268c..7b7d94bda3 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -370,6 +370,11 @@ export abstract class ExtHostDataProtocolShape { */ $deleteJob(handle: number, ownerUri: string, job: sqlops.AgentJobInfo): Thenable { throw ni(); } + /** + * Deletes a job step + */ + $deleteJobStep(handle: number, ownerUri: string, step: sqlops.AgentJobStepInfo): Thenable { throw ni(); } + /** * Get Agent Alerts list */ From 34f6811eea6c3315fec8d43d1dce476766445a04 Mon Sep 17 00:00:00 2001 From: Matt Irvine Date: Tue, 9 Oct 2018 11:12:12 -0700 Subject: [PATCH 05/11] Bring back Connection Config tests (#2795) --- .../parts/connection/connectionConfig.test.ts | 1761 +++++++++-------- 1 file changed, 891 insertions(+), 870 deletions(-) diff --git a/src/sqltest/parts/connection/connectionConfig.test.ts b/src/sqltest/parts/connection/connectionConfig.test.ts index 4f00287202..b190847e04 100644 --- a/src/sqltest/parts/connection/connectionConfig.test.ts +++ b/src/sqltest/parts/connection/connectionConfig.test.ts @@ -12,29 +12,88 @@ import { IConnectionProfile, IConnectionProfileStore } from 'sql/parts/connectio import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { WorkspaceConfigurationTestService } from 'sqltest/stubs/workspaceConfigurationTestService'; -import { IConfigurationValue as TConfigurationValue, ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService'; +import { IConfigurationValue, ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService'; import * as Constants from 'sql/parts/connection/common/constants'; import { IConnectionProfileGroup, ConnectionProfileGroup } from 'sql/parts/connection/common/connectionProfileGroup'; import { TPromise } from 'vs/base/common/winjs.base'; import * as assert from 'assert'; -import { CapabilitiesService } from 'sql/services/capabilities/capabilitiesService'; +import { ProviderFeatures, ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService'; import * as sqlops from 'sqlops'; import { Emitter } from 'vs/base/common/event'; +import { ConnectionOptionSpecialType, ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { StorageService, InMemoryLocalStorage } from 'vs/platform/storage/common/storageService'; +import { CapabilitiesTestService } from 'sqltest/stubs/capabilitiesTestService'; suite('SQL ConnectionConfig tests', () => { - /* - let capabilitiesService: TypeMoq.Mock; - let workspaceConfigurationServiceMock: TypeMoq.Mock; - let configEditingServiceMock: TypeMoq.Mock; - let msSQLCapabilities: sqlops.DataProtocolServerCapabilities; - let capabilities: sqlops.DataProtocolServerCapabilities[]; - let onProviderRegistered = new Emitter(); - - let configValueToConcat: TConfigurationValue = { - workspace: [{ + let capabilitiesService: TypeMoq.Mock; + let workspaceConfigurationServiceMock: TypeMoq.Mock; + let configEditingServiceMock: TypeMoq.Mock; + let msSQLCapabilities: ProviderFeatures; + let capabilities: ProviderFeatures[]; + let onCapabilitiesRegistered = new Emitter(); + + let configValueToConcat = { + workspace: [{ + name: 'g1', + id: 'g1', + parentId: 'ROOT', + color: 'pink', + description: 'g1' + }, + { + name: 'g1-1', + id: 'g1-1', + parentId: 'g1', + color: 'blue', + description: 'g1-1' + } + ], + user: [{ + name: 'ROOT', + id: 'ROOT', + parentId: '', + color: 'white', + description: 'ROOT' + }, { + name: 'g2', + id: 'g2', + parentId: 'ROOT', + color: 'green', + description: 'g2' + }, + { + name: 'g2-1', + id: 'g2-1', + parentId: 'g2', + color: 'yellow', + description: 'g2' + }, + { + name: 'g3', + id: 'g3', + parentId: '', + color: 'orange', + description: 'g3' + }, + { + name: 'g3-1', + id: 'g3-1', + parentId: 'g3', + color: 'purple', + description: 'g3-1' + } + ], + value: [], + default: [], + workspaceFolder: [] + }; + + let configValueToMerge = { + workspace: [ + { name: 'g1', id: 'g1', - parentId: 'ROOT', + parentId: '', color: 'pink', description: 'g1' }, @@ -45,17 +104,12 @@ suite('SQL ConnectionConfig tests', () => { color: 'blue', description: 'g1-1' } - ], - user: [{ - name: 'ROOT', - id: 'ROOT', - parentId: '', - color: 'white', - description: 'ROOT' - }, { + ], + user: [ + { name: 'g2', id: 'g2', - parentId: 'ROOT', + parentId: '', color: 'green', description: 'g2' }, @@ -67,871 +121,838 @@ suite('SQL ConnectionConfig tests', () => { description: 'g2' }, { - name: 'g3', - id: 'g3', + name: 'g1', + id: 'g1', parentId: '', - color: 'orange', - description: 'g3' + color: 'pink', + description: 'g1' }, { - name: 'g3-1', - id: 'g3-1', - parentId: 'g3', - color: 'purple', - description: 'g3-1' - } - ], - value: [], - default: [], - folder: [] - }; - - let configValueToMerge: TConfigurationValue = { - workspace: [ - { - name: 'g1', - id: 'g1', - parentId: '', - color: 'pink', - description: 'g1' - }, - { - name: 'g1-1', - id: 'g1-1', - parentId: 'g1', - color: 'blue', - description: 'g1-1' - } - ], - user: [ - { - name: 'g2', - id: 'g2', - parentId: '', - color: 'green', - description: 'g2' - }, - { - name: 'g2-1', - id: 'g2-1', - parentId: 'g2', - color: 'yellow', - description: 'g2' - }, - { - name: 'g1', - id: 'g1', - parentId: '', - color: 'pink', - description: 'g1' - }, - { - name: 'g1-2', - id: 'g1-2', - parentId: 'g1', - color: 'silver', - description: 'g1-2' - }], - value: [], - default: [], - folder: [] - }; - - let connections: TConfigurationValue = { - workspace: [{ - options: { - serverName: 'server1', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '' - }, - providerName: 'MSSQL', - groupId: 'test', - savePassword: true, - id: 'server1' - } - - - ], - user: [{ - options: { - serverName: 'server2', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '' - }, - providerName: 'MSSQL', - groupId: 'test', - savePassword: true, - id: 'server2' - }, { - options: { - serverName: 'server3', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '' - }, - providerName: 'MSSQL', - groupId: 'g3', - savePassword: true, - id: 'server3' - } - ], - value: [], - default: [], - folder: [] - }; - setup(() => { - capabilitiesService = TypeMoq.Mock.ofType(CapabilitiesService); - capabilities = []; - let connectionProvider: sqlops.ConnectionProviderOptions = { - options: [ - { - name: 'serverName', - displayName: undefined, - description: undefined, - groupName: undefined, - categoryValues: undefined, - defaultValue: undefined, - isIdentity: true, - isRequired: true, - specialValueType: 0, - valueType: 0 - }, - { - name: 'databaseName', - displayName: undefined, - description: undefined, - groupName: undefined, - categoryValues: undefined, - defaultValue: undefined, - isIdentity: true, - isRequired: true, - specialValueType: 1, - valueType: 0 - }, - { - name: 'userName', - displayName: undefined, - description: undefined, - groupName: undefined, - categoryValues: undefined, - defaultValue: undefined, - isIdentity: true, - isRequired: true, - specialValueType: 3, - valueType: 0 - }, - { - name: 'authenticationType', - displayName: undefined, - description: undefined, - groupName: undefined, - categoryValues: undefined, - defaultValue: undefined, - isIdentity: true, - isRequired: true, - specialValueType: 2, - valueType: 0 - }, - { - name: 'password', - displayName: undefined, - description: undefined, - groupName: undefined, - categoryValues: undefined, - defaultValue: undefined, - isIdentity: true, - isRequired: true, - specialValueType: 4, - valueType: 0 - } - ] - }; - msSQLCapabilities = { - protocolVersion: '1', - providerName: 'MSSQL', - providerDisplayName: 'MSSQL', - connectionProvider: connectionProvider, - adminServicesProvider: undefined, - features: undefined - }; - capabilities.push(msSQLCapabilities); - - capabilitiesService.setup(x => x.getCapabilities()).returns(() => capabilities); - capabilitiesService.setup(x => x.onProviderRegisteredEvent).returns(() => onProviderRegistered.event); - - workspaceConfigurationServiceMock = TypeMoq.Mock.ofType(WorkspaceConfigurationTestService); - workspaceConfigurationServiceMock.setup(x => x.reloadConfiguration()) - .returns(() => TPromise.as<{}>({})); - - configEditingServiceMock = TypeMoq.Mock.ofType(ConfigurationEditingService); - let nothing: void; - configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).returns(() => TPromise.as(nothing)); - configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.WORKSPACE, TypeMoq.It.isAny())).returns(() => TPromise.as(nothing)); - }); - - function groupsAreEqual(groups1: IConnectionProfileGroup[], groups2: IConnectionProfileGroup[]): Boolean { - if (!groups1 && !groups2) { - return true; - } else if ((!groups1 && groups2 && groups2.length === 0) || (!groups2 && groups1 && groups1.length === 0)) { - return true; - } - - if (groups1.length !== groups2.length) { - return false; - } - - let areEqual = true; - - groups1.map(g1 => { - if (areEqual) { - let g2 = groups2.find(g => g.name === g1.name); - if (!g2) { - areEqual = false; - } else { - let result = groupsAreEqual(groups1.filter(a => a.parentId === g1.id), groups2.filter(b => b.parentId === g2.id)); - if (result === false) { - areEqual = false; - } - } - } - }); - - return areEqual; + name: 'g1-2', + id: 'g1-2', + parentId: 'g1', + color: 'silver', + description: 'g1-2' + }], + value: [], + default: [], + workspaceFolder: [] + }; + + let connections = { + workspace: [{ + options: { + serverName: 'server1', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '' + }, + providerName: 'MSSQL', + groupId: 'test', + savePassword: true, + id: 'server1' } - - test('allGroups should return groups from user and workspace settings', () => { - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - let allGroups = config.getAllGroups(); - - - assert.notEqual(allGroups, undefined); - assert.equal(allGroups.length, configValueToConcat.workspace.length + configValueToConcat.user.length); - }); - - test('allGroups should merge groups from user and workspace settings', () => { - let expectedAllGroups: IConnectionProfileGroup[] = [ - { - name: 'g1', - id: 'g1', - parentId: '', - color: 'pink', - description: 'g1' - }, - { - name: 'g1-1', - id: 'g1-1', - parentId: 'g1', - color: 'blue', - description: 'g1-1' - }, - { - name: 'g2', - id: 'g2', - parentId: '', - color: 'yellow', - description: 'g2' - }, - { - name: 'g2-1', - id: 'g2-1', - parentId: 'g2', - color: 'red', - description: 'g2' - }, - { - name: 'g1-2', - id: 'g1-2', - parentId: 'g1', - color: 'green', - description: 'g1-2' - }]; - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToMerge); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - let allGroups = config.getAllGroups(); - - - assert.notEqual(allGroups, undefined); - assert.equal(groupsAreEqual(allGroups, expectedAllGroups), true); - }); - - test('addConnection should add the new profile to user settings if does not exist', done => { - let newProfile: IConnectionProfile = { - serverName: 'new server', + + + ], + user: [{ + options: { + serverName: 'server2', databaseName: 'database', userName: 'user', password: 'password', - authenticationType: '', - savePassword: true, - groupFullName: undefined, - groupId: undefined, - getOptionsKey: undefined, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: undefined - }; - - let expectedNumberOfConnections = connections.user.length + 1; - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - connectionProfile.options['databaseDisplayName'] = 'database'; - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.addConnection(connectionProfile).then(savedConnectionProfile => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - assert.notEqual(savedConnectionProfile.id, undefined); - done(); - }).catch(error => { - assert.fail(); - done(); - }); - }); - - test('addConnection should not add the new profile to user settings if already exists', done => { - let profileFromConfig = connections.user[0]; - let newProfile: IConnectionProfile = { - serverName: profileFromConfig.options['serverName'], - databaseName: profileFromConfig.options['databaseName'], - userName: profileFromConfig.options['userName'], - password: profileFromConfig.options['password'], - authenticationType: profileFromConfig.options['authenticationType'], - groupId: profileFromConfig.groupId, - savePassword: true, - groupFullName: undefined, - getOptionsKey: undefined, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: undefined - }; - - let expectedNumberOfConnections = connections.user.length; - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - connectionProfile.options['databaseDisplayName'] = profileFromConfig.options['databaseName']; - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.addConnection(connectionProfile).then(savedConnectionProfile => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - assert.equal(savedConnectionProfile.id, profileFromConfig.id); - done(); - }).catch(error => { - assert.fail(); - done(); - }); - }); - - test('addConnection should add the new group to user settings if does not exist', done => { - let newProfile: IConnectionProfile = { - serverName: 'new server', + authenticationType: '' + }, + providerName: 'MSSQL', + groupId: 'test', + savePassword: true, + id: 'server2' + }, { + options: { + serverName: 'server3', databaseName: 'database', userName: 'user', password: 'password', - authenticationType: '', - savePassword: true, - groupFullName: 'g2/g2-2', - groupId: undefined, - getOptionsKey: undefined, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: undefined - }; - - let expectedNumberOfConnections = connections.user.length + 1; - let expectedNumberOfGroups = configValueToConcat.user.length + 1; - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.addConnection(connectionProfile).then(success => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileGroup[]).length === expectedNumberOfGroups)), TypeMoq.Times.once()); - done(); - }).catch(error => { - assert.fail(); - done(); - }); - }); - - test('getConnections should return connections from user and workspace settings given getWorkspaceConnections set to true', () => { - let getWorkspaceConnections: boolean = true; - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - let allConnections = config.getConnections(getWorkspaceConnections); - assert.equal(allConnections.length, connections.user.length + connections.workspace.length); - }); - - test('getConnections should return connections from user settings given getWorkspaceConnections set to false', () => { - let getWorkspaceConnections: boolean = false; - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - let allConnections = config.getConnections(getWorkspaceConnections); - assert.equal(allConnections.length, connections.user.length); - }); - - test('getConnections should return connections with a valid id', () => { - let getWorkspaceConnections: boolean = false; - let connectionsWithNoId: TConfigurationValue = { - user: connections.user.map(c => { - c.id = undefined; - return c; - }), - default: connections.default, - workspace: connections.workspace.map(c => { - c.id = c.options['serverName']; - return c; - }), - value: connections.value, - folder: [] - }; - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connectionsWithNoId); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - let allConnections = config.getConnections(getWorkspaceConnections); - assert.equal(allConnections.length, connections.user.length); - allConnections.forEach(connection => { - let userConnection = connectionsWithNoId.user.find(u => u.options['serverName'] === connection.serverName); - if (userConnection !== undefined) { - assert.notEqual(connection.id, connection.getOptionsKey()); - assert.notEqual(connection.id, undefined); - } else { - let workspaceConnection = connectionsWithNoId.workspace.find(u => u.options['serverName'] === connection.serverName); - assert.notEqual(connection.id, connection.getOptionsKey()); - assert.equal(workspaceConnection.id, connection.id); + authenticationType: '' + }, + providerName: 'MSSQL', + groupId: 'g3', + savePassword: true, + id: 'server3' + } + ], + value: [], + default: [], + workspaceFolder: [] + }; + setup(() => { + capabilitiesService = TypeMoq.Mock.ofType(CapabilitiesTestService); + capabilities = []; + let connectionProvider: sqlops.ConnectionProviderOptions = { + options: [ + { + name: 'serverName', + displayName: undefined, + description: undefined, + groupName: undefined, + categoryValues: undefined, + defaultValue: undefined, + isIdentity: true, + isRequired: true, + specialValueType: ConnectionOptionSpecialType.serverName, + valueType: ServiceOptionType.string + }, + { + name: 'databaseName', + displayName: undefined, + description: undefined, + groupName: undefined, + categoryValues: undefined, + defaultValue: undefined, + isIdentity: true, + isRequired: true, + specialValueType: ConnectionOptionSpecialType.databaseName, + valueType: ServiceOptionType.string + }, + { + name: 'userName', + displayName: undefined, + description: undefined, + groupName: undefined, + categoryValues: undefined, + defaultValue: undefined, + isIdentity: true, + isRequired: true, + specialValueType: ConnectionOptionSpecialType.userName, + valueType: ServiceOptionType.string + }, + { + name: 'authenticationType', + displayName: undefined, + description: undefined, + groupName: undefined, + categoryValues: undefined, + defaultValue: undefined, + isIdentity: true, + isRequired: true, + specialValueType: ConnectionOptionSpecialType.authType, + valueType: ServiceOptionType.string + }, + { + name: 'password', + displayName: undefined, + description: undefined, + groupName: undefined, + categoryValues: undefined, + defaultValue: undefined, + isIdentity: true, + isRequired: true, + specialValueType: ConnectionOptionSpecialType.password, + valueType: ServiceOptionType.string + } + ] + }; + msSQLCapabilities = { + connection: { + providerId: 'MSSQL', + displayName: 'MSSQL', + connectionOptions: connectionProvider.options + } + }; + capabilities.push(msSQLCapabilities); + + capabilitiesService.setup(x => x.getCapabilities('MSSQL')).returns(() => msSQLCapabilities); + (capabilitiesService.object as any).onCapabilitiesRegistered = onCapabilitiesRegistered.event; + + workspaceConfigurationServiceMock = TypeMoq.Mock.ofType(WorkspaceConfigurationTestService); + workspaceConfigurationServiceMock.setup(x => x.reloadConfiguration()) + .returns(() => TPromise.as(null)); + + configEditingServiceMock = TypeMoq.Mock.ofType(ConfigurationEditingService); + let nothing: void; + configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).returns(() => TPromise.as(nothing)); + configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.WORKSPACE, TypeMoq.It.isAny())).returns(() => TPromise.as(nothing)); + }); + + function groupsAreEqual(groups1: IConnectionProfileGroup[], groups2: IConnectionProfileGroup[]): Boolean { + if (!groups1 && !groups2) { + return true; + } else if ((!groups1 && groups2 && groups2.length === 0) || (!groups2 && groups1 && groups1.length === 0)) { + return true; + } + + if (groups1.length !== groups2.length) { + return false; + } + + let areEqual = true; + + groups1.map(g1 => { + if (areEqual) { + let g2 = groups2.find(g => g.name === g1.name); + if (!g2) { + areEqual = false; + } else { + let result = groupsAreEqual(groups1.filter(a => a.parentId === g1.id), groups2.filter(b => b.parentId === g2.id)); + if (result === false) { + areEqual = false; + } } - }); - }); - - test('getConnections update the capabilities in each profile when the provider capabilities is registered', () => { - let oldOptionName: string = 'oldOptionName'; - let optionsMetadataFromConfig = capabilities[0].connectionProvider.options.concat({ - name: oldOptionName, - displayName: undefined, - description: undefined, - groupName: undefined, - categoryValues: undefined, - defaultValue: undefined, - isIdentity: true, - isRequired: true, - specialValueType: 0, - valueType: 0 - }); - - let capabilitiesFromConfig: sqlops.DataProtocolServerCapabilities[] = []; - let connectionProvider: sqlops.ConnectionProviderOptions = { - options: optionsMetadataFromConfig - }; - let msSQLCapabilities2 = { - protocolVersion: '1', - providerName: 'MSSQL', - providerDisplayName: 'MSSQL', - connectionProvider: connectionProvider, - adminServicesProvider: undefined, - features: undefined - }; - capabilitiesFromConfig.push(msSQLCapabilities2); - let connectionUsingOldMetadata = connections.user.map(c => { - c.options[oldOptionName] = 'oldOptionValue'; - return c; - }); - let configValue = Object.assign({}, connections, { user: connectionUsingOldMetadata }); - let capabilitiesService2: TypeMoq.Mock = TypeMoq.Mock.ofType(CapabilitiesService); - capabilitiesService2.setup(x => x.getCapabilities()).returns(() => []); - capabilitiesService2.setup(x => x.onProviderRegisteredEvent).returns(() => onProviderRegistered.event); - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => configValue); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService2.object, capabilitiesFromConfig); - let allConnections = config.getConnections(false); - allConnections.forEach(element => { - assert.notEqual(element.serverName, undefined); - assert.notEqual(element.getOptionsKey().indexOf('oldOptionValue|'), -1); - }); - - onProviderRegistered.fire(msSQLCapabilities); - allConnections.forEach(element => { - assert.notEqual(element.serverName, undefined); - assert.equal(element.getOptionsKey().indexOf('oldOptionValue|'), -1); - }); - }); - - test('saveGroup should save the new groups to tree and return the id of the last group name', () => { - let config = new ConnectionConfig(undefined, undefined, undefined, undefined); - let groups: IConnectionProfileGroup[] = configValueToConcat.user; - let expectedLength = configValueToConcat.user.length + 2; - let newGroups: string = 'ROOT/g1/g1-1'; - let color: string = 'red'; - - let result: ISaveGroupResult = config.saveGroup(groups, newGroups, color, newGroups); - assert.notEqual(result, undefined); - assert.equal(result.groups.length, expectedLength, 'The result groups length is invalid'); - let newGroup = result.groups.find(g => g.name === 'g1-1'); - assert.equal(result.newGroupId, newGroup.id, 'The groups id is invalid'); - }); - - test('saveGroup should only add the groups that are not in the tree', () => { - let config = new ConnectionConfig(undefined, undefined, undefined, undefined); - let groups: IConnectionProfileGroup[] = configValueToConcat.user; - let expectedLength = configValueToConcat.user.length + 1; - let newGroups: string = 'ROOT/g2/g2-5'; - let color: string = 'red'; - - let result: ISaveGroupResult = config.saveGroup(groups, newGroups, color, newGroups); - assert.notEqual(result, undefined); - assert.equal(result.groups.length, expectedLength, 'The result groups length is invalid'); - let newGroup = result.groups.find(g => g.name === 'g2-5'); - assert.equal(result.newGroupId, newGroup.id, 'The groups id is invalid'); - }); - - test('saveGroup should not add any new group if tree already has all the groups in the full path', () => { - let config = new ConnectionConfig(undefined, undefined, undefined, undefined); - let groups: IConnectionProfileGroup[] = configValueToConcat.user; - let expectedLength = configValueToConcat.user.length; - let newGroups: string = 'ROOT/g2/g2-1'; - let color: string = 'red'; - - let result: ISaveGroupResult = config.saveGroup(groups, newGroups, color, newGroups); - assert.notEqual(result, undefined); - assert.equal(result.groups.length, expectedLength, 'The result groups length is invalid'); - let newGroup = result.groups.find(g => g.name === 'g2-1'); - assert.equal(result.newGroupId, newGroup.id, 'The groups id is invalid'); - }); - - test('deleteConnection should remove the connection from config', done => { - let newProfile: IConnectionProfile = { - serverName: 'server3', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '', - savePassword: true, - groupFullName: 'g3', - groupId: 'g3', - getOptionsKey: undefined, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: undefined - }; - - let expectedNumberOfConnections = connections.user.length - 1; - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - connectionProfile.options['databaseDisplayName'] = 'database'; - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.deleteConnection(connectionProfile).then(() => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - done(); - }).catch(error => { - assert.fail(); - done(); - }); - }); - - test('deleteConnectionGroup should remove the children connections and subgroups from config', done => { - let newProfile: IConnectionProfile = { - serverName: 'server3', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '', - savePassword: true, - groupFullName: 'g3', - groupId: 'g3', - getOptionsKey: undefined, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: undefined - }; - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - connectionProfile.options['databaseDisplayName'] = 'database'; - - let connectionProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); - let childGroup = new ConnectionProfileGroup('g3-1', connectionProfileGroup, 'g3-1', undefined, undefined); - connectionProfileGroup.addGroups([childGroup]); - connectionProfileGroup.addConnections([connectionProfile]); - - let expectedNumberOfConnections = connections.user.length - 1; - let expectedNumberOfGroups = configValueToConcat.user.length - 2; - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.deleteGroup(connectionProfileGroup).then(() => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileGroup[]).length === expectedNumberOfGroups)), TypeMoq.Times.once()); - done(); - }).catch(error => { - assert.fail(); - done(); - }); - }); - - test('deleteConnection should not throw error for connection not in config', done => { - let newProfile: IConnectionProfile = { - serverName: 'server3', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '', - savePassword: true, - groupFullName: 'g3', - groupId: 'newid', - getOptionsKey: undefined, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: undefined - }; - - let expectedNumberOfConnections = connections.user.length; - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.deleteConnection(connectionProfile).then(() => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - done(); - }).catch(error => { - assert.fail(); - done(); - }); - }); - - test('renameGroup should change group name', done => { - - let expectedNumberOfConnections = configValueToConcat.user.length; - let calledValue: any; - let called: boolean = false; - let nothing: void; - let configEditingServiceMock: TypeMoq.Mock = TypeMoq.Mock.ofType(ConfigurationEditingService); - configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).callback((x: any, val: any) => { - calledValue = val.value as IConnectionProfileStore[]; - }).returns(() => TPromise.as(nothing)); - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - let connectionProfileGroup = new ConnectionProfileGroup('g-renamed', undefined, 'g2', undefined, undefined); - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.editGroup(connectionProfileGroup).then(() => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - calledValue.forEach(con => { - if (con.id === 'g2') { - assert.equal(con.name, 'g-renamed', 'Group was not renamed'); - called = true; - } - }); - assert.equal(called, true, 'group was not renamed'); - }).then(() => done(), (err) => done(err)); - }); - - - test('change group(parent) for connection group', done => { - let expectedNumberOfConnections = configValueToConcat.user.length; - let calledValue: any; - let nothing: void; - let configEditingServiceMock: TypeMoq.Mock = TypeMoq.Mock.ofType(ConfigurationEditingService); - configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).callback((x: any, val: any) => { - calledValue = val.value as IConnectionProfileStore[]; - }).returns(() => TPromise.as(nothing)); - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionGroupsArrayName)) - .returns(() => configValueToConcat); - - let sourceProfileGroup = new ConnectionProfileGroup('g2', undefined, 'g2', undefined, undefined); - let targetProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.changeGroupIdForConnectionGroup(sourceProfileGroup, targetProfileGroup).then(() => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); - calledValue.forEach(con => { - if (con.id === 'g2') { - assert.equal(con.parentId, 'g3', 'Group parent was not changed'); - } - }); - }).then(() => done(), (err) => done(err)); - }); - - - test('change group(parent) for connection', done => { - let newProfile: IConnectionProfile = { - serverName: 'server3', - databaseName: 'database', - userName: 'user', - password: 'password', - authenticationType: '', - savePassword: true, - groupFullName: 'g3', - groupId: 'g3', - getOptionsKey: () => { return 'connectionId'; }, - matches: undefined, - providerName: 'MSSQL', - options: {}, - saveProfile: true, - id: 'test' - }; - - let expectedNumberOfConnections = connections.user.length; - workspaceConfigurationServiceMock.setup(x => x.lookup( - Constants.connectionsArrayName)) - .returns(() => connections); - - let connectionProfile = new ConnectionProfile(msSQLCapabilities, newProfile); - let newId = 'newid'; - let calledValue: any; - let nothing: void; - let configEditingServiceMock: TypeMoq.Mock = TypeMoq.Mock.ofType(ConfigurationEditingService); - configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).callback((x: any, val: any) => { - calledValue = val.value as IConnectionProfileStore[]; - }).returns(() => TPromise.as(nothing)); - configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.WORKSPACE, TypeMoq.It.isAny())).callback((x: any, val: any) => { - - }).returns(() => TPromise.as(nothing)); - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.changeGroupIdForConnection(connectionProfile, newId).then(() => { - configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, - TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.atLeastOnce()); - calledValue.forEach(con => { - }); - }).then(() => done(), (err) => done(err)); - }); - - test('fixConnectionIds should replace duplicate ids with new ones', (done) => { - let profiles: IConnectionProfileStore[] = [ - { - options: {}, - groupId: '1', - id: '1', - providerName: undefined, - savePassword: true, - }, { - options: {}, - groupId: '1', - id: '2', - providerName: undefined, - savePassword: true, - }, { - options: {}, - groupId: '1', - id: '3', - providerName: undefined, - savePassword: true, - }, { - options: {}, - groupId: '1', - id: '2', - providerName: undefined, - savePassword: true, - }, { - options: {}, - groupId: '1', - id: '4', - providerName: undefined, - savePassword: true, - }, { - options: {}, - groupId: '1', - id: '3', - providerName: undefined, - savePassword: true, - }, { - options: {}, - groupId: '1', - id: '2', - providerName: undefined, - savePassword: true, - } - ]; - - let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); - config.fixConnectionIds(profiles); - let ids = profiles.map(x => x.id); - for (var index = 0; index < ids.length; index++) { - var id = ids[index]; - assert.equal(ids.lastIndexOf(id), index); } - done(); }); - */ + + return areEqual; + } + + test('allGroups should return groups from user and workspace settings', () => { + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + let allGroups = config.getAllGroups(); + + + assert.notEqual(allGroups, undefined); + assert.equal(allGroups.length, configValueToConcat.workspace.length + configValueToConcat.user.length); + }); + + test('allGroups should merge groups from user and workspace settings', () => { + let expectedAllGroups: IConnectionProfileGroup[] = [ + { + name: 'g1', + id: 'g1', + parentId: '', + color: 'pink', + description: 'g1' + }, + { + name: 'g1-1', + id: 'g1-1', + parentId: 'g1', + color: 'blue', + description: 'g1-1' + }, + { + name: 'g2', + id: 'g2', + parentId: '', + color: 'yellow', + description: 'g2' + }, + { + name: 'g2-1', + id: 'g2-1', + parentId: 'g2', + color: 'red', + description: 'g2' + }, + { + name: 'g1-2', + id: 'g1-2', + parentId: 'g1', + color: 'green', + description: 'g1-2' + }]; + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToMerge); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + let allGroups = config.getAllGroups(); + + + assert.notEqual(allGroups, undefined); + assert.equal(groupsAreEqual(allGroups, expectedAllGroups), true); + }); + + test('addConnection should add the new profile to user settings if does not exist', done => { + let newProfile: IConnectionProfile = { + serverName: 'new server', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: undefined, + groupId: undefined, + getOptionsKey: undefined, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined, + connectionName: undefined + }; + + let expectedNumberOfConnections = connections.user.length + 1; + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + connectionProfile.options['databaseDisplayName'] = 'database'; + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.addConnection(connectionProfile).then(savedConnectionProfile => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + assert.notEqual(savedConnectionProfile.id, undefined); + done(); + }).catch(error => { + done(error); + }); + }); + + test('addConnection should not add the new profile to user settings if already exists', done => { + let profileFromConfig = connections.user[0]; + let newProfile: IConnectionProfile = { + serverName: profileFromConfig.options['serverName'], + databaseName: profileFromConfig.options['databaseName'], + userName: profileFromConfig.options['userName'], + password: profileFromConfig.options['password'], + authenticationType: profileFromConfig.options['authenticationType'], + groupId: profileFromConfig.groupId, + savePassword: true, + groupFullName: undefined, + getOptionsKey: undefined, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined, + connectionName: undefined + }; + + let expectedNumberOfConnections = connections.user.length; + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + connectionProfile.options['databaseDisplayName'] = profileFromConfig.options['databaseName']; + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.addConnection(connectionProfile).then(savedConnectionProfile => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + assert.equal(savedConnectionProfile.id, profileFromConfig.id); + done(); + }).catch(error => { + done(error); + }); + }); + + test('addConnection should add the new group to user settings if does not exist', done => { + let newProfile: IConnectionProfile = { + serverName: 'new server', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g2/g2-2', + groupId: undefined, + getOptionsKey: undefined, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined, + connectionName: undefined + }; + + let expectedNumberOfConnections = connections.user.length + 1; + let expectedNumberOfGroups = configValueToConcat.user.length + 1; + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.addConnection(connectionProfile).then(success => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileGroup[]).length === expectedNumberOfGroups)), TypeMoq.Times.once()); + done(); + }).catch(error => { + done(error); + }); + }); + + test('getConnections should return connections from user and workspace settings given getWorkspaceConnections set to true', () => { + let getWorkspaceConnections: boolean = true; + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + let allConnections = config.getConnections(getWorkspaceConnections); + assert.equal(allConnections.length, connections.user.length + connections.workspace.length); + }); + + test('getConnections should return connections from user settings given getWorkspaceConnections set to false', () => { + let getWorkspaceConnections: boolean = false; + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + let allConnections = config.getConnections(getWorkspaceConnections); + assert.equal(allConnections.length, connections.user.length); + }); + + test('getConnections should return connections with a valid id', () => { + let getWorkspaceConnections: boolean = false; + let connectionsWithNoId = { + user: connections.user.map(c => { + c.id = undefined; + return c; + }), + default: connections.default, + workspace: connections.workspace.map(c => { + c.id = c.options['serverName']; + return c; + }), + value: connections.value, + workspaceFolder: [] + }; + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connectionsWithNoId); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + let allConnections = config.getConnections(getWorkspaceConnections); + assert.equal(allConnections.length, connections.user.length); + allConnections.forEach(connection => { + let userConnection = connectionsWithNoId.user.find(u => u.options['serverName'] === connection.serverName); + if (userConnection !== undefined) { + assert.notEqual(connection.id, connection.getOptionsKey()); + assert.notEqual(connection.id, undefined); + } else { + let workspaceConnection = connectionsWithNoId.workspace.find(u => u.options['serverName'] === connection.serverName); + assert.notEqual(connection.id, connection.getOptionsKey()); + assert.equal(workspaceConnection.id, connection.id); + } + }); + }); + + test('getConnections update the capabilities in each profile when the provider capabilities is registered', () => { + let oldOptionName: string = 'oldOptionName'; + let optionsMetadataFromConfig = capabilities[0].connection.connectionOptions.concat({ + name: oldOptionName, + displayName: undefined, + description: undefined, + groupName: undefined, + categoryValues: undefined, + defaultValue: undefined, + isIdentity: true, + isRequired: true, + specialValueType: undefined, + valueType: ServiceOptionType.string + }); + + let capabilitiesFromConfig: ProviderFeatures[] = []; + let msSQLCapabilities2: ProviderFeatures = { + connection: { + providerId: 'MSSQL', + displayName: 'MSSQL', + connectionOptions: optionsMetadataFromConfig + } + }; + capabilitiesFromConfig.push(msSQLCapabilities2); + let connectionUsingOldMetadata = connections.user.map(c => { + c.options[oldOptionName] = 'oldOptionValue'; + return c; + }); + let configValue = Object.assign({}, connections, { user: connectionUsingOldMetadata }); + let capabilitiesService2: TypeMoq.Mock = TypeMoq.Mock.ofType(CapabilitiesTestService); + capabilitiesService2.setup(x => x.getCapabilities('MSSQL')).returns(() => msSQLCapabilities2); + (capabilitiesService2.object as any).onCapabilitiesRegistered = onCapabilitiesRegistered.event; + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => configValue); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService2.object); + let allConnections = config.getConnections(false); + allConnections.forEach(element => { + assert.notEqual(element.serverName, undefined); + assert.notEqual(element.getOptionsKey().indexOf('oldOptionValue|'), -1); + }); + + onCapabilitiesRegistered.fire(msSQLCapabilities); + allConnections.forEach(element => { + assert.notEqual(element.serverName, undefined); + assert.equal(element.getOptionsKey().indexOf('oldOptionValue|'), -1); + }); + }); + + test('saveGroup should save the new groups to tree and return the id of the last group name', () => { + let config = new ConnectionConfig(undefined, undefined, undefined); + let groups: IConnectionProfileGroup[] = configValueToConcat.user; + let expectedLength = configValueToConcat.user.length + 2; + let newGroups: string = 'ROOT/g1/g1-1'; + let color: string = 'red'; + + let result: ISaveGroupResult = config.saveGroup(groups, newGroups, color, newGroups); + assert.notEqual(result, undefined); + assert.equal(result.groups.length, expectedLength, 'The result groups length is invalid'); + let newGroup = result.groups.find(g => g.name === 'g1-1'); + assert.equal(result.newGroupId, newGroup.id, 'The groups id is invalid'); + }); + + test('saveGroup should only add the groups that are not in the tree', () => { + let config = new ConnectionConfig(undefined, undefined, undefined); + let groups: IConnectionProfileGroup[] = configValueToConcat.user; + let expectedLength = configValueToConcat.user.length + 1; + let newGroups: string = 'ROOT/g2/g2-5'; + let color: string = 'red'; + + let result: ISaveGroupResult = config.saveGroup(groups, newGroups, color, newGroups); + assert.notEqual(result, undefined); + assert.equal(result.groups.length, expectedLength, 'The result groups length is invalid'); + let newGroup = result.groups.find(g => g.name === 'g2-5'); + assert.equal(result.newGroupId, newGroup.id, 'The groups id is invalid'); + }); + + test('saveGroup should not add any new group if tree already has all the groups in the full path', () => { + let config = new ConnectionConfig(undefined, undefined, undefined); + let groups: IConnectionProfileGroup[] = configValueToConcat.user; + let expectedLength = configValueToConcat.user.length; + let newGroups: string = 'ROOT/g2/g2-1'; + let color: string = 'red'; + + let result: ISaveGroupResult = config.saveGroup(groups, newGroups, color, newGroups); + assert.notEqual(result, undefined); + assert.equal(result.groups.length, expectedLength, 'The result groups length is invalid'); + let newGroup = result.groups.find(g => g.name === 'g2-1'); + assert.equal(result.newGroupId, newGroup.id, 'The groups id is invalid'); + }); + + test('deleteConnection should remove the connection from config', done => { + let newProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + getOptionsKey: undefined, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined, + connectionName: undefined + }; + + let expectedNumberOfConnections = connections.user.length - 1; + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + connectionProfile.options['databaseDisplayName'] = 'database'; + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.deleteConnection(connectionProfile).then(() => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + done(); + }).catch(error => { + done(error); + }); + }); + + test('deleteConnectionGroup should remove the children connections and subgroups from config', done => { + let newProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + getOptionsKey: undefined, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined, + connectionName: undefined + }; + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + connectionProfile.options['databaseDisplayName'] = 'database'; + + let connectionProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); + let childGroup = new ConnectionProfileGroup('g3-1', connectionProfileGroup, 'g3-1', undefined, undefined); + connectionProfileGroup.addGroups([childGroup]); + connectionProfileGroup.addConnections([connectionProfile]); + + let expectedNumberOfConnections = connections.user.length - 1; + let expectedNumberOfGroups = configValueToConcat.user.length - 2; + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.deleteGroup(connectionProfileGroup).then(() => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileGroup[]).length === expectedNumberOfGroups)), TypeMoq.Times.once()); + done(); + }).catch(error => { + done(error); + }); + }); + + test('deleteConnection should not throw error for connection not in config', done => { + let newProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'newid', + getOptionsKey: undefined, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: undefined, + connectionName: undefined + }; + + let expectedNumberOfConnections = connections.user.length; + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.deleteConnection(connectionProfile).then(() => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + done(); + }).catch(error => { + done(error); + }); + }); + + test('renameGroup should change group name', done => { + + let expectedNumberOfConnections = configValueToConcat.user.length; + let calledValue: any; + let called: boolean = false; + let nothing: void; + let configEditingServiceMock: TypeMoq.Mock = TypeMoq.Mock.ofType(ConfigurationEditingService); + configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).callback((x: any, val: any) => { + calledValue = val.value as IConnectionProfileStore[]; + }).returns(() => TPromise.as(nothing)); + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + let connectionProfileGroup = new ConnectionProfileGroup('g-renamed', undefined, 'g2', undefined, undefined); + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.editGroup(connectionProfileGroup).then(() => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + calledValue.forEach(con => { + if (con.id === 'g2') { + assert.equal(con.name, 'g-renamed', 'Group was not renamed'); + called = true; + } + }); + assert.equal(called, true, 'group was not renamed'); + }).then(() => done(), (err) => done(err)); + }); + + + test('change group(parent) for connection group', done => { + let expectedNumberOfConnections = configValueToConcat.user.length; + let calledValue: any; + let nothing: void; + let configEditingServiceMock: TypeMoq.Mock = TypeMoq.Mock.ofType(ConfigurationEditingService); + configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).callback((x: any, val: any) => { + calledValue = val.value as IConnectionProfileStore[]; + }).returns(() => TPromise.as(nothing)); + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + + let sourceProfileGroup = new ConnectionProfileGroup('g2', undefined, 'g2', undefined, undefined); + let targetProfileGroup = new ConnectionProfileGroup('g3', undefined, 'g3', undefined, undefined); + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.changeGroupIdForConnectionGroup(sourceProfileGroup, targetProfileGroup).then(() => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.once()); + calledValue.forEach(con => { + if (con.id === 'g2') { + assert.equal(con.parentId, 'g3', 'Group parent was not changed'); + } + }); + }).then(() => done(), (err) => done(err)); + }); + + + test('change group(parent) for connection', done => { + let newProfile: IConnectionProfile = { + serverName: 'server3', + databaseName: 'database', + userName: 'user', + password: 'password', + authenticationType: '', + savePassword: true, + groupFullName: 'g3', + groupId: 'g3', + getOptionsKey: () => { return 'connectionId'; }, + matches: undefined, + providerName: 'MSSQL', + options: {}, + saveProfile: true, + id: 'test', + connectionName: undefined + }; + + let expectedNumberOfConnections = connections.user.length; + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + + let connectionProfile = new ConnectionProfile(capabilitiesService.object, newProfile); + let newId = 'newid'; + let calledValue: any; + let nothing: void; + let configEditingServiceMock: TypeMoq.Mock = TypeMoq.Mock.ofType(ConfigurationEditingService); + configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.USER, TypeMoq.It.isAny())).callback((x: any, val: any) => { + calledValue = val.value as IConnectionProfileStore[]; + }).returns(() => TPromise.as(nothing)); + configEditingServiceMock.setup(x => x.writeConfiguration(ConfigurationTarget.WORKSPACE, TypeMoq.It.isAny())).callback((x: any, val: any) => { + + }).returns(() => TPromise.as(nothing)); + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.changeGroupIdForConnection(connectionProfile, newId).then(() => { + configEditingServiceMock.verify(y => y.writeConfiguration(ConfigurationTarget.USER, + TypeMoq.It.is(c => (c.value as IConnectionProfileStore[]).length === expectedNumberOfConnections)), TypeMoq.Times.atLeastOnce()); + calledValue.forEach(con => { + }); + }).then(() => done(), (err) => done(err)); + }); test('fixConnectionIds should replace duplicate ids with new ones', (done) => { + let profiles: IConnectionProfileStore[] = [ + { + options: {}, + groupId: '1', + id: '1', + providerName: undefined, + savePassword: true, + }, { + options: {}, + groupId: '1', + id: '2', + providerName: undefined, + savePassword: true, + }, { + options: {}, + groupId: '1', + id: '3', + providerName: undefined, + savePassword: true, + }, { + options: {}, + groupId: '1', + id: '2', + providerName: undefined, + savePassword: true, + }, { + options: {}, + groupId: '1', + id: '4', + providerName: undefined, + savePassword: true, + }, { + options: {}, + groupId: '1', + id: '3', + providerName: undefined, + savePassword: true, + }, { + options: {}, + groupId: '1', + id: '2', + providerName: undefined, + savePassword: true, + } + ]; + + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + config.fixConnectionIds(profiles); + let ids = profiles.map(x => x.id); + for (var index = 0; index < ids.length; index++) { + var id = ids[index]; + assert.equal(ids.lastIndexOf(id), index); + } done(); }); + test('addConnection should not move the connection when editing', async () => { + // Set up the connection config + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionsArrayName)) + .returns(() => connections); + workspaceConfigurationServiceMock.setup(x => x.inspect( + Constants.connectionGroupsArrayName)) + .returns(() => configValueToConcat); + let config = new ConnectionConfig(configEditingServiceMock.object, workspaceConfigurationServiceMock.object, capabilitiesService.object); + + // Clone a connection and modify an option + const connectionIndex = 1; + const optionKey = 'testOption'; + const optionValue = 'testValue'; + let allConnections = config.getConnections(false); + let oldLength = allConnections.length; + let connectionToEdit = allConnections[connectionIndex].clone(); + connectionToEdit.options[optionKey] = optionValue; + await config.addConnection(connectionToEdit); + + // Get the connection and verify that it is in the same place and has been updated + let newConnections = config.getConnections(false); + assert.equal(newConnections.length, oldLength); + let editedConnection = newConnections[connectionIndex]; + assert.equal(editedConnection.getOptionsKey(), connectionToEdit.getOptionsKey()); + assert.equal(editedConnection.options[optionKey], optionValue); + }); + }); From c3a81b5bf3ca30aca9863c8b366fdb4134ae3b68 Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Tue, 9 Oct 2018 19:43:55 -0700 Subject: [PATCH 06/11] Add back Azure Resource Explorer extension with updated build script (#2805) * Revert "Revert "Port the Azure Resource Explorer extension to core." (#2770)" This reverts commit 210447cd3716d0ad5c12e1a81649de3f9ac5ccf2. * WIP1 * Add custom build task for azurecore extension * Fix azurecore output path for macOS build * Fix linux gulp task name --- build/gulpfile.vscode.js | 52 +- extensions/azurecore/.vscodeignore | 0 extensions/azurecore/README.md | 21 + extensions/azurecore/package.json | 128 ++++ extensions/azurecore/package.nls.json | 13 + extensions/azurecore/resources/azure.svg | 6 + .../resources/dark/account_inverse.svg | 1 + .../resources/dark/connect_to_inverse.svg | 1 + .../resources/dark/filter_inverse.svg | 1 + .../resources/dark/folder_inverse.svg | 1 + .../resources/dark/refresh_inverse.svg | 1 + .../resources/dark/sql_database_inverse.svg | 1 + .../resources/dark/sql_server_inverse.svg | 1 + .../resources/dark/subscription_inverse.svg | 1 + .../azurecore/resources/light/account.svg | 1 + .../azurecore/resources/light/connect_to.svg | 1 + .../azurecore/resources/light/filter.svg | 1 + .../azurecore/resources/light/folder.svg | 1 + .../azurecore/resources/light/refresh.svg | 1 + .../resources/light/sql_database.svg | 1 + .../azurecore/resources/light/sql_server.svg | 1 + .../resources/light/subscription.svg | 1 + extensions/azurecore/src/apiWrapper.ts | 225 +++++++ extensions/azurecore/src/appContext.ts | 28 + .../azurecore/src/azureResource/commands.ts | 120 ++++ .../azurecore/src/azureResource/constants.ts | 16 + .../azurecore/src/azureResource/errors.ts | 15 + .../azurecore/src/azureResource/interfaces.ts | 54 ++ .../azurecore/src/azureResource/models.ts | 25 + .../src/azureResource/servicePool.ts | 35 + .../azureResource/services/accountService.ts | 32 + .../azureResource/services/cacheService.ts | 25 + .../azureResource/services/contextService.ts | 36 ++ .../services/credentialService.ts | 43 ++ .../services/databaseServerService.ts | 39 ++ .../azureResource/services/databaseService.ts | 51 ++ .../services/subscriptionFilterService.ts | 77 +++ .../services/subscriptionService.ts | 33 + .../tree/accountNotSignedInTreeNode.ts | 51 ++ .../src/azureResource/tree/accountTreeNode.ts | 155 +++++ .../src/azureResource/tree/baseTreeNodes.ts | 71 ++ .../tree/databaseContainerTreeNode.ts | 103 +++ .../tree/databaseServerContainerTreeNode.ts | 103 +++ .../tree/databaseServerTreeNode.ts | 57 ++ .../azureResource/tree/databaseTreeNode.ts | 61 ++ .../src/azureResource/tree/messageTreeNode.ts | 60 ++ .../tree/subscriptionTreeNode.ts | 65 ++ .../azureResource/tree/treeChangeHandler.ts | 12 + .../src/azureResource/tree/treeProvider.ts | 101 +++ .../azurecore/src/azureResource/utils.ts | 44 ++ extensions/azurecore/src/constants.ts | 8 + .../src/controllers/controllerBase.ts | 34 + .../src/controllers/mainController.ts | 54 ++ extensions/azurecore/src/extension.ts | 40 ++ .../tree/accountNotSignedInTreeNode.test.ts | 37 ++ .../tree/accountTreeNode.test.ts | 294 +++++++++ .../tree/databaseContainerTreeNode.test.ts | 219 +++++++ .../databaseServerContainerTreeNode.test.ts | 219 +++++++ .../tree/databaseServerTreeNode.test.ts | 62 ++ .../tree/databaseTreeNode.test.ts | 62 ++ .../tree/messageTreeNode.test.ts | 41 ++ .../tree/subscriptionTreeNode.test.ts | 91 +++ .../azureResource/tree/treeProvider.test.ts | 113 ++++ extensions/azurecore/src/test/index.ts | 30 + extensions/azurecore/src/treeNodes.ts | 77 +++ extensions/azurecore/src/typings/ref.d.ts | 9 + extensions/azurecore/tsconfig.json | 22 + extensions/azurecore/yarn.lock | 608 ++++++++++++++++++ scripts/test-integration.bat | 5 +- scripts/test-integration.sh | 4 +- .../common/connectionManagementService.ts | 10 +- 71 files changed, 4001 insertions(+), 11 deletions(-) create mode 100644 extensions/azurecore/.vscodeignore create mode 100644 extensions/azurecore/README.md create mode 100644 extensions/azurecore/package.json create mode 100644 extensions/azurecore/package.nls.json create mode 100644 extensions/azurecore/resources/azure.svg create mode 100644 extensions/azurecore/resources/dark/account_inverse.svg create mode 100644 extensions/azurecore/resources/dark/connect_to_inverse.svg create mode 100644 extensions/azurecore/resources/dark/filter_inverse.svg create mode 100644 extensions/azurecore/resources/dark/folder_inverse.svg create mode 100644 extensions/azurecore/resources/dark/refresh_inverse.svg create mode 100644 extensions/azurecore/resources/dark/sql_database_inverse.svg create mode 100644 extensions/azurecore/resources/dark/sql_server_inverse.svg create mode 100644 extensions/azurecore/resources/dark/subscription_inverse.svg create mode 100644 extensions/azurecore/resources/light/account.svg create mode 100644 extensions/azurecore/resources/light/connect_to.svg create mode 100644 extensions/azurecore/resources/light/filter.svg create mode 100644 extensions/azurecore/resources/light/folder.svg create mode 100644 extensions/azurecore/resources/light/refresh.svg create mode 100644 extensions/azurecore/resources/light/sql_database.svg create mode 100644 extensions/azurecore/resources/light/sql_server.svg create mode 100644 extensions/azurecore/resources/light/subscription.svg create mode 100644 extensions/azurecore/src/apiWrapper.ts create mode 100644 extensions/azurecore/src/appContext.ts create mode 100644 extensions/azurecore/src/azureResource/commands.ts create mode 100644 extensions/azurecore/src/azureResource/constants.ts create mode 100644 extensions/azurecore/src/azureResource/errors.ts create mode 100644 extensions/azurecore/src/azureResource/interfaces.ts create mode 100644 extensions/azurecore/src/azureResource/models.ts create mode 100644 extensions/azurecore/src/azureResource/servicePool.ts create mode 100644 extensions/azurecore/src/azureResource/services/accountService.ts create mode 100644 extensions/azurecore/src/azureResource/services/cacheService.ts create mode 100644 extensions/azurecore/src/azureResource/services/contextService.ts create mode 100644 extensions/azurecore/src/azureResource/services/credentialService.ts create mode 100644 extensions/azurecore/src/azureResource/services/databaseServerService.ts create mode 100644 extensions/azurecore/src/azureResource/services/databaseService.ts create mode 100644 extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts create mode 100644 extensions/azurecore/src/azureResource/services/subscriptionService.ts create mode 100644 extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/accountTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts create mode 100644 extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/messageTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts create mode 100644 extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts create mode 100644 extensions/azurecore/src/azureResource/tree/treeProvider.ts create mode 100644 extensions/azurecore/src/azureResource/utils.ts create mode 100644 extensions/azurecore/src/constants.ts create mode 100644 extensions/azurecore/src/controllers/controllerBase.ts create mode 100644 extensions/azurecore/src/controllers/mainController.ts create mode 100644 extensions/azurecore/src/extension.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts create mode 100644 extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts create mode 100644 extensions/azurecore/src/test/index.ts create mode 100644 extensions/azurecore/src/treeNodes.ts create mode 100644 extensions/azurecore/src/typings/ref.d.ts create mode 100644 extensions/azurecore/tsconfig.json create mode 100644 extensions/azurecore/yarn.lock diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 51422b2a29..39199be6a7 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -76,6 +76,7 @@ const sqlBuiltInExtensions = [ 'import', 'profiler' ]; +var azureExtensions = [ 'azurecore']; const vscodeEntryPoints = _.flatten([ buildfile.entrypoint('vs/workbench/workbench.main'), @@ -276,6 +277,40 @@ function packageBuiltInExtensions() { }); } +// {{SQL CARBON EDIT}} +function packageAzureCoreTask(platform, arch) { + var destination = path.join(path.dirname(root), 'azuredatastudio') + (platform ? '-' + platform : '') + (arch ? '-' + arch : ''); + if (platform === 'darwin') { + destination = path.join(destination, 'Azure Data Studio.app', 'Contents', 'Resources', 'app', 'extensions', 'azurecore'); + } else { + destination = path.join(destination, 'resources', 'app', 'extensions', 'azurecore'); + } + + platform = platform || process.platform; + + return () => { + const root = path.resolve(path.join(__dirname, '..')); + const localExtensionDescriptions = glob.sync('extensions/*/package.json') + .map(manifestPath => { + const extensionPath = path.dirname(path.join(root, manifestPath)); + const extensionName = path.basename(extensionPath); + return { name: extensionName, path: extensionPath }; + }) + .filter(({ name }) => azureExtensions.indexOf(name) > -1); + + const localExtensions = es.merge(...localExtensionDescriptions.map(extension => { + return ext.fromLocal(extension.path); + })); + + let result = localExtensions + .pipe(util.skipDirectories()) + .pipe(util.fixWin32DirectoryPermissions()) + .pipe(filter(['**', '!LICENSE', '!LICENSES.chromium.html', '!version'])); + + return result.pipe(vfs.dest(destination)); + }; +} + function packageTask(platform, arch, opts) { opts = opts || {}; @@ -307,8 +342,10 @@ function packageTask(platform, arch, opts) { .filter(({ name }) => excludedExtensions.indexOf(name) === -1) .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) // {{SQL CARBON EDIT}} - .filter(({ name }) => sqlBuiltInExtensions.indexOf(name) === -1); - packageBuiltInExtensions(); + .filter(({ name }) => sqlBuiltInExtensions.indexOf(name) === -1) + .filter(({ name }) => azureExtensions.indexOf(name) === -1); + + packageBuiltInExtensions(); const localExtensions = es.merge(...localExtensionDescriptions.map(extension => { return ext.fromLocal(extension.path) @@ -325,7 +362,6 @@ function packageTask(platform, arch, opts) { .pipe(util.cleanNodeModule('account-provider-azure', ['node_modules/date-utils/doc/**', 'node_modules/adal_node/node_modules/**'], undefined)) .pipe(util.cleanNodeModule('typescript', ['**/**'], undefined)); - const sources = es.merge(src, localExtensions, localExtensionDependencies) .pipe(util.setExecutableBit(['**/*.sh'])) .pipe(filter(['**', '!**/*.js.map'])); @@ -461,6 +497,10 @@ function packageTask(platform, arch, opts) { const buildRoot = path.dirname(root); // {{SQL CARBON EDIT}} +gulp.task('vscode-win32-x64-azurecore', ['optimize-vscode'], packageAzureCoreTask('win32', 'x64')); +gulp.task('vscode-darwin-azurecore', ['optimize-vscode'], packageAzureCoreTask('darwin')); +gulp.task('vscode-linux-x64-azurecore', ['optimize-vscode'], packageAzureCoreTask('linux', 'x64')); + gulp.task('clean-vscode-win32-ia32', util.rimraf(path.join(buildRoot, 'azuredatastudio-win32-ia32'))); gulp.task('clean-vscode-win32-x64', util.rimraf(path.join(buildRoot, 'azuredatastudio-win32-x64'))); gulp.task('clean-vscode-darwin', util.rimraf(path.join(buildRoot, 'azuredatastudio-darwin'))); @@ -469,10 +509,10 @@ gulp.task('clean-vscode-linux-x64', util.rimraf(path.join(buildRoot, 'azuredatas gulp.task('clean-vscode-linux-arm', util.rimraf(path.join(buildRoot, 'azuredatastudio-linux-arm'))); gulp.task('vscode-win32-ia32', ['optimize-vscode', 'clean-vscode-win32-ia32'], packageTask('win32', 'ia32')); -gulp.task('vscode-win32-x64', ['optimize-vscode', 'clean-vscode-win32-x64'], packageTask('win32', 'x64')); -gulp.task('vscode-darwin', ['optimize-vscode', 'clean-vscode-darwin'], packageTask('darwin')); +gulp.task('vscode-win32-x64', ['vscode-win32-x64-azurecore', 'optimize-vscode', 'clean-vscode-win32-x64'], packageTask('win32', 'x64')); +gulp.task('vscode-darwin', ['vscode-darwin-azurecore', 'optimize-vscode', 'clean-vscode-darwin'], packageTask('darwin')); gulp.task('vscode-linux-ia32', ['optimize-vscode', 'clean-vscode-linux-ia32'], packageTask('linux', 'ia32')); -gulp.task('vscode-linux-x64', ['optimize-vscode', 'clean-vscode-linux-x64'], packageTask('linux', 'x64')); +gulp.task('vscode-linux-x64', ['vscode-linux-x64-azurecore', 'optimize-vscode', 'clean-vscode-linux-x64'], packageTask('linux', 'x64')); gulp.task('vscode-linux-arm', ['optimize-vscode', 'clean-vscode-linux-arm'], packageTask('linux', 'arm')); gulp.task('vscode-win32-ia32-min', ['minify-vscode', 'clean-vscode-win32-ia32'], packageTask('win32', 'ia32', { minified: true })); diff --git a/extensions/azurecore/.vscodeignore b/extensions/azurecore/.vscodeignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/extensions/azurecore/README.md b/extensions/azurecore/README.md new file mode 100644 index 0000000000..003e776621 --- /dev/null +++ b/extensions/azurecore/README.md @@ -0,0 +1,21 @@ +# Azure (Core) extension for Azure Data Studio + +Welcome to the Azure (Core) extension for Azure Data Studio! This extension supports core Azure functionality such as browsing and connecting to Azure data endpoints. In the current release the following features are supported: + +* Log in to Azure and browse your accounts, subscriptions and data resources +* See Azure SQL Databases and Servers in the tree, and open these connections in Object Explorer +* Filter the list of subscriptions for a given account, to make finding specific databases easier + +## 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 + +The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) describes the privacy statement of this software. + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt). diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json new file mode 100644 index 0000000000..d490feffbb --- /dev/null +++ b/extensions/azurecore/package.json @@ -0,0 +1,128 @@ +{ + "name": "azurecore", + "displayName": "%azure.displayName%", + "description": "%azure.description", + "version": "0.1.0", + "publisher": "Microsoft", + "preview": true, + "engines": { + "vscode": "^1.25.0", + "sqlops": "*" + }, + "activationEvents": [ + "onView:azureResourceExplorer" + ], + "main": "./out/extension", + "contributes": { + "configuration": [ + { + "type": "object", + "title": "%azure.config.title%", + "properties": { + "azureResource.resourceFilter": { + "type": "array", + "default": null, + "description": "%azure.resourceFilter.description%" + } + } + } + ], + "commands": [ + { + "command": "azureresource.refreshall", + "title": "%azureresource.refreshall%", + "icon": { + "dark": "resources/dark/refresh_inverse.svg", + "light": "resources/light/refresh.svg" + } + }, + { + "command": "azureresource.refresh", + "title": "%azureresource.refresh%", + "icon": { + "dark": "resources/dark/refresh_inverse.svg", + "light": "resources/light/refresh.svg" + } + }, + { + "command": "azureresource.signin", + "title": "%azureresource.signin%" + }, + { + "command": "azureresource.connectsqldb", + "title": "%azureresource.connectsqldb%", + "icon": { + "dark": "resources/dark/connect_to_inverse.svg", + "light": "resources/light/connect_to.svg" + } + }, + { + "command": "azureresource.selectsubscriptions", + "title": "%azureresource.selectsubscriptions%", + "icon": { + "dark": "resources/dark/filter_inverse.svg", + "light": "resources/light/filter.svg" + } + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "azureResource", + "title": "%azure.title%", + "icon": "resources/azure.svg" + } + ] + }, + "views": { + "azureResource": [ + { + "id": "azureResourceExplorer", + "name": "%azure.resourceExplorer.title%" + } + ] + }, + "menus": { + "view/title": [ + { + "command": "azureresource.refreshall", + "when": "view == azureResourceExplorer", + "group": "navigation@1" + } + ], + "view/item/context": [ + { + "command": "azureresource.connectsqldb", + "when": "viewItem =~ /^azureResource.itemType.database/ && viewItem != azureResource.itemType.databaseContainer && viewItem != azureResource.itemType.databaseServerContainer", + "group": "1azureresource@1" + }, + { + "command": "azureresource.connectsqldb", + "when": "viewItem =~ /^azureResource.itemType.database/ && viewItem != azureResource.itemType.databaseContainer && viewItem != azureResource.itemType.databaseServerContainer", + "group": "inline" + }, + { + "command": "azureresource.selectsubscriptions", + "when": "viewItem == azureResource.itemType.account", + "group": "inline" + }, + { + "command": "azureresource.refresh", + "when": "viewItem != azureResource.itemType.database && viewItem != azureResource.itemType.databaseServer && viewItem != azureResource.itemType.message", + "group": "inline" + } + ] + } + }, + "dependencies": { + "azure-arm-resource": "^7.0.0", + "azure-arm-sql": "^5.0.1", + "vscode-nls": "^4.0.0" + }, + "devDependencies": { + "@types/mocha": "^5.2.5", + "mocha": "^5.2.0", + "should": "^13.2.1", + "typemoq": "^2.1.0" + } +} diff --git a/extensions/azurecore/package.nls.json b/extensions/azurecore/package.nls.json new file mode 100644 index 0000000000..5b3abedb0b --- /dev/null +++ b/extensions/azurecore/package.nls.json @@ -0,0 +1,13 @@ +{ + "azure.displayName": "Azure (Core)", + "azure.description": "Browse and work with Azure resources", + "azure.config.title": "Azure Resource Configuration", + "azure.resourceFilter.description": "The resource filter, each element is an account id, a subscription id and name separated by a slash", + "azureresource.refreshall": "Refresh All", + "azureresource.refresh": "Refresh", + "azureresource.signin": "Sign In", + "azureresource.connectsqldb": "Connect", + "azureresource.selectsubscriptions": "Select Subscriptions", + "azure.title": "Azure", + "azure.resourceExplorer.title": "Resource Explorer" +} \ No newline at end of file diff --git a/extensions/azurecore/resources/azure.svg b/extensions/azurecore/resources/azure.svg new file mode 100644 index 0000000000..dd17231563 --- /dev/null +++ b/extensions/azurecore/resources/azure.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/extensions/azurecore/resources/dark/account_inverse.svg b/extensions/azurecore/resources/dark/account_inverse.svg new file mode 100644 index 0000000000..0429cb4941 --- /dev/null +++ b/extensions/azurecore/resources/dark/account_inverse.svg @@ -0,0 +1 @@ +account_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/connect_to_inverse.svg b/extensions/azurecore/resources/dark/connect_to_inverse.svg new file mode 100644 index 0000000000..9d796bdc76 --- /dev/null +++ b/extensions/azurecore/resources/dark/connect_to_inverse.svg @@ -0,0 +1 @@ +connect_to_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/filter_inverse.svg b/extensions/azurecore/resources/dark/filter_inverse.svg new file mode 100644 index 0000000000..60b90abd6d --- /dev/null +++ b/extensions/azurecore/resources/dark/filter_inverse.svg @@ -0,0 +1 @@ +filter_inverse_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/folder_inverse.svg b/extensions/azurecore/resources/dark/folder_inverse.svg new file mode 100644 index 0000000000..f94d427cb1 --- /dev/null +++ b/extensions/azurecore/resources/dark/folder_inverse.svg @@ -0,0 +1 @@ +folder_inverse_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/refresh_inverse.svg b/extensions/azurecore/resources/dark/refresh_inverse.svg new file mode 100644 index 0000000000..d79fdaa4e8 --- /dev/null +++ b/extensions/azurecore/resources/dark/refresh_inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/sql_database_inverse.svg b/extensions/azurecore/resources/dark/sql_database_inverse.svg new file mode 100644 index 0000000000..5eaaf7e5f0 --- /dev/null +++ b/extensions/azurecore/resources/dark/sql_database_inverse.svg @@ -0,0 +1 @@ +sql_database_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/sql_server_inverse.svg b/extensions/azurecore/resources/dark/sql_server_inverse.svg new file mode 100644 index 0000000000..1ce3f48b39 --- /dev/null +++ b/extensions/azurecore/resources/dark/sql_server_inverse.svg @@ -0,0 +1 @@ +sql_server_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/dark/subscription_inverse.svg b/extensions/azurecore/resources/dark/subscription_inverse.svg new file mode 100644 index 0000000000..491fb5e895 --- /dev/null +++ b/extensions/azurecore/resources/dark/subscription_inverse.svg @@ -0,0 +1 @@ +subscription_inverse \ No newline at end of file diff --git a/extensions/azurecore/resources/light/account.svg b/extensions/azurecore/resources/light/account.svg new file mode 100644 index 0000000000..91de1865c0 --- /dev/null +++ b/extensions/azurecore/resources/light/account.svg @@ -0,0 +1 @@ +account \ No newline at end of file diff --git a/extensions/azurecore/resources/light/connect_to.svg b/extensions/azurecore/resources/light/connect_to.svg new file mode 100644 index 0000000000..1f420533d6 --- /dev/null +++ b/extensions/azurecore/resources/light/connect_to.svg @@ -0,0 +1 @@ +connect_to \ No newline at end of file diff --git a/extensions/azurecore/resources/light/filter.svg b/extensions/azurecore/resources/light/filter.svg new file mode 100644 index 0000000000..32f914f54a --- /dev/null +++ b/extensions/azurecore/resources/light/filter.svg @@ -0,0 +1 @@ +filter_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/light/folder.svg b/extensions/azurecore/resources/light/folder.svg new file mode 100644 index 0000000000..517c9b185d --- /dev/null +++ b/extensions/azurecore/resources/light/folder.svg @@ -0,0 +1 @@ +folder_16x16 \ No newline at end of file diff --git a/extensions/azurecore/resources/light/refresh.svg b/extensions/azurecore/resources/light/refresh.svg new file mode 100644 index 0000000000..e034574819 --- /dev/null +++ b/extensions/azurecore/resources/light/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/azurecore/resources/light/sql_database.svg b/extensions/azurecore/resources/light/sql_database.svg new file mode 100644 index 0000000000..e0c1584d98 --- /dev/null +++ b/extensions/azurecore/resources/light/sql_database.svg @@ -0,0 +1 @@ +sql_database \ No newline at end of file diff --git a/extensions/azurecore/resources/light/sql_server.svg b/extensions/azurecore/resources/light/sql_server.svg new file mode 100644 index 0000000000..03cff4d932 --- /dev/null +++ b/extensions/azurecore/resources/light/sql_server.svg @@ -0,0 +1 @@ +sql_server \ No newline at end of file diff --git a/extensions/azurecore/resources/light/subscription.svg b/extensions/azurecore/resources/light/subscription.svg new file mode 100644 index 0000000000..5fda717712 --- /dev/null +++ b/extensions/azurecore/resources/light/subscription.svg @@ -0,0 +1 @@ +subscription \ No newline at end of file diff --git a/extensions/azurecore/src/apiWrapper.ts b/extensions/azurecore/src/apiWrapper.ts new file mode 100644 index 0000000000..7a13231e26 --- /dev/null +++ b/extensions/azurecore/src/apiWrapper.ts @@ -0,0 +1,225 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as sqlops from 'sqlops'; + +import * as constants from './constants'; + +/** + * Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into + * this API from our code + * + * @export + * @class ApiWrapper + */ +export class ApiWrapper { + // Data APIs + public registerConnectionProvider(provider: sqlops.ConnectionProvider): vscode.Disposable { + return sqlops.dataprotocol.registerConnectionProvider(provider); + } + + public registerObjectExplorerProvider(provider: sqlops.ObjectExplorerProvider): vscode.Disposable { + return sqlops.dataprotocol.registerObjectExplorerProvider(provider); + } + + public registerTaskServicesProvider(provider: sqlops.TaskServicesProvider): vscode.Disposable { + return sqlops.dataprotocol.registerTaskServicesProvider(provider); + } + + public registerFileBrowserProvider(provider: sqlops.FileBrowserProvider): vscode.Disposable { + return sqlops.dataprotocol.registerFileBrowserProvider(provider); + } + + public registerCapabilitiesServiceProvider(provider: sqlops.CapabilitiesProvider): vscode.Disposable { + return sqlops.dataprotocol.registerCapabilitiesServiceProvider(provider); + } + + public registerModelViewProvider(widgetId: string, handler: (modelView: sqlops.ModelView) => void): void { + return sqlops.ui.registerModelViewProvider(widgetId, handler); + } + + public registerWebviewProvider(widgetId: string, handler: (webview: sqlops.DashboardWebview) => void): void { + return sqlops.dashboard.registerWebviewProvider(widgetId, handler); + } + + public createDialog(title: string): sqlops.window.modelviewdialog.Dialog { + return sqlops.window.modelviewdialog.createDialog(title); + } + + public openDialog(dialog: sqlops.window.modelviewdialog.Dialog): void { + return sqlops.window.modelviewdialog.openDialog(dialog); + } + + public closeDialog(dialog: sqlops.window.modelviewdialog.Dialog): void { + return sqlops.window.modelviewdialog.closeDialog(dialog); + } + + public registerTaskHandler(taskId: string, handler: (profile: sqlops.IConnectionProfile) => void): void { + sqlops.tasks.registerTask(taskId, handler); + } + + public startBackgroundOperation(operationInfo: sqlops.BackgroundOperationInfo): void { + sqlops.tasks.startBackgroundOperation(operationInfo); + } + + public getActiveConnections(): Thenable { + return sqlops.connection.getActiveConnections(); + } + + public getCurrentConnection(): Thenable { + return sqlops.connection.getCurrentConnection(); + } + + public createModelViewEditor(title: string, options?: sqlops.ModelViewEditorOptions): sqlops.workspace.ModelViewEditor { + return sqlops.workspace.createModelViewEditor(title, options); + } + + // VSCode APIs + public createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { + return vscode.window.createTerminal(name, shellPath, shellArgs); + } + + public createTerminalWithOptions(options: vscode.TerminalOptions): vscode.Terminal { + return vscode.window.createTerminal(options); + } + + public executeCommand(command: string, ...rest: any[]): Thenable { + return vscode.commands.executeCommand(command, ...rest); + } + + public getFilePathRelativeToWorkspace(uri: vscode.Uri): string { + return vscode.workspace.asRelativePath(uri); + } + + public getWorkspaceFolders(): vscode.WorkspaceFolder[] { + return vscode.workspace.workspaceFolders; + } + + public getWorkspacePathFromUri(uri: vscode.Uri): string | undefined { + let workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); + return workspaceFolder ? workspaceFolder.uri.fsPath : undefined; + } + + public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable { + return vscode.commands.registerCommand(command, callback, thisArg); + } + + public registerDocumentOpenHandler(handler: (doc: vscode.TextDocument) => any): vscode.Disposable { + return vscode.workspace.onDidOpenTextDocument(handler); + } + + public registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { + return vscode.window.registerTreeDataProvider(viewId, treeDataProvider); + } + + public setCommandContext(key: string, value: any): Thenable { + return vscode.commands.executeCommand(constants.BuiltInCommands.SetContext, key, value); + } + + /** + * Get the configuration for a extensionName + * @param extensionName The string name of the extension to get the configuration for + * @param resource The optional URI, as a URI object or a string, to use to get resource-scoped configurations + */ + public getConfiguration(extensionName?: string, resource?: vscode.Uri | string): vscode.WorkspaceConfiguration { + if (typeof resource === 'string') { + try { + resource = this.parseUri(resource); + } catch (e) { + resource = undefined; + } + } + return vscode.workspace.getConfiguration(extensionName, resource as vscode.Uri); + } + + public getExtensionConfiguration(): vscode.WorkspaceConfiguration { + return this.getConfiguration(constants.extensionConfigSectionName); + } + + /** + * Parse uri + */ + public parseUri(uri: string): vscode.Uri { + return vscode.Uri.parse(uri); + } + + public showOpenDialog(options: vscode.OpenDialogOptions): Thenable { + return vscode.window.showOpenDialog(options); + } + + public showSaveDialog(options: vscode.SaveDialogOptions): Thenable { + return vscode.window.showSaveDialog(options); + } + + public openTextDocument(uri: vscode.Uri): Thenable; + public openTextDocument(options: { language?: string; content?: string; }): Thenable; + public openTextDocument(uriOrOptions): Thenable { + return vscode.workspace.openTextDocument(uriOrOptions); + } + + public showTextDocument(document: vscode.TextDocument, column?: vscode.ViewColumn, preserveFocus?: boolean, preview?: boolean): Thenable { + let options: vscode.TextDocumentShowOptions = { + viewColumn: column, + preserveFocus: preserveFocus, + preview: preview + }; + return vscode.window.showTextDocument(document, options); + } + + public showErrorMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showErrorMessage(message, ...items); + } + + public showWarningMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showWarningMessage(message, ...items); + } + + public showInformationMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showInformationMessage(message, ...items); + } + + public createStatusBarItem(alignment?: vscode.StatusBarAlignment, priority?: number): vscode.StatusBarItem { + return vscode.window.createStatusBarItem(alignment, priority); + } + + public get workspaceFolders(): vscode.WorkspaceFolder[] { + return vscode.workspace.workspaceFolders; + } + + public createOutputChannel(name: string): vscode.OutputChannel { + return vscode.window.createOutputChannel(name); + } + + public createWizardPage(title: string): sqlops.window.modelviewdialog.WizardPage { + return sqlops.window.modelviewdialog.createWizardPage(title); + } + + public registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable { + return vscode.languages.registerCompletionItemProvider(selector, provider, ...triggerCharacters); + } + + public createTab(title: string): sqlops.window.modelviewdialog.DialogTab { + return sqlops.window.modelviewdialog.createTab(title); + } + + // Account APIs + public getAllAccounts(): Thenable { + return sqlops.accounts.getAllAccounts(); + } + + public getSecurityToken(account: sqlops.Account): Thenable<{}> { + return sqlops.accounts.getSecurityToken(account); + } + + public readonly onDidChangeAccounts = sqlops.accounts.onDidChangeAccounts; + + // Connection APIs + public openConnectionDialog(providers: string[], initialConnectionProfile?: sqlops.IConnectionProfile, connectionCompletionOptions?: sqlops.IConnectionCompletionOptions): Thenable { + return sqlops.connection.openConnectionDialog(providers, initialConnectionProfile, connectionCompletionOptions); + } +} diff --git a/extensions/azurecore/src/appContext.ts b/extensions/azurecore/src/appContext.ts new file mode 100644 index 0000000000..4abec27439 --- /dev/null +++ b/extensions/azurecore/src/appContext.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import { ApiWrapper } from './apiWrapper'; + +/** + * Global context for the application + */ +export class AppContext { + + private serviceMap: Map = new Map(); + constructor(public readonly extensionContext: vscode.ExtensionContext, public readonly apiWrapper: ApiWrapper) { + this.apiWrapper = apiWrapper || new ApiWrapper(); + } + + public getService(serviceName: string): T { + return this.serviceMap.get(serviceName) as T; + } + + public registerService(serviceName: string, service: T): void { + this.serviceMap.set(serviceName, service); + } +} diff --git a/extensions/azurecore/src/azureResource/commands.ts b/extensions/azurecore/src/azureResource/commands.ts new file mode 100644 index 0000000000..0edb925c6f --- /dev/null +++ b/extensions/azurecore/src/azureResource/commands.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { window, QuickPickItem } from 'vscode'; +import { IConnectionProfile } from 'sqlops'; +import { generateGuid } from './utils'; +import { ApiWrapper } from '../apiWrapper'; +import { TreeNode } from '../treeNodes'; + +import { AzureResourceTreeProvider } from './tree/treeProvider'; +import { AzureResourceDatabaseServerTreeNode } from './tree/databaseServerTreeNode'; +import { AzureResourceDatabaseTreeNode } from './tree/databaseTreeNode'; +import { AzureResourceAccountTreeNode } from './tree/accountTreeNode'; +import { AzureResourceServicePool } from './servicePool'; +import { AzureResourceSubscription } from './models'; + +export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: AzureResourceTreeProvider): void { + apiWrapper.registerCommand('azureresource.selectsubscriptions', async (node?: TreeNode) => { + if (!(node instanceof AzureResourceAccountTreeNode)) { + return; + } + + const accountNode = node as AzureResourceAccountTreeNode; + + const servicePool = AzureResourceServicePool.getInstance(); + + let subscriptions = await accountNode.getCachedSubscriptions(); + if (!subscriptions || subscriptions.length === 0) { + const credentials = await servicePool.credentialService.getCredentials(accountNode.account); + subscriptions = await servicePool.subscriptionService.getSubscriptions(accountNode.account, credentials); + } + + const selectedSubscriptions = (await servicePool.subscriptionFilterService.getSelectedSubscriptions(accountNode.account)) || []; + const selectedSubscriptionIds: string[] = []; + if (selectedSubscriptions.length > 0) { + selectedSubscriptionIds.push(...selectedSubscriptions.map((subscription) => subscription.id)); + } else { + // ALL subscriptions are selected by default + selectedSubscriptionIds.push(...subscriptions.map((subscription) => subscription.id)); + } + + interface SubscriptionQuickPickItem extends QuickPickItem { + subscription: AzureResourceSubscription; + } + + const subscriptionItems: SubscriptionQuickPickItem[] = subscriptions.map((subscription) => { + return { + label: subscription.name, + picked: selectedSubscriptionIds.indexOf(subscription.id) !== -1, + subscription: subscription + }; + }); + + const pickedSubscriptionItems = (await window.showQuickPick(subscriptionItems, { canPickMany: true })); + if (pickedSubscriptionItems && pickedSubscriptionItems.length > 0) { + tree.refresh(node, false); + + const pickedSubscriptions = pickedSubscriptionItems.map((subscriptionItem) => subscriptionItem.subscription); + await servicePool.subscriptionFilterService.saveSelectedSubscriptions(accountNode.account, pickedSubscriptions); + } + }); + + apiWrapper.registerCommand('azureresource.refreshall', () => tree.notifyNodeChanged(undefined)); + + apiWrapper.registerCommand('azureresource.refresh', async (node?: TreeNode) => { + tree.refresh(node, true); + }); + + apiWrapper.registerCommand('azureresource.connectsqldb', async (node?: TreeNode) => { + let connectionProfile: IConnectionProfile = { + id: generateGuid(), + connectionName: undefined, + serverName: undefined, + databaseName: undefined, + userName: undefined, + password: '', + authenticationType: undefined, + savePassword: true, + groupFullName: '', + groupId: '', + providerName: undefined, + saveProfile: true, + options: { + } + }; + + if (node instanceof AzureResourceDatabaseServerTreeNode) { + let databaseServer = node.databaseServer; + connectionProfile.connectionName = `connection to '${databaseServer.defaultDatabaseName}' on '${databaseServer.fullName}'`; + connectionProfile.serverName = databaseServer.fullName; + connectionProfile.databaseName = databaseServer.defaultDatabaseName; + connectionProfile.userName = databaseServer.loginName; + connectionProfile.authenticationType = 'SqlLogin'; + connectionProfile.providerName = 'MSSQL'; + } + + if (node instanceof AzureResourceDatabaseTreeNode) { + let database = node.database; + connectionProfile.connectionName = `connection to '${database.name}' on '${database.serverFullName}'`; + connectionProfile.serverName = database.serverFullName; + connectionProfile.databaseName = database.name; + connectionProfile.userName = database.loginName; + connectionProfile.authenticationType = 'SqlLogin'; + connectionProfile.providerName = 'MSSQL'; + } + + const conn = await apiWrapper.openConnectionDialog(undefined, connectionProfile, { saveConnection: true, showDashboard: true }); + if (conn) { + apiWrapper.executeCommand('workbench.view.connections'); + } + }); + + apiWrapper.registerCommand('azureresource.signin', async (node?: TreeNode) => { + apiWrapper.executeCommand('sql.action.accounts.manageLinkedAccount'); + }); +} diff --git a/extensions/azurecore/src/azureResource/constants.ts b/extensions/azurecore/src/azureResource/constants.ts new file mode 100644 index 0000000000..3fb5368927 --- /dev/null +++ b/extensions/azurecore/src/azureResource/constants.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * 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 enum AzureResourceItemType { + account = 'azureResource.itemType.account', + subscription = 'azureResource.itemType.subscription', + databaseContainer = 'azureResource.itemType.databaseContainer', + database = 'azureResource.itemType.database', + databaseServerContainer = 'azureResource.itemType.databaseServerContainer', + databaseServer = 'azureResource.itemType.databaseServer', + message = 'azureResource.itemType.message' +} diff --git a/extensions/azurecore/src/azureResource/errors.ts b/extensions/azurecore/src/azureResource/errors.ts new file mode 100644 index 0000000000..c0fe4a842f --- /dev/null +++ b/extensions/azurecore/src/azureResource/errors.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +export class AzureResourceCredentialError extends Error { + constructor( + message: string, + public innerError: Error + ) { + super(message); + } +} diff --git a/extensions/azurecore/src/azureResource/interfaces.ts b/extensions/azurecore/src/azureResource/interfaces.ts new file mode 100644 index 0000000000..5d21c5d408 --- /dev/null +++ b/extensions/azurecore/src/azureResource/interfaces.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ServiceClientCredentials } from 'ms-rest'; +import { Account, DidChangeAccountsParams } from 'sqlops'; +import { Event } from 'vscode'; + +import { AzureResourceSubscription, AzureResourceDatabaseServer, AzureResourceDatabase } from './models'; + +export interface IAzureResourceAccountService { + getAccounts(): Promise; + + readonly onDidChangeAccounts: Event; +} + +export interface IAzureResourceCredentialService { + getCredentials(account: Account): Promise; +} + +export interface IAzureResourceSubscriptionService { + getSubscriptions(account: Account, credentials: ServiceClientCredentials[]): Promise; +} + +export interface IAzureResourceSubscriptionFilterService { + getSelectedSubscriptions(account: Account): Promise; + + saveSelectedSubscriptions(account: Account, selectedSubscriptions: AzureResourceSubscription[]): Promise; +} + +export interface IAzureResourceDatabaseServerService { + getDatabaseServers(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise; +} + +export interface IAzureResourceDatabaseService { + getDatabases(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise; +} + +export interface IAzureResourceCacheService { + get(key: string): T | undefined; + + update(key: string, value: T): void; +} + +export interface IAzureResourceContextService { + getAbsolutePath(relativePath: string): string; + + executeCommand(commandId: string, ...args: any[]): void; + + showErrorMessage(errorMessage: string): void; +} diff --git a/extensions/azurecore/src/azureResource/models.ts b/extensions/azurecore/src/azureResource/models.ts new file mode 100644 index 0000000000..21e5b4ab37 --- /dev/null +++ b/extensions/azurecore/src/azureResource/models.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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 AzureResourceSubscription { + id: string; + name: string; +} + +export interface AzureResourceDatabaseServer { + name: string; + fullName: string; + loginName: string; + defaultDatabaseName: string; +} + +export interface AzureResourceDatabase { + name: string; + serverName: string; + serverFullName: string; + loginName: string; +} diff --git a/extensions/azurecore/src/azureResource/servicePool.ts b/extensions/azurecore/src/azureResource/servicePool.ts new file mode 100644 index 0000000000..27aff58d98 --- /dev/null +++ b/extensions/azurecore/src/azureResource/servicePool.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + IAzureResourceAccountService, + IAzureResourceCredentialService, + IAzureResourceSubscriptionService, + IAzureResourceSubscriptionFilterService, + IAzureResourceDatabaseService, + IAzureResourceDatabaseServerService, + IAzureResourceCacheService, + IAzureResourceContextService } from './interfaces'; + +export class AzureResourceServicePool { + private constructor() { } + + public static getInstance(): AzureResourceServicePool { + return AzureResourceServicePool._instance; + } + + public contextService: IAzureResourceContextService; + public cacheService: IAzureResourceCacheService; + public accountService: IAzureResourceAccountService; + public credentialService: IAzureResourceCredentialService; + public subscriptionService: IAzureResourceSubscriptionService; + public subscriptionFilterService: IAzureResourceSubscriptionFilterService; + public databaseService: IAzureResourceDatabaseService; + public databaseServerService: IAzureResourceDatabaseServerService; + + private static readonly _instance = new AzureResourceServicePool(); +} diff --git a/extensions/azurecore/src/azureResource/services/accountService.ts b/extensions/azurecore/src/azureResource/services/accountService.ts new file mode 100644 index 0000000000..bc38f823a5 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/accountService.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Event } from 'vscode'; +import { Account, DidChangeAccountsParams } from 'sqlops'; +import { ApiWrapper } from '../../apiWrapper'; + +import { IAzureResourceAccountService } from '../interfaces'; + +export class AzureResourceAccountService implements IAzureResourceAccountService { + public constructor( + apiWrapper: ApiWrapper + ) { + this._apiWrapper = apiWrapper; + this._onDidChangeAccounts = this._apiWrapper.onDidChangeAccounts; + } + + public async getAccounts(): Promise { + return await this._apiWrapper.getAllAccounts(); + } + + public get onDidChangeAccounts(): Event { + return this._onDidChangeAccounts; + } + + private _apiWrapper: ApiWrapper = undefined; + private _onDidChangeAccounts: Event = undefined; +} diff --git a/extensions/azurecore/src/azureResource/services/cacheService.ts b/extensions/azurecore/src/azureResource/services/cacheService.ts new file mode 100644 index 0000000000..ba0d100273 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/cacheService.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ExtensionContext } from "vscode"; + +import { IAzureResourceCacheService } from "../interfaces"; + +export class AzureResourceCacheService implements IAzureResourceCacheService { + public constructor( + public readonly context: ExtensionContext + ) { + } + + public get(key: string): T | undefined { + return this.context.workspaceState.get(key); + } + + public update(key: string, value: T): void { + this.context.workspaceState.update(key, value); + } +} \ No newline at end of file diff --git a/extensions/azurecore/src/azureResource/services/contextService.ts b/extensions/azurecore/src/azureResource/services/contextService.ts new file mode 100644 index 0000000000..0e153d48e5 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/contextService.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ExtensionContext } from "vscode"; +import { ApiWrapper } from "../../apiWrapper"; + +import { IAzureResourceContextService } from "../interfaces"; + +export class AzureResourceContextService implements IAzureResourceContextService { + public constructor( + context: ExtensionContext, + apiWrapper: ApiWrapper + ) { + this._context = context; + this._apiWrapper = apiWrapper; + } + + public getAbsolutePath(relativePath: string): string { + return this._context.asAbsolutePath(relativePath); + } + + public executeCommand(commandId: string, ...args: any[]): void { + this._apiWrapper.executeCommand(commandId, args); + } + + public showErrorMessage(errorMessage: string): void { + this._apiWrapper.showErrorMessage(errorMessage); + } + + private _context: ExtensionContext = undefined; + private _apiWrapper: ApiWrapper = undefined; +} diff --git a/extensions/azurecore/src/azureResource/services/credentialService.ts b/extensions/azurecore/src/azureResource/services/credentialService.ts new file mode 100644 index 0000000000..028181fb87 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/credentialService.ts @@ -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 { Account } from 'sqlops'; +import { TokenCredentials, ServiceClientCredentials } from 'ms-rest'; +import { ApiWrapper } from '../../apiWrapper'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { IAzureResourceCredentialService } from '../interfaces'; +import { AzureResourceCredentialError } from '../errors'; + +export class AzureResourceCredentialService implements IAzureResourceCredentialService { + public constructor( + apiWrapper: ApiWrapper + ) { + this._apiWrapper = apiWrapper; + } + + public async getCredentials(account: Account): Promise { + try { + let credentials: TokenCredentials[] = []; + let tokens = await this._apiWrapper.getSecurityToken(account); + + for (let tenant of account.properties.tenants) { + let token = tokens[tenant.id].token; + let tokenType = tokens[tenant.id].tokenType; + + credentials.push(new TokenCredentials(token, tokenType)); + } + + return credentials; + } catch (error) { + throw new AzureResourceCredentialError(localize('azureResource.services.credentialService.credentialError', 'Failed to get credential for account {0}. Please refresh the account.', account.key.accountId), error); + } + } + + private _apiWrapper: ApiWrapper = undefined; +} diff --git a/extensions/azurecore/src/azureResource/services/databaseServerService.ts b/extensions/azurecore/src/azureResource/services/databaseServerService.ts new file mode 100644 index 0000000000..9145629a34 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/databaseServerService.ts @@ -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 { ServiceClientCredentials } from 'ms-rest'; +import { SqlManagementClient } from 'azure-arm-sql'; + +import { IAzureResourceDatabaseServerService } from '../interfaces'; +import { AzureResourceSubscription, AzureResourceDatabaseServer } from '../models'; + +export class AzureResourceDatabaseServerService implements IAzureResourceDatabaseServerService { + public async getDatabaseServers(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise { + let databaseServers: AzureResourceDatabaseServer[] = []; + for (let cred of credentials) { + let sqlManagementClient = new SqlManagementClient(cred, subscription.id); + try { + let svrs = await sqlManagementClient.servers.list(); + svrs.forEach((svr) => databaseServers.push({ + name: svr.name, + fullName: svr.fullyQualifiedDomainName, + loginName: svr.administratorLogin, + defaultDatabaseName: 'master' + })); + } catch (error) { + if (error.code === 'InvalidAuthenticationTokenTenant' && error.statusCode === 401) { + /** + * There may be multiple tenants for an account and it may throw exceptions like following. Just swallow the exception here. + * The access token is from the wrong issuer. It must match one of the tenants associated with this subscription. + */ + } + } + } + + return databaseServers; + } +} diff --git a/extensions/azurecore/src/azureResource/services/databaseService.ts b/extensions/azurecore/src/azureResource/services/databaseService.ts new file mode 100644 index 0000000000..643db5c1e6 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/databaseService.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ServiceClientCredentials } from 'ms-rest'; +import { SqlManagementClient } from 'azure-arm-sql'; + +import { IAzureResourceDatabaseService } from '../interfaces'; +import { AzureResourceSubscription, AzureResourceDatabase } from '../models'; + +export class AzureResourceDatabaseService implements IAzureResourceDatabaseService { + public async getDatabases(subscription: AzureResourceSubscription, credentials: ServiceClientCredentials[]): Promise { + let databases: AzureResourceDatabase[] = []; + for (let cred of credentials) { + let sqlManagementClient = new SqlManagementClient(cred, subscription.id); + try { + let svrs = await sqlManagementClient.servers.list(); + for (let svr of svrs) { + // Extract resource group name from svr.id + let svrIdRegExp = new RegExp(`\/subscriptions\/${subscription.id}\/resourceGroups\/(.+)\/providers\/Microsoft\.Sql\/servers\/${svr.name}`); + if (!svrIdRegExp.test(svr.id)) { + continue; + } + + let founds = svrIdRegExp.exec(svr.id); + let resouceGroup = founds[1]; + + let dbs = await sqlManagementClient.databases.listByServer(resouceGroup, svr.name); + dbs.forEach((db) => databases.push({ + name: db.name, + serverName: svr.name, + serverFullName: svr.fullyQualifiedDomainName, + loginName: svr.administratorLogin + })); + } + } catch (error) { + if (error.code === 'InvalidAuthenticationTokenTenant' && error.statusCode === 401) { + /** + * There may be multiple tenants for an account and it may throw exceptions like following. Just swallow the exception here. + * The access token is from the wrong issuer. It must match one of the tenants associated with this subscription. + */ + } + } + } + + return databases; + } +} diff --git a/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts b/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts new file mode 100644 index 0000000000..9e6bea1b08 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/subscriptionFilterService.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { WorkspaceConfiguration, ConfigurationTarget } from 'vscode'; +import { Account } from 'sqlops'; + +import { IAzureResourceSubscriptionFilterService, IAzureResourceCacheService } from '../interfaces'; +import { AzureResourceSubscription } from '../models'; + +interface AzureResourceSelectedSubscriptionsCache { + selectedSubscriptions: { [accountId: string]: AzureResourceSubscription[]}; +} + +export class AzureResourceSubscriptionFilterService implements IAzureResourceSubscriptionFilterService { + public constructor( + cacheService: IAzureResourceCacheService + ) { + this._cacheService = cacheService; + } + + public async getSelectedSubscriptions(account: Account): Promise { + let selectedSubscriptions: AzureResourceSubscription[] = []; + + const cache = this._cacheService.get(AzureResourceSubscriptionFilterService.CacheKey); + if (cache) { + selectedSubscriptions = cache.selectedSubscriptions[account.key.accountId]; + } + + return selectedSubscriptions; + } + + public async saveSelectedSubscriptions(account: Account, selectedSubscriptions: AzureResourceSubscription[]): Promise { + let selectedSubscriptionsCache: { [accountId: string]: AzureResourceSubscription[]} = {}; + + const cache = this._cacheService.get(AzureResourceSubscriptionFilterService.CacheKey); + if (cache) { + selectedSubscriptionsCache = cache.selectedSubscriptions; + } + + if (!selectedSubscriptionsCache) { + selectedSubscriptionsCache = {}; + } + + selectedSubscriptionsCache[account.key.accountId] = selectedSubscriptions; + + this._cacheService.update(AzureResourceSubscriptionFilterService.CacheKey, { selectedSubscriptions: selectedSubscriptionsCache }); + + const filters: string[] = []; + for (const accountId in selectedSubscriptionsCache) { + filters.push(...selectedSubscriptionsCache[accountId].map((subcription) => `${accountId}/${subcription.id}/${subcription.name}`)); + } + + const resourceFilterConfig = this._config.inspect(AzureResourceSubscriptionFilterService.FilterConfigName); + let configTarget = ConfigurationTarget.Global; + if (resourceFilterConfig) { + if (resourceFilterConfig.workspaceFolderValue) { + configTarget = ConfigurationTarget.WorkspaceFolder; + } else if (resourceFilterConfig.workspaceValue) { + configTarget = ConfigurationTarget.Workspace; + } else if (resourceFilterConfig.globalValue) { + configTarget = ConfigurationTarget.Global; + } + } + + await this._config.update(AzureResourceSubscriptionFilterService.FilterConfigName, filters, configTarget); + } + + private _config: WorkspaceConfiguration = undefined; + private _cacheService: IAzureResourceCacheService = undefined; + + private static readonly FilterConfigName = 'resourceFilter'; + private static readonly CacheKey = 'azureResource.cache.selectedSubscriptions'; +} diff --git a/extensions/azurecore/src/azureResource/services/subscriptionService.ts b/extensions/azurecore/src/azureResource/services/subscriptionService.ts new file mode 100644 index 0000000000..9a8cfe0779 --- /dev/null +++ b/extensions/azurecore/src/azureResource/services/subscriptionService.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Account } from 'sqlops'; +import { ServiceClientCredentials } from 'ms-rest'; +import { SubscriptionClient } from 'azure-arm-resource'; + +import { IAzureResourceSubscriptionService } from '../interfaces'; +import { AzureResourceSubscription } from '../models'; + +export class AzureResourceSubscriptionService implements IAzureResourceSubscriptionService { + public async getSubscriptions(account: Account, credentials: ServiceClientCredentials[]): Promise { + let subscriptions: AzureResourceSubscription[] = []; + for (let cred of credentials) { + let subClient = new SubscriptionClient.SubscriptionClient(cred); + try { + let subs = await subClient.subscriptions.list(); + subs.forEach((sub) => subscriptions.push({ + id: sub.subscriptionId, + name: sub.displayName + })); + } catch (error) { + // Swallow the exception here. + } + } + + return subscriptions; + } +} diff --git a/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts b/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts new file mode 100644 index 0000000000..47008428aa --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/accountNotSignedInTreeNode.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { AzureResourceItemType } from '../constants'; + +export class AzureResourceAccountNotSignedInTreeNode extends TreeNode { + public getChildren(): TreeNode[] | Promise { + return []; + } + + public getTreeItem(): TreeItem | Promise { + let item = new TreeItem(AzureResourceAccountNotSignedInTreeNode.SignInLabel, TreeItemCollapsibleState.None); + item.contextValue = AzureResourceItemType.message; + item.command = { + title: AzureResourceAccountNotSignedInTreeNode.SignInLabel, + command: 'azureresource.signin', + arguments: [this] + }; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: AzureResourceAccountNotSignedInTreeNode.SignInLabel, + isLeaf: true, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.message, + nodeSubType: undefined, + iconType: AzureResourceItemType.message + }; + } + + public get nodePathValue(): string { + return 'message_accountNotSignedIn'; + } + + private static readonly SignInLabel = localize('azureResource.tree.accountNotSignedInTreeNode.signIn', 'Sign in to Azure ...'); +} diff --git a/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts b/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts new file mode 100644 index 0000000000..a95a485ece --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/accountTreeNode.ts @@ -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 { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Account, NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; +import { AzureResourceItemType } from '../constants'; +import { AzureResourceSubscriptionTreeNode } from './subscriptionTreeNode'; +import { AzureResourceMessageTreeNode } from './messageTreeNode'; +import { AzureResourceErrorMessageUtil } from '../utils'; +import { AzureResourceSubscription } from '../models'; +import { IAzureResourceTreeChangeHandler } from './treeProvider'; + +export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNodeBase { + public constructor( + account: Account, + treeChangeHandler: IAzureResourceTreeChangeHandler + ) { + super(account, treeChangeHandler, undefined); + + this._id = `account_${this.account.key.accountId}`; + this._label = this.generateLabel(); + } + + public async getChildren(): Promise { + try { + let subscriptions: AzureResourceSubscription[] = []; + + if (this._isClearingCache) { + const credentials = await this.getCredentials(); + subscriptions = (await this.servicePool.subscriptionService.getSubscriptions(this.account, credentials)) || []; + + let cache = this.getCache(); + if (!cache) { + cache = { subscriptions: { } }; + } + cache.subscriptions[this.account.key.accountId] = subscriptions; + this.updateCache(cache); + + this._isClearingCache = false; + } else { + subscriptions = await this.getCachedSubscriptions(); + } + + this._totalSubscriptionCount = subscriptions.length; + + let selectedSubscriptions = await this.servicePool.subscriptionFilterService.getSelectedSubscriptions(this.account); + let selectedSubscriptionIds = (selectedSubscriptions || []).map((subscription) => subscription.id); + if (selectedSubscriptionIds.length > 0) { + subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1); + this._selectedSubscriptionCount = selectedSubscriptionIds.length; + } else { + // ALL subscriptions are listed by default + this._selectedSubscriptionCount = this._totalSubscriptionCount; + } + + this.refreshLabel(); + + if (subscriptions.length === 0) { + return [AzureResourceMessageTreeNode.create(AzureResourceAccountTreeNode.NoSubscriptions, this)]; + } else { + return subscriptions.map((subscription) => new AzureResourceSubscriptionTreeNode(subscription, this.account, this.treeChangeHandler, this)); + } + } catch (error) { + return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)]; + } + } + + public async getCachedSubscriptions(): Promise { + const subscriptions: AzureResourceSubscription[] = []; + const cache = this.getCache(); + if (cache) { + subscriptions.push(...cache.subscriptions[this.account.key.accountId]); + } + return subscriptions; + } + + public getTreeItem(): TreeItem | Promise { + let item = new TreeItem(this._label, TreeItemCollapsibleState.Collapsed); + item.id = this._id; + item.contextValue = AzureResourceItemType.account; + item.iconPath = { + dark: this.servicePool.contextService.getAbsolutePath('resources/dark/account_inverse.svg'), + light: this.servicePool.contextService.getAbsolutePath('resources/light/account.svg') + }; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: this._label, + isLeaf: false, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.account, + nodeSubType: undefined, + iconType: AzureResourceItemType.account + }; + } + + public get nodePathValue(): string { + return this._id; + } + + public get totalSubscriptionCount(): number { + return this._totalSubscriptionCount; + } + + public get selectedSubscriptionCount(): number { + return this._selectedSubscriptionCount; + } + + protected refreshLabel(): void { + const newLabel = this.generateLabel(); + if (this._label !== newLabel) { + this._label = newLabel; + this.treeChangeHandler.notifyNodeChanged(this); + } + } + + protected get cacheKey(): string { + return 'azureResource.cache.subscriptions'; + } + + private generateLabel(): string { + let label = `${this.account.displayInfo.displayName} (${this.account.key.accountId})`; + + if (this._totalSubscriptionCount !== 0) { + label += ` (${this._selectedSubscriptionCount} / ${this._totalSubscriptionCount} subscriptions)`; + } + + return label; + } + + private _id: string = undefined; + private _label: string = undefined; + private _totalSubscriptionCount = 0; + private _selectedSubscriptionCount = 0; + + private static readonly NoSubscriptions = localize('azureResource.tree.accountTreeNode.noSubscriptions', 'No Subscriptions found.'); +} + +interface AzureResourceSubscriptionsCache { + subscriptions: { [accountId: string]: AzureResourceSubscription[] }; +} diff --git a/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts b/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts new file mode 100644 index 0000000000..ecea876ec1 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/baseTreeNodes.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Account } from 'sqlops'; +import { ServiceClientCredentials } from 'ms-rest'; +import { TreeNode } from '../../treeNodes'; + +import { AzureResourceServicePool } from '../servicePool'; +import { AzureResourceCredentialError } from '../errors'; +import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; + +export abstract class AzureResourceTreeNodeBase extends TreeNode { + public constructor( + public readonly treeChangeHandler: IAzureResourceTreeChangeHandler, + parent: TreeNode + ) { + super(); + + this.parent = parent; + } + + public readonly servicePool = AzureResourceServicePool.getInstance(); +} + +export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTreeNodeBase { + public constructor( + public readonly account: Account, + treeChangeHandler: IAzureResourceTreeChangeHandler, + parent: TreeNode + ) { + super(treeChangeHandler, parent); + } + + public clearCache(): void { + this._isClearingCache = true; + } + + public get isClearingCache(): boolean { + return this._isClearingCache; + } + + protected async getCredentials(): Promise { + try { + return await this.servicePool.credentialService.getCredentials(this.account); + } catch (error) { + if (error instanceof AzureResourceCredentialError) { + this.servicePool.contextService.showErrorMessage(error.message); + + this.servicePool.contextService.executeCommand('azureresource.signin'); + } else { + throw error; + } + } + } + + protected updateCache(cache: T): void { + this.servicePool.cacheService.update(this.cacheKey, cache); + } + + protected getCache(): T { + return this.servicePool.cacheService.get(this.cacheKey); + } + + protected abstract get cacheKey(): string; + + protected _isClearingCache = true; +} diff --git a/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts new file mode 100644 index 0000000000..26d5f6d70c --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/databaseContainerTreeNode.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Account, NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; +import { AzureResourceItemType } from '../constants'; +import { AzureResourceErrorMessageUtil } from '../utils'; +import { AzureResourceDatabaseTreeNode } from './databaseTreeNode'; +import { AzureResourceMessageTreeNode } from './messageTreeNode'; +import { AzureResourceSubscription, AzureResourceDatabase } from '../models'; +import { IAzureResourceTreeChangeHandler } from './treeProvider'; + +export class AzureResourceDatabaseContainerTreeNode extends AzureResourceContainerTreeNodeBase { + public constructor( + public readonly subscription: AzureResourceSubscription, + account: Account, + treeChangeHandler: IAzureResourceTreeChangeHandler, + parent: TreeNode + ) { + super(account, treeChangeHandler, parent); + } + + public async getChildren(): Promise { + try { + let databases: AzureResourceDatabase[] = []; + + if (this._isClearingCache) { + let credentials = await this.getCredentials(); + databases = (await this.servicePool.databaseService.getDatabases(this.subscription, credentials)) || []; + + let cache = this.getCache(); + if (!cache) { + cache = { databases: { } }; + } + cache.databases[this.subscription.id] = databases; + this.updateCache(cache); + + this._isClearingCache = false; + } else { + const cache = this.getCache(); + if (cache) { + databases = cache.databases[this.subscription.id] || []; + } + } + + if (databases.length === 0) { + return [AzureResourceMessageTreeNode.create(AzureResourceDatabaseContainerTreeNode.NoDatabases, this)]; + } else { + return databases.map((database) => new AzureResourceDatabaseTreeNode(database, this.treeChangeHandler, this)); + } + } catch (error) { + return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)]; + } + } + + public getTreeItem(): TreeItem | Promise { + let item = new TreeItem(AzureResourceDatabaseContainerTreeNode.Label, TreeItemCollapsibleState.Collapsed); + item.contextValue = AzureResourceItemType.databaseContainer; + item.iconPath = { + dark: this.servicePool.contextService.getAbsolutePath('resources/dark/folder_inverse.svg'), + light: this.servicePool.contextService.getAbsolutePath('resources/light/folder.svg') + }; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: AzureResourceDatabaseContainerTreeNode.Label, + isLeaf: false, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.databaseContainer, + nodeSubType: undefined, + iconType: AzureResourceItemType.databaseContainer + }; + } + + public get nodePathValue(): string { + return 'databaseContainer'; + } + + protected get cacheKey(): string { + return 'azureResource.cache.databases'; + } + + private static readonly Label = localize('azureResource.tree.databaseContainerTreeNode.label', 'SQL Databases'); + private static readonly NoDatabases = localize('azureResource.tree.databaseContainerTreeNode.noDatabases', 'No SQL Databases found.'); +} + +interface AzureResourceDatabasesCache { + databases: { [subscriptionId: string]: AzureResourceDatabase[] }; +} diff --git a/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts new file mode 100644 index 0000000000..6f2921e092 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/databaseServerContainerTreeNode.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Account, NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; +import { AzureResourceItemType } from '../constants'; +import { AzureResourceMessageTreeNode } from './messageTreeNode'; +import { AzureResourceErrorMessageUtil } from '../utils'; +import { AzureResourceSubscription, AzureResourceDatabaseServer } from '../models'; +import { AzureResourceDatabaseServerTreeNode } from './databaseServerTreeNode'; +import { IAzureResourceTreeChangeHandler } from './treeProvider'; + +export class AzureResourceDatabaseServerContainerTreeNode extends AzureResourceContainerTreeNodeBase { + public constructor( + public readonly subscription: AzureResourceSubscription, + account: Account, + treeChangeHandler: IAzureResourceTreeChangeHandler, + parent: TreeNode + ) { + super(account, treeChangeHandler, parent); + } + + public async getChildren(): Promise { + try { + let databaseServers: AzureResourceDatabaseServer[] = []; + + if (this._isClearingCache) { + let credentials = await this.getCredentials(); + databaseServers = (await this.servicePool.databaseServerService.getDatabaseServers(this.subscription, credentials)) || []; + + let cache = this.getCache(); + if (!cache) { + cache = { databaseServers: { } }; + } + cache.databaseServers[this.subscription.id] = databaseServers; + this.updateCache(cache); + + this._isClearingCache = false; + } else { + const cache = this.getCache(); + if (cache) { + databaseServers = cache.databaseServers[this.subscription.id] || []; + } + } + + if (databaseServers.length === 0) { + return [AzureResourceMessageTreeNode.create(AzureResourceDatabaseServerContainerTreeNode.NoDatabaseServers, this)]; + } else { + return databaseServers.map((server) => new AzureResourceDatabaseServerTreeNode(server, this.treeChangeHandler, this)); + } + } catch (error) { + return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)]; + } + } + + public getTreeItem(): TreeItem | Promise { + let item = new TreeItem(AzureResourceDatabaseServerContainerTreeNode.Label, TreeItemCollapsibleState.Collapsed); + item.contextValue = AzureResourceItemType.databaseServerContainer; + item.iconPath = { + dark: this.servicePool.contextService.getAbsolutePath('resources/dark/folder_inverse.svg'), + light: this.servicePool.contextService.getAbsolutePath('resources/light/folder.svg') + }; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: AzureResourceDatabaseServerContainerTreeNode.Label, + isLeaf: false, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.databaseServerContainer, + nodeSubType: undefined, + iconType: AzureResourceItemType.databaseServerContainer + }; + } + + public get nodePathValue(): string { + return 'databaseServerContainer'; + } + + protected get cacheKey(): string { + return 'azureResource.cache.databaseServers'; + } + + private static readonly Label = localize('azureResource.tree.databaseServerContainerTreeNode.label', 'SQL Servers'); + private static readonly NoDatabaseServers = localize('azureResource.tree.databaseContainerTreeNode.noDatabaseServers', 'No SQL Servers found.'); +} + +interface AzureResourceDatabaseServersCache { + databaseServers: { [subscriptionId: string]: AzureResourceDatabaseServer[] }; +} diff --git a/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts new file mode 100644 index 0000000000..2c8cf5bd4b --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/databaseServerTreeNode.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; + +import { AzureResourceTreeNodeBase } from './baseTreeNodes'; +import { AzureResourceItemType } from '../constants'; +import { AzureResourceDatabaseServer } from '../models'; +import { IAzureResourceTreeChangeHandler } from './treeProvider'; + +export class AzureResourceDatabaseServerTreeNode extends AzureResourceTreeNodeBase { + public constructor( + public readonly databaseServer: AzureResourceDatabaseServer, + treeChangeHandler: IAzureResourceTreeChangeHandler, + parent: TreeNode + ) { + super(treeChangeHandler, parent); + } + + public async getChildren(): Promise { + return []; + } + + public getTreeItem(): TreeItem | Promise { + let item = new TreeItem(this.databaseServer.name, TreeItemCollapsibleState.None); + item.contextValue = AzureResourceItemType.databaseServer; + item.iconPath = { + dark: this.servicePool.contextService.getAbsolutePath('resources/dark/sql_server_inverse.svg'), + light: this.servicePool.contextService.getAbsolutePath('resources/light/sql_server.svg') + }; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: this.databaseServer.name, + isLeaf: true, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.databaseServer, + nodeSubType: undefined, + iconType: AzureResourceItemType.databaseServer + }; + } + + public get nodePathValue(): string { + return `databaseServer_${this.databaseServer.name}`; + } +} diff --git a/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts b/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts new file mode 100644 index 0000000000..1089a6e0c5 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/databaseTreeNode.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; + +import { AzureResourceTreeNodeBase } from './baseTreeNodes'; +import { AzureResourceItemType } from '../constants'; +import { AzureResourceDatabase } from '../models'; +import { IAzureResourceTreeChangeHandler } from './treeProvider'; + +export class AzureResourceDatabaseTreeNode extends AzureResourceTreeNodeBase { + public constructor( + public readonly database: AzureResourceDatabase, + treeChangeHandler: IAzureResourceTreeChangeHandler, + parent: TreeNode + ) { + super(treeChangeHandler, parent); + + this._label = `${this.database.name} (${this.database.serverName})`; + } + + public async getChildren(): Promise { + return []; + } + + public getTreeItem(): TreeItem | Promise { + let item = new TreeItem(this._label, TreeItemCollapsibleState.None); + item.contextValue = AzureResourceItemType.database; + item.iconPath = { + dark: this.servicePool.contextService.getAbsolutePath('resources/dark/sql_database_inverse.svg'), + light: this.servicePool.contextService.getAbsolutePath('resources/light/sql_database.svg') + }; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: this._label, + isLeaf: true, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.database, + nodeSubType: undefined, + iconType: AzureResourceItemType.database + }; + } + + public get nodePathValue(): string { + return `database_${this.database.name}`; + } + + private _label: string = undefined; +} diff --git a/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts b/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts new file mode 100644 index 0000000000..edea2e5a76 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/messageTreeNode.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; + +import { AzureResourceItemType } from '../constants'; + +export class AzureResourceMessageTreeNode extends TreeNode { + public constructor( + public readonly message: string, + parent: TreeNode + ) { + super(); + + this.parent = parent; + this._id = `message_${AzureResourceMessageTreeNode._messageNum++}`; + } + + public static create(message: string, parent: TreeNode): AzureResourceMessageTreeNode { + return new AzureResourceMessageTreeNode(message, parent); + } + + public getChildren(): TreeNode[] | Promise { + return []; + } + + public getTreeItem(): TreeItem | Promise { + let item = new TreeItem(this.message, TreeItemCollapsibleState.None); + item.contextValue = AzureResourceItemType.message; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: this.message, + isLeaf: true, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.message, + nodeSubType: undefined, + iconType: AzureResourceItemType.message + }; + } + + public get nodePathValue(): string { + return this._id; + } + + private _id: string; + + private static _messageNum: number = 0; +} diff --git a/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts b/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts new file mode 100644 index 0000000000..ff8474ca3f --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/subscriptionTreeNode.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Account, NodeInfo } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; + +import { AzureResourceTreeNodeBase, AzureResourceContainerTreeNodeBase } from './baseTreeNodes'; +import { AzureResourceItemType } from '../constants'; +import { AzureResourceDatabaseContainerTreeNode } from './databaseContainerTreeNode'; +import { AzureResourceDatabaseServerContainerTreeNode } from './databaseServerContainerTreeNode'; +import { AzureResourceSubscription } from '../models'; +import { IAzureResourceTreeChangeHandler } from './treeChangeHandler'; + +export class AzureResourceSubscriptionTreeNode extends AzureResourceTreeNodeBase { + public constructor( + public readonly subscription: AzureResourceSubscription, + account: Account, + treeChangeHandler: IAzureResourceTreeChangeHandler, + parent: TreeNode + ) { + super(treeChangeHandler, parent); + + this._children.push(new AzureResourceDatabaseContainerTreeNode(subscription, account, treeChangeHandler, this)); + this._children.push(new AzureResourceDatabaseServerContainerTreeNode(subscription, account, treeChangeHandler, this)); + } + + public async getChildren(): Promise { + return this._children; + } + + public getTreeItem(): TreeItem | Promise { + let item = new TreeItem(this.subscription.name, TreeItemCollapsibleState.Collapsed); + item.contextValue = AzureResourceItemType.subscription; + item.iconPath = { + dark: this.servicePool.contextService.getAbsolutePath('resources/dark/subscription_inverse.svg'), + light: this.servicePool.contextService.getAbsolutePath('resources/light/subscription.svg') + }; + return item; + } + + public getNodeInfo(): NodeInfo { + return { + label: this.subscription.name, + isLeaf: false, + errorMessage: undefined, + metadata: undefined, + nodePath: this.generateNodePath(), + nodeStatus: undefined, + nodeType: AzureResourceItemType.subscription, + nodeSubType: undefined, + iconType: AzureResourceItemType.subscription + }; + } + + public get nodePathValue(): string { + return `subscription_${this.subscription.id}`; + } + + private _children: AzureResourceContainerTreeNodeBase[] = []; +} diff --git a/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts b/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts new file mode 100644 index 0000000000..b7db2ed6b4 --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/treeChangeHandler.ts @@ -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 '../../treeNodes'; + +export interface IAzureResourceTreeChangeHandler { + notifyNodeChanged(node: TreeNode): void; +} diff --git a/extensions/azurecore/src/azureResource/tree/treeProvider.ts b/extensions/azurecore/src/azureResource/tree/treeProvider.ts new file mode 100644 index 0000000000..7cfd4ce89f --- /dev/null +++ b/extensions/azurecore/src/azureResource/tree/treeProvider.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TreeDataProvider, EventEmitter, Event, TreeItem } from 'vscode'; +import { DidChangeAccountsParams } from 'sqlops'; +import { TreeNode } from '../../treeNodes'; +import { setInterval, clearInterval } from 'timers'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { AzureResourceServicePool } from '../servicePool'; +import { AzureResourceAccountTreeNode } from './accountTreeNode'; +import { AzureResourceAccountNotSignedInTreeNode } from './accountNotSignedInTreeNode'; +import { AzureResourceMessageTreeNode } from './messageTreeNode'; +import { AzureResourceContainerTreeNodeBase, AzureResourceTreeNodeBase } from './baseTreeNodes'; +import { AzureResourceErrorMessageUtil } from '../utils'; + +export interface IAzureResourceTreeChangeHandler { + notifyNodeChanged(node: TreeNode): void; +} + +export class AzureResourceTreeProvider implements TreeDataProvider, IAzureResourceTreeChangeHandler { + public constructor() { + AzureResourceServicePool.getInstance().accountService.onDidChangeAccounts((e: DidChangeAccountsParams) => { this._onDidChangeTreeData.fire(undefined); }); + } + + public async getChildren(element?: TreeNode): Promise { + if (element) { + return element.getChildren(true); + } + + if (!this.isSystemInitialized) { + this._loadingTimer = setInterval(async () => { + try { + // Call sqlops.accounts.getAllAccounts() to determine whether the system has been initialized. + await AzureResourceServicePool.getInstance().accountService.getAccounts(); + + // System has been initialized + this.isSystemInitialized = true; + + if (this._loadingTimer) { + clearInterval(this._loadingTimer); + } + + this._onDidChangeTreeData.fire(undefined); + } catch (error) { + // System not initialized yet + this.isSystemInitialized = false; + } + }, AzureResourceTreeProvider.LoadingTimerInterval); + + return [AzureResourceMessageTreeNode.create(AzureResourceTreeProvider.Loading, undefined)]; + } + + try { + const accounts = await AzureResourceServicePool.getInstance().accountService.getAccounts(); + + if (accounts && accounts.length > 0) { + return accounts.map((account) => new AzureResourceAccountTreeNode(account, this)); + } else { + return [new AzureResourceAccountNotSignedInTreeNode()]; + } + } catch (error) { + return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), undefined)]; + } + } + + public get onDidChangeTreeData(): Event { + return this._onDidChangeTreeData.event; + } + + public notifyNodeChanged(node: TreeNode): void { + this._onDidChangeTreeData.fire(node); + } + + public async refresh(node: TreeNode, isClearingCache: boolean): Promise { + if (isClearingCache) { + if ((node instanceof AzureResourceContainerTreeNodeBase)) { + node.clearCache(); + } + } + + this._onDidChangeTreeData.fire(node); + } + + public getTreeItem(element: TreeNode): TreeItem | Thenable { + return element.getTreeItem(); + } + + public isSystemInitialized: boolean = false; + + private _loadingTimer: NodeJS.Timer = undefined; + private _onDidChangeTreeData = new EventEmitter(); + + private static readonly Loading = localize('azureResource.tree.treeProvider.loading', 'Loading ...'); + private static readonly LoadingTimerInterval = 5000; +} diff --git a/extensions/azurecore/src/azureResource/utils.ts b/extensions/azurecore/src/azureResource/utils.ts new file mode 100644 index 0000000000..99f177eafb --- /dev/null +++ b/extensions/azurecore/src/azureResource/utils.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export function getErrorMessage(error: Error | string): string { + return (error instanceof Error) ? error.message : error; +} + + +export class AzureResourceErrorMessageUtil { + public static getErrorMessage(error: Error | string): string { + return localize('azureResource.error', 'Error: {0}', getErrorMessage(error)); + } +} + +export function generateGuid(): string { + let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; + // c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx) + let oct: string = ''; + let tmp: number; + /* tslint:disable:no-bitwise */ + 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]; + } + + // 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively' + 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); + /* tslint:enable:no-bitwise */ +} \ No newline at end of file diff --git a/extensions/azurecore/src/constants.ts b/extensions/azurecore/src/constants.ts new file mode 100644 index 0000000000..994cfc1f6a --- /dev/null +++ b/extensions/azurecore/src/constants.ts @@ -0,0 +1,8 @@ +'use strict'; + +export const extensionConfigSectionName = 'azure'; +export const ViewType = 'view'; + +export enum BuiltInCommands { + SetContext = 'setContext' +} diff --git a/extensions/azurecore/src/controllers/controllerBase.ts b/extensions/azurecore/src/controllers/controllerBase.ts new file mode 100644 index 0000000000..15e31972b4 --- /dev/null +++ b/extensions/azurecore/src/controllers/controllerBase.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AppContext } from '../appContext'; +import { ApiWrapper } from '../apiWrapper'; + +export default abstract class ControllerBase implements vscode.Disposable { + + public constructor(protected appContext: AppContext) { + } + + protected get apiWrapper(): ApiWrapper { + return this.appContext.apiWrapper; + } + + public get extensionContext(): vscode.ExtensionContext { + return this.appContext && this.appContext.extensionContext; + } + + abstract activate(): Promise; + + abstract deactivate(): void; + + public dispose(): void { + this.deactivate(); + } +} + diff --git a/extensions/azurecore/src/controllers/mainController.ts b/extensions/azurecore/src/controllers/mainController.ts new file mode 100644 index 0000000000..7f38fd8667 --- /dev/null +++ b/extensions/azurecore/src/controllers/mainController.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * 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 ControllerBase from './controllerBase'; + +import { AzureResourceTreeProvider } from '../azureResource/tree/treeProvider'; +import { registerAzureResourceCommands } from '../azureResource/commands'; +import { AzureResourceServicePool } from '../azureResource/servicePool'; +import { AzureResourceCredentialService } from '../azureResource/services/credentialService'; +import { AzureResourceAccountService } from '../azureResource/services/accountService'; +import { AzureResourceSubscriptionService } from '../azureResource/services/subscriptionService'; +import { AzureResourceSubscriptionFilterService } from '../azureResource/services/subscriptionFilterService'; +import { AzureResourceDatabaseServerService } from '../azureResource/services/databaseServerService'; +import { AzureResourceDatabaseService } from '../azureResource/services/databaseService'; +import { AzureResourceCacheService } from '../azureResource/services/cacheService'; +import { AzureResourceContextService } from '../azureResource/services/contextService'; + +/** + * The main controller class that initializes the extension + */ +export default class MainController extends ControllerBase { + // PUBLIC METHODS ////////////////////////////////////////////////////// + /** + * Deactivates the extension + */ + public deactivate(): void { + } + + public activate(): Promise { + this.configureAzureResource(); + return Promise.resolve(true); + } + + private configureAzureResource(): void { + let servicePool = AzureResourceServicePool.getInstance(); + servicePool.cacheService = new AzureResourceCacheService(this.extensionContext); + servicePool.contextService = new AzureResourceContextService(this.extensionContext, this.apiWrapper); + servicePool.accountService = new AzureResourceAccountService(this.apiWrapper); + servicePool.credentialService = new AzureResourceCredentialService(this.apiWrapper); + servicePool.subscriptionService = new AzureResourceSubscriptionService(); + servicePool.subscriptionFilterService = new AzureResourceSubscriptionFilterService(new AzureResourceCacheService(this.extensionContext)); + servicePool.databaseService = new AzureResourceDatabaseService(); + servicePool.databaseServerService = new AzureResourceDatabaseServerService(); + + let azureResourceTree = new AzureResourceTreeProvider(); + this.extensionContext.subscriptions.push(this.apiWrapper.registerTreeDataProvider('azureResourceExplorer', azureResourceTree)); + + registerAzureResourceCommands(this.apiWrapper, azureResourceTree); + } +} diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts new file mode 100644 index 0000000000..9d693b3fec --- /dev/null +++ b/extensions/azurecore/src/extension.ts @@ -0,0 +1,40 @@ +'use strict'; + +import * as vscode from 'vscode'; + +import MainController from './controllers/mainController'; +import { AppContext } from './appContext'; +import ControllerBase from './controllers/controllerBase'; +import { ApiWrapper } from './apiWrapper'; + +let controllers: ControllerBase[] = []; + +// this method is called when your extension is activated +// your extension is activated the very first time the command is executed +export function activate(extensionContext: vscode.ExtensionContext) { + let appContext = new AppContext(extensionContext, new ApiWrapper()); + let activations: Promise[] = []; + + // Start the main controller + let mainController = new MainController(appContext); + controllers.push(mainController); + extensionContext.subscriptions.push(mainController); + activations.push(mainController.activate()); + + return Promise.all(activations) + .then((results: boolean[]) => { + for (let result of results) { + if (!result) { + return false; + } + } + return true; + }); +} + +// this method is called when your extension is deactivated +export function deactivate() { + for (let controller of controllers) { + controller.deactivate(); + } +} diff --git a/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts new file mode 100644 index 0000000000..9a3cb3f866 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/accountNotSignedInTreeNode.test.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import * as vscode from 'vscode'; +import 'mocha'; + +import { AzureResourceItemType } from '../../../azureResource/constants'; +import { AzureResourceAccountNotSignedInTreeNode } from '../../../azureResource/tree/accountNotSignedInTreeNode'; + +describe('AzureResourceAccountNotSignedInTreeNode.info', function(): void { + it('Should be correct.', async function(): Promise { + const label = 'Sign in to Azure ...'; + + const treeNode = new AzureResourceAccountNotSignedInTreeNode(); + + should(treeNode.nodePathValue).equal('message_accountNotSignedIn'); + + const treeItem = await treeNode.getTreeItem(); + should(treeItem.label).equal(label); + should(treeItem.contextValue).equal(AzureResourceItemType.message); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None); + should(treeItem.command).not.undefined(); + should(treeItem.command.title).equal(label); + should(treeItem.command.command).equal('azureresource.signin'); + + const nodeInfo = treeNode.getNodeInfo(); + should(nodeInfo.isLeaf).true(); + should(nodeInfo.label).equal(label); + should(nodeInfo.nodeType).equal(AzureResourceItemType.message); + should(nodeInfo.iconType).equal(AzureResourceItemType.message); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts new file mode 100644 index 0000000000..27b64e0160 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/accountTreeNode.test.ts @@ -0,0 +1,294 @@ +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import * as TypeMoq from 'typemoq'; +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; +import 'mocha'; +import { ServiceClientCredentials } from 'ms-rest'; + +import { AzureResourceServicePool } from '../../../azureResource/servicePool'; +import { + IAzureResourceCacheService, + IAzureResourceContextService, + IAzureResourceCredentialService, + IAzureResourceSubscriptionService, + IAzureResourceSubscriptionFilterService +} from '../../../azureResource/interfaces'; +import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler'; +import { AzureResourceAccountTreeNode } from '../../../azureResource/tree/accountTreeNode'; +import { AzureResourceSubscription } from '../../../azureResource/models'; +import { AzureResourceSubscriptionTreeNode } from '../../../azureResource/tree/subscriptionTreeNode'; +import { AzureResourceItemType } from '../../../azureResource/constants'; +import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode'; + +// Mock services +const mockServicePool = AzureResourceServicePool.getInstance(); + +let mockCacheService: TypeMoq.IMock; +let mockContextService: TypeMoq.IMock; +let mockCredentialService: TypeMoq.IMock; +let mockSubscriptionService: TypeMoq.IMock; +let mockSubscriptionFilterService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// Mock test data +const mockAccount: sqlops.Account = { + key: { + accountId: 'mock_account', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; + +const mockCredential = TypeMoq.Mock.ofType().object; +const mockCredentials = [mockCredential]; + +const mockSubscription1: AzureResourceSubscription = { + id: 'mock_subscription_1', + name: 'mock subscription 1' +}; +const mockSubscription2: AzureResourceSubscription = { + id: 'mock_subscription_2', + name: 'mock subscription 2' +}; +const mockSubscriptions = [mockSubscription1, mockSubscription2]; +const mockFilteredSubscriptions = [mockSubscription1]; + +let mockSubscriptionCache: { subscriptions: { [accountId: string]: AzureResourceSubscription[]} }; + +describe('AzureResourceAccountTreeNode.info', function(): void { + beforeEach(() => { + mockContextService = TypeMoq.Mock.ofType(); + mockCacheService = TypeMoq.Mock.ofType(); + mockCredentialService = TypeMoq.Mock.ofType(); + mockSubscriptionService = TypeMoq.Mock.ofType(); + mockSubscriptionFilterService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockSubscriptionCache = { subscriptions: {} }; + + mockServicePool.contextService = mockContextService.object; + mockServicePool.cacheService = mockCacheService.object; + mockServicePool.credentialService = mockCredentialService.object; + mockServicePool.subscriptionService = mockSubscriptionService.object; + mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object; + + mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials)); + mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache); + mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions); + }); + + it('Should be correct when created.', async function(): Promise { + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const accountTreeNodeId = `account_${mockAccount.key.accountId}`; + const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockAccount.key.accountId})`; + + should(accountTreeNode.nodePathValue).equal(accountTreeNodeId); + + const treeItem = await accountTreeNode.getTreeItem(); + should(treeItem.id).equal(accountTreeNodeId); + should(treeItem.label).equal(accountTreeNodeLabel); + should(treeItem.contextValue).equal(AzureResourceItemType.account); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); + + const nodeInfo = accountTreeNode.getNodeInfo(); + should(nodeInfo.label).equal(accountTreeNodeLabel); + should(nodeInfo.isLeaf).false(); + should(nodeInfo.nodeType).equal(AzureResourceItemType.account); + should(nodeInfo.iconType).equal(AzureResourceItemType.account); + }); + + it('Should be correct when there are subscriptions listed.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined)); + + const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockAccount.key.accountId}) (${mockSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`; + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + await accountTreeNode.getChildren(); + + const treeItem = await accountTreeNode.getTreeItem(); + should(treeItem.label).equal(accountTreeNodeLabel); + + const nodeInfo = accountTreeNode.getNodeInfo(); + should(nodeInfo.label).equal(accountTreeNodeLabel); + }); + + it('Should be correct when there are subscriptions filtered.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions)); + + const accountTreeNodeLabel = `${mockAccount.displayInfo.displayName} (${mockAccount.key.accountId}) (${mockFilteredSubscriptions.length} / ${mockSubscriptions.length} subscriptions)`; + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + await accountTreeNode.getChildren(); + + const treeItem = await accountTreeNode.getTreeItem(); + should(treeItem.label).equal(accountTreeNodeLabel); + + const nodeInfo = accountTreeNode.getNodeInfo(); + should(nodeInfo.label).equal(accountTreeNodeLabel); + }); +}); + +describe('AzureResourceAccountTreeNode.getChildren', function(): void { + beforeEach(() => { + mockCacheService = TypeMoq.Mock.ofType(); + mockCredentialService = TypeMoq.Mock.ofType(); + mockSubscriptionService = TypeMoq.Mock.ofType(); + mockSubscriptionFilterService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockSubscriptionCache = { subscriptions: {} }; + + mockServicePool.cacheService = mockCacheService.object; + mockServicePool.credentialService = mockCredentialService.object; + mockServicePool.subscriptionService = mockSubscriptionService.object; + mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object; + + mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials)); + mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache); + mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions); + }); + + it('Should load subscriptions from scratch and update cache when it is clearing cache.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined)); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const children = await accountTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once()); + mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once()); + + mockTreeChangeHandler.verify((o) => o.notifyNodeChanged(accountTreeNode), TypeMoq.Times.once()); + + should(accountTreeNode.totalSubscriptionCount).equal(mockSubscriptions.length); + should(accountTreeNode.selectedSubscriptionCount).equal(mockSubscriptions.length); + should(accountTreeNode.isClearingCache).false(); + + should(children).Array(); + should(children.length).equal(mockSubscriptions.length); + + should(Object.keys(mockSubscriptionCache.subscriptions)).deepEqual([mockAccount.key.accountId]); + should(mockSubscriptionCache.subscriptions[mockAccount.key.accountId]).deepEqual(mockSubscriptions); + + for (let ix = 0; ix < mockSubscriptions.length; ix++) { + const child = children[ix]; + const subscription = mockSubscriptions[ix]; + + should(child).instanceof(AzureResourceSubscriptionTreeNode); + should(child.nodePathValue).equal(`subscription_${subscription.id}`); + } + }); + + it('Should load subscriptions from cache when it is not clearing cache.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(undefined)); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + await accountTreeNode.getChildren(); + const children = await accountTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.exactly(1)); + mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.exactly(1)); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2)); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1)); + + should(children.length).equal(mockSubscriptionCache.subscriptions[mockAccount.key.accountId].length); + + for (let ix = 0; ix < mockSubscriptionCache.subscriptions[mockAccount.key.accountId].length; ix++) { + should(children[ix].nodePathValue).equal(`subscription_${mockSubscriptionCache.subscriptions[mockAccount.key.accountId][ix].id}`); + } + }); + + it('Should handle when there is no subscriptions.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(undefined)); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const children = await accountTreeNode.getChildren(); + + should(accountTreeNode.totalSubscriptionCount).equal(0); + + should(children).Array(); + should(children.length).equal(1); + should(children[0]).instanceof(AzureResourceMessageTreeNode); + should(children[0].nodePathValue).startWith('message_'); + should(children[0].getNodeInfo().label).equal('No Subscriptions found.'); + }); + + it('Should honor subscription filtering.', async function(): Promise { + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => Promise.resolve(mockSubscriptions)); + mockSubscriptionFilterService.setup((o) => o.getSelectedSubscriptions(mockAccount)).returns(() => Promise.resolve(mockFilteredSubscriptions)); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const children = await accountTreeNode.getChildren(); + + mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.once()); + + should(accountTreeNode.selectedSubscriptionCount).equal(mockFilteredSubscriptions.length); + should(children.length).equal(mockFilteredSubscriptions.length); + + for (let ix = 0; ix < mockFilteredSubscriptions.length; ix++) { + should(children[ix].nodePathValue).equal(`subscription_${mockFilteredSubscriptions[ix].id}`); + } + }); + + it('Should handle errors.', async function(): Promise { + const mockError = 'Test error'; + mockSubscriptionService.setup((o) => o.getSubscriptions(mockAccount, mockCredentials)).returns(() => { throw new Error(mockError); }); + + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + + const children = await accountTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once()); + mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never()); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + mockSubscriptionFilterService.verify((o) => o.getSelectedSubscriptions(mockAccount), TypeMoq.Times.never()); + + should(children).Array(); + should(children.length).equal(1); + should(children[0]).instanceof(AzureResourceMessageTreeNode); + should(children[0].nodePathValue).startWith('message_'); + should(children[0].getNodeInfo().label).equal(`Error: ${mockError}`); + }); +}); + +describe('AzureResourceAccountTreeNode.clearCache', function() : void { + beforeEach(() => { + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + }); + + it('Should clear cache.', async function(): Promise { + const accountTreeNode = new AzureResourceAccountTreeNode(mockAccount, mockTreeChangeHandler.object); + accountTreeNode.clearCache(); + should(accountTreeNode.isClearingCache).true(); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts new file mode 100644 index 0000000000..a2e8640265 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/databaseContainerTreeNode.test.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import * as TypeMoq from 'typemoq'; +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; +import 'mocha'; +import { ServiceClientCredentials } from 'ms-rest'; + +import { AzureResourceServicePool } from '../../../azureResource/servicePool'; +import { + IAzureResourceCacheService, + IAzureResourceContextService, + IAzureResourceCredentialService, + IAzureResourceDatabaseService +} from '../../../azureResource/interfaces'; +import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler'; +import { AzureResourceSubscription, AzureResourceDatabase } from '../../../azureResource/models'; +import { AzureResourceItemType } from '../../../azureResource/constants'; +import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode'; +import { AzureResourceDatabaseContainerTreeNode } from '../../../azureResource/tree/databaseContainerTreeNode'; +import { AzureResourceDatabaseTreeNode } from '../../../azureResource/tree/databaseTreeNode'; + +// Mock services +const mockServicePool = AzureResourceServicePool.getInstance(); + +let mockCacheService: TypeMoq.IMock; +let mockContextService: TypeMoq.IMock; +let mockCredentialService: TypeMoq.IMock; +let mockDatabaseService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// Mock test data +const mockAccount: sqlops.Account = { + key: { + accountId: 'mock_account', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; + +const mockCredential = TypeMoq.Mock.ofType().object; +const mockCredentials = [mockCredential]; + +const mockSubscription: AzureResourceSubscription = { + id: 'mock_subscription', + name: 'mock subscription' +}; + +const mockDatabase1: AzureResourceDatabase = { + name: 'mock database 1', + serverName: 'mock server 1', + serverFullName: 'mock server 1', + loginName: 'mock user 1' +}; +const mockDatabase2: AzureResourceDatabase = { + name: 'mock database 2', + serverName: 'mock server 2', + serverFullName: 'mock server 2', + loginName: 'mock user 2' +}; +const mockDatabases = [mockDatabase1, mockDatabase2]; + +let mockDatabaseContainerCache: { databases: { [subscriptionId: string]: AzureResourceDatabase[] } }; + +describe('AzureResourceDatabaseContainerTreeNode.info', function(): void { + beforeEach(() => { + mockContextService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct when created.', async function(): Promise { + const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + + const databaseContainerTreeNodeLabel = 'SQL Databases'; + + should(databaseContainerTreeNode.nodePathValue).equal('databaseContainer'); + + const treeItem = await databaseContainerTreeNode.getTreeItem(); + should(treeItem.label).equal(databaseContainerTreeNodeLabel); + should(treeItem.contextValue).equal(AzureResourceItemType.databaseContainer); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); + + const nodeInfo = databaseContainerTreeNode.getNodeInfo(); + should(nodeInfo.isLeaf).false(); + should(nodeInfo.label).equal(databaseContainerTreeNodeLabel); + should(nodeInfo.nodeType).equal(AzureResourceItemType.databaseContainer); + should(nodeInfo.iconType).equal(AzureResourceItemType.databaseContainer); + }); +}); + +describe('AzureResourceDatabaseContainerTreeNode.getChildren', function(): void { + beforeEach(() => { + mockCacheService = TypeMoq.Mock.ofType(); + mockCredentialService = TypeMoq.Mock.ofType(); + mockDatabaseService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockDatabaseContainerCache = { databases: {} }; + + mockServicePool.cacheService = mockCacheService.object; + mockServicePool.credentialService = mockCredentialService.object; + mockServicePool.databaseService = mockDatabaseService.object; + + mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials)); + mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockDatabaseContainerCache); + mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockDatabaseContainerCache.databases[mockSubscription.id] = mockDatabases); + }); + + it('Should load databases from scratch and update cache when it is clearing cache.', async function(): Promise { + mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, mockCredentials)).returns(() => Promise.resolve(mockDatabases)); + + const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + + const children = await databaseContainerTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once()); + mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + + should(databaseContainerTreeNode.isClearingCache).false(); + + should(children).Array(); + should(children.length).equal(mockDatabases.length); + + should(Object.keys(mockDatabaseContainerCache.databases)).deepEqual([mockSubscription.id]); + should(mockDatabaseContainerCache.databases[mockSubscription.id]).deepEqual(mockDatabases); + + for (let ix = 0; ix < mockDatabases.length; ix++) { + const child = children[ix]; + const database = mockDatabases[ix]; + + should(child).instanceof(AzureResourceDatabaseTreeNode); + should(child.nodePathValue).equal(`database_${database.name}`); + } + }); + + it('Should load databases from cache when it is not clearing cache.', async function(): Promise { + mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, mockCredentials)).returns(() => Promise.resolve(mockDatabases)); + + const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + + await databaseContainerTreeNode.getChildren(); + const children = await databaseContainerTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.exactly(1)); + mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.exactly(1)); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2)); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1)); + + should(children.length).equal(mockDatabaseContainerCache.databases[mockSubscription.id].length); + + for (let ix = 0; ix < mockDatabaseContainerCache.databases[mockSubscription.id].length; ix++) { + should(children[ix].nodePathValue).equal(`database_${mockDatabaseContainerCache.databases[mockSubscription.id][ix].name}`); + } + }); + + it('Should handle when there is no databases.', async function(): Promise { + mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, mockCredentials)).returns(() => Promise.resolve(undefined)); + + const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + + const children = await databaseContainerTreeNode.getChildren(); + + should(children).Array(); + should(children.length).equal(1); + should(children[0]).instanceof(AzureResourceMessageTreeNode); + should(children[0].nodePathValue).startWith('message_'); + should(children[0].getNodeInfo().label).equal('No SQL Databases found.'); + }); + + it('Should handle errors.', async function(): Promise { + const mockError = 'Test error'; + mockDatabaseService.setup((o) => o.getDatabases(mockSubscription, mockCredentials)).returns(() => { throw new Error(mockError); }); + + const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + const children = await databaseContainerTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once()); + mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never()); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + + should(children).Array(); + should(children.length).equal(1); + should(children[0]).instanceof(AzureResourceMessageTreeNode); + should(children[0].nodePathValue).startWith('message_'); + should(children[0].getNodeInfo().label).equal(`Error: ${mockError}`); + }); +}); + +describe('AzureResourceDatabaseContainerTreeNode.clearCache', function() : void { + beforeEach(() => { + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + }); + + it('Should clear cache.', async function(): Promise { + const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + databaseContainerTreeNode.clearCache(); + should(databaseContainerTreeNode.isClearingCache).true(); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts new file mode 100644 index 0000000000..5bbed2f812 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/databaseServerContainerTreeNode.test.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import * as TypeMoq from 'typemoq'; +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; +import 'mocha'; +import { ServiceClientCredentials } from 'ms-rest'; + +import { AzureResourceServicePool } from '../../../azureResource/servicePool'; +import { + IAzureResourceCacheService, + IAzureResourceContextService, + IAzureResourceCredentialService, + IAzureResourceDatabaseServerService +} from '../../../azureResource/interfaces'; +import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler'; +import { AzureResourceSubscription, AzureResourceDatabaseServer } from '../../../azureResource/models'; +import { AzureResourceItemType } from '../../../azureResource/constants'; +import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode'; +import { AzureResourceDatabaseServerContainerTreeNode } from '../../../azureResource/tree/databaseServerContainerTreeNode'; +import { AzureResourceDatabaseServerTreeNode } from '../../../azureResource/tree/databaseServerTreeNode'; + +// Mock services +const mockServicePool = AzureResourceServicePool.getInstance(); + +let mockCacheService: TypeMoq.IMock; +let mockContextService: TypeMoq.IMock; +let mockCredentialService: TypeMoq.IMock; +let mockDatabaseServerService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// Mock test data +const mockAccount: sqlops.Account = { + key: { + accountId: 'mock_account', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; + +const mockCredential = TypeMoq.Mock.ofType().object; +const mockCredentials = [mockCredential]; + +const mockSubscription: AzureResourceSubscription = { + id: 'mock_subscription', + name: 'mock subscription' +}; + +const mockDatabaseServer1: AzureResourceDatabaseServer = { + name: 'mock server 1', + fullName: 'mock server 1', + loginName: 'mock user 1', + defaultDatabaseName: 'master' +}; +const mockDatabaseServer2: AzureResourceDatabaseServer = { + name: 'mock server 2', + fullName: 'mock server 2', + loginName: 'mock user 2', + defaultDatabaseName: 'master' +}; +const mockDatabaseServers = [mockDatabaseServer1, mockDatabaseServer2]; + +let mockDatabaseServerContainerCache: { databaseServers: { [subscriptionId: string]: AzureResourceDatabaseServer[] } }; + +describe('AzureResourceDatabaseServerContainerTreeNode.info', function(): void { + beforeEach(() => { + mockContextService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct when created.', async function(): Promise { + const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + + const databaseServerContainerTreeNodeLabel = 'SQL Servers'; + + should(databaseServerContainerTreeNode.nodePathValue).equal('databaseServerContainer'); + + const treeItem = await databaseServerContainerTreeNode.getTreeItem(); + should(treeItem.label).equal(databaseServerContainerTreeNodeLabel); + should(treeItem.contextValue).equal(AzureResourceItemType.databaseServerContainer); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); + + const nodeInfo = databaseServerContainerTreeNode.getNodeInfo(); + should(nodeInfo.isLeaf).false(); + should(nodeInfo.label).equal(databaseServerContainerTreeNodeLabel); + should(nodeInfo.nodeType).equal(AzureResourceItemType.databaseServerContainer); + should(nodeInfo.iconType).equal(AzureResourceItemType.databaseServerContainer); + }); +}); + +describe('AzureResourceDatabaseServerContainerTreeNode.getChildren', function(): void { + beforeEach(() => { + mockCacheService = TypeMoq.Mock.ofType(); + mockCredentialService = TypeMoq.Mock.ofType(); + mockDatabaseServerService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockDatabaseServerContainerCache = { databaseServers: {} }; + + mockServicePool.cacheService = mockCacheService.object; + mockServicePool.credentialService = mockCredentialService.object; + mockServicePool.databaseServerService = mockDatabaseServerService.object; + + mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials)); + mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockDatabaseServerContainerCache); + mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockDatabaseServerContainerCache.databaseServers[mockSubscription.id] = mockDatabaseServers); + }); + + it('Should load database servers from scratch and update cache when it is clearing cache.', async function(): Promise { + mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, mockCredentials)).returns(() => Promise.resolve(mockDatabaseServers)); + + const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + + const children = await databaseServerContainerTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once()); + mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + + should(databaseServerContainerTreeNode.isClearingCache).false(); + + should(children).Array(); + should(children.length).equal(mockDatabaseServers.length); + + should(Object.keys(mockDatabaseServerContainerCache.databaseServers)).deepEqual([mockSubscription.id]); + should(mockDatabaseServerContainerCache.databaseServers[mockSubscription.id]).deepEqual(mockDatabaseServers); + + for (let ix = 0; ix < mockDatabaseServers.length; ix++) { + const child = children[ix]; + const databaseServer = mockDatabaseServers[ix]; + + should(child).instanceof(AzureResourceDatabaseServerTreeNode); + should(child.nodePathValue).equal(`databaseServer_${databaseServer.name}`); + } + }); + + it('Should load database servers from cache when it is not clearing cache.', async function(): Promise { + mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, mockCredentials)).returns(() => Promise.resolve(mockDatabaseServers)); + + const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + + await databaseServerContainerTreeNode.getChildren(); + const children = await databaseServerContainerTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.exactly(1)); + mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.exactly(1)); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2)); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1)); + + should(children.length).equal(mockDatabaseServerContainerCache.databaseServers[mockSubscription.id].length); + + for (let ix = 0; ix < mockDatabaseServerContainerCache.databaseServers[mockSubscription.id].length; ix++) { + should(children[ix].nodePathValue).equal(`databaseServer_${mockDatabaseServerContainerCache.databaseServers[mockSubscription.id][ix].name}`); + } + }); + + it('Should handle when there is no database servers.', async function(): Promise { + mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, mockCredentials)).returns(() => Promise.resolve(undefined)); + + const databaseContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + + const children = await databaseContainerTreeNode.getChildren(); + + should(children).Array(); + should(children.length).equal(1); + should(children[0]).instanceof(AzureResourceMessageTreeNode); + should(children[0].nodePathValue).startWith('message_'); + should(children[0].getNodeInfo().label).equal('No SQL Servers found.'); + }); + + it('Should handle errors.', async function(): Promise { + const mockError = 'Test error'; + mockDatabaseServerService.setup((o) => o.getDatabaseServers(mockSubscription, mockCredentials)).returns(() => { throw new Error(mockError); }); + + const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + const children = await databaseServerContainerTreeNode.getChildren(); + + mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once()); + mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.once()); + mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never()); + mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + + should(children).Array(); + should(children.length).equal(1); + should(children[0]).instanceof(AzureResourceMessageTreeNode); + should(children[0].nodePathValue).startWith('message_'); + should(children[0].getNodeInfo().label).equal(`Error: ${mockError}`); + }); +}); + +describe('AzureResourceDatabaseServerContainerTreeNode.clearCache', function() : void { + beforeEach(() => { + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + }); + + it('Should clear cache.', async function(): Promise { + const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + databaseServerContainerTreeNode.clearCache(); + should(databaseServerContainerTreeNode.isClearingCache).true(); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts new file mode 100644 index 0000000000..cef163b670 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/databaseServerTreeNode.test.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import * as TypeMoq from 'typemoq'; +import * as vscode from 'vscode'; +import 'mocha'; + +import { AzureResourceServicePool } from '../../../azureResource/servicePool'; +import { IAzureResourceContextService } from '../../../azureResource/interfaces'; +import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler'; +import { AzureResourceDatabaseServer } from '../../../azureResource/models'; +import { AzureResourceItemType } from '../../../azureResource/constants'; +import { AzureResourceDatabaseServerTreeNode } from '../../../azureResource/tree/databaseServerTreeNode'; + +// Mock services +const mockServicePool = AzureResourceServicePool.getInstance(); + +let mockContextService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// Mock test data +const mockDatabaseServer: AzureResourceDatabaseServer = { + name: 'mock database 1', + fullName: 'mock server 1', + loginName: 'mock user 1', + defaultDatabaseName: 'master' +}; + +describe('AzureResourceDatabaseServerTreeNode.info', function(): void { + beforeEach(() => { + mockContextService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct when created.', async function(): Promise { + const databaseServerTreeNode = new AzureResourceDatabaseServerTreeNode(mockDatabaseServer, mockTreeChangeHandler.object, undefined); + + const databaseServerTreeNodeLabel = mockDatabaseServer.name; + + should(databaseServerTreeNode.nodePathValue).equal(`databaseServer_${mockDatabaseServer.name}`); + + const treeItem = await databaseServerTreeNode.getTreeItem(); + should(treeItem.label).equal(databaseServerTreeNodeLabel); + should(treeItem.contextValue).equal(AzureResourceItemType.databaseServer); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None); + + const nodeInfo = databaseServerTreeNode.getNodeInfo(); + should(nodeInfo.isLeaf).true(); + should(nodeInfo.label).equal(databaseServerTreeNodeLabel); + should(nodeInfo.nodeType).equal(AzureResourceItemType.databaseServer); + should(nodeInfo.iconType).equal(AzureResourceItemType.databaseServer); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts new file mode 100644 index 0000000000..a1350771b8 --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/databaseTreeNode.test.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import * as TypeMoq from 'typemoq'; +import * as vscode from 'vscode'; +import 'mocha'; + +import { AzureResourceServicePool } from '../../../azureResource/servicePool'; +import { IAzureResourceContextService } from '../../../azureResource/interfaces'; +import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler'; +import { AzureResourceDatabase } from '../../../azureResource/models'; +import { AzureResourceItemType } from '../../../azureResource/constants'; +import { AzureResourceDatabaseTreeNode } from '../../../azureResource/tree/databaseTreeNode'; + +// Mock services +const mockServicePool = AzureResourceServicePool.getInstance(); + +let mockContextService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// Mock test data +const mockDatabase: AzureResourceDatabase = { + name: 'mock database 1', + serverName: 'mock server 1', + serverFullName: 'mock server 1', + loginName: 'mock user 1' +}; + +describe('AzureResourceDatabaseTreeNode.info', function(): void { + beforeEach(() => { + mockContextService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct.', async function(): Promise { + const databaseTreeNode = new AzureResourceDatabaseTreeNode(mockDatabase, mockTreeChangeHandler.object, undefined); + + const databaseTreeNodeLabel = `${mockDatabase.name} (${mockDatabase.serverName})`; + + should(databaseTreeNode.nodePathValue).equal(`database_${mockDatabase.name}`); + + const treeItem = await databaseTreeNode.getTreeItem(); + should(treeItem.label).equal(databaseTreeNodeLabel); + should(treeItem.contextValue).equal(AzureResourceItemType.database); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None); + + const nodeInfo = databaseTreeNode.getNodeInfo(); + should(nodeInfo.isLeaf).true(); + should(nodeInfo.label).equal(databaseTreeNodeLabel); + should(nodeInfo.nodeType).equal(AzureResourceItemType.database); + should(nodeInfo.iconType).equal(AzureResourceItemType.database); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts new file mode 100644 index 0000000000..ddd867c60f --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/messageTreeNode.test.ts @@ -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 should from 'should'; +import * as vscode from 'vscode'; +import 'mocha'; + +import { AzureResourceItemType } from '../../../azureResource/constants'; +import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode'; + +describe('AzureResourceMessageTreeNode.info', function(): void { + it('Should be correct when created.', async function(): Promise { + const mockMessage = 'Test messagse'; + const treeNode = new AzureResourceMessageTreeNode(mockMessage, undefined); + + should(treeNode.nodePathValue).startWith('message_'); + + const treeItem = await treeNode.getTreeItem(); + should(treeItem.label).equal(mockMessage); + should(treeItem.contextValue).equal(AzureResourceItemType.message); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.None); + + const nodeInfo = treeNode.getNodeInfo(); + should(nodeInfo.isLeaf).true(); + should(nodeInfo.label).equal(mockMessage); + should(nodeInfo.nodeType).equal(AzureResourceItemType.message); + should(nodeInfo.iconType).equal(AzureResourceItemType.message); + }); +}); + +describe('AzureResourceMessageTreeNode.create', function(): void { + it('Should create a message node.', async function(): Promise { + const mockMessage = 'Test messagse'; + const treeNode = AzureResourceMessageTreeNode.create(mockMessage, undefined); + should(treeNode).instanceof(AzureResourceMessageTreeNode); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts b/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts new file mode 100644 index 0000000000..e55621dfed --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/subscriptionTreeNode.test.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import * as TypeMoq from 'typemoq'; +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; +import 'mocha'; + +import { AzureResourceServicePool } from '../../../azureResource/servicePool'; +import { IAzureResourceContextService } from '../../../azureResource/interfaces'; +import { IAzureResourceTreeChangeHandler } from '../../../azureResource/tree/treeChangeHandler'; +import { AzureResourceSubscription } from '../../../azureResource/models'; +import { AzureResourceSubscriptionTreeNode } from '../../../azureResource/tree/subscriptionTreeNode'; +import { AzureResourceDatabaseContainerTreeNode } from '../../../azureResource/tree/databaseContainerTreeNode'; +import { AzureResourceDatabaseServerContainerTreeNode } from '../../../azureResource/tree/databaseServerContainerTreeNode'; +import { AzureResourceItemType } from '../../../azureResource/constants'; + +// Mock services +const mockServicePool = AzureResourceServicePool.getInstance(); + +let mockContextService: TypeMoq.IMock; + +let mockTreeChangeHandler: TypeMoq.IMock; + +// Mock test data +const mockAccount: sqlops.Account = { + key: { + accountId: 'mock_account', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; + +const mockSubscription: AzureResourceSubscription = { + id: 'mock_subscription', + name: 'mock subscription' +}; + +describe('AzureResourceSubscriptionTreeNode.info', function(): void { + beforeEach(() => { + mockContextService = TypeMoq.Mock.ofType(); + + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + + mockServicePool.contextService = mockContextService.object; + }); + + it('Should be correct when created.', async function(): Promise { + const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + + should(subscriptionTreeNode.nodePathValue).equal(`subscription_${mockSubscription.id}`); + + const treeItem = await subscriptionTreeNode.getTreeItem(); + should(treeItem.label).equal(mockSubscription.name); + should(treeItem.collapsibleState).equal(vscode.TreeItemCollapsibleState.Collapsed); + should(treeItem.contextValue).equal(AzureResourceItemType.subscription); + + const nodeInfo = subscriptionTreeNode.getNodeInfo(); + should(nodeInfo.label).equal(mockSubscription.name); + should(nodeInfo.isLeaf).equal(false); + should(nodeInfo.nodeType).equal(AzureResourceItemType.subscription); + should(nodeInfo.iconType).equal(AzureResourceItemType.subscription); + }); +}); + +describe('AzureResourceSubscriptionTreeNode.getChildren', function(): void { + beforeEach(() => { + mockTreeChangeHandler = TypeMoq.Mock.ofType(); + }); + + it('Should load database containers.', async function(): Promise { + const subscriptionTreeNode = new AzureResourceSubscriptionTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined); + const children = await subscriptionTreeNode.getChildren(); + + should(children).Array(); + should(children.length).equal(2); + should(children[0]).instanceof(AzureResourceDatabaseContainerTreeNode); + should(children[1]).instanceof(AzureResourceDatabaseServerContainerTreeNode); + }); +}); diff --git a/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts b/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts new file mode 100644 index 0000000000..f8cf33c84c --- /dev/null +++ b/extensions/azurecore/src/test/azureResource/tree/treeProvider.test.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * 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 should from 'should'; +import * as TypeMoq from 'typemoq'; +import * as sqlops from 'sqlops'; +import 'mocha'; + +import { AzureResourceServicePool } from '../../../azureResource/servicePool'; +import { IAzureResourceAccountService } from '../../../azureResource/interfaces'; +import { AzureResourceTreeProvider } from '../../../azureResource/tree/treeProvider'; +import { AzureResourceAccountTreeNode } from '../../../azureResource/tree/accountTreeNode'; +import { AzureResourceAccountNotSignedInTreeNode } from '../../../azureResource/tree/accountNotSignedInTreeNode'; +import { AzureResourceMessageTreeNode } from '../../../azureResource/tree/messageTreeNode'; + +// Mock services +const mockServicePool = AzureResourceServicePool.getInstance(); + +let mockAccountService: TypeMoq.IMock; + +// Mock test data +const mockAccount1: sqlops.Account = { + key: { + accountId: 'mock_account_1', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account_1@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; +const mockAccount2: sqlops.Account = { + key: { + accountId: 'mock_account_2', + providerId: 'mock_provider' + }, + displayInfo: { + displayName: 'mock_account_2@test.com', + accountType: 'Microsoft', + contextualDisplayName: 'test' + }, + properties: undefined, + isStale: false +}; +const mockAccounts = [mockAccount1, mockAccount2]; + +describe('AzureResourceTreeProvider.getChildren', function(): void { + beforeEach(() => { + mockAccountService = TypeMoq.Mock.ofType(); + + mockServicePool.accountService = mockAccountService.object; + }); + + it('Should load accounts.', async function(): Promise { + mockAccountService.setup((o) => o.getAccounts()).returns(() => Promise.resolve(mockAccounts)); + + const treeProvider = new AzureResourceTreeProvider(); + treeProvider.isSystemInitialized = true; + + const children = await treeProvider.getChildren(undefined); + + mockAccountService.verify((o) => o.getAccounts(), TypeMoq.Times.once()); + + should(children).Array(); + should(children.length).equal(mockAccounts.length); + + for (let ix = 0; ix < mockAccounts.length; ix++) { + const child = children[ix]; + const account = mockAccounts[ix]; + + should(child).instanceof(AzureResourceAccountTreeNode); + should(child.nodePathValue).equal(`account_${account.key.accountId}`); + } + }); + + it('Should handle when there is no accounts.', async function(): Promise { + mockAccountService.setup((o) => o.getAccounts()).returns(() => Promise.resolve(undefined)); + + const treeProvider = new AzureResourceTreeProvider(); + treeProvider.isSystemInitialized = true; + + const children = await treeProvider.getChildren(undefined); + + should(children).Array(); + should(children.length).equal(1); + should(children[0]).instanceof(AzureResourceAccountNotSignedInTreeNode); + }); + + it('Should handle errors.', async function(): Promise { + const mockAccountError = 'Test account error'; + mockAccountService.setup((o) => o.getAccounts()).returns(() => { throw new Error(mockAccountError); }); + + const treeProvider = new AzureResourceTreeProvider(); + treeProvider.isSystemInitialized = true; + + const children = await treeProvider.getChildren(undefined); + + mockAccountService.verify((o) => o.getAccounts(), TypeMoq.Times.once()); + + should(children).Array(); + should(children.length).equal(1); + should(children[0]).instanceof(AzureResourceMessageTreeNode); + should(children[0].nodePathValue).startWith('message_'); + should(children[0].getNodeInfo().label).equal(`Error: ${mockAccountError}`); + }); +}); diff --git a/extensions/azurecore/src/test/index.ts b/extensions/azurecore/src/test/index.ts new file mode 100644 index 0000000000..a9f0ffbd82 --- /dev/null +++ b/extensions/azurecore/src/test/index.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const path = require('path'); +const testRunner = require('vscode/lib/testrunner'); + +const suite = 'Integration Azure Tests'; + +const options: any = { + ui: 'bdd', + useColors: true, + timeout: 60000 +}; + +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + options.reporter = 'mocha-multi-reporters'; + options.reporterOptions = { + reporterEnabled: 'spec, mocha-junit-reporter', + mochaJunitReporterReporterOptions: { + testsuitesTitle: `${suite} ${process.platform}`, + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + } + }; +} + +testRunner.configure(options); + +export = testRunner; diff --git a/extensions/azurecore/src/treeNodes.ts b/extensions/azurecore/src/treeNodes.ts new file mode 100644 index 0000000000..7136ba98bd --- /dev/null +++ b/extensions/azurecore/src/treeNodes.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; + +type TreeNodePredicate = (node: TreeNode) => boolean; + +export abstract class TreeNode { + private _parent: TreeNode = undefined; + + public get parent(): TreeNode { + return this._parent; + } + + public set parent(node: TreeNode) { + this._parent = node; + } + + public generateNodePath(): string { + let path = undefined; + if (this.parent) { + path = this.parent.generateNodePath(); + } + path = path ? `${path}/${this.nodePathValue}` : this.nodePathValue; + return path; + } + + public findNodeByPath(path: string, expandIfNeeded: boolean = false): Promise { + let condition: TreeNodePredicate = (node: TreeNode) => node.getNodeInfo().nodePath === path; + let filter: TreeNodePredicate = (node: TreeNode) => path.startsWith(node.getNodeInfo().nodePath); + return TreeNode.findNode(this, condition, filter, true); + } + + public static async findNode(node: TreeNode, condition: TreeNodePredicate, filter: TreeNodePredicate, expandIfNeeded: boolean): Promise { + if (!node) { + return undefined; + } + + if (condition(node)) { + return node; + } + + let nodeInfo = node.getNodeInfo(); + if (nodeInfo.isLeaf) { + return undefined; + } + + // TODO support filtering by already expanded / not yet expanded + let children = await node.getChildren(false); + if (children) { + for (let child of children) { + if (filter && filter(child)) { + let childNode = await this.findNode(child, condition, filter, expandIfNeeded); + if (childNode) { + return childNode; + } + } + } + } + return undefined; + } + + /** + * The value to use for this node in the node path + */ + public abstract get nodePathValue(): string; + + abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise; + abstract getTreeItem(): vscode.TreeItem | Promise; + + abstract getNodeInfo(): sqlops.NodeInfo; +} diff --git a/extensions/azurecore/src/typings/ref.d.ts b/extensions/azurecore/src/typings/ref.d.ts new file mode 100644 index 0000000000..41e273db7f --- /dev/null +++ b/extensions/azurecore/src/typings/ref.d.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// +/// \ No newline at end of file diff --git a/extensions/azurecore/tsconfig.json b/extensions/azurecore/tsconfig.json new file mode 100644 index 0000000000..b341a65dab --- /dev/null +++ b/extensions/azurecore/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "./out", + "lib": [ + "es6", "es2015.promise" + ], + "typeRoots": [ + "./node_modules/@types" + ], + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "declaration": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/extensions/azurecore/yarn.lock b/extensions/azurecore/yarn.lock new file mode 100644 index 0000000000..8c2535a991 --- /dev/null +++ b/extensions/azurecore/yarn.lock @@ -0,0 +1,608 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/mocha@^5.2.5": + version "5.2.5" + resolved "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz#8a4accfc403c124a0bafe8a9fc61a05ec1032073" + +"@types/node@^8.0.47": + version "8.10.30" + resolved "https://registry.npmjs.org/@types/node/-/node-8.10.30.tgz#2c82cbed5f79d72280c131d2acffa88fbd8dd353" + +adal-node@^0.1.28: + version "0.1.28" + resolved "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz#468c4bb3ebbd96b1270669f4b9cba4e0065ea485" + dependencies: + "@types/node" "^8.0.47" + async ">=0.6.0" + date-utils "*" + jws "3.x.x" + request ">= 2.52.0" + underscore ">= 1.3.1" + uuid "^3.1.0" + xmldom ">= 0.1.x" + xpath.js "~1.1.0" + +ajv@^5.3.0: + version "5.5.2" + resolved "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +async@2.6.0: + version "2.6.0" + resolved "https://registry.npmjs.org/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" + dependencies: + lodash "^4.14.0" + +async@>=0.6.0: + version "2.6.1" + resolved "https://registry.npmjs.org/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" + dependencies: + lodash "^4.17.10" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + +azure-arm-resource@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/azure-arm-resource/-/azure-arm-resource-7.0.0.tgz#e76fe2195abe354b607346c2fa0f690544176294" + dependencies: + ms-rest "^2.3.3" + ms-rest-azure "^2.5.5" + +azure-arm-sql@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/azure-arm-sql/-/azure-arm-sql-5.0.1.tgz#75c0b115525d2270ab16122d47d0c666aca175d4" + dependencies: + ms-rest "^2.3.3" + ms-rest-azure "^2.5.5" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + dependencies: + tweetnacl "^0.14.3" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +combined-stream@1.0.6: + version "1.0.6" + resolved "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + dependencies: + delayed-stream "~1.0.0" + +combined-stream@~1.0.6: + version "1.0.7" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + dependencies: + delayed-stream "~1.0.0" + +commander@2.15.1: + version "2.15.1" + resolved "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-utils@*: + version "1.2.21" + resolved "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz#61fb16cdc1274b3c9acaaffe9fc69df8720a2b64" + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +diff@3.5.0: + version "3.5.0" + resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + +duplexer@^0.1.1: + version "0.1.1" + resolved "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + dependencies: + safe-buffer "^5.0.1" + +escape-string-regexp@1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob@7.1.2: + version "7.1.2" + resolved "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" + dependencies: + ajv "^5.3.0" + har-schema "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +he@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.3" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +is-buffer@^1.1.6: + version "1.1.6" + resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@3.x.x: + version "3.1.5" + resolved "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + +lodash@^4.14.0, lodash@^4.17.10, lodash@^4.17.4: + version "4.17.11" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + +mime-db@~1.36.0: + version "1.36.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.20" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" + dependencies: + mime-db "~1.36.0" + +minimatch@3.0.4, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mkdirp@0.5.1: + version "0.5.1" + resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mocha@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.5" + he "1.1.1" + minimatch "3.0.4" + mkdirp "0.5.1" + supports-color "5.4.0" + +moment@^2.21.0, moment@^2.22.2: + version "2.22.2" + resolved "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" + +ms-rest-azure@^2.5.5: + version "2.5.9" + resolved "https://registry.npmjs.org/ms-rest-azure/-/ms-rest-azure-2.5.9.tgz#8599943e349c91eb367d2d1dcb885017518dc712" + dependencies: + adal-node "^0.1.28" + async "2.6.0" + moment "^2.22.2" + ms-rest "^2.3.2" + request "^2.88.0" + uuid "^3.2.1" + +ms-rest@^2.3.2, ms-rest@^2.3.3: + version "2.3.7" + resolved "https://registry.npmjs.org/ms-rest/-/ms-rest-2.3.7.tgz#8bfc82fb91807643fcaa487c5fc9698cd18a018c" + dependencies: + duplexer "^0.1.1" + is-buffer "^1.1.6" + is-stream "^1.1.0" + moment "^2.21.0" + request "^2.88.0" + through "^2.3.8" + tunnel "0.0.5" + uuid "^3.2.1" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +postinstall-build@^5.0.1: + version "5.0.3" + resolved "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7" + +psl@^1.1.24: + version "1.1.29" + resolved "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + +"request@>= 2.52.0", request@^2.88.0: + version "2.88.0" + resolved "https://registry.npmjs.org/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + +should-equal@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" + dependencies: + should-type "^1.4.0" + +should-format@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1" + dependencies: + should-type "^1.3.0" + should-type-adaptors "^1.0.1" + +should-type-adaptors@^1.0.1: + version "1.1.0" + resolved "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a" + dependencies: + should-type "^1.3.0" + should-util "^1.0.0" + +should-type@^1.3.0, should-type@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3" + +should-util@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz#c98cda374aa6b190df8ba87c9889c2b4db620063" + +should@^13.2.1: + version "13.2.3" + resolved "https://registry.npmjs.org/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10" + dependencies: + should-equal "^2.0.0" + should-format "^3.0.3" + should-type "^1.4.0" + should-type-adaptors "^1.0.1" + should-util "^1.0.0" + +sshpk@^1.7.0: + version "1.14.2" + resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + safer-buffer "^2.0.2" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +supports-color@5.4.0: + version "5.4.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + dependencies: + has-flag "^3.0.0" + +through@^2.3.8: + version "2.3.8" + resolved "http://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tunnel@0.0.5: + version "0.0.5" + resolved "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz#d1532254749ed36620fcd1010865495a1fa9d0ae" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +typemoq@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8" + dependencies: + circular-json "^0.3.1" + lodash "^4.17.4" + postinstall-build "^5.0.1" + +"underscore@>= 1.3.1": + version "1.9.1" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" + +uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vscode-nls@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +"xmldom@>= 0.1.x": + version "0.1.27" + resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" + +xpath.js@~1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1" diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 929b92b9bc..4e613f5592 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -6,8 +6,11 @@ pushd %~dp0\.. set VSCODEUSERDATADIR=%TMP%\vscodeuserfolder-%RANDOM%-%TIME:~6,5% :: Tests in the extension host -call .\scripts\code.bat %~dp0\..\extensions\vscode-api-tests\testWorkspace --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% +:: TODO port over an re-enable API tests +:: call .\scripts\code.bat %~dp0\..\extensions\vscode-api-tests\testWorkspace --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% call .\scripts\code.bat %~dp0\..\extensions\vscode-colorize-tests\test --extensionDevelopmentPath=%~dp0\..\extensions\vscode-colorize-tests --extensionTestsPath=%~dp0\..\extensions\vscode-colorize-tests\out --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% +call .\scripts\code.bat %~dp0\..\extensions\markdown-language-features\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\markdown-language-features --extensionTestsPath=%~dp0\..\extensions\markdown-language-features\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% +call .\scripts\code.bat %~dp0\..\extensions\azure\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\azure --extensionTestsPath=%~dp0\..\extensions\azure\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% if %errorlevel% neq 0 exit /b %errorlevel% call .\scripts\code.bat $%~dp0\..\extensions\emmet\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% . diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index a48257ad4f..fba91607f9 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -13,9 +13,11 @@ fi cd $ROOT # Tests in the extension host -./scripts/code.sh $ROOT/extensions/vscode-api-tests/testWorkspace --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started +# TODO port over an re-enable API tests +# ./scripts/code.sh $ROOT/extensions/vscode-api-tests/testWorkspace --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started ./scripts/code.sh $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started ./scripts/code.sh $ROOT/extensions/markdown-language-features/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started +./scripts/code.sh $ROOT/extensions/azure/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/azure --extensionTestsPath=$ROOT/extensions/azure/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started mkdir $ROOT/extensions/emmet/test-fixtures ./scripts/code.sh $ROOT/extensions/emmet/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started . diff --git a/src/sql/parts/connection/common/connectionManagementService.ts b/src/sql/parts/connection/common/connectionManagementService.ts index af123e2d0c..80f466fa9e 100644 --- a/src/sql/parts/connection/common/connectionManagementService.ts +++ b/src/sql/parts/connection/common/connectionManagementService.ts @@ -1351,8 +1351,12 @@ export class ConnectionManagementService extends Disposable implements IConnecti * TODO this could be a map reduce operation */ public buildConnectionInfo(connectionString: string, provider: string): Thenable { - return this._providers.get(provider).onReady.then(e => { - return e.buildConnectionInfo(connectionString); - }); + let connectionProvider = this._providers.get(provider); + if (connectionProvider) { + return connectionProvider.onReady.then(e => { + return e.buildConnectionInfo(connectionString); + }); + } + return Promise.resolve(undefined); } } From 5a62035ed7ab1d3712ba39f14b669f06d3faf65d Mon Sep 17 00:00:00 2001 From: ranasaria <41588310+ranasaria@users.noreply.github.com> Date: Wed, 10 Oct 2018 11:24:13 -0700 Subject: [PATCH 07/11] Support to configure logging levels for sqltools services (#2731) * Adding support for configuring SqlTools log levels from user configuration. This also adds changes to see the tail of the sqltoolsservicelayer log file in the newly created 'Output->Log (SqlTools)' channel * Three new user settings control how logging happens. tracingLevel, logRetentionMinutes & logFilesRemovalLimit. Default tracingLevel is set to 'Critical'. * The logfiles include ui Extension host process id in their log file names. This ensures that filenames from multiple instances of Azure Data Studio running do not collide with each other. Furthermore log directory for being used for the tools service backend processes. This ensures that there is no name conflict when multiple instances of azuredatastudio are running on the same box. Also when azuredatastudio is started from vscode under debugger the log directory is set to %APPDATA%\Code\mssql while the official location is %APPDATA%\azuredatastudio\mssql. So dev environment should not affect other running instances. Kindly note that all debug runs of azuredatastudio share the same directory and all non debug runs share a directory different from those running under debugger. * Log files older than a week get cleaned up upon start-up. The log file cleanup behavior can be controlled at user level by logRetentionMinutes & logFilesRemovalLimit settings. --- .vscode/launch.json | 4 +- ThirdPartyNotices.txt | 1 + extensions/mssql/package.json | 26 ++++++- extensions/mssql/src/constants.ts | 3 +- .../src/credentialstore/credentialstore.ts | 7 +- extensions/mssql/src/main.ts | 13 +--- .../src/resourceProvider/resourceProvider.ts | 6 +- extensions/mssql/src/utils.ts | 72 ++++++++++++++++++- package.json | 1 + .../parts/logs/common/logConstants.ts | 4 +- .../electron-browser/logs.contribution.ts | 14 +++- .../logs/electron-browser/logsActions.ts | 4 +- .../electron-browser/extensionHost.ts | 5 ++ .../electron-browser/extensionService.ts | 13 ++++ yarn.lock | 23 ++++-- 15 files changed, 156 insertions(+), 40 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d8788d99cb..880b5c1ddb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -90,7 +90,7 @@ "**/winjs*.js" ], "webRoot": "${workspaceFolder}", - "timeout": 15000 + "timeout": 45000 }, { "type": "node", @@ -153,4 +153,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 7b4d414763..1ca7809193 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -21,6 +21,7 @@ expressly granted herein, whether by implication, estoppel or otherwise. error-ex: https://github.com/Qix-/node-error-ex escape-string-regexp: https://github.com/sindresorhus/escape-string-regexp fast-plist: https://github.com/Microsoft/node-fast-plist + find-remove: https://www.npmjs.com/package/find-remove fs-extra: https://github.com/jprichardson/node-fs-extra gc-signals: https://github.com/Microsoft/node-gc-signals getmac: https://github.com/bevry/getmac diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 7302c1915a..023e235259 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -93,7 +93,31 @@ "mssql.logDebugInfo": { "type": "boolean", "default": false, - "description": "[Optional] Log debug output to the VS Code console (Help -> Toggle Developer Tools)" + "description": "[Optional] Log debug output to the console (View -> Output) and then select appropriate output channel from the dropdown" + }, + "mssql.tracingLevel": { + "type": "string", + "description": "[Optional] Log level for backend services. Azure Data Studio generates a file name every time it starts and if the file already exists the logs entries are appended to that file. For cleanup of old log files see logRetentionMinutes and logFilesRemovalLimit settings. The default tracingLevel does not log much. Changing verbosity could lead to extensive logging and disk space requirements for the logs. Error includes Critical, Warning includes Error, Information includes Warning and Verbose includes Information", + "default": "Critical", + "enum": [ + "All", + "Off", + "Critical", + "Error", + "Warning", + "Information", + "Verbose" + ] + }, + "mssql.logRetentionMinutes": { + "type": "number", + "default": 10080, + "description": "Number of minutes to retain log files for backend services. Default is 1 week." + }, + "mssql.logFilesRemovalLimit": { + "type": "number", + "default": 100, + "description": "Maximum number of old files to remove upon startup that have expired mssql.logRetentionMinutes. Files that do not get cleaned up due to this limitation get cleaned up next time Azure Data Studio starts up." }, "ignorePlatformWarning": { "type": "boolean", diff --git a/extensions/mssql/src/constants.ts b/extensions/mssql/src/constants.ts index 0503be5bf1..1dc58ff589 100644 --- a/extensions/mssql/src/constants.ts +++ b/extensions/mssql/src/constants.ts @@ -9,5 +9,4 @@ export const providerId = 'MSSQL'; export const serviceCrashMessage = 'SQL Tools Service component exited unexpectedly. Please restart Azure Data Studio.'; export const serviceCrashButton = 'View Known Issues'; export const serviceCrashLink = 'https://github.com/Microsoft/vscode-mssql/wiki/SqlToolsService-Known-Issues'; -export const configLogDebugInfo = 'logDebugInfo'; -export const extensionConfigSectionName = 'mssql'; \ No newline at end of file +export const extensionConfigSectionName = 'mssql'; diff --git a/extensions/mssql/src/credentialstore/credentialstore.ts b/extensions/mssql/src/credentialstore/credentialstore.ts index 247c81f37d..b304551ae7 100644 --- a/extensions/mssql/src/credentialstore/credentialstore.ts +++ b/extensions/mssql/src/credentialstore/credentialstore.ts @@ -10,7 +10,6 @@ import { IConfig, ServerProvider } from 'service-downloader'; import { ServerOptions, RPCMessageType, ClientCapabilities, ServerCapabilities, TransportKind } from 'vscode-languageclient'; import { Disposable } from 'vscode'; import * as UUID from 'vscode-languageclient/lib/utils/uuid'; - import * as sqlops from 'sqlops'; import * as Contracts from './contracts'; @@ -100,11 +99,7 @@ export class CredentialStore { } private generateServerOptions(executablePath: string): ServerOptions { - let launchArgs = []; - launchArgs.push('--log-dir'); - let logFileLocation = path.join(Utils.getDefaultLogLocation(), 'mssql'); - launchArgs.push(logFileLocation); - + let launchArgs = Utils.getCommonLaunchArgsAndCleanupOldLogFiles('credentialstore', executablePath); return { command: executablePath, args: launchArgs, transport: TransportKind.stdio }; } } diff --git a/extensions/mssql/src/main.ts b/extensions/mssql/src/main.ts index 6261732942..401a6d3b15 100644 --- a/extensions/mssql/src/main.ts +++ b/extensions/mssql/src/main.ts @@ -97,18 +97,7 @@ export async function activate(context: vscode.ExtensionContext) { } function generateServerOptions(executablePath: string): ServerOptions { - let launchArgs = []; - launchArgs.push('--log-dir'); - let logFileLocation = path.join(Utils.getDefaultLogLocation(), 'mssql'); - launchArgs.push(logFileLocation); - let config = vscode.workspace.getConfiguration(Constants.extensionConfigSectionName); - if (config) { - let logDebugInfo = config[Constants.configLogDebugInfo]; - if (logDebugInfo) { - launchArgs.push('--enable-logging'); - } - } - + let launchArgs = Utils.getCommonLaunchArgsAndCleanupOldLogFiles('sqltools', executablePath); return { command: executablePath, args: launchArgs, transport: TransportKind.stdio }; } diff --git a/extensions/mssql/src/resourceProvider/resourceProvider.ts b/extensions/mssql/src/resourceProvider/resourceProvider.ts index fefe992dae..5cb23770a5 100644 --- a/extensions/mssql/src/resourceProvider/resourceProvider.ts +++ b/extensions/mssql/src/resourceProvider/resourceProvider.ts @@ -105,11 +105,7 @@ export class AzureResourceProvider { } private generateServerOptions(executablePath: string): ServerOptions { - let launchArgs = []; - launchArgs.push('--log-dir'); - let logFileLocation = path.join(Utils.getDefaultLogLocation(), 'mssql'); - launchArgs.push(logFileLocation); - + let launchArgs = Utils.getCommonLaunchArgsAndCleanupOldLogFiles('resourceprovider', executablePath); return { command: executablePath, args: launchArgs, transport: TransportKind.stdio }; } } diff --git a/extensions/mssql/src/utils.ts b/extensions/mssql/src/utils.ts index 0f4f8b932c..3ce734f3c3 100644 --- a/extensions/mssql/src/utils.ts +++ b/extensions/mssql/src/utils.ts @@ -7,6 +7,13 @@ import * as path from 'path'; import * as crypto from 'crypto'; import * as os from 'os'; +import {workspace, WorkspaceConfiguration} from 'vscode'; +import * as findRemoveSync from 'find-remove'; + +const configTracingLevel = 'tracingLevel'; +const configLogRetentionMinutes = 'logRetentionMinutes'; +const configLogFilesRemovalLimit = 'logFilesRemovalLimit'; +const extensionConfigSectionName = 'mssql'; // The function is a duplicate of \src\paths.js. IT would be better to import path.js but it doesn't // work for now because the extension is running in different process. @@ -20,8 +27,69 @@ export function getAppDataPath() { } } -export function getDefaultLogLocation() { - return path.join(getAppDataPath(), 'azuredatastudio'); +export function removeOldLogFiles(prefix: string) : JSON { + return findRemoveSync(getDefaultLogDir(), {prefix: `${prefix}_`, age: {seconds: getConfigLogRetentionSeconds()}, limit: getConfigLogFilesRemovalLimit()}); +} + +export function getConfiguration(config: string = extensionConfigSectionName) : WorkspaceConfiguration { + return workspace.getConfiguration(extensionConfigSectionName); +} + +export function getConfigLogFilesRemovalLimit() : number { + let config = getConfiguration(); + if (config) { + return Number((config[configLogFilesRemovalLimit]).toFixed(0)); + } + else + { + return undefined; + } +} + +export function getConfigLogRetentionSeconds() : number { + let config = getConfiguration(); + if (config) { + return Number((config[configLogRetentionMinutes] * 60).toFixed(0)); + } + else + { + return undefined; + } +} + +export function getConfigTracingLevel() : string { + let config = getConfiguration(); + if (config) { + return config[configTracingLevel]; + } + else + { + return undefined; + } +} + +export function getDefaultLogDir() : string { + return path.join(process.env['VSCODE_LOGS'], '..', '..','mssql'); +} + +export function getDefaultLogFile(prefix: string, pid: number) : string { + return path.join(getDefaultLogDir(), `${prefix}_${pid}.log`); +} + +export function getCommonLaunchArgsAndCleanupOldLogFiles(prefix: string, executablePath: string) : string [] { + let launchArgs = []; + launchArgs.push('--log-file'); + let logFile = getDefaultLogFile(prefix, process.pid); + launchArgs.push(logFile); + + console.log(`logFile for ${path.basename(executablePath)} is ${logFile}`); + console.log(`This process (ui Extenstion Host) is pid: ${process.pid}`); + // Delete old log files + let deletedLogFiles = removeOldLogFiles(prefix); + console.log(`Old log files deletion report: ${JSON.stringify(deletedLogFiles)}`); + launchArgs.push('--tracing-level'); + launchArgs.push(getConfigTracingLevel()); + return launchArgs; } export function ensure(target: object, key: string): any { diff --git a/package.json b/package.json index c79a535016..d9a5e85f50 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "applicationinsights": "0.18.0", "chart.js": "^2.6.0", "fast-plist": "0.1.2", + "find-remove": "1.2.1", "fs-extra": "^3.0.1", "gc-signals": "^0.0.1", "getmac": "1.4.1", diff --git a/src/vs/workbench/parts/logs/common/logConstants.ts b/src/vs/workbench/parts/logs/common/logConstants.ts index f75a511917..8302b6242a 100644 --- a/src/vs/workbench/parts/logs/common/logConstants.ts +++ b/src/vs/workbench/parts/logs/common/logConstants.ts @@ -6,4 +6,6 @@ export const mainLogChannelId = 'mainLog'; export const sharedLogChannelId = 'sharedLog'; export const rendererLogChannelId = 'rendererLog'; -export const extHostLogChannelId = 'extHostLog'; \ No newline at end of file +export const extHostLogChannelId = 'extHostLog'; +// {{SQL CARBON EDIT}} +export const sqlToolsLogChannellId = 'sqlToolsLog'; diff --git a/src/vs/workbench/parts/logs/electron-browser/logs.contribution.ts b/src/vs/workbench/parts/logs/electron-browser/logs.contribution.ts index c3a9afeee2..6259308590 100644 --- a/src/vs/workbench/parts/logs/electron-browser/logs.contribution.ts +++ b/src/vs/workbench/parts/logs/electron-browser/logs.contribution.ts @@ -18,14 +18,18 @@ import * as Constants from 'vs/workbench/parts/logs/common/logConstants'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { ShowLogsAction, OpenLogsFolderAction, SetLogLevelAction, OpenLogFileAction } from 'vs/workbench/parts/logs/electron-browser/logsActions'; - +// {{SQL CARBON EDIT}} +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionService } from 'vs/workbench/services/extensions/electron-browser/extensionService'; class LogOutputChannels extends Disposable implements IWorkbenchContribution { constructor( @IWindowService private windowService: IWindowService, @IEnvironmentService private environmentService: IEnvironmentService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + // {{SQL CARBON EDIT}} + @IExtensionService private extensionService: ExtensionService ) { super(); let outputChannelRegistry = Registry.as(OutputExt.OutputChannels); @@ -33,6 +37,12 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { outputChannelRegistry.registerChannel(Constants.sharedLogChannelId, nls.localize('sharedLog', "Log (Shared)"), URI.file(join(this.environmentService.logsPath, `sharedprocess.log`))); outputChannelRegistry.registerChannel(Constants.rendererLogChannelId, nls.localize('rendererLog', "Log (Window)"), URI.file(join(this.environmentService.logsPath, `renderer${this.windowService.getCurrentWindowId()}.log`))); outputChannelRegistry.registerChannel(Constants.extHostLogChannelId, nls.localize('extensionsLog', "Log (Extension Host)"), URI.file(join(this.environmentService.logsPath, `exthost${this.windowService.getCurrentWindowId()}.log`))); + // {{SQL CARBON EDIT}} + let extHostPid : number = extensionService.getExtenstionHostProcessId(); + console.log(`extensionHost process id is ${extHostPid}`); + let toolsServiceLogFile : string = join(this.environmentService.logsPath, '..', '..', 'mssql', `sqltools_${extHostPid}.log`); + console.log(`SqlTools Log file is: ${toolsServiceLogFile}`); + outputChannelRegistry.registerChannel(Constants.sqlToolsLogChannellId, nls.localize('sqlToolsLog', "Log (SqlTools)"), URI.file(toolsServiceLogFile)); const workbenchActionsRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); const devCategory = nls.localize('developer', "Developer"); diff --git a/src/vs/workbench/parts/logs/electron-browser/logsActions.ts b/src/vs/workbench/parts/logs/electron-browser/logsActions.ts index 88eb7bfc6e..bab378c18a 100644 --- a/src/vs/workbench/parts/logs/electron-browser/logsActions.ts +++ b/src/vs/workbench/parts/logs/electron-browser/logsActions.ts @@ -52,7 +52,9 @@ export class ShowLogsAction extends Action { { id: Constants.rendererLogChannelId, label: this.contextService.getWorkspace().name ? nls.localize('rendererProcess', "Window ({0})", this.contextService.getWorkspace().name) : nls.localize('emptyWindow', "Window") }, { id: Constants.extHostLogChannelId, label: nls.localize('extensionHost', "Extension Host") }, { id: Constants.sharedLogChannelId, label: nls.localize('sharedProcess', "Shared") }, - { id: Constants.mainLogChannelId, label: nls.localize('mainProcess', "Main") } + { id: Constants.mainLogChannelId, label: nls.localize('mainProcess', "Main") }, + // {{SQL CARBON EDIT}} + { id: Constants.sqlToolsLogChannellId, label: nls.localize('sqlToolsHost', "SqlTools") } ]; return this.quickOpenService.pick(entries, { placeHolder: nls.localize('selectProcess', "Select Log for Process") }) diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index bc9e359308..52b8e6f10e 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -101,6 +101,11 @@ export class ExtensionHostProcessWorker { })); } + // {{SQL CARBON EDIT}} + public getExtenstionHostProcess(): ChildProcess { + return this._extensionHostProcess; + } + public dispose(): void { this.terminate(); } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 24592aab8e..40e0ba7fa8 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -152,6 +152,11 @@ export class ExtensionHostProcessManager extends Disposable { }); } + // {{SQL CARBON EDIT}} + public getExtenstionHostProcessWorker(): ExtensionHostProcessWorker { + return this._extensionHostProcessWorker; + } + public dispose(): void { if (this._extensionHostProcessWorker) { this._extensionHostProcessWorker.dispose(); @@ -296,6 +301,14 @@ export class ExtensionService extends Disposable implements IExtensionService { } } + // {{SQL CARBON EDIT}} + public getExtenstionHostProcessId(): number { + if (this._extensionHostProcessManagers.length !== 1) + { + this._logOrShowMessage(Severity.Warning, 'Exactly one Extension Host Process Manager was expected'); + } + return this._extensionHostProcessManagers[0].getExtenstionHostProcessWorker().getExtenstionHostProcess().pid; + } private startDelayed(lifecycleService: ILifecycleService): void { let started = false; const startOnce = () => { diff --git a/yarn.lock b/yarn.lock index 4406bdfae4..6af0ff8e4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2159,6 +2159,13 @@ find-parent-dir@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" +find-remove@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/find-remove/-/find-remove-1.2.1.tgz#afd93400d23890e018ea197591e9d850d3d049a2" + dependencies: + fmerge "1.2.0" + rimraf "2.6.2" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -2225,6 +2232,10 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" +fmerge@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fmerge/-/fmerge-1.2.0.tgz#36e99d2ae255e3ee1af666b4df780553671cf692" + for-in@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.5.tgz#007374e2b6d5c67420a1479bdb75a04872b738c4" @@ -5892,15 +5903,15 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@^2.2.8: - version "2.6.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" +rimraf@2.6.2, rimraf@^2.4.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: glob "^7.0.5" -rimraf@^2.4.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" +rimraf@^2.2.8: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: glob "^7.0.5" From f8e7623f23c02d19d56e9525499a160f4d504f2d Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Wed, 10 Oct 2018 11:49:37 -0700 Subject: [PATCH 08/11] Bump minimatch node module (#2808) --- build/package.json | 1 + build/yarn.lock | 4 ++++ package.json | 2 +- yarn.lock | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/build/package.json b/build/package.json index 4042420fe1..2a6b648e81 100644 --- a/build/package.json +++ b/build/package.json @@ -7,6 +7,7 @@ "@types/es6-collections": "0.5.31", "@types/es6-promise": "0.0.33", "@types/mime": "0.0.29", + "@types/minimatch": "^3.0.3", "@types/node": "8.0.33", "@types/xml2js": "0.0.33", "@types/request": "^2.47.0", diff --git a/build/yarn.lock b/build/yarn.lock index d5667ff783..a95851227e 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -36,6 +36,10 @@ version "0.0.29" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-0.0.29.tgz#fbcfd330573b912ef59eeee14602bface630754b" +"@types/minimatch@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + "@types/node@*": version "8.0.51" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" diff --git a/package.json b/package.json index d9a5e85f50..b41104d765 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "jsdom-no-contextify": "^3.1.0", "lazy.js": "^0.4.2", "mime": "^1.4.1", - "minimatch": "^2.0.10", + "minimatch": "^3.0.4", "mkdirp": "^0.5.0", "mocha": "^2.2.5", "mocha-junit-reporter": "^1.17.0", diff --git a/yarn.lock b/yarn.lock index 6af0ff8e4f..05d55a5b10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4394,7 +4394,7 @@ minimatch@0.3: dependencies: brace-expansion "^1.1.7" -minimatch@2.x, minimatch@^2.0.1, minimatch@^2.0.10: +minimatch@2.x, minimatch@^2.0.1: version "2.0.10" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-2.0.10.tgz#8d087c39c6b38c001b97fca7ce6d0e1e80afbac7" dependencies: From 0adb0255737684f4e5ad8561f38de78eb3d11df2 Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Wed, 10 Oct 2018 15:44:30 -0700 Subject: [PATCH 09/11] add horizontal scroll to message pane (#2787) --- src/sql/parts/query/editor/messagePanel.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sql/parts/query/editor/messagePanel.ts b/src/sql/parts/query/editor/messagePanel.ts index 45e08b3b28..3f0f699255 100644 --- a/src/sql/parts/query/editor/messagePanel.ts +++ b/src/sql/parts/query/editor/messagePanel.ts @@ -30,6 +30,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditor } from 'vs/editor/common/editorCommon'; import { QueryInput } from 'sql/parts/query/common/queryInput'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; export interface IResultMessageIntern extends IResultMessage { id?: string; @@ -99,7 +100,7 @@ export class MessagePanel extends ViewletPanel { dataSource: this.ds, renderer: this.renderer, controller: this.controller - }, { keyboardSupport: false }); + }, { keyboardSupport: false, horizontalScrollMode: ScrollbarVisibility.Auto }); this.tree.onDidScroll(e => { if (this.state) { this.state.scrollPosition = this.tree.getScrollPosition(); From 5f1bde588569a953193f5e7d5efd15a80fe5d85d Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Thu, 11 Oct 2018 09:52:43 -0700 Subject: [PATCH 10/11] Merge azure account provider and azurecore extensions (#2810) --- extensions/account-provider-azure/.gitignore | 0 .../account-provider-azure/.vscodeignore | 3 - .../npm-shrinkwrap.json | 348 ---------- .../account-provider-azure/package.json | 55 -- .../account-provider-azure/package.nls.json | 7 - .../account-provider-azure/src/constants.ts | 12 - extensions/account-provider-azure/src/main.ts | 51 -- .../src/typings/ref.d.ts | 7 - .../account-provider-azure/tsconfig.json | 20 - extensions/account-provider-azure/yarn.lock | 608 ------------------ .../package.disabled.json | 0 extensions/azurecore/package.json | 38 +- extensions/azurecore/package.nls.json | 7 +- .../account-provider/azureAccountProvider.ts | 0 .../azureAccountProviderService.ts | 0 .../src/account-provider/interfaces.ts | 0 .../media/microsoft_account_dark.svg | 0 .../media/microsoft_account_light.svg | 0 .../media/work_school_account_dark.svg | 0 .../media/work_school_account_light.svg | 0 .../src/account-provider/providerSettings.ts | 0 .../src/account-provider/tokenCache.ts | 0 extensions/azurecore/src/constants.ts | 6 + extensions/azurecore/src/extension.ts | 43 +- extensions/azurecore/yarn.lock | 281 +++++++- 25 files changed, 349 insertions(+), 1137 deletions(-) delete mode 100644 extensions/account-provider-azure/.gitignore delete mode 100644 extensions/account-provider-azure/.vscodeignore delete mode 100644 extensions/account-provider-azure/npm-shrinkwrap.json delete mode 100644 extensions/account-provider-azure/package.json delete mode 100644 extensions/account-provider-azure/package.nls.json delete mode 100644 extensions/account-provider-azure/src/constants.ts delete mode 100644 extensions/account-provider-azure/src/main.ts delete mode 100644 extensions/account-provider-azure/src/typings/ref.d.ts delete mode 100644 extensions/account-provider-azure/tsconfig.json delete mode 100644 extensions/account-provider-azure/yarn.lock rename extensions/{account-provider-azure => azurecore}/package.disabled.json (100%) rename extensions/{account-provider-azure => azurecore}/src/account-provider/azureAccountProvider.ts (100%) rename extensions/{account-provider-azure => azurecore}/src/account-provider/azureAccountProviderService.ts (100%) rename extensions/{account-provider-azure => azurecore}/src/account-provider/interfaces.ts (100%) rename extensions/{account-provider-azure => azurecore}/src/account-provider/media/microsoft_account_dark.svg (100%) rename extensions/{account-provider-azure => azurecore}/src/account-provider/media/microsoft_account_light.svg (100%) rename extensions/{account-provider-azure => azurecore}/src/account-provider/media/work_school_account_dark.svg (100%) rename extensions/{account-provider-azure => azurecore}/src/account-provider/media/work_school_account_light.svg (100%) rename extensions/{account-provider-azure => azurecore}/src/account-provider/providerSettings.ts (100%) rename extensions/{account-provider-azure => azurecore}/src/account-provider/tokenCache.ts (100%) diff --git a/extensions/account-provider-azure/.gitignore b/extensions/account-provider-azure/.gitignore deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/extensions/account-provider-azure/.vscodeignore b/extensions/account-provider-azure/.vscodeignore deleted file mode 100644 index 8aea468642..0000000000 --- a/extensions/account-provider-azure/.vscodeignore +++ /dev/null @@ -1,3 +0,0 @@ -src/** -tsconfig.json -npm-shrinkwrap.json \ No newline at end of file diff --git a/extensions/account-provider-azure/npm-shrinkwrap.json b/extensions/account-provider-azure/npm-shrinkwrap.json deleted file mode 100644 index b727e66177..0000000000 --- a/extensions/account-provider-azure/npm-shrinkwrap.json +++ /dev/null @@ -1,348 +0,0 @@ -{ - "name": "account-provider-azure", - "version": "0.0.1", - "dependencies": { - "@types/node": { - "version": "8.5.1", - "from": "@types/node@>=8.0.47 <9.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.1.tgz" - }, - "adal-node": { - "version": "0.1.25", - "from": "adal-node@0.1.25", - "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.25.tgz" - }, - "ansi-regex": { - "version": "2.1.1", - "from": "ansi-regex@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" - }, - "ansi-styles": { - "version": "2.2.1", - "from": "ansi-styles@>=2.2.1 <3.0.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" - }, - "asn1": { - "version": "0.1.11", - "from": "asn1@0.1.11", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz" - }, - "assert-plus": { - "version": "0.1.5", - "from": "assert-plus@>=0.1.5 <0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz" - }, - "async": { - "version": "2.6.0", - "from": "async@>=0.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz" - }, - "aws-sign2": { - "version": "0.5.0", - "from": "aws-sign2@>=0.5.0 <0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz" - }, - "base64url": { - "version": "2.0.0", - "from": "base64url@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz" - }, - "bl": { - "version": "1.0.3", - "from": "bl@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz" - }, - "bluebird": { - "version": "2.11.0", - "from": "bluebird@>=2.9.30 <3.0.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz" - }, - "boom": { - "version": "2.10.1", - "from": "boom@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "from": "buffer-equal-constant-time@1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" - }, - "caseless": { - "version": "0.11.0", - "from": "caseless@>=0.11.0 <0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" - }, - "chalk": { - "version": "1.1.3", - "from": "chalk@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" - }, - "combined-stream": { - "version": "1.0.5", - "from": "combined-stream@>=1.0.1 <1.1.0", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz" - }, - "commander": { - "version": "2.12.2", - "from": "commander@>=2.8.1 <3.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz" - }, - "core-util-is": { - "version": "1.0.2", - "from": "core-util-is@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" - }, - "cryptiles": { - "version": "2.0.5", - "from": "cryptiles@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" - }, - "ctype": { - "version": "0.5.3", - "from": "ctype@0.5.3", - "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz" - }, - "date-utils": { - "version": "1.2.21", - "from": "date-utils@*", - "resolved": "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz" - }, - "delayed-stream": { - "version": "1.0.0", - "from": "delayed-stream@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - }, - "ecdsa-sig-formatter": { - "version": "1.0.9", - "from": "ecdsa-sig-formatter@1.0.9", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz" - }, - "escape-string-regexp": { - "version": "1.0.5", - "from": "escape-string-regexp@>=1.0.2 <2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - }, - "extend": { - "version": "3.0.1", - "from": "extend@>=3.0.0 <3.1.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz" - }, - "forever-agent": { - "version": "0.6.1", - "from": "forever-agent@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" - }, - "form-data": { - "version": "1.0.1", - "from": "form-data@>=1.0.0-rc1 <1.1.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz" - }, - "generate-function": { - "version": "2.0.0", - "from": "generate-function@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" - }, - "generate-object-property": { - "version": "1.2.0", - "from": "generate-object-property@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz" - }, - "har-validator": { - "version": "1.8.0", - "from": "har-validator@>=1.6.1 <2.0.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-1.8.0.tgz" - }, - "has-ansi": { - "version": "2.0.0", - "from": "has-ansi@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" - }, - "hawk": { - "version": "3.1.3", - "from": "hawk@>=3.1.0 <3.2.0", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz" - }, - "hoek": { - "version": "2.16.3", - "from": "hoek@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" - }, - "http-signature": { - "version": "0.11.0", - "from": "http-signature@>=0.11.0 <0.12.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.11.0.tgz" - }, - "inherits": { - "version": "2.0.3", - "from": "inherits@>=2.0.1 <2.1.0", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" - }, - "is-my-json-valid": { - "version": "2.17.1", - "from": "is-my-json-valid@>=2.12.0 <3.0.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz" - }, - "is-property": { - "version": "1.0.2", - "from": "is-property@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" - }, - "isarray": { - "version": "1.0.0", - "from": "isarray@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - }, - "isstream": { - "version": "0.1.2", - "from": "isstream@>=0.1.1 <0.2.0", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" - }, - "json-stringify-safe": { - "version": "5.0.1", - "from": "json-stringify-safe@>=5.0.0 <5.1.0", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" - }, - "jsonpointer": { - "version": "4.0.1", - "from": "jsonpointer@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz" - }, - "jwa": { - "version": "1.1.5", - "from": "jwa@>=1.1.4 <2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz" - }, - "jws": { - "version": "3.1.4", - "from": "jws@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz" - }, - "lodash": { - "version": "4.17.4", - "from": "lodash@>=4.14.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz" - }, - "mime-db": { - "version": "1.30.0", - "from": "mime-db@>=1.30.0 <1.31.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz" - }, - "mime-types": { - "version": "2.1.17", - "from": "mime-types@>=2.1.2 <2.2.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz" - }, - "oauth-sign": { - "version": "0.8.2", - "from": "oauth-sign@>=0.8.0 <0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz" - }, - "process-nextick-args": { - "version": "1.0.7", - "from": "process-nextick-args@>=1.0.6 <1.1.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" - }, - "punycode": { - "version": "1.4.1", - "from": "punycode@>=1.4.1 <2.0.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" - }, - "qs": { - "version": "5.1.0", - "from": "qs@>=5.1.0 <5.2.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-5.1.0.tgz" - }, - "readable-stream": { - "version": "2.0.6", - "from": "readable-stream@>=2.0.5 <2.1.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz" - }, - "request": { - "version": "2.63.0", - "from": "request@2.63.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.63.0.tgz", - "dependencies": { - "node-uuid": { - "version": "1.4.8", - "from": "node-uuid@>=1.4.0 <1.5.0", - "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz" - } - } - }, - "safe-buffer": { - "version": "5.1.1", - "from": "safe-buffer@>=5.0.1 <6.0.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" - }, - "sntp": { - "version": "1.0.9", - "from": "sntp@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" - }, - "string_decoder": { - "version": "0.10.31", - "from": "string_decoder@>=0.10.0 <0.11.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - }, - "stringstream": { - "version": "0.0.5", - "from": "stringstream@>=0.0.4 <0.1.0", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz" - }, - "strip-ansi": { - "version": "3.0.1", - "from": "strip-ansi@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - }, - "supports-color": { - "version": "2.0.0", - "from": "supports-color@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" - }, - "tough-cookie": { - "version": "2.3.3", - "from": "tough-cookie@>=0.12.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz" - }, - "tunnel-agent": { - "version": "0.4.3", - "from": "tunnel-agent@>=0.4.0 <0.5.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz" - }, - "underscore": { - "version": "1.8.3", - "from": "underscore@>=1.3.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz" - }, - "util-deprecate": { - "version": "1.0.2", - "from": "util-deprecate@>=1.0.1 <1.1.0", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - }, - "uuid": { - "version": "3.1.0", - "from": "uuid@>=3.1.0 <4.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz" - }, - "vscode-nls": { - "version": "2.0.2", - "from": "vscode-nls@2.0.2", - "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-2.0.2.tgz" - }, - "xmldom": { - "version": "0.1.27", - "from": "xmldom@>=0.1.0", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz" - }, - "xpath.js": { - "version": "1.0.7", - "from": "xpath.js@>=1.0.5 <1.1.0", - "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.0.7.tgz" - }, - "xtend": { - "version": "4.0.1", - "from": "xtend@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" - } - } -} diff --git a/extensions/account-provider-azure/package.json b/extensions/account-provider-azure/package.json deleted file mode 100644 index 5e015c5c18..0000000000 --- a/extensions/account-provider-azure/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "account-provider-azure", - "version": "0.0.1", - "publisher": "Microsoft", - "engines": { "vscode": "*" }, - "main": "./out/main", - "activationEvents": [ "*" ], - "scripts": { - "compile": "gulp compile-extension:account-provider-azure" - }, - "dependencies": { - "adal-node": "0.1.25", - "request": "2.63.0", - "vscode-nls": "^3.2.1" - }, - "devDependencies": { - "@types/node": "^8.0.24" - }, - "contributes": { - "commands": [ - { - "command": "accounts.clearTokenCache", - "title": "%accounts.clearTokenCache%", - "category": "Azure Accounts" - } - ], - "configuration": { - "type": "object", - "title": "Azure Account Configuration", - "properties": { - "accounts.azure.enablePublicCloud": { - "type": "boolean", - "default": true, - "description": "%config.enablePublicCloudDescription%" - } - } - }, - "account-type": [ - { - "id": "microsoft", - "icon": { - "light": "./out/account-provider/media/microsoft_account_light.svg", - "dark": "./out/account-provider/media/microsoft_account_dark.svg" - } - }, - { - "id": "work_school", - "icon": { - "light": "./out/account-provider/media/work_school_account_light.svg", - "dark": "./out/account-provider/media/work_school_account_dark.svg" - } - } - ] - } -} diff --git a/extensions/account-provider-azure/package.nls.json b/extensions/account-provider-azure/package.nls.json deleted file mode 100644 index b077c956dd..0000000000 --- a/extensions/account-provider-azure/package.nls.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "accounts.clearTokenCache": "Clear Azure Account Token Cache", - "config.enablePublicCloudDescription": "Should Azure public cloud integration be enabled", - "config.enableUsGovCloudDescription": "Should US Government Azure cloud (Fairfax) integration be enabled", - "config.enableChinaCloudDescription": "Should Azure China integration be enabled", - "config.enableGermanyCloudDescription": "Should Azure Germany integration be enabled" -} \ No newline at end of file diff --git a/extensions/account-provider-azure/src/constants.ts b/extensions/account-provider-azure/src/constants.ts deleted file mode 100644 index 8f6d985719..0000000000 --- a/extensions/account-provider-azure/src/constants.ts +++ /dev/null @@ -1,12 +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 nls from 'vscode-nls'; - -const localize = nls.loadMessageBundle(); - -export const extensionName = localize('extensionName', 'Azure Accounts'); diff --git a/extensions/account-provider-azure/src/main.ts b/extensions/account-provider-azure/src/main.ts deleted file mode 100644 index d20abf8d41..0000000000 --- a/extensions/account-provider-azure/src/main.ts +++ /dev/null @@ -1,51 +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 fs from 'fs'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import * as os from 'os'; - -import * as constants from './constants'; -import { AzureAccountProviderService } from './account-provider/azureAccountProviderService'; - -// The function is a duplicate of \src\paths.js. IT would be better to import path.js but it doesn't -// work for now because the extension is running in different process. -export function getAppDataPath() { - var platform = process.platform; - switch (platform) { - case 'win32': return process.env['APPDATA'] || path.join(process.env['USERPROFILE'], 'AppData', 'Roaming'); - case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support'); - case 'linux': return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config'); - default: throw new Error('Platform not supported'); - } -} - -export function getDefaultLogLocation() { - return path.join(getAppDataPath(), 'azuredatastudio'); -} - -// EXTENSION ACTIVATION //////////////////////////////////////////////////// -export function activate(context: vscode.ExtensionContext): void { - // Create the folder for storing the token caches - let storagePath = path.join(getDefaultLogLocation(), constants.extensionName); - try { - if (!fs.existsSync(storagePath)) { - fs.mkdirSync(storagePath); - console.log('Initialized Azure account extension storage.'); - } - } catch (e) { - console.error(`Initialization of Azure account extension storage failed: ${e}`); - console.error('Azure accounts will not be available'); - return; - } - - // Create the provider service and activate - const accountProviderService = new AzureAccountProviderService(context, storagePath); - context.subscriptions.push(accountProviderService); - accountProviderService.activate(); -} diff --git a/extensions/account-provider-azure/src/typings/ref.d.ts b/extensions/account-provider-azure/src/typings/ref.d.ts deleted file mode 100644 index 797c797001..0000000000 --- a/extensions/account-provider-azure/src/typings/ref.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/// -/// diff --git a/extensions/account-provider-azure/tsconfig.json b/extensions/account-provider-azure/tsconfig.json deleted file mode 100644 index be1e5ee9ce..0000000000 --- a/extensions/account-provider-azure/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compileOnSave": true, - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "outDir": "./out", - "lib": [ - "es6", "es2015.promise" - ], - "sourceMap": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "moduleResolution": "node", - "declaration": true, - "types": ["node"] - }, - "exclude": [ - "./node_modules" - ] -} diff --git a/extensions/account-provider-azure/yarn.lock b/extensions/account-provider-azure/yarn.lock deleted file mode 100644 index bc01d1e168..0000000000 --- a/extensions/account-provider-azure/yarn.lock +++ /dev/null @@ -1,608 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@types/node@^8.0.24", "@types/node@^8.0.47": - version "8.5.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.9.tgz#7155cfb4ae405bca4dd8df1a214c339e939109bf" - -adal-node@0.1.25: - version "0.1.25" - resolved "https://registry.yarnpkg.com/adal-node/-/adal-node-0.1.25.tgz#6554350ab42914870004c45c0d64448f3dbfcd03" - dependencies: - "@types/node" "^8.0.47" - async ">=0.6.0" - date-utils "*" - jws "3.x.x" - request ">= 2.52.0" - underscore ">= 1.3.1" - uuid "^3.1.0" - xmldom ">= 0.1.x" - xpath.js "~1.0.5" - -ajv@^5.1.0: - version "5.5.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" - dependencies: - co "^4.6.0" - fast-deep-equal "^1.0.0" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - -asn1@0.1.11: - version "0.1.11" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.1.11.tgz#559be18376d08a4ec4dbe80877d27818639b2df7" - -asn1@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - -assert-plus@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.1.5.tgz#ee74009413002d84cec7219c6ac811812e723160" - -async@>=0.6.0, async@^2.0.1: - version "2.6.0" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" - dependencies: - lodash "^4.14.0" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - -aws-sign2@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.5.0.tgz#c57103f7a17fc037f02d7c2e64b602ea223f7d63" - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - -aws4@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" - -base64url@2.0.0, base64url@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" - -bcrypt-pbkdf@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" - dependencies: - tweetnacl "^0.14.3" - -bl@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.0.3.tgz#fc5421a28fd4226036c3b3891a66a25bc64d226e" - dependencies: - readable-stream "~2.0.5" - -bluebird@^2.9.30: - version "2.11.0" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" - -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" - dependencies: - hoek "2.x.x" - -boom@4.x.x: - version "4.3.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" - dependencies: - hoek "4.x.x" - -boom@5.x.x: - version "5.2.0" - resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" - dependencies: - hoek "4.x.x" - -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - -caseless@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - -chalk@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - -combined-stream@^1.0.5, combined-stream@~1.0.1, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" - dependencies: - delayed-stream "~1.0.0" - -commander@^2.8.1: - version "2.13.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" - -core-util-is@1.0.2, core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" - dependencies: - boom "2.x.x" - -cryptiles@3.x.x: - version "3.1.2" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" - dependencies: - boom "5.x.x" - -ctype@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/ctype/-/ctype-0.5.3.tgz#82c18c2461f74114ef16c135224ad0b9144ca12f" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - dependencies: - assert-plus "^1.0.0" - -date-utils@*: - version "1.2.21" - resolved "https://registry.yarnpkg.com/date-utils/-/date-utils-1.2.21.tgz#61fb16cdc1274b3c9acaaffe9fc69df8720a2b64" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - -ecc-jsbn@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" - dependencies: - jsbn "~0.1.0" - -ecdsa-sig-formatter@1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" - dependencies: - base64url "^2.0.0" - safe-buffer "^5.0.1" - -escape-string-regexp@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -extend@~3.0.0, extend@~3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - -fast-deep-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" - -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - -forever-agent@~0.6.0, forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - -form-data@~1.0.0-rc1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.1.tgz#ae315db9a4907fa065502304a66d7733475ee37c" - dependencies: - async "^2.0.1" - combined-stream "^1.0.5" - mime-types "^2.1.11" - -form-data@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - -generate-function@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" - -generate-object-property@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" - dependencies: - is-property "^1.0.0" - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - dependencies: - assert-plus "^1.0.0" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - -har-validator@^1.6.1: - version "1.8.0" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-1.8.0.tgz#d83842b0eb4c435960aeb108a067a3aa94c0eeb2" - dependencies: - bluebird "^2.9.30" - chalk "^1.0.0" - commander "^2.8.1" - is-my-json-valid "^2.12.0" - -har-validator@~5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" - dependencies: - ajv "^5.1.0" - har-schema "^2.0.0" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - dependencies: - ansi-regex "^2.0.0" - -hawk@~3.1.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" - dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" - -hawk@~6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" - dependencies: - boom "4.x.x" - cryptiles "3.x.x" - hoek "4.x.x" - sntp "2.x.x" - -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" - -hoek@4.x.x: - version "4.2.0" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" - -http-signature@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-0.11.0.tgz#1796cf67a001ad5cd6849dca0991485f09089fe6" - dependencies: - asn1 "0.1.11" - assert-plus "^0.1.5" - ctype "0.5.3" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -inherits@~2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - -is-my-json-valid@^2.12.0: - version "2.17.1" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz#3da98914a70a22f0a8563ef1511a246c6fc55471" - dependencies: - generate-function "^2.0.0" - generate-object-property "^1.1.0" - jsonpointer "^4.0.0" - xtend "^4.0.0" - -is-property@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - -isstream@~0.1.1, isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - -json-schema-traverse@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - -json-stringify-safe@~5.0.0, json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - -jsonpointer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -jwa@^1.1.4: - version "1.1.5" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" - dependencies: - base64url "2.0.0" - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.9" - safe-buffer "^5.0.1" - -jws@3.x.x: - version "3.1.4" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" - dependencies: - base64url "^2.0.0" - jwa "^1.1.4" - safe-buffer "^5.0.1" - -lodash@^4.14.0: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" - -mime-db@~1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" - -mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.2: - version "2.1.17" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" - dependencies: - mime-db "~1.30.0" - -node-uuid@~1.4.0: - version "1.4.8" - resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" - -oauth-sign@~0.8.0, oauth-sign@~0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - -qs@~5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-5.1.0.tgz#4d932e5c7ea411cca76a312d39a606200fd50cd9" - -qs@~6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" - -readable-stream@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" - util-deprecate "~1.0.1" - -request@2.63.0: - version "2.63.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.63.0.tgz#c83e7c3485e5d9bf9b146318429bc48f1253d8be" - dependencies: - aws-sign2 "~0.5.0" - bl "~1.0.0" - caseless "~0.11.0" - combined-stream "~1.0.1" - extend "~3.0.0" - forever-agent "~0.6.0" - form-data "~1.0.0-rc1" - har-validator "^1.6.1" - hawk "~3.1.0" - http-signature "~0.11.0" - isstream "~0.1.1" - json-stringify-safe "~5.0.0" - mime-types "~2.1.2" - node-uuid "~1.4.0" - oauth-sign "~0.8.0" - qs "~5.1.0" - stringstream "~0.0.4" - tough-cookie ">=0.12.0" - tunnel-agent "~0.4.0" - -"request@>= 2.52.0": - version "2.83.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.6.0" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.1" - forever-agent "~0.6.1" - form-data "~2.3.1" - har-validator "~5.0.3" - hawk "~6.0.2" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.17" - oauth-sign "~0.8.2" - performance-now "^2.1.0" - qs "~6.5.1" - safe-buffer "^5.1.1" - stringstream "~0.0.5" - tough-cookie "~2.3.3" - tunnel-agent "^0.6.0" - uuid "^3.1.0" - -safe-buffer@^5.0.1, safe-buffer@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" - -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" - dependencies: - hoek "2.x.x" - -sntp@2.x.x: - version "2.1.0" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" - dependencies: - hoek "4.x.x" - -sshpk@^1.7.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - optionalDependencies: - bcrypt-pbkdf "^1.0.0" - ecc-jsbn "~0.1.1" - jsbn "~0.1.0" - tweetnacl "~0.14.0" - -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - -stringstream@~0.0.4, stringstream@~0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - dependencies: - ansi-regex "^2.0.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - -tough-cookie@>=0.12.0, tough-cookie@~2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" - dependencies: - punycode "^1.4.1" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - dependencies: - safe-buffer "^5.0.1" - -tunnel-agent@~0.4.0: - version "0.4.3" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - -"underscore@>= 1.3.1": - version "1.8.3" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" - -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - -uuid@^3.1.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -vscode-nls@^3.2.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.2.tgz#3817eca5b985c2393de325197cf4e15eb2aa5350" - -"xmldom@>= 0.1.x": - version "0.1.27" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" - -xpath.js@~1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/xpath.js/-/xpath.js-1.0.7.tgz#7e94627f541276cbc6a6b02b5d35e9418565b3e4" - -xtend@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" diff --git a/extensions/account-provider-azure/package.disabled.json b/extensions/azurecore/package.disabled.json similarity index 100% rename from extensions/account-provider-azure/package.disabled.json rename to extensions/azurecore/package.disabled.json diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json index d490feffbb..1e12bda3d0 100644 --- a/extensions/azurecore/package.json +++ b/extensions/azurecore/package.json @@ -10,7 +10,7 @@ "sqlops": "*" }, "activationEvents": [ - "onView:azureResourceExplorer" + "*" ], "main": "./out/extension", "contributes": { @@ -25,9 +25,41 @@ "description": "%azure.resourceFilter.description%" } } + }, + { + "type": "object", + "title": "Azure Account Configuration", + "properties": { + "accounts.azure.enablePublicCloud": { + "type": "boolean", + "default": true, + "description": "%config.enablePublicCloudDescription%" + } + } + } + ], + "account-type": [ + { + "id": "microsoft", + "icon": { + "light": "./out/account-provider/media/microsoft_account_light.svg", + "dark": "./out/account-provider/media/microsoft_account_dark.svg" + } + }, + { + "id": "work_school", + "icon": { + "light": "./out/account-provider/media/work_school_account_light.svg", + "dark": "./out/account-provider/media/work_school_account_dark.svg" + } } ], "commands": [ + { + "command": "accounts.clearTokenCache", + "title": "%accounts.clearTokenCache%", + "category": "Azure Accounts" + }, { "command": "azureresource.refreshall", "title": "%azureresource.refreshall%", @@ -115,14 +147,16 @@ } }, "dependencies": { + "request": "2.63.0", "azure-arm-resource": "^7.0.0", "azure-arm-sql": "^5.0.1", "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" } -} +} \ No newline at end of file diff --git a/extensions/azurecore/package.nls.json b/extensions/azurecore/package.nls.json index 5b3abedb0b..5af2a7968d 100644 --- a/extensions/azurecore/package.nls.json +++ b/extensions/azurecore/package.nls.json @@ -9,5 +9,10 @@ "azureresource.connectsqldb": "Connect", "azureresource.selectsubscriptions": "Select Subscriptions", "azure.title": "Azure", - "azure.resourceExplorer.title": "Resource Explorer" + "azure.resourceExplorer.title": "Resource Explorer", + "accounts.clearTokenCache": "Clear Azure Account Token Cache", + "config.enablePublicCloudDescription": "Should Azure public cloud integration be enabled", + "config.enableUsGovCloudDescription": "Should US Government Azure cloud (Fairfax) integration be enabled", + "config.enableChinaCloudDescription": "Should Azure China integration be enabled", + "config.enableGermanyCloudDescription": "Should Azure Germany integration be enabled" } \ No newline at end of file diff --git a/extensions/account-provider-azure/src/account-provider/azureAccountProvider.ts b/extensions/azurecore/src/account-provider/azureAccountProvider.ts similarity index 100% rename from extensions/account-provider-azure/src/account-provider/azureAccountProvider.ts rename to extensions/azurecore/src/account-provider/azureAccountProvider.ts diff --git a/extensions/account-provider-azure/src/account-provider/azureAccountProviderService.ts b/extensions/azurecore/src/account-provider/azureAccountProviderService.ts similarity index 100% rename from extensions/account-provider-azure/src/account-provider/azureAccountProviderService.ts rename to extensions/azurecore/src/account-provider/azureAccountProviderService.ts diff --git a/extensions/account-provider-azure/src/account-provider/interfaces.ts b/extensions/azurecore/src/account-provider/interfaces.ts similarity index 100% rename from extensions/account-provider-azure/src/account-provider/interfaces.ts rename to extensions/azurecore/src/account-provider/interfaces.ts diff --git a/extensions/account-provider-azure/src/account-provider/media/microsoft_account_dark.svg b/extensions/azurecore/src/account-provider/media/microsoft_account_dark.svg similarity index 100% rename from extensions/account-provider-azure/src/account-provider/media/microsoft_account_dark.svg rename to extensions/azurecore/src/account-provider/media/microsoft_account_dark.svg diff --git a/extensions/account-provider-azure/src/account-provider/media/microsoft_account_light.svg b/extensions/azurecore/src/account-provider/media/microsoft_account_light.svg similarity index 100% rename from extensions/account-provider-azure/src/account-provider/media/microsoft_account_light.svg rename to extensions/azurecore/src/account-provider/media/microsoft_account_light.svg diff --git a/extensions/account-provider-azure/src/account-provider/media/work_school_account_dark.svg b/extensions/azurecore/src/account-provider/media/work_school_account_dark.svg similarity index 100% rename from extensions/account-provider-azure/src/account-provider/media/work_school_account_dark.svg rename to extensions/azurecore/src/account-provider/media/work_school_account_dark.svg diff --git a/extensions/account-provider-azure/src/account-provider/media/work_school_account_light.svg b/extensions/azurecore/src/account-provider/media/work_school_account_light.svg similarity index 100% rename from extensions/account-provider-azure/src/account-provider/media/work_school_account_light.svg rename to extensions/azurecore/src/account-provider/media/work_school_account_light.svg diff --git a/extensions/account-provider-azure/src/account-provider/providerSettings.ts b/extensions/azurecore/src/account-provider/providerSettings.ts similarity index 100% rename from extensions/account-provider-azure/src/account-provider/providerSettings.ts rename to extensions/azurecore/src/account-provider/providerSettings.ts diff --git a/extensions/account-provider-azure/src/account-provider/tokenCache.ts b/extensions/azurecore/src/account-provider/tokenCache.ts similarity index 100% rename from extensions/account-provider-azure/src/account-provider/tokenCache.ts rename to extensions/azurecore/src/account-provider/tokenCache.ts diff --git a/extensions/azurecore/src/constants.ts b/extensions/azurecore/src/constants.ts index 994cfc1f6a..591a359587 100644 --- a/extensions/azurecore/src/constants.ts +++ b/extensions/azurecore/src/constants.ts @@ -1,8 +1,14 @@ 'use strict'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + export const extensionConfigSectionName = 'azure'; export const ViewType = 'view'; export enum BuiltInCommands { SetContext = 'setContext' } + +export const extensionName = localize('extensionName', 'Azure Accounts'); diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts index 9d693b3fec..a2e4463648 100644 --- a/extensions/azurecore/src/extension.ts +++ b/extensions/azurecore/src/extension.ts @@ -1,19 +1,60 @@ 'use strict'; import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import * as constants from './constants'; import MainController from './controllers/mainController'; import { AppContext } from './appContext'; import ControllerBase from './controllers/controllerBase'; import { ApiWrapper } from './apiWrapper'; +import { AzureAccountProviderService } from './account-provider/azureAccountProviderService'; let controllers: ControllerBase[] = []; + +// The function is a duplicate of \src\paths.js. IT would be better to import path.js but it doesn't +// work for now because the extension is running in different process. +export function getAppDataPath() { + var platform = process.platform; + switch (platform) { + case 'win32': return process.env['APPDATA'] || path.join(process.env['USERPROFILE'], 'AppData', 'Roaming'); + case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support'); + case 'linux': return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config'); + default: throw new Error('Platform not supported'); + } +} + +export function getDefaultLogLocation() { + return path.join(getAppDataPath(), 'azuredatastudio'); +} + + // this method is called when your extension is activated // your extension is activated the very first time the command is executed export function activate(extensionContext: vscode.ExtensionContext) { let appContext = new AppContext(extensionContext, new ApiWrapper()); - let activations: Promise[] = []; + let activations: Thenable[] = []; + + // Create the folder for storing the token caches + let storagePath = path.join(getDefaultLogLocation(), constants.extensionName); + try { + if (!fs.existsSync(storagePath)) { + fs.mkdirSync(storagePath); + console.log('Initialized Azure account extension storage.'); + } + } catch (e) { + console.error(`Initialization of Azure account extension storage failed: ${e}`); + console.error('Azure accounts will not be available'); + return; + } + + // Create the provider service and activate + const accountProviderService = new AzureAccountProviderService(extensionContext, storagePath); + extensionContext.subscriptions.push(accountProviderService); + accountProviderService.activate(); // Start the main controller let mainController = new MainController(appContext); diff --git a/extensions/azurecore/yarn.lock b/extensions/azurecore/yarn.lock index 8c2535a991..0d93c417a6 100644 --- a/extensions/azurecore/yarn.lock +++ b/extensions/azurecore/yarn.lock @@ -6,6 +6,10 @@ version "5.2.5" resolved "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz#8a4accfc403c124a0bafe8a9fc61a05ec1032073" +"@types/node@^8.0.24": + version "8.10.36" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.36.tgz#eac05d576fbcd0b4ea3c912dc58c20475c08d9e4" + "@types/node@^8.0.47": version "8.10.30" resolved "https://registry.npmjs.org/@types/node/-/node-8.10.30.tgz#2c82cbed5f79d72280c131d2acffa88fbd8dd353" @@ -33,6 +37,18 @@ ajv@^5.3.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +asn1@0.1.11: + version "0.1.11" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.1.11.tgz#559be18376d08a4ec4dbe80877d27818639b2df7" + asn1@~0.2.3: version "0.2.4" resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -43,15 +59,19 @@ assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" +assert-plus@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.1.5.tgz#ee74009413002d84cec7219c6ac811812e723160" + async@2.6.0: version "2.6.0" resolved "https://registry.npmjs.org/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" dependencies: lodash "^4.14.0" -async@>=0.6.0: +async@>=0.6.0, async@^2.0.1: version "2.6.1" - resolved "https://registry.npmjs.org/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" dependencies: lodash "^4.17.10" @@ -59,6 +79,10 @@ asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" +aws-sign2@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.5.0.tgz#c57103f7a17fc037f02d7c2e64b602ea223f7d63" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -91,6 +115,22 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bl@~1.0.0: + version "1.0.3" + resolved "http://registry.npmjs.org/bl/-/bl-1.0.3.tgz#fc5421a28fd4226036c3b3891a66a25bc64d226e" + dependencies: + readable-stream "~2.0.5" + +bluebird@^2.9.30: + version "2.11.0" + resolved "http://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -106,10 +146,24 @@ buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + caseless@~0.12.0: version "0.12.0" resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" +chalk@^1.0.0: + version "1.1.3" + resolved "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + circular-json@^0.3.1: version "0.3.3" resolved "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" @@ -124,9 +178,9 @@ combined-stream@1.0.6: dependencies: delayed-stream "~1.0.0" -combined-stream@~1.0.6: +combined-stream@^1.0.5, combined-stream@~1.0.1, combined-stream@~1.0.6: version "1.0.7" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" dependencies: delayed-stream "~1.0.0" @@ -134,13 +188,27 @@ commander@2.15.1: version "2.15.1" resolved "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" +commander@^2.8.1: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -core-util-is@1.0.2: +core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +ctype@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/ctype/-/ctype-0.5.3.tgz#82c18c2461f74114ef16c135224ad0b9144ca12f" dashdash@^1.12.0: version "1.14.1" @@ -183,13 +251,13 @@ ecdsa-sig-formatter@1.0.10: dependencies: safe-buffer "^5.0.1" -escape-string-regexp@1.0.5: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2: version "1.0.5" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" -extend@~3.0.2: +extend@~3.0.0, extend@~3.0.2: version "3.0.2" - resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" extsprintf@1.3.0: version "1.3.0" @@ -207,9 +275,17 @@ fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" -forever-agent@~0.6.1: +forever-agent@~0.6.0, forever-agent@~0.6.1: version "0.6.1" - resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~1.0.0-rc1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.1.tgz#ae315db9a4907fa065502304a66d7733475ee37c" + dependencies: + async "^2.0.1" + combined-stream "^1.0.5" + mime-types "^2.1.11" form-data@~2.3.2: version "2.3.2" @@ -223,6 +299,18 @@ fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" +generate-function@^2.0.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + dependencies: + is-property "^1.0.2" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + dependencies: + is-property "^1.0.0" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -248,6 +336,15 @@ har-schema@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" +har-validator@^1.6.1: + version "1.8.0" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-1.8.0.tgz#d83842b0eb4c435960aeb108a067a3aa94c0eeb2" + dependencies: + bluebird "^2.9.30" + chalk "^1.0.0" + commander "^2.8.1" + is-my-json-valid "^2.12.0" + har-validator@~5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" @@ -255,14 +352,41 @@ har-validator@~5.1.0: ajv "^5.3.0" har-schema "^2.0.0" +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" +hawk@~3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + he@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +http-signature@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-0.11.0.tgz#1796cf67a001ad5cd6849dca0991485f09089fe6" + dependencies: + asn1 "0.1.11" + assert-plus "^0.1.5" + ctype "0.5.3" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -278,14 +402,32 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@~2.0.1: version "2.0.3" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" is-buffer@^1.1.6: version "1.1.6" resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" +is-my-ip-valid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" + +is-my-json-valid@^2.12.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz#8fd6e40363cd06b963fa877d444bfb5eddc62175" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + is-my-ip-valid "^1.0.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + +is-property@^1.0.0, is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + is-stream@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -294,9 +436,13 @@ is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" -isstream@~0.1.2: +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isstream@~0.1.1, isstream@~0.1.2: version "0.1.2" - resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" jsbn@~0.1.0: version "0.1.1" @@ -310,9 +456,13 @@ json-schema@0.2.3: version "0.2.3" resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" -json-stringify-safe@~5.0.1: +json-stringify-safe@~5.0.0, json-stringify-safe@~5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +jsonpointer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" jsprim@^1.2.2: version "1.4.1" @@ -346,9 +496,9 @@ mime-db@~1.36.0: version "1.36.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" -mime-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.2: version "2.1.20" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" dependencies: mime-db "~1.36.0" @@ -416,6 +566,14 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" +node-uuid@~1.4.0: + version "1.4.8" + resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" + +oauth-sign@~0.8.0: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" @@ -438,6 +596,10 @@ postinstall-build@^5.0.1: version "5.0.3" resolved "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7" +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + psl@^1.1.24: version "1.1.29" resolved "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" @@ -446,10 +608,49 @@ punycode@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" +qs@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-5.1.0.tgz#4d932e5c7ea411cca76a312d39a606200fd50cd9" + qs@~6.5.2: version "6.5.2" resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" +readable-stream@~2.0.5: + version "2.0.6" + resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + +request@2.63.0: + version "2.63.0" + resolved "http://registry.npmjs.org/request/-/request-2.63.0.tgz#c83e7c3485e5d9bf9b146318429bc48f1253d8be" + dependencies: + aws-sign2 "~0.5.0" + bl "~1.0.0" + caseless "~0.11.0" + combined-stream "~1.0.1" + extend "~3.0.0" + forever-agent "~0.6.0" + form-data "~1.0.0-rc1" + har-validator "^1.6.1" + hawk "~3.1.0" + http-signature "~0.11.0" + isstream "~0.1.1" + json-stringify-safe "~5.0.0" + mime-types "~2.1.2" + node-uuid "~1.4.0" + oauth-sign "~0.8.0" + qs "~5.1.0" + stringstream "~0.0.4" + tough-cookie ">=0.12.0" + tunnel-agent "~0.4.0" + "request@>= 2.52.0", request@^2.88.0: version "2.88.0" resolved "https://registry.npmjs.org/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" @@ -521,6 +722,12 @@ should@^13.2.1: should-type-adaptors "^1.0.1" should-util "^1.0.0" +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + sshpk@^1.7.0: version "1.14.2" resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98" @@ -536,19 +743,37 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +stringstream@~0.0.4: + version "0.0.6" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + supports-color@5.4.0: version "5.4.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" dependencies: has-flag "^3.0.0" +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + through@^2.3.8: version "2.3.8" resolved "http://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" -tough-cookie@~2.4.3: +tough-cookie@>=0.12.0, tough-cookie@~2.4.3: version "2.4.3" - resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" dependencies: psl "^1.1.24" punycode "^1.4.1" @@ -559,6 +784,10 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel-agent@~0.4.0: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + tunnel@0.0.5: version "0.0.5" resolved "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz#d1532254749ed36620fcd1010865495a1fa9d0ae" @@ -579,6 +808,10 @@ typemoq@^2.1.0: version "1.9.1" resolved "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: version "3.3.2" resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" @@ -606,3 +839,7 @@ wrappy@1: xpath.js@~1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1" + +xtend@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" From 1d8132bcaa7367215f9421aeb129acfe7627d8f9 Mon Sep 17 00:00:00 2001 From: Alan Ren Date: Thu, 11 Oct 2018 09:53:41 -0700 Subject: [PATCH 11/11] Alanren/edit data1004 (#2781) * edit data bug fix * rename the event and use undefined instead of null * use thenable instead of callback * handle the new SlickGrid OnRendered event to control the keyboard focus * use the new event to control the focus and change the add row behavior --- package.json | 4 +- .../views/editData/editData.component.html | 1 + .../grid/views/editData/editData.component.ts | 187 +++++++++++------- src/typings/globals/slickgrid/index.d.ts | 6 + .../modules/angular2-slickgrid/index.d.ts | 4 + yarn.lock | 12 +- 6 files changed, 133 insertions(+), 81 deletions(-) diff --git a/package.json b/package.json index b41104d765..5415c57d2f 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@angular/upgrade": "~4.1.3", "@types/chart.js": "^2.7.31", "angular2-grid": "2.0.6", - "angular2-slickgrid": "github:Microsoft/angular2-slickgrid#1.4.5", + "angular2-slickgrid": "github:Microsoft/angular2-slickgrid#1.4.6", "applicationinsights": "0.18.0", "chart.js": "^2.6.0", "fast-plist": "0.1.2", @@ -65,7 +65,7 @@ "reflect-metadata": "^0.1.8", "rxjs": "5.4.0", "semver": "^5.5.0", - "slickgrid": "github:anthonydresser/SlickGrid#2.3.27", + "slickgrid": "github:anthonydresser/SlickGrid#2.3.28", "spdlog": "0.7.1", "sudo-prompt": "8.2.0", "svg.js": "^2.2.5", diff --git a/src/sql/parts/grid/views/editData/editData.component.html b/src/sql/parts/grid/views/editData/editData.component.html index 106fbd2e82..c90b81a280 100644 --- a/src/sql/parts/grid/views/editData/editData.component.html +++ b/src/sql/parts/grid/views/editData/editData.component.html @@ -21,6 +21,7 @@ (onActiveCellChanged)="onActiveCellChanged($event)" (onCellChange)="onCellEditEnd($event)" (onContextMenu)="openContextMenu($event, dataSet.batchId, dataSet.resultId, i)" + (onRendered)="onGridRendered($event)" [isCellEditValid]="onIsCellEditValid" [overrideCellFn]="overrideCellFn" [onBeforeAppendCell]="onBeforeAppendCell" diff --git a/src/sql/parts/grid/views/editData/editData.component.ts b/src/sql/parts/grid/views/editData/editData.component.ts index 5c0ea466bd..82107bc5c2 100644 --- a/src/sql/parts/grid/views/editData/editData.component.ts +++ b/src/sql/parts/grid/views/editData/editData.component.ts @@ -13,7 +13,7 @@ import 'vs/css!sql/parts/grid/media/slickGrid'; import 'vs/css!./media/editData'; import { ElementRef, ChangeDetectorRef, OnInit, OnDestroy, Component, Inject, forwardRef, EventEmitter } from '@angular/core'; -import { VirtualizedCollection } from 'angular2-slickgrid'; +import { VirtualizedCollection, OnRangeRenderCompletedEventArgs } from 'angular2-slickgrid'; import { IGridDataSet } from 'sql/parts/grid/common/interfaces'; import * as Services from 'sql/parts/grid/services/sharedServices'; @@ -49,8 +49,12 @@ export const EDITDATA_SELECTOR: string = 'editdata-component'; }) export class EditDataComponent extends GridParentComponent implements OnInit, OnDestroy { - // CONSTANTS - private scrollTimeOutTime = 200; + // The time(in milliseconds) we wait before refreshing the grid. + // We use clearTimeout and setTimeout pair to avoid unnecessary refreshes. + private refreshGridTimeoutInMs = 200; + + // The timeout handle for the refresh grid task + private refreshGridTimeoutHandle: number; // Optimized for the edit top 200 rows scenario, only need to retrieve the data once // to make the scroll experience smoother @@ -59,12 +63,9 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On // FIELDS // All datasets private dataSet: IGridDataSet; - private scrollTimeOut: number; - private scrollEnabled = true; private firstRender = true; private totalElapsedTimeSpan: number; private complete = false; - // Current selected cell state private currentCell: { row: number, column: number, isEditable: boolean }; private currentEditCellValue: string; @@ -82,6 +83,7 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On public overrideCellFn: (rowNumber, columnId, value?, data?) => string; public loadDataFunction: (offset: number, count: number) => Promise<{}[]>; public onBeforeAppendCell: (row: number, column: number) => string; + public onGridRendered: (event: Slick.OnRenderedEventArgs) => void; private savedViewState: { gridSelections: Slick.Range[]; @@ -194,9 +196,18 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On } else if (this.isCellDirty(row, column)) { cellClass = ' dirtyRowHeader '; } + return cellClass; }; + this.onGridRendered = (args: Slick.OnRenderedEventArgs): void => { + // After rendering move the focus back to the previous active cell + if (this.currentCell.column !== undefined && this.currentCell.row !== undefined + && this.isCellOnScreen(this.currentCell.row, this.currentCell.column)) { + this.focusCell(this.currentCell.row, this.currentCell.column, false); + } + }; + // Setup a function for generating a promise to lookup result subsets this.loadDataFunction = (offset: number, count: number): Promise<{}[]> => { return new Promise<{}[]>((resolve, reject) => { @@ -280,7 +291,7 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On if (this.currentCell.row !== row) { // If we're currently adding a new row, only commit it if it has changes or the user is trying to add another new row - if (this.newRowVisible && this.currentCell.row === this.dataSet.dataRows.getLength() - 2 && !this.isNullRow(row) && this.currentEditCellValue === null) { + if (this.newRowVisible && this.currentCell.row === this.dataSet.dataRows.getLength() - 2 && !this.isNullRow(row) && this.currentEditCellValue === undefined) { cellSelectTasks = cellSelectTasks.then(() => { return this.revertCurrentRow().then(() => this.focusCell(row, column)); }); @@ -302,22 +313,9 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On } } - if (this.isNullRow(row) && !this.removingNewRow) { - // We've entered the "new row", so we need to add a row and jump to it - cellSelectTasks = cellSelectTasks.then(() => { - self.addRow(row); - }); - } - // At the end of a successful cell select, update the currently selected cell cellSelectTasks = cellSelectTasks.then(() => { - self.currentCell = { - row: row, - column: column, - isEditable: self.dataSet.columnDefinitions[column] - ? self.dataSet.columnDefinitions[column].isEditable - : false - }; + self.setCurrentCell(row, column); }); // Cap off any failed promises, since they'll be handled @@ -388,13 +386,14 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On undefinedDataSet.dataRows = undefined; undefinedDataSet.resized = new EventEmitter(); self.placeHolderDataSets.push(undefinedDataSet); - self.onScroll(0); + self.refreshGrid(); // Setup the state of the selected cell - this.currentCell = { row: null, column: null, isEditable: null }; - this.currentEditCellValue = null; + this.currentCell = { row: 0, column: 1, isEditable: undefined }; + this.currentEditCellValue = undefined; this.removingNewRow = false; this.newRowVisible = false; + this.dirtyCells = []; } /** @@ -403,30 +402,36 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On * @param scrollTop The scrolltop value, if not called by the scroll event should be 0 */ onScroll(scrollTop): void { - const self = this; - clearTimeout(self.scrollTimeOut); - this.scrollTimeOut = setTimeout(() => { - self.scrollEnabled = false; - for (let i = 0; i < self.placeHolderDataSets.length; i++) { - self.placeHolderDataSets[i].dataRows = self.dataSet.dataRows; - self.placeHolderDataSets[i].resized.emit(); - } + this.refreshGrid(); + } - self._cd.detectChanges(); + private refreshGrid(): Thenable { + return new Promise((resolve, reject) => { + const self = this; + clearTimeout(self.refreshGridTimeoutHandle); + this.refreshGridTimeoutHandle = setTimeout(() => { + for (let i = 0; i < self.placeHolderDataSets.length; i++) { + self.placeHolderDataSets[i].dataRows = self.dataSet.dataRows; + self.placeHolderDataSets[i].resized.emit(); + } - if (self.firstRender) { - let setActive = function () { - if (self.firstRender && self.slickgrids.toArray().length > 0) { - self.slickgrids.toArray()[0].setActive(); - self.firstRender = false; - } - }; + self._cd.detectChanges(); - setTimeout(() => { - setActive(); - }); - } - }, self.scrollTimeOutTime); + if (self.firstRender) { + let setActive = function () { + if (self.firstRender && self.slickgrids.toArray().length > 0) { + self.slickgrids.toArray()[0].setActive(); + self.firstRender = false; + } + }; + + setTimeout(() => { + setActive(); + }); + } + resolve(); + }, self.refreshGridTimeoutInMs); + }); } protected tryHandleKeyEvent(e: StandardKeyboardEvent): boolean { @@ -451,13 +456,15 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On this.dataService.revertRow(this.rowIdMappings[currentNewRowIndex]) .then(() => { - this.removeRow(currentNewRowIndex); + return this.removeRow(currentNewRowIndex); + }).then(() => { this.newRowVisible = false; + this.resetCurrentCell(); }); } else { try { // Perform a revert row operation - if (this.currentCell && this.currentCell.row !== undefined && this.currentCell.row !== null) { + if (this.currentCell && this.currentCell.row !== undefined) { await this.dataService.revertRow(this.currentCell.row); } } finally { @@ -465,9 +472,13 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On // so clear any existing client-side edit and refresh on-screen data // do not refresh the whole dataset as it will move the focus away to the first row. // - this.currentEditCellValue = null; + this.currentEditCellValue = undefined; this.dirtyCells = []; - this.dataSet.dataRows.resetWindowsAroundIndex(this.currentCell.row); + this.resetCurrentCell(); + + if (this.currentCell.row !== undefined) { + this.dataSet.dataRows.resetWindowsAroundIndex(this.currentCell.row); + } } } } @@ -475,8 +486,15 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On private submitCurrentCellChange(resultHandler, errorHandler): Promise { let self = this; let updateCellPromise: Promise = Promise.resolve(); - - if (this.currentCell && this.currentCell.isEditable && this.currentEditCellValue !== null && !this.removingNewRow) { + let refreshGrid = false; + if (this.currentCell && this.currentCell.isEditable && this.currentEditCellValue !== undefined && !this.removingNewRow) { + if (this.isNullRow(this.currentCell.row)) { + refreshGrid = true; + // We've entered the "new row", so we need to add a row and jump to it + updateCellPromise = updateCellPromise.then(() => { + return self.addRow(this.currentCell.row); + }); + } // We're exiting a read/write cell after having changed the value, update the cell value in the service updateCellPromise = updateCellPromise.then(() => { // Use the mapped row ID if we're on that row @@ -487,8 +505,14 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On return self.dataService.updateCell(sessionRowId, self.currentCell.column - 1, self.currentEditCellValue); }).then( result => { - self.currentEditCellValue = null; - return resultHandler(result); + self.currentEditCellValue = undefined; + let refreshPromise: Thenable = Promise.resolve(); + if (refreshGrid) { + refreshPromise = self.refreshGrid(); + } + return refreshPromise.then(() => { + return resultHandler(result); + }); }, error => { return errorHandler(error); @@ -542,11 +566,11 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On // Adds an extra row to the end of slickgrid (just for rendering purposes) // Then sets the focused call afterwards - private addRow(row: number): void { + private addRow(row: number): Thenable { let self = this; // Add a new row to the edit session in the tools service - this.dataService.createRow() + return this.dataService.createRow() .then(result => { // Map the new row ID to the row ID we have self.rowIdMappings[row] = result.newRowId; @@ -555,28 +579,21 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On // Add a new "new row" to the end of the results // Adding an extra row for 'new row' functionality self.dataSet.totalRows++; - self.dataSet.maxHeight = self.getMaxHeight(this.dataSet.totalRows); - self.dataSet.minHeight = self.getMinHeight(this.dataSet.totalRows); + self.dataSet.maxHeight = self.getMaxHeight(self.dataSet.totalRows); + self.dataSet.minHeight = self.getMinHeight(self.dataSet.totalRows); self.dataSet.dataRows = new VirtualizedCollection( self.windowSize, self.dataSet.totalRows, self.loadDataFunction, index => { return {}; } ); - - // Refresh grid - self.onScroll(0); - - // Mark the row as dirty once the scroll has completed - setTimeout(() => { - self.setRowDirtyState(row, true); - }, self.scrollTimeOutTime); }); } + // removes a row from the end of slickgrid (just for rendering purposes) // Then sets the focused call afterwards - private removeRow(row: number): void { + private removeRow(row: number): Thenable { // Removing the new row this.dataSet.totalRows--; this.dataSet.dataRows = new VirtualizedCollection( @@ -587,15 +604,13 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On ); // refresh results view - this.onScroll(0); - - // Set focus to the row index column of the removed row if the current selection is in the removed row - setTimeout(() => { - if (this.currentCell.row === row) { - this.focusCell(row, 0); + return this.refreshGrid().then(() => { + // Set focus to the row index column of the removed row if the current selection is in the removed row + if (this.currentCell.row === row && !this.removingNewRow) { + this.focusCell(row, 1); } this.removingNewRow = false; - }, this.scrollTimeOutTime); + }); } private focusCell(row: number, column: number, forceEdit: boolean = true): void { @@ -673,4 +688,30 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On private isCellDirty(row: number, column: number): boolean { return this.currentCell.row === row && this.dirtyCells.indexOf(column) !== -1; } + + private isCellOnScreen(row: number, column: number): boolean { + let slick: any = this.slickgrids.toArray()[0]; + let grid = slick._grid; + let viewport = grid.getViewport(); + let cellBox = grid.getCellNodeBox(row, column); + return viewport && cellBox + && viewport.leftPx <= cellBox.left && viewport.rightPx >= cellBox.right + && viewport.top <= row && viewport.bottom >= row; + } + + private resetCurrentCell() { + this.currentCell.row = undefined; + this.currentCell.column = undefined; + this.currentCell.isEditable = false; + } + + private setCurrentCell(row: number, column: number) { + this.currentCell = { + row: row, + column: column, + isEditable: this.dataSet.columnDefinitions[column] + ? this.dataSet.columnDefinitions[column].isEditable + : false + }; + } } diff --git a/src/typings/globals/slickgrid/index.d.ts b/src/typings/globals/slickgrid/index.d.ts index 4f181e38de..07ef5289ac 100644 --- a/src/typings/globals/slickgrid/index.d.ts +++ b/src/typings/globals/slickgrid/index.d.ts @@ -1208,6 +1208,7 @@ declare namespace Slick { public onSelectedRowsChanged: Slick.Event>; public onCellCssStylesChanged: Slick.Event>; public onViewportChanged: Slick.Event>; + public onRendered: Slick.Event>; // #endregion Events // #region Plugins @@ -1416,6 +1417,11 @@ declare namespace Slick { } + export interface OnRenderedEventArgs extends GridEventArgs{ + startRow: number; + endRow: number; + } + export interface SortColumn { sortCol: Column; sortAsc: boolean; diff --git a/src/typings/modules/angular2-slickgrid/index.d.ts b/src/typings/modules/angular2-slickgrid/index.d.ts index 2c84ce4784..cb7dc2ff41 100644 --- a/src/typings/modules/angular2-slickgrid/index.d.ts +++ b/src/typings/modules/angular2-slickgrid/index.d.ts @@ -111,6 +111,10 @@ declare module '~angular2-slickgrid/out/js/slickGrid' { import { OnChanges, OnInit, OnDestroy, SimpleChange, EventEmitter, AfterViewInit } from '@angular/core'; import { Observable } from 'rxjs/Rx'; import { IObservableCollection, IGridDataRow, ISlickColumn } from '~angular2-slickgrid/out/js/interfaces'; +export class OnRangeRenderCompletedEventArgs { + startRow: number; + endRow: number; +} export function getOverridableTextEditorClass(grid: SlickGrid): any; export class SlickGrid implements OnChanges, OnInit, OnDestroy, AfterViewInit { private _el; diff --git a/yarn.lock b/yarn.lock index 05d55a5b10..c3b139ec94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -178,9 +178,9 @@ angular2-grid@2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/angular2-grid/-/angular2-grid-2.0.6.tgz#01fe225dc13b2822370b6c61f9a6913b3a26f989" -"angular2-slickgrid@github:Microsoft/angular2-slickgrid#1.4.5": - version "1.4.5" - resolved "https://codeload.github.com/Microsoft/angular2-slickgrid/tar.gz/4a4dd9333f6295c9539c2c3f9a945a204c9a6449" +"angular2-slickgrid@github:Microsoft/angular2-slickgrid#1.4.6": + version "1.4.6" + resolved "https://codeload.github.com/Microsoft/angular2-slickgrid/tar.gz/09579fdc90b1ec469578ec1040d1515585ce2a11" ansi-colors@^1.0.1: version "1.1.0" @@ -6109,9 +6109,9 @@ slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" -"slickgrid@github:anthonydresser/SlickGrid#2.3.27": - version "2.3.27" - resolved "https://codeload.github.com/anthonydresser/SlickGrid/tar.gz/712a59307942c8fdb18708b6d1a501880a74db8d" +"slickgrid@github:anthonydresser/SlickGrid#2.3.28": + version "2.3.28" + resolved "https://codeload.github.com/anthonydresser/SlickGrid/tar.gz/57db056e2bd15451e6ad30946a71df49e1c9451f" dependencies: jquery ">=1.8.0" jquery-ui ">=1.8.0"