UI for the Backup/Restore Managed Instance Feature (#19244)

* Rebase from main branch

* Made mssql a module

* remove rpc specific stuff

* Added create sas RPC call

* Backup to url works now

* Moved createSas RPC to the BlobService

* Relocated createSas RPC from sql-dataprotocolclient to the mssql

* After rebase

* Removed duplicate symbol

* Renamed Blob to AzureBlob and relocated CreateSasResponse to mssql extension

* Removed AzureBlobProvider, removed AzureBlobService feature

* renamed blob to azureblob, converted thenable to promise

* Simplify API

* fixes

* docs update, blob to azureblob update

* UI design first commit

* Detected Managed Instance, trying to script backup to url

* azure subscription api added, but ADS crashes

* Created url dialog component and added link account

* Changed backup component UI logic

* Changed b/r UI, added restore from URL, detected MI from restore component

* Removed mocked and added real Azure API, changed RestoreDialog UI

* Added file fetching API

* added create sas RPC call

* Backup to url works now

* Fixed some bugs

* Moved createSas RPC to the BlobService

* Relocated createSas RPC from sql-dataprotocolclient to the mssql

* Rebase createSas changed to the backupRestoreManagedInstance

* PR comments fix

* Enabled backup to url for gov clouds

* Replaced anchor element with Link class

* Fixed pick azure account logic

* Removed duplicates from eslintrc

* Fixed url browser dialog

* Fixed restore UI, disabled url empty url browser dropdowns, fixed backup OK button

* bumped sts version

* bumped sts version

* Fix config

* Fixed URL browser dialog UX

* Backup and restore dialog fix

* Referencing azure resource types directly

* Scoped url browser dialog css classes

* Made the url browser dialog field a local variable

* moved url browser files from fileBrowser to the urlBrowser folder

* Changed deviceType from number to enum

* Added all device type options

* Moved mssql

* Added MI backup button comment

* Removed unhelpful comment

* Revert differential copy only backup mistake

* Renamed azurebrowser to urlBrowserDialog

* Localize create sas button label

* Removed unnecessary spinner

* Use UTC date instead of locale date

* Removed * and added required flag

* Use async instead of nested thens

* Added target database tooltip

* Using deferred promise instead of event emitter

* Added error handling to the url browser dialog

* Registered backup component elements

* Register backup component listeners

* Removed redundant setDefaultBackupPaths call

* Added setBackupPathList docs

* Add return types

* Remove code from comment

* Register restore dialog elements

* Register restore dialog listeners

* Pass engine edition enum instead of boolean

* Capitalize enum values

* DatabaseEngineEdition fix

* Use DeviceType instead of number

* Use deferred pointer

* Add new ModalDialogName

* Use constructor fields

* Register URL browser dialog components

* Remove unnecessary helper function

* nextYear function doc and move

* split registerListeners method

* showDialog returns promise

* Backup device type comment

* Pass aria label through constructor

* Fix backup button

* Remove comment

* Comment unsupported MI backup options

* Remove one liner helper function

* Restore dialog methods return types

* Remove comment

* JS doc format

* Renamed UrlBrowserDialog to BackupRestoreUrlBrowserDialog

* Moved MediaDeviceType, added PhisicalDeviceType

* Reorder and rename physical device type

* remove extra spaces

Co-authored-by: chgagnon <chgagnon@microsoft.com>
This commit is contained in:
Nemanja Milovančević
2022-05-11 20:01:06 +02:00
committed by GitHub
parent d38dcc853d
commit 65ef41d53d
24 changed files with 1154 additions and 162 deletions

View File

@@ -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",

View File

@@ -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

View File

@@ -13,5 +13,8 @@ export const IAzureAccountService = createDecorator<IAzureAccountService>(SERVIC
export interface IAzureAccountService {
_serviceBrand: undefined;
getSubscriptions(account: azurecore.AzureAccount): Promise<azurecore.GetSubscriptionsResult>;
getStorageAccounts(account: azurecore.AzureAccount, subscriptions: azurecore.azureResource.AzureResourceSubscription[]): Promise<azurecore.GetStorageAccountResult>;
getBlobContainers(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource): Promise<azurecore.GetBlobContainersResult>;
getBlobs(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, containerName: string, ignoreErrors?: boolean): Promise<azurecore.GetBlobsResult>;
getStorageAccountAccessKey(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<azurecore.GetStorageAccountAccessKeyResult>;
}

View File

@@ -17,6 +17,7 @@ export const enum ModalDialogName {
Connection = 'Connection',
Backup = 'Backup',
FileBrowser = 'FileBrowser',
UrlBrowser = 'UrlBrowser',
Restore = 'Restore',
Insights = 'Insights',
Profiler = 'Profiler',

View File

@@ -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<azurecore.GetStorageAccountResult> {
return this._proxy.$getStorageAccounts(account, subscriptions, ignoreErrors);
}
public getBlobContainers(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<azurecore.GetBlobContainersResult> {
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<azurecore.GetBlobsResult> {
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<azurecore.GetStorageAccountAccessKeyResult> {
return this._proxy.$getStorageAccountAccessKey(account, subscription, storageAccount, ignoreErrors);
}
}

View File

@@ -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.
*/

View File

@@ -15,8 +15,31 @@ export class ExtHostAzureAccount extends ExtHostAzureAccountShape {
}
public override $getSubscriptions(account: azurecore.AzureAccount, ignoreErrors?: boolean, selectedOnly?: boolean): Thenable<azurecore.GetSubscriptionsResult> {
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<azurecore.GetStorageAccountResult> {
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<azurecore.GetBlobContainersResult> {
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<azurecore.GetBlobsResult> {
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<azurecore.GetStorageAccountAccessKeyResult> {
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;
}
}

View File

@@ -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));

View File

@@ -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<mssql.CreateSasResponse> { throw ni(); }
}
export abstract class ExtHostAzureAccountShape {
public $getSubscriptions(account: azurecore.AzureAccount, ignoreErrors?: boolean, selectedOnly?: boolean): Thenable<azurecore.GetSubscriptionsResult> { throw ni(); }
public $getStorageAccounts(account: azurecore.AzureAccount, subscriptions: azurecore.azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<azurecore.GetStorageAccountResult> { throw ni(); }
public $getBlobContainers(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<azurecore.GetBlobContainersResult> { throw ni(); }
public $getBlobs(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, containerName: string, ignoreErrors: boolean): Promise<azurecore.GetBlobsResult> { throw ni(); }
public $getStorageAccountAccessKey(account: azurecore.AzureAccount, subscription: azurecore.azureResource.AzureResourceSubscription, storageAccount: azurecore.azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<azurecore.GetStorageAccountAccessKeyResult> { 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<any>;
$unregisterResourceProvider(handle: number): Thenable<any>;

View File

@@ -24,20 +24,36 @@
</div>
<div class="input-divider check" #copyOnlyContainer>
</div>
<div class="dialog-label">
<div class="input-divider check" #toUrlContainer>
</div>
<div class="dialog-label" #filePathContainerLabel>
{{localizedStrings.BACKUP_DEVICE}}
</div>
<div class="dialog-label" #urlPathContainerLabel hidden="true">
{{localizedStrings.BACKUP_URL}}
</div>
<div class="backup-path-list">
<div #pathContainer>
<div #urlPathContainer hidden="true">
</div>
</div>
<table class="backup-path-table">
<div class="backup-path-list">
<div #filePathContainer>
</div>
</div>
<table class="backup-path-table" #filePathButtonsContainer>
<tr>
<td style="padding-left: 0px; padding-right: 0px;">
<div class="backup-path-button" #addPathContainer></div>
<div class="backup-path-button" #addFilePathContainer></div>
</td>
<td>
<div class="backup-path-button" #removePathContainer></div>
<div class="backup-path-button" #removeFilePathContainer></div>
</td>
</tr>
</table>
<table class="backup-path-table" #urlPathButtonsContainer hidden="true">
<tr>
<td style="padding-left: 0px; padding-right: 0px;">
<div #addUrlPathContainer></div>
</td>
</tr>
</table>
@@ -91,19 +107,19 @@
</div>
<div role="radiogroup" aria-labelledby="media" class="radio-indent">
<div class="option">
<input role="radio" type="radio" name="media-option" value="no_format" [checked]="!isFormatChecked" (change)="onChangeMediaFormat()" [disabled]="isEncryptChecked" aria-labelledby="mediaOption"><span id="mediaOption">{{localizedStrings.MEDIA_OPTION}}</span>
<input role="radio" type="radio" name="media-option" value="no_format" [checked]="!isFormatChecked" (change)="onChangeMediaFormat()" [disabled]="disableMedia" aria-labelledby="mediaOption"><span id="mediaOption">{{localizedStrings.MEDIA_OPTION}}</span>
</div>
<div role="radiogroup" aria-labelledby="mediaOption" style="margin-left:15px">
<div class="option">
<input role="radio" type="radio" name="existing-media" value="append" [(ngModel)]="selectedInitOption" [disabled]="isFormatChecked" aria-labelledby="existingMediaAppend"><span id="existingMediaAppend">{{localizedStrings.EXISTING_MEDIA_APPEND}}</span>
<input role="radio" type="radio" name="existing-media" value="append" [(ngModel)]="selectedInitOption" [disabled]="disableMedia" aria-labelledby="existingMediaAppend"><span id="existingMediaAppend">{{localizedStrings.EXISTING_MEDIA_APPEND}}</span>
</div>
<div class="option">
<input role="radio" type="radio" name="existing-media" value="overwrite" [(ngModel)]="selectedInitOption" [disabled]="isFormatChecked" aria-labelledby="existingMediaOverwrite"><span id="existingMediaOverwrite">{{localizedStrings.EXISTING_MEDIA_OVERWRITE}}</span>
<input role="radio" type="radio" name="existing-media" value="overwrite" [(ngModel)]="selectedInitOption" [disabled]="disableMedia" aria-labelledby="existingMediaOverwrite"><span id="existingMediaOverwrite">{{localizedStrings.EXISTING_MEDIA_OVERWRITE}}</span>
</div>
</div>
<div class="option">
<input role="radio" type="radio" name="media-option" value="format" [checked]="isFormatChecked" (change)="onChangeMediaFormat()" aria-labelledby="mediaOptionFormat"><span id="mediaOptionFormat">{{localizedStrings.MEDIA_OPTION_FORMAT}}</span>
<input role="radio" type="radio" name="media-option" value="format" [checked]="isFormatChecked" (change)="onChangeMediaFormat()" [disabled]="disableMedia" aria-labelledby="mediaOptionFormat"><span id="mediaOptionFormat">{{localizedStrings.MEDIA_OPTION_FORMAT}}</span>
</div>
<div style="margin-left: 22px">
<div class="dialog-label">

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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<azurecore.GetSubscriptionsResult> {
public async getSubscriptions(account: azurecore.AzureAccount, ignoreErrors?: boolean, selectedOnly?: boolean): Promise<azurecore.GetSubscriptionsResult> {
this.checkProxy();
return this._proxy.getSubscriptions(account, ignoreErrors, selectedOnly);
}
public async getStorageAccounts(account: azurecore.AzureAccount, subscriptions: azurecore.azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<azurecore.GetStorageAccountResult> {
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<azurecore.GetBlobContainersResult> {
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<azurecore.GetBlobsResult> {
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<azurecore.GetStorageAccountAccessKeyResult> {
this.checkProxy();
return this._proxy.getStorageAccountAccessKey(account, subscription, storageAccount, ignoreErrors);
}
private checkProxy(): void {
if (!this._proxy) {
throw new Error('Azure Account proxy not initialized');
}
}
}

View File

@@ -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;
}

View File

@@ -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<string> | 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));
}
}

View File

@@ -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<string> {
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;
}
}

View File

@@ -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<IBackupRestoreUrlBrowserDialogService>('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<string>;
}

View File

@@ -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<FileListElement>;
private _fileListData?: TableDataView<FileListElement>;
@@ -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<Slick.SlickData>();
this._restorePlanTable = new Table<Slick.SlickData>(this._restorePlanTableContainer,
{ dataProvider: this._restorePlanData, columns: this._restorePlanColumn }, { enableColumnReorder: false });
this._restorePlanTable = this._register(new Table<Slick.SlickData>(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<FileListElement>();
this._fileListTable = new Table<FileListElement>(this._fileListTableContainer,
{ dataProvider: this._fileListData, columns }, { enableColumnReorder: false });
this._fileListTable = this._register(new Table<FileListElement>(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(<unknown>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, (<Checkbox>this._optionsMap[optionName]).checked);
this.validateRestore(false);
}
private onCatagoryOptionChanged(optionName: string) {
private onCatagoryOptionChanged(optionName: string): void {
this.viewModel.setOptionValue(optionName, (<SelectBox>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<Slick.SlickData>): 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++) {

View File

@@ -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);

View File

@@ -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<string>();
public onSetLastBackupTaken: Event<string> = this._onSetLastBackupTaken.event;

View File

@@ -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: ['*'] }
];

View File

@@ -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'];
}

View File

@@ -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);