add severity support for issues (#18761)

* add severity support for issues

* vbump STS

* pr comments
This commit is contained in:
Alan Ren
2022-03-17 14:09:02 -07:00
committed by GitHub
parent aeb4e87c1f
commit 9f2940e8f8
8 changed files with 195 additions and 160 deletions

View File

@@ -1,6 +1,6 @@
{ {
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "3.0.0-release.214", "version": "3.0.0-release.216",
"downloadFileNames": { "downloadFileNames": {
"Windows_86": "win-x86-net6.0.zip", "Windows_86": "win-x86-net6.0.zip",
"Windows_64": "win-x64-net6.0.zip", "Windows_64": "win-x64-net6.0.zip",

View File

@@ -1061,6 +1061,14 @@ declare module 'azdata' {
*/ */
export type DesignerEditPath = (string | number)[]; export type DesignerEditPath = (string | number)[];
/**
* Severity of the messages returned by the provider after processing an edit.
* 'error': The issue must be fixed in order to commit the changes.
* 'warning': Inform the user the potential risks with the current state. e.g. Having multiple edge constraints is only useful as a temporary state.
* 'information': Informational message.
*/
export type DesignerIssueSeverity = 'error' | 'warning' | 'information';
/** /**
* The result returned by the table designer provider after handling an edit request. * The result returned by the table designer provider after handling an edit request.
*/ */
@@ -1078,9 +1086,9 @@ declare module 'azdata' {
*/ */
isValid: boolean; isValid: boolean;
/** /**
* Error messages of current state, and the property the caused the error. * Issues of current state.
*/ */
errors?: { message: string, propertyPath?: DesignerEditPath }[]; issues?: { severity: DesignerIssueSeverity, description: string, propertyPath?: DesignerEditPath }[];
} }
/** /**

View File

@@ -36,7 +36,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { INotificationService } from 'vs/platform/notification/common/notification'; import { INotificationService } from 'vs/platform/notification/common/notification';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { DesignerMessagesTabPanelView } from 'sql/workbench/browser/designer/designerMessagesTabPanelView'; import { DesignerIssuesTabPanelView } from 'sql/workbench/browser/designer/designerIssuesTabPanelView';
import { DesignerScriptEditorTabPanelView } from 'sql/workbench/browser/designer/designerScriptEditorTabPanelView'; import { DesignerScriptEditorTabPanelView } from 'sql/workbench/browser/designer/designerScriptEditorTabPanelView';
import { DesignerPropertyPathValidator } from 'sql/workbench/browser/designer/designerPropertyPathValidator'; import { DesignerPropertyPathValidator } from 'sql/workbench/browser/designer/designerPropertyPathValidator';
import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IThemeService } from 'vs/platform/theme/common/themeService';
@@ -70,7 +70,7 @@ interface DesignerTableCellContext {
} }
const ScriptTabId = 'scripts'; const ScriptTabId = 'scripts';
const MessagesTabId = 'messages'; const IssuesTabId = 'issues';
export class Designer extends Disposable implements IThemable { export class Designer extends Disposable implements IThemable {
private _loadingSpinner: LoadingSpinner; private _loadingSpinner: LoadingSpinner;
@@ -95,7 +95,7 @@ export class Designer extends Disposable implements IThemable {
private _inputDisposable: DisposableStore; private _inputDisposable: DisposableStore;
private _loadingTimeoutHandle: any; private _loadingTimeoutHandle: any;
private _groupHeaders: HTMLElement[] = []; private _groupHeaders: HTMLElement[] = [];
private _messagesView: DesignerMessagesTabPanelView; private _issuesView: DesignerIssuesTabPanelView;
private _scriptEditorView: DesignerScriptEditorTabPanelView; private _scriptEditorView: DesignerScriptEditorTabPanelView;
private _onStyleChangeEventEmitter = new Emitter<void>(); private _onStyleChangeEventEmitter = new Emitter<void>();
@@ -152,8 +152,8 @@ export class Designer extends Disposable implements IThemable {
onDidChange: Event.None onDidChange: Event.None
}, Sizing.Distribute); }, Sizing.Distribute);
this._scriptTabbedPannel = new TabbedPanel(this._editorContainer); this._scriptTabbedPannel = new TabbedPanel(this._editorContainer);
this._messagesView = this._instantiationService.createInstance(DesignerMessagesTabPanelView); this._issuesView = this._instantiationService.createInstance(DesignerIssuesTabPanelView);
this._register(this._messagesView.onMessageSelected((path) => { this._register(this._issuesView.onIssueSelected((path) => {
if (path && path.length > 0) { if (path && path.length > 0) {
this.selectProperty(path); this.selectProperty(path);
} }
@@ -332,14 +332,14 @@ export class Designer extends Disposable implements IThemable {
private handleEditProcessedEvent(args: DesignerEditProcessedEventArgs): void { private handleEditProcessedEvent(args: DesignerEditProcessedEventArgs): void {
const edit = args.edit; const edit = args.edit;
this._supressEditProcessing = true; this._supressEditProcessing = true;
if (!args.result.isValid) { if (args.result.issues?.length > 0) {
alert(localize('designer.errorCountAlert', "{0} validation errors found.", args.result.errors.length)); alert(localize('designer.issueCountAlert', "{0} validation issues found.", args.result.issues.length));
} }
try { try {
if (args.result.refreshView) { if (args.result.refreshView) {
this.refresh(); this.refresh();
if (!args.result.isValid) { if (!args.result.isValid) {
this._scriptTabbedPannel.showTab(MessagesTabId); this._scriptTabbedPannel.showTab(IssuesTabId);
} }
} else { } else {
this.updateComponentValues(); this.updateComponentValues();
@@ -466,7 +466,7 @@ export class Designer extends Disposable implements IThemable {
} }
private updateComponentValues(): void { private updateComponentValues(): void {
this.updateMessagesTab(); this.updateIssuesTab();
const viewModel = this._input.viewModel; const viewModel = this._input.viewModel;
const scriptProperty = viewModel[ScriptProperty] as InputBoxProperties; const scriptProperty = viewModel[ScriptProperty] as InputBoxProperties;
if (scriptProperty) { if (scriptProperty) {
@@ -477,23 +477,24 @@ export class Designer extends Disposable implements IThemable {
}); });
} }
private updateMessagesTab(): void { private updateIssuesTab(): void {
if (!this._input) { if (!this._input) {
return; return;
} }
if (this._scriptTabbedPannel.contains(MessagesTabId)) { if (this._scriptTabbedPannel.contains(IssuesTabId)) {
this._scriptTabbedPannel.removeTab(MessagesTabId); this._scriptTabbedPannel.removeTab(IssuesTabId);
} }
if (this._input.validationErrors === undefined || this._input.validationErrors.length === 0) {
if (this._input.issues === undefined || this._input.issues.length === 0) {
return; return;
} }
this._scriptTabbedPannel.pushTab({ this._scriptTabbedPannel.pushTab({
title: localize('designer.messagesTabTitle', "Errors ({0})", this._input.validationErrors.length), title: localize('designer.issuesTabTitle', "Issues ({0})", this._input.issues.length),
identifier: MessagesTabId, identifier: IssuesTabId,
view: this._messagesView view: this._issuesView
}); });
this._scriptTabbedPannel.showTab(MessagesTabId); this._scriptTabbedPannel.showTab(IssuesTabId);
this._messagesView.updateMessages(this._input.validationErrors); this._issuesView.updateIssues(this._input.issues);
} }
private selectProperty(path: DesignerPropertyPath, view?: DesignerUIArea, highlight: boolean = true): void { private selectProperty(path: DesignerPropertyPath, view?: DesignerUIArea, highlight: boolean = true): void {

View File

@@ -0,0 +1,143 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IPanelView } from 'sql/base/browser/ui/panel/panel';
import { Disposable } from 'vs/base/common/lifecycle';
import * as DOM from 'vs/base/browser/dom';
import { DesignerPropertyPath, DesignerIssue } from 'sql/workbench/browser/designer/interfaces';
import { Emitter, Event } from 'vs/base/common/event';
import { IListAccessibilityProvider, List } from 'vs/base/browser/ui/list/listWidget';
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { localize } from 'vs/nls';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { problemsErrorIconForeground, problemsInfoIconForeground, problemsWarningIconForeground } from 'vs/platform/theme/common/colorRegistry';
import { Codicon } from 'vs/base/common/codicons';
export class DesignerIssuesTabPanelView extends Disposable implements IPanelView {
private _container: HTMLElement;
private _onIssueSelected = new Emitter<DesignerPropertyPath>();
private _issueList: List<DesignerIssue>;
public readonly onIssueSelected: Event<DesignerPropertyPath> = this._onIssueSelected.event;
constructor(@IThemeService private _themeService: IThemeService) {
super();
}
render(container: HTMLElement): void {
this._container = container.appendChild(DOM.$('.issues-container'));
this._issueList = new List<DesignerIssue>('designerIssueList', this._container, new DesignerIssueListDelegate(), [new TableFilterListRenderer()], {
multipleSelectionSupport: false,
keyboardSupport: true,
mouseSupport: true,
accessibilityProvider: new DesignerIssueListAccessibilityProvider()
});
this._register(this._issueList.onDidChangeSelection((e) => {
if (e.elements && e.elements.length === 1) {
this._onIssueSelected.fire(e.elements[0].propertyPath);
}
}));
this._register(attachListStyler(this._issueList, this._themeService));
}
layout(dimension: DOM.Dimension): void {
this._issueList.layout(dimension.height, dimension.width);
}
updateIssues(errors: DesignerIssue[]) {
if (this._issueList) {
this._issueList.splice(0, this._issueList.length, errors);
}
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const errorForegroundColor = theme.getColor(problemsErrorIconForeground);
const warningForegroundColor = theme.getColor(problemsWarningIconForeground);
const informationalForegroundColor = theme.getColor(problemsInfoIconForeground);
if (errorForegroundColor) {
collector.addRule(`
.designer-component .issues-container .issue-item .issue-icon.codicon-error {
color: ${errorForegroundColor};
}
.designer-component .issues-container .issue-item .issue-icon.codicon-warning {
color: ${warningForegroundColor};
}
.designer-component .issues-container .issue-item .issue-icon.codicon-info {
color: ${informationalForegroundColor};
}
`);
}
});
const DesignerIssueListTemplateId = 'DesignerIssueListTemplate';
class DesignerIssueListDelegate implements IListVirtualDelegate<DesignerIssue> {
getHeight(element: DesignerIssue): number {
return 25;
}
getTemplateId(element: DesignerIssue): string {
return DesignerIssueListTemplateId;
}
}
interface DesignerIssueListItemTemplate {
issueText: HTMLDivElement;
issueIcon: HTMLDivElement;
}
class TableFilterListRenderer implements IListRenderer<DesignerIssue, DesignerIssueListItemTemplate> {
renderTemplate(container: HTMLElement): DesignerIssueListItemTemplate {
const data: DesignerIssueListItemTemplate = Object.create(null);
const issueItem = container.appendChild(DOM.$('.issue-item'));
data.issueIcon = issueItem.appendChild(DOM.$(''));
data.issueText = issueItem.appendChild(DOM.$('.issue-text'));
return data;
}
renderElement(element: DesignerIssue, index: number, templateData: DesignerIssueListItemTemplate, height: number): void {
templateData.issueText.innerText = element.description;
templateData.issueText.title = element.description;
let iconClass;
switch (element.severity) {
case 'warning':
iconClass = Codicon.warning.classNames;
break;
case 'information':
iconClass = Codicon.info.classNames;
break;
default:
iconClass = Codicon.error.classNames;
break;
}
templateData.issueIcon.className = `issue-icon ${iconClass}`;
}
public disposeTemplate(templateData: DesignerIssueListItemTemplate): void {
}
public get templateId(): string {
return DesignerIssueListTemplateId;
}
}
class DesignerIssueListAccessibilityProvider implements IListAccessibilityProvider<DesignerIssue> {
getAriaLabel(element: DesignerIssue): string {
return element.description;
}
getWidgetAriaLabel(): string {
return localize('designer.IssueListAriaLabel', "Issues");
}
getWidgetRole() {
return 'listbox';
}
getRole(element: DesignerIssue): string {
return 'option';
}
}

View File

@@ -1,123 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IPanelView } from 'sql/base/browser/ui/panel/panel';
import { Disposable } from 'vs/base/common/lifecycle';
import * as DOM from 'vs/base/browser/dom';
import { DesignerPropertyPath, DesignerValidationError } from 'sql/workbench/browser/designer/interfaces';
import { Emitter, Event } from 'vs/base/common/event';
import { IListAccessibilityProvider, List } from 'vs/base/browser/ui/list/listWidget';
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { localize } from 'vs/nls';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { problemsErrorIconForeground } from 'vs/platform/theme/common/colorRegistry';
import { Codicon } from 'vs/base/common/codicons';
export class DesignerMessagesTabPanelView extends Disposable implements IPanelView {
private _container: HTMLElement;
private _onMessageSelected = new Emitter<DesignerPropertyPath>();
private _messageList: List<DesignerValidationError>;
public readonly onMessageSelected: Event<DesignerPropertyPath> = this._onMessageSelected.event;
constructor(@IThemeService private _themeService: IThemeService) {
super();
}
render(container: HTMLElement): void {
this._container = container.appendChild(DOM.$('.messages-container'));
this._messageList = new List<DesignerValidationError>('designerMessageList', this._container, new DesignerMessageListDelegate(), [new TableFilterListRenderer()], {
multipleSelectionSupport: false,
keyboardSupport: true,
mouseSupport: true,
accessibilityProvider: new DesignerMessagesListAccessibilityProvider()
});
this._register(this._messageList.onDidChangeSelection((e) => {
if (e.elements && e.elements.length === 1) {
this._onMessageSelected.fire(e.elements[0].propertyPath);
}
}));
this._register(attachListStyler(this._messageList, this._themeService));
}
layout(dimension: DOM.Dimension): void {
this._messageList.layout(dimension.height, dimension.width);
}
updateMessages(errors: DesignerValidationError[]) {
if (this._messageList) {
this._messageList.splice(0, this._messageList.length, errors);
}
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const errorForegroundColor = theme.getColor(problemsErrorIconForeground);
if (errorForegroundColor) {
collector.addRule(`
.designer-component .messages-container .message-item .message-icon {
color: ${errorForegroundColor};
}
`);
}
});
const DesignerMessageListTemplateId = 'DesignerMessageListTemplate';
class DesignerMessageListDelegate implements IListVirtualDelegate<DesignerValidationError> {
getHeight(element: DesignerValidationError): number {
return 25;
}
getTemplateId(element: DesignerValidationError): string {
return DesignerMessageListTemplateId;
}
}
interface DesignerMessageListItemTemplate {
messageText: HTMLDivElement;
}
class TableFilterListRenderer implements IListRenderer<DesignerValidationError, DesignerMessageListItemTemplate> {
renderTemplate(container: HTMLElement): DesignerMessageListItemTemplate {
const data: DesignerMessageListItemTemplate = Object.create(null);
const messageItem = container.appendChild(DOM.$('.message-item'));
messageItem.appendChild(DOM.$(`.message-icon${Codicon.error.cssSelector}`));
data.messageText = messageItem.appendChild(DOM.$('.message-text'));
return data;
}
renderElement(element: DesignerValidationError, index: number, templateData: DesignerMessageListItemTemplate, height: number): void {
templateData.messageText.innerText = element.message;
}
disposeElement?(element: DesignerValidationError, index: number, templateData: DesignerMessageListItemTemplate, height: number): void {
}
public disposeTemplate(templateData: DesignerMessageListItemTemplate): void {
}
public get templateId(): string {
return DesignerMessageListTemplateId;
}
}
class DesignerMessagesListAccessibilityProvider implements IListAccessibilityProvider<DesignerValidationError> {
getAriaLabel(element: DesignerValidationError): string {
return element.message;
}
getWidgetAriaLabel(): string {
return localize('designer.MessageListAriaLabel', "Errors");
}
getWidgetRole() {
return 'listbox';
}
getRole(element: DesignerValidationError): string {
return 'option';
}
}

View File

@@ -44,9 +44,9 @@ export interface DesignerComponentInput {
readonly viewModel: DesignerViewModel; readonly viewModel: DesignerViewModel;
/** /**
* Gets the validation errors. * Gets the issues.
*/ */
readonly validationErrors: DesignerValidationError[] | undefined; readonly issues: DesignerIssue[] | undefined;
/** /**
* Start initilizing the designer input object. * Start initilizing the designer input object.
@@ -229,12 +229,13 @@ export type DesignerUIArea = 'PropertiesView' | 'ScriptView' | 'TopContentView'
export type DesignerPropertyPath = (string | number)[]; export type DesignerPropertyPath = (string | number)[];
export const DesignerRootObjectPath: DesignerPropertyPath = []; export const DesignerRootObjectPath: DesignerPropertyPath = [];
export type DesignerValidationError = { message: string, propertyPath?: DesignerPropertyPath }; export type DesignerIssueSeverity = 'error' | 'warning' | 'information';
export type DesignerIssue = { description: string, propertyPath?: DesignerPropertyPath, severity: DesignerIssueSeverity };
export interface DesignerEditResult { export interface DesignerEditResult {
isValid: boolean; isValid: boolean;
refreshView?: boolean; refreshView?: boolean;
errors?: DesignerValidationError[]; issues?: DesignerIssue[];
} }
export interface DesignerTextEditor { export interface DesignerTextEditor {

View File

@@ -29,24 +29,27 @@
height: 100%; height: 100%;
} }
.designer-component .messages-container { .designer-component .issues-container {
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.designer-component .messages-container .message-item { .designer-component .issues-container .issue-item {
display: flex; display: flex;
} }
.designer-component .messages-container .message-item .message-icon { .designer-component .issues-container .issue-item .issue-icon {
margin: 0px 6px; margin: 0px 6px;
flex: 0 0 auto; flex: 0 0 auto;
line-height: 25px; line-height: 25px;
} }
.designer-component .messages-container .message-item .message-text { .designer-component .issues-container .issue-item .issue-text {
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
user-select: text;
} }
.designer-component .tabbed-panel-container { .designer-component .tabbed-panel-container {

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import { DesignerViewModel, DesignerEdit, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties, DesignerEditProcessedEventArgs, DesignerAction, DesignerStateChangedEventArgs, DesignerPropertyPath, DesignerValidationError, ScriptProperty } from 'sql/workbench/browser/designer/interfaces'; import { DesignerViewModel, DesignerEdit, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties, DesignerEditProcessedEventArgs, DesignerAction, DesignerStateChangedEventArgs, DesignerPropertyPath, DesignerIssue, ScriptProperty } from 'sql/workbench/browser/designer/interfaces';
import { TableDesignerProvider } from 'sql/workbench/services/tableDesigner/common/interface'; import { TableDesignerProvider } from 'sql/workbench/services/tableDesigner/common/interface';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { designers } from 'sql/workbench/api/common/sqlExtHostTypes'; import { designers } from 'sql/workbench/api/common/sqlExtHostTypes';
@@ -22,7 +22,7 @@ const ErrorDialogTitle: string = localize('tableDesigner.ErrorDialogTitle', "Tab
export class TableDesignerComponentInput implements DesignerComponentInput { export class TableDesignerComponentInput implements DesignerComponentInput {
private _viewModel: DesignerViewModel; private _viewModel: DesignerViewModel;
private _validationErrors?: DesignerValidationError[]; private _issues?: DesignerIssue[];
private _view: DesignerView; private _view: DesignerView;
private _valid: boolean = true; private _valid: boolean = true;
private _dirty: boolean = false; private _dirty: boolean = false;
@@ -76,8 +76,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
return this._viewModel; return this._viewModel;
} }
get validationErrors(): DesignerValidationError[] | undefined { get issues(): DesignerIssue[] | undefined {
return this._validationErrors; return this._issues;
} }
processEdit(edit: DesignerEdit): void { processEdit(edit: DesignerEdit): void {
@@ -93,14 +93,14 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
if (result.view) { if (result.view) {
this.setDesignerView(result.view); this.setDesignerView(result.view);
} }
this._validationErrors = result.errors; this._issues = result.issues;
this.updateState(result.isValid, this.isDirty(), undefined); this.updateState(result.isValid, this.isDirty(), undefined);
this._onEditProcessed.fire({ this._onEditProcessed.fire({
edit: edit, edit: edit,
result: { result: {
isValid: result.isValid, isValid: result.isValid,
errors: result.errors, issues: result.issues,
refreshView: !!result.view refreshView: !!result.view
} }
}); });
@@ -372,7 +372,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
description: localize('designer.column.description.precision', "For numeric data, the maximum number of decimal digits that can be stored in this database object."), description: localize('designer.column.description.precision', "For numeric data, the maximum number of decimal digits that can be stored in this database object."),
componentProperties: { componentProperties: {
title: localize('tableDesigner.columnPrecisionTitle', "Precision"), title: localize('tableDesigner.columnPrecisionTitle', "Precision"),
width: 60 width: 60,
inputType: 'number'
} }
}, { }, {
componentType: 'input', componentType: 'input',
@@ -380,7 +381,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
description: localize('designer.column.description.scale', "For numeric data, the maximum number of decimal digits that can be stored in this database object to the right of decimal point."), description: localize('designer.column.description.scale', "For numeric data, the maximum number of decimal digits that can be stored in this database object to the right of decimal point."),
componentProperties: { componentProperties: {
title: localize('tableDesigner.columnScaleTitle', "Scale"), title: localize('tableDesigner.columnScaleTitle', "Scale"),
width: 60 width: 60,
inputType: 'number'
} }
} }
]; ];