BDC Dashboard WIP (#6812)

* Further work on dashboards

* More work on dashboard

* Undo adding manage to context menu until feature is complete

* Update cluster status name
This commit is contained in:
Charles Gagnon
2019-08-20 16:06:05 -07:00
committed by GitHub
parent 8fbbd5127c
commit b7b01fafd0
11 changed files with 2398 additions and 1522 deletions

View File

@@ -23,6 +23,8 @@ export class IconPath {
public static controllerNode: { dark: string, light: string };
public static folderNode: { dark: string, light: string };
public static sqlMasterNode: { dark: string, light: string };
public static copy: { dark: string, light: string };
public static refresh: { dark: string, light: string };
public static setExtensionContext(extensionContext: vscode.ExtensionContext) {
IconPath.extensionContext = extensionContext;
@@ -38,5 +40,13 @@ export class IconPath {
dark: IconPath.extensionContext.asAbsolutePath('resources/dark/sql_bigdata_cluster_inverse.svg'),
light: IconPath.extensionContext.asAbsolutePath('resources/light/sql_bigdata_cluster.svg')
};
IconPath.copy = {
light: IconPath.extensionContext.asAbsolutePath('resources/light/copy.svg'),
dark: IconPath.extensionContext.asAbsolutePath('resources/dark/copy_inverse.svg')
};
IconPath.refresh = {
light: IconPath.extensionContext.asAbsolutePath('resources/light/refresh.svg'),
dark: IconPath.extensionContext.asAbsolutePath('resources/dark/refresh_inverse.svg')
};
}
}
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as request from 'request';
import { ClusterRouterApi, Authentication, DefaultApi, EndpointModel } from './apiGenerated';
import { BdcRouterApi, Authentication, DefaultApi, EndpointModel, BdcStatusModel } from './apiGenerated';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
@@ -23,15 +23,16 @@ class AuthConfiguration implements Authentication {
}
}
class ClusterApiWrapper extends ClusterRouterApi {
class BdcApiWrapper extends BdcRouterApi {
constructor(basePathOrUsername: string, password?: string, basePath?: string, ignoreSslVerification?: boolean) {
super(basePathOrUsername, password, basePath);
this.authentications.default = new AuthConfiguration(!!ignoreSslVerification);
this.password = password;
this.username = basePathOrUsername;
}
}
export async function getEndPoints(
clusterName: string,
url: string,
username: string,
password: string,
@@ -43,10 +44,10 @@ export async function getEndPoints(
}
url = adjustUrl(url);
let endPointApi = new ClusterApiWrapper(username, password, url, !!ignoreSslVerification);
let endPointApi = new BdcApiWrapper(username, password, url, !!ignoreSslVerification);
try {
let result = await endPointApi.endpointsGet(clusterName);
let result = await endPointApi.endpointsGet();
return {
response: result.response as IHttpResponse,
endPoints: result.body as EndpointModel[]
@@ -56,36 +57,28 @@ export async function getEndPoints(
}
}
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,
export async function getBdcStatus(
url: string,
username: string,
password: string,
ignoreSslVerification?: boolean
): Promise<IClusterStatusResponse> {
): Promise<IBdcStatusResponse> {
if (!url) {
return undefined;
}
url = adjustUrl(url);
const defaultApi = new DefaultApiWrapper(username, password, url, ignoreSslVerification);
const bdcApi = new BdcApiWrapper(username, password, url, ignoreSslVerification);
try {
const clusterStatus = await defaultApi.getClusterStatus('', '', clusterName);
const bdcStatus = await bdcApi.getBdcStatus('', '', /*all*/ true);
return {
response: clusterStatus.response,
clusterStatus: clusterStatus.body
response: bdcStatus.response,
bdcStatus: bdcStatus.body
};
} catch (error) {
throw new ControllerError(error, localize('bdc.error.getClusterStatus', "Error retrieving cluster status from {0}", url));
throw new ControllerError(error, localize('bdc.error.getBdcStatus', "Error retrieving BDC status from {0}", url));
}
}
@@ -116,9 +109,9 @@ export interface IEndPointsResponse {
endPoints: EndpointModel[];
}
export interface IClusterStatusResponse {
export interface IBdcStatusResponse {
response: IHttpResponse;
clusterStatus: IBdcStatus;
bdcStatus: BdcStatusModel;
}
export interface IHttpResponse {
@@ -128,42 +121,6 @@ export interface IHttpResponse {
statusMessage?: string;
}
export interface IBdcStatus {
name: string;
status: IStatus;
services: IServiceStatus[];
}
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;
@@ -183,6 +140,9 @@ export class ControllerError extends Error {
this.message += `${error.response.statusMessage ? ` - ${error.response.statusMessage}` : ''}` || '';
this.address = error.response.url || '';
}
else if (error.message) {
this.message += ` - ${error.message}`;
}
// The body message contains more specific information about the failure
if (error.body && error.body.reason) {

View File

@@ -7,7 +7,7 @@
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { ControllerError, getEndPoints } from '../controller/clusterControllerApi';
import { getEndPoints, ControllerError } from '../controller/clusterControllerApi';
import { ControllerTreeDataProvider } from '../tree/controllerTreeDataProvider';
import { TreeNode } from '../tree/treeNode';
import { showErrorMessage } from '../utils';
@@ -38,7 +38,7 @@ export class AddControllerDialogModel {
public async onComplete(clusterName: string, url: string, username: string, password: string, rememberPassword: boolean): Promise<void> {
try {
// 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(url, username, password, true);
if (response && response.endPoints) {
let masterInstance: EndpointModel = undefined;
if (response.endPoints) {
@@ -56,6 +56,7 @@ export class AddControllerDialogModel {
throw error;
}
}
}
public async onError(error: ControllerError): Promise<void> {

View File

@@ -5,30 +5,33 @@
'use strict';
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { BdcDashboardModel } from './bdcDashboardModel';
import { IconPath } from '../constants';
import { BdcServiceStatusPage } from './bdcServiceStatusPage';
import { BdcDashboardOverviewPage } from './bdcDashboardOverviewPage';
import { EndpointModel, BdcStatusModel, ServiceStatusModel } from '../controller/apiGenerated';
const localize = nls.loadMessageBundle();
const navWidth = '175px';
export class BdcDashboard {
private dashboard: azdata.workspace.ModelViewEditor;
private copyIconPath: { light: string, dark: string };
private refreshIconPath: { light: string, dark: string };
private initialized: boolean = false;
private serviceTabsCreated: boolean = false;
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')
};
private modelView: azdata.ModelView;
private mainAreaContainer: azdata.FlexContainer;
private navContainer: azdata.FlexContainer;
private currentPage: azdata.FlexContainer;
this.refreshIconPath = {
light: context.asAbsolutePath('resources/light/refresh.svg'),
dark: context.asAbsolutePath('resources/dark/refresh_inverse.svg')
};
constructor(private title: string, private model: BdcDashboardModel) {
this.model.onDidUpdateEndpoints(endpoints => this.handleEndpointsUpdate(endpoints));
this.model.onDidUpdateBdcStatus(bdcStatus => this.handleBdcStatusUpdate(bdcStatus));
}
public showDashboard(): void {
@@ -38,9 +41,9 @@ export class BdcDashboard {
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(
this.dashboard.registerContent(async (modelView: azdata.ModelView) => {
this.modelView = modelView;
const rootContainer = modelView.modelBuilder.flexContainer().withLayout(
{
flexFlow: 'column',
width: '100%',
@@ -53,171 +56,129 @@ export class BdcDashboard {
// ###########
// Refresh button
const refreshButton = view.modelBuilder.button()
const refreshButton = modelView.modelBuilder.button()
.withProperties({
label: localize('bdc.dashboard.refreshButton', "Refresh"),
iconPath: this.refreshIconPath,
iconPath: IconPath.refresh,
height: '50'
}).component();
refreshButton.onDidClick(() => this.model.refresh());
const toolbarContainer = view.modelBuilder.toolbarContainer().withToolbarItems([{ component: refreshButton }]).component();
const toolbarContainer = modelView.modelBuilder.toolbarContainer().withToolbarItems([{ component: refreshButton }]).component();
dashboardRootContainer.addItem(toolbarContainer);
rootContainer.addItem(toolbarContainer, { flex: '0 0 auto' });
// ################
// # CONTENT AREA #
// ################
// #############
// # MAIN AREA #
// #############
const contentContainer = view.modelBuilder.flexContainer().withLayout(
this.mainAreaContainer = modelView.modelBuilder.flexContainer().withLayout(
{
flexFlow: 'column',
flexFlow: 'row',
width: '100%',
height: '100%',
alignItems: 'left'
}).component();
dashboardRootContainer.addItem(contentContainer, { flex: '0 0 100%' });
rootContainer.addItem(this.mainAreaContainer, { flex: '0 0 100%' });
// ##############
// # PROPERTIES #
// ##############
// #################
// # NAV CONTAINER #
// #################
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' } });
this.navContainer = modelView.modelBuilder.flexContainer().withLayout(
{
flexFlow: 'column',
width: navWidth,
height: '100%',
alignItems: 'left'
}
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' } });
).component();
endpointsContainer.addItem(endPointRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } });
this.mainAreaContainer.addItem(this.navContainer, { flex: `0 0 ${navWidth}`, CSSStyles: { 'padding-left': '10px', 'border-right': 'solid 1px #ccc' } });
// Overview nav item - this will be the initial page
const overviewNavItem = modelView.modelBuilder.divContainer().withLayout({ width: navWidth, height: '30px' }).component();
overviewNavItem.addItem(modelView.modelBuilder.text().withProperties({ value: localize('bdc.dashboard.overviewNavTitle', 'Big data cluster overview') }).component(), { CSSStyles: { 'user-select': 'text' } });
const overviewPage = new BdcDashboardOverviewPage(this.model).create(modelView);
this.currentPage = overviewPage;
this.mainAreaContainer.addItem(overviewPage, { flex: '0 0 100%' });
overviewNavItem.onDidClick(() => {
this.mainAreaContainer.removeItem(this.currentPage);
this.mainAreaContainer.addItem(overviewPage, { flex: '0 0 100%' });
this.currentPage = overviewPage;
});
this.navContainer.addItem(overviewNavItem, { flex: '0 0 auto' });
contentContainer.addItem(endpointsContainer);
await modelView.initializeModel(rootContainer);
await view.initializeModel(dashboardRootContainer);
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);
});
}
private handleEndpointsUpdate(endpoints: EndpointModel[]): void {
if (!this.initialized || !endpoints) {
return;
}
}
private handleBdcStatusUpdate(bdcStatus: BdcStatusModel): void {
if (!this.initialized || !bdcStatus) {
return;
}
this.createServiceNavTabs(bdcStatus.services);
}
/**
* Helper to create the navigation tabs for the services once the status has been loaded
*/
private createServiceNavTabs(services: ServiceStatusModel[]): void {
if (this.initialized && !this.serviceTabsCreated && services) {
// Add a nav item for each service
services.forEach(s => {
const navItem = createServiceNavTab(this.modelView.modelBuilder, getFriendlyServiceName(s.serviceName));
const serviceStatusPage = new BdcServiceStatusPage(s.serviceName, this.model, this.modelView).container;
navItem.onDidClick(() => {
this.mainAreaContainer.removeItem(this.currentPage);
this.mainAreaContainer.addItem(serviceStatusPage);
this.currentPage = serviceStatusPage;
});
this.navContainer.addItem(navItem, { flex: '0 0 auto' });
});
this.serviceTabsCreated = true;
}
}
}
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;
function createServiceNavTab(modelBuilder: azdata.ModelBuilder, serviceName: string): azdata.DivContainer {
const navItem = modelBuilder.divContainer().withLayout({ width: navWidth, height: '30px' }).component();
navItem.addItem(modelBuilder.text().withProperties({ value: serviceName }).component(), { CSSStyles: { 'user-select': 'text' } });
return navItem;
}
function getFriendlyServiceName(serviceName: string): string {
serviceName = serviceName || '';
switch (serviceName.toLowerCase()) {
case 'sql':
return localize('bdc.dashboard.sql', "SQL Server");
case 'hdfs':
return localize('bdc.dashboard.hdfs', "HDFS");
case 'spark':
return localize('bdc.dashboard.spark', "Spark");
case 'control':
return localize('bdc.dashboard.control', "Control");
case 'gateway':
return localize('bdc.dashboard.gateway', "Gateway");
case 'app':
return localize('bdc.dashboard.app', "App");
default:
return serviceName;
}
}

View File

@@ -5,43 +5,66 @@
'use strict';
import { IBdcStatus, IStatus, IServiceStatus } from '../controller/clusterControllerApi';
import * as vscode from 'vscode';
import { getBdcStatus, getEndPoints } from '../controller/clusterControllerApi';
import { EndpointModel, BdcStatusModel } from '../controller/apiGenerated';
import { showErrorMessage } from '../utils';
export class BdcDashboardModel {
private _clusterStatus: IBdcStatus;
constructor(public clusterName: string, url: string, username: string, password: string) {
private _bdcStatus: BdcStatusModel;
private _endpoints: EndpointModel[] = [];
private readonly _onDidUpdateEndpoints = new vscode.EventEmitter<EndpointModel[]>();
private readonly _onDidUpdateBdcStatus = new vscode.EventEmitter<BdcStatusModel>();
public onDidUpdateEndpoints = this._onDidUpdateEndpoints.event;
public onDidUpdateBdcStatus = this._onDidUpdateBdcStatus.event;
constructor(public clusterName: string, private url: string, private username: string, private password: string) {
this.refresh();
}
public get clusterStatus(): IStatus {
return { healthStatus: 'Warning detected', state: 'ready' };
public get bdcStatus(): BdcStatusModel | undefined {
return this._bdcStatus;
}
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(): EndpointModel[] {
return this._endpoints || [];
}
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 {
public async refresh(): Promise<void> {
await Promise.all([
getBdcStatus(this.url, this.username, this.password, true).then(response => {
this._bdcStatus = response.bdcStatus;
this._onDidUpdateBdcStatus.fire(this.bdcStatus);
}),
getEndPoints(this.url, this.username, this.password, true).then(response => {
this._endpoints = response.endPoints || [];
this._onDidUpdateEndpoints.fire(this.serviceEndpoints);
})
]).catch(error => showErrorMessage(error));
}
}
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'
}

View File

@@ -0,0 +1,350 @@
/*---------------------------------------------------------------------------------------------
* 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 azdata from 'azdata';
import * as nls from 'vscode-nls';
import { BdcDashboardModel, Endpoint, Service } from './bdcDashboardModel';
import { IconPath } from '../constants';
import { EndpointModel, ServiceStatusModel, BdcStatusModel } from '../controller/apiGenerated';
const localize = nls.loadMessageBundle();
interface IServiceStatusRow {
stateLoadingComponent: azdata.LoadingComponent;
healthStatusLoadingComponent: azdata.LoadingComponent;
}
interface IServiceEndpointRow {
endpointLoadingComponent: azdata.LoadingComponent;
isHyperlink: boolean;
}
const navWidth = '175px';
const overviewServiceNameCellWidth = '100px';
const overviewStateCellWidth = '75px';
const overviewHealthStatusCellWidth = '75px';
const serviceEndpointRowServiceNameCellWidth = '125px';
const serviceEndpointRowEndpointCellWidth = '350px';
export class BdcDashboardOverviewPage {
private initialized: boolean = false;
private clusterStateLoadingComponent: azdata.LoadingComponent;
private clusterHealthStatusLoadingComponent: azdata.LoadingComponent;
private sqlServerStatusRow: IServiceStatusRow;
private hdfsStatusRow: IServiceStatusRow;
private sparkStatusRow: IServiceStatusRow;
private controlStatusRow: IServiceStatusRow;
private gatewayStatusRow: IServiceStatusRow;
private appStatusRow: IServiceStatusRow;
private sqlServerEndpointRow: IServiceEndpointRow;
private controllerEndpointRow: IServiceEndpointRow;
private hdfsSparkGatewayEndpointRow: IServiceEndpointRow;
private sparkHistoryEndpointRow: IServiceEndpointRow;
private yarnHistoryEndpointRow: IServiceEndpointRow;
private grafanaDashboardEndpointRow: IServiceEndpointRow;
private kibanaDashboardEndpointRow: IServiceEndpointRow;
constructor(private model: BdcDashboardModel) {
this.model.onDidUpdateEndpoints(endpoints => this.handleEndpointsUpdate(endpoints));
this.model.onDidUpdateBdcStatus(bdcStatus => this.handleBdcStatusUpdate(bdcStatus));
}
public create(view: azdata.ModelView): azdata.FlexContainer {
const rootContainer = view.modelBuilder.flexContainer().withLayout(
{
flexFlow: 'column',
width: '100%',
height: '100%',
alignItems: 'left'
}).component();
// ##############
// # PROPERTIES #
// ##############
const propertiesLabel = view.modelBuilder.text()
.withProperties<azdata.TextComponentProperties>({ value: localize('bdc.dashboard.propertiesHeader', "Cluster Properties"), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '10px' } })
.component();
rootContainer.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': '100px', 'min-width': '100px', 'user-select': 'text' } });
row1.addItem(clusterNameValue, { CSSStyles: { 'user-select': 'text' } });
rootContainer.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().component();
this.clusterStateLoadingComponent = view.modelBuilder.loadingComponent().withItem(clusterStateValue).component();
row2.addItem(clusterStateLabel, { CSSStyles: { 'width': '125px', 'min-width': '125px', 'user-select': 'text' } });
row2.addItem(this.clusterStateLoadingComponent, { CSSStyles: { 'width': '125px', 'min-width': '125px', '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().component();
this.clusterHealthStatusLoadingComponent = view.modelBuilder.loadingComponent().withItem(healthStatusValue).component();
row2.addItem(healthStatusLabel, { CSSStyles: { 'width': '125px', 'min-width': '125px', 'user-select': 'text' } });
row2.addItem(this.clusterHealthStatusLoadingComponent, { CSSStyles: { 'width': '125px', 'min-width': '125px', 'user-select': 'text' } });
rootContainer.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"), CSSStyles: { 'margin-block-start': '20px', 'margin-block-end': '0px' } })
.component();
rootContainer.addItem(overviewLabel, { CSSStyles: { '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': overviewServiceNameCellWidth, 'min-width': overviewServiceNameCellWidth, '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': overviewStateCellWidth, 'min-width': overviewStateCellWidth, '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': overviewHealthStatusCellWidth, 'min-width': overviewHealthStatusCellWidth, 'font-weight': 'bold', 'user-select': 'text' } });
overviewContainer.addItem(serviceStatusHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } });
this.sqlServerStatusRow = createServiceStatusRow(view.modelBuilder, overviewContainer, localize('bdc.dashboard.sqlServerLabel', "SQL Server"));
this.hdfsStatusRow = createServiceStatusRow(view.modelBuilder, overviewContainer, localize('bdc.dashboard.hdfsLabel', "HDFS"));
this.sparkStatusRow = createServiceStatusRow(view.modelBuilder, overviewContainer, localize('bdc.dashboard.sparkLabel', "Spark"));
this.controlStatusRow = createServiceStatusRow(view.modelBuilder, overviewContainer, localize('bdc.dashboard.controlLabel', "Control"));
this.gatewayStatusRow = createServiceStatusRow(view.modelBuilder, overviewContainer, localize('bdc.dashboard.gatewayLabel', "Gateway"));
this.appStatusRow = createServiceStatusRow(view.modelBuilder, overviewContainer, localize('bdc.dashboard.appLabel', "App"));
rootContainer.addItem(overviewContainer, { flex: '0 0 auto' });
// #####################
// # SERVICE ENDPOINTS #
// #####################
const endpointsLabel = view.modelBuilder.text()
.withProperties<azdata.TextComponentProperties>({ value: localize('bdc.dashboard.endpointsLabel', "Service Endpoints"), CSSStyles: { 'margin-block-start': '20px', 'margin-block-end': '0px' } })
.component();
rootContainer.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': serviceEndpointRowServiceNameCellWidth, 'min-width': serviceEndpointRowServiceNameCellWidth, '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': serviceEndpointRowEndpointCellWidth, 'min-width': serviceEndpointRowEndpointCellWidth, 'font-weight': 'bold', 'user-select': 'text' } });
endpointsContainer.addItem(endpointsHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } });
this.sqlServerEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('sql-server'), false);
this.controllerEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('controller'), false);
this.hdfsSparkGatewayEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('gateway'), false);
this.sparkHistoryEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('spark-history'), true);
this.yarnHistoryEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('yarn-history'), true);
this.grafanaDashboardEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('grafana'), true);
this.kibanaDashboardEndpointRow = createServiceEndpointRow(view.modelBuilder, endpointsContainer, getFriendlyEndpointNames('kibana'), true);
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;
}
private handleBdcStatusUpdate(bdcStatus: BdcStatusModel): void {
if (!this.initialized || !bdcStatus) {
return;
}
this.clusterStateLoadingComponent.loading = false;
this.clusterHealthStatusLoadingComponent.loading = false;
this.clusterStateLoadingComponent.component.updateProperty('value', bdcStatus.state);
this.clusterHealthStatusLoadingComponent.component.updateProperty('value', bdcStatus.healthStatus);
if (bdcStatus.services) {
// Service Status
const sqlServerServiceStatus = bdcStatus.services.find(s => s.serviceName === Service.sql);
updateServiceStatusRow(this.sqlServerStatusRow, sqlServerServiceStatus);
const hdfsServiceStatus = bdcStatus.services.find(s => s.serviceName === Service.hdfs);
updateServiceStatusRow(this.hdfsStatusRow, hdfsServiceStatus);
const sparkServiceStatus = bdcStatus.services.find(s => s.serviceName === Service.spark);
updateServiceStatusRow(this.sparkStatusRow, sparkServiceStatus);
const controlServiceStatus = bdcStatus.services.find(s => s.serviceName === Service.control);
updateServiceStatusRow(this.controlStatusRow, controlServiceStatus);
const gatewayServiceStatus = bdcStatus.services.find(s => s.serviceName === Service.gateway);
updateServiceStatusRow(this.gatewayStatusRow, gatewayServiceStatus);
const appServiceStatus = bdcStatus.services.find(s => s.serviceName === Service.app);
updateServiceStatusRow(this.appStatusRow, appServiceStatus);
}
}
private handleEndpointsUpdate(endpoints: EndpointModel[]): void {
if (!this.initialized || !endpoints) {
return;
}
// Service Endpoints
const sqlServerEndpoint = endpoints.find(e => e.name === Endpoint.sqlServerMaster);
updateServiceEndpointRow(this.sqlServerEndpointRow, sqlServerEndpoint);
const controllerEndpoint = endpoints.find(e => e.name === Endpoint.controller);
updateServiceEndpointRow(this.controllerEndpointRow, controllerEndpoint);
const gatewayEndpoint = endpoints.find(e => e.name === Endpoint.gateway);
updateServiceEndpointRow(this.hdfsSparkGatewayEndpointRow, gatewayEndpoint);
const yarnHistoryEndpoint = endpoints.find(e => e.name === Endpoint.yarnUi);
updateServiceEndpointRow(this.yarnHistoryEndpointRow, yarnHistoryEndpoint);
const sparkHistoryEndpoint = endpoints.find(e => e.name === Endpoint.sparkHistory);
updateServiceEndpointRow(this.sparkHistoryEndpointRow, sparkHistoryEndpoint);
const grafanaDashboardEndpoint = endpoints.find(e => e.name === Endpoint.metricsui);
updateServiceEndpointRow(this.grafanaDashboardEndpointRow, grafanaDashboardEndpoint);
const kibanaDashboardEndpoint = endpoints.find(e => e.name === Endpoint.logsui);
updateServiceEndpointRow(this.kibanaDashboardEndpointRow, kibanaDashboardEndpoint);
}
}
function updateServiceStatusRow(serviceStatusRow: IServiceStatusRow, serviceStatus: ServiceStatusModel) {
if (serviceStatus) {
serviceStatusRow.stateLoadingComponent.loading = false;
serviceStatusRow.healthStatusLoadingComponent.loading = false;
serviceStatusRow.stateLoadingComponent.component.updateProperty('value', serviceStatus.state);
serviceStatusRow.healthStatusLoadingComponent.component.updateProperty('value', serviceStatus.healthStatus);
}
else {
serviceStatusRow.stateLoadingComponent.loading = true;
serviceStatusRow.healthStatusLoadingComponent.loading = true;
}
}
function updateServiceEndpointRow(serviceEndpointRow: IServiceEndpointRow, endpoint: EndpointModel) {
if (endpoint) {
serviceEndpointRow.endpointLoadingComponent.loading = false;
if (serviceEndpointRow.isHyperlink) {
serviceEndpointRow.endpointLoadingComponent.component.updateProperties({ label: endpoint.endpoint, url: endpoint.endpoint });
}
else {
serviceEndpointRow.endpointLoadingComponent.component.updateProperty('value', endpoint.endpoint);
}
}
else {
serviceEndpointRow.endpointLoadingComponent.loading = true;
}
}
function createServiceStatusRow(modelBuilder: azdata.ModelBuilder, container: azdata.FlexContainer, name: string): IServiceStatusRow {
const serviceStatusRow = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '30px' }).component();
const nameCell = modelBuilder.text().withProperties({ value: name, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component();
serviceStatusRow.addItem(nameCell, { CSSStyles: { 'width': '100px', 'min-width': '100px', 'user-select': 'text', 'margin-block-start': '0px', 'margin-block-end': '0px' } });
const stateCell = modelBuilder.text().withProperties({ CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component();
const stateLoadingComponent = modelBuilder.loadingComponent()
.withItem(stateCell)
.withProperties({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } })
.component();
serviceStatusRow.addItem(stateLoadingComponent, { CSSStyles: { 'width': '75px', 'min-width': '75px' } });
const healthStatusCell = modelBuilder.text().withProperties({ CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component();
const healthStatusLoadingComponent = modelBuilder.loadingComponent()
.withItem(healthStatusCell)
.withProperties({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } })
.component();
serviceStatusRow.addItem(healthStatusLoadingComponent, { CSSStyles: { 'width': '75px', 'min-width': '75px' } });
container.addItem(serviceStatusRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } });
return { stateLoadingComponent: stateLoadingComponent, healthStatusLoadingComponent: healthStatusLoadingComponent };
}
function createServiceEndpointRow(modelBuilder: azdata.ModelBuilder, container: azdata.FlexContainer, name: string, isHyperlink: boolean): IServiceEndpointRow {
const endPointRow = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '30px' }).component();
const nameCell = modelBuilder.text().withProperties({ value: name, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component();
endPointRow.addItem(nameCell, { CSSStyles: { 'width': serviceEndpointRowServiceNameCellWidth, 'min-width': serviceEndpointRowServiceNameCellWidth, 'user-select': 'text' } });
let retRow: IServiceEndpointRow;
if (isHyperlink) {
const endpointCell = modelBuilder.hyperlink().withProperties({ CSSStyles: { 'height': '15px' } }).component();
const endpointLoadingComponent = modelBuilder.loadingComponent()
.withItem(endpointCell)
.withProperties({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } })
.component();
retRow = { endpointLoadingComponent: endpointLoadingComponent, isHyperlink: true };
endPointRow.addItem(endpointLoadingComponent, { CSSStyles: { 'width': serviceEndpointRowEndpointCellWidth, 'min-width': serviceEndpointRowEndpointCellWidth, 'color': '#0078d4', 'text-decoration': 'underline', 'overflow': 'hidden' } });
}
else {
const endpointCell = modelBuilder.text().withProperties({ CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component();
const endpointLoadingComponent = modelBuilder.loadingComponent()
.withItem(endpointCell)
.withProperties({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } })
.component();
retRow = { endpointLoadingComponent: endpointLoadingComponent, isHyperlink: false };
endPointRow.addItem(endpointLoadingComponent, { CSSStyles: { 'width': serviceEndpointRowEndpointCellWidth, 'min-width': serviceEndpointRowEndpointCellWidth, 'overflow': 'hidden' } });
}
const copyValueCell = modelBuilder.button().component();
copyValueCell.iconPath = IconPath.copy;
copyValueCell.onDidClick(() => {
// vscode.env.clipboard.writeText(hyperlink);
});
copyValueCell.title = localize('bdc.dashboard.copyTitle', "Copy");
copyValueCell.iconHeight = '14px';
copyValueCell.iconWidth = '14px';
endPointRow.addItem(copyValueCell, { CSSStyles: { 'width': '50px', 'min-width': '50px', 'padding-left': '10px', 'margin-block-start': '0px', 'margin-block-end': '0px' } });
container.addItem(endPointRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } });
return retRow;
}
function getFriendlyEndpointNames(name: string): string {
switch (name) {
case 'app-proxy':
return localize('bdc.dashboard.appproxy', "Application Proxy");
case 'controller':
return localize('bdc.dashboard.controller', "Controller");
case 'gateway':
return localize('bdc.dashboard.gateway', "HDFS/Spark Gateway");
case 'management-proxy':
return localize('bdc.dashboard.managementproxy', "Management Proxy");
case 'mgmtproxy':
return localize('bdc.dashboard.mgmtproxy', "Management Proxy");
case 'sql-server':
return localize('bdc.dashboard.sqlServerEndpoint', "SQL Server Master Instance");
case 'grafana':
return localize('bdc.dashboard.grafana', "Metrics Dashboard");
case 'kibana':
return localize('bdc.dashboard.kibana', "Log Search Dashboard");
case 'yarn-history':
localize('bdc.dashboard.yarnHistory', "Spark Resource Management");
case 'spark-history':
localize('sparkHistory', "Spark Job Monitoring");
default:
return name;
}
}

View File

@@ -0,0 +1,163 @@
/*---------------------------------------------------------------------------------------------
* 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 azdata from 'azdata';
import * as nls from 'vscode-nls';
import { BdcDashboardModel } from './bdcDashboardModel';
import { BdcStatusModel, InstanceStatusModel } from '../controller/apiGenerated';
const localize = nls.loadMessageBundle();
export interface IGroup {
groupName: string;
instances: IInstanceStatus[];
}
export interface IInstanceStatus {
instanceName: string;
state: string;
healthStatus: string;
}
const metricsAndLogsInstanceNameCellWidth = '100px';
const metricsAndLogsMetricsCellWidth = '75px';
const metricsAndLogsLogsCellWidth = '75px';
export class BdcDashboardResourceStatusPage {
private rootContainer: azdata.FlexContainer;
private instanceHealthStatusRowsContainer: azdata.FlexContainer;
private metricsAndLogsRowsContainer: azdata.FlexContainer;
private initialized: boolean = false;
constructor(private model: BdcDashboardModel, private modelView: azdata.ModelView, private serviceName: string, private resourceName: string) {
this.model.onDidUpdateBdcStatus(bdcStatus => this.handleBdcStatusUpdate(bdcStatus));
this.rootContainer = this.createContainer(modelView);
}
public get container(): azdata.FlexContainer {
return this.rootContainer;
}
private createContainer(view: azdata.ModelView): azdata.FlexContainer {
const rootContainer = view.modelBuilder.flexContainer().withLayout(
{
flexFlow: 'column',
width: '100%',
height: '100%',
alignItems: 'left'
}).component();
// ##############################
// # INSTANCE HEALTH AND STATUS #
// ##############################
// Instance Health Label label
const propertiesLabel = view.modelBuilder.text()
.withProperties<azdata.TextComponentProperties>({ value: localize('bdc.dashboard.healthStatusDetailsHeader', "Health Status Details"), CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '10px' } })
.component();
rootContainer.addItem(propertiesLabel, { CSSStyles: { 'margin-top': '15px', 'font-size': '20px', 'font-weight': 'bold', 'padding-left': '10px' } });
// Header row
const instanceHealthStatusHeaderRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component();
const instanceHealthAndStatusNameHeaderRow = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: localize('bdc.dashboard.metricsAndLogsHeader', "Metrics and Logs") }).component();
instanceHealthStatusHeaderRow.addItem(instanceHealthAndStatusNameHeaderRow, { CSSStyles: { 'width': metricsAndLogsInstanceNameCellWidth, 'min-width': metricsAndLogsInstanceNameCellWidth, 'font-weight': 'bold', 'user-select': 'text' } });
const instanceHealthAndStatusStateRow = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: localize('bdc.dashboard.stateHeader', "State") }).component();
instanceHealthStatusHeaderRow.addItem(instanceHealthAndStatusStateRow, { CSSStyles: { 'width': metricsAndLogsMetricsCellWidth, 'min-width': metricsAndLogsMetricsCellWidth, 'font-weight': 'bold', 'user-select': 'text' } });
const instanceHealthAndStatusHealthStatusRow = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: localize('bdc.dashboard.healthStatusHeader', "Health Status") }).component();
instanceHealthStatusHeaderRow.addItem(instanceHealthAndStatusHealthStatusRow, { CSSStyles: { 'width': metricsAndLogsLogsCellWidth, 'min-width': metricsAndLogsLogsCellWidth, 'font-weight': 'bold', 'user-select': 'text' } });
rootContainer.addItem(instanceHealthStatusHeaderRow, { flex: '0 0 auto', CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } });
this.instanceHealthStatusRowsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
rootContainer.addItem(this.instanceHealthStatusRowsContainer, { flex: '0 0 auto' });
// ####################
// # METRICS AND LOGS #
// ####################
// Title label
const endpointsLabel = view.modelBuilder.text()
.withProperties<azdata.TextComponentProperties>({ value: localize('bdc.dashboard.metricsAndLogsLabel', "Metrics and Logs"), CSSStyles: { 'margin-block-start': '20px', 'margin-block-end': '0px' } })
.component();
rootContainer.addItem(endpointsLabel, { CSSStyles: { 'font-size': '20px', 'font-weight': 'bold', 'padding-left': '10px' } });
// Header row
const metricsAndLogsHeaderRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component();
const nameCell = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: localize('bdc.dashboard.metricsAndLogsHeader', "Metrics and Logs") }).component();
metricsAndLogsHeaderRow.addItem(nameCell, { CSSStyles: { 'width': metricsAndLogsInstanceNameCellWidth, 'min-width': metricsAndLogsInstanceNameCellWidth, 'font-weight': 'bold', 'user-select': 'text' } });
const metricsCell = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: localize('bdc.dashboard.metricsHeader', "Metrics") }).component();
metricsAndLogsHeaderRow.addItem(metricsCell, { CSSStyles: { 'width': metricsAndLogsMetricsCellWidth, 'min-width': metricsAndLogsMetricsCellWidth, 'font-weight': 'bold', 'user-select': 'text' } });
const healthStatusCell = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: localize('bdc.dashboard.logsHeader', "Logs") }).component();
metricsAndLogsHeaderRow.addItem(healthStatusCell, { CSSStyles: { 'width': metricsAndLogsLogsCellWidth, 'min-width': metricsAndLogsLogsCellWidth, 'font-weight': 'bold', 'user-select': 'text' } });
rootContainer.addItem(metricsAndLogsHeaderRow, { flex: '0 0 auto', CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } });
this.metricsAndLogsRowsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
rootContainer.addItem(this.metricsAndLogsRowsContainer, { flex: '0 0 auto' });
this.initialized = true;
this.handleBdcStatusUpdate(this.model.bdcStatus);
return rootContainer;
}
private handleBdcStatusUpdate(bdcStatus: BdcStatusModel): void {
const service = bdcStatus.services ? bdcStatus.services.find(s => s.serviceName === this.serviceName) : undefined;
const resource = service ? service.resources.find(r => r.resourceName === this.resourceName) : undefined;
if (!this.initialized || !resource) {
return;
}
this.instanceHealthStatusRowsContainer.clearItems();
this.metricsAndLogsRowsContainer.clearItems();
resource.instances.forEach(i => {
const instanceHealthStatusRow = createInstanceHealthStatusRow(this.modelView.modelBuilder, i);
this.instanceHealthStatusRowsContainer.addItem(instanceHealthStatusRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } });
let metricsAndLogsRow = createMetricsAndLogsRow(this.modelView.modelBuilder, i);
this.metricsAndLogsRowsContainer.addItem(metricsAndLogsRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } });
});
}
}
/**
* Creates a row with the name, state and health status for a particular instance on this resource
*
* @param modelBuilder The builder used to create the component
* @param instanceStatus The status object for the instance this row is for
*/
function createInstanceHealthStatusRow(modelBuilder: azdata.ModelBuilder, instanceStatus: InstanceStatusModel): azdata.FlexContainer {
const instanceHealthStatusRow = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '30px' }).component();
const nameCell = modelBuilder.text().withProperties({ value: instanceStatus.instanceName, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component();
instanceHealthStatusRow.addItem(nameCell, { CSSStyles: { 'width': '100px', 'min-width': '100px', 'user-select': 'text', 'margin-block-start': '0px', 'margin-block-end': '0px' } });
const stateCell = modelBuilder.text().withProperties({ value: instanceStatus.state, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component();
instanceHealthStatusRow.addItem(stateCell, { CSSStyles: { 'width': '75px', 'min-width': '75px' } });
const healthStatusCell = modelBuilder.text().withProperties({ value: instanceStatus.healthStatus, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component();
instanceHealthStatusRow.addItem(healthStatusCell, { CSSStyles: { 'width': '75px', 'min-width': '75px' } });
return instanceHealthStatusRow;
}
/**
* Creates a row with the name, link to the metrics and a link to the logs for a particular instance on this resource
* @param modelBuilder The builder used to create the component
* @param instanceStatus The status object for the instance this row is for
*/
function createMetricsAndLogsRow(modelBuilder: azdata.ModelBuilder, instanceStatus: InstanceStatusModel): azdata.FlexContainer {
const metricsAndLogsRow = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center', height: '30px' }).component();
const nameCell = modelBuilder.text().withProperties({ value: instanceStatus.instanceName, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component();
metricsAndLogsRow.addItem(nameCell, { CSSStyles: { 'width': '100px', 'min-width': '100px', 'user-select': 'text', 'margin-block-start': '0px', 'margin-block-end': '0px' } });
const metricsCell = modelBuilder.hyperlink().withProperties({ label: localize('bdc.dashboard.viewHyperlink', "View"), url: instanceStatus.dashboards.nodeMetricsUrl, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component();
metricsAndLogsRow.addItem(metricsCell, { CSSStyles: { 'width': '75px', 'min-width': '75px' } });
const logsCell = modelBuilder.hyperlink().withProperties({ label: localize('bdc.dashboard.viewHyperlink', "View"), url: instanceStatus.dashboards.logsUrl, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px', 'user-select': 'text' } }).component();
metricsAndLogsRow.addItem(logsCell, { CSSStyles: { 'width': '75px', 'min-width': '75px' } });
return metricsAndLogsRow;
}

View File

@@ -0,0 +1,103 @@
/*---------------------------------------------------------------------------------------------
* 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 azdata from 'azdata';
import { BdcStatusModel, ResourceStatusModel } from '../controller/apiGenerated';
import { BdcDashboardResourceStatusPage } from './bdcDashboardResourceStatusPage';
import { BdcDashboardModel } from './bdcDashboardModel';
export class BdcServiceStatusPage {
private initialized: boolean = false;
private resourceTabsCreated: boolean = false;
private currentTabPage: azdata.FlexContainer;
private rootContainer: azdata.FlexContainer;
private resourceHeader: azdata.FlexContainer;
constructor(private serviceName: string, private model: BdcDashboardModel, private modelView: azdata.ModelView) {
this.model.onDidUpdateBdcStatus(bdcStatus => this.handleBdcStatusUpdate(bdcStatus));
this.createPage();
}
public get container(): azdata.FlexContainer {
return this.rootContainer;
}
private createPage(): void {
this.rootContainer = this.modelView.modelBuilder.flexContainer().withLayout(
{
flexFlow: 'column',
width: '100%',
height: '100%',
alignItems: 'left'
}).component();
this.resourceHeader = this.modelView.modelBuilder.flexContainer().withLayout(
{
flexFlow: 'row',
width: '100%',
height: '25px',
alignItems: 'left'
}
).component();
this.rootContainer.addItem(this.resourceHeader, { CSSStyles: { 'padding-top': '15px' } });
this.initialized = true;
this.handleBdcStatusUpdate(this.model.bdcStatus);
}
private handleBdcStatusUpdate(bdcStatus: BdcStatusModel): void {
if (!this.initialized || !bdcStatus) {
return;
}
const service = bdcStatus.services.find(s => s.serviceName === this.serviceName);
this.createResourceNavTabs(service.resources);
}
private changeSelectedTabPage(newPage: azdata.FlexContainer): void {
if (this.currentTabPage) {
this.rootContainer.removeItem(this.currentTabPage);
}
this.rootContainer.addItem(newPage);
this.currentTabPage = newPage;
}
/**
* Helper to create the navigation tabs for the resources
*/
private createResourceNavTabs(resources: ResourceStatusModel[]) {
if (this.initialized && !this.resourceTabsCreated) {
resources.forEach(resource => {
const resourceHeaderTab = createResourceHeaderTab(this.modelView, resource.resourceName);
const resourceStatusPage: azdata.FlexContainer = new BdcDashboardResourceStatusPage(this.model, this.modelView, this.serviceName, resource.resourceName).container;
resourceHeaderTab.onDidClick(() => {
this.changeSelectedTabPage(resourceStatusPage);
});
if (!this.currentTabPage) {
this.changeSelectedTabPage(resourceStatusPage);
}
this.resourceHeader.addItem(resourceHeaderTab, { flex: '0 0 auto', CSSStyles: { 'border-bottom': 'solid #ccc' } });
});
this.resourceTabsCreated = true;
}
}
}
/**
* Creates a single resource header tab
* @param view TheModelView used to construct the object
* @param title The text to display in the tab
*/
function createResourceHeaderTab(view: azdata.ModelView, title: string): azdata.DivContainer {
const resourceHeaderTab = view.modelBuilder.divContainer().withLayout({ width: '100px', height: '25px' }).withProperties({ CSSStyles: { 'text-align': 'center' } }).component();
const resourceHeaderLabel = view.modelBuilder.text().withProperties({ value: title, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component();
resourceHeaderTab.addItem(resourceHeaderLabel);
return resourceHeaderTab;
}

View File

@@ -192,7 +192,7 @@ export class ControllerNode extends ControllerTreeNode {
}
try {
let response = await getEndPoints(this._clusterName, this._url, this._username, this._password, true);
let response = await getEndPoints(this._url, this._username, this._password, true);
if (response && response.endPoints) {
let master = response.endPoints.find(e => e.name && e.name === 'sql-server-master');
this.addSqlMasterNode(master.endpoint, master.description);

View File

@@ -27,7 +27,6 @@ let throttleTimers: { [key: string]: any } = {};
export function activate(extensionContext: vscode.ExtensionContext) {
IconPath.setExtensionContext(extensionContext);
let treeDataProvider = new ControllerTreeDataProvider(extensionContext.globalState);
registerTreeDataProvider(treeDataProvider);
registerCommands(extensionContext, treeDataProvider);
}
@@ -56,8 +55,8 @@ function registerCommands(context: vscode.ExtensionContext, treeDataProvider: Co
});
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);
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));
dashboard.showDashboard();
});
}