From 5528a90eaf345f13fe220493f84706c7b3959cff Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Wed, 14 Aug 2019 10:17:01 -0700 Subject: [PATCH] Initial work for BDC Dashboard (#6646) --- extensions/big-data-cluster/package.nls.json | 3 +- .../resources/dark/copy_inverse.svg | 1 + .../big-data-cluster/resources/light/copy.svg | 1 + .../controller/clusterControllerApi.ts | 148 ++++++++---- .../dialog/addControllerDialog.ts | 10 +- .../src/bigDataCluster/dialog/bdcDashboard.ts | 223 ++++++++++++++++++ .../dialog/bdcDashboardModel.ts | 47 ++++ .../tree/controllerTreeDataProvider.ts | 4 +- .../bigDataCluster/tree/controllerTreeNode.ts | 7 +- .../src/bigDataCluster/utils.ts | 10 +- extensions/big-data-cluster/src/extension.ts | 17 +- 11 files changed, 407 insertions(+), 64 deletions(-) create mode 100644 extensions/big-data-cluster/resources/dark/copy_inverse.svg create mode 100644 extensions/big-data-cluster/resources/light/copy.svg create mode 100644 extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts create mode 100644 extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts diff --git a/extensions/big-data-cluster/package.nls.json b/extensions/big-data-cluster/package.nls.json index 2106fe603b..f69a8e2d89 100644 --- a/extensions/big-data-cluster/package.nls.json +++ b/extensions/big-data-cluster/package.nls.json @@ -4,5 +4,6 @@ "text.sqlServerBigDataClustersEnabledConfig.description":"Whether the SQL Server big data clusters data explorer view is enabled", "command.addController.title": "Connect to Controller", "command.deleteController.title" : "Delete", - "command.refreshController.title" : "Refresh" + "command.refreshController.title" : "Refresh", + "command.manageController.title" : "Manage", } diff --git a/extensions/big-data-cluster/resources/dark/copy_inverse.svg b/extensions/big-data-cluster/resources/dark/copy_inverse.svg new file mode 100644 index 0000000000..fa65571127 --- /dev/null +++ b/extensions/big-data-cluster/resources/dark/copy_inverse.svg @@ -0,0 +1 @@ +copy_inverse \ No newline at end of file diff --git a/extensions/big-data-cluster/resources/light/copy.svg b/extensions/big-data-cluster/resources/light/copy.svg new file mode 100644 index 0000000000..91692de258 --- /dev/null +++ b/extensions/big-data-cluster/resources/light/copy.svg @@ -0,0 +1 @@ +copy \ No newline at end of file diff --git a/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts b/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts index 3fedafb0fd..b959384122 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as request from 'request'; -import { ClusterRouterApi, Authentication } from './apiGenerated'; +import { ClusterRouterApi, Authentication, DefaultApi, EndpointModel } from './apiGenerated'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); class AuthConfiguration implements Authentication { public username: string = ''; @@ -43,40 +45,47 @@ export async function getEndPoints( url = adjustUrl(url); let endPointApi = new ClusterApiWrapper(username, password, url, !!ignoreSslVerification); - let controllerResponse: IEndPointsResponse = undefined; - let controllerError: IControllerError = undefined; - let request = { - url: url, - username: username, - password: password, - method: 'endPointsGet' - }; - try { let result = await endPointApi.endpointsGet(clusterName); - controllerResponse = { + return { response: result.response as IHttpResponse, - endPoints: result.body as IEndPoint[], - request + endPoints: result.body as EndpointModel[] }; - return controllerResponse; } catch (error) { - if ('response' in error) { - let err: IEndPointsResponse = error as IEndPointsResponse; - let errCode = `${err.response.statusCode || ''}`; - let errMessage = err.response.statusMessage; - let errUrl = err.response.url; - controllerError = { - address: errUrl, - code: errCode, - errno: errCode, - message: errMessage, - name: undefined - }; - } else { - controllerError = error as IControllerError; - } - throw Object.assign(controllerError, { request }) as IControllerError; + throw new ControllerError(error, localize('bdc.error.getEndPoints', "Error retrieving endpoints from {0}", url)); + } +} + +class DefaultApiWrapper extends DefaultApi { + constructor(basePathOrUsername: string, password?: string, basePath?: string, ignoreSslVerification?: boolean) { + super(basePathOrUsername, password, basePath); + this.authentications.default = new AuthConfiguration(!!ignoreSslVerification); + } +} + +export async function getClusterStatus( + clusterName: string, + url: string, + username: string, + password: string, + ignoreSslVerification?: boolean +): Promise { + + if (!url) { + return undefined; + } + + url = adjustUrl(url); + const defaultApi = new DefaultApiWrapper(username, password, url, ignoreSslVerification); + + try { + const clusterStatus = await defaultApi.getClusterStatus('', '', clusterName); + return { + response: clusterStatus.response, + clusterStatus: clusterStatus.body + }; + } catch (error) { + throw new ControllerError(error, localize('bdc.error.getClusterStatus', "Error retrieving cluster status from {0}", url)); } } @@ -95,7 +104,7 @@ function adjustUrl(url: string): string { return url; } -export interface IEndPointsRequest { +export interface IClusterRequest { url: string; username: string; password?: string; @@ -104,7 +113,12 @@ export interface IEndPointsRequest { export interface IEndPointsResponse { response: IHttpResponse; - endPoints: IEndPoint[]; + endPoints: EndpointModel[]; +} + +export interface IClusterStatusResponse { + response: IHttpResponse; + clusterStatus: IBdcStatus; } export interface IHttpResponse { @@ -114,17 +128,65 @@ export interface IHttpResponse { statusMessage?: string; } -export interface IEndPoint { - name?: string; - description?: string; - endpoint?: string; - ip?: string; - port?: number; +export interface IBdcStatus { + name: string; + status: IStatus; + services: IServiceStatus[]; } -export interface IControllerError extends Error { - code?: string; - errno?: string; - message: string; - request?: any; +export interface IServiceStatus { + serviceName: string; + status: IStatus; + resources: IResourceStatus[]; +} + +export interface IResourceStatus { + resourceName: string; + status: IStatus; + instances?: IInstanceStatus[]; +} + +export interface IInstanceStatus { + instanceName: string; + status: IStatus; + dashboards: IDashboard[]; +} + +export interface IDashboard { + nodeMetricsUrl: string; + sqlMetricsUrl: string; + logsUrl: string; +} + +export interface IStatus { + state: string; + healthStatus: string; + details?: string; +} + +export class ControllerError extends Error { + public code?: string; + public errno?: string; + public reason?: string; + public address?: 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 || ''; + } + + // The body message contains more specific information about the failure + if (error.body && error.body.reason) { + this.message += ` - ${error.body.reason}`; + } + } } diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/addControllerDialog.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/addControllerDialog.ts index 16421ebefc..e4cc3a7f8f 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/addControllerDialog.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/addControllerDialog.ts @@ -7,10 +7,11 @@ import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; -import { IEndPoint, IControllerError, getEndPoints } from '../controller/clusterControllerApi'; +import { ControllerError, getEndPoints } from '../controller/clusterControllerApi'; import { ControllerTreeDataProvider } from '../tree/controllerTreeDataProvider'; import { TreeNode } from '../tree/treeNode'; import { showErrorMessage } from '../utils'; +import { EndpointModel } from '../controller/apiGenerated'; const localize = nls.loadMessageBundle(); @@ -39,7 +40,7 @@ export class AddControllerDialogModel { // We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect) let response = await getEndPoints(clusterName, url, username, password, true); if (response && response.endPoints) { - let masterInstance: IEndPoint = undefined; + let masterInstance: EndpointModel = undefined; if (response.endPoints) { masterInstance = response.endPoints.find(e => e.name && e.name === 'sql-server-master'); } @@ -55,10 +56,9 @@ export class AddControllerDialogModel { throw error; } } - } - public async onError(error: IControllerError): Promise { + public async onError(error: ControllerError): Promise { // implement } @@ -171,7 +171,7 @@ export class AddControllerDialog { } catch (error) { showErrorMessage(error); if (this.model && this.model.onError) { - await this.model.onError(error as IControllerError); + await this.model.onError(error as ControllerError); } return false; } diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts new file mode 100644 index 0000000000..d76d32ee43 --- /dev/null +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboard.ts @@ -0,0 +1,223 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +import { BdcDashboardModel } from './bdcDashboardModel'; + +const localize = nls.loadMessageBundle(); + +export class BdcDashboard { + + private dashboard: azdata.workspace.ModelViewEditor; + + private copyIconPath: { light: string, dark: string }; + private refreshIconPath: { light: string, dark: string }; + + constructor(private title: string, private model: BdcDashboardModel, context: vscode.ExtensionContext) { + this.copyIconPath = { + light: context.asAbsolutePath('resources/light/copy.svg'), + dark: context.asAbsolutePath('resources/dark/copy_inverse.svg') + }; + + this.refreshIconPath = { + light: context.asAbsolutePath('resources/light/refresh.svg'), + dark: context.asAbsolutePath('resources/dark/refresh_inverse.svg') + }; + } + + public showDashboard(): void { + this.createDashboard(); + this.dashboard.openEditor(); + } + + private createDashboard(): void { + this.dashboard = azdata.workspace.createModelViewEditor(this.title, { retainContextWhenHidden: true, supportsSave: false }); + this.dashboard.registerContent(async (view: azdata.ModelView) => { + + const dashboardRootContainer = view.modelBuilder.flexContainer().withLayout( + { + flexFlow: 'column', + width: '100%', + height: '100%', + alignItems: 'left' + }).component(); + + // ########### + // # TOOLBAR # + // ########### + + // Refresh button + const refreshButton = view.modelBuilder.button() + .withProperties({ + label: localize('bdc.dashboard.refreshButton', "Refresh"), + iconPath: this.refreshIconPath, + height: '50' + }).component(); + + refreshButton.onDidClick(() => this.model.refresh()); + + const toolbarContainer = view.modelBuilder.toolbarContainer().withToolbarItems([{ component: refreshButton }]).component(); + + dashboardRootContainer.addItem(toolbarContainer); + + // ################ + // # CONTENT AREA # + // ################ + + const contentContainer = view.modelBuilder.flexContainer().withLayout( + { + flexFlow: 'column', + width: '100%', + height: '100%', + alignItems: 'left' + }).component(); + + dashboardRootContainer.addItem(contentContainer, { flex: '0 0 100%' }); + + // ############## + // # PROPERTIES # + // ############## + + const propertiesLabel = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.propertiesHeader', "Cluster Properties") }).component(); + contentContainer.addItem(propertiesLabel, { CSSStyles: { 'margin-top': '15px', 'font-size': '20px', 'font-weight': 'bold', 'padding-left': '10px' } }); + + // Row 1 + const row1 = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', height: '30px', alignItems: 'center' }).component(); + // Cluster Name + const clusterNameLabel = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.clusterName', "Cluster Name :") }).component(); + const clusterNameValue = view.modelBuilder.text().withProperties({ value: this.model.clusterName }).component(); + row1.addItem(clusterNameLabel, { CSSStyles: { 'width': '25%', 'user-select': 'text' } }); + row1.addItem(clusterNameValue, { CSSStyles: { 'width': '25%', 'user-select': 'text' } }); + + contentContainer.addItem(row1, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } }); + + // Row 2 + const row2 = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', height: '30px', alignItems: 'center' }).component(); + + // Cluster State + const clusterStateLabel = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.clusterState', "Cluster State :") }).component(); + const clusterStateValue = view.modelBuilder.text().withProperties({ value: this.model.clusterStatus.state }).component(); + row2.addItem(clusterStateLabel, { CSSStyles: { 'width': '25%', 'user-select': 'text' } }); + row2.addItem(clusterStateValue, { CSSStyles: { 'width': '25%', 'user-select': 'text' } }); + + // Health Status + const healthStatusLabel = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.healthStatus', "Health Status :") }).component(); + const healthStatusValue = view.modelBuilder.text().withProperties({ value: this.model.clusterStatus.healthStatus }).component(); + row2.addItem(healthStatusLabel, { CSSStyles: { 'width': '25%', 'user-select': 'text' } }); + row2.addItem(healthStatusValue, { CSSStyles: { 'width': '25%', 'user-select': 'text' } }); + + contentContainer.addItem(row2, { CSSStyles: { 'padding-left': '10px', 'border-bottom': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } }); + + // ############ + // # OVERVIEW # + // ############ + + const overviewLabel = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.overviewHeader', "Cluster Overview") }).component(); + contentContainer.addItem(overviewLabel, { CSSStyles: { 'margin-top': '15px', 'font-size': '20px', 'font-weight': 'bold', 'padding-left': '10px' } }); + + const overviewContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '100%', height: '100%', alignItems: 'left' }).component(); + + // Service Status header row + const serviceStatusHeaderRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); + const nameCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.serviceNameHeader', "Service Name") }).component(); + serviceStatusHeaderRow.addItem(nameCell, { CSSStyles: { 'width': '25%', 'font-weight': 'bold', 'user-select': 'text' } }); + const stateCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.stateHeader', "State") }).component(); + serviceStatusHeaderRow.addItem(stateCell, { CSSStyles: { 'width': '15%', 'font-weight': 'bold', 'user-select': 'text' } }); + const healthStatusCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.healthStatusHeader', "Health Status") }).component(); + serviceStatusHeaderRow.addItem(healthStatusCell, { CSSStyles: { 'width': '15%', 'font-weight': 'bold', 'user-select': 'text' } }); + overviewContainer.addItem(serviceStatusHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } }); + + // Service Status rows + this.model.serviceStatus.forEach(serviceStatus => { + const serviceStatusRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '20px' }).component(); + const nameCell = view.modelBuilder.text().withProperties({ value: serviceStatus.serviceName }).component(); + serviceStatusRow.addItem(nameCell, { CSSStyles: { 'width': '25%', 'user-select': 'text' } }); + const stateCell = view.modelBuilder.text().withProperties({ value: serviceStatus.status.state }).component(); + serviceStatusRow.addItem(stateCell, { CSSStyles: { 'width': '15%', 'user-select': 'text' } }); + const healthStatusCell = view.modelBuilder.text().withProperties({ value: serviceStatus.status.healthStatus }).component(); + serviceStatusRow.addItem(healthStatusCell, { CSSStyles: { 'width': '15%', 'user-select': 'text' } }); + + overviewContainer.addItem(serviceStatusRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } }); + }); + + contentContainer.addItem(overviewContainer); + + // ##################### + // # SERVICE ENDPOINTS # + // ##################### + + const endpointsLabel = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.endpointsLabel', "Service Endpoints") }).component(); + contentContainer.addItem(endpointsLabel, { CSSStyles: { 'font-size': '20px', 'font-weight': 'bold', 'padding-left': '10px' } }); + + const endpointsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '100%', height: '100%', alignItems: 'left' }).component(); + + // Service endpoints header row + const endpointsHeaderRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); + const endpointsServiceNameHeaderCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.serviceHeader', "Service") }).component(); + endpointsHeaderRow.addItem(endpointsServiceNameHeaderCell, { CSSStyles: { 'width': '25%', 'font-weight': 'bold', 'user-select': 'text' } }); + const endpointsEndpointHeaderCell = view.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.endpointHeader', "Endpoint") }).component(); + endpointsHeaderRow.addItem(endpointsEndpointHeaderCell, { CSSStyles: { 'width': '15%', 'font-weight': 'bold', 'user-select': 'text' } }); + endpointsContainer.addItem(endpointsHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } }); + + // Service endpoints rows + this.model.serviceEndpoints.forEach(endpointInfo => { + const endPointRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '20px' }).component(); + const nameCell = view.modelBuilder.text().withProperties({ value: getFriendlyEndpointNames(endpointInfo.serviceName) }).component(); + endPointRow.addItem(nameCell, { CSSStyles: { 'width': '25%', 'user-select': 'text' } }); + if (endpointInfo.isHyperlink) { + const linkCell = view.modelBuilder.hyperlink().withProperties({ label: endpointInfo.hyperlink, url: endpointInfo.hyperlink }).component(); + endPointRow.addItem(linkCell, { CSSStyles: { 'width': '35%', 'color': '#0078d4', 'text-decoration': 'underline', 'overflow': 'hidden' } }); + } + else { + const endpointCell = view.modelBuilder.text().withProperties({ value: endpointInfo.ipAddress + ':' + endpointInfo.port }).component(); + endPointRow.addItem(endpointCell, { CSSStyles: { 'width': '35%', 'user-select': 'text', 'overflow': 'hidden' } }); + } + const copyValueCell = view.modelBuilder.button().component(); + copyValueCell.iconPath = this.copyIconPath; + copyValueCell.onDidClick(() => { + vscode.env.clipboard.writeText(endpointInfo.hyperlink); + }); + copyValueCell.title = localize("bdc.dashboard.copyTitle", "Copy"); + copyValueCell.iconHeight = '14px'; + copyValueCell.iconWidth = '14px'; + endPointRow.addItem(copyValueCell, { CSSStyles: { 'width': '5%', 'padding-left': '10px' } }); + + endpointsContainer.addItem(endPointRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } }); + }); + + contentContainer.addItem(endpointsContainer); + + await view.initializeModel(dashboardRootContainer); + }); + } +} + +function getFriendlyEndpointNames(name: string): string { + let friendlyName: string = name; + switch (name) { + case 'app-proxy': + friendlyName = localize('bdc.dashboard.appproxy', "Application Proxy"); + break; + case 'controller': + friendlyName = localize('bdc.dashboard.controller', "Cluster Management Service"); + break; + case 'gateway': + friendlyName = localize('bdc.dashboard.gateway', "HDFS and Spark"); + break; + case 'management-proxy': + friendlyName = localize('bdc.dashboard.managementproxy', "Management Proxy"); + break; + case 'mgmtproxy': + friendlyName = localize('bdc.dashboard.mgmtproxy', "Management Proxy"); + break; + default: + break; + } + return friendlyName; +} diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts new file mode 100644 index 0000000000..4dfa2b47d5 --- /dev/null +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/bdcDashboardModel.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IBdcStatus, IStatus, IServiceStatus } from '../controller/clusterControllerApi'; + +export class BdcDashboardModel { + + private _clusterStatus: IBdcStatus; + + constructor(public clusterName: string, url: string, username: string, password: string) { + + } + + public get clusterStatus(): IStatus { + return { healthStatus: 'Warning detected', state: 'ready' }; + } + + public get serviceStatus(): IServiceStatus[] { + return [ + { serviceName: 'SQL Server', status: { state: 'Ready', healthStatus: 'Warning' }, resources: undefined }, + { serviceName: 'HDFS', status: { state: 'Ready', healthStatus: 'Healthy' }, resources: undefined }, + { serviceName: 'Spark', status: { state: 'Ready', healthStatus: 'Healthy' }, resources: undefined }, + { serviceName: 'Control', status: { state: 'Ready', healthStatus: 'Healthy' }, resources: undefined }, + { serviceName: 'Gateway', status: { state: 'Ready', healthStatus: 'Healthy' }, resources: undefined }, + { serviceName: 'App', status: { state: 'Ready', healthStatus: 'Healthy' }, resources: undefined } + ]; + } + + public get serviceEndpoints(): { serviceName: string, hyperlink?: string, isHyperlink: boolean, ipAddress?: string, port?: string }[] { + return [ + { serviceName: 'SQL Server Master Instance', ipAddress: '10.91.134.112', port: '31433', isHyperlink: false }, + { serviceName: 'Controller', ipAddress: '10.91.134.112', port: '31433', isHyperlink: false }, + { serviceName: 'HDFS/Spark Gateway', ipAddress: '10.91.134.112', port: '31433', isHyperlink: false }, + { serviceName: 'Spark Job Management', hyperlink: 'https://10.91.134.112:30443/gateway/default/yarn', isHyperlink: true }, + { serviceName: 'Grafana Dashboard', hyperlink: 'https://10.91.134.112/grafana/d/wZx3OUdmz', isHyperlink: true }, + { serviceName: 'Kibana Dashboard', hyperlink: 'https://10.91.134.112/kibana/app/kibana#/discover', isHyperlink: true }, + ]; + } + + public refresh(): void { + + } +} diff --git a/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeDataProvider.ts b/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeDataProvider.ts index 10c9784d88..88878d022f 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeDataProvider.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/tree/controllerTreeDataProvider.ts @@ -11,9 +11,9 @@ import { TreeNode } from './treeNode'; import { IControllerTreeChangeHandler } from './controllerTreeChangeHandler'; import { AddControllerNode } from './addControllerNode'; import { ControllerRootNode, ControllerNode } from './controllerTreeNode'; -import { IEndPoint } from '../controller/clusterControllerApi'; import { showErrorMessage } from '../utils'; import { LoadingControllerNode } from './loadingControllerNode'; +import { EndpointModel } from '../controller/apiGenerated'; const CredentialNamespace = 'clusterControllerCredentials'; @@ -63,7 +63,7 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider { runThrottledAction(AddControllerCommand, () => addBdcController(treeDataProvider, node)); }); @@ -51,6 +54,12 @@ function registerCommands(treeDataProvider: ControllerTreeDataProvider): void { } treeDataProvider.notifyNodeChanged(node); }); + + vscode.commands.registerCommand(ManageControllerCommand, async (node: ControllerNode) => { + const title: string = `${localize('bdc.dashboard.title', "Big Data Cluster Dashboard -")} ${ControllerNode.toIpAndPort(node.url)} ${localize('bdc.Dash', "-")} ${node.clusterName}`; + const dashboard: BdcDashboard = new BdcDashboard(title, new BdcDashboardModel(node.clusterName, node.url, node.username, node.password), context); + dashboard.showDashboard(); + }); } function addBdcController(treeDataProvider: ControllerTreeDataProvider, node?: TreeNode): void { @@ -90,8 +99,6 @@ function deleteControllerInternal(treeDataProvider: ControllerTreeDataProvider, } } - - /** * 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 @@ -106,4 +113,4 @@ function runThrottledAction(id: string, action: () => void) { }, 150); } // else ignore this as we got an identical action in the last 150ms -} \ No newline at end of file +}