mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Initial work for BDC Dashboard (#6646)
This commit is contained in:
@@ -4,5 +4,6 @@
|
|||||||
"text.sqlServerBigDataClustersEnabledConfig.description":"Whether the SQL Server big data clusters data explorer view is enabled",
|
"text.sqlServerBigDataClustersEnabledConfig.description":"Whether the SQL Server big data clusters data explorer view is enabled",
|
||||||
"command.addController.title": "Connect to Controller",
|
"command.addController.title": "Connect to Controller",
|
||||||
"command.deleteController.title" : "Delete",
|
"command.deleteController.title" : "Delete",
|
||||||
"command.refreshController.title" : "Refresh"
|
"command.refreshController.title" : "Refresh",
|
||||||
|
"command.manageController.title" : "Manage",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>copy_inverse</title><path class="cls-1" d="M3,0V3.36H0V16.1H13V12.73h3V0Zm9,15.19H1.08V4.27H12Zm3-3.36H13V3.36H4V.9H15Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 274 B |
1
extensions/big-data-cluster/resources/light/copy.svg
Normal file
1
extensions/big-data-cluster/resources/light/copy.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>copy</title><path d="M3-.15V3.21H0V15.95H13V12.58h3V-.15ZM12,15H1.08V4.12H12Zm3-3.36H13V3.21h-9V.75H15Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 212 B |
@@ -4,8 +4,10 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as request from 'request';
|
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 {
|
class AuthConfiguration implements Authentication {
|
||||||
public username: string = '';
|
public username: string = '';
|
||||||
@@ -43,40 +45,47 @@ export async function getEndPoints(
|
|||||||
url = adjustUrl(url);
|
url = adjustUrl(url);
|
||||||
let endPointApi = new ClusterApiWrapper(username, password, url, !!ignoreSslVerification);
|
let endPointApi = new ClusterApiWrapper(username, password, url, !!ignoreSslVerification);
|
||||||
|
|
||||||
let controllerResponse: IEndPointsResponse = undefined;
|
|
||||||
let controllerError: IControllerError = undefined;
|
|
||||||
let request = <IEndPointsRequest>{
|
|
||||||
url: url,
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
method: 'endPointsGet'
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result = await endPointApi.endpointsGet(clusterName);
|
let result = await endPointApi.endpointsGet(clusterName);
|
||||||
controllerResponse = <IEndPointsResponse>{
|
return {
|
||||||
response: result.response as IHttpResponse,
|
response: result.response as IHttpResponse,
|
||||||
endPoints: result.body as IEndPoint[],
|
endPoints: result.body as EndpointModel[]
|
||||||
request
|
|
||||||
};
|
};
|
||||||
return controllerResponse;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ('response' in error) {
|
throw new ControllerError(error, localize('bdc.error.getEndPoints', "Error retrieving endpoints from {0}", url));
|
||||||
let err: IEndPointsResponse = error as IEndPointsResponse;
|
}
|
||||||
let errCode = `${err.response.statusCode || ''}`;
|
}
|
||||||
let errMessage = err.response.statusMessage;
|
|
||||||
let errUrl = err.response.url;
|
class DefaultApiWrapper extends DefaultApi {
|
||||||
controllerError = <IControllerError>{
|
constructor(basePathOrUsername: string, password?: string, basePath?: string, ignoreSslVerification?: boolean) {
|
||||||
address: errUrl,
|
super(basePathOrUsername, password, basePath);
|
||||||
code: errCode,
|
this.authentications.default = new AuthConfiguration(!!ignoreSslVerification);
|
||||||
errno: errCode,
|
}
|
||||||
message: errMessage,
|
}
|
||||||
name: undefined
|
|
||||||
};
|
export async function getClusterStatus(
|
||||||
} else {
|
clusterName: string,
|
||||||
controllerError = error as IControllerError;
|
url: string,
|
||||||
}
|
username: string,
|
||||||
throw Object.assign(controllerError, { request }) as IControllerError;
|
password: string,
|
||||||
|
ignoreSslVerification?: boolean
|
||||||
|
): Promise<IClusterStatusResponse> {
|
||||||
|
|
||||||
|
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;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEndPointsRequest {
|
export interface IClusterRequest {
|
||||||
url: string;
|
url: string;
|
||||||
username: string;
|
username: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
@@ -104,7 +113,12 @@ export interface IEndPointsRequest {
|
|||||||
|
|
||||||
export interface IEndPointsResponse {
|
export interface IEndPointsResponse {
|
||||||
response: IHttpResponse;
|
response: IHttpResponse;
|
||||||
endPoints: IEndPoint[];
|
endPoints: EndpointModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IClusterStatusResponse {
|
||||||
|
response: IHttpResponse;
|
||||||
|
clusterStatus: IBdcStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IHttpResponse {
|
export interface IHttpResponse {
|
||||||
@@ -114,17 +128,65 @@ export interface IHttpResponse {
|
|||||||
statusMessage?: string;
|
statusMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEndPoint {
|
export interface IBdcStatus {
|
||||||
name?: string;
|
name: string;
|
||||||
description?: string;
|
status: IStatus;
|
||||||
endpoint?: string;
|
services: IServiceStatus[];
|
||||||
ip?: string;
|
|
||||||
port?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IControllerError extends Error {
|
export interface IServiceStatus {
|
||||||
code?: string;
|
serviceName: string;
|
||||||
errno?: string;
|
status: IStatus;
|
||||||
message: string;
|
resources: IResourceStatus[];
|
||||||
request?: any;
|
}
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,11 @@
|
|||||||
|
|
||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
import * as nls from 'vscode-nls';
|
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 { ControllerTreeDataProvider } from '../tree/controllerTreeDataProvider';
|
||||||
import { TreeNode } from '../tree/treeNode';
|
import { TreeNode } from '../tree/treeNode';
|
||||||
import { showErrorMessage } from '../utils';
|
import { showErrorMessage } from '../utils';
|
||||||
|
import { EndpointModel } from '../controller/apiGenerated';
|
||||||
|
|
||||||
const localize = nls.loadMessageBundle();
|
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)
|
// 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);
|
let response = await getEndPoints(clusterName, url, username, password, true);
|
||||||
if (response && response.endPoints) {
|
if (response && response.endPoints) {
|
||||||
let masterInstance: IEndPoint = undefined;
|
let masterInstance: EndpointModel = undefined;
|
||||||
if (response.endPoints) {
|
if (response.endPoints) {
|
||||||
masterInstance = response.endPoints.find(e => e.name && e.name === 'sql-server-master');
|
masterInstance = response.endPoints.find(e => e.name && e.name === 'sql-server-master');
|
||||||
}
|
}
|
||||||
@@ -55,10 +56,9 @@ export class AddControllerDialogModel {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onError(error: IControllerError): Promise<void> {
|
public async onError(error: ControllerError): Promise<void> {
|
||||||
// implement
|
// implement
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ export class AddControllerDialog {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorMessage(error);
|
showErrorMessage(error);
|
||||||
if (this.model && this.model.onError) {
|
if (this.model && this.model.onError) {
|
||||||
await this.model.onError(error as IControllerError);
|
await this.model.onError(error as ControllerError);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<azdata.TextComponentProperties>({ 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<azdata.TextComponentProperties>({ value: localize('bdc.dashboard.clusterName', "Cluster Name :") }).component();
|
||||||
|
const clusterNameValue = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ 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<azdata.TextComponentProperties>({ value: localize('bdc.dashboard.clusterState', "Cluster State :") }).component();
|
||||||
|
const clusterStateValue = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ 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<azdata.TextComponentProperties>({ value: localize('bdc.dashboard.healthStatus', "Health Status :") }).component();
|
||||||
|
const healthStatusValue = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ 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<azdata.TextComponentProperties>({ 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<azdata.TextComponentProperties>({ 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<azdata.TextComponentProperties>({ 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<azdata.TextComponentProperties>({ 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<azdata.TextComponentProperties>({ value: serviceStatus.serviceName }).component();
|
||||||
|
serviceStatusRow.addItem(nameCell, { CSSStyles: { 'width': '25%', 'user-select': 'text' } });
|
||||||
|
const stateCell = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: serviceStatus.status.state }).component();
|
||||||
|
serviceStatusRow.addItem(stateCell, { CSSStyles: { 'width': '15%', 'user-select': 'text' } });
|
||||||
|
const healthStatusCell = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ 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<azdata.TextComponentProperties>({ 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<azdata.TextComponentProperties>({ 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<azdata.TextComponentProperties>({ 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<azdata.TextComponentProperties>({ value: getFriendlyEndpointNames(endpointInfo.serviceName) }).component();
|
||||||
|
endPointRow.addItem(nameCell, { CSSStyles: { 'width': '25%', 'user-select': 'text' } });
|
||||||
|
if (endpointInfo.isHyperlink) {
|
||||||
|
const linkCell = view.modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({ 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<azdata.TextComponentProperties>({ 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;
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,9 +11,9 @@ import { TreeNode } from './treeNode';
|
|||||||
import { IControllerTreeChangeHandler } from './controllerTreeChangeHandler';
|
import { IControllerTreeChangeHandler } from './controllerTreeChangeHandler';
|
||||||
import { AddControllerNode } from './addControllerNode';
|
import { AddControllerNode } from './addControllerNode';
|
||||||
import { ControllerRootNode, ControllerNode } from './controllerTreeNode';
|
import { ControllerRootNode, ControllerNode } from './controllerTreeNode';
|
||||||
import { IEndPoint } from '../controller/clusterControllerApi';
|
|
||||||
import { showErrorMessage } from '../utils';
|
import { showErrorMessage } from '../utils';
|
||||||
import { LoadingControllerNode } from './loadingControllerNode';
|
import { LoadingControllerNode } from './loadingControllerNode';
|
||||||
|
import { EndpointModel } from '../controller/apiGenerated';
|
||||||
|
|
||||||
const CredentialNamespace = 'clusterControllerCredentials';
|
const CredentialNamespace = 'clusterControllerCredentials';
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
|
|||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
rememberPassword: boolean,
|
rememberPassword: boolean,
|
||||||
masterInstance?: IEndPoint
|
masterInstance?: EndpointModel
|
||||||
): void {
|
): void {
|
||||||
this.removeNonControllerNodes();
|
this.removeNonControllerNodes();
|
||||||
this.root.addControllerNode(clusterName, url, username, password, rememberPassword, masterInstance);
|
this.root.addControllerNode(clusterName, url, username, password, rememberPassword, masterInstance);
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import * as nls from 'vscode-nls';
|
|||||||
import { IControllerTreeChangeHandler } from './controllerTreeChangeHandler';
|
import { IControllerTreeChangeHandler } from './controllerTreeChangeHandler';
|
||||||
import { TreeNode } from './treeNode';
|
import { TreeNode } from './treeNode';
|
||||||
import { IconPath, BdcItemType } from '../constants';
|
import { IconPath, BdcItemType } from '../constants';
|
||||||
import { IEndPoint, IControllerError, getEndPoints } from '../controller/clusterControllerApi';
|
import { getEndPoints } from '../controller/clusterControllerApi';
|
||||||
import { showErrorMessage } from '../utils';
|
import { showErrorMessage } from '../utils';
|
||||||
|
import { EndpointModel } from '../controller/apiGenerated';
|
||||||
|
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
@@ -118,7 +119,7 @@ export class ControllerRootNode extends ControllerTreeNode {
|
|||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
rememberPassword: boolean,
|
rememberPassword: boolean,
|
||||||
masterInstance?: IEndPoint
|
masterInstance?: EndpointModel
|
||||||
): void {
|
): void {
|
||||||
let controllerNode = this.getExistingControllerNode(url, username);
|
let controllerNode = this.getExistingControllerNode(url, username);
|
||||||
if (controllerNode) {
|
if (controllerNode) {
|
||||||
@@ -203,7 +204,7 @@ export class ControllerNode extends ControllerTreeNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static toIpAndPort(url: string): string {
|
public static toIpAndPort(url: string): string {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,17 +26,17 @@ export function generateGuid(): string {
|
|||||||
return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12);
|
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): void {
|
export function showErrorMessage(error: any, prefixText?: string): void {
|
||||||
if (error) {
|
if (error) {
|
||||||
let text: string = undefined;
|
let text: string = prefixText || '';
|
||||||
if (typeof error === 'string') {
|
if (typeof error === 'string') {
|
||||||
text = error as string;
|
text += error as string;
|
||||||
} else if (typeof error === 'object' && error !== null) {
|
} else if (typeof error === 'object' && error !== null) {
|
||||||
let message = error.message;
|
let message = error.message;
|
||||||
let code = error.code || error.errno;
|
let code = error.code || error.errno;
|
||||||
text = (code ? `${code} ` : '') + message;
|
text += `${message}${code ? ` (${code})` : ''}`;
|
||||||
} else {
|
} else {
|
||||||
text = `${error}`;
|
text += `${error}`;
|
||||||
}
|
}
|
||||||
vscode.window.showErrorMessage(text);
|
vscode.window.showErrorMessage(text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,15 @@ import { IconPath } from './bigDataCluster/constants';
|
|||||||
import { TreeNode } from './bigDataCluster/tree/treeNode';
|
import { TreeNode } from './bigDataCluster/tree/treeNode';
|
||||||
import { AddControllerDialogModel, AddControllerDialog } from './bigDataCluster/dialog/addControllerDialog';
|
import { AddControllerDialogModel, AddControllerDialog } from './bigDataCluster/dialog/addControllerDialog';
|
||||||
import { ControllerNode } from './bigDataCluster/tree/controllerTreeNode';
|
import { ControllerNode } from './bigDataCluster/tree/controllerTreeNode';
|
||||||
|
import { BdcDashboard } from './bigDataCluster/dialog/bdcDashboard';
|
||||||
|
import { BdcDashboardModel } from './bigDataCluster/dialog/bdcDashboardModel';
|
||||||
|
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
const AddControllerCommand = 'bigDataClusters.command.addController';
|
const AddControllerCommand = 'bigDataClusters.command.addController';
|
||||||
const DeleteControllerCommand = 'bigDataClusters.command.deleteController';
|
const DeleteControllerCommand = 'bigDataClusters.command.deleteController';
|
||||||
const RefreshControllerCommand = 'bigDataClusters.command.refreshController';
|
const RefreshControllerCommand = 'bigDataClusters.command.refreshController';
|
||||||
|
const ManageControllerCommand = 'bigDataClusters.command.manageController';
|
||||||
|
|
||||||
let throttleTimers: { [key: string]: any } = {};
|
let throttleTimers: { [key: string]: any } = {};
|
||||||
|
|
||||||
@@ -26,7 +29,7 @@ export function activate(extensionContext: vscode.ExtensionContext) {
|
|||||||
let treeDataProvider = new ControllerTreeDataProvider(extensionContext.globalState);
|
let treeDataProvider = new ControllerTreeDataProvider(extensionContext.globalState);
|
||||||
|
|
||||||
registerTreeDataProvider(treeDataProvider);
|
registerTreeDataProvider(treeDataProvider);
|
||||||
registerCommands(treeDataProvider);
|
registerCommands(extensionContext, treeDataProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate() {
|
export function deactivate() {
|
||||||
@@ -36,7 +39,7 @@ function registerTreeDataProvider(treeDataProvider: ControllerTreeDataProvider):
|
|||||||
vscode.window.registerTreeDataProvider('sqlBigDataCluster', treeDataProvider);
|
vscode.window.registerTreeDataProvider('sqlBigDataCluster', treeDataProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerCommands(treeDataProvider: ControllerTreeDataProvider): void {
|
function registerCommands(context: vscode.ExtensionContext, treeDataProvider: ControllerTreeDataProvider): void {
|
||||||
vscode.commands.registerCommand(AddControllerCommand, (node?: TreeNode) => {
|
vscode.commands.registerCommand(AddControllerCommand, (node?: TreeNode) => {
|
||||||
runThrottledAction(AddControllerCommand, () => addBdcController(treeDataProvider, node));
|
runThrottledAction(AddControllerCommand, () => addBdcController(treeDataProvider, node));
|
||||||
});
|
});
|
||||||
@@ -51,6 +54,12 @@ function registerCommands(treeDataProvider: ControllerTreeDataProvider): void {
|
|||||||
}
|
}
|
||||||
treeDataProvider.notifyNodeChanged(node);
|
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 {
|
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
|
* 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
|
* instead of once. Any right-click action is safe, just the default on-click action in a tree
|
||||||
|
|||||||
Reference in New Issue
Block a user