Sql Assessment HTML report (#11558)

This commit is contained in:
Vladimir Chernov
2020-07-31 13:00:00 +03:00
committed by GitHub
parent 4f58f53e57
commit 89753577ab
9 changed files with 509 additions and 76 deletions

View File

@@ -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`;
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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 } = {

View File

@@ -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>
`;
}
}

View 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);
},
};

View File

@@ -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());
});
});

View File

@@ -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 {