Revert "Remove all Big Data Cluster features (#21369)" (#21618)

* Revert "Remove all Big Data Cluster features (#21369)"

This reverts commit e2327c393a.

* Bump STS
This commit is contained in:
Karl Burtram
2023-01-17 17:03:21 -08:00
committed by GitHub
parent 68cbff38f4
commit 44dd917100
214 changed files with 46797 additions and 343 deletions

View File

@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'bdc' {
export const enum constants {
extensionName = 'Microsoft.big-data-cluster'
}
export interface IExtension {
getClusterController(url: string, authType: AuthType, username?: string, password?: string): IClusterController;
}
export interface IEndpointModel {
name?: string;
description?: string;
endpoint?: string;
protocol?: string;
}
export interface IHttpResponse {
method?: string;
url?: string;
statusCode?: number;
statusMessage?: string;
}
export interface IEndPointsResponse {
response: IHttpResponse;
endPoints: IEndpointModel[];
}
export type AuthType = 'integrated' | 'basic';
export interface IClusterController {
getClusterConfig(): Promise<any>;
getKnoxUsername(defaultUsername: string): Promise<string>;
getEndPoints(promptConnect?: boolean): Promise<IEndPointsResponse>
username: string;
password: string;
}
}

View File

@@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as kerberos from '@microsoft/ads-kerberos';
import * as vscode from 'vscode';
export async function authenticateKerberos(hostname: string): Promise<string> {
const service = 'HTTP' + (process.platform === 'win32' ? '/' : '@') + hostname;
const mechOID = kerberos.GSS_MECH_OID_KRB5;
let client = await kerberos.initializeClient(service, { mechOID });
let response = await client.step('');
return response;
}
type HostAndIp = { host: string, port: string };
export function getHostAndPortFromEndpoint(endpoint: string): HostAndIp {
let authority = vscode.Uri.parse(endpoint).authority;
let hostAndPortRegex = /^(.*)([,:](\d+))/g;
let match = hostAndPortRegex.exec(authority);
if (match) {
return {
host: match[1],
port: match[3]
};
}
return {
host: authority,
port: undefined
};
}

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export enum BdcItemType {
controllerRoot = 'bigDataClusters.itemType.controllerRootNode',
controller = 'bigDataClusters.itemType.controllerNode',
loadingController = 'bigDataClusters.itemType.loadingControllerNode'
}
export interface IconPath {
dark: string;
light: string;
}
export class IconPathHelper {
private static extensionContext: vscode.ExtensionContext;
public static controllerNode: IconPath;
public static copy: IconPath;
public static refresh: IconPath;
public static status_ok: IconPath;
public static status_warning: IconPath;
public static notebook: IconPath;
public static status_circle_red: IconPath;
public static status_circle_blank: IconPath;
public static setExtensionContext(extensionContext: vscode.ExtensionContext) {
IconPathHelper.extensionContext = extensionContext;
IconPathHelper.controllerNode = {
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/bigDataCluster_controller.svg'),
light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/bigDataCluster_controller.svg')
};
IconPathHelper.copy = {
light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/copy.svg'),
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/copy_inverse.svg')
};
IconPathHelper.refresh = {
light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/refresh.svg'),
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/refresh_inverse.svg')
};
IconPathHelper.status_ok = {
light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/status_ok_light.svg'),
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/status_ok_dark.svg')
};
IconPathHelper.status_warning = {
light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/status_warning_light.svg'),
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/status_warning_dark.svg')
};
IconPathHelper.notebook = {
light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/notebook.svg'),
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/notebook_inverse.svg')
};
IconPathHelper.status_circle_red = {
light: IconPathHelper.extensionContext.asAbsolutePath('resources/status_circle_red.svg'),
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/status_circle_red.svg')
};
IconPathHelper.status_circle_blank = {
light: IconPathHelper.extensionContext.asAbsolutePath('resources/status_circle_blank.svg'),
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/status_circle_blank.svg')
};
}
}
export namespace cssStyles {
export const title = { 'font-size': '14px', 'font-weight': '600' };
export const tableHeader = { 'text-align': 'left', 'font-weight': 'bold', 'text-transform': 'uppercase', 'font-size': '10px', 'user-select': 'text' };
export const text = { 'margin-block-start': '0px', 'margin-block-end': '0px' };
export const lastUpdatedText = { ...text, 'color': '#595959' };
export const errorText = { ...text, 'color': 'red' };
}
export const clusterEndpointsProperty = 'clusterEndpoints';
export const controllerEndpointName = 'controller';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,455 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as request from 'request';
import { authenticateKerberos, getHostAndPortFromEndpoint } from '../auth';
import { BdcRouterApi, Authentication, EndpointModel, BdcStatusModel, DefaultApi } from './apiGenerated';
import { TokenRouterApi } from './clusterApiGenerated2';
import * as nls from 'vscode-nls';
import { ConnectControllerDialog, ConnectControllerModel } from '../dialog/connectControllerDialog';
import { getIgnoreSslVerificationConfigSetting } from '../utils';
import { IClusterController, AuthType, IEndPointsResponse, IHttpResponse } from 'bdc';
const localize = nls.loadMessageBundle();
const DEFAULT_KNOX_USERNAME = 'root';
class SslAuth implements Authentication {
constructor() { }
applyToRequest(requestOptions: request.Options): void {
requestOptions.rejectUnauthorized = !getIgnoreSslVerificationConfigSetting();
}
}
export class KerberosAuth extends SslAuth implements Authentication {
constructor(public kerberosToken: string) {
super();
}
override applyToRequest(requestOptions: request.Options): void {
super.applyToRequest(requestOptions);
if (requestOptions && requestOptions.headers) {
requestOptions.headers['Authorization'] = `Negotiate ${this.kerberosToken}`;
}
requestOptions.auth = undefined;
}
}
export class BasicAuth extends SslAuth implements Authentication {
constructor(public username: string, public password: string) {
super();
}
override applyToRequest(requestOptions: request.Options): void {
super.applyToRequest(requestOptions);
requestOptions.auth = {
username: this.username, password: this.password
};
}
}
export class OAuthWithSsl extends SslAuth implements Authentication {
public accessToken: string = '';
override applyToRequest(requestOptions: request.Options): void {
super.applyToRequest(requestOptions);
if (requestOptions && requestOptions.headers) {
requestOptions.headers['Authorization'] = `Bearer ${this.accessToken}`;
}
requestOptions.auth = undefined;
}
}
class BdcApiWrapper extends BdcRouterApi {
constructor(basePathOrUsername: string, password: string, basePath: string, auth: Authentication) {
if (password) {
super(basePathOrUsername, password, basePath);
} else {
super(basePath, undefined, undefined);
}
this.authentications.default = auth;
}
}
class DefaultApiWrapper extends DefaultApi {
constructor(basePathOrUsername: string, password: string, basePath: string, auth: Authentication) {
if (password) {
super(basePathOrUsername, password, basePath);
} else {
super(basePath, undefined, undefined);
}
this.authentications.default = auth;
}
}
export class ClusterController implements IClusterController {
private _authPromise: Promise<Authentication>;
private _url: string;
private readonly _dialog: ConnectControllerDialog;
private _connectionPromise: Promise<ClusterController>;
constructor(url: string,
private _authType: AuthType,
private _username?: string,
private _password?: string
) {
if (!url || (_authType === 'basic' && (!_username || !_password))) {
throw new Error('Missing required inputs for Cluster controller API (URL, username, password)');
}
this._url = adjustUrl(url);
if (this._authType === 'basic') {
this._authPromise = Promise.resolve(new BasicAuth(_username, _password));
} else {
this._authPromise = this.requestTokenUsingKerberos();
}
this._dialog = new ConnectControllerDialog(new ConnectControllerModel(
{
url: this._url,
auth: this._authType,
username: this._username,
password: this._password
}));
}
public get url(): string {
return this._url;
}
public get authType(): AuthType {
return this._authType;
}
public get username(): string | undefined {
return this._username;
}
public get password(): string | undefined {
return this._password;
}
private async requestTokenUsingKerberos(): Promise<Authentication> {
let supportsKerberos = await this.verifyKerberosSupported();
if (!supportsKerberos) {
throw new Error(localize('error.no.activedirectory', "This cluster does not support Windows authentication"));
}
try {
// AD auth is available, login to keberos and convert to token auth for all future calls
let host = getHostAndPortFromEndpoint(this._url).host;
let kerberosToken = await authenticateKerberos(host);
let tokenApi = new TokenRouterApi(this._url);
tokenApi.setDefaultAuthentication(new KerberosAuth(kerberosToken));
let result = await tokenApi.apiV1TokenPost();
let auth = new OAuthWithSsl();
auth.accessToken = result.body.accessToken;
return auth;
} catch (error) {
let controllerErr = new ControllerError(error, localize('bdc.error.tokenPost', "Error during authentication"));
if (controllerErr.code === 401) {
throw new Error(localize('bdc.error.unauthorized', "You do not have permission to log into this cluster using Windows Authentication"));
}
// Else throw the error as-is
throw controllerErr;
}
}
/**
* Verify that this cluster supports Kerberos authentication. It does this by sending a request to the Token API route
* without any credentials and verifying that it gets a 401 response back with a Negotiate www-authenticate header.
*/
private async verifyKerberosSupported(): Promise<boolean> {
let tokenApi = new TokenRouterApi(this._url);
tokenApi.setDefaultAuthentication(new SslAuth());
try {
await tokenApi.apiV1TokenPost();
console.warn(`Token API returned success without any auth while verifying Kerberos support for BDC Cluster ${this._url}`);
// If we get to here, the route for tokens doesn't require auth which is an unexpected error state
return false;
}
catch (error) {
if (!error.response) {
console.warn(`No response when verifying Kerberos support for BDC Cluster ${this._url} - ${error}`);
return false;
}
if (error.response.statusCode !== 401) {
console.warn(`Got unexpected status code ${error.response.statusCode} when verifying Kerberos support for BDC Cluster ${this._url}`);
return false;
}
const auths = error.response.headers['www-authenticate'] as string[] ?? [];
if (auths.includes('Negotiate')) {
return true;
}
console.warn(`Didn't get expected Negotiate auth type when verifying Kerberos support for BDC Cluster ${this.url}. Supported types : ${auths.join(', ')}`);
return false;
}
}
public async getKnoxUsername(defaultUsername: string): Promise<string> {
// This all is necessary because prior to CU5 BDC deployments all had the same default username for
// accessing the Knox gateway. But in the allowRunAsRoot setting was added and defaulted to false - so
// if that exists and is false then we use the username instead.
// Note that the SQL username may not necessarily be correct here either - but currently this is what
// we're requiring to run Notebooks in a BDC
const config = await this.getClusterConfig();
return config.spec?.spec?.security?.allowRunAsRoot === false ? defaultUsername : DEFAULT_KNOX_USERNAME;
}
public async getClusterConfig(promptConnect: boolean = false): Promise<any> {
return await this.withConnectRetry<any>(
this.getClusterConfigImpl,
promptConnect,
localize('bdc.error.getClusterConfig', "Error retrieving cluster config from {0}", this._url));
}
private async getClusterConfigImpl(self: ClusterController): Promise<any> {
let auth = await self._authPromise;
let endPointApi = new BdcApiWrapper(self._username, self._password, self._url, auth);
let options: any = {};
let result = await endPointApi.getCluster(options);
return {
response: result.response as IHttpResponse,
spec: JSON.parse(result.body.spec)
};
}
public async getEndPoints(promptConnect: boolean = false): Promise<IEndPointsResponse> {
return await this.withConnectRetry<IEndPointsResponse>(
this.getEndpointsImpl,
promptConnect,
localize('bdc.error.getEndPoints', "Error retrieving endpoints from {0}", this._url));
}
private async getEndpointsImpl(self: ClusterController): Promise<IEndPointsResponse> {
let auth = await self._authPromise;
let endPointApi = new BdcApiWrapper(self._username, self._password, self._url, auth);
let options: any = {};
let result = await endPointApi.endpointsGet(options);
return {
response: result.response as IHttpResponse,
endPoints: result.body as EndpointModel[]
};
}
public async getBdcStatus(promptConnect: boolean = false): Promise<IBdcStatusResponse> {
return await this.withConnectRetry<IBdcStatusResponse>(
this.getBdcStatusImpl,
promptConnect,
localize('bdc.error.getBdcStatus', "Error retrieving BDC status from {0}", this._url));
}
private async getBdcStatusImpl(self: ClusterController): Promise<IBdcStatusResponse> {
let auth = await self._authPromise;
const bdcApi = new BdcApiWrapper(self._username, self._password, self._url, auth);
const bdcStatus = await bdcApi.getBdcStatus('', '', /*all*/ true);
return {
response: bdcStatus.response,
bdcStatus: bdcStatus.body
};
}
public async mountHdfs(mountPath: string, remoteUri: string, credentials: {}, promptConnection: boolean = false): Promise<MountResponse> {
return await this.withConnectRetry<MountResponse>(
this.mountHdfsImpl,
promptConnection,
localize('bdc.error.mountHdfs', "Error creating mount"),
mountPath,
remoteUri,
credentials);
}
private async mountHdfsImpl(self: ClusterController, mountPath: string, remoteUri: string, credentials: {}): Promise<MountResponse> {
let auth = await self._authPromise;
const api = new DefaultApiWrapper(self._username, self._password, self._url, auth);
const mountStatus = await api.createMount('', '', remoteUri, mountPath, credentials);
return {
response: mountStatus.response,
status: mountStatus.body
};
}
public async getMountStatus(mountPath?: string, promptConnect: boolean = false): Promise<MountStatusResponse> {
return await this.withConnectRetry<MountStatusResponse>(
this.getMountStatusImpl,
promptConnect,
localize('bdc.error.statusHdfs', "Error getting mount status"),
mountPath);
}
private async getMountStatusImpl(self: ClusterController, mountPath?: string): Promise<MountStatusResponse> {
const auth = await self._authPromise;
const api = new DefaultApiWrapper(self._username, self._password, self._url, auth);
const mountStatus = await api.listMounts('', '', mountPath);
return {
response: mountStatus.response,
mount: mountStatus.body ? JSON.parse(mountStatus.body) : undefined
};
}
public async refreshMount(mountPath: string, promptConnect: boolean = false): Promise<MountResponse> {
return await this.withConnectRetry<MountResponse>(
this.refreshMountImpl,
promptConnect,
localize('bdc.error.refreshHdfs', "Error refreshing mount"),
mountPath);
}
private async refreshMountImpl(self: ClusterController, mountPath: string): Promise<MountResponse> {
const auth = await self._authPromise;
const api = new DefaultApiWrapper(self._username, self._password, self._url, auth);
const mountStatus = await api.refreshMount('', '', mountPath);
return {
response: mountStatus.response,
status: mountStatus.body
};
}
public async deleteMount(mountPath: string, promptConnect: boolean = false): Promise<MountResponse> {
return await this.withConnectRetry<MountResponse>(
this.deleteMountImpl,
promptConnect,
localize('bdc.error.deleteHdfs', "Error deleting mount"),
mountPath);
}
private async deleteMountImpl(self: ClusterController, mountPath: string): Promise<MountResponse> {
let auth = await self._authPromise;
const api = new DefaultApiWrapper(self._username, self._password, self._url, auth);
const mountStatus = await api.deleteMount('', '', mountPath);
return {
response: mountStatus.response,
status: mountStatus.body
};
}
/**
* Helper function that wraps a function call in a try/catch and if promptConnect is true
* will prompt the user to re-enter connection information and if that succeeds updates
* this with the new information.
* @param f The API function we're wrapping
* @param promptConnect Whether to actually prompt for connection on failure
* @param errorMessage The message to include in the wrapped error thrown
* @param args The args to pass to the function
*/
private async withConnectRetry<T>(f: (...args: any[]) => Promise<T>, promptConnect: boolean, errorMessage: string, ...args: any[]): Promise<T> {
try {
try {
return await f(this, ...args);
} catch (error) {
if (promptConnect) {
// We don't want to open multiple dialogs here if multiple calls come in the same time so check
// and see if we have are actively waiting on an open dialog to return and if so then just wait
// on that promise.
if (!this._connectionPromise) {
this._connectionPromise = this._dialog.showDialog();
}
const controller = await this._connectionPromise;
if (controller) {
this._username = controller._username;
this._password = controller._password;
this._url = controller._url;
this._authType = controller._authType;
this._authPromise = controller._authPromise;
}
return await f(this, args);
}
throw error;
}
} catch (error) {
throw new ControllerError(error, errorMessage);
} finally {
this._connectionPromise = undefined;
}
}
}
/**
* Fixes missing protocol and wrong character for port entered by user
*/
function adjustUrl(url: string): string {
if (!url) {
return undefined;
}
url = url.trim().replace(/ /g, '').replace(/,(\d+)$/, ':$1');
if (!url.includes('://')) {
url = `https://${url}`;
}
return url;
}
export interface IClusterRequest {
url: string;
username: string;
password?: string;
method?: string;
}
export interface IBdcStatusResponse {
response: IHttpResponse;
bdcStatus: BdcStatusModel;
}
export enum MountState {
Creating = 'Creating',
Ready = 'Ready',
Error = 'Error'
}
export interface MountInfo {
mount: string;
remote: string;
state: MountState;
error?: string;
}
export interface MountResponse {
response: IHttpResponse;
status: any;
}
export interface MountStatusResponse {
response: IHttpResponse;
mount: MountInfo[];
}
export class ControllerError extends Error {
public code?: number;
public reason?: string;
public address?: string;
public statusMessage?: string;
/**
*
* @param error The original error to wrap
* @param messagePrefix Optional text to prefix the error message with
*/
constructor(error: any, messagePrefix?: string) {
super(messagePrefix);
// Pull out the response information containing details about the failure
if (error.response) {
this.code = error.response.statusCode;
this.message += `${error.response.statusMessage ? ` - ${error.response.statusMessage}` : ''}` || '';
this.address = error.response.url || '';
this.statusMessage = error.response.statusMessage;
}
else if (error.message) {
this.message += ` - ${error.message}`;
}
// The body message contains more specific information about the failure
if (error.body && error.body.reason) {
this.message += ` - ${error.body.reason}`;
}
}
}

View File

@@ -0,0 +1,229 @@
/*---------------------------------------------------------------------------------------------
* 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 { ClusterController, ControllerError } from '../controller/clusterControllerApi';
import { ControllerTreeDataProvider } from '../tree/controllerTreeDataProvider';
import { BdcDashboardOptions } from './bdcDashboardModel';
import { ControllerNode } from '../tree/controllerTreeNode';
import { ManageControllerCommand } from '../../commands';
import * as loc from '../localizedConstants';
import { AuthType } from 'bdc';
function getAuthCategory(name: AuthType): azdata.CategoryValue {
if (name === 'basic') {
return { name: name, displayName: loc.basic };
}
return { name: name, displayName: loc.windowsAuth };
}
export class AddControllerDialogModel {
private _canceled = false;
private _authTypes: azdata.CategoryValue[];
constructor(
public treeDataProvider: ControllerTreeDataProvider,
public node?: ControllerNode,
public prefilledUrl?: string,
public prefilledAuth?: azdata.CategoryValue,
public prefilledUsername?: string,
public prefilledPassword?: string,
public prefilledRememberPassword?: boolean
) {
this.prefilledUrl = prefilledUrl || (node && node['url']);
this.prefilledAuth = prefilledAuth;
if (!prefilledAuth) {
let auth = (node && node['auth']) || 'basic';
this.prefilledAuth = getAuthCategory(auth);
}
this.prefilledUsername = prefilledUsername || (node && node['username']);
this.prefilledPassword = prefilledPassword || (node && node['password']);
this.prefilledRememberPassword = prefilledRememberPassword || (node && node['rememberPassword']);
}
public get authCategories(): azdata.CategoryValue[] {
if (!this._authTypes) {
this._authTypes = [getAuthCategory('basic'), getAuthCategory('integrated')];
}
return this._authTypes;
}
public async onComplete(url: string, auth: AuthType, username: string, password: string, rememberPassword: boolean): Promise<void> {
try {
if (auth === 'basic') {
// Verify username and password as we can't make them required in the UI
if (!username) {
throw new Error(loc.usernameRequired);
} else if (!password) {
throw new Error(loc.passwordRequired);
}
}
// We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect)
let controller = new ClusterController(url, auth, username, password);
let response = await controller.getEndPoints();
if (response && response.endPoints) {
if (this._canceled) {
return;
}
this.treeDataProvider.addOrUpdateController(url, auth, username, password, rememberPassword);
vscode.commands.executeCommand(ManageControllerCommand, <BdcDashboardOptions>{ url: url, auth: auth, username: username, password: password });
await this.treeDataProvider.saveControllers();
}
} catch (error) {
// Ignore the error if we cancelled the request since we can't stop the actual request from completing
if (!this._canceled) {
throw error;
}
}
}
public async onError(error: ControllerError): Promise<void> {
// implement
}
public async onCancel(): Promise<void> {
this._canceled = true;
if (this.node) {
this.node.refresh();
}
}
}
export class AddControllerDialog {
private dialog: azdata.window.Dialog;
private uiModelBuilder: azdata.ModelBuilder;
private urlInputBox: azdata.InputBoxComponent;
private authDropdown: azdata.DropDownComponent;
private usernameInputBox: azdata.InputBoxComponent;
private passwordInputBox: azdata.InputBoxComponent;
private rememberPwCheckBox: azdata.CheckBoxComponent;
constructor(private model: AddControllerDialogModel) {
}
public showDialog(): void {
this.createDialog();
azdata.window.openDialog(this.dialog);
}
private createDialog(): void {
this.dialog = azdata.window.createModelViewDialog(loc.addNewController);
this.dialog.registerContent(async view => {
this.uiModelBuilder = view.modelBuilder;
this.urlInputBox = this.uiModelBuilder.inputBox()
.withProps({
placeHolder: loc.url.toLocaleLowerCase(),
value: this.model.prefilledUrl
}).component();
this.authDropdown = this.uiModelBuilder.dropDown().withProps({
values: this.model.authCategories,
value: this.model.prefilledAuth,
editable: false,
}).component();
this.authDropdown.onValueChanged(e => this.onAuthChanged());
this.usernameInputBox = this.uiModelBuilder.inputBox()
.withProps({
placeHolder: loc.usernameRequired.toLocaleLowerCase(),
value: this.model.prefilledUsername
}).component();
this.passwordInputBox = this.uiModelBuilder.inputBox()
.withProps({
placeHolder: loc.password,
inputType: 'password',
value: this.model.prefilledPassword
})
.component();
this.rememberPwCheckBox = this.uiModelBuilder.checkBox()
.withProps({
label: loc.rememberPassword,
checked: this.model.prefilledRememberPassword
}).component();
let formModel = this.uiModelBuilder.formContainer()
.withFormItems([{
components: [
{
component: this.urlInputBox,
title: loc.clusterUrl,
required: true
}, {
component: this.authDropdown,
title: loc.authType,
required: true
}, {
component: this.usernameInputBox,
title: loc.username,
required: false
}, {
component: this.passwordInputBox,
title: loc.password,
required: false
}, {
component: this.rememberPwCheckBox,
title: ''
}
],
title: ''
}]).withLayout({ width: '100%' }).component();
this.onAuthChanged();
await view.initializeModel(formModel);
this.urlInputBox.focus();
});
this.dialog.registerCloseValidator(async () => await this.validate());
this.dialog.cancelButton.onClick(async () => await this.cancel());
this.dialog.okButton.label = loc.add;
this.dialog.cancelButton.label = loc.cancel;
}
private get authValue(): AuthType {
return (<azdata.CategoryValue>this.authDropdown.value).name as AuthType;
}
private onAuthChanged(): void {
let isBasic = this.authValue === 'basic';
this.usernameInputBox.enabled = isBasic;
this.passwordInputBox.enabled = isBasic;
this.rememberPwCheckBox.enabled = isBasic;
if (!isBasic) {
this.usernameInputBox.value = '';
this.passwordInputBox.value = '';
}
}
private async validate(): Promise<boolean> {
let url = this.urlInputBox && this.urlInputBox.value;
let auth = this.authValue;
let username = this.usernameInputBox && this.usernameInputBox.value;
let password = this.passwordInputBox && this.passwordInputBox.value;
let rememberPassword = this.passwordInputBox && !!this.rememberPwCheckBox.checked;
try {
await this.model.onComplete(url, auth, username, password, rememberPassword);
return true;
} catch (error) {
this.dialog.message = {
text: (typeof error === 'string') ? error : error.message,
level: azdata.window.MessageLevel.Error
};
if (this.model && this.model.onError) {
await this.model.onError(error as ControllerError);
}
return false;
}
}
private async cancel(): Promise<void> {
if (this.model && this.model.onCancel) {
await this.model.onCancel();
}
}
}

View File

@@ -0,0 +1,106 @@
/*---------------------------------------------------------------------------------------------
* 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 { BdcDashboardModel, BdcErrorEvent } from './bdcDashboardModel';
import { BdcServiceStatusPage } from './bdcServiceStatusPage';
import { BdcDashboardOverviewPage } from './bdcDashboardOverviewPage';
import { BdcStatusModel, ServiceStatusModel } from '../controller/apiGenerated';
import { getServiceNameDisplayText, showErrorMessage, getHealthStatusDotIcon } from '../utils';
import { HdfsDialogCancelledError } from './hdfsDialogBase';
import { InitializingComponent } from './intializingComponent';
import * as loc from '../localizedConstants';
export class BdcDashboard extends InitializingComponent {
private dashboard: azdata.window.ModelViewDashboard;
private modelView: azdata.ModelView;
private createdServicePages: Map<string, azdata.DashboardTab> = new Map<string, azdata.DashboardTab>();
private overviewTab: azdata.DashboardTab;
constructor(private title: string, private model: BdcDashboardModel) {
super();
model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus)));
model.onBdcError(errorEvent => this.eventuallyRunOnInitialized(() => this.handleError(errorEvent)));
}
public async showDashboard(): Promise<void> {
await this.createDashboard();
await this.dashboard.open();
}
private async createDashboard(): Promise<void> {
this.dashboard = azdata.window.createModelViewDashboard(this.title, 'BdcDashboard', { alwaysShowTabs: true });
this.dashboard.registerTabs(async (modelView: azdata.ModelView) => {
this.modelView = modelView;
const overviewPage = new BdcDashboardOverviewPage(this.model, modelView, this.dashboard);
this.overviewTab = {
title: loc.bdcOverview,
id: 'overview-tab',
content: overviewPage.container,
toolbar: overviewPage.toolbarContainer
};
return [
this.overviewTab
];
});
this.initialized = true;
// Now that we've created the UI load data from the model in case it already had data
this.handleBdcStatusUpdate(this.model.bdcStatus);
}
private handleBdcStatusUpdate(bdcStatus?: BdcStatusModel): void {
if (!bdcStatus) {
return;
}
this.updateServicePages(bdcStatus.services);
}
private handleError(errorEvent: BdcErrorEvent): void {
if (errorEvent.errorType !== 'general') {
return;
}
// We don't want to show an error for the connection dialog being
// canceled since that's a normal case.
if (!(errorEvent.error instanceof HdfsDialogCancelledError)) {
showErrorMessage(errorEvent.error.message);
}
}
/**
* Update the service tab pages, creating any new ones as necessary
*/
private updateServicePages(services?: ServiceStatusModel[]): void {
if (services) {
// Create a service page for each new service. We currently don't support services being removed.
services.forEach(s => {
const existingPage = this.createdServicePages.get(s.serviceName);
if (existingPage) {
existingPage.icon = getHealthStatusDotIcon(s.healthStatus);
} else {
const serviceStatusPage = new BdcServiceStatusPage(s.serviceName, this.model, this.modelView);
const newTab = <azdata.Tab>{
title: getServiceNameDisplayText(s.serviceName),
id: s.serviceName,
icon: getHealthStatusDotIcon(s.healthStatus),
content: serviceStatusPage.container,
toolbar: serviceStatusPage.toolbarContainer
};
this.createdServicePages.set(s.serviceName, newTab);
}
});
this.dashboard.updateTabs([
this.overviewTab,
{
title: loc.clusterDetails,
tabs: Array.from(this.createdServicePages.values())
}]);
}
}
}

View File

@@ -0,0 +1,182 @@
/*---------------------------------------------------------------------------------------------
* 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 { ClusterController } from '../controller/clusterControllerApi';
import { EndpointModel, BdcStatusModel } from '../controller/apiGenerated';
import { Endpoint, Service } from '../utils';
import { ConnectControllerDialog, ConnectControllerModel } from './connectControllerDialog';
import { ControllerTreeDataProvider } from '../tree/controllerTreeDataProvider';
import { AuthType } from 'bdc';
export type BdcDashboardOptions = { url: string, auth: AuthType, username: string, password: string, rememberPassword: boolean };
type BdcErrorType = 'bdcStatus' | 'bdcEndpoints' | 'general';
export type BdcErrorEvent = { error: Error, errorType: BdcErrorType };
export class BdcDashboardModel {
private _clusterController: ClusterController;
private _bdcStatus: BdcStatusModel | undefined;
private _endpoints: EndpointModel[] | undefined;
private _bdcStatusLastUpdated: Date | undefined;
private _endpointsLastUpdated: Date | undefined;
private readonly _onDidUpdateEndpoints = new vscode.EventEmitter<EndpointModel[]>();
private readonly _onDidUpdateBdcStatus = new vscode.EventEmitter<BdcStatusModel>();
private readonly _onBdcError = new vscode.EventEmitter<BdcErrorEvent>();
public onDidUpdateEndpoints = this._onDidUpdateEndpoints.event;
public onDidUpdateBdcStatus = this._onDidUpdateBdcStatus.event;
public onBdcError = this._onBdcError.event;
constructor(private _options: BdcDashboardOptions, private _treeDataProvider: ControllerTreeDataProvider) {
try {
this._clusterController = new ClusterController(_options.url, _options.auth, _options.username, _options.password);
this.refresh().catch(e => console.log(`Unexpected error refreshing BdcModel ${e instanceof Error ? e.message : e}`));
} catch {
this.promptReconnect().then(async () => {
await this.refresh();
}).catch(error => {
this._onBdcError.fire({ error: error, errorType: 'general' });
});
}
}
public get bdcStatus(): BdcStatusModel | undefined {
return this._bdcStatus;
}
public get serviceEndpoints(): EndpointModel[] | undefined {
return this._endpoints;
}
public get bdcStatusLastUpdated(): Date | undefined {
return this._bdcStatusLastUpdated;
}
public get endpointsLastUpdated(): Date | undefined {
return this._endpointsLastUpdated;
}
public async refresh(): Promise<void> {
try {
if (!this._clusterController) {
// If this succeeds without error we know we have a clusterController at this point
await this.promptReconnect();
}
await Promise.all([
this._clusterController.getBdcStatus(true).then(response => {
this._bdcStatus = response.bdcStatus;
this._bdcStatusLastUpdated = new Date();
this._onDidUpdateBdcStatus.fire(this.bdcStatus);
}).catch(error => this._onBdcError.fire({ error: error, errorType: 'bdcStatus' })),
this._clusterController.getEndPoints(true).then(response => {
this._endpoints = response.endPoints;
fixEndpoints(this._endpoints);
this._endpointsLastUpdated = new Date();
this._onDidUpdateEndpoints.fire(this.serviceEndpoints);
}).catch(error => this._onBdcError.fire({ error: error, errorType: 'bdcEndpoints' }))
]);
} catch (error) {
this._onBdcError.fire({ error: error, errorType: 'general' });
}
}
/**
* Gets a partially filled connection profile for the SQL Server Master Instance endpoint
* associated with this cluster.
* @returns The IConnectionProfile - or undefined if the endpoints haven't been loaded yet
*/
public getSqlServerMasterConnectionProfile(): azdata.IConnectionProfile | undefined {
const sqlServerMasterEndpoint = this.serviceEndpoints && this.serviceEndpoints.find(e => e.name === Endpoint.sqlServerMaster);
if (!sqlServerMasterEndpoint) {
return undefined;
}
// We default to sa - if that doesn't work then callers of this should open up a connection
// dialog so the user can enter in the correct connection information
return {
connectionName: undefined,
serverName: sqlServerMasterEndpoint.endpoint,
databaseName: undefined,
userName: 'sa',
password: this._options.password,
authenticationType: '',
savePassword: true,
groupFullName: undefined,
groupId: undefined,
providerName: 'MSSQL',
saveProfile: true,
id: undefined,
options: {}
};
}
/**
* Opens up a dialog prompting the user to re-enter credentials for the controller
*/
private async promptReconnect(): Promise<void> {
this._clusterController = await new ConnectControllerDialog(new ConnectControllerModel(this._options)).showDialog();
await this.updateController();
}
private async updateController(): Promise<void> {
if (!this._clusterController) {
return;
}
this._treeDataProvider.addOrUpdateController(
this._clusterController.url,
this._clusterController.authType,
this._clusterController.username,
this._clusterController.password,
this._options.rememberPassword);
await this._treeDataProvider.saveControllers();
}
}
/**
* Retrieves the troubleshoot book URL for the specified service, defaulting to the BDC
* troubleshoot notebook if the service name is unknown.
* @param service The service name to get the troubleshoot notebook URL for
*/
export function getTroubleshootNotebookUrl(service?: string): string {
service = service || '';
switch (service.toLowerCase()) {
case Service.sql:
return 'troubleshooters/tsg101-troubleshoot-sql-server';
case Service.hdfs:
return 'troubleshooters/tsg102-troubleshoot-hdfs';
case Service.spark:
return 'troubleshooters/tsg103-troubleshoot-spark';
case Service.control:
return 'troubleshooters/tsg104-troubleshoot-control';
case Service.gateway:
return 'troubleshooters/tsg105-troubleshoot-gateway';
case Service.app:
return 'troubleshooters/tsg106-troubleshoot-app';
}
return 'troubleshooters/tsg100-troubleshoot-bdc';
}
/**
* Applies fixes to the endpoints received so they are displayed correctly
* @param endpoints The endpoints received to modify
*/
function fixEndpoints(endpoints?: EndpointModel[]): void {
if (!endpoints) {
return;
}
endpoints.forEach(e => {
if (e.name === Endpoint.metricsui && e.endpoint && e.endpoint.indexOf('/d/wZx3OUdmz') === -1) {
// Update to have correct URL
e.endpoint += '/d/wZx3OUdmz';
}
if (e.name === Endpoint.logsui && e.endpoint && e.endpoint.indexOf('/app/kibana#/discover') === -1) {
// Update to have correct URL
e.endpoint += '/app/kibana#/discover';
}
});
}

View File

@@ -0,0 +1,468 @@
/*---------------------------------------------------------------------------------------------
* 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 { BdcDashboardModel, BdcErrorEvent } from './bdcDashboardModel';
import { IconPathHelper, cssStyles } from '../constants';
import { getStateDisplayText, getHealthStatusDisplayText, getEndpointDisplayText, getHealthStatusIcon, getServiceNameDisplayText, Endpoint, getBdcStatusErrorMessage } from '../utils';
import { EndpointModel, BdcStatusModel } from '../controller/apiGenerated';
import { createViewDetailsButton } from './commonControls';
import { HdfsDialogCancelledError } from './hdfsDialogBase';
import { BdcDashboardPage } from './bdcDashboardPage';
import * as loc from '../localizedConstants';
const hyperlinkedEndpoints = [Endpoint.metricsui, Endpoint.logsui, Endpoint.sparkHistory, Endpoint.yarnUi];
export class BdcDashboardOverviewPage extends BdcDashboardPage {
private rootContainer: azdata.FlexContainer;
private lastUpdatedLabel: azdata.TextComponent;
private propertiesContainerLoadingComponent: azdata.LoadingComponent;
private serviceStatusTable: azdata.DeclarativeTableComponent;
private endpointsTable: azdata.DeclarativeTableComponent;
private endpointsLoadingComponent: azdata.LoadingComponent;
private endpointsDisplayContainer: azdata.FlexContainer;
private serviceStatusLoadingComponent: azdata.LoadingComponent;
private serviceStatusDisplayContainer: azdata.FlexContainer;
private propertiesErrorMessage: azdata.TextComponent;
private endpointsErrorMessage: azdata.TextComponent;
private serviceStatusErrorMessage: azdata.TextComponent;
constructor(model: BdcDashboardModel, modelView: azdata.ModelView, private dashboard: azdata.window.ModelViewDashboard) {
super(model, modelView);
this.model.onDidUpdateEndpoints(endpoints => this.eventuallyRunOnInitialized(() => this.handleEndpointsUpdate(endpoints)));
this.model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus)));
this.model.onBdcError(error => this.eventuallyRunOnInitialized(() => this.handleBdcError(error)));
}
public get container(): azdata.FlexContainer {
// Lazily create the container only when needed
if (!this.rootContainer) {
this.rootContainer = this.createContainer();
}
return this.rootContainer;
}
public createContainer(): azdata.FlexContainer {
const rootContainer = this.modelView.modelBuilder.flexContainer().withLayout(
{
flexFlow: 'column',
width: '100%',
height: '100%'
}).component();
// ##############
// # PROPERTIES #
// ##############
const propertiesLabel = this.modelView.modelBuilder.text()
.withProps({ value: loc.clusterProperties, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '10px' } })
.component();
rootContainer.addItem(propertiesLabel, { CSSStyles: { 'margin-top': '15px', 'padding-left': '10px', ...cssStyles.title } });
const propertiesContainer = this.modelView.modelBuilder.propertiesContainer().component();
this.propertiesContainerLoadingComponent = this.modelView.modelBuilder.loadingComponent().withItem(propertiesContainer).component();
rootContainer.addItem(this.propertiesContainerLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'padding-left': '10px' } });
// ############
// # OVERVIEW #
// ############
const overviewHeaderContainer = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', height: '20px' }).component();
rootContainer.addItem(overviewHeaderContainer, { CSSStyles: { 'padding-left': '10px', 'padding-top': '15px' } });
const overviewLabel = this.modelView.modelBuilder.text()
.withProps({
value: loc.clusterOverview,
CSSStyles: { ...cssStyles.text }
})
.component();
overviewHeaderContainer.addItem(overviewLabel, { CSSStyles: { ...cssStyles.title } });
this.lastUpdatedLabel = this.modelView.modelBuilder.text()
.withProps({
value: loc.lastUpdated(),
CSSStyles: { ...cssStyles.lastUpdatedText }
}).component();
overviewHeaderContainer.addItem(this.lastUpdatedLabel, { CSSStyles: { 'margin-left': '45px' } });
const overviewContainer = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '100%', height: '100%' }).component();
this.serviceStatusTable = this.modelView.modelBuilder.declarativeTable()
.withProps(
{
columns: [
{ // status icon
displayName: '',
ariaLabel: loc.statusIcon,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: 25,
headerCssStyles: {
'border': 'none'
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
},
},
{ // service
displayName: loc.serviceName,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: 175,
headerCssStyles: {
'border': 'none',
...cssStyles.tableHeader
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
},
},
{ // state
displayName: loc.state,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: 150,
headerCssStyles: {
'border': 'none',
...cssStyles.tableHeader
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
},
},
{ // health status
displayName: loc.healthStatus,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: 100,
headerCssStyles: {
'border': 'none',
'text-align': 'left',
...cssStyles.tableHeader
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
}
},
{ // view details button
displayName: '',
ariaLabel: loc.viewErrorDetails,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: 150,
headerCssStyles: {
'border': 'none',
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
},
},
],
data: [],
ariaLabel: loc.clusterOverview
})
.component();
this.serviceStatusDisplayContainer = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
this.serviceStatusDisplayContainer.addItem(this.serviceStatusTable);
// Note we don't make the table a child of the loading component since making the loading component align correctly
// messes up the layout for the table that we display after loading is finished. Instead we'll just remove the loading
// component once it's finished loading the content
this.serviceStatusLoadingComponent = this.modelView.modelBuilder.loadingComponent()
.withProps({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } })
.component();
this.serviceStatusDisplayContainer.addItem(this.serviceStatusLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'padding-left': '150px', width: '30px' } });
this.serviceStatusErrorMessage = this.modelView.modelBuilder.text().withProps({ display: 'none', CSSStyles: { ...cssStyles.errorText } }).component();
overviewContainer.addItem(this.serviceStatusErrorMessage);
overviewContainer.addItem(this.serviceStatusDisplayContainer);
rootContainer.addItem(overviewContainer, { flex: '0 0 auto' });
// #####################
// # SERVICE ENDPOINTS #
// #####################
const endpointsLabel = this.modelView.modelBuilder.text()
.withProps({ value: loc.serviceEndpoints, CSSStyles: { 'margin-block-start': '20px', 'margin-block-end': '0px' } })
.component();
rootContainer.addItem(endpointsLabel, { CSSStyles: { 'padding-left': '10px', ...cssStyles.title } });
this.endpointsErrorMessage = this.modelView.modelBuilder.text().withProps({ display: 'none', CSSStyles: { ...cssStyles.errorText } }).component();
const endpointsContainer = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '100%', height: '100%' }).component();
this.endpointsTable = this.modelView.modelBuilder.declarativeTable()
.withProps(
{
columns: [
{ // service
displayName: loc.service,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: 200,
headerCssStyles: {
'border': 'none',
...cssStyles.tableHeader
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
},
},
{ // endpoint
displayName: loc.endpoint,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: 350,
headerCssStyles: {
'border': 'none',
...cssStyles.tableHeader
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none',
'overflow': 'hidden',
'text-overflow': 'ellipsis'
},
},
{ // copy
displayName: '',
ariaLabel: loc.copy,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: 50,
headerCssStyles: {
'border': 'none',
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
}
}
],
data: [],
ariaLabel: loc.serviceEndpoints
}).component();
this.endpointsDisplayContainer = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
this.endpointsDisplayContainer.addItem(this.endpointsTable);
// Note we don't make the table a child of the loading component since making the loading component align correctly
// messes up the layout for the table that we display after loading is finished. Instead we'll just remove the loading
// component once it's finished loading the content
this.endpointsLoadingComponent = this.modelView.modelBuilder.loadingComponent()
.withProps({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } })
.component();
this.endpointsDisplayContainer.addItem(this.endpointsLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'padding-left': '150px', width: '30px' } });
endpointsContainer.addItem(this.endpointsErrorMessage);
endpointsContainer.addItem(this.endpointsDisplayContainer);
rootContainer.addItem(endpointsContainer, { flex: '0 0 auto' });
this.initialized = true;
// Now that we've created the UI load data from the model in case it already had data
this.handleEndpointsUpdate(this.model.serviceEndpoints);
this.handleBdcStatusUpdate(this.model.bdcStatus);
return rootContainer;
}
public onRefreshStarted(): void {
this.propertiesErrorMessage.display = 'none';
this.serviceStatusErrorMessage.display = 'none';
this.endpointsErrorMessage.display = 'none';
this.serviceStatusDisplayContainer.display = undefined;
this.propertiesContainerLoadingComponent.display = undefined;
this.endpointsDisplayContainer.display = undefined;
}
private handleBdcStatusUpdate(bdcStatus?: BdcStatusModel): void {
if (!bdcStatus) {
return;
}
this.lastUpdatedLabel.value = loc.lastUpdated(this.model.bdcStatusLastUpdated);
this.propertiesContainerLoadingComponent.loading = false;
(<azdata.PropertiesContainerComponentProperties>this.propertiesContainerLoadingComponent.component).propertyItems = [
{ displayName: loc.clusterState, value: getStateDisplayText(bdcStatus.state) },
{ displayName: loc.healthStatus, value: getHealthStatusDisplayText(bdcStatus.healthStatus) }
];
if (bdcStatus.services) {
this.serviceStatusTable.data = bdcStatus.services.map(serviceStatus => {
const statusIconCell = this.modelView.modelBuilder.text()
.withProps({
value: getHealthStatusIcon(serviceStatus.healthStatus),
ariaRole: 'img',
title: getHealthStatusDisplayText(serviceStatus.healthStatus),
CSSStyles: { 'user-select': 'none', ...cssStyles.text }
}).component();
const nameCell = this.modelView.modelBuilder.hyperlink()
.withProps({
label: getServiceNameDisplayText(serviceStatus.serviceName),
url: '',
CSSStyles: { ...cssStyles.text }
}).component();
nameCell.onDidClick(() => {
this.dashboard.selectTab(serviceStatus.serviceName);
});
const viewDetailsButton = serviceStatus.healthStatus !== 'healthy' && serviceStatus.details && serviceStatus.details.length > 0 ? createViewDetailsButton(this.modelView.modelBuilder, serviceStatus.details) : undefined;
return [
statusIconCell,
nameCell,
getStateDisplayText(serviceStatus.state),
getHealthStatusDisplayText(serviceStatus.healthStatus),
viewDetailsButton];
});
this.serviceStatusDisplayContainer.removeItem(this.serviceStatusLoadingComponent);
}
}
private handleEndpointsUpdate(endpoints?: EndpointModel[]): void {
if (!endpoints) {
return;
}
// Sort the endpoints. The sort method is that SQL Server Master is first - followed by all
// others in alphabetical order by endpoint
const sqlServerMasterEndpoints = endpoints.filter(e => e.name === Endpoint.sqlServerMaster);
endpoints = endpoints.filter(e => e.name !== Endpoint.sqlServerMaster)
.sort((e1, e2) => {
if (e1.endpoint < e2.endpoint) { return -1; }
if (e1.endpoint > e2.endpoint) { return 1; }
return 0;
});
endpoints.unshift(...sqlServerMasterEndpoints);
this.endpointsTable.dataValues = endpoints.map(e => {
const copyValueCell = this.modelView.modelBuilder.button().withProps({ title: loc.copy }).component();
copyValueCell.iconPath = IconPathHelper.copy;
copyValueCell.onDidClick(() => {
vscode.env.clipboard.writeText(e.endpoint);
vscode.window.showInformationMessage(loc.copiedEndpoint(getEndpointDisplayText(e.name, e.description)));
});
return [{ value: getEndpointDisplayText(e.name, e.description) },
{ value: createEndpointComponent(this.modelView.modelBuilder, e, this.model, hyperlinkedEndpoints.some(he => he === e.name)) },
{ value: copyValueCell }];
});
this.endpointsDisplayContainer.removeItem(this.endpointsLoadingComponent);
}
private handleBdcError(errorEvent: BdcErrorEvent): void {
if (errorEvent.errorType === 'bdcEndpoints') {
const errorMessage = loc.endpointsError(errorEvent.error.message);
this.showEndpointsError(errorMessage);
} else if (errorEvent.errorType === 'bdcStatus') {
this.showBdcStatusError(getBdcStatusErrorMessage(errorEvent.error));
} else {
this.handleGeneralError(errorEvent.error);
}
}
private showBdcStatusError(errorMessage: string): void {
this.serviceStatusDisplayContainer.display = 'none';
this.propertiesContainerLoadingComponent.display = 'none';
this.serviceStatusErrorMessage.value = errorMessage;
this.serviceStatusErrorMessage.display = undefined;
this.propertiesErrorMessage.value = errorMessage;
this.propertiesErrorMessage.display = undefined;
}
private showEndpointsError(errorMessage: string): void {
this.endpointsDisplayContainer.display = 'none';
this.endpointsErrorMessage.display = undefined;
this.endpointsErrorMessage.value = errorMessage;
}
private handleGeneralError(error: Error): void {
if (error instanceof HdfsDialogCancelledError) {
const errorMessage = loc.noConnectionError;
this.showBdcStatusError(errorMessage);
this.showEndpointsError(errorMessage);
} else {
const errorMessage = loc.unexpectedError(error);
this.showBdcStatusError(errorMessage);
this.showEndpointsError(errorMessage);
}
}
}
function createEndpointComponent(modelBuilder: azdata.ModelBuilder, endpoint: EndpointModel, bdcModel: BdcDashboardModel, isHyperlink: boolean): azdata.HyperlinkComponent | azdata.TextComponent {
if (isHyperlink) {
return modelBuilder.hyperlink()
.withProps({
label: endpoint.endpoint,
title: endpoint.endpoint,
url: endpoint.endpoint
})
.component();
}
else if (endpoint.name === Endpoint.sqlServerMaster) {
const endpointCell = modelBuilder.hyperlink()
.withProps({
title: endpoint.endpoint,
label: endpoint.endpoint,
url: '',
CSSStyles: { ...cssStyles.text }
}).component();
endpointCell.onDidClick(async () => {
const connProfile = bdcModel.getSqlServerMasterConnectionProfile();
const result = await azdata.connection.connect(connProfile, true, true);
if (!result.connected) {
if (result.errorMessage && result.errorMessage.length > 0) {
vscode.window.showErrorMessage(result.errorMessage);
}
// Clear out the password and username before connecting since those being wrong are likely the issue
connProfile.userName = undefined;
connProfile.password = undefined;
azdata.connection.openConnectionDialog(undefined, connProfile);
}
});
return endpointCell;
}
else {
return modelBuilder.text()
.withProps({
value: endpoint.endpoint,
title: endpoint.endpoint,
CSSStyles: { ...cssStyles.text }
})
.component();
}
}

View File

@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IconPathHelper } from '../constants';
import { BdcDashboardModel, getTroubleshootNotebookUrl } from './bdcDashboardModel';
import * as loc from '../localizedConstants';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { InitializingComponent } from './intializingComponent';
export abstract class BdcDashboardPage extends InitializingComponent {
private _toolbarContainer: azdata.ToolbarContainer;
private _refreshButton: azdata.ButtonComponent;
constructor(protected model: BdcDashboardModel, protected modelView: azdata.ModelView, protected serviceName?: string) {
super();
}
public get toolbarContainer(): azdata.ToolbarContainer {
// Lazily create the container only when needed
if (!this._toolbarContainer) {
this._toolbarContainer = this.createToolbarContainer();
}
return this._toolbarContainer;
}
protected createToolbarContainer(): azdata.ToolbarContainer {
// Refresh button
this._refreshButton = this.modelView.modelBuilder.button()
.withProps({
label: loc.refresh,
iconPath: IconPathHelper.refresh
}).component();
this._refreshButton.onDidClick(async () => {
await this.doRefresh();
});
const openTroubleshootNotebookButton = this.modelView.modelBuilder.button()
.withProps({
label: loc.troubleshoot,
iconPath: IconPathHelper.notebook
}).component();
openTroubleshootNotebookButton.onDidClick(() => {
vscode.commands.executeCommand('books.sqlserver2019', getTroubleshootNotebookUrl(this.serviceName));
});
return this.modelView.modelBuilder.toolbarContainer()
.withToolbarItems(
[
{ component: this._refreshButton },
{ component: openTroubleshootNotebookButton }
]
).component();
}
private async doRefresh(): Promise<void> {
try {
this._refreshButton.enabled = false;
await this.model.refresh();
} finally {
this._refreshButton.enabled = true;
}
}
}

View File

@@ -0,0 +1,358 @@
/*---------------------------------------------------------------------------------------------
* 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 { BdcDashboardModel } from './bdcDashboardModel';
import { BdcStatusModel, InstanceStatusModel, ResourceStatusModel } from '../controller/apiGenerated';
import { getHealthStatusDisplayText, getHealthStatusIcon, getStateDisplayText, Service } from '../utils';
import { cssStyles } from '../constants';
import { isNullOrUndefined } from 'util';
import { createViewDetailsButton } from './commonControls';
import { BdcDashboardPage } from './bdcDashboardPage';
import * as loc from '../localizedConstants';
export class BdcDashboardResourceStatusPage extends BdcDashboardPage {
private resourceStatusModel: ResourceStatusModel;
private rootContainer: azdata.FlexContainer;
private instanceHealthStatusTable: azdata.DeclarativeTableComponent;
private metricsAndLogsRowsTable: azdata.DeclarativeTableComponent;
private lastUpdatedLabel: azdata.TextComponent;
constructor(model: BdcDashboardModel, modelView: azdata.ModelView, serviceName: string, private resourceName: string) {
super(model, modelView, serviceName);
this.model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus)));
}
public get container(): azdata.FlexContainer {
// Lazily create the container only when needed
if (!this.rootContainer) {
// We do this here so that we can have the resource model to use for populating the data
// in the tables. This is to get around a timing issue with ModelView tables
this.updateResourceStatusModel(this.model.bdcStatus);
this.createContainer();
}
return this.rootContainer;
}
private createContainer(): void {
this.rootContainer = this.modelView.modelBuilder.flexContainer().withLayout(
{
flexFlow: 'column',
width: '100%',
height: '100%'
}).component();
// ##############################
// # INSTANCE HEALTH AND STATUS #
// ##############################
const healthStatusHeaderContainer = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', height: '20px' }).component();
this.rootContainer.addItem(healthStatusHeaderContainer, { CSSStyles: { 'padding-left': '10px', 'padding-top': '15px' } });
// Header label
const healthStatusHeaderLabel = this.modelView.modelBuilder.text()
.withProps({
value: loc.healthStatusDetails,
CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '10px' }
})
.component();
healthStatusHeaderContainer.addItem(healthStatusHeaderLabel, { CSSStyles: { ...cssStyles.title } });
// Last updated label
this.lastUpdatedLabel = this.modelView.modelBuilder.text()
.withProps({
value: loc.lastUpdated(this.model.bdcStatusLastUpdated),
CSSStyles: { ...cssStyles.lastUpdatedText }
}).component();
healthStatusHeaderContainer.addItem(this.lastUpdatedLabel, { CSSStyles: { 'margin-left': '45px' } });
this.instanceHealthStatusTable = this.modelView.modelBuilder.declarativeTable()
.withProps(
{
columns: [
{ // status icon
displayName: '',
ariaLabel: loc.statusIcon,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: 25,
headerCssStyles: {
'border': 'none'
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
},
},
{ // instance
displayName: loc.instance,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: 100,
headerCssStyles: {
'border': 'none',
...cssStyles.tableHeader
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
},
},
{ // state
displayName: loc.state,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: 150,
headerCssStyles: {
'border': 'none',
...cssStyles.tableHeader
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
},
},
{ // health status
displayName: loc.healthStatus,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: 100,
headerCssStyles: {
'border': 'none',
'text-align': 'left',
...cssStyles.tableHeader
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
}
},
{ // view details button
displayName: '',
ariaLabel: loc.viewErrorDetails,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: 150,
headerCssStyles: {
'border': 'none'
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
},
},
],
data: this.createHealthStatusRows(),
ariaLabel: loc.healthStatusDetails
}).component();
this.rootContainer.addItem(this.instanceHealthStatusTable, { flex: '0 0 auto' });
// ####################
// # METRICS AND LOGS #
// ####################
// Title label
const endpointsLabel = this.modelView.modelBuilder.text()
.withProps({ value: loc.metricsAndLogs, CSSStyles: { 'margin-block-start': '20px', 'margin-block-end': '0px' } })
.component();
this.rootContainer.addItem(endpointsLabel, { CSSStyles: { 'padding-left': '10px', ...cssStyles.title } });
let metricsAndLogsColumns: azdata.DeclarativeTableColumn[] =
[
{ // instance
displayName: loc.instance,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: 125,
headerCssStyles: {
'border': 'none',
...cssStyles.tableHeader
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
}
},
{ // node metrics
displayName: loc.nodeMetrics,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: 100,
headerCssStyles: {
'border': 'none',
...cssStyles.tableHeader
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
}
}
];
// Only show SQL metrics column for SQL resource instances
if (this.serviceName.toLowerCase() === Service.sql) {
metricsAndLogsColumns.push(
{ // sql metrics
displayName: loc.sqlMetrics,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: 100,
headerCssStyles: {
'border': 'none',
'text-align': 'left',
...cssStyles.tableHeader
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
}
});
}
metricsAndLogsColumns.push(
{ // logs
displayName: loc.logs,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: 100,
headerCssStyles: {
'border': 'none',
'text-align': 'left',
...cssStyles.tableHeader
},
rowCssStyles: {
'border-top': 'solid 1px #ccc',
'border-bottom': 'solid 1px #ccc',
'border-left': 'none',
'border-right': 'none'
}
});
this.metricsAndLogsRowsTable = this.modelView.modelBuilder.declarativeTable()
.withProps(
{
columns: metricsAndLogsColumns,
data: this.createMetricsAndLogsRows(),
ariaLabel: loc.metricsAndLogs
}).component();
this.rootContainer.addItem(this.metricsAndLogsRowsTable, { flex: '0 0 auto' });
this.initialized = true;
}
private updateResourceStatusModel(bdcStatus?: BdcStatusModel): void {
// If we can't find the resource model for this resource then just
// default to keeping what we had originally
if (!bdcStatus) {
return;
}
const service = bdcStatus.services ? bdcStatus.services.find(s => s.serviceName === this.serviceName) : undefined;
this.resourceStatusModel = service ? service.resources.find(r => r.resourceName === this.resourceName) : this.resourceStatusModel;
}
private handleBdcStatusUpdate(bdcStatus?: BdcStatusModel): void {
this.updateResourceStatusModel(bdcStatus);
if (!this.resourceStatusModel || isNullOrUndefined(this.resourceStatusModel.instances)) {
return;
}
this.lastUpdatedLabel.value = loc.lastUpdated(this.model.bdcStatusLastUpdated);
this.instanceHealthStatusTable.data = this.createHealthStatusRows();
this.metricsAndLogsRowsTable.data = this.createMetricsAndLogsRows();
}
private createMetricsAndLogsRows(): any[][] {
return this.resourceStatusModel ? this.resourceStatusModel.instances.map(instanceStatus => this.createMetricsAndLogsRow(instanceStatus)) : [];
}
private createHealthStatusRows(): any[][] {
return this.resourceStatusModel ? this.resourceStatusModel.instances.map(instanceStatus => this.createHealthStatusRow(instanceStatus)) : [];
}
private createMetricsAndLogsRow(instanceStatus: InstanceStatusModel): any[] {
const row: any[] = [instanceStatus.instanceName];
// Not all instances have all logs available - in that case just display N/A instead of a link
if (isNullOrUndefined(instanceStatus.dashboards) || isNullOrUndefined(instanceStatus.dashboards.nodeMetricsUrl)) {
row.push(this.modelView.modelBuilder.text().withProps({ value: loc.notAvailable, CSSStyles: { ...cssStyles.text } }).component());
} else {
row.push(this.modelView.modelBuilder.hyperlink().withProps({
label: loc.view,
url: instanceStatus.dashboards.nodeMetricsUrl,
title: instanceStatus.dashboards.nodeMetricsUrl,
ariaLabel: loc.viewNodeMetrics(instanceStatus.dashboards.nodeMetricsUrl),
CSSStyles: { ...cssStyles.text }
}).component());
}
// Only show SQL metrics column for SQL resource instances
if (this.serviceName === Service.sql) {
// Not all instances have all logs available - in that case just display N/A instead of a link
if (isNullOrUndefined(instanceStatus.dashboards) || isNullOrUndefined(instanceStatus.dashboards.sqlMetricsUrl)) {
row.push(this.modelView.modelBuilder.text().withProps({ value: loc.notAvailable, CSSStyles: { ...cssStyles.text } }).component());
} else {
row.push(this.modelView.modelBuilder.hyperlink().withProps({
label: loc.view,
url: instanceStatus.dashboards.sqlMetricsUrl,
title: instanceStatus.dashboards.sqlMetricsUrl,
ariaLabel: loc.viewSqlMetrics(instanceStatus.dashboards.sqlMetricsUrl),
CSSStyles: { ...cssStyles.text }
}).component());
}
}
if (isNullOrUndefined(instanceStatus.dashboards) || isNullOrUndefined(instanceStatus.dashboards.logsUrl)) {
row.push(this.modelView.modelBuilder.text().withProps({ value: loc.notAvailable, CSSStyles: { ...cssStyles.text } }).component());
} else {
row.push(this.modelView.modelBuilder.hyperlink().withProps({
label: loc.view,
url: instanceStatus.dashboards.logsUrl,
title: instanceStatus.dashboards.logsUrl,
ariaLabel: loc.viewLogs(instanceStatus.dashboards.logsUrl),
CSSStyles: { ...cssStyles.text }
}).component());
}
return row;
}
private createHealthStatusRow(instanceStatus: InstanceStatusModel): any[] {
const statusIconCell = this.modelView.modelBuilder.text()
.withProps({
value: getHealthStatusIcon(instanceStatus.healthStatus),
ariaRole: 'img',
title: getHealthStatusDisplayText(instanceStatus.healthStatus),
CSSStyles: { 'user-select': 'none', ...cssStyles.text }
}).component();
const viewDetailsButton = instanceStatus.healthStatus !== 'healthy' && instanceStatus.details && instanceStatus.details.length > 0 ? createViewDetailsButton(this.modelView.modelBuilder, instanceStatus.details) : undefined;
return [
statusIconCell,
instanceStatus.instanceName,
getStateDisplayText(instanceStatus.state),
getHealthStatusDisplayText(instanceStatus.healthStatus),
viewDetailsButton];
}
}

View File

@@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* 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 { BdcStatusModel, ResourceStatusModel } from '../controller/apiGenerated';
import { BdcDashboardResourceStatusPage } from './bdcDashboardResourceStatusPage';
import { BdcDashboardModel } from './bdcDashboardModel';
import { BdcDashboardPage } from './bdcDashboardPage';
import { getHealthStatusDotIcon } from '../utils';
export class BdcServiceStatusPage extends BdcDashboardPage {
private createdResourceTabs: Map<string, azdata.Tab> = new Map<string, azdata.Tab>();
private tabbedPanel: azdata.TabbedPanelComponent;
constructor(serviceName: string, model: BdcDashboardModel, modelView: azdata.ModelView) {
super(model, modelView, serviceName);
this.model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus)));
}
public get container(): azdata.TabbedPanelComponent {
// Lazily create the container only when needed
if (!this.tabbedPanel) {
this.createPage();
}
return this.tabbedPanel;
}
private createPage(): void {
this.tabbedPanel = this.modelView.modelBuilder.tabbedPanel()
.withLayout({ showIcon: true, alwaysShowTabs: true }).component();
// Initialize our set of tab pages
this.handleBdcStatusUpdate(this.model.bdcStatus);
this.initialized = true;
}
private handleBdcStatusUpdate(bdcStatus: BdcStatusModel): void {
if (!bdcStatus) {
return;
}
const service = bdcStatus.services.find(s => s.serviceName === this.serviceName);
if (service && service.resources) {
this.updateResourcePages(service.resources);
}
}
/**
* Update the resource tab pages, creating any new ones as necessary
*/
private updateResourcePages(resources: ResourceStatusModel[]): void {
resources.forEach(resource => {
const existingTab = this.createdResourceTabs.get(resource.resourceName);
if (existingTab) {
existingTab.icon = getHealthStatusDotIcon(resource.healthStatus);
} else {
const resourceStatusPage = new BdcDashboardResourceStatusPage(this.model, this.modelView, this.serviceName, resource.resourceName);
const newTab: azdata.Tab = {
title: resource.resourceName,
id: resource.resourceName,
content: resourceStatusPage.container,
icon: getHealthStatusDotIcon(resource.healthStatus)
};
this.createdResourceTabs.set(resource.resourceName, newTab);
}
});
this.tabbedPanel.updateTabs(Array.from(this.createdResourceTabs.values()));
}
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* 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 loc from '../localizedConstants';
export function createViewDetailsButton(modelBuilder: azdata.ModelBuilder, text: string): azdata.ButtonComponent {
const viewDetailsButton = modelBuilder.button().withProps({
label: loc.viewDetails,
ariaLabel: loc.viewErrorDetails,
secondary: true
}).component();
viewDetailsButton.onDidClick(() => {
vscode.window.showErrorMessage(text, { modal: true });
});
return viewDetailsButton;
}

View File

@@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* 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 { HdfsDialogBase, HdfsDialogModelBase, HdfsDialogProperties } from './hdfsDialogBase';
import { ClusterController } from '../controller/clusterControllerApi';
import * as loc from '../localizedConstants';
export class ConnectControllerDialog extends HdfsDialogBase<HdfsDialogProperties, ClusterController> {
constructor(model: ConnectControllerModel) {
super(loc.connectToController, model);
}
protected getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[] {
return [];
}
protected async validate(): Promise<{ validated: boolean, value?: ClusterController }> {
try {
const controller = await this.model.onComplete({
url: this.urlInputBox && this.urlInputBox.value,
auth: this.authValue,
username: this.usernameInputBox && this.usernameInputBox.value,
password: this.passwordInputBox && this.passwordInputBox.value
});
return { validated: true, value: controller };
} catch (error) {
await this.reportError(error);
return { validated: false, value: undefined };
}
}
}
export class ConnectControllerModel extends HdfsDialogModelBase<HdfsDialogProperties, ClusterController> {
constructor(props: HdfsDialogProperties) {
super(props);
}
protected async handleCompleted(): Promise<ClusterController> {
this.throwIfMissingUsernamePassword();
// We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect)
return await this.createAndVerifyControllerConnection();
}
}

View File

@@ -0,0 +1,240 @@
/*---------------------------------------------------------------------------------------------
* 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 { ClusterController, ControllerError } from '../controller/clusterControllerApi';
import { Deferred } from '../../common/promise';
import * as loc from '../localizedConstants';
import { AuthType, IEndPointsResponse } from 'bdc';
function getAuthCategory(name: AuthType): azdata.CategoryValue {
if (name === 'basic') {
return { name: name, displayName: loc.basic };
}
return { name: name, displayName: loc.windowsAuth };
}
export interface HdfsDialogProperties {
url?: string;
auth?: AuthType;
username?: string;
password?: string;
}
export class HdfsDialogCancelledError extends Error {
constructor(message: string = 'Dialog cancelled') {
super(message);
}
}
export abstract class HdfsDialogModelBase<T extends HdfsDialogProperties, R> {
protected _canceled = false;
private _authTypes: azdata.CategoryValue[];
constructor(
public props: T
) {
if (!props.auth) {
this.props.auth = 'basic';
}
}
public get authCategories(): azdata.CategoryValue[] {
if (!this._authTypes) {
this._authTypes = [getAuthCategory('basic'), getAuthCategory('integrated')];
}
return this._authTypes;
}
public get authCategory(): azdata.CategoryValue {
return getAuthCategory(this.props.auth);
}
public async onComplete(props: T): Promise<R | undefined> {
try {
this.props = props;
return await this.handleCompleted();
} catch (error) {
// Ignore the error if we cancelled the request since we can't stop the actual request from completing
if (!this._canceled) {
throw error;
}
return undefined;
}
}
protected abstract handleCompleted(): Promise<R>;
public async onError(error: ControllerError): Promise<void> {
// implement
}
public async onCancel(): Promise<void> {
this._canceled = true;
}
protected createController(): ClusterController {
return new ClusterController(this.props.url, this.props.auth, this.props.username, this.props.password);
}
protected async createAndVerifyControllerConnection(): Promise<ClusterController> {
// We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect)
let controller = this.createController();
let response: IEndPointsResponse;
try {
response = await controller.getEndPoints();
if (!response || !response.endPoints) {
throw new Error(loc.loginFailed);
}
} catch (err) {
throw new Error(loc.loginFailedWithError(err));
}
return controller;
}
protected throwIfMissingUsernamePassword(): void {
if (this.props.auth === 'basic') {
// Verify username and password as we can't make them required in the UI
if (!this.props.username) {
throw new Error(loc.usernameRequired);
} else if (!this.props.password) {
throw new Error(loc.passwordRequired);
}
}
}
}
export abstract class HdfsDialogBase<T extends HdfsDialogProperties, R> {
protected dialog: azdata.window.Dialog;
protected uiModelBuilder!: azdata.ModelBuilder;
protected urlInputBox!: azdata.InputBoxComponent;
protected authDropdown!: azdata.DropDownComponent;
protected usernameInputBox!: azdata.InputBoxComponent;
protected passwordInputBox!: azdata.InputBoxComponent;
private returnPromise: Deferred<R>;
constructor(private title: string, protected model: HdfsDialogModelBase<T, R>) {
}
public async showDialog(): Promise<R> {
this.returnPromise = new Deferred<R>();
this.createDialog();
azdata.window.openDialog(this.dialog);
return this.returnPromise.promise;
}
private createDialog(): void {
this.dialog = azdata.window.createModelViewDialog(this.title);
this.dialog.registerContent(async view => {
this.uiModelBuilder = view.modelBuilder;
this.urlInputBox = this.uiModelBuilder.inputBox()
.withProps({
placeHolder: loc.url.toLocaleLowerCase(),
value: this.model.props.url,
enabled: false
}).component();
this.authDropdown = this.uiModelBuilder.dropDown().withProps({
values: this.model.authCategories,
value: this.model.authCategory,
editable: false,
}).component();
this.authDropdown.onValueChanged(e => this.onAuthChanged());
this.usernameInputBox = this.uiModelBuilder.inputBox()
.withProps({
placeHolder: loc.username.toLocaleLowerCase(),
value: this.model.props.username
}).component();
this.passwordInputBox = this.uiModelBuilder.inputBox()
.withProps({
placeHolder: loc.password.toLocaleLowerCase(),
inputType: 'password',
value: this.model.props.password
})
.component();
let connectionSection: azdata.FormComponentGroup = {
components: [
{
component: this.urlInputBox,
title: loc.clusterUrl,
required: true
}, {
component: this.authDropdown,
title: loc.authType,
required: true
}, {
component: this.usernameInputBox,
title: loc.username,
required: false
}, {
component: this.passwordInputBox,
title: loc.password,
required: false
}
],
title: loc.clusterConnection
};
let formModel = this.uiModelBuilder.formContainer()
.withFormItems(
this.getMainSectionComponents().concat(
connectionSection)
).withLayout({ width: '100%' }).component();
await view.initializeModel(formModel);
this.onAuthChanged();
});
this.dialog.registerCloseValidator(async () => {
const result = await this.validate();
if (result.validated) {
this.returnPromise.resolve(result.value);
this.returnPromise = undefined;
}
return result.validated;
});
this.dialog.cancelButton.onClick(async () => await this.cancel());
this.dialog.okButton.label = loc.ok;
this.dialog.cancelButton.label = loc.cancel;
}
protected abstract getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[];
protected get authValue(): AuthType {
return (<azdata.CategoryValue>this.authDropdown.value).name as AuthType;
}
private onAuthChanged(): void {
let isBasic = this.authValue === 'basic';
this.usernameInputBox.enabled = isBasic;
this.passwordInputBox.enabled = isBasic;
if (!isBasic) {
this.usernameInputBox.value = '';
this.passwordInputBox.value = '';
}
}
protected abstract validate(): Promise<{ validated: boolean, value?: R }>;
private async cancel(): Promise<void> {
if (this.model && this.model.onCancel) {
await this.model.onCancel();
}
this.returnPromise.reject(new HdfsDialogCancelledError());
}
protected async reportError(error: any): Promise<void> {
this.dialog.message = {
text: (typeof error === 'string') ? error : error.message,
level: azdata.window.MessageLevel.Error
};
if (this.model && this.model.onError) {
await this.model.onError(error as ControllerError);
}
}
}

View File

@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Deferred } from '../../common/promise';
export abstract class InitializingComponent {
private _initialized: boolean = false;
private _onInitializedPromise: Deferred<void> = new Deferred();
constructor() { }
protected get initialized(): boolean {
return this._initialized;
}
protected set initialized(value: boolean) {
if (!this._initialized && value) {
this._initialized = true;
this._onInitializedPromise.resolve();
}
}
/**
* Runs the specified action when the component is initialized. If already initialized just runs
* the action immediately.
* @param action The action to be ran when the page is initialized
*/
protected eventuallyRunOnInitialized(action: () => void): void {
if (!this._initialized) {
this._onInitializedPromise.promise.then(() => action()).catch(error => console.error(`Unexpected error running onInitialized action for BDC Page : ${error}`));
} else {
action();
}
}
}

View File

@@ -0,0 +1,378 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import { ClusterController, MountInfo, MountState } from '../controller/clusterControllerApi';
import { HdfsDialogBase, HdfsDialogModelBase, HdfsDialogProperties } from './hdfsDialogBase';
import * as loc from '../localizedConstants';
/**
* Converts a comma-delimited set of key value pair credentials to a JSON object.
* This code is taken from the azdata implementation written in Python
*/
function convertCredsToJson(creds: string): { credentials: {} } {
if (!creds) {
return undefined;
}
let credObj: { 'credentials': { [key: string]: any } } = { 'credentials': {} };
let pairs = creds.split(',');
let validPairs: string[] = [];
for (let i = 0; i < pairs.length; i++) {
// handle escaped commas in a browser-agnostic way using regex:
// this matches a string ending in a single escape character \, but not \\.
// In this case we split on ',` when we should've ignored it as it was a \, instead.
// Restore the escaped comma by combining the 2 split strings
if (i < (pairs.length - 1) && pairs[i].match(/(?!\\).*\\$/)) {
pairs[i + 1] = `${pairs[i]},${pairs[i + 1]}`;
} else {
validPairs.push(pairs[i]);
}
}
validPairs.forEach(pair => {
const formattingErr = loc.badCredentialsFormatting(pair);
try {
// # remove escaped characters for ,
pair = pair.replace('\\,', ',').trim();
let firstEquals = pair.indexOf('=');
if (firstEquals <= 0 || firstEquals >= pair.length) {
throw new Error(formattingErr);
}
let key = pair.substring(0, firstEquals);
let value = pair.substring(firstEquals + 1);
credObj.credentials[key] = value;
} catch (err) {
throw new Error(formattingErr);
}
});
return credObj;
}
export interface MountHdfsProperties extends HdfsDialogProperties {
hdfsPath?: string;
remoteUri?: string;
credentials?: string;
}
export class MountHdfsDialogModel extends HdfsDialogModelBase<MountHdfsProperties, void> {
private credentials: {};
constructor(props: MountHdfsProperties) {
super(props);
}
protected async handleCompleted(): Promise<void> {
this.throwIfMissingUsernamePassword();
// Validate credentials
this.credentials = convertCredsToJson(this.props.credentials);
// We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect)
let controller = await this.createAndVerifyControllerConnection();
if (this._canceled) {
return;
}
azdata.tasks.startBackgroundOperation(
{
connection: undefined,
displayName: loc.mountTask(this.props.hdfsPath),
description: '',
isCancelable: false,
operation: op => {
this.onSubmit(controller, op);
}
}
);
}
private async onSubmit(controller: ClusterController, op: azdata.BackgroundOperation): Promise<void> {
try {
await controller.mountHdfs(this.props.hdfsPath, this.props.remoteUri, this.credentials);
op.updateStatus(azdata.TaskStatus.InProgress, loc.mountTaskSubmitted);
// Wait until status has changed or some sensible time expired. If it goes over 2 minutes we assume it's "working"
// as there's no other API that'll give us this for now
let result = await this.waitOnMountStatusChange(controller);
let msg = result.state === MountState.Ready ? loc.mountCompleted : loc.mountInProgress;
op.updateStatus(azdata.TaskStatus.Succeeded, msg);
} catch (error) {
const errMsg = loc.mountError(error);
vscode.window.showErrorMessage(errMsg);
op.updateStatus(azdata.TaskStatus.Failed, errMsg);
}
}
private waitOnMountStatusChange(controller: ClusterController): Promise<MountInfo> {
return new Promise<MountInfo>((resolve, reject) => {
const waitTime = 5 * 1000; // 5 seconds
const maxRetries = 30; // 5 x 30 = 150 seconds. After this time, can assume things are "working" as 2 min timeout passed
let waitOnChange = async (retries: number) => {
try {
let mountInfo = await this.getMountStatus(controller, this.props.hdfsPath);
if (mountInfo && mountInfo.error || mountInfo.state === MountState.Error) {
reject(new Error(mountInfo.error ? mountInfo.error : loc.mountErrorUnknown));
} else if (mountInfo.state === MountState.Ready || retries <= 0) {
resolve(mountInfo);
} else {
setTimeout(() => {
waitOnChange(retries - 1).catch(e => reject(e));
}, waitTime);
}
} catch (err) {
reject(err);
}
};
waitOnChange(maxRetries);
});
}
private async getMountStatus(controller: ClusterController, path: string): Promise<MountInfo> {
let statusResponse = await controller.getMountStatus(path);
if (statusResponse.mount) {
return Array.isArray(statusResponse.mount) ? statusResponse.mount[0] : statusResponse.mount;
}
return undefined;
}
}
export class MountHdfsDialog extends HdfsDialogBase<MountHdfsProperties, void> {
private pathInputBox: azdata.InputBoxComponent;
private remoteUriInputBox: azdata.InputBoxComponent;
private credentialsInputBox: azdata.InputBoxComponent;
constructor(model: MountHdfsDialogModel) {
super(loc.mountFolder, model);
}
protected getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[] {
const newMountName = '/mymount';
let pathVal = this.model.props.hdfsPath;
pathVal = (!pathVal || pathVal === '/') ? newMountName : (pathVal + newMountName);
this.pathInputBox = this.uiModelBuilder.inputBox()
.withProps({
value: pathVal
}).component();
this.remoteUriInputBox = this.uiModelBuilder.inputBox()
.withProps({
value: this.model.props.remoteUri
})
.component();
this.credentialsInputBox = this.uiModelBuilder.inputBox()
.withProps({
inputType: 'password',
value: this.model.props.credentials
})
.component();
return [
{
components: [
{
component: this.pathInputBox,
title: loc.hdfsPath,
required: true,
layout: {
info: loc.hdfsPathInfo
}
}, {
component: this.remoteUriInputBox,
title: loc.remoteUri,
required: true,
layout: {
info: loc.remoteUriInfo
}
}, {
component: this.credentialsInputBox,
title: loc.credentials,
required: false,
layout: {
info: loc.credentialsInfo
}
}
],
title: loc.mountConfiguration
}];
}
protected async validate(): Promise<{ validated: boolean }> {
try {
await this.model.onComplete({
url: this.urlInputBox && this.urlInputBox.value,
auth: this.authValue,
username: this.usernameInputBox && this.usernameInputBox.value,
password: this.passwordInputBox && this.passwordInputBox.value,
hdfsPath: this.pathInputBox && this.pathInputBox.value,
remoteUri: this.remoteUriInputBox && this.remoteUriInputBox.value,
credentials: this.credentialsInputBox && this.credentialsInputBox.value
});
return { validated: true };
} catch (error) {
await this.reportError(error);
return { validated: false };
}
}
}
export class RefreshMountDialog extends HdfsDialogBase<MountHdfsProperties, void> {
private pathInputBox: azdata.InputBoxComponent;
constructor(model: RefreshMountModel) {
super(loc.refreshMount, model);
}
protected getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[] {
this.pathInputBox = this.uiModelBuilder.inputBox()
.withProps({
value: this.model.props.hdfsPath
}).component();
return [
{
components: [
{
component: this.pathInputBox,
title: loc.hdfsPath,
required: true
}
],
title: loc.mountConfiguration
}];
}
protected async validate(): Promise<{ validated: boolean }> {
try {
await this.model.onComplete({
url: this.urlInputBox && this.urlInputBox.value,
auth: this.authValue,
username: this.usernameInputBox && this.usernameInputBox.value,
password: this.passwordInputBox && this.passwordInputBox.value,
hdfsPath: this.pathInputBox && this.pathInputBox.value
});
return { validated: true };
} catch (error) {
await this.reportError(error);
return { validated: false };
}
}
}
export class RefreshMountModel extends HdfsDialogModelBase<MountHdfsProperties, void> {
constructor(props: MountHdfsProperties) {
super(props);
}
protected async handleCompleted(): Promise<void> {
this.throwIfMissingUsernamePassword();
// We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect)
let controller = await this.createAndVerifyControllerConnection();
if (this._canceled) {
return;
}
azdata.tasks.startBackgroundOperation(
{
connection: undefined,
displayName: loc.refreshMountTask(this.props.hdfsPath),
description: '',
isCancelable: false,
operation: op => {
this.onSubmit(controller, op);
}
}
);
}
private async onSubmit(controller: ClusterController, op: azdata.BackgroundOperation): Promise<void> {
try {
await controller.refreshMount(this.props.hdfsPath);
op.updateStatus(azdata.TaskStatus.Succeeded, loc.refreshMountTaskSubmitted);
} catch (error) {
const errMsg = (error instanceof Error) ? error.message : error;
vscode.window.showErrorMessage(errMsg);
op.updateStatus(azdata.TaskStatus.Failed, errMsg);
}
}
}
export class DeleteMountDialog extends HdfsDialogBase<MountHdfsProperties, void> {
private pathInputBox: azdata.InputBoxComponent;
constructor(model: DeleteMountModel) {
super(loc.deleteMount, model);
}
protected getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[] {
this.pathInputBox = this.uiModelBuilder.inputBox()
.withProps({
value: this.model.props.hdfsPath
}).component();
return [
{
components: [
{
component: this.pathInputBox,
title: loc.hdfsPath,
required: true
}
],
title: loc.mountConfiguration
}];
}
protected async validate(): Promise<{ validated: boolean }> {
try {
await this.model.onComplete({
url: this.urlInputBox && this.urlInputBox.value,
auth: this.authValue,
username: this.usernameInputBox && this.usernameInputBox.value,
password: this.passwordInputBox && this.passwordInputBox.value,
hdfsPath: this.pathInputBox && this.pathInputBox.value
});
return { validated: true };
} catch (error) {
await this.reportError(error);
return { validated: false };
}
}
}
export class DeleteMountModel extends HdfsDialogModelBase<MountHdfsProperties, void> {
constructor(props: MountHdfsProperties) {
super(props);
}
protected async handleCompleted(): Promise<void> {
this.throwIfMissingUsernamePassword();
// We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect)
let controller = await this.createAndVerifyControllerConnection();
if (this._canceled) {
return;
}
azdata.tasks.startBackgroundOperation(
{
connection: undefined,
displayName: loc.deleteMountTask(this.props.hdfsPath),
description: '',
isCancelable: false,
operation: op => {
this.onSubmit(controller, op);
}
}
);
}
private async onSubmit(controller: ClusterController, op: azdata.BackgroundOperation): Promise<void> {
try {
await controller.deleteMount(this.props.hdfsPath);
op.updateStatus(azdata.TaskStatus.Succeeded, loc.deleteMountTaskSubmitted);
} catch (error) {
const errMsg = (error instanceof Error) ? error.message : error;
vscode.window.showErrorMessage(errMsg);
op.updateStatus(azdata.TaskStatus.Failed, errMsg);
}
}
}

View File

@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { ControllerError } from './controller/clusterControllerApi';
const localize = nls.loadMessageBundle();
// Labels
export const statusIcon = localize('bdc.dashboard.status', "Status Icon");
export const instance = localize('bdc.dashboard.instance', "Instance");
export const state = localize('bdc.dashboard.state', "State");
export const view = localize('bdc.dashboard.view', "View");
export const notAvailable = localize('bdc.dashboard.notAvailable', "N/A");
export const healthStatusDetails = localize('bdc.dashboard.healthStatusDetails', "Health Status Details");
export const metricsAndLogs = localize('bdc.dashboard.metricsAndLogs', "Metrics and Logs");
export const healthStatus = localize('bdc.dashboard.healthStatus', "Health Status");
export const nodeMetrics = localize('bdc.dashboard.nodeMetrics', "Node Metrics");
export const sqlMetrics = localize('bdc.dashboard.sqlMetrics', "SQL Metrics");
export const logs = localize('bdc.dashboard.logs', "Logs");
export function viewNodeMetrics(uri: string): string { return localize('bdc.dashboard.viewNodeMetrics', "View Node Metrics {0}", uri); }
export function viewSqlMetrics(uri: string): string { return localize('bdc.dashboard.viewSqlMetrics', "View SQL Metrics {0}", uri); }
export function viewLogs(uri: string): string { return localize('bdc.dashboard.viewLogs', "View Kibana Logs {0}", uri); }
export function lastUpdated(date?: Date): string {
return localize('bdc.dashboard.lastUpdated', "Last Updated : {0}",
date ?
`${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
: '-');
}
export const basic = localize('basicAuthName', "Basic");
export const windowsAuth = localize('integratedAuthName', "Windows Authentication");
export const addNewController = localize('addNewController', "Add New Controller");
export const url = localize('url', "URL");
export const username = localize('username', "Username");
export const password = localize('password', "Password");
export const rememberPassword = localize('rememberPassword', "Remember Password");
export const clusterUrl = localize('clusterManagementUrl', "Cluster Management URL");
export const authType = localize('textAuthCapital', "Authentication type");
export const clusterConnection = localize('hdsf.dialog.connection.section', "Cluster Connection");
export const add = localize('add', "Add");
export const cancel = localize('cancel', "Cancel");
export const ok = localize('ok', "OK");
export const refresh = localize('bdc.dashboard.refresh', "Refresh");
export const troubleshoot = localize('bdc.dashboard.troubleshoot', "Troubleshoot");
export const bdcOverview = localize('bdc.dashboard.bdcOverview', "Big Data Cluster overview");
export const clusterDetails = localize('bdc.dashboard.clusterDetails', "Cluster Details");
export const clusterOverview = localize('bdc.dashboard.clusterOverview', "Cluster Overview");
export const serviceEndpoints = localize('bdc.dashboard.serviceEndpoints', "Service Endpoints");
export const clusterProperties = localize('bdc.dashboard.clusterProperties', "Cluster Properties");
export const clusterState = localize('bdc.dashboard.clusterState', "Cluster State");
export const serviceName = localize('bdc.dashboard.serviceName', "Service Name");
export const service = localize('bdc.dashboard.service', "Service");
export const endpoint = localize('bdc.dashboard.endpoint', "Endpoint");
export function copiedEndpoint(endpointName: string): string { return localize('copiedEndpoint', "Endpoint '{0}' copied to clipboard", endpointName); }
export const copy = localize('bdc.dashboard.copy', "Copy");
export const viewDetails = localize('bdc.dashboard.viewDetails', "View Details");
export const viewErrorDetails = localize('bdc.dashboard.viewErrorDetails', "View Error Details");
export const connectToController = localize('connectController.dialog.title', "Connect to Controller");
export const mountConfiguration = localize('mount.main.section', "Mount Configuration");
export function mountTask(path: string): string { return localize('mount.task.name', "Mounting HDFS folder on path {0}", path); }
export function refreshMountTask(path: string): string { return localize('refreshmount.task.name', "Refreshing HDFS Mount on path {0}", path); }
export function deleteMountTask(path: string): string { return localize('deletemount.task.name', "Deleting HDFS Mount on path {0}", path); }
export const mountTaskSubmitted = localize('mount.task.submitted', "Mount creation has started");
export const refreshMountTaskSubmitted = localize('refreshmount.task.submitted', "Refresh mount request submitted");
export const deleteMountTaskSubmitted = localize('deletemount.task.submitted', "Delete mount request submitted");
export const mountCompleted = localize('mount.task.complete', "Mounting HDFS folder is complete");
export const mountInProgress = localize('mount.task.inprogress', "Mounting is likely to complete, check back later to verify");
export const mountFolder = localize('mount.dialog.title', "Mount HDFS Folder");
export const hdfsPath = localize('mount.hdfsPath.title', "HDFS Path");
export const hdfsPathInfo = localize('mount.hdfsPath.info', "Path to a new (non-existing) directory which you want to associate with the mount");
export const remoteUri = localize('mount.remoteUri.title', "Remote URI");
export const remoteUriInfo = localize('mount.remoteUri.info', "The URI to the remote data source. Example for ADLS: abfs://fs@saccount.dfs.core.windows.net/");
export const credentials = localize('mount.credentials.title', "Credentials");
export const credentialsInfo = localize('mount.credentials.info', "Mount credentials for authentication to remote data source for reads");
export const refreshMount = localize('refreshmount.dialog.title', "Refresh Mount");
export const deleteMount = localize('deleteMount.dialog.title', "Delete Mount");
export const loadingClusterStateCompleted = localize('bdc.dashboard.loadingClusterStateCompleted', "Loading cluster state completed");
export const loadingHealthStatusCompleted = localize('bdc.dashboard.loadingHealthStatusCompleted', "Loading health status completed");
// Errors
export const usernameRequired = localize('err.controller.username.required', "Username is required");
export const passwordRequired = localize('err.controller.password.required', "Password is required");
export function endpointsError(msg: string): string { return localize('endpointsError', "Unexpected error retrieving BDC Endpoints: {0}", msg); }
export const noConnectionError = localize('bdc.dashboard.noConnection', "The dashboard requires a connection. Please click retry to enter your credentials.");
export function unexpectedError(error: Error): string { return localize('bdc.dashboard.unexpectedError', "Unexpected error occurred: {0}", error.message); }
export const loginFailed = localize('mount.hdfs.loginerror1', "Login to controller failed");
export function loginFailedWithError(error: ControllerError): string { return localize('mount.hdfs.loginerror2', "Login to controller failed: {0}", error.statusMessage || error.message); }
export function badCredentialsFormatting(pair: string): string { return localize('mount.err.formatting', "Bad formatting of credentials at {0}", pair); }
export function mountError(error: any): string { return localize('mount.task.error', "Error mounting folder: {0}", (error instanceof Error ? error.message : error)); }
export const mountErrorUnknown = localize('mount.error.unknown', "Unknown error occurred during the mount process");

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TreeNode } from './treeNode';
export interface IControllerTreeChangeHandler {
notifyNodeChanged(node?: TreeNode): void;
}

View File

@@ -0,0 +1,209 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { TreeNode } from './treeNode';
import { IControllerTreeChangeHandler } from './controllerTreeChangeHandler';
import { ControllerRootNode, ControllerNode } from './controllerTreeNode';
import { showErrorMessage } from '../utils';
import { AuthType } from 'bdc';
const localize = nls.loadMessageBundle();
const CredentialNamespace = 'clusterControllerCredentials';
interface IControllerInfoSlim {
url: string;
auth: AuthType;
username: string;
password?: string;
rememberPassword: boolean;
}
export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeNode>, IControllerTreeChangeHandler {
private _onDidChangeTreeData: vscode.EventEmitter<TreeNode> = new vscode.EventEmitter<TreeNode>();
public readonly onDidChangeTreeData: vscode.Event<TreeNode> = this._onDidChangeTreeData.event;
private root: ControllerRootNode;
private credentialProvider: azdata.CredentialProvider;
private initialized: boolean = false;
constructor(private memento: vscode.Memento) {
this.root = new ControllerRootNode(this);
}
public async getChildren(element?: TreeNode): Promise<TreeNode[]> {
if (element) {
return element.getChildren();
}
if (!this.initialized) {
try {
await this.loadSavedControllers();
} catch (err) {
void vscode.window.showErrorMessage(localize('bdc.controllerTreeDataProvider.error', "Unexpected error loading saved controllers: {0}", err));
}
}
return this.root.getChildren();
}
public getTreeItem(element: TreeNode): vscode.TreeItem | Thenable<vscode.TreeItem> {
return element.getTreeItem();
}
public notifyNodeChanged(node?: TreeNode): void {
this._onDidChangeTreeData.fire(node);
}
/**
* Creates or updates a node in the tree with the specified connection information
* @param url The URL for the BDC management endpoint
* @param auth The type of auth to use
* @param username The username (if basic auth)
* @param password The password (if basic auth)
* @param rememberPassword Whether to store the password in the password store when saving
*/
public addOrUpdateController(
url: string,
auth: AuthType,
username: string,
password: string,
rememberPassword: boolean
): void {
this.removeNonControllerNodes();
this.root.addOrUpdateControllerNode(url, auth, username, password, rememberPassword);
this.notifyNodeChanged();
}
public removeController(url: string, auth: AuthType, username: string): ControllerNode[] {
let removed = this.root.removeControllerNode(url, auth, username);
if (removed) {
this.notifyNodeChanged();
}
return removed;
}
private removeNonControllerNodes(): void {
this.removeDefectiveControllerNodes(this.root.children);
}
private removeDefectiveControllerNodes(nodes: TreeNode[]): void {
if (nodes.length > 0) {
for (let i = 0; i < nodes.length; ++i) {
if (nodes[i] instanceof ControllerNode) {
let controller = nodes[i] as ControllerNode;
if (!controller.url || !controller.id) {
nodes.splice(i--, 1);
}
}
}
}
}
private async loadSavedControllers(): Promise<void> {
// Optimistically set to true so we don't double-load the tree
this.initialized = true;
try {
let controllers: IControllerInfoSlim[] = this.memento.get('controllers');
let treeNodes: TreeNode[] = [];
if (controllers) {
for (const c of controllers) {
let password = undefined;
if (c.rememberPassword) {
password = await this.getPassword(c.url, c.username);
}
if (!c.auth) {
// Added before we had added authentication
c.auth = 'basic';
}
treeNodes.push(new ControllerNode(
c.url, c.auth, c.username, password, c.rememberPassword,
undefined, this.root, this, undefined
));
}
this.removeDefectiveControllerNodes(treeNodes);
}
this.root.clearChildren();
treeNodes.forEach(node => this.root.addChild(node));
await vscode.commands.executeCommand('setContext', 'bdc.loaded', true);
} catch (err) {
// Reset so we can try again if the tree refreshes
this.initialized = false;
throw err;
}
}
public async saveControllers(): Promise<void> {
const controllers = this.root.children.map((e): IControllerInfoSlim => {
const controller = e as ControllerNode;
return {
url: controller.url,
auth: controller.auth,
username: controller.username,
password: controller.password,
rememberPassword: controller.rememberPassword
};
});
const controllersWithoutPassword = controllers.map((e): IControllerInfoSlim => {
return {
url: e.url,
auth: e.auth,
username: e.username,
rememberPassword: e.rememberPassword
};
});
try {
await this.memento.update('controllers', controllersWithoutPassword);
} catch (error) {
showErrorMessage(error);
}
for (const e of controllers) {
if (e.rememberPassword) {
await this.savePassword(e.url, e.username, e.password);
} else {
await this.deletePassword(e.url, e.username);
}
}
}
private async savePassword(url: string, username: string, password: string): Promise<boolean> {
let provider = await this.getCredentialProvider();
let id = this.createId(url, username);
let result = await provider.saveCredential(id, password);
return result;
}
private async deletePassword(url: string, username: string): Promise<boolean> {
let provider = await this.getCredentialProvider();
let id = this.createId(url, username);
let result = await provider.deleteCredential(id);
return result;
}
private async getPassword(url: string, username: string): Promise<string | undefined> {
let provider = await this.getCredentialProvider();
let id = this.createId(url, username);
let credential = await provider.readCredential(id);
return credential ? credential.password : undefined;
}
private async getCredentialProvider(): Promise<azdata.CredentialProvider> {
if (!this.credentialProvider) {
this.credentialProvider = await azdata.credentials.getProvider(CredentialNamespace);
}
return this.credentialProvider;
}
private createId(url: string, username: string): string {
return `${url}::${username}`;
}
}

View File

@@ -0,0 +1,247 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import { IControllerTreeChangeHandler } from './controllerTreeChangeHandler';
import { TreeNode } from './treeNode';
import { IconPathHelper, BdcItemType, IconPath } from '../constants';
import { AuthType } from 'bdc';
abstract class ControllerTreeNode extends TreeNode {
constructor(
label: string,
parent: ControllerTreeNode,
private _treeChangeHandler: IControllerTreeChangeHandler,
private _description?: string,
private _nodeType?: string,
private _iconPath?: IconPath
) {
super(label, parent);
this._description = this._description || this.label;
}
public async getChildren(): Promise<ControllerTreeNode[]> {
return this.children as ControllerTreeNode[];
}
public override refresh(): void {
super.refresh();
this.treeChangeHandler.notifyNodeChanged(this);
}
public getTreeItem(): vscode.TreeItem {
let item: vscode.TreeItem = {};
item.id = this.id;
item.label = this.label;
item.collapsibleState = vscode.TreeItemCollapsibleState.None;
item.iconPath = this._iconPath;
item.contextValue = this._nodeType;
item.tooltip = this._description;
item.iconPath = this._iconPath;
return item;
}
public getNodeInfo(): azdata.NodeInfo {
return {
label: this.label,
isLeaf: this.isLeaf,
errorMessage: undefined,
metadata: undefined,
nodePath: this.nodePath,
nodeStatus: undefined,
nodeType: this._nodeType,
iconType: this._nodeType,
nodeSubType: undefined
};
}
public get description(): string {
return this._description;
}
public set description(description: string) {
this._description = description;
}
public get nodeType(): string {
return this._nodeType;
}
public set nodeType(nodeType: string) {
this._nodeType = nodeType;
}
public set iconPath(iconPath: IconPath) {
this._iconPath = iconPath;
}
public get iconPath(): IconPath {
return this._iconPath;
}
public set treeChangeHandler(treeChangeHandler: IControllerTreeChangeHandler) {
this._treeChangeHandler = treeChangeHandler;
}
public get treeChangeHandler(): IControllerTreeChangeHandler {
return this._treeChangeHandler;
}
}
export class ControllerRootNode extends ControllerTreeNode {
constructor(treeChangeHandler: IControllerTreeChangeHandler) {
super('root', undefined, treeChangeHandler, undefined, BdcItemType.controllerRoot);
}
public override async getChildren(): Promise<ControllerNode[]> {
return this.children as ControllerNode[];
}
/**
* Creates or updates a node in the tree with the specified connection information
* @param url The URL for the BDC management endpoint
* @param auth The type of auth to use
* @param username The username (if basic auth)
* @param password The password (if basic auth)
* @param rememberPassword Whether to store the password in the password store when saving
*/
public addOrUpdateControllerNode(
url: string,
auth: AuthType,
username: string,
password: string,
rememberPassword: boolean
): void {
let controllerNode = this.getExistingControllerNode(url, auth, username);
if (controllerNode) {
controllerNode.password = password;
controllerNode.rememberPassword = rememberPassword;
controllerNode.clearChildren();
} else {
controllerNode = new ControllerNode(url, auth, username, password, rememberPassword, undefined, this, this.treeChangeHandler, undefined);
this.addChild(controllerNode);
}
}
public removeControllerNode(url: string, auth: AuthType, username: string): ControllerNode[] | undefined {
if (!url || (auth === 'basic' && !username)) {
return undefined;
}
let nodes = this.children as ControllerNode[];
let index = nodes.findIndex(e => isControllerMatch(e, url, auth, username));
let deleted: ControllerNode[] | undefined;
if (index >= 0) {
deleted = nodes.splice(index, 1);
}
return deleted;
}
private getExistingControllerNode(url: string, auth: AuthType, username: string): ControllerNode | undefined {
if (!url || !username) {
return undefined;
}
let nodes = this.children as ControllerNode[];
return nodes.find(e => isControllerMatch(e, url, auth, username));
}
}
export class ControllerNode extends ControllerTreeNode {
constructor(
private _url: string,
private _auth: AuthType,
private _username: string,
private _password: string,
private _rememberPassword: boolean,
label: string,
parent: ControllerTreeNode,
treeChangeHandler: IControllerTreeChangeHandler,
description?: string,
) {
super(label, parent, treeChangeHandler, description, BdcItemType.controller, IconPathHelper.controllerNode);
this.label = label;
this.description = description;
}
public override async getChildren(): Promise<ControllerTreeNode[] | undefined> {
if (this.children && this.children.length > 0) {
this.clearChildren();
}
if (!this._password) {
vscode.commands.executeCommand('bigDataClusters.command.connectController', this);
return this.children as ControllerTreeNode[];
}
return undefined;
}
public static toIpAndPort(url: string): string | undefined {
if (!url) {
return undefined;
}
return url.trim().replace(/ /g, '').replace(/^.+\:\/\//, '');
}
public get url(): string {
return this._url;
}
public get auth(): AuthType {
return this._auth;
}
public get username(): string {
return this._username;
}
public get password(): string {
return this._password;
}
public set password(pw: string) {
this._password = pw;
}
public override set label(label: string) {
super.label = label || this.generateLabel();
}
public get rememberPassword() {
return this._rememberPassword;
}
public set rememberPassword(rememberPassword: boolean) {
this._rememberPassword = rememberPassword;
}
private generateLabel(): string {
let label = `controller: ${ControllerNode.toIpAndPort(this._url)}`;
if (this._auth === 'basic') {
label += ` (${this._username})`;
}
return label;
}
public override get label(): string {
return super.label;
}
public override set description(description: string) {
super.description = description || super.label;
}
public override get description(): string {
return super.description;
}
}
function isControllerMatch(node: ControllerNode, url: string, auth: string, username: string): unknown {
return node.url === url && node.auth === auth && node.username === username;
}

View File

@@ -0,0 +1,193 @@
/*---------------------------------------------------------------------------------------------
* 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 { generateGuid } from '../utils';
export abstract class TreeNode {
private _id: string;
private _children: TreeNode[];
private _isLeaf: boolean;
constructor(private _label: string, private _parent?: TreeNode) {
this.resetId();
}
public resetId(): void {
this._id = (this._label || '_') + `::${generateGuid()}`;
}
public get id(): string {
return this._id;
}
public set label(label: string) {
if (!this._label) {
this._label = label;
this.resetId();
} else {
this._label = label;
}
}
public get label(): string {
return this._label;
}
public set parent(parent: TreeNode) {
this._parent = parent;
}
public get parent(): TreeNode {
return this._parent;
}
public get children(): TreeNode[] {
if (!this._children) {
this._children = [];
}
return this._children;
}
public get hasChildren(): boolean {
return this.children && this.children.length > 0;
}
public set isLeaf(isLeaf: boolean) {
this._isLeaf = isLeaf;
}
public get isLeaf(): boolean {
return this._isLeaf;
}
public get root(): TreeNode {
return TreeNode.getRoot(this);
}
public equals(node: TreeNode): boolean {
if (!node) {
return undefined;
}
return this.nodePath === node.nodePath;
}
public refresh(): void {
this.resetId();
}
public static getRoot(node: TreeNode): TreeNode {
if (!node) {
return undefined;
}
let current: TreeNode = node;
while (current.parent) {
current = current.parent;
}
return current;
}
public get nodePath(): string {
return TreeNode.getNodePath(this);
}
public static getNodePath(node: TreeNode): string {
if (!node) {
return undefined;
}
let current: TreeNode = node;
let path = current._id;
while (current.parent) {
current = current.parent;
path = `${current._id}/${path}`;
}
return path;
}
public async findNode(condition: (node: TreeNode) => boolean, expandIfNeeded?: boolean): Promise<TreeNode> {
return TreeNode.findNode(this, condition, expandIfNeeded);
}
public static async findNode(node: TreeNode, condition: (node: TreeNode) => boolean, expandIfNeeded?: boolean): Promise<TreeNode> {
if (!node || !condition) {
return undefined;
}
let result: TreeNode = undefined;
let nodesToCheck: TreeNode[] = [node];
while (nodesToCheck.length > 0) {
let current = nodesToCheck.shift();
if (condition(current)) {
result = current;
break;
}
if (current.hasChildren) {
nodesToCheck = nodesToCheck.concat(current.children);
} else if (expandIfNeeded) {
let children = await current.getChildren();
if (children && children.length > 0) {
nodesToCheck = nodesToCheck.concat(children);
}
}
}
return result;
}
public async filterNode(condition: (node: TreeNode) => boolean, expandIfNeeded?: boolean): Promise<TreeNode[]> {
return TreeNode.filterNode(this, condition, expandIfNeeded);
}
public static async filterNode(node: TreeNode, condition: (node: TreeNode) => boolean, expandIfNeeded?: boolean): Promise<TreeNode[]> {
if (!node || !condition) {
return undefined;
}
let result: TreeNode[] = [];
let nodesToCheck: TreeNode[] = [node];
while (nodesToCheck.length > 0) {
let current = nodesToCheck.shift();
if (condition(current)) {
result.push(current);
}
if (current.hasChildren) {
nodesToCheck = nodesToCheck.concat(current.children);
} else if (expandIfNeeded) {
let children = await current.getChildren();
if (children && children.length > 0) {
nodesToCheck = nodesToCheck.concat(children);
}
}
}
return result;
}
public async findNodeByPath(path: string, expandIfNeeded?: boolean): Promise<TreeNode> {
return TreeNode.findNodeByPath(this, path, expandIfNeeded);
}
public static async findNodeByPath(node: TreeNode, path: string, expandIfNeeded?: boolean): Promise<TreeNode> {
return TreeNode.findNode(node, node => {
return node.nodePath && (node.nodePath === path || node.nodePath.startsWith(path));
}, expandIfNeeded);
}
public addChild(node: TreeNode): void {
if (!this._children) {
this._children = [];
}
this._children.push(node);
}
public clearChildren(): void {
if (this._children) {
this._children = [];
}
}
public abstract getChildren(): Promise<TreeNode[]>;
public abstract getTreeItem(): vscode.TreeItem;
public abstract getNodeInfo(): azdata.NodeInfo;
}

View File

@@ -0,0 +1,289 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import * as constants from './constants';
const localize = nls.loadMessageBundle();
export enum Endpoint {
gateway = 'gateway',
sparkHistory = 'spark-history',
yarnUi = 'yarn-ui',
appProxy = 'app-proxy',
mgmtproxy = 'mgmtproxy',
managementProxy = 'management-proxy',
logsui = 'logsui',
metricsui = 'metricsui',
controller = 'controller',
sqlServerMaster = 'sql-server-master',
webhdfs = 'webhdfs',
livy = 'livy'
}
export enum Service {
sql = 'sql',
hdfs = 'hdfs',
spark = 'spark',
control = 'control',
gateway = 'gateway',
app = 'app'
}
export function generateGuid(): string {
let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
let oct: string = '';
let tmp: number;
for (let a: number = 0; a < 4; a++) {
tmp = (4294967296 * Math.random()) | 0;
oct += hexValues[tmp & 0xF] +
hexValues[tmp >> 4 & 0xF] +
hexValues[tmp >> 8 & 0xF] +
hexValues[tmp >> 12 & 0xF] +
hexValues[tmp >> 16 & 0xF] +
hexValues[tmp >> 20 & 0xF] +
hexValues[tmp >> 24 & 0xF] +
hexValues[tmp >> 28 & 0xF];
}
let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12);
}
export function showErrorMessage(error: any, prefixText?: string): void {
if (error) {
let text: string = prefixText || '';
if (typeof error === 'string') {
text += error as string;
} else if (typeof error === 'object' && error !== null) {
text += error.message;
if (error.code && error.code > 0) {
text += ` (${error.code})`;
}
} else {
text += `${error}`;
}
vscode.window.showErrorMessage(text);
}
}
/**
* Mappings of the different expected state values to their localized friendly names.
* These are defined in aris/projects/controller/src/Microsoft.SqlServer.Controller/StateMachines
*/
const stateToDisplayTextMap: { [key: string]: string } = {
// K8sScaledSetStateMachine
'creating': localize('state.creating', "Creating"),
'waiting': localize('state.waiting', "Waiting"),
'ready': localize('state.ready', "Ready"),
'deleting': localize('state.deleting', "Deleting"),
'deleted': localize('state.deleted', "Deleted"),
'applyingupgrade': localize('state.applyingUpgrade', "Applying Upgrade"),
'upgrading': localize('state.upgrading', "Upgrading"),
'applyingmanagedupgrade': localize('state.applyingmanagedupgrade', "Applying Managed Upgrade"),
'managedupgrading': localize('state.managedUpgrading', "Managed Upgrading"),
'rollback': localize('state.rollback', "Rollback"),
'rollbackinprogress': localize('state.rollbackInProgress', "Rollback In Progress"),
'rollbackcomplete': localize('state.rollbackComplete', "Rollback Complete"),
'error': localize('state.error', "Error"),
// BigDataClusterStateMachine
'creatingsecrets': localize('state.creatingSecrets', "Creating Secrets"),
'waitingforsecrets': localize('state.waitingForSecrets', "Waiting For Secrets"),
'creatinggroups': localize('state.creatingGroups', "Creating Groups"),
'waitingforgroups': localize('state.waitingForGroups', "Waiting For Groups"),
'creatingresources': localize('state.creatingResources', "Creating Resources"),
'waitingforresources': localize('state.waitingForResources', "Waiting For Resources"),
'creatingkerberosdelegationsetup': localize('state.creatingKerberosDelegationSetup', "Creating Kerberos Delegation Setup"),
'waitingforkerberosdelegationsetup': localize('state.waitingForKerberosDelegationSetup', "Waiting For Kerberos Delegation Setup"),
'waitingfordeletion': localize('state.waitingForDeletion', "Waiting For Deletion"),
'waitingforupgrade': localize('state.waitingForUpgrade', "Waiting For Upgrade"),
'upgradePaused': localize('state.upgradePaused', "Upgrade Paused"),
// Other
'running': localize('state.running', "Running"),
};
/**
* Gets the localized text to display for a corresponding state
* @param state The state to get the display text for
*/
export function getStateDisplayText(state?: string): string {
state = state || '';
return stateToDisplayTextMap[state.toLowerCase()] || state;
}
/**
* Gets the localized text to display for a corresponding endpoint
* @param endpointName The endpoint name to get the display text for
* @param description The backup description to use if we don't have our own
*/
export function getEndpointDisplayText(endpointName?: string, description?: string): string {
endpointName = endpointName || '';
switch (endpointName.toLowerCase()) {
case Endpoint.appProxy:
return localize('endpoint.appproxy', "Application Proxy");
case Endpoint.controller:
return localize('endpoint.controller', "Cluster Management Service");
case Endpoint.gateway:
return localize('endpoint.gateway', "Gateway to access HDFS files, Spark");
case Endpoint.managementProxy:
return localize('endpoint.managementproxy', "Management Proxy");
case Endpoint.mgmtproxy:
return localize('endpoint.mgmtproxy', "Management Proxy");
case Endpoint.sqlServerMaster:
return localize('endpoint.sqlServerEndpoint', "SQL Server Master Instance Front-End");
case Endpoint.metricsui:
return localize('endpoint.grafana', "Metrics Dashboard");
case Endpoint.logsui:
return localize('endpoint.kibana', "Log Search Dashboard");
case Endpoint.yarnUi:
return localize('endpoint.yarnHistory', "Spark Diagnostics and Monitoring Dashboard");
case Endpoint.sparkHistory:
return localize('endpoint.sparkHistory', "Spark Jobs Management and Monitoring Dashboard");
case Endpoint.webhdfs:
return localize('endpoint.webhdfs', "HDFS File System Proxy");
case Endpoint.livy:
return localize('endpoint.livy', "Proxy for running Spark statements, jobs, applications");
default:
// Default is to use the description if one was given, otherwise worst case just fall back to using the
// original endpoint name
return description && description.length > 0 ? description : endpointName;
}
}
/**
* Gets the localized text to display for a corresponding service
* @param serviceName The service name to get the display text for
*/
export function getServiceNameDisplayText(serviceName?: string): string {
serviceName = serviceName || '';
switch (serviceName.toLowerCase()) {
case Service.sql:
return localize('service.sql', "SQL Server");
case Service.hdfs:
return localize('service.hdfs', "HDFS");
case Service.spark:
return localize('service.spark', "Spark");
case Service.control:
return localize('service.control', "Control");
case Service.gateway:
return localize('service.gateway', "Gateway");
case Service.app:
return localize('service.app', "App");
default:
return serviceName;
}
}
/**
* Gets the localized text to display for a corresponding health status
* @param healthStatus The health status to get the display text for
*/
export function getHealthStatusDisplayText(healthStatus?: string) {
healthStatus = healthStatus || '';
switch (healthStatus.toLowerCase()) {
case 'healthy':
return localize('bdc.healthy', "Healthy");
case 'unhealthy':
return localize('bdc.unhealthy', "Unhealthy");
default:
return healthStatus;
}
}
/**
* Returns the status icon for the corresponding health status
* @param healthStatus The status to check
*/
export function getHealthStatusIcon(healthStatus?: string): string {
healthStatus = healthStatus || '';
switch (healthStatus.toLowerCase()) {
case 'healthy':
return '✔️';
default:
// Consider all non-healthy status' as errors
return '⚠️';
}
}
/**
* Returns the status dot icon which will be a • for all non-healthy states
* @param healthStatus The status to check
*/
export function getHealthStatusDotIcon(healthStatus?: string): constants.IconPath {
healthStatus = healthStatus || '';
switch (healthStatus.toLowerCase()) {
case 'healthy':
return constants.IconPathHelper.status_circle_blank;
default:
// Display status dot for all non-healthy status'
return constants.IconPathHelper.status_circle_red;
}
}
interface RawEndpoint {
serviceName: string;
description?: string;
endpoint?: string;
protocol?: string;
ipAddress?: string;
port?: number;
}
interface IEndpoint {
serviceName: string;
description: string;
endpoint: string;
protocol: string;
}
function getClusterEndpoints(serverInfo: azdata.ServerInfo): IEndpoint[] {
let endpoints: RawEndpoint[] = serverInfo.options[constants.clusterEndpointsProperty];
if (!endpoints || endpoints.length === 0) { return []; }
return endpoints.map(e => {
// If endpoint is missing, we're on CTP bits. All endpoints from the CTP serverInfo should be treated as HTTPS
let endpoint = e.endpoint ? e.endpoint : `https://${e.ipAddress}:${e.port}`;
let updatedEndpoint: IEndpoint = {
serviceName: e.serviceName,
description: e.description,
endpoint: endpoint,
protocol: e.protocol
};
return updatedEndpoint;
});
}
export function getControllerEndpoint(serverInfo: azdata.ServerInfo): string | undefined {
let endpoints = getClusterEndpoints(serverInfo);
if (endpoints) {
let index = endpoints.findIndex(ep => ep.serviceName.toLowerCase() === constants.controllerEndpointName.toLowerCase());
if (index < 0) { return undefined; }
return endpoints[index].endpoint;
}
return undefined;
}
export function getBdcStatusErrorMessage(error: Error): string {
return localize('endpointsError', "Unexpected error retrieving BDC Endpoints: {0}", error.message);
}
const bdcConfigSectionName = 'bigDataCluster';
const ignoreSslConfigName = 'ignoreSslVerification';
/**
* Retrieves the current setting for whether to ignore SSL verification errors
*/
export function getIgnoreSslVerificationConfigSetting(): boolean {
try {
const config = vscode.workspace.getConfiguration(bdcConfigSectionName);
return config.get<boolean>(ignoreSslConfigName, true);
} catch (error) {
console.error(`Unexpected error retrieving ${bdcConfigSectionName}.${ignoreSslConfigName} setting : ${error}`);
}
return true;
}

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const ManageControllerCommand = 'bigDataClusters.command.manageController';
export const CreateControllerCommand = 'bigDataClusters.command.createController';
export const ConnectControllerCommand = 'bigDataClusters.command.connectController';
export const RemoveControllerCommand = 'bigDataClusters.command.removeController';
export const RefreshControllerCommand = 'bigDataClusters.command.refreshController';
export const MountHdfsCommand = 'bigDataClusters.command.mount';
export const RefreshMountCommand = 'bigDataClusters.command.refreshmount';
export const DeleteMountCommand = 'bigDataClusters.command.deletemount';

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Deferred promise
*/
export class Deferred<T> {
promise: Promise<T>;
resolve: (value?: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult> {
return this.promise.then(onfulfilled, onrejected);
}
}

View File

@@ -0,0 +1,225 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { ControllerTreeDataProvider } from './bigDataCluster/tree/controllerTreeDataProvider';
import { IconPathHelper } from './bigDataCluster/constants';
import { TreeNode } from './bigDataCluster/tree/treeNode';
import { AddControllerDialogModel, AddControllerDialog } from './bigDataCluster/dialog/addControllerDialog';
import { ControllerNode } from './bigDataCluster/tree/controllerTreeNode';
import { BdcDashboard } from './bigDataCluster/dialog/bdcDashboard';
import { BdcDashboardModel, BdcDashboardOptions } from './bigDataCluster/dialog/bdcDashboardModel';
import { MountHdfsDialogModel as MountHdfsModel, MountHdfsProperties, MountHdfsDialog, DeleteMountDialog, DeleteMountModel, RefreshMountDialog, RefreshMountModel } from './bigDataCluster/dialog/mountHdfsDialog';
import { getControllerEndpoint } from './bigDataCluster/utils';
import * as commands from './commands';
import { HdfsDialogCancelledError } from './bigDataCluster/dialog/hdfsDialogBase';
import { IExtension, AuthType, IClusterController } from 'bdc';
import { ClusterController } from './bigDataCluster/controller/clusterControllerApi';
const localize = nls.loadMessageBundle();
const endpointNotFoundError = localize('mount.error.endpointNotFound', "Controller endpoint information was not found");
let throttleTimers: { [key: string]: any } = {};
export async function activate(extensionContext: vscode.ExtensionContext): Promise<IExtension> {
IconPathHelper.setExtensionContext(extensionContext);
await vscode.commands.executeCommand('setContext', 'bdc.loaded', false);
const treeDataProvider = new ControllerTreeDataProvider(extensionContext.globalState);
let controllers: any[] = extensionContext.globalState.get('controllers', []);
if (controllers.length > 0) {
const deprecationNoticeKey = 'bdc.deprecationNoticeShown';
const deprecationNoticeShown = extensionContext.globalState.get(deprecationNoticeKey, false);
if (!deprecationNoticeShown) {
void vscode.window.showWarningMessage(localize('bdc.deprecationWarning', 'The Big Data Cluster add-on is being retired and Azure Data Studio functionality for it will be removed in an upcoming release. Read more about this and support going forward [here](https://go.microsoft.com/fwlink/?linkid=2207340).'));
void extensionContext.globalState.update(deprecationNoticeKey, true);
}
}
vscode.window.registerTreeDataProvider('sqlBigDataCluster', treeDataProvider);
registerCommands(extensionContext, treeDataProvider);
return {
getClusterController(url: string, authType: AuthType, username?: string, password?: string): IClusterController {
return new ClusterController(url, authType, username, password);
}
};
}
export function deactivate() {
}
function registerCommands(context: vscode.ExtensionContext, treeDataProvider: ControllerTreeDataProvider): void {
vscode.commands.registerCommand(commands.ConnectControllerCommand, (node?: TreeNode) => {
runThrottledAction(commands.ConnectControllerCommand, () => addBdcController(treeDataProvider, node));
});
vscode.commands.registerCommand(commands.CreateControllerCommand, () => {
runThrottledAction(commands.CreateControllerCommand, () => vscode.commands.executeCommand('azdata.resource.deploy', 'sql-bdc', ['sql-bdc']));
});
vscode.commands.registerCommand(commands.RemoveControllerCommand, async (node: TreeNode) => {
await deleteBdcController(treeDataProvider, node);
});
vscode.commands.registerCommand(commands.RefreshControllerCommand, (node: TreeNode) => {
if (!node) {
return;
}
treeDataProvider.notifyNodeChanged(node);
});
vscode.commands.registerCommand(commands.ManageControllerCommand, async (info: ControllerNode | BdcDashboardOptions, addOrUpdateController: boolean = false) => {
const title: string = `${localize('bdc.dashboard.title', "Big Data Cluster Dashboard -")} ${ControllerNode.toIpAndPort(info.url)}`;
if (addOrUpdateController) {
// The info may be wrong, but if it is then we'll prompt to reconnect when the dashboard is opened
// and update with the correct info then
treeDataProvider.addOrUpdateController(
info.url,
info.auth,
info.username,
info.password,
info.rememberPassword);
await treeDataProvider.saveControllers();
}
const dashboard: BdcDashboard = new BdcDashboard(title, new BdcDashboardModel(info, treeDataProvider));
await dashboard.showDashboard();
});
vscode.commands.registerCommand(commands.MountHdfsCommand, e => mountHdfs(e).catch(error => {
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
}));
vscode.commands.registerCommand(commands.RefreshMountCommand, e => refreshMount(e).catch(error => {
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
}));
vscode.commands.registerCommand(commands.DeleteMountCommand, e => deleteMount(e).catch(error => {
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
}));
}
async function mountHdfs(explorerContext?: azdata.ObjectExplorerContext): Promise<void> {
const mountProps = await getMountProps(explorerContext);
if (mountProps) {
const dialog = new MountHdfsDialog(new MountHdfsModel(mountProps));
try {
await dialog.showDialog();
} catch (error) {
if (!(error instanceof HdfsDialogCancelledError)) {
throw error;
}
}
}
}
async function refreshMount(explorerContext?: azdata.ObjectExplorerContext): Promise<void> {
const mountProps = await getMountProps(explorerContext);
if (mountProps) {
const dialog = new RefreshMountDialog(new RefreshMountModel(mountProps));
await dialog.showDialog();
}
}
async function deleteMount(explorerContext?: azdata.ObjectExplorerContext): Promise<void> {
const mountProps = await getMountProps(explorerContext);
if (mountProps) {
const dialog = new DeleteMountDialog(new DeleteMountModel(mountProps));
await dialog.showDialog();
}
}
async function getMountProps(explorerContext?: azdata.ObjectExplorerContext): Promise<MountHdfsProperties | undefined> {
let endpoint = await lookupController(explorerContext);
if (!endpoint) {
vscode.window.showErrorMessage(endpointNotFoundError);
return undefined;
}
let profile = explorerContext.connectionProfile;
let mountProps: MountHdfsProperties = {
url: endpoint,
auth: profile.authenticationType === azdata.connection.AuthenticationType.SqlLogin ? 'basic' : 'integrated',
username: profile.userName,
password: profile.password,
hdfsPath: getHdsfPath(explorerContext.nodeInfo.nodePath)
};
return mountProps;
}
function getHdsfPath(nodePath: string): string {
const hdfsNodeLabel = '/HDFS';
let index = nodePath.indexOf(hdfsNodeLabel);
if (index >= 0) {
let subPath = nodePath.substring(index + hdfsNodeLabel.length);
return subPath.length > 0 ? subPath : '/';
}
// Use the root
return '/';
}
async function lookupController(explorerContext?: azdata.ObjectExplorerContext): Promise<string | undefined> {
if (!explorerContext) {
return undefined;
}
let serverInfo = await azdata.connection.getServerInfo(explorerContext.connectionProfile.id);
if (!serverInfo || !serverInfo.options) {
vscode.window.showErrorMessage(endpointNotFoundError);
return undefined;
}
return getControllerEndpoint(serverInfo);
}
function addBdcController(treeDataProvider: ControllerTreeDataProvider, node?: TreeNode): void {
let model = new AddControllerDialogModel(treeDataProvider, node as ControllerNode);
let dialog = new AddControllerDialog(model);
dialog.showDialog();
}
async function deleteBdcController(treeDataProvider: ControllerTreeDataProvider, node: TreeNode): Promise<boolean | undefined> {
if (!node && !(node instanceof ControllerNode)) {
return undefined;
}
let controllerNode = node as ControllerNode;
let choices: { [id: string]: boolean } = {};
choices[localize('textYes', "Yes")] = true;
choices[localize('textNo', "No")] = false;
let options = {
ignoreFocusOut: false,
placeHolder: localize('textConfirmRemoveController', "Are you sure you want to remove \'{0}\'?", controllerNode.label)
};
let result = await vscode.window.showQuickPick(Object.keys(choices), options);
let remove: boolean = !!(result && choices[result]);
if (remove) {
await removeControllerInternal(treeDataProvider, controllerNode);
}
return remove;
}
async function removeControllerInternal(treeDataProvider: ControllerTreeDataProvider, controllerNode: ControllerNode): Promise<void> {
const removed = treeDataProvider.removeController(controllerNode.url, controllerNode.auth, controllerNode.username);
if (removed) {
await treeDataProvider.saveControllers();
}
}
/**
* Throttles actions to avoid bug where on clicking in tree, action gets called twice
* instead of once. Any right-click action is safe, just the default on-click action in a tree
*/
function runThrottledAction(id: string, action: () => void) {
let timer = throttleTimers[id];
if (!timer) {
throttleTimers[id] = timer = setTimeout(() => {
action();
clearTimeout(timer);
throttleTimers[id] = undefined;
}, 150);
}
// else ignore this as we got an identical action in the last 150ms
}

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/sql/azdata.d.ts'/>
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
/// <reference path='../../../../src/vscode-dts/vscode.d.ts'/>
/// <reference types='@types/node'/>