profile page and summary page (#4769)

* add cluster name to page

* implement profile page -1

* fix compilation error due to new method

* profile page 0328

* summary page

* make divcontainer accessible

* handle disposable

* add support for "coming soon" cards
This commit is contained in:
Alan Ren
2019-04-02 13:52:39 -07:00
committed by GitHub
parent 63485c8c78
commit e83a6f9c2e
15 changed files with 816 additions and 99 deletions

View File

@@ -50,6 +50,7 @@ export interface ContainerRegistryInfo {
} }
export interface TargetClusterTypeInfo { export interface TargetClusterTypeInfo {
enabled: boolean;
type: TargetClusterType; type: TargetClusterType;
name: string; name: string;
fullName: string; fullName: string;
@@ -80,4 +81,44 @@ export enum ClusterType {
Minikube, Minikube,
Kubernetes, Kubernetes,
Other 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;
} }

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
'use strict'; '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 { getContexts, KubectlContext, setContext, inferCurrentClusterType } from '../../kubectl/kubectlUtils';
import { Kubectl } from '../../kubectl/kubectl'; import { Kubectl } from '../../kubectl/kubectl';
import { Scriptable, ScriptingDictionary } from '../../scripting/scripting'; import { Scriptable, ScriptingDictionary } from '../../scripting/scripting';
@@ -57,6 +57,7 @@ export class CreateClusterModel implements Scriptable {
public getAllTargetClusterTypeInfo(): Thenable<TargetClusterTypeInfo[]> { public getAllTargetClusterTypeInfo(): Thenable<TargetClusterTypeInfo[]> {
let promise = new Promise<TargetClusterTypeInfo[]>(resolve => { let promise = new Promise<TargetClusterTypeInfo[]>(resolve => {
let aksCluster: TargetClusterTypeInfo = { let aksCluster: TargetClusterTypeInfo = {
enabled: false,
type: TargetClusterType.NewAksCluster, type: TargetClusterType.NewAksCluster,
name: localize('bdc-create.AKSClusterCardText', 'New AKS Cluster'), name: localize('bdc-create.AKSClusterCardText', 'New AKS Cluster'),
fullName: localize('bdc-create.AKSClusterFullName', 'New Azure Kubernetes Service cluster'), fullName: localize('bdc-create.AKSClusterFullName', 'New Azure Kubernetes Service cluster'),
@@ -69,9 +70,10 @@ export class CreateClusterModel implements Scriptable {
}; };
let existingCluster: TargetClusterTypeInfo = { let existingCluster: TargetClusterTypeInfo = {
enabled: true,
type: TargetClusterType.ExistingKubernetesCluster, type: TargetClusterType.ExistingKubernetesCluster,
name: localize('bdc-create.ExistingClusterCardText', 'Existing Cluster'), 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.'), 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: { iconPath: {
dark: 'images/kubernetes.svg', dark: 'images/kubernetes.svg',
@@ -106,7 +108,7 @@ export class CreateClusterModel implements Scriptable {
setTimeout(() => { setTimeout(() => {
let tools = this.targetClusterType === TargetClusterType.ExistingKubernetesCluster ? [kubeCtl, mssqlCtl] : [kubeCtl, mssqlCtl, azureCli]; let tools = this.targetClusterType === TargetClusterType.ExistingKubernetesCluster ? [kubeCtl, mssqlCtl] : [kubeCtl, mssqlCtl, azureCli];
resolve(tools); resolve(tools);
}, 2000); }, 1000);
}); });
return promise; return promise;
} }
@@ -117,7 +119,7 @@ export class CreateClusterModel implements Scriptable {
tool.status = ToolInstallationStatus.Installed; tool.status = ToolInstallationStatus.Installed;
this._tmp_tools_installed = true; this._tmp_tools_installed = true;
resolve(); resolve();
}, 2000); }, 1000);
}); });
return promise; return promise;
} }
@@ -126,6 +128,8 @@ export class CreateClusterModel implements Scriptable {
return path.join(os.homedir(), '.kube', 'config'); return path.join(os.homedir(), '.kube', 'config');
} }
public clusterName: string;
public targetClusterType: TargetClusterType; public targetClusterType: TargetClusterType;
public selectedCluster: KubectlContext; public selectedCluster: KubectlContext;
@@ -156,6 +160,8 @@ export class CreateClusterModel implements Scriptable {
public containerRegistryPassword: string; public containerRegistryPassword: string;
public profile: ClusterProfile;
public async getTargetClusterPlatform(targetContextName: string): Promise<string> { public async getTargetClusterPlatform(targetContextName: string): Promise<string> {
await setContext(this._kubectl, targetContextName); await setContext(this._kubectl, targetContextName);
let clusterType = await inferCurrentClusterType(this._kubectl); let clusterType = await inferCurrentClusterType(this._kubectl);
@@ -207,4 +213,116 @@ export class CreateClusterModel implements Scriptable {
public getTargetKubectlContext(): KubectlContext { public getTargetKubectlContext(): KubectlContext {
return this.selectedCluster; return this.selectedCluster;
} }
public getClusterResource(): Thenable<ClusterResourceSummary> {
let promise = new Promise<ClusterResourceSummary>(resolve => {
setTimeout(() => {
let resoureSummary: ClusterResourceSummary = {
hardwareLabels: [
{
name: '<Default>',
totalNodes: 10,
totalCores: 22,
totalDisks: 128,
totalMemoryInGB: 77
},
{
name: '#data',
totalNodes: 4,
totalCores: 22,
totalDisks: 200,
totalMemoryInGB: 100
},
{
name: '#compute',
totalNodes: 12,
totalCores: 124,
totalDisks: 24,
totalMemoryInGB: 100
},
{
name: '#premium',
totalNodes: 10,
totalCores: 100,
totalDisks: 200,
totalMemoryInGB: 770
}
]
};
resolve(resoureSummary);
}, 1000);
});
return promise;
}
public getProfiles(): Thenable<ClusterProfile[]> {
let promise = new Promise<ClusterProfile[]>(resolve => {
setTimeout(() => {
let profiles: ClusterProfile[] = [];
profiles.push({
name: 'Basic',
sqlServerMasterConfiguration: this.createSQLPoolConfiguration(1, 1),
computePoolConfiguration: this.createComputePoolConfiguration(2),
dataPoolConfiguration: this.createDataPoolConfiguration(2),
storagePoolConfiguration: this.createStoragePoolConfiguration(2),
sparkPoolConfiguration: this.createSparkPoolConfiguration(2)
});
profiles.push({
name: 'Standard',
sqlServerMasterConfiguration: this.createSQLPoolConfiguration(3, 9),
computePoolConfiguration: this.createComputePoolConfiguration(5),
dataPoolConfiguration: this.createDataPoolConfiguration(5),
storagePoolConfiguration: this.createStoragePoolConfiguration(5),
sparkPoolConfiguration: this.createSparkPoolConfiguration(5)
});
profiles.push({
name: 'Premium',
sqlServerMasterConfiguration: this.createSQLPoolConfiguration(5, 9),
computePoolConfiguration: this.createComputePoolConfiguration(7),
dataPoolConfiguration: this.createDataPoolConfiguration(7),
storagePoolConfiguration: this.createStoragePoolConfiguration(7),
sparkPoolConfiguration: this.createSparkPoolConfiguration(7)
});
resolve(profiles);
}, 1000);
});
return promise;
}
private createSQLPoolConfiguration(scale: number, maxScale: number): SQLServerMasterConfiguration {
return <SQLServerMasterConfiguration>{
type: ClusterPoolType.SQL,
engineOnly: false,
scale: scale,
maxScale: maxScale
};
}
private createComputePoolConfiguration(scale: number): PoolConfiguration {
return {
type: ClusterPoolType.Compute,
scale: scale
};
}
private createDataPoolConfiguration(scale: number): PoolConfiguration {
return {
type: ClusterPoolType.Data,
scale: scale
};
}
private createStoragePoolConfiguration(scale: number): PoolConfiguration {
return {
type: ClusterPoolType.Storage,
scale: scale
};
}
private createSparkPoolConfiguration(scale: number): PoolConfiguration {
return {
type: ClusterPoolType.Spark,
scale: scale
};
}
} }

View File

@@ -18,7 +18,7 @@ import { ScriptGenerator } from '../../scripting/scripting';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
export class CreateClusterWizard extends WizardBase<CreateClusterModel, CreateClusterWizard> { export class CreateClusterWizard extends WizardBase<CreateClusterModel, CreateClusterWizard> {
private scripter : ScriptGenerator; private scripter: ScriptGenerator;
constructor(context: ExtensionContext, kubectl: Kubectl) { constructor(context: ExtensionContext, kubectl: Kubectl) {
let model = new CreateClusterModel(kubectl); let model = new CreateClusterModel(kubectl);
super(model, context, localize('bdc-create.wizardTitle', 'Create a big data cluster')); super(model, context, localize('bdc-create.wizardTitle', 'Create a big data cluster'));
@@ -31,19 +31,24 @@ export class CreateClusterWizard extends WizardBase<CreateClusterModel, CreateCl
let selectTargetClusterPage = new SelectExistingClusterPage(this); let selectTargetClusterPage = new SelectExistingClusterPage(this);
let summaryPage = new SummaryPage(this); let summaryPage = new SummaryPage(this);
let targetClusterTypePage = new SelectTargetClusterTypePage(this); let targetClusterTypePage = new SelectTargetClusterTypePage(this);
this.setPages([targetClusterTypePage, selectTargetClusterPage, clusterProfilePage, settingsPage, summaryPage]); this.setPages([targetClusterTypePage, selectTargetClusterPage, settingsPage, clusterProfilePage, summaryPage]);
this.wizardObject.generateScriptButton.label = localize('bdc-create.generateScriptsButtonText', 'Generate Scripts'); this.wizardObject.generateScriptButton.label = localize('bdc-create.generateScriptsButtonText', 'Generate Scripts');
this.wizardObject.generateScriptButton.hidden = false; this.wizardObject.generateScriptButton.hidden = false;
this.wizardObject.doneButton.label = localize('bdc-create.createClusterButtonText', 'Create'); this.wizardObject.doneButton.label = localize('bdc-create.createClusterButtonText', 'Create');
this.wizardObject.generateScriptButton.onClick(async () => { this.registerDisposable(this.wizardObject.generateScriptButton.onClick(async () => {
this.wizardObject.generateScriptButton.enabled = false; this.wizardObject.generateScriptButton.enabled = false;
this.scripter.generateDeploymentScript(this.model).then( () => { this.scripter.generateDeploymentScript(this.model).then(() => {
this.wizardObject.generateScriptButton.enabled = true; this.wizardObject.generateScriptButton.enabled = true;
//TODO: Add error handling. //TODO: Add error handling.
}); });
}); }));
this.wizardObject.doneButton.onClick(() => { }); }
protected onCancel(): void {
}
protected onOk(): void {
} }
} }

View File

@@ -5,29 +5,387 @@
'use strict'; 'use strict';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { WizardPageBase } from '../../wizardPageBase'; import { WizardPageBase } from '../../wizardPageBase';
import { CreateClusterWizard } from '../createClusterWizard'; import { CreateClusterWizard } from '../createClusterWizard';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { ClusterProfile, PoolConfiguration, ClusterPoolType, SQLServerMasterConfiguration, ClusterResourceSummary } from '../../../interfaces';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
const LabelWidth = '200px';
const InputWidth = '300px';
export class ClusterProfilePage extends WizardPageBase<CreateClusterWizard> { export class ClusterProfilePage extends WizardPageBase<CreateClusterWizard> {
private view: azdata.ModelView;
private clusterProfiles: ClusterProfile[];
private poolList: azdata.FlexContainer;
private detailContainer: azdata.FlexContainer;
private clusterResourceView: azdata.GroupContainer;
private poolListMap = {};
private clusterResourceContainer: azdata.FlexContainer;
private clusterResourceLoadingComponent: azdata.LoadingComponent;
private clusterResource: ClusterResourceSummary;
constructor(wizard: CreateClusterWizard) { constructor(wizard: CreateClusterWizard) {
super(localize('bdc-create.clusterProfilePageTitle', 'Select a cluster profile'), 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.'), 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); 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(() => { this.wizard.wizardObject.registerNavigationValidator(() => {
return true; return true;
}); });
} }
protected initialize(view: azdata.ModelView): Thenable<void> { protected initialize(view: azdata.ModelView): Thenable<void> {
let formBuilder = view.modelBuilder.formContainer(); this.view = view;
let form = formBuilder.component(); let fetchProfilePromise = this.wizard.model.getProfiles().then(p => { this.clusterProfiles = p; });
return view.initializeModel(form); return Promise.all([fetchProfilePromise]).then(() => {
this.wizard.model.profile = this.clusterProfiles[0];
this.clusterResourceView = this.view.modelBuilder.groupContainer().withLayout({
header: localize('bdc-create.TargetClusterOverview', 'Target cluster scale overview'),
collapsed: true,
collapsible: true
}).component();
this.clusterResourceContainer = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
this.clusterResourceLoadingComponent = this.view.modelBuilder.loadingComponent().withItem(this.clusterResourceContainer).component();
this.clusterResourceView.addItem(this.clusterResourceLoadingComponent);
let profileLabel = view.modelBuilder.text().withProperties({ value: localize('bdc-create.clusterProfileLabel', 'Deployment profile') }).component();
let profileDropdown = view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({
values: this.clusterProfiles.map(profile => profile.name),
width: '300px'
}).component();
let dropdownRow = this.view.modelBuilder.flexContainer().withItems([profileLabel, profileDropdown], { CSSStyles: { 'margin-right': '30px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
let poolContainer = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', width: '100%', height: '100%' }).component();
this.poolList = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '300px', height: '100%' }).component();
poolContainer.addItem(this.poolList, {
CSSStyles: {
'border-top-style': 'solid',
'border-top-width': '2px',
'border-right-style': 'solid',
'border-right-width': '2px',
'border-color': 'lightgray'
}
});
this.detailContainer = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '760px', height: '100%' }).component();
poolContainer.addItem(this.detailContainer, {
CSSStyles: {
'border-top-style': 'solid',
'border-top-width': '2px',
'border-color': 'lightgray'
}
});
this.wizard.registerDisposable(profileDropdown.onValueChanged(() => {
let profiles = this.clusterProfiles.filter(p => profileDropdown.value === p.name);
if (profiles && profiles.length === 1) {
this.wizard.model.profile = profiles[0];
this.updatePoolList();
this.clearPoolDetail();
}
}));
this.initializePoolList();
let pageContainer = this.view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
height: '800px'
}).component();
pageContainer.addItem(this.clusterResourceView, {
flex: '0 0 auto',
CSSStyles: {
'margin-bottom': '20px',
'padding-bottom': '5px',
'padding-top': '5px'
}
});
pageContainer.addItem(dropdownRow, {
flex: '0 0 auto',
CSSStyles: { 'margin-bottom': '10px' }
});
pageContainer.addItem(poolContainer, {
flex: '1 1 auto',
CSSStyles: {
'display': 'flex'
}
});
let formBuilder = view.modelBuilder.formContainer();
let form = formBuilder.withFormItems([{
title: '',
component: pageContainer
}], {
horizontal: false,
componentWidth: '100%'
}).component();
return view.initializeModel(form);
});
}
private initializeClusterResourceView(): void {
this.clusterResourceContainer.clearItems();
let text = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.HardwareProfileText', 'Hardware profile') }).component();
let height = (this.clusterResource.hardwareLabels.length * 25) + 30;
let labelColumn: azdata.TableColumn = {
value: localize('bdc-create.HardwareLabelColumnName', 'Label'),
width: 100
};
let totalNodesColumn: azdata.TableColumn = {
value: localize('bdc-create.TotalNodesColumnName', 'Nodes'),
width: 50
};
let totalCoresColumn: azdata.TableColumn = {
value: localize('bdc-create.TotalCoresColumnName', 'Cores'),
width: 50
};
let totalMemoryColumn: azdata.TableColumn = {
value: localize('bdc-create.TotalMemoryColumnName', 'Memory'),
width: 50
};
let totalDisksColumn: azdata.TableColumn = {
value: localize('bdc-create.TotalDisksColumnName', 'Disks'),
width: 50
};
let table = this.view.modelBuilder.table().withProperties<azdata.TableComponentProperties>({
height: `${height}px`,
data: this.clusterResource.hardwareLabels.map(label => [label.name, label.totalNodes, label.totalCores, label.totalMemoryInGB, label.totalDisks]),
columns: [labelColumn, totalNodesColumn, totalCoresColumn, totalMemoryColumn, totalDisksColumn],
width: '300px'
}).component();
this.clusterResourceContainer.addItems([text, table]);
this.clusterResourceLoadingComponent.loading = false;
}
private initializePoolList(): void {
let pools = [this.wizard.model.profile.sqlServerMasterConfiguration,
this.wizard.model.profile.computePoolConfiguration,
this.wizard.model.profile.dataPoolConfiguration,
this.wizard.model.profile.sparkPoolConfiguration,
this.wizard.model.profile.storagePoolConfiguration];
pools.forEach(pool => {
let poolSummaryButton = this.view.modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: true }).component();
let container = this.view.modelBuilder.flexContainer().component();
this.wizard.registerDisposable(poolSummaryButton.onDidClick(() => {
this.clearPoolDetail();
let currentPool: PoolConfiguration;
switch (pool.type) {
case ClusterPoolType.SQL:
currentPool = this.wizard.model.profile.sqlServerMasterConfiguration;
break;
case ClusterPoolType.Compute:
currentPool = this.wizard.model.profile.computePoolConfiguration;
break;
case ClusterPoolType.Data:
currentPool = this.wizard.model.profile.dataPoolConfiguration;
break;
case ClusterPoolType.Storage:
currentPool = this.wizard.model.profile.storagePoolConfiguration;
break;
case ClusterPoolType.Spark:
currentPool = this.wizard.model.profile.sparkPoolConfiguration;
break;
default:
break;
}
if (currentPool) {
this.detailContainer.addItem(this.createPoolConfigurationPart(currentPool), { CSSStyles: { 'margin-left': '10px' } });
}
}));
let text = this.view.modelBuilder.text().component();
this.poolListMap[pool.type] = text;
text.width = '250px';
let chrevron = this.view.modelBuilder.text().withProperties({ value: '>' }).component();
chrevron.width = '30px';
container.addItem(text);
container.addItem(chrevron, {
CSSStyles: {
'font-size': '20px',
'line-height': '0px'
}
});
poolSummaryButton.addItem(container);
this.poolList.addItem(poolSummaryButton, {
CSSStyles: {
'border-bottom-style': 'solid',
'border-bottom-width': '1px',
'border-color': 'lightgray',
'cursor': 'pointer'
}
});
});
}
private createPoolConfigurationPart(configuration: PoolConfiguration): azdata.Component {
let container = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
switch (configuration.type) {
case ClusterPoolType.SQL:
this.createSQLConfigurationPart(container, configuration as SQLServerMasterConfiguration);
break;
default:
this.createDefaultPoolConfigurationPart(container, configuration);
break;
}
return container;
}
private createSQLConfigurationPart(container: azdata.FlexContainer, configuration: SQLServerMasterConfiguration): void {
this.createDefaultPoolConfigurationPart(container, configuration);
this.addFeatureSetRow(container, configuration);
}
private createDefaultPoolConfigurationPart(container: azdata.FlexContainer, configuration: PoolConfiguration): void {
this.addPoolNameLabel(container, this.getPoolDisplayName(configuration.type));
this.addPoolDescriptionLabel(container, this.getPoolDescription(configuration.type));
this.addScaleRow(container, configuration);
this.addHardwareLabelRow(container, configuration);
}
private addPoolNameLabel(container: azdata.FlexContainer, text: string): void {
let poolNameLabel = this.view.modelBuilder.text().withProperties({ value: text }).component();
container.addItem(poolNameLabel, {
flex: '0 0 auto', CSSStyles: {
'font-size': '13px',
'font-weight': 'bold'
}
});
}
private addPoolDescriptionLabel(container: azdata.FlexContainer, text: string): void {
let label = this.view.modelBuilder.text().withProperties({ value: text }).component();
container.addItem(label, {
flex: '0 0 auto',
CSSStyles: {
'margin-bottom': '20px'
}
});
}
private addScaleRow(container: azdata.FlexContainer, configuration: PoolConfiguration): void {
let label = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.ScaleLabel', 'Scale') }).component();
label.width = LabelWidth;
let input = this.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
inputType: 'number',
value: configuration.scale.toString(),
min: 1,
max: configuration.maxScale
}).component();
this.wizard.registerDisposable(input.onTextChanged(() => {
configuration.scale = Number(input.value);
this.updatePoolList();
}));
input.width = InputWidth;
let row = this.createRow([label, input]);
container.addItem(row);
}
private addHardwareLabelRow(container: azdata.FlexContainer, configuration: PoolConfiguration): void {
let label = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.HardwareProfileLabel', 'Hardware profile label') }).component();
label.width = LabelWidth;
let optionalValues = this.clusterResource.hardwareLabels.map(label => label.name);
configuration.hardwareLabel = configuration.hardwareLabel ? configuration.hardwareLabel : optionalValues[0];
let input = this.view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({ value: configuration.hardwareLabel, values: optionalValues }).component();
this.wizard.registerDisposable(input.onValueChanged(() => {
configuration.hardwareLabel = input.value.toString();
}));
input.width = InputWidth;
let row = this.createRow([label, input]);
container.addItem(row);
}
private addFeatureSetRow(container: azdata.FlexContainer, configuration: SQLServerMasterConfiguration): void {
const radioGroupName = 'featureset';
let label = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.FeatureSetLabel', 'Feature set') }).component();
label.width = LabelWidth;
let engineOnlyOption = this.view.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({ label: localize('bdc-create.EngineOnlyText', 'Engine only'), name: radioGroupName, checked: configuration.engineOnly }).component();
let engineWithFeaturesOption = this.view.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({ label: localize('bdc-create.EngineWithFeaturesText', 'Engine with optional features'), name: radioGroupName, checked: !configuration.engineOnly }).component();
let optionContainer = this.view.modelBuilder.divContainer().component();
optionContainer.width = InputWidth;
optionContainer.addItems([engineOnlyOption, engineWithFeaturesOption]);
container.addItem(this.createRow([label, optionContainer]));
this.wizard.registerDisposable(engineOnlyOption.onDidClick(() => {
configuration.engineOnly = true;
}));
this.wizard.registerDisposable(engineWithFeaturesOption.onDidClick(() => {
configuration.engineOnly = false;
}));
}
private createRow(items: azdata.Component[]): azdata.FlexContainer {
return this.view.modelBuilder.flexContainer().withItems(items, {
CSSStyles: {
'margin-right': '5px'
}
}).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
}
private getPoolDisplayName(poolType: ClusterPoolType): string {
switch (poolType) {
case ClusterPoolType.SQL:
return localize('bdc-create.SQLServerMasterDisplayName', 'SQL Server master');
case ClusterPoolType.Compute:
return localize('bdc-create.ComputePoolDisplayName', 'Compute pool');
case ClusterPoolType.Data:
return localize('bdc-create.DataPoolDisplayName', 'Data pool');
case ClusterPoolType.Storage:
return localize('bdc-create.StoragePoolDisplayName', 'Storage pool');
case ClusterPoolType.Spark:
return localize('bdc-create.SparkPoolDisplayName', 'Spark pool');
default:
throw '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();
} }
} }

View File

@@ -21,7 +21,7 @@ export class SelectExistingClusterPage extends WizardPageBase<CreateClusterWizar
private existingClusterControl: azdata.FlexContainer; private existingClusterControl: azdata.FlexContainer;
private clusterContextsLabel: azdata.TextComponent; private clusterContextsLabel: azdata.TextComponent;
private errorLoadingClustersLabel: azdata.TextComponent; private errorLoadingClustersLabel: azdata.TextComponent;
private clusterContextContainer: azdata.DivContainer; private clusterContextList: azdata.DivContainer;
private clusterContextLoadingComponent: azdata.LoadingComponent; private clusterContextLoadingComponent: azdata.LoadingComponent;
private configFileInput: azdata.InputBoxComponent; private configFileInput: azdata.InputBoxComponent;
private browseFileButton: azdata.ButtonComponent; private browseFileButton: azdata.ButtonComponent;
@@ -80,7 +80,9 @@ export class SelectExistingClusterPage extends WizardPageBase<CreateClusterWizar
private initExistingClusterControl(): void { private initExistingClusterControl(): void {
let self = this; let self = this;
const labelWidth = '150px';
let configFileLabel = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.kubeConfigFileLabelText', 'Kube config file path') }).component(); let configFileLabel = this.view.modelBuilder.text().withProperties({ value: localize('bdc-create.kubeConfigFileLabelText', 'Kube config file path') }).component();
configFileLabel.width = labelWidth;
this.configFileInput = this.view.modelBuilder.inputBox().withProperties({ width: '300px' }).component(); this.configFileInput = this.view.modelBuilder.inputBox().withProperties({ width: '300px' }).component();
this.configFileInput.enabled = false; this.configFileInput.enabled = false;
this.browseFileButton = this.view.modelBuilder.button().withProperties({ label: localize('bdc-browseText', 'Browse'), width: '100px' }).component(); this.browseFileButton = this.view.modelBuilder.button().withProperties({ label: localize('bdc-browseText', 'Browse'), width: '100px' }).component();
@@ -88,14 +90,21 @@ export class SelectExistingClusterPage extends WizardPageBase<CreateClusterWizar
.withLayout({ flexFlow: 'row', alignItems: 'baseline' }) .withLayout({ flexFlow: 'row', alignItems: 'baseline' })
.withItems([configFileLabel, this.configFileInput, this.browseFileButton], { CSSStyles: { 'margin-right': '10px' } }).component(); .withItems([configFileLabel, this.configFileInput, this.browseFileButton], { CSSStyles: { 'margin-right': '10px' } }).component();
this.clusterContextsLabel = this.view.modelBuilder.text().withProperties({ value: localize('bdc-clusterContextsLabelText', 'Cluster Contexts') }).component(); this.clusterContextsLabel = this.view.modelBuilder.text().withProperties({ value: localize('bdc-clusterContextsLabelText', 'Cluster Contexts') }).component();
this.clusterContextsLabel.width = labelWidth;
this.errorLoadingClustersLabel = this.view.modelBuilder.text().withProperties({ value: localize('bdc-errorLoadingClustersText', 'No cluster information is found in the config file or an error ocurred while loading the config file') }).component(); this.errorLoadingClustersLabel = this.view.modelBuilder.text().withProperties({ value: localize('bdc-errorLoadingClustersText', 'No cluster information is found in the config file or an error ocurred while loading the config file') }).component();
this.clusterContextContainer = this.view.modelBuilder.divContainer().component(); this.clusterContextList = this.view.modelBuilder.divContainer().component();
this.clusterContextLoadingComponent = this.view.modelBuilder.loadingComponent().withItem(this.clusterContextContainer).component(); this.clusterContextLoadingComponent = this.view.modelBuilder.loadingComponent().withItem(this.clusterContextList).component();
this.existingClusterControl = this.view.modelBuilder.divContainer().component(); this.existingClusterControl = this.view.modelBuilder.divContainer().component();
this.existingClusterControl.addItem(configFileContainer, { CSSStyles: { 'margin-top': '0px' } }); let clusterContextContainer = this.view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'start' }).component();
this.existingClusterControl.addItem(this.clusterContextLoadingComponent, { CSSStyles: { 'width': '400px', 'margin-top': '10px' } }); clusterContextContainer.addItem(this.clusterContextsLabel, { flex: '0 0 auto' });
clusterContextContainer.addItem(this.clusterContextLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'width': '400px', 'margin-left': '10px', 'margin-top': '10px' } });
this.browseFileButton.onDidClick(async () => { 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( let fileUris = await vscode.window.showOpenDialog(
{ {
canSelectFiles: true, canSelectFiles: true,
@@ -112,12 +121,12 @@ export class SelectExistingClusterPage extends WizardPageBase<CreateClusterWizar
if (!fileUris || fileUris.length === 0) { if (!fileUris || fileUris.length === 0) {
return; return;
} }
self.clusterContextContainer.clearItems(); self.clusterContextList.clearItems();
let fileUri = fileUris[0]; let fileUri = fileUris[0];
self.loadClusterContexts(fileUri.fsPath); self.loadClusterContexts(fileUri.fsPath);
}); }));
} }
private async loadClusterContexts(configPath: string): Promise<void> { private async loadClusterContexts(configPath: string): Promise<void> {
@@ -140,17 +149,15 @@ export class SelectExistingClusterPage extends WizardPageBase<CreateClusterWizar
self.wizard.wizardObject.message = null; self.wizard.wizardObject.message = null;
} }
option.onDidClick(() => { this.wizard.registerDisposable(option.onDidClick(() => {
self.wizard.model.selectedCluster = cluster; self.wizard.model.selectedCluster = cluster;
self.wizard.wizardObject.message = null; self.wizard.wizardObject.message = null;
}); }));
return option; return option;
}); });
self.clusterContextList.addItems(options);
self.clusterContextContainer.addItem(self.clusterContextsLabel);
self.clusterContextContainer.addItems(options);
} else { } else {
self.clusterContextContainer.addItem(this.errorLoadingClustersLabel); self.clusterContextList.addItem(this.errorLoadingClustersLabel);
} }
this.clusterContextLoadingComponent.loading = false; this.clusterContextLoadingComponent.loading = false;
} }

View File

@@ -35,7 +35,7 @@ export class SelectTargetClusterTypePage extends WizardPageBase<CreateClusterWiz
wizard); wizard);
this.installToolsButton = azdata.window.createButton(InstallToolsButtonText); this.installToolsButton = azdata.window.createButton(InstallToolsButtonText);
this.installToolsButton.hidden = true; this.installToolsButton.hidden = true;
this.installToolsButton.onClick(async () => { this.wizard.registerDisposable(this.installToolsButton.onClick(async () => {
this.wizard.wizardObject.message = null; this.wizard.wizardObject.message = null;
this.installToolsButton.label = InstallingButtonText; this.installToolsButton.label = InstallingButtonText;
this.installToolsButton.enabled = false; this.installToolsButton.enabled = false;
@@ -53,14 +53,14 @@ export class SelectTargetClusterTypePage extends WizardPageBase<CreateClusterWiz
this.installToolsButton.label = InstallToolsButtonText; this.installToolsButton.label = InstallToolsButtonText;
this.updateRequiredToolStatus(); this.updateRequiredToolStatus();
}); }));
this.wizard.addButton(this.installToolsButton); this.wizard.addButton(this.installToolsButton);
this.refreshToolsButton = azdata.window.createButton(localize('bdc-create.RefreshToolsButtonText', 'Refresh Status')); this.refreshToolsButton = azdata.window.createButton(localize('bdc-create.RefreshToolsButtonText', 'Refresh Status'));
this.refreshToolsButton.hidden = true; this.refreshToolsButton.hidden = true;
this.refreshToolsButton.onClick(() => { this.wizard.registerDisposable(this.refreshToolsButton.onClick(() => {
this.updateRequiredToolStatus(); this.updateRequiredToolStatus();
}); }));
this.wizard.addButton(this.refreshToolsButton); this.wizard.addButton(this.refreshToolsButton);
} }
@@ -115,7 +115,6 @@ export class SelectTargetClusterTypePage extends WizardPageBase<CreateClusterWiz
} }
); );
self.form = self.formBuilder.withLayout({ width: '100%' }).component(); self.form = self.formBuilder.withLayout({ width: '100%' }).component();
return view.initializeModel(self.form); return view.initializeModel(self.form);
}); });
@@ -160,51 +159,61 @@ export class SelectTargetClusterTypePage extends WizardPageBase<CreateClusterWiz
private createCard(view: azdata.ModelView, targetClusterTypeInfo: TargetClusterTypeInfo): azdata.CardComponent { private createCard(view: azdata.ModelView, targetClusterTypeInfo: TargetClusterTypeInfo): azdata.CardComponent {
let self = this; let self = this;
let descriptions = targetClusterTypeInfo.enabled ? [] : [localize('bdc-create.ComingSoonText', '(Coming Soon)')];
let card = view.modelBuilder.card().withProperties<azdata.CardProperties>({ let card = view.modelBuilder.card().withProperties<azdata.CardProperties>({
cardType: azdata.CardType.VerticalButton, cardType: azdata.CardType.VerticalButton,
iconPath: { iconPath: {
dark: self.wizard.context.asAbsolutePath(targetClusterTypeInfo.iconPath.dark), dark: self.wizard.context.asAbsolutePath(targetClusterTypeInfo.iconPath.dark),
light: self.wizard.context.asAbsolutePath(targetClusterTypeInfo.iconPath.light) light: self.wizard.context.asAbsolutePath(targetClusterTypeInfo.iconPath.light)
}, },
label: targetClusterTypeInfo.name label: targetClusterTypeInfo.name,
descriptions: descriptions
}).component(); }).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.wizard.registerDisposable(card.onCardSelectedChanged(() => {
self.formBuilder.addFormItem({ self.onCardSelected(card, targetClusterTypeInfo);
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;
}
}
});
return card; return card;
} }
private onCardSelected(card: azdata.CardComponent, targetClusterTypeInfo: TargetClusterTypeInfo): void {
let self = this;
if (card.selected) {
self.wizard.wizardObject.message = null;
self.wizard.model.targetClusterType = targetClusterTypeInfo.type;
self.cards.forEach(c => {
if (c !== card) {
c.selected = false;
}
});
self.targetDescriptionText.value = targetClusterTypeInfo.description;
if (self.form.items.length === 1) {
self.formBuilder.addFormItem({
title: localize('bdc-create.RequiredToolsText', 'Required tools'),
component: self.toolsLoadingWrapper
});
} else {
self.formBuilder.removeFormItem(self.targetDescriptionGroup);
}
self.targetDescriptionGroup = {
title: targetClusterTypeInfo.fullName,
component: self.targetDescriptionText
};
self.formBuilder.insertFormItem(self.targetDescriptionGroup, 1);
self.updateRequiredToolStatus();
} else {
if (self.cards.filter(c => { return c !== card && c.selected; }).length === 0) {
card.selected = true;
}
}
}
private updateRequiredToolStatus(): Thenable<void> { private updateRequiredToolStatus(): Thenable<void> {
this.isLoading = true; this.isLoading = true;
this.installToolsButton.hidden = false; this.installToolsButton.hidden = false;

View File

@@ -16,12 +16,32 @@ const PortInputWidth = '100px';
const RestoreDefaultValuesText = localize('bdc-create.RestoreDefaultValuesText', 'Restore Default Values'); const RestoreDefaultValuesText = localize('bdc-create.RestoreDefaultValuesText', 'Restore Default Values');
export class SettingsPage extends WizardPageBase<CreateClusterWizard> { export class SettingsPage extends WizardPageBase<CreateClusterWizard> {
private acceptEulaCheckbox: azdata.CheckBoxComponent;
constructor(wizard: CreateClusterWizard) { constructor(wizard: CreateClusterWizard) {
super(localize('bdc-create.settingsPageTitle', 'Settings'), super(localize('bdc-create.settingsPageTitle', 'Settings'),
localize('bdc-create.settingsPageDescription', 'Configure the settings required for deploying SQL Server big data cluster'), localize('bdc-create.settingsPageDescription', 'Configure the settings required for deploying SQL Server big data cluster'),
wizard); wizard);
} }
public onEnter(): void {
this.wizard.wizardObject.registerNavigationValidator((e) => {
if (e.lastPage > e.newPage) {
this.wizard.wizardObject.message = null;
return true;
}
if (!this.acceptEulaCheckbox.checked) {
this.wizard.wizardObject.message = {
text: localize('bdc-create.EulaNotAccepted', 'You need to accept the terms of services and privacy policy in order to proceed'),
level: azdata.window.MessageLevel.Error
};
} else {
this.wizard.wizardObject.message = null;
}
return this.acceptEulaCheckbox.checked;
});
}
protected initialize(view: azdata.ModelView): Thenable<void> { protected initialize(view: azdata.ModelView): Thenable<void> {
let clusterPorts: ClusterPorts; let clusterPorts: ClusterPorts;
let containerRegistryInfo: ContainerRegistryInfo; let containerRegistryInfo: ContainerRegistryInfo;
@@ -37,6 +57,14 @@ export class SettingsPage extends WizardPageBase<CreateClusterWizard> {
let formBuilder = view.modelBuilder.formContainer(); let formBuilder = view.modelBuilder.formContainer();
// User settings // 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, { let adminUserNameInput = this.createInputWithLabel(view, {
label: localize('bdc-create.AdminUsernameText', 'Admin username'), label: localize('bdc-create.AdminUsernameText', 'Admin username'),
isRequiredField: true, isRequiredField: true,
@@ -106,14 +134,14 @@ export class SettingsPage extends WizardPageBase<CreateClusterWizard> {
label: RestoreDefaultValuesText, label: RestoreDefaultValuesText,
width: 200 width: 200
}).component(); }).component();
restorePortSettingsButton.onDidClick(() => { this.wizard.registerDisposable(restorePortSettingsButton.onDidClick(() => {
sqlPortInput.input.value = clusterPorts.sql; sqlPortInput.input.value = clusterPorts.sql;
knoxPortInput.input.value = clusterPorts.knox; knoxPortInput.input.value = clusterPorts.knox;
controllerPortInput.input.value = clusterPorts.controller; controllerPortInput.input.value = clusterPorts.controller;
proxyPortInput.input.value = clusterPorts.proxy; proxyPortInput.input.value = clusterPorts.proxy;
grafanaPortInput.input.value = clusterPorts.grafana; grafanaPortInput.input.value = clusterPorts.grafana;
kibanaPortInput.input.value = clusterPorts.kibana; kibanaPortInput.input.value = clusterPorts.kibana;
}); }));
// Container Registry Settings // Container Registry Settings
const registryUserNamePasswordHintText = localize('bdc-create.RegistryUserNamePasswordHintText', 'only required for private registries'); const registryUserNamePasswordHintText = localize('bdc-create.RegistryUserNamePasswordHintText', 'only required for private registries');
@@ -166,18 +194,18 @@ export class SettingsPage extends WizardPageBase<CreateClusterWizard> {
label: RestoreDefaultValuesText, label: RestoreDefaultValuesText,
width: 200 width: 200
}).component(); }).component();
restoreContainerSettingsButton.onDidClick(() => { this.wizard.registerDisposable(restoreContainerSettingsButton.onDidClick(() => {
registryInput.input.value = containerRegistryInfo.registry; registryInput.input.value = containerRegistryInfo.registry;
repositoryInput.input.value = containerRegistryInfo.repository; repositoryInput.input.value = containerRegistryInfo.repository;
imageTagInput.input.value = containerRegistryInfo.imageTag; 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 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 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(); this.acceptEulaCheckbox = view.modelBuilder.checkBox().component();
acceptEulaCheckbox.checked = false; this.acceptEulaCheckbox.checked = false;
let eulaLink: azdata.LinkArea = { let eulaLink: azdata.LinkArea = {
text: localize('bdc-create.LicenseTerms', 'license terms'), text: localize('bdc-create.LicenseTerms', 'license terms'),
@@ -196,14 +224,13 @@ export class SettingsPage extends WizardPageBase<CreateClusterWizard> {
links: [eulaLink, privacyPolicyLink] links: [eulaLink, privacyPolicyLink]
}).component(); }).component();
let eulaContainer = this.createRow(view, [acceptEulaCheckbox, checkboxText]); let eulaContainer = this.createRow(view, [this.acceptEulaCheckbox, checkboxText]);
let form = formBuilder.withFormItems([ let form = formBuilder.withFormItems([
{ {
title: '', title: '',
component: eulaContainer component: eulaContainer
}, }, {
{
title: '', title: '',
component: basicSettingsGroup component: basicSettingsGroup
}, { }, {
@@ -234,9 +261,9 @@ export class SettingsPage extends WizardPageBase<CreateClusterWizard> {
input.width = options.inputWidth; input.width = options.inputWidth;
text.width = '150px'; text.width = '150px';
input.placeHolder = options.placeHolder; input.placeHolder = options.placeHolder;
input.onTextChanged(() => { this.wizard.registerDisposable(input.onTextChanged(() => {
textChangedHandler(input); textChangedHandler(input);
}); }));
input.value = options.initialValue; input.value = options.initialValue;
let row = this.createRow(view, [text, input]); let row = this.createRow(view, [text, input]);
return { return {

View File

@@ -10,23 +10,95 @@ import { CreateClusterWizard } from '../createClusterWizard';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
const LabelWidth = '250px';
export class SummaryPage extends WizardPageBase<CreateClusterWizard> { export class SummaryPage extends WizardPageBase<CreateClusterWizard> {
private view: azdata.ModelView;
private targetTypeText: azdata.TextComponent;
private targetClusterContextText: azdata.TextComponent;
private clusterNameText: azdata.TextComponent;
private clusterAdminUsernameText: azdata.TextComponent;
private acceptEulaText: azdata.TextComponent;
private deploymentProfileText: azdata.TextComponent;
private sqlServerMasterScaleText: azdata.TextComponent;
private storagePoolScaleText: azdata.TextComponent;
private computePoolScaleText: azdata.TextComponent;
private dataPoolScaleText: azdata.TextComponent;
private sparkPoolScaleText: azdata.TextComponent;
constructor(wizard: CreateClusterWizard) { constructor(wizard: CreateClusterWizard) {
super(localize('bdc-create.summaryPageTitle', 'Summary'), '', wizard); super(localize('bdc-create.summaryPageTitle', 'Summary'), '', wizard);
} }
protected initialize(view: azdata.ModelView): Thenable<void> { protected initialize(view: azdata.ModelView): Thenable<void> {
this.view = view;
let targetClusterInfoGroup = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
let bdcClusterInfoGroup = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
this.targetTypeText = this.view.modelBuilder.text().component();
this.targetClusterContextText = this.view.modelBuilder.text().component();
this.clusterNameText = this.view.modelBuilder.text().component();
this.clusterAdminUsernameText = this.view.modelBuilder.text().component();
this.acceptEulaText = this.view.modelBuilder.text().component();
this.deploymentProfileText = this.view.modelBuilder.text().component();
this.sqlServerMasterScaleText = this.view.modelBuilder.text().component();
this.storagePoolScaleText = this.view.modelBuilder.text().component();
this.computePoolScaleText = this.view.modelBuilder.text().component();
this.dataPoolScaleText = this.view.modelBuilder.text().component();
this.sparkPoolScaleText = this.view.modelBuilder.text().component();
targetClusterInfoGroup.addItem(this.createRow(localize('bdc-create.TargetClusterTypeText', 'Cluster type'), this.targetTypeText));
targetClusterInfoGroup.addItem(this.createRow(localize('bdc-create.ClusterContextText', 'Cluster context'), this.targetClusterContextText));
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.ClusterNameText', 'Cluster name'), this.clusterNameText));
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.ClusterAdminUsernameText', 'Cluster Admin username'), this.clusterAdminUsernameText));
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.AcceptEulaText', 'Accept license agreement'), this.acceptEulaText));
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.DeploymentProfileText', 'Deployment profile'), this.deploymentProfileText));
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.SqlServerMasterScaleText', 'SQL Server master scale'), this.sqlServerMasterScaleText));
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.ComputePoolScaleText', 'Compute pool scale'), this.computePoolScaleText));
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.DataPoolScaleText', 'Data pool scale'), this.dataPoolScaleText));
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.StoragePoolScaleText', 'Storage pool scale'), this.storagePoolScaleText));
bdcClusterInfoGroup.addItem(this.createRow(localize('bdc-create.SparkPoolScaleText', 'Spark pool scale'), this.sparkPoolScaleText));
let formBuilder = view.modelBuilder.formContainer(); let 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); return view.initializeModel(form);
} }
public onEnter(): void { 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; this.wizard.wizardObject.generateScriptButton.hidden = false;
} }
public onLeave(): void { public onLeave(): void {
this.wizard.wizardObject.generateScriptButton.hidden = true; 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;
}
} }

View File

@@ -5,15 +5,17 @@
'use strict'; 'use strict';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import { ExtensionContext } from 'vscode'; import { ExtensionContext, Disposable } from 'vscode';
import { WizardPageBase } from './wizardPageBase'; import { WizardPageBase } from './wizardPageBase';
export abstract class WizardBase<T,W> { export abstract class WizardBase<T, W> {
public wizardObject: azdata.window.Wizard; public wizardObject: azdata.window.Wizard;
private customButtons: azdata.window.Button[]; private customButtons: azdata.window.Button[];
private pages: WizardPageBase<W>[]; private pages: WizardPageBase<W>[];
private toDispose: Disposable[] = [];
constructor(public model: T, public context: ExtensionContext, private title: string) { constructor(public model: T, public context: ExtensionContext, private title: string) {
this.customButtons = []; this.customButtons = [];
} }
@@ -22,17 +24,33 @@ export abstract class WizardBase<T,W> {
this.wizardObject = azdata.window.createWizard(this.title); this.wizardObject = azdata.window.createWizard(this.title);
this.initialize(); this.initialize();
this.wizardObject.customButtons = this.customButtons; this.wizardObject.customButtons = this.customButtons;
this.wizardObject.onPageChanged((e) => { this.toDispose.push(this.wizardObject.onPageChanged((e) => {
let previousPage = this.pages[e.lastPage]; let previousPage = this.pages[e.lastPage];
let newPage = this.pages[e.newPage]; let newPage = this.pages[e.newPage];
previousPage.onLeave(); previousPage.onLeave();
newPage.onEnter(); 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 initialize(): void;
protected abstract onOk(): void;
protected abstract onCancel(): void;
public addButton(button: azdata.window.Button) { public addButton(button: azdata.window.Button) {
this.customButtons.push(button); this.customButtons.push(button);
@@ -42,4 +60,17 @@ export abstract class WizardBase<T,W> {
this.wizardObject.pages = pages.map(p => p.pageObject); this.wizardObject.pages = pages.map(p => p.pageObject);
this.pages = pages; this.pages = pages;
} }
private dispose() {
this.toDispose.forEach((disposable: Disposable) => {
try {
disposable.dispose();
}
catch{ }
});
}
public registerDisposable(disposable: Disposable): void {
this.toDispose.push(disposable);
}
} }

View File

@@ -2760,6 +2760,10 @@ declare module 'azdata' {
} }
export interface DivContainer extends Container<DivLayout, DivItemLayout>, DivContainerProperties { export interface DivContainer extends Container<DivLayout, DivItemLayout>, DivContainerProperties {
/**
* An event called when the div is clicked
*/
onDidClick: vscode.Event<any>;
} }
export interface FlexContainer extends Container<FlexLayout, FlexItemLayout> { export interface FlexContainer extends Container<FlexLayout, FlexItemLayout> {
@@ -3023,6 +3027,11 @@ declare module 'azdata' {
* This is used when its child component is webview * This is used when its child component is webview
*/ */
yOffsetChange?: number; yOffsetChange?: number;
/**
* Indicates whether the element is clickable
*/
clickable?: boolean;
} }
export interface CardComponent extends Component, CardProperties { export interface CardComponent extends Component, CardProperties {

View File

@@ -1,5 +1,5 @@
<div *ngIf="label" [class]="getClass()" (click)="onCardClick()" (mouseover)="onCardHoverChanged($event)" (mouseout)="onCardHoverChanged($event)" <div *ngIf="label" [class]="getClass()" (click)="onCardClick()" (mouseover)="onCardHoverChanged($event)"
tabIndex="0"> (mouseout)="onCardHoverChanged($event)" tabIndex="0">
<ng-container *ngIf="isVerticalButton || isDetailsCard"> <ng-container *ngIf="isVerticalButton || isDetailsCard">
<span *ngIf="hasStatus" class="card-status"> <span *ngIf="hasStatus" class="card-status">
<div class="status-content" [style.backgroundColor]="statusColor"></div> <div class="status-content" [style.backgroundColor]="statusColor"></div>
@@ -13,6 +13,9 @@
<div [class]="iconClass" [style.maxWidth]="iconWidth" [style.maxHeight]="iconHeight"></div> <div [class]="iconClass" [style.maxWidth]="iconWidth" [style.maxHeight]="iconHeight"></div>
</div> </div>
<h4 class="card-label">{{label}}</h4> <h4 class="card-label">{{label}}</h4>
<div *ngFor="let desc of descriptions">
<div class="list-item-description">{{desc}}</div>
</div>
</div> </div>
</ng-container> </ng-container>
@@ -25,7 +28,8 @@
<tr *ngFor="let action of actions"> <tr *ngFor="let action of actions">
<td class="table-row">{{action.label}}</td> <td class="table-row">{{action.label}}</td>
<td *ngIf="action.actionTitle" class="table-row"> <td *ngIf="action.actionTitle" class="table-row">
<a class="pointer prominent" (click)="onDidActionClick(action)">{{action.actionTitle}}</a> <a class="pointer prominent"
(click)="onDidActionClick(action)">{{action.actionTitle}}</a>
</td> </td>
</tr> </tr>
</table> </table>

View File

@@ -103,7 +103,7 @@ export default class CardComponent extends ComponentWithIconBase implements ICom
} }
private get selectable(): boolean { private get selectable(): boolean {
return this.cardType === 'VerticalButton' || this.cardType === 'ListItem'; return this.enabled && (this.cardType === 'VerticalButton' || this.cardType === 'ListItem');
} }
// CSS-bound properties // CSS-bound properties

View File

@@ -9,7 +9,7 @@ import {
ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList, ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList,
} from '@angular/core'; } 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 * as azdata from 'azdata';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service'; 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 { ModelComponentWrapper } from 'sql/parts/modelComponents/modelComponentWrapper.component';
import types = require('vs/base/common/types'); import types = require('vs/base/common/types');
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
class DivItem { class DivItem {
constructor(public descriptor: IComponentDescriptor, public config: azdata.DivItemLayout) { } constructor(public descriptor: IComponentDescriptor, public config: azdata.DivItemLayout) { }
@@ -24,7 +26,7 @@ class DivItem {
@Component({ @Component({
template: ` template: `
<div #divContainer *ngIf="items" class="divContainer" [style.height]="height" [style.width]="width"> <div #divContainer *ngIf="items" class="divContainer" [style.height]="height" [style.width]="width" (click)="onClick()" (keyup)="onKey($event)" [tabIndex]="tabIndex">
<div *ngFor="let item of items" [style.order]="getItemOrder(item)" [ngStyle]="getItemStyles(item)"> <div *ngFor="let item of items" [style.order]="getItemOrder(item)" [ngStyle]="getItemStyles(item)">
<model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore"> <model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore">
</model-component-wrapper> </model-component-wrapper>
@@ -76,17 +78,24 @@ export default class DivContainer extends ContainerBase<azdata.DivItemLayout> im
private updateOverflowY() { private updateOverflowY() {
this._overflowY = this.overflowY; this._overflowY = this.overflowY;
if (this._overflowY) { if (this._overflowY) {
let element = <HTMLElement> this.divContainer.nativeElement; let element = <HTMLElement>this.divContainer.nativeElement;
element.style.overflowY = this._overflowY; element.style.overflowY = this._overflowY;
} }
} }
private updateScroll() { private updateScroll() {
let element = <HTMLElement> this.divContainer.nativeElement; let element = <HTMLElement>this.divContainer.nativeElement;
element.scrollTop = element.scrollTop - this.yOffsetChange; element.scrollTop = element.scrollTop - this.yOffsetChange;
element.dispatchEvent(new Event('scroll')); element.dispatchEvent(new Event('scroll'));
} }
private onClick() {
this.fireEvent({
eventType: ComponentEventType.onDidClick,
args: undefined
});
}
// CSS-bound properties // CSS-bound properties
public get height(): string { public get height(): string {
return this._height; return this._height;
@@ -111,6 +120,22 @@ export default class DivContainer extends ContainerBase<azdata.DivItemLayout> im
this.setPropertyFromUI<azdata.DivContainerProperties, any>((properties, newValue) => { properties.yOffsetChange = newValue; }, newValue); this.setPropertyFromUI<azdata.DivContainerProperties, any>((properties, newValue) => { properties.yOffsetChange = newValue; }, newValue);
} }
public get clickable(): boolean {
return this.getPropertyOrDefault<azdata.DivContainerProperties, boolean>((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 { private getItemOrder(item: DivItem): number {
return item.config ? item.config.order : 0; return item.config ? item.config.order : 0;
} }

View File

@@ -1224,6 +1224,12 @@ class FileBrowserTreeComponentWrapper extends ComponentWrapper implements azdata
} }
class DivContainerWrapper extends ComponentWrapper implements azdata.DivContainer { 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<any>());
}
public get overflowY(): string { public get overflowY(): string {
return this.properties['overflowY']; return this.properties['overflowY'];
} }
@@ -1239,6 +1245,11 @@ class DivContainerWrapper extends ComponentWrapper implements azdata.DivContaine
public set yOffsetChange(value: number) { public set yOffsetChange(value: number) {
this.setProperty('yOffsetChange', value); this.setProperty('yOffsetChange', value);
} }
public get onDidClick(): vscode.Event<any> {
let emitter = this._emitterMap.get(ComponentEventType.onDidClick);
return emitter && emitter.event;
}
} }
class TreeComponentWrapper<T> extends ComponentWrapper implements azdata.TreeComponent<T> { class TreeComponentWrapper<T> extends ComponentWrapper implements azdata.TreeComponent<T> {

View File

@@ -832,11 +832,11 @@ export function createApiFactory(
}, },
openDialog(dialog: sqlops.window.modelviewdialog.Dialog) { openDialog(dialog: sqlops.window.modelviewdialog.Dialog) {
console.warn('the method sqlops.window.modelviewdialog.openDialog has been deprecated, replace it with azdata.window.openDialog'); 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) { closeDialog(dialog: sqlops.window.modelviewdialog.Dialog) {
console.warn('the method sqlops.window.modelviewdialog.closeDialog has been deprecated, replace it with azdata.window.closeDialog'); 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 { createWizardPage(title: string): sqlops.window.modelviewdialog.WizardPage {
console.warn('the method sqlops.window.modelviewdialog.createWizardPage has been deprecated, replace it with azdata.window.createWizardPage'); 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); return extHostModelViewDialog.createButton(label);
}, },
openDialog(dialog: sqlops.window.Dialog) { openDialog(dialog: sqlops.window.Dialog) {
return extHostModelViewDialog.openDialog(dialog); return extHostModelViewDialog.openDialog(dialog as azdata.window.Dialog);
}, },
closeDialog(dialog: sqlops.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 { createWizardPage(title: string): sqlops.window.WizardPage {
return extHostModelViewDialog.createWizardPage(title); return extHostModelViewDialog.createWizardPage(title);