diff --git a/extensions/big-data-cluster/src/interfaces.ts b/extensions/big-data-cluster/src/interfaces.ts index 6c575ee322..a4f9657374 100644 --- a/extensions/big-data-cluster/src/interfaces.ts +++ b/extensions/big-data-cluster/src/interfaces.ts @@ -50,6 +50,7 @@ export interface ContainerRegistryInfo { } export interface TargetClusterTypeInfo { + enabled: boolean; type: TargetClusterType; name: string; fullName: string; @@ -80,4 +81,44 @@ export enum ClusterType { Minikube, Kubernetes, Other +} + +export interface ClusterProfile { + name: string; + sqlServerMasterConfiguration: SQLServerMasterConfiguration; + computePoolConfiguration: PoolConfiguration; + dataPoolConfiguration: PoolConfiguration; + storagePoolConfiguration: PoolConfiguration; + sparkPoolConfiguration: PoolConfiguration; +} + +export interface PoolConfiguration { + type: ClusterPoolType; + scale: number; + maxScale?: number; + hardwareLabel?: string; +} + +export interface SQLServerMasterConfiguration extends PoolConfiguration { + engineOnly: boolean; +} + +export enum ClusterPoolType { + SQL, + Compute, + Data, + Storage, + Spark +} + +export interface ClusterResourceSummary { + hardwareLabels: HardwareLabel[]; +} + +export interface HardwareLabel { + name: string; + totalNodes: number; + totalCores: number; + totalMemoryInGB: number; + totalDisks: number; } \ No newline at end of file diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/createClusterModel.ts b/extensions/big-data-cluster/src/wizards/create-cluster/createClusterModel.ts index 3674c255d3..c0c9ed1ed5 100644 --- a/extensions/big-data-cluster/src/wizards/create-cluster/createClusterModel.ts +++ b/extensions/big-data-cluster/src/wizards/create-cluster/createClusterModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TargetClusterType, ClusterPorts, ClusterType, ContainerRegistryInfo, TargetClusterTypeInfo, ToolInfo, ToolInstallationStatus } from '../../interfaces'; +import { TargetClusterType, ClusterPorts, ClusterType, ContainerRegistryInfo, TargetClusterTypeInfo, ToolInfo, ToolInstallationStatus, ClusterProfile, PoolConfiguration, SQLServerMasterConfiguration, ClusterPoolType, ClusterResourceSummary } from '../../interfaces'; import { getContexts, KubectlContext, setContext, inferCurrentClusterType } from '../../kubectl/kubectlUtils'; import { Kubectl } from '../../kubectl/kubectl'; import { Scriptable, ScriptingDictionary } from '../../scripting/scripting'; @@ -57,6 +57,7 @@ export class CreateClusterModel implements Scriptable { public getAllTargetClusterTypeInfo(): Thenable { let promise = new Promise(resolve => { let aksCluster: TargetClusterTypeInfo = { + enabled: false, type: TargetClusterType.NewAksCluster, name: localize('bdc-create.AKSClusterCardText', 'New AKS Cluster'), fullName: localize('bdc-create.AKSClusterFullName', 'New Azure Kubernetes Service cluster'), @@ -69,9 +70,10 @@ export class CreateClusterModel implements Scriptable { }; let existingCluster: TargetClusterTypeInfo = { + enabled: true, type: TargetClusterType.ExistingKubernetesCluster, name: localize('bdc-create.ExistingClusterCardText', 'Existing Cluster'), - fullName: localize('bdc-create.ExistingClusterFullName', 'Existing Kubernetes Cluster'), + fullName: localize('bdc-create.ExistingClusterFullName', 'Existing Kubernetes cluster'), description: localize('bdc-create.ExistingClusterDescription', 'This option assumes you already have a Kubernetes cluster installed, Once a prerequisite check is done, ensure the correct cluster context is selected.'), iconPath: { dark: 'images/kubernetes.svg', @@ -106,7 +108,7 @@ export class CreateClusterModel implements Scriptable { setTimeout(() => { let tools = this.targetClusterType === TargetClusterType.ExistingKubernetesCluster ? [kubeCtl, mssqlCtl] : [kubeCtl, mssqlCtl, azureCli]; resolve(tools); - }, 2000); + }, 1000); }); return promise; } @@ -117,7 +119,7 @@ export class CreateClusterModel implements Scriptable { tool.status = ToolInstallationStatus.Installed; this._tmp_tools_installed = true; resolve(); - }, 2000); + }, 1000); }); return promise; } @@ -126,6 +128,8 @@ export class CreateClusterModel implements Scriptable { return path.join(os.homedir(), '.kube', 'config'); } + public clusterName: string; + public targetClusterType: TargetClusterType; public selectedCluster: KubectlContext; @@ -156,6 +160,8 @@ export class CreateClusterModel implements Scriptable { public containerRegistryPassword: string; + public profile: ClusterProfile; + public async getTargetClusterPlatform(targetContextName: string): Promise { await setContext(this._kubectl, targetContextName); let clusterType = await inferCurrentClusterType(this._kubectl); @@ -207,4 +213,116 @@ export class CreateClusterModel implements Scriptable { public getTargetKubectlContext(): KubectlContext { return this.selectedCluster; } + + public getClusterResource(): Thenable { + let promise = new Promise(resolve => { + setTimeout(() => { + let resoureSummary: ClusterResourceSummary = { + hardwareLabels: [ + { + name: '', + totalNodes: 10, + totalCores: 22, + totalDisks: 128, + totalMemoryInGB: 77 + }, + { + name: '#data', + totalNodes: 4, + totalCores: 22, + totalDisks: 200, + totalMemoryInGB: 100 + }, + { + name: '#compute', + totalNodes: 12, + totalCores: 124, + totalDisks: 24, + totalMemoryInGB: 100 + }, + { + name: '#premium', + totalNodes: 10, + totalCores: 100, + totalDisks: 200, + totalMemoryInGB: 770 + } + ] + }; + resolve(resoureSummary); + }, 1000); + }); + return promise; + } + + public getProfiles(): Thenable { + let promise = new Promise(resolve => { + setTimeout(() => { + let profiles: ClusterProfile[] = []; + profiles.push({ + name: 'Basic', + sqlServerMasterConfiguration: this.createSQLPoolConfiguration(1, 1), + computePoolConfiguration: this.createComputePoolConfiguration(2), + dataPoolConfiguration: this.createDataPoolConfiguration(2), + storagePoolConfiguration: this.createStoragePoolConfiguration(2), + sparkPoolConfiguration: this.createSparkPoolConfiguration(2) + }); + profiles.push({ + name: 'Standard', + sqlServerMasterConfiguration: this.createSQLPoolConfiguration(3, 9), + computePoolConfiguration: this.createComputePoolConfiguration(5), + dataPoolConfiguration: this.createDataPoolConfiguration(5), + storagePoolConfiguration: this.createStoragePoolConfiguration(5), + sparkPoolConfiguration: this.createSparkPoolConfiguration(5) + }); + profiles.push({ + name: 'Premium', + sqlServerMasterConfiguration: this.createSQLPoolConfiguration(5, 9), + computePoolConfiguration: this.createComputePoolConfiguration(7), + dataPoolConfiguration: this.createDataPoolConfiguration(7), + storagePoolConfiguration: this.createStoragePoolConfiguration(7), + sparkPoolConfiguration: this.createSparkPoolConfiguration(7) + }); + resolve(profiles); + }, 1000); + }); + return promise; + } + + private createSQLPoolConfiguration(scale: number, maxScale: number): SQLServerMasterConfiguration { + return { + type: ClusterPoolType.SQL, + engineOnly: false, + scale: scale, + maxScale: maxScale + }; + } + + private createComputePoolConfiguration(scale: number): PoolConfiguration { + return { + type: ClusterPoolType.Compute, + scale: scale + }; + } + + private createDataPoolConfiguration(scale: number): PoolConfiguration { + return { + type: ClusterPoolType.Data, + scale: scale + }; + } + + private createStoragePoolConfiguration(scale: number): PoolConfiguration { + return { + type: ClusterPoolType.Storage, + scale: scale + }; + } + + private createSparkPoolConfiguration(scale: number): PoolConfiguration { + return { + type: ClusterPoolType.Spark, + scale: scale + }; + } } diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/createClusterWizard.ts b/extensions/big-data-cluster/src/wizards/create-cluster/createClusterWizard.ts index af5c85b848..e31cc25648 100644 --- a/extensions/big-data-cluster/src/wizards/create-cluster/createClusterWizard.ts +++ b/extensions/big-data-cluster/src/wizards/create-cluster/createClusterWizard.ts @@ -18,7 +18,7 @@ import { ScriptGenerator } from '../../scripting/scripting'; const localize = nls.loadMessageBundle(); export class CreateClusterWizard extends WizardBase { - private scripter : ScriptGenerator; + private scripter: ScriptGenerator; constructor(context: ExtensionContext, kubectl: Kubectl) { let model = new CreateClusterModel(kubectl); super(model, context, localize('bdc-create.wizardTitle', 'Create a big data cluster')); @@ -31,19 +31,24 @@ export class CreateClusterWizard extends WizardBase { - this.wizardObject.generateScriptButton.enabled = false; - this.scripter.generateDeploymentScript(this.model).then( () => { - this.wizardObject.generateScriptButton.enabled = true; - //TODO: Add error handling. - }); - }); - this.wizardObject.doneButton.onClick(() => { }); + this.registerDisposable(this.wizardObject.generateScriptButton.onClick(async () => { + this.wizardObject.generateScriptButton.enabled = false; + this.scripter.generateDeploymentScript(this.model).then(() => { + this.wizardObject.generateScriptButton.enabled = true; + //TODO: Add error handling. + }); + })); + } + + protected onCancel(): void { + } + + protected onOk(): void { } } diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/pages/clusterProfilePage.ts b/extensions/big-data-cluster/src/wizards/create-cluster/pages/clusterProfilePage.ts index e155fb1b3e..f847e05a4b 100644 --- a/extensions/big-data-cluster/src/wizards/create-cluster/pages/clusterProfilePage.ts +++ b/extensions/big-data-cluster/src/wizards/create-cluster/pages/clusterProfilePage.ts @@ -5,29 +5,387 @@ 'use strict'; import * as azdata from 'azdata'; +import * as vscode from 'vscode'; import { WizardPageBase } from '../../wizardPageBase'; import { CreateClusterWizard } from '../createClusterWizard'; import * as nls from 'vscode-nls'; +import { ClusterProfile, PoolConfiguration, ClusterPoolType, SQLServerMasterConfiguration, ClusterResourceSummary } from '../../../interfaces'; const localize = nls.loadMessageBundle(); +const LabelWidth = '200px'; +const InputWidth = '300px'; export class ClusterProfilePage extends WizardPageBase { + private view: azdata.ModelView; + private clusterProfiles: ClusterProfile[]; + private poolList: azdata.FlexContainer; + private detailContainer: azdata.FlexContainer; + private clusterResourceView: azdata.GroupContainer; + private poolListMap = {}; + private clusterResourceContainer: azdata.FlexContainer; + private clusterResourceLoadingComponent: azdata.LoadingComponent; + private clusterResource: ClusterResourceSummary; + + constructor(wizard: CreateClusterWizard) { super(localize('bdc-create.clusterProfilePageTitle', 'Select a cluster profile'), localize('bdc-create.clusterProfilePageDescription', 'Select your requirement and we will provide you a pre-defined default scaling. You can later go to cluster configuration and customize it.'), wizard); } - public onEnter() { + public onEnter(): void { + this.updatePoolList(); + this.clusterResourceLoadingComponent.loading = true; + this.wizard.model.getClusterResource().then((resource) => { + this.clusterResource = resource; + this.initializeClusterResourceView(); + }); this.wizard.wizardObject.registerNavigationValidator(() => { return true; }); } protected initialize(view: azdata.ModelView): Thenable { - let formBuilder = view.modelBuilder.formContainer(); - let form = formBuilder.component(); - return view.initializeModel(form); + this.view = view; + let fetchProfilePromise = this.wizard.model.getProfiles().then(p => { this.clusterProfiles = p; }); + return Promise.all([fetchProfilePromise]).then(() => { + this.wizard.model.profile = this.clusterProfiles[0]; + this.clusterResourceView = this.view.modelBuilder.groupContainer().withLayout({ + header: localize('bdc-create.TargetClusterOverview', 'Target cluster scale overview'), + collapsed: true, + collapsible: true + }).component(); + + this.clusterResourceContainer = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + this.clusterResourceLoadingComponent = this.view.modelBuilder.loadingComponent().withItem(this.clusterResourceContainer).component(); + this.clusterResourceView.addItem(this.clusterResourceLoadingComponent); + + let profileLabel = view.modelBuilder.text().withProperties({ value: localize('bdc-create.clusterProfileLabel', 'Deployment profile') }).component(); + let profileDropdown = view.modelBuilder.dropDown().withProperties({ + values: this.clusterProfiles.map(profile => profile.name), + width: '300px' + }).component(); + let dropdownRow = this.view.modelBuilder.flexContainer().withItems([profileLabel, profileDropdown], { CSSStyles: { 'margin-right': '30px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + let poolContainer = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', width: '100%', height: '100%' }).component(); + this.poolList = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '300px', height: '100%' }).component(); + poolContainer.addItem(this.poolList, { + CSSStyles: { + 'border-top-style': 'solid', + 'border-top-width': '2px', + 'border-right-style': 'solid', + 'border-right-width': '2px', + 'border-color': 'lightgray' + } + }); + + this.detailContainer = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '760px', height: '100%' }).component(); + poolContainer.addItem(this.detailContainer, { + CSSStyles: { + 'border-top-style': 'solid', + 'border-top-width': '2px', + 'border-color': 'lightgray' + } + }); + + this.wizard.registerDisposable(profileDropdown.onValueChanged(() => { + let profiles = this.clusterProfiles.filter(p => profileDropdown.value === p.name); + if (profiles && profiles.length === 1) { + this.wizard.model.profile = profiles[0]; + this.updatePoolList(); + this.clearPoolDetail(); + } + })); + + this.initializePoolList(); + + let pageContainer = this.view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + height: '800px' + }).component(); + pageContainer.addItem(this.clusterResourceView, { + flex: '0 0 auto', + CSSStyles: { + 'margin-bottom': '20px', + 'padding-bottom': '5px', + 'padding-top': '5px' + } + }); + pageContainer.addItem(dropdownRow, { + flex: '0 0 auto', + CSSStyles: { 'margin-bottom': '10px' } + }); + pageContainer.addItem(poolContainer, { + flex: '1 1 auto', + CSSStyles: { + 'display': 'flex' + } + }); + let formBuilder = view.modelBuilder.formContainer(); + let form = formBuilder.withFormItems([{ + title: '', + component: pageContainer + }], { + horizontal: false, + componentWidth: '100%' + }).component(); + + return view.initializeModel(form); + }); + } + + private initializeClusterResourceView(): void { + this.clusterResourceContainer.clearItems(); + let text = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.HardwareProfileText', 'Hardware profile') }).component(); + let height = (this.clusterResource.hardwareLabels.length * 25) + 30; + let labelColumn: azdata.TableColumn = { + value: localize('bdc-create.HardwareLabelColumnName', 'Label'), + width: 100 + }; + let totalNodesColumn: azdata.TableColumn = { + value: localize('bdc-create.TotalNodesColumnName', 'Nodes'), + width: 50 + }; + let totalCoresColumn: azdata.TableColumn = { + value: localize('bdc-create.TotalCoresColumnName', 'Cores'), + width: 50 + }; + let totalMemoryColumn: azdata.TableColumn = { + value: localize('bdc-create.TotalMemoryColumnName', 'Memory'), + width: 50 + }; + let totalDisksColumn: azdata.TableColumn = { + value: localize('bdc-create.TotalDisksColumnName', 'Disks'), + width: 50 + }; + + let table = this.view.modelBuilder.table().withProperties({ + height: `${height}px`, + data: this.clusterResource.hardwareLabels.map(label => [label.name, label.totalNodes, label.totalCores, label.totalMemoryInGB, label.totalDisks]), + columns: [labelColumn, totalNodesColumn, totalCoresColumn, totalMemoryColumn, totalDisksColumn], + width: '300px' + + }).component(); + this.clusterResourceContainer.addItems([text, table]); + this.clusterResourceLoadingComponent.loading = false; + } + + private initializePoolList(): void { + let pools = [this.wizard.model.profile.sqlServerMasterConfiguration, + this.wizard.model.profile.computePoolConfiguration, + this.wizard.model.profile.dataPoolConfiguration, + this.wizard.model.profile.sparkPoolConfiguration, + this.wizard.model.profile.storagePoolConfiguration]; + pools.forEach(pool => { + let poolSummaryButton = this.view.modelBuilder.divContainer().withProperties({ clickable: true }).component(); + let container = this.view.modelBuilder.flexContainer().component(); + this.wizard.registerDisposable(poolSummaryButton.onDidClick(() => { + this.clearPoolDetail(); + let currentPool: PoolConfiguration; + switch (pool.type) { + case ClusterPoolType.SQL: + currentPool = this.wizard.model.profile.sqlServerMasterConfiguration; + break; + case ClusterPoolType.Compute: + currentPool = this.wizard.model.profile.computePoolConfiguration; + break; + case ClusterPoolType.Data: + currentPool = this.wizard.model.profile.dataPoolConfiguration; + break; + case ClusterPoolType.Storage: + currentPool = this.wizard.model.profile.storagePoolConfiguration; + break; + case ClusterPoolType.Spark: + currentPool = this.wizard.model.profile.sparkPoolConfiguration; + break; + default: + break; + } + if (currentPool) { + this.detailContainer.addItem(this.createPoolConfigurationPart(currentPool), { CSSStyles: { 'margin-left': '10px' } }); + } + })); + + let text = this.view.modelBuilder.text().component(); + this.poolListMap[pool.type] = text; + text.width = '250px'; + let chrevron = this.view.modelBuilder.text().withProperties({ value: '>' }).component(); + chrevron.width = '30px'; + container.addItem(text); + container.addItem(chrevron, { + CSSStyles: { + 'font-size': '20px', + 'line-height': '0px' + } + }); + poolSummaryButton.addItem(container); + this.poolList.addItem(poolSummaryButton, { + CSSStyles: { + 'border-bottom-style': 'solid', + 'border-bottom-width': '1px', + 'border-color': 'lightgray', + 'cursor': 'pointer' + } + }); + }); + } + + private createPoolConfigurationPart(configuration: PoolConfiguration): azdata.Component { + let container = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + switch (configuration.type) { + case ClusterPoolType.SQL: + this.createSQLConfigurationPart(container, configuration as SQLServerMasterConfiguration); + break; + default: + this.createDefaultPoolConfigurationPart(container, configuration); + break; + } + return container; + } + + private createSQLConfigurationPart(container: azdata.FlexContainer, configuration: SQLServerMasterConfiguration): void { + this.createDefaultPoolConfigurationPart(container, configuration); + this.addFeatureSetRow(container, configuration); + } + + private createDefaultPoolConfigurationPart(container: azdata.FlexContainer, configuration: PoolConfiguration): void { + this.addPoolNameLabel(container, this.getPoolDisplayName(configuration.type)); + this.addPoolDescriptionLabel(container, this.getPoolDescription(configuration.type)); + this.addScaleRow(container, configuration); + this.addHardwareLabelRow(container, configuration); + } + + private addPoolNameLabel(container: azdata.FlexContainer, text: string): void { + let poolNameLabel = this.view.modelBuilder.text().withProperties({ value: text }).component(); + container.addItem(poolNameLabel, { + flex: '0 0 auto', CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } + }); + } + + private addPoolDescriptionLabel(container: azdata.FlexContainer, text: string): void { + let label = this.view.modelBuilder.text().withProperties({ value: text }).component(); + container.addItem(label, { + flex: '0 0 auto', + CSSStyles: { + 'margin-bottom': '20px' + } + }); + } + + private addScaleRow(container: azdata.FlexContainer, configuration: PoolConfiguration): void { + let label = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.ScaleLabel', 'Scale') }).component(); + label.width = LabelWidth; + let input = this.view.modelBuilder.inputBox().withProperties({ + inputType: 'number', + value: configuration.scale.toString(), + min: 1, + max: configuration.maxScale + }).component(); + + this.wizard.registerDisposable(input.onTextChanged(() => { + configuration.scale = Number(input.value); + this.updatePoolList(); + })); + input.width = InputWidth; + let row = this.createRow([label, input]); + container.addItem(row); + } + + private addHardwareLabelRow(container: azdata.FlexContainer, configuration: PoolConfiguration): void { + let label = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.HardwareProfileLabel', 'Hardware profile label') }).component(); + label.width = LabelWidth; + let optionalValues = this.clusterResource.hardwareLabels.map(label => label.name); + configuration.hardwareLabel = configuration.hardwareLabel ? configuration.hardwareLabel : optionalValues[0]; + let input = this.view.modelBuilder.dropDown().withProperties({ value: configuration.hardwareLabel, values: optionalValues }).component(); + this.wizard.registerDisposable(input.onValueChanged(() => { + configuration.hardwareLabel = input.value.toString(); + })); + input.width = InputWidth; + let row = this.createRow([label, input]); + container.addItem(row); + } + + private addFeatureSetRow(container: azdata.FlexContainer, configuration: SQLServerMasterConfiguration): void { + const radioGroupName = 'featureset'; + let label = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.FeatureSetLabel', 'Feature set') }).component(); + label.width = LabelWidth; + let engineOnlyOption = this.view.modelBuilder.radioButton().withProperties({ label: localize('bdc-create.EngineOnlyText', 'Engine only'), name: radioGroupName, checked: configuration.engineOnly }).component(); + let engineWithFeaturesOption = this.view.modelBuilder.radioButton().withProperties({ label: localize('bdc-create.EngineWithFeaturesText', 'Engine with optional features'), name: radioGroupName, checked: !configuration.engineOnly }).component(); + let optionContainer = this.view.modelBuilder.divContainer().component(); + optionContainer.width = InputWidth; + optionContainer.addItems([engineOnlyOption, engineWithFeaturesOption]); + container.addItem(this.createRow([label, optionContainer])); + this.wizard.registerDisposable(engineOnlyOption.onDidClick(() => { + configuration.engineOnly = true; + })); + this.wizard.registerDisposable(engineWithFeaturesOption.onDidClick(() => { + configuration.engineOnly = false; + })); + } + + private createRow(items: azdata.Component[]): azdata.FlexContainer { + return this.view.modelBuilder.flexContainer().withItems(items, { + CSSStyles: { + 'margin-right': '5px' + } + }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + } + + private getPoolDisplayName(poolType: ClusterPoolType): string { + switch (poolType) { + case ClusterPoolType.SQL: + return localize('bdc-create.SQLServerMasterDisplayName', 'SQL Server master'); + case ClusterPoolType.Compute: + return localize('bdc-create.ComputePoolDisplayName', 'Compute pool'); + case ClusterPoolType.Data: + return localize('bdc-create.DataPoolDisplayName', 'Data pool'); + case ClusterPoolType.Storage: + return localize('bdc-create.StoragePoolDisplayName', 'Storage pool'); + case ClusterPoolType.Spark: + return localize('bdc-create.SparkPoolDisplayName', 'Spark pool'); + default: + throw 'unknown pool type'; + } + } + + private getPoolDescription(poolType: ClusterPoolType): string { + switch (poolType) { + case ClusterPoolType.SQL: + return localize('bdc-create.SQLServerMasterDescription', 'The SQL Server instance provides an externally accessible TDS endpoint for the cluster'); + case ClusterPoolType.Compute: + return localize('bdc-create.ComputePoolDescription', 'TODO: Add description'); + case ClusterPoolType.Data: + return localize('bdc-create.DataPoolDescription', 'TODO: Add description'); + case ClusterPoolType.Storage: + return localize('bdc-create.StoragePoolDescription', 'TODO: Add description'); + case ClusterPoolType.Spark: + return localize('bdc-create.SparkPoolDescription', 'TODO: Add description'); + default: + throw 'unknown pool type'; + } + } + + private updatePoolList(): void { + let pools = [this.wizard.model.profile.sqlServerMasterConfiguration, + this.wizard.model.profile.computePoolConfiguration, + this.wizard.model.profile.dataPoolConfiguration, + this.wizard.model.profile.sparkPoolConfiguration, + this.wizard.model.profile.storagePoolConfiguration]; + pools.forEach(pool => { + let text = this.poolListMap[pool.type] as azdata.TextComponent; + if (text) { + text.value = localize({ + key: 'bdc-create.poolLabelTemplate', + comment: ['{0} is the pool name, {1} is the scale number'] + }, '{0} ({1})', this.getPoolDisplayName(pool.type), pool.scale); + } + }); + } + + private clearPoolDetail(): void { + this.detailContainer.clearItems(); } } diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/pages/selectExistingClusterPage.ts b/extensions/big-data-cluster/src/wizards/create-cluster/pages/selectExistingClusterPage.ts index f78e97acd6..e996439d13 100644 --- a/extensions/big-data-cluster/src/wizards/create-cluster/pages/selectExistingClusterPage.ts +++ b/extensions/big-data-cluster/src/wizards/create-cluster/pages/selectExistingClusterPage.ts @@ -21,7 +21,7 @@ export class SelectExistingClusterPage extends WizardPageBase { + this.existingClusterControl.addItem(configFileContainer, { CSSStyles: { 'margin-top': '0px' } }); + this.existingClusterControl.addItem(clusterContextContainer, { + CSSStyles: { 'margin- top': '10px' } + }); + + this.wizard.registerDisposable(this.browseFileButton.onDidClick(async () => { let fileUris = await vscode.window.showOpenDialog( { canSelectFiles: true, @@ -112,12 +121,12 @@ export class SelectExistingClusterPage extends WizardPageBase { @@ -140,17 +149,15 @@ export class SelectExistingClusterPage extends WizardPageBase { + this.wizard.registerDisposable(option.onDidClick(() => { self.wizard.model.selectedCluster = cluster; self.wizard.wizardObject.message = null; - }); + })); return option; }); - - self.clusterContextContainer.addItem(self.clusterContextsLabel); - self.clusterContextContainer.addItems(options); + self.clusterContextList.addItems(options); } else { - self.clusterContextContainer.addItem(this.errorLoadingClustersLabel); + self.clusterContextList.addItem(this.errorLoadingClustersLabel); } this.clusterContextLoadingComponent.loading = false; } diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/pages/selectTargetClusterTypePage.ts b/extensions/big-data-cluster/src/wizards/create-cluster/pages/selectTargetClusterTypePage.ts index f1149e81d3..0ae97e0067 100644 --- a/extensions/big-data-cluster/src/wizards/create-cluster/pages/selectTargetClusterTypePage.ts +++ b/extensions/big-data-cluster/src/wizards/create-cluster/pages/selectTargetClusterTypePage.ts @@ -35,7 +35,7 @@ export class SelectTargetClusterTypePage extends WizardPageBase { + this.wizard.registerDisposable(this.installToolsButton.onClick(async () => { this.wizard.wizardObject.message = null; this.installToolsButton.label = InstallingButtonText; this.installToolsButton.enabled = false; @@ -53,14 +53,14 @@ export class SelectTargetClusterTypePage extends WizardPageBase { + this.wizard.registerDisposable(this.refreshToolsButton.onClick(() => { this.updateRequiredToolStatus(); - }); + })); this.wizard.addButton(this.refreshToolsButton); } @@ -115,7 +115,6 @@ export class SelectTargetClusterTypePage extends WizardPageBase({ cardType: azdata.CardType.VerticalButton, iconPath: { dark: self.wizard.context.asAbsolutePath(targetClusterTypeInfo.iconPath.dark), light: self.wizard.context.asAbsolutePath(targetClusterTypeInfo.iconPath.light) }, - label: targetClusterTypeInfo.name + label: targetClusterTypeInfo.name, + descriptions: descriptions }).component(); - card.onCardSelectedChanged(() => { - if (card.selected) { - self.wizard.wizardObject.message = null; - self.wizard.model.targetClusterType = targetClusterTypeInfo.type; - self.cards.forEach(c => { - if (c !== card) { - c.selected = false; - } - }); - self.targetDescriptionText.value = targetClusterTypeInfo.description; + card.enabled = targetClusterTypeInfo.enabled; - if (self.form.items.length === 1) { - self.formBuilder.addFormItem({ - title: localize('bdc-create.RequiredToolsText', 'Required tools'), - component: self.toolsLoadingWrapper - }); - } else { - self.formBuilder.removeFormItem(self.targetDescriptionGroup); - } - - self.targetDescriptionGroup = { - title: targetClusterTypeInfo.fullName, - component: self.targetDescriptionText - }; - self.formBuilder.insertFormItem(self.targetDescriptionGroup, 1); - - self.updateRequiredToolStatus(); - } else { - if (self.cards.filter(c => { return c !== card && c.selected; }).length === 0) { - card.selected = true; - } - } - }); + self.wizard.registerDisposable(card.onCardSelectedChanged(() => { + self.onCardSelected(card, targetClusterTypeInfo); + })); return card; } + private onCardSelected(card: azdata.CardComponent, targetClusterTypeInfo: TargetClusterTypeInfo): void { + let self = this; + if (card.selected) { + self.wizard.wizardObject.message = null; + self.wizard.model.targetClusterType = targetClusterTypeInfo.type; + self.cards.forEach(c => { + if (c !== card) { + c.selected = false; + } + }); + + self.targetDescriptionText.value = targetClusterTypeInfo.description; + + if (self.form.items.length === 1) { + self.formBuilder.addFormItem({ + title: localize('bdc-create.RequiredToolsText', 'Required tools'), + component: self.toolsLoadingWrapper + }); + } else { + self.formBuilder.removeFormItem(self.targetDescriptionGroup); + } + + self.targetDescriptionGroup = { + title: targetClusterTypeInfo.fullName, + component: self.targetDescriptionText + }; + self.formBuilder.insertFormItem(self.targetDescriptionGroup, 1); + + self.updateRequiredToolStatus(); + } else { + if (self.cards.filter(c => { return c !== card && c.selected; }).length === 0) { + card.selected = true; + } + } + } + private updateRequiredToolStatus(): Thenable { this.isLoading = true; this.installToolsButton.hidden = false; diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/pages/settingsPage.ts b/extensions/big-data-cluster/src/wizards/create-cluster/pages/settingsPage.ts index 147fe8c954..e6dd775666 100644 --- a/extensions/big-data-cluster/src/wizards/create-cluster/pages/settingsPage.ts +++ b/extensions/big-data-cluster/src/wizards/create-cluster/pages/settingsPage.ts @@ -16,12 +16,32 @@ const PortInputWidth = '100px'; const RestoreDefaultValuesText = localize('bdc-create.RestoreDefaultValuesText', 'Restore Default Values'); export class SettingsPage extends WizardPageBase { + private acceptEulaCheckbox: azdata.CheckBoxComponent; + constructor(wizard: CreateClusterWizard) { super(localize('bdc-create.settingsPageTitle', 'Settings'), localize('bdc-create.settingsPageDescription', 'Configure the settings required for deploying SQL Server big data cluster'), wizard); } + public onEnter(): void { + this.wizard.wizardObject.registerNavigationValidator((e) => { + if (e.lastPage > e.newPage) { + this.wizard.wizardObject.message = null; + return true; + } + if (!this.acceptEulaCheckbox.checked) { + this.wizard.wizardObject.message = { + text: localize('bdc-create.EulaNotAccepted', 'You need to accept the terms of services and privacy policy in order to proceed'), + level: azdata.window.MessageLevel.Error + }; + } else { + this.wizard.wizardObject.message = null; + } + return this.acceptEulaCheckbox.checked; + }); + } + protected initialize(view: azdata.ModelView): Thenable { let clusterPorts: ClusterPorts; let containerRegistryInfo: ContainerRegistryInfo; @@ -37,6 +57,14 @@ export class SettingsPage extends WizardPageBase { let formBuilder = view.modelBuilder.formContainer(); // User settings + let clusterNameInput = this.createInputWithLabel(view, { + label: localize('bdc-create.ClusterName', 'Cluster name'), + inputWidth: UserNameInputWidth, + isRequiredField: true + }, (input) => { + this.wizard.model.clusterName = input.value; + }); + let adminUserNameInput = this.createInputWithLabel(view, { label: localize('bdc-create.AdminUsernameText', 'Admin username'), isRequiredField: true, @@ -106,14 +134,14 @@ export class SettingsPage extends WizardPageBase { label: RestoreDefaultValuesText, width: 200 }).component(); - restorePortSettingsButton.onDidClick(() => { + this.wizard.registerDisposable(restorePortSettingsButton.onDidClick(() => { sqlPortInput.input.value = clusterPorts.sql; knoxPortInput.input.value = clusterPorts.knox; controllerPortInput.input.value = clusterPorts.controller; proxyPortInput.input.value = clusterPorts.proxy; grafanaPortInput.input.value = clusterPorts.grafana; kibanaPortInput.input.value = clusterPorts.kibana; - }); + })); // Container Registry Settings const registryUserNamePasswordHintText = localize('bdc-create.RegistryUserNamePasswordHintText', 'only required for private registries'); @@ -166,18 +194,18 @@ export class SettingsPage extends WizardPageBase { label: RestoreDefaultValuesText, width: 200 }).component(); - restoreContainerSettingsButton.onDidClick(() => { + this.wizard.registerDisposable(restoreContainerSettingsButton.onDidClick(() => { registryInput.input.value = containerRegistryInfo.registry; repositoryInput.input.value = containerRegistryInfo.repository; imageTagInput.input.value = containerRegistryInfo.imageTag; - }); + })); - let basicSettingsGroup = view.modelBuilder.groupContainer().withItems([adminUserNameInput.row, adminPasswordInput.row]).withLayout({ header: localize('bdc-create.BasicSettingsText', 'Basic Settings'), collapsible: true }).component(); + let basicSettingsGroup = view.modelBuilder.groupContainer().withItems([clusterNameInput.row, adminUserNameInput.row, adminPasswordInput.row]).withLayout({ header: localize('bdc-create.BasicSettingsText', 'Basic Settings'), collapsible: true }).component(); let containerSettingsGroup = view.modelBuilder.groupContainer().withItems([registryInput.row, repositoryInput.row, imageTagInput.row, registryUserNameInput.row, registryPasswordInput.row, restoreContainerSettingsButton]).withLayout({ header: localize('bdc-create.ContainerRegistrySettings', 'Container Registry Settings'), collapsible: true }).component(); let portSettingsGroup = view.modelBuilder.groupContainer().withItems([sqlPortInput.row, knoxPortInput.row, controllerPortInput.row, proxyPortInput.row, grafanaPortInput.row, kibanaPortInput.row, restorePortSettingsButton]).withLayout({ header: localize('bdc-create.PortSettings', 'Port Settings (Optional)'), collapsible: true, collapsed: true }).component(); - let acceptEulaCheckbox = view.modelBuilder.checkBox().component(); - acceptEulaCheckbox.checked = false; + this.acceptEulaCheckbox = view.modelBuilder.checkBox().component(); + this.acceptEulaCheckbox.checked = false; let eulaLink: azdata.LinkArea = { text: localize('bdc-create.LicenseTerms', 'license terms'), @@ -196,14 +224,13 @@ export class SettingsPage extends WizardPageBase { links: [eulaLink, privacyPolicyLink] }).component(); - let eulaContainer = this.createRow(view, [acceptEulaCheckbox, checkboxText]); + let eulaContainer = this.createRow(view, [this.acceptEulaCheckbox, checkboxText]); let form = formBuilder.withFormItems([ { title: '', component: eulaContainer - }, - { + }, { title: '', component: basicSettingsGroup }, { @@ -234,9 +261,9 @@ export class SettingsPage extends WizardPageBase { input.width = options.inputWidth; text.width = '150px'; input.placeHolder = options.placeHolder; - input.onTextChanged(() => { + this.wizard.registerDisposable(input.onTextChanged(() => { textChangedHandler(input); - }); + })); input.value = options.initialValue; let row = this.createRow(view, [text, input]); return { diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/pages/summaryPage.ts b/extensions/big-data-cluster/src/wizards/create-cluster/pages/summaryPage.ts index fbe30de742..48e51a4a84 100644 --- a/extensions/big-data-cluster/src/wizards/create-cluster/pages/summaryPage.ts +++ b/extensions/big-data-cluster/src/wizards/create-cluster/pages/summaryPage.ts @@ -10,23 +10,95 @@ import { CreateClusterWizard } from '../createClusterWizard'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); +const LabelWidth = '250px'; export class SummaryPage extends WizardPageBase { + private view: azdata.ModelView; + private targetTypeText: azdata.TextComponent; + private targetClusterContextText: azdata.TextComponent; + private clusterNameText: azdata.TextComponent; + private clusterAdminUsernameText: azdata.TextComponent; + private acceptEulaText: azdata.TextComponent; + private deploymentProfileText: azdata.TextComponent; + private sqlServerMasterScaleText: azdata.TextComponent; + private storagePoolScaleText: azdata.TextComponent; + private computePoolScaleText: azdata.TextComponent; + private dataPoolScaleText: azdata.TextComponent; + private sparkPoolScaleText: azdata.TextComponent; + constructor(wizard: CreateClusterWizard) { super(localize('bdc-create.summaryPageTitle', 'Summary'), '', wizard); } protected initialize(view: azdata.ModelView): Thenable { + this.view = view; + let targetClusterInfoGroup = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + let bdcClusterInfoGroup = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + this.targetTypeText = this.view.modelBuilder.text().component(); + this.targetClusterContextText = this.view.modelBuilder.text().component(); + this.clusterNameText = this.view.modelBuilder.text().component(); + this.clusterAdminUsernameText = this.view.modelBuilder.text().component(); + this.acceptEulaText = this.view.modelBuilder.text().component(); + this.deploymentProfileText = this.view.modelBuilder.text().component(); + this.sqlServerMasterScaleText = this.view.modelBuilder.text().component(); + this.storagePoolScaleText = this.view.modelBuilder.text().component(); + this.computePoolScaleText = this.view.modelBuilder.text().component(); + this.dataPoolScaleText = this.view.modelBuilder.text().component(); + this.sparkPoolScaleText = this.view.modelBuilder.text().component(); + targetClusterInfoGroup.addItem(this.createRow(localize('bdc-create.TargetClusterTypeText', 'Cluster type'), this.targetTypeText)); + targetClusterInfoGroup.addItem(this.createRow(localize('bdc-create.ClusterContextText', 'Cluster context'), this.targetClusterContextText)); + + bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.ClusterNameText', 'Cluster name'), this.clusterNameText)); + bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.ClusterAdminUsernameText', 'Cluster Admin username'), this.clusterAdminUsernameText)); + bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.AcceptEulaText', 'Accept license agreement'), this.acceptEulaText)); + bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.DeploymentProfileText', 'Deployment profile'), this.deploymentProfileText)); + bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.SqlServerMasterScaleText', 'SQL Server master scale'), this.sqlServerMasterScaleText)); + bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.ComputePoolScaleText', 'Compute pool scale'), this.computePoolScaleText)); + bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.DataPoolScaleText', 'Data pool scale'), this.dataPoolScaleText)); + bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.StoragePoolScaleText', 'Storage pool scale'), this.storagePoolScaleText)); + bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.SparkPoolScaleText', 'Spark pool scale'), this.sparkPoolScaleText)); + let formBuilder = view.modelBuilder.formContainer(); - let form = formBuilder.component(); + let form = formBuilder.withFormItems([{ + title: localize('bdc-create.TargetClusterGroupTitle', 'TARGET CLUSTER'), + component: targetClusterInfoGroup + }, { + title: localize('bdc-create.BigDataClusterGroupTitle', 'SQL SERVER BIG DATA CLUSTER'), + component: bdcClusterInfoGroup + }]).component(); + return view.initializeModel(form); } public onEnter(): void { + this.wizard.model.getAllTargetClusterTypeInfo().then((clusterTypes) => { + let selectedClusterType = clusterTypes.filter(clusterType => clusterType.type === this.wizard.model.targetClusterType)[0]; + this.targetTypeText.value = selectedClusterType.fullName; + this.targetClusterContextText.value = this.wizard.model.selectedCluster.contextName; + this.clusterNameText.value = this.wizard.model.clusterName; + this.clusterAdminUsernameText.value = this.wizard.model.adminUserName; + this.acceptEulaText.value = localize('bdc-create.YesText', 'Yes'); + this.deploymentProfileText.value = this.wizard.model.profile.name; + this.sqlServerMasterScaleText.value = this.wizard.model.profile.sqlServerMasterConfiguration.scale.toString(); + this.computePoolScaleText.value = this.wizard.model.profile.computePoolConfiguration.scale.toString(); + this.dataPoolScaleText.value = this.wizard.model.profile.dataPoolConfiguration.scale.toString(); + this.storagePoolScaleText.value = this.wizard.model.profile.storagePoolConfiguration.scale.toString(); + this.sparkPoolScaleText.value = this.wizard.model.profile.sparkPoolConfiguration.scale.toString(); + + }); this.wizard.wizardObject.generateScriptButton.hidden = false; } public onLeave(): void { this.wizard.wizardObject.generateScriptButton.hidden = true; } + + private createRow(label: string, textComponent: azdata.TextComponent): azdata.FlexContainer { + let row = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'baseline' }).component(); + let labelComponent = this.view.modelBuilder.text().withProperties({ value: label }).component(); + labelComponent.width = LabelWidth; + textComponent.width = LabelWidth; + row.addItems([labelComponent, textComponent]); + return row; + } } diff --git a/extensions/big-data-cluster/src/wizards/wizardBase.ts b/extensions/big-data-cluster/src/wizards/wizardBase.ts index 1183d58c13..724329552d 100644 --- a/extensions/big-data-cluster/src/wizards/wizardBase.ts +++ b/extensions/big-data-cluster/src/wizards/wizardBase.ts @@ -5,15 +5,17 @@ 'use strict'; import * as azdata from 'azdata'; -import { ExtensionContext } from 'vscode'; +import { ExtensionContext, Disposable } from 'vscode'; import { WizardPageBase } from './wizardPageBase'; -export abstract class WizardBase { +export abstract class WizardBase { public wizardObject: azdata.window.Wizard; private customButtons: azdata.window.Button[]; private pages: WizardPageBase[]; + private toDispose: Disposable[] = []; + constructor(public model: T, public context: ExtensionContext, private title: string) { this.customButtons = []; } @@ -22,17 +24,33 @@ export abstract class WizardBase { this.wizardObject = azdata.window.createWizard(this.title); this.initialize(); this.wizardObject.customButtons = this.customButtons; - this.wizardObject.onPageChanged((e) => { + this.toDispose.push(this.wizardObject.onPageChanged((e) => { let previousPage = this.pages[e.lastPage]; let newPage = this.pages[e.newPage]; previousPage.onLeave(); newPage.onEnter(); + })); + + this.toDispose.push(this.wizardObject.doneButton.onClick(() => { + this.onOk(); + this.dispose(); + })); + this.toDispose.push(this.wizardObject.cancelButton.onClick(() => { + this.onCancel(); + this.dispose(); + })); + + return this.wizardObject.open().then(() => { + if (this.pages && this.pages.length > 0) { + this.pages[0].onEnter(); + } }); - return this.wizardObject.open(); } protected abstract initialize(): void; + protected abstract onOk(): void; + protected abstract onCancel(): void; public addButton(button: azdata.window.Button) { this.customButtons.push(button); @@ -42,4 +60,17 @@ export abstract class WizardBase { this.wizardObject.pages = pages.map(p => p.pageObject); this.pages = pages; } + + private dispose() { + this.toDispose.forEach((disposable: Disposable) => { + try { + disposable.dispose(); + } + catch{ } + }); + } + + public registerDisposable(disposable: Disposable): void { + this.toDispose.push(disposable); + } } diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index f7d0d7f6f8..282f96fab7 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -2760,6 +2760,10 @@ declare module 'azdata' { } export interface DivContainer extends Container, DivContainerProperties { + /** + * An event called when the div is clicked + */ + onDidClick: vscode.Event; } export interface FlexContainer extends Container { @@ -3023,6 +3027,11 @@ declare module 'azdata' { * This is used when its child component is webview */ yOffsetChange?: number; + + /** + * Indicates whether the element is clickable + */ + clickable?: boolean; } export interface CardComponent extends Component, CardProperties { diff --git a/src/sql/parts/modelComponents/card.component.html b/src/sql/parts/modelComponents/card.component.html index 350f88a426..b166fdfcb7 100644 --- a/src/sql/parts/modelComponents/card.component.html +++ b/src/sql/parts/modelComponents/card.component.html @@ -1,5 +1,5 @@ -
+
@@ -13,6 +13,9 @@

{{label}}

+
+
{{desc}}
+
@@ -25,7 +28,8 @@ {{action.label}} - {{action.actionTitle}} + {{action.actionTitle}} diff --git a/src/sql/parts/modelComponents/card.component.ts b/src/sql/parts/modelComponents/card.component.ts index 8fc299469e..279a268f8f 100644 --- a/src/sql/parts/modelComponents/card.component.ts +++ b/src/sql/parts/modelComponents/card.component.ts @@ -103,7 +103,7 @@ export default class CardComponent extends ComponentWithIconBase implements ICom } private get selectable(): boolean { - return this.cardType === 'VerticalButton' || this.cardType === 'ListItem'; + return this.enabled && (this.cardType === 'VerticalButton' || this.cardType === 'ListItem'); } // CSS-bound properties diff --git a/src/sql/parts/modelComponents/divContainer.component.ts b/src/sql/parts/modelComponents/divContainer.component.ts index daace63a9b..a114771636 100644 --- a/src/sql/parts/modelComponents/divContainer.component.ts +++ b/src/sql/parts/modelComponents/divContainer.component.ts @@ -9,7 +9,7 @@ import { ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList, } from '@angular/core'; -import { IComponent, IComponentDescriptor, IModelStore } from 'sql/parts/modelComponents/interfaces'; +import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces'; import * as azdata from 'azdata'; import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service'; @@ -17,6 +17,8 @@ import { ContainerBase } from 'sql/parts/modelComponents/componentBase'; import { ModelComponentWrapper } from 'sql/parts/modelComponents/modelComponentWrapper.component'; import types = require('vs/base/common/types'); +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; class DivItem { constructor(public descriptor: IComponentDescriptor, public config: azdata.DivItemLayout) { } @@ -24,7 +26,7 @@ class DivItem { @Component({ template: ` -
+
@@ -76,17 +78,24 @@ export default class DivContainer extends ContainerBase im private updateOverflowY() { this._overflowY = this.overflowY; if (this._overflowY) { - let element = this.divContainer.nativeElement; + let element = this.divContainer.nativeElement; element.style.overflowY = this._overflowY; } } private updateScroll() { - let element = this.divContainer.nativeElement; + let element = this.divContainer.nativeElement; element.scrollTop = element.scrollTop - this.yOffsetChange; element.dispatchEvent(new Event('scroll')); } + private onClick() { + this.fireEvent({ + eventType: ComponentEventType.onDidClick, + args: undefined + }); + } + // CSS-bound properties public get height(): string { return this._height; @@ -111,6 +120,22 @@ export default class DivContainer extends ContainerBase im this.setPropertyFromUI((properties, newValue) => { properties.yOffsetChange = newValue; }, newValue); } + public get clickable(): boolean { + return this.getPropertyOrDefault((props) => props.clickable, false); + } + + public get tabIndex(): number { + return this.clickable ? 0 : -1; + } + + private onKey(e: KeyboardEvent) { + let event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + this.onClick(); + e.stopPropagation(); + } + } + private getItemOrder(item: DivItem): number { return item.config ? item.config.order : 0; } diff --git a/src/sql/workbench/api/node/extHostModelView.ts b/src/sql/workbench/api/node/extHostModelView.ts index 65e38a8b50..9448d0928a 100644 --- a/src/sql/workbench/api/node/extHostModelView.ts +++ b/src/sql/workbench/api/node/extHostModelView.ts @@ -1224,6 +1224,12 @@ class FileBrowserTreeComponentWrapper extends ComponentWrapper implements azdata } class DivContainerWrapper extends ComponentWrapper implements azdata.DivContainer { + constructor(proxy: MainThreadModelViewShape, handle: number, type: ModelComponentTypes, id: string) { + super(proxy, handle, type, id); + this.properties = {}; + this._emitterMap.set(ComponentEventType.onDidClick, new Emitter()); + } + public get overflowY(): string { return this.properties['overflowY']; } @@ -1239,6 +1245,11 @@ class DivContainerWrapper extends ComponentWrapper implements azdata.DivContaine public set yOffsetChange(value: number) { this.setProperty('yOffsetChange', value); } + + public get onDidClick(): vscode.Event { + let emitter = this._emitterMap.get(ComponentEventType.onDidClick); + return emitter && emitter.event; + } } class TreeComponentWrapper extends ComponentWrapper implements azdata.TreeComponent { diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index 5035a6fdaa..005cf76f46 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -832,11 +832,11 @@ export function createApiFactory( }, openDialog(dialog: sqlops.window.modelviewdialog.Dialog) { console.warn('the method sqlops.window.modelviewdialog.openDialog has been deprecated, replace it with azdata.window.openDialog'); - return extHostModelViewDialog.openDialog(dialog); + return extHostModelViewDialog.openDialog(dialog as azdata.window.Dialog); }, closeDialog(dialog: sqlops.window.modelviewdialog.Dialog) { console.warn('the method sqlops.window.modelviewdialog.closeDialog has been deprecated, replace it with azdata.window.closeDialog'); - return extHostModelViewDialog.closeDialog(dialog); + return extHostModelViewDialog.closeDialog(dialog as azdata.window.Dialog); }, createWizardPage(title: string): sqlops.window.modelviewdialog.WizardPage { console.warn('the method sqlops.window.modelviewdialog.createWizardPage has been deprecated, replace it with azdata.window.createWizardPage'); @@ -868,10 +868,10 @@ export function createApiFactory( return extHostModelViewDialog.createButton(label); }, openDialog(dialog: sqlops.window.Dialog) { - return extHostModelViewDialog.openDialog(dialog); + return extHostModelViewDialog.openDialog(dialog as azdata.window.Dialog); }, closeDialog(dialog: sqlops.window.Dialog) { - return extHostModelViewDialog.closeDialog(dialog); + return extHostModelViewDialog.closeDialog(dialog as azdata.window.Dialog); }, createWizardPage(title: string): sqlops.window.WizardPage { return extHostModelViewDialog.createWizardPage(title);