mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Mount HDFS Dialog: basic support (#7580)
Implemented in this PR - New base dialog for anything needing to work with the controller. This is important since going from SQL -> Controller we "should" have the right permissions but aren't guaranteed - Support for Mount HDFS via a dialog. Includes basic polling for success/failure, but have to give up after 2.5min as mounting could take hours. By then it's assumed to be successful since server-side has 2min timeout built in. Not implemented in this PR - Script as Notebook button. This should convert the inputs to a set of cells in a notebook so users can run things themselves - Updates based on PM / UX reviews. I think we'll need a round of feedback before completing this work.
This commit is contained in:
@@ -72,3 +72,6 @@ export namespace cssStyles {
|
||||
}
|
||||
|
||||
export type AuthType = 'integrated' | 'basic';
|
||||
|
||||
export const clusterEndpointsProperty = 'clusterEndpoints';
|
||||
export const controllerEndpointName = 'controller';
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as request from 'request';
|
||||
|
||||
import { authenticateKerberos, getHostAndPortFromEndpoint } from '../auth';
|
||||
import { BdcRouterApi, Authentication, EndpointModel, BdcStatusModel } from './apiGenerated';
|
||||
import { BdcRouterApi, Authentication, EndpointModel, BdcStatusModel, DefaultApi } from './apiGenerated';
|
||||
import { TokenRouterApi } from './clusterApiGenerated2';
|
||||
import { AuthType } from '../constants';
|
||||
import * as nls from 'vscode-nls';
|
||||
@@ -73,6 +74,16 @@ class BdcApiWrapper extends BdcRouterApi {
|
||||
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 {
|
||||
|
||||
@@ -170,6 +181,39 @@ export class ClusterController {
|
||||
throw new ControllerError(error, localize('bdc.error.getBdcStatus', "Error retrieving BDC status from {0}", this._url));
|
||||
}
|
||||
}
|
||||
|
||||
public async mountHdfs(mountPath: string, remoteUri: string, credentials: {}): Promise<MountResponse> {
|
||||
let auth = await this.authPromise;
|
||||
const api = new DefaultApiWrapper(this.username, this.password, this._url, auth);
|
||||
|
||||
try {
|
||||
const mountStatus = await api.createMount('', '', remoteUri, mountPath, credentials);
|
||||
return {
|
||||
response: mountStatus.response,
|
||||
status: mountStatus.body
|
||||
};
|
||||
} catch (error) {
|
||||
// TODO handle 401 by reauthenticating
|
||||
throw new ControllerError(error, localize('bdc.error.mountHdfs', "Error creating mount"));
|
||||
}
|
||||
}
|
||||
|
||||
public async getMountStatus(mountPath?: string): Promise<MountStatusResponse> {
|
||||
let auth = await this.authPromise;
|
||||
const api = new DefaultApiWrapper(this.username, this.password, this._url, auth);
|
||||
|
||||
try {
|
||||
const mountStatus = await api.listMounts('', '', mountPath);
|
||||
return {
|
||||
response: mountStatus.response,
|
||||
mount: mountStatus.body ? JSON.parse(mountStatus.body) : undefined
|
||||
};
|
||||
} catch (error) {
|
||||
// TODO handle 401 by reauthenticating
|
||||
throw new ControllerError(error, localize('bdc.error.mountHdfs', "Error creating mount"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Fixes missing protocol and wrong character for port entered by user
|
||||
@@ -203,6 +247,28 @@ export interface IBdcStatusResponse {
|
||||
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 interface IHttpResponse {
|
||||
method?: string;
|
||||
url?: string;
|
||||
|
||||
@@ -10,7 +10,6 @@ import * as nls from 'vscode-nls';
|
||||
import { ClusterController, ControllerError } from '../controller/clusterControllerApi';
|
||||
import { ControllerTreeDataProvider } from '../tree/controllerTreeDataProvider';
|
||||
import { TreeNode } from '../tree/treeNode';
|
||||
import { showErrorMessage } from '../utils';
|
||||
import { AuthType } from '../constants';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
@@ -177,7 +176,7 @@ export class AddControllerDialog {
|
||||
],
|
||||
title: ''
|
||||
}]).withLayout({ width: '100%' }).component();
|
||||
|
||||
this.onAuthChanged();
|
||||
await view.initializeModel(formModel);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ClusterController, ControllerError, MountInfo, MountState } from '../controller/clusterControllerApi';
|
||||
import { AuthType } from '../constants';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const basicAuthDisplay = localize('basicAuthName', "Basic");
|
||||
const integratedAuthDisplay = localize('integratedAuthName', "Windows Authentication");
|
||||
|
||||
function getAuthCategory(name: AuthType): azdata.CategoryValue {
|
||||
if (name === 'basic') {
|
||||
return { name: name, displayName: basicAuthDisplay };
|
||||
}
|
||||
return { name: name, displayName: integratedAuthDisplay };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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': {} };
|
||||
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 = localize('mount.err.formatting', "Bad formatting of credentials at {0}", 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 DialogProperties {
|
||||
url?: string;
|
||||
auth?: AuthType;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface MountHdfsProperties extends DialogProperties {
|
||||
hdfsPath?: string;
|
||||
remoteUri?: string;
|
||||
credentials?: string;
|
||||
}
|
||||
|
||||
abstract class HdfsDialogModelBase<T extends DialogProperties> {
|
||||
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<void> {
|
||||
try {
|
||||
this.props = props;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract handleCompleted(): Promise<void>;
|
||||
|
||||
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, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class MountHdfsDialogModel extends HdfsDialogModelBase<MountHdfsProperties> {
|
||||
private credentials: {};
|
||||
|
||||
constructor(props: MountHdfsProperties) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected async handleCompleted(): Promise<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(localize('err.controller.username.required', "Username is required"));
|
||||
} else if (!this.props.password) {
|
||||
throw new Error(localize('err.controller.password.required', "Password is required"));
|
||||
}
|
||||
}
|
||||
// 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 = this.createController();
|
||||
let response = await controller.getEndPoints();
|
||||
if (response && response.endPoints) {
|
||||
if (this._canceled) {
|
||||
return;
|
||||
}
|
||||
//
|
||||
azdata.tasks.startBackgroundOperation(
|
||||
{
|
||||
connection: undefined,
|
||||
displayName: localize('mount.task.name', "Mounting HDFS folder on path {0}", 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, localize('mount.task.submitted', "Mount creation has started"));
|
||||
|
||||
// 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 ? localize('mount.task.complete', "Mounting HDFS folder is complete")
|
||||
: localize('mount.task.inprogress', "Mounting is likely to complete, check back later to verify");
|
||||
op.updateStatus(azdata.TaskStatus.Succeeded, msg);
|
||||
} catch (error) {
|
||||
const errMsg = localize('mount.task.error', "Error mounting folder: {0}", (error instanceof Error ? error.message : 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 : localize('mount.error.unknown', "Unknown error occurred during the mount process")));
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class HdfsDialogBase<T extends DialogProperties> {
|
||||
|
||||
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;
|
||||
|
||||
constructor(private title: string, protected model: HdfsDialogModelBase<T>) {
|
||||
}
|
||||
|
||||
public showDialog(): void {
|
||||
this.createDialog();
|
||||
azdata.window.openDialog(this.dialog);
|
||||
}
|
||||
|
||||
private createDialog(): void {
|
||||
this.dialog = azdata.window.createModelViewDialog(this.title);
|
||||
this.dialog.registerContent(async view => {
|
||||
this.uiModelBuilder = view.modelBuilder;
|
||||
|
||||
this.urlInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
placeHolder: localize('textUrlLower', "url"),
|
||||
value: this.model.props.url,
|
||||
}).component();
|
||||
this.urlInputBox.enabled = false;
|
||||
|
||||
this.authDropdown = this.uiModelBuilder.dropDown().withProperties({
|
||||
values: this.model.authCategories,
|
||||
value: this.model.authCategory,
|
||||
editable: false,
|
||||
}).component();
|
||||
this.authDropdown.onValueChanged(e => this.onAuthChanged());
|
||||
this.usernameInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
placeHolder: localize('textUsernameLower', "username"),
|
||||
value: this.model.props.username
|
||||
}).component();
|
||||
this.passwordInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
placeHolder: localize('textPasswordLower', "password"),
|
||||
inputType: 'password',
|
||||
value: this.model.props.password
|
||||
})
|
||||
.component();
|
||||
|
||||
let connectionSection: azdata.FormComponentGroup = {
|
||||
components: [
|
||||
{
|
||||
component: this.urlInputBox,
|
||||
title: localize('textUrlCapital', "URL"),
|
||||
required: true
|
||||
}, {
|
||||
component: this.authDropdown,
|
||||
title: localize('textAuthCapital', "Authentication type"),
|
||||
required: true
|
||||
}, {
|
||||
component: this.usernameInputBox,
|
||||
title: localize('textUsernameCapital', "Username"),
|
||||
required: false
|
||||
}, {
|
||||
component: this.passwordInputBox,
|
||||
title: localize('textPasswordCapital', "Password"),
|
||||
required: false
|
||||
}
|
||||
],
|
||||
title: localize('hdsf.dialog.connection.section', "Cluster Connection")
|
||||
};
|
||||
let formModel = this.uiModelBuilder.formContainer()
|
||||
.withFormItems([
|
||||
this.getMainSection(),
|
||||
connectionSection
|
||||
]).withLayout({ width: '100%' }).component();
|
||||
this.onAuthChanged();
|
||||
|
||||
await view.initializeModel(formModel);
|
||||
});
|
||||
|
||||
this.dialog.registerCloseValidator(async () => await this.validate());
|
||||
this.dialog.cancelButton.onClick(async () => await this.cancel());
|
||||
this.dialog.okButton.label = localize('hdfs.dialog.ok', "OK");
|
||||
this.dialog.cancelButton.label = localize('hdfs.dialog.cancel', "Cancel");
|
||||
}
|
||||
|
||||
protected abstract getMainSection(): azdata.FormComponentGroup;
|
||||
|
||||
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<boolean>;
|
||||
|
||||
private async cancel(): Promise<void> {
|
||||
if (this.model && this.model.onCancel) {
|
||||
await this.model.onCancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
export class MountHdfsDialog extends HdfsDialogBase<MountHdfsProperties> {
|
||||
private pathInputBox: azdata.InputBoxComponent;
|
||||
private remoteUriInputBox: azdata.InputBoxComponent;
|
||||
private credentialsInputBox: azdata.InputBoxComponent;
|
||||
|
||||
constructor(model: MountHdfsDialogModel) {
|
||||
super(localize('mount.dialog.title', "Mount HDFS Folder"), model);
|
||||
}
|
||||
|
||||
protected getMainSection(): azdata.FormComponentGroup {
|
||||
this.pathInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
value: this.model.props.hdfsPath
|
||||
}).component();
|
||||
this.remoteUriInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
value: this.model.props.remoteUri
|
||||
})
|
||||
.component();
|
||||
this.credentialsInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
inputType: 'password',
|
||||
value: this.model.props.credentials
|
||||
})
|
||||
.component();
|
||||
|
||||
return {
|
||||
components: [
|
||||
{
|
||||
component: this.pathInputBox,
|
||||
title: localize('mount.hdfsPath', "HDFS Path"),
|
||||
required: true
|
||||
}, {
|
||||
component: this.remoteUriInputBox,
|
||||
title: localize('mount.remoteUri', "Remote URI"),
|
||||
required: true
|
||||
}, {
|
||||
component: this.credentialsInputBox,
|
||||
title: localize('mount.credentials', "Credentials"),
|
||||
required: false
|
||||
}
|
||||
],
|
||||
title: localize('mount.main.section', "Mount Configuration")
|
||||
};
|
||||
}
|
||||
|
||||
protected async validate(): Promise<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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,10 @@
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
import * as constants from './constants';
|
||||
|
||||
export enum Endpoint {
|
||||
gateway = 'gateway',
|
||||
@@ -210,3 +211,46 @@ export function getHealthStatusDot(healthStatus?: string): string {
|
||||
return '•';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface RawEndpoint {
|
||||
serviceName: string;
|
||||
description?: string;
|
||||
endpoint?: string;
|
||||
protocol?: string;
|
||||
ipAddress?: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export interface IEndpoint {
|
||||
serviceName: string;
|
||||
description: string;
|
||||
endpoint: string;
|
||||
protocol: string;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
'use strict';
|
||||
|
||||
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';
|
||||
@@ -14,6 +15,8 @@ import { AddControllerDialogModel, AddControllerDialog } from './bigDataCluster/
|
||||
import { ControllerNode } from './bigDataCluster/tree/controllerTreeNode';
|
||||
import { BdcDashboard } from './bigDataCluster/dialog/bdcDashboard';
|
||||
import { BdcDashboardModel } from './bigDataCluster/dialog/bdcDashboardModel';
|
||||
import { MountHdfsDialogModel, MountHdfsProperties, MountHdfsDialog } from './bigDataCluster/dialog/mountHdfsDialog';
|
||||
import { getControllerEndpoint } from './bigDataCluster/utils';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -21,6 +24,9 @@ const AddControllerCommand = 'bigDataClusters.command.addController';
|
||||
const DeleteControllerCommand = 'bigDataClusters.command.deleteController';
|
||||
const RefreshControllerCommand = 'bigDataClusters.command.refreshController';
|
||||
const ManageControllerCommand = 'bigDataClusters.command.manageController';
|
||||
const MountHdfsCommand = 'bigDataClusters.command.mount';
|
||||
|
||||
const endpointNotFoundError = localize('mount.error.endpointNotFound', "Controller endpoint information was not found");
|
||||
|
||||
let throttleTimers: { [key: string]: any } = {};
|
||||
|
||||
@@ -59,6 +65,52 @@ function registerCommands(context: vscode.ExtensionContext, treeDataProvider: Co
|
||||
const dashboard: BdcDashboard = new BdcDashboard(title, new BdcDashboardModel(node.url, node.auth, node.username, node.password));
|
||||
dashboard.showDashboard();
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand(MountHdfsCommand, e => mountHdfs(e).catch(error => {
|
||||
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
|
||||
}));
|
||||
}
|
||||
|
||||
async function mountHdfs(explorerContext?: azdata.ObjectExplorerContext): Promise<void> {
|
||||
let endpoint = await lookupController(explorerContext);
|
||||
if (!endpoint) {
|
||||
vscode.window.showErrorMessage(endpointNotFoundError);
|
||||
return;
|
||||
}
|
||||
let profile = explorerContext.connectionProfile;
|
||||
let mountProps: MountHdfsProperties = {
|
||||
url: endpoint,
|
||||
auth: profile.authenticationType === 'SqlLogin' ? 'basic' : 'integrated',
|
||||
username: profile.userName,
|
||||
password: profile.password,
|
||||
hdfsPath: getHdsfPath(explorerContext.nodeInfo.nodePath)
|
||||
};
|
||||
let dialog = new MountHdfsDialog(new MountHdfsDialogModel(mountProps));
|
||||
dialog.showDialog();
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user