/*--------------------------------------------------------------------------------------------- * 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 { 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 { localize } from 'vs/nls'; import { designers } from 'sql/workbench/api/common/sqlExtHostTypes'; import { Emitter, Event } from 'vs/base/common/event'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { deepClone, equals } from 'vs/base/common/objects'; import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TableDesignerPublishDialogResult, TableDesignerPublishDialog } from 'sql/workbench/services/tableDesigner/browser/tableDesignerPublishDialog'; import { IAdsTelemetryService, ITelemetryEventProperties } from 'sql/platform/telemetry/common/telemetry'; import { TelemetryAction, TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys'; import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService'; import { TableDesignerMetadata } from 'sql/workbench/services/tableDesigner/browser/tableDesignerMetadata'; const ErrorDialogTitle: string = localize('tableDesigner.ErrorDialogTitle', "Table Designer Error"); export class TableDesignerComponentInput implements DesignerComponentInput { private _viewModel: DesignerViewModel; private _issues?: DesignerIssue[]; private _view: DesignerView; private _valid: boolean = true; private _dirty: boolean = false; private _pendingAction?: DesignerAction = undefined; private _onStateChange = new Emitter(); private _onInitialized = new Emitter(); private _onEditProcessed = new Emitter(); private _onRefreshRequested = new Emitter(); private _originalViewModel: DesignerViewModel; private _tableDesignerView: azdata.designers.TableDesignerView; public readonly onInitialized: Event = this._onInitialized.event; public readonly onEditProcessed: Event = this._onEditProcessed.event; public readonly onStateChange: Event = this._onStateChange.event; public readonly onRefreshRequested: Event = this._onRefreshRequested.event; private readonly designerEditTypeDisplayValue: { [key: number]: string } = { 0: 'Add', 1: 'Remove', 2: 'Update' }; constructor(private readonly _provider: TableDesignerProvider, public tableInfo: azdata.designers.TableInfo, private _telemetryInfo: ITelemetryEventProperties, @INotificationService private readonly _notificationService: INotificationService, @IAdsTelemetryService readonly _adsTelemetryService: IAdsTelemetryService, @IQueryEditorService private readonly _queryEditorService: IQueryEditorService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IErrorMessageService private readonly _errorMessageService: IErrorMessageService) { } get valid(): boolean { return this._valid; } get dirty(): boolean { return this._dirty; } get pendingAction(): DesignerAction | undefined { return this._pendingAction; } get objectTypeDisplayName(): string { return localize('tableDesigner.tableObjectType', "Table"); } get view(): DesignerView { return this._view; } get viewModel(): DesignerViewModel { return this._viewModel; } get issues(): DesignerIssue[] | undefined { return this._issues; } get tableDesignerView(): azdata.designers.TableDesignerView { return this._tableDesignerView; } processEdit(edit: DesignerEdit): void { 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._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)); this.updateState(this.valid, this.dirty); this._adsTelemetryService.createErrorEvent(TelemetryView.TableDesigner, this.designerEditTypeDisplayValue[edit.type]).withAdditionalProperties(telemetryInfo).send(); } ); } async generateScript(): Promise { const notificationHandle = this._notificationService.notify({ severity: Severity.Info, message: localize('tableDesigner.generatingScript', "Generating script..."), sticky: true }); const telemetryInfo = this.createTelemetryInfo(); const generateScriptEvent = this._adsTelemetryService.createActionEvent(TelemetryView.TableDesigner, TelemetryAction.GenerateScript).withAdditionalProperties(telemetryInfo); const startTime = new Date().getTime(); try { this.updateState(this.valid, this.dirty, 'generateScript'); const script = await this._provider.generateScript(this.tableInfo); this._queryEditorService.newSqlEditor({ initalContent: script }); this.updateState(this.valid, this.dirty); notificationHandle.updateMessage(localize('tableDesigner.generatingScriptCompleted', "Script generated.")); generateScriptEvent.withAdditionalMeasurements({ 'elapsedTimeMs': new Date().getTime() - startTime }).send(); } catch (error) { this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.generateScriptError', "An error occured while generating the script: {0}", error?.message ?? error)); this.updateState(this.valid, this.dirty); this._adsTelemetryService.createErrorEvent(TelemetryView.TableDesigner, TelemetryAction.GenerateScript).withAdditionalProperties(telemetryInfo).send(); } } async publishChanges(): Promise { const telemetryInfo = this.createTelemetryInfo(); const publishEvent = this._adsTelemetryService.createActionEvent(TelemetryView.TableDesigner, TelemetryAction.PublishChanges).withAdditionalProperties(telemetryInfo); const saveNotificationHandle = this._notificationService.notify({ severity: Severity.Info, message: localize('tableDesigner.savingChanges', "Publishing table designer changes..."), sticky: true }); const startTime = new Date().getTime(); try { this.updateState(this.valid, this.dirty, 'publish'); const result = await this._provider.publishChanges(this.tableInfo); this._viewModel = result.viewModel; this._originalViewModel = result.viewModel; this.setDesignerView(result.view); saveNotificationHandle.updateMessage(localize('tableDesigner.publishChangeSuccess', "The changes have been successfully published.")); this.tableInfo = result.newTableInfo; this.updateState(true, false); this._onRefreshRequested.fire(); const metadataTelemetryInfo = TableDesignerMetadata.getTelemetryInfo(this._provider.providerId, result.metadata); publishEvent.withAdditionalMeasurements({ 'elapsedTimeMs': new Date().getTime() - startTime }).withAdditionalProperties(metadataTelemetryInfo).send(); } catch (error) { this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.publishChangeError', "An error occured while publishing changes: {0}", error?.message ?? error)); this.updateState(this.valid, this.dirty); this._adsTelemetryService.createErrorEvent(TelemetryView.TableDesigner, TelemetryAction.PublishChanges).withAdditionalProperties(telemetryInfo).send(); } } async save(): Promise { if (this.tableDesignerView?.useAdvancedSaveMode) { await this.openPublishDialog(); } else { await this.publishChanges(); } } async openPublishDialog(): Promise { const reportNotificationHandle = this._notificationService.notify({ severity: Severity.Info, message: localize('tableDesigner.generatingPreviewReport', "Generating preview report..."), sticky: true }); const telemetryInfo = this.createTelemetryInfo(); const generatePreviewEvent = this._adsTelemetryService.createActionEvent(TelemetryView.TableDesigner, TelemetryAction.GeneratePreviewReport).withAdditionalProperties(telemetryInfo); const startTime = new Date().getTime(); let previewReportResult: azdata.designers.GeneratePreviewReportResult; try { this.updateState(this.valid, this.dirty, 'generateReport'); previewReportResult = await this._provider.generatePreviewReport(this.tableInfo); const metadataTelemetryInfo = TableDesignerMetadata.getTelemetryInfo(this._provider.providerId, previewReportResult.metadata); generatePreviewEvent.withAdditionalMeasurements({ 'elapsedTimeMs': new Date().getTime() - startTime }).withAdditionalProperties(metadataTelemetryInfo).send(); reportNotificationHandle.close(); this.updateState(this.valid, this.dirty); } catch (error) { this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.generatePreviewReportError', "An error occurred while generating preview report: {0}", error?.message ?? error)); this.updateState(this.valid, this.dirty); this._adsTelemetryService.createErrorEvent(TelemetryView.TableDesigner, TelemetryAction.GeneratePreviewReport).withAdditionalProperties(telemetryInfo).send(); return; } if (previewReportResult.schemaValidationError) { this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.TableSchemaValidationError', "Table schema validation failed with error: {0}", previewReportResult.schemaValidationError)); return; } const dialog = this._instantiationService.createInstance(TableDesignerPublishDialog); const result = await dialog.open(previewReportResult.report, previewReportResult.mimeType); if (result === TableDesignerPublishDialogResult.GenerateScript) { await this.generateScript(); } else if (result === TableDesignerPublishDialogResult.UpdateDatabase) { await this.publishChanges(); } } async revert(): Promise { this.updateState(true, false); } private updateState(valid: boolean, dirty: boolean, pendingAction?: DesignerAction): void { if (this._dirty !== dirty || this._valid !== valid || this._pendingAction !== pendingAction) { const previousState = { valid: this._valid, dirty: this._dirty, pendingAction: this._pendingAction }; this._dirty = dirty; this._valid = valid; this._pendingAction = pendingAction; const currentState = { valid: this._valid, dirty: this._dirty, pendingAction: this._pendingAction }; this._onStateChange.fire({ currentState, previousState, }); } } async initialize(): Promise { if (this._view !== undefined || this.pendingAction === 'initialize') { return; } this.updateState(this.valid, this.dirty, 'initialize'); try { const result = await this._provider.initializeTableDesigner(this.tableInfo); this.doInitialization(result); this._onInitialized.fire(); } catch (error) { this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.errorInitializingTableDesigner', "An error occurred while initializing the table designer: {0}", error?.message ?? error)); } } private doInitialization(designerInfo: azdata.designers.TableDesignerInfo): void { this.tableInfo = designerInfo.tableInfo; this.updateState(true, this.tableInfo.isNewTable); this._viewModel = designerInfo.viewModel; this._originalViewModel = this.tableInfo.isNewTable ? undefined : deepClone(this._viewModel); this._tableDesignerView = designerInfo.view; this._issues = designerInfo.issues; this.setDesignerView(designerInfo.view); } private setDesignerView(tableDesignerView: azdata.designers.TableDesignerView) { const tabs = []; if (tableDesignerView.columnTableOptions?.showTable) { tabs.push(this.getColumnsTab(tableDesignerView.columnTableOptions)); } tabs.push(this.getPrimaryKeyTab(tableDesignerView)); if (tableDesignerView.foreignKeyTableOptions?.showTable) { tabs.push(this.getForeignKeysTab(tableDesignerView.foreignKeyTableOptions, tableDesignerView.foreignKeyColumnMappingTableOptions)); } if (tableDesignerView.checkConstraintTableOptions?.showTable) { tabs.push(this.getCheckConstraintsTab(tableDesignerView.checkConstraintTableOptions)); } if (tableDesignerView.indexTableOptions?.showTable) { tabs.push(this.getIndexesTab(tableDesignerView.indexTableOptions, tableDesignerView.indexColumnSpecificationTableOptions)); } if (tableDesignerView.additionalTabs) { tabs.push(...tableDesignerView.additionalTabs); } tabs.push(this.getGeneralTab(tableDesignerView)); this._view = { components: [{ componentType: 'input', propertyName: designers.TableColumnProperty.Name, description: localize('designer.table.description.name', "The name of the table object."), componentProperties: { title: localize('tableDesigner.nameTitle', "Table name"), width: 200 } }], tabs: tabs }; } private getGeneralTab(tableDesignerView: azdata.designers.TableDesignerView): DesignerTab { const generalTabComponents: DesignerDataPropertyInfo[] = [ { componentType: 'dropdown', propertyName: designers.TableProperty.Schema, description: localize('designer.table.description.schema', "The schema that contains the table."), componentProperties: { title: localize('tableDesigner.schemaTitle', "Schema"), } }, { componentType: 'input', propertyName: designers.TableProperty.Description, description: localize('designer.table.description.description', "Description for the table."), componentProperties: { title: localize('tableDesigner.descriptionTitle', "Description") } } ]; if (tableDesignerView.additionalTableProperties) { generalTabComponents.push(...tableDesignerView.additionalTableProperties); } return { title: localize('tableDesigner.generalTab', "General"), components: generalTabComponents }; } private getColumnsTab(options: azdata.designers.TableDesignerBuiltInTableViewOptions): DesignerTab { const columnProperties: DesignerDataPropertyInfo[] = [ { componentType: 'input', propertyName: designers.TableColumnProperty.Name, description: localize('designer.column.description.name', "The name of the column object."), componentProperties: { title: localize('tableDesigner.columnNameTitle', "Name"), width: 150 } }, { componentType: 'input', propertyName: designers.TableColumnProperty.Description, description: localize('designer.column.description.description', "Displays the description of the column"), componentProperties: { title: localize('tableDesigner.columnDescriptionTitle', "Description"), } }, { componentType: 'dropdown', propertyName: designers.TableColumnProperty.AdvancedType, showInPropertiesView: false, description: localize('designer.column.description.advancedType', "Displays the unified data type (including length, scale and precision) for the column"), componentProperties: { title: localize('tableDesigner.columnAdvancedTypeTitle', "Type"), width: 120, isEditable: true } }, { componentType: 'dropdown', propertyName: designers.TableColumnProperty.Type, description: localize('designer.column.description.dataType', "Displays the data type name for the column"), componentProperties: { title: localize('tableDesigner.columnTypeTitle', "Type"), width: 100 } }, { componentType: 'input', propertyName: designers.TableColumnProperty.Length, description: localize('designer.column.description.length', "The maximum length (in characters) that can be stored in this database object."), componentProperties: { title: localize('tableDesigner.columnLengthTitle', "Length"), width: 60 } }, { componentType: 'input', propertyName: designers.TableColumnProperty.DefaultValue, description: localize('designer.column.description.defaultValueBinding', "A predefined global default value for the column or binding."), componentProperties: { title: localize('tableDesigner.columnDefaultValueTitle', "Default Value"), width: 150 } }, { componentType: 'checkbox', propertyName: designers.TableColumnProperty.AllowNulls, description: localize('designer.column.description.allowNulls', "Specifies whether the column may have a NULL value."), componentProperties: { title: localize('tableDesigner.columnAllowNullTitle', "Allow Nulls"), } }, { componentType: 'checkbox', propertyName: designers.TableColumnProperty.IsPrimaryKey, description: localize('designer.column.description.primaryKey', "Specifies whether the column is included in the primary key for the table."), componentProperties: { title: localize('tableDesigner.columnIsPrimaryKeyTitle', "Primary Key"), } }, { componentType: 'input', propertyName: designers.TableColumnProperty.Precision, description: localize('designer.column.description.precision', "For numeric data, the maximum number of decimal digits that can be stored in this database object."), componentProperties: { title: localize('tableDesigner.columnPrecisionTitle', "Precision"), width: 60, inputType: 'number' } }, { componentType: 'input', propertyName: designers.TableColumnProperty.Scale, 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: { title: localize('tableDesigner.columnScaleTitle', "Scale"), width: 60, inputType: 'number' } } ]; const displayProperties = this.getTableDisplayProperties(options, [ designers.TableColumnProperty.Name, designers.TableColumnProperty.AdvancedType, designers.TableColumnProperty.IsPrimaryKey, designers.TableColumnProperty.AllowNulls, designers.TableColumnProperty.DefaultValue, ]); return { title: localize('tableDesigner.columnsTabTitle', "Columns"), components: [ { componentType: 'table', propertyName: designers.TableProperty.Columns, showInPropertiesView: false, componentProperties: { ariaLabel: localize('tableDesigner.columnsTabTitle', "Columns"), columns: displayProperties, itemProperties: this.addAdditionalTableProperties(options, columnProperties), objectTypeDisplayName: localize('tableDesigner.columnTypeName', "Column"), canAddRows: options.canAddRows, canInsertRows: options.canInsertRows, canMoveRows: options.canMoveRows, canRemoveRows: options.canRemoveRows, removeRowConfirmationMessage: options.removeRowConfirmationMessage, showRemoveRowConfirmation: options.showRemoveRowConfirmation, labelForAddNewButton: options.labelForAddNewButton ?? localize('tableDesigner.addNewColumn', "New Column") } } ] }; } private getForeignKeysTab(options: azdata.designers.TableDesignerBuiltInTableViewOptions, columnMappingTableOptions: azdata.designers.TableDesignerBuiltInTableViewOptions): DesignerTab { const foreignKeyColumnMappingProperties: DesignerDataPropertyInfo[] = [ { componentType: 'dropdown', propertyName: designers.ForeignKeyColumnMappingProperty.ForeignColumn, componentProperties: { title: localize('tableDesigner.foreignKey.foreignColumn', "Foreign Column"), width: 150 } }, { componentType: 'dropdown', propertyName: designers.ForeignKeyColumnMappingProperty.Column, componentProperties: { title: localize('tableDesigner.foreignKey.column', "Column"), width: 150 } }, ]; const foreignKeyProperties: DesignerDataPropertyInfo[] = [ { componentType: 'input', propertyName: designers.TableForeignKeyProperty.Name, description: localize('designer.foreignkey.description.name', "The name of the foreign key."), componentProperties: { title: localize('tableDesigner.foreignKeyNameTitle', "Name"), width: 300 } }, { componentType: 'input', propertyName: designers.TableForeignKeyProperty.Description, description: localize('designer.foreignkey.description.description', "The description of the foreign key."), componentProperties: { title: localize('tableDesigner.foreignKeyDescriptionTitle', "Description"), } }, { componentType: 'dropdown', propertyName: designers.TableForeignKeyProperty.ForeignTable, description: localize('designer.foreignkey.description.primaryKeyTable', "The table which contains the primary or unique key column."), componentProperties: { title: localize('tableDesigner.ForeignTableName', "Foreign Table"), width: 200 } }, { componentType: 'dropdown', propertyName: designers.TableForeignKeyProperty.OnUpdateAction, description: localize('designer.foreignkey.description.onUpdateAction', "The behavior when a user tries to update a row with data that is involved in a foreign key relationship."), componentProperties: { title: localize('tableDesigner.foreignKeyOnUpdateAction', "On Update Action"), width: 100 } }, { componentType: 'dropdown', propertyName: designers.TableForeignKeyProperty.OnDeleteAction, description: localize('designer.foreignkey.description.onDeleteAction', "The behavior when a user tries to delete a row with data that is involved in a foreign key relationship."), componentProperties: { title: localize('tableDesigner.foreignKeyOnDeleteAction', "On Delete Action"), width: 100 } }, { componentType: 'table', propertyName: designers.TableForeignKeyProperty.Columns, description: localize('designer.foreignkey.description.columnMapping', "The mapping between foreign key columns and primary key columns."), group: localize('tableDesigner.foreignKeyColumns', "Columns"), componentProperties: { ariaLabel: localize('tableDesigner.foreignKeyColumns', "Columns"), columns: this.getTableDisplayProperties(columnMappingTableOptions, [designers.ForeignKeyColumnMappingProperty.Column, designers.ForeignKeyColumnMappingProperty.ForeignColumn]), itemProperties: this.addAdditionalTableProperties(columnMappingTableOptions, foreignKeyColumnMappingProperties), canAddRows: columnMappingTableOptions.canAddRows, canRemoveRows: columnMappingTableOptions.canRemoveRows, removeRowConfirmationMessage: columnMappingTableOptions.removeRowConfirmationMessage, labelForAddNewButton: columnMappingTableOptions.labelForAddNewButton ?? localize('tableDesigner.addNewColumnMapping', "New Column Mapping") } } ]; return { title: localize('tableDesigner.foreignKeysTabTitle', "Foreign Keys"), components: [ { componentType: 'table', propertyName: designers.TableProperty.ForeignKeys, showInPropertiesView: false, componentProperties: { ariaLabel: localize('tableDesigner.foreignKeysTabTitle', "Foreign Keys"), columns: this.getTableDisplayProperties(options, [designers.TableForeignKeyProperty.Name, designers.TableForeignKeyProperty.ForeignTable]), itemProperties: this.addAdditionalTableProperties(options, foreignKeyProperties), objectTypeDisplayName: localize('tableDesigner.ForeignKeyTypeName', "Foreign Key"), canAddRows: options.canAddRows, canRemoveRows: options.canRemoveRows, removeRowConfirmationMessage: options.removeRowConfirmationMessage, showRemoveRowConfirmation: options.showRemoveRowConfirmation, labelForAddNewButton: options.labelForAddNewButton ?? localize('tableDesigner.addForeignKey', "New Foreign Key") } } ] }; } private getPrimaryKeyTab(view: azdata.designers.TableDesignerView): DesignerTab { const options = view.primaryKeyColumnSpecificationTableOptions; const columnSpecProperties: DesignerDataPropertyInfo[] = [ { componentType: 'dropdown', propertyName: designers.TableIndexColumnSpecificationProperty.Column, description: localize('designer.index.column.description.name', "The name of the column."), componentProperties: { title: localize('tableDesigner.index.column.name', "Column"), width: 100 } }]; const tabComponents = []; tabComponents.push( { componentType: 'input', propertyName: designers.TableProperty.PrimaryKeyName, showInPropertiesView: false, description: localize('designer.table.primaryKeyName.description', "Name of the primary key."), componentProperties: { title: localize('tableDesigner.primaryKeyNameTitle', "Name") } }, { componentType: 'input', propertyName: designers.TableProperty.PrimaryKeyDescription, showInPropertiesView: false, description: localize('designer.table.primaryKeyDescription.description', "The description of the primary key."), componentProperties: { title: localize('tableDesigner.primaryKeyDescriptionTitle', "Description"), } }); if (view.additionalPrimaryKeyProperties) { view.additionalPrimaryKeyProperties.forEach(component => { component.showInPropertiesView = false; tabComponents.push(component); }); } tabComponents.push({ componentType: 'table', propertyName: designers.TableProperty.PrimaryKeyColumns, showInPropertiesView: false, description: localize('designer.table.primaryKeyColumns.description', "Columns in the primary key."), componentProperties: { title: localize('tableDesigner.primaryKeyColumnsTitle', "Primary Key Columns"), ariaLabel: localize('tableDesigner.primaryKeyColumnsTitle', "Primary Key Columns"), columns: this.getTableDisplayProperties(options, [designers.TableIndexColumnSpecificationProperty.Column]), itemProperties: this.addAdditionalTableProperties(options, columnSpecProperties), objectTypeDisplayName: '', canAddRows: options.canAddRows, canRemoveRows: options.canRemoveRows, removeRowConfirmationMessage: options.removeRowConfirmationMessage, showRemoveRowConfirmation: options.showRemoveRowConfirmation, showItemDetailInPropertiesView: false, labelForAddNewButton: options.labelForAddNewButton ?? localize('tableDesigner.addNewColumnToPrimaryKey', "Add Column") } }); return { title: localize('tableDesigner.PrimaryKeyTabTitle', "Primary Key"), components: tabComponents }; } private getCheckConstraintsTab(options: azdata.designers.TableDesignerBuiltInTableViewOptions): DesignerTab { const checkConstraintProperties: DesignerDataPropertyInfo[] = [ { componentType: 'input', propertyName: designers.TableCheckConstraintProperty.Name, description: localize('designer.checkConstraint.description.name', "The name of the check constraint."), componentProperties: { title: localize('tableDesigner.checkConstraintNameTitle', "Name"), width: 200 } }, { componentType: 'input', propertyName: designers.TableCheckConstraintProperty.Description, description: localize('designer.checkConstraint.description.description', "The description of the check constraint."), componentProperties: { title: localize('tableDesigner.checkConstraintDescriptionTitle', "Description"), } }, { componentType: 'input', propertyName: designers.TableCheckConstraintProperty.Expression, description: localize('designer.checkConstraint.description.expression', "The expression defining the check constraint."), componentProperties: { title: localize('tableDesigner.checkConstraintExpressionTitle', "Expression"), width: 300 } } ]; return { title: localize('tableDesigner.checkConstraintsTabTitle', "Check Constraints"), components: [ { componentType: 'table', propertyName: designers.TableProperty.CheckConstraints, showInPropertiesView: false, componentProperties: { ariaLabel: localize('tableDesigner.checkConstraintsTabTitle', "Check Constraints"), columns: this.getTableDisplayProperties(options, [designers.TableCheckConstraintProperty.Name, designers.TableCheckConstraintProperty.Expression]), itemProperties: this.addAdditionalTableProperties(options, checkConstraintProperties), objectTypeDisplayName: localize('tableDesigner.checkConstraintTypeName', "Check Constraint"), canAddRows: options.canAddRows, canRemoveRows: options.canRemoveRows, removeRowConfirmationMessage: options.removeRowConfirmationMessage, showRemoveRowConfirmation: options.showRemoveRowConfirmation, labelForAddNewButton: options.labelForAddNewButton ?? localize('tableDesigner.addNewCheckConstraint', "New Check Constraint") } } ] }; } private getIndexesTab(options: azdata.designers.TableDesignerBuiltInTableViewOptions, columnSpecTableOptions: azdata.designers.TableDesignerBuiltInTableViewOptions): DesignerTab { const columnSpecProperties: DesignerDataPropertyInfo[] = [ { componentType: 'dropdown', propertyName: designers.TableIndexColumnSpecificationProperty.Column, description: localize('designer.index.column.description.name', "The name of the column."), componentProperties: { title: localize('tableDesigner.index.column.name', "Column"), width: 100 } }]; const indexProperties: DesignerDataPropertyInfo[] = [ { componentType: 'input', propertyName: designers.TableIndexProperty.Name, description: localize('designer.index.description.name', "The name of the index."), componentProperties: { title: localize('tableDesigner.indexName', "Name"), width: 200 } }, { componentType: 'input', propertyName: designers.TableIndexProperty.Description, description: localize('designer.index.description.description', "The description of the index."), componentProperties: { title: localize('tableDesigner.indexDescription', "Description"), width: 200 } }, { componentType: 'table', propertyName: designers.TableIndexProperty.Columns, description: localize('designer.index.description.columns', "The columns of the index."), group: localize('tableDesigner.indexColumns', "Columns"), componentProperties: { ariaLabel: localize('tableDesigner.indexColumns', "Columns"), columns: this.getTableDisplayProperties(columnSpecTableOptions, [designers.TableIndexColumnSpecificationProperty.Column]), itemProperties: this.addAdditionalTableProperties(columnSpecTableOptions, columnSpecProperties), objectTypeDisplayName: '', canAddRows: columnSpecTableOptions.canAddRows, canRemoveRows: columnSpecTableOptions.canRemoveRows, removeRowConfirmationMessage: columnSpecTableOptions.removeRowConfirmationMessage, showRemoveRowConfirmation: columnSpecTableOptions.showRemoveRowConfirmation, labelForAddNewButton: columnSpecTableOptions.labelForAddNewButton ?? localize('tableDesigner.addNewColumnToIndex', "Add Column") } } ]; return { title: localize('tableDesigner.indexesTabTitle', "Indexes"), components: [ { componentType: 'table', propertyName: designers.TableProperty.Indexes, showInPropertiesView: false, componentProperties: { ariaLabel: localize('tableDesigner.indexesTabTitle', "Indexes"), columns: this.getTableDisplayProperties(options, [designers.TableIndexProperty.Name]), itemProperties: this.addAdditionalTableProperties(options, indexProperties), objectTypeDisplayName: localize('tableDesigner.IndexTypeName', "Index"), canAddRows: options.canAddRows, canRemoveRows: options.canRemoveRows, removeRowConfirmationMessage: options.removeRowConfirmationMessage, showRemoveRowConfirmation: options.showRemoveRowConfirmation, labelForAddNewButton: options.labelForAddNewButton ?? localize('tableDesigner.addNewIndex', "New Index") } } ] }; } private getTableDisplayProperties(options: azdata.designers.TableDesignerBuiltInTableViewOptions, defaultProperties: string[]): string[] { return options.propertiesToDisplay?.length > 0 ? options.propertiesToDisplay : defaultProperties; } private addAdditionalTableProperties(options: azdata.designers.TableDesignerBuiltInTableViewOptions, properties: DesignerDataPropertyInfo[]): DesignerDataPropertyInfo[] { if (options.additionalProperties) { properties.push(...options.additionalProperties); } return properties; } private createTelemetryInfo(): ITelemetryEventProperties { let telemetryInfo = { provider: this._provider.providerId, isNewTable: this.tableInfo.isNewTable, }; Object.assign(telemetryInfo, this._telemetryInfo); return telemetryInfo; } private isDirty(): boolean { const copyOfViewModel = deepClone(this._viewModel); const copyOfOriginalViewModel = deepClone(this._originalViewModel); // The generated script might be slightly different even though the models are the same // espeically the order of the description property statements. // we should take the script out for comparison. if (copyOfViewModel) { delete copyOfViewModel[ScriptProperty]; } if (copyOfOriginalViewModel) { delete copyOfOriginalViewModel[ScriptProperty]; } return !equals(copyOfViewModel, copyOfOriginalViewModel); } /** * 1. 'Add' scenario a. ['propertyName1']. Example: add a column to the columns property: ['columns']. b. ['propertyName1',index-1,'propertyName2']. Example: add a column mapping to the first foreign key: ['foreignKeys',0,'mappings']. 2. 'Update' scenario a. ['propertyName1']. Example: update the name of the table: ['name']. b. ['propertyName1',index-1,'propertyName2']. Example: update the name of a column: ['columns',0,'name']. c. ['propertyName1',index-1,'propertyName2',index-2,'propertyName3']. Example: update the source column of an entry in a foreign key's column mapping table: ['foreignKeys',0,'mappings',0,'source']. 3. 'Remove' scenario a. ['propertyName1',index-1]. Example: remove a column from the columns property: ['columns',0']. b. ['propertyName1',index-1,'proper The return values would be the propertyNames followed by slashes in level order. Eg.: propertyName1/propertyName2/... */ private getObjectTypeFromPath(path: DesignerPropertyPath): string { let typeArray = []; for (let i = 0; i < path.length; i++) { if (i % 2 === 0) { typeArray.push(path[i]); } } return typeArray.join('/'); } }