mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
handle edit and save race condition (#20462)
* handle edit and save race condition * handle more race condition scenarios * fix error
This commit is contained in:
@@ -51,6 +51,8 @@ import { RowMoveManager, RowMoveOnDragEventData } from 'sql/base/browser/ui/tabl
|
|||||||
import { ITaskbarContent, Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
import { ITaskbarContent, Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||||
import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin';
|
import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin';
|
||||||
import { listFocusAndSelectionBackground } from 'sql/platform/theme/common/colors';
|
import { listFocusAndSelectionBackground } from 'sql/platform/theme/common/colors';
|
||||||
|
import { timeout } from 'vs/base/common/async';
|
||||||
|
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||||
|
|
||||||
export interface IDesignerStyle {
|
export interface IDesignerStyle {
|
||||||
tabbedPanelStyles?: ITabbedPanelStyles;
|
tabbedPanelStyles?: ITabbedPanelStyles;
|
||||||
@@ -282,6 +284,9 @@ export class Designer extends Disposable implements IThemable {
|
|||||||
|
|
||||||
|
|
||||||
public setInput(input: DesignerComponentInput): void {
|
public setInput(input: DesignerComponentInput): void {
|
||||||
|
if (this._input) {
|
||||||
|
void this.submitPendingChanges().catch(onUnexpectedError);
|
||||||
|
}
|
||||||
this.saveUIState();
|
this.saveUIState();
|
||||||
if (this._loadingTimeoutHandle) {
|
if (this._loadingTimeoutHandle) {
|
||||||
this.stopLoading();
|
this.stopLoading();
|
||||||
@@ -303,7 +308,9 @@ export class Designer extends Disposable implements IThemable {
|
|||||||
this._inputDisposable.add(this._input.onRefreshRequested(() => {
|
this._inputDisposable.add(this._input.onRefreshRequested(() => {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}));
|
}));
|
||||||
|
this._inputDisposable.add(this._input.onSubmitPendingEditRequested(async () => {
|
||||||
|
await this.submitPendingChanges();
|
||||||
|
}));
|
||||||
if (this._input.view === undefined) {
|
if (this._input.view === undefined) {
|
||||||
this._input.initialize();
|
this._input.initialize();
|
||||||
} else {
|
} else {
|
||||||
@@ -319,6 +326,14 @@ export class Designer extends Disposable implements IThemable {
|
|||||||
this._inputDisposable?.dispose();
|
this._inputDisposable?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async submitPendingChanges(): Promise<void> {
|
||||||
|
if (this._container.contains(document.activeElement) && document.activeElement instanceof HTMLInputElement) {
|
||||||
|
// Force the elements to fire the blur event to submit the pending changes.
|
||||||
|
document.activeElement.blur();
|
||||||
|
return timeout(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private clearUI(): void {
|
private clearUI(): void {
|
||||||
this._componentMap.forEach(item => item.component.dispose());
|
this._componentMap.forEach(item => item.component.dispose());
|
||||||
this._componentMap.clear();
|
this._componentMap.clear();
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ export interface DesignerComponentInput {
|
|||||||
*/
|
*/
|
||||||
readonly onRefreshRequested: Event<void>;
|
readonly onRefreshRequested: Event<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event that is triggerd when force submit of the pending edit is requested.
|
||||||
|
*/
|
||||||
|
readonly onSubmitPendingEditRequested: Event<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the object type display name.
|
* Gets the object type display name.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export class DesignerTableAction extends Action {
|
|||||||
protected _table: Table<Slick.SlickData>;
|
protected _table: Table<Slick.SlickData>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private _designer: Designer,
|
||||||
id: string,
|
id: string,
|
||||||
label: string,
|
label: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
@@ -43,6 +44,10 @@ export class DesignerTableAction extends Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async run(context: DesignerTableActionContext): Promise<void> {
|
||||||
|
await this._designer.submitPendingChanges();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AddRowAction extends DesignerTableAction {
|
export class AddRowAction extends DesignerTableAction {
|
||||||
@@ -54,12 +59,13 @@ export class AddRowAction extends DesignerTableAction {
|
|||||||
private designer: Designer,
|
private designer: Designer,
|
||||||
tableProperties: DesignerTableProperties,
|
tableProperties: DesignerTableProperties,
|
||||||
) {
|
) {
|
||||||
super(AddRowAction.ID, tableProperties.labelForAddNewButton || AddRowAction.LABEL, AddRowAction.ICON, false);
|
super(designer, AddRowAction.ID, tableProperties.labelForAddNewButton || AddRowAction.LABEL, AddRowAction.ICON, false);
|
||||||
this.designer = designer;
|
this.designer = designer;
|
||||||
this._tooltip = localize('designer.newRowButtonAriaLabel', "Add new row to '{0}' table", tableProperties.ariaLabel);
|
this._tooltip = localize('designer.newRowButtonAriaLabel', "Add new row to '{0}' table", tableProperties.ariaLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async run(context: DesignerTableActionContext): Promise<void> {
|
public override async run(context: DesignerTableActionContext): Promise<void> {
|
||||||
|
await super.run(context);
|
||||||
const lastIndex = context.table.getData().getItems().length;
|
const lastIndex = context.table.getData().getItems().length;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.designer.handleEdit({
|
this.designer.handleEdit({
|
||||||
@@ -78,13 +84,14 @@ export class MoveRowUpAction extends DesignerTableAction {
|
|||||||
public static LABEL = localize('designer.moveRowUpAction', 'Move Up');
|
public static LABEL = localize('designer.moveRowUpAction', 'Move Up');
|
||||||
|
|
||||||
constructor(private designer: Designer) {
|
constructor(private designer: Designer) {
|
||||||
super(MoveRowUpAction.ID, MoveRowUpAction.LABEL, MoveRowUpAction.ICON, true);
|
super(designer, MoveRowUpAction.ID, MoveRowUpAction.LABEL, MoveRowUpAction.ICON, true);
|
||||||
this.designer = designer;
|
this.designer = designer;
|
||||||
this._tooltip = localize('designer.moveRowUpButtonAriaLabel', "Move selected row up one position");
|
this._tooltip = localize('designer.moveRowUpButtonAriaLabel', "Move selected row up one position");
|
||||||
this.enabled = false;
|
this.enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async run(context: DesignerTableActionContext): Promise<void> {
|
public override async run(context: DesignerTableActionContext): Promise<void> {
|
||||||
|
await super.run(context);
|
||||||
let rowIndex = context.selectedRow ?? context.table.getSelectedRows()[0];
|
let rowIndex = context.selectedRow ?? context.table.getSelectedRows()[0];
|
||||||
if (rowIndex - 1 < 0) {
|
if (rowIndex - 1 < 0) {
|
||||||
return;
|
return;
|
||||||
@@ -116,13 +123,14 @@ export class MoveRowDownAction extends DesignerTableAction {
|
|||||||
public static LABEL = localize('designer.moveRowDownAction', 'Move Down');
|
public static LABEL = localize('designer.moveRowDownAction', 'Move Down');
|
||||||
|
|
||||||
constructor(private designer: Designer) {
|
constructor(private designer: Designer) {
|
||||||
super(MoveRowDownAction.ID, MoveRowDownAction.LABEL, MoveRowDownAction.ICON, true);
|
super(designer, MoveRowDownAction.ID, MoveRowDownAction.LABEL, MoveRowDownAction.ICON, true);
|
||||||
this.designer = designer;
|
this.designer = designer;
|
||||||
this._tooltip = localize('designer.moveRowDownButtonAriaLabel', "Move selected row down one position");
|
this._tooltip = localize('designer.moveRowDownButtonAriaLabel', "Move selected row down one position");
|
||||||
this.enabled = false;
|
this.enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async run(context: DesignerTableActionContext): Promise<void> {
|
public override async run(context: DesignerTableActionContext): Promise<void> {
|
||||||
|
await super.run(context);
|
||||||
let rowIndex = context.selectedRow ?? context.table.getSelectedRows()[0];
|
let rowIndex = context.selectedRow ?? context.table.getSelectedRows()[0];
|
||||||
const tableData = context.table.getData().getItems();
|
const tableData = context.table.getData().getItems();
|
||||||
if (rowIndex + 1 >= tableData.length) {
|
if (rowIndex + 1 >= tableData.length) {
|
||||||
|
|||||||
@@ -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, DesignerIssue, ScriptProperty } from 'sql/workbench/browser/designer/interfaces';
|
import { DesignerViewModel, DesignerEdit, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties, DesignerEditProcessedEventArgs, DesignerAction, DesignerStateChangedEventArgs, DesignerPropertyPath, DesignerIssue, ScriptProperty, DesignerUIState } 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';
|
||||||
@@ -18,6 +18,7 @@ import { IAdsTelemetryService, ITelemetryEventProperties } from 'sql/platform/te
|
|||||||
import { TelemetryAction, TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys';
|
import { TelemetryAction, TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys';
|
||||||
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
||||||
import { TableDesignerMetadata } from 'sql/workbench/services/tableDesigner/browser/tableDesignerMetadata';
|
import { TableDesignerMetadata } from 'sql/workbench/services/tableDesigner/browser/tableDesignerMetadata';
|
||||||
|
import { Queue, timeout } from 'vs/base/common/async';
|
||||||
|
|
||||||
const ErrorDialogTitle: string = localize('tableDesigner.ErrorDialogTitle', "Table Designer Error");
|
const ErrorDialogTitle: string = localize('tableDesigner.ErrorDialogTitle', "Table Designer Error");
|
||||||
export class TableDesignerComponentInput implements DesignerComponentInput {
|
export class TableDesignerComponentInput implements DesignerComponentInput {
|
||||||
@@ -32,13 +33,20 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
|||||||
private _onInitialized = new Emitter<void>();
|
private _onInitialized = new Emitter<void>();
|
||||||
private _onEditProcessed = new Emitter<DesignerEditProcessedEventArgs>();
|
private _onEditProcessed = new Emitter<DesignerEditProcessedEventArgs>();
|
||||||
private _onRefreshRequested = new Emitter<void>();
|
private _onRefreshRequested = new Emitter<void>();
|
||||||
|
private _onSubmitPendingEditRequested = new Emitter<void>();
|
||||||
private _originalViewModel: DesignerViewModel;
|
private _originalViewModel: DesignerViewModel;
|
||||||
private _tableDesignerView: azdata.designers.TableDesignerView;
|
private _tableDesignerView: azdata.designers.TableDesignerView;
|
||||||
|
private _activeEditPromise: Promise<void>;
|
||||||
|
private _isEditInProgress: boolean = false;
|
||||||
|
private _recentEditAccepted: boolean = true;
|
||||||
|
private _editQueue: Queue<void> = new Queue<void>();
|
||||||
|
|
||||||
public readonly onInitialized: Event<void> = this._onInitialized.event;
|
public readonly onInitialized: Event<void> = this._onInitialized.event;
|
||||||
public readonly onEditProcessed: Event<DesignerEditProcessedEventArgs> = this._onEditProcessed.event;
|
public readonly onEditProcessed: Event<DesignerEditProcessedEventArgs> = this._onEditProcessed.event;
|
||||||
public readonly onStateChange: Event<DesignerStateChangedEventArgs> = this._onStateChange.event;
|
public readonly onStateChange: Event<DesignerStateChangedEventArgs> = this._onStateChange.event;
|
||||||
public readonly onRefreshRequested: Event<void> = this._onRefreshRequested.event;
|
public readonly onRefreshRequested: Event<void> = this._onRefreshRequested.event;
|
||||||
|
public readonly onSubmitPendingEditRequested: Event<void> = this._onSubmitPendingEditRequested.event;
|
||||||
|
|
||||||
|
|
||||||
private readonly designerEditTypeDisplayValue: { [key: number]: string } = {
|
private readonly designerEditTypeDisplayValue: { [key: number]: string } = {
|
||||||
0: 'Add', 1: 'Remove', 2: 'Update'
|
0: 'Add', 1: 'Remove', 2: 'Update'
|
||||||
@@ -54,6 +62,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
|||||||
@IErrorMessageService private readonly _errorMessageService: IErrorMessageService) {
|
@IErrorMessageService private readonly _errorMessageService: IErrorMessageService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public designerUIState?: DesignerUIState = undefined;
|
||||||
|
|
||||||
get valid(): boolean {
|
get valid(): boolean {
|
||||||
return this._valid;
|
return this._valid;
|
||||||
}
|
}
|
||||||
@@ -87,44 +97,11 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
processEdit(edit: DesignerEdit): void {
|
processEdit(edit: DesignerEdit): void {
|
||||||
const telemetryInfo = this.createTelemetryInfo();
|
// If there is already an edit being processed, the new edit will be skipped if the previous edit is not accepted.
|
||||||
telemetryInfo.tableObjectType = this.getObjectTypeFromPath(edit.path);
|
const checkPreviousEditResult = this._editQueue.size !== 0;
|
||||||
const editAction = this._adsTelemetryService.createActionEvent(TelemetryView.TableDesigner,
|
this._editQueue.queue(async () => {
|
||||||
this.designerEditTypeDisplayValue[edit.type]).withAdditionalProperties(telemetryInfo);
|
await this.doProcessEdit(edit, checkPreviousEditResult);
|
||||||
const startTime = new Date().getTime();
|
});
|
||||||
this.updateState(this.valid, this.dirty, 'processEdit');
|
|
||||||
this._provider.processTableEdit(this.tableInfo, edit).then(
|
|
||||||
result => {
|
|
||||||
if (result.inputValidationError) {
|
|
||||||
this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.inputValidationError', "The input validation failed with error: {0}", result.inputValidationError));
|
|
||||||
}
|
|
||||||
this._viewModel = result.viewModel;
|
|
||||||
if (result.view) {
|
|
||||||
this.setDesignerView(result.view);
|
|
||||||
}
|
|
||||||
this._issues = result.issues;
|
|
||||||
this.updateState(result.isValid, this.isDirty(), undefined);
|
|
||||||
|
|
||||||
this._onEditProcessed.fire({
|
|
||||||
edit: edit,
|
|
||||||
result: {
|
|
||||||
isValid: result.isValid,
|
|
||||||
issues: result.issues,
|
|
||||||
refreshView: !!result.view
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const metadataTelemetryInfo = TableDesignerMetadata.getTelemetryInfo(this._provider.providerId, result.metadata);
|
|
||||||
editAction.withAdditionalMeasurements({
|
|
||||||
'elapsedTimeMs': new Date().getTime() - startTime
|
|
||||||
}).withAdditionalProperties(metadataTelemetryInfo).send();
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.errorProcessingEdit', "An error occured while processing the change: {0}", error?.message ?? error), error?.data);
|
|
||||||
this.updateState(this.valid, this.dirty);
|
|
||||||
this._adsTelemetryService.createErrorEvent(TelemetryView.TableDesigner,
|
|
||||||
this.designerEditTypeDisplayValue[edit.type]).withAdditionalProperties(telemetryInfo).send();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateScript(): Promise<void> {
|
async generateScript(): Promise<void> {
|
||||||
@@ -183,6 +160,14 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async save(): Promise<void> {
|
async save(): Promise<void> {
|
||||||
|
this._onSubmitPendingEditRequested.fire();
|
||||||
|
await timeout(10);
|
||||||
|
if (this._isEditInProgress) {
|
||||||
|
await this._activeEditPromise;
|
||||||
|
}
|
||||||
|
if (!this.valid || !this._recentEditAccepted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.isDirty()) {
|
if (!this.isDirty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -235,6 +220,60 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
|||||||
this.updateState(true, false);
|
this.updateState(true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async doProcessEdit(edit: DesignerEdit, checkPreviousEditResult: boolean): Promise<void> {
|
||||||
|
if (checkPreviousEditResult && !this._recentEditAccepted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const telemetryInfo = this.createTelemetryInfo();
|
||||||
|
telemetryInfo.tableObjectType = this.getObjectTypeFromPath(edit.path);
|
||||||
|
const editAction = this._adsTelemetryService.createActionEvent(TelemetryView.TableDesigner,
|
||||||
|
this.designerEditTypeDisplayValue[edit.type]).withAdditionalProperties(telemetryInfo);
|
||||||
|
const startTime = new Date().getTime();
|
||||||
|
this.updateState(this.valid, this.dirty, 'processEdit');
|
||||||
|
this._activeEditPromise = new Promise(async (resolve) => {
|
||||||
|
this._isEditInProgress = true;
|
||||||
|
this._recentEditAccepted = true;
|
||||||
|
try {
|
||||||
|
const result = await this._provider.processTableEdit(this.tableInfo, edit);
|
||||||
|
if (result.inputValidationError) {
|
||||||
|
this._recentEditAccepted = false;
|
||||||
|
this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.inputValidationError', "The input validation failed with error: {0}", result.inputValidationError));
|
||||||
|
}
|
||||||
|
this._viewModel = result.viewModel;
|
||||||
|
if (result.view) {
|
||||||
|
this.setDesignerView(result.view);
|
||||||
|
}
|
||||||
|
this._issues = result.issues;
|
||||||
|
this.updateState(result.isValid, this.isDirty(), undefined);
|
||||||
|
|
||||||
|
this._onEditProcessed.fire({
|
||||||
|
edit: edit,
|
||||||
|
result: {
|
||||||
|
isValid: result.isValid,
|
||||||
|
issues: result.issues,
|
||||||
|
refreshView: !!result.view
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const metadataTelemetryInfo = TableDesignerMetadata.getTelemetryInfo(this._provider.providerId, result.metadata);
|
||||||
|
editAction.withAdditionalMeasurements({
|
||||||
|
'elapsedTimeMs': new Date().getTime() - startTime
|
||||||
|
}).withAdditionalProperties(metadataTelemetryInfo).send();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.errorProcessingEdit', "An error occured while processing the change: {0}", error?.message ?? error), error?.data);
|
||||||
|
this.updateState(this.valid, this.dirty);
|
||||||
|
this._adsTelemetryService.createErrorEvent(TelemetryView.TableDesigner,
|
||||||
|
this.designerEditTypeDisplayValue[edit.type]).withAdditionalProperties(telemetryInfo).send();
|
||||||
|
this._recentEditAccepted = false;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this._isEditInProgress = false;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this._activeEditPromise;
|
||||||
|
}
|
||||||
|
|
||||||
private updateState(valid: boolean, dirty: boolean, pendingAction?: DesignerAction): void {
|
private updateState(valid: boolean, dirty: boolean, pendingAction?: DesignerAction): void {
|
||||||
if (this._dirty !== dirty || this._valid !== valid || this._pendingAction !== pendingAction) {
|
if (this._dirty !== dirty || this._valid !== valid || this._pendingAction !== pendingAction) {
|
||||||
const previousState = {
|
const previousState = {
|
||||||
|
|||||||
Reference in New Issue
Block a user