Assessment core extension (#10154)

This commit is contained in:
Vladimir Chernov
2020-05-29 01:17:41 +03:00
committed by GitHub
parent 0d544660e0
commit 8288360cc4
47 changed files with 1813 additions and 58 deletions

View File

@@ -0,0 +1,2 @@
<div #actionbarContainer class="asmt-actionbar-container"></div>
<div #resultsgrid class="asmtview-grid"></div>

View File

@@ -0,0 +1,627 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/asmt';
import 'vs/css!./media/detailview';
import * as nls from 'vs/nls';
import * as azdata from 'azdata';
import * as dom from 'vs/base/browser/dom';
import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnInit, OnDestroy, AfterContentChecked } from '@angular/core';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { Table } from 'sql/base/browser/ui/table/table';
import { AsmtViewComponent } from 'sql/workbench/contrib/assessment/browser/asmtView.component';
import { HeaderFilter } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin';
import { CommonServiceInterface } from 'sql/workbench/services/bootstrap/browser/commonServiceInterface.service';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IColorTheme } from 'vs/platform/theme/common/themeService';
import { attachButtonStyler } from 'sql/platform/theme/common/styler';
import { find } from 'vs/base/common/arrays';
import { RowDetailView, ExtendedItem } from 'sql/base/browser/ui/table/plugins/rowDetailView';
import {
IAssessmentComponent,
IAsmtActionInfo,
AsmtServerSelectItemsAction,
AsmtServerInvokeItemsAction,
AsmtDatabaseSelectItemsAction,
AsmtDatabaseInvokeItemsAction,
AsmtExportAsScriptAction,
AsmtSamplesLinkAction
} from 'sql/workbench/contrib/assessment/common/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';
import { escape } from 'sql/base/common/strings';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { AssessmentType, TARGET_ICON_CLASS } from 'sql/workbench/contrib/assessment/common/consts';
import { ILogService } from 'vs/platform/log/common/log';
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
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';
export const ASMTRESULTSVIEW_SELECTOR: string = 'asmt-results-view-component';
export const ROW_HEIGHT: number = 25;
export const ACTIONBAR_PADDING: number = 10;
const PLACEHOLDER_LABEL = nls.localize('asmt.NoResultsInitial', "Nothing to show. Invoke assessment to get results");
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"),
};
enum AssessmentResultItemKind {
RealResult = 0,
Warning = 1,
Error = 2
}
const KIND_CLASS: { [kind: number]: string } = {
[AssessmentResultItemKind.Error]: 'error-val',
[AssessmentResultItemKind.Warning]: 'warning-val',
[AssessmentResultItemKind.RealResult]: ''
};
@Component({
selector: ASMTRESULTSVIEW_SELECTOR,
templateUrl: decodeURI(require.toUrl('./asmtResultsView.component.html')),
providers: [{ provide: TabChild, useExisting: forwardRef(() => AsmtResultsViewComponent) }],
})
export class AsmtResultsViewComponent extends TabChild implements IAssessmentComponent, OnInit, OnDestroy, AfterContentChecked {
protected _parentComponent: AsmtViewComponent;
protected _table: Table<any>;
protected _visibilityElement: ElementRef;
protected isVisible: boolean = false;
protected isInitialized: boolean = false;
protected isRefreshing: boolean = false;
protected _actionBar: Taskbar;
private columns: Array<Slick.Column<any>> = [
{
name: nls.localize('asmt.column.target', "Target"),
formatter: this.renderTarget,
field: 'targetName',
width: 80,
id: 'target'
},
{ name: nls.localize('asmt.column.severity', "Serverity"), field: 'severity', maxWidth: 90, id: 'severity' },
{
name: nls.localize('asmt.column.message', "Message"),
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"),
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' }
];
private dataView: any;
private filterPlugin: any;
private isServerMode: boolean;
private rowDetail: RowDetailView<Slick.SlickData>;
private exportActionItem: IAction;
private placeholderElem: HTMLElement;
private placeholderNoResultsLabel: string;
private spinner: { [mode: number]: HTMLElement } = Object.create(null);
private lastInvokedResults: azdata.SqlAssessmentResultItem[];
@ViewChild('resultsgrid') _gridEl: ElementRef;
@ViewChild('actionbarContainer') protected actionBarContainer: ElementRef;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(forwardRef(() => AsmtViewComponent)) private _asmtViewComponent: AsmtViewComponent,
@Inject(IWorkbenchThemeService) private readonly _themeService: IWorkbenchThemeService,
@Inject(IWorkbenchLayoutService) private readonly layoutService: IWorkbenchLayoutService,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
@Inject(IDashboardService) _dashboardService: IDashboardService,
@Inject(IAdsTelemetryService) private _telemetryService: IAdsTelemetryService,
@Inject(ILogService) protected _logService: ILogService
) {
super();
let self = this;
let profile = this._commonService.connectionManagementService.connectionInfo.connectionProfile;
this.isServerMode = !profile.databaseName || Utils.isMaster(profile);
if (this.isServerMode) {
this.placeholderNoResultsLabel = nls.localize('asmt.TargetInstanceComplient', "Instance {0} is totally compliant with the best practices. Good job!", profile.serverName);
} else {
this.placeholderNoResultsLabel = nls.localize('asmt.TargetDatabaseComplient', "Database {0} is totally compliant with the best practices. Good job!", profile.databaseName);
}
this._register(_dashboardService.onLayout(d => self.layout()));
this._register(_themeService.onDidColorThemeChange(this._updateStyles, this));
}
ngOnInit(): void {
this._visibilityElement = this._gridEl;
this._parentComponent = this._asmtViewComponent;
this._telemetryService.sendViewEvent(TelemetryView.SqlAssessment);
}
ngOnDestroy(): void {
this.isVisible = false;
}
ngAfterContentChecked(): void {
if (this._visibilityElement && this._parentComponent) {
if (this.isVisible === false && this._visibilityElement.nativeElement.offsetParent !== null) {
this.isVisible = true;
if (!this.isInitialized) {
this.initializeComponent();
this.layout();
this.isInitialized = true;
}
} else if (this.isVisible === true && this._visibilityElement.nativeElement.offsetParent === null) {
this.isVisible = false;
}
}
}
public get resultItems(): azdata.SqlAssessmentResultItem[] {
return this.lastInvokedResults;
}
public get isActive(): boolean {
return this.isVisible;
}
public layout(): void {
let statusBar = this.layoutService.getContainer(Parts.STATUSBAR_PART);
if (dom.isInDOM(this.actionBarContainer.nativeElement) && dom.isInDOM(statusBar)) {
let toolbarBottom = this.actionBarContainer.nativeElement.getBoundingClientRect().bottom + ACTIONBAR_PADDING;
let statusTop = statusBar.getBoundingClientRect().top;
this._table.layout(new dom.Dimension(
dom.getContentWidth(this._gridEl.nativeElement),
statusTop - toolbarBottom));
let gridCanvasWidth = this._table.grid.getCanvasNode().clientWidth;
let placeholderWidth = dom.getDomNodePagePosition(this.placeholderElem).width;
dom.position(this.placeholderElem, null, null, null, (gridCanvasWidth - placeholderWidth) / 2, 'relative');
this._updateStyles(this._themeService.getColorTheme());
}
}
public showProgress(mode: AssessmentType) {
this.spinner[mode].style.visibility = 'visible';
if (this.isVisible) {
this._cd.detectChanges();
}
}
public showInitialResults(result: azdata.SqlAssessmentResult, method: AssessmentType) {
if (result) {
if (method === AssessmentType.InvokeAssessment) {
this.lastInvokedResults = result.items;
} else {
this.lastInvokedResults = [];
}
this.displayResults(result.items, method);
if (result.items.length > 0) {
this._asmtViewComponent.displayAssessmentInfo(result.apiVersion, result.items[0].rulesetVersion);
}
}
if (this.isVisible) {
this._cd.detectChanges();
}
this._table.grid.invalidate();
}
public appendResults(result: azdata.SqlAssessmentResult, method: AssessmentType) {
if (method === AssessmentType.InvokeAssessment) {
this.lastInvokedResults.push(...result.items);
}
if (result) {
this.dataView.beginUpdate();
result.items.forEach((asmtResult, index) => {
this.dataView.addItem(this.convertToDataViewItems(asmtResult, index, method));
});
this.dataView.reSort();
this.dataView.endUpdate();
this.dataView.refresh();
this._table.autosizeColumns();
this._table.resizeCanvas();
}
if (this.isVisible) {
this._cd.detectChanges();
}
this._table.grid.invalidate();
}
public stopProgress(mode: AssessmentType) {
this.spinner[mode].style.visibility = 'hidden';
if (this.isVisible) {
this._cd.detectChanges();
}
}
private initializeComponent() {
let columns = this.columns.map((column) => {
column.rerenderOnResize = true;
return column;
});
let options = <Slick.GridOptions<any>>{
syncColumnCellResize: true,
enableColumnReorder: false,
rowHeight: ROW_HEIGHT,
enableCellNavigation: true,
forceFitColumns: false
};
this.dataView = new Slick.Data.DataView({ inlineFilters: false });
let rowDetail = new RowDetailView({
cssClass: '_detail_selector',
process: (item) => {
(<any>rowDetail).onAsyncResponse.notify({
'itemDetail': item,
}, undefined, this);
},
useRowClick: true,
panelRows: 2,
postTemplate: (itemDetail) => this.appendHelplink(itemDetail.description, itemDetail.helpLink, itemDetail.kind, this.wrapByKind),
preTemplate: () => '',
loadOnce: true
});
this.rowDetail = rowDetail;
let columnDef = this.rowDetail.getColumnDefinition();
columnDef.formatter = (row, cell, value, columnDef, dataContext) => this.detailSelectionFormatter(row, cell, value, columnDef, dataContext as ExtendedItem<Slick.SlickData>);
columns.unshift(columnDef);
let filterPlugin = new HeaderFilter<Slick.SlickData>();
this._register(attachButtonStyler(filterPlugin, this._themeService));
this.filterPlugin = filterPlugin;
this.filterPlugin.onFilterApplied.subscribe((e, args) => {
let filterValues = args.column.filterValues;
if (filterValues) {
this.dataView.refresh();
this._table.grid.resetActiveCell();
}
});
this.filterPlugin.onCommand.subscribe((e, args: any) => {
this.columnSort(args.column.field, args.command === 'sort-asc');
});
// we need to be able to show distinct array values in filter dialog for columns with array data
filterPlugin['getFilterValues'] = this.getFilterValues;
filterPlugin['getAllFilterValues'] = this.getAllFilterValues;
filterPlugin['getFilterValuesByInput'] = this.getFilterValuesByInput;
dom.clearNode(this._gridEl.nativeElement);
dom.clearNode(this.actionBarContainer.nativeElement);
if (this.isServerMode) {
this.initActionBar(
this._register(this._instantiationService.createInstance(AsmtServerInvokeItemsAction)),
this._register(this._instantiationService.createInstance(AsmtServerSelectItemsAction)));
} else {
let connectionInfo = this._commonService.connectionManagementService.connectionInfo;
let databaseSelectAsmt = this._register(this._instantiationService.createInstance(AsmtDatabaseSelectItemsAction, connectionInfo.connectionProfile.databaseName));
let databaseInvokeAsmt = this._register(this._instantiationService.createInstance(AsmtDatabaseInvokeItemsAction, connectionInfo.connectionProfile.databaseName));
this.initActionBar(databaseInvokeAsmt, databaseSelectAsmt);
}
this._table = this._register(new Table(this._gridEl.nativeElement, { columns }, options));
this._table.grid.setData(this.dataView, true);
this._table.registerPlugin(<any>this.rowDetail);
this._table.registerPlugin(filterPlugin);
this.placeholderElem = document.createElement('span');
this.placeholderElem.className = 'placeholder';
this.placeholderElem.innerText = PLACEHOLDER_LABEL;
dom.append(this._table.grid.getCanvasNode(), this.placeholderElem);
}
private initActionBar(invokeAction: IAction, selectAction: IAction) {
this.exportActionItem = this._register(this._instantiationService.createInstance(AsmtExportAsScriptAction));
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._actionBar.setContent([
{ action: invokeAction },
{ element: this.spinner[AssessmentType.InvokeAssessment] },
{ action: selectAction },
{ element: this.spinner[AssessmentType.AvailableRules] },
{ action: this.exportActionItem },
{ action: this._instantiationService.createInstance(AsmtSamplesLinkAction) }
]);
let connectionInfo = this._commonService.connectionManagementService.connectionInfo;
let context: IAsmtActionInfo = { component: this, ownerUri: Utils.generateUri(connectionInfo.connectionProfile.clone(), 'dashboard'), connectionId: connectionInfo.connectionProfile.id };
this._actionBar.context = context;
this.exportActionItem.enabled = false;
}
private convertToDataViewItems(asmtResult: azdata.SqlAssessmentResultItem, index: number, method: AssessmentType) {
return {
id: `${asmtResult.targetType}${this.escapeId(asmtResult.targetName)}${asmtResult.checkId}${index}`,
severity: asmtResult.level,
message: method === AssessmentType.InvokeAssessment ? asmtResult.message : asmtResult.displayName,
tags: this.clearOutDefaultRuleset(asmtResult.tags),
checkId: asmtResult.checkId,
targetName: asmtResult.targetName,
targetType: asmtResult.targetType,
helpLink: asmtResult.helpLink,
description: method === AssessmentType.InvokeAssessment ? asmtResult.message : asmtResult.description,
mode: method,
kind: asmtResult.kind !== undefined ? asmtResult.kind : AssessmentResultItemKind.RealResult
};
}
private displayResults(results: azdata.SqlAssessmentResultItem[], method: AssessmentType) {
this._table.grid.updateColumnHeader(COLUMN_MESSAGE_ID, COLUMN_MESSAGE_TITLE[method]);
let resultViews = results.map((item, index) => this.convertToDataViewItems(item, index, method));
this.dataView.beginUpdate();
this.dataView.setItems(resultViews);
this.dataView.setFilter((item) => this.filter(item));
this.dataView.endUpdate();
this.dataView.refresh();
this._table.autosizeColumns();
this._table.resizeCanvas();
this.exportActionItem.enabled = (results.length > 0 && method === AssessmentType.InvokeAssessment);
if (results.length > 0) {
dom.hide(this.placeholderElem);
} else {
this.placeholderElem.innerText = this.placeholderNoResultsLabel;
}
}
private escapeId(value: string): string {
return escape(value).replace(/[*//]/g, function (match) {
switch (match) {
case '*':
case '/':
return '_';
default:
return match;
}
});
}
private clearOutDefaultRuleset(tags: string[]): string[] {
let idx = tags.indexOf('DefaultRuleset');
if (idx > -1) {
tags.splice(idx, 1);
}
return tags;
}
private columnSort(field: string, isAscending: boolean) {
this.dataView.sort((item1, item2) => {
if (item1.checkId === undefined || item2.checkId === undefined) {
return;
}
switch (field) {
case 'tags':
return item1.tags.toString().localeCompare(item2.tags.toString());
case 'targetName':
if (item1.targetType > item2.targetType) {
return 1;
} else if (item1.targetType < item2.targetType) {
return -1;
} else {
return item1.targetName.localeCompare(item2.targetName);
}
}
return item1[field].localeCompare(item2[field]);
}, isAscending);
}
private filter(item: any) {
let columns = this._table.grid.getColumns();
let value = true;
for (let i = 0; i < columns.length; i++) {
let col: any = columns[i];
let filterValues = col.filterValues;
if (filterValues && filterValues.length > 0) {
if (item._parent) {
value = value && find(filterValues, x => x === item._parent[col.field]);
} else {
let colValue = item[col.field];
if (colValue instanceof Array) {
value = value && find(filterValues, x => colValue.indexOf(x) >= 0);
} else {
value = value && find(filterValues, x => x === colValue);
}
}
}
}
return value;
}
private wrapByKind(kind: AssessmentResultItemKind, element: string): string {
if (kind !== AssessmentResultItemKind.RealResult) {
return `<span class='excl ${KIND_CLASS[kind]}'>${element}</span>`;
}
return element;
}
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 undefined;
}
private renderTags(_row, _cell, _value, _columnDef, dataContext) {
if (dataContext.tags !== undefined) {
return dataContext.tags.join(`, `);
}
return dataContext.tags;
}
private renderTarget(_row, _cell, _value, _columnDef, dataContext) {
return `<div class='carbon-taskbar'><span class='action-label codicon ${TARGET_ICON_CLASS[dataContext.targetType]}'>${dataContext.targetName}</span></div>`;
}
private detailSelectionFormatter(_row: number, _cell: number, _value: any, _columnDef: Slick.Column<Slick.SlickData>, dataContext: Slick.SlickData): string | undefined {
if (dataContext._collapsed === undefined) {
dataContext._collapsed = true;
dataContext._sizePadding = 0; //the required number of pading rows
dataContext._height = 0; //the actual height in pixels of the detail field
dataContext._isPadding = false;
dataContext._parent = undefined;
}
if (dataContext._isPadding === true) {
//render nothing
} else if (dataContext._collapsed) {
return '<div class=\'detailView-toggle expand\'></div>';
} else {
const html: Array<string> = [];
const rowHeight = ROW_HEIGHT;
const bottomMargin = 5;
html.push('<div class="detailView-toggle collapse"></div></div>');
html.push(`<div id='cellDetailView_${dataContext.id}' class='dynamic-cell-detail dynamic-cell-detail-color' `); //apply custom css to detail
html.push(`style=\'height:${dataContext._height}px;`); //set total height of padding
html.push(`top:${rowHeight}px'>`); //shift detail below 1st row
html.push(`<div id='detailViewContainer_${dataContext.id}"' class='detail-container' style='max-height:${(dataContext._height! - rowHeight + bottomMargin)}px'>`); //sub ctr for custom styling
html.push(`<div id='innerDetailView_${dataContext.id}'>${dataContext._detailContent!}</div></div>`);
return html.join('');
}
return undefined;
}
private getFilterValues(dataView: Slick.DataProvider<Slick.SlickData>, column: Slick.Column<any>): Array<any> {
const seen: Array<string> = [];
for (let i = 0; i < dataView.getLength(); i++) {
const value = dataView.getItem(i)[column.field!];
if (value instanceof Array) {
for (let item = 0; item < value.length; item++) {
if (!seen.some(x => x === value[item])) {
seen.push(value[item]);
}
}
} else {
if (!seen.some(x => x === value)) {
seen.push(value);
}
}
}
return seen;
}
private getAllFilterValues(data: Array<Slick.SlickData>, column: Slick.Column<any>) {
const seen: Array<any> = [];
for (let i = 0; i < data.length; i++) {
const value = data[i][column.field!];
if (value instanceof Array) {
for (let item = 0; item < value.length; item++) {
if (!seen.some(x => x === value[item])) {
seen.push(value[item]);
}
}
} else {
if (!seen.some(x => x === value)) {
seen.push(value);
}
}
}
return seen.sort((v) => { return v; });
}
private getFilterValuesByInput($input: JQuery<HTMLElement>): Array<string> {
const column = $input.data('column'),
filter = $input.val() as string,
dataView = this['grid'].getData() as Slick.DataProvider<Slick.SlickData>,
seen: Array<any> = [];
for (let i = 0; i < dataView.getLength(); i++) {
const value = dataView.getItem(i)[column.field];
if (value instanceof Array) {
if (filter.length > 0) {
const itemValue = !value ? [] : value;
const lowercaseFilter = filter.toString().toLowerCase();
const lowercaseVals = itemValue.map(v => v.toLowerCase());
for (let valIdx = 0; valIdx < value.length; valIdx++) {
if (!seen.some(x => x === value[valIdx]) && lowercaseVals[valIdx].indexOf(lowercaseFilter) > -1) {
seen.push(value[valIdx]);
}
}
}
else {
for (let item = 0; item < value.length; item++) {
if (!seen.some(x => x === value[item])) {
seen.push(value[item]);
}
}
}
} else {
if (filter.length > 0) {
const itemValue = !value ? '' : value;
const lowercaseFilter = filter.toString().toLowerCase();
const lowercaseVal = itemValue.toString().toLowerCase();
if (!seen.some(x => x === value) && lowercaseVal.indexOf(lowercaseFilter) > -1) {
seen.push(value);
}
}
else {
if (!seen.some(x => x === value)) {
seen.push(value);
}
}
}
}
return seen.sort((v) => { return v; });
}
private _updateStyles(theme: IColorTheme): void {
this.actionBarContainer.nativeElement.style.borderTopColor = theme.getColor(themeColors.DASHBOARD_BORDER, true).toString();
let tableStyle: ITableStyles = {
tableHeaderBackground: theme.getColor(themeColors.PANEL_BACKGROUND)
};
this._table.style(tableStyle);
const rowExclSelector = '.asmtview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row';
dom.removeCSSRulesContainingSelector(`${rowExclSelector} .${KIND_CLASS[AssessmentResultItemKind.Error]}`);
dom.createCSSRule(`${rowExclSelector} .${KIND_CLASS[AssessmentResultItemKind.Error]}`, `color: ${theme.getColor(themeColors.NOTIFICATIONS_ERROR_ICON_FOREGROUND).toString()}`);
dom.removeCSSRulesContainingSelector(`${rowExclSelector} .${KIND_CLASS[AssessmentResultItemKind.Warning]}`);
dom.createCSSRule(`${rowExclSelector} .${KIND_CLASS[AssessmentResultItemKind.Warning]}`, `color: ${theme.getColor(themeColors.NOTIFICATIONS_WARNING_ICON_FOREGROUND).toString()}`);
const detailRowSelector = '.asmtview-grid .grid-canvas > .ui-widget-content.slick-row .dynamic-cell-detail-color';
dom.removeCSSRulesContainingSelector(detailRowSelector);
dom.createCSSRule(detailRowSelector, `background-color: ${theme.getColor(themeColors.EDITOR_GROUP_HEADER_TABS_BACKGROUND).toString()}`);
}
}

View File

@@ -0,0 +1,57 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div id="asmtViewDiv" class="fullsize">
<div style="margin: 10px 0px 0px 10px;">
<div style="float: left; margin-right: 30px;">
<div class="propertiesSectionTitle">
<span>{{localizedStrings.SECTION_TITLE_API}}</span>
</div>
<div>
<div class="propertyBlock">
<div>
<span class="propertyLabel">{{localizedStrings.API_VERSION}}</span>
<span>{{api}}</span>
</div>
<div>
<span class="propertyLabel">{{localizedStrings.DEFAULT_RULESET_VERSION}}</span>
<span>{{ruleset}}</span>
</div>
</div>
</div>
</div>
<div>
<div class="propertiesSectionTitle">
<span>{{localizedStrings.SECTION_TITLE_SQL_SERVER}}</span>
</div>
<div>
<div class="propertyBlock">
<div>
<span class="propertyLabel">{{localizedStrings.SERVER_VERSION}}</span>
<span>{{connectionInfo.serverVersion}}</span>
</div>
<div>
<span class="propertyLabel">{{localizedStrings.SERVER_EDITION}}</span>
<span>{{connectionInfo.serverEdition}}</span>
</div>
</div>
<div class="propertyBlock">
<div>
<span class="propertyLabel">{{localizedStrings.SERVER_INSTANCENAME}}</span>
<span>{{instanceName}}</span>
</div>
<div>
<span class="propertyLabel">{{localizedStrings.SERVER_OSVERSION}}</span>
<span>{{connectionInfo.osVersion}}</span>
</div>
</div>
</div>
</div>
</div>
<div #asmtresultcomponent id="asmtDiv" class="fullsize">
<asmt-results-view-component></asmt-results-view-component>
</div>
</div>

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/asmt';
import { Component, Inject, forwardRef, ChangeDetectorRef, ViewChild, 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:")
};
export const DASHBOARD_SELECTOR: string = 'asmtview-component';
@Component({
selector: DASHBOARD_SELECTOR,
templateUrl: decodeURI(require.toUrl('./asmtView.component.html'))
})
@Injectable()
export class AsmtViewComponent extends AngularDisposable implements OnInit {
@ViewChild('asmtresultcomponent') private _asmtResultView: AsmtResultsViewComponent;
protected localizedStrings = LocalizedStrings;
connectionInfo: ServerInfo = null;
instanceName: string = '';
ruleset: string = '';
api: string = '';
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface) {
super();
}
ngOnInit() {
this.displayConnectionInfo();
}
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;
}
}
public displayAssessmentInfo(apiVersion: string, rulesetVersion: string) {
this.api = apiVersion;
this.ruleset = rulesetVersion;
this._cd.detectChanges();
}
public layout() {
this._asmtResultView.layout();
//this._panel.layout();
}
}

View File

@@ -0,0 +1,132 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
asmtview-component {
height: 100%;
width: 100%;
display: block;
}
.asmt-heading {
font-weight: bold;
font-size: large;
}
#asmtViewDiv .propertiesSectionTitle {
margin-bottom: 20px;
font-size: larger;
}
#asmtViewDiv .propertyBlock {
display: inline-block;
margin: 0px 0px 20px 20px;
}
#asmtViewDiv .propertyLabel {
font-size: 11px;
}
.asmt-actionbar-container {
padding-bottom: 10px;
border-top: 3px solid;
}
.asmtview-grid {
height: calc(100% - 75px);
width: 100%;
display: block;
}
#asmtViewDiv .slick-header-column {
border: 0px !important;
font-weight: bold;
}
.asmtview-grid>.monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted;
}
.asmtview-grid .grid-canvas>.ui-widget-content.slick-row>.slick-cell {
cursor: pointer;
border-right: none;
}
.asmtview-grid>.monaco-table .slick-viewport>.grid-canvas>.ui-widget-content.slick-row .slick-cell>.excl,
.asmtview-grid .detail-container .excl {
width: 100%;
opacity: 1;
font-weight: 700;
text-overflow: ellipsis;
}
#asmtDiv .detail {
padding: 5px;
}
#asmtDiv .preload {
font-size: 13px;
}
#asmtDiv .codicon.in-progress {
padding-left: 0px;
}
#asmtDiv .carbon-taskbar .action-item {
margin-left: 0px;
}
.asmt-actionbar-container .monaco-action-bar>ul.actions-container>li.action-item {
padding-left: 20px;
}
.asmt-actionbar-container .actions-container .action-item .action-label {
padding-right: 0px;
}
asmtview-component .asmtview-grid .slick-cell.error-row {
opacity: 0;
}
#asmtDiv asmtview-component .monaco-toolbar.carbon-taskbar {
margin: 10px 0px 10px 0px;
}
#asmtDiv .helpLink {
margin-left: 5px;
}
.vs asmtview-component .action-label.codicon.exportAsScriptIcon {
background-image: url("newquery.svg");
}
.vs-dark asmtview-component .action-label.codicon.exportAsScriptIcon,
.hc-black asmtview-component .action-label.codicon.exportAsScriptIcon {
background-image: url("newquery_inverse.svg");
}
.vs asmtview-component .action-label.codicon.asmt-learnmore {
background-image: url("configuredashboard.svg");
}
.vs-dark asmtview-component .action-label.codicon.asmt-learnmore,
.hc-black asmtview-component .action-label.codicon.asmt-learnmore {
background-image: url("configuredashboard_inverse.svg");
}
.asmtview-grid>.monaco-table .slick-viewport>.grid-canvas>.ui-widget-content.slick-row .slick-cell .codicon {
background-position: left;
padding-left: 18px;
}
.asmt-actionbar-container .action-item>.action-label.codicon.database {
background-size: 12px;
}
#asmtDiv .placeholder {
font-style: italic;
position: relative;
top: 50px;
}

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 8H8V15H0V0H15V8ZM7 8H1V14H7V8ZM7 1H1V7H7V1ZM14 1H8V7H14V1ZM14.9531 12.0234C14.9844 12.1797 15 12.3385 15 12.5C15 12.6615 14.9844 12.8203 14.9531 12.9766L15.9219 13.375L15.5391 14.3047L14.5703 13.8984C14.4818 14.0339 14.3802 14.1589 14.2656 14.2734C14.1562 14.3828 14.0339 14.4818 13.8984 14.5703L14.3047 15.5391L13.375 15.9219L12.9766 14.9531C12.8203 14.9844 12.6615 15 12.5 15C12.3385 15 12.1797 14.9844 12.0234 14.9531L11.625 15.9219L10.6953 15.5391L11.1016 14.5703C10.8307 14.3932 10.6068 14.1693 10.4297 13.8984L9.46094 14.3047L9.07812 13.375L10.0469 12.9766C10.0156 12.8203 10 12.6615 10 12.5C10 12.3385 10.0156 12.1797 10.0469 12.0234L9.07812 11.625L9.46094 10.6953L10.4297 11.1016C10.5182 10.9661 10.6172 10.8438 10.7266 10.7344C10.8411 10.6198 10.9661 10.5182 11.1016 10.4297L10.6953 9.46094L11.625 9.07812L12.0234 10.0469C12.0964 10.0365 12.1667 10.0286 12.2344 10.0234C12.3073 10.013 12.3802 10.0078 12.4531 10.0078C12.5417 10.0078 12.6276 10.013 12.7109 10.0234C12.7943 10.0339 12.8802 10.0469 12.9688 10.0625L13.375 9.07812L14.3047 9.46094L13.8906 10.4453C14.026 10.5339 14.151 10.6328 14.2656 10.7422C14.3802 10.8464 14.4818 10.9661 14.5703 11.1016L15.5391 10.6953L15.9219 11.625L14.9531 12.0234ZM12.5078 14C12.7109 14 12.9036 13.9609 13.0859 13.8828C13.2682 13.7995 13.4271 13.6901 13.5625 13.5547C13.6979 13.4193 13.8047 13.2604 13.8828 13.0781C13.9609 12.8958 14 12.7031 14 12.5C14 12.2917 13.9609 12.0964 13.8828 11.9141C13.8047 11.7318 13.6979 11.5729 13.5625 11.4375C13.4271 11.3021 13.2682 11.1953 13.0859 11.1172C12.9036 11.0391 12.7083 11 12.5 11C12.2969 11 12.1042 11.0391 11.9219 11.1172C11.7396 11.1953 11.5781 11.3047 11.4375 11.4453C11.3021 11.5807 11.1953 11.7396 11.1172 11.9219C11.0391 12.1042 11 12.2969 11 12.5C11 12.7083 11.0391 12.9036 11.1172 13.0859C11.2005 13.2682 11.3099 13.4271 11.4453 13.5625C11.5807 13.6979 11.7396 13.8047 11.9219 13.8828C12.1094 13.9609 12.3047 14 12.5078 14Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 8H8V15H0V0H15V8ZM7 8H1V14H7V8ZM7 1H1V7H7V1ZM14 1H8V7H14V1ZM14.9531 12.0234C14.9844 12.1797 15 12.3385 15 12.5C15 12.6615 14.9844 12.8203 14.9531 12.9766L15.9219 13.375L15.5391 14.3047L14.5703 13.8984C14.4818 14.0339 14.3802 14.1589 14.2656 14.2734C14.1562 14.3828 14.0339 14.4818 13.8984 14.5703L14.3047 15.5391L13.375 15.9219L12.9766 14.9531C12.8203 14.9844 12.6615 15 12.5 15C12.3385 15 12.1797 14.9844 12.0234 14.9531L11.625 15.9219L10.6953 15.5391L11.1016 14.5703C10.8307 14.3932 10.6068 14.1693 10.4297 13.8984L9.46094 14.3047L9.07812 13.375L10.0469 12.9766C10.0156 12.8203 10 12.6615 10 12.5C10 12.3385 10.0156 12.1797 10.0469 12.0234L9.07812 11.625L9.46094 10.6953L10.4297 11.1016C10.5182 10.9661 10.6172 10.8438 10.7266 10.7344C10.8411 10.6198 10.9661 10.5182 11.1016 10.4297L10.6953 9.46094L11.625 9.07812L12.0234 10.0469C12.0964 10.0365 12.1667 10.0286 12.2344 10.0234C12.3073 10.013 12.3802 10.0078 12.4531 10.0078C12.5417 10.0078 12.6276 10.013 12.7109 10.0234C12.7943 10.0339 12.8802 10.0469 12.9688 10.0625L13.375 9.07812L14.3047 9.46094L13.8906 10.4453C14.026 10.5339 14.151 10.6328 14.2656 10.7422C14.3802 10.8464 14.4818 10.9661 14.5703 11.1016L15.5391 10.6953L15.9219 11.625L14.9531 12.0234ZM12.5078 14C12.7109 14 12.9036 13.9609 13.0859 13.8828C13.2682 13.7995 13.4271 13.6901 13.5625 13.5547C13.6979 13.4193 13.8047 13.2604 13.8828 13.0781C13.9609 12.8958 14 12.7031 14 12.5C14 12.2917 13.9609 12.0964 13.8828 11.9141C13.8047 11.7318 13.6979 11.5729 13.5625 11.4375C13.4271 11.3021 13.2682 11.1953 13.0859 11.1172C12.9036 11.0391 12.7083 11 12.5 11C12.2969 11 12.1042 11.0391 11.9219 11.1172C11.7396 11.1953 11.5781 11.3047 11.4375 11.4453C11.3021 11.5807 11.1953 11.7396 11.1172 11.9219C11.0391 12.1042 11 12.2969 11 12.5C11 12.7083 11.0391 12.9036 11.1172 13.0859C11.2005 13.2682 11.3099 13.4271 11.4453 13.5625C11.5807 13.6979 11.7396 13.8047 11.9219 13.8828C12.1094 13.9609 12.3047 14 12.5078 14Z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.asmtview-grid .grid-canvas>.ui-widget-content.slick-row .detailView-toggle {
display: inline-block;
cursor: pointer;
}
.asmtview-grid .grid-canvas>.ui-widget-content.slick-row .dynamic-cell-detail {
z-index: 101;
position: absolute;
margin: 0;
padding: 0;
width: 100%;
overflow: auto;
}
.asmtview-grid .grid-canvas>.ui-widget-content.slick-row .dynamic-cell-detail> :first-child {
vertical-align: middle;
line-height: 13px;
padding: 10px;
margin-left: 20px;
}
.asmtview-grid .grid-canvas>.ui-widget-content.slick-row .dynamic-cell-detail>.detail-container {
overflow: auto;
display: block !important;
max-height: 100px !important;
line-height: 20px;
}
.asmtview-grid .grid-canvas>.ui-widget-content.slick-row .dynamic-cell-detail>.detail-container>div,
.asmtview-grid .grid-canvas>.ui-widget-content.slick-row .dynamic-cell-detail>.detail-container>div>span {
white-space: normal;
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#212121;}</style></defs><title>newquery_16x16</title><path class="cls-1" d="M1.9,4H.38V2.45H1.9Zm0,9.14H.38V11.59H1.9ZM15.63,4H4.19V2.45H15.63Zm0,9.14H4.19V11.59H15.63Zm0-6.09H7.23V5.5h8.39Zm0,3H10.29V8.54h5.34Z"/></svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1,.cls-2{fill:#fff;}.cls-2{font-size:12px;font-family:FullMDL2Assets, Full MDL2 Assets;}</style></defs><title>newquery_inverse_16x16</title><path class="cls-1" d="M1.79,3.85H.26V2.33H1.79Zm0,9.14H.26V11.47H1.79ZM15.51,3.85H4.08V2.33H15.51Zm0,9.14H4.08V11.47H15.51Zm0-6.09H7.12V5.38h8.39Zm0,3H10.18V8.42h5.34Z"/><text class="cls-2" transform="translate(0.01 11.59)"> </text></svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@@ -0,0 +1,227 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 { IAssessmentService } from 'sql/workbench/services/assessment/common/interfaces';
import { SqlAssessmentResult, SqlAssessmentResultItem } from 'azdata';
import { IOpenerService } from 'vs/platform/opener/common/opener';
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';
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[];
isActive: boolean;
}
export class IAsmtActionInfo {
ownerUri?: string;
component: IAssessmentComponent;
connectionId: string;
}
abstract class AsmtServerAction extends Action {
constructor(
id: string,
label: string,
private asmtType: AssessmentType,
@IConnectionManagementService private _connectionManagement: IConnectionManagementService,
@ILogService protected _logService: ILogService,
@IAdsTelemetryService protected _telemetryService: IAdsTelemetryService
) {
super(id, label, TARGET_ICON_CLASS[AssessmentTargetType.Server]);
}
public async run(context: IAsmtActionInfo): Promise<boolean> {
this._telemetryService.sendActionEvent(TelemetryView.SqlAssessment, this.id);
if (context && context.component) {
context.component.showProgress(this.asmtType);
let serverResults = this.getServerItems(context.ownerUri);
let connectionUri: string = this._connectionManagement.getConnectionUriFromId(context.connectionId);
let connection = this._connectionManagement.getConnection(connectionUri);
let databaseListResult = this._connectionManagement.listDatabases(connectionUri);
context.component.showInitialResults(await serverResults, this.asmtType);
let dbList = await databaseListResult;
if (dbList) {
for (let nDbName = 0; nDbName < dbList.databaseNames.length; nDbName++) {
if (!context.component.isActive) {
break;
}
let dbName = dbList.databaseNames[nDbName];
let newUri = await this._connectionManagement.connectIfNotConnected(connection.cloneWithDatabase(dbName).clone());
this._logService.info(`Database ${dbName} assessment started`);
let dbResult = await this.getDatabaseItems(newUri);
this._logService.info(`Database ${dbName} assessment completed`);
context.component.appendResults(dbResult, this.asmtType);
}
}
context.component.stopProgress(this.asmtType);
return true;
}
return false;
}
abstract getServerItems(ownerUri: string): Thenable<SqlAssessmentResult>;
abstract getDatabaseItems(ownerUri: string): Thenable<SqlAssessmentResult>;
}
export class AsmtServerSelectItemsAction extends AsmtServerAction {
public static ID = 'asmtaction.server.getitems';
public static LABEL = nls.localize('asmtaction.server.getitems', "View applicable rules");
constructor(
@IConnectionManagementService _connectionManagement: IConnectionManagementService,
@ILogService _logService: ILogService,
@IAssessmentService private _assessmentService: IAssessmentService,
@IAdsTelemetryService _telemetryService: IAdsTelemetryService
) {
super(AsmtServerSelectItemsAction.ID, AsmtServerSelectItemsAction.LABEL,
AssessmentType.AvailableRules,
_connectionManagement,
_logService, _telemetryService);
}
getServerItems(ownerUri: string): Thenable<SqlAssessmentResult> {
return this._assessmentService.getAssessmentItems(ownerUri, AssessmentTargetType.Server);
}
getDatabaseItems(ownerUri: string): Thenable<SqlAssessmentResult> {
return this._assessmentService.getAssessmentItems(ownerUri, AssessmentTargetType.Database);
}
}
export class AsmtDatabaseSelectItemsAction extends Action {
public static ID = 'asmtaction.database.getitems';
constructor(
databaseName: string,
@IAssessmentService private _assessmentService: IAssessmentService,
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService
) {
super(AsmtDatabaseSelectItemsAction.ID,
nls.localize('asmtaction.database.getitems', "View applicable rules for {0}", databaseName),
TARGET_ICON_CLASS[AssessmentTargetType.Database]);
}
public async run(context: IAsmtActionInfo): Promise<boolean> {
this._telemetryService.sendActionEvent(TelemetryView.SqlAssessment, this.id);
if (context && context.component) {
context.component.showProgress(AssessmentType.AvailableRules);
let dbAsmtResults = await this._assessmentService.getAssessmentItems(context.ownerUri, AssessmentTargetType.Database);
context.component.showInitialResults(dbAsmtResults, AssessmentType.AvailableRules);
context.component.stopProgress(AssessmentType.AvailableRules);
return true;
}
return false;
}
}
export class AsmtServerInvokeItemsAction extends AsmtServerAction {
public static ID = 'asmtaction.server.invokeitems';
public static LABEL = nls.localize('asmtaction.server.invokeitems', "Invoke Assessment");
constructor(
@IConnectionManagementService _connectionManagement: IConnectionManagementService,
@ILogService _logService: ILogService,
@IAssessmentService private _assessmentService: IAssessmentService,
@IAdsTelemetryService _telemetryService: IAdsTelemetryService
) {
super(AsmtServerInvokeItemsAction.ID, AsmtServerInvokeItemsAction.LABEL, AssessmentType.InvokeAssessment, _connectionManagement, _logService, _telemetryService);
}
getServerItems(ownerUri: string): Thenable<SqlAssessmentResult> {
this._logService.info(`Requesting server items`);
return this._assessmentService.assessmentInvoke(ownerUri, AssessmentTargetType.Server);
}
getDatabaseItems(ownerUri: string): Thenable<SqlAssessmentResult> {
return this._assessmentService.assessmentInvoke(ownerUri, AssessmentTargetType.Database);
}
}
export class AsmtDatabaseInvokeItemsAction extends Action {
public static ID = 'asmtaction.database.invokeitems';
constructor(
databaseName: string,
@IAssessmentService private _assessmentService: IAssessmentService,
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService
) {
super(AsmtDatabaseInvokeItemsAction.ID,
nls.localize('asmtaction.database.invokeitems', "Invoke Assessment for {0}", databaseName),
TARGET_ICON_CLASS[AssessmentTargetType.Database]);
}
public async run(context: IAsmtActionInfo): Promise<boolean> {
this._telemetryService.sendActionEvent(TelemetryView.SqlAssessment, this.id);
if (context && context.component) {
context.component.showProgress(AssessmentType.InvokeAssessment);
let dbAsmtResults = await this._assessmentService.assessmentInvoke(context.ownerUri, AssessmentTargetType.Database);
context.component.showInitialResults(dbAsmtResults, AssessmentType.InvokeAssessment);
context.component.stopProgress(AssessmentType.InvokeAssessment);
return true;
}
return false;
}
}
export class AsmtExportAsScriptAction extends Action {
public static ID = 'asmtaction.exportasscript';
public static LABEL = nls.localize('asmtaction.exportasscript', "Export As Script");
constructor(
@IAssessmentService private _assessmentService: IAssessmentService,
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService
) {
super(AsmtExportAsScriptAction.ID, AsmtExportAsScriptAction.LABEL, 'exportAsScriptIcon');
}
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);
return true;
}
return false;
}
}
export class AsmtSamplesLinkAction extends Action {
public static readonly ID = 'asmtaction.showsamples';
public static readonly LABEL = nls.localize('asmtaction.showsamples', "View all rules and learn more on GitHub");
public static readonly ICON = 'asmt-learnmore';
private static readonly configHelpUri = 'https://aka.ms/sql-assessment-api';
constructor(
@IOpenerService private _openerService: IOpenerService,
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService
) {
super(AsmtSamplesLinkAction.ID, AsmtSamplesLinkAction.LABEL, AsmtSamplesLinkAction.ICON);
}
public async run(): Promise<boolean> {
this._telemetryService.sendActionEvent(TelemetryView.SqlAssessment, AsmtSamplesLinkAction.ID);
return this._openerService.open(URI.parse(AsmtSamplesLinkAction.configHelpUri));
}
}

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export enum AssessmentTargetType {
Server = 1,
Database = 2
}
export enum AssessmentType {
AvailableRules = 1,
InvokeAssessment = 2
}
export const TARGET_ICON_CLASS: { [targetType: number]: string } = {
[AssessmentTargetType.Database]: 'database',
[AssessmentTargetType.Server]: 'server-page'
};

View File

@@ -0,0 +1,184 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as TypeMoq from 'typemoq';
import * as assert from 'assert';
import { AssessmentType, AssessmentTargetType } from 'sql/workbench/contrib/assessment/common/consts';
import {
IAssessmentComponent,
AsmtServerInvokeItemsAction,
AsmtServerSelectItemsAction,
AsmtExportAsScriptAction,
AsmtSamplesLinkAction,
AsmtDatabaseInvokeItemsAction,
AsmtDatabaseSelectItemsAction
} from 'sql/workbench/contrib/assessment/common/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';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { TestConnectionManagementService } from 'sql/platform/connection/test/common/testConnectionManagementService';
import { NullLogService } from 'vs/platform/log/common/log';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { OpenerServiceStub } from 'sql/platform/opener/common/openerServiceStub';
/**
* Class to test Assessment Management Actions
*/
let assessmentResultItems: azdata.SqlAssessmentResultItem[] = [
<azdata.SqlAssessmentResultItem>{ checkId: 'check1' },
<azdata.SqlAssessmentResultItem>{ checkId: 'check2' },
<azdata.SqlAssessmentResultItem>{ checkId: 'check3' }
];
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: '',
apiVersion: '',
items: assessmentResultItems
};
// Tests
suite('Assessment Actions', () => {
// Actions
setup(() => {
mockAsmtViewComponent = TypeMoq.Mock.ofType<IAssessmentComponent>(AssessmentTestViewComponent);
mockAssessmentService = TypeMoq.Mock.ofType<AssessmentService>(AssessmentService);
mockAssessmentService.setup(s => s.assessmentInvoke(TypeMoq.It.isAny(), AssessmentTargetType.Server)).returns(() => Promise.resolve(assessmentResult));
mockAssessmentService.setup(s => s.assessmentInvoke(TypeMoq.It.isAny(), AssessmentTargetType.Database)).returns(() => Promise.resolve(assessmentResult));
mockAssessmentService.setup(s => s.getAssessmentItems(TypeMoq.It.isAny(), AssessmentTargetType.Server)).returns(() => Promise.resolve(assessmentResult));
mockAssessmentService.setup(s => s.getAssessmentItems(TypeMoq.It.isAny(), AssessmentTargetType.Database)).returns(() => Promise.resolve(assessmentResult));
let resultStatus: azdata.ResultStatus = {
success: true,
errorMessage: null
};
mockAssessmentService.setup(s => s.generateAssessmentScript(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => Promise.resolve(resultStatus));
});
function createConnectionManagementService(dbListResult: azdata.ListDatabasesResult): TypeMoq.Mock<IConnectionManagementService> {
let connectionProfile = TypeMoq.Mock.ofType<ConnectionProfile>(ConnectionProfile);
connectionProfile.setup(cp => cp.cloneWithDatabase(TypeMoq.It.isAnyString())).returns(() => connectionProfile.object);
connectionProfile.setup(cp => cp.clone()).returns(() => connectionProfile.object);
let connectionManagementService = TypeMoq.Mock.ofType<IConnectionManagementService>(TestConnectionManagementService);
connectionManagementService.setup(c => c.listDatabases(TypeMoq.It.isAny())).returns(() => Promise.resolve(dbListResult));
connectionManagementService.setup(c => c.getConnectionUriFromId(TypeMoq.It.isAny())).returns(() => '');
connectionManagementService.setup(c => c.getConnection(TypeMoq.It.isAny())).returns(() => connectionProfile.object);
connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.isAny())).returns(() => Promise.resolve(''));
return connectionManagementService;
}
test('Get Server Assessment Items Action', async () => {
const dbListResult: azdata.ListDatabasesResult = {
databaseNames: ['db1', 'db2']
};
const connectionManagementService = createConnectionManagementService(dbListResult);
const action = new AsmtServerSelectItemsAction(connectionManagementService.object, new NullLogService(), mockAssessmentService.object, new NullAdsTelemetryService());
assert.equal(action.id, AsmtServerSelectItemsAction.ID, 'Get Server Rules id action mismatch');
assert.equal(action.label, AsmtServerSelectItemsAction.LABEL, 'Get Server Rules label action mismatch');
let result = await action.run({ ownerUri: '', component: mockAsmtViewComponent.object, connectionId: '' });
assert.ok(result, 'Get Server Rules action should succeed');
mockAsmtViewComponent.verify(s => s.showProgress(AssessmentType.AvailableRules), TypeMoq.Times.once());
mockAssessmentService.verify(s => s.getAssessmentItems(TypeMoq.It.isAny(), AssessmentTargetType.Server), TypeMoq.Times.once());
mockAsmtViewComponent.verify(s => s.showInitialResults(TypeMoq.It.isAny(), AssessmentType.AvailableRules), TypeMoq.Times.once());
// should be executed for every db in database list
mockAssessmentService.verify(s => s.getAssessmentItems(TypeMoq.It.isAny(), AssessmentTargetType.Database), TypeMoq.Times.exactly(dbListResult.databaseNames.length));
mockAsmtViewComponent.verify(s => s.appendResults(TypeMoq.It.isAny(), AssessmentType.AvailableRules), TypeMoq.Times.exactly(dbListResult.databaseNames.length));
mockAsmtViewComponent.verify(s => s.stopProgress(AssessmentType.AvailableRules), TypeMoq.Times.once());
});
test('Invoke Server Assessment Action', async () => {
const dbListResult: azdata.ListDatabasesResult = {
databaseNames: ['db1', 'db2']
};
const connectionManagementService = createConnectionManagementService(dbListResult);
const action = new AsmtServerInvokeItemsAction(connectionManagementService.object, new NullLogService(), mockAssessmentService.object, new NullAdsTelemetryService());
assert.equal(action.id, AsmtServerInvokeItemsAction.ID, 'Invoke Server Assessment id action mismatch');
assert.equal(action.label, AsmtServerInvokeItemsAction.LABEL, 'Invoke Server Assessment label action mismatch');
let result = await action.run({ ownerUri: '', component: mockAsmtViewComponent.object, connectionId: '' });
assert.ok(result, 'Invoke Server Assessment action should succeed');
mockAsmtViewComponent.verify(s => s.showProgress(AssessmentType.InvokeAssessment), TypeMoq.Times.once());
mockAssessmentService.verify(s => s.assessmentInvoke(TypeMoq.It.isAny(), AssessmentTargetType.Server), TypeMoq.Times.once());
mockAsmtViewComponent.verify(s => s.showInitialResults(TypeMoq.It.isAny(), AssessmentType.InvokeAssessment), TypeMoq.Times.once());
// should be executed for every db in database list
mockAssessmentService.verify(s => s.assessmentInvoke(TypeMoq.It.isAny(), AssessmentTargetType.Database), TypeMoq.Times.exactly(dbListResult.databaseNames.length));
mockAsmtViewComponent.verify(s => s.appendResults(TypeMoq.It.isAny(), AssessmentType.InvokeAssessment), TypeMoq.Times.exactly(dbListResult.databaseNames.length));
mockAsmtViewComponent.verify(s => s.stopProgress(AssessmentType.InvokeAssessment), TypeMoq.Times.once());
});
test('Get Assessment Items Database Action', async () => {
const action = new AsmtDatabaseSelectItemsAction('databaseName', mockAssessmentService.object, new NullAdsTelemetryService());
assert.equal(action.id, AsmtDatabaseSelectItemsAction.ID, 'Get Database Rules id action mismatch');
let result = await action.run({ ownerUri: '', component: mockAsmtViewComponent.object, connectionId: '' });
assert.ok(result, 'Get Assessment Database action should succeed');
mockAsmtViewComponent.verify(s => s.showProgress(AssessmentType.AvailableRules), TypeMoq.Times.once());
mockAsmtViewComponent.verify(s => s.showInitialResults(TypeMoq.It.isAny(), AssessmentType.AvailableRules), TypeMoq.Times.once());
mockAsmtViewComponent.verify(s => s.stopProgress(AssessmentType.AvailableRules), TypeMoq.Times.once());
mockAssessmentService.verify(s => s.getAssessmentItems(TypeMoq.It.isAny(), AssessmentTargetType.Database), TypeMoq.Times.once());
});
test('Invoke Database Assessment Action', async () => {
const action = new AsmtDatabaseInvokeItemsAction('databaseName', mockAssessmentService.object, new NullAdsTelemetryService());
assert.equal(action.id, AsmtDatabaseInvokeItemsAction.ID, 'Invoke Database Assessment id action mismatch');
let result = await action.run({ ownerUri: '', component: mockAsmtViewComponent.object, connectionId: '' });
assert.ok(result, 'Invoke Database Assessment action should succeed');
mockAsmtViewComponent.verify(s => s.showProgress(AssessmentType.InvokeAssessment), TypeMoq.Times.once());
mockAsmtViewComponent.verify(s => s.showInitialResults(TypeMoq.It.isAny(), AssessmentType.InvokeAssessment), TypeMoq.Times.once());
mockAsmtViewComponent.verify(s => s.stopProgress(AssessmentType.InvokeAssessment), TypeMoq.Times.once());
mockAssessmentService.verify(s => s.assessmentInvoke(TypeMoq.It.isAny(), AssessmentTargetType.Database), TypeMoq.Times.once());
});
test('Generate Script Action', async () => {
const action = new AsmtExportAsScriptAction(mockAssessmentService.object, new NullAdsTelemetryService());
assert.equal(action.id, AsmtExportAsScriptAction.ID, 'Generate Assessment script id action mismatch');
assert.equal(action.label, AsmtExportAsScriptAction.LABEL, 'Generate Assessment script label action mismatch');
let result = await action.run({ ownerUri: '', component: mockAsmtViewComponent.object, connectionId: '' });
assert.ok(result, 'Generate Script action should succeed');
mockAssessmentService.verify(s => s.generateAssessmentScript(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
});
test('Samples Link Action', async () => {
let openerService = TypeMoq.Mock.ofType<IOpenerService>(OpenerServiceStub);
openerService.setup(s => s.open(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
const action = new AsmtSamplesLinkAction(openerService.object, new NullAdsTelemetryService());
assert.equal(action.id, AsmtSamplesLinkAction.ID, 'Samples Link id action mismatch');
assert.equal(action.label, AsmtSamplesLinkAction.LABEL, 'Samples Link label action mismatch');
let result = await action.run();
assert.ok(result, 'Samples Link action should succeed');
openerService.verify(s => s.open(TypeMoq.It.isAny()), TypeMoq.Times.once());
});
});