wizard for deploying bdc (#7183)

* wip

* wip2

* wip eod 820

* wip 822

* text component improvements and misc changes

* aria-label

* targetClusterPage wip

* target cluster page

* target cluster page

* wip 827

* wip deployment profile page

* profile page

* service settings page

* wip 0903

* 0909 wip

* 0910

* 0911

* sql instance and working directory

* notebooks

* docker version on windows

* EULA env var

* 917 updates

* address comments

* use async file access

* fix the summary page display issue for ad auth

* add save json file buttons

* use promise for private methds

* review feedbacks

* refactor

* pass json to notebooks

* fix no tool scenario

* bypass tool check if installed

* update hint text

* update notebooks

* workaround azdata first time use

* comments

* accept eula and some text update

* fix the error in package.json

* promise instead of thenable

* comments

* fix typo
This commit is contained in:
Alan Ren
2019-09-25 10:04:13 -07:00
committed by GitHub
parent 6a6048d40f
commit a0e31fc723
51 changed files with 4137 additions and 855 deletions

View File

@@ -2,7 +2,10 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as azdata from 'azdata';
import { SemVer } from 'semver';
export const NoteBookEnvironmentVariablePrefix = 'AZDATA_NB_VAR_';
export interface ResourceType {
name: string;
@@ -12,9 +15,15 @@ export interface ResourceType {
icon: { light: string; dark: string };
options: ResourceTypeOption[];
providers: DeploymentProvider[];
agreement?: AgreementInfo;
getProvider(selectedOptions: { option: string, value: string }[]): DeploymentProvider | undefined;
}
export interface AgreementInfo {
template: string;
links: azdata.LinkArea[];
}
export interface ResourceTypeOption {
name: string;
displayName: string;
@@ -32,10 +41,16 @@ export interface DeploymentProvider {
notebook: string | NotebookInfo;
downloadUrl: string;
webPageUrl: string;
wizard: WizardInfo;
requiredTools: ToolRequirementInfo[];
when: string;
}
export interface WizardInfo {
notebook: string | NotebookInfo;
type: BdcDeploymentType;
}
export interface DialogInfo {
notebook: string | NotebookInfo;
title: string;
@@ -45,27 +60,56 @@ export interface DialogInfo {
export interface DialogTabInfo {
title: string;
sections: DialogSectionInfo[];
sections: SectionInfo[];
labelWidth?: string;
inputWidth?: string;
}
export interface DialogSectionInfo {
export interface SectionInfo {
title: string;
fields: DialogFieldInfo[];
fields?: FieldInfo[]; // Use this if the dialog is not wide. All fields will be displayed in one column, label will be placed on top of the input component.
rows?: RowInfo[]; // Use this for wide dialog or wizard. label will be placed to the left of the input component.
labelWidth?: string;
inputWidth?: string;
labelPosition?: LabelPosition; // Default value is top
collapsible?: boolean;
collapsed?: boolean;
spaceBetweenFields?: string;
}
export interface DialogFieldInfo {
export interface RowInfo {
fields: FieldInfo[];
}
export interface FieldInfo {
label: string;
variableName: string;
variableName?: string;
type: FieldType;
defaultValue: string;
confirmationRequired: boolean;
confirmationLabel: string;
defaultValue?: string;
confirmationRequired?: boolean;
confirmationLabel?: string;
min?: number;
max?: number;
required: boolean;
options: string[];
placeHolder: string;
userName?: string; //needed for sql server's password complexity requirement check, password can not include the login name.
required?: boolean;
options?: string[] | azdata.CategoryValue[];
placeHolder?: string;
userName?: string; // needed for sql server's password complexity requirement check, password can not include the login name.
labelWidth?: string;
inputWidth?: string;
description?: string;
useCustomValidator?: boolean;
labelPosition?: LabelPosition; // overwrite the labelPosition of SectionInfo.
fontStyle?: FontStyle;
}
export enum LabelPosition {
Top = 'top',
Left = 'left'
}
export enum FontStyle {
Normal = 'normal',
Italic = 'italic'
}
export enum FieldType {
@@ -74,7 +118,9 @@ export enum FieldType {
DateTimeText = 'datetime_text',
SQLPassword = 'sql_password',
Password = 'password',
Options = 'options'
Options = 'options',
ReadonlyText = 'readonly_text',
Checkbox = 'checkbox'
}
export interface NotebookInfo {
@@ -100,4 +146,15 @@ export interface ITool {
readonly displayName: string;
readonly description: string;
readonly type: ToolType;
readonly version: SemVer | undefined;
readonly homePage: string;
readonly isInstalled: boolean;
loadInformation(): Promise<void>;
readonly statusDescription: string | undefined;
}
export enum BdcDeploymentType {
NewAKS = 'new-aks',
ExistingAKS = 'existing-aks',
ExistingKubeAdm = 'existing-kubeadm'
}

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
@@ -17,8 +16,8 @@ import { ResourceTypePickerDialog } from './ui/resourceTypePickerDialog';
const localize = nls.loadMessageBundle();
export function activate(context: vscode.ExtensionContext) {
const platformService = new PlatformService();
const toolsService = new ToolsService();
const platformService = new PlatformService(context.globalStoragePath);
const toolsService = new ToolsService(platformService);
const notebookService = new NotebookService(platformService, context.extensionPath);
const resourceTypeService = new ResourceTypeService(platformService, toolsService, notebookService);
const resourceTypes = resourceTypeService.getResourceTypes();

View File

@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { IPlatformService } from './platformService';
import { BigDataClusterDeploymentProfile } from './bigDataClusterDeploymentProfile';
interface BdcConfigListOutput {
stdout: string[];
}
export interface IAzdataService {
getDeploymentProfiles(): Promise<BigDataClusterDeploymentProfile[]>;
}
export class AzdataService implements IAzdataService {
constructor(private platformService: IPlatformService) {
}
public async getDeploymentProfiles(): Promise<BigDataClusterDeploymentProfile[]> {
await this.ensureWorkingDirectoryExists();
const profileNames = await this.getDeploymentProfileNames();
return await Promise.all(profileNames.map(profile => this.getDeploymentProfileInfo(profile)));
}
private async getDeploymentProfileNames(): Promise<string[]> {
const env: NodeJS.ProcessEnv = {};
// azdata requires this environment variables to be set
env['ACCEPT_EULA'] = 'yes';
const cmd = 'azdata bdc config list -o json';
// Run the command twice to workaround the issue:
// First time use of the azdata will have extra EULA related string in the output
// there is no easy and reliable way to filter out the profile names from it.
await this.platformService.runCommand(cmd, { additionalEnvironmentVariables: env });
const stdout = await this.platformService.runCommand(cmd);
const output = <BdcConfigListOutput>JSON.parse(stdout);
return output.stdout;
}
private async getDeploymentProfileInfo(profileName: string): Promise<BigDataClusterDeploymentProfile> {
await this.platformService.runCommand(`azdata bdc config init --source ${profileName} --target ${profileName} --force`, { workingDirectory: this.platformService.storagePath() });
const configObjects = await Promise.all([
this.getJsonObjectFromFile(path.join(this.platformService.storagePath(), profileName, 'bdc.json')),
this.getJsonObjectFromFile(path.join(this.platformService.storagePath(), profileName, 'control.json'))
]);
return new BigDataClusterDeploymentProfile(profileName, configObjects[0], configObjects[1]);
}
private async ensureWorkingDirectoryExists(): Promise<void> {
if (! await this.platformService.fileExists(this.platformService.storagePath())) {
await this.platformService.makeDirectory(this.platformService.storagePath());
}
}
private async getJsonObjectFromFile(path: string): Promise<any> {
return JSON.parse(await this.platformService.readTextFile(path));
}
}

View File

@@ -0,0 +1,264 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const SqlServerMasterResource = 'master';
export const DataResource = 'data-0';
export const HdfsResource = 'storage-0';
export const ComputeResource = 'compute-0';
export const NameNodeResource = 'nmnode-0';
export const SparkHeadResource = 'sparkhead';
export const ZooKeeperResource = 'zookeeper';
export const SparkResource = 'spark-0';
export const HadrEnabledSetting = 'hadr.enabled';
interface ServiceEndpoint {
port: number;
serviceType: ServiceType;
name: EndpointName;
}
type ServiceType = 'NodePort' | 'LoadBalancer';
type EndpointName = 'Controller' | 'Master' | 'Knox' | 'MasterSecondary';
export class BigDataClusterDeploymentProfile {
constructor(private _profileName: string, private _bdcConfig: any, private _controlConfig: any) {
// TODO: add validation logic for these 2 objects
// https://github.com/microsoft/azuredatastudio/issues/7344
}
public get profileName(): string {
return this._profileName;
}
public get clusterName(): string {
return this._bdcConfig.metadata.name;
}
public set clusterName(value: string) {
this._bdcConfig.metadata.name = value;
}
public get bdcConfig(): any {
return this._bdcConfig;
}
public get controlConfig(): any {
return this._controlConfig;
}
public get sqlServerReplicas(): number {
return this.getReplicas(SqlServerMasterResource);
}
public set sqlServerReplicas(replicas: number) {
this.setReplicas(SqlServerMasterResource, replicas);
}
public get hdfsNameNodeReplicas(): number {
return this.getReplicas(NameNodeResource);
}
public set hdfsNameNodeReplicas(replicas: number) {
this.setReplicas(NameNodeResource, replicas);
}
public get sparkHeadReplicas(): number {
return this.getReplicas(SparkHeadResource);
}
public set sparkHeadReplicas(replicas: number) {
this.setReplicas(SparkHeadResource, replicas);
}
public get dataReplicas(): number {
return this.getReplicas(DataResource);
}
public set dataReplicas(replicas: number) {
this.setReplicas(SparkHeadResource, replicas);
}
public get hdfsReplicas(): number {
return this.getReplicas(HdfsResource);
}
public set hdfsReplicas(replicas: number) {
this.setReplicas(HdfsResource, replicas);
}
public get zooKeeperReplicas(): number {
return this.getReplicas(ZooKeeperResource);
}
public set zooKeeperReplicas(replicas: number) {
this.setReplicas(ZooKeeperResource, replicas);
}
public get computeReplicas(): number {
return this.getReplicas(ComputeResource);
}
public set computeReplicas(replicas: number) {
this.setReplicas(ComputeResource, replicas);
}
public get sparkReplicas(): number {
return this._bdcConfig.spec.resources[SparkResource] ? this.getReplicas(SparkResource) : 0;
}
public get hadrEnabled(): boolean {
const value = this._bdcConfig.spec.resources[SqlServerMasterResource].spec.settings.sql[HadrEnabledSetting];
return value === true || value === 'true';
}
public set hadrEnabled(value: boolean) {
this._bdcConfig.spec.resources[SqlServerMasterResource].spec.settings.sql[HadrEnabledSetting] = value;
}
public get includeSpark(): boolean {
return <boolean>this._bdcConfig.spec.resources[HdfsResource].spec.settings.spark.includeSpark;
}
public set includeSpark(value: boolean) {
this._bdcConfig.spec.resources[HdfsResource].spec.settings.spark.includeSpark = value;
}
public get controllerDataStorageClass(): string {
return <string>this._controlConfig.spec.storage.data.className;
}
public set controllerDataStorageClass(value: string) {
this._controlConfig.spec.storage.data.className = value;
}
public get controllerDataStorageSize(): number {
return <number>this._controlConfig.spec.storage.data.size.replace('Gi', '');
}
public set controllerDataStorageSize(value: number) {
this._controlConfig.spec.storage.data.size = value;
}
public get controllerLogsStorageClass(): string {
return <string>this._controlConfig.spec.storage.logs.className;
}
public set controllerLogsStorageClass(value: string) {
this._controlConfig.spec.storage.logs.className = value;
}
public get controllerLogsStorageSize(): number {
return <number>this._controlConfig.spec.storage.logs.size.replace('Gi', '');
}
public set controllerLogsStorageSize(value: number) {
this._controlConfig.spec.storage.logs.size = value;
}
public setResourceStorage(resourceName: 'data-0' | 'master' | 'storage-0', dataStorageClass: string, dataStorageSize: number, logsStorageClass: string, logsStorageSize: number) {
this.bdcConfig.spec.resources[resourceName]['storage'] = {
data: {
size: `${dataStorageSize}Gi`,
className: dataStorageClass,
accessMode: 'ReadWriteOnce'
},
logs: {
size: `${logsStorageSize}Gi`,
className: logsStorageClass,
accessMode: 'ReadWriteOnce'
}
};
}
public get controllerPort(): number {
return this.getEndpointPort(this._controlConfig.spec.endpoints, 'Controller', 30080);
}
public set controllerPort(port: number) {
this.setEndpointPort(this._controlConfig.spec.endpoints, 'Controller', port);
}
public get sqlServerPort(): number {
return this.getEndpointPort(this._bdcConfig.spec.resources.master.spec.endpoints, 'Master', 31433);
}
public set sqlServerPort(port: number) {
this.setEndpointPort(this._bdcConfig.spec.resources.master.spec.endpoints, 'Master', port);
}
public get sqlServerReadableSecondaryPort(): number {
return this.getEndpointPort(this._bdcConfig.spec.resources.master.spec.endpoints, 'MasterSecondary', 31436);
}
public set sqlServerReadableSecondaryPort(port: number) {
this.setEndpointPort(this._bdcConfig.spec.resources.master.spec.endpoints, 'MasterSecondary', port);
}
public get gatewayPort(): number {
return this.getEndpointPort(this._bdcConfig.spec.resources.gateway.spec.endpoints, 'Knox', 30443);
}
public set gatewayPort(port: number) {
this.setEndpointPort(this._bdcConfig.spec.resources.gateway.spec.endpoints, 'Knox', port);
}
public addSparkResource(replicas: number): void {
this._bdcConfig.spec.resources[SparkResource] = {
metadata: {
kind: 'Pool',
name: 'default'
},
spec: {
type: 'Spark',
replicas: replicas
}
};
this._bdcConfig.spec.services.spark.resources.push(SparkResource);
this._bdcConfig.spec.services.hdfs.resources.push(SparkResource);
}
public get activeDirectorySupported(): boolean {
// TODO: Implement AD authentication
return false;
}
public getBdcJson(readable: boolean = true): string {
return this.stringifyJson(this._bdcConfig, readable);
}
public getControlJson(readable: boolean = true): string {
return this.stringifyJson(this._controlConfig, readable);
}
private stringifyJson(obj: any, readable: boolean): string {
return JSON.stringify(obj, undefined, readable ? 4 : 0);
}
private getReplicas(resourceName: string): number {
return <number>this._bdcConfig.spec.resources[resourceName].spec.replicas;
}
private setReplicas(resourceName: string, replicas: number): void {
this._bdcConfig.spec.resources[resourceName].spec.replicas = replicas;
}
private getEndpointPort(endpoints: ServiceEndpoint[], name: EndpointName, defaultValue: number): number {
const endpoint = endpoints.find(endpoint => endpoint.name === name);
return endpoint ? endpoint.port : defaultValue;
}
private setEndpointPort(endpoints: ServiceEndpoint[], name: EndpointName, port: number): void {
const endpoint = endpoints.find(endpoint => endpoint.name === name);
if (endpoint) {
endpoint.port = port;
} else {
endpoints.push({
name: name,
serviceType: 'NodePort',
port: port
});
}
}
}

View File

@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as os from 'os';
import * as yamljs from 'yamljs';
import * as fs from 'fs';
export interface KubeClusterContext {
name: string;
isCurrentContext: boolean;
}
export interface IKubeService {
getDefautConfigPath(): string;
getClusterContexts(configFile: string): Promise<KubeClusterContext[]>;
}
export class KubeService implements IKubeService {
getDefautConfigPath(): string {
return path.join(os.homedir(), '.kube', 'config');
}
getClusterContexts(configFile: string): Promise<KubeClusterContext[]> {
return fs.promises.access(configFile).catch((error) => {
if (error && error.code === 'ENOENT') {
return [];
} else {
throw error;
}
}).then(() => {
const config = yamljs.load(configFile);
const rawContexts = <any[]>config['contexts'];
const currentContext = <string>config['current-context'];
const contexts: KubeClusterContext[] = [];
if (currentContext && rawContexts && rawContexts.length > 0) {
rawContexts.forEach(rawContext => {
const name = <string>rawContext['name'];
if (name) {
contexts.push({
name: name,
isCurrentContext: name === currentContext
});
}
});
}
return contexts;
});
}
}

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as azdata from 'azdata';
import * as path from 'path';
@@ -14,7 +13,7 @@ import { NotebookInfo } from '../interfaces';
const localize = nls.loadMessageBundle();
export interface INotebookService {
launchNotebook(notebook: string | NotebookInfo): void;
launchNotebook(notebook: string | NotebookInfo): Thenable<azdata.nb.NotebookEditor>;
}
export class NotebookService implements INotebookService {
@@ -25,18 +24,22 @@ export class NotebookService implements INotebookService {
* Copy the notebook to the user's home directory and launch the notebook from there.
* @param notebook the path of the notebook
*/
launchNotebook(notebook: string | NotebookInfo): void {
launchNotebook(notebook: string | NotebookInfo): Thenable<azdata.nb.NotebookEditor> {
const notebookPath = this.getNotebook(notebook);
const notebookFullPath = path.join(this.extensionPath, notebookPath);
if (notebookPath && this.platformService.fileExists(notebookPath)) {
this.showNotebookAsUntitled(notebookPath);
}
else if (notebookPath && this.platformService.fileExists(notebookFullPath)) {
this.showNotebookAsUntitled(notebookFullPath);
}
else {
this.platformService.showErrorMessage(localize('resourceDeployment.notebookNotFound', "The notebook {0} does not exist", notebookPath));
}
return this.platformService.fileExists(notebookPath).then((notebookPathExists) => {
if (notebookPathExists) {
return this.showNotebookAsUntitled(notebookPath);
} else {
return this.platformService.fileExists(notebookFullPath).then(notebookFullPathExists => {
if (notebookFullPathExists) {
return this.showNotebookAsUntitled(notebookFullPath);
} else {
throw localize('resourceDeployment.notebookNotFound', "The notebook {0} does not exist", notebookPath);
}
});
}
});
}
/**
@@ -74,12 +77,12 @@ export class NotebookService implements INotebookService {
return title;
}
showNotebookAsUntitled(notebookPath: string): void {
showNotebookAsUntitled(notebookPath: string): Thenable<azdata.nb.NotebookEditor> {
let targetFileName: string = this.findNextUntitledEditorName(notebookPath);
const untitledFileName: vscode.Uri = vscode.Uri.parse(`untitled:${targetFileName}`);
vscode.workspace.openTextDocument(notebookPath).then((document) => {
return vscode.workspace.openTextDocument(notebookPath).then((document) => {
let initialContent = document.getText();
azdata.nb.showNotebookDocument(untitledFileName, {
return azdata.nb.showNotebookDocument(untitledFileName, {
connectionProfile: undefined,
preview: false,
initialContent: initialContent,

View File

@@ -2,37 +2,58 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as fs from 'fs';
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as cp from 'child_process';
/**
* Abstract of platform dependencies
*/
export interface IPlatformService {
platform(): string;
copyFile(source: string, target: string): void;
fileExists(file: string): boolean;
storagePath(): string;
copyFile(source: string, target: string): Promise<void>;
fileExists(file: string): Promise<boolean>;
openFile(filePath: string): void;
showErrorMessage(message: string): void;
isNotebookNameUsed(title: string): boolean;
makeDirectory(path: string): Promise<void>;
readTextFile(filePath: string): Promise<string>;
runCommand(command: string, options?: CommandOptions): Promise<string>;
}
export interface CommandOptions {
workingDirectory?: string;
additionalEnvironmentVariables?: NodeJS.ProcessEnv;
}
export class PlatformService implements IPlatformService {
constructor(private _storagePath: string = '') {
}
storagePath(): string {
return this._storagePath;
}
platform(): string {
return process.platform;
}
copyFile(source: string, target: string): void {
// tslint:disable-next-line:no-sync
fs.copyFileSync(source, target);
copyFile(source: string, target: string): Promise<void> {
return fs.promises.copyFile(source, target);
}
fileExists(file: string): boolean {
// tslint:disable-next-line:no-sync
return fs.existsSync(file);
fileExists(file: string): Promise<boolean> {
return fs.promises.access(file).then(() => {
return true;
}).catch(error => {
if (error && error.code === 'ENOENT') {
return false;
}
throw error;
});
}
openFile(filePath: string): void {
@@ -46,4 +67,28 @@ export class PlatformService implements IPlatformService {
isNotebookNameUsed(title: string): boolean {
return (azdata.nb.notebookDocuments.findIndex(doc => doc.isUntitled && doc.fileName === title) > -1);
}
makeDirectory(path: string): Promise<void> {
return fs.promises.mkdir(path);
}
readTextFile(filePath: string): Promise<string> {
return fs.promises.readFile(filePath, 'utf8');
}
runCommand(command: string, options?: CommandOptions): Promise<string> {
return new Promise<string>((resolve, reject) => {
const env = Object.assign({}, process.env, options && options.additionalEnvironmentVariables);
cp.exec(command, {
cwd: options && options.workingDirectory,
env: env
}, (error, stdout, stderror) => {
if (error) {
reject(error);
} else {
resolve(stdout);
}
});
});
}
}

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as azdata from 'azdata';
import * as cp from 'child_process';
@@ -16,7 +15,10 @@ import { INotebookService } from './notebookService';
import { IPlatformService } from './platformService';
import { IToolsService } from './toolsService';
import { ResourceType, ResourceTypeOption, DeploymentProvider } from '../interfaces';
import { DeployClusterWizard } from '../ui/deployClusterWizard/deployClusterWizard';
import { NotebookInputDialog } from '../ui/notebookInputDialog';
import { KubeService } from './kubeService';
import { AzdataService } from './azdataService';
const localize = nls.loadMessageBundle();
export interface IResourceTypeService {
@@ -138,7 +140,7 @@ export class ResourceTypeService implements IResourceTypeService {
let providerIndex = 1;
resourceType.providers.forEach(provider => {
const providerPositionInfo = `${positionInfo}, provider index: ${providerIndex} `;
if (!provider.dialog && !provider.notebook && !provider.downloadUrl && !provider.webPageUrl) {
if (!provider.wizard && !provider.dialog && !provider.notebook && !provider.downloadUrl && !provider.webPageUrl) {
errorMessages.push(`No deployment method defined for the provider, ${providerPositionInfo}`);
}
@@ -195,7 +197,10 @@ export class ResourceTypeService implements IResourceTypeService {
public startDeployment(provider: DeploymentProvider): void {
const self = this;
if (provider.dialog) {
if (provider.wizard) {
const wizard = new DeployClusterWizard(provider.wizard, new KubeService(), new AzdataService(this.platformService), this.notebookService);
wizard.open();
} else if (provider.dialog) {
const dialog = new NotebookInputDialog(this.notebookService, provider.dialog);
dialog.open();
} else if (provider.notebook) {

View File

@@ -2,12 +2,20 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ToolType, ITool } from '../../interfaces';
import { ToolType } from '../../interfaces';
import * as nls from 'vscode-nls';
import { SemVer } from 'semver';
import { IPlatformService } from '../platformService';
import { EOL } from 'os';
import { ToolBase } from './toolBase';
const localize = nls.loadMessageBundle();
export class AzCliTool implements ITool {
export class AzCliTool extends ToolBase {
constructor(platformService: IPlatformService) {
super(platformService);
}
get name(): string {
return 'azcli';
}
@@ -23,4 +31,19 @@ export class AzCliTool implements ITool {
get displayName(): string {
return localize('resourceDeployment.AzCLIDisplayName', 'Azure CLI');
}
get homePage(): string {
return 'https://docs.microsoft.com/cli/azure/install-azure-cli';
}
protected getVersionFromOutput(output: string): SemVer | undefined {
if (output && output.includes('azure-cli')) {
return new SemVer(output.split(EOL)[0].replace('azure-cli', '').replace(/ /g, '').replace('*', ''));
} else {
return undefined;
}
}
protected get versionCommand(): string {
return 'az --version';
}
}

View File

@@ -2,12 +2,21 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ToolType, ITool } from '../../interfaces';
import { ToolType } from '../../interfaces';
import * as nls from 'vscode-nls';
import { SemVer } from 'semver';
import { EOL } from 'os';
import { IPlatformService } from '../platformService';
import { ToolBase } from './toolBase';
const localize = nls.loadMessageBundle();
export class AzdataTool implements ITool {
export class AzdataTool extends ToolBase {
constructor(platformService: IPlatformService) {
super(platformService);
}
get name(): string {
return 'azdata';
}
@@ -23,4 +32,20 @@ export class AzdataTool implements ITool {
get displayName(): string {
return localize('resourceDeployment.AzdataDisplayName', "azdata");
}
}
get homePage(): string {
return 'https://docs.microsoft.com/sql/big-data-cluster/deploy-install-azdata';
}
protected get versionCommand(): string {
return 'azdata -v';
}
protected getVersionFromOutput(output: string): SemVer | undefined {
let version: SemVer | undefined = undefined;
if (output && output.split(EOL).length > 0) {
version = new SemVer(output.split(EOL)[0].replace(/ /g, ''));
}
return version;
}
}

View File

@@ -2,12 +2,20 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ToolType, ITool } from '../../interfaces';
import { ToolType } from '../../interfaces';
import * as nls from 'vscode-nls';
import { SemVer } from 'semver';
import { IPlatformService } from '../platformService';
import { ToolBase } from './toolBase';
const localize = nls.loadMessageBundle();
export class DockerTool implements ITool {
export class DockerTool extends ToolBase {
constructor(platformService: IPlatformService) {
super(platformService);
}
get name(): string {
return 'docker';
}
@@ -23,4 +31,19 @@ export class DockerTool implements ITool {
get displayName(): string {
return localize('resourceDeployment.DockerDisplayName', 'Docker');
}
}
get homePage(): string {
return 'https://docs.docker.com/install';
}
protected getVersionFromOutput(output: string): SemVer | undefined {
let version: SemVer | undefined = undefined;
if (output) {
version = new SemVer(JSON.parse(output).Client.Version, true);
}
return version;
}
protected get versionCommand(): string {
return 'docker version --format "{{json .}}"';
}
}

View File

@@ -2,12 +2,20 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ToolType, ITool } from '../../interfaces';
import { ToolType } from '../../interfaces';
import * as nls from 'vscode-nls';
import { SemVer } from 'semver';
import { IPlatformService } from '../platformService';
import { ToolBase } from './toolBase';
const localize = nls.loadMessageBundle();
export class KubeCtlTool implements ITool {
export class KubeCtlTool extends ToolBase {
constructor(platformService: IPlatformService) {
super(platformService);
}
get name(): string {
return 'kubectl';
}
@@ -23,4 +31,21 @@ export class KubeCtlTool implements ITool {
get displayName(): string {
return localize('resourceDeployment.KubeCtlDisplayName', 'kubectl');
}
get homePage(): string {
return 'https://kubernetes.io/docs/tasks/tools/install-kubectl';
}
protected getVersionFromOutput(output: string): SemVer | undefined {
let version: SemVer | undefined = undefined;
if (output) {
const versionJson = JSON.parse(output);
version = new SemVer(`${versionJson.clientVersion.major}.${versionJson.clientVersion.minor}.0`);
}
return version;
}
protected get versionCommand(): string {
return 'kubectl version -o json --client';
}
}

View File

@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ToolType, ITool } from '../../interfaces';
import { SemVer } from 'semver';
import { IPlatformService } from '../platformService';
import * as nls from 'vscode-nls';
import { EOL } from 'os';
const localize = nls.loadMessageBundle();
export abstract class ToolBase implements ITool {
constructor(private _platformService: IPlatformService) { }
abstract name: string;
abstract displayName: string;
abstract description: string;
abstract type: ToolType;
abstract homePage: string;
protected abstract getVersionFromOutput(output: string): SemVer | undefined;
protected abstract readonly versionCommand: string;
public get version(): SemVer | undefined {
return this._version;
}
public get isInstalled(): boolean {
return this._isInstalled;
}
public get statusDescription(): string | undefined {
return this._statusDescription;
}
public loadInformation(): Promise<void> {
if (this._isInstalled) {
return Promise.resolve();
}
this._isInstalled = false;
this._statusDescription = undefined;
this._version = undefined;
this._versionOutput = undefined;
return this._platformService.runCommand(this.versionCommand).then((stdout) => {
this._versionOutput = stdout;
this._version = this.getVersionFromOutput(stdout);
if (this._version) {
this._isInstalled = true;
} else {
throw localize('deployCluster.InvalidToolVersionOutput', "Invalid output received.");
}
}).catch((error) => {
const errorMessage = typeof error === 'string' ? error :
typeof error.message === 'string' ? error.message : '';
this._statusDescription = localize('deployCluster.GetToolVersionError', "Error retrieving version information.{0}Error: {1}{0}stdout: {2} ", EOL, errorMessage, this._versionOutput);
});
}
private _isInstalled: boolean = false;
private _version?: SemVer;
private _statusDescription?: string;
private _versionOutput?: string;
}

View File

@@ -2,12 +2,12 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ITool } from '../interfaces';
import { DockerTool } from './tools/dockerTool';
import { AzCliTool } from './tools/azCliTool';
import { AzdataTool } from './tools/azdataTool';
import { KubeCtlTool } from './tools/kubeCtlTool';
import { IPlatformService } from './platformService';
export interface IToolsService {
getToolByName(toolName: string): ITool | undefined;
@@ -16,11 +16,11 @@ export interface IToolsService {
export class ToolsService implements IToolsService {
private supportedTools: ITool[];
constructor() {
this.supportedTools = [new DockerTool(), new AzCliTool(), new AzdataTool(), new KubeCtlTool()];
constructor(private _platformService: IPlatformService) {
this.supportedTools = [new DockerTool(this._platformService), new AzCliTool(this._platformService), new AzdataTool(this._platformService), new KubeCtlTool(this._platformService)];
}
getToolByName(toolName: string): ITool | undefined {
return this.supportedTools.find(t => t.name === toolName);
}
}
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as TypeMoq from 'typemoq';
import 'mocha';
import { NotebookService } from '../services/notebookService';
@@ -96,4 +94,4 @@ suite('Notebook Service Tests', function (): void {
mockPlatformService.verify((service) => service.isNotebookNameUsed(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(3));
assert.equal(actualFileName, expectedFileName, 'target file name is not correct');
});
});
});

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'mocha';
import * as TypeMoq from 'typemoq';
import assert = require('assert');
@@ -18,7 +16,7 @@ suite('Resource Type Service Tests', function (): void {
test('test resource types', () => {
const mockPlatformService = TypeMoq.Mock.ofType<IPlatformService>();
const toolsService = new ToolsService();
const toolsService = new ToolsService(mockPlatformService.object);
const notebookService = new NotebookService(mockPlatformService.object, '');
const resourceTypeService = new ResourceTypeService(mockPlatformService.object, toolsService, notebookService);
// index 0: platform name, index 1: number of expected resource types

View File

@@ -3,18 +3,19 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'mocha';
import assert = require('assert');
import * as TypeMoq from 'typemoq';
import { ToolsService } from '../services/toolsService';
import { ToolType } from '../interfaces';
import { isNumber } from 'util';
import { IPlatformService } from '../services/platformService';
suite('Tools Service Tests', function (): void {
test('run getToolByName with all known values', () => {
const toolsService = new ToolsService();
const mockPlatformService = TypeMoq.Mock.ofType<IPlatformService>();
const toolsService = new ToolsService(mockPlatformService.object);
const tools: { name: string; type: ToolType }[] = [
{ name: 'azcli', type: ToolType.AzCli },
@@ -42,8 +43,9 @@ suite('Tools Service Tests', function (): void {
});
test('run getToolByName with a name that is not defined', () => {
const toolsService = new ToolsService();
const mockPlatformService = TypeMoq.Mock.ofType<IPlatformService>();
const toolsService = new ToolsService(mockPlatformService.object);
const tool = toolsService.getToolByName('no-such-tool');
assert(tool === undefined, 'for a not defined tool, expected value is undefined');
});
});
});

View File

@@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const DeploymentProfile_VariableName = 'AZDATA_NB_VAR_BDC_DEPLOYMENT_PROFILE';
export const ClusterName_VariableName = 'AZDATA_NB_VAR_BDC_CLUSTER_NAME';
export const AdminUserName_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_USERNAME';
export const AdminPassword_VariableName = 'AZDATA_NB_VAR_BDC_ADMIN_PASSWORD';
export const AuthenticationMode_VariableName = 'AZDATA_NB_VAR_BDC_AUTHENTICATION_MODE';
export const DistinguishedName_VariableName = 'AZDATA_NB_VAR_BDC_AD_DN';
export const AdminPrincipals_VariableName = 'AZDATA_NB_VAR_BDC_AD_ADMIN_PRINCIPALS';
export const UserPrincipals_VariableName = 'AZDATA_NB_VAR_BDC_AD_USER_PRINCIPALS';
export const UpstreamIPAddresses_VariableName = 'AZDATA_NB_VAR_BDC_AD_UPSTREAM_IPADDRESSES';
export const DnsName_VariableName = 'AZDATA_NB_VAR_BDC_AD_DNS_NAME';
export const Realm_VariableName = 'AZDATA_NB_VAR_BDC_AD_REALM';
export const AppOwnerPrincipals_VariableName = 'AZDATA_NB_VAR_AD_BDC_APP_OWNER_PRINCIPALS';
export const AppReaderPrincipals_VariableName = 'AZDATA_NB_VAR_AD_BDC_APP_READER_PRINCIPALS';
export const SubscriptionId_VariableName = 'AZDATA_NB_VAR_BDC_AZURE_SUBSCRIPTION';
export const ResourceGroup_VariableName = 'AZDATA_NB_VAR_BDC_RESOURCEGROUP_NAME';
export const Region_VariableName = 'AZDATA_NB_VAR_BDC_AZURE_REGION';
export const AksName_VariableName = 'AZDATA_NB_VAR_BDC_AKS_NAME';
export const VMSize_VariableName = 'AZDATA_NB_VAR_BDC_AZURE_VM_SIZE';
export const VMCount_VariableName = 'AZDATA_NB_VAR_BDC_VM_COUNT';
export const KubeConfigPath_VariableName = 'AZDATA_NB_VAR_BDC_KUBECONFIG_PATH';
export const ClusterContext_VariableName = 'AZDATA_NB_VAR_BDC_CLUSTER_CONTEXT';
export const SQLServerScale_VariableName = 'AZDATA_NB_VAR_BDC_SQLSERVER_SCALE';
export const HDFSPoolScale_VariableName = 'AZDATA_NB_VAR_BDC_HDFSPOOL_SCALE';
export const HDFSNameNodeScale_VariableName = 'AZDATA_NB_VAR_BDC_NAMENODE_SCALE';
export const ZooKeeperScale_VariableName = 'AZDATA_NB_VAR_BDC_ZOOKEEPER_SCALE';
export const SparkHeadScale_VariableName = 'AZDATA_NB_VAR_BDC_SPARKHEAD_SCALE';
export const IncludeSpark_VariableName = 'AZDATA_NB_VAR_BDC_INCLUDESPARK';
export const ComputePoolScale_VariableName = 'AZDATA_NB_VAR_BDC_COMPUTEPOOL_SCALE';
export const DataPoolScale_VariableName = 'AZDATA_NB_VAR_BDC_DATAPOOL_SCALE';
export const SparkPoolScale_VariableName = 'AZDATA_NB_VAR_BDC_SPARKPOOL_SCALE';
export const ControllerDataStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_DATA_STORAGE_CLASS';
export const ControllerDataStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_DATA_STORAGE_SIZE';
export const ControllerLogsStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_LOGS_STORAGE_CLASS';
export const ControllerLogsStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_LOGS_STORAGE_SIZE';
export const DataPoolDataStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_DATA_DATA_STORAGE_CLASS';
export const DataPoolDataStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_DATA_DATA_STORAGE_SIZE';
export const DataPoolLogsStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_DATA_LOGS_STORAGE_CLASS';
export const DataPoolLogsStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_DATA_LOGS_STORAGE_SIZE';
export const HDFSDataStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_HDFS_DATA_STORAGE_CLASS';
export const HDFSDataStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_HDFS_DATA_STORAGE_SIZE';
export const HDFSLogsStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_HDFS_LOGS_STORAGE_CLASS';
export const HDFSLogsStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_HDFS_LOGS_STORAGE_SIZE';
export const SQLServerDataStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_SQL_DATA_STORAGE_CLASS';
export const SQLServerDataStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_SQL_DATA_STORAGE_SIZE';
export const SQLServerLogsStorageClassName_VariableName = 'AZDATA_NB_VAR_BDC_SQL_LOGS_STORAGE_CLASS';
export const SQLServerLogsStorageSize_VariableName = 'AZDATA_NB_VAR_BDC_SQL_LOGS_STORAGE_SIZE';
export const ControllerDNSName_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_DNS';
export const ControllerPort_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_PORT';
export const SQLServerDNSName_VariableName = 'AZDATA_NB_VAR_BDC_SQL_DNS';
export const SQLServerPort_VariableName = 'AZDATA_NB_VAR_BDC_SQL_PORT';
export const GatewayDNSName_VariableName = 'AZDATA_NB_VAR_BDC_GATEWAY_DNS';
export const GateWayPort_VariableName = 'AZDATA_NB_VAR_BDC_GATEWAY_PORT';
export const ReadableSecondaryDNSName_VariableName = 'AZDATA_NB_VAR_BDC_READABLE_SECONDARY_DNS';
export const ReadableSecondaryPort_VariableName = 'AZDATA_NB_VAR_BDC_READABLE_SECONDARY_PORT';
export const EnableHADR_VariableName = 'AZDATA_NB_VAR_BDC_ENABLE_HADR';

View File

@@ -0,0 +1,115 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { SummaryPage } from './pages/summaryPage';
import { WizardBase } from '../wizardBase';
import * as nls from 'vscode-nls';
import { WizardInfo, BdcDeploymentType } from '../../interfaces';
import { WizardPageBase } from '../wizardPageBase';
import { AzureSettingsPage } from './pages/azureSettingsPage';
import { ClusterSettingsPage } from './pages/clusterSettingsPage';
import { ServiceSettingsPage } from './pages/serviceSettingsPage';
import { TargetClusterContextPage } from './pages/targetClusterPage';
import { IKubeService } from '../../services/kubeService';
import { IAzdataService } from '../../services/azdataService';
import { DeploymentProfilePage } from './pages/deploymentProfilePage';
import { INotebookService } from '../../services/notebookService';
import { DeployClusterWizardModel } from './deployClusterWizardModel';
import * as VariableNames from './constants';
const localize = nls.loadMessageBundle();
export class DeployClusterWizard extends WizardBase<DeployClusterWizard, DeployClusterWizardModel> {
public get kubeService(): IKubeService {
return this._kubeService;
}
public get azdataService(): IAzdataService {
return this._azdataService;
}
public get notebookService(): INotebookService {
return this._notebookService;
}
constructor(private wizardInfo: WizardInfo, private _kubeService: IKubeService, private _azdataService: IAzdataService, private _notebookService: INotebookService) {
super(DeployClusterWizard.getTitle(wizardInfo.type), new DeployClusterWizardModel(wizardInfo.type));
}
public get deploymentType(): BdcDeploymentType {
return this.wizardInfo.type;
}
protected initialize(): void {
this.setPages(this.getPages());
this.wizardObject.generateScriptButton.hidden = true;
this.wizardObject.doneButton.label = localize('deployCluster.openNotebook', 'Open Notebook');
}
protected onCancel(): void {
}
protected onOk(): void {
process.env[VariableNames.AdminPassword_VariableName] = this.model.getStringValue(VariableNames.AdminPassword_VariableName);
this.notebookService.launchNotebook(this.wizardInfo.notebook).then((notebook: azdata.nb.NotebookEditor) => {
notebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => {
editBuilder.insertCell({
cell_type: 'code',
source: this.model.getCodeCellContentForNotebook()
}, 7);
});
}, (error) => {
vscode.window.showErrorMessage(error);
});
}
private getPages(): WizardPageBase<DeployClusterWizard>[] {
const pages: WizardPageBase<DeployClusterWizard>[] = [];
switch (this.deploymentType) {
case BdcDeploymentType.NewAKS:
pages.push(
new DeploymentProfilePage(this),
new AzureSettingsPage(this),
new ClusterSettingsPage(this),
new ServiceSettingsPage(this),
new SummaryPage(this));
break;
case BdcDeploymentType.ExistingAKS:
pages.push(
new DeploymentProfilePage(this),
new TargetClusterContextPage(this),
new ClusterSettingsPage(this),
new ServiceSettingsPage(this),
new SummaryPage(this));
break;
case BdcDeploymentType.ExistingKubeAdm:
pages.push(
new DeploymentProfilePage(this),
new TargetClusterContextPage(this),
new ClusterSettingsPage(this),
new ServiceSettingsPage(this),
new SummaryPage(this));
break;
default:
throw new Error(`Unknown deployment type: ${this.deploymentType}`);
}
return pages;
}
static getTitle(type: BdcDeploymentType): string {
switch (type) {
case BdcDeploymentType.NewAKS:
return localize('deployCluster.NewAKSWizardTitle', "Deploy SQL Server 2019 Big Data Cluster on a new AKS cluster");
case BdcDeploymentType.ExistingAKS:
return localize('deployCluster.ExistingAKSWizardTitle', "Deploy SQL Server 2019 Big Data Cluster on an existing AKS cluster");
case BdcDeploymentType.ExistingKubeAdm:
return localize('deployCluster.ExistingKubeAdm', "Deploy SQL Server 2019 Big Data Cluster on an existing kubeadm cluster");
default:
throw new Error(`Unknown deployment type: ${type}`);
}
}
}

View File

@@ -0,0 +1,152 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Model } from '../model';
import * as VariableNames from './constants';
import { BigDataClusterDeploymentProfile, DataResource, SqlServerMasterResource, HdfsResource } from '../../services/bigDataClusterDeploymentProfile';
import { BdcDeploymentType } from '../../interfaces';
import { EOL } from 'os';
export class DeployClusterWizardModel extends Model {
constructor(public deploymentTarget: BdcDeploymentType) {
super();
}
public adAuthSupported: boolean = false;
public get hadrEnabled(): boolean {
return this.getBooleanValue(VariableNames.EnableHADR_VariableName);
}
public set hadrEnabled(value: boolean) {
this.setPropertyValue(VariableNames.EnableHADR_VariableName, value);
}
public get authenticationMode(): string | undefined {
return this.getStringValue(VariableNames.AuthenticationMode_VariableName);
}
public set authenticationMode(value: string | undefined) {
this.setPropertyValue(VariableNames.AuthenticationMode_VariableName, value);
}
public getStorageSettingValue(propertyName: string, defaultValuePropertyName: string): string | undefined {
const value = this.getStringValue(propertyName);
return (value === undefined || value === '') ? this.getStringValue(defaultValuePropertyName) : value;
}
private setStorageSettingValue(propertyName: string, defaultValuePropertyName: string): void {
const value = this.getStringValue(propertyName);
if (value === undefined || value === '') {
this.setPropertyValue(propertyName, this.getStringValue(defaultValuePropertyName));
}
}
private setStorageSettingValues(): void {
this.setStorageSettingValue(VariableNames.DataPoolDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName);
this.setStorageSettingValue(VariableNames.DataPoolDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName);
this.setStorageSettingValue(VariableNames.DataPoolLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName);
this.setStorageSettingValue(VariableNames.DataPoolLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName);
this.setStorageSettingValue(VariableNames.HDFSDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName);
this.setStorageSettingValue(VariableNames.HDFSDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName);
this.setStorageSettingValue(VariableNames.HDFSLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName);
this.setStorageSettingValue(VariableNames.HDFSLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName);
this.setStorageSettingValue(VariableNames.SQLServerDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName);
this.setStorageSettingValue(VariableNames.SQLServerDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName);
this.setStorageSettingValue(VariableNames.SQLServerLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName);
this.setStorageSettingValue(VariableNames.SQLServerLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName);
}
public setEnvironmentVariables(): void {
this.setStorageSettingValues();
}
public selectedProfile: BigDataClusterDeploymentProfile | undefined;
public createTargetProfile(): BigDataClusterDeploymentProfile {
// create a copy of the source files to avoid changing the source profile values
const sourceBdcJson = Object.assign({}, this.selectedProfile!.bdcConfig);
const sourceControlJson = Object.assign({}, this.selectedProfile!.controlConfig);
const targetDeploymentProfile = new BigDataClusterDeploymentProfile('', sourceBdcJson, sourceControlJson);
// cluster name
targetDeploymentProfile.clusterName = this.getStringValue(VariableNames.ClusterName_VariableName)!;
// storage settings
targetDeploymentProfile.controllerDataStorageClass = this.getStringValue(VariableNames.ControllerDataStorageClassName_VariableName)!;
targetDeploymentProfile.controllerDataStorageSize = this.getIntegerValue(VariableNames.ControllerDataStorageSize_VariableName)!;
targetDeploymentProfile.controllerLogsStorageClass = this.getStringValue(VariableNames.ControllerLogsStorageClassName_VariableName)!;
targetDeploymentProfile.controllerLogsStorageSize = this.getIntegerValue(VariableNames.ControllerLogsStorageSize_VariableName)!;
targetDeploymentProfile.setResourceStorage(DataResource,
this.getStorageSettingValue(VariableNames.DataPoolDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName)!,
Number.parseInt(this.getStorageSettingValue(VariableNames.DataPoolDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName)!),
this.getStorageSettingValue(VariableNames.DataPoolLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName)!,
Number.parseInt(this.getStorageSettingValue(VariableNames.DataPoolLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName)!)
);
targetDeploymentProfile.setResourceStorage(SqlServerMasterResource,
this.getStorageSettingValue(VariableNames.SQLServerDNSName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName)!,
Number.parseInt(this.getStorageSettingValue(VariableNames.SQLServerDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName)!),
this.getStorageSettingValue(VariableNames.SQLServerLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName)!,
Number.parseInt(this.getStorageSettingValue(VariableNames.SQLServerLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName)!)
);
targetDeploymentProfile.setResourceStorage(HdfsResource,
this.getStorageSettingValue(VariableNames.HDFSDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName)!,
Number.parseInt(this.getStorageSettingValue(VariableNames.HDFSDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName)!),
this.getStorageSettingValue(VariableNames.HDFSLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName)!,
Number.parseInt(this.getStorageSettingValue(VariableNames.HDFSLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName)!)
);
// scale settings
targetDeploymentProfile.dataReplicas = this.getIntegerValue(VariableNames.DataPoolScale_VariableName);
targetDeploymentProfile.computeReplicas = this.getIntegerValue(VariableNames.ComputePoolScale_VariableName);
targetDeploymentProfile.hdfsReplicas = this.getIntegerValue(VariableNames.HDFSPoolScale_VariableName);
targetDeploymentProfile.sqlServerReplicas = this.getIntegerValue(VariableNames.SQLServerScale_VariableName);
targetDeploymentProfile.hdfsNameNodeReplicas = this.getIntegerValue(VariableNames.HDFSNameNodeScale_VariableName);
targetDeploymentProfile.sparkHeadReplicas = this.getIntegerValue(VariableNames.SparkHeadScale_VariableName);
targetDeploymentProfile.zooKeeperReplicas = this.getIntegerValue(VariableNames.ZooKeeperScale_VariableName);
const sparkScale = this.getIntegerValue(VariableNames.SparkPoolScale_VariableName);
if (sparkScale > 0) {
targetDeploymentProfile.addSparkResource(sparkScale);
}
targetDeploymentProfile.includeSpark = this.getBooleanValue(VariableNames.IncludeSpark_VariableName);
targetDeploymentProfile.hadrEnabled = this.getBooleanValue(VariableNames.EnableHADR_VariableName);
// port settings
targetDeploymentProfile.gatewayPort = this.getIntegerValue(VariableNames.GateWayPort_VariableName);
targetDeploymentProfile.sqlServerPort = this.getIntegerValue(VariableNames.SQLServerPort_VariableName);
targetDeploymentProfile.controllerPort = this.getIntegerValue(VariableNames.ControllerPort_VariableName);
targetDeploymentProfile.sqlServerReadableSecondaryPort = this.getIntegerValue(VariableNames.ReadableSecondaryPort_VariableName);
return targetDeploymentProfile;
}
public getCodeCellContentForNotebook(): string {
const profile = this.createTargetProfile();
const statements: string[] = [];
if (this.deploymentTarget === BdcDeploymentType.NewAKS) {
statements.push(`azure_subscription_id = '${this.getStringValue(VariableNames.SubscriptionId_VariableName, '')}'`);
statements.push(`azure_region = '${this.getStringValue(VariableNames.Region_VariableName)}'`);
statements.push(`azure_resource_group = '${this.getStringValue(VariableNames.ResourceGroup_VariableName)}'`);
statements.push(`azure_vm_size = '${this.getStringValue(VariableNames.VMSize_VariableName)}'`);
statements.push(`azure_vm_count = '${this.getStringValue(VariableNames.VMCount_VariableName)}'`);
statements.push(`aks_cluster_name = '${this.getStringValue(VariableNames.AksName_VariableName)}'`);
} else if (this.deploymentTarget === BdcDeploymentType.ExistingAKS || this.deploymentTarget === BdcDeploymentType.ExistingKubeAdm) {
statements.push(`mssql_kube_config_path = '${this.getStringValue(VariableNames.KubeConfigPath_VariableName)}'`);
statements.push(`mssql_cluster_context = '${this.getStringValue(VariableNames.ClusterContext_VariableName)}'`);
statements.push('os.environ["KUBECONFIG"] = mssql_kube_config_path');
}
statements.push(`mssql_cluster_name = '${this.getStringValue(VariableNames.ClusterName_VariableName)}'`);
statements.push(`mssql_controller_username = '${this.getStringValue(VariableNames.AdminUserName_VariableName)}'`);
statements.push(`bdc_json = '${profile.getBdcJson(false)}'`);
statements.push(`control_json = '${profile.getControlJson(false)}'`);
statements.push(`print('Variables have been set successfully.')`);
return statements.join(EOL);
}
}
export enum AuthenticationMode {
ActiveDirectory = 'ad',
Basic = 'basic'
}

View File

@@ -0,0 +1,107 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { DeployClusterWizard } from '../deployClusterWizard';
import { SectionInfo, FieldType, LabelPosition } from '../../../interfaces';
import { WizardPageBase } from '../../wizardPageBase';
import { createSection, InputComponents, setModelValues, Validator } from '../../modelViewUtils';
import { SubscriptionId_VariableName, ResourceGroup_VariableName, Region_VariableName, AksName_VariableName, VMCount_VariableName, VMSize_VariableName } from '../constants';
const localize = nls.loadMessageBundle();
export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
private inputComponents: InputComponents = {};
constructor(wizard: DeployClusterWizard) {
super(localize('deployCluster.AzureSettingsPageTitle', "Azure settings"),
localize('deployCluster.AzureSettingsPageDescription', "Configure the settings to create an Azure Kubernetes Service cluster"), wizard);
}
public initialize(): void {
const self = this;
const azureSection: SectionInfo = {
title: '',
labelPosition: LabelPosition.Left,
fields: [
{
type: FieldType.Text,
label: localize('deployCluster.SubscriptionField', "Subscription id"),
required: false,
variableName: SubscriptionId_VariableName,
placeHolder: localize('deployCluster.SubscriptionPlaceholder', "Use my default Azure subscription"),
description: localize('deployCluster.SubscriptionDescription', "The default subscription will be used if you leave this field blank.")
}, {
type: FieldType.DateTimeText,
label: localize('deployCluster.ResourceGroupName', "New resource group name"),
required: true,
variableName: ResourceGroup_VariableName,
defaultValue: 'mssql-'
}, {
type: FieldType.Text,
label: localize('deployCluster.Region', "Region"),
required: true,
variableName: Region_VariableName,
defaultValue: 'eastus'
}, {
type: FieldType.DateTimeText,
label: localize('deployCluster.AksName', "AKS cluster name"),
required: true,
variableName: AksName_VariableName,
defaultValue: 'mssql-',
}, {
type: FieldType.Number,
label: localize('deployCluster.VMCount', "VM count"),
required: true,
variableName: VMCount_VariableName,
defaultValue: '5',
min: 1,
max: 999
}, {
type: FieldType.Text,
label: localize('deployCluster.VMSize', "VM size"),
required: true,
variableName: VMSize_VariableName,
defaultValue: 'Standard_E4s_v3'
}
]
};
this.pageObject.registerContent((view: azdata.ModelView) => {
const azureGroup = createSection({
sectionInfo: azureSection,
view: view,
onNewDisposableCreated: (disposable: vscode.Disposable): void => {
self.wizard.registerDisposable(disposable);
},
onNewInputComponentCreated: (name: string, component: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent): void => {
self.inputComponents[name] = component;
},
onNewValidatorCreated: (validator: Validator): void => {
self.validators.push(validator);
},
container: this.wizard.wizardObject
});
const formBuilder = view.modelBuilder.formContainer().withFormItems(
[{
title: '',
component: azureGroup
}],
{
horizontal: false,
componentWidth: '100%'
}
);
const form = formBuilder.withLayout({ width: '100%' }).component();
return view.initializeModel(form);
});
}
public onLeave(): void {
setModelValues(this.inputComponents, this.wizard.model);
}
}

View File

@@ -0,0 +1,251 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { DeployClusterWizard } from '../deployClusterWizard';
import { SectionInfo, FieldType, LabelPosition } from '../../../interfaces';
import { createSection, InputComponents, setModelValues, Validator, isInputBoxEmpty, getInputBoxComponent, isValidSQLPassword, getInvalidSQLPasswordMessage, getPasswordMismatchMessage, MissingRequiredInformationErrorMessage } from '../../modelViewUtils';
import { WizardPageBase } from '../../wizardPageBase';
import * as VariableNames from '../constants';
import { EOL } from 'os';
import { AuthenticationMode } from '../deployClusterWizardModel';
const localize = nls.loadMessageBundle();
const ConfirmPasswordName = 'ConfirmPassword';
export class ClusterSettingsPage extends WizardPageBase<DeployClusterWizard> {
private inputComponents: InputComponents = {};
constructor(wizard: DeployClusterWizard) {
super(localize('deployCluster.ClusterSettingsPageTitle', "Cluster settings"),
localize('deployCluster.ClusterSettingsPageDescription', "Configure the SQL Server Big Data Cluster settings"), wizard);
}
public initialize(): void {
const self = this;
const basicSection: SectionInfo = {
labelPosition: LabelPosition.Left,
title: '',
fields: [
{
type: FieldType.Text,
label: localize('deployCluster.ClusterName', "Cluster name"),
required: true,
variableName: VariableNames.ClusterName_VariableName,
defaultValue: 'mssql-cluster',
useCustomValidator: true
}, {
type: FieldType.Text,
label: localize('deployCluster.ControllerUsername', "Controller username"),
required: true,
variableName: VariableNames.AdminUserName_VariableName,
defaultValue: 'admin',
useCustomValidator: true
}, {
type: FieldType.Password,
label: localize('deployCluster.AdminPassword', "Password"),
required: true,
variableName: VariableNames.AdminPassword_VariableName,
defaultValue: '',
useCustomValidator: true,
description: localize('deployCluster.AdminPasswordDescription', "You can also use this password to access SQL Server and gateway.")
}, {
type: FieldType.Password,
label: localize('deployCluster.ConfirmPassword', "Confirm password"),
required: true,
variableName: ConfirmPasswordName,
defaultValue: '',
useCustomValidator: true,
}, {
type: FieldType.Options,
label: localize('deployCluster.AuthenticationMode', "Authentication mode"),
required: true,
variableName: VariableNames.AuthenticationMode_VariableName,
defaultValue: AuthenticationMode.Basic,
options: [
{
name: AuthenticationMode.Basic,
displayName: localize('deployCluster.AuthenticationMode.Basic', "Basic")
},
{
name: AuthenticationMode.ActiveDirectory,
displayName: localize('deployCluster.AuthenticationMode.ActiveDirectory', "Active Directory")
}
]
}
]
};
const activeDirectorySection: SectionInfo = {
labelPosition: LabelPosition.Left,
title: localize('deployCluster.ActiveDirectorySettings', "Active Directory settings"),
fields: [
{
type: FieldType.Text,
label: localize('deployCluster.DistinguishedName', "Distinguished name"),
required: true,
variableName: VariableNames.DistinguishedName_VariableName,
useCustomValidator: true
}, {
type: FieldType.Text,
label: localize('deployCluster.AdminPrincipals', "Admin principals"),
required: true,
variableName: VariableNames.AdminPrincipals_VariableName,
useCustomValidator: true
}, {
type: FieldType.Text,
label: localize('deployCluster.UserPrincipals', "User principals"),
required: true,
variableName: VariableNames.UserPrincipals_VariableName,
useCustomValidator: true
}, {
type: FieldType.Text,
label: localize('deployCluster.UpstreamIPAddresses', "Upstream IP Addresses"),
required: true,
variableName: VariableNames.UpstreamIPAddresses_VariableName,
useCustomValidator: true
}, {
type: FieldType.Text,
label: localize('deployCluster.DNSName', "DNS name"),
required: true,
variableName: VariableNames.DnsName_VariableName,
useCustomValidator: true
}, {
type: FieldType.Text,
label: localize('deployCluster.Realm', "Realm"),
required: true,
variableName: VariableNames.Realm_VariableName,
useCustomValidator: true
}, {
type: FieldType.Text,
label: localize('deployCluster.AppOnwerPrincipals', "App owner principals"),
required: true,
variableName: VariableNames.AppOwnerPrincipals_VariableName,
useCustomValidator: true
}, {
type: FieldType.Text,
label: localize('deployCluster.AppReaderPrincipals', "App reader principals"),
required: true,
variableName: VariableNames.AppReaderPrincipals_VariableName,
useCustomValidator: true
}
]
};
this.pageObject.registerContent((view: azdata.ModelView) => {
const basicSettingsGroup = createSection({
view: view,
container: self.wizard.wizardObject,
sectionInfo: basicSection,
onNewDisposableCreated: (disposable: vscode.Disposable): void => {
self.wizard.registerDisposable(disposable);
},
onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => {
self.inputComponents[name] = component;
},
onNewValidatorCreated: (validator: Validator): void => {
self.validators.push(validator);
}
});
const activeDirectorySettingsGroup = createSection({
view: view,
container: self.wizard.wizardObject,
sectionInfo: activeDirectorySection,
onNewDisposableCreated: (disposable: vscode.Disposable): void => {
self.wizard.registerDisposable(disposable);
},
onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => {
self.inputComponents[name] = component;
},
onNewValidatorCreated: (validator: Validator): void => {
self.validators.push(validator);
}
});
const basicSettingsFormItem = { title: '', component: basicSettingsGroup };
const activeDirectoryFormItem = { title: '', component: activeDirectorySettingsGroup };
const authModeDropdown = <azdata.DropDownComponent>this.inputComponents[VariableNames.AuthenticationMode_VariableName];
const formBuilder = view.modelBuilder.formContainer().withFormItems(
[basicSettingsFormItem],
{
horizontal: false,
componentWidth: '100%'
}
);
this.wizard.registerDisposable(authModeDropdown.onValueChanged(() => {
const isBasicAuthMode = (<azdata.CategoryValue>authModeDropdown.value).name === 'basic';
if (isBasicAuthMode) {
formBuilder.removeFormItem(activeDirectoryFormItem);
} else {
formBuilder.insertFormItem(activeDirectoryFormItem);
}
}));
const form = formBuilder.withLayout({ width: '100%' }).component();
return view.initializeModel(form);
});
}
public onLeave() {
setModelValues(this.inputComponents, this.wizard.model);
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true;
});
}
public onEnter() {
const authModeDropdown = <azdata.DropDownComponent>this.inputComponents[VariableNames.AuthenticationMode_VariableName];
if (authModeDropdown) {
authModeDropdown.enabled = this.wizard.model.adAuthSupported;
}
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
this.wizard.wizardObject.message = { text: '' };
if (pcInfo.newPage > pcInfo.lastPage) {
const messages: string[] = [];
const authMode = typeof authModeDropdown.value === 'string' ? authModeDropdown.value : authModeDropdown.value!.name;
const requiredFieldsFilled: boolean = !isInputBoxEmpty(getInputBoxComponent(VariableNames.ClusterName_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminUserName_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminPassword_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(ConfirmPasswordName, this.inputComponents))
&& (!(authMode === AuthenticationMode.ActiveDirectory) || (
!isInputBoxEmpty(getInputBoxComponent(VariableNames.DistinguishedName_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminPrincipals_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.UserPrincipals_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.UpstreamIPAddresses_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.DnsName_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.Realm_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.AppOwnerPrincipals_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.AppReaderPrincipals_VariableName, this.inputComponents))));
if (!requiredFieldsFilled) {
messages.push(MissingRequiredInformationErrorMessage);
}
if (!isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminPassword_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(ConfirmPasswordName, this.inputComponents))) {
const password = getInputBoxComponent(VariableNames.AdminPassword_VariableName, this.inputComponents).value!;
const confirmPassword = getInputBoxComponent(ConfirmPasswordName, this.inputComponents).value!;
if (password !== confirmPassword) {
messages.push(getPasswordMismatchMessage(localize('deployCluster.AdminPasswordField', "Password")));
}
if (!isValidSQLPassword(password)) {
messages.push(getInvalidSQLPasswordMessage(localize('deployCluster.AdminPasswordField', "Password")));
}
}
if (messages.length > 0) {
this.wizard.wizardObject.message = {
text: messages.length === 1 ? messages[0] : localize('deployCluster.ValidationError', "There are some errors on this page, click 'Show Details' to view the errors."),
description: messages.length === 1 ? undefined : messages.join(EOL),
level: azdata.window.MessageLevel.Error
};
}
return messages.length === 0;
}
return true;
});
}
}

View File

@@ -0,0 +1,214 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { DeployClusterWizard } from '../deployClusterWizard';
import { WizardPageBase } from '../../wizardPageBase';
import * as VariableNames from '../constants';
import { createFlexContainer } from '../../modelViewUtils';
import { BdcDeploymentType } from '../../../interfaces';
import { BigDataClusterDeploymentProfile } from '../../../services/bigDataClusterDeploymentProfile';
const localize = nls.loadMessageBundle();
export class DeploymentProfilePage extends WizardPageBase<DeployClusterWizard> {
private _cards: azdata.CardComponent[] = [];
private _cardContainer: azdata.FlexContainer | undefined;
private _loadingComponent: azdata.LoadingComponent | undefined;
private _view: azdata.ModelView | undefined;
constructor(wizard: DeployClusterWizard) {
super(localize('deployCluster.summaryPageTitle', "Deployment configuration template"),
localize('deployCluster.summaryPageDescription', "Select the target configuration template"), wizard);
}
public initialize(): void {
this.pageObject.registerContent((view: azdata.ModelView) => {
this._view = view;
this._cardContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', flexWrap: 'wrap' }).component();
const hintText = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: localize('deployCluster.ProfileHintText', "Note: The settings of the deployment profile can be customized in later steps.")
}).component();
const container = createFlexContainer(view, [this._cardContainer, hintText], false);
this._loadingComponent = view.modelBuilder.loadingComponent().withItem(container).withProperties<azdata.LoadingComponentProperties>({
loading: true
}).component();
let formBuilder = view.modelBuilder.formContainer().withFormItems(
[
{
title: '',
component: this._loadingComponent
}
],
{
horizontal: false
}
).withLayout({ width: '100%', height: '100%' });
const form = formBuilder.withLayout({ width: '100%' }).component();
this.loadCards().then(() => {
this._loadingComponent!.loading = false;
}, (error) => {
this.wizard.wizardObject.message = {
level: azdata.window.MessageLevel.Error,
text: localize('deployCluster.loadProfileFailed', "Failed to load the deployment profiles: {0}", error.message)
};
this._loadingComponent!.loading = false;
});
return view.initializeModel(form);
});
}
private createProfileCard(profile: BigDataClusterDeploymentProfile, view: azdata.ModelView): azdata.CardComponent {
const descriptions: azdata.CardDescriptionItem[] = [{
label: localize('deployCluster.serviceLabel', "Service"),
value: localize('deployCluster.instancesLabel', "Instances"),
fontWeight: 'bold'
}, {
label: localize('deployCluster.masterPoolLabel', "SQL Server Master"),
value: profile.sqlServerReplicas.toString()
}, {
label: localize('deployCluster.computePoolLable', "Compute"),
value: profile.computeReplicas.toString()
}, {
label: localize('deployCluster.dataPoolLabel', "Data"),
value: profile.dataReplicas.toString()
}, {
label: localize('deployCluster.hdfsLabel', "HDFS + Spark"),
value: profile.hdfsReplicas.toString()
}, {
label: '' // line separator
}, {
label: localize('deployCluster.defaultDataStorage', "Data storage size (GB)"),
value: profile.controllerDataStorageSize.toString()
}, {
label: localize('deployCluster.defaultLogStorage', "Log storage size (GB)"),
value: profile.controllerLogsStorageSize.toString()
}, {
label: '' // line separator
}
];
if (profile.activeDirectorySupported) {
descriptions.push({
label: localize('deployCluster.activeDirectoryAuthentication', "Active Directory authentication"),
value: '✅'
});
} else {
descriptions.push({
label: localize('deployCluster.basicAuthentication', "Basic authentication"),
value: '✅'
});
}
if (profile.hadrEnabled) {
descriptions.push({
label: localize('deployCluster.hadr', "High Availability"),
value: '✅'
});
}
const card = view.modelBuilder.card().withProperties<azdata.CardProperties>({
cardType: azdata.CardType.VerticalButton,
label: profile.profileName,
descriptions: descriptions,
width: '240px',
height: '300px',
}).component();
this._cards.push(card);
this.wizard.registerDisposable(card.onCardSelectedChanged(() => {
if (card.selected) {
this.wizard.wizardObject.message = { text: '' };
this.setModelValuesByProfile(profile);
// clear the selected state of the previously selected card
this._cards.forEach(c => {
if (c !== card) {
c.selected = false;
}
});
} else {
// keep the selected state if no other card is selected
if (this._cards.filter(c => { return c !== card && c.selected; }).length === 0) {
card.selected = true;
}
}
}));
return card;
}
private setModelValuesByProfile(selectedProfile: BigDataClusterDeploymentProfile): void {
this.wizard.model.setPropertyValue(VariableNames.DeploymentProfile_VariableName, selectedProfile.profileName);
this.wizard.model.setPropertyValue(VariableNames.SparkPoolScale_VariableName, selectedProfile.sparkReplicas);
this.wizard.model.setPropertyValue(VariableNames.DataPoolScale_VariableName, selectedProfile.dataReplicas);
this.wizard.model.setPropertyValue(VariableNames.HDFSPoolScale_VariableName, selectedProfile.hdfsReplicas);
this.wizard.model.setPropertyValue(VariableNames.ComputePoolScale_VariableName, selectedProfile.computeReplicas);
this.wizard.model.setPropertyValue(VariableNames.HDFSNameNodeScale_VariableName, selectedProfile.hdfsNameNodeReplicas);
this.wizard.model.setPropertyValue(VariableNames.SQLServerScale_VariableName, selectedProfile.sqlServerReplicas);
this.wizard.model.setPropertyValue(VariableNames.SparkHeadScale_VariableName, selectedProfile.sparkHeadReplicas);
this.wizard.model.setPropertyValue(VariableNames.ZooKeeperScale_VariableName, selectedProfile.zooKeeperReplicas);
this.wizard.model.setPropertyValue(VariableNames.ControllerDataStorageSize_VariableName, selectedProfile.controllerDataStorageSize);
this.wizard.model.setPropertyValue(VariableNames.ControllerLogsStorageSize_VariableName, selectedProfile.controllerLogsStorageSize);
this.wizard.model.setPropertyValue(VariableNames.EnableHADR_VariableName, selectedProfile.hadrEnabled);
this.wizard.model.setPropertyValue(VariableNames.SQLServerPort_VariableName, selectedProfile.sqlServerPort);
this.wizard.model.setPropertyValue(VariableNames.GateWayPort_VariableName, selectedProfile.gatewayPort);
this.wizard.model.setPropertyValue(VariableNames.ControllerPort_VariableName, selectedProfile.controllerPort);
this.wizard.model.setPropertyValue(VariableNames.IncludeSpark_VariableName, selectedProfile.includeSpark);
this.wizard.model.setPropertyValue(VariableNames.ControllerDataStorageClassName_VariableName, selectedProfile.controllerDataStorageClass);
this.wizard.model.setPropertyValue(VariableNames.ControllerLogsStorageClassName_VariableName, selectedProfile.controllerLogsStorageClass);
this.wizard.model.setPropertyValue(VariableNames.ReadableSecondaryPort_VariableName, selectedProfile.sqlServerReadableSecondaryPort);
this.wizard.model.adAuthSupported = selectedProfile.activeDirectorySupported;
this.wizard.model.selectedProfile = selectedProfile;
}
private loadCards(): Promise<void> {
return this.wizard.azdataService.getDeploymentProfiles().then((profiles: BigDataClusterDeploymentProfile[]) => {
const defaultProfile: string = this.getDefaultProfile();
profiles.forEach(profile => {
const card = this.createProfileCard(profile, this._view!);
if (profile.profileName === defaultProfile) {
card.selected = true;
this.setModelValuesByProfile(profile);
}
this._cardContainer!.addItem(card, { flex: '0 0 auto' });
});
});
}
public onEnter() {
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
this.wizard.wizardObject.message = { text: '' };
if (pcInfo.newPage > pcInfo.lastPage) {
const isValid = this.wizard.model.getStringValue(VariableNames.DeploymentProfile_VariableName) !== undefined;
if (!isValid) {
this.wizard.wizardObject.message = {
text: localize('deployCluster.ProfileNotSelectedError', "Please select a deployment profile."),
level: azdata.window.MessageLevel.Error
};
}
return isValid;
}
return true;
});
}
public onLeave() {
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true;
});
}
private getDefaultProfile(): string {
switch (this.wizard.deploymentType) {
case BdcDeploymentType.NewAKS:
case BdcDeploymentType.ExistingAKS:
return 'aks-dev-test';
case BdcDeploymentType.ExistingKubeAdm:
return 'kubeadm-dev-test';
default:
throw new Error(`Unknown deployment type: ${this.wizard.deploymentType}`);
}
}
}

View File

@@ -0,0 +1,563 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { DeployClusterWizard } from '../deployClusterWizard';
import { SectionInfo, FieldType } from '../../../interfaces';
import { Validator, InputComponents, createSection, createGroupContainer, createLabel, createFlexContainer, createTextInput, createNumberInput, setModelValues, getInputBoxComponent, getCheckboxComponent, isInputBoxEmpty, getDropdownComponent, MissingRequiredInformationErrorMessage } from '../../modelViewUtils';
import { WizardPageBase } from '../../wizardPageBase';
import * as VariableNames from '../constants';
import { AuthenticationMode } from '../deployClusterWizardModel';
const localize = nls.loadMessageBundle();
const PortInputWidth = '100px';
const inputWidth = '180px';
const labelWidth = '150px';
const spaceBetweenFields = '5px';
export class ServiceSettingsPage extends WizardPageBase<DeployClusterWizard> {
private inputComponents: InputComponents = {};
private endpointHeaderRow!: azdata.FlexContainer;
private dnsColumnHeader!: azdata.TextComponent;
private portColumnHeader!: azdata.TextComponent;
private controllerDNSInput!: azdata.InputBoxComponent;
private controllerPortInput!: azdata.InputBoxComponent;
private controllerEndpointRow!: azdata.FlexContainer;
private sqlServerDNSInput!: azdata.InputBoxComponent;
private sqlServerEndpointRow!: azdata.FlexContainer;
private sqlServerPortInput!: azdata.InputBoxComponent;
private gatewayDNSInput!: azdata.InputBoxComponent;
private gatewayPortInput!: azdata.InputBoxComponent;
private gatewayEndpointRow!: azdata.FlexContainer;
private readableSecondaryDNSInput!: azdata.InputBoxComponent;
private readableSecondaryPortInput!: azdata.InputBoxComponent;
private readableSecondaryEndpointRow!: azdata.FlexContainer;
private endpointNameColumnHeader!: azdata.TextComponent;
private controllerNameLabel!: azdata.TextComponent;
private SqlServerNameLabel!: azdata.TextComponent;
private gatewayNameLabel!: azdata.TextComponent;
private readableSecondaryNameLabel!: azdata.TextComponent;
constructor(wizard: DeployClusterWizard) {
super(localize('deployCluster.ServiceSettingsPageTitle', "Service settings"), '', wizard);
}
public initialize(): void {
const scaleSectionInfo: SectionInfo = {
title: localize('deployCluster.scaleSectionTitle', "Scale settings"),
labelWidth: labelWidth,
inputWidth: inputWidth,
spaceBetweenFields: spaceBetweenFields,
rows: [{
fields: [
{
type: FieldType.Number,
label: localize('deployCluster.ComputeText', "Compute"),
min: 1,
max: 100,
defaultValue: '1',
useCustomValidator: true,
required: true,
variableName: VariableNames.ComputePoolScale_VariableName,
}
]
}, {
fields: [{
type: FieldType.Number,
label: localize('deployCluster.DataText', "Data"),
min: 1,
max: 100,
defaultValue: '1',
useCustomValidator: true,
required: true,
variableName: VariableNames.DataPoolScale_VariableName,
}]
}, {
fields: [
{
type: FieldType.Number,
label: localize('deployCluster.HDFSText', "HDFS"),
min: 1,
max: 100,
defaultValue: '1',
useCustomValidator: true,
required: true,
variableName: VariableNames.HDFSPoolScale_VariableName
}, {
type: FieldType.Checkbox,
label: localize('deployCluster.includeSparkInHDFSPool', "Include Spark"),
defaultValue: 'true',
variableName: VariableNames.IncludeSpark_VariableName,
required: false
}
]
}, {
fields: [
{
type: FieldType.Number,
label: localize('deployCluster.SparkText', "Spark"),
min: 0,
max: 100,
defaultValue: '0',
useCustomValidator: true,
required: true,
variableName: VariableNames.SparkPoolScale_VariableName
}
]
}
]
};
const hadrSectionInfo: SectionInfo = {
title: localize('deployCluster.HadrSection', "High availability settings"),
labelWidth: labelWidth,
inputWidth: inputWidth,
spaceBetweenFields: spaceBetweenFields,
rows: [{
fields: [
{
type: FieldType.Options,
label: localize('deployCluster.MasterSqlText', "SQL Server Master"),
options: ['1', '3', '4', '5', '6', '7', '8', '9'],
defaultValue: '1',
required: true,
variableName: VariableNames.SQLServerScale_VariableName,
}, {
type: FieldType.Checkbox,
label: localize('deployCluster.EnableHADR', "Enable Availability Groups"),
defaultValue: 'false',
variableName: VariableNames.EnableHADR_VariableName,
required: false
}
]
}, {
fields: [
{
type: FieldType.Number,
label: localize('deployCluster.HDFSNameNodeText', "HDFS name node"),
min: 1,
max: 100,
defaultValue: '1',
useCustomValidator: true,
required: true,
variableName: VariableNames.HDFSNameNodeScale_VariableName
}
]
}, {
fields: [
{
type: FieldType.Number,
label: localize('deployCluster.SparkHeadText', "SparkHead"),
min: 0,
max: 100,
defaultValue: '1',
useCustomValidator: true,
required: true,
variableName: VariableNames.SparkHeadScale_VariableName
}
]
}, {
fields: [
{
type: FieldType.Number,
label: localize('deployCluster.ZooKeeperText', "ZooKeeper"),
min: 0,
max: 100,
defaultValue: '1',
useCustomValidator: true,
required: true,
variableName: VariableNames.ZooKeeperScale_VariableName
}
]
}
]
};
const hintTextForStorageFields = localize('deployCluster.storageFieldTooltip', "Use controller settings");
const storageSectionInfo: SectionInfo = {
title: '',
labelWidth: '0px',
inputWidth: inputWidth,
spaceBetweenFields: spaceBetweenFields,
rows: [{
fields: [
{
type: FieldType.ReadonlyText,
label: '',
required: false,
defaultValue: localize('deployCluster.DataStorageClassName', "Storage class for data"),
variableName: '',
labelWidth: labelWidth
}, {
type: FieldType.ReadonlyText,
label: '',
required: false,
defaultValue: localize('deployCluster.DataClaimSize', "Claim size for data (GB)"),
variableName: ''
}, {
type: FieldType.ReadonlyText,
label: '',
required: false,
defaultValue: localize('deployCluster.LogStorageClassName', "Storage class for logs"),
variableName: '',
}, {
type: FieldType.ReadonlyText,
label: '',
required: false,
defaultValue: localize('deployCluster.LogsClaimSize', "Claim size for logs (GB)"),
variableName: ''
}
]
},
{
fields: [
{
type: FieldType.Text,
label: localize('deployCluster.ControllerText', "Controller"),
useCustomValidator: true,
variableName: VariableNames.ControllerDataStorageClassName_VariableName,
required: true,
description: localize('deployCluster.AdvancedStorageDescription', "By default Controller storage settings will be applied to other services as well, you can expand the advanced storage settings to configure storage for other services."),
labelWidth: labelWidth
}, {
type: FieldType.Number,
label: '',
useCustomValidator: true,
min: 1,
variableName: VariableNames.ControllerDataStorageSize_VariableName,
}, {
type: FieldType.Text,
label: '',
useCustomValidator: true,
min: 1,
variableName: VariableNames.ControllerLogsStorageClassName_VariableName,
}, {
type: FieldType.Number,
label: '',
useCustomValidator: true,
min: 1,
variableName: VariableNames.ControllerLogsStorageSize_VariableName,
}
]
}
]
};
const advancedStorageSectionInfo: SectionInfo = {
title: localize('deployCluster.AdvancedStorageSectionTitle', "Advanced storage settings"),
labelWidth: '0px',
inputWidth: inputWidth,
spaceBetweenFields: spaceBetweenFields,
collapsible: true,
collapsed: true,
rows: [{
fields: [
{
type: FieldType.Text,
label: localize('deployCluster.HDFSText', "HDFS"),
required: false,
variableName: VariableNames.HDFSDataStorageClassName_VariableName,
placeHolder: hintTextForStorageFields,
labelWidth: labelWidth
}, {
type: FieldType.Number,
label: '',
required: false,
min: 1,
variableName: VariableNames.HDFSDataStorageSize_VariableName,
placeHolder: hintTextForStorageFields
}, {
type: FieldType.Text,
label: '',
required: false,
variableName: VariableNames.HDFSLogsStorageClassName_VariableName,
placeHolder: hintTextForStorageFields
}, {
type: FieldType.Number,
label: '',
required: false,
min: 1,
variableName: VariableNames.HDFSLogsStorageSize_VariableName,
placeHolder: hintTextForStorageFields
}
]
}, {
fields: [
{
type: FieldType.Text,
label: localize('deployCluster.DataText', "Data"),
required: false,
variableName: VariableNames.DataPoolDataStorageClassName_VariableName,
labelWidth: labelWidth,
placeHolder: hintTextForStorageFields
}, {
type: FieldType.Number,
label: '',
required: false,
min: 1,
variableName: VariableNames.DataPoolDataStorageSize_VariableName,
placeHolder: hintTextForStorageFields
}, {
type: FieldType.Text,
label: '',
required: false,
variableName: VariableNames.DataPoolLogsStorageClassName_VariableName,
placeHolder: hintTextForStorageFields
}, {
type: FieldType.Number,
label: '',
required: false,
min: 1,
variableName: VariableNames.DataPoolLogsStorageSize_VariableName,
placeHolder: hintTextForStorageFields
}
]
}, {
fields: [
{
type: FieldType.Text,
label: localize('deployCluster.MasterSqlText', "SQL Server Master"),
required: false,
variableName: VariableNames.SQLServerDataStorageClassName_VariableName,
labelWidth: labelWidth,
placeHolder: hintTextForStorageFields
}, {
type: FieldType.Number,
label: '',
required: false,
min: 1,
variableName: VariableNames.SQLServerDataStorageSize_VariableName,
placeHolder: hintTextForStorageFields
}, {
type: FieldType.Text,
label: '',
required: false,
variableName: VariableNames.SQLServerLogsStorageClassName_VariableName,
placeHolder: hintTextForStorageFields
}, {
type: FieldType.Number,
label: '',
required: false,
min: 1,
variableName: VariableNames.SQLServerLogsStorageSize_VariableName,
placeHolder: hintTextForStorageFields
}
]
}]
};
this.pageObject.registerContent((view: azdata.ModelView) => {
const createSectionFunc = (sectionInfo: SectionInfo): azdata.GroupContainer => {
return createSection({
view: view,
container: this.wizard.wizardObject,
sectionInfo: sectionInfo,
onNewDisposableCreated: (disposable: vscode.Disposable): void => {
this.wizard.registerDisposable(disposable);
},
onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => {
this.inputComponents[name] = component;
},
onNewValidatorCreated: (validator: Validator): void => {
}
});
};
const scaleSection = createSectionFunc(scaleSectionInfo);
const hadrSection = createSectionFunc(hadrSectionInfo);
const endpointSection = this.createEndpointSection(view);
const storageSection = createSectionFunc(storageSectionInfo);
const advancedStorageSection = createSectionFunc(advancedStorageSectionInfo);
const storageContainer = createGroupContainer(view, [storageSection, advancedStorageSection], {
header: localize('deployCluster.StorageSectionTitle', "Storage settings"),
collapsible: true
});
this.setSQLServerMasterFieldEventHandler();
const form = view.modelBuilder.formContainer().withFormItems([
{
title: '',
component: scaleSection
}, {
title: '',
component: hadrSection
}, {
title: '',
component: endpointSection
}, {
title: '',
component: storageContainer
}
]).withLayout({ width: '100%' }).component();
return view.initializeModel(form);
});
}
private createEndpointSection(view: azdata.ModelView): azdata.GroupContainer {
this.endpointNameColumnHeader = createLabel(view, { text: '', width: labelWidth });
this.dnsColumnHeader = createLabel(view, { text: localize('deployCluster.DNSNameHeader', "DNS name"), width: inputWidth });
this.portColumnHeader = createLabel(view, { text: localize('deployCluster.PortHeader', "Port"), width: PortInputWidth });
this.endpointHeaderRow = createFlexContainer(view, [this.endpointNameColumnHeader, this.dnsColumnHeader, this.portColumnHeader]);
this.controllerNameLabel = createLabel(view, { text: localize('deployCluster.ControllerText', "Controller"), width: labelWidth, required: true });
this.controllerDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.ControllerDNSName', "Controller DNS name"), required: false, width: inputWidth });
this.controllerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ControllerPortName', "Controller port"), required: true, width: PortInputWidth, min: 1 });
this.controllerEndpointRow = createFlexContainer(view, [this.controllerNameLabel, this.controllerDNSInput, this.controllerPortInput]);
this.inputComponents[VariableNames.ControllerDNSName_VariableName] = this.controllerDNSInput;
this.inputComponents[VariableNames.ControllerPort_VariableName] = this.controllerPortInput;
this.SqlServerNameLabel = createLabel(view, { text: localize('deployCluster.MasterSqlText', "SQL Server Master"), width: labelWidth, required: true });
this.sqlServerDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.MasterSQLServerDNSName', "SQL Server Master DNS name"), required: false, width: inputWidth });
this.sqlServerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.MasterSQLServerPortName', "SQL Server Master port"), required: true, width: PortInputWidth, min: 1 });
this.sqlServerEndpointRow = createFlexContainer(view, [this.SqlServerNameLabel, this.sqlServerDNSInput, this.sqlServerPortInput]);
this.inputComponents[VariableNames.SQLServerDNSName_VariableName] = this.sqlServerDNSInput;
this.inputComponents[VariableNames.SQLServerPort_VariableName] = this.sqlServerPortInput;
this.gatewayNameLabel = createLabel(view, { text: localize('deployCluster.GatewayText', "Gateway"), width: labelWidth, required: true });
this.gatewayDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.GatewayDNSName', "Gateway DNS name"), required: false, width: inputWidth });
this.gatewayPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.GatewayPortName', "Gateway port"), required: true, width: PortInputWidth, min: 1 });
this.gatewayEndpointRow = createFlexContainer(view, [this.gatewayNameLabel, this.gatewayDNSInput, this.gatewayPortInput]);
this.inputComponents[VariableNames.GatewayDNSName_VariableName] = this.gatewayDNSInput;
this.inputComponents[VariableNames.GateWayPort_VariableName] = this.gatewayPortInput;
this.readableSecondaryNameLabel = createLabel(view, { text: localize('deployCluster.ReadableSecondaryText', "Readable secondary"), width: labelWidth, required: true });
this.readableSecondaryDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.ReadableSecondaryDNSName', "Readable secondary DNS name"), required: false, width: inputWidth });
this.readableSecondaryPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ReadableSecondaryPortName', "Readable secondary port"), required: false, width: PortInputWidth, min: 1 });
this.readableSecondaryEndpointRow = createFlexContainer(view, [this.readableSecondaryNameLabel, this.readableSecondaryDNSInput, this.readableSecondaryPortInput]);
this.inputComponents[VariableNames.ReadableSecondaryDNSName_VariableName] = this.readableSecondaryDNSInput;
this.inputComponents[VariableNames.ReadableSecondaryPort_VariableName] = this.readableSecondaryPortInput;
return createGroupContainer(view, [this.endpointHeaderRow, this.controllerEndpointRow, this.sqlServerEndpointRow, this.gatewayEndpointRow, this.readableSecondaryEndpointRow], {
header: localize('deployCluster.EndpointSettings', "Endpoint settings"),
collapsible: true
});
}
public onEnter(): void {
this.setDropdownValue(VariableNames.SQLServerScale_VariableName);
this.setCheckboxValue(VariableNames.EnableHADR_VariableName);
this.setInputBoxValue(VariableNames.ComputePoolScale_VariableName);
this.setInputBoxValue(VariableNames.DataPoolScale_VariableName);
this.setInputBoxValue(VariableNames.HDFSPoolScale_VariableName);
this.setInputBoxValue(VariableNames.HDFSNameNodeScale_VariableName);
this.setInputBoxValue(VariableNames.SparkPoolScale_VariableName);
this.setInputBoxValue(VariableNames.SparkHeadScale_VariableName);
this.setInputBoxValue(VariableNames.ZooKeeperScale_VariableName);
this.setCheckboxValue(VariableNames.IncludeSpark_VariableName);
this.setEnableHadrCheckboxState(this.wizard.model.getIntegerValue(VariableNames.SQLServerScale_VariableName));
this.setInputBoxValue(VariableNames.ControllerPort_VariableName);
this.setInputBoxValue(VariableNames.SQLServerPort_VariableName);
this.setInputBoxValue(VariableNames.GateWayPort_VariableName);
this.setInputBoxValue(VariableNames.ReadableSecondaryPort_VariableName);
this.setInputBoxValue(VariableNames.ControllerDataStorageClassName_VariableName);
this.setInputBoxValue(VariableNames.ControllerDataStorageSize_VariableName);
this.setInputBoxValue(VariableNames.ControllerLogsStorageClassName_VariableName);
this.setInputBoxValue(VariableNames.ControllerLogsStorageSize_VariableName);
this.endpointHeaderRow.clearItems();
if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) {
this.endpointHeaderRow.addItems([this.endpointNameColumnHeader, this.dnsColumnHeader, this.portColumnHeader]);
}
this.loadEndpointRow(this.controllerEndpointRow, this.controllerNameLabel, this.controllerDNSInput, this.controllerPortInput);
this.loadEndpointRow(this.gatewayEndpointRow, this.gatewayNameLabel, this.gatewayDNSInput, this.gatewayPortInput);
this.loadEndpointRow(this.sqlServerEndpointRow, this.SqlServerNameLabel, this.sqlServerDNSInput, this.sqlServerPortInput);
this.updateReadableSecondaryEndpointComponents(this.wizard.model.hadrEnabled);
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
this.wizard.wizardObject.message = { text: '' };
if (pcInfo.newPage > pcInfo.lastPage) {
const isValid: boolean = !isInputBoxEmpty(getInputBoxComponent(VariableNames.ComputePoolScale_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.DataPoolScale_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.HDFSNameNodeScale_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.HDFSPoolScale_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.SparkPoolScale_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.SparkHeadScale_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.ZooKeeperScale_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerDataStorageClassName_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerDataStorageSize_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerLogsStorageClassName_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerLogsStorageSize_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerPort_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.SQLServerPort_VariableName, this.inputComponents))
&& !isInputBoxEmpty(getInputBoxComponent(VariableNames.GateWayPort_VariableName, this.inputComponents))
&& (!getCheckboxComponent(VariableNames.EnableHADR_VariableName, this.inputComponents).checked
|| !isInputBoxEmpty(this.readableSecondaryPortInput))
&& (this.wizard.model.authenticationMode !== AuthenticationMode.ActiveDirectory
|| (!isInputBoxEmpty(this.gatewayDNSInput)
&& !isInputBoxEmpty(this.controllerDNSInput)
&& !isInputBoxEmpty(this.sqlServerDNSInput)
&& !isInputBoxEmpty(this.readableSecondaryDNSInput)
));
if (!isValid) {
this.wizard.wizardObject.message = {
text: MissingRequiredInformationErrorMessage,
level: azdata.window.MessageLevel.Error
};
}
return isValid;
}
return true;
});
}
public onLeave(): void {
setModelValues(this.inputComponents, this.wizard.model);
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true;
});
}
private setInputBoxValue(variableName: string): void {
getInputBoxComponent(variableName, this.inputComponents).value = this.wizard.model.getStringValue(variableName);
}
private setCheckboxValue(variableName: string): void {
getCheckboxComponent(variableName, this.inputComponents).checked = this.wizard.model.getBooleanValue(variableName);
}
private setDropdownValue(variableName: string): void {
getDropdownComponent(variableName, this.inputComponents).value = this.wizard.model.getStringValue(variableName);
}
private setSQLServerMasterFieldEventHandler() {
const sqlScaleDropdown = getDropdownComponent(VariableNames.SQLServerScale_VariableName, this.inputComponents);
const enableHadrCheckbox = getCheckboxComponent(VariableNames.EnableHADR_VariableName, this.inputComponents);
this.wizard.registerDisposable(sqlScaleDropdown.onValueChanged(() => {
const selectedValue = typeof sqlScaleDropdown.value === 'string' ? sqlScaleDropdown.value : sqlScaleDropdown.value!.name;
this.setEnableHadrCheckboxState(Number.parseInt(selectedValue));
}));
this.wizard.registerDisposable(enableHadrCheckbox.onChanged(() => {
this.updateReadableSecondaryEndpointComponents(enableHadrCheckbox.checked);
}));
}
private setEnableHadrCheckboxState(sqlInstances: number) {
// 1. it is ok to enable HADR when there is only 1 replica
// 2. if there are multiple replicas, the hadr.enabled switch must be set to true.
const enableHadrCheckbox = getCheckboxComponent(VariableNames.EnableHADR_VariableName, this.inputComponents);
const hadrEnabled = sqlInstances === 1 ? enableHadrCheckbox.checked : true;
if (sqlInstances === 1) {
enableHadrCheckbox.enabled = true;
} else {
enableHadrCheckbox.enabled = false;
}
enableHadrCheckbox.checked = hadrEnabled;
this.updateReadableSecondaryEndpointComponents(hadrEnabled);
}
private updateReadableSecondaryEndpointComponents(hadrEnabled: boolean) {
this.readableSecondaryEndpointRow.clearItems();
if (hadrEnabled) {
this.loadEndpointRow(this.readableSecondaryEndpointRow, this.readableSecondaryNameLabel, this.readableSecondaryDNSInput, this.readableSecondaryPortInput);
}
}
private loadEndpointRow(row: azdata.FlexContainer, label: azdata.TextComponent, dnsInput: azdata.InputBoxComponent, portInput: azdata.InputBoxComponent): void {
row.clearItems();
const itemLayout: azdata.FlexItemLayout = { CSSStyles: { 'margin-right': '20px' } };
row.addItem(label);
if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) {
row.addItem(dnsInput, itemLayout);
}
row.addItem(portInput);
}
}

View File

@@ -0,0 +1,415 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import { DeployClusterWizard } from '../deployClusterWizard';
import { SectionInfo, FieldType, LabelPosition, FontStyle, BdcDeploymentType } from '../../../interfaces';
import { createSection, createGroupContainer, createFlexContainer, createLabel } from '../../modelViewUtils';
import { WizardPageBase } from '../../wizardPageBase';
import * as VariableNames from '../constants';
import * as os from 'os';
import { join } from 'path';
import * as fs from 'fs';
import { AuthenticationMode } from '../deployClusterWizardModel';
import { BigDataClusterDeploymentProfile } from '../../../services/bigDataClusterDeploymentProfile';
const localize = nls.loadMessageBundle();
export class SummaryPage extends WizardPageBase<DeployClusterWizard> {
private formItems: azdata.FormComponent[] = [];
private form!: azdata.FormBuilder;
private view!: azdata.ModelView;
private targetDeploymentProfile!: BigDataClusterDeploymentProfile;
constructor(wizard: DeployClusterWizard) {
super(localize('deployCluster.summaryPageTitle', "Summary"), '', wizard);
}
public initialize(): void {
this.pageObject.registerContent((view: azdata.ModelView) => {
this.view = view;
const deploymentJsonSection = createGroupContainer(view, [
view.modelBuilder.flexContainer().withItems([
this.createSaveJsonButton(localize('deployCluster.SaveBdcJson', "Save bdc.json"), 'bdc.json', () => { return this.targetDeploymentProfile.getBdcJson(); }),
this.createSaveJsonButton(localize('deployCluster.SaveControlJson', "Save control.json"), 'control.json', () => { return this.targetDeploymentProfile.getControlJson(); })
], {
CSSStyles: { 'margin-right': '10px' }
}).withLayout({ flexFlow: 'row', alignItems: 'center' }).component()
], {
header: localize('deployCluster.DeploymentJSON', "Deployment JSON files"),
collapsible: true
});
this.form = view.modelBuilder.formContainer().withFormItems([
{
title: '',
component: deploymentJsonSection
}
]);
return view.initializeModel(this.form!.withLayout({ width: '100%' }).component());
});
}
public onEnter() {
this.targetDeploymentProfile = this.wizard.model.createTargetProfile();
this.formItems.forEach(item => {
this.form!.removeFormItem(item);
});
this.formItems = [];
const deploymentTargetSectionInfo: SectionInfo = {
labelPosition: LabelPosition.Left,
labelWidth: '150px',
inputWidth: '200px',
title: localize('deployCluster.DeploymentTarget', "Deployment target"),
rows: [
{
fields: [
{
type: FieldType.ReadonlyText,
label: localize('deployCluster.Kubeconfig', "Kube config"),
defaultValue: this.wizard.model.getStringValue(VariableNames.KubeConfigPath_VariableName),
fontStyle: FontStyle.Italic
},
{
type: FieldType.ReadonlyText,
label: localize('deployCluster.ClusterContext', "Cluster context"),
defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterContext_VariableName),
fontStyle: FontStyle.Italic
}]
}
]
};
const clusterSectionInfo: SectionInfo = {
labelPosition: LabelPosition.Left,
labelWidth: '150px',
inputWidth: '200px',
title: localize('deployCluster.ClusterSettings', "Cluster settings"),
rows: [
{
fields: [
{
type: FieldType.ReadonlyText,
label: localize('deployCluster.DeploymentProfile', "Deployment profile"),
defaultValue: this.wizard.model.getStringValue(VariableNames.DeploymentProfile_VariableName),
fontStyle: FontStyle.Italic
},
{
type: FieldType.ReadonlyText,
label: localize('deployCluster.ClusterName', "Cluster name"),
defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterName_VariableName),
fontStyle: FontStyle.Italic
}]
}, {
fields: [
{
type: FieldType.ReadonlyText,
label: localize('deployCluster.ControllerUsername', "Controller username"),
defaultValue: this.wizard.model.getStringValue(VariableNames.AdminUserName_VariableName),
fontStyle: FontStyle.Italic
}, {
type: FieldType.ReadonlyText,
label: localize('deployCluster.AuthenticationMode', "Authentication mode"),
defaultValue: this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory ?
localize('deployCluster.AuthenticationMode.ActiveDirectory', "Active Directory") :
localize('deployCluster.AuthenticationMode.Basic', "Basic"),
fontStyle: FontStyle.Italic
}
]
}
]
};
const azureSectionInfo: SectionInfo = {
labelPosition: LabelPosition.Left,
labelWidth: '150px',
inputWidth: '200px',
title: localize('deployCluster.AzureSettings', "Azure settings"),
rows: [{
fields: [
{
type: FieldType.ReadonlyText,
label: localize('deployCluster.SubscriptionId', "Subscription id"),
defaultValue: this.wizard.model.getStringValue(VariableNames.SubscriptionId_VariableName) || localize('deployCluster.DefaultSubscription', "Default Azure Subscription"),
fontStyle: FontStyle.Italic
}, {
type: FieldType.ReadonlyText,
label: localize('deployCluster.ResourceGroup', "Resource group"),
defaultValue: this.wizard.model.getStringValue(VariableNames.ResourceGroup_VariableName),
fontStyle: FontStyle.Italic
}
]
}, {
fields: [
{
type: FieldType.ReadonlyText,
label: localize('deployCluster.Region', "Region"),
defaultValue: this.wizard.model.getStringValue(VariableNames.DeploymentProfile_VariableName),
fontStyle: FontStyle.Italic
}, {
type: FieldType.ReadonlyText,
label: localize('deployCluster.AksClusterName', "AKS cluster name"),
defaultValue: this.wizard.model.getStringValue(VariableNames.AksName_VariableName),
fontStyle: FontStyle.Italic
}
]
}, {
fields: [
{
type: FieldType.ReadonlyText,
label: localize('deployCluster.VMSize', "VM size"),
defaultValue: this.wizard.model.getStringValue(VariableNames.VMSize_VariableName),
fontStyle: FontStyle.Italic
}, {
type: FieldType.ReadonlyText,
label: localize('deployCluster.VMCount', "VM count"),
defaultValue: this.wizard.model.getStringValue(VariableNames.VMCount_VariableName),
fontStyle: FontStyle.Italic
}
]
}
]
};
const scaleSectionInfo: SectionInfo = {
labelPosition: LabelPosition.Left,
labelWidth: '150px',
inputWidth: '200px',
title: localize('deployCluster.ScaleSettings', "Scale settings"),
rows: [
{
fields: [{
type: FieldType.ReadonlyText,
label: localize('deployCluster.ComputeText', "Compute"),
defaultValue: this.wizard.model.getStringValue(VariableNames.ComputePoolScale_VariableName),
fontStyle: FontStyle.Italic
}, {
type: FieldType.ReadonlyText,
label: localize('deployCluster.DataText', "Data"),
defaultValue: this.wizard.model.getStringValue(VariableNames.DataPoolScale_VariableName),
fontStyle: FontStyle.Italic
}
]
}, {
fields: [
{
type: FieldType.ReadonlyText,
label: localize('deployCluster.HDFSText', "HDFS"),
defaultValue: `${this.wizard.model.getStringValue(VariableNames.HDFSPoolScale_VariableName)} ${this.wizard.model.getBooleanValue(VariableNames.IncludeSpark_VariableName) ? localize('deployCluster.WithSpark', "(Spark included)") : ''}`,
fontStyle: FontStyle.Italic
}, {
type: FieldType.ReadonlyText,
label: localize('deployCluster.SparkText', "Spark"),
defaultValue: this.wizard.model.getStringValue(VariableNames.SparkPoolScale_VariableName),
fontStyle: FontStyle.Italic
}
]
}
]
};
const hadrSectionInfo: SectionInfo = {
labelPosition: LabelPosition.Left,
labelWidth: '150px',
inputWidth: '200px',
title: localize('deployCluster.HadrSection', "High availability settings"),
rows: [
{
fields: [
{
type: FieldType.ReadonlyText,
label: localize('deployCluster.SqlServerText', "SQL Server Master"),
defaultValue: `${this.wizard.model.getStringValue(VariableNames.SQLServerScale_VariableName)} ${this.wizard.model.hadrEnabled ? localize('deployCluster.WithHADR', "(Availability Groups Enabled)") : ''}`,
fontStyle: FontStyle.Italic
}, {
type: FieldType.ReadonlyText,
label: localize('deployCluster.HDFSNameNodeText', "HDFS name node"),
defaultValue: this.wizard.model.getStringValue(VariableNames.HDFSNameNodeScale_VariableName),
fontStyle: FontStyle.Italic
}
]
}, {
fields: [
{
type: FieldType.ReadonlyText,
label: localize('deployCluster.ZooKeeperText', "ZooKeeper"),
defaultValue: this.wizard.model.getStringValue(VariableNames.ZooKeeperScale_VariableName),
fontStyle: FontStyle.Italic
}, {
type: FieldType.ReadonlyText,
label: localize('deployCluster.SparkHeadText', "SparkHead"),
defaultValue: this.wizard.model.getStringValue(VariableNames.SparkHeadScale_VariableName),
fontStyle: FontStyle.Italic
}
]
}
]
};
const createSectionFunc = (sectionInfo: SectionInfo): azdata.FormComponent => {
return {
title: '',
component: createSection({
container: this.wizard.wizardObject,
sectionInfo: sectionInfo,
view: this.view,
onNewDisposableCreated: () => { },
onNewInputComponentCreated: () => { },
onNewValidatorCreated: () => { }
})
};
};
if (this.wizard.deploymentType === BdcDeploymentType.ExistingAKS || this.wizard.deploymentType === BdcDeploymentType.ExistingKubeAdm) {
const deploymentTargetSection = createSectionFunc(deploymentTargetSectionInfo);
this.formItems.push(deploymentTargetSection);
}
const clusterSection = createSectionFunc(clusterSectionInfo);
const scaleSection = createSectionFunc(scaleSectionInfo);
const hadrSection = createSectionFunc(hadrSectionInfo);
const endpointSection = {
title: '',
component: this.createEndpointSection()
};
const storageSection = {
title: '',
component: this.createStorageSection()
};
if (this.wizard.model.getStringValue(VariableNames.AksName_VariableName)) {
const azureSection = createSectionFunc(azureSectionInfo);
this.formItems.push(azureSection);
}
this.formItems.push(clusterSection, scaleSection, hadrSection, endpointSection, storageSection);
this.form.addFormItems(this.formItems);
}
private getStorageSettingValue(propertyName: string, defaultValuePropertyName: string): string | undefined {
const value = this.wizard.model.getStringValue(propertyName);
return (value === undefined || value === '') ? this.wizard.model.getStringValue(defaultValuePropertyName) : value;
}
private createStorageSection(): azdata.GroupContainer {
const serviceNameColumn: azdata.TableColumn = {
value: ' ',
width: 150
};
const dataStorageClassColumn: azdata.TableColumn = {
value: localize('deployCluster.DataStorageClassName', "Storage class for data"),
width: 180
};
const dataStorageSizeColumn: azdata.TableColumn = {
value: localize('deployCluster.DataClaimSize', "Claim size for data (GB)"),
width: 180
};
const logStorageClassColumn: azdata.TableColumn = {
value: localize('deployCluster.LogStorageClassName', "Storage class for logs"),
width: 180
};
const logStorageSizeColumn: azdata.TableColumn = {
value: localize('deployCluster.LogsClaimSize', "Claim size for logs (GB)"),
width: 180
};
const storageTable = this.view.modelBuilder.table().withProperties<azdata.TableComponentProperties>({
data: [
[
localize('deployCluster.ControllerText', "Controller"),
this.wizard.model.getStringValue(VariableNames.ControllerDataStorageClassName_VariableName),
this.wizard.model.getStringValue(VariableNames.ControllerDataStorageSize_VariableName),
this.wizard.model.getStringValue(VariableNames.ControllerLogsStorageClassName_VariableName),
this.wizard.model.getStringValue(VariableNames.ControllerLogsStorageSize_VariableName)],
[
localize('deployCluster.HDFSText', "HDFS"),
this.getStorageSettingValue(VariableNames.HDFSDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName),
this.getStorageSettingValue(VariableNames.HDFSDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName),
this.getStorageSettingValue(VariableNames.HDFSLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName),
this.getStorageSettingValue(VariableNames.HDFSLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName)
], [
localize('deployCluster.DataText', "Data"),
this.getStorageSettingValue(VariableNames.DataPoolDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName),
this.getStorageSettingValue(VariableNames.DataPoolDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName),
this.getStorageSettingValue(VariableNames.DataPoolLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName),
this.getStorageSettingValue(VariableNames.DataPoolLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName)
], [
localize('deployCluster.MasterSqlText', "SQL Server Master"),
this.getStorageSettingValue(VariableNames.SQLServerDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName),
this.getStorageSettingValue(VariableNames.SQLServerDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName),
this.getStorageSettingValue(VariableNames.SQLServerLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName),
this.getStorageSettingValue(VariableNames.SQLServerLogsStorageSize_VariableName, VariableNames.ControllerLogsStorageSize_VariableName)
]
],
columns: [serviceNameColumn, dataStorageClassColumn, dataStorageSizeColumn, logStorageClassColumn, logStorageSizeColumn],
width: '1000px',
height: '140px'
}).component();
return createGroupContainer(this.view, [storageTable], {
header: localize('deployCluster.StorageSettings', "Storage settings"),
collapsible: true
});
}
private createEndpointSection(): azdata.GroupContainer {
const endpointRows = [
this.createEndpointRow(localize('deployCluster.ControllerText', "Controller"), VariableNames.ControllerDNSName_VariableName, VariableNames.ControllerPort_VariableName),
this.createEndpointRow(localize('deployCluster.SqlServerText', "SQL Server Master"), VariableNames.SQLServerDNSName_VariableName, VariableNames.SQLServerPort_VariableName),
this.createEndpointRow(localize('deployCluster.GatewayText', "Gateway"), VariableNames.GatewayDNSName_VariableName, VariableNames.GateWayPort_VariableName)
];
if (this.wizard.model.hadrEnabled) {
endpointRows.push(
this.createEndpointRow(localize('deployCluster.ReadableSecondaryText', "Readable secondary"), VariableNames.ReadableSecondaryDNSName_VariableName, VariableNames.ReadableSecondaryPort_VariableName)
);
}
return createGroupContainer(this.view, endpointRows, {
header: localize('deployCluster.EndpointSettings', "Endpoint settings"),
collapsible: true
});
}
private createEndpointRow(name: string, dnsVariableName: string, portVariableName: string): azdata.FlexContainer {
const items = [];
items.push(createLabel(this.view, { text: name, width: '150px' }));
if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) {
items.push(createLabel(this.view, { text: this.wizard.model.getStringValue(dnsVariableName)!, width: '200px', fontStyle: FontStyle.Italic }));
}
items.push(createLabel(this.view, { text: this.wizard.model.getStringValue(portVariableName)!, width: '100px', fontStyle: FontStyle.Italic }));
return createFlexContainer(this.view, items);
}
private createSaveJsonButton(label: string, fileName: string, getContent: () => string): azdata.ButtonComponent {
const button = this.view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
title: label,
label: fileName,
ariaLabel: label,
width: '150px'
}).component();
this.wizard.registerDisposable(button.onDidClick(() => {
vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file(join(os.homedir(), fileName)),
filters: {
'JSON': ['json']
}
}).then((path) => {
if (path) {
fs.promises.writeFile(path.fsPath, getContent()).then(() => {
this.wizard.wizardObject.message = {
text: localize('deployCluster.SaveJsonFileMessage', "File saved: {0}", path.fsPath),
level: azdata.window.MessageLevel.Information
};
}).catch((error) => {
this.wizard.wizardObject.message = {
text: error.message,
level: azdata.window.MessageLevel.Error
};
});
}
});
}));
return button;
}
}

View File

@@ -0,0 +1,176 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as os from 'os';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { DeployClusterWizard } from '../deployClusterWizard';
import { WizardPageBase } from '../../wizardPageBase';
import { KubeClusterContext } from '../../../services/kubeService';
import { ClusterContext_VariableName, KubeConfigPath_VariableName } from '../constants';
const localize = nls.loadMessageBundle();
const ClusterRadioButtonGroupName = 'ClusterRadioGroup';
export class TargetClusterContextPage extends WizardPageBase<DeployClusterWizard> {
private existingClusterControl: azdata.FlexContainer | undefined;
private clusterContextsLabel: azdata.TextComponent | undefined;
private errorLoadingClustersLabel: azdata.TextComponent | undefined;
private clusterContextList: azdata.DivContainer | undefined;
private clusterContextLoadingComponent: azdata.LoadingComponent | undefined;
private configFileInput: azdata.InputBoxComponent | undefined;
private browseFileButton: azdata.ButtonComponent | undefined;
private loadDefaultKubeConfigFile: boolean = true;
private view: azdata.ModelView | undefined;
constructor(wizard: DeployClusterWizard) {
super(localize('deployCluster.TargetClusterContextPageTitle', "Target cluster context"),
localize('deployCluster.TargetClusterContextPageDescription', "Select the kube config file and then select a cluster context from the list"), wizard);
}
public initialize(): void {
this.pageObject.registerContent((view: azdata.ModelView) => {
this.view = view;
this.initExistingClusterControl();
let formBuilder = view.modelBuilder.formContainer().withFormItems(
[
{
component: this.existingClusterControl!,
title: ''
}
],
{
horizontal: false
}
).withLayout({ width: '100%', height: '100%' });
const form = formBuilder.withLayout({ width: '100%' }).component();
return view.initializeModel(form);
});
}
public onEnter() {
if (this.loadDefaultKubeConfigFile) {
let defaultKubeConfigPath = this.wizard.kubeService.getDefautConfigPath();
this.loadClusterContexts(defaultKubeConfigPath);
this.loadDefaultKubeConfigFile = false;
}
this.wizard.wizardObject.registerNavigationValidator((e) => {
if (e.lastPage > e.newPage) {
this.wizard.wizardObject.message = { text: '' };
return true;
}
let clusterSelected = this.wizard.model.getStringValue(ClusterContext_VariableName) !== undefined;
if (!clusterSelected) {
this.wizard.wizardObject.message = {
text: localize('deployCluster.ClusterContextNotSelectedMessage', 'Please select a cluster context.'),
level: azdata.window.MessageLevel.Error
};
}
return clusterSelected;
});
}
public onLeave() {
this.wizard.wizardObject.registerNavigationValidator((e) => {
return true;
});
}
private initExistingClusterControl(): void {
let self = this;
const labelWidth = '150px';
let configFileLabel = this.view!.modelBuilder.text().withProperties({ value: localize('deployCluster.kubeConfigFileLabelText', 'Kube config file path') }).component();
configFileLabel.width = labelWidth;
this.configFileInput = this.view!.modelBuilder.inputBox().withProperties({ width: '300px' }).component();
this.configFileInput.enabled = false;
this.browseFileButton = this.view!.modelBuilder.button().withProperties({ label: localize('deployCluster.browseText', 'Browse'), width: '100px' }).component();
let configFileContainer = this.view!.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'row', alignItems: 'baseline' })
.withItems([configFileLabel, this.configFileInput, this.browseFileButton], { CSSStyles: { 'margin-right': '10px' } }).component();
this.clusterContextsLabel = this.view!.modelBuilder.text().withProperties({ value: localize('deployCluster.clusterContextsLabelText', 'Cluster Contexts') }).component();
this.clusterContextsLabel.width = labelWidth;
this.errorLoadingClustersLabel = this.view!.modelBuilder.text().withProperties({ value: localize('deployCluster.errorLoadingClustersText', 'No cluster information is found in the config file or an error ocurred while loading the config file') }).component();
this.clusterContextList = this.view!.modelBuilder.divContainer().component();
this.clusterContextLoadingComponent = this.view!.modelBuilder.loadingComponent().withItem(this.clusterContextList).component();
this.existingClusterControl = this.view!.modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: false }).component();
let clusterContextContainer = this.view!.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'start' }).component();
clusterContextContainer.addItem(this.clusterContextsLabel, { flex: '0 0 auto' });
clusterContextContainer.addItem(this.clusterContextLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'width': '400px', 'margin-left': '10px', 'margin-top': '10px' } });
this.existingClusterControl.addItem(configFileContainer, { CSSStyles: { 'margin-top': '0px' } });
this.existingClusterControl.addItem(clusterContextContainer, {
CSSStyles: { 'margin- top': '10px' }
});
this.wizard.registerDisposable(this.browseFileButton.onDidClick(async () => {
let fileUris = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(os.homedir()),
openLabel: localize('deployCluster.selectKubeConfigFileText', 'Select'),
filters: {
'Config Files': ['*'],
}
}
);
if (!fileUris || fileUris.length === 0) {
return;
}
self.clusterContextList!.clearItems();
let fileUri = fileUris[0];
self.loadClusterContexts(fileUri.fsPath);
}));
}
private async loadClusterContexts(configPath: string): Promise<void> {
this.clusterContextLoadingComponent!.loading = true;
this.wizard.model.setPropertyValue(ClusterContext_VariableName, undefined);
this.wizard.wizardObject.message = { text: '' };
let self = this;
this.configFileInput!.value = configPath;
let clusterContexts: KubeClusterContext[] = [];
try {
clusterContexts = await this.wizard.kubeService.getClusterContexts(configPath);
} catch (error) {
this.wizard.wizardObject.message = {
text: localize('deployCluster.ConfigParseError', "Failed to load the config file"),
description: error.message || error, level: azdata.window.MessageLevel.Error
};
}
if (clusterContexts.length !== 0) {
self.wizard.model.setPropertyValue(KubeConfigPath_VariableName, configPath);
let options = clusterContexts.map(clusterContext => {
let option = this.view!.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
label: clusterContext.name,
checked: clusterContext.isCurrentContext,
name: ClusterRadioButtonGroupName
}).component();
if (clusterContext.isCurrentContext) {
self.wizard.model.setPropertyValue(ClusterContext_VariableName, clusterContext.name);
self.wizard.wizardObject.message = { text: '' };
}
this.wizard.registerDisposable(option.onDidClick(() => {
self.wizard.model.setPropertyValue(ClusterContext_VariableName, clusterContext.name);
self.wizard.wizardObject.message = { text: '' };
}));
return option;
});
self.clusterContextList!.addItems(options);
} else {
self.clusterContextList!.addItem(this.errorLoadingClustersLabel!);
}
this.clusterContextLoadingComponent!.loading = false;
}
}

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
@@ -16,10 +15,10 @@ export abstract class DialogBase {
this._dialogObject.cancelButton.onClick(() => this.onCancel());
}
protected abstract initializeDialog(): void;
protected abstract initialize(): void;
public open(): void {
this.initializeDialog();
this.initialize();
azdata.window.openDialog(this._dialogObject);
}

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class Model {
private propValueObject: { [s: string]: string | undefined } = {};
public setPropertyValue(property: string, value: string | number | boolean | undefined): void {
if (typeof value === 'boolean') {
this.propValueObject[property] = value ? 'true' : 'false';
} else if (typeof value === 'number') {
this.propValueObject[property] = value.toString();
} else {
this.propValueObject[property] = value;
}
}
public getIntegerValue(propName: string, defaultValue: number = 0): number {
const value = this.propValueObject[propName];
return value === undefined ? defaultValue : Number.parseInt(value);
}
public getStringValue(propName: string, defaultValue?: string): string | undefined {
const value = this.propValueObject[propName];
return value === undefined ? defaultValue : value;
}
public getBooleanValue(propName: string, defaultValue: boolean = false): boolean {
const value = this.propValueObject[propName];
return value === undefined ? defaultValue : value === 'true';
}
public setEnvironmentVariables(): void {
Object.keys(this.propValueObject).filter(propertyName => propertyName.startsWith('AZDATA_NB_VAR_')).forEach(propertyName => {
const value = this.getStringValue(propertyName);
if (value !== undefined && value !== '') {
process.env[propertyName] = value;
}
process.env[propertyName] = value === undefined ? '' : value;
});
}
}

View File

@@ -0,0 +1,437 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { DialogInfo, FieldType, FieldInfo, SectionInfo, LabelPosition } from '../interfaces';
import { Model } from './model';
const localize = nls.loadMessageBundle();
export type Validator = () => { valid: boolean, message: string };
export type InputComponents = { [s: string]: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent; };
export function getInputBoxComponent(name: string, inputComponents: InputComponents): azdata.InputBoxComponent {
return <azdata.InputBoxComponent>inputComponents[name];
}
export function getDropdownComponent(name: string, inputComponents: InputComponents): azdata.DropDownComponent {
return <azdata.DropDownComponent>inputComponents[name];
}
export function getCheckboxComponent(name: string, inputComponents: InputComponents): azdata.CheckBoxComponent {
return <azdata.CheckBoxComponent>inputComponents[name];
}
export const DefaultInputComponentWidth = '400px';
export const DefaultLabelComponentWidth = '200px';
export interface DialogContext extends CreateContext {
dialogInfo: DialogInfo;
container: azdata.window.Dialog;
}
export interface WizardPageContext extends CreateContext {
sections: SectionInfo[];
page: azdata.window.WizardPage;
container: azdata.window.Wizard;
}
export interface SectionContext extends CreateContext {
sectionInfo: SectionInfo;
view: azdata.ModelView;
}
interface FieldContext extends CreateContext {
fieldInfo: FieldInfo;
components: azdata.Component[];
view: azdata.ModelView;
}
interface CreateContext {
container: azdata.window.Dialog | azdata.window.Wizard;
onNewValidatorCreated: (validator: Validator) => void;
onNewDisposableCreated: (disposable: vscode.Disposable) => void;
onNewInputComponentCreated: (name: string, component: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent) => void;
}
export function createTextInput(view: azdata.ModelView, inputInfo: { defaultValue?: string, ariaLabel: string, required?: boolean, placeHolder?: string, width?: string }): azdata.InputBoxComponent {
return view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
value: inputInfo.defaultValue,
ariaLabel: inputInfo.ariaLabel,
inputType: 'text',
required: inputInfo.required,
placeHolder: inputInfo.placeHolder,
width: inputInfo.width
}).component();
}
export function createLabel(view: azdata.ModelView, info: { text: string, description?: string, required?: boolean, width?: string, fontStyle?: string }): azdata.TextComponent {
const text = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: info.text,
description: info.description,
requiredIndicator: info.required,
CSSStyles: { 'font-style': info.fontStyle || 'normal' }
}).component();
text.width = info.width;
return text;
}
export function createNumberInput(view: azdata.ModelView, info: { defaultValue?: string, ariaLabel?: string, min?: number, max?: number, required?: boolean, width?: string, placeHolder?: string }): azdata.InputBoxComponent {
return view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
value: info.defaultValue,
ariaLabel: info.ariaLabel,
inputType: 'number',
min: info.min,
max: info.max,
required: info.required,
width: info.width,
placeHolder: info.placeHolder
}).component();
}
export function createCheckbox(view: azdata.ModelView, info: { initialValue: boolean, label: string }): azdata.CheckBoxComponent {
return view.modelBuilder.checkBox().withProperties<azdata.CheckBoxProperties>({
checked: info.initialValue,
label: info.label
}).component();
}
export function createDropdown(view: azdata.ModelView, info: { defaultValue?: string | azdata.CategoryValue, values?: string[] | azdata.CategoryValue[], width?: string }): azdata.DropDownComponent {
return view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({
values: info.values,
value: info.defaultValue,
width: info.width
}).component();
}
export function initializeDialog(dialogContext: DialogContext): void {
const tabs: azdata.window.DialogTab[] = [];
dialogContext.dialogInfo.tabs.forEach(tabInfo => {
const tab = azdata.window.createTab(tabInfo.title);
tab.registerContent((view: azdata.ModelView) => {
const sections = tabInfo.sections.map(sectionInfo => {
sectionInfo.inputWidth = sectionInfo.inputWidth || tabInfo.inputWidth || DefaultInputComponentWidth;
sectionInfo.labelWidth = sectionInfo.labelWidth || tabInfo.labelWidth || DefaultLabelComponentWidth;
return createSection({
sectionInfo: sectionInfo,
view: view,
onNewDisposableCreated: dialogContext.onNewDisposableCreated,
onNewInputComponentCreated: dialogContext.onNewInputComponentCreated,
onNewValidatorCreated: dialogContext.onNewValidatorCreated,
container: dialogContext.container
});
});
const formBuilder = view.modelBuilder.formContainer().withFormItems(
sections.map(section => {
return { title: '', component: section };
}),
{
horizontal: false,
componentWidth: '100%'
}
);
const form = formBuilder.withLayout({ width: '100%' }).component();
return view.initializeModel(form);
});
tabs.push(tab);
});
dialogContext.container.content = tabs;
}
export function initializeWizardPage(context: WizardPageContext): void {
context.page.registerContent((view: azdata.ModelView) => {
const sections = context.sections.map(sectionInfo => {
sectionInfo.inputWidth = sectionInfo.inputWidth || DefaultInputComponentWidth;
sectionInfo.labelWidth = sectionInfo.labelWidth || DefaultLabelComponentWidth;
return createSection({
view: view,
container: context.container,
onNewDisposableCreated: context.onNewDisposableCreated,
onNewInputComponentCreated: context.onNewInputComponentCreated,
onNewValidatorCreated: context.onNewValidatorCreated,
sectionInfo: sectionInfo
});
});
const formBuilder = view.modelBuilder.formContainer().withFormItems(
sections.map(section => { return { title: '', component: section }; }),
{
horizontal: false,
componentWidth: '100%'
}
);
const form = formBuilder.withLayout({ width: '100%' }).component();
return view.initializeModel(form);
});
}
export function createSection(context: SectionContext): azdata.GroupContainer {
const components: azdata.Component[] = [];
context.sectionInfo.inputWidth = context.sectionInfo.inputWidth || DefaultInputComponentWidth;
context.sectionInfo.labelWidth = context.sectionInfo.labelWidth || DefaultLabelComponentWidth;
if (context.sectionInfo.fields) {
processFields(context.sectionInfo.fields, components, context);
} else if (context.sectionInfo.rows) {
context.sectionInfo.rows.forEach(rowInfo => {
const rowItems: azdata.Component[] = [];
processFields(rowInfo.fields, rowItems, context, context.sectionInfo.spaceBetweenFields || '50px');
const row = createFlexContainer(context.view, rowItems);
components.push(row);
});
}
return createGroupContainer(context.view, components, {
header: context.sectionInfo.title,
collapsible: context.sectionInfo.collapsible === undefined ? true : context.sectionInfo.collapsible,
collapsed: context.sectionInfo.collapsed === undefined ? false : context.sectionInfo.collapsed
});
}
function processFields(fieldInfoArray: FieldInfo[], components: azdata.Component[], context: SectionContext, spaceBetweenFields?: string): void {
for (let i = 0; i < fieldInfoArray.length; i++) {
const fieldInfo = fieldInfoArray[i];
fieldInfo.labelWidth = fieldInfo.labelWidth || context.sectionInfo.labelWidth;
fieldInfo.inputWidth = fieldInfo.inputWidth || context.sectionInfo.inputWidth;
fieldInfo.labelPosition = fieldInfo.labelPosition === undefined ? context.sectionInfo.labelPosition : fieldInfo.labelPosition;
processField({
view: context.view,
onNewDisposableCreated: context.onNewDisposableCreated,
onNewInputComponentCreated: context.onNewInputComponentCreated,
onNewValidatorCreated: context.onNewValidatorCreated,
fieldInfo: fieldInfo,
container: context.container,
components: components
});
if (spaceBetweenFields && i < fieldInfoArray.length - 1) {
components.push(context.view.modelBuilder.divContainer().withLayout({ width: spaceBetweenFields }).component());
}
}
}
export function createFlexContainer(view: azdata.ModelView, items: azdata.Component[], rowLayout: boolean = true): azdata.FlexContainer {
const flexFlow = rowLayout ? 'row' : 'column';
const alignItems = rowLayout ? 'center' : '';
const itemsStyle = rowLayout ? { CSSStyles: { 'margin-right': '5px' } } : {};
return view.modelBuilder.flexContainer().withItems(items, itemsStyle).withLayout({ flexFlow: flexFlow, alignItems: alignItems }).component();
}
export function createGroupContainer(view: azdata.ModelView, items: azdata.Component[], layout: azdata.GroupLayout): azdata.GroupContainer {
return view.modelBuilder.groupContainer().withItems(items).withLayout(layout).component();
}
function addLabelInputPairToContainer(view: azdata.ModelView, components: azdata.Component[], label: azdata.Component, input: azdata.Component, labelPosition?: LabelPosition) {
if (labelPosition && labelPosition === LabelPosition.Left) {
const row = createFlexContainer(view, [label, input]);
components.push(row);
} else {
components.push(label, input);
}
}
function processField(context: FieldContext): void {
switch (context.fieldInfo.type) {
case FieldType.Options:
processOptionsTypeField(context);
break;
case FieldType.DateTimeText:
processDateTimeTextField(context);
break;
case FieldType.Number:
processNumberField(context);
break;
case FieldType.SQLPassword:
case FieldType.Password:
processPasswordField(context);
break;
case FieldType.Text:
processTextField(context);
break;
case FieldType.ReadonlyText:
processReadonlyTextField(context);
break;
case FieldType.Checkbox:
processCheckboxField(context);
break;
default:
throw new Error(localize('UnknownFieldTypeError', "Unknown field type: \"{0}\"", context.fieldInfo.type));
}
}
function processOptionsTypeField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: false, width: context.fieldInfo.labelWidth });
const dropdown = createDropdown(context.view, {
values: context.fieldInfo.options,
defaultValue: context.fieldInfo.defaultValue,
width: context.fieldInfo.inputWidth
});
context.onNewInputComponentCreated(context.fieldInfo.variableName!, dropdown);
addLabelInputPairToContainer(context.view, context.components, label, dropdown, context.fieldInfo.labelPosition);
}
function processDateTimeTextField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth });
const defaultValue = context.fieldInfo.defaultValue + new Date().toISOString().slice(0, 19).replace(/[^0-9]/g, ''); // Take the date time information and only leaving the numbers
const input = context.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
value: defaultValue,
ariaLabel: context.fieldInfo.label,
inputType: 'text',
required: !context.fieldInfo.useCustomValidator && context.fieldInfo.required,
placeHolder: context.fieldInfo.placeHolder
}).component();
input.width = context.fieldInfo.inputWidth;
context.onNewInputComponentCreated(context.fieldInfo.variableName!, input);
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo.labelPosition);
}
function processNumberField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth });
const input = createNumberInput(context.view, {
defaultValue: context.fieldInfo.defaultValue,
ariaLabel: context.fieldInfo.label,
min: context.fieldInfo.min,
max: context.fieldInfo.max,
required: !context.fieldInfo.useCustomValidator && context.fieldInfo.required,
width: context.fieldInfo.inputWidth,
placeHolder: context.fieldInfo.placeHolder
});
context.onNewInputComponentCreated(context.fieldInfo.variableName!, input);
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo.labelPosition);
}
function processTextField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth });
const input = createTextInput(context.view, {
defaultValue: context.fieldInfo.defaultValue,
ariaLabel: context.fieldInfo.label,
required: !context.fieldInfo.useCustomValidator && context.fieldInfo.required,
placeHolder: context.fieldInfo.placeHolder,
width: context.fieldInfo.inputWidth
});
context.onNewInputComponentCreated(context.fieldInfo.variableName!, input);
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo.labelPosition);
}
function processPasswordField(context: FieldContext): void {
const passwordLabel = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth });
const passwordInput = context.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: context.fieldInfo.label,
inputType: 'password',
required: !context.fieldInfo.useCustomValidator && context.fieldInfo.required,
placeHolder: context.fieldInfo.placeHolder,
width: context.fieldInfo.inputWidth
}).component();
context.onNewInputComponentCreated(context.fieldInfo.variableName!, passwordInput);
addLabelInputPairToContainer(context.view, context.components, passwordLabel, passwordInput, context.fieldInfo.labelPosition);
if (context.fieldInfo.type === FieldType.SQLPassword) {
const invalidPasswordMessage = getInvalidSQLPasswordMessage(context.fieldInfo.label);
context.onNewDisposableCreated(passwordInput.onTextChanged(() => {
if (context.fieldInfo.type === FieldType.SQLPassword && isValidSQLPassword(passwordInput.value!, context.fieldInfo.userName)) {
removeValidationMessage(context.container, invalidPasswordMessage);
}
}));
context.onNewValidatorCreated((): { valid: boolean, message: string } => {
return { valid: isValidSQLPassword(passwordInput.value!, context.fieldInfo.userName), message: invalidPasswordMessage };
});
}
if (context.fieldInfo.confirmationRequired) {
const passwordNotMatchMessage = getPasswordMismatchMessage(context.fieldInfo.label);
const confirmPasswordLabel = createLabel(context.view, { text: context.fieldInfo.confirmationLabel!, required: true, width: context.fieldInfo.labelWidth });
const confirmPasswordInput = context.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: context.fieldInfo.confirmationLabel,
inputType: 'password',
required: !context.fieldInfo.useCustomValidator,
width: context.fieldInfo.inputWidth
}).component();
addLabelInputPairToContainer(context.view, context.components, confirmPasswordLabel, confirmPasswordInput, context.fieldInfo.labelPosition);
context.onNewValidatorCreated((): { valid: boolean, message: string } => {
const passwordMatches = passwordInput.value === confirmPasswordInput.value;
return { valid: passwordMatches, message: passwordNotMatchMessage };
});
const updatePasswordMismatchMessage = () => {
if (passwordInput.value === confirmPasswordInput.value) {
removeValidationMessage(context.container, passwordNotMatchMessage);
}
};
context.onNewDisposableCreated(passwordInput.onTextChanged(() => {
updatePasswordMismatchMessage();
}));
context.onNewDisposableCreated(confirmPasswordInput.onTextChanged(() => {
updatePasswordMismatchMessage();
}));
}
}
function processReadonlyTextField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: false, width: context.fieldInfo.labelWidth });
const text = createLabel(context.view, { text: context.fieldInfo.defaultValue!, description: '', required: false, width: context.fieldInfo.inputWidth, fontStyle: context.fieldInfo.fontStyle });
addLabelInputPairToContainer(context.view, context.components, label, text, context.fieldInfo.labelPosition);
}
function processCheckboxField(context: FieldContext): void {
const checkbox = createCheckbox(context.view, { initialValue: context.fieldInfo.defaultValue! === 'true', label: context.fieldInfo.label });
context.components.push(checkbox);
context.onNewInputComponentCreated(context.fieldInfo.variableName!, checkbox);
}
export function isValidSQLPassword(password: string, userName: string = 'sa'): boolean {
// Validate SQL Server password
const containsUserName = password && userName !== undefined && password.toUpperCase().includes(userName.toUpperCase());
// Instead of using one RegEx, I am seperating it to make it more readable.
const hasUpperCase = /[A-Z]/.test(password) ? 1 : 0;
const hasLowerCase = /[a-z]/.test(password) ? 1 : 0;
const hasNumbers = /\d/.test(password) ? 1 : 0;
const hasNonalphas = /\W/.test(password) ? 1 : 0;
return !containsUserName && password.length >= 8 && password.length <= 128 && (hasUpperCase + hasLowerCase + hasNumbers + hasNonalphas >= 3);
}
export function removeValidationMessage(container: azdata.window.Dialog | azdata.window.Wizard, message: string): void {
if (container.message && container.message.text.includes(message)) {
const messageWithLineBreak = message + '\n';
const searchText = container.message.text.includes(messageWithLineBreak) ? messageWithLineBreak : message;
container.message = { text: container.message.text.replace(searchText, '') };
}
}
export function getInvalidSQLPasswordMessage(fieldName: string): string {
return localize('invalidSQLPassword', "{0} doesn't meet the password complexity requirement. For more information: https://docs.microsoft.com/sql/relational-databases/security/password-policy", fieldName);
}
export function getPasswordMismatchMessage(fieldName: string): string {
return localize('passwordNotMatch', "{0} doesn't match the confirmation password", fieldName);
}
export function setModelValues(inputComponents: InputComponents, model: Model): void {
Object.keys(inputComponents).forEach(key => {
let value;
const input = inputComponents[key];
if ('checked' in input) {
value = input.checked ? 'true' : 'false';
} else {
const inputValue = input.value;
if (typeof inputValue === 'string' || typeof inputValue === 'undefined') {
value = inputValue;
} else {
value = inputValue.name;
}
}
model.setPropertyValue(key, value);
});
}
export function isInputBoxEmpty(input: azdata.InputBoxComponent): boolean {
return input.value === undefined || input.value === '';
}
export const MissingRequiredInformationErrorMessage = localize('deployCluster.MissingRequiredInfoError', "Please fill out the required fields marked with red asterisks.");

View File

@@ -2,20 +2,22 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { DialogBase } from './dialogBase';
import { INotebookService } from '../services/notebookService';
import { DialogFieldInfo, FieldType, DialogInfo } from '../interfaces';
import { DialogInfo } from '../interfaces';
import { Validator, initializeDialog, InputComponents, setModelValues } from './modelViewUtils';
import { Model } from './model';
import { EOL } from 'os';
const localize = nls.loadMessageBundle();
export class NotebookInputDialog extends DialogBase {
private variables: { [s: string]: string | undefined; } = {};
private validators: (() => { valid: boolean, message: string })[] = [];
private inputComponents: InputComponents = {};
constructor(private notebookService: INotebookService,
private dialogInfo: DialogInfo) {
@@ -24,192 +26,46 @@ export class NotebookInputDialog extends DialogBase {
this._dialogObject.okButton.onClick(() => this.onComplete());
}
protected initializeDialog() {
const tabs: azdata.window.DialogTab[] = [];
this.dialogInfo.tabs.forEach(tabInfo => {
const tab = azdata.window.createTab(tabInfo.title);
tab.registerContent((view: azdata.ModelView) => {
const sections: azdata.FormComponentGroup[] = [];
tabInfo.sections.forEach(sectionInfo => {
const fields: azdata.FormComponent[] = [];
sectionInfo.fields.forEach(fieldInfo => {
this.addField(view, fields, fieldInfo);
});
sections.push({ title: sectionInfo.title, components: fields });
});
const formBuilder = view.modelBuilder.formContainer().withFormItems(
sections,
{
horizontal: false
}
);
const form = formBuilder.withLayout({ width: '100%' }).component();
const self = this;
this._dialogObject.registerCloseValidator(() => {
const messages: string[] = [];
self.validators.forEach(validator => {
const result = validator();
if (!result.valid) {
messages.push(result.message);
}
});
if (messages.length > 0) {
self._dialogObject.message = { level: azdata.window.MessageLevel.Error, text: messages.join('\n') };
} else {
self._dialogObject.message = { text: '' };
}
return messages.length === 0;
});
return view.initializeModel(form);
});
tabs.push(tab);
});
this._dialogObject.content = tabs;
}
private addField(view: azdata.ModelView, fields: azdata.FormComponent[], fieldInfo: DialogFieldInfo): void {
this.variables[fieldInfo.variableName] = fieldInfo.defaultValue;
let component: { component: azdata.Component, title: string }[] | azdata.Component | undefined = undefined;
switch (fieldInfo.type) {
case FieldType.Options:
component = this.createOptionsTypeField(view, fieldInfo);
break;
case FieldType.DateTimeText:
component = this.createDateTimeTextField(view, fieldInfo);
break;
case FieldType.Number:
component = this.createNumberField(view, fieldInfo);
break;
case FieldType.SQLPassword:
case FieldType.Password:
component = this.createPasswordField(view, fieldInfo);
break;
case FieldType.Text:
component = this.createTextField(view, fieldInfo);
break;
default:
throw new Error(localize('deploymentDialog.UnknownFieldTypeError', "Unknown field type: \"{0}\"", fieldInfo.type));
}
if (component) {
if (Array.isArray(component)) {
fields.push(...component);
} else {
fields.push({ title: fieldInfo.label, component: component });
protected initialize() {
const self = this;
const validators: Validator[] = [];
initializeDialog({
dialogInfo: this.dialogInfo,
container: this._dialogObject,
onNewDisposableCreated: (disposable: vscode.Disposable): void => {
this._toDispose.push(disposable);
},
onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => {
this.inputComponents[name] = component;
},
onNewValidatorCreated: (validator: Validator): void => {
validators.push(validator);
}
} else {
throw new Error(localize('deploymentDialog.addFieldError', "Failed to add field: \"{0}\"", fieldInfo.label));
}
}
private createOptionsTypeField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.DropDownComponent {
const dropdown = view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({ values: fieldInfo.options, value: fieldInfo.defaultValue }).component();
this._toDispose.push(dropdown.onValueChanged(() => { this.variables[fieldInfo.variableName] = <string>dropdown.value; }));
return dropdown;
}
private createDateTimeTextField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.InputBoxComponent {
const defaultValue = fieldInfo.defaultValue + new Date().toISOString().slice(0, 19).replace(/[^0-9]/g, '');
const input = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
value: defaultValue, ariaLabel: fieldInfo.label, inputType: 'text', required: fieldInfo.required, placeHolder: fieldInfo.placeHolder
}).component();
this.variables[fieldInfo.variableName] = defaultValue;
this._toDispose.push(input.onTextChanged(() => { this.variables[fieldInfo.variableName] = input.value; }));
return input;
}
private createNumberField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.InputBoxComponent {
const input = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
value: fieldInfo.defaultValue, ariaLabel: fieldInfo.label, inputType: 'number', min: fieldInfo.min, max: fieldInfo.max, required: fieldInfo.required
}).component();
this._toDispose.push(input.onTextChanged(() => { this.variables[fieldInfo.variableName] = input.value; }));
return input;
}
private createTextField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): azdata.InputBoxComponent {
const input = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
value: fieldInfo.defaultValue, ariaLabel: fieldInfo.label, inputType: 'text', min: fieldInfo.min, max: fieldInfo.max, required: fieldInfo.required, placeHolder: fieldInfo.placeHolder
}).component();
this._toDispose.push(input.onTextChanged(() => { this.variables[fieldInfo.variableName] = input.value; }));
return input;
}
private createPasswordField(view: azdata.ModelView, fieldInfo: DialogFieldInfo): { title: string, component: azdata.Component }[] {
const components: { title: string, component: azdata.Component }[] = [];
const passwordInput = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: fieldInfo.label, inputType: 'password', required: fieldInfo.required, placeHolder: fieldInfo.placeHolder
}).component();
this._toDispose.push(passwordInput.onTextChanged(() => { this.variables[fieldInfo.variableName] = passwordInput.value; }));
components.push({ title: fieldInfo.label, component: passwordInput });
if (fieldInfo.type === FieldType.SQLPassword) {
const invalidPasswordMessage = localize('invalidSQLPassword', "{0} doesn't meet the password complexity requirement. For more information: https://docs.microsoft.com/sql/relational-databases/security/password-policy", fieldInfo.label);
this._toDispose.push(passwordInput.onTextChanged(() => {
if (fieldInfo.type === FieldType.SQLPassword && this.isValidSQLPassword(fieldInfo, passwordInput)) {
this.removeValidationMessage(invalidPasswordMessage);
});
this._dialogObject.registerCloseValidator(() => {
const messages: string[] = [];
validators.forEach(validator => {
const result = validator();
if (!result.valid) {
messages.push(result.message);
}
}));
this.validators.push((): { valid: boolean, message: string } => {
return { valid: this.isValidSQLPassword(fieldInfo, passwordInput), message: invalidPasswordMessage };
});
}
if (fieldInfo.confirmationRequired) {
const passwordNotMatchMessage = localize('passwordNotMatch', "{0} doesn't match the confirmation password", fieldInfo.label);
const confirmPasswordInput = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({ ariaLabel: fieldInfo.confirmationLabel, inputType: 'password', required: true }).component();
components.push({ title: fieldInfo.confirmationLabel, component: confirmPasswordInput });
this.validators.push((): { valid: boolean, message: string } => {
const passwordMatches = passwordInput.value === confirmPasswordInput.value;
return { valid: passwordMatches, message: passwordNotMatchMessage };
});
const updatePasswordMismatchMessage = () => {
if (passwordInput.value === confirmPasswordInput.value) {
this.removeValidationMessage(passwordNotMatchMessage);
}
};
this._toDispose.push(passwordInput.onTextChanged(() => {
updatePasswordMismatchMessage();
}));
this._toDispose.push(confirmPasswordInput.onTextChanged(() => {
updatePasswordMismatchMessage();
}));
}
return components;
if (messages.length > 0) {
self._dialogObject.message = { level: azdata.window.MessageLevel.Error, text: messages.join(EOL) };
} else {
self._dialogObject.message = { text: '' };
}
return messages.length === 0;
});
}
private onComplete(): void {
Object.keys(this.variables).forEach(key => {
process.env[key] = this.variables[key];
const model: Model = new Model();
setModelValues(this.inputComponents, model);
model.setEnvironmentVariables();
this.notebookService.launchNotebook(this.dialogInfo.notebook).then(() => { }, (error) => {
vscode.window.showErrorMessage(error);
});
this.notebookService.launchNotebook(this.dialogInfo.notebook);
this.dispose();
}
private isValidSQLPassword(field: DialogFieldInfo, component: azdata.InputBoxComponent): boolean {
const password = component.value!;
// Validate SQL Server password
const containsUserName = password && field.userName && password.toUpperCase().includes(field.userName.toUpperCase());
// Instead of using one RegEx, I am seperating it to make it more readable.
const hasUpperCase = /[A-Z]/.test(password) ? 1 : 0;
const hasLowerCase = /[a-z]/.test(password) ? 1 : 0;
const hasNumbers = /\d/.test(password) ? 1 : 0;
const hasNonalphas = /\W/.test(password) ? 1 : 0;
return !containsUserName && password.length >= 8 && password.length <= 128 && (hasUpperCase + hasLowerCase + hasNumbers + hasNonalphas >= 3);
}
private removeValidationMessage(message: string): void {
if (this._dialogObject.message && this._dialogObject.message.text.includes(message)) {
const messageWithLineBreak = message + '\n';
const searchText = this._dialogObject.message.text.includes(messageWithLineBreak) ? messageWithLineBreak : message;
this._dialogObject.message = { text: this._dialogObject.message.text.replace(searchText, '') };
}
}
}

View File

@@ -2,19 +2,21 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { DialogBase } from './dialogBase';
import { ResourceType, DeploymentProvider } from '../interfaces';
import { ResourceType, DeploymentProvider, AgreementInfo } from '../interfaces';
import { IResourceTypeService } from '../services/resourceTypeService';
import { IToolsService } from '../services/toolsService';
import { EOL } from 'os';
import { createFlexContainer } from './modelViewUtils';
const localize = nls.loadMessageBundle();
export class ResourceTypePickerDialog extends DialogBase {
private toolRefreshTimestamp: number = 0;
private _selectedResourceType: ResourceType;
private _resourceTypeCards: azdata.CardComponent[] = [];
private _view!: azdata.ModelView;
@@ -23,6 +25,9 @@ export class ResourceTypePickerDialog extends DialogBase {
private _toolsTable!: azdata.TableComponent;
private _cardResourceTypeMap: Map<string, azdata.CardComponent> = new Map();
private _optionDropDownMap: Map<string, azdata.DropDownComponent> = new Map();
private _toolsLoadingComponent!: azdata.LoadingComponent;
private _agreementContainer!: azdata.DivContainer;
private _agreementCheckboxChecked: boolean = false;
constructor(private extensionContext: vscode.ExtensionContext,
private toolsService: IToolsService,
@@ -34,8 +39,18 @@ export class ResourceTypePickerDialog extends DialogBase {
this._dialogObject.okButton.onClick(() => this.onComplete());
}
initializeDialog() {
initialize() {
let tab = azdata.window.createTab('');
this._dialogObject.registerCloseValidator(() => {
const isValid = this._selectedResourceType && (this._selectedResourceType.agreement === undefined || this._agreementCheckboxChecked);
if (!isValid) {
this._dialogObject.message = {
text: localize('deploymentDialog.AcceptAgreements', "You must agree to the license agreements in order to proceed."),
level: azdata.window.MessageLevel.Error
};
}
return isValid;
});
tab.registerContent((view: azdata.ModelView) => {
const tableWidth = 1126;
this._view = view;
@@ -43,25 +58,33 @@ export class ResourceTypePickerDialog extends DialogBase {
const cardsContainer = view.modelBuilder.flexContainer().withItems(this._resourceTypeCards, { flex: '0 0 auto', CSSStyles: { 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'left' }).component();
this._resourceDescriptionLabel = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: this._selectedResourceType ? this._selectedResourceType.description : undefined }).component();
this._optionsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
this._agreementContainer = view.modelBuilder.divContainer().component();
const toolColumn: azdata.TableColumn = {
value: localize('deploymentDialog.toolNameColumnHeader', 'Tool'),
width: 150
};
const descriptionColumn: azdata.TableColumn = {
value: localize('deploymentDialog.toolDescriptionColumnHeader', 'Description'),
width: 850
width: 650
};
const installStatusColumn: azdata.TableColumn = {
value: localize('deploymentDialog.toolStatusColumnHeader', 'Installed'),
width: 100
};
const versionColumn: azdata.TableColumn = {
value: localize('deploymentDialog.toolVersionColumnHeader', 'Version'),
width: 100
};
this._toolsTable = view.modelBuilder.table().withProperties<azdata.TableComponentProperties>({
data: [],
columns: [toolColumn, descriptionColumn],
columns: [toolColumn, descriptionColumn, installStatusColumn, versionColumn],
width: tableWidth
}).component();
const toolsTableWrapper = view.modelBuilder.divContainer().withLayout({ width: tableWidth }).component();
toolsTableWrapper.addItem(this._toolsTable, { CSSStyles: { 'border-left': '1px solid silver', 'border-top': '1px solid silver' } });
this._toolsLoadingComponent = view.modelBuilder.loadingComponent().withItem(toolsTableWrapper).component();
const formBuilder = view.modelBuilder.formContainer().withFormItems(
[
{
@@ -71,10 +94,14 @@ export class ResourceTypePickerDialog extends DialogBase {
component: this._resourceDescriptionLabel,
title: ''
}, {
component: this._agreementContainer,
title: ''
},
{
component: this._optionsContainer,
title: localize('deploymentDialog.OptionsTitle', 'Options')
}, {
component: toolsTableWrapper,
component: this._toolsLoadingComponent,
title: localize('deploymentDialog.RequiredToolsTitle', 'Required tools')
}
],
@@ -128,6 +155,12 @@ export class ResourceTypePickerDialog extends DialogBase {
}
this._resourceDescriptionLabel.value = resourceType.description;
this._agreementCheckboxChecked = false;
this._agreementContainer.clearItems();
if (resourceType.agreement) {
this._agreementContainer.addItem(this.createAgreementCheckbox(resourceType.agreement));
}
this._optionsContainer.clearItems();
this._optionDropDownMap.clear();
resourceType.options.forEach(option => {
@@ -151,19 +184,67 @@ export class ResourceTypePickerDialog extends DialogBase {
}
private updateTools(): void {
const tools = this.getCurrentProvider().requiredTools;
this.toolRefreshTimestamp = new Date().getTime();
const currentRefreshTimestamp = this.toolRefreshTimestamp;
const toolRequirements = this.getCurrentProvider().requiredTools;
const headerRowHeight = 28;
this._toolsTable.height = 25 * Math.max(tools.length, 1) + headerRowHeight;
if (tools.length === 0) {
this._toolsTable.height = 25 * Math.max(toolRequirements.length, 1) + headerRowHeight;
if (toolRequirements.length === 0) {
this._dialogObject.okButton.enabled = true;
this._toolsTable.data = [[localize('deploymentDialog.NoRequiredTool', "No tools required"), '']];
} else {
this._toolsTable.data = tools.map(toolRef => {
const tool = this.toolsService.getToolByName(toolRef.name)!;
return [tool.displayName, tool.description];
const tools = toolRequirements.map(toolReq => {
return this.toolsService.getToolByName(toolReq.name)!;
});
this._toolsLoadingComponent.loading = true;
this._dialogObject.okButton.enabled = false;
this._dialogObject.message = {
text: ''
};
Promise.all(tools.map(tool => tool.loadInformation())).then(() => {
// If the local timestamp does not match the class level timestamp, it means user has changed options, ignore the results
if (this.toolRefreshTimestamp !== currentRefreshTimestamp) {
return;
}
const messages: string[] = [];
this._toolsTable.data = toolRequirements.map(toolRef => {
const tool = this.toolsService.getToolByName(toolRef.name)!;
if (!tool.isInstalled) {
messages.push(localize('deploymentDialog.ToolInformation', "{0}: {1}", tool.displayName, tool.homePage));
if (tool.statusDescription !== undefined) {
console.warn(localize('deploymentDialog.DetailToolStatusDescription', "Additional status information for tool: {0}. {1}", tool.name, tool.statusDescription));
}
}
return [tool.displayName, tool.description, tool.isInstalled ? localize('deploymentDialog.YesText', "Yes") : localize('deploymentDialog.NoText', "No"), tool.version ? tool.version.version : ''];
});
this._dialogObject.okButton.enabled = messages.length === 0;
if (messages.length !== 0) {
messages.push(localize('deploymentDialog.VersionInformationDebugHint', "You will need to restart Azure Data Studio if the tools are installed after Azure Data Studio is launched to pick up the updated PATH environment variable. You may find additional details in the debug console."));
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: localize('deploymentDialog.ToolCheckFailed', "Some required tools are not installed or do not meet the minimum version requirement."),
description: messages.join(EOL)
};
}
this._toolsLoadingComponent.loading = false;
});
}
}
private createAgreementCheckbox(agreementInfo: AgreementInfo): azdata.FlexContainer {
const checkbox = this._view.modelBuilder.checkBox().component();
checkbox.checked = false;
this._toDispose.push(checkbox.onChanged(() => {
this._agreementCheckboxChecked = checkbox.checked;
}));
const text = this._view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: agreementInfo.template,
links: agreementInfo.links
}).component();
return createFlexContainer(this._view, [checkbox, text]);
}
private getCurrentProvider(): DeploymentProvider {
const options: { option: string, value: string }[] = [];

View File

@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { WizardPageBase } from './wizardPageBase';
import { Model } from './model';
const localize = nls.loadMessageBundle();
export abstract class WizardBase<T, M extends Model> {
private customButtons: azdata.window.Button[] = [];
private pages: WizardPageBase<T>[] = [];
public wizardObject: azdata.window.Wizard;
public toDispose: vscode.Disposable[] = [];
public get model(): M {
return this._model;
}
constructor(private title: string, private _model: M) {
this.wizardObject = azdata.window.createWizard(title);
}
public open(): Thenable<void> {
this.initialize();
this.wizardObject.customButtons = this.customButtons;
this.toDispose.push(this.wizardObject.onPageChanged((e) => {
let previousPage = this.pages[e.lastPage];
let newPage = this.pages[e.newPage];
previousPage.onLeave();
newPage.onEnter();
}));
this.toDispose.push(this.wizardObject.doneButton.onClick(() => {
this.onOk();
this.dispose();
}));
this.toDispose.push(this.wizardObject.cancelButton.onClick(() => {
this.onCancel();
this.dispose();
}));
return this.wizardObject.open().then(() => {
if (this.pages && this.pages.length > 0) {
this.pages[0].onEnter();
}
});
}
protected abstract initialize(): void;
protected abstract onOk(): void;
protected abstract onCancel(): void;
public addButton(button: azdata.window.Button) {
this.customButtons.push(button);
}
protected setPages(pages: WizardPageBase<T>[]) {
this.wizardObject!.pages = pages.map(p => p.pageObject);
this.pages = pages;
this.pages.forEach((page) => {
page.initialize();
});
}
private dispose() {
let errorOccured = false;
this.toDispose.forEach((disposable: vscode.Disposable) => {
try {
disposable.dispose();
}
catch (error) {
errorOccured = true;
console.error(error);
}
});
if (errorOccured) {
vscode.window.showErrorMessage(localize('resourceDeployment.DisposableError', "Error occured while closing the wizard: {0}, open 'Debugger Console' for more information."), this.title);
}
}
public registerDisposable(disposable: vscode.Disposable): void {
this.toDispose.push(disposable);
}
}

View File

@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { Validator } from './modelViewUtils';
export abstract class WizardPageBase<T> {
private _page: azdata.window.WizardPage;
private _validators: Validator[] = [];
constructor(title: string, description: string, private _wizard: T) {
this._page = azdata.window.createWizardPage(title);
this._page.description = description;
}
public get pageObject(): azdata.window.WizardPage {
return this._page;
}
public get wizard(): T {
return this._wizard;
}
public onEnter(): void { }
public onLeave(): void { }
public abstract initialize(): void;
protected get validators(): Validator[] {
return this._validators;
}
}