mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-26 17:23:15 -05:00
Sql Assessment HTML report (#11558)
This commit is contained in:
@@ -5,23 +5,36 @@
|
||||
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IAssessmentService } from 'sql/workbench/services/assessment/common/interfaces';
|
||||
import { SqlAssessmentResult, SqlAssessmentResultItem } from 'azdata';
|
||||
import { SqlAssessmentResult } from 'azdata';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
import { AssessmentType, AssessmentTargetType, TARGET_ICON_CLASS } from 'sql/workbench/contrib/assessment/common/consts';
|
||||
import { TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { HTMLReportBuilder } from 'sql/workbench/contrib/assessment/common/htmlReportGenerator';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
|
||||
export interface SqlAssessmentResultInfo {
|
||||
result: SqlAssessmentResult;
|
||||
dateUpdated: number;
|
||||
connectionInfo: any
|
||||
}
|
||||
|
||||
export interface IAssessmentComponent {
|
||||
showProgress(mode: AssessmentType): any;
|
||||
showInitialResults(result: SqlAssessmentResult, method: AssessmentType): any;
|
||||
appendResults(result: SqlAssessmentResult, method: AssessmentType): any;
|
||||
stopProgress(mode: AssessmentType): any;
|
||||
resultItems: SqlAssessmentResultItem[];
|
||||
recentResult: SqlAssessmentResultInfo;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
@@ -136,7 +149,6 @@ export class AsmtDatabaseSelectItemsAction extends Action {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class AsmtServerInvokeItemsAction extends AsmtServerAction {
|
||||
public static ID = 'asmtaction.server.invokeitems';
|
||||
public static LABEL = nls.localize('asmtaction.server.invokeitems', "Invoke Assessment");
|
||||
@@ -198,8 +210,9 @@ export class AsmtExportAsScriptAction extends Action {
|
||||
|
||||
public async run(context: IAsmtActionInfo): Promise<boolean> {
|
||||
this._telemetryService.sendActionEvent(TelemetryView.SqlAssessment, AsmtExportAsScriptAction.ID);
|
||||
if (context && context.component && context.component.resultItems) {
|
||||
await this._assessmentService.generateAssessmentScript(context.ownerUri, context.component.resultItems);
|
||||
const items = context?.component?.recentResult?.result.items;
|
||||
if (items) {
|
||||
await this._assessmentService.generateAssessmentScript(context.ownerUri, items);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -225,3 +238,64 @@ export class AsmtSamplesLinkAction extends Action {
|
||||
return this._openerService.open(URI.parse(AsmtSamplesLinkAction.configHelpUri));
|
||||
}
|
||||
}
|
||||
|
||||
export class AsmtGenerateHTMLReportAction extends Action {
|
||||
public static readonly ID = 'asmtaction.generatehtmlreport';
|
||||
public static readonly LABEL = nls.localize('asmtaction.generatehtmlreport', "Make HTML Report");
|
||||
public static readonly ICON = 'bookreport';
|
||||
|
||||
constructor(
|
||||
@IFileService private _fileService: IFileService,
|
||||
@IOpenerService private _openerService: IOpenerService,
|
||||
@IEnvironmentService private _environmentService: IEnvironmentService,
|
||||
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IFileDialogService private readonly _fileDialogService: IFileDialogService
|
||||
) {
|
||||
super(AsmtGenerateHTMLReportAction.ID, AsmtGenerateHTMLReportAction.LABEL, AsmtGenerateHTMLReportAction.ICON);
|
||||
}
|
||||
|
||||
private suggestReportFile(date: number): URI {
|
||||
const fileName = generateReportFileName(new Date(date));
|
||||
const filePath = path.join(this._environmentService.userRoamingDataHome.fsPath, 'SqlAssessmentReports', fileName);
|
||||
return URI.file(filePath);
|
||||
}
|
||||
|
||||
public async run(context: IAsmtActionInfo): Promise<boolean> {
|
||||
context.component.showProgress(AssessmentType.ReportGeneration);
|
||||
const choosenPath = await this._fileDialogService.pickFileToSave(this.suggestReportFile(context.component.recentResult.dateUpdated));
|
||||
context.component.stopProgress(AssessmentType.ReportGeneration);
|
||||
if (!choosenPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._telemetryService.sendActionEvent(TelemetryView.SqlAssessment, AsmtGenerateHTMLReportAction.ID);
|
||||
|
||||
const result = await this._fileService.createFile(
|
||||
choosenPath,
|
||||
VSBuffer.fromString(new HTMLReportBuilder(context.component.recentResult.result,
|
||||
context.component.recentResult.dateUpdated,
|
||||
context.component.recentResult.connectionInfo).build()),
|
||||
{ overwrite: true });
|
||||
if (result) {
|
||||
this._notificationService.prompt(Severity.Info, nls.localize('asmtaction.openReport', "Report has been saved. Do you want to open it?"),
|
||||
[{
|
||||
label: nls.localize('asmtaction.label.open', "Open"),
|
||||
run: () => {
|
||||
return this._openerService.open(result.resource.fsPath, { openExternal: true });
|
||||
}
|
||||
},
|
||||
{
|
||||
label: nls.localize('asmtaction.label.cancel', "Cancel"),
|
||||
run: () => { }
|
||||
}]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function generateReportFileName(resultDate): string {
|
||||
const datetime = `${resultDate.toISOString().replace(/-/g, '').replace('T', '').replace(/:/g, '').split('.')[0]}`;
|
||||
return `SqlAssessmentReport_${datetime}.html`;
|
||||
|
||||
}
|
||||
@@ -25,13 +25,15 @@ import { RowDetailView, ExtendedItem } from 'sql/base/browser/ui/table/plugins/r
|
||||
import {
|
||||
IAssessmentComponent,
|
||||
IAsmtActionInfo,
|
||||
SqlAssessmentResultInfo,
|
||||
AsmtServerSelectItemsAction,
|
||||
AsmtServerInvokeItemsAction,
|
||||
AsmtDatabaseSelectItemsAction,
|
||||
AsmtDatabaseInvokeItemsAction,
|
||||
AsmtExportAsScriptAction,
|
||||
AsmtSamplesLinkAction
|
||||
} from 'sql/workbench/contrib/assessment/common/asmtActions';
|
||||
AsmtSamplesLinkAction,
|
||||
AsmtGenerateHTMLReportAction
|
||||
} from 'sql/workbench/contrib/assessment/browser/asmtActions';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import * as Utils from 'sql/platform/connection/common/utils';
|
||||
@@ -43,6 +45,8 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { ITableStyles } from 'sql/base/browser/ui/table/interfaces';
|
||||
import { TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
import { LocalizedStrings } from 'sql/workbench/contrib/assessment/common/strings';
|
||||
import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo';
|
||||
|
||||
export const ASMTRESULTSVIEW_SELECTOR: string = 'asmt-results-view-component';
|
||||
export const ROW_HEIGHT: number = 25;
|
||||
@@ -53,7 +57,7 @@ const COLUMN_MESSAGE_ID: string = 'message';
|
||||
|
||||
const COLUMN_MESSAGE_TITLE: { [mode: number]: string } = {
|
||||
[AssessmentType.AvailableRules]: nls.localize('asmt.column.displayName', "Display Name"),
|
||||
[AssessmentType.InvokeAssessment]: nls.localize('asmt.column.message', "Message"),
|
||||
[AssessmentType.InvokeAssessment]: LocalizedStrings.MESSAGE_COLUMN_NAME,
|
||||
};
|
||||
|
||||
enum AssessmentResultItemKind {
|
||||
@@ -93,30 +97,32 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
|
||||
},
|
||||
{ name: nls.localize('asmt.column.severity', "Severity"), field: 'severity', maxWidth: 90, id: 'severity' },
|
||||
{
|
||||
name: nls.localize('asmt.column.message', "Message"),
|
||||
name: LocalizedStrings.MESSAGE_COLUMN_NAME,
|
||||
field: 'message',
|
||||
width: 300,
|
||||
id: COLUMN_MESSAGE_ID,
|
||||
formatter: (_row, _cell, _value, _columnDef, dataContext) => this.appendHelplink(dataContext.message, dataContext.helpLink, dataContext.kind, this.wrapByKind),
|
||||
},
|
||||
{
|
||||
name: nls.localize('asmt.column.tags', "Tags"),
|
||||
name: LocalizedStrings.TAGS_COLUMN_NAME,
|
||||
field: 'tags',
|
||||
width: 80,
|
||||
id: 'tags',
|
||||
formatter: (row, cell, value, columnDef, dataContext) => this.renderTags(row, cell, value, columnDef, dataContext)
|
||||
},
|
||||
{ name: nls.localize('asmt.column.checkId', "Check ID"), field: 'checkId', maxWidth: 140, id: 'checkId' }
|
||||
{ name: LocalizedStrings.CHECKID_COLUMN_NAME, field: 'checkId', maxWidth: 140, id: 'checkId' }
|
||||
];
|
||||
private dataView: any;
|
||||
private filterPlugin: any;
|
||||
private isServerMode: boolean;
|
||||
private rowDetail: RowDetailView<Slick.SlickData>;
|
||||
private exportActionItem: IAction;
|
||||
private generateReportActionItem: IAction;
|
||||
private placeholderElem: HTMLElement;
|
||||
private placeholderNoResultsLabel: string;
|
||||
private spinner: { [mode: number]: HTMLElement } = Object.create(null);
|
||||
private lastInvokedResults: azdata.SqlAssessmentResultItem[];
|
||||
private lastInvokedResult: SqlAssessmentResultInfo;
|
||||
private initialConnectionInfo: ConnectionManagementInfo;
|
||||
|
||||
@ViewChild('resultsgrid') _gridEl: ElementRef;
|
||||
@ViewChild('actionbarContainer') protected actionBarContainer: ElementRef;
|
||||
@@ -134,7 +140,7 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
|
||||
) {
|
||||
super();
|
||||
let self = this;
|
||||
let profile = this._commonService.connectionManagementService.connectionInfo.connectionProfile;
|
||||
const profile = this._commonService.connectionManagementService.connectionInfo.connectionProfile;
|
||||
|
||||
this.isServerMode = !profile.databaseName || Utils.isMaster(profile);
|
||||
|
||||
@@ -146,6 +152,7 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
|
||||
|
||||
this._register(_dashboardService.onLayout(d => self.layout()));
|
||||
this._register(_themeService.onDidColorThemeChange(this._updateStyles, this));
|
||||
this.initialConnectionInfo = this._commonService.connectionManagementService.connectionInfo;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -156,6 +163,8 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.isVisible = false;
|
||||
this.rowDetail?.destroy();
|
||||
this.filterPlugin.destroy();
|
||||
}
|
||||
|
||||
ngAfterContentChecked(): void {
|
||||
@@ -173,8 +182,8 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
|
||||
}
|
||||
}
|
||||
|
||||
public get resultItems(): azdata.SqlAssessmentResultItem[] {
|
||||
return this.lastInvokedResults;
|
||||
public get recentResult(): SqlAssessmentResultInfo {
|
||||
return this.lastInvokedResult;
|
||||
}
|
||||
|
||||
public get isActive(): boolean {
|
||||
@@ -209,9 +218,13 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
|
||||
public showInitialResults(result: azdata.SqlAssessmentResult, method: AssessmentType) {
|
||||
if (result) {
|
||||
if (method === AssessmentType.InvokeAssessment) {
|
||||
this.lastInvokedResults = result.items;
|
||||
this.lastInvokedResult = {
|
||||
result: result,
|
||||
dateUpdated: Date.now(),
|
||||
connectionInfo: this.initialConnectionInfo
|
||||
};
|
||||
} else {
|
||||
this.lastInvokedResults = [];
|
||||
this.lastInvokedResult = undefined;
|
||||
}
|
||||
|
||||
this.displayResults(result.items, method);
|
||||
@@ -229,7 +242,8 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
|
||||
|
||||
public appendResults(result: azdata.SqlAssessmentResult, method: AssessmentType) {
|
||||
if (method === AssessmentType.InvokeAssessment) {
|
||||
this.lastInvokedResults.push(...result.items);
|
||||
this.lastInvokedResult.dateUpdated = Date.now();
|
||||
this.lastInvokedResult.result.items.push(...result.items);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
@@ -341,11 +355,13 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
|
||||
|
||||
private initActionBar(invokeAction: IAction, selectAction: IAction) {
|
||||
this.exportActionItem = this._register(this._instantiationService.createInstance(AsmtExportAsScriptAction));
|
||||
this.generateReportActionItem = this._register(this._instantiationService.createInstance(AsmtGenerateHTMLReportAction));
|
||||
|
||||
let taskbar = <HTMLElement>this.actionBarContainer.nativeElement;
|
||||
this._actionBar = this._register(new Taskbar(taskbar));
|
||||
this.spinner[AssessmentType.InvokeAssessment] = Taskbar.createTaskbarSpinner();
|
||||
this.spinner[AssessmentType.AvailableRules] = Taskbar.createTaskbarSpinner();
|
||||
this.spinner[AssessmentType.ReportGeneration] = Taskbar.createTaskbarSpinner();
|
||||
|
||||
this._actionBar.setContent([
|
||||
{ action: invokeAction },
|
||||
@@ -353,6 +369,8 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
|
||||
{ action: selectAction },
|
||||
{ element: this.spinner[AssessmentType.AvailableRules] },
|
||||
{ action: this.exportActionItem },
|
||||
{ action: this.generateReportActionItem },
|
||||
{ element: this.spinner[AssessmentType.ReportGeneration] },
|
||||
{ action: this._instantiationService.createInstance(AsmtSamplesLinkAction) }
|
||||
]);
|
||||
|
||||
@@ -360,6 +378,7 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
|
||||
let context: IAsmtActionInfo = { component: this, ownerUri: Utils.generateUri(connectionInfo.connectionProfile.clone(), 'dashboard'), connectionId: connectionInfo.connectionProfile.id };
|
||||
this._actionBar.context = context;
|
||||
this.exportActionItem.enabled = false;
|
||||
this.generateReportActionItem.enabled = false;
|
||||
}
|
||||
|
||||
private convertToDataViewItems(asmtResult: azdata.SqlAssessmentResultItem, index: number, method: AssessmentType) {
|
||||
@@ -392,7 +411,7 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
|
||||
this._table.autosizeColumns();
|
||||
this._table.resizeCanvas();
|
||||
this.exportActionItem.enabled = (results.length > 0 && method === AssessmentType.InvokeAssessment);
|
||||
|
||||
this.generateReportActionItem.enabled = (results.length > 0 && method === AssessmentType.InvokeAssessment);
|
||||
if (results.length > 0) {
|
||||
dom.hide(this.placeholderElem);
|
||||
} else {
|
||||
@@ -472,7 +491,7 @@ export class AsmtResultsViewComponent extends TabChild implements IAssessmentCom
|
||||
|
||||
private appendHelplink(msg: string, helpLink: string, kind: AssessmentResultItemKind, wrapByKindFunc): string {
|
||||
if (msg !== undefined) {
|
||||
return `${wrapByKindFunc(kind, escape(msg))}<a class='helpLink' href='${helpLink}' \>${nls.localize('asmt.learnMore', "Learn More")}</a>`;
|
||||
return `${wrapByKindFunc(kind, escape(msg))}<a class='helpLink' href='${helpLink}' \>${LocalizedStrings.LEARN_MORE_LINK}</a>`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -4,25 +4,11 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/asmt';
|
||||
import { Component, Inject, forwardRef, ChangeDetectorRef, ViewChild, Injectable, OnInit } from '@angular/core';
|
||||
import { Component, Inject, forwardRef, ChangeDetectorRef, Injectable, OnInit } from '@angular/core';
|
||||
import { ServerInfo } from 'azdata';
|
||||
//import { PanelComponent, IPanelOptions, NavigationBarLayout } from 'sql/base/browser/ui/panel/panel.component';
|
||||
import { AngularDisposable } from 'sql/base/browser/lifecycle';
|
||||
import { localize } from 'vs/nls';
|
||||
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
|
||||
import { AsmtResultsViewComponent } from 'sql/workbench/contrib/assessment/browser/asmtResultsView.component';
|
||||
|
||||
|
||||
const LocalizedStrings = {
|
||||
SECTION_TITLE_API: localize('asmt.section.api.title', "API information"),
|
||||
API_VERSION: localize('asmt.apiversion', "API Version:"),
|
||||
DEFAULT_RULESET_VERSION: localize('asmt.rulesetversion', "Default Ruleset Version:"),
|
||||
SECTION_TITLE_SQL_SERVER: localize('asmt.section.instance.title', "SQL Server Instance Details"),
|
||||
SERVER_VERSION: localize('asmt.serverversion', "Version:"),
|
||||
SERVER_EDITION: localize('asmt.serveredition', "Edition:"),
|
||||
SERVER_INSTANCENAME: localize('asmt.instancename', "Instance Name:"),
|
||||
SERVER_OSVERSION: localize('asmt.osversion', "OS Version:")
|
||||
};
|
||||
import { LocalizedStrings } from 'sql/workbench/contrib/assessment/common/strings';
|
||||
|
||||
export const DASHBOARD_SELECTOR: string = 'asmtview-component';
|
||||
|
||||
@@ -33,7 +19,6 @@ export const DASHBOARD_SELECTOR: string = 'asmtview-component';
|
||||
@Injectable()
|
||||
export class AsmtViewComponent extends AngularDisposable implements OnInit {
|
||||
|
||||
@ViewChild('asmtresultcomponent') private _asmtResultView: AsmtResultsViewComponent;
|
||||
protected localizedStrings = LocalizedStrings;
|
||||
|
||||
connectionInfo: ServerInfo = null;
|
||||
@@ -41,9 +26,6 @@ export class AsmtViewComponent extends AngularDisposable implements OnInit {
|
||||
ruleset: string = '';
|
||||
api: string = '';
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface) {
|
||||
@@ -57,13 +39,7 @@ export class AsmtViewComponent extends AngularDisposable implements OnInit {
|
||||
private displayConnectionInfo() {
|
||||
this.connectionInfo = this._commonService.connectionManagementService.connectionInfo.serverInfo;
|
||||
let serverName = this._commonService.connectionManagementService.connectionInfo.connectionProfile.serverName;
|
||||
let machineName = this.connectionInfo['machineName'];
|
||||
if ((['local', '(local)', '(local);'].indexOf(serverName.toLowerCase()) >= 0) || machineName.toLowerCase() === serverName.toLowerCase()) {
|
||||
this.instanceName = machineName;
|
||||
}
|
||||
else {
|
||||
this.instanceName = machineName + '\\' + serverName;
|
||||
}
|
||||
this.instanceName = serverName;
|
||||
}
|
||||
|
||||
public displayAssessmentInfo(apiVersion: string, rulesetVersion: string) {
|
||||
@@ -71,9 +47,4 @@ export class AsmtViewComponent extends AngularDisposable implements OnInit {
|
||||
this.ruleset = rulesetVersion;
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
|
||||
public layout() {
|
||||
this._asmtResultView.layout();
|
||||
//this._panel.layout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ export enum AssessmentTargetType {
|
||||
|
||||
export enum AssessmentType {
|
||||
AvailableRules = 1,
|
||||
InvokeAssessment = 2
|
||||
InvokeAssessment = 2,
|
||||
ReportGeneration = 3
|
||||
}
|
||||
|
||||
export const TARGET_ICON_CLASS: { [targetType: number]: string } = {
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo';
|
||||
import { escape } from 'vs/base/common/strings';
|
||||
import { SqlAssessmentResult, SqlAssessmentResultItem } from 'azdata';
|
||||
import { SqlAssessmentTargetType } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { LocalizedStrings } from 'sql/workbench/contrib/assessment/common/strings';
|
||||
|
||||
export class HTMLReportBuilder {
|
||||
constructor(
|
||||
private _assessmentResult: SqlAssessmentResult,
|
||||
private _dateUpdated: number,
|
||||
private _connectionInfo: ConnectionManagementInfo
|
||||
) {
|
||||
}
|
||||
|
||||
public build(): string {
|
||||
let mainContent = `
|
||||
<html>
|
||||
<head>
|
||||
<title>${LocalizedStrings.REPORT_TITLE}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div>${LocalizedStrings.REPORT_TITLE}</div>
|
||||
</div>
|
||||
<div style="font-style: italic;">${new Date(this._dateUpdated).toLocaleString(platform.locale)}</div>
|
||||
${this.buildVersionDetails()}
|
||||
<div style="margin-top: 20px;">
|
||||
${this.buildResultsSection()}
|
||||
</div>
|
||||
${this.buildStyleSection()}
|
||||
</body>
|
||||
</html>`;
|
||||
return mainContent;
|
||||
}
|
||||
|
||||
private instanceName(): string {
|
||||
const serverName = this._connectionInfo.connectionProfile.serverName;
|
||||
if (['local', '(local)', '(local);'].indexOf(serverName.toLowerCase()) >= 0) {
|
||||
return this._connectionInfo.serverInfo !== undefined
|
||||
? this._connectionInfo.serverInfo['machineName']
|
||||
: serverName;
|
||||
}
|
||||
return serverName;
|
||||
}
|
||||
|
||||
private buildVersionDetails(): string {
|
||||
return `
|
||||
<div class="details">
|
||||
<div>
|
||||
<span>${LocalizedStrings.API_VERSION} ${this._assessmentResult.apiVersion}</span><br />
|
||||
<span>${LocalizedStrings.DEFAULT_RULESET_VERSION} ${this._assessmentResult.items[0].rulesetVersion}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>${LocalizedStrings.SECTION_TITLE_SQL_SERVER}: ${this._connectionInfo.serverInfo?.serverEdition} ${this._connectionInfo.serverInfo?.serverVersion}</span><br>
|
||||
<span>${LocalizedStrings.SERVER_INSTANCENAME} ${this.instanceName()}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private buildResultsSection(): string {
|
||||
let resultByTarget = [];
|
||||
this._assessmentResult.items.forEach(resultItem => {
|
||||
if (resultByTarget[resultItem.targetType] === undefined) {
|
||||
resultByTarget[resultItem.targetType] = [];
|
||||
}
|
||||
if (resultByTarget[resultItem.targetType][resultItem.targetName] === undefined) {
|
||||
resultByTarget[resultItem.targetType][resultItem.targetName] = [];
|
||||
}
|
||||
resultByTarget[resultItem.targetType][resultItem.targetName].push(resultItem);
|
||||
});
|
||||
|
||||
let result = '';
|
||||
if (resultByTarget[SqlAssessmentTargetType.Server] !== undefined) {
|
||||
Object.keys(resultByTarget[SqlAssessmentTargetType.Server]).forEach(instanceName => {
|
||||
result += this.buildTargetAssessmentSection(resultByTarget[SqlAssessmentTargetType.Server][instanceName]);
|
||||
});
|
||||
}
|
||||
if (resultByTarget[SqlAssessmentTargetType.Database] !== undefined) {
|
||||
Object.keys(resultByTarget[SqlAssessmentTargetType.Database]).forEach(dbName => {
|
||||
result += this.buildTargetAssessmentSection(resultByTarget[SqlAssessmentTargetType.Database][dbName]);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private buildTargetAssessmentSection(targetResults: SqlAssessmentResultItem[]): string {
|
||||
let content = `
|
||||
<div>
|
||||
<div class="target">${targetResults[0].targetType === SqlAssessmentTargetType.Server ? LocalizedStrings.RESULTS_FOR_INSTANCE : LocalizedStrings.RESULTS_FOR_DATABASE}: ${targetResults[0].targetName}</div>
|
||||
${this.buildSeveritySection(LocalizedStrings.REPORT_ERROR, targetResults.filter(item => item.level === 'Error'))}
|
||||
${this.buildSeveritySection(LocalizedStrings.REPORT_WARNING, targetResults.filter(item => item.level === 'Warning'))}
|
||||
${this.buildSeveritySection(LocalizedStrings.REPORT_INFO, targetResults.filter(item => item.level === 'Information'))}
|
||||
</div>`;
|
||||
return content;
|
||||
}
|
||||
private buildSeveritySection(severityName: string, items: SqlAssessmentResultItem[]) {
|
||||
if (items.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="severityBlock">
|
||||
<div>${LocalizedStrings.REPORT_SEVERITY_MESSAGE(severityName, items.length)}</div>
|
||||
<table>
|
||||
<tr><th>${LocalizedStrings.MESSAGE_COLUMN_NAME}</th><th>${LocalizedStrings.HELP_LINK_COLUMN_NAME}</th><th>${LocalizedStrings.TAGS_COLUMN_NAME}</th><th>${LocalizedStrings.CHECKID_COLUMN_NAME}</th></tr>
|
||||
${this.buildItemsRows(items)}
|
||||
</table>
|
||||
</div>`;
|
||||
}
|
||||
private buildItemsRows(items: SqlAssessmentResultItem[]): string {
|
||||
let content = '';
|
||||
items.forEach(item => {
|
||||
content += `<tr>
|
||||
<td>${escape(item.message)}</td>
|
||||
<td><a href='${item.helpLink}' target='_blank;'>${LocalizedStrings.LEARN_MORE_LINK}</a></td>
|
||||
<td>${this.formatTags(item.tags)}</td>
|
||||
<td>${item.checkId}</td>
|
||||
</tr>`;
|
||||
});
|
||||
return content;
|
||||
}
|
||||
private formatTags(tags: string[]): string {
|
||||
return tags?.join(', ');
|
||||
}
|
||||
|
||||
private buildStyleSection(): string {
|
||||
return `
|
||||
<style>
|
||||
* {
|
||||
color: #4a4a4a;
|
||||
font-family: "Segoe WPC", "Segoe UI", sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header>* {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.target {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
border: 1px solid silver;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
table th:nth-child(1) {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
table th:nth-child(2) {
|
||||
width: 80px;
|
||||
}
|
||||
table th:nth-child(3) {
|
||||
width: 200px;
|
||||
}
|
||||
table th:nth-child(4) {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
table td,
|
||||
table th {
|
||||
border-bottom: 1px solid silver;
|
||||
border-right: 1px dotted silver;
|
||||
padding: 3px 5px;
|
||||
white-space: normal;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
table th {
|
||||
background-color: silver;
|
||||
}
|
||||
|
||||
div.severityBlock>div {
|
||||
font-size: larger;
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
table th:nth-child(2),
|
||||
table td:nth-child(2) {
|
||||
display:none;
|
||||
}
|
||||
table th:nth-child(3) {
|
||||
width: 120px;
|
||||
}
|
||||
table th:nth-child(4) {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
}
|
||||
31
src/sql/workbench/contrib/assessment/common/strings.ts
Normal file
31
src/sql/workbench/contrib/assessment/common/strings.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export const LocalizedStrings = {
|
||||
SECTION_TITLE_API: localize('asmt.section.api.title', "API information"),
|
||||
API_VERSION: localize('asmt.apiversion', "API Version:"),
|
||||
DEFAULT_RULESET_VERSION: localize('asmt.rulesetversion', "Default Ruleset Version:"),
|
||||
SECTION_TITLE_SQL_SERVER: localize('asmt.section.instance.title', "SQL Server Instance Details"),
|
||||
SERVER_VERSION: localize('asmt.serverversion', "Version:"),
|
||||
SERVER_EDITION: localize('asmt.serveredition', "Edition:"),
|
||||
SERVER_INSTANCENAME: localize('asmt.instancename', "Instance Name:"),
|
||||
SERVER_OSVERSION: localize('asmt.osversion', "OS Version:"),
|
||||
MESSAGE_COLUMN_NAME: localize('asmt.column.message', "Message"),
|
||||
CHECKID_COLUMN_NAME: localize('asmt.column.checkId', "Check ID"),
|
||||
TAGS_COLUMN_NAME: localize('asmt.column.tags', "Tags"),
|
||||
LEARN_MORE_LINK: localize('asmt.learnMore', "Learn More"),
|
||||
REPORT_TITLE: localize('asmt.sqlReportTitle', "SQL Assessment Report"),
|
||||
RESULTS_FOR_DATABASE: localize('asmt.sqlReport.resultForDatabase', "Results for database"),
|
||||
RESULTS_FOR_INSTANCE: localize('asmt.sqlReport.resultForInstance', "Results for server"),
|
||||
REPORT_ERROR: localize('asmt.sqlReport.Error', "Error"),
|
||||
REPORT_WARNING: localize('asmt.sqlReport.Warning', "Warning"),
|
||||
REPORT_INFO: localize('asmt.sqlReport.Info', "Information"),
|
||||
HELP_LINK_COLUMN_NAME: localize('asmt.column.helpLink', "Help Link"),
|
||||
REPORT_SEVERITY_MESSAGE: function (severity: string, count: number) {
|
||||
return localize('asmt.sqlReport.severityMsg', "{0}: {1} item(s)", severity, count);
|
||||
},
|
||||
};
|
||||
@@ -8,13 +8,15 @@ import * as assert from 'assert';
|
||||
import { AssessmentType, AssessmentTargetType } from 'sql/workbench/contrib/assessment/common/consts';
|
||||
import {
|
||||
IAssessmentComponent,
|
||||
SqlAssessmentResultInfo,
|
||||
AsmtServerInvokeItemsAction,
|
||||
AsmtServerSelectItemsAction,
|
||||
AsmtExportAsScriptAction,
|
||||
AsmtSamplesLinkAction,
|
||||
AsmtDatabaseInvokeItemsAction,
|
||||
AsmtDatabaseSelectItemsAction
|
||||
} from 'sql/workbench/contrib/assessment/common/asmtActions';
|
||||
AsmtDatabaseSelectItemsAction,
|
||||
AsmtGenerateHTMLReportAction
|
||||
} from 'sql/workbench/contrib/assessment/browser/asmtActions';
|
||||
import { AssessmentService } from 'sql/workbench/services/assessment/common/assessmentService';
|
||||
import { NullAdsTelemetryService } from 'sql/platform/telemetry/common/adsTelemetryService';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
@@ -23,28 +25,44 @@ import { TestConnectionManagementService } from 'sql/platform/connection/test/co
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { OpenerServiceStub } from 'sql/platform/opener/common/openerServiceStub';
|
||||
import { SqlAssessmentTargetType } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { TestFileService, TestEnvironmentService, TestFileDialogService } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
/**
|
||||
* Class to test Assessment Management Actions
|
||||
*/
|
||||
|
||||
let assessmentResultItems: azdata.SqlAssessmentResultItem[] = [
|
||||
<azdata.SqlAssessmentResultItem>{ checkId: 'check1' },
|
||||
<azdata.SqlAssessmentResultItem>{ checkId: 'check2' },
|
||||
<azdata.SqlAssessmentResultItem>{ checkId: 'check3' }
|
||||
<azdata.SqlAssessmentResultItem>{
|
||||
checkId: 'check1',
|
||||
rulesetVersion: '1.0.0',
|
||||
targetType: SqlAssessmentTargetType.Database,
|
||||
targetName: 'db1',
|
||||
level: 'Warning',
|
||||
message: '',
|
||||
tags: ['tag2'],
|
||||
},
|
||||
<azdata.SqlAssessmentResultItem>{
|
||||
checkId: 'check2',
|
||||
rulesetVersion: '1.0.0',
|
||||
targetType: SqlAssessmentTargetType.Database,
|
||||
targetName: 'db1',
|
||||
level: 'Error',
|
||||
message: '',
|
||||
tags: ['tag1', 'tag2']
|
||||
},
|
||||
<azdata.SqlAssessmentResultItem>{
|
||||
checkId: 'check3',
|
||||
rulesetVersion: '1.0.0',
|
||||
targetType: SqlAssessmentTargetType.Database,
|
||||
targetName: 'db1',
|
||||
level: 'Information',
|
||||
message: '',
|
||||
tags: ['tag3', 'tag4']
|
||||
}
|
||||
];
|
||||
|
||||
class AssessmentTestViewComponent implements IAssessmentComponent {
|
||||
showProgress(mode: AssessmentType) { return undefined; }
|
||||
showInitialResults(result: azdata.SqlAssessmentResult, method: AssessmentType) { return undefined; }
|
||||
appendResults(result: azdata.SqlAssessmentResult, method: AssessmentType) { }
|
||||
stopProgress(mode: AssessmentType) { return undefined; }
|
||||
resultItems: azdata.SqlAssessmentResultItem[] = assessmentResultItems;
|
||||
isActive: boolean = true;
|
||||
}
|
||||
|
||||
let mockAssessmentService: TypeMoq.Mock<AssessmentService>;
|
||||
let mockAsmtViewComponent: TypeMoq.Mock<IAssessmentComponent>;
|
||||
|
||||
let assessmentResult: azdata.SqlAssessmentResult = {
|
||||
success: true,
|
||||
errorMessage: '',
|
||||
@@ -52,6 +70,28 @@ let assessmentResult: azdata.SqlAssessmentResult = {
|
||||
items: assessmentResultItems
|
||||
};
|
||||
|
||||
class AssessmentTestViewComponent implements IAssessmentComponent {
|
||||
showProgress(mode: AssessmentType) { return undefined; }
|
||||
showInitialResults(result: azdata.SqlAssessmentResult, method: AssessmentType) { return undefined; }
|
||||
appendResults(result: azdata.SqlAssessmentResult, method: AssessmentType) { }
|
||||
stopProgress(mode: AssessmentType) { return undefined; }
|
||||
recentResult: SqlAssessmentResultInfo = {
|
||||
result: assessmentResult,
|
||||
connectionInfo: {
|
||||
serverInfo: null,
|
||||
connectionProfile: {
|
||||
serverName: 'testServer'
|
||||
}
|
||||
},
|
||||
dateUpdated: Date.now()
|
||||
};
|
||||
isActive: boolean = true;
|
||||
}
|
||||
|
||||
let mockAssessmentService: TypeMoq.Mock<AssessmentService>;
|
||||
let mockAsmtViewComponent: TypeMoq.Mock<IAssessmentComponent>;
|
||||
|
||||
|
||||
// Tests
|
||||
suite('Assessment Actions', () => {
|
||||
|
||||
@@ -181,4 +221,46 @@ suite('Assessment Actions', () => {
|
||||
openerService.verify(s => s.open(TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
|
||||
test('Make HTML Report Action', async () => {
|
||||
const openerService = TypeMoq.Mock.ofType<IOpenerService>(OpenerServiceStub);
|
||||
openerService.setup(s => s.open(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
|
||||
|
||||
const fileUri = URI.file('/user/home');
|
||||
const fileDialogService = new TestFileDialogService();
|
||||
fileDialogService.setPickFileToSave(fileUri);
|
||||
|
||||
const notificationService = TypeMoq.Mock.ofType<INotificationService>(TestNotificationService);
|
||||
notificationService.setup(s => s.prompt(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => null);
|
||||
|
||||
const fileService = TypeMoq.Mock.ofType<IFileService>(TestFileService);
|
||||
fileService.setup(s => s.createFile(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {
|
||||
return Promise.resolve({
|
||||
ctime: Date.now(),
|
||||
etag: 'index.txt',
|
||||
isFile: true,
|
||||
isDirectory: false,
|
||||
isSymbolicLink: false,
|
||||
mtime: Date.now(),
|
||||
name: '',
|
||||
resource: fileUri,
|
||||
size: 42
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
let action = new AsmtGenerateHTMLReportAction(fileService.object,
|
||||
openerService.object,
|
||||
TestEnvironmentService,
|
||||
new NullAdsTelemetryService(),
|
||||
notificationService.object,
|
||||
fileDialogService);
|
||||
assert.equal(action.id, AsmtGenerateHTMLReportAction.ID, 'Generate HTML Report id action mismatch');
|
||||
assert.equal(action.label, AsmtGenerateHTMLReportAction.LABEL, 'Generate HTML Report label action mismatch');
|
||||
|
||||
let result = await action.run({ ownerUri: '', component: mockAsmtViewComponent.object, connectionId: '' });
|
||||
assert.ok(result, 'Generate HTML Report action should succeed');
|
||||
notificationService.verify(s => s.prompt(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser
|
||||
import * as azdata from 'azdata';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { AgentViewComponent } from 'sql/workbench/contrib/jobManagement/browser/agentView.component';
|
||||
import { AsmtViewComponent } from 'sql/workbench/contrib/assessment/browser/asmtView.component';
|
||||
|
||||
@Component({
|
||||
templateUrl: decodeURI(require.toUrl('./controlHostContent.component.html')),
|
||||
@@ -31,7 +30,6 @@ export class ControlHostContent {
|
||||
|
||||
/* Children components */
|
||||
@ViewChild('agent') private _agentViewComponent: AgentViewComponent;
|
||||
@ViewChild('asmt') private _asmtViewComponent: AsmtViewComponent;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _dashboardService: CommonServiceInterface,
|
||||
@@ -41,7 +39,6 @@ export class ControlHostContent {
|
||||
|
||||
public layout(): void {
|
||||
this._agentViewComponent?.layout();
|
||||
this._asmtViewComponent?.layout();
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
|
||||
Reference in New Issue
Block a user