From 63fb4e2827a0a2f3d49c97e8b90a102c3bcff1cc Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Thu, 7 Jun 2018 16:19:26 -0700 Subject: [PATCH] Bootstrap Service Abstract injection (#1534) * change to generic injection * formatting * fixed missed merge * change to keep a record of the services per selector * formatting * adding back in tests * apply back tests * remove fundamentally broken test --- .../parts/admin/security/createLoginEditor.ts | 2 +- src/sql/parts/dashboard/dashboardEditor.ts | 2 +- .../disasterRecovery/backup/backupDialog.ts | 2 +- .../editData/editor/editDataResultsEditor.ts | 2 +- .../parts/query/editor/queryResultsEditor.ts | 2 +- src/sql/parts/queryPlan/queryPlanEditor.ts | 2 +- .../parts/tasks/dialog/taskDialogEditor.ts | 4 +- src/sql/platform/dialog/dialogPane.ts | 2 +- .../services/bootstrap/bootstrapService.ts | 108 +++--------------- .../platform/dialog/dialogPane.test.ts | 99 ++++++---------- 10 files changed, 66 insertions(+), 159 deletions(-) diff --git a/src/sql/parts/admin/security/createLoginEditor.ts b/src/sql/parts/admin/security/createLoginEditor.ts index 94c9f409a4..35f6adf916 100644 --- a/src/sql/parts/admin/security/createLoginEditor.ts +++ b/src/sql/parts/admin/security/createLoginEditor.ts @@ -98,7 +98,7 @@ export class CreateLoginEditor extends BaseEditor { connection: input.getConnectionProfile(), ownerUri: input.getUri() }; - let uniqueSelector = this.instantiationService.invokeFunction(bootstrapAngular, + let uniqueSelector = bootstrapAngular(this.instantiationService, CreateLoginModule, this.getContainer(), CREATELOGIN_SELECTOR, diff --git a/src/sql/parts/dashboard/dashboardEditor.ts b/src/sql/parts/dashboard/dashboardEditor.ts index 1feb447fa2..8d4356f149 100644 --- a/src/sql/parts/dashboard/dashboardEditor.ts +++ b/src/sql/parts/dashboard/dashboardEditor.ts @@ -122,7 +122,7 @@ export class DashboardEditor extends BaseEditor { input.hasBootstrapped = true; - let uniqueSelector = this.instantiationService.invokeFunction(bootstrapAngular, + let uniqueSelector = bootstrapAngular(this.instantiationService, DashboardModule, this._dashboardContainer, DASHBOARD_SELECTOR, diff --git a/src/sql/parts/disasterRecovery/backup/backupDialog.ts b/src/sql/parts/disasterRecovery/backup/backupDialog.ts index 8241df7ca9..e810adb47e 100644 --- a/src/sql/parts/disasterRecovery/backup/backupDialog.ts +++ b/src/sql/parts/disasterRecovery/backup/backupDialog.ts @@ -54,7 +54,7 @@ export class BackupDialog extends Modal { * Get the bootstrap params and perform the bootstrap */ private bootstrapAngular(bodyContainer: HTMLElement) { - this._uniqueSelector = this._instantiationService.invokeFunction(bootstrapAngular, + this._uniqueSelector = bootstrapAngular(this._instantiationService, BackupModule, bodyContainer, BACKUP_SELECTOR, diff --git a/src/sql/parts/editData/editor/editDataResultsEditor.ts b/src/sql/parts/editData/editor/editDataResultsEditor.ts index 5e47427263..571e75fdbd 100644 --- a/src/sql/parts/editData/editor/editDataResultsEditor.ts +++ b/src/sql/parts/editData/editor/editDataResultsEditor.ts @@ -110,7 +110,7 @@ export class EditDataResultsEditor extends BaseEditor { // to events from the backing data service const parent = input.container; let params: IEditDataComponentParams = { dataService: dataService }; - this._instantiationService.invokeFunction(bootstrapAngular, + bootstrapAngular(this._instantiationService, EditDataModule, parent, EDITDATA_SELECTOR, diff --git a/src/sql/parts/query/editor/queryResultsEditor.ts b/src/sql/parts/query/editor/queryResultsEditor.ts index 0b9e7c0a2e..34df4508bb 100644 --- a/src/sql/parts/query/editor/queryResultsEditor.ts +++ b/src/sql/parts/query/editor/queryResultsEditor.ts @@ -170,7 +170,7 @@ export class QueryResultsEditor extends BaseEditor { // Otherwise many components will be left around and be subscribed // to events from the backing data service let params: IQueryComponentParams = { dataService: dataService }; - this._instantiationService.invokeFunction(bootstrapAngular, + bootstrapAngular(this._instantiationService, QueryOutputModule, this.getContainer(), QUERY_OUTPUT_SELECTOR, diff --git a/src/sql/parts/queryPlan/queryPlanEditor.ts b/src/sql/parts/queryPlan/queryPlanEditor.ts index bc9972f9bb..1dc9d04aa4 100644 --- a/src/sql/parts/queryPlan/queryPlanEditor.ts +++ b/src/sql/parts/queryPlan/queryPlanEditor.ts @@ -111,7 +111,7 @@ export class QueryPlanEditor extends BaseEditor { planXml: input.planXml }; - let uniqueSelector = this.instantiationService.invokeFunction(bootstrapAngular, + let uniqueSelector = bootstrapAngular(this.instantiationService, QueryPlanModule, this.getContainer(), QUERYPLAN_SELECTOR, diff --git a/src/sql/parts/tasks/dialog/taskDialogEditor.ts b/src/sql/parts/tasks/dialog/taskDialogEditor.ts index 434f59969b..e51347dab1 100644 --- a/src/sql/parts/tasks/dialog/taskDialogEditor.ts +++ b/src/sql/parts/tasks/dialog/taskDialogEditor.ts @@ -69,7 +69,7 @@ export class TaskDialogEditor extends BaseEditor { private revealElementWithTagName(tagName: string, parent: HTMLElement): void { let elementToReveal: HTMLElement; - for(let i = 0; i < parent.children.length; i++) { + for (let i = 0; i < parent.children.length; i++) { let child: HTMLElement = parent.children[i]; if (child.tagName && child.tagName.toLowerCase() === tagName && !elementToReveal) { elementToReveal = child; @@ -92,7 +92,7 @@ export class TaskDialogEditor extends BaseEditor { let params: ITaskDialogComponentParams = { ownerUri: input.getUri() }; - let uniqueSelector = this.instantiationService.invokeFunction(bootstrapAngular, + let uniqueSelector = bootstrapAngular(this.instantiationService, TaskDialogModule, this.getContainer(), TASKDIALOG_SELECTOR, diff --git a/src/sql/platform/dialog/dialogPane.ts b/src/sql/platform/dialog/dialogPane.ts index 8a6cdbb155..5fb52a2006 100644 --- a/src/sql/platform/dialog/dialogPane.ts +++ b/src/sql/platform/dialog/dialogPane.ts @@ -90,7 +90,7 @@ export class DialogPane extends Disposable implements IThemable { * Bootstrap angular for the dialog's model view controller with the given model view ID */ private initializeModelViewContainer(bodyContainer: HTMLElement, modelViewId: string, tab?: DialogTab) { - this._instantiationService.invokeFunction(bootstrapAngular, + bootstrapAngular(this._instantiationService, DialogModule, bodyContainer, 'dialog-modelview-container', diff --git a/src/sql/services/bootstrap/bootstrapService.ts b/src/sql/services/bootstrap/bootstrapService.ts index d49243c964..e7ccc1272b 100644 --- a/src/sql/services/bootstrap/bootstrapService.ts +++ b/src/sql/services/bootstrap/bootstrapService.ts @@ -3,51 +3,14 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { NgModuleRef, enableProdMode, InjectionToken, ReflectiveInjector, Type, PlatformRef } from '@angular/core'; +import { NgModuleRef, enableProdMode, InjectionToken, ReflectiveInjector, Type, PlatformRef, Provider } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { IConnectionManagementService, IConnectionDialogService, IErrorMessageService } - from 'sql/parts/connection/common/connectionManagement'; -import { IMetadataService } from 'sql/services/metadata/metadataService'; -import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService'; -import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService'; -import { IScriptingService } from 'sql/services/scripting/scriptingService'; -import { IQueryManagementService } from 'sql/parts/query/common/queryManagement'; -import { IQueryModelService } from 'sql/parts/query/execution/queryModel'; -import { IAdminService } from 'sql/parts/admin/common/adminService'; -import { IRestoreDialogController, IRestoreService } from 'sql/parts/disasterRecovery/restore/common/restoreService'; -import { IBackupService, IBackupUiService } from 'sql/parts/disasterRecovery/backup/common/backupService'; -import { IAngularEventingService } from 'sql/services/angularEventing/angularEventingService'; -import { IInsightsDialogService } from 'sql/parts/insights/common/interfaces'; -import { ISqlOAuthService } from 'sql/common/sqlOAuthService'; -import { IFileBrowserService, IFileBrowserDialogController } from 'sql/parts/fileBrowser/common/interfaces'; -import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; -import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService'; -import { IDashboardViewService } from 'sql/services/dashboard/common/dashboardViewService'; -import { IModelViewService } from 'sql/services/modelComponents/modelViewService'; -import { IJobManagementService } from 'sql/parts/jobManagement/common/interfaces'; - -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IEditorInput } from 'vs/platform/editor/common/editor'; -import { IInstantiationService, ServicesAccessor, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IAccountManagementService } from 'sql/services/accountManagement/interfaces'; -import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { IClipboardService as vsIClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IProgressService } from 'vs/platform/progress/common/progress'; +import { IInstantiationService, _util } from 'vs/platform/instantiation/common/instantiation'; const selectorCounter = new Map(); +const serviceMap = new Map(); export const IBootstrapParams = new InjectionToken('bootstrap_params'); export interface IBootstrapParams { @@ -68,63 +31,26 @@ function createUniqueSelector(selector: string): string { let platform: PlatformRef; -export function bootstrapAngular(collection: ServicesAccessor, moduleType: IModuleFactory, container: HTMLElement, selectorString: string, params: IBootstrapParams, input?: IEditorInput, callbackSetModule?: (value: NgModuleRef) => void): string { +export function bootstrapAngular(service: IInstantiationService, moduleType: IModuleFactory, container: HTMLElement, selectorString: string, params: IBootstrapParams, input?: IEditorInput, callbackSetModule?: (value: NgModuleRef) => void): string { // Create the uniqueSelectorString let uniqueSelectorString = createUniqueSelector(selectorString); let selector = document.createElement(uniqueSelectorString); container.appendChild(selector); + serviceMap.set(uniqueSelectorString, service); + if (!platform) { // Perform the bootsrap - const providers: { provide: ServiceIdentifier | InjectionToken, useValue: any }[] = [ - // sql services - { provide: IConnectionManagementService, useValue: collection.get(IConnectionManagementService) }, - { provide: IConnectionDialogService, useValue: collection.get(IConnectionDialogService) }, - { provide: IErrorMessageService, useValue: collection.get(IErrorMessageService) }, - { provide: IMetadataService, useValue: collection.get(IMetadataService) }, - { provide: IObjectExplorerService, useValue: collection.get(IObjectExplorerService) }, - { provide: IQueryEditorService, useValue: collection.get(IQueryEditorService) }, - { provide: IScriptingService, useValue: collection.get(IScriptingService) }, - { provide: IQueryManagementService, useValue: collection.get(IQueryManagementService) }, - { provide: IQueryModelService, useValue: collection.get(IQueryModelService) }, - { provide: IAdminService, useValue: collection.get(IAdminService) }, - { provide: IRestoreDialogController, useValue: collection.get(IRestoreDialogController) }, - { provide: IRestoreService, useValue: collection.get(IRestoreService) }, - { provide: IBackupService, useValue: collection.get(IBackupService) }, - { provide: IBackupUiService, useValue: collection.get(IBackupUiService) }, - { provide: IAngularEventingService, useValue: collection.get(IAngularEventingService) }, - { provide: IInsightsDialogService, useValue: collection.get(IInsightsDialogService) }, - { provide: ISqlOAuthService, useValue: collection.get(ISqlOAuthService) }, - { provide: IFileBrowserService, useValue: collection.get(IFileBrowserService) }, - { provide: IFileBrowserDialogController, useValue: collection.get(IFileBrowserDialogController) }, - { provide: IClipboardService, useValue: collection.get(IClipboardService) }, - { provide: ICapabilitiesService, useValue: collection.get(ICapabilitiesService) }, - { provide: IDashboardViewService, useValue: collection.get(IDashboardViewService) }, - { provide: IModelViewService, useValue: collection.get(IModelViewService) }, - // vscode services - { provide: vsIClipboardService, useValue: collection.get(vsIClipboardService) }, - { provide: IKeybindingService, useValue: collection.get(IKeybindingService) }, - { provide: IContextKeyService, useValue: collection.get(IContextKeyService) }, - { provide: IContextMenuService, useValue: collection.get(IContextMenuService) }, - { provide: IContextViewService, useValue: collection.get(IContextViewService) }, - { provide: IWorkbenchEditorService, useValue: collection.get(IWorkbenchEditorService) }, - { provide: IPartService, useValue: collection.get(IPartService) }, - { provide: IInstantiationService, useValue: collection.get(IInstantiationService) }, - { provide: IConfigurationService, useValue: collection.get(IConfigurationService) }, - { provide: IWorkspaceContextService, useValue: collection.get(IWorkspaceContextService) }, - { provide: IAccountManagementService, useValue: collection.get(IAccountManagementService) }, - { provide: IWindowsService, useValue: collection.get(IWindowsService) }, - { provide: IWindowService, useValue: collection.get(IWindowService) }, - { provide: ITelemetryService, useValue: collection.get(ITelemetryService) }, - { provide: IStorageService, useValue: collection.get(IStorageService) }, - { provide: ICommandService, useValue: collection.get(ICommandService) }, - { provide: IJobManagementService, useValue: collection.get(IJobManagementService) }, - { provide: IEnvironmentService, useValue: collection.get(IEnvironmentService) }, - { provide: INotificationService, useValue: collection.get(INotificationService) }, - { provide: IWorkbenchThemeService, useValue: collection.get(IWorkbenchThemeService) }, - { provide: IProgressService, useValue: collection.get(IProgressService) } - ]; + const providers: Provider = []; + + _util.serviceIds.forEach(id => { + providers.push({ + provide: id, useFactory: () => { + return (serviceMap.get(uniqueSelectorString))._getOrCreateServiceInstance(id); + } + }); + }); platform = platformBrowserDynamic(providers); } @@ -132,6 +58,10 @@ export function bootstrapAngular(collection: ServicesAccessor, moduleType: IM platform.bootstrapModule(moduleType(params, uniqueSelectorString)).then(moduleRef => { if (input) { input.onDispose(() => { + serviceMap.delete(uniqueSelectorString); + moduleRef.onDestroy(() => { + serviceMap.delete(uniqueSelectorString); + }); moduleRef.destroy(); }); } diff --git a/src/sqltest/platform/dialog/dialogPane.test.ts b/src/sqltest/platform/dialog/dialogPane.test.ts index e628d6351f..0c9110977c 100644 --- a/src/sqltest/platform/dialog/dialogPane.test.ts +++ b/src/sqltest/platform/dialog/dialogPane.test.ts @@ -4,124 +4,101 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Mock, It, Times } from 'typemoq'; +import { Mock, It, Times, GlobalMock } from 'typemoq'; import { Dialog, DialogTab } from 'sql/platform/dialog/dialogTypes'; import { DialogPane } from 'sql/platform/dialog/dialogPane'; import { DialogComponentParams } from 'sql/platform/dialog/dialogContainer.component'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { bootstrapAngular } from 'sql/services/bootstrap/bootstrapService'; 'use strict'; +interface BootstrapAngular { + (collection, moduleType, container, selectorString, params: DialogComponentParams, input, callbackSetModule): void; +} + suite('Dialog Pane Tests', () => { let dialog: Dialog; - let mockInstantiationService: Mock; let container: HTMLElement; + let bootstrapSave: BootstrapAngular; + + function setupBootstrap(fn: BootstrapAngular): void { + (bootstrapAngular) = fn; + } + setup(() => { dialog = new Dialog('test_dialog'); - mockInstantiationService = Mock.ofInstance({ - invokeFunction: () => undefined - } as any); - mockInstantiationService.setup(x => x.invokeFunction(It.isAny(), It.isAny(), It.isAny(), It.isAny(), It.isAny(), undefined, It.isAny())); container = document.createElement('div'); + bootstrapSave = bootstrapAngular; }); test('Creating a pane from content without tabs initializes the model view content correctly', () => { // If I fill in a dialog's content with the ID of a specific model view provider and then render the dialog let modelViewId = 'test_content'; + let bootstrapCalls = 0; + setupBootstrap((collection, moduleType, container, selectorString, params: DialogComponentParams, input, callbackSetModule) => { + assert.equal(params.modelViewId, modelViewId); + bootstrapCalls++; + }); dialog.content = modelViewId; - let dialogPane = new DialogPane(dialog.title, dialog.content, () => undefined, mockInstantiationService.object); + let dialogPane = new DialogPane(dialog.title, dialog.content, () => undefined, undefined); dialogPane.createBody(container); - - // Then a single dialog-modelview-container element is added directly to the dialog pane - mockInstantiationService.verify(x => x.invokeFunction( - It.isAny(), - It.isAny(), - It.isAny(), - It.isAny(), - It.is((x: DialogComponentParams) => x.modelViewId === modelViewId), - undefined, - It.isAny()), Times.once()); + assert.equal(bootstrapCalls, 1); }); test('Creating a pane from content with a single tab initializes without showing tabs', () => { // If I fill in a dialog's content with a single tab and then render the dialog let modelViewId = 'test_content'; + let bootstrapCalls = 0; + setupBootstrap((collection, moduleType, container, selectorString, params: DialogComponentParams, input, callbackSetModule) => { + assert.equal(params.modelViewId, modelViewId); + bootstrapCalls++; + }); dialog.content = [new DialogTab('', modelViewId)]; - let dialogPane = new DialogPane(dialog.title, dialog.content, () => undefined, mockInstantiationService.object); + let dialogPane = new DialogPane(dialog.title, dialog.content, () => undefined, undefined); dialogPane.createBody(container); - - // Then a single dialog-modelview-container element is added directly to the dialog pane - mockInstantiationService.verify(x => x.invokeFunction( - It.isAny(), - It.isAny(), - It.isAny(), - It.isAny(), - It.is((x: DialogComponentParams) => x.modelViewId === modelViewId), - undefined, - It.isAny()), Times.once()); - }); - - test('Creating a pane from content with multiple tabs initializes multiple model view tabs', () => { - // If I fill in a dialog's content with a single tab and then render the dialog - let modelViewId1 = 'test_content_1'; - let modelViewId2 = 'test_content_2'; - dialog.content = [new DialogTab('tab1', modelViewId1), new DialogTab('tab2', modelViewId2)]; - let dialogPane = new DialogPane(dialog.title, dialog.content, () => undefined, mockInstantiationService.object); - dialogPane.createBody(container); - - // Then a dialog-modelview-container element is added for the first tab (subsequent ones get added when the tab is actually clicked) - mockInstantiationService.verify(x => x.invokeFunction( - It.isAny(), - It.isAny(), - It.isAny(), - It.isAny(), - It.is((x: DialogComponentParams) => x.modelViewId === modelViewId1), - undefined, - It.isAny()), Times.once()); + assert.equal(bootstrapCalls, 1); }); test('Dialog validation gets set based on the validity of the model view content', () => { // Set up the mock bootstrap service to intercept validation callbacks let validationCallbacks: ((valid: boolean) => void)[] = []; - mockInstantiationService.reset(); - mockInstantiationService.setup(x => x.invokeFunction(It.isAny(), It.isAny(), It.isAny(), It.isAny(), It.isAny(), undefined, It.isAny())).callback( - (collection, moduleType, container, selectorString, params: DialogComponentParams, input, callbackSetModule) => { - validationCallbacks.push(params.validityChangedCallback); - }); + + setupBootstrap((collection, moduleType, container, selectorString, params: DialogComponentParams, input, callbackSetModule) => { + validationCallbacks.push(params.validityChangedCallback); + }); let modelViewId1 = 'test_content_1'; let modelViewId2 = 'test_content_2'; dialog.content = [new DialogTab('tab1', modelViewId1), new DialogTab('tab2', modelViewId2)]; - let dialogPane = new DialogPane(dialog.title, dialog.content, valid => dialog.notifyValidityChanged(valid), mockInstantiationService.object); + let dialogPane = new DialogPane(dialog.title, dialog.content, valid => dialog.notifyValidityChanged(valid), undefined); dialogPane.createBody(container); let validityChanges: boolean[] = []; - dialog.onValidityChanged(valid => validityChanges.push(valid)); + dialog.onValidityChanged(valid => validityChanges.push(valid)); // If I set tab 2's validation to false validationCallbacks[1](false); - // Then the whole dialog's validation is false assert.equal(dialog.valid, false); assert.equal(validityChanges.length, 1); assert.equal(validityChanges[0], false); - // If I then set it back to true validationCallbacks[1](true); - // Then the whole dialog's validation is true assert.equal(dialog.valid, true); assert.equal(validityChanges.length, 2); assert.equal(validityChanges[1], true); - // If I set tab 1's validation to false validationCallbacks[0](false); - // Then the whole dialog's validation is false assert.equal(dialog.valid, false); assert.equal(validityChanges.length, 3); assert.equal(validityChanges[2], false); - }); -}); \ No newline at end of file + + teardown(() => { + (bootstrapAngular) = bootstrapSave; + }); +});