mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Remove all Big Data Cluster features (#21369)
This commit is contained in:
44
extensions/big-data-cluster/src/bdc.d.ts
vendored
44
extensions/big-data-cluster/src/bdc.d.ts
vendored
@@ -1,44 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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
|
||||
};
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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
@@ -1,455 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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())
|
||||
}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,468 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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];
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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()));
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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");
|
||||
@@ -1,10 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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}`;
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
@@ -1,25 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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'/>
|
||||
Reference in New Issue
Block a user