diff --git a/.eslintrc.json b/.eslintrc.json index 40cd9251a6..08c503d03f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -554,6 +554,7 @@ "**/{vs,sql}/workbench/common/**", "**/{vs,sql}/workbench/services/**/common/**", "**/{vs,sql}/workbench/api/**/common/**", + "**/{vs,sql}/workbench/contrib/**/common/**", "vs/workbench/contrib/files/common/editors/fileEditorInput", // this should be fine, it only accesses constants from contrib "vscode-textmate", "vscode-oniguruma", @@ -591,6 +592,7 @@ "**/{vs,sql}/workbench/{common,browser}/**", "**/{vs,sql}/workbench/api/{common,browser}/**", "**/{vs,sql}/workbench/services/**/{common,browser}/**", + "**/{vs,sql}/workbench/contrib/**/common/**", "vscode-textmate", "vscode-oniguruma", "iconv-lite-umd", diff --git a/extensions/azurecore/src/azurecore.d.ts b/extensions/azurecore/src/azurecore.d.ts index e3b90916ca..6c1d7df944 100644 --- a/extensions/azurecore/src/azurecore.d.ts +++ b/extensions/azurecore/src/azurecore.d.ts @@ -122,7 +122,7 @@ declare module 'azurecore' { /** * Information that describes the Azure Kusto resource */ - azureKustoResource?: Resource; + azureKustoResource?: Resource; /** * Information that describes the Azure Log Analytics resource diff --git a/src/sql/platform/azureAccount/common/azureAccountService.ts b/src/sql/platform/azureAccount/common/azureAccountService.ts index 83472bf244..eb9096edee 100644 --- a/src/sql/platform/azureAccount/common/azureAccountService.ts +++ b/src/sql/platform/azureAccount/common/azureAccountService.ts @@ -13,5 +13,8 @@ export const IAzureAccountService = createDecorator(SERVIC export interface IAzureAccountService { _serviceBrand: undefined; getSubscriptions(account: azurecore.AzureAccount): Promise; + getStorageAccounts(account: azurecore.AzureAccount, subscriptions: azurecore.azureResource.AzureResourceSubscription[]): Promise; + getBlobContainers(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource): Promise; + getBlobs(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, containerName: string, ignoreErrors?: boolean): Promise; + getStorageAccountAccessKey(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise; } - diff --git a/src/sql/platform/telemetry/common/telemetryKeys.ts b/src/sql/platform/telemetry/common/telemetryKeys.ts index cd0de80263..06203be35f 100644 --- a/src/sql/platform/telemetry/common/telemetryKeys.ts +++ b/src/sql/platform/telemetry/common/telemetryKeys.ts @@ -17,6 +17,7 @@ export const enum ModalDialogName { Connection = 'Connection', Backup = 'Backup', FileBrowser = 'FileBrowser', + UrlBrowser = 'UrlBrowser', Restore = 'Restore', Insights = 'Insights', Profiler = 'Profiler', diff --git a/src/sql/workbench/api/browser/mainThreadAzureAccount.ts b/src/sql/workbench/api/browser/mainThreadAzureAccount.ts index ce6e677588..72d94e8ed1 100644 --- a/src/sql/workbench/api/browser/mainThreadAzureAccount.ts +++ b/src/sql/workbench/api/browser/mainThreadAzureAccount.ts @@ -34,4 +34,20 @@ export class MainThreadAzureAccount extends Disposable implements MainThreadAzur return this._proxy.$getSubscriptions(account, ignoreErrors, selectedOnly); } + public getStorageAccounts(account: azurecore.AzureAccount, subscriptions: azurecore.azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise { + return this._proxy.$getStorageAccounts(account, subscriptions, ignoreErrors); + } + + public getBlobContainers(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise { + return this._proxy.$getBlobContainers(account, subscription, storageAccount, ignoreErrors); + } + + public getBlobs(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, containerName: string, ignoreErrors?: boolean): Promise { + return this._proxy.$getBlobs(account, subscription, storageAccount, containerName, ignoreErrors); + } + + public getStorageAccountAccessKey(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise { + return this._proxy.$getStorageAccountAccessKey(account, subscription, storageAccount, ignoreErrors); + } + } diff --git a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts index 76088c38ac..5c5891b6f0 100644 --- a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts @@ -32,7 +32,6 @@ import { IAdsTelemetryService, ITelemetryEventProperties } from 'sql/platform/te import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { ITableDesignerService } from 'sql/workbench/services/tableDesigner/common/interface'; import { IExecutionPlanService } from 'sql/workbench/services/executionPlan/common/interfaces'; - /** * Main thread class for handling data protocol management registration. */ diff --git a/src/sql/workbench/api/common/extHostAzureAccount.ts b/src/sql/workbench/api/common/extHostAzureAccount.ts index c217d2c86a..6f9f6ea45e 100644 --- a/src/sql/workbench/api/common/extHostAzureAccount.ts +++ b/src/sql/workbench/api/common/extHostAzureAccount.ts @@ -15,8 +15,31 @@ export class ExtHostAzureAccount extends ExtHostAzureAccountShape { } public override $getSubscriptions(account: azurecore.AzureAccount, ignoreErrors?: boolean, selectedOnly?: boolean): Thenable { - const api = this._extHostExtensionService.getExtensionExports(new ExtensionIdentifier(azurecore.extension.name)) as azurecore.IExtension; + const api = this.getApi(); return api.getSubscriptions(account, ignoreErrors, selectedOnly); } -} + public override $getStorageAccounts(account: azurecore.AzureAccount, subscriptions: azurecore.azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise { + const api = this.getApi(); + return api.getStorageAccounts(account, subscriptions, ignoreErrors); + } + + public override $getBlobContainers(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise { + const api = this.getApi(); + return api.getBlobContainers(account, subscription, storageAccount); + } + + public override $getBlobs(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, containerName: string, ignoreErrors?: boolean): Promise { + const api = this.getApi(); + return api.getBlobs(account, subscription, storageAccount, containerName, ignoreErrors); + } + + public override $getStorageAccountAccessKey(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise { + const api = this.getApi(); + return api.getStorageAccountAccessKey(account, subscription, storageAccount, ignoreErrors); + } + + private getApi(): azurecore.IExtension { + return this._extHostExtensionService.getExtensionExports(new ExtensionIdentifier(azurecore.extension.name)) as azurecore.IExtension; + } +} diff --git a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts index 2abdea8532..3dd992d470 100644 --- a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts @@ -84,6 +84,7 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp const extHostConnectionManagement = rpcProtocol.set(SqlExtHostContext.ExtHostConnectionManagement, new ExtHostConnectionManagement(rpcProtocol)); const extHostCredentialManagement = rpcProtocol.set(SqlExtHostContext.ExtHostCredentialManagement, new ExtHostCredentialManagement(rpcProtocol)); rpcProtocol.set(SqlExtHostContext.ExtHostAzureBlob, new ExtHostAzureBlob(accessor.get(IExtHostExtensionService))); + rpcProtocol.set(SqlExtHostContext.ExtHostAzureAccount, new ExtHostAzureAccount(accessor.get(IExtHostExtensionService))); const extHostDataProvider = rpcProtocol.set(SqlExtHostContext.ExtHostDataProtocol, new ExtHostDataProtocol(rpcProtocol, uriTransformer)); const extHostObjectExplorer = rpcProtocol.set(SqlExtHostContext.ExtHostObjectExplorer, new ExtHostObjectExplorer(rpcProtocol, commands)); const extHostResourceProvider = rpcProtocol.set(SqlExtHostContext.ExtHostResourceProvider, new ExtHostResourceProvider(rpcProtocol)); diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index 88385fd26f..4491e65abb 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -35,8 +35,13 @@ import { ITelemetryEventProperties } from 'sql/platform/telemetry/common/telemet export abstract class ExtHostAzureBlobShape { public $createSas(connectionUri: string, blobContainerUri: string, blobStorageKey: string, storageAccountName: string, expirationDate: string): Thenable { throw ni(); } } + export abstract class ExtHostAzureAccountShape { public $getSubscriptions(account: azurecore.AzureAccount, ignoreErrors?: boolean, selectedOnly?: boolean): Thenable { throw ni(); } + public $getStorageAccounts(account: azurecore.AzureAccount, subscriptions: azurecore.azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise { throw ni(); } + public $getBlobContainers(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise { throw ni(); } + public $getBlobs(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, containerName: string, ignoreErrors: boolean): Promise { throw ni(); } + public $getStorageAccountAccessKey(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise { throw ni(); } } export abstract class ExtHostAccountManagementShape { @@ -628,6 +633,10 @@ export interface MainThreadAzureAccountShape extends IDisposable { } +export interface MainThreadAzureBlobShape extends IDisposable { + +} + export interface MainThreadResourceProviderShape extends IDisposable { $registerResourceProvider(providerMetadata: azdata.ResourceProviderMetadata, handle: number): Thenable; $unregisterResourceProvider(handle: number): Thenable; diff --git a/src/sql/workbench/contrib/backup/browser/backup.component.html b/src/sql/workbench/contrib/backup/browser/backup.component.html index 3297de6ef6..60f7a31dad 100644 --- a/src/sql/workbench/contrib/backup/browser/backup.component.html +++ b/src/sql/workbench/contrib/backup/browser/backup.component.html @@ -24,20 +24,36 @@
-
+
+
+
{{localizedStrings.BACKUP_DEVICE}}
+
-
+
- +
+
+
+
+
+ +
-
+
-
+
+
+ + + @@ -91,19 +107,19 @@
- {{localizedStrings.MEDIA_OPTION}} + {{localizedStrings.MEDIA_OPTION}}
- {{localizedStrings.EXISTING_MEDIA_APPEND}} + {{localizedStrings.EXISTING_MEDIA_APPEND}}
- {{localizedStrings.EXISTING_MEDIA_OVERWRITE}} + {{localizedStrings.EXISTING_MEDIA_OVERWRITE}}
- {{localizedStrings.MEDIA_OPTION_FORMAT}} + {{localizedStrings.MEDIA_OPTION_FORMAT}}
diff --git a/src/sql/workbench/contrib/backup/browser/backup.component.ts b/src/sql/workbench/contrib/backup/browser/backup.component.ts index c57296d10a..26b977f28b 100644 --- a/src/sql/workbench/contrib/backup/browser/backup.component.ts +++ b/src/sql/workbench/contrib/backup/browser/backup.component.ts @@ -36,6 +36,9 @@ import { fileFiltersSet } from 'sql/workbench/services/restore/common/constants' import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { attachButtonStyler } from 'vs/platform/theme/common/styler'; +import { DatabaseEngineEdition } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { IBackupRestoreUrlBrowserDialogService } from 'sql/workbench/services/backupRestoreUrlBrowser/common/urlBrowserDialogService'; + export const BACKUP_SELECTOR: string = 'backup-component'; export class RestoreItemSource { @@ -86,6 +89,7 @@ const LocalizedStrings = { RECOVERY_MODEL: localize('backup.recoveryModel', "Recovery model"), BACKUP_TYPE: localize('backup.backupType', "Backup type"), BACKUP_DEVICE: localize('backup.backupDevice', "Backup files"), + BACKUP_URL: localize('backup.backupUrl', "Backup URL"), ALGORITHM: localize('backup.algorithm', "Algorithm"), CERTIFICATE_OR_ASYMMETRIC_KEY: localize('backup.certificateOrAsymmetricKey', "Certificate or Asymmetric key"), MEDIA: localize('backup.media', "Media"), @@ -101,6 +105,7 @@ const LocalizedStrings = { EXPIRATION: localize('backup.expiration', "Expiration"), SET_BACKUP_RETAIN_DAYS: localize('backup.setBackupRetainDays', "Set backup retain days"), COPY_ONLY: localize('backup.copyOnly', "Copy-only backup"), + TO_URL: localize('backup.toUrl', "Save backup to URL"), ADVANCED_CONFIGURATION: localize('backup.advancedConfiguration', "Advanced Configuration"), COMPRESSION: localize('backup.compression', "Compression"), SET_BACKUP_COMPRESSION: localize('backup.setBackupCompression', "Set backup compression"), @@ -118,7 +123,8 @@ const LocalizedStrings = { templateUrl: decodeURI(require.toUrl('./backup.component.html')) }) export class BackupComponent extends AngularDisposable { - @ViewChild('pathContainer', { read: ElementRef }) pathElement?: ElementRef; + @ViewChild('urlPathContainer', { read: ElementRef }) urlPathElement?: ElementRef; + @ViewChild('filePathContainer', { read: ElementRef }) filePathElement?: ElementRef; @ViewChild('backupTypeContainer', { read: ElementRef }) backupTypeElement?: ElementRef; @ViewChild('backupsetName', { read: ElementRef }) backupNameElement?: ElementRef; @ViewChild('compressionContainer', { read: ElementRef }) compressionElement?: ElementRef; @@ -131,9 +137,15 @@ export class BackupComponent extends AngularDisposable { @ViewChild('backupDaysContainer', { read: ElementRef }) backupDaysElement?: ElementRef; @ViewChild('backupButtonContainer', { read: ElementRef }) backupButtonElement?: ElementRef; @ViewChild('cancelButtonContainer', { read: ElementRef }) cancelButtonElement?: ElementRef; - @ViewChild('addPathContainer', { read: ElementRef }) addPathElement?: ElementRef; - @ViewChild('removePathContainer', { read: ElementRef }) removePathElement?: ElementRef; + @ViewChild('filePathButtonsContainer', { read: ElementRef }) filePathButtonsElement?: ElementRef; + @ViewChild('addFilePathContainer', { read: ElementRef }) addFilePathElement?: ElementRef; + @ViewChild('removeFilePathContainer', { read: ElementRef }) removeFilePathElement?: ElementRef; + @ViewChild('urlPathButtonsContainer', { read: ElementRef }) urlPathButtonsElement?: ElementRef; + @ViewChild('addUrlPathContainer', { read: ElementRef }) addUrlPathElement?: ElementRef; @ViewChild('copyOnlyContainer', { read: ElementRef }) copyOnlyElement?: ElementRef; + @ViewChild('filePathContainerLabel', { read: ElementRef }) filePathLabelElement?: ElementRef; + @ViewChild('urlPathContainerLabel', { read: ElementRef }) urlPathLabelElement?: ElementRef; + @ViewChild('toUrlContainer', { read: ElementRef }) toUrlElement?: ElementRef; @ViewChild('encryptCheckContainer', { read: ElementRef }) encryptElement?: ElementRef; @ViewChild('encryptContainer', { read: ElementRef }) encryptContainerElement?: ElementRef; @ViewChild('verifyContainer', { read: ElementRef }) verifyElement?: ElementRef; @@ -149,6 +161,7 @@ export class BackupComponent extends AngularDisposable { private localizedStrings = LocalizedStrings; private _uri?: string; + private _engineEdition?: number; private connection?: IConnectionProfile; private databaseName?: string; @@ -162,6 +175,7 @@ export class BackupComponent extends AngularDisposable { // UI element disable flag public disableTlog?: boolean; + public disableMedia?: boolean; public selectedBackupComponent?: string; private selectedFilesText?: string; @@ -185,15 +199,18 @@ export class BackupComponent extends AngularDisposable { private backupButton?: Button; private cancelButton?: Button; private scriptButton?: Button; - private addPathButton?: Button; - private removePathButton?: Button; + private addUrlPathButton?: Button; + private addFilePathButton?: Button; + private removeFilePathButton?: Button; private pathListBox?: ListBox; + private urlInputBox?: InputBox; private compressionSelectBox?: SelectBox; private algorithmSelectBox?: SelectBox; private encryptorSelectBox?: SelectBox; private mediaNameBox?: InputBox; private mediaDescriptionBox?: InputBox; private copyOnlyCheckBox?: Checkbox; + private toUrlCheckBox?: Checkbox; private encryptCheckBox?: Checkbox; private verifyCheckBox?: Checkbox; private checksumCheckBox?: Checkbox; @@ -204,6 +221,7 @@ export class BackupComponent extends AngularDisposable { @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService, @Inject(IContextViewService) private contextViewService: IContextViewService, @Inject(IFileBrowserDialogController) private fileBrowserDialogService: IFileBrowserDialogController, + @Inject(IBackupRestoreUrlBrowserDialogService) private backupRestoreUrlBrowserDialogService: IBackupRestoreUrlBrowserDialogService, @Inject(IBackupUiService) private _backupUiService: IBackupUiService, @Inject(IBackupService) private _backupService: IBackupService, @Inject(IClipboardService) private clipboardService: IClipboardService, @@ -216,63 +234,76 @@ export class BackupComponent extends AngularDisposable { ngOnInit() { this.addFooterButtons(); - this.recoveryBox = new InputBox(this.recoveryModelElement!.nativeElement, this.contextViewService, { + this.recoveryBox = this._register(new InputBox(this.recoveryModelElement!.nativeElement, this.contextViewService, { placeholder: this.recoveryModel, ariaLabel: LocalizedStrings.RECOVERY_MODEL - }); + })); // Set backup type - this.backupTypeSelectBox = new SelectBox([], '', this.contextViewService, undefined, { ariaLabel: this.localizedStrings.BACKUP_TYPE }); + this.backupTypeSelectBox = this._register(new SelectBox([], '', this.contextViewService, undefined, { ariaLabel: this.localizedStrings.BACKUP_TYPE })); this.backupTypeSelectBox.render(this.backupTypeElement!.nativeElement); // Set copy-only check box - this.copyOnlyCheckBox = new Checkbox(this.copyOnlyElement!.nativeElement, { + this.copyOnlyCheckBox = this._register(new Checkbox(this.copyOnlyElement!.nativeElement, { label: LocalizedStrings.COPY_ONLY, checked: false, onChange: (viaKeyboard) => { }, ariaLabel: LocalizedStrings.COPY_ONLY - }); + })); + + // Set to url check box + this.toUrlCheckBox = this._register(new Checkbox(this.toUrlElement!.nativeElement, { + label: LocalizedStrings.TO_URL, + checked: false, + onChange: () => this.onChangeToUrl(), + ariaLabel: LocalizedStrings.TO_URL + })); // Encryption checkbox - this.encryptCheckBox = new Checkbox(this.encryptElement!.nativeElement, { + this.encryptCheckBox = this._register(new Checkbox(this.encryptElement!.nativeElement, { label: LocalizedStrings.ENCRYPTION, checked: false, onChange: () => this.onChangeEncrypt(), ariaLabel: LocalizedStrings.ENCRYPTION - }); + })); // Verify backup checkbox - this.verifyCheckBox = new Checkbox(this.verifyElement!.nativeElement, { + this.verifyCheckBox = this._register(new Checkbox(this.verifyElement!.nativeElement, { label: LocalizedStrings.VERIFY_CONTAINER, checked: false, onChange: () => { }, ariaLabel: LocalizedStrings.VERIFY_CONTAINER - }); + })); // Perform checksum checkbox - this.checksumCheckBox = new Checkbox(this.checksumElement!.nativeElement, { + this.checksumCheckBox = this._register(new Checkbox(this.checksumElement!.nativeElement, { label: LocalizedStrings.CHECKSUM_CONTAINER, checked: false, onChange: () => { }, ariaLabel: LocalizedStrings.CHECKSUM_CONTAINER - }); + })); // Continue on error checkbox - this.continueOnErrorCheckBox = new Checkbox(this.continueOnErrorElement!.nativeElement, { + this.continueOnErrorCheckBox = this._register(new Checkbox(this.continueOnErrorElement!.nativeElement, { label: LocalizedStrings.CONTINUE_ON_ERROR_CONTAINER, checked: false, onChange: () => { }, ariaLabel: LocalizedStrings.CONTINUE_ON_ERROR_CONTAINER - }); + })); // Set backup name - this.backupNameBox = new InputBox(this.backupNameElement!.nativeElement, this.contextViewService, { + this.backupNameBox = this._register(new InputBox(this.backupNameElement!.nativeElement, this.contextViewService, { ariaLabel: LocalizedStrings.BACKUP_NAME - }); + })); // Set backup path list - this.pathListBox = new ListBox([], this.contextViewService); + this.urlInputBox = this._register(new InputBox(this.urlPathElement!.nativeElement, this.contextViewService, { + ariaLabel: LocalizedStrings.BACKUP_URL + })); + this._register(this.urlInputBox.onDidChange((value) => this.onUrlInputBoxChanged(value))); + + this.pathListBox = this._register(new ListBox([], this.contextViewService)); this.pathListBox.setAriaLabel(LocalizedStrings.BACKUP_DEVICE); - this.pathListBox.onKeyDown(e => { + this._register(this.pathListBox.onKeyDown(e => { if (this.pathListBox!.selectedOptions.length > 0) { const key = e.keyCode; const ctrlOrCmd = e.ctrlKey || e.metaKey; @@ -289,17 +320,19 @@ export class BackupComponent extends AngularDisposable { e.stopPropagation(); } } - }); - this.pathListBox.render(this.pathElement!.nativeElement); + })); + this.pathListBox.render(this.filePathElement!.nativeElement); // Set backup path add/remove buttons - this.addPathButton = this._register(new Button(this.addPathElement!.nativeElement, { secondary: true })); - this.addPathButton.label = '+'; - this.addPathButton.title = localize('addFile', "Add a file"); - - this.removePathButton = this._register(new Button(this.removePathElement!.nativeElement, { secondary: true })); - this.removePathButton.label = '-'; - this.removePathButton.title = localize('removeFile', "Remove files"); + this.addUrlPathButton = this._register(new Button(this.addUrlPathElement!.nativeElement, { secondary: true })); + this.addUrlPathButton.label = localize('backupBrowseButton', "Browse"); + this.addUrlPathButton.title = localize('addUrl', "Add URL"); + this.addFilePathButton = this._register(new Button(this.addFilePathElement!.nativeElement, { secondary: true })); + this.addFilePathButton.label = '+'; + this.addFilePathButton.title = localize('addFile', "Add File"); + this.removeFilePathButton = this._register(new Button(this.removeFilePathElement!.nativeElement, { secondary: true })); + this.removeFilePathButton.label = '-'; + this.removeFilePathButton.title = localize('removeFile', "Remove files"); // Set compression this.compressionSelectBox = this._register(new SelectBox(this.compressionOptions, this.compressionOptions[0], this.contextViewService, undefined, { ariaLabel: this.localizedStrings.SET_BACKUP_COMPRESSION })); @@ -350,6 +383,10 @@ export class BackupComponent extends AngularDisposable { this.recoveryBox.disable(); this.mediaNameBox.disable(); this.mediaDescriptionBox.disable(); + this.backupTypeSelectBox.disable(); + this.copyOnlyCheckBox.disable(); + this.backupRetainDaysBox.disable(); + this.toUrlCheckBox.disable(); this.registerListeners(); this.updateTheme(this.themeService.getColorTheme()); @@ -371,6 +408,8 @@ export class BackupComponent extends AngularDisposable { this.connection = param.connection; this._uri = param.ownerUri; + this._engineEdition = this.connectionManagementService.getConnectionInfo(this._uri).serverInfo.engineEditionId; + // Get backup configuration info this._backupService.getBackupConfigInfo(this._uri).then(configInfo => { if (configInfo) { @@ -404,21 +443,21 @@ export class BackupComponent extends AngularDisposable { // Set script footer button this.scriptButton = this._register(new Button(this.scriptButtonElement!.nativeElement, { secondary: true })); this.scriptButton.label = localize('backupComponent.script', "Script"); - this.scriptButton.onDidClick(() => this.onScript()); + this._register(this.scriptButton.onDidClick(() => this.onScript())); this._register(attachButtonStyler(this.scriptButton, this.themeService)); this.scriptButton.enabled = false; // Set backup footer button this.backupButton = this._register(new Button(this.backupButtonElement!.nativeElement)); this.backupButton.label = localize('backupComponent.backup', "Backup"); - this.backupButton.onDidClick(() => this.onOk()); + this._register(this.backupButton.onDidClick(() => this.onOk())); this._register(attachButtonStyler(this.backupButton, this.themeService)); this.backupEnabled = false; // Set cancel footer button this.cancelButton = this._register(new Button(this.cancelButtonElement!.nativeElement, { secondary: true })); this.cancelButton.label = localize('backupComponent.cancel', "Cancel"); - this.cancelButton.onDidClick(() => this.onCancel()); + this._register(this.cancelButton.onDidClick(() => this.onCancel())); this._register(attachButtonStyler(this.cancelButton, this.themeService)); } @@ -433,7 +472,7 @@ export class BackupComponent extends AngularDisposable { this.backupTypeOptions = []; if (isMetadataPopulated) { - this.backupEnabled = true; + this.enableBackupButton(); // Set recovery model this.setControlsForRecoveryModel(); @@ -447,14 +486,6 @@ export class BackupComponent extends AngularDisposable { this.setDefaultBackupName(); this.backupNameBox!.focus(); - // Set backup path list - this.setDefaultBackupPaths(); - let pathlist: ISelectOptionItem[] = []; - for (let i in this.backupPathTypePairs) { - pathlist.push({ text: i }); - } - this.pathListBox!.setOptions(pathlist, 0); - // Set encryption let encryptorItems = this.populateEncryptorCombo(); this.encryptorSelectBox!.setOptions(encryptorItems, 0); @@ -482,6 +513,26 @@ export class BackupComponent extends AngularDisposable { this.recoveryBox!.disable(); this.mediaNameBox!.disable(); this.mediaDescriptionBox!.disable(); + if (this._engineEdition === DatabaseEngineEdition.SqlManagedInstance) { + this.toUrlCheckBox.checked = true; + this.copyOnlyCheckBox.checked = true; + this.backupTypeSelectBox!.disable(); + this.copyOnlyCheckBox!.disable(); + this.backupRetainDaysBox!.disable(); + this.disableMedia = true; + } else { + this.toUrlCheckBox.checked = false; + this.copyOnlyCheckBox.checked = false; + this.backupRetainDaysBox!.enable(); + this.copyOnlyCheckBox!.enable(); + this.backupTypeSelectBox!.enable(); + this.disableMedia = false; + } + this.onChangeToUrl(); + + // Set backup path list + this.setBackupPathList(); + this.recoveryBox!.value = this.recoveryModel!; // show warning message if latest backup file path contains url @@ -494,6 +545,20 @@ export class BackupComponent extends AngularDisposable { this._changeDetectorRef.detectChanges(); } + /** + * Set backup file path options in the list box + */ + private setBackupPathList() { + let pathlist: ISelectOptionItem[] = []; + if (!this.toUrlCheckBox.checked) { + for (let i in this.backupPathTypePairs) { + pathlist.push({ text: i }); + } + this.setDefaultBackupPaths(); + this.pathListBox!.setOptions(pathlist, 0); + } + } + /** * Reset dialog controls to their initial state. */ @@ -501,8 +566,9 @@ export class BackupComponent extends AngularDisposable { this.isFormatChecked = false; this.isEncryptChecked = false; - this.copyOnlyCheckBox!.checked = false; - this.copyOnlyCheckBox!.enable(); + this.copyOnlyCheckBox!.checked = true; + this.copyOnlyCheckBox!.disable(); + this.toUrlCheckBox!.checked = false; this.compressionSelectBox!.setOptions(this.compressionOptions, 0); this.encryptCheckBox!.checked = false; this.encryptCheckBox!.enable(); @@ -517,6 +583,7 @@ export class BackupComponent extends AngularDisposable { this.algorithmSelectBox!.setOptions(this.encryptionAlgorithms, 0); this.selectedInitOption = this.existingMediaOptions[0]; this.containsBackupToUrl = false; + this.urlInputBox!.value = ''; this.pathListBox!.setValidation(true); this.cancelButton!.applyStyles(); @@ -530,23 +597,27 @@ export class BackupComponent extends AngularDisposable { this._register(attachInputBoxStyler(this.recoveryBox!, this.themeService)); this._register(attachSelectBoxStyler(this.backupTypeSelectBox!, this.themeService)); this._register(attachListBoxStyler(this.pathListBox!, this.themeService)); - this._register(attachButtonStyler(this.addPathButton!, this.themeService)); - this._register(attachButtonStyler(this.removePathButton!, this.themeService)); + this._register(attachButtonStyler(this.addUrlPathButton!, this.themeService)); + this._register(attachButtonStyler(this.addFilePathButton!, this.themeService)); + this._register(attachButtonStyler(this.removeFilePathButton!, this.themeService)); this._register(attachSelectBoxStyler(this.compressionSelectBox!, this.themeService)); this._register(attachSelectBoxStyler(this.algorithmSelectBox!, this.themeService)); this._register(attachSelectBoxStyler(this.encryptorSelectBox!, this.themeService)); this._register(attachInputBoxStyler(this.mediaNameBox!, this.themeService)); + this._register(attachInputBoxStyler(this.urlInputBox!, this.themeService)); this._register(attachInputBoxStyler(this.mediaDescriptionBox!, this.themeService)); this._register(attachInputBoxStyler(this.backupRetainDaysBox!, this.themeService)); this._register(attachCheckboxStyler(this.copyOnlyCheckBox!, this.themeService)); + this._register(attachCheckboxStyler(this.toUrlCheckBox!, this.themeService)); this._register(attachCheckboxStyler(this.encryptCheckBox!, this.themeService)); this._register(attachCheckboxStyler(this.verifyCheckBox!, this.themeService)); this._register(attachCheckboxStyler(this.checksumCheckBox!, this.themeService)); this._register(attachCheckboxStyler(this.continueOnErrorCheckBox!, this.themeService)); this._register(this.backupTypeSelectBox!.onDidSelect(selected => this.onBackupTypeChanged())); - this.addPathButton!.onDidClick(() => this.onAddClick()); - this.removePathButton!.onDidClick(() => this.onRemoveClick()); + this._register(this.addUrlPathButton!.onDidClick(() => this.onAddUrlClick())); + this._register(this.addFilePathButton!.onDidClick(() => this.onAddFileClick())); + this._register(this.removeFilePathButton!.onDidClick(() => this.onRemoveClick())); this._register(this.mediaNameBox!.onDidChange(mediaName => { this.mediaNameChanged(mediaName); })); @@ -605,7 +676,7 @@ export class BackupComponent extends AngularDisposable { this.setEncryptOptionsEnabled(true); // Force to choose format media option since otherwise encryption cannot be done - if (!this.isFormatChecked) { + if (!this.isFormatChecked && !this.toUrlCheckBox.checked) { this.onChangeMediaFormat(); } } else { @@ -615,6 +686,31 @@ export class BackupComponent extends AngularDisposable { this.detectChange(); } + private onChangeToUrl(): void { + if (this.toUrlCheckBox!.checked) { + this.filePathElement.nativeElement.hidden = true; + this.filePathButtonsElement.nativeElement.hidden = true; + this.filePathLabelElement.nativeElement.hidden = true; + this.urlPathLabelElement.nativeElement.hidden = false; + this.urlPathElement.nativeElement.hidden = false; + this.urlPathButtonsElement.nativeElement.hidden = false; + } else { + this.filePathElement.nativeElement.hidden = false; + this.filePathButtonsElement.nativeElement.hidden = false; + this.filePathLabelElement.nativeElement.hidden = false; + this.urlPathLabelElement.nativeElement.hidden = true; + this.urlPathElement.nativeElement.hidden = true; + this.urlPathButtonsElement.nativeElement.hidden = true; + } + this.setBackupPathList(); + } + + private onUrlInputBoxChanged(value: string) { + this.backupPathTypePairs = {}; + this.backupPathTypePairs[value] = BackupConstants.MediaDeviceType.Url; + this.enableBackupButton(); + } + private onChangeMediaFormat(): void { this.isFormatChecked = !this.isFormatChecked; this.enableMediaInput(this.isFormatChecked); @@ -648,19 +744,40 @@ export class BackupComponent extends AngularDisposable { this._changeDetectorRef.detectChanges(); } - private onAddClick(): void { + private onAddUrlClick(): void { + this.backupRestoreUrlBrowserDialogService.showDialog(this._uri!, + this.defaultNewBackupFolder!, + fileFiltersSet, + FileValidationConstants.backup, + false, + false, + this.getDefaultBackupFileName()).then(url => this.handleUrlPathAdded(url)); + } + + private onAddFileClick(): void { this.fileBrowserDialogService.showDialog(this._uri!, this.defaultNewBackupFolder!, fileFiltersSet, FileValidationConstants.backup, false, - (filepath => this.handlePathAdded(filepath))); + (filepath => this.handleFilePathAdded(filepath))); } - private handlePathAdded(filepath: string) { + private handleUrlPathAdded(url: string): void { + if (url && !this.backupPathTypePairs![url]) { + this.backupPathTypePairs![url] = BackupConstants.MediaDeviceType.File; + this.urlInputBox.value = url; + this.enableBackupButton(); + + this._changeDetectorRef.detectChanges(); + } + } + + private handleFilePathAdded(filepath: string): void { if (filepath && !this.backupPathTypePairs![filepath]) { if ((this.getBackupPathCount() < BackupConstants.maxDevices)) { - this.backupPathTypePairs![filepath] = BackupConstants.deviceTypeFile; + this.backupPathTypePairs![filepath] = BackupConstants.MediaDeviceType.File; + this.pathListBox!.add(filepath); this.enableBackupButton(); this.enableAddRemoveButtons(); @@ -678,7 +795,7 @@ export class BackupComponent extends AngularDisposable { private onRemoveClick(): void { this.pathListBox!.selectedOptions.forEach(selected => { if (this.backupPathTypePairs![selected]) { - if (this.backupPathTypePairs![selected] === BackupConstants.deviceTypeURL) { + if (this.backupPathTypePairs![selected] === BackupConstants.MediaDeviceType.Url) { // stop showing warning message since url path is getting removed this.pathListBox!.setValidation(true); this.containsBackupToUrl = false; @@ -703,12 +820,12 @@ export class BackupComponent extends AngularDisposable { private enableAddRemoveButtons(): void { if (this.pathListBox!.count === 0) { - this.removePathButton!.enabled = false; + this.removeFilePathButton!.enabled = false; } else if (this.pathListBox!.count === BackupConstants.maxDevices) { - this.addPathButton!.enabled = false; + this.addFilePathButton!.enabled = false; } else { - this.removePathButton!.enabled = true; - this.addPathButton!.enabled = true; + this.removeFilePathButton!.enabled = true; + this.addFilePathButton!.enabled = true; } } @@ -758,15 +875,24 @@ export class BackupComponent extends AngularDisposable { if (this.defaultNewBackupFolder[0] === '/') { serverPathSeparator = '/'; } - let d: Date = new Date(); - let formattedDateTime: string = `-${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}-${d.getHours()}-${d.getMinutes()}-${d.getSeconds()}`; - let defaultNewBackupLocation = this.defaultNewBackupFolder + serverPathSeparator + this.databaseName + formattedDateTime + '.bak'; + let defaultNewBackupLocation = this.defaultNewBackupFolder + serverPathSeparator + this.getDefaultBackupFileName(); // Add a default new backup location - this.backupPathTypePairs![defaultNewBackupLocation] = BackupConstants.deviceTypeFile; + if (this.toUrlCheckBox!.checked) { + this.backupPathTypePairs![defaultNewBackupLocation] = BackupConstants.MediaDeviceType.Url; + } else { + this.backupPathTypePairs![defaultNewBackupLocation] = BackupConstants.MediaDeviceType.File; + } } } + private getDefaultBackupFileName(): string { + let d: Date = new Date(); + let formattedDateTime: string = `-${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}-${d.getHours()}-${d.getMinutes()}-${d.getSeconds()}`; + let defaultBackupFileName = this.databaseName + formattedDateTime + '.bak'; + return defaultBackupFileName; + } + private enableMediaInput(enable: boolean): void { if (enable) { this.mediaNameBox!.enable(); @@ -812,8 +938,15 @@ export class BackupComponent extends AngularDisposable { return backupType!; } + private getBackupDeviceType(): number { + if (this.toUrlCheckBox!.checked) { + return BackupConstants.PhysicalDeviceType.Url; + } + return BackupConstants.PhysicalDeviceType.Disk; + } + private getBackupPathCount(): number { - return this.pathListBox!.count; + return this.urlInputBox!.value.length; } private getSelectedBackupType(): string { @@ -826,9 +959,15 @@ export class BackupComponent extends AngularDisposable { private enableBackupButton(): void { if (!this.backupButton!.enabled) { - if (this.pathListBox!.count > 0 && (!this.isFormatChecked || this.mediaNameBox!.value) && this.backupRetainDaysBox!.validate() === undefined) { + //Managed Instance backup doesn't support backup expiration date nor backup media set + if ((this._engineEdition === DatabaseEngineEdition.SqlManagedInstance && this.urlInputBox.value.length > 0) || + (this._engineEdition !== DatabaseEngineEdition.SqlManagedInstance && (!this.isFormatChecked || this.mediaNameBox!.value) && this.backupRetainDaysBox!.validate() === undefined)) { this.backupEnabled = true; } + } else { + if (this._engineEdition === DatabaseEngineEdition.SqlManagedInstance && this.urlInputBox.value.length === 0) { + this.backupEnabled = false; + } } } @@ -880,7 +1019,7 @@ export class BackupComponent extends AngularDisposable { databaseName: this.databaseName!, backupType: this.getBackupTypeNumber(), backupComponent: 0, - backupDeviceType: BackupConstants.backupDeviceTypeDisk, + backupDeviceType: this.getBackupDeviceType(), backupPathList: backupPathArray, selectedFiles: this.selectedFilesText!, backupsetName: this.backupNameBox!.value, diff --git a/src/sql/workbench/contrib/backup/browser/media/backupDialog.css b/src/sql/workbench/contrib/backup/browser/media/backupDialog.css index 9e28f0e147..ce66940a8d 100644 --- a/src/sql/workbench/contrib/backup/browser/media/backupDialog.css +++ b/src/sql/workbench/contrib/backup/browser/media/backupDialog.css @@ -18,7 +18,7 @@ } .backup-dialog { - height: 100% + height: 100%; } .backup-dialog .advanced-main-header { @@ -67,6 +67,6 @@ float: left; } -.backup-dialog .warning-message{ +.backup-dialog .warning-message { padding-left: 20px; } diff --git a/src/sql/workbench/contrib/backup/common/constants.ts b/src/sql/workbench/contrib/backup/common/constants.ts index fa60e8815e..ccf70e47da 100644 --- a/src/sql/workbench/contrib/backup/common/constants.ts +++ b/src/sql/workbench/contrib/backup/common/constants.ts @@ -8,16 +8,31 @@ import { localize } from 'vs/nls'; // Constants export const maxDevices: number = 64; -// Constants for backup physical device type -export const backupDeviceTypeDisk = 2; -export const backupDeviceTypeTape = 5; -export const backupDeviceTypeURL = 9; +/** + * Backup phisical device type: https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.management.smo.backupdevicetype + */ +export enum PhysicalDeviceType { + Disk = 2, + FloppyA = 3, + FloppyB = 4, + Tape = 5, + Pipe = 6, + CDRom = 7, + Url = 9, + Unknown = 100 +} -// Constants for backup media device type -export const deviceTypeLogicalDevice = 0; -export const deviceTypeTape = 1; -export const deviceTypeFile = 2; -export const deviceTypeURL = 5; +/** + * Backup media device type: https://docs.microsoft.com/en-us/dotnet/api/microsoft.sqlserver.management.smo.devicetype + */ +export enum MediaDeviceType { + LogicalDevice = 0, + Tape = 1, + File = 2, + Pipe = 3, + VirtualDevice = 4, + Url = 5 +} export const recoveryModelSimple = 'Simple'; export const recoveryModelFull = 'Full'; diff --git a/src/sql/workbench/services/azureAccount/browser/azureAccountService.ts b/src/sql/workbench/services/azureAccount/browser/azureAccountService.ts index 549742f625..31248029a1 100644 --- a/src/sql/workbench/services/azureAccount/browser/azureAccountService.ts +++ b/src/sql/workbench/services/azureAccount/browser/azureAccountService.ts @@ -31,15 +31,34 @@ export class AzureAccountService implements IAzureAccountService { * @param ignoreErrors If true any errors are not thrown and instead collected and returned as part of the result * @param selectedOnly Whether to only list subscriptions the user has selected to filter to for this account */ - public getSubscriptions(account: azurecore.AzureAccount, ignoreErrors?: boolean, selectedOnly?: boolean): Promise { + public async getSubscriptions(account: azurecore.AzureAccount, ignoreErrors?: boolean, selectedOnly?: boolean): Promise { this.checkProxy(); return this._proxy.getSubscriptions(account, ignoreErrors, selectedOnly); } + public async getStorageAccounts(account: azurecore.AzureAccount, subscriptions: azurecore.azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise { + this.checkProxy(); + return this._proxy.getStorageAccounts(account, subscriptions, ignoreErrors); + } + + public async getBlobContainers(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise { + this.checkProxy(); + return this._proxy.getBlobContainers(account, subscription, storageAccount, ignoreErrors); + } + + public async getBlobs(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, containerName: string, ignoreErrors?: boolean): Promise { + this.checkProxy(); + return this._proxy.getBlobs(account, subscription, storageAccount, containerName, ignoreErrors); + } + + public async getStorageAccountAccessKey(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise { + this.checkProxy(); + return this._proxy.getStorageAccountAccessKey(account, subscription, storageAccount, ignoreErrors); + } + private checkProxy(): void { if (!this._proxy) { throw new Error('Azure Account proxy not initialized'); } } } - diff --git a/src/sql/workbench/services/backupRestoreUrlBrowser/browser/media/urlBrowserDialog.css b/src/sql/workbench/services/backupRestoreUrlBrowser/browser/media/urlBrowserDialog.css new file mode 100644 index 0000000000..af951a348c --- /dev/null +++ b/src/sql/workbench/services/backupRestoreUrlBrowser/browser/media/urlBrowserDialog.css @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.url-browser-dialog { + height: 100%; + padding-top: 12px; + padding-left: 12px; + padding-right: 12px; + box-sizing: border-box; +} + +.url-browser-dialog .url-table-content { + width: 100%; +} + +.url-browser-dialog .option-section { + padding-top: 10px; + height: 90px; + box-sizing: border-box; +} + +.url-browser-dialog .url-input-label { + width: 50px; + padding-bottom: 5px; +} + +.url-browser-dialog .url-input-box { + width: 200px; + padding-bottom: 5px; +} diff --git a/src/sql/workbench/services/backupRestoreUrlBrowser/browser/urlBrowserDialog.ts b/src/sql/workbench/services/backupRestoreUrlBrowser/browser/urlBrowserDialog.ts new file mode 100644 index 0000000000..4570c37123 --- /dev/null +++ b/src/sql/workbench/services/backupRestoreUrlBrowser/browser/urlBrowserDialog.ts @@ -0,0 +1,450 @@ +/*--------------------------------------------------------------------------------------------- + * 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/urlBrowserDialog'; +import { Button } from 'sql/base/browser/ui/button/button'; +import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox'; +import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox'; +import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper'; +import { HideReason, Modal } from 'sql/workbench/browser/modal/modal'; +import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { localize } from 'vs/nls'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import * as DOM from 'vs/base/browser/dom'; +import * as strings from 'vs/base/common/strings'; +import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; +import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; +import { attachModalDialogStyler } from 'sql/workbench/common/styler'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { Account } from 'azdata'; +import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces'; +import { IAzureAccountService } from 'sql/platform/azureAccount/common/azureAccountService'; +import { azureResource } from 'azurecore'; +import { IAzureBlobService } from 'sql/platform/azureBlob/common/azureBlobService'; +import { Link } from 'vs/platform/opener/browser/link'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Deferred } from 'sql/base/common/promise'; + +/** + * This function adds one year to the current date and returns it in the UTC format. + * It's used to pass an expiration date argument to the create shared access signature RPC. + * It returns the date in the UTC format for locale time zone independence. + * @returns next year's UTC date + */ +function nextYear(): string { + const today = new Date(); + const nextYear = new Date(today.getFullYear() + 1, today.getMonth(), today.getDate()); + return nextYear.toUTCString(); +} + +export class BackupRestoreUrlBrowserDialog extends Modal { + + private _accounts: Account[]; + private _selectedAccount: Account; + private _subscriptions: azureResource.AzureResourceSubscription[]; + private _selectedSubscription: azureResource.AzureResourceSubscription; + private _storageAccounts: azureResource.AzureGraphResource[]; + private _selectedStorageAccount: azureResource.AzureGraphResource; + private _blobContainers: azureResource.BlobContainer[]; + private _selectedBlobContainer: azureResource.BlobContainer; + private _backupFiles: azureResource.Blob[]; + + private _ownerUri: string; + private _body: HTMLElement; + private _accountSelectorBox: SelectBox; + private _tenantSelectorBox: SelectBox; + private _subscriptionSelectorBox: SelectBox; + private _storageAccountSelectorBox: SelectBox; + private _blobContainerSelectorBox: SelectBox; + private _sasInputBox: InputBox; + private _sasButton: Button; + private _backupFileInputBox: InputBox; + private _backupFileSelectorBox: SelectBox; + private _okButton: Button; + private _cancelButton: Button; + public onOk: Deferred | undefined = new Deferred(); + + + constructor(title: string, + private _restoreDialog: boolean, + private _defaultBackupName: string, + @ILayoutService layoutService: ILayoutService, + @IThemeService themeService: IThemeService, + @IContextViewService private _contextViewService: IContextViewService, + @IAdsTelemetryService telemetryService: IAdsTelemetryService, + @IContextKeyService contextKeyService: IContextKeyService, + @IClipboardService clipboardService: IClipboardService, + @ILogService logService: ILogService, + @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService, + @IAccountManagementService private _accountManagementService: IAccountManagementService, + @IAzureAccountService private _azureAccountService: IAzureAccountService, + @IAzureBlobService private _blobService: IAzureBlobService, + @IInstantiationService private _instantiationService: IInstantiationService + ) { + super(title, TelemetryKeys.ModalDialogName.UrlBrowser, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { dialogStyle: 'flyout', hasTitleIcon: false, hasBackButton: true, hasSpinner: true }); + } + + protected layout(height?: number): void { + } + + protected renderBody(container: HTMLElement) { + this._body = DOM.append(container, DOM.$('.url-browser-dialog')); + } + + public override render() { + super.render(); + attachModalDialogStyler(this, this._themeService); + + if (this.backButton) { + + this._register(this.backButton.onDidClick(() => { + this.close(); + })); + + this._register(attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND })); + } + + let tableContainer: HTMLElement = DOM.append(DOM.append(this._body, DOM.$('.option-section')), DOM.$('table.url-table-content')); + tableContainer.setAttribute('role', 'presentation'); + + let azureAccountLabel = localize('backupRestoreUrlBrowserDialog.account', "Azure Account"); + this._accountSelectorBox = this._register(new SelectBox([''], '', this._contextViewService, null, { ariaLabel: azureAccountLabel })); + this._accountSelectorBox.disable(); + let accountSelector = DialogHelper.appendRow(tableContainer, azureAccountLabel, 'url-input-label', 'url-input-box', null, true); + DialogHelper.appendInputSelectBox(accountSelector, this._accountSelectorBox); + this._accountManagementService.getAccounts().then((accounts) => this.setAccountSelectorBoxOptions(accounts)).catch((err) => { + this.setAccountSelectorBoxOptions([]); + onUnexpectedError(err); + }); + + let linkAccountText = localize('backupRestoreUrlBrowserDialog.linkAccount', "Link account"); + let linkAccountButton = DialogHelper.appendRow(tableContainer, '', 'url-input-label', 'url-input-box'); + const linkAccount: Link = this._register(this._instantiationService.createInstance(Link, + { + label: linkAccountText, + title: linkAccountText, + href: '' + }, + { + opener: async (href: string) => { + await this._accountManagementService.openAccountListDialog(); + this._accountManagementService.getAccounts().then((accounts) => this.setAccountSelectorBoxOptions(accounts)).catch((err) => { + this.setAccountSelectorBoxOptions([]); + onUnexpectedError(err); + }); + } + } + )); + linkAccountButton.appendChild(linkAccount.el); + + let tenantLabel = localize('backupRestoreUrlBrowserDialog.tenant', "Azure AD Tenant"); + this._tenantSelectorBox = this._register(new SelectBox([], '', this._contextViewService, null, { ariaLabel: tenantLabel })); + this._tenantSelectorBox.disable(); + let tenantSelector = DialogHelper.appendRow(tableContainer, tenantLabel, 'url-input-label', 'url-input-box', null, true); + DialogHelper.appendInputSelectBox(tenantSelector, this._tenantSelectorBox); + + let subscriptionLabel = localize('backupRestoreUrlBrowserDialog.subscription', "Azure subscription"); + this._subscriptionSelectorBox = this._register(new SelectBox([], '', this._contextViewService, null, { ariaLabel: subscriptionLabel })); + this._subscriptionSelectorBox.disable(); + let subscriptionSelector = DialogHelper.appendRow(tableContainer, subscriptionLabel, 'url-input-label', 'url-input-box', null, true); + DialogHelper.appendInputSelectBox(subscriptionSelector, this._subscriptionSelectorBox); + + let storageAccountLabel = localize('backupRestoreUrlBrowserDialog.storageAccount', "Storage account"); + this._storageAccountSelectorBox = this._register(new SelectBox([], '', this._contextViewService, null, { ariaLabel: storageAccountLabel })); + this._storageAccountSelectorBox.disable(); + let storageAccountSelector = DialogHelper.appendRow(tableContainer, storageAccountLabel, 'url-input-label', 'url-input-box', null, true); + DialogHelper.appendInputSelectBox(storageAccountSelector, this._storageAccountSelectorBox); + + let blobContainerLabel = localize('backupRestoreUrlBrowserDialog.blobContainer', "Blob container"); + this._blobContainerSelectorBox = this._register(new SelectBox([], '', this._contextViewService, null, { ariaLabel: blobContainerLabel })); + this._blobContainerSelectorBox.disable(); + let blobContainerSelector = DialogHelper.appendRow(tableContainer, blobContainerLabel, 'url-input-label', 'url-input-box', null, true); + DialogHelper.appendInputSelectBox(blobContainerSelector, this._blobContainerSelectorBox); + + + let sharedAccessSignatureLabel = localize('backupRestoreUrlBrowserDialog.sharedAccessSignature', "Shared access signature generated"); + let sasInput = DialogHelper.appendRow(tableContainer, sharedAccessSignatureLabel, 'url-input-label', 'url-input-box', null, true); + this._sasInputBox = this._register(new InputBox(sasInput, this._contextViewService, { flexibleHeight: true })); + this._sasInputBox.disable(); + this._register(this._sasInputBox.onDidChange(() => this.enableOkButton())); + + let sasButtonContainer = DialogHelper.appendRow(tableContainer, '', 'url-input-label', 'url-input-box'); + let sasButtonLabel = localize('backupRestoreUrlBrowserDialog.sharedAccessSignatureButton', "Create Credentials"); + this._sasButton = this._register(new Button(sasButtonContainer, { title: sasButtonLabel })); + this._sasButton.label = sasButtonLabel; + this._sasButton.title = sasButtonLabel; + this._register(this._sasButton.onDidClick(e => this.generateSharedAccessSignature())); + + let backupFileLabel = localize('backupRestoreUrlBrowserDialog.backupFile', "Backup file"); + + if (this._restoreDialog) { + this._backupFileSelectorBox = this._register(new SelectBox([], '', this._contextViewService, null, { ariaLabel: backupFileLabel })); + let backupFileSelector = DialogHelper.appendRow(tableContainer, backupFileLabel, 'url-input-label', 'url-input-box', null, true); + DialogHelper.appendInputSelectBox(backupFileSelector, this._backupFileSelectorBox); + this._backupFileSelectorBox.setOptions([]); + this._backupFileSelectorBox.disable(); + } else { + let fileInput = DialogHelper.appendRow(tableContainer, backupFileLabel, 'url-input-label', 'url-input-box', null, true); + this._backupFileInputBox = this._register(new InputBox(fileInput, this._contextViewService, { flexibleHeight: true })); + this._backupFileInputBox.value = this._defaultBackupName; + } + + this._okButton = this.addFooterButton(localize('fileBrowser.ok', "OK"), () => this.ok()); + this._okButton.enabled = false; + this._cancelButton = this.addFooterButton(localize('fileBrowser.discard', "Discard"), () => this.close(), 'right', true); + + this.registerListeners(); + this.registerThemeStylers(); + } + + private setAccountSelectorBoxOptions(accounts: Account[]) { + this._accounts = accounts.filter(account => !account.isStale); + const accountDisplayNames: string[] = this._accounts.map(account => account.displayInfo.displayName); + this._accountSelectorBox.setOptions(accountDisplayNames); + this._accountSelectorBox.select(0); + if (this._accounts.length === 0) { + this._accountSelectorBox.disable(); + this.onTenantSelectorBoxChanged(0); + } else { + this._accountSelectorBox.enable(); + } + } + + private onAccountSelectorBoxChanged(checkedAccount: number) { + if (this._accounts.length !== 0) { + this._selectedAccount = this._accounts[checkedAccount]; + const tenants = this._selectedAccount.properties.tenants; + const tenantsDisplayNames = tenants.map(tenant => tenant.displayName); + this._tenantSelectorBox.setOptions(tenantsDisplayNames); + this._tenantSelectorBox.select(0); + if (tenantsDisplayNames.length === 0) { + this._tenantSelectorBox.disable(); + } else { + this._tenantSelectorBox.enable(); + } + } else { + this._tenantSelectorBox.setOptions([]); + this._tenantSelectorBox.select(0); + this._tenantSelectorBox.disable(); + } + } + + private onTenantSelectorBoxChanged(checkedTenant: number) { + if (this._accounts.length !== 0) { + this._azureAccountService.getSubscriptions(this._selectedAccount) + .then(getSubscriptionResult => this.setSubscriptionsSelectorBoxOptions(getSubscriptionResult.subscriptions)) + .catch(getSubscriptionResult => { + this.setSubscriptionsSelectorBoxOptions([]); + onUnexpectedError(getSubscriptionResult.errors); + }); + } else { + this._tenantSelectorBox.setOptions([]); + this._tenantSelectorBox.disable(); + this.setSubscriptionsSelectorBoxOptions([]); + } + } + + private setSubscriptionsSelectorBoxOptions(subscriptions: azureResource.AzureResourceSubscription[]) { + this._subscriptions = subscriptions; + const subscriptionDisplayNames: string[] = subscriptions.map(subscription => subscription.name); + this._subscriptionSelectorBox.setOptions(subscriptionDisplayNames); + this._subscriptionSelectorBox.select(0); + if (this._subscriptions.length === 0) { + this._subscriptionSelectorBox.disable(); + } else { + this._subscriptionSelectorBox.enable(); + } + } + + private onSubscriptionSelectorBoxChanged(checkedSubscription: number) { + if (this._subscriptions.length !== 0) { + this._selectedSubscription = this._subscriptions[checkedSubscription]; + this._azureAccountService.getStorageAccounts(this._selectedAccount, [this._selectedSubscription]) + .then(getStorageAccountsResult => this.setStorageAccountSelectorBoxOptions(getStorageAccountsResult.resources)) + .catch(getStorageAccountsResult => { + this.setStorageAccountSelectorBoxOptions([]); + onUnexpectedError(getStorageAccountsResult.errors); + }); + } else { + this.setStorageAccountSelectorBoxOptions([]); + } + } + + private setStorageAccountSelectorBoxOptions(storageAccounts: azureResource.AzureGraphResource[]) { + this._storageAccounts = storageAccounts; + const storageAccountDisplayNames: string[] = this._storageAccounts.map(storageAccount => storageAccount.name); + this._storageAccountSelectorBox.setOptions(storageAccountDisplayNames); + this._storageAccountSelectorBox.select(0); + if (storageAccounts.length === 0) { + this._storageAccountSelectorBox.disable(); + } else { + this._storageAccountSelectorBox.enable(); + } + } + + private onStorageAccountSelectorBoxChanged(checkedStorageAccount: number) { + if (this._storageAccounts.length !== 0) { + this._selectedStorageAccount = this._storageAccounts[checkedStorageAccount]; + this._azureAccountService.getBlobContainers(this._selectedAccount, this._selectedSubscription, this._selectedStorageAccount) + .then(getBlobContainersResult => this.setBlobContainersSelectorBoxOptions(getBlobContainersResult.blobContainers)) + .catch(getBlobContainersResult => { + this.setBlobContainersSelectorBoxOptions([]); + onUnexpectedError(getBlobContainersResult.errors); + }); + } else { + this.setBlobContainersSelectorBoxOptions([]); + } + } + + private setBlobContainersSelectorBoxOptions(blobContainers: azureResource.BlobContainer[]) { + this._blobContainers = blobContainers; + const blobContainersDisplayNames: string[] = this._blobContainers.map(blobContainer => blobContainer.name); + this._blobContainerSelectorBox.setOptions(blobContainersDisplayNames); + this._blobContainerSelectorBox.select(0); + if (this._blobContainers.length === 0) { + this._blobContainerSelectorBox.disable(); + } else { + this._blobContainerSelectorBox.enable(); + } + } + + private onBlobContainersSelectorBoxChanged(checkedBlobContainer: number) { + this._sasInputBox.value = ''; + if (this._restoreDialog) { + if (this._blobContainers.length !== 0) { + this._selectedBlobContainer = this._blobContainers[checkedBlobContainer]; + this._azureAccountService.getBlobs(this._selectedAccount, this._selectedSubscription, this._selectedStorageAccount, this._selectedBlobContainer.name, true) + .then(getBlobsResult => this.setBackupFilesOptions(getBlobsResult.blobs)) + .catch(getBlobsResult => { + this.setBackupFilesOptions([]); + onUnexpectedError(getBlobsResult.errors); + }); + } else { + this.setBackupFilesOptions([]); + } + } + this.enableCreateCredentialsButton(); + } + + private setBackupFilesOptions(blobs: azureResource.Blob[]) { + this._backupFiles = blobs; + const backupFilesDisplayNames: string[] = this._backupFiles.map(backupFile => backupFile.name); + this._backupFileSelectorBox.setOptions(backupFilesDisplayNames); + this._backupFileSelectorBox.select(0); + if (this._backupFiles.length === 0) { + this._backupFileSelectorBox.disable(); + } else { + this._backupFileSelectorBox.enable(); + } + } + + public open(ownerUri: string, + expandPath: string, + fileFilters: [{ label: string, filters: string[] }], + fileValidationServiceType: string, + ): void { + this._ownerUri = ownerUri; + this.enableOkButton(); + this.enableCreateCredentialsButton(); + this.show(); + } + + /* enter key */ + protected override onAccept() { + let selectedValue = this._sasInputBox.value; + if (this._okButton.enabled === true && selectedValue !== '') { + this.ok(); + } + } + + + private enableOkButton() { + if (strings.isFalsyOrWhitespace(this._blobContainerSelectorBox.value) || strings.isFalsyOrWhitespace(this._sasInputBox.value) || (this._restoreDialog && strings.isFalsyOrWhitespace(this._blobContainerSelectorBox.value)) || (!this._restoreDialog && strings.isFalsyOrWhitespace(this._backupFileInputBox.value))) { + this._okButton.enabled = false; + } else { + this._okButton.enabled = true; + } + } + + private enableCreateCredentialsButton() { + if (strings.isFalsyOrWhitespace(this._blobContainerSelectorBox.label)) { + this._sasButton.enabled = false; + } else { + this._sasButton.enabled = true; + } + } + + private ok() { + let returnValue = ''; + if (this._restoreDialog) { + returnValue = `https://${this._storageAccountSelectorBox.value}.blob${this._selectedAccount.properties.providerSettings.settings.azureStorageResource.endpointSuffix}/${this._blobContainerSelectorBox.value}/${this._backupFileSelectorBox.value}`; + } else { + returnValue = `https://${this._storageAccountSelectorBox.value}.blob${this._selectedAccount.properties.providerSettings.settings.azureStorageResource.endpointSuffix}/${this._blobContainerSelectorBox.value}/${this._backupFileInputBox.value}`; + } + this.onOk.resolve(returnValue); + this.close('ok'); + } + + + private close(hideReason: HideReason = 'close'): void { + this.hide(hideReason); + } + + private async generateSharedAccessSignature() { + this.spinner = true; + const blobContainerUri = `https://${this._storageAccountSelectorBox.value}.blob${this._selectedAccount.properties.providerSettings.settings.azureStorageResource.endpointSuffix}/${this._blobContainerSelectorBox.value}`; + const getStorageAccountAccessKeyResult = await this._azureAccountService.getStorageAccountAccessKey(this._selectedAccount, this._selectedSubscription, this._selectedStorageAccount); + const key1 = getStorageAccountAccessKeyResult.keyName1; + const createSasResult = await this._blobService.createSas(this._ownerUri, blobContainerUri, key1, this._selectedStorageAccount.name, nextYear()); + const sas = createSasResult.sharedAccessSignature; + this._sasInputBox.value = sas; + this.spinner = false; + } + + private registerListeners(): void { + this._register(this._accountSelectorBox.onDidSelect(e => this.onAccountSelectorBoxChanged(e.index))); + this._register(this._tenantSelectorBox.onDidSelect(selectedTenant => this.onTenantSelectorBoxChanged(selectedTenant.index))); + this._register(this._subscriptionSelectorBox.onDidSelect(selectedSubscription => this.onSubscriptionSelectorBoxChanged(selectedSubscription.index))); + this._register(this._storageAccountSelectorBox.onDidSelect(selectedStorageAccount => this.onStorageAccountSelectorBoxChanged(selectedStorageAccount.index))); + this._register(this._blobContainerSelectorBox.onDidSelect(selectedBlobContainer => { + this.onBlobContainersSelectorBoxChanged(selectedBlobContainer.index); + this.enableOkButton(); + })); + + if (this._backupFileInputBox) { + this._register(this._backupFileInputBox.onDidChange(e => this.enableOkButton())); + } + if (this._backupFileSelectorBox) { + this._register(this._backupFileSelectorBox.onDidSelect(e => this.enableOkButton())); + } + } + + + private registerThemeStylers(): void { + this._register(attachSelectBoxStyler(this._tenantSelectorBox, this._themeService)); + this._register(attachSelectBoxStyler(this._accountSelectorBox, this._themeService)); + this._register(attachSelectBoxStyler(this._subscriptionSelectorBox, this._themeService)); + this._register(attachSelectBoxStyler(this._storageAccountSelectorBox, this._themeService)); + this._register(attachSelectBoxStyler(this._blobContainerSelectorBox, this._themeService)); + this._register(attachInputBoxStyler(this._sasInputBox, this._themeService)); + + if (this._backupFileInputBox) { + this._register(attachInputBoxStyler(this._backupFileInputBox, this._themeService)); + } + if (this._backupFileSelectorBox) { + this._register(attachSelectBoxStyler(this._backupFileSelectorBox, this._themeService)); + } + this._register(attachButtonStyler(this._sasButton, this._themeService)); + this._register(attachButtonStyler(this._okButton, this._themeService)); + this._register(attachButtonStyler(this._cancelButton, this._themeService)); + } +} diff --git a/src/sql/workbench/services/backupRestoreUrlBrowser/browser/urlBrowserDialogService.ts b/src/sql/workbench/services/backupRestoreUrlBrowser/browser/urlBrowserDialogService.ts new file mode 100644 index 0000000000..54dfe3d70c --- /dev/null +++ b/src/sql/workbench/services/backupRestoreUrlBrowser/browser/urlBrowserDialogService.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BackupRestoreUrlBrowserDialog } from 'sql/workbench/services/backupRestoreUrlBrowser/browser/urlBrowserDialog'; +import { IBackupRestoreUrlBrowserDialogService } from 'sql/workbench/services/backupRestoreUrlBrowser/common/urlBrowserDialogService'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +/** + * Url browser dialog service + */ +export class BackupRestoreUrlBrowserDialogService implements IBackupRestoreUrlBrowserDialogService { + _serviceBrand: undefined; + + constructor( + @IInstantiationService private _instantiationService: IInstantiationService + ) { + } + + public showDialog(ownerUri: string, + expandPath: string, + fileFilters: [{ label: string, filters: string[] }], + fileValidationServiceType: string, + isWide: boolean, + isRestoreDialog: boolean, + defaultBackupName: string + ): Promise { + const backupRestoreUrlBrowserDialog = this._instantiationService.createInstance(BackupRestoreUrlBrowserDialog, localize('filebrowser.selectBlob', "Select a blob"), isRestoreDialog, defaultBackupName); + backupRestoreUrlBrowserDialog.render(); + + backupRestoreUrlBrowserDialog.setWide(isWide); + backupRestoreUrlBrowserDialog.open(ownerUri, expandPath, fileFilters, fileValidationServiceType); + return backupRestoreUrlBrowserDialog.onOk; + } +} diff --git a/src/sql/workbench/services/backupRestoreUrlBrowser/common/urlBrowserDialogService.ts b/src/sql/workbench/services/backupRestoreUrlBrowser/common/urlBrowserDialogService.ts new file mode 100644 index 0000000000..c208a3dc25 --- /dev/null +++ b/src/sql/workbench/services/backupRestoreUrlBrowser/common/urlBrowserDialogService.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const IBackupRestoreUrlBrowserDialogService = createDecorator('backupRestoreUrlBrowserDialogService'); +export interface IBackupRestoreUrlBrowserDialogService { + _serviceBrand: undefined; + /** + * Show url browser dialog + */ + showDialog(ownerUri: string, + expandPath: string, + fileFilters: { label: string, filters: string[] }[], + fileValidationServiceType: string, + isWide: boolean, + isRestoreDialog: boolean, + defaultBackupName: string): Promise; +} diff --git a/src/sql/workbench/services/restore/browser/restoreDialog.ts b/src/sql/workbench/services/restore/browser/restoreDialog.ts index 47b29dabdf..467e9251d0 100644 --- a/src/sql/workbench/services/restore/browser/restoreDialog.ts +++ b/src/sql/workbench/services/restore/browser/restoreDialog.ts @@ -34,8 +34,8 @@ import { attachTableStyler, attachInputBoxStyler, attachSelectBoxStyler, attachE import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { RestoreViewModel, RestoreOptionParam, SouceDatabaseNamesParam } from 'sql/workbench/services/restore/browser/restoreViewModel'; import * as FileValidationConstants from 'sql/workbench/services/fileBrowser/common/fileValidationServiceConstants'; -import { TabbedPanel, PanelTabIdentifier } from 'sql/base/browser/ui/panel/panel'; -import { ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { IPanelTab, TabbedPanel } from 'sql/base/browser/ui/panel/panel'; +import { DatabaseEngineEdition, ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes'; import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; import { IFileBrowserDialogController } from 'sql/workbench/services/fileBrowser/common/fileBrowserDialogController'; import { ILogService } from 'vs/platform/log/common/log'; @@ -46,6 +46,8 @@ import { fileFiltersSet } from 'sql/workbench/services/restore/common/constants' import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { Dropdown } from 'sql/base/browser/ui/editableDropdown/browser/dropdown'; +import { IBackupRestoreUrlBrowserDialogService } from 'sql/workbench/services/backupRestoreUrlBrowser/common/urlBrowserDialogService'; +import { MediaDeviceType } from 'sql/workbench/contrib/backup/common/constants'; interface FileListElement { logicalFileName: string; @@ -55,6 +57,7 @@ interface FileListElement { } const LocalizedStrings = { + BACKURL: localize('backupUrl', "Backup URL"), BACKFILEPATH: localize('backupFilePath', "Backup file path"), TARGETDATABASE: localize('targetDatabase', "Target database") }; @@ -70,19 +73,27 @@ export class RestoreDialog extends Modal { private _restoreTitle = localize('restoreDialog.restoreTitle', "Restore database"); private _databaseTitle = localize('restoreDialog.database', "Database"); private _backupFileTitle = localize('restoreDialog.backupFile', "Backup file"); + private _urlTitle = localize('restoreDialog.url', "URL"); private _ownerUri?: string; private _databaseDropdown?: Dropdown; private _isBackupFileCheckboxChanged?: boolean; // General options private _filePathInputBox?: InputBox; + private _urlInputBox?: InputBox; + private _browseUrlButton?: Button; private _browseFileButton?: Button; private _destinationRestoreToInputBox?: InputBox; private _restoreFromSelectBox?: SelectBox; private _sourceDatabaseSelectBox?: SelectBox; + public _targetDatabaseInputBox: InputBox; private _panel?: TabbedPanel; - private _generalTabId?: PanelTabIdentifier; + private _generalTab?: IPanelTab; + private _fileTab?: IPanelTab; + private _optionsTab?: IPanelTab; + + private _engineEdition?: DatabaseEngineEdition; // File option private readonly _relocateDatabaseFilesOption = 'relocateDbFiles'; @@ -104,6 +115,11 @@ export class RestoreDialog extends Modal { private readonly _closeExistingConnectionsOption = 'closeExistingConnections'; private _restoreFromBackupFileElement?: HTMLElement; + private _restoreFromUrlElement?: HTMLElement; + private _destinationRestoreToContainer?: HTMLElement; + private _sourceDatabasesElement?: HTMLElement; + private _targetDatabaseElement?: HTMLElement; + private _targetDatabaseInputElement?: HTMLElement; private _fileListTable?: Table; private _fileListData?: TableDataView; @@ -138,12 +154,12 @@ export class RestoreDialog extends Modal { @IAdsTelemetryService telemetryService: IAdsTelemetryService, @IContextKeyService contextKeyService: IContextKeyService, @IFileBrowserDialogController private fileBrowserDialogService: IFileBrowserDialogController, + @IBackupRestoreUrlBrowserDialogService private backupRestoreUrlBrowserDialogService: IBackupRestoreUrlBrowserDialogService, @IClipboardService clipboardService: IClipboardService, @ILogService logService: ILogService, @ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService ) { super(localize('RestoreDialogTitle', "Restore database"), TelemetryKeys.ModalDialogName.Restore, telemetryService, layoutService, clipboardService, themeService, logService, textResourcePropertiesService, contextKeyService, { hasErrors: true, width: 'wide', hasSpinner: true }); - // view model this.viewModel = new RestoreViewModel(optionsMetadata); this.viewModel.onSetLastBackupTaken((value) => this.updateLastBackupTaken(value)); @@ -170,7 +186,31 @@ export class RestoreDialog extends Modal { protected renderBody(container: HTMLElement) { const restoreFromElement = DOM.$('.restore-from'); this.createLabelElement(restoreFromElement, localize('source', "Source"), true); - this._restoreFromSelectBox = this.createSelectBoxHelper(restoreFromElement, localize('restoreFrom', "Restore from"), [this._databaseTitle, this._backupFileTitle], this._databaseTitle); + this._restoreFromSelectBox = this.createSelectBoxHelper(restoreFromElement, localize('restoreFrom', "Restore from"), [this._databaseTitle, this._backupFileTitle, this._urlTitle], this._databaseTitle); + + this._restoreFromUrlElement = DOM.$('.backup-url'); + DOM.hide(this._restoreFromUrlElement); + const urlErrorMessage = localize('missingBackupUrlError', "Backup url is required."); + const urlValidationOptions: IInputOptions = { + validationOptions: { + validation: (value: string) => !value ? ({ type: MessageType.ERROR, content: urlErrorMessage }) : null + }, + placeholder: localize('enterBackupUrl', "Please enter URL"), + ariaLabel: LocalizedStrings.BACKURL + }; + const urlInputContainer = DOM.append(this._restoreFromUrlElement, DOM.$('.dialog-input-section')); + DOM.append(urlInputContainer, DOM.$('.dialog-label')).innerText = LocalizedStrings.BACKURL; + + this._urlInputBox = this._register(new InputBox(DOM.append(urlInputContainer, DOM.$('.dialog-input')), this._contextViewService, urlValidationOptions)); + + const urlBrowseContainer = DOM.append(this._restoreFromUrlElement, DOM.$('.dialog-input-section')); + DOM.append(urlBrowseContainer, DOM.$('.dialog-label')).innerText = ''; + + let browseLabel = localize('restoreDialog.browse', "Browse"); + this._browseUrlButton = this._register(new Button(DOM.append(urlBrowseContainer, DOM.$('.file-browser')), { secondary: true })); + this._browseUrlButton.label = browseLabel; + this._browseUrlButton.setWidth('50px'); + this._restoreFromBackupFileElement = DOM.$('.backup-file-path'); DOM.hide(this._restoreFromBackupFileElement); @@ -185,38 +225,40 @@ export class RestoreDialog extends Modal { const filePathInputContainer = DOM.append(this._restoreFromBackupFileElement, DOM.$('.dialog-input-section')); DOM.append(filePathInputContainer, DOM.$('.dialog-label')).innerText = LocalizedStrings.BACKFILEPATH; - this._filePathInputBox = new InputBox(DOM.append(filePathInputContainer, DOM.$('.dialog-input')), this._contextViewService, validationOptions); + this._filePathInputBox = this._register(new InputBox(DOM.append(filePathInputContainer, DOM.$('.dialog-input')), this._contextViewService, validationOptions)); - this._browseFileButton = new Button(DOM.append(filePathInputContainer, DOM.$('.file-browser')), { secondary: true }); + this._browseFileButton = this._register(new Button(DOM.append(filePathInputContainer, DOM.$('.file-browser')), { secondary: true })); this._browseFileButton.label = '...'; - const sourceDatabasesElement = DOM.$('.source-database-list'); - this._sourceDatabaseSelectBox = this.createSelectBoxHelper(sourceDatabasesElement, localize('database', "Database"), [], ''); + this._sourceDatabasesElement = DOM.$('.source-database-list'); + this._sourceDatabaseSelectBox = this.createSelectBoxHelper(this._sourceDatabasesElement, localize('database', "Database"), [], ''); // Source section const sourceElement = DOM.$('.source-section.new-section'); sourceElement.append(restoreFromElement); + sourceElement.append(this._restoreFromUrlElement); sourceElement.append(this._restoreFromBackupFileElement); - sourceElement.append(sourceDatabasesElement); + sourceElement.append(this._sourceDatabasesElement); // Destination section const destinationElement = DOM.$('.destination-section.new-section'); this.createLabelElement(destinationElement, localize('destination', "Destination"), true); - const destinationInputContainer = DOM.append(destinationElement, DOM.$('.dialog-input-section')); - DOM.append(destinationInputContainer, DOM.$('.dialog-label')).innerText = LocalizedStrings.TARGETDATABASE; + this._targetDatabaseElement = DOM.append(destinationElement, DOM.$('.dialog-input-section')); + DOM.append(this._targetDatabaseElement, DOM.$('.dialog-label')).innerText = LocalizedStrings.TARGETDATABASE; - const dropdownContainer = DOM.append(destinationInputContainer, DOM.$('.dialog-input')); + + const dropdownContainer = DOM.append(this._targetDatabaseElement, DOM.$('.dialog-input')); // Get the bootstrap params and perform the bootstrap dropdownContainer.style.width = '100%'; - this._databaseDropdown = new Dropdown(dropdownContainer, this._contextViewService, + this._databaseDropdown = this._register(new Dropdown(dropdownContainer, this._contextViewService, { strictSelection: false, ariaLabel: LocalizedStrings.TARGETDATABASE } - ); + )); this._databaseDropdown.onValueChange(s => { this.databaseSelected(s); }); @@ -232,7 +274,30 @@ export class RestoreDialog extends Modal { this._databaseDropdown.value = this.viewModel.targetDatabaseName!; attachEditableDropdownStyler(this._databaseDropdown, this._themeService); - this._destinationRestoreToInputBox = this.createInputBoxHelper(destinationElement, localize('restoreTo', "Restore to")); + this._targetDatabaseInputElement = DOM.append(destinationElement, DOM.$('.dialog-input-section')); + DOM.append(this._targetDatabaseInputElement, DOM.$('.dialog-label')).innerText = LocalizedStrings.TARGETDATABASE; + DOM.hide(this._targetDatabaseInputElement); + + const inputTargetDatabaseContainer = DOM.append(this._targetDatabaseInputElement, DOM.$('.dialog-input')); + + // Get the bootstrap params and perform the bootstrap + inputTargetDatabaseContainer.style.width = '100%'; + + this._targetDatabaseInputBox = this._register(new InputBox(inputTargetDatabaseContainer, this._contextViewService, { + ariaLabel: LocalizedStrings.TARGETDATABASE, + placeholder: localize('targetDatabaseTooltip', "Please enter target database name"), + validationOptions: { + validation: (value: string) => this.viewModel.databases.includes(value) ? ({ type: MessageType.ERROR, content: localize('restoreDialog.targetDatabaseAlreadyExists', "Target database already exists") }) : null + }, + })); + + const restoreToLabel = localize('restoreTo', "Restore to"); + const destinationRestoreToAriaOptions = { + ariaLabel: restoreToLabel + }; + this._destinationRestoreToContainer = DOM.append(destinationElement, DOM.$('.dialog-input-section')); + DOM.append(this._destinationRestoreToContainer, DOM.$('.dialog-label')).innerText = restoreToLabel; + this._destinationRestoreToInputBox = this._register(new InputBox(DOM.append(this._destinationRestoreToContainer, DOM.$('.dialog-input')), this._contextViewService, mixin(destinationRestoreToAriaOptions, null))); // Restore plan section const restorePlanElement = DOM.$('.restore-plan-section.new-section'); @@ -243,8 +308,8 @@ export class RestoreDialog extends Modal { this._restorePlanTableContainer = DOM.append(restorePlanElement, DOM.$('.dialog-input-section.restore-list')); DOM.hide(this._restorePlanTableContainer); this._restorePlanData = new TableDataView(); - this._restorePlanTable = new Table(this._restorePlanTableContainer, - { dataProvider: this._restorePlanData, columns: this._restorePlanColumn }, { enableColumnReorder: false }); + this._restorePlanTable = this._register(new Table(this._restorePlanTableContainer, + { dataProvider: this._restorePlanData, columns: this._restorePlanColumn }, { enableColumnReorder: false })); this._restorePlanTable.setTableTitle(localize('restorePlan', "Restore plan")); this._restorePlanTable.setSelectionModel(new RowSelectionModel({ selectActiveRow: false })); this._restorePlanTable.onSelectedRowsChanged((e, data) => this.backupFileCheckboxChanged(e, data)); @@ -294,8 +359,8 @@ export class RestoreDialog extends Modal { field: 'restoreAs' }]; this._fileListData = new TableDataView(); - this._fileListTable = new Table(this._fileListTableContainer, - { dataProvider: this._fileListData, columns }, { enableColumnReorder: false }); + this._fileListTable = this._register(new Table(this._fileListTableContainer, + { dataProvider: this._fileListData, columns }, { enableColumnReorder: false })); this._fileListTable.setSelectionModel(new RowSelectionModel()); // Content in options tab @@ -327,7 +392,7 @@ export class RestoreDialog extends Modal { container.appendChild(restorePanel); this._panel = new TabbedPanel(restorePanel); attachTabbedPanelStyler(this._panel, this._themeService); - this._generalTabId = this._panel.pushTab({ + this._generalTab = { identifier: 'general', title: localize('generalTitle', "General"), view: { @@ -336,9 +401,10 @@ export class RestoreDialog extends Modal { }, layout: () => { } } - }); + }; + this._panel.pushTab(this._generalTab); - const fileTab = this._panel.pushTab({ + this._fileTab = { identifier: 'fileContent', title: localize('filesTitle', "Files"), view: { @@ -347,9 +413,10 @@ export class RestoreDialog extends Modal { c.appendChild(fileContentElement); } } - }); + }; + this._panel.pushTab(this._fileTab); - this._panel.pushTab({ + this._optionsTab = { identifier: 'options', title: localize('optionsTitle', "Options"), view: { @@ -358,17 +425,18 @@ export class RestoreDialog extends Modal { c.appendChild(optionsContentElement); } } - }); + }; + this._panel.pushTab(this._optionsTab); - this._panel.onTabChange(c => { - if (c === fileTab && this._fileListTable) { + this._register(this._panel.onTabChange(c => { + if (c === this._fileTab.identifier && this._fileListTable) { this._fileListTable.resizeCanvas(); this._fileListTable.autosizeColumns(); } - if (c !== this._generalTabId) { + if (c !== this._generalTab.identifier) { this._restoreFromSelectBox!.hideMessage(); } - }); + })); this._restorePlanTable.grid.onKeyDown.subscribe(e => { let event = new StandardKeyboardEvent(e as KeyboardEvent); @@ -399,7 +467,7 @@ export class RestoreDialog extends Modal { }); } - private focusOnFirstEnabledFooterButton() { + private focusOnFirstEnabledFooterButton(): void { if (this._scriptButton!.enabled) { this._scriptButton!.focus(); } else if (this._restoreButton!.enabled) { @@ -418,9 +486,10 @@ export class RestoreDialog extends Modal { public set databaseListOptions(vals: string[]) { this._databaseDropdown!.values = vals; + this.viewModel.databases = vals; } - private createLabelElement(container: HTMLElement, content: string, isHeader?: boolean) { + private createLabelElement(container: HTMLElement, content: string, isHeader?: boolean): void { let className = 'dialog-label'; if (isHeader) { className += ' header'; @@ -454,17 +523,17 @@ export class RestoreDialog extends Modal { this._optionsMap[optionName] = propertyWidget!; } - private onBooleanOptionChecked(optionName: string) { + private onBooleanOptionChecked(optionName: string): void { this.viewModel.setOptionValue(optionName, (this._optionsMap[optionName]).checked); this.validateRestore(false); } - private onCatagoryOptionChanged(optionName: string) { + private onCatagoryOptionChanged(optionName: string): void { this.viewModel.setOptionValue(optionName, (this._optionsMap[optionName]).value); this.validateRestore(false); } - private onStringOptionChanged(optionName: string, params: OnLoseFocusParams) { + private onStringOptionChanged(optionName: string, params: OnLoseFocusParams): void { if (params.hasChanged && params.value) { this.viewModel.setOptionValue(optionName, params.value); this.validateRestore(false); @@ -472,12 +541,12 @@ export class RestoreDialog extends Modal { } private createCheckBoxHelper(container: HTMLElement, label: string, isChecked: boolean, onCheck: (viaKeyboard: boolean) => void): Checkbox { - const checkbox = new Checkbox(DOM.append(container, DOM.$('.dialog-input-section')), { + const checkbox = this._register(new Checkbox(DOM.append(container, DOM.$('.dialog-input-section')), { label: label, checked: isChecked, onChange: onCheck, ariaLabel: label - }); + })); this._register(attachCheckboxStyler(checkbox, this._themeService)); return checkbox; } @@ -486,7 +555,7 @@ export class RestoreDialog extends Modal { const inputContainer = DOM.append(container, DOM.$('.dialog-input-section')); DOM.append(inputContainer, DOM.$('.dialog-label')).innerText = label; const inputCellContainer = DOM.append(inputContainer, DOM.$('.dialog-input')); - const selectBox = new SelectBox(options, selectedOption, this._contextViewService, inputCellContainer, { ariaLabel: label }); + const selectBox = this._register(new SelectBox(options, selectedOption, this._contextViewService, inputCellContainer, { ariaLabel: label })); selectBox.render(inputCellContainer); return selectBox; } @@ -497,7 +566,7 @@ export class RestoreDialog extends Modal { }; const inputContainer = DOM.append(container, DOM.$('.dialog-input-section')); DOM.append(inputContainer, DOM.$('.dialog-label')).innerText = label; - return new InputBox(DOM.append(inputContainer, DOM.$('.dialog-input')), this._contextViewService, mixin(ariaOptions, options)); + return this._register(new InputBox(DOM.append(inputContainer, DOM.$('.dialog-input')), this._contextViewService, mixin(ariaOptions, options))); } private clearRestorePlanDataTable(): void { @@ -521,7 +590,7 @@ export class RestoreDialog extends Modal { this._scriptButton!.enabled = false; } - public onValidateResponseFail(errorMessage: string) { + public onValidateResponseFail(errorMessage: string): void { this.resetRestoreContent(); if (this.isRestoreFromDatabaseSelected) { this._sourceDatabaseSelectBox!.showMessage({ type: MessageType.ERROR, content: errorMessage }); @@ -531,16 +600,22 @@ export class RestoreDialog extends Modal { } } - public removeErrorMessage() { + public removeErrorMessage(): void { this._filePathInputBox!.hideMessage(); this._sourceDatabaseSelectBox!.hideMessage(); this._destinationRestoreToInputBox!.hideMessage(); } - public enableRestoreButton(enabled: boolean) { + public enableRestoreButton(enabled: boolean): void { this.spinner = false; - this._restoreButton!.enabled = enabled; - this._scriptButton!.enabled = enabled; + if (this._engineEdition === DatabaseEngineEdition.SqlManagedInstance && this.viewModel.databases.includes(this._targetDatabaseInputBox.value)) { + this._restoreButton!.enabled = false; + this._scriptButton!.enabled = false; + } + else { + this._restoreButton!.enabled = enabled; + this._scriptButton!.enabled = enabled; + } } public showError(errorMessage: string): void { @@ -549,10 +624,20 @@ export class RestoreDialog extends Modal { private backupFileCheckboxChanged(e: Slick.EventData, data: Slick.OnSelectedRowsChangedEventArgs): void { let selectedFiles: string[] = []; + let selectedDatabases: string[] = []; data.grid.getSelectedRows().forEach(row => { selectedFiles.push(data.grid.getDataItem(row)['Id']); + selectedDatabases.push(data.grid.getDataItem(row)['Database']); }); + if (selectedDatabases.length !== 0) { + if (this._targetDatabaseInputBox.value === '') { + this._targetDatabaseInputBox.value = selectedDatabases[0]; + } + } else { + this._targetDatabaseInputBox.value = ''; + } + let isSame = false; if (this.viewModel.selectedBackupSets && this.viewModel.selectedBackupSets.length === selectedFiles.length) { isSame = this.viewModel.selectedBackupSets.some(item => selectedFiles.some(x => x === item)); @@ -566,24 +651,45 @@ export class RestoreDialog extends Modal { private registerListeners(): void { // Theme styler + this._register(attachInputBoxStyler(this._targetDatabaseInputBox, this._themeService)); + this._register(attachInputBoxStyler(this._urlInputBox!, this._themeService)); this._register(attachInputBoxStyler(this._filePathInputBox!, this._themeService)); this._register(attachInputBoxStyler(this._destinationRestoreToInputBox!, this._themeService)); this._register(attachSelectBoxStyler(this._restoreFromSelectBox!, this._themeService)); this._register(attachSelectBoxStyler(this._sourceDatabaseSelectBox!, this._themeService)); this._register(attachButtonStyler(this._browseFileButton!, this._themeService)); + this._register(attachButtonStyler(this._browseUrlButton!, this._themeService)); this._register(attachButtonStyler(this._scriptButton!, this._themeService)); this._register(attachButtonStyler(this._restoreButton!, this._themeService)); this._register(attachButtonStyler(this._closeButton!, this._themeService)); this._register(attachTableStyler(this._fileListTable!, this._themeService)); this._register(attachTableStyler(this._restorePlanTable!, this._themeService)); + this._register(this._targetDatabaseInputBox.onDidChange(dbName => { + if (!this.viewModel.databases.includes(dbName)) { + if (this.viewModel.targetDatabaseName !== dbName) { + this.viewModel.targetDatabaseName = dbName; + this.validateRestore(); + } + } else { + if (this.viewModel.targetDatabaseName !== dbName) { + this.viewModel.targetDatabaseName = dbName; + this.enableRestoreButton(false); + } + } + })); + this._register(this._filePathInputBox!.onLoseFocus(params => { this.onFilePathLoseFocus(params); })); - this._browseFileButton!.onDidClick(() => { + this._register(this._browseFileButton!.onDidClick(() => { this.onFileBrowserRequested(); - }); + })); + + this._register(this._browseUrlButton!.onDidClick(() => { + this.onUrlBrowserRequested(); + })); this._register(this._sourceDatabaseSelectBox!.onDidSelect(selectedDatabase => { this.onSourceDatabaseChanged(selectedDatabase.selected); @@ -592,6 +698,10 @@ export class RestoreDialog extends Modal { this._register(this._restoreFromSelectBox!.onDidSelect(selectedRestoreFrom => { this.onRestoreFromChanged(selectedRestoreFrom.selected); })); + + this._register(this._urlInputBox!.onDidChange(url => { + this.onUrlPathChanged(url); + })); } private onFileBrowserRequested(): void { @@ -603,7 +713,18 @@ export class RestoreDialog extends Modal { filepath => this.onFileBrowsed(filepath)); } - private onFileBrowsed(filepath: string) { + private onUrlBrowserRequested(): void { + this.backupRestoreUrlBrowserDialogService.showDialog(this._ownerUri!, + this.viewModel.defaultBackupFolder!, + fileFiltersSet, + FileValidationConstants.restore, + true, + true, + '') + .then(url => this._urlInputBox!.value = url); + } + + private onFileBrowsed(filepath: string): void { const oldFilePath = this._filePathInputBox!.value; if (strings.isFalsyOrWhitespace(this._filePathInputBox!.value)) { this._filePathInputBox!.value = filepath; @@ -616,7 +737,7 @@ export class RestoreDialog extends Modal { } } - private onFilePathLoseFocus(params: OnLoseFocusParams) { + private onFilePathLoseFocus(params: OnLoseFocusParams): void { if (params.value) { if (params.hasChanged || (this.viewModel.filePath !== params.value)) { this.onFilePathChanged(params.value); @@ -624,14 +745,26 @@ export class RestoreDialog extends Modal { } } - private onFilePathChanged(filePath: string) { + private onFilePathChanged(filePath: string): void { this.viewModel.filePath = filePath; this.viewModel.selectedBackupSets = undefined; this.validateRestore(true); } - private onSourceDatabaseChanged(selectedDatabase: string) { + private onUrlPathChanged(urlPath: string): void { + this.viewModel.filePath = urlPath; + this.viewModel.selectedBackupSets = undefined; + this.validateRestore(true); + } + + private onSourceDatabaseChanged(selectedDatabase: string): void { // This check is to avoid any unnecessary even firing (to remove flickering) + if (this.viewModel.sourceDatabaseName === undefined) { + this.viewModel.sourceDatabaseName = null; + } + if (selectedDatabase === undefined) { + selectedDatabase = null; + } if (this.viewModel.sourceDatabaseName !== selectedDatabase) { this.viewModel.sourceDatabaseName = selectedDatabase; this.viewModel.selectedBackupSets = undefined; @@ -639,14 +772,52 @@ export class RestoreDialog extends Modal { } } - private onRestoreFromChanged(selectedRestoreFrom: string) { + private onRestoreFromChanged(selectedRestoreFrom: string): void { this.removeErrorMessage(); if (selectedRestoreFrom === this._backupFileTitle) { + this._sourceDatabaseSelectBox.enable(); this.viewModel.onRestoreFromChanged(true); + DOM.show(this._destinationRestoreToContainer!); + DOM.show(this._sourceDatabasesElement!); DOM.show(this._restoreFromBackupFileElement!); - } else { + DOM.hide(this._restoreFromUrlElement); + DOM.show(this._targetDatabaseElement!); + DOM.hide(this._targetDatabaseInputElement!); + if (!this._panel.contains(this._fileTab.identifier)) { + this._panel.pushTab(this._fileTab); + } + if (!this._panel.contains(this._optionsTab.identifier)) { + this._panel.pushTab(this._optionsTab); + } + this.viewModel.deviceType = MediaDeviceType.File; + } else if (selectedRestoreFrom === this._databaseTitle) { + this._sourceDatabaseSelectBox.enable(); this.viewModel.onRestoreFromChanged(false); + DOM.show(this._destinationRestoreToContainer!); + DOM.show(this._sourceDatabasesElement!); DOM.hide(this._restoreFromBackupFileElement!); + DOM.hide(this._restoreFromUrlElement); + DOM.show(this._targetDatabaseElement!); + DOM.hide(this._targetDatabaseInputElement!); + if (!this._panel.contains(this._fileTab.identifier)) { + this._panel.pushTab(this._fileTab); + } + if (!this._panel.contains(this._optionsTab.identifier)) { + this._panel.pushTab(this._optionsTab); + } + this.viewModel.deviceType = MediaDeviceType.File; + } else if (selectedRestoreFrom === this._urlTitle) { + this.viewModel.onRestoreFromChanged(true); + DOM.hide(this._destinationRestoreToContainer!); + DOM.show(this._sourceDatabasesElement!); + DOM.hide(this._restoreFromBackupFileElement!); + DOM.show(this._restoreFromUrlElement!); + DOM.hide(this._targetDatabaseElement!); + DOM.show(this._targetDatabaseInputElement!); + this._panel.removeTab(this._fileTab.identifier); + this._panel.removeTab(this._optionsTab.identifier); + this._databaseDropdown.value = ''; + this.viewModel.deviceType = MediaDeviceType.Url; } this.resetRestoreContent(); } @@ -669,26 +840,26 @@ export class RestoreDialog extends Modal { } } - public hideError() { + public hideError(): void { this.setError(''); } /* Overwrite esapce key behavior */ - protected override onClose() { + protected override onClose(): void { this.cancel(); } /* Overwrite enter key behavior */ - protected override onAccept() { + protected override onAccept(): void { this.restore(false); } - public cancel() { + public cancel(): void { this._onCancel.fire(); this.close('cancel'); } - public close(hideReason: HideReason = 'close') { + public close(hideReason: HideReason = 'close'): void { this.resetDialog(); this.hide(hideReason); this._onCloseEvent.fire(); @@ -696,19 +867,38 @@ export class RestoreDialog extends Modal { private resetDialog(): void { this.hideError(); - this._restoreFromSelectBox!.selectWithOptionName(this._databaseTitle); - this.onRestoreFromChanged(this._databaseTitle); + if (this._engineEdition !== DatabaseEngineEdition.SqlManagedInstance) { + this._restoreFromSelectBox!.selectWithOptionName(this._databaseTitle); + this.onRestoreFromChanged(this._databaseTitle); + } this._sourceDatabaseSelectBox!.select(0); - this._panel!.showTab(this._generalTabId!); + this._panel!.showTab(this._generalTab.identifier!); this._isBackupFileCheckboxChanged = false; this.removeErrorMessage(); this.resetRestoreContent(); } - public open(serverName: string, ownerUri: string) { + public open(serverName: string, ownerUri: string, engineEdition: DatabaseEngineEdition): void { + this._engineEdition = engineEdition; this.title = this._restoreTitle + ' - ' + serverName; this._ownerUri = ownerUri; + this._urlInputBox.value = ''; + this._targetDatabaseInputBox.value = ''; + let title; + if (this._engineEdition === DatabaseEngineEdition.SqlManagedInstance) { + this._restoreFromSelectBox.setOptions([this._urlTitle]); + title = this._urlTitle; + // to fetch databases + this._onDatabaseListFocused.fire(); + this._restoreFromSelectBox.disable(); + } else { + this._restoreFromSelectBox.setOptions([this._databaseTitle, this._backupFileTitle]); + title = this._databaseTitle; + this._restoreFromSelectBox.enable(); + } + this._restoreFromSelectBox.select(0); + this.onRestoreFromChanged(title); this.show(); this._restoreFromSelectBox!.focus(); } @@ -726,18 +916,18 @@ export class RestoreDialog extends Modal { } } - private updateLastBackupTaken(value: string) { + private updateLastBackupTaken(value: string): void { this._destinationRestoreToInputBox!.value = value; } - private updateFilePath(value: string) { + private updateFilePath(value: string): void { this._filePathInputBox!.value = value; if (!value) { this._filePathInputBox!.hideMessage(); } } - private updateSourceDatabaseName(databaseNamesParam: SouceDatabaseNamesParam) { + private updateSourceDatabaseName(databaseNamesParam: SouceDatabaseNamesParam): void { // Always adding an empty name as the first item so if the selected db name is not in the list, // The empty string would be selected and not the first db in the list let dbNames: string[] = []; @@ -755,7 +945,7 @@ export class RestoreDialog extends Modal { this._databaseDropdown!.value = value; } - private updateRestoreOption(optionParam: RestoreOptionParam) { + private updateRestoreOption(optionParam: RestoreOptionParam): void { const widget = this._optionsMap[optionParam.optionName]; if (widget) { if (widget instanceof Checkbox) { @@ -771,7 +961,7 @@ export class RestoreDialog extends Modal { } } - private enableDisableWiget(widget: Checkbox | SelectBox | InputBox, isReadOnly: boolean) { + private enableDisableWiget(widget: Checkbox | SelectBox | InputBox, isReadOnly: boolean): void { if (isReadOnly) { widget.disable(); } else { @@ -779,7 +969,7 @@ export class RestoreDialog extends Modal { } } - private updateRestoreDatabaseFiles(dbFiles: azdata.RestoreDatabaseFileInfo[]) { + private updateRestoreDatabaseFiles(dbFiles: azdata.RestoreDatabaseFileInfo[]): void { this.clearFileListTable(); if (dbFiles && dbFiles.length > 0) { const data = []; @@ -801,7 +991,7 @@ export class RestoreDialog extends Modal { } } - private updateBackupSetsToRestore(backupSetsToRestore: azdata.DatabaseFileInfo[]) { + private updateBackupSetsToRestore(backupSetsToRestore: azdata.DatabaseFileInfo[]): void { if (this._isBackupFileCheckboxChanged) { const selectedRow = []; for (let i = 0; i < backupSetsToRestore.length; i++) { diff --git a/src/sql/workbench/services/restore/browser/restoreServiceImpl.ts b/src/sql/workbench/services/restore/browser/restoreServiceImpl.ts index f100c995a3..3d6ac1654f 100644 --- a/src/sql/workbench/services/restore/browser/restoreServiceImpl.ts +++ b/src/sql/workbench/services/restore/browser/restoreServiceImpl.ts @@ -23,6 +23,7 @@ import { IConnectionManagementService } from 'sql/platform/connection/common/con import { invalidProvider } from 'sql/base/common/errors'; import { ILogService } from 'vs/platform/log/common/log'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; +import { DatabaseEngineEdition } from 'sql/workbench/api/common/sqlExtHostTypes'; export class RestoreService implements IRestoreService { @@ -249,6 +250,7 @@ export class RestoreDialogController implements IRestoreDialogController { restoreInfo.targetDatabaseName = restoreDialog.viewModel.targetDatabaseName; } restoreInfo.overwriteTargetDatabase = overwriteTargetDatabase; + restoreInfo.deviceType = restoreDialog.viewModel.deviceType; // Set other restore options restoreDialog.viewModel.getRestoreAdvancedOptions(restoreInfo.options); @@ -320,9 +322,10 @@ export class RestoreDialogController implements IRestoreDialogController { if (this._currentProvider === ConnectionConstants.mssqlProviderName) { let restoreDialog = this._restoreDialogs[this._currentProvider] as RestoreDialog; this.getMssqlRestoreConfigInfo().then(() => { + const engineEdition: DatabaseEngineEdition = this._connectionService.getConnectionInfo(this._ownerUri).serverInfo.engineEditionId; // database list is filled only after getMssqlRestoreConfigInfo() calling before will always set to empty value restoreDialog.viewModel.resetRestoreOptions(connection.databaseName!, restoreDialog.viewModel.databaseList); - restoreDialog.open(connection.serverName, this._ownerUri!); + restoreDialog.open(connection.serverName, this._ownerUri!, engineEdition); restoreDialog.validateRestore(); }, restoreConfigError => { reject(restoreConfigError); diff --git a/src/sql/workbench/services/restore/browser/restoreViewModel.ts b/src/sql/workbench/services/restore/browser/restoreViewModel.ts index ebc3e0013b..e705b93da5 100644 --- a/src/sql/workbench/services/restore/browser/restoreViewModel.ts +++ b/src/sql/workbench/services/restore/browser/restoreViewModel.ts @@ -10,6 +10,7 @@ import * as types from 'vs/base/common/types'; import { Event, Emitter } from 'vs/base/common/event'; import { ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes'; import { coalesce } from 'vs/base/common/arrays'; +import { MediaDeviceType } from 'sql/workbench/contrib/backup/common/constants'; export interface RestoreOptionsElement { optionMetadata: azdata.ServiceOption; @@ -46,6 +47,8 @@ export class RestoreViewModel { public readHeaderFromMedia?: boolean; public selectedBackupSets?: string[]; public defaultBackupFolder?: string; + public deviceType?: MediaDeviceType; + public databases: string[]; private _onSetLastBackupTaken = new Emitter(); public onSetLastBackupTaken: Event = this._onSetLastBackupTaken.event; diff --git a/src/sql/workbench/services/restore/common/constants.ts b/src/sql/workbench/services/restore/common/constants.ts index 6fdad9a273..5f6556dabf 100644 --- a/src/sql/workbench/services/restore/common/constants.ts +++ b/src/sql/workbench/services/restore/common/constants.ts @@ -10,3 +10,4 @@ export const fileFiltersSet: { label: string, filters: string[] }[] = [ { label: localize('backup.filterBackupFiles', "Backup Files"), filters: ['*.bak', '*.trn', '*.log'] }, { label: localize('backup.allFiles', "All Files"), filters: ['*'] } ]; + diff --git a/src/sql/workbench/services/restore/common/mssqlRestoreInfo.ts b/src/sql/workbench/services/restore/common/mssqlRestoreInfo.ts index e5b30ee557..dfb759f7fe 100644 --- a/src/sql/workbench/services/restore/common/mssqlRestoreInfo.ts +++ b/src/sql/workbench/services/restore/common/mssqlRestoreInfo.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import { MediaDeviceType } from 'sql/workbench/contrib/backup/common/constants'; export class MssqlRestoreInfo implements azdata.RestoreInfo { @@ -29,6 +30,14 @@ export class MssqlRestoreInfo implements azdata.RestoreInfo { this.options['backupFilePaths'] = value; } + public get deviceType(): MediaDeviceType { + return this.options['deviceType']; + } + + public set deviceType(value: MediaDeviceType) { + this.options['deviceType'] = value; + } + public get targetDatabaseName(): string { return this.options['targetDatabaseName']; } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index cb601df08d..b93ff71d65 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -192,6 +192,8 @@ import { IFileBrowserService } from 'sql/workbench/services/fileBrowser/common/i import { FileBrowserService } from 'sql/workbench/services/fileBrowser/common/fileBrowserService'; import { IFileBrowserDialogController } from 'sql/workbench/services/fileBrowser/common/fileBrowserDialogController'; import { FileBrowserDialogController } from 'sql/workbench/services/fileBrowser/browser/fileBrowserDialogController'; +import { IBackupRestoreUrlBrowserDialogService } from 'sql/workbench/services/backupRestoreUrlBrowser/common/urlBrowserDialogService'; +import { BackupRestoreUrlBrowserDialogService } from 'sql/workbench/services/backupRestoreUrlBrowser/browser/urlBrowserDialogService'; import { IInsightsDialogService } from 'sql/workbench/services/insights/browser/insightsDialogService'; import { InsightsDialogService } from 'sql/workbench/services/insights/browser/insightsDialogServiceImpl'; import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces'; @@ -238,6 +240,7 @@ registerSingleton(IRestoreService, RestoreService); registerSingleton(IRestoreDialogController, RestoreDialogController); registerSingleton(IFileBrowserService, FileBrowserService); registerSingleton(IFileBrowserDialogController, FileBrowserDialogController); +registerSingleton(IBackupRestoreUrlBrowserDialogService, BackupRestoreUrlBrowserDialogService); registerSingleton(IInsightsDialogService, InsightsDialogService); registerSingleton(INotebookService, NotebookService); registerSingleton(IAccountPickerService, AccountPickerService);