Layering of everything else but query (#5085)

* layer profiler and edit data

* relayering everything but query

* fix css import

* readd qp

* fix script src

* fix hygiene
This commit is contained in:
Anthony Dresser
2019-04-18 01:28:43 -07:00
committed by GitHub
parent ddd89fc52a
commit 9c0e56d640
170 changed files with 265 additions and 357 deletions

View File

@@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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;
// Constants for backup media device type
export const deviceTypeLogicalDevice = 0;
export const deviceTypeTape = 1;
export const deviceTypeFile = 2;
export const deviceTypeURL = 5;
export const recoveryModelSimple = 'Simple';
export const recoveryModelFull = 'Full';
// Constants for UI strings
export const labelDatabase = localize('backup.labelDatabase', 'Database');
export const labelFilegroup = localize('backup.labelFilegroup', 'Files and filegroups');
export const labelFull = localize('backup.labelFull', 'Full');
export const labelDifferential = localize('backup.labelDifferential', 'Differential');
export const labelLog = localize('backup.labelLog', 'Transaction Log');
export const labelDisk = localize('backup.labelDisk', 'Disk');
export const labelUrl = localize('backup.labelUrl', 'Url');
export const defaultCompression = localize('backup.defaultCompression', 'Use the default server setting');
export const compressionOn = localize('backup.compressBackup', 'Compress backup');
export const compressionOff = localize('backup.doNotCompress', 'Do not compress backup');
export const aes128 = 'AES 128';
export const aes192 = 'AES 192';
export const aes256 = 'AES 256';
export const tripleDES = 'Triple DES';
export const serverCertificate = localize('backup.serverCertificate', "Server Certificate");
export const asymmetricKey = localize('backup.asymmetricKey', "Asymmetric Key");
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

@@ -0,0 +1,176 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<form class="angular-form" #myForm="ngForm" (ngSubmit)="onSubmit(f)">
<div class="angular-modal-body" style="display: flex; flex-direction: column;">
<div class="angular-modal-body-content">
<div class="dialog-label">
{{localizedStrings.BACKUP_NAME}}
</div>
<div class="input-divider" #backupsetName>
</div>
<div class="dialog-label">
{{localizedStrings.RECOVERY_MODEL}}
</div>
<div class="input-divider" #recoveryModelContainer>
</div>
<div class="dialog-label">
{{localizedStrings.BACKUP_TYPE}}
</div>
<div class="input-divider" #backupTypeContainer>
</div>
<div class="input-divider check" #copyOnlyContainer>
</div>
<div class="dialog-label">
{{localizedStrings.BACKUP_DEVICE}}
</div>
<div class="backup-path-list">
<div #pathContainer>
</div>
</div>
<table class="backup-path-table">
<tr>
<td style="padding-left: 0px; padding-right: 0px;">
<div class="backup-path-button" #addPathContainer></div>
</td>
<td>
<div class="backup-path-button" #removePathContainer></div>
</td>
</tr>
</table>
<div class="advanced-main-header" #advancedOptionContainer>
<div class="advanced-main-body" #advancedOptionBodyContainer>
<!-- Compression -->
<div class="dialog-label advanced-header">
{{localizedStrings.COMPRESSION}}
</div>
<div class="indent">
<div class="dialog-label">
{{localizedStrings.SET_BACKUP_COMPRESSION}}
</div>
<div class="dialog-label" #compressionContainer>
</div>
</div>
<!-- Encryption -->
<div class="dialog-label advanced-header">
{{localizedStrings.ENCRYPTION}}
</div>
<div class="indent">
<div class="option check" #encryptCheckContainer>
</div>
<div class="option" #encryptWarningContainer>
<div class="sql icon warning">
</div>
<div class="warning-message">
{{localizedStrings.NO_ENCRYPTOR_WARNING}}
</div>
</div>
<div #encryptContainer>
<div class="dialog-label">
{{localizedStrings.ALGORITHM}}
</div>
<div class="dialog-label" #algorithmContainer>
</div>
<div class="dialog-label">
{{localizedStrings.CERTIFICATE_OR_ASYMMETRIC_KEY}}
</div>
<div class="dialog-label" #encryptorContainer>
</div>
</div>
</div>
<!-- Overwrite media -->
<div id="media" class="dialog-label advanced-header">
{{localizedStrings.MEDIA}}
</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>
</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>
</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>
</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>
</div>
<div style="margin-left: 22px">
<div class="dialog-label">
{{localizedStrings.NEW_MEDIA_SET_NAME}}
</div>
<div class="dialog-label" #mediaName>
</div>
<div class="dialog-label">
{{localizedStrings.NEW_MEDIA_SET_DESCRIPTION}}
</div>
<div class="dialog-label" #mediaDescription>
</div>
</div>
</div>
<!-- Transaction log -->
<div id="transactionLog" class="dialog-label advanced-header">
{{localizedStrings.TRANSACTION_LOG}}
</div>
<div role="radiogroup" aria-labelledby="transactionLog" class="radio-indent">
<div class="option">
<input role="radio" type="radio" name="t-log" value="truncate" [checked]="isTruncateChecked" (change)="onChangeTlog()" [disabled]="disableTlog" aria-labelledby="truncateTransaction"><span id="truncateTransaction">{{localizedStrings.TRUNCATE_TRANSACTION_LOG}}</span>
</div>
<div class="option">
<input role="radio" type="radio" name="t-log" value="taillog" [checked]="isTaillogChecked" (change)="onChangeTlog()" [disabled]="disableTlog" aria-labelledby="backupTail"><span id="backupTail">{{localizedStrings.BACKUP_TAIL}}</span>
</div>
</div>
<!-- Reliability -->
<div class="dialog-label advanced-header">
{{localizedStrings.RELIABILITY}}
</div>
<div class="indent">
<div class="option check" #checksumContainer>
</div>
<div class="option check" #verifyContainer>
</div>
<div class="option check" #continueOnErrorContainer>
</div>
</div>
<!-- Backup expiration -->
<div class="dialog-label advanced-header">
{{localizedStrings.EXPIRATION}}
</div>
<div class="indent">
<div class="dialog-label">
{{localizedStrings.SET_BACKUP_RETAIN_DAYS}}
</div>
<div class="dialog-label">
<div #backupDaysContainer></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer" #modalFooterContainer>
<div class="icon in-progress" #inProgressContainer></div>
<div class="right-footer">
<div class="footer-button" #scriptButtonContainer>
</div>
<div class="footer-button" #backupButtonContainer>
</div>
<div class="footer-button" #cancelButtonContainer>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,910 @@
/*---------------------------------------------------------------------------------------------
* 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/backupDialog';
import { ElementRef, Component, Inject, forwardRef, ViewChild, ChangeDetectorRef } from '@angular/core';
import { Button } from 'sql/base/browser/ui/button/button';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { ListBox } from 'sql/base/browser/ui/listBox/listBox';
import { ModalFooterStyle } from 'sql/workbench/browser/modal/modal';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { attachButtonStyler, attachListBoxStyler, attachInputBoxStyler, attachSelectBoxStyler, attachCheckboxStyler } from 'sql/platform/theme/common/styler';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import * as BackupConstants from 'sql/workbench/parts/backup/common/constants';
import { IBackupService, TaskExecutionMode } from 'sql/platform/backup/common/backupService';
import * as FileValidationConstants from 'sql/workbench/services/fileBrowser/common/fileValidationServiceConstants';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { IFileBrowserDialogController } from 'sql/workbench/services/fileBrowser/common/fileBrowserDialogController';
import { IBackupUiService } from 'sql/workbench/services/backup/common/backupUiService';
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import * as lifecycle from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import * as types from 'vs/base/common/types';
import * as strings from 'vs/base/common/strings';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox';
export const BACKUP_SELECTOR: string = 'backup-component';
export class RestoreItemSource {
restoreItemLocation: string;
restoreItemDeviceType: number;
isLogicalDevice: boolean;
constructor(location: any) {
this.restoreItemDeviceType = location.restoreItemDeviceType;
this.restoreItemLocation = location.restoreItemLocation;
this.isLogicalDevice = location.isLogicalDevice;
}
}
interface MssqlBackupInfo {
ownerUri: string;
databaseName: string;
backupType: number;
backupComponent: number;
backupDeviceType: number;
selectedFiles: string;
backupsetName: string;
selectedFileGroup: { [path: string]: string };
// List of {key: backup path, value: device type}
backupPathDevices: { [path: string]: number };
backupPathList: [string];
isCopyOnly: boolean;
formatMedia: boolean;
initialize: boolean;
skipTapeHeader: boolean;
mediaName: string;
mediaDescription: string;
checksum: boolean;
continueAfterError: boolean;
logTruncation: boolean;
tailLogBackup: boolean;
retainDays: number;
compressionOption: number;
verifyBackupRequired: boolean;
encryptionAlgorithm: number;
encryptorType: number;
encryptorName: string;
}
const LocalizedStrings = {
BACKUP_NAME: localize('backup.backupName', 'Backup name'),
RECOVERY_MODEL: localize('backup.recoveryModel', 'Recovery model'),
BACKUP_TYPE: localize('backup.backupType', 'Backup type'),
BACKUP_DEVICE: localize('backup.backupDevice', 'Backup files'),
ALGORITHM: localize('backup.algorithm', 'Algorithm'),
CERTIFICATE_OR_ASYMMETRIC_KEY: localize('backup.certificateOrAsymmetricKey', 'Certificate or Asymmetric key'),
MEDIA: localize('backup.media', 'Media'),
MEDIA_OPTION: localize('backup.mediaOption', 'Backup to the existing media set'),
MEDIA_OPTION_FORMAT: localize('backup.mediaOptionFormat', 'Backup to a new media set'),
EXISTING_MEDIA_APPEND: localize('backup.existingMediaAppend', 'Append to the existing backup set'),
EXISTING_MEDIA_OVERWRITE: localize('backup.existingMediaOverwrite', 'Overwrite all existing backup sets'),
NEW_MEDIA_SET_NAME: localize('backup.newMediaSetName', 'New media set name'),
NEW_MEDIA_SET_DESCRIPTION: localize('backup.newMediaSetDescription', 'New media set description'),
CHECKSUM_CONTAINER: localize('backup.checksumContainer', 'Perform checksum before writing to media'),
VERIFY_CONTAINER: localize('backup.verifyContainer', 'Verify backup when finished'),
CONTINUE_ON_ERROR_CONTAINER: localize('backup.continueOnErrorContainer', 'Continue on error'),
EXPIRATION: localize('backup.expiration', 'Expiration'),
SET_BACKUP_RETAIN_DAYS: localize('backup.setBackupRetainDays', 'Set backup retain days'),
COPY_ONLY: localize('backup.copyOnly', 'Copy-only backup'),
ADVANCED_CONFIGURATION: localize('backup.advancedConfiguration', 'Advanced Configuration'),
COMPRESSION: localize('backup.compression', 'Compression'),
SET_BACKUP_COMPRESSION: localize('backup.setBackupCompression', 'Set backup compression'),
ENCRYPTION: localize('backup.encryption', 'Encryption'),
TRANSACTION_LOG: localize('backup.transactionLog', 'Transaction log'),
TRUNCATE_TRANSACTION_LOG: localize('backup.truncateTransactionLog', 'Truncate the transaction log'),
BACKUP_TAIL: localize('backup.backupTail', 'Backup the tail of the log'),
RELIABILITY: localize('backup.reliability', 'Reliability'),
MEDIA_NAME_REQUIRED_ERROR: localize('backup.mediaNameRequired', 'Media name is required'),
NO_ENCRYPTOR_WARNING: localize('backup.noEncryptorWarning', "No certificate or asymmetric key is available")
};
@Component({
selector: BACKUP_SELECTOR,
templateUrl: decodeURI(require.toUrl('sql/workbench/parts/backup/electron-browser/backup.component.html'))
})
export class BackupComponent {
@ViewChild('pathContainer', { read: ElementRef }) pathElement;
@ViewChild('backupTypeContainer', { read: ElementRef }) backupTypeElement;
@ViewChild('backupsetName', { read: ElementRef }) backupNameElement;
@ViewChild('compressionContainer', { read: ElementRef }) compressionElement;
@ViewChild('tlogOption', { read: ElementRef }) tlogOptionElement;
@ViewChild('algorithmContainer', { read: ElementRef }) encryptionAlgorithmElement;
@ViewChild('encryptorContainer', { read: ElementRef }) encryptorElement;
@ViewChild('mediaName', { read: ElementRef }) mediaNameElement;
@ViewChild('mediaDescription', { read: ElementRef }) mediaDescriptionElement;
@ViewChild('recoveryModelContainer', { read: ElementRef }) recoveryModelElement;
@ViewChild('backupDaysContainer', { read: ElementRef }) backupDaysElement;
@ViewChild('backupButtonContainer', { read: ElementRef }) backupButtonElement;
@ViewChild('cancelButtonContainer', { read: ElementRef }) cancelButtonElement;
@ViewChild('addPathContainer', { read: ElementRef }) addPathElement;
@ViewChild('removePathContainer', { read: ElementRef }) removePathElement;
@ViewChild('copyOnlyContainer', { read: ElementRef }) copyOnlyElement;
@ViewChild('encryptCheckContainer', { read: ElementRef }) encryptElement;
@ViewChild('encryptContainer', { read: ElementRef }) encryptContainerElement;
@ViewChild('verifyContainer', { read: ElementRef }) verifyElement;
@ViewChild('checksumContainer', { read: ElementRef }) checksumElement;
@ViewChild('continueOnErrorContainer', { read: ElementRef }) continueOnErrorElement;
@ViewChild('encryptWarningContainer', { read: ElementRef }) encryptWarningElement;
@ViewChild('inProgressContainer', { read: ElementRef }) inProgressElement;
@ViewChild('modalFooterContainer', { read: ElementRef }) modalFooterElement;
@ViewChild('scriptButtonContainer', { read: ElementRef }) scriptButtonElement;
@ViewChild('advancedOptionContainer', { read: ElementRef }) advancedOptionElement;
@ViewChild('advancedOptionBodyContainer', { read: ElementRef }) advancedOptionBodyElement;
private localizedStrings = LocalizedStrings;
private _uri: string;
private _toDispose: lifecycle.IDisposable[] = [];
private _advancedHeaderSize = 32;
private connection: IConnectionProfile;
private databaseName: string;
private defaultNewBackupFolder: string;
private recoveryModel: string;
private backupEncryptors;
private containsBackupToUrl: boolean;
// UI element disable flag
private disableFileComponent: boolean;
private disableTlog: boolean;
private selectedBackupComponent: string;
private selectedFilesText: string;
private selectedInitOption: string;
private isTruncateChecked: boolean;
private isTaillogChecked: boolean;
private isFormatChecked: boolean;
private isEncryptChecked: boolean;
// Key: backup path, Value: device type
private backupPathTypePairs: { [path: string]: number };
private compressionOptions = [BackupConstants.defaultCompression, BackupConstants.compressionOn, BackupConstants.compressionOff];
private encryptionAlgorithms = [BackupConstants.aes128, BackupConstants.aes192, BackupConstants.aes256, BackupConstants.tripleDES];
private existingMediaOptions = ['append', 'overwrite'];
private backupTypeOptions: string[];
private backupTypeSelectBox: SelectBox;
private backupNameBox: InputBox;
private recoveryBox: InputBox;
private backupRetainDaysBox: InputBox;
private backupButton: Button;
private cancelButton: Button;
private scriptButton: Button;
private addPathButton: Button;
private removePathButton: Button;
private pathListBox: ListBox;
private compressionSelectBox: SelectBox;
private algorithmSelectBox: SelectBox;
private encryptorSelectBox: SelectBox;
private mediaNameBox: InputBox;
private mediaDescriptionBox: InputBox;
private copyOnlyCheckBox: Checkbox;
private encryptCheckBox: Checkbox;
private verifyCheckBox: Checkbox;
private checksumCheckBox: Checkbox;
private continueOnErrorCheckBox: Checkbox;
constructor(
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeDetectorRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IContextViewService) private contextViewService: IContextViewService,
@Inject(IFileBrowserDialogController) private fileBrowserDialogService: IFileBrowserDialogController,
@Inject(IBackupUiService) private _backupUiService: IBackupUiService,
@Inject(IBackupService) private _backupService: IBackupService,
@Inject(IClipboardService) private clipboardService: IClipboardService,
@Inject(IConnectionManagementService) private connectionManagementService: IConnectionManagementService,
@Inject(IInstantiationService) private instantiationService: IInstantiationService
) {
this._backupUiService.onShowBackupEvent((param) => this.onGetBackupConfigInfo(param));
}
ngOnInit() {
let self = this;
this.addFooterButtons();
this.recoveryBox = 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.render(this.backupTypeElement.nativeElement);
// Set copy-only check box
this.copyOnlyCheckBox = new Checkbox(this.copyOnlyElement.nativeElement, {
label: LocalizedStrings.COPY_ONLY,
checked: false,
onChange: (viaKeyboard) => { },
ariaLabel: LocalizedStrings.COPY_ONLY
});
// Encryption checkbox
this.encryptCheckBox = new Checkbox(this.encryptElement.nativeElement, {
label: LocalizedStrings.ENCRYPTION,
checked: false,
onChange: (viaKeyboard) => self.onChangeEncrypt(),
ariaLabel: LocalizedStrings.ENCRYPTION
});
// Verify backup checkbox
this.verifyCheckBox = new Checkbox(this.verifyElement.nativeElement, {
label: LocalizedStrings.VERIFY_CONTAINER,
checked: false,
onChange: (viaKeyboard) => { },
ariaLabel: LocalizedStrings.VERIFY_CONTAINER
});
// Perform checksum checkbox
this.checksumCheckBox = new Checkbox(this.checksumElement.nativeElement, {
label: LocalizedStrings.CHECKSUM_CONTAINER,
checked: false,
onChange: (viaKeyboard) => { },
ariaLabel: LocalizedStrings.CHECKSUM_CONTAINER
});
// Continue on error checkbox
this.continueOnErrorCheckBox = new Checkbox(this.continueOnErrorElement.nativeElement, {
label: LocalizedStrings.CONTINUE_ON_ERROR_CONTAINER,
checked: false,
onChange: (viaKeyboard) => { },
ariaLabel: LocalizedStrings.CONTINUE_ON_ERROR_CONTAINER
});
// Set backup name
this.backupNameBox = new InputBox(this.backupNameElement.nativeElement, this.contextViewService, {
ariaLabel: LocalizedStrings.BACKUP_NAME
});
// Set backup path list
this.pathListBox = new ListBox([], this.contextViewService, this.clipboardService);
this.pathListBox.render(this.pathElement.nativeElement);
// Set backup path add/remove buttons
this.addPathButton = new Button(this.addPathElement.nativeElement);
this.addPathButton.label = '+';
this.addPathButton.title = localize('addFile', 'Add a file');
this.removePathButton = new Button(this.removePathElement.nativeElement);
this.removePathButton.label = '-';
this.removePathButton.title = localize('removeFile', 'Remove files');
// Set compression
this.compressionSelectBox = new SelectBox(this.compressionOptions, this.compressionOptions[0], this.contextViewService, undefined, { ariaLabel: this.localizedStrings.SET_BACKUP_COMPRESSION });
this.compressionSelectBox.render(this.compressionElement.nativeElement);
// Set encryption
this.algorithmSelectBox = new SelectBox(this.encryptionAlgorithms, this.encryptionAlgorithms[0], this.contextViewService, undefined, { ariaLabel: this.localizedStrings.ALGORITHM });
this.algorithmSelectBox.render(this.encryptionAlgorithmElement.nativeElement);
this.encryptorSelectBox = new SelectBox([], '', this.contextViewService, undefined, { ariaLabel: this.localizedStrings.CERTIFICATE_OR_ASYMMETRIC_KEY });
this.encryptorSelectBox.render(this.encryptorElement.nativeElement);
// Set media
this.mediaNameBox = new InputBox(this.mediaNameElement.nativeElement,
this.contextViewService,
{
validationOptions: {
validation: (value: string) => !value ? ({ type: MessageType.ERROR, content: LocalizedStrings.MEDIA_NAME_REQUIRED_ERROR }) : null
},
ariaLabel: LocalizedStrings.NEW_MEDIA_SET_NAME
}
);
this.mediaDescriptionBox = new InputBox(this.mediaDescriptionElement.nativeElement, this.contextViewService, {
ariaLabel: LocalizedStrings.NEW_MEDIA_SET_DESCRIPTION
});
// Set backup retain days
let invalidInputMessage = localize('backupComponent.invalidInput', 'Invalid input. Value must be greater than or equal 0.');
this.backupRetainDaysBox = new InputBox(this.backupDaysElement.nativeElement,
this.contextViewService,
{
placeholder: '0',
type: 'number',
min: '0',
validationOptions: {
validation: (value: string) => {
if (types.isNumber(Number(value)) && Number(value) < 0) {
return { type: MessageType.ERROR, content: invalidInputMessage };
} else {
return null;
}
}
},
ariaLabel: LocalizedStrings.SET_BACKUP_RETAIN_DAYS
});
// Disable elements
this.recoveryBox.disable();
this.mediaNameBox.disable();
this.mediaDescriptionBox.disable();
this.registerListeners();
this.updateTheme();
}
ngAfterViewInit() {
this._backupUiService.onShowBackupDialog();
}
private onGetBackupConfigInfo(param: { connection: IConnectionProfile, ownerUri: string }) {
// Show spinner
this.showSpinner();
this.backupEnabled = false;
// Reset backup values
this.backupNameBox.value = '';
this.pathListBox.setOptions([], 0);
this.connection = param.connection;
this._uri = param.ownerUri;
// Get backup configuration info
this._backupService.getBackupConfigInfo(this._uri).then(configInfo => {
if (configInfo) {
this.defaultNewBackupFolder = configInfo.defaultBackupFolder;
this.recoveryModel = configInfo.recoveryModel;
this.backupEncryptors = configInfo.backupEncryptors;
this.initialize(true);
} else {
this.initialize(false);
}
// Hide spinner
this.hideSpinner();
});
}
/**
* Show spinner in the backup dialog
*/
private showSpinner(): void {
this.inProgressElement.nativeElement.style.visibility = 'visible';
}
/**
* Hide spinner in the backup dialog
*/
private hideSpinner(): void {
this.inProgressElement.nativeElement.style.visibility = 'hidden';
}
private addFooterButtons(): void {
// Set script footer button
this.scriptButton = new Button(this.scriptButtonElement.nativeElement);
this.scriptButton.label = localize('backupComponent.script', 'Script');
this.addButtonClickHandler(this.scriptButton, () => this.onScript());
this._toDispose.push(attachButtonStyler(this.scriptButton, this.themeService));
this.scriptButton.enabled = false;
// Set backup footer button
this.backupButton = new Button(this.backupButtonElement.nativeElement);
this.backupButton.label = localize('backupComponent.backup', 'Backup');
this.addButtonClickHandler(this.backupButton, () => this.onOk());
this._toDispose.push(attachButtonStyler(this.backupButton, this.themeService));
this.backupEnabled = false;
// Set cancel footer button
this.cancelButton = new Button(this.cancelButtonElement.nativeElement);
this.cancelButton.label = localize('backupComponent.cancel', 'Cancel');
this.addButtonClickHandler(this.cancelButton, () => this.onCancel());
this._toDispose.push(attachButtonStyler(this.cancelButton, this.themeService));
}
private initialize(isMetadataPopulated: boolean): void {
this.databaseName = this.connection.databaseName;
this.selectedBackupComponent = BackupConstants.labelDatabase;
this.backupPathTypePairs = {};
this.isFormatChecked = false;
this.isEncryptChecked = false;
this.selectedInitOption = this.existingMediaOptions[0];
this.backupTypeOptions = [];
if (isMetadataPopulated) {
this.backupEnabled = true;
// Set recovery model
this.setControlsForRecoveryModel();
// Set backup type
this.backupTypeSelectBox.setOptions(this.backupTypeOptions, 0);
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);
if (encryptorItems.length === 0) {
// Disable encryption checkbox
this.encryptCheckBox.disable();
// Show warning instead of algorithm select boxes
(<HTMLElement>this.encryptWarningElement.nativeElement).style.display = 'inline';
(<HTMLElement>this.encryptContainerElement.nativeElement).style.display = 'none';
}
else {
// Show algorithm select boxes instead of warning
(<HTMLElement>this.encryptWarningElement.nativeElement).style.display = 'none';
(<HTMLElement>this.encryptContainerElement.nativeElement).style.display = 'inline';
// Disable the algorithm select boxes since encryption is not checked by default
this.setEncryptOptionsEnabled(false);
}
this.setTLogOptions();
// disable elements
this.recoveryBox.disable();
this.mediaNameBox.disable();
this.mediaDescriptionBox.disable();
this.recoveryBox.value = this.recoveryModel;
// show warning message if latest backup file path contains url
if (this.containsBackupToUrl) {
this.pathListBox.setValidation(false, { content: localize('backup.containsBackupToUrlError', 'Only backup to file is supported'), type: MessageType.WARNING });
this.pathListBox.focus();
}
}
this._changeDetectorRef.detectChanges();
}
/**
* Reset dialog controls to their initial state.
*/
private resetDialog(): void {
this.isFormatChecked = false;
this.isEncryptChecked = false;
this.copyOnlyCheckBox.checked = false;
this.copyOnlyCheckBox.enable();
this.compressionSelectBox.setOptions(this.compressionOptions, 0);
this.encryptCheckBox.checked = false;
this.encryptCheckBox.enable();
this.onChangeEncrypt();
this.mediaNameBox.value = '';
this.mediaDescriptionBox.value = '';
this.checksumCheckBox.checked = false;
this.verifyCheckBox.checked = false;
this.continueOnErrorCheckBox.checked = false;
this.backupRetainDaysBox.value = '0';
this.algorithmSelectBox.setOptions(this.encryptionAlgorithms, 0);
this.selectedInitOption = this.existingMediaOptions[0];
this.containsBackupToUrl = false;
this.pathListBox.setValidation(true);
this.cancelButton.applyStyles();
this.scriptButton.applyStyles();
this.backupButton.applyStyles();
}
private registerListeners(): void {
// Theme styler
this._toDispose.push(attachInputBoxStyler(this.backupNameBox, this.themeService));
this._toDispose.push(attachInputBoxStyler(this.recoveryBox, this.themeService));
this._toDispose.push(attachSelectBoxStyler(this.backupTypeSelectBox, this.themeService));
this._toDispose.push(attachListBoxStyler(this.pathListBox, this.themeService));
this._toDispose.push(attachButtonStyler(this.addPathButton, this.themeService));
this._toDispose.push(attachButtonStyler(this.removePathButton, this.themeService));
this._toDispose.push(attachSelectBoxStyler(this.compressionSelectBox, this.themeService));
this._toDispose.push(attachSelectBoxStyler(this.algorithmSelectBox, this.themeService));
this._toDispose.push(attachSelectBoxStyler(this.encryptorSelectBox, this.themeService));
this._toDispose.push(attachInputBoxStyler(this.mediaNameBox, this.themeService));
this._toDispose.push(attachInputBoxStyler(this.mediaDescriptionBox, this.themeService));
this._toDispose.push(attachInputBoxStyler(this.backupRetainDaysBox, this.themeService));
this._toDispose.push(attachCheckboxStyler(this.copyOnlyCheckBox, this.themeService));
this._toDispose.push(attachCheckboxStyler(this.encryptCheckBox, this.themeService));
this._toDispose.push(attachCheckboxStyler(this.verifyCheckBox, this.themeService));
this._toDispose.push(attachCheckboxStyler(this.checksumCheckBox, this.themeService));
this._toDispose.push(attachCheckboxStyler(this.continueOnErrorCheckBox, this.themeService));
this._toDispose.push(this.backupTypeSelectBox.onDidSelect(selected => this.onBackupTypeChanged()));
this.addButtonClickHandler(this.addPathButton, () => this.onAddClick());
this.addButtonClickHandler(this.removePathButton, () => this.onRemoveClick());
this._toDispose.push(this.mediaNameBox.onDidChange(mediaName => {
this.mediaNameChanged(mediaName);
}));
this._toDispose.push(this.backupRetainDaysBox.onDidChange(days => {
this.backupRetainDaysChanged(days);
}));
this._toDispose.push(this.themeService.onDidColorThemeChange(e => this.updateTheme()));
}
// Update theming that is specific to backup dialog
private updateTheme(): void {
// set modal footer style
let footerHtmlElement: HTMLElement = <HTMLElement>this.modalFooterElement.nativeElement;
footerHtmlElement.style.backgroundColor = ModalFooterStyle.backgroundColor;
footerHtmlElement.style.borderTopWidth = ModalFooterStyle.borderTopWidth;
footerHtmlElement.style.borderTopStyle = ModalFooterStyle.borderTopStyle;
footerHtmlElement.style.borderTopColor = ModalFooterStyle.borderTopColor;
}
private addButtonClickHandler(button: Button, handler: () => void) {
if (button && handler) {
button.onDidClick(() => {
if (button.enabled) {
handler();
}
});
}
}
/*
* UI event handlers
*/
private onScript(): void {
this._backupService.backup(this._uri, this.createBackupInfo(), TaskExecutionMode.script);
this.close();
}
private onOk(): void {
this._backupService.backup(this._uri, this.createBackupInfo(), TaskExecutionMode.executeAndScript);
this.close();
}
private onCancel(): void {
this.close();
this.connectionManagementService.disconnect(this._uri);
}
private close(): void {
this._backupUiService.closeBackup();
this.resetDialog();
}
private onChangeTlog(): void {
this.isTruncateChecked = !this.isTruncateChecked;
this.isTaillogChecked = !this.isTaillogChecked;
this.detectChange();
}
private onChangeEncrypt(): void {
if (this.encryptCheckBox.checked) {
this.setEncryptOptionsEnabled(true);
// Force to choose format media option since otherwise encryption cannot be done
if (!this.isFormatChecked) {
this.onChangeMediaFormat();
}
} else {
this.setEncryptOptionsEnabled(false);
}
this.isEncryptChecked = this.encryptCheckBox.checked;
this.detectChange();
}
private onChangeMediaFormat(): void {
this.isFormatChecked = !this.isFormatChecked;
this.enableMediaInput(this.isFormatChecked);
if (this.isFormatChecked) {
if (strings.isFalsyOrWhitespace(this.mediaNameBox.value)) {
this.backupEnabled = false;
this.backupButton.enabled = false;
this.mediaNameBox.showMessage({ type: MessageType.ERROR, content: LocalizedStrings.MEDIA_NAME_REQUIRED_ERROR });
}
} else {
this.enableBackupButton();
}
this.detectChange();
}
private set backupEnabled(value: boolean) {
this.backupButton.enabled = value;
this.scriptButton.enabled = value;
}
private onBackupTypeChanged(): void {
if (this.getSelectedBackupType() === BackupConstants.labelDifferential) {
this.copyOnlyCheckBox.checked = false;
this.copyOnlyCheckBox.disable();
} else {
this.copyOnlyCheckBox.enable();
}
this.setTLogOptions();
this.setDefaultBackupName();
this._changeDetectorRef.detectChanges();
}
private onAddClick(): void {
this.fileBrowserDialogService.showDialog(this._uri,
this.defaultNewBackupFolder,
BackupConstants.fileFiltersSet,
FileValidationConstants.backup,
false,
(filepath => this.handlePathAdded(filepath)));
}
private handlePathAdded(filepath: string) {
if (filepath && !this.backupPathTypePairs[filepath]) {
if ((this.getBackupPathCount() < BackupConstants.maxDevices)) {
this.backupPathTypePairs[filepath] = BackupConstants.deviceTypeFile;
this.pathListBox.add(filepath);
this.enableBackupButton();
this.enableAddRemoveButtons();
// stop showing error message if the list content was invalid due to no file path
if (!this.pathListBox.isContentValid && this.pathListBox.count === 1) {
this.pathListBox.setValidation(true);
}
this._changeDetectorRef.detectChanges();
}
}
}
private onRemoveClick(): void {
let self = this;
this.pathListBox.selectedOptions.forEach(selected => {
if (self.backupPathTypePairs[selected]) {
if (self.backupPathTypePairs[selected] === BackupConstants.deviceTypeURL) {
// stop showing warning message since url path is getting removed
this.pathListBox.setValidation(true);
this.containsBackupToUrl = false;
}
delete self.backupPathTypePairs[selected];
}
});
this.pathListBox.remove();
if (this.pathListBox.count === 0) {
this.backupEnabled = false;
// show input validation error
this.pathListBox.setValidation(false, { content: localize('backup.backupFileRequired', 'Backup file path is required'), type: MessageType.ERROR });
this.pathListBox.focus();
}
this.enableAddRemoveButtons();
this._changeDetectorRef.detectChanges();
}
private enableAddRemoveButtons(): void {
if (this.pathListBox.count === 0) {
this.removePathButton.enabled = false;
} else if (this.pathListBox.count === BackupConstants.maxDevices) {
this.addPathButton.enabled = false;
} else {
this.removePathButton.enabled = true;
this.addPathButton.enabled = true;
}
}
/*
* Helper methods
*/
private setControlsForRecoveryModel(): void {
if (this.recoveryModel === BackupConstants.recoveryModelSimple) {
this.selectedBackupComponent = BackupConstants.labelDatabase;
this.disableFileComponent = true;
} else {
this.disableFileComponent = false;
}
this.populateBackupTypes();
}
private populateBackupTypes(): void {
this.backupTypeOptions.push(BackupConstants.labelFull);
if (this.databaseName !== 'master') {
this.backupTypeOptions.push(BackupConstants.labelDifferential);
if (this.recoveryModel !== BackupConstants.recoveryModelSimple) {
this.backupTypeOptions.push(BackupConstants.labelLog);
}
}
}
private populateEncryptorCombo(): string[] {
let encryptorCombo = [];
this.backupEncryptors.forEach((encryptor) => {
let encryptorTypeStr = (encryptor.encryptorType === 0 ? BackupConstants.serverCertificate : BackupConstants.asymmetricKey);
encryptorCombo.push(encryptor.encryptorName + '(' + encryptorTypeStr + ')');
});
return encryptorCombo;
}
private setDefaultBackupName(): void {
if (this.backupNameBox && (!this.backupNameBox.value || this.backupNameBox.value.trim().length === 0)) {
let utc = new Date().toJSON().slice(0, 19);
this.backupNameBox.value = this.databaseName + '-' + this.getSelectedBackupType() + '-' + utc;
}
}
private setDefaultBackupPaths(): void {
if (this.defaultNewBackupFolder && this.defaultNewBackupFolder.length > 0) {
// TEMPORARY WORKAROUND: karlb 5/27 - try to guess path separator on server based on first character in path
let serverPathSeparator: string = '\\';
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';
// Add a default new backup location
this.backupPathTypePairs[defaultNewBackupLocation] = BackupConstants.deviceTypeFile;
}
}
private isBackupToFile(controllerType: number): boolean {
let isfile = false;
if (controllerType === 102) {
isfile = true;
} else if (controllerType === 105) {
isfile = false;
} else if (controllerType === BackupConstants.backupDeviceTypeDisk) {
isfile = true;
} else if (controllerType === BackupConstants.backupDeviceTypeTape || controllerType === BackupConstants.backupDeviceTypeURL) {
isfile = false;
}
return isfile;
}
private enableMediaInput(enable: boolean): void {
if (enable) {
this.mediaNameBox.enable();
this.mediaDescriptionBox.enable();
} else {
this.mediaNameBox.disable();
this.mediaDescriptionBox.disable();
}
}
private detectChange(): void {
this._changeDetectorRef.detectChanges();
}
private setTLogOptions(): void {
if (this.getSelectedBackupType() === BackupConstants.labelLog) {
// Enable log options
this.disableTlog = false;
// Choose the default option
this.isTruncateChecked = true;
} else {
// Unselect log options
this.isTruncateChecked = false;
this.isTaillogChecked = false;
// Disable log options
this.disableTlog = true;
}
}
private getBackupTypeNumber(): number {
let backupType;
switch (this.getSelectedBackupType()) {
case BackupConstants.labelFull:
backupType = 0;
break;
case BackupConstants.labelDifferential:
backupType = 1;
break;
case BackupConstants.labelLog:
backupType = 2;
break;
}
return backupType;
}
private getBackupPathCount(): number {
return this.pathListBox.count;
}
private getSelectedBackupType(): string {
let backupType = '';
if (this.backupTypeSelectBox) {
backupType = this.backupTypeSelectBox.value;
}
return backupType;
}
private enableBackupButton(): void {
if (!this.backupButton.enabled) {
if (this.pathListBox.count > 0 && (!this.isFormatChecked || this.mediaNameBox.value) && this.backupRetainDaysBox.validate()) {
this.backupEnabled = true;
}
}
}
private setEncryptOptionsEnabled(enabled: boolean): void {
if (enabled) {
this.algorithmSelectBox.enable();
this.encryptorSelectBox.enable();
} else {
this.algorithmSelectBox.disable();
this.encryptorSelectBox.disable();
}
}
private mediaNameChanged(mediaName: string): void {
if (!mediaName) {
this.backupEnabled = false;
} else {
this.enableBackupButton();
}
}
private backupRetainDaysChanged(days: string): void {
if (!this.backupRetainDaysBox.validate()) {
this.backupEnabled = false;
} else {
this.enableBackupButton();
}
}
private createBackupInfo(): MssqlBackupInfo {
let backupPathArray = [];
for (let i in this.backupPathTypePairs) {
backupPathArray.push(i);
}
// get encryptor type and name
let encryptorName = '';
let encryptorType;
if (this.encryptCheckBox.checked && this.encryptorSelectBox.value !== '') {
let selectedEncryptor = this.encryptorSelectBox.value;
let encryptorTypeStr = selectedEncryptor.substring(selectedEncryptor.lastIndexOf('(') + 1, selectedEncryptor.lastIndexOf(')'));
encryptorType = (encryptorTypeStr === BackupConstants.serverCertificate ? 0 : 1);
encryptorName = selectedEncryptor.substring(0, selectedEncryptor.lastIndexOf('('));
}
let backupInfo = <MssqlBackupInfo>{
ownerUri: this._uri,
databaseName: this.databaseName,
backupType: this.getBackupTypeNumber(),
backupComponent: 0,
backupDeviceType: BackupConstants.backupDeviceTypeDisk,
backupPathList: backupPathArray,
selectedFiles: this.selectedFilesText,
backupsetName: this.backupNameBox.value,
selectedFileGroup: undefined,
backupPathDevices: this.backupPathTypePairs,
isCopyOnly: this.copyOnlyCheckBox.checked,
// Get advanced options
formatMedia: this.isFormatChecked,
initialize: (this.isFormatChecked ? true : (this.selectedInitOption === this.existingMediaOptions[1])),
skipTapeHeader: this.isFormatChecked,
mediaName: (this.isFormatChecked ? this.mediaNameBox.value : ''),
mediaDescription: (this.isFormatChecked ? this.mediaDescriptionBox.value : ''),
checksum: this.checksumCheckBox.checked,
continueAfterError: this.continueOnErrorCheckBox.checked,
logTruncation: this.isTruncateChecked,
tailLogBackup: this.isTaillogChecked,
retainDays: strings.isFalsyOrWhitespace(this.backupRetainDaysBox.value) ? 0 : this.backupRetainDaysBox.value,
compressionOption: this.compressionOptions.indexOf(this.compressionSelectBox.value),
verifyBackupRequired: this.verifyCheckBox.checked,
encryptionAlgorithm: (this.encryptCheckBox.checked ? this.encryptionAlgorithms.indexOf(this.algorithmSelectBox.value) : 0),
encryptorType: encryptorType,
encryptorName: encryptorName
};
return backupInfo;
}
}

View File

@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
ApplicationRef, ComponentFactoryResolver, NgModule,
Inject, forwardRef, Type
} from '@angular/core';
import { APP_BASE_HREF, CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { IBootstrapParams, ISelector, providerIterator } from 'sql/platform/bootstrap/node/bootstrapService';
import { BackupComponent } from 'sql/workbench/parts/backup/electron-browser/backup.component';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
// work around
const BrowserAnimationsModule = (<any>require.__$__nodeRequire('@angular/platform-browser/animations')).BrowserAnimationsModule;
// Backup wizard main angular module
export const BackupModule = (params: IBootstrapParams, selector: string, instantiationService: IInstantiationService): Type<any> => {
@NgModule({
declarations: [
BackupComponent
],
entryComponents: [BackupComponent],
imports: [
FormsModule,
CommonModule,
BrowserModule,
BrowserAnimationsModule,
],
providers: [
{ provide: APP_BASE_HREF, useValue: '/' },
{ provide: IBootstrapParams, useValue: params },
{ provide: ISelector, useValue: selector },
...providerIterator(instantiationService)
]
})
class ModuleClass {
constructor(
@Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver,
@Inject(ISelector) private selector: string
) {
}
ngDoBootstrap(appRef: ApplicationRef) {
const factory = this._resolver.resolveComponentFactory(BackupComponent);
(<any>factory).factory.selector = this.selector;
appRef.bootstrap(factory);
}
}
return ModuleClass;
};

View File

@@ -0,0 +1,103 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Modal } from 'sql/workbench/browser/modal/modal';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { BackupModule } from 'sql/workbench/parts/backup/electron-browser/backup.module';
import { BACKUP_SELECTOR } from 'sql/workbench/parts/backup/electron-browser/backup.component';
import { attachModalDialogStyler } from 'sql/platform/theme/common/styler';
import * as TelemetryKeys from 'sql/platform/telemetry/telemetryKeys';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { bootstrapAngular } from 'sql/platform/bootstrap/node/bootstrapService';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { append, $ } from 'vs/base/browser/dom';
export class BackupDialog extends Modal {
private _body: HTMLElement;
private _backupTitle: string;
private _moduleRef: any;
constructor(
@IThemeService themeService: IThemeService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@ITelemetryService telemetryService: ITelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IClipboardService clipboardService: IClipboardService
) {
super('', TelemetryKeys.Backup, telemetryService, layoutService, clipboardService, themeService, contextKeyService, { isAngular: true, hasErrors: true });
}
protected renderBody(container: HTMLElement) {
this._body = append(container, $('.backup-dialog'));
}
public render() {
super.render();
attachModalDialogStyler(this, this._themeService);
// Add angular component template to dialog body
this.bootstrapAngular(this._body);
}
/**
* Get the bootstrap params and perform the bootstrap
*/
private bootstrapAngular(bodyContainer: HTMLElement) {
bootstrapAngular(this._instantiationService,
BackupModule,
bodyContainer,
BACKUP_SELECTOR,
undefined,
undefined,
(moduleRef) => this._moduleRef = moduleRef);
}
public hideError() {
this.showError('');
}
public showError(err: string) {
this.showError(err);
}
/* Overwrite escape key behavior */
protected onClose() {
this.close();
}
/**
* Clean up the module and DOM element and close the dialog
*/
public close() {
this.hide();
}
public dispose(): void {
super.dispose();
if (this._moduleRef) {
this._moduleRef.destroy();
}
}
/**
* Open the dialog
*/
public open(connection: IConnectionProfile) {
this._backupTitle = 'Backup database - ' + connection.serverName + ':' + connection.databaseName;
this.title = this._backupTitle;
this.show();
}
protected layout(height?: number): void {
// Nothing currently laid out in this class
}
}

View File

@@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.backup-path-list {
overflow-x: auto;
}
.backup-path-table {
border: 0px;
border-collapse: collapse;
border-spacing: 0px;
}
.backup-path-button {
width: 22px;
}
.backup-dialog {
height: 100%
}
.backup-dialog .advanced-main-header {
padding-top: 20px;
}
.backup-dialog .advanced-header {
padding-top: 15px;
font-size: 14px;
}
.backup-dialog input[type="checkbox"] {
margin-left: 0px;
}
.backup-dialog input[type="radio"] {
margin-top: -2px;
vertical-align: middle;
}
.backup-dialog .indent {
margin-left: 7px;
}
.backup-dialog .radio-indent {
margin-left: 2px;
}
.backup-dialog .option {
width: 100%;
padding-bottom: 7px;
}
.backup-dialog .option.check {
display: flex;
padding-bottom: 4px;
}
.backup-dialog .check {
display: flex;
}
.backup-dialog .icon.warning {
width: 15px;
height: 15px;
float: left;
}
.backup-dialog .warning-message{
padding-left: 20px;
}

View File

@@ -13,7 +13,7 @@ import Severity from 'vs/base/common/severity';
import { IDialogService, IConfirmation, IConfirmationResult } from 'vs/platform/dialogs/common/dialogs';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { QueryInput } from 'sql/parts/query/common/queryInput';
import { EditDataInput } from 'sql/parts/editData/common/editDataInput';
import { EditDataInput } from 'sql/workbench/parts/editData/common/editDataInput';
import { DashboardInput } from 'sql/workbench/parts/dashboard/dashboardInput';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';

View File

@@ -11,7 +11,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { DashboardTab } from 'sql/workbench/parts/dashboard/common/interfaces';
import { TabConfig } from 'sql/workbench/parts/dashboard/common/dashboardWidget';
import { ModelViewContent } from 'sql/parts/modelComponents/modelViewContent.component';
import { ModelViewContent } from 'sql/workbench/electron-browser/modelComponents/modelViewContent.component';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
@Component({

View File

@@ -13,7 +13,7 @@ import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServic
import * as azdata from 'azdata';
import { memoize } from 'vs/base/common/decorators';
import { AgentViewComponent } from '../../../../parts/jobManagement/agent/agentView.component';
import { AgentViewComponent } from '../../jobManagement/electron-browser/agentView.component';
@Component({
templateUrl: decodeURI(require.toUrl('sql/workbench/parts/dashboard/contents/controlHostContent.component.html')),

View File

@@ -41,25 +41,25 @@ import { DashboardModelViewContainer } from 'sql/workbench/parts/dashboard/conta
import { DashboardErrorContainer } from 'sql/workbench/parts/dashboard/containers/dashboardErrorContainer.component';
import { DashboardNavSection } from 'sql/workbench/parts/dashboard/containers/dashboardNavSection.component';
import { WidgetContent } from 'sql/workbench/parts/dashboard/contents/widgetContent.component';
import { ModelViewContent } from 'sql/parts/modelComponents/modelViewContent.component';
import { ModelComponentWrapper } from 'sql/parts/modelComponents/modelComponentWrapper.component';
import { ModelViewContent } from 'sql/workbench/electron-browser/modelComponents/modelViewContent.component';
import { ModelComponentWrapper } from 'sql/workbench/electron-browser/modelComponents/modelComponentWrapper.component';
import { WebviewContent } from 'sql/workbench/parts/dashboard/contents/webviewContent.component';
import { BreadcrumbComponent } from 'sql/base/browser/ui/breadcrumb/breadcrumb.component';
import { IBreadcrumbService } from 'sql/base/browser/ui/breadcrumb/interfaces';
import { DashboardHomeContainer } from 'sql/workbench/parts/dashboard/containers/dashboardHomeContainer.component';
import { ControlHostContent } from 'sql/workbench/parts/dashboard/contents/controlHostContent.component';
import { DashboardControlHostContainer } from 'sql/workbench/parts/dashboard/containers/dashboardControlHostContainer.component';
import { JobsViewComponent } from 'sql/parts/jobManagement/views/jobsView.component';
import { AgentViewComponent } from 'sql/parts/jobManagement/agent/agentView.component';
import { AlertsViewComponent } from 'sql/parts/jobManagement/views/alertsView.component';
import { JobHistoryComponent } from 'sql/parts/jobManagement/views/jobHistory.component';
import { OperatorsViewComponent } from 'sql/parts/jobManagement/views/operatorsView.component';
import { ProxiesViewComponent } from 'sql/parts/jobManagement/views/proxiesView.component';
import { JobsViewComponent } from 'sql/workbench/parts/jobManagement/electron-browser/jobsView.component';
import { AgentViewComponent } from 'sql/workbench/parts/jobManagement/electron-browser/agentView.component';
import { AlertsViewComponent } from 'sql/workbench/parts/jobManagement/electron-browser/alertsView.component';
import { JobHistoryComponent } from 'sql/workbench/parts/jobManagement/electron-browser/jobHistory.component';
import { OperatorsViewComponent } from 'sql/workbench/parts/jobManagement/electron-browser/operatorsView.component';
import { ProxiesViewComponent } from 'sql/workbench/parts/jobManagement/electron-browser/proxiesView.component';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox.component';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox.component';
import { EditableDropDown } from 'sql/base/browser/ui/editableDropdown/editableDropdown.component';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox.component';
import LoadingSpinner from 'sql/parts/modelComponents/loadingSpinner.component';
import LoadingSpinner from 'sql/workbench/electron-browser/modelComponents/loadingSpinner.component';
const baseComponents = [DashboardHomeContainer, DashboardComponent, DashboardWidgetWrapper, DashboardWebviewContainer,
DashboardWidgetContainer, DashboardGridContainer, DashboardErrorContainer, DashboardNavSection, ModelViewContent, WebviewContent, WidgetContent,
@@ -84,7 +84,7 @@ import { ExplorerWidget } from 'sql/workbench/parts/dashboard/widgets/explorer/e
import { TasksWidget } from 'sql/workbench/parts/dashboard/widgets/tasks/tasksWidget.component';
import { InsightsWidget } from 'sql/workbench/parts/dashboard/widgets/insights/insightsWidget.component';
import { WebviewWidget } from 'sql/workbench/parts/dashboard/widgets/webview/webviewWidget.component';
import { JobStepsViewComponent } from 'sql/parts/jobManagement/views/jobStepsView.component';
import { JobStepsViewComponent } from 'sql/workbench/parts/jobManagement/electron-browser/jobStepsView.component';
import { IInstantiationService, _util } from 'vs/platform/instantiation/common/instantiation';
const widgetComponents = [

View File

@@ -0,0 +1,268 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Action, IActionItem, IActionRunner } from 'vs/base/common/actions';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { EventEmitter } from 'sql/base/common/eventEmitter';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { EditDataEditor } from 'sql/workbench/parts/editData/browser/editDataEditor';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { INotificationService } from 'vs/platform/notification/common/notification';
import Severity from 'vs/base/common/severity';
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
const $ = dom.$;
/**
* Action class that edit data based actions will extend
*/
export abstract class EditDataAction extends Action {
private _classes: string[];
constructor(protected editor: EditDataEditor, id: string, enabledClass: string,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService) {
super(id);
this.enabled = true;
this.setClass(enabledClass);
}
/**
* This method is executed when the button is clicked.
*/
public abstract run(): Promise<void>;
protected setClass(enabledClass: string): void {
this._classes = [];
if (enabledClass) {
this._classes.push(enabledClass);
}
this.class = this._classes.join(' ');
}
/**
* Returns the URI of the given editor if it is not undefined and is connected.
*/
public isConnected(editor: EditDataEditor): boolean {
if (!editor || !editor.uri) {
return false;
}
return this._connectionManagementService.isConnected(editor.uri);
}
}
/**
* Action class that refreshes the table for an edit data session
*/
export class RefreshTableAction extends EditDataAction {
private static EnabledClass = 'start';
public static ID = 'refreshTableAction';
constructor(editor: EditDataEditor,
@IQueryModelService private _queryModelService: IQueryModelService,
@IConnectionManagementService _connectionManagementService: IConnectionManagementService,
@INotificationService private _notificationService: INotificationService,
) {
super(editor, RefreshTableAction.ID, RefreshTableAction.EnabledClass, _connectionManagementService);
this.label = nls.localize('editData.run', 'Run');
}
public run(): Promise<void> {
if (this.isConnected(this.editor)) {
let input = this.editor.editDataInput;
let rowLimit: number = undefined;
let queryString: string = undefined;
if (input.queryPaneEnabled) {
queryString = input.queryString = this.editor.getEditorText();
} else {
rowLimit = input.rowLimit;
}
this._queryModelService.disposeEdit(input.uri).then((result) => {
this._queryModelService.initializeEdit(input.uri, input.schemaName, input.tableName, input.objectType, rowLimit, queryString);
input.showResultsEditor();
}, error => {
this._notificationService.notify({
severity: Severity.Error,
message: nls.localize('disposeEditFailure', 'Dispose Edit Failed With Error: ') + error
});
});
}
return Promise.resolve(null);
}
}
/**
* Action class that cancels the refresh data trigger in an edit data session
*/
export class StopRefreshTableAction extends EditDataAction {
private static EnabledClass = 'stop';
public static ID = 'stopRefreshAction';
constructor(editor: EditDataEditor,
@IQueryModelService private _queryModelService: IQueryModelService,
@IConnectionManagementService _connectionManagementService: IConnectionManagementService
) {
super(editor, StopRefreshTableAction.ID, StopRefreshTableAction.EnabledClass, _connectionManagementService);
this.enabled = false;
this.label = nls.localize('editData.stop', 'Stop');
}
public run(): Promise<void> {
let input = this.editor.editDataInput;
this._queryModelService.disposeEdit(input.uri);
return Promise.resolve(null);
}
}
/**
* Action class that is tied with ChangeMaxRowsActionItem
*/
export class ChangeMaxRowsAction extends EditDataAction {
private static EnabledClass = '';
public static ID = 'changeMaxRowsAction';
constructor(editor: EditDataEditor,
@IQueryModelService private _queryModelService: IQueryModelService,
@IConnectionManagementService _connectionManagementService: IConnectionManagementService
) {
super(editor, ChangeMaxRowsAction.ID, undefined, _connectionManagementService);
this.enabled = false;
this.class = ChangeMaxRowsAction.EnabledClass;
}
public run(): Promise<void> {
return Promise.resolve(null);
}
}
/*
* Action item that handles the dropdown (combobox) that lists the avaliable number of row selections
* for an edit data session
*/
export class ChangeMaxRowsActionItem extends EventEmitter implements IActionItem {
public actionRunner: IActionRunner;
public defaultRowCount: number;
private container: HTMLElement;
private start: HTMLElement;
private selectBox: SelectBox;
private toDispose: IDisposable[];
private context: any;
private _options: string[];
private _currentOptionsIndex: number;
constructor(
private _editor: EditDataEditor,
@IContextViewService contextViewService: IContextViewService,
@IThemeService private _themeService: IThemeService) {
super();
this._options = ['200', '1000', '10000'];
this._currentOptionsIndex = 0;
this.toDispose = [];
this.selectBox = new SelectBox(this._options, this._options[this._currentOptionsIndex], contextViewService);
this._registerListeners();
this._refreshOptions();
this.defaultRowCount = Number(this._options[this._currentOptionsIndex]);
this.toDispose.push(attachSelectBoxStyler(this.selectBox, _themeService));
}
public render(container: HTMLElement): void {
this.container = container;
this.selectBox.render(dom.append(container, $('.configuration.listDatabasesSelectBox')));
}
public setActionContext(context: any): void {
this.context = context;
}
public isEnabled(): boolean {
return true;
}
public enable(): void {
this.selectBox.enable();
}
public disable(): void {
this.selectBox.disable();
}
public set setCurrentOptionIndex(selection: number) {
this._currentOptionsIndex = this._options.findIndex(x => x === selection.toString());
this._refreshOptions();
}
public focus(): void {
this.start.focus();
}
public blur(): void {
this.container.blur();
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
}
private _refreshOptions(databaseIndex?: number): void {
this.selectBox.setOptions(this._options, this._currentOptionsIndex);
}
private _registerListeners(): void {
this.toDispose.push(this.selectBox.onDidSelect(selection => {
this._currentOptionsIndex = this._options.findIndex(x => x === selection.selected);
this._editor.editDataInput.onRowDropDownSet(Number(selection.selected));
}));
this.toDispose.push(attachSelectBoxStyler(this.selectBox, this._themeService));
}
}
/**
* Action class that is tied with toggling the Query editor
*/
export class ShowQueryPaneAction extends EditDataAction {
private static EnabledClass = 'filterLabel';
public static ID = 'showQueryPaneAction';
private readonly showSqlLabel = nls.localize('editData.showSql', 'Show SQL Pane');
private readonly closeSqlLabel = nls.localize('editData.closeSql', 'Close SQL Pane');
constructor(editor: EditDataEditor,
@IQueryModelService private _queryModelService: IQueryModelService,
@IConnectionManagementService _connectionManagementService: IConnectionManagementService
) {
super(editor, ShowQueryPaneAction.ID, ShowQueryPaneAction.EnabledClass, _connectionManagementService);
this.label = this.showSqlLabel;
}
public set queryPaneEnabled(value: boolean) {
this.updateLabel(value);
}
private updateLabel(queryPaneEnabled: boolean): void {
if (queryPaneEnabled) {
this.label = this.closeSqlLabel;
} else {
this.label = this.showSqlLabel;
}
}
public run(): Promise<void> {
this.editor.toggleQueryPane();
this.updateLabel(this.editor.queryPaneEnabled());
return Promise.resolve(null);
}
}

View File

@@ -0,0 +1,737 @@
/*---------------------------------------------------------------------------------------------
* 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!sql/parts/query/editor/media/queryEditor';
import * as strings from 'vs/base/common/strings';
import * as DOM from 'vs/base/browser/dom';
import * as nls from 'vs/nls';
import { EditorOptions, EditorInput, IEditorControl, IEditor } from 'vs/workbench/common/editor';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { EditDataInput } from 'sql/workbench/parts/editData/common/editDataInput';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import * as queryContext from 'sql/parts/query/common/queryContext';
import { Taskbar, ITaskbarContent } from 'sql/base/browser/ui/taskbar/taskbar';
import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action } from 'vs/base/common/actions';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { IEditorDescriptorService } from 'sql/workbench/services/queryEditor/common/editorDescriptorService';
import {
RefreshTableAction, StopRefreshTableAction, ChangeMaxRowsAction, ChangeMaxRowsActionItem, ShowQueryPaneAction
} from 'sql/workbench/parts/editData/browser/editDataActions';
import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { IFlexibleSash, HorizontalFlexibleSash } from 'sql/parts/query/views/flexibleSash';
import { EditDataResultsEditor } from 'sql/workbench/parts/editData/browser/editDataResultsEditor';
import { EditDataResultsInput } from 'sql/workbench/parts/editData/common/editDataResultsInput';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
/**
* Editor that hosts an action bar and a resultSetInput for an edit data session
*/
export class EditDataEditor extends BaseEditor {
public static ID: string = 'workbench.editor.editDataEditor';
// The minimum width/height of the editors hosted in the QueryEditor
private readonly _minEditorSize: number = 220;
private _sash: IFlexibleSash;
private _dimension: DOM.Dimension;
private _resultsEditor: EditDataResultsEditor;
private _resultsEditorContainer: HTMLElement;
private _sqlEditor: TextResourceEditor;
private _sqlEditorContainer: HTMLElement;
private _taskbar: Taskbar;
private _taskbarContainer: HTMLElement;
private _changeMaxRowsActionItem: ChangeMaxRowsActionItem;
private _stopRefreshTableAction: StopRefreshTableAction;
private _refreshTableAction: RefreshTableAction;
private _changeMaxRowsAction: ChangeMaxRowsAction;
private _showQueryPaneAction: ShowQueryPaneAction;
private _spinnerElement: HTMLElement;
private _initialized: boolean = false;
private _queryEditorVisible: IContextKey<boolean>;
private hideQueryResultsView = false;
constructor(
@ITelemetryService _telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IEditorService private _editorService: IEditorService,
@IQueryModelService private _queryModelService: IQueryModelService,
@IEditorDescriptorService private _editorDescriptorService: IEditorDescriptorService,
@IContextKeyService contextKeyService: IContextKeyService,
@IStorageService storageService: IStorageService
) {
super(EditDataEditor.ID, _telemetryService, themeService, storageService);
if (contextKeyService) {
this._queryEditorVisible = queryContext.QueryEditorVisibleContext.bindTo(contextKeyService);
}
if (_editorService) {
_editorService.overrideOpenEditor((editor, options, group) => {
if (this.isVisible() && (editor !== this.input || group !== this.group)) {
this.saveEditorViewState();
}
return {};
});
}
}
// PUBLIC METHODS ////////////////////////////////////////////////////////////
// Getters and Setters
public get editDataInput(): EditDataInput { return <EditDataInput>this.input; }
public get tableName(): string { return this.editDataInput.tableName; }
public get uri(): string { return this.input ? this.editDataInput.uri.toString() : undefined; }
public set resultsEditorVisibility(isVisible: boolean) {
let input: EditDataInput = <EditDataInput>this.input;
input.results.visible = isVisible;
}
/**
* Called to indicate to the editor that the input should be cleared and resources associated with the
* input should be freed.
*/
public clearInput(): void {
if (this._resultsEditor) {
this._resultsEditor.clearInput();
}
if (this._sqlEditor) {
this._sqlEditor.clearInput();
}
this._disposeEditors();
super.clearInput();
}
public close(): void {
this.editDataInput.close();
}
/**
* Called to create the editor in the parent element.
*/
public createEditor(parent: HTMLElement): void {
const parentElement = parent;
DOM.addClass(parentElement, 'side-by-side-editor');
this._createTaskbar(parentElement);
}
public dispose(): void {
this._disposeEditors();
super.dispose();
}
/**
* Sets focus on this editor. Specifically, it sets the focus on the hosted text editor.
*/
public focus(): void {
if (this._sqlEditor) {
this._sqlEditor.focus();
}
}
public getControl(): IEditorControl {
if (this._sqlEditor) {
return this._sqlEditor.getControl();
}
return null;
}
public getEditorText(): string {
if (this._sqlEditor && this._sqlEditor.getControl()) {
let control = this._sqlEditor.getControl();
let codeEditor: ICodeEditor = <ICodeEditor>control;
if (codeEditor) {
let value = codeEditor.getModel().getValue();
if (value !== undefined && value.length > 0) {
return value;
}
}
}
return '';
}
/**
* Hide the spinner element to show that something was happening, hidden by default
*/
public hideSpinner(): void {
this._spinnerElement.style.visibility = 'hidden';
}
/**
* Updates the internal variable keeping track of the editor's size, and re-calculates the sash position.
* To be called when the container of this editor changes size.
*/
public layout(dimension: DOM.Dimension): void {
this._dimension = dimension;
if (this._sash) {
this._setSashDimension();
this._sash.layout();
}
this._doLayout();
this._resizeGridContents();
}
/**
* Sets this editor and the sub-editors to visible.
*/
public setEditorVisible(visible: boolean, group: IEditorGroup): void {
if (this._resultsEditor) {
this._resultsEditor.setVisible(visible, group);
}
if (this._sqlEditor) {
this._sqlEditor.setVisible(visible, group);
}
super.setEditorVisible(visible, group);
// Note: must update after calling super.setEditorVisible so that the accurate count is handled
this._updateQueryEditorVisible(visible);
}
/**
* Sets the input data for this editor.
*/
public setInput(newInput: EditDataInput, options?: EditorOptions): Promise<void> {
let oldInput = <EditDataInput>this.input;
if (!newInput.setup) {
this._initialized = false;
this._register(newInput.updateTaskbarEvent((owner) => this._updateTaskbar(owner)));
this._register(newInput.editorInitializingEvent((initializing) => this._onEditorInitializingChanged(initializing)));
this._register(newInput.showResultsEditorEvent(() => this._showResultsEditor()));
newInput.onRowDropDownSet(this._changeMaxRowsActionItem.defaultRowCount);
newInput.setupComplete();
}
return super.setInput(newInput, options, CancellationToken.None)
.then(() => this._updateInput(oldInput, newInput, options));
}
/**
* Show the spinner element that shows something is happening, hidden by default
*/
public showSpinner(): void {
setTimeout(() => {
if (!this._initialized) {
this._spinnerElement.style.visibility = 'visible';
}
}, 200);
}
public toggleResultsEditorVisibility(): void {
let input = <EditDataInput>this.input;
let hideResults = this.hideQueryResultsView;
this.hideQueryResultsView = !this.hideQueryResultsView;
if (!input.results) {
return;
}
this.resultsEditorVisibility = hideResults;
this._doLayout();
}
// PRIVATE METHODS ////////////////////////////////////////////////////////////
private _createEditor(editorInput: EditorInput, container: HTMLElement): Promise<BaseEditor> {
const descriptor = this._editorDescriptorService.getEditor(editorInput);
if (!descriptor) {
return Promise.reject(new Error(strings.format('Can not find a registered editor for the input {0}', editorInput)));
}
let editor = descriptor.instantiate(this._instantiationService);
editor.create(container);
editor.setVisible(this.isVisible(), editor.group);
return Promise.resolve(editor);
}
/**
* Appends the HTML for the EditDataResultsEditor to the EditDataEditor. If the HTML has not yet been
* created, it creates it and appends it. If it has already been created, it locates it and
* appends it.
*/
private _createResultsEditorContainer() {
this._createSash();
const parentElement = this.getContainer();
let input = <EditDataInput>this.input;
if (!input.results.container) {
this._resultsEditorContainer = DOM.append(parentElement, DOM.$('.editDataContainer-horizontal'));
this._resultsEditorContainer.style.position = 'absolute';
input.results.container = this._resultsEditorContainer;
} else {
this._resultsEditorContainer = DOM.append(parentElement, input.results.container);
}
}
/**
* Creates the sash with the requested orientation and registers sash callbacks
*/
private _createSash(): void {
if (!this._sash) {
let parentElement: HTMLElement = this.getContainer();
this._sash = this._register(new HorizontalFlexibleSash(parentElement, this._minEditorSize));
this._setSashDimension();
this._register(this._sash.onPositionChange(position => this._doLayout()));
}
this._sash.show();
}
/**
* Appends the HTML for the SQL editor. Creates new HTML every time.
*/
private _createSqlEditorContainer() {
const parentElement = this.getContainer();
this._sqlEditorContainer = DOM.append(parentElement, DOM.$('.details-editor-container'));
this._sqlEditorContainer.style.position = 'absolute';
}
private _createTaskbar(parentElement: HTMLElement): void {
// Create QueryTaskbar
this._taskbarContainer = DOM.append(parentElement, DOM.$('div'));
this._taskbar = new Taskbar(this._taskbarContainer, {
actionItemProvider: (action: Action) => this._getChangeMaxRowsAction(action)
});
// Create Actions for the toolbar
this._refreshTableAction = this._instantiationService.createInstance(RefreshTableAction, this);
this._stopRefreshTableAction = this._instantiationService.createInstance(StopRefreshTableAction, this);
this._changeMaxRowsAction = this._instantiationService.createInstance(ChangeMaxRowsAction, this);
this._showQueryPaneAction = this._instantiationService.createInstance(ShowQueryPaneAction, this);
// Create HTML Elements for the taskbar
let separator = Taskbar.createTaskbarSeparator();
let textSeparator = Taskbar.createTaskbarText(nls.localize('maxRowTaskbar', 'Max Rows:'));
this._spinnerElement = Taskbar.createTaskbarSpinner();
// Set the content in the order we desire
let content: ITaskbarContent[] = [
{ action: this._refreshTableAction },
{ action: this._stopRefreshTableAction },
{ element: separator },
{ element: textSeparator },
{ action: this._changeMaxRowsAction },
{ action: this._showQueryPaneAction },
{ element: this._spinnerElement }
];
this._taskbar.setContent(content);
}
/**
* Gets the IActionItem for the list of row number drop down
*/
private _getChangeMaxRowsAction(action: Action): IActionItem {
let actionID = ChangeMaxRowsAction.ID;
if (action.id === actionID) {
if (!this._changeMaxRowsActionItem) {
this._changeMaxRowsActionItem = this._instantiationService.createInstance(ChangeMaxRowsActionItem, this);
}
return this._changeMaxRowsActionItem;
}
return null;
}
private _disposeEditors(): void {
if (this._sqlEditor) {
this._sqlEditor.dispose();
this._sqlEditor = null;
}
if (this._resultsEditor) {
this._resultsEditor.dispose();
this._resultsEditor = null;
}
let thisEditorParent: HTMLElement = this.getContainer();
if (this._sqlEditorContainer) {
let sqlEditorParent: HTMLElement = this._sqlEditorContainer.parentElement;
if (sqlEditorParent && sqlEditorParent === thisEditorParent) {
this._sqlEditorContainer.parentElement.removeChild(this._sqlEditorContainer);
}
this._sqlEditorContainer = null;
}
if (this._resultsEditorContainer) {
let resultsEditorParent: HTMLElement = this._resultsEditorContainer.parentElement;
if (resultsEditorParent && resultsEditorParent === thisEditorParent) {
this._resultsEditorContainer.parentElement.removeChild(this._resultsEditorContainer);
}
this._resultsEditorContainer = null;
this.hideQueryResultsView = false;
}
}
private _doLayout(skipResizeGridContent: boolean = false): void {
if (!this._isResultsEditorVisible() && this._sqlEditor) {
this._doLayoutSql();
return;
}
if (!this._sqlEditor || !this._resultsEditor || !this._dimension || !this._sash) {
return;
}
this._doLayoutHorizontal();
if (!skipResizeGridContent) {
this._resizeGridContents();
}
}
private _doLayoutHorizontal(): void {
let splitPointTop: number = this._sash.getSplitPoint();
let parent: ClientRect = this.getContainer().getBoundingClientRect();
let sqlEditorHeight: number;
let sqlEditorTop: number;
let resultsEditorHeight: number;
let resultsEditorTop: number;
let editorTopOffset = parent.top + this._getTaskBarHeight();
this._resultsEditorContainer.hidden = false;
let titleBar = document.getElementById('workbench.parts.titlebar');
if (this.queryPaneEnabled()) {
this._sqlEditorContainer.hidden = false;
sqlEditorTop = editorTopOffset;
sqlEditorHeight = splitPointTop - sqlEditorTop;
resultsEditorTop = splitPointTop;
resultsEditorHeight = parent.bottom - resultsEditorTop;
if (titleBar) {
sqlEditorHeight += DOM.getContentHeight(titleBar);
}
} else {
this._sqlEditorContainer.hidden = true;
sqlEditorTop = editorTopOffset;
sqlEditorHeight = 0;
resultsEditorTop = editorTopOffset;
resultsEditorHeight = parent.bottom - resultsEditorTop;
if (titleBar) {
resultsEditorHeight += DOM.getContentHeight(titleBar);
}
}
this._sqlEditorContainer.style.height = `${sqlEditorHeight}px`;
this._sqlEditorContainer.style.width = `${this._dimension.width}px`;
this._sqlEditorContainer.style.top = `${sqlEditorTop}px`;
this._resultsEditorContainer.style.height = `${resultsEditorHeight}px`;
this._resultsEditorContainer.style.width = `${this._dimension.width}px`;
this._resultsEditorContainer.style.top = `${resultsEditorTop}px`;
this._sqlEditor.layout(new DOM.Dimension(this._dimension.width, sqlEditorHeight));
this._resultsEditor.layout(new DOM.Dimension(this._dimension.width, resultsEditorHeight));
}
private _doLayoutSql() {
if (this._resultsEditorContainer) {
this._resultsEditorContainer.style.width = '0px';
this._resultsEditorContainer.style.height = '0px';
this._resultsEditorContainer.style.left = '0px';
this._resultsEditorContainer.hidden = true;
}
if (this._dimension) {
let sqlEditorHeight: number;
if (this.queryPaneEnabled()) {
this._sqlEditorContainer.hidden = false;
sqlEditorHeight = this._dimension.height - this._getTaskBarHeight();
} else {
this._sqlEditorContainer.hidden = true;
sqlEditorHeight = 0;
}
this._sqlEditorContainer.style.height = `${sqlEditorHeight}px`;
this._sqlEditorContainer.style.width = `${this._dimension.width}px`;
this._sqlEditor.layout(new DOM.Dimension(this._dimension.width, sqlEditorHeight));
}
}
private _getTaskBarHeight(): number {
let taskBarElement = this._taskbar.getContainer();
return DOM.getContentHeight(taskBarElement);
}
/**
* Returns true if the results table for the current edit data session is visible
* Public for testing only.
*/
private _isResultsEditorVisible(): boolean {
let input: EditDataInput = <EditDataInput>this.input;
if (!input) {
return false;
}
return input.results.visible;
}
private _onEditorInitializingChanged(initializing: boolean): void {
if (initializing) {
this.showSpinner();
} else {
this._initialized = true;
this.hideSpinner();
}
}
/**
* Sets input for the results editor after it has been created.
*/
private _onResultsEditorCreated(resultsEditor: EditDataResultsEditor, resultsInput: EditDataResultsInput, options: EditorOptions): Promise<void> {
this._resultsEditor = resultsEditor;
return this._resultsEditor.setInput(resultsInput, options);
}
/**
* Sets input for the SQL editor after it has been created.
*/
private _onSqlEditorCreated(sqlEditor: TextResourceEditor, sqlInput: UntitledEditorInput, options: EditorOptions): Thenable<void> {
this._sqlEditor = sqlEditor;
return this._sqlEditor.setInput(sqlInput, options, CancellationToken.None);
}
private _resizeGridContents(): void {
if (this._isResultsEditorVisible()) {
let queryInput: EditDataInput = <EditDataInput>this.input;
let uri: string = queryInput.uri;
if (uri) {
this._queryModelService.resizeResultsets(uri);
}
}
}
/**
* Handles setting input and creating editors when this QueryEditor is either:
* - Opened for the first time
* - Opened with a new EditDataInput
*/
private _setNewInput(newInput: EditDataInput, options?: EditorOptions): Promise<any> {
// Promises that will ensure proper ordering of editor creation logic
let createEditors: () => Promise<any>;
let onEditorsCreated: (result) => Promise<any>;
// If both editors exist, create joined promises - one for each editor
if (this._isResultsEditorVisible()) {
createEditors = () => {
return Promise.all([
this._createEditor(<EditDataResultsInput>newInput.results, this._resultsEditorContainer),
this._createEditor(<UntitledEditorInput>newInput.sql, this._sqlEditorContainer)
]);
};
onEditorsCreated = (result: IEditor[]) => {
return Promise.all([
this._onResultsEditorCreated(<EditDataResultsEditor>result[0], newInput.results, options),
this._onSqlEditorCreated(<TextResourceEditor>result[1], newInput.sql, options)
]);
};
// If only the sql editor exists, create a promise and wait for the sql editor to be created
} else {
createEditors = () => {
return this._createEditor(<UntitledEditorInput>newInput.sql, this._sqlEditorContainer);
};
onEditorsCreated = (result: TextResourceEditor) => {
return Promise.all([
this._onSqlEditorCreated(result, newInput.sql, options)
]);
};
}
// Create a promise to re render the layout after the editor creation logic
let doLayout: () => Promise<any> = () => {
this._doLayout();
return Promise.resolve(undefined);
};
// Run all three steps synchronously
return createEditors()
.then(onEditorsCreated)
.then(doLayout)
.then(() => {
if (newInput.results) {
newInput.results.onRestoreViewStateEmitter.fire();
}
if (newInput.savedViewState) {
this._sqlEditor.getControl().restoreViewState(newInput.savedViewState);
}
});
}
private _setSashDimension(): void {
if (!this._dimension) {
return;
}
this._sash.setDimenesion(this._dimension);
}
/**
* Makes visible the results table for the current edit data session
*/
private _showResultsEditor(): void {
if (this._isResultsEditorVisible()) {
return;
}
//this._editorGroupService.pinEditor(this.position, this.input);
let input = <EditDataInput>this.input;
this._createResultsEditorContainer();
this._createEditor(<EditDataResultsInput>input.results, this._resultsEditorContainer)
.then(result => {
this._onResultsEditorCreated(<EditDataResultsEditor>result, input.results, this.options);
this.resultsEditorVisibility = true;
this.hideQueryResultsView = false;
this._doLayout(true);
});
}
/**
* Handles setting input for this editor. If this new input does not match the old input (e.g. a new file
* has been opened with the same editor, or we are opening the editor for the first time).
*/
private _updateInput(oldInput: EditDataInput, newInput: EditDataInput, options?: EditorOptions): Promise<void> {
if (this._sqlEditor) {
this._sqlEditor.clearInput();
}
if (oldInput) {
this._disposeEditors();
}
this._createSqlEditorContainer();
if (this._isResultsEditorVisible()) {
this._createResultsEditorContainer();
let uri: string = newInput.uri;
if (uri) {
this._queryModelService.refreshResultsets(uri);
}
}
if (this._sash) {
if (this._isResultsEditorVisible()) {
this._sash.show();
} else {
this._sash.hide();
}
}
this._updateTaskbar(newInput);
return this._setNewInput(newInput, options);
}
private _updateQueryEditorVisible(currentEditorIsVisible: boolean): void {
if (this._queryEditorVisible) {
let visible = currentEditorIsVisible;
if (!currentEditorIsVisible) {
// Current editor is closing but still tracked as visible. Check if any other editor is visible
const candidates = [...this._editorService.visibleControls].filter(e => {
if (e && e.getId) {
return e.getId() === EditDataEditor.ID;
}
return false;
});
// Note: require 2 or more candidates since current is closing but still
// counted as visible
visible = candidates.length > 1;
}
this._queryEditorVisible.set(visible);
}
}
private _updateTaskbar(owner: EditDataInput): void {
// Update the taskbar if the owner of this call is being presented
if (owner.matches(this.editDataInput)) {
this._refreshTableAction.enabled = owner.refreshButtonEnabled;
this._stopRefreshTableAction.enabled = owner.stopButtonEnabled;
this._changeMaxRowsActionItem.setCurrentOptionIndex = owner.rowLimit;
this._showQueryPaneAction.queryPaneEnabled = owner.queryPaneEnabled;
}
}
/**
* Calls the run method of this editor's RunQueryAction
*/
public runQuery(): void {
this._refreshTableAction.run();
}
/**
* Calls the run method of this editor's CancelQueryAction
*/
public cancelQuery(): void {
this._stopRefreshTableAction.run();
}
public toggleQueryPane(): void {
this.editDataInput.queryPaneEnabled = !this.queryPaneEnabled();
if (this.queryPaneEnabled()) {
this._showQueryEditor();
} else {
this._hideQueryEditor();
}
this._doLayout(false);
}
private _showQueryEditor(): void {
this._sqlEditorContainer.hidden = false;
this._changeMaxRowsActionItem.disable();
}
private _hideQueryEditor(): void {
this._sqlEditorContainer.hidden = true;
this._changeMaxRowsActionItem.enable();
}
public queryPaneEnabled(): boolean {
return this.editDataInput.queryPaneEnabled;
}
private saveEditorViewState(): void {
let editDataInput = this.input as EditDataInput;
if (editDataInput) {
if (this._sqlEditor) {
editDataInput.savedViewState = this._sqlEditor.getControl().saveViewState();
}
if (editDataInput.results) {
editDataInput.results.onSaveViewStateEmitter.fire();
}
}
}
}

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { EditorOptions } from 'vs/workbench/common/editor';
import { getZoomLevel } from 'vs/base/browser/browser';
import { Configuration } from 'vs/editor/browser/config/configuration';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import * as types from 'vs/base/common/types';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { bootstrapAngular } from 'sql/platform/bootstrap/node/bootstrapService';
import { BareResultsGridInfo } from 'sql/parts/query/editor/queryResultsEditor';
import { IEditDataComponentParams } from 'sql/platform/bootstrap/node/bootstrapParams';
import { EditDataModule } from 'sql/workbench/parts/grid/views/editData/editData.module';
import { EDITDATA_SELECTOR } from 'sql/workbench/parts/grid/views/editData/editData.component';
import { EditDataResultsInput } from 'sql/workbench/parts/editData/common/editDataResultsInput';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService } from 'vs/platform/storage/common/storage';
export class EditDataResultsEditor extends BaseEditor {
public static ID: string = 'workbench.editor.editDataResultsEditor';
public static AngularSelectorString: string = 'slickgrid-container.slickgridContainer';
protected _input: EditDataResultsInput;
protected _rawOptions: BareResultsGridInfo;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IQueryModelService private _queryModelService: IQueryModelService,
@IConfigurationService private _configurationService: IConfigurationService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService
) {
super(EditDataResultsEditor.ID, telemetryService, themeService, storageService);
this._rawOptions = BareResultsGridInfo.createFromRawSettings(this._configurationService.getValue('resultsGrid'), getZoomLevel());
this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('resultsGrid')) {
this._rawOptions = BareResultsGridInfo.createFromRawSettings(this._configurationService.getValue('resultsGrid'), getZoomLevel());
this._applySettings();
}
});
}
public get input(): EditDataResultsInput {
return this._input;
}
public createEditor(parent: HTMLElement): void {
}
public dispose(): void {
super.dispose();
}
public layout(dimension: DOM.Dimension): void {
}
public setInput(input: EditDataResultsInput, options: EditorOptions): Promise<void> {
super.setInput(input, options, CancellationToken.None);
this._applySettings();
if (!input.hasBootstrapped) {
this._bootstrapAngular();
}
return Promise.resolve<void>(null);
}
private _applySettings() {
if (this.input && this.input.container) {
Configuration.applyFontInfoSlow(this.getContainer(), this._rawOptions);
if (!this.input.css) {
this.input.css = DOM.createStyleSheet(this.input.container);
}
let cssRuleText = '';
if (types.isNumber(this._rawOptions.cellPadding)) {
cssRuleText = this._rawOptions.cellPadding + 'px';
} else {
cssRuleText = this._rawOptions.cellPadding.join('px ') + 'px;';
}
let content = `.grid .slick-cell { padding: ${cssRuleText}; }`;
this.input.css.innerHTML = content;
}
}
/**
* Load the angular components and record for this input that we have done so
*/
private _bootstrapAngular(): void {
let input = <EditDataResultsInput>this.input;
let uri = input.uri;
// Pass the correct DataService to the new angular component
let dataService = this._queryModelService.getDataService(uri);
if (!dataService) {
throw new Error('DataService not found for URI: ' + uri);
}
// Mark that we have bootstrapped
input.setBootstrappedTrue();
// Get the bootstrap params and perform the bootstrap
// Note: pass in input so on disposal this is cleaned up.
// Otherwise many components will be left around and be subscribed
// to events from the backing data service
const parent = input.container;
let params: IEditDataComponentParams = {
dataService: dataService,
onSaveViewState: input.onSaveViewStateEmitter.event,
onRestoreViewState: input.onRestoreViewStateEmitter.event
};
bootstrapAngular(this._instantiationService,
EditDataModule,
parent,
EDITDATA_SELECTOR,
params,
input);
}
}

View File

@@ -0,0 +1,242 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EditorInput, EditorModel, ConfirmResult, EncodingMode } from 'vs/workbench/common/editor';
import { IConnectionManagementService, IConnectableInput, INewConnectionParams } from 'sql/platform/connection/common/connectionManagement';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { dispose } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { EditSessionReadyParams } from 'azdata';
import { URI } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import { INotificationService } from 'vs/platform/notification/common/notification';
import Severity from 'vs/base/common/severity';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { EditDataResultsInput } from 'sql/workbench/parts/editData/common/editDataResultsInput';
import { IEditorViewState } from 'vs/editor/common/editorCommon';
/**
* Input for the EditDataEditor.
*/
export class EditDataInput extends EditorInput implements IConnectableInput {
public static ID: string = 'workbench.editorinputs.editDataInput';
private _hasBootstrapped: boolean;
private _editorContainer: HTMLElement;
private _updateTaskbar: Emitter<EditDataInput>;
private _editorInitializing: Emitter<boolean>;
private _showResultsEditor: Emitter<EditDataInput>;
private _refreshButtonEnabled: boolean;
private _stopButtonEnabled: boolean;
private _setup: boolean;
private _rowLimit: number;
private _objectType: string;
private _css: HTMLStyleElement;
private _useQueryFilter: boolean;
public savedViewState: IEditorViewState;
constructor(
private _uri: URI,
private _schemaName,
private _tableName,
private _sql: UntitledEditorInput,
private _queryString: string,
private _results: EditDataResultsInput,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IQueryModelService private _queryModelService: IQueryModelService,
@INotificationService private notificationService: INotificationService
) {
super();
this._hasBootstrapped = false;
this._updateTaskbar = new Emitter<EditDataInput>();
this._showResultsEditor = new Emitter<EditDataInput>();
this._editorInitializing = new Emitter<boolean>();
this._setup = false;
this._stopButtonEnabled = false;
this._refreshButtonEnabled = false;
this._toDispose = [];
this._useQueryFilter = false;
// re-emit sql editor events through this editor if it exists
if (this._sql) {
this._toDispose.push(this._sql.onDidChangeDirty(() => this._onDidChangeDirty.fire()));
this._sql.disableSaving();
}
this.disableSaving();
//TODO determine is this is a table or a view
this._objectType = 'TABLE';
// Attach to event callbacks
if (this._queryModelService) {
let self = this;
// Register callbacks for the Actions
this._toDispose.push(
this._queryModelService.onRunQueryStart(uri => {
if (self.uri === uri) {
self.initEditStart();
}
})
);
this._toDispose.push(
this._queryModelService.onEditSessionReady((result) => {
if (self.uri === result.ownerUri) {
self.initEditEnd(result);
}
})
);
}
}
// Getters/Setters
public get tableName(): string { return this._tableName; }
public get schemaName(): string { return this._schemaName; }
public get uri(): string { return this._uri.toString(); }
public get sql(): UntitledEditorInput { return this._sql; }
public get results(): EditDataResultsInput { return this._results; }
public getResultsInputResource(): string { return this._results.uri; }
public get updateTaskbarEvent(): Event<EditDataInput> { return this._updateTaskbar.event; }
public get editorInitializingEvent(): Event<boolean> { return this._editorInitializing.event; }
public get showResultsEditorEvent(): Event<EditDataInput> { return this._showResultsEditor.event; }
public get stopButtonEnabled(): boolean { return this._stopButtonEnabled; }
public get refreshButtonEnabled(): boolean { return this._refreshButtonEnabled; }
public get container(): HTMLElement { return this._editorContainer; }
public get hasBootstrapped(): boolean { return this._hasBootstrapped; }
public get setup(): boolean { return this._setup; }
public get rowLimit(): number { return this._rowLimit; }
public get objectType(): string { return this._objectType; }
public showResultsEditor(): void { this._showResultsEditor.fire(undefined); }
public isDirty(): boolean { return false; }
public save(): Promise<boolean> { return Promise.resolve(false); }
public confirmSave(): Promise<ConfirmResult> { return Promise.resolve(ConfirmResult.DONT_SAVE); }
public getTypeId(): string { return EditDataInput.ID; }
public setBootstrappedTrue(): void { this._hasBootstrapped = true; }
public getResource(): URI { return this._uri; }
public supportsSplitEditor(): boolean { return false; }
public setupComplete() { this._setup = true; }
public get queryString(): string {
return this._queryString;
}
public set queryString(queryString: string) {
this._queryString = queryString;
}
public get css(): HTMLStyleElement {
return this._css;
}
public set css(css: HTMLStyleElement) {
this._css = css;
}
public get queryPaneEnabled(): boolean {
return this._useQueryFilter;
}
public set queryPaneEnabled(useQueryFilter: boolean) {
this._useQueryFilter = useQueryFilter;
}
// State Update Callbacks
public initEditStart(): void {
this._editorInitializing.fire(true);
this._refreshButtonEnabled = false;
this._stopButtonEnabled = true;
this._updateTaskbar.fire(this);
}
public initEditEnd(result: EditSessionReadyParams): void {
this._refreshButtonEnabled = true;
this._stopButtonEnabled = false;
if (!result.success) {
this.notificationService.notify({
severity: Severity.Error,
message: result.message
});
}
this._editorInitializing.fire(false);
this._updateTaskbar.fire(this);
}
public onConnectStart(): void {
// TODO: Indicate connection started
}
public onConnectReject(error?: string): void {
if (error) {
this.notificationService.notify({
severity: Severity.Error,
message: nls.localize('connectionFailure', 'Edit Data Session Failed To Connect')
});
}
}
public onConnectCanceled(): void {
}
public onConnectSuccess(params?: INewConnectionParams): void {
let rowLimit: number = undefined;
let queryString: string = undefined;
if (this._useQueryFilter) {
queryString = this._queryString;
} else {
rowLimit = this._rowLimit;
}
this._queryModelService.initializeEdit(this.uri, this.schemaName, this.tableName, this._objectType, rowLimit, queryString);
this.showResultsEditor();
this._onDidChangeLabel.fire();
}
public onDisconnect(): void {
// TODO: deal with disconnections
}
public onRowDropDownSet(rows: number) {
this._rowLimit = rows;
}
// Boiler Plate Functions
public matches(otherInput: any): boolean {
if (otherInput instanceof EditDataInput) {
return this._sql.matches(otherInput.sql);
}
return this._sql.matches(otherInput);
}
public dispose(): void {
this._queryModelService.disposeQuery(this.uri);
this._sql.dispose();
this._results.dispose();
this._toDispose = dispose(this._toDispose);
super.dispose();
}
public close(): void {
// Dispose our edit session then disconnect our input
this._queryModelService.disposeEdit(this.uri).then(() => {
return this._connectionManagementService.disconnectEditor(this, true);
}).then(() => {
this.dispose();
});
}
public get tabColor(): string {
return this._connectionManagementService.getTabColorForUri(this.uri);
}
public get onDidModelChangeContent(): Event<void> { return this._sql.onDidModelChangeContent; }
public get onDidModelChangeEncoding(): Event<void> { return this._sql.onDidModelChangeEncoding; }
public resolve(refresh?: boolean): Promise<EditorModel> { return this._sql.resolve(); }
public getEncoding(): string { return this._sql.getEncoding(); }
public suggestFileName(): string { return this._sql.suggestFileName(); }
public getName(): string { return this._sql.getName(); }
public get hasAssociatedFilePath(): boolean { return this._sql.hasAssociatedFilePath; }
public setEncoding(encoding: string, mode: EncodingMode /* ignored, we only have Encode */): void {
this._sql.setEncoding(encoding, mode);
}
}

View File

@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EditorInput } from 'vs/workbench/common/editor';
import { Emitter } from 'vs/base/common/event';
/**
* Input for the EditDataResultsEditor. This input helps with logic for the viewing and editing of
* data in the results grid.
*/
export class EditDataResultsInput extends EditorInput {
// Tracks if the editor that holds this input should be visible (i.e. true if a query has been run)
private _visible: boolean;
// Tracks if the editor has holds this input has has bootstrapped angular yet
private _hasBootstrapped: boolean;
// Holds the HTML content for the editor when the editor discards this input and loads another
private _editorContainer: HTMLElement;
public css: HTMLStyleElement;
public readonly onRestoreViewStateEmitter = new Emitter<void>();
public readonly onSaveViewStateEmitter = new Emitter<void>();
constructor(private _uri: string) {
super();
this._visible = false;
this._hasBootstrapped = false;
}
getTypeId(): string {
return EditDataResultsInput.ID;
}
matches(other: any): boolean {
if (other instanceof EditDataResultsInput) {
return (other._uri === this._uri);
}
return false;
}
resolve(refresh?: boolean): Promise<any> {
return Promise.resolve(null);
}
supportsSplitEditor(): boolean {
return false;
}
public setBootstrappedTrue(): void {
this._hasBootstrapped = true;
}
public dispose(): void {
this._disposeContainer();
super.dispose();
}
private _disposeContainer() {
if (!this._editorContainer) {
return;
}
let parentContainer = this._editorContainer.parentNode;
if (parentContainer) {
parentContainer.removeChild(this._editorContainer);
this._editorContainer = null;
}
}
//// Properties
static get ID() {
return 'workbench.editorinputs.editDataResultsInput';
}
set container(container: HTMLElement) {
this._disposeContainer();
this._editorContainer = container;
}
get container(): HTMLElement {
return this._editorContainer;
}
get hasBootstrapped(): boolean {
return this._hasBootstrapped;
}
get visible(): boolean {
return this._visible;
}
set visible(visible: boolean) {
this._visible = visible;
}
get uri(): string {
return this._uri;
}
}

View File

@@ -6,7 +6,7 @@
import * as GridContentEvents from 'sql/workbench/parts/grid/common/gridContentEvents';
import { IQueryModelService } from 'sql/platform/query/common/queryModel';
import { QueryEditor } from 'sql/parts/query/editor/queryEditor';
import { EditDataEditor } from 'sql/parts/editData/editor/editDataEditor';
import { EditDataEditor } from 'sql/workbench/parts/editData/browser/editDataEditor';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';

View File

@@ -0,0 +1,45 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div id="agentViewDiv" class="fullsize">
<panel class="dashboard-panel" [options]="panelOpt">
<tab [title]="jobsComponentTitle" class="fullsize" [identifier]="jobsTabIdentifier"
[iconClass]="jobsIconClass">
<ng-template>
<div id="jobsDiv" class="fullsize" *ngIf="showHistory === false">
<jobsview-component></jobsview-component>
</div>
<div id="historyDiv" class="fullsize" *ngIf="showHistory === true">
<jobhistory-component></jobhistory-component>
</div>
</ng-template>
</tab>
<tab [title]="alertsComponentTitle" class="fullsize" [identifier]="alertsTabIdentifier"
[iconClass]="alertsIconClass">
<ng-template>
<div id="alertsDiv" class="fullsize">
<jobalertsview-component></jobalertsview-component>
</div>
</ng-template>
</tab>
<tab [title]="operatorsComponentTitle" class="fullsize" [identifier]="operatorsTabIdentifier"
[iconClass]="operatorsIconClass">
<ng-template>
<div id="operatorsDiv" class="fullsize">
<joboperatorsview-component></joboperatorsview-component>
</div>
</ng-template>
</tab>
<tab [title]="proxiesComponentTitle" class="fullsize" [identifier]="proxiesTabIdentifier"
[iconClass]="proxiesIconClass">
<ng-template>
<div id="proxiesDiv" class="fullsize">
<jobproxiesview-component></jobproxiesview-component>
</div>
</ng-template>
</tab>
</panel>
</div>

View File

@@ -0,0 +1,119 @@
/*---------------------------------------------------------------------------------------------
* 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/jobs';
import * as nls from 'vs/nls';
import { Component, Inject, forwardRef, ChangeDetectorRef, ViewChild, Injectable } from '@angular/core';
import { AgentJobInfo } from 'azdata';
import { PanelComponent, IPanelOptions, NavigationBarLayout } from 'sql/base/browser/ui/panel/panel.component';
import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces';
import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService';
export const DASHBOARD_SELECTOR: string = 'agentview-component';
@Component({
selector: DASHBOARD_SELECTOR,
templateUrl: decodeURI(require.toUrl('./agentView.component.html'))
})
@Injectable()
export class AgentViewComponent {
@ViewChild(PanelComponent) private _panel: PanelComponent;
private _showHistory: boolean = false;
private _jobId: string = null;
private _agentJobInfo: AgentJobInfo = null;
private _refresh: boolean = undefined;
private _expanded: Map<string, string>;
public jobsIconClass: string = 'jobsview-icon';
public alertsIconClass: string = 'alertsview-icon';
public proxiesIconClass: string = 'proxiesview-icon';
public operatorsIconClass: string = 'operatorsview-icon';
private readonly jobsComponentTitle: string = nls.localize('jobview.Jobs', "Jobs");
private readonly alertsComponentTitle: string = nls.localize('jobview.Alerts', "Alerts");
private readonly proxiesComponentTitle: string = nls.localize('jobview.Proxies', "Proxies");
private readonly operatorsComponentTitle: string = nls.localize('jobview.Operators', "Operators");
// tslint:disable-next-line:no-unused-variable
private readonly panelOpt: IPanelOptions = {
showTabsWhenOne: true,
layout: NavigationBarLayout.vertical,
showIcon: true
};
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(IJobManagementService) jobManagementService: IJobManagementService,
@Inject(IDashboardService) dashboardService: IDashboardService, ) {
this._expanded = new Map<string, string>();
let self = this;
jobManagementService.onDidChange((args) => {
self.refresh = true;
self._cd.detectChanges();
});
}
/**
* Public Getters
*/
public get jobId(): string {
return this._jobId;
}
public get showHistory(): boolean {
return this._showHistory;
}
public get agentJobInfo(): AgentJobInfo {
return this._agentJobInfo;
}
public get refresh(): boolean {
return this._refresh;
}
public get expanded(): Map<string, string> {
return this._expanded;
}
/**
* Public Setters
*/
public set jobId(value: string) {
this._jobId = value;
}
public set showHistory(value: boolean) {
this._showHistory = value;
this._cd.detectChanges();
}
public set agentJobInfo(value: AgentJobInfo) {
this._agentJobInfo = value;
}
public set refresh(value: boolean) {
this._refresh = value;
this._cd.detectChanges();
}
public setExpanded(jobId: string, errorMessage: string) {
this._expanded.set(jobId, errorMessage);
}
public set expanded(value: Map<string, string>) {
this._expanded = value;
}
public layout() {
this._panel.layout();
}
}

View File

@@ -0,0 +1,15 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="job-heading-container">
<h1 class="job-heading" *ngIf="_isCloud === false">Alerts</h1>
<h1 class="job-heading" *ngIf="_isCloud === true">No Alerts Available</h1>
<div class="icon in-progress" *ngIf="_showProgressWheel === true"></div>
</div>
<div #actionbarContainer class="agent-actionbar-container"></div>
<div #jobalertsgrid class="jobalertsview-grid"></div>

View File

@@ -0,0 +1,226 @@
/*---------------------------------------------------------------------------------------------
* 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/jobs';
import * as dom from 'vs/base/browser/dom';
import * as nls from 'vs/nls';
import * as azdata from 'azdata';
import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { Table } from 'sql/base/browser/ui/table/table';
import { AgentViewComponent } from 'sql/workbench/parts/jobManagement/electron-browser/agentView.component';
import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces';
import { EditAlertAction, DeleteAlertAction, NewAlertAction } from 'sql/platform/jobManagement/common/jobActions';
import { JobManagementView } from 'sql/workbench/parts/jobManagement/electron-browser/jobManagementView';
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IAction } from 'vs/base/common/actions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService';
import { AlertsCacheObject } from 'sql/platform/jobManagement/common/jobManagementService';
import { RowDetailView } from 'sql/base/browser/ui/table/plugins/rowDetailView';
export const VIEW_SELECTOR: string = 'jobalertsview-component';
export const ROW_HEIGHT: number = 45;
@Component({
selector: VIEW_SELECTOR,
templateUrl: decodeURI(require.toUrl('./alertsView.component.html')),
providers: [{ provide: TabChild, useExisting: forwardRef(() => AlertsViewComponent) }],
})
export class AlertsViewComponent extends JobManagementView implements OnInit, OnDestroy {
private columns: Array<Slick.Column<any>> = [
{
name: nls.localize('jobAlertColumns.name', 'Name'),
field: 'name',
formatter: (row, cell, value, columnDef, dataContext) => this.renderName(row, cell, value, columnDef, dataContext),
width: 500,
id: 'name'
},
{ name: nls.localize('jobAlertColumns.lastOccurrenceDate', 'Last Occurrence'), field: 'lastOccurrenceDate', width: 150, id: 'lastOccurrenceDate' },
{ name: nls.localize('jobAlertColumns.enabled', 'Enabled'), field: 'enabled', width: 80, id: 'enabled' },
{ name: nls.localize('jobAlertColumns.delayBetweenResponses', 'Delay Between Responses (in secs)'), field: 'delayBetweenResponses', width: 200, id: 'delayBetweenResponses' },
{ name: nls.localize('jobAlertColumns.categoryName', 'Category Name'), field: 'categoryName', width: 250, id: 'categoryName' },
];
private options: Slick.GridOptions<any> = {
syncColumnCellResize: true,
enableColumnReorder: false,
rowHeight: ROW_HEIGHT,
enableCellNavigation: true,
editable: false
};
private dataView: any;
private _isCloud: boolean;
private _alertsCacheObject: AlertsCacheObject;
private _didTabChange: boolean;
@ViewChild('jobalertsgrid') _gridEl: ElementRef;
public alerts: azdata.AgentAlertInfo[];
public contextAction = NewAlertAction;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
@Inject(forwardRef(() => AgentViewComponent)) _agentViewComponent: AgentViewComponent,
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService,
@Inject(ICommandService) private _commandService: ICommandService,
@Inject(IInstantiationService) instantiationService: IInstantiationService,
@Inject(forwardRef(() => CommonServiceInterface)) commonService: CommonServiceInterface,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) _dashboardService: IDashboardService) {
super(commonService, _dashboardService, contextMenuService, keybindingService, instantiationService, _agentViewComponent);
this._didTabChange = false;
this._isCloud = commonService.connectionManagementService.connectionInfo.serverInfo.isCloud;
let alertsCacheObjectMap = this._jobManagementService.alertsCacheObjectMap;
let alertsCache = alertsCacheObjectMap[this._serverName];
if (alertsCache) {
this._alertsCacheObject = alertsCache;
} else {
this._alertsCacheObject = new AlertsCacheObject();
this._alertsCacheObject.serverName = this._serverName;
this._jobManagementService.addToCache(this._serverName, this._alertsCacheObject);
}
}
ngOnInit() {
// set base class elements
this._visibilityElement = this._gridEl;
this._parentComponent = this._agentViewComponent;
}
ngOnDestroy() {
this._didTabChange = true;
}
public layout() {
let height = dom.getContentHeight(this._gridEl.nativeElement) - 10;
if (height < 0) {
height = 0;
}
if (this._table) {
this._table.layout(new dom.Dimension(
dom.getContentWidth(this._gridEl.nativeElement),
height));
}
}
onFirstVisible() {
let self = this;
let cached: boolean = false;
if (this._alertsCacheObject.serverName === this._serverName) {
if (this._alertsCacheObject.alerts && this._alertsCacheObject.alerts.length > 0) {
cached = true;
this.alerts = this._alertsCacheObject.alerts;
}
}
let columns = this.columns.map((column) => {
column.rerenderOnResize = true;
return column;
});
this.dataView = new Slick.Data.DataView({ inlineFilters: false });
let rowDetail = new RowDetailView({
cssClass: '_detail_selector',
useRowClick: false,
panelRows: 1
});
columns.unshift(rowDetail.getColumnDefinition());
jQuery(this._gridEl.nativeElement).empty();
jQuery(this.actionBarContainer.nativeElement).empty();
this.initActionBar();
this._table = new Table(this._gridEl.nativeElement, { columns }, this.options);
this._table.grid.setData(this.dataView, true);
this._register(this._table.onContextMenu(e => {
self.openContextMenu(e);
}));
// check for cached state
if (cached && this._agentViewComponent.refresh !== true) {
self.onAlertsAvailable(this.alerts);
this._showProgressWheel = false;
if (this.isVisible) {
this._cd.detectChanges();
}
} else {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
this._jobManagementService.getAlerts(ownerUri).then((result) => {
if (result && result.alerts) {
self.alerts = result.alerts;
self._alertsCacheObject.alerts = result.alerts;
self.onAlertsAvailable(result.alerts);
} else {
// TODO: handle error
}
this._showProgressWheel = false;
if (this.isVisible && !this._didTabChange) {
this._cd.detectChanges();
} else if (this._didTabChange) {
return;
}
});
}
}
private onAlertsAvailable(alerts: azdata.AgentAlertInfo[]) {
let items: any = alerts.map((item) => {
return {
id: item.id,
name: item.name,
lastOccurrenceDate: item.lastOccurrenceDate,
enabled: item.isEnabled,
delayBetweenResponses: item.delayBetweenResponses,
categoryName: item.categoryName
};
});
this.dataView.beginUpdate();
this.dataView.setItems(items);
this.dataView.endUpdate();
this._alertsCacheObject.dataview = this.dataView;
this._table.autosizeColumns();
this._table.resizeCanvas();
}
protected getTableActions(targetObject: any): IAction[] {
let actions: IAction[] = [];
actions.push(this._instantiationService.createInstance(EditAlertAction));
actions.push(this._instantiationService.createInstance(DeleteAlertAction));
return actions;
}
protected getCurrentTableObject(rowIndex: number): any {
let targetObject = {
alertInfo: this.alerts && this.alerts.length >= rowIndex ? this.alerts[rowIndex] : undefined
};
return targetObject;
}
private renderName(row, cell, value, columnDef, dataContext) {
let resultIndicatorClass = dataContext.enabled ? 'alertview-alertnameindicatorenabled' :
'alertview-alertnameindicatordisabled';
return '<table class="alertview-alertnametable"><tr class="alertview-alertnamerow">' +
'<td nowrap class=' + resultIndicatorClass + '></td>' +
'<td nowrap class="alertview-alertnametext">' + dataContext.name + '</td>' +
'</tr></table>';
}
public openCreateAlertDialog() {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
this._commandService.executeCommand('agent.openAlertDialog', ownerUri, null, null);
}
}

View File

@@ -0,0 +1,164 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="jobhistory-heading-container">
<h1 class="job-heading">Jobs | {{this._agentJobInfo?.name}} </h1>
<div class="icon in-progress" *ngIf="showProgressWheel()"></div>
</div>
<!-- Back -->
<div class="all-jobs">
<div class="back-button-icon" (click)="goToJobs()"></div>All Jobs
</div>
<!-- Actions -->
<div #actionbarContainer class="agent-actionbar-container"></div>
<!-- Overview -->
<div class="overview-container">
<div class="overview-tab" (click)='toggleCollapse()' tabindex="0">
<input id="accordion" type="checkbox">
<label for="accordion">
<div class="resultsViewCollapsible collapsed" (click)='toggleCollapse()'></div>
Overview
</label>
<div class="accordion-content">
<table align='left'>
<tr>
<td id='col1'>
Category:
</td>
<td id='col2'>
{{this._agentJobInfo?.category}}
</td>
<td id='col3'>
Enabled:
</td>
<td id='col4'>
{{this._agentJobInfo?.enabled}}
</td>
</tr>
<tr>
<td id='col1'>
Has Alert:
</td>
<td id='col2'>
{{this._agentJobInfo?.hasTarget}}
</td>
<td id='col3'>
Has Schedule:
</td>
<td id='col4'>
{{this._agentJobInfo?.hasSchedule}}
</td>
</tr>
<tr>
<td id='col1'>
Last Run:
</td>
<td id='col2'>
{{this._agentJobInfo?.lastRun}}
</td>
<td id='col3'>
Next Run:
</td>
<td id='col4'>
{{this._agentJobInfo?.nextRun}}
</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Job History details -->
<div class='history-details'>
<!-- Previous run list -->
<div class="prev-run-list-container" style="min-width: 270px">
<table *ngIf="_showPreviousRuns === true">
<tr>
<td class="date-column">
<b>Date</b>
</td>
<td>
<b>Status</b>
</td>
</tr>
</table>
<h3 *ngIf="_showPreviousRuns === false" style="text-align: center">No Previous Runs Available</h3>
<div class="step-table prev-run-list" style="position: relative; width: 100%">
<div #table style="position: absolute; width: 100%; height: 100%"></div>
</div>
</div>
<!-- Job Steps -->
<div class="job-steps" id="job-steps">
<h1 class="job-heading">
{{agentJobHistoryInfo?.runDate}}
</h1>
<table class="step-list">
<tr class="step-row">
<td height="20">
<h3>Status:</h3>
</td>
<td height="20">
<h3>{{_runStatus}}</h3>
</td>
</tr>
<tr class="step-row">
<td height="20">
Job ID:
</td>
<td height="20" style="user-select: initial">
{{agentJobHistoryInfo?.jobId || agentJobInfo?.jobId}}
</td>
</tr>
<tr class="step-row">
<td height="20">
Message:
</td>
<td height="20" style="user-select: initial">
{{agentJobHistoryInfo?.message}}
</td>
</tr>
<tr class="step-row">
<td height="20">
Duration:
</td>
<td height="20">
{{agentJobHistoryInfo?.runDuration}}
</td>
</tr>
<tr class="step-row">
<td height="20">
Server:
</td>
<td>
{{agentJobHistoryInfo?.server}}
</td>
</tr>
<tr class="step-row">
<td height="20">
SQL message ID:
</td>
<td height="20">
{{agentJobHistoryInfo?.sqlMessageId}}
</td>
</tr>
<tr class="step-row">
<td height="20">
Retries Attempted:
</td>
<td height="20">
{{agentJobHistoryInfo?.retriesAttempted}}
</td>
</tr>
</table>
<div #jobsteps *ngIf="showSteps === true" style="flex: 1 1 auto; position: relative">
<jobstepsview-component *ngIf="showSteps === true"></jobstepsview-component>
</div>
<h3 *ngIf="showSteps === false">No Steps Available</h3>
</div>
</div>

View File

@@ -0,0 +1,382 @@
/*---------------------------------------------------------------------------------------------
* 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/jobHistory';
import * as azdata from 'azdata';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { OnInit, Component, Inject, Input, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy, Injectable } from '@angular/core';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { AgentViewComponent } from 'sql/workbench/parts/jobManagement/electron-browser/agentView.component';
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
import { RunJobAction, StopJobAction, EditJobAction, JobsRefreshAction } from 'sql/platform/jobManagement/common/jobActions';
import { JobCacheObject } from 'sql/platform/jobManagement/common/jobManagementService';
import { JobManagementUtilities } from 'sql/platform/jobManagement/common/jobManagementUtilities';
import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces';
import {
JobHistoryController, JobHistoryDataSource,
JobHistoryRenderer, JobHistoryFilter, JobHistoryModel, JobHistoryRow
} from 'sql/workbench/parts/jobManagement/electron-browser/jobHistoryTree';
import { JobStepsViewRow } from 'sql/workbench/parts/jobManagement/electron-browser/jobStepsViewTree';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { JobManagementView } from 'sql/workbench/parts/jobManagement/electron-browser/jobManagementView';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/telemetryKeys';
export const DASHBOARD_SELECTOR: string = 'jobhistory-component';
@Component({
selector: DASHBOARD_SELECTOR,
templateUrl: decodeURI(require.toUrl('./jobHistory.component.html')),
providers: [{ provide: TabChild, useExisting: forwardRef(() => JobHistoryComponent) }],
changeDetection: ChangeDetectionStrategy.OnPush
})
@Injectable()
export class JobHistoryComponent extends JobManagementView implements OnInit {
private _tree: Tree;
private _treeController: JobHistoryController;
private _treeDataSource: JobHistoryDataSource;
private _treeRenderer: JobHistoryRenderer;
private _treeFilter: JobHistoryFilter;
@ViewChild('table') private _tableContainer: ElementRef;
@ViewChild('jobsteps') private _jobStepsView: ElementRef;
@Input() public agentJobInfo: azdata.AgentJobInfo = undefined;
@Input() public agentJobHistories: azdata.AgentJobHistoryInfo[] = undefined;
public agentJobHistoryInfo: azdata.AgentJobHistoryInfo = undefined;
private _isVisible: boolean = false;
private _stepRows: JobStepsViewRow[] = [];
private _showSteps: boolean = undefined;
private _showPreviousRuns: boolean = undefined;
private _runStatus: string = undefined;
private _jobCacheObject: JobCacheObject;
private _agentJobInfo: azdata.AgentJobInfo;
private _noJobsAvailable: boolean = false;
private static readonly HEADING_HEIGHT: number = 24;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(forwardRef(() => CommonServiceInterface)) commonService: CommonServiceInterface,
@Inject(forwardRef(() => AgentViewComponent)) _agentViewComponent: AgentViewComponent,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) dashboardService: IDashboardService,
@Inject(ITelemetryService) private _telemetryService: ITelemetryService
) {
super(commonService, dashboardService, contextMenuService, keybindingService, instantiationService, _agentViewComponent);
this._treeController = new JobHistoryController();
this._treeDataSource = new JobHistoryDataSource();
this._treeRenderer = new JobHistoryRenderer();
this._treeFilter = new JobHistoryFilter();
let jobCacheObjectMap = this._jobManagementService.jobCacheObjectMap;
this._serverName = commonService.connectionManagementService.connectionInfo.connectionProfile.serverName;
let jobCache = jobCacheObjectMap[this._serverName];
if (jobCache) {
this._jobCacheObject = jobCache;
} else {
this._jobCacheObject = new JobCacheObject();
this._jobCacheObject.serverName = this._serverName;
this._jobManagementService.addToCache(this._serverName, this._jobCacheObject);
}
}
ngOnInit() {
// set base class elements
this._visibilityElement = this._tableContainer;
this._parentComponent = this._agentViewComponent;
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
this._agentJobInfo = this._agentViewComponent.agentJobInfo;
const self = this;
this._treeController.onClick = (tree, element, event, origin = 'mouse') => {
const payload = { origin: origin };
const isDoubleClick = (origin === 'mouse' && event.detail === 2);
// Cancel Event
const isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown';
if (!isMouseDown) {
event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise
}
event.stopPropagation();
tree.setFocus(element, payload);
if (element && isDoubleClick) {
event.preventDefault(); // focus moves to editor, we need to prevent default
} else {
tree.setFocus(element, payload);
tree.setSelection([element], payload);
if (element.rowID) {
self.setStepsTree(element);
} else {
event.preventDefault();
}
}
return true;
};
this._treeController.onKeyDown = (tree, event) => {
this._treeController.onKeyDownWrapper(tree, event);
let element = tree.getFocus();
if (element) {
self.setStepsTree(element);
}
return true;
};
this._tree = new Tree(this._tableContainer.nativeElement, {
controller: this._treeController,
dataSource: this._treeDataSource,
filter: this._treeFilter,
renderer: this._treeRenderer
}, { verticalScrollMode: ScrollbarVisibility.Visible });
this._register(attachListStyler(this._tree, this.themeService));
this._tree.layout(dom.getContentHeight(this._tableContainer.nativeElement));
this._telemetryService.publicLog(TelemetryKeys.JobHistoryView);
this.initActionBar();
}
private loadHistory() {
const self = this;
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
let jobName = this._agentViewComponent.agentJobInfo.name;
let jobId = this._agentViewComponent.jobId;
this._jobManagementService.getJobHistory(ownerUri, jobId, jobName).then((result) => {
if (result && result.histories) {
self._jobCacheObject.setJobHistory(jobId, result.histories);
self._jobCacheObject.setJobAlerts(jobId, result.alerts);
self._jobCacheObject.setJobSchedules(jobId, result.schedules);
self._jobCacheObject.setJobSteps(jobId, result.steps);
this._agentViewComponent.agentJobInfo.jobSteps = this._jobCacheObject.getJobSteps(jobId);
this._agentViewComponent.agentJobInfo.jobSchedules = this._jobCacheObject.getJobSchedules(jobId);
this._agentViewComponent.agentJobInfo.alerts = this._jobCacheObject.getJobAlerts(jobId);
this._agentJobInfo = this._agentViewComponent.agentJobInfo;
if (result.histories.length > 0) {
self._showPreviousRuns = true;
self.buildHistoryTree(self, result.histories);
if (self._agentViewComponent.showHistory) {
self._cd.detectChanges();
}
} else {
self._jobCacheObject.setJobHistory(self._agentViewComponent.jobId, result.histories);
self._showPreviousRuns = false;
}
} else {
self._showPreviousRuns = false;
self._showSteps = false;
if (self._agentViewComponent.showHistory) {
self._cd.detectChanges();
}
}
});
}
private setStepsTree(element: JobHistoryRow) {
const self = this;
let cachedHistory = self._jobCacheObject.getJobHistory(element.jobID);
if (cachedHistory) {
self.agentJobHistoryInfo = cachedHistory.find(
history => self.formatTime(history.runDate) === self.formatTime(element.runDate));
} else {
self.agentJobHistoryInfo = self._treeController.jobHistories.find(
history => self.formatTime(history.runDate) === self.formatTime(element.runDate));
}
if (self.agentJobHistoryInfo) {
self.agentJobHistoryInfo.runDate = self.formatTime(self.agentJobHistoryInfo.runDate);
if (self.agentJobHistoryInfo.steps) {
let jobStepStatus = this.didJobFail(self.agentJobHistoryInfo);
self._stepRows = self.agentJobHistoryInfo.steps.map(step => {
let stepViewRow = new JobStepsViewRow();
stepViewRow.message = step.message;
stepViewRow.runStatus = jobStepStatus ? JobManagementUtilities.convertToStatusString(0) :
JobManagementUtilities.convertToStatusString(step.runStatus);
self._runStatus = JobManagementUtilities.convertToStatusString(self.agentJobHistoryInfo.runStatus);
stepViewRow.stepName = step.stepDetails.stepName;
stepViewRow.stepId = step.stepDetails.id.toString();
return stepViewRow;
});
self._stepRows.unshift(new JobStepsViewRow());
self._stepRows[0].rowID = 'stepsColumn' + self._agentJobInfo.jobId;
self._stepRows[0].stepId = nls.localize('stepRow.stepID', 'Step ID');
self._stepRows[0].stepName = nls.localize('stepRow.stepName', 'Step Name');
self._stepRows[0].message = nls.localize('stepRow.message', 'Message');
this._showSteps = self._stepRows.length > 1;
} else {
self._showSteps = false;
}
if (self._agentViewComponent.showHistory) {
self._cd.detectChanges();
}
}
}
private didJobFail(job: azdata.AgentJobHistoryInfo): boolean {
for (let i = 0; i < job.steps.length; i++) {
if (job.steps[i].runStatus === 0) {
return true;
}
}
return false;
}
private buildHistoryTree(self: any, jobHistories: azdata.AgentJobHistoryInfo[]) {
self._treeController.jobHistories = jobHistories;
let jobHistoryRows = this._treeController.jobHistories.map(job => self.convertToJobHistoryRow(job));
self._treeDataSource.data = jobHistoryRows;
self._tree.setInput(new JobHistoryModel());
self.agentJobHistoryInfo = self._treeController.jobHistories[0];
if (self.agentJobHistoryInfo) {
self.agentJobHistoryInfo.runDate = self.formatTime(self.agentJobHistoryInfo.runDate);
}
const payload = { origin: 'origin' };
let element = this._treeDataSource.getFirstElement();
this._tree.setFocus(element, payload);
this._tree.setSelection([element], payload);
if (element.rowID) {
self.setStepsTree(element);
}
}
private toggleCollapse(): void {
let arrow: HTMLElement = jQuery('.resultsViewCollapsible').get(0);
let checkbox: any = document.getElementById('accordion');
if (arrow.className === 'resultsViewCollapsible' && checkbox.checked === false) {
arrow.className = 'resultsViewCollapsible collapsed';
} else if (arrow.className === 'resultsViewCollapsible collapsed' && checkbox.checked === true) {
arrow.className = 'resultsViewCollapsible';
}
}
private goToJobs(): void {
this._isVisible = false;
this._agentViewComponent.showHistory = false;
}
private convertToJobHistoryRow(historyInfo: azdata.AgentJobHistoryInfo): JobHistoryRow {
let jobHistoryRow = new JobHistoryRow();
jobHistoryRow.runDate = this.formatTime(historyInfo.runDate);
jobHistoryRow.runStatus = JobManagementUtilities.convertToStatusString(historyInfo.runStatus);
jobHistoryRow.instanceID = historyInfo.instanceId;
jobHistoryRow.jobID = historyInfo.jobId;
return jobHistoryRow;
}
private formatTime(time: string): string {
return time.replace('T', ' ');
}
private showProgressWheel(): boolean {
return this._showPreviousRuns !== true && this._noJobsAvailable === false;
}
public onFirstVisible() {
this._agentJobInfo = this._agentViewComponent.agentJobInfo;
if (!this.agentJobInfo) {
this.agentJobInfo = this._agentJobInfo;
}
if (this.isRefreshing) {
this.loadHistory();
return;
}
let jobHistories = this._jobCacheObject.jobHistories[this._agentViewComponent.jobId];
if (jobHistories && jobHistories.length > 0) {
const self = this;
if (this._jobCacheObject.prevJobID === this._agentViewComponent.jobId || jobHistories[0].jobId === this._agentViewComponent.jobId) {
this._showPreviousRuns = true;
this._agentViewComponent.agentJobInfo.jobSteps = this._jobCacheObject.getJobSteps(this._agentJobInfo.jobId);
this._agentViewComponent.agentJobInfo.jobSchedules = this._jobCacheObject.getJobSchedules(this._agentJobInfo.jobId);
this._agentViewComponent.agentJobInfo.alerts = this._jobCacheObject.getJobAlerts(this._agentJobInfo.jobId);
this._agentJobInfo = this._agentViewComponent.agentJobInfo;
this.buildHistoryTree(self, jobHistories);
this._actionBar.context = { targetObject: this._agentJobInfo, ownerUri: this.ownerUri, jobHistoryComponent: this };
this._cd.detectChanges();
}
} else if (jobHistories && jobHistories.length === 0) {
this._showPreviousRuns = false;
this._showSteps = false;
this._noJobsAvailable = true;
this._cd.detectChanges();
} else {
this.loadHistory();
}
this._jobCacheObject.prevJobID = this._agentViewComponent.jobId;
}
public layout() {
let historyDetails = jQuery('.overview-container').get(0);
let statusBar = jQuery('.part.statusbar').get(0);
if (historyDetails && statusBar) {
let historyBottom = historyDetails.getBoundingClientRect().bottom;
let statusTop = statusBar.getBoundingClientRect().top;
let height: number = statusTop - historyBottom - JobHistoryComponent.HEADING_HEIGHT;
if (this._table) {
this._table.layout(new dom.Dimension(
dom.getContentWidth(this._tableContainer.nativeElement),
height));
}
if (this._tree) {
this._tree.layout(dom.getContentHeight(this._tableContainer.nativeElement));
}
}
}
protected initActionBar() {
let runJobAction = this.instantiationService.createInstance(RunJobAction);
let stopJobAction = this.instantiationService.createInstance(StopJobAction);
let editJobAction = this.instantiationService.createInstance(EditJobAction);
let refreshAction = this.instantiationService.createInstance(JobsRefreshAction);
let taskbar = <HTMLElement>this.actionBarContainer.nativeElement;
this._actionBar = new Taskbar(taskbar);
this._actionBar.context = { targetObject: this._agentJobInfo, ownerUri: this.ownerUri, component: this };
this._actionBar.setContent([
{ action: runJobAction },
{ action: stopJobAction },
{ action: refreshAction },
{ action: editJobAction }
]);
}
/** GETTERS */
public get showSteps(): boolean {
return this._showSteps;
}
public get stepRows() {
return this._stepRows;
}
public get ownerUri(): string {
return this._commonService.connectionManagementService.connectionInfo.ownerUri;
}
public get serverName(): string {
return this._serverName;
}
/** SETTERS */
public set showSteps(value: boolean) {
this._showSteps = value;
this._cd.detectChanges();
}
}

View File

@@ -0,0 +1,168 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as tree from 'vs/base/parts/tree/browser/tree';
import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { generateUuid } from 'vs/base/common/uuid';
import * as DOM from 'vs/base/browser/dom';
import { AgentJobHistoryInfo } from 'azdata';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
export class JobHistoryRow {
runDate: string;
runStatus: string;
instanceID: number;
rowID: string = generateUuid();
jobID: string;
}
// Empty class just for tree input
export class JobHistoryModel {
public static readonly id = generateUuid();
}
export class JobHistoryController extends TreeDefaults.DefaultController {
private _jobHistories: AgentJobHistoryInfo[];
protected onLeftClick(tree: tree.ITree, element: JobHistoryRow, event: IMouseEvent, origin: string = 'mouse'): boolean {
return true;
}
public set jobHistories(value: AgentJobHistoryInfo[]) {
this._jobHistories = value;
}
public get jobHistories(): AgentJobHistoryInfo[] {
return this._jobHistories;
}
public onKeyDownWrapper(tree: tree.ITree, event: IKeyboardEvent): boolean {
if (event.code === 'ArrowDown' || event.keyCode === 40) {
super.onDown(tree, event);
return super.onEnter(tree, event);
} else if (event.code === 'ArrowUp' || event.keyCode === 38) {
super.onUp(tree, event);
return super.onEnter(tree, event);
} else if (event.code !== 'Tab' && event.keyCode !== 2) {
event.preventDefault();
event.stopPropagation();
return true;
}
return false;
}
}
export class JobHistoryDataSource implements tree.IDataSource {
private _data: JobHistoryRow[];
public getId(tree: tree.ITree, element: JobHistoryRow | JobHistoryModel): string {
if (element instanceof JobHistoryModel) {
return JobHistoryModel.id;
} else {
return (element as JobHistoryRow).rowID;
}
}
public hasChildren(tree: tree.ITree, element: JobHistoryRow | JobHistoryModel): boolean {
if (element instanceof JobHistoryModel) {
return true;
} else {
return false;
}
}
public getChildren(tree: tree.ITree, element: JobHistoryRow | JobHistoryModel): Promise<JobHistoryRow[]> {
if (element instanceof JobHistoryModel) {
return Promise.resolve(this._data);
} else {
return Promise.resolve(undefined);
}
}
public getParent(tree: tree.ITree, element: JobHistoryRow | JobHistoryModel): Promise<JobHistoryModel> {
if (element instanceof JobHistoryModel) {
return Promise.resolve(undefined);
} else {
return Promise.resolve(new JobHistoryModel());
}
}
public set data(data: JobHistoryRow[]) {
this._data = data;
}
public getFirstElement() {
return this._data[0];
}
}
export interface IListTemplate {
statusIcon: HTMLElement;
label: HTMLElement;
}
export class JobHistoryRenderer implements tree.IRenderer {
public getHeight(tree: tree.ITree, element: JobHistoryRow): number {
return 30;
}
public getTemplateId(tree: tree.ITree, element: JobHistoryRow | JobHistoryModel): string {
if (element instanceof JobHistoryModel) {
return 'jobHistoryModel';
} else {
return 'jobHistoryInfo';
}
}
public renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement): IListTemplate {
let row = DOM.$('.list-row');
let label = DOM.$('.label');
let statusIcon = this.createStatusIcon();
row.appendChild(statusIcon);
row.appendChild(label);
container.appendChild(row);
return { statusIcon, label };
}
public renderElement(tree: tree.ITree, element: JobHistoryRow, templateId: string, templateData: IListTemplate): void {
templateData.label.innerHTML = element.runDate + '&nbsp;&nbsp;' + element.runStatus;
let statusClass: string;
if (element.runStatus === 'Succeeded') {
statusClass = 'status-icon job-passed';
} else if (element.runStatus === 'Failed') {
statusClass = 'status-icon job-failed';
} else {
statusClass = 'status-icon job-unknown';
}
templateData.statusIcon.className = statusClass;
}
public disposeTemplate(tree: tree.ITree, templateId: string, templateData: IListTemplate): void {
// no op
}
private createStatusIcon(): HTMLElement {
let statusIcon: HTMLElement = DOM.$('div');
return statusIcon;
}
}
export class JobHistoryFilter implements tree.IFilter {
private _filterString: string;
public isVisible(tree: tree.ITree, element: JobHistoryRow): boolean {
return this._isJobVisible();
}
private _isJobVisible(): boolean {
return true;
}
public set filterString(val: string) {
this._filterString = val;
}
}

View File

@@ -0,0 +1,132 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { ElementRef, AfterContentChecked, ViewChild } from '@angular/core';
import { Table } from 'sql/base/browser/ui/table/table';
import { AgentViewComponent } from 'sql/workbench/parts/jobManagement/electron-browser/agentView.component';
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
import { IAction, Action } from 'vs/base/common/actions';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { JobsRefreshAction, IJobActionInfo } from 'sql/platform/jobManagement/common/jobActions';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService';
export abstract class JobManagementView extends TabChild implements AfterContentChecked {
protected isVisible: boolean = false;
protected isInitialized: boolean = false;
protected isRefreshing: boolean = false;
protected _showProgressWheel: boolean;
protected _visibilityElement: ElementRef;
protected _parentComponent: AgentViewComponent;
protected _table: Table<any>;
protected _actionBar: Taskbar;
protected _serverName: string;
public contextAction: any;
@ViewChild('actionbarContainer') protected actionBarContainer: ElementRef;
constructor(
protected _commonService: CommonServiceInterface,
protected _dashboardService: IDashboardService,
protected _contextMenuService: IContextMenuService,
protected _keybindingService: IKeybindingService,
protected _instantiationService: IInstantiationService,
protected _agentViewComponent: AgentViewComponent) {
super();
let self = this;
this._serverName = this._commonService.connectionManagementService.connectionInfo.connectionProfile.serverName;
this._dashboardService.onLayout((d) => {
self.layout();
});
}
ngAfterContentChecked() {
if (this._visibilityElement && this._parentComponent) {
if (this.isVisible === false && this._visibilityElement.nativeElement.offsetParent !== null) {
this.isVisible = true;
if (!this.isInitialized) {
this._showProgressWheel = true;
this.onFirstVisible();
this.layout();
this.isInitialized = true;
}
} else if (this.isVisible === true && this._parentComponent.refresh === true) {
this._showProgressWheel = true;
this.isRefreshing = true;
this.onFirstVisible();
this.layout();
this._parentComponent.refresh = false;
} else if (this.isVisible === true && this._visibilityElement.nativeElement.offsetParent === null) {
this.isVisible = false;
}
}
}
abstract onFirstVisible();
protected openContextMenu(event): void {
let rowIndex = event.cell.row;
let targetObject = this.getCurrentTableObject(rowIndex);
let actions = this.getTableActions(targetObject);
if (actions) {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
let actionContext = {
ownerUri: ownerUri,
targetObject: targetObject
};
let anchor = { x: event.pageX + 1, y: event.pageY };
this._contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => actions,
getKeyBinding: (action) => this._keybindingFor(action),
getActionsContext: () => (actionContext)
});
}
}
protected _keybindingFor(action: IAction): ResolvedKeybinding {
let [kb] = this._keybindingService.lookupKeybindings(action.id);
return kb;
}
protected getTableActions(targetObject?: any): IAction[] {
return undefined;
}
protected getCurrentTableObject(rowIndex: number): JobActionContext {
return undefined;
}
protected initActionBar() {
let refreshAction = this._instantiationService.createInstance(JobsRefreshAction);
let newAction: Action = this._instantiationService.createInstance(this.contextAction);
let taskbar = <HTMLElement>this.actionBarContainer.nativeElement;
this._actionBar = new Taskbar(taskbar);
this._actionBar.setContent([
{ action: refreshAction },
{ action: newAction }
]);
let context: IJobActionInfo = { component: this };
this._actionBar.context = context;
}
public refreshJobs() {
this._agentViewComponent.refresh = true;
}
}
export interface JobActionContext {
canEdit: boolean;
job: azdata.AgentJobInfo;
}

View File

@@ -0,0 +1,14 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="steps-header">
<div class="steps-icon"></div>
<h1 style="display: inline">Steps</h1>
</div>
<div class='steps-tree' style="flex: 1 1 auto; position: relative">
<div #table style="position: absolute; height: 100%; width: 100%" ></div>
</div>

View File

@@ -0,0 +1,120 @@
/*---------------------------------------------------------------------------------------------
* 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/jobStepsView';
import * as dom from 'vs/base/browser/dom';
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, AfterContentChecked } from '@angular/core';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
import {
JobStepsViewController, JobStepsViewDataSource, JobStepsViewFilter,
JobStepsViewRenderer, JobStepsViewModel
} from 'sql/workbench/parts/jobManagement/electron-browser/jobStepsViewTree';
import { JobHistoryComponent } from 'sql/workbench/parts/jobManagement/electron-browser/jobHistory.component';
import { JobManagementView } from 'sql/workbench/parts/jobManagement/electron-browser/jobManagementView';
import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/telemetryKeys';
export const JOBSTEPSVIEW_SELECTOR: string = 'jobstepsview-component';
@Component({
selector: JOBSTEPSVIEW_SELECTOR,
templateUrl: decodeURI(require.toUrl('./jobStepsView.component.html')),
providers: [{ provide: TabChild, useExisting: forwardRef(() => JobStepsViewComponent) }],
})
export class JobStepsViewComponent extends JobManagementView implements OnInit, AfterContentChecked {
private _tree: Tree;
private _treeController = new JobStepsViewController();
private _treeDataSource = new JobStepsViewDataSource();
private _treeRenderer = new JobStepsViewRenderer();
private _treeFilter = new JobStepsViewFilter();
@ViewChild('table') private _tableContainer: ElementRef;
constructor(
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(forwardRef(() => CommonServiceInterface)) commonService: CommonServiceInterface,
@Inject(forwardRef(() => JobHistoryComponent)) private _jobHistoryComponent: JobHistoryComponent,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IInstantiationService) instantiationService: IInstantiationService,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) dashboardService: IDashboardService,
@Inject(ITelemetryService) private _telemetryService: ITelemetryService
) {
super(commonService, dashboardService, contextMenuService, keybindingService, instantiationService, undefined);
}
ngAfterContentChecked() {
jQuery('.steps-tree .step-column-heading').closest('.monaco-tree-row').addClass('step-column-row');
this.layout();
this._tree.onDidScroll(() => {
jQuery('.steps-tree .step-column-heading').closest('.monaco-tree-row').addClass('step-column-row');
});
this._treeController.onClick = (tree, element, event, origin = 'mouse') => {
const payload = { origin: origin };
const isDoubleClick = (origin === 'mouse' && event.detail === 2);
// Cancel Event
const isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown';
if (!isMouseDown) {
event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise
}
event.stopPropagation();
tree.setFocus(element, payload);
if (element && isDoubleClick) {
event.preventDefault(); // focus moves to editor, we need to prevent default
} else {
tree.setFocus(element, payload);
tree.setSelection([element], payload);
}
jQuery('.steps-tree .step-column-heading').closest('.monaco-tree-row').addClass('step-column-row');
return true;
};
this._treeController.onKeyDown = (tree, event) => {
this._treeController.onKeyDownWrapper(tree, event);
jQuery('.steps-tree .step-column-heading').closest('.monaco-tree-row').addClass('step-column-row');
return true;
};
this._tree.onDidFocus(() => {
this._tree.focusNth(1);
let element = this._tree.getFocus();
this._tree.select(element);
});
this._tree.setInput(new JobStepsViewModel());
}
ngOnInit() {
this._treeDataSource.data = this._jobHistoryComponent.stepRows;
this._tree = new Tree(this._tableContainer.nativeElement, {
controller: this._treeController,
dataSource: this._treeDataSource,
filter: this._treeFilter,
renderer: this._treeRenderer
}, { verticalScrollMode: ScrollbarVisibility.Visible, horizontalScrollMode: ScrollbarVisibility.Visible });
this._register(attachListStyler(this._tree, this.themeService));
this._telemetryService.publicLog(TelemetryKeys.JobStepsView);
}
public onFirstVisible() {
}
public layout() {
if (this._tree) {
let treeheight = dom.getContentHeight(this._tableContainer.nativeElement);
this._tree.layout(treeheight);
}
}
}

View File

@@ -0,0 +1,178 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import * as tree from 'vs/base/parts/tree/browser/tree';
import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { generateUuid } from 'vs/base/common/uuid';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
export class JobStepsViewRow {
public stepId: string;
public stepName: string;
public message: string;
public rowID: string = generateUuid();
public runStatus: string;
}
// Empty class just for tree input
export class JobStepsViewModel {
public static readonly id = generateUuid();
}
export class JobStepsViewController extends TreeDefaults.DefaultController {
protected onLeftClick(tree: tree.ITree, element: JobStepsViewRow, event: IMouseEvent, origin: string = 'mouse'): boolean {
return true;
}
public onContextMenu(tree: tree.ITree, element: JobStepsViewRow, event: tree.ContextMenuEvent): boolean {
return true;
}
public onKeyDownWrapper(tree: tree.ITree, event: IKeyboardEvent): boolean {
if (event.code === 'ArrowDown' || event.keyCode === 40) {
super.onDown(tree, event);
return super.onEnter(tree, event);
} else if (event.code === 'ArrowUp' || event.keyCode === 38) {
super.onUp(tree, event);
return super.onEnter(tree, event);
} else if (event.code !== 'Tab' && event.keyCode !== 2) {
event.preventDefault();
event.stopPropagation();
return true;
}
return false;
}
}
export class JobStepsViewDataSource implements tree.IDataSource {
private _data: JobStepsViewRow[];
public getId(tree: tree.ITree, element: JobStepsViewRow | JobStepsViewModel): string {
if (element instanceof JobStepsViewModel) {
return JobStepsViewModel.id;
} else {
return (element as JobStepsViewRow).rowID;
}
}
public hasChildren(tree: tree.ITree, element: JobStepsViewRow | JobStepsViewModel): boolean {
if (element instanceof JobStepsViewModel) {
return true;
} else {
return false;
}
}
public getChildren(tree: tree.ITree, element: JobStepsViewRow | JobStepsViewModel): Promise<JobStepsViewRow[]> {
if (element instanceof JobStepsViewModel) {
return Promise.resolve(this._data);
} else {
return Promise.resolve(undefined);
}
}
public getParent(tree: tree.ITree, element: JobStepsViewRow | JobStepsViewModel): Promise<JobStepsViewModel> {
if (element instanceof JobStepsViewModel) {
return Promise.resolve(undefined);
} else {
return Promise.resolve(new JobStepsViewModel());
}
}
public set data(data: JobStepsViewRow[]) {
this._data = data;
}
}
export interface IListTemplate {
statusIcon: HTMLElement;
label: HTMLElement;
}
export class JobStepsViewRenderer implements tree.IRenderer {
public getHeight(tree: tree.ITree, element: JobStepsViewRow): number {
return 40;
}
public getTemplateId(tree: tree.ITree, element: JobStepsViewRow | JobStepsViewModel): string {
if (element instanceof JobStepsViewModel) {
return 'jobStepsViewModel';
} else {
return 'jobStepsViewRow';
}
}
public renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement): IListTemplate {
let row = DOM.$('.list-row');
let label = DOM.$('.label');
let statusIcon = this.createStatusIcon();
row.appendChild(statusIcon);
row.appendChild(label);
container.appendChild(row);
return { statusIcon, label };
}
public renderElement(tree: tree.ITree, element: JobStepsViewRow, templateId: string, templateData: IListTemplate): void {
let stepIdCol: HTMLElement = DOM.$('div');
stepIdCol.className = 'tree-id-col';
stepIdCol.innerText = element.stepId;
let stepNameCol: HTMLElement = DOM.$('div');
stepNameCol.className = 'tree-name-col';
stepNameCol.innerText = element.stepName;
let stepMessageCol: HTMLElement = DOM.$('div');
stepMessageCol.className = 'tree-message-col';
stepMessageCol.innerText = element.message;
if (element.rowID.includes('stepsColumn')) {
stepNameCol.className += ' step-column-heading';
stepIdCol.className += ' step-column-heading';
stepMessageCol.className += ' step-column-heading';
}
DOM.clearNode(templateData.label);
templateData.label.appendChild(stepIdCol);
templateData.label.appendChild(stepNameCol);
templateData.label.appendChild(stepMessageCol);
if (element.runStatus) {
if (element.runStatus === 'Succeeded') {
templateData.statusIcon.className = 'status-icon step-passed';
} else if (element.runStatus === 'Failed') {
templateData.statusIcon.className = 'status-icon step-failed';
} else {
templateData.statusIcon.className = 'status-icon step-unknown';
}
} else {
templateData.statusIcon.className = '';
}
}
public disposeTemplate(tree: tree.ITree, templateId: string, templateData: IListTemplate): void {
// no op
}
private createStatusIcon(): HTMLElement {
let statusIcon: HTMLElement = DOM.$('div');
return statusIcon;
}
}
export class JobStepsViewFilter implements tree.IFilter {
private _filterString: string;
public isVisible(tree: tree.ITree, element: JobStepsViewRow): boolean {
return this._isJobVisible();
}
private _isJobVisible(): boolean {
return true;
}
public set filterString(val: string) {
this._filterString = val;
}
}

View File

@@ -0,0 +1,15 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="job-heading-container">
<h1 class="job-heading" *ngIf="_isCloud === false">Jobs</h1>
<h1 class="job-heading" *ngIf="_isCloud === true">No Jobs Available</h1>
<div class="icon in-progress" *ngIf="_showProgressWheel === true"></div>
</div>
<div #actionbarContainer class="agent-actionbar-container"></div>
<div #jobsgrid class="jobview-grid"></div>

View File

@@ -0,0 +1,948 @@
/*---------------------------------------------------------------------------------------------
* 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/jobs';
import * as azdata from 'azdata';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { Table } from 'sql/base/browser/ui/table/table';
import { AgentViewComponent } from 'sql/workbench/parts/jobManagement/electron-browser/agentView.component';
import { RowDetailView } from 'sql/base/browser/ui/table/plugins/rowDetailView';
import { JobCacheObject } from 'sql/platform/jobManagement/common/jobManagementService';
import { EditJobAction, DeleteJobAction, NewJobAction } from 'sql/platform/jobManagement/common/jobActions';
import { JobManagementUtilities } from 'sql/platform/jobManagement/common/jobManagementUtilities';
import { HeaderFilter } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin';
import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces';
import { JobManagementView, JobActionContext } from 'sql/workbench/parts/jobManagement/electron-browser/jobManagementView';
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IAction } from 'vs/base/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService';
import { escape } from 'sql/base/common/strings';
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { tableBackground, cellBackground, cellBorderColor } from 'sql/platform/theme/common/colors';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/telemetryKeys';
export const JOBSVIEW_SELECTOR: string = 'jobsview-component';
export const ROW_HEIGHT: number = 45;
export const ACTIONBAR_PADDING: number = 10;
@Component({
selector: JOBSVIEW_SELECTOR,
templateUrl: decodeURI(require.toUrl('./jobsView.component.html')),
providers: [{ provide: TabChild, useExisting: forwardRef(() => JobsViewComponent) }],
})
export class JobsViewComponent extends JobManagementView implements OnInit, OnDestroy {
private columns: Array<Slick.Column<any>> = [
{
name: nls.localize('jobColumns.name', 'Name'),
field: 'name',
formatter: (row, cell, value, columnDef, dataContext) => this.renderName(row, cell, value, columnDef, dataContext),
width: 150,
id: 'name'
},
{ name: nls.localize('jobColumns.lastRun', 'Last Run'), field: 'lastRun', width: 80, id: 'lastRun' },
{ name: nls.localize('jobColumns.nextRun', 'Next Run'), field: 'nextRun', width: 80, id: 'nextRun' },
{ name: nls.localize('jobColumns.enabled', 'Enabled'), field: 'enabled', width: 60, id: 'enabled' },
{ name: nls.localize('jobColumns.status', 'Status'), field: 'currentExecutionStatus', width: 50, id: 'currentExecutionStatus' },
{ name: nls.localize('jobColumns.category', 'Category'), field: 'category', width: 100, id: 'category' },
{ name: nls.localize('jobColumns.runnable', 'Runnable'), field: 'runnable', width: 70, id: 'runnable' },
{ name: nls.localize('jobColumns.schedule', 'Schedule'), field: 'hasSchedule', width: 60, id: 'hasSchedule' },
{ name: nls.localize('jobColumns.lastRunOutcome', 'Last Run Outcome'), field: 'lastRunOutcome', width: 100, id: 'lastRunOutcome' },
{
name: nls.localize('jobColumns.previousRuns', 'Previous Runs'),
formatter: (row, cell, value, columnDef, dataContext) => this.renderChartsPostHistory(row, cell, value, columnDef, dataContext),
field: 'previousRuns',
width: 100,
id: 'previousRuns'
}
];
private _jobCacheObject: JobCacheObject;
private rowDetail: RowDetailView;
private filterPlugin: any;
private dataView: any;
private _isCloud: boolean;
private filterStylingMap: { [columnName: string]: [any]; } = {};
private filterStack = ['start'];
private filterValueMap: { [columnName: string]: string[]; } = {};
private sortingStylingMap: { [columnName: string]: any; } = {};
public jobs: azdata.AgentJobInfo[];
private jobHistories: { [jobId: string]: azdata.AgentJobHistoryInfo[]; } = Object.create(null);
private jobSteps: { [jobId: string]: azdata.AgentJobStepInfo[]; } = Object.create(null);
private jobAlerts: { [jobId: string]: azdata.AgentAlertInfo[]; } = Object.create(null);
private jobSchedules: { [jobId: string]: azdata.AgentJobScheduleInfo[]; } = Object.create(null);
public contextAction = NewJobAction;
@ViewChild('jobsgrid') _gridEl: ElementRef;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
@Inject(forwardRef(() => AgentViewComponent)) _agentViewComponent: AgentViewComponent,
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService,
@Inject(IWorkbenchThemeService) private _themeService: IWorkbenchThemeService,
@Inject(ICommandService) private _commandService: ICommandService,
@Inject(IInstantiationService) instantiationService: IInstantiationService,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) _dashboardService: IDashboardService,
@Inject(ITelemetryService) private _telemetryService: ITelemetryService
) {
super(commonService, _dashboardService, contextMenuService, keybindingService, instantiationService, _agentViewComponent);
let jobCacheObjectMap = this._jobManagementService.jobCacheObjectMap;
let jobCache = jobCacheObjectMap[this._serverName];
if (jobCache) {
this._jobCacheObject = jobCache;
} else {
this._jobCacheObject = new JobCacheObject();
this._jobCacheObject.serverName = this._serverName;
this._jobManagementService.addToCache(this._serverName, this._jobCacheObject);
}
this._isCloud = commonService.connectionManagementService.connectionInfo.serverInfo.isCloud;
}
ngOnInit() {
// set base class elements
this._visibilityElement = this._gridEl;
this._parentComponent = this._agentViewComponent;
this._register(this._themeService.onDidColorThemeChange(e => this.updateTheme(e)));
this._telemetryService.publicLog(TelemetryKeys.JobsView);
}
ngOnDestroy() {
}
public layout() {
let jobsViewToolbar = jQuery('jobsview-component .agent-actionbar-container').get(0);
let statusBar = jQuery('.part.statusbar').get(0);
if (jobsViewToolbar && statusBar) {
let toolbarBottom = jobsViewToolbar.getBoundingClientRect().bottom + ACTIONBAR_PADDING;
let statusTop = statusBar.getBoundingClientRect().top;
this._table.layout(new dom.Dimension(
dom.getContentWidth(this._gridEl.nativeElement),
statusTop - toolbarBottom));
}
}
onFirstVisible() {
let self = this;
let cached: boolean = false;
if (this._jobCacheObject.serverName === this._serverName && this._jobCacheObject.jobs.length > 0) {
cached = true;
this.jobs = this._jobCacheObject.jobs;
}
let columns = this.columns.map((column) => {
column.rerenderOnResize = true;
return column;
});
let options = <Slick.GridOptions<any>>{
syncColumnCellResize: true,
enableColumnReorder: false,
rowHeight: ROW_HEIGHT,
enableCellNavigation: true,
forceFitColumns: false
};
this.dataView = new Slick.Data.DataView({ inlineFilters: false });
let rowDetail = new RowDetailView({
cssClass: '_detail_selector',
process: (job) => {
(<any>rowDetail).onAsyncResponse.notify({
'itemDetail': job
}, undefined, this);
},
useRowClick: false,
panelRows: 1
});
this.rowDetail = rowDetail;
columns.unshift(this.rowDetail.getColumnDefinition());
let filterPlugin = new HeaderFilter({}, this._themeService);
this.filterPlugin = filterPlugin;
jQuery(this._gridEl.nativeElement).empty();
jQuery(this.actionBarContainer.nativeElement).empty();
this.initActionBar();
this._table = new Table(this._gridEl.nativeElement, { columns }, options);
this._table.grid.setData(this.dataView, true);
this._table.grid.onClick.subscribe((e, args) => {
let job = self.getJob(args);
self._agentViewComponent.jobId = job.jobId;
self._agentViewComponent.agentJobInfo = job;
self._agentViewComponent.showHistory = true;
});
this._register(this._table.onContextMenu(e => {
self.openContextMenu(e);
}));
if (cached && this._agentViewComponent.refresh !== true) {
this.onJobsAvailable(null);
this._showProgressWheel = false;
if (this.isVisible) {
this._cd.detectChanges();
}
} else {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
this._jobManagementService.getJobs(ownerUri).then((result) => {
if (result && result.jobs) {
self.jobs = result.jobs;
self._jobCacheObject.jobs = self.jobs;
self.onJobsAvailable(result.jobs);
} else {
// TODO: handle error
}
this._showProgressWheel = false;
if (this.isVisible) {
this._cd.detectChanges();
}
});
}
}
private onJobsAvailable(jobs: azdata.AgentJobInfo[]) {
let jobViews: any;
let start: boolean = true;
if (!jobs) {
let dataView = this._jobCacheObject.dataView;
jobViews = dataView.getItems();
start = false;
} else {
jobViews = jobs.map((job) => {
return {
id: job.jobId,
jobId: job.jobId,
name: job.name,
lastRun: JobManagementUtilities.convertToLastRun(job.lastRun),
nextRun: JobManagementUtilities.convertToNextRun(job.nextRun),
enabled: JobManagementUtilities.convertToResponse(job.enabled),
currentExecutionStatus: JobManagementUtilities.convertToExecutionStatusString(job.currentExecutionStatus),
category: job.category,
runnable: JobManagementUtilities.convertToResponse(job.runnable),
hasSchedule: JobManagementUtilities.convertToResponse(job.hasSchedule),
lastRunOutcome: JobManagementUtilities.convertToStatusString(job.lastRunOutcome)
};
});
}
this._table.registerPlugin(<any>this.rowDetail);
this.filterPlugin.onFilterApplied.subscribe((e, args) => {
this.dataView.refresh();
this._table.grid.resetActiveCell();
let filterValues = args.column.filterValues;
if (filterValues) {
if (filterValues.length === 0) {
// if an associated styling exists with the current filters
if (this.filterStylingMap[args.column.name]) {
let filterLength = this.filterStylingMap[args.column.name].length;
// then remove the filtered styling
for (let i = 0; i < filterLength; i++) {
let lastAppliedStyle = this.filterStylingMap[args.column.name].pop();
this._table.grid.removeCellCssStyles(lastAppliedStyle[0]);
}
delete this.filterStylingMap[args.column.name];
let index = this.filterStack.indexOf(args.column.name, 0);
if (index > -1) {
this.filterStack.splice(index, 1);
delete this.filterValueMap[args.column.name];
}
// apply the previous filter styling
let currentItems = this.dataView.getFilteredItems();
let styledItems = this.filterValueMap[this.filterStack[this.filterStack.length - 1]][1];
if (styledItems === currentItems) {
let lastColStyle = this.filterStylingMap[this.filterStack[this.filterStack.length - 1]];
for (let i = 0; i < lastColStyle.length; i++) {
this._table.grid.setCellCssStyles(lastColStyle[i][0], lastColStyle[i][1]);
}
} else {
// style it all over again
let seenJobs = 0;
for (let i = 0; i < currentItems.length; i++) {
this._table.grid.removeCellCssStyles('error-row' + i.toString());
let item = this.dataView.getFilteredItems()[i];
if (item.lastRunOutcome === 'Failed') {
this.addToStyleHash(seenJobs, false, this.filterStylingMap, args.column.name);
if (this.filterStack.indexOf(args.column.name) < 0) {
this.filterStack.push(args.column.name);
this.filterValueMap[args.column.name] = [filterValues];
}
// one expansion for the row and one for
// the error detail
seenJobs++;
i++;
}
seenJobs++;
}
this.dataView.refresh();
this.filterValueMap[args.column.name].push(this.dataView.getFilteredItems());
this._table.grid.resetActiveCell();
}
if (this.filterStack.length === 0) {
this.filterStack = ['start'];
}
}
} else {
let seenJobs = 0;
for (let i = 0; i < this.jobs.length; i++) {
this._table.grid.removeCellCssStyles('error-row' + i.toString());
let item = this.dataView.getItemByIdx(i);
// current filter
if (_.contains(filterValues, item[args.column.field])) {
// check all previous filters
if (this.checkPreviousFilters(item)) {
if (item.lastRunOutcome === 'Failed') {
this.addToStyleHash(seenJobs, false, this.filterStylingMap, args.column.name);
if (this.filterStack.indexOf(args.column.name) < 0) {
this.filterStack.push(args.column.name);
this.filterValueMap[args.column.name] = [filterValues];
}
// one expansion for the row and one for
// the error detail
seenJobs++;
i++;
}
seenJobs++;
}
}
}
this.dataView.refresh();
if (this.filterValueMap[args.column.name]) {
this.filterValueMap[args.column.name].push(this.dataView.getFilteredItems());
} else {
this.filterValueMap[args.column.name] = this.dataView.getFilteredItems();
}
this._table.grid.resetActiveCell();
}
} else {
this.expandJobs(false);
}
});
this.filterPlugin.onCommand.subscribe((e, args: any) => {
this.columnSort(args.column.name, args.command === 'sort-asc');
});
this._table.registerPlugin(<HeaderFilter>this.filterPlugin);
this.dataView.beginUpdate();
this.dataView.setItems(jobViews);
this.dataView.setFilter((item) => this.filter(item));
this.dataView.endUpdate();
this._table.autosizeColumns();
this._table.resizeCanvas();
this.expandJobs(start);
// tooltip for job name
jQuery('.jobview-jobnamerow').hover(e => {
let currentTarget = e.currentTarget;
currentTarget.title = currentTarget.innerText;
});
const self = this;
this._table.grid.onColumnsResized.subscribe((e, data: any) => {
let nameWidth: number = data.grid.getColumns()[1].width;
// adjust job name when resized
jQuery('#jobsDiv .jobview-grid .slick-cell.l1.r1 .jobview-jobnametext').css('width', `${nameWidth - 10}px`);
// adjust error message when resized
jQuery('#jobsDiv .jobview-grid .slick-cell.l1.r1.error-row .jobview-jobnametext').css('width', '100%');
// generate job charts again
self.jobs.forEach(job => {
let jobHistories = self._jobCacheObject.getJobHistory(job.jobId);
if (jobHistories) {
let previousRuns = jobHistories.slice(jobHistories.length - 5, jobHistories.length);
self.createJobChart(job.jobId, previousRuns);
}
});
});
jQuery('#jobsDiv .jobview-grid .monaco-table .slick-viewport .grid-canvas .ui-widget-content.slick-row').hover((e1) =>
this.highlightErrorRows(e1), (e2) => this.hightlightNonErrorRows(e2));
this._table.grid.onScroll.subscribe((e) => {
jQuery('#jobsDiv .jobview-grid .monaco-table .slick-viewport .grid-canvas .ui-widget-content.slick-row').hover((e1) =>
this.highlightErrorRows(e1), (e2) => this.hightlightNonErrorRows(e2));
});
// cache the dataview for future use
this._jobCacheObject.dataView = this.dataView;
this.filterValueMap['start'] = [[], this.dataView.getItems()];
this.loadJobHistories();
}
private highlightErrorRows(e) {
// highlight the error row as well if a failing job row is hovered
if (e.currentTarget.children.item(0).classList.contains('job-with-error')) {
let target = jQuery(e.currentTarget);
let targetChildren = jQuery(e.currentTarget.children);
let siblings = target.nextAll().toArray();
let top = parseInt(target.css('top'), 10);
for (let i = 0; i < siblings.length; i++) {
let sibling = siblings[i];
let siblingTop = parseInt(jQuery(sibling).css('top'), 10);
if (siblingTop === top + ROW_HEIGHT) {
jQuery(sibling.children).addClass('hovered');
sibling.onmouseenter = (e) => {
targetChildren.addClass('hovered');
};
sibling.onmouseleave = (e) => {
targetChildren.removeClass('hovered');
};
break;
}
}
}
}
private hightlightNonErrorRows(e) {
// switch back to original background
if (e.currentTarget.children.item(0).classList.contains('job-with-error')) {
let target = jQuery(e.currentTarget);
let siblings = target.nextAll().toArray();
let top = parseInt(target.css('top'), 10);
for (let i = 0; i < siblings.length; i++) {
let sibling = siblings[i];
let siblingTop = parseInt(jQuery(sibling).css('top'), 10);
if (siblingTop === top + ROW_HEIGHT) {
jQuery(sibling.children).removeClass('hovered');
break;
}
}
}
}
private setRowWithErrorClass(hash: { [index: number]: { [id: string]: string; } }, row: number, errorClass: string) {
hash[row] = {
'_detail_selector': errorClass,
'id': errorClass,
'jobId': errorClass,
'name': errorClass,
'lastRun': errorClass,
'nextRun': errorClass,
'enabled': errorClass,
'currentExecutionStatus': errorClass,
'category': errorClass,
'runnable': errorClass,
'hasSchedule': errorClass,
'lastRunOutcome': errorClass,
'previousRuns': errorClass
};
return hash;
}
private addToStyleHash(row: number, start: boolean, map: any, columnName: string) {
let hash: {
[index: number]: {
[id: string]: string;
}
} = {};
hash = this.setRowWithErrorClass(hash, row, 'job-with-error');
hash = this.setRowWithErrorClass(hash, row + 1, 'error-row');
if (start) {
if (map['start']) {
map['start'].push(['error-row' + row.toString(), hash]);
} else {
map['start'] = [['error-row' + row.toString(), hash]];
}
} else {
if (map[columnName]) {
map[columnName].push(['error-row' + row.toString(), hash]);
} else {
map[columnName] = [['error-row' + row.toString(), hash]];
}
}
this._table.grid.setCellCssStyles('error-row' + row.toString(), hash);
}
private renderName(row, cell, value, columnDef, dataContext) {
let resultIndicatorClass: string;
switch (dataContext.lastRunOutcome) {
case ('Succeeded'):
resultIndicatorClass = 'jobview-jobnameindicatorsuccess';
break;
case ('Failed'):
resultIndicatorClass = 'jobview-jobnameindicatorfailure';
break;
case ('Cancelled'):
resultIndicatorClass = 'jobview-jobnameindicatorcancel';
break;
case ('Status Unknown'):
resultIndicatorClass = 'jobview-jobnameindicatorunknown';
break;
default:
resultIndicatorClass = 'jobview-jobnameindicatorfailure';
break;
}
return '<table class="jobview-jobnametable"><tr class="jobview-jobnamerow">' +
'<td nowrap class=' + resultIndicatorClass + '></td>' +
'<td nowrap class="jobview-jobnametext">' + escape(dataContext.name) + '</td>' +
'</tr></table>';
}
private renderChartsPostHistory(row, cell, value, columnDef, dataContext) {
let runChart = this._jobCacheObject.getRunChart(dataContext.id);
if (runChart && runChart.length > 0) {
return `<table class="jobprevruns" id="${dataContext.id}">
<tr>
<td>${runChart[0] ? runChart[0] : '<div></div>'}</td>
<td>${runChart[1] ? runChart[1] : '<div></div>'}</td>
<td>${runChart[2] ? runChart[2] : '<div></div>'}</td>
<td>${runChart[3] ? runChart[3] : '<div></div>'}</td>
<td>${runChart[4] ? runChart[4] : '<div></div>'}</td>
</tr>
</table>`;
} else {
return `<table class="jobprevruns" id="${dataContext.id}">
<tr>
<td><div class="bar0"></div></td>
<td><div class="bar1"></div></td>
<td><div class="bar2"></div></td>
<td><div class="bar3"></div></td>
<td><div class="bar4"></div></td>
</tr>
</table>`;
}
}
private expandJobRowDetails(rowIdx: number, message?: string): void {
let item = this.dataView.getItemByIdx(rowIdx);
item.message = this._agentViewComponent.expanded.get(item.jobId);
this.rowDetail.applyTemplateNewLineHeight(item, true);
}
private async loadJobHistories() {
if (this.jobs) {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
let separatedJobs = this.separateFailingJobs();
// grab histories of the failing jobs first
// so they can be expanded quicker
let failing = separatedJobs[0];
let passing = separatedJobs[1];
Promise.all([this.curateJobHistory(failing, ownerUri), this.curateJobHistory(passing, ownerUri)]);
}
}
private separateFailingJobs(): azdata.AgentJobInfo[][] {
let failing = [];
let nonFailing = [];
for (let i = 0; i < this.jobs.length; i++) {
if (this.jobs[i].lastRunOutcome === 0) {
failing.push(this.jobs[i]);
} else {
nonFailing.push(this.jobs[i]);
}
}
return [failing, nonFailing];
}
private checkPreviousFilters(item): boolean {
for (let column in this.filterValueMap) {
if (column !== 'start' && this.filterValueMap[column][0].length > 0) {
if (!_.contains(this.filterValueMap[column][0], item[JobManagementUtilities.convertColNameToField(column)])) {
return false;
}
}
}
return true;
}
private isErrorRow(cell: HTMLElement) {
return cell.classList.contains('error-row');
}
private getJob(args: Slick.OnClickEventArgs<any>): azdata.AgentJobInfo {
let row = args.row;
let jobName: string;
let cell = args.grid.getCellNode(row, 1);
if (this.isErrorRow(cell)) {
jobName = args.grid.getCellNode(row - 1, 1).innerText.trim();
} else {
jobName = cell.innerText.trim();
}
let job = this.jobs.filter(job => job.name === jobName)[0];
return job;
}
private async curateJobHistory(jobs: azdata.AgentJobInfo[], ownerUri: string) {
const self = this;
for (let job of jobs) {
let result = await this._jobManagementService.getJobHistory(ownerUri, job.jobId, job.name);
if (result) {
self.jobSteps[job.jobId] = result.steps ? result.steps : [];
self.jobAlerts[job.jobId] = result.alerts ? result.alerts : [];
self.jobSchedules[job.jobId] = result.schedules ? result.schedules : [];
self.jobHistories[job.jobId] = result.histories ? result.histories : [];
self._jobCacheObject.setJobSteps(job.jobId, self.jobSteps[job.jobId]);
self._jobCacheObject.setJobHistory(job.jobId, self.jobHistories[job.jobId]);
self._jobCacheObject.setJobAlerts(job.jobId, self.jobAlerts[job.jobId]);
self._jobCacheObject.setJobSchedules(job.jobId, self.jobSchedules[job.jobId]);
let jobHistories = self._jobCacheObject.getJobHistory(job.jobId);
let previousRuns: azdata.AgentJobHistoryInfo[];
if (jobHistories.length >= 5) {
previousRuns = jobHistories.slice(jobHistories.length - 5, jobHistories.length);
} else {
previousRuns = jobHistories;
}
self.createJobChart(job.jobId, previousRuns);
if (self._agentViewComponent.expanded.has(job.jobId)) {
let lastJobHistory = jobHistories[jobHistories.length - 1];
let item = self.dataView.getItemById(job.jobId + '.error');
let noStepsMessage = nls.localize('jobsView.noSteps', 'No Steps available for this job.');
let errorMessage = lastJobHistory ? lastJobHistory.message : noStepsMessage;
item['name'] = nls.localize('jobsView.error', 'Error: ') + errorMessage;
self._agentViewComponent.setExpanded(job.jobId, item['name']);
self.dataView.updateItem(job.jobId + '.error', item);
}
}
}
}
private createJobChart(jobId: string, jobHistories: azdata.AgentJobHistoryInfo[]): void {
let chartHeights = this.getChartHeights(jobHistories);
let runCharts = [];
for (let i = 0; i < chartHeights.length; i++) {
let runGraph = jQuery(`table.jobprevruns#${jobId} > tbody > tr > td > div.bar${i}`);
if (runGraph.length > 0) {
runGraph.css('height', chartHeights[i]);
let bgColor = jobHistories[i].runStatus === 0 ? 'red' : 'green';
runGraph.css('background', bgColor);
runGraph.hover((e) => {
let currentTarget = e.currentTarget;
currentTarget.title = jobHistories[i].runDuration;
});
runCharts.push(runGraph.get(0).outerHTML);
}
}
if (runCharts.length > 0) {
this._jobCacheObject.setRunChart(jobId, runCharts);
}
}
// chart height normalization logic
private getChartHeights(jobHistories: azdata.AgentJobHistoryInfo[]): string[] {
if (!jobHistories || jobHistories.length === 0) {
return [];
}
let maxDuration: number = 0;
jobHistories.forEach(history => {
let historyDuration = JobManagementUtilities.convertDurationToSeconds(history.runDuration);
if (historyDuration > maxDuration) {
maxDuration = historyDuration;
}
});
maxDuration = maxDuration === 0 ? 1 : maxDuration;
let maxBarHeight: number = 24;
let chartHeights = [];
let zeroDurationJobCount = 0;
for (let i = 0; i < jobHistories.length; i++) {
let duration = jobHistories[i].runDuration;
let chartHeight = (maxBarHeight * JobManagementUtilities.convertDurationToSeconds(duration)) / maxDuration;
chartHeights.push(`${chartHeight}px`);
if (chartHeight === 0) {
zeroDurationJobCount++;
}
}
// if the durations are all 0 secs, show minimal chart
// instead of nothing
if (zeroDurationJobCount === jobHistories.length) {
return Array(jobHistories.length).fill('5px');
} else {
return chartHeights;
}
}
private expandJobs(start: boolean): void {
if (start) {
this._agentViewComponent.expanded = new Map<string, string>();
}
let expandedJobs = this._agentViewComponent.expanded;
let expansions = 0;
for (let i = 0; i < this.jobs.length; i++) {
let job = this.jobs[i];
if (job.lastRunOutcome === 0 && !expandedJobs.get(job.jobId)) {
this.expandJobRowDetails(i + expandedJobs.size);
this.addToStyleHash(i + expandedJobs.size, start, this.filterStylingMap, undefined);
this._agentViewComponent.setExpanded(job.jobId, 'Loading Error...');
} else if (job.lastRunOutcome === 0 && expandedJobs.get(job.jobId)) {
this.addToStyleHash(i + expansions, start, this.filterStylingMap, undefined);
expansions++;
}
}
}
private filter(item: any) {
let columns = this._table.grid.getColumns();
let value = true;
for (let i = 0; i < columns.length; i++) {
let col: any = columns[i];
let filterValues = col.filterValues;
if (filterValues && filterValues.length > 0) {
if (item._parent) {
value = value && _.contains(filterValues, item._parent[col.field]);
} else {
value = value && _.contains(filterValues, item[col.field]);
}
}
}
return value;
}
private columnSort(column: string, isAscending: boolean) {
let items = this.dataView.getItems();
// get error items here and remove them
let jobItems = items.filter(x => x._parent === undefined);
let errorItems = items.filter(x => x._parent !== undefined);
this.sortingStylingMap[column] = items;
switch (column) {
case ('Name'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.name.localeCompare(item2.name);
}, isAscending);
break;
}
case ('Last Run'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => this.dateCompare(item1, item2, true), isAscending);
break;
}
case ('Next Run'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => this.dateCompare(item1, item2, false), isAscending);
break;
}
case ('Enabled'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.enabled.localeCompare(item2.enabled);
}, isAscending);
break;
}
case ('Status'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.currentExecutionStatus.localeCompare(item2.currentExecutionStatus);
}, isAscending);
break;
}
case ('Category'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.category.localeCompare(item2.category);
}, isAscending);
break;
}
case ('Runnable'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.runnable.localeCompare(item2.runnable);
}, isAscending);
break;
}
case ('Schedule'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.hasSchedule.localeCompare(item2.hasSchedule);
}, isAscending);
break;
}
case ('Last Run Outcome'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.lastRunOutcome.localeCompare(item2.lastRunOutcome);
}, isAscending);
break;
}
}
// insert the errors back again
let jobItemsLength = jobItems.length;
for (let i = 0; i < jobItemsLength; i++) {
let item = jobItems[i];
if (item._child) {
let child = errorItems.find(error => error === item._child);
jobItems.splice(i + 1, 0, child);
jobItemsLength++;
}
}
this.dataView.setItems(jobItems);
// remove old style
if (this.filterStylingMap[column]) {
let filterLength = this.filterStylingMap[column].length;
for (let i = 0; i < filterLength; i++) {
let lastAppliedStyle = this.filterStylingMap[column].pop();
this._table.grid.removeCellCssStyles(lastAppliedStyle[0]);
}
} else {
for (let i = 0; i < this.jobs.length; i++) {
this._table.grid.removeCellCssStyles('error-row' + i.toString());
}
}
// add new style to the items back again
items = this.filterStack.length > 1 ? this.dataView.getFilteredItems() : this.dataView.getItems();
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (item.lastRunOutcome === 'Failed') {
this.addToStyleHash(i, false, this.sortingStylingMap, column);
}
}
}
private dateCompare(item1: any, item2: any, lastRun: boolean): number {
let exceptionString = lastRun ? 'Never Run' : 'Not Scheduled';
if (item2.lastRun === exceptionString && item1.lastRun !== exceptionString) {
return -1;
} else if (item1.lastRun === exceptionString && item2.lastRun !== exceptionString) {
return 1;
} else if (item1.lastRun === exceptionString && item2.lastRun === exceptionString) {
return 0;
} else {
let date1 = new Date(item1.lastRun);
let date2 = new Date(item2.lastRun);
if (date1 > date2) {
return 1;
} else if (date1 === date2) {
return 0;
} else {
return -1;
}
}
}
private updateTheme(theme: IColorTheme) {
let bgColor = theme.getColor(tableBackground);
let cellColor = theme.getColor(cellBackground);
let borderColor = theme.getColor(cellBorderColor);
let headerColumns = jQuery('#agentViewDiv .slick-header-column');
let cells = jQuery('.grid-canvas .ui-widget-content.slick-row .slick-cell');
let cellDetails = jQuery('#jobsDiv .dynamic-cell-detail');
headerColumns.toArray().forEach(col => {
col.style.background = bgColor.toString();
});
cells.toArray().forEach(cell => {
cell.style.background = bgColor.toString();
cell.style.border = borderColor ? '1px solid ' + borderColor.toString() : null;
});
cellDetails.toArray().forEach(cellDetail => {
cellDetail.style.background = cellColor.toString();
});
}
protected getTableActions(targetObject: JobActionContext): IAction[] {
let actions: IAction[] = [];
let editAction = this._instantiationService.createInstance(EditJobAction);
if (!targetObject.canEdit) {
editAction.enabled = false;
}
actions.push(editAction);
actions.push(this._instantiationService.createInstance(DeleteJobAction));
return actions;
}
protected convertStepsToStepInfos(steps: azdata.AgentJobStep[], job: azdata.AgentJobInfo): azdata.AgentJobStepInfo[] {
let result = [];
steps.forEach(step => {
let stepInfo: azdata.AgentJobStepInfo = {
jobId: job.jobId,
jobName: job.name,
script: null,
scriptName: null,
stepName: step.stepName,
subSystem: null,
id: +step.stepId,
failureAction: null,
successAction: null,
failStepId: null,
successStepId: null,
command: null,
commandExecutionSuccessCode: null,
databaseName: null,
databaseUserName: null,
server: null,
outputFileName: null,
appendToLogFile: null,
appendToStepHist: null,
writeLogToTable: null,
appendLogToTable: null,
retryAttempts: null,
retryInterval: null,
proxyName: null
};
result.push(stepInfo);
});
return result;
}
protected getCurrentTableObject(rowIndex: number): JobActionContext {
let data = this._table.grid.getData();
if (!data || rowIndex >= data.getLength()) {
return undefined;
}
let jobId = data.getItem(rowIndex).jobId;
if (!jobId) {
// if we couldn't find the ID, check if it's an
// error row
let isErrorRow: boolean = data.getItem(rowIndex).id.indexOf('error') >= 0;
if (isErrorRow) {
jobId = data.getItem(rowIndex - 1).jobId;
}
}
let job: azdata.AgentJobInfo[] = this.jobs.filter(job => {
return job.jobId === jobId;
});
if (job && job.length > 0) {
// add steps
if (this.jobSteps && this.jobSteps[jobId]) {
let steps = this.jobSteps[jobId];
job[0].jobSteps = steps;
}
// add schedules
if (this.jobSchedules && this.jobSchedules[jobId]) {
let schedules = this.jobSchedules[jobId];
job[0].jobSchedules = schedules;
}
// add alerts
if (this.jobAlerts && this.jobAlerts[jobId]) {
let alerts = this.jobAlerts[jobId];
job[0].alerts = alerts;
}
if (job[0].jobSteps && job[0].jobSchedules && job[0].alerts) {
return { job: job[0], canEdit: true };
}
return { job: job[0], canEdit: false };
}
return undefined;
}
public async openCreateJobDialog() {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
await this._commandService.executeCommand('agent.openJobDialog', ownerUri);
}
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>jobalert</title><path d="M16,2.24V7.58A5.38,5.38,0,0,0,15,6.5V4.3L12.8,5.39l-.64-.11a5,5,0,0,0-.65,0l-.36,0-.36,0,4.11-2.05H1.12L7.87,6.61,7.49,7a4.7,4.7,0,0,0-.34.39L1,4.3v6.94H6a4.64,4.64,0,0,0,.07.5c0,.17.07.33.11.5H0v-10Zm-4.5,4a4.35,4.35,0,0,1,1.75.36A4.53,4.53,0,0,1,15.64,9a4.49,4.49,0,0,1,0,3.5,4.53,4.53,0,0,1-2.39,2.39,4.49,4.49,0,0,1-3.5,0,4.53,4.53,0,0,1-2.39-2.39,4.49,4.49,0,0,1,0-3.5A4.53,4.53,0,0,1,9.75,6.59,4.35,4.35,0,0,1,11.5,6.24Zm0,8A3.38,3.38,0,0,0,12.86,14a3.53,3.53,0,0,0,1.86-1.86,3.49,3.49,0,0,0,0-2.73,3.53,3.53,0,0,0-1.86-1.86,3.49,3.49,0,0,0-2.73,0A3.53,3.53,0,0,0,8.28,9.37a3.49,3.49,0,0,0,0,2.73A3.53,3.53,0,0,0,10.14,14,3.38,3.38,0,0,0,11.5,14.24Zm-.5-6h1v3H11Zm0,4h1v1H11Z"/></svg>

After

Width:  |  Height:  |  Size: 815 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>jobalert_inverse</title><path class="cls-1" d="M16,2.24V7.58A5.38,5.38,0,0,0,15,6.5V4.3L12.8,5.39l-.64-.11a5,5,0,0,0-.65,0l-.36,0-.36,0,4.11-2.05H1.12L7.87,6.61,7.49,7a4.7,4.7,0,0,0-.34.39L1,4.3v6.94H6a4.64,4.64,0,0,0,.07.5c0,.17.07.33.11.5H0v-10Zm-4.5,4a4.35,4.35,0,0,1,1.75.36A4.53,4.53,0,0,1,15.64,9a4.49,4.49,0,0,1,0,3.5,4.53,4.53,0,0,1-2.39,2.39,4.49,4.49,0,0,1-3.5,0,4.53,4.53,0,0,1-2.39-2.39,4.49,4.49,0,0,1,0-3.5A4.53,4.53,0,0,1,9.75,6.59,4.35,4.35,0,0,1,11.5,6.24Zm0,8A3.38,3.38,0,0,0,12.86,14a3.53,3.53,0,0,0,1.86-1.86,3.49,3.49,0,0,0,0-2.73,3.53,3.53,0,0,0-1.86-1.86,3.49,3.49,0,0,0-2.73,0A3.53,3.53,0,0,0,8.28,9.37a3.49,3.49,0,0,0,0,2.73A3.53,3.53,0,0,0,10.14,14,3.38,3.38,0,0,0,11.5,14.24Zm-.5-6h1v3H11Zm0,4h1v1H11Z"/></svg>

After

Width:  |  Height:  |  Size: 883 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#212121;}</style></defs><title>back_16x16</title><path class="cls-1" d="M16.15,8.5H2.1l6.15,6.15-.7.7L.19,8,7.55.65l.7.7L2.1,7.5h14Z"/></svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>back_inverse_16x16</title><path class="cls-1" d="M16.15,8.5H2.1l6.15,6.15-.7.7L.19,8,7.55.65l.7.7L2.1,7.5h14Z"/></svg>

After

Width:  |  Height:  |  Size: 264 B

View File

@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.detailView-toggle
{
display: inline-block;
cursor: pointer;
}
.detailView-toggle.expand
{
height: 20px;
width: 20px;
background: url(../images/arrow-right.gif) no-repeat center center;
}
.detailView-toggle.collapse
{
height: 20px;
width: 20px;
background: url(../images/sort-desc.gif) no-repeat center center;
}
.dynamic-cell-detail
{
z-index: 10000;
position: absolute;
background-color: #dae5e8;
margin: 0;
padding: 0;
width: 100%;
overflow: auto;
}
.dynamic-cell-detail > :first-child
{
vertical-align: middle;
line-height: 13px;
}
.dynamic-cell-detail > .detail-container {
overflow: auto;
display: block !important;
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;clip-rule:evenodd;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#d02e00;}</style><clipPath id="clip-path"><path class="cls-1" d="M8.88,8l2.67-2.67-.88-.88L8,7.12,5.33,4.45l-.88.88L7.12,8,4.45,10.67l.88.88L8,8.88l2.67,2.67.88-.88ZM8,0a7.92,7.92,0,0,1,4,1.09A8.15,8.15,0,0,1,14.91,4a8,8,0,0,1,0,8.07A8.15,8.15,0,0,1,12,14.91a8,8,0,0,1-8.07,0A8.15,8.15,0,0,1,1.09,12,8,8,0,0,1,1.09,4,8.15,8.15,0,0,1,4,1.09,7.92,7.92,0,0,1,8,0Z"/></clipPath></defs><title>failed</title><g class="cls-2"><rect class="cls-3" x="-5" y="-5" width="26" height="26"/></g></svg>

After

Width:  |  Height:  |  Size: 718 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>job</title><path d="M11,4h5v9H0V4H5V2h6Zm4,1H1V7H4V6H5V7h6V6h1V7h3ZM1,12H15V8H12V9H11V8H5V9H4V8H1ZM6,3V4h4V3Z"/></svg>

After

Width:  |  Height:  |  Size: 218 B

View File

@@ -0,0 +1,293 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
jobhistory-component .all-jobs {
display: inline-block;
font-size: 15px;
}
.overview-container .overview-tab .resultsViewCollapsible {
padding: 15px;
display: inline;
}
.job-heading {
text-align: left;
padding-left: 13px;
font-size: 1.5vw;
}
.overview-container {
padding-top: 10px;
}
.vs-dark .overview-container .overview-tab {
color: #fff;
}
.hc-black .overview-container .overview-tab {
color: #ffffff;
}
.overview-container > .overview-tab {
position: relative;
margin-bottom: 1px;
width: 100%;
color: #4a4a4a;
overflow: hidden;
}
input#accordion {
position: absolute;
opacity: 0;
z-index: -1;
}
.vs-dark .overview-container .overview-tab label {
background: #444444;
}
.hc-black .overview-container .overview-tab label {
background: #000000;
border: 1px solid #2b56f2;
}
.overview-container .overview-tab label {
position: relative;
display: block;
padding: 0 0 0 1em;
background: #f4f4f4;
font-weight: bold;
line-height: 3;
cursor: pointer;
width: 100%;
}
.vs-dark .overview-tab .accordion-content {
background: #333333;
}
.hc-black .overview-tab .accordion-content {
background: #000000;
border: 1px solid #2b56f2;
}
.overview-tab .accordion-content {
max-height: 0;
overflow: hidden;
background: #eaeaea;
-webkit-transition: max-height .35s;
-o-transition: max-height .35s;
transition: max-height .35s;
width: 100%;
}
.overview-tab .accordion-content p {
margin: 1em;
}
/* :checked */
input#accordion:checked ~ .accordion-content {
max-height: 10em;
}
/* Icon */
.overview-container .overview-tab label::after {
position: absolute;
right: 0;
top: 0;
display: block;
width: 3em;
height: 3em;
line-height: 3;
text-align: center;
-webkit-transition: all .3s;
-o-transition: all .3s;
transition: all .3s;
}
.all-jobs > .back-button-icon {
content: url('../common/media/back.svg');
width: 20px;
margin-right: 10px;
float: left;
cursor: pointer;
padding-left: 13px;
padding-bottom: 10px;
}
.vs-dark.monaco-shell .all-jobs >.back-button-icon,
.hc-black.monaco-shell .all-jobs >.back-button-icon {
content: url('../common/media/back_inverse.svg');
}
.vs .action-label.icon.newStepIcon {
background-image: url('../common/media/new.svg');
}
.vs-dark .action-label.icon.newStepIcon,
.hc-black .action-label.icon.newStepIcon {
background-image: url('../common/media/new_inverse.svg');
}
jobhistory-component .hc-black .icon.edit,
jobhistory-component .vs-dark .icon.edit {
background-image: url('../../../media/icons/edit_inverse.svg');
}
jobhistory-component .vs .icon.edit {
background-image: url('../../../media/icons/edit.svg');
}
jobhistory-component .actions-container .icon.edit {
background-position: 0% 50%;
background-repeat: no-repeat;
background-size: 12px;
}
a.action-label.icon.runJobIcon.non-runnable {
opacity: 0.4;
cursor: default;
}
a.action-label.icon.stopJobIcon.non-runnable {
opacity: 0.4;
cursor: default;
}
.accordion-content #col1,
.accordion-content #col2,
.accordion-content #col3,
.accordion-content #col4 {
padding: 10px;
}
.accordion-content #col1,
.accordion-content #col3 {
font-weight: bold;
}
.accordion-content #col2 {
padding-right: 300px;
}
table.step-list tr.step-row td {
padding-right: 10px;
}
.history-details {
flex: 1 1 auto;
display: flex;
}
.history-details > .job-steps {
flex: 1 1 auto;
display: flex;
border-left: 3px solid #f4f4f4;
padding-left: 10px;
flex-direction: column;
width: 100%;
}
.vs-dark .history-details > .job-steps {
border-left: 3px solid #444444;
}
.hc-black .history-details > .job-steps {
border-left: 3px solid #2b56f2;
}
.history-details > .job-steps > table.step-list {
padding-bottom: 10px;
display: flex;
flex: 1 1;
overflow: scroll;
max-height: 200px;
}
.step-table .monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content:before {
background: none;
background-image: none;
}
.step-table .monaco-tree.focused .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children.selected:not(.loading) > .content:before {
background-image: none;
}
.step-table .list-row .status-icon {
height: 10px;
width: 10px;
display: inline-block;
vertical-align: middle;
}
.step-table .list-row .label {
padding-left: 10px;
display: inline-block;
vertical-align: middle;
}
.job-passed {
background: green;
}
.job-failed {
background: red;
}
.job-unknown {
background: yellow;
}
.date-column {
padding-left: 50px;
width: 140px;
}
.step-table {
flex: 1 1 auto;
}
.prev-run-list-container {
display: flex;
flex-direction: column;
}
jobhistory-component {
display: flex;
flex-direction: column;
}
jobhistory-component > .jobhistory-heading-container {
display: flex;
}
jobhistory-component > .jobhistory-heading-container > .icon.in-progress {
width: 20px;
height: 20px;
padding-top: 16px;
padding-left: 20px;
}
jobhistory-component > .agent-actionbar-container {
border-top: 3px solid #f4f4f4;
}
.vs-dark jobhistory-component > .agent-actionbar-container {
border-top: 3px solid #444444;
}
.hc-black jobhistory-component > .agent-actionbar-container {
border-top: 3px solid #2b56f2;
}
jobhistory-component .step-table.prev-run-list .monaco-tree-wrapper .monaco-tree-row {
width: 96%;
}
jobhistory-component .agent-actionbar-container > .monaco-toolbar.carbon-taskbar {
margin: 10px 0px 5px 0px;
}

View File

@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.steps-tree .list-row .status-icon.step-passed {
content: url("../common/media/success_complete.svg");
height: 100%;
}
.steps-tree .list-row .status-icon.step-failed {
content: url("../common/media/failed.svg");
height: 100%;
}
.steps-tree .list-row .label {
padding-left: 10px;
display: flex;
text-align: center;
vertical-align: middle;
user-select: initial;
}
.steps-tree .list-row {
display: inline-flex;
height: 20px
}
.step-columns {
padding-left: 50px;
padding-top: 10px;
}
.steps-tree .tree-id-col {
padding-left: 10px;
white-space: normal;
text-align: left;
width: 60px;
}
.steps-tree .tree-name-col {
padding-right: 10px;
white-space: normal;
text-align: left;
width: 300px;
}
.steps-tree .tree-message-col {
padding-right: 10px;
white-space: normal;
text-align: left;
width: 700px;
}
.steps-tree .step-column-heading {
font-weight: bold;
text-align: center;
height: 40px;
}
.steps-header > .steps-icon {
height: 25px;
padding-right: 10px;
display: inline;
vertical-align: middle;
}
.vs-dark .steps-header > .steps-icon,
.hc-black .steps-header > .steps-icon {
content: url("../common/media/step_inverse.svg");
}
.steps-header > .steps-icon {
content: url("../common/media/step.svg");
}
jobstepsview-component {
display: flex;
flex-direction: column;
position: absolute;
height: 100%;
width: 100%;
}
jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row {
width: 99.2%;
}
.vs-dark jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row,
.vs-dark jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.focused,
.vs-dark jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.selected,
.vs-dark jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.focused.selected {
background: #444444 !important;
}
.hc-black jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row,
.hc-black jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.selected,
.hc-black jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.focused,
.hc-black jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.focused.selected {
background: none !important;
}
jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row,
jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.focused,
jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.selected,
jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row.step-column-row.focused.selected {
background: #dcdcdc !important;
cursor: none;
padding-left: 0px;
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>job_inverse</title><path class="cls-1" d="M11,4h5v9H0V4H5V2h6Zm4,1H1V7H4V6H5V7h6V6h1V7h3ZM1,12H15V8H12V9H11V8H5V9H4V8H1ZM6,3V4h4V3Z"/></svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@@ -0,0 +1,416 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
agentview-component {
height: 100%;
width : 100%;
display: block;
}
jobsview-component {
height: 100%;
width : 100%;
display: block;
}
jobhistory-component {
height: 100%;
width : 100%;
display: block;
}
.job-heading-container {
height: 50px;
border-bottom: 3px solid #f4f4f4;
display: -webkit-box;
}
.vs-dark .job-heading-container {
border-bottom: 3px solid #444444;
}
.hc-black .job-heading-container {
border-bottom: 3px solid #2b56f2;
}
.jobview-grid {
height: calc(100% - 75px);
width : 100%;
display: block;
}
.vs-dark #agentViewDiv .slick-header-column {
background: #333333 !important;
}
#agentViewDiv .slick-header-column {
background-color: transparent !important;
border: 0px !important;
font-weight: bold;
}
.hc-black #jobsDiv jobsview-component .jobview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell {
border: 1px solid #2b56f2;
}
#jobsDiv jobsview-component .jobview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell {
border-right: transparent !important;
border-left: transparent !important;
line-height: 33px !important;
}
#jobsDiv .jobview-joblist {
height: 100%;
width: 100%;
}
#jobsDiv .jobview-jobnametable {
border: 0px;
width: 100%;
height: 100%;
}
#jobsDiv .jobview-jobnameindicatorsuccess {
width: 5px;
background: green;
}
#jobsDiv .slick-cell.l1.r1 .jobview-jobnameindicatorfailure {
width: 5px;
background: red;
}
#jobsDiv .jobview-jobnameindicatorcancel {
width: 5px;
background: orange;
}
#jobsDiv .jobview-grid .jobview-jobnameindicatorunknown {
width: 5px;
background: grey;
}
#jobsDiv .jobview-grid .slick-cell.l1.r1.error-row .jobview-jobnametext {
width: 100%;
}
#jobsDiv .jobview-grid .slick-cell.l1.r1 .jobview-jobnametext {
text-overflow: ellipsis;
width: 250px;
overflow: hidden;
white-space: nowrap;
display: inline-block;
}
#operatorsDiv .joboperatorsview-grid .slick-cell.l1.r1 .operatorview-operatornametext,
#alertsDiv .jobalertsview-grid .slick-cell.l1.r1 .alertview-alertnametext,
#proxiesDiv .jobproxiesview-grid .slick-cell.l1.r1 .proxyview-proxynametext {
text-overflow: ellipsis;
width: 100%;
overflow: hidden;
white-space: nowrap;
display: inline-block;
}
#jobsDiv .job-with-error {
border-bottom: none;
}
.jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell.l1.r1.error-row {
width: 100%;
opacity: 1;
font-weight: 700;
color: orangered;
}
.jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell._detail_selector.error-row {
opacity: 1;
}
#jobsDiv .jobview-splitter {
height: 1px;
width: 100%;
background-color: gray;
}
#jobsDiv .jobview-jobitem {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
flex-flow: row wrap;
white-space: nowrap;
}
#jobsDiv .jobview-label {
padding-bottom: 10px;
padding-top: 10px;
}
#jobsDiv .jobview-highlight-none {
width: 5px;
margin-right: 10px;
}
#jobsDiv .detail-container {
max-height: 100px !important;
line-height: 20px;
}
#jobsDiv .detail {
padding: 5px
}
#jobsDiv .preload {
font-size: 13px;
}
#jobsDiv .dynamic-cell-detail > :first-child {
vertical-align: middle;
line-height: 13px;
padding: 10px;
margin-left: 20px;
}
.jobsview-icon {
background-image: url('./job.svg');
}
.vs-dark .jobsview-icon,
.hc-black .jobsview-icon {
background-image: url('./job_inverse.svg');
}
.alertsview-icon {
background-image: url('./alert.svg');
}
.vs-dark .alertsview-icon,
.hc-black .alertsview-icon {
background-image: url('./alert_inverse.svg');
}
.proxiesview-icon {
background-image: url('./proxy.svg');
}
.vs-dark .proxiesview-icon,
.hc-black .proxiesview-icon {
background-image: url('./proxy_inverse.svg');
}
.operatorsview-icon {
background-image: url('./operator.svg');
}
.vs-dark .operatorsview-icon,
.hc-black .operatorsview-icon {
background-image: url('./operator_inverse.svg');
}
agentview-component .jobview-grid .grid-canvas > .ui-widget-content.slick-row.even > .slick-cell,
agentview-component .jobview-grid .grid-canvas > .ui-widget-content.slick-row.odd > .slick-cell {
cursor: pointer;
}
.vs-dark .jobview-grid > .monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted white;
}
.hc-black .jobview-grid > .monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted #2b56f2;
}
.jobview-grid > .monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted #444444;
}
.job-heading-container > .icon.in-progress {
height: 20px;
width: 20px;
padding-top: 16px;
padding-left: 15px;
}
#jobsDiv jobsview-component .jobview-grid .slick-cell.l1.r1.error-row td.jobview-jobnameindicatorfailure {
width: 0;
background: none;
}
#jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
#jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell,
#jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row> .slick-cell.hovered {
background: #dcdcdc !important;
}
.vs-dark #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
.vs-dark #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered,
.vs-dark #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell {
background: #444444 !important;
}
.hc-black #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
.hc-black #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered,
.hc-black #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell {
background: none !important;
}
table.jobprevruns div.bar0, table.jobprevruns div.bar1, table.jobprevruns div.bar2,
table.jobprevruns div.bar3, table.jobprevruns div.bar4, table.jobprevruns div.bar5 {
padding-top: 3px;
padding-left: 5px;
width: 10px;
}
.jobview-grid .slick-cell.l10.r10 {
text-align: center;
display: inline-flex;
}
table.jobprevruns {
height: 100%;
}
table.jobprevruns > tbody {
vertical-align: bottom;
}
#alertsDiv .jobalertsview-grid {
height: calc(100% - 75px);
width : 100%;
display: block;
}
#operatorsDiv .joboperatorsview-grid {
height: calc(100% - 75px);
width : 100%;
display: block;
overflow: scroll;
}
#proxiesDiv .jobproxiesview-grid {
height: calc(100% - 75px);
width : 100%;
display: block;
}
.vs .action-label.icon.refreshIcon {
background-image: url('refresh.svg');
}
.vs-dark .action-label.icon.refreshIcon,
.hc-black .action-label.icon.refreshIcon {
background-image: url('refresh_inverse.svg');
}
.agent-actionbar-container .monaco-action-bar > ul.actions-container > li.action-item {
padding-left: 20px;
}
jobsview-component .jobview-grid .slick-cell.error-row {
opacity: 0;
}
#alertsDiv jobalertsview-component .jobalertsview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell {
border-right: transparent !important;
border-left: transparent !important;
line-height: 33px !important;
}
#alertsDiv .jobalertsview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
#alertsDiv .jobalertsview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell,
#alertsDiv .jobalertsview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row> .slick-cell.hovered {
background: #dcdcdc !important;
}
.vs-dark #alertsDiv .jobalertsview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
.vs-dark #alertsDiv .jobalertsview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered,
.vs-dark #alertsDiv .jobalertsview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell {
background: #444444 !important;
}
.vs-dark .jobalertsview-grid > .monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted white;
}
.jobalertsview-grid > .monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted #444444;
}
#operatorsDiv joboperatorsview-component .joboperatorsview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell {
border-right: transparent !important;
border-left: transparent !important;
line-height: 33px !important;
}
#operatorsDiv .joboperatorsview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
#operatorsDiv .joboperatorsview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell,
#operatorsDiv .joboperatorsview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row> .slick-cell.hovered {
background: #dcdcdc !important;
}
.vs-dark #operatorsDiv .joboperatorsview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
.vs-dark #operatorsDiv .joboperatorsview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered,
.vs-dark #operatorsDiv .joboperatorsview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell {
background: #444444 !important;
}
.vs-dark .joboperatorsview-grid > .monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted white;
}
.joboperatorsview-grid > .monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted #444444;
}
#proxiesDiv jobproxiesview-component .jobproxiesview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell {
border-right: transparent !important;
border-left: transparent !important;
line-height: 33px !important;
}
#proxiesDiv .jobproxiesview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
#proxiesDiv .jobproxiesview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell,
#proxiesDiv .jobproxiesview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row> .slick-cell.hovered {
background: #dcdcdc !important;
}
.vs-dark #proxiesDiv .jobproxiesview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
.vs-dark #proxiesDiv .jobproxiesview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered,
.vs-dark #proxiesDiv .jobproxiesview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell {
background: #444444 !important;
}
.vs-dark .jobproxiesview-grid > .monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted white;
}
.jobproxiesview-grid > .monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted #444444;
}
.overview-container > .overview-tab > label {
margin-bottom: 0px;
}
#operatorsDiv .operatorview-operatornameindicatorenabled,
#alertsDiv .alertview-alertnameindicatorenabled,
#proxiesDiv .proxyview-proxynameindicatorenabled {
width: 5px;
background: green;
}
#operatorsDiv .operatorview-operatornameindicatordisabled,
#alertsDiv .alertview-alertnameindicatordisabled,
#proxiesDiv .proxyview-proxynameindicatordisabled {
width: 5px;
background: red;
}
#jobsDiv jobsview-component .monaco-toolbar.carbon-taskbar,
#operatorsDiv joboperatorsview-component .monaco-toolbar.carbon-taskbar,
#alertsDiv jobalertsview-component .monaco-toolbar.carbon-taskbar,
#proxiesDiv jobproxiesview-component .monaco-toolbar.carbon-taskbar {
margin: 10px 0px 10px 0px;
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#231f20;}.cls-2{fill:#212121;}</style></defs><title>new_16x16</title><polygon class="cls-1" points="13.59 2.21 13.58 2.22 13.58 2.2 13.59 2.21"/><path class="cls-2" d="M16,7.5v1H8.5V16h-1V8.5H0v-1H7.5V0h1V7.5Z"/></svg>

After

Width:  |  Height:  |  Size: 336 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>new_inverse_16x16</title><polygon class="cls-1" points="13.59 2.21 13.58 2.22 13.58 2.2 13.59 2.21"/><path class="cls-1" d="M16,7.5v1H8.5V16h-1V8.5H0v-1H7.5V0h1V7.5Z"/></svg>

After

Width:  |  Height:  |  Size: 320 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>operator</title><path d="M10.36,9.41a7.54,7.54,0,0,1,1.9,1.05A7,7,0,0,1,13.73,12,6.9,6.9,0,0,1,15,16H14a5.79,5.79,0,0,0-.47-2.33,6.07,6.07,0,0,0-3.2-3.2A5.79,5.79,0,0,0,8,10a5.79,5.79,0,0,0-2.33.47,6.07,6.07,0,0,0-3.2,3.2A5.79,5.79,0,0,0,2,16H1a6.89,6.89,0,0,1,2.74-5.54,7.54,7.54,0,0,1,1.9-1.05A5.07,5.07,0,0,1,4,8a4.24,4.24,0,0,1-.73-.06,1.92,1.92,0,0,1-.64-.23,1.25,1.25,0,0,1-.45-.46A1.48,1.48,0,0,1,2,6.5v-3a1.47,1.47,0,0,1,.12-.59,1.49,1.49,0,0,1,.8-.8A1.47,1.47,0,0,1,3.5,2a1.4,1.4,0,0,1,.45.07,4.88,4.88,0,0,1,.8-.87,5.21,5.21,0,0,1,1-.65A4.95,4.95,0,0,1,8,0,4.88,4.88,0,0,1,9.95.39a5,5,0,0,1,2.66,2.66A4.88,4.88,0,0,1,13,5a4.93,4.93,0,0,1-.18,1.34,5,5,0,0,1-.53,1.23,5.12,5.12,0,0,1-.83,1A4.73,4.73,0,0,1,10.36,9.41ZM3,6.5a.51.51,0,0,0,.5.5H4V3.5a.5.5,0,0,0-.85-.35A.48.48,0,0,0,3,3.5ZM5.35,8a3.92,3.92,0,0,0,1.23.74A4,4,0,0,0,8,9a3.85,3.85,0,0,0,1.55-.32,4.05,4.05,0,0,0,2.13-2.13A3.85,3.85,0,0,0,12,5a3.85,3.85,0,0,0-.32-1.55A4.05,4.05,0,0,0,9.55,1.32,3.85,3.85,0,0,0,8,1a4,4,0,0,0-1.83.45A4,4,0,0,0,4.75,2.67a1.47,1.47,0,0,1,.18.51A5.75,5.75,0,0,1,5,3.91q0,.41,0,.85t0,.87q0,.42,0,.78T5,7H7.5a.5.5,0,0,1,0,1Z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>operator_inverse</title><path class="cls-1" d="M10.36,9.41a7.54,7.54,0,0,1,1.9,1.05A7,7,0,0,1,13.73,12,6.9,6.9,0,0,1,15,16H14a5.79,5.79,0,0,0-.47-2.33,6.07,6.07,0,0,0-3.2-3.2A5.79,5.79,0,0,0,8,10a5.79,5.79,0,0,0-2.33.47,6.07,6.07,0,0,0-3.2,3.2A5.79,5.79,0,0,0,2,16H1a6.89,6.89,0,0,1,2.74-5.54,7.54,7.54,0,0,1,1.9-1.05A5.07,5.07,0,0,1,4,8a4.24,4.24,0,0,1-.73-.06,1.92,1.92,0,0,1-.64-.23,1.25,1.25,0,0,1-.45-.46A1.48,1.48,0,0,1,2,6.5v-3a1.47,1.47,0,0,1,.12-.59,1.49,1.49,0,0,1,.8-.8A1.47,1.47,0,0,1,3.5,2a1.4,1.4,0,0,1,.45.07,4.88,4.88,0,0,1,.8-.87,5.21,5.21,0,0,1,1-.65A4.95,4.95,0,0,1,8,0,4.88,4.88,0,0,1,9.95.39a5,5,0,0,1,2.66,2.66A4.88,4.88,0,0,1,13,5a4.93,4.93,0,0,1-.18,1.34,5,5,0,0,1-.53,1.23,5.12,5.12,0,0,1-.83,1A4.73,4.73,0,0,1,10.36,9.41ZM3,6.5a.51.51,0,0,0,.5.5H4V3.5a.5.5,0,0,0-.85-.35A.48.48,0,0,0,3,3.5ZM5.35,8a3.92,3.92,0,0,0,1.23.74A4,4,0,0,0,8,9a3.85,3.85,0,0,0,1.55-.32,4.05,4.05,0,0,0,2.13-2.13A3.85,3.85,0,0,0,12,5a3.85,3.85,0,0,0-.32-1.55A4.05,4.05,0,0,0,9.55,1.32,3.85,3.85,0,0,0,8,1a4,4,0,0,0-1.83.45A4,4,0,0,0,4.75,2.67a1.47,1.47,0,0,1,.18.51A5.75,5.75,0,0,1,5,3.91q0,.41,0,.85t0,.87q0,.42,0,.78T5,7H7.5a.5.5,0,0,1,0,1Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>proxy_account</title><path d="M12.23,7a3,3,0,0,0-3.39-.77,3,3,0,0,0-1.6,1.6A2.93,2.93,0,0,0,7,9a3,3,0,0,1-1.23,2.41,4.31,4.31,0,0,1,.66.42,4.2,4.2,0,0,1,.57.53V15a2.93,2.93,0,0,0-.23-1.16,3,3,0,0,0-1.6-1.6,3,3,0,0,0-2.33,0,3,3,0,0,0-1.6,1.6A2.93,2.93,0,0,0,1,15H0a3.92,3.92,0,0,1,.16-1.11,4.11,4.11,0,0,1,.45-1,3.87,3.87,0,0,1,.7-.84,4.2,4.2,0,0,1,.92-.63,3,3,0,0,1-1-3.58,3,3,0,0,1,1.6-1.6A2.92,2.92,0,0,1,4,6,3,3,0,0,1,6.41,7.23,4.12,4.12,0,0,1,8.23,5.41,3,3,0,0,1,7,3a2.93,2.93,0,0,1,.23-1.16A3,3,0,0,1,8.83.23a3,3,0,0,1,2.33,0,3,3,0,0,1,1.6,1.6,3,3,0,0,1-.09,2.52,2.94,2.94,0,0,1-.9,1.06,4.07,4.07,0,0,1,1,.67,4,4,0,0,1,.73.92ZM4,11a1.94,1.94,0,0,0,.78-.16A2,2,0,0,0,5.84,9.78a2,2,0,0,0,0-1.55A2,2,0,0,0,4.78,7.16a2,2,0,0,0-1.55,0A2,2,0,0,0,2.16,8.22a2,2,0,0,0,0,1.55,2,2,0,0,0,1.07,1.07A1.94,1.94,0,0,0,4,11ZM8,3a1.94,1.94,0,0,0,.16.78A2,2,0,0,0,9.22,4.84a2,2,0,0,0,1.55,0,2,2,0,0,0,1.07-1.07,2,2,0,0,0,0-1.55,2,2,0,0,0-1.07-1.07,2,2,0,0,0-1.55,0A2,2,0,0,0,8.16,2.22,1.94,1.94,0,0,0,8,3Zm8,7v6H8V10h2V8h4v2Zm-1,1H9v1h6Zm0,2H14v1H13V13H11v1H10V13H9v2h6Zm-4-3h2V9H11Z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>proxy_account_inverse</title><path class="cls-1" d="M12.23,7a3,3,0,0,0-3.39-.77,3,3,0,0,0-1.6,1.6A2.93,2.93,0,0,0,7,9a3,3,0,0,1-1.23,2.41,4.31,4.31,0,0,1,.66.42,4.2,4.2,0,0,1,.57.53V15a2.93,2.93,0,0,0-.23-1.16,3,3,0,0,0-1.6-1.6,3,3,0,0,0-2.33,0,3,3,0,0,0-1.6,1.6A2.93,2.93,0,0,0,1,15H0a3.92,3.92,0,0,1,.16-1.11,4.11,4.11,0,0,1,.45-1A3.87,3.87,0,0,1,1.3,12a4.2,4.2,0,0,1,.92-.63,3,3,0,0,1-1-3.58,3,3,0,0,1,1.6-1.6A2.92,2.92,0,0,1,4,6,3,3,0,0,1,6.41,7.23,4.12,4.12,0,0,1,8.23,5.41,3,3,0,0,1,7,3a2.93,2.93,0,0,1,.23-1.16A3,3,0,0,1,8.83.23a3,3,0,0,1,2.33,0,3,3,0,0,1,1.6,1.6,3,3,0,0,1-.09,2.52,2.94,2.94,0,0,1-.9,1.06,4.07,4.07,0,0,1,1,.67,4,4,0,0,1,.73.92ZM4,11a1.94,1.94,0,0,0,.78-.16A2,2,0,0,0,5.84,9.78a2,2,0,0,0,0-1.55A2,2,0,0,0,4.78,7.16a2,2,0,0,0-1.55,0A2,2,0,0,0,2.16,8.22a2,2,0,0,0,0,1.55,2,2,0,0,0,1.07,1.07A1.94,1.94,0,0,0,4,11ZM8,3a1.94,1.94,0,0,0,.16.78A2,2,0,0,0,9.22,4.84a2,2,0,0,0,1.55,0,2,2,0,0,0,1.07-1.07,2,2,0,0,0,0-1.55,2,2,0,0,0-1.07-1.07,2,2,0,0,0-1.55,0A2,2,0,0,0,8.16,2.22,1.94,1.94,0,0,0,8,3Zm8,7v6H8V10h2V8h4v2Zm-1,1H9v1h6Zm0,2H14v1H13V13H11v1H10V13H9v2h6Zm-4-3h2V9H11Z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#212121;}</style></defs><title>refresh</title><path class="cls-1" d="M12.51,1.59a8.06,8.06,0,0,1,3.06,4A7.83,7.83,0,0,1,16,8.2a7.91,7.91,0,0,1-.29,2.12,8.13,8.13,0,0,1-.8,1.91A8,8,0,0,1,12,15.11a8.1,8.1,0,0,1-1.91.8,8.06,8.06,0,0,1-4.25,0A8.08,8.08,0,0,1,4,15.11a8,8,0,0,1-2.87-2.87,8.07,8.07,0,0,1-.8-1.91,8,8,0,0,1,0-4.25,8.11,8.11,0,0,1,.82-1.94,7.86,7.86,0,0,1,1.3-1.66A8,8,0,0,1,4.14,1.2H2V.2H6v4H5V1.88A7,7,0,0,0,1.28,6.24a7,7,0,0,0,0,3.82,7,7,0,0,0,1.8,3.09A7,7,0,0,0,6.14,15a7,7,0,0,0,3.71,0,7,7,0,0,0,1.67-.71,7,7,0,0,0,3.22-4.18,7,7,0,0,0-.13-4.12,7.07,7.07,0,0,0-2.68-3.52,6.78,6.78,0,0,0-2.07-1l.27-1A7.67,7.67,0,0,1,12.51,1.59Z"/></svg>

After

Width:  |  Height:  |  Size: 767 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>refresh_inverse</title><path class="cls-1" d="M12.51,1.59a8.06,8.06,0,0,1,3.06,4A7.83,7.83,0,0,1,16,8.2a7.91,7.91,0,0,1-.29,2.12,8.13,8.13,0,0,1-.8,1.91A8,8,0,0,1,12,15.11a8.1,8.1,0,0,1-1.91.8,8.06,8.06,0,0,1-4.25,0A8.08,8.08,0,0,1,4,15.11a8,8,0,0,1-2.87-2.87,8.07,8.07,0,0,1-.8-1.91,8,8,0,0,1,0-4.25,8.11,8.11,0,0,1,.82-1.94,7.86,7.86,0,0,1,1.3-1.66A8,8,0,0,1,4.14,1.2H2V.2H6v4H5V1.88A7,7,0,0,0,1.28,6.24a7,7,0,0,0,0,3.82,7,7,0,0,0,1.8,3.09A7,7,0,0,0,6.14,15a7,7,0,0,0,3.71,0,7,7,0,0,0,1.67-.71,7,7,0,0,0,3.22-4.18,7,7,0,0,0-.13-4.12,7.07,7.07,0,0,0-2.68-3.52,6.78,6.78,0,0,0-2.07-1l.27-1A7.67,7.67,0,0,1,12.51,1.59Z"/></svg>

After

Width:  |  Height:  |  Size: 772 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;clip-rule:evenodd;}.cls-2{clip-path:url(#clip-path);}</style><clipPath id="clip-path"><path class="cls-1" d="M1.14,10.29H3.43V8H1.14ZM4.57,6.86H6.86V4.57H4.57ZM1.14,3.43H3.43V1.14H1.14ZM8,3.43h2.29V1.14H8Zm-6.86,8H4.57V8H8V4.57h3.43V1.14h3.43V14.86H1.14ZM6.86,0V3.43H4.57V0H0V4.57H3.43V6.86H0V16H16V0Z"/></clipPath></defs><title>step</title><g class="cls-2"><rect x="-5.71" y="-5.71" width="27.43" height="27.43"/></g></svg>

After

Width:  |  Height:  |  Size: 590 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1,.cls-2,.cls-4{fill:#fff;}.cls-1{clip-rule:evenodd;}.cls-2{fill-rule:evenodd;}.cls-3{clip-path:url(#clip-path);}</style><clipPath id="clip-path"><path class="cls-1" d="M1.14,10.29H3.43V8H1.14ZM4.57,6.86H6.86V4.57H4.57ZM1.14,3.43H3.43V1.14H1.14ZM8,3.43h2.29V1.14H8Zm-6.86,8H4.57V8H8V4.57h3.43V1.14h3.43V14.86H1.14ZM6.86,0V3.43H4.57V0H0V4.57H3.43V6.86H0V16H16V0Z"/></clipPath></defs><title>step_inverse</title><path class="cls-2" d="M1.14,10.29H3.43V8H1.14ZM4.57,6.86H6.86V4.57H4.57ZM1.14,3.43H3.43V1.14H1.14ZM8,3.43h2.29V1.14H8Zm-6.86,8H4.57V8H8V4.57h3.43V1.14h3.43V14.86H1.14ZM6.86,0V3.43H4.57V0H0V4.57H3.43V6.86H0V16H16V0Z"/><g class="cls-3"><rect class="cls-4" x="-5.71" y="-5.71" width="27.43" height="27.43"/></g></svg>

After

Width:  |  Height:  |  Size: 878 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;clip-rule:evenodd;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#3bb44a;}.cls-4{clip-path:url(#clip-path-2);}.cls-5{fill:#fff;}</style><clipPath id="clip-path"><path class="cls-1" d="M16,8a7.92,7.92,0,0,1-1.09,4A8.15,8.15,0,0,1,12,14.91a8,8,0,0,1-8.07,0A8.15,8.15,0,0,1,1.09,12,8,8,0,0,1,1.09,4,8.15,8.15,0,0,1,4,1.09a8,8,0,0,1,8.07,0A8.15,8.15,0,0,1,14.91,4,7.92,7.92,0,0,1,16,8Z"/></clipPath><clipPath id="clip-path-2"><polygon class="cls-1" points="10.9 4.9 11.6 5.6 6.5 10.71 3.65 7.85 4.35 7.15 6.5 9.29 10.9 4.9"/></clipPath></defs><title>success_complete</title><g class="cls-2"><rect class="cls-3" x="-5" y="-5" width="26" height="26"/></g><g class="cls-4"><rect class="cls-5" x="-1.35" y="-0.1" width="17.95" height="15.81"/></g></svg>

After

Width:  |  Height:  |  Size: 911 B

View File

@@ -0,0 +1,15 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="job-heading-container">
<h1 class="job-heading" *ngIf="_isCloud === false">Operators</h1>
<h1 class="job-heading" *ngIf="_isCloud === true">No Operators Available</h1>
<div class="icon in-progress" *ngIf="_showProgressWheel === true"></div>
</div>
<div #actionbarContainer class="agent-actionbar-container"></div>
<div #operatorsgrid class="joboperatorsview-grid"></div>

View File

@@ -0,0 +1,222 @@
/*---------------------------------------------------------------------------------------------
* 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/jobs';
import * as dom from 'vs/base/browser/dom';
import * as nls from 'vs/nls';
import * as azdata from 'azdata';
import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { Table } from 'sql/base/browser/ui/table/table';
import { AgentViewComponent } from 'sql/workbench/parts/jobManagement/electron-browser/agentView.component';
import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces';
import { EditOperatorAction, DeleteOperatorAction, NewOperatorAction } from 'sql/platform/jobManagement/common/jobActions';
import { JobManagementView } from 'sql/workbench/parts/jobManagement/electron-browser/jobManagementView';
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IAction } from 'vs/base/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService';
import { OperatorsCacheObject } from 'sql/platform/jobManagement/common/jobManagementService';
import { RowDetailView } from 'sql/base/browser/ui/table/plugins/rowDetailView';
export const VIEW_SELECTOR: string = 'joboperatorsview-component';
export const ROW_HEIGHT: number = 45;
@Component({
selector: VIEW_SELECTOR,
templateUrl: decodeURI(require.toUrl('./operatorsView.component.html')),
providers: [{ provide: TabChild, useExisting: forwardRef(() => OperatorsViewComponent) }],
})
export class OperatorsViewComponent extends JobManagementView implements OnInit, OnDestroy {
private columns: Array<Slick.Column<any>> = [
{
name: nls.localize('jobOperatorsView.name', 'Name'),
field: 'name',
formatter: (row, cell, value, columnDef, dataContext) => this.renderName(row, cell, value, columnDef, dataContext),
width: 200,
id: 'name'
},
{ name: nls.localize('jobOperatorsView.emailAddress', 'Email Address'), field: 'emailAddress', width: 200, id: 'emailAddress' },
{ name: nls.localize('jobOperatorsView.enabled', 'Enabled'), field: 'enabled', width: 200, id: 'enabled' },
];
private options: Slick.GridOptions<any> = {
syncColumnCellResize: true,
enableColumnReorder: false,
rowHeight: ROW_HEIGHT,
enableCellNavigation: true,
editable: false
};
private dataView: any;
private _isCloud: boolean;
private _operatorsCacheObject: OperatorsCacheObject;
private _didTabChange: boolean;
@ViewChild('operatorsgrid') _gridEl: ElementRef;
public operators: azdata.AgentOperatorInfo[];
public contextAction = NewOperatorAction;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
@Inject(forwardRef(() => AgentViewComponent)) _agentViewComponent: AgentViewComponent,
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService,
@Inject(ICommandService) private _commandService: ICommandService,
@Inject(IInstantiationService) instantiationService: IInstantiationService,
@Inject(forwardRef(() => CommonServiceInterface)) commonService: CommonServiceInterface,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) _dashboardService: IDashboardService
) {
super(commonService, _dashboardService, contextMenuService, keybindingService, instantiationService, _agentViewComponent);
this._isCloud = commonService.connectionManagementService.connectionInfo.serverInfo.isCloud;
let operatorsCacheObject = this._jobManagementService.operatorsCacheObjectMap;
let operatorsCache = operatorsCacheObject[this._serverName];
if (operatorsCache) {
this._operatorsCacheObject = operatorsCache;
} else {
this._operatorsCacheObject = new OperatorsCacheObject();
this._operatorsCacheObject.serverName = this._serverName;
this._jobManagementService.addToCache(this._serverName, this._operatorsCacheObject);
}
}
ngOnInit() {
// set base class elements
this._visibilityElement = this._gridEl;
this._parentComponent = this._agentViewComponent;
}
ngOnDestroy() {
this._didTabChange = true;
}
public layout() {
let height = dom.getContentHeight(this._gridEl.nativeElement) - 10;
if (height < 0) {
height = 0;
}
if (this._table) {
this._table.layout(new dom.Dimension(
dom.getContentWidth(this._gridEl.nativeElement),
height));
}
}
onFirstVisible() {
let self = this;
let cached: boolean = false;
if (this._operatorsCacheObject.serverName === this._serverName) {
if (this._operatorsCacheObject.operators && this._operatorsCacheObject.operators.length > 0) {
cached = true;
this.operators = this._operatorsCacheObject.operators;
}
}
let columns = this.columns.map((column) => {
column.rerenderOnResize = true;
return column;
});
this.dataView = new Slick.Data.DataView({ inlineFilters: false });
let rowDetail = new RowDetailView({
cssClass: '_detail_selector',
useRowClick: false,
panelRows: 1
});
columns.unshift(rowDetail.getColumnDefinition());
jQuery(this._gridEl.nativeElement).empty();
jQuery(this.actionBarContainer.nativeElement).empty();
this.initActionBar();
this._table = new Table(this._gridEl.nativeElement, { columns }, this.options);
this._table.grid.setData(this.dataView, true);
this._register(this._table.onContextMenu(e => {
self.openContextMenu(e);
}));
// check for cached state
if (cached && this._agentViewComponent.refresh !== true) {
this.onOperatorsAvailable(this.operators);
this._showProgressWheel = false;
if (this.isVisible) {
this._cd.detectChanges();
}
} else {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
this._jobManagementService.getOperators(ownerUri).then((result) => {
if (result && result.operators) {
self.operators = result.operators;
self._operatorsCacheObject.operators = result.operators;
self.onOperatorsAvailable(result.operators);
} else {
// TODO: handle error
}
this._showProgressWheel = false;
if (this.isVisible && !this._didTabChange) {
this._cd.detectChanges();
} else if (this._didTabChange) {
return;
}
});
}
}
private onOperatorsAvailable(operators: azdata.AgentOperatorInfo[]) {
let items: any = operators.map((item) => {
return {
id: item.id,
name: item.name,
emailAddress: item.emailAddress,
enabled: item.enabled
};
});
this.dataView.beginUpdate();
this.dataView.setItems(items);
this.dataView.endUpdate();
this._operatorsCacheObject.dataview = this.dataView;
this._table.autosizeColumns();
this._table.resizeCanvas();
}
protected getTableActions(): IAction[] {
let actions: IAction[] = [];
actions.push(this._instantiationService.createInstance(EditOperatorAction));
actions.push(this._instantiationService.createInstance(DeleteOperatorAction));
return actions;
}
protected getCurrentTableObject(rowIndex: number): any {
return (this.operators && this.operators.length >= rowIndex)
? this.operators[rowIndex]
: undefined;
}
private renderName(row, cell, value, columnDef, dataContext) {
let resultIndicatorClass = dataContext.enabled ? 'operatorview-operatornameindicatorenabled' :
'operatorview-operatornameindicatordisabled';
return '<table class="operatorview-operatornametable"><tr class="operatorview-operatornamerow">' +
'<td nowrap class=' + resultIndicatorClass + '></td>' +
'<td nowrap class="operatorview-operatornametext">' + dataContext.name + '</td>' +
'</tr></table>';
}
public openCreateOperatorDialog() {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
this._commandService.executeCommand('agent.openOperatorDialog', ownerUri);
}
}

View File

@@ -0,0 +1,15 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="job-heading-container">
<h1 class="job-heading" *ngIf="_isCloud === false">Proxies</h1>
<h1 class="job-heading" *ngIf="_isCloud === true">No Proxies Available</h1>
<div class="icon in-progress" *ngIf="_showProgressWheel === true"></div>
</div>
<div #actionbarContainer class="agent-actionbar-container"></div>
<div #proxiesgrid class="jobproxiesview-grid"></div>

View File

@@ -0,0 +1,230 @@
/*---------------------------------------------------------------------------------------------
* 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/jobs';
import * as dom from 'vs/base/browser/dom';
import * as azdata from 'azdata';
import * as nls from 'vs/nls';
import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { Table } from 'sql/base/browser/ui/table/table';
import { AgentViewComponent } from 'sql/workbench/parts/jobManagement/electron-browser/agentView.component';
import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces';
import { EditProxyAction, DeleteProxyAction, NewProxyAction } from 'sql/platform/jobManagement/common/jobActions';
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { JobManagementView } from 'sql/workbench/parts/jobManagement/electron-browser/jobManagementView';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IAction } from 'vs/base/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService';
import { ProxiesCacheObject } from 'sql/platform/jobManagement/common/jobManagementService';
import { RowDetailView } from 'sql/base/browser/ui/table/plugins/rowDetailView';
export const VIEW_SELECTOR: string = 'jobproxiesview-component';
export const ROW_HEIGHT: number = 45;
@Component({
selector: VIEW_SELECTOR,
templateUrl: decodeURI(require.toUrl('./proxiesView.component.html')),
providers: [{ provide: TabChild, useExisting: forwardRef(() => ProxiesViewComponent) }],
})
export class ProxiesViewComponent extends JobManagementView implements OnInit, OnDestroy {
private NewProxyText: string = nls.localize('jobProxyToolbar-NewItem', "New Proxy");
private RefreshText: string = nls.localize('jobProxyToolbar-Refresh', "Refresh");
private columns: Array<Slick.Column<any>> = [
{
name: nls.localize('jobProxiesView.accountName', 'Account Name'),
field: 'accountName',
formatter: (row, cell, value, columnDef, dataContext) => this.renderName(row, cell, value, columnDef, dataContext),
width: 200,
id: 'accountName'
},
{ name: nls.localize('jobProxiesView.credentialName', 'Credential Name'), field: 'credentialName', width: 200, id: 'credentialName' },
{ name: nls.localize('jobProxiesView.description', 'Description'), field: 'description', width: 200, id: 'description' },
{ name: nls.localize('jobProxiesView.isEnabled', 'Enabled'), field: 'isEnabled', width: 200, id: 'isEnabled' }
];
private options: Slick.GridOptions<any> = {
syncColumnCellResize: true,
enableColumnReorder: false,
rowHeight: ROW_HEIGHT,
enableCellNavigation: true,
editable: false
};
private dataView: any;
private _isCloud: boolean;
private _proxiesCacheObject: ProxiesCacheObject;
public proxies: azdata.AgentProxyInfo[];
public readonly contextAction = NewProxyAction;
private _didTabChange: boolean;
@ViewChild('proxiesgrid') _gridEl: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
@Inject(forwardRef(() => AgentViewComponent)) _agentViewComponent: AgentViewComponent,
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService,
@Inject(ICommandService) private _commandService: ICommandService,
@Inject(IInstantiationService) instantiationService: IInstantiationService,
@Inject(forwardRef(() => CommonServiceInterface)) commonService: CommonServiceInterface,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) _dashboardService: IDashboardService
) {
super(commonService, _dashboardService, contextMenuService, keybindingService, instantiationService, _agentViewComponent);
this._isCloud = commonService.connectionManagementService.connectionInfo.serverInfo.isCloud;
let proxiesCacheObjectMap = this._jobManagementService.proxiesCacheObjectMap;
let proxiesCacheObject = proxiesCacheObjectMap[this._serverName];
if (proxiesCacheObject) {
this._proxiesCacheObject = proxiesCacheObject;
} else {
this._proxiesCacheObject = new ProxiesCacheObject();
this._proxiesCacheObject.serverName = this._serverName;
this._jobManagementService.addToCache(this._serverName, this._proxiesCacheObject);
}
}
ngOnInit() {
// set base class elements
this._visibilityElement = this._gridEl;
this._parentComponent = this._agentViewComponent;
}
ngOnDestroy() {
this._didTabChange = true;
}
public layout() {
let height = dom.getContentHeight(this._gridEl.nativeElement) - 10;
if (height < 0) {
height = 0;
}
if (this._table) {
this._table.layout(new dom.Dimension(
dom.getContentWidth(this._gridEl.nativeElement),
height));
}
}
onFirstVisible() {
let self = this;
let cached: boolean = false;
if (this._proxiesCacheObject.serverName === this._serverName) {
if (this._proxiesCacheObject.proxies && this._proxiesCacheObject.proxies.length > 0) {
cached = true;
this.proxies = this._proxiesCacheObject.proxies;
}
}
let columns = this.columns.map((column) => {
column.rerenderOnResize = true;
return column;
});
this.dataView = new Slick.Data.DataView({ inlineFilters: false });
let rowDetail = new RowDetailView({
cssClass: '_detail_selector',
useRowClick: false,
panelRows: 1
});
columns.unshift(rowDetail.getColumnDefinition());
jQuery(this._gridEl.nativeElement).empty();
jQuery(this.actionBarContainer.nativeElement).empty();
this.initActionBar();
this._table = new Table(this._gridEl.nativeElement, { columns }, this.options);
this._table.grid.setData(this.dataView, true);
this._register(this._table.onContextMenu(e => {
self.openContextMenu(e);
}));
// checked for cached state
if (cached && this._agentViewComponent.refresh !== true) {
self.onProxiesAvailable(this.proxies);
this._showProgressWheel = false;
if (this.isVisible) {
this._cd.detectChanges();
}
} else {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
this._jobManagementService.getProxies(ownerUri).then((result) => {
if (result && result.proxies) {
self.proxies = result.proxies;
self._proxiesCacheObject.proxies = result.proxies;
self.onProxiesAvailable(result.proxies);
} else {
// TODO: handle error
}
this._showProgressWheel = false;
if (this.isVisible && !this._didTabChange) {
this._cd.detectChanges();
} else if (this._didTabChange) {
return;
}
});
}
}
private onProxiesAvailable(proxies: azdata.AgentProxyInfo[]) {
let items: any = proxies.map((item) => {
return {
id: item.accountName,
accountName: item.accountName,
credentialName: item.credentialName,
description: item.description,
isEnabled: item.isEnabled
};
});
this.dataView.beginUpdate();
this.dataView.setItems(items);
this.dataView.endUpdate();
this._proxiesCacheObject.dataview = this.dataView;
this._table.autosizeColumns();
this._table.resizeCanvas();
}
protected getTableActions(): IAction[] {
let actions: IAction[] = [];
actions.push(this._instantiationService.createInstance(EditProxyAction));
actions.push(this._instantiationService.createInstance(DeleteProxyAction));
return actions;
}
protected getCurrentTableObject(rowIndex: number): any {
return (this.proxies && this.proxies.length >= rowIndex)
? this.proxies[rowIndex]
: undefined;
}
private renderName(row, cell, value, columnDef, dataContext) {
let resultIndicatorClass = dataContext.isEnabled ? 'proxyview-proxynameindicatorenabled' :
'proxyview-proxynameindicatordisabled';
return '<table class="proxyview-proxynametable"><tr class="proxyview-proxynamerow">' +
'<td nowrap class=' + resultIndicatorClass + '></td>' +
'<td nowrap class="proxyview-proxynametext">' + dataContext.accountName + '</td>' +
'</tr></table>';
}
public openCreateProxyDialog() {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
this._jobManagementService.getCredentials(ownerUri).then((result) => {
if (result && result.credentials) {
this._commandService.executeCommand('agent.openProxyDialog', ownerUri, undefined, result.credentials);
}
});
}
}

View File

@@ -7,7 +7,7 @@ import 'vs/css!./code';
import { OnInit, Component, Input, Inject, ElementRef, ViewChild, Output, EventEmitter, OnChanges, SimpleChange, forwardRef, ChangeDetectorRef } from '@angular/core';
import { AngularDisposable } from 'sql/base/node/lifecycle';
import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor';
import { QueryTextEditor } from 'sql/workbench/electron-browser/modelComponents/queryTextEditor';
import { CellToggleMoreActions } from 'sql/workbench/parts/notebook/cellToggleMoreActions';
import { ICellModel, notebookConstants } from 'sql/workbench/parts/notebook/models/modelInterfaces';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';

View File

@@ -27,7 +27,7 @@ import { TextCellComponent } from 'sql/workbench/parts/notebook/cellViews/textCe
import { OutputAreaComponent } from 'sql/workbench/parts/notebook/cellViews/outputArea.component';
import { OutputComponent } from 'sql/workbench/parts/notebook/cellViews/output.component';
import { PlaceholderCellComponent } from 'sql/workbench/parts/notebook/cellViews/placeholderCell.component';
import LoadingSpinner from 'sql/parts/modelComponents/loadingSpinner.component';
import LoadingSpinner from 'sql/workbench/electron-browser/modelComponents/loadingSpinner.component';
export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => {
@NgModule({

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.065 13H15v2H2.056v-2h5.009zm3.661-12H7.385L8.44 2.061 7.505 3H15V1h-4.274zM3.237 9H2.056v2H15V9H3.237zm4.208-4l.995 1-.995 1H15V5H7.445z" fill="#424242"/><path d="M5.072 4.03L7.032 6 5.978 7.061l-1.96-1.97-1.961 1.97L1 6l1.96-1.97L1 2.061 2.056 1l1.96 1.97L5.977 1l1.057 1.061L5.072 4.03z" fill="#A1260D"/></svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.065 13H15v2H2.056v-2h5.009zm3.661-12H7.385L8.44 2.061 7.505 3H15V1h-4.274zM3.237 9H2.056v2H15V9H3.237zm4.208-4l.995 1-.995 1H15V5H7.445z" fill="#C5C5C5"/><path d="M5.072 4.03L7.032 6 5.978 7.061l-1.96-1.97-1.961 1.97L1 6l1.96-1.97L1 2.061 2.056 1l1.96 1.97L5.977 1l1.057 1.061L5.072 4.03z" fill="#F48771"/></svg>

After

Width:  |  Height:  |  Size: 419 B

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.
*--------------------------------------------------------------------------------------------*/
.profiler-column-tree {
height: calc(100% - 25px);
width: 100%;
}
.tree-row > * {
display: inline-block;
}
.vs .icon.clear-results {
background-image: url('clear.svg');
}
.vs-dark .icon.clear-results {
background-image: url('clear_inverse.svg');
}

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.profiler-filter-dialog {
height: 300px;
padding: 10px;
overflow-y: scroll;
}
.profiler-filter-clause-table {
width: 100%;
margin-bottom: 10px;
}
.profiler-filter-remove-condition {
width:20px;
height:20px;
cursor: pointer;
}
.profiler-filter-add-clause-prompt {
cursor: pointer;
margin: 0px 2px 0px 2px
}

View File

@@ -0,0 +1,324 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import * as nls from 'vs/nls';
import { ProfilerInput } from 'sql/workbench/parts/profiler/browser/profilerInput';
import { ProfilerEditor } from 'sql/workbench/parts/profiler/browser/profilerEditor';
import { PROFILER_VIEW_TEMPLATE_SETTINGS, PROFILER_SESSION_TEMPLATE_SETTINGS, IProfilerViewTemplate, IProfilerSessionTemplate } from 'sql/workbench/services/profiler/common/interfaces';
const profilerDescriptor = new EditorDescriptor(
ProfilerEditor,
ProfilerEditor.ID,
'ProfilerEditor'
);
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
.registerEditor(profilerDescriptor, [new SyncDescriptor(ProfilerInput)]);
const profilerViewTemplateSchema: IJSONSchema = {
description: nls.localize('profiler.settings.viewTemplates', "Specifies view templates"),
type: 'array',
items: <IJSONSchema>{
type: 'object',
properties: {
name: {
type: 'string'
}
}
},
default: <Array<IProfilerViewTemplate>>[
{
name: 'Standard View',
columns: [
{
name: 'EventClass',
eventsMapped: ['name']
},
{
name: 'TextData',
eventsMapped: ['options_text', 'batch_text', 'statement']
},
{
name: 'ApplicationName',
width: '1',
eventsMapped: ['client_app_name']
},
{
name: 'NTUserName',
eventsMapped: ['nt_username']
},
{
name: 'LoginName',
eventsMapped: ['server_principal_name']
},
{
name: 'ClientProcessID',
eventsMapped: ['client_pid']
},
{
name: 'SPID',
eventsMapped: ['session_id']
},
{
name: 'StartTime',
eventsMapped: ['timestamp']
},
{
name: 'CPU',
eventsMapped: ['cpu_time']
},
{
name: 'Reads',
eventsMapped: ['logical_reads']
},
{
name: 'Writes',
eventsMapped: ['writes']
},
{
name: 'Duration',
eventsMapped: ['duration']
}
]
},
{
name: 'TSQL View',
columns: [
{
name: 'EventClass',
eventsMapped: ['name']
},
{
name: 'TextData',
eventsMapped: ['options_text', 'batch_text', 'statement']
},
{
name: 'SPID',
eventsMapped: ['session_id']
},
{
name: 'StartTime',
eventsMapped: ['timestamp']
}
]
},
{
name: 'Tuning View',
columns: [
{
name: 'EventClass',
eventsMapped: ['name']
},
{
name: 'TextData',
eventsMapped: ['options_text', 'batch_text', 'statement']
},
{
name: 'Duration',
eventsMapped: ['duration']
},
{
name: 'SPID',
eventsMapped: ['session_id']
},
{
name: 'DatabaseID',
eventsMapped: ['database_id']
},
{
name: 'DatabaseName',
eventsMapped: ['database_name']
},
{
name: 'ObjectType',
eventsMapped: ['object_type']
},
{
name: 'LoginName',
eventsMapped: ['server_principal_name']
}
]
},
{
name: 'TSQL_Locks View',
columns: [
{
name: 'EventClass',
eventsMapped: ['name']
},
{
name: 'TextData',
eventsMapped: ['options_text', 'batch_text', 'statement']
},
{
name: 'ApplicationName',
eventsMapped: ['client_app_name']
},
{
name: 'NTUserName',
eventsMapped: ['nt_username']
},
{
name: 'LoginName',
eventsMapped: ['server_principal_name']
},
{
name: 'ClientProcessID',
eventsMapped: ['client_pid']
},
{
name: 'SPID',
eventsMapped: ['session_id']
},
{
name: 'StartTime',
eventsMapped: ['timestamp']
},
{
name: 'CPU',
eventsMapped: ['cpu_time']
},
{
name: 'Reads',
eventsMapped: ['logical_reads']
},
{
name: 'Writes',
eventsMapped: ['writes']
},
{
name: 'Duration',
eventsMapped: ['duration']
}
]
},
{
name: 'TSQL_Duration View',
columns: [
{
name: 'EventClass',
eventsMapped: ['name']
},
{
name: 'Duration',
eventsMapped: ['duration']
},
{
name: 'TextData',
eventsMapped: ['options_text', 'batch_text', 'statement']
},
{
name: 'SPID',
eventsMapped: ['session_id']
}
]
}
]
};
const profilerSessionTemplateSchema: IJSONSchema = {
description: nls.localize('profiler.settings.sessionTemplates', "Specifies session templates"),
type: 'array',
items: <IJSONSchema>{
type: 'object',
properties: {
name: {
type: 'string'
}
}
},
default: <Array<IProfilerSessionTemplate>>[
{
name: 'Standard_OnPrem',
defaultView: 'Standard View',
createStatement:
`CREATE EVENT SESSION [{sessionName}] ON SERVER
ADD EVENT sqlserver.attention(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.nt_username,sqlserver.query_hash,sqlserver.server_principal_name,sqlserver.session_id)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.existing_connection(SET collect_options_text=(1)
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.nt_username,sqlserver.server_principal_name,sqlserver.session_id)),
ADD EVENT sqlserver.login(SET collect_options_text=(1)
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.nt_username,sqlserver.server_principal_name,sqlserver.session_id)),
ADD EVENT sqlserver.logout(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.nt_username,sqlserver.server_principal_name,sqlserver.session_id)),
ADD EVENT sqlserver.rpc_completed(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.nt_username,sqlserver.query_hash,sqlserver.server_principal_name,sqlserver.session_id)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.sql_batch_completed(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.nt_username,sqlserver.query_hash,sqlserver.server_principal_name,sqlserver.session_id)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.sql_batch_starting(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.nt_username,sqlserver.query_hash,sqlserver.server_principal_name,sqlserver.session_id)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0))))
ADD TARGET package0.ring_buffer(SET max_events_limit=(1000),max_memory=(51200))
WITH (MAX_MEMORY=8192 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,MAX_DISPATCH_LATENCY=5 SECONDS,MAX_EVENT_SIZE=0 KB,MEMORY_PARTITION_MODE=PER_CPU,TRACK_CAUSALITY=ON,STARTUP_STATE=OFF)`
},
{
name: 'Standard_Azure',
defaultView: 'Standard View',
createStatement:
`CREATE EVENT SESSION [{sessionName}] ON DATABASE
ADD EVENT sqlserver.attention(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.username,sqlserver.query_hash,sqlserver.session_id)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.existing_connection(SET collect_options_text=(1)
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.username,sqlserver.session_id)),
ADD EVENT sqlserver.login(SET collect_options_text=(1)
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.username,sqlserver.session_id)),
ADD EVENT sqlserver.logout(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.username,sqlserver.session_id)),
ADD EVENT sqlserver.rpc_completed(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.username,sqlserver.query_hash,sqlserver.session_id)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.sql_batch_completed(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.username,sqlserver.query_hash,sqlserver.session_id)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.sql_batch_starting(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.username,sqlserver.query_hash,sqlserver.session_id)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0))))
ADD TARGET package0.ring_buffer(SET max_events_limit=(1000),max_memory=(51200))
WITH (MAX_MEMORY=8192 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,MAX_DISPATCH_LATENCY=5 SECONDS,MAX_EVENT_SIZE=0 KB,MEMORY_PARTITION_MODE=PER_CPU,TRACK_CAUSALITY=ON,STARTUP_STATE=OFF)`
},
{
name: 'TSQL_OnPrem',
defaultView: 'TSQL View',
createStatement:
`CREATE EVENT SESSION [{sessionName}] ON SERVER
ADD EVENT sqlserver.existing_connection(
ACTION(package0.event_sequence,sqlserver.session_id,sqlserver.client_hostname)),
ADD EVENT sqlserver.login(SET collect_options_text=(1)
ACTION(package0.event_sequence,sqlserver.session_id,sqlserver.client_hostname)),
ADD EVENT sqlserver.logout(
ACTION(package0.event_sequence,sqlserver.session_id)),
ADD EVENT sqlserver.rpc_starting(
ACTION(package0.event_sequence,sqlserver.session_id,sqlserver.database_name)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.sql_batch_starting(
ACTION(package0.event_sequence,sqlserver.session_id,sqlserver.database_name)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0))))
ADD TARGET package0.ring_buffer(SET max_events_limit=(1000),max_memory=(51200))
WITH (MAX_MEMORY=8192 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,MAX_DISPATCH_LATENCY=5 SECONDS,MAX_EVENT_SIZE=0 KB,MEMORY_PARTITION_MODE=PER_CPU,TRACK_CAUSALITY=ON,STARTUP_STATE=OFF)`
}
]
};
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
const dashboardConfig: IConfigurationNode = {
id: 'Profiler',
type: 'object',
properties: {
[PROFILER_VIEW_TEMPLATE_SETTINGS]: profilerViewTemplateSchema,
[PROFILER_SESSION_TEMPLATE_SETTINGS]: profilerSessionTemplateSchema
}
};
configurationRegistry.registerConfiguration(dashboardConfig);

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { ProfilerInput } from 'sql/workbench/parts/profiler/browser/profilerInput';
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
import { IProfilerService } from 'sql/workbench/services/profiler/common/interfaces';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ProfilerEditor } from 'sql/workbench/parts/profiler/browser/profilerEditor';
import { ObjectExplorerActionsContext } from 'sql/workbench/parts/objectExplorer/browser/objectExplorerActions';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
CommandsRegistry.registerCommand({
id: 'profiler.newProfiler',
handler: (accessor: ServicesAccessor, ...args: any[]) => {
let connectionProfile: ConnectionProfile = undefined;
let instantiationService: IInstantiationService = accessor.get(IInstantiationService);
let editorService: IEditorService = accessor.get(IEditorService);
let connectionService: IConnectionManagementService = accessor.get(IConnectionManagementService);
let objectExplorerService: IObjectExplorerService = accessor.get(IObjectExplorerService);
let connectionDialogService: IConnectionDialogService = accessor.get(IConnectionDialogService);
let capabilitiesService: ICapabilitiesService = accessor.get(ICapabilitiesService);
// If a context is available if invoked from the context menu, we will use the connection profiler of the server node
if (args && args.length === 1 && args[0] && args[0] instanceof ObjectExplorerActionsContext) {
let context = args[0] as ObjectExplorerActionsContext;
connectionProfile = ConnectionProfile.fromIConnectionProfile(capabilitiesService, context.connectionProfile);
}
else {
// No context available, we will try to get the current global active connection
connectionProfile = TaskUtilities.getCurrentGlobalConnection(objectExplorerService, connectionService, editorService) as ConnectionProfile;
}
let promise;
if (connectionProfile) {
promise = connectionService.connectIfNotConnected(connectionProfile, 'connection', true);
} else {
// if still no luck, we will open the Connection dialog and let user connect to a server
promise = connectionDialogService.openDialogAndWait(connectionService, { connectionType: 0, showDashboard: false, providers: [mssqlProviderName] }).then((profile) => {
connectionProfile = profile as ConnectionProfile;
});
}
return promise.then(() => {
if (!connectionProfile) {
connectionProfile = TaskUtilities.getCurrentGlobalConnection(objectExplorerService, connectionService, editorService) as ConnectionProfile;
}
if (connectionProfile && connectionProfile.providerName === mssqlProviderName) {
let profilerInput = instantiationService.createInstance(ProfilerInput, connectionProfile);
editorService.openEditor(profilerInput, { pinned: true }, ACTIVE_GROUP).then(() => Promise.resolve(true));
}
});
}
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'profiler.newProfiler',
weight: KeybindingWeight.BuiltinExtension,
when: undefined,
primary: KeyMod.Alt | KeyCode.KEY_P,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.KEY_P },
handler: CommandsRegistry.getCommand('profiler.newProfiler').handler
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: 'profiler.toggleStartStop',
weight: KeybindingWeight.EditorContrib,
when: undefined,
primary: KeyMod.Alt | KeyCode.KEY_S,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.KEY_S },
handler: (accessor: ServicesAccessor) => {
let profilerService: IProfilerService = accessor.get(IProfilerService);
let editorService: IEditorService = accessor.get(IEditorService);
let activeEditor = editorService.activeControl;
if (activeEditor instanceof ProfilerEditor) {
let profilerInput = activeEditor.input;
if (profilerInput.state.isRunning) {
return profilerService.stopSession(profilerInput.id);
} else {
// clear data when profiler is started
profilerInput.data.clear();
return profilerService.startSession(profilerInput.id, profilerInput.sessionName);
}
}
return Promise.resolve(false);
}
});

View File

@@ -0,0 +1,329 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IProfilerService } from 'sql/workbench/services/profiler/common/interfaces';
import { IProfilerController } from 'sql/workbench/parts/profiler/common/interfaces';
import { ProfilerInput } from 'sql/workbench/parts/profiler/browser/profilerInput';
import { Task } from 'sql/platform/tasks/common/tasks';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { IConnectionManagementService, IConnectionCompletionOptions } from 'sql/platform/connection/common/connectionManagement';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { Action } from 'vs/base/common/actions';
import * as nls from 'vs/nls';
import { IEditorAction } from 'vs/editor/common/editorCommon';
import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
export class ProfilerConnect extends Action {
private static readonly ConnectText = nls.localize('profilerAction.connect', 'Connect');
private static readonly DisconnectText = nls.localize('profilerAction.disconnect', 'Disconnect');
public static ID = 'profiler.connect';
public static LABEL = ProfilerConnect.ConnectText;
private _connected: boolean = false;
constructor(
id: string, label: string,
@IProfilerService private _profilerService: IProfilerService
) {
super(id, label, 'connect');
}
public run(input: ProfilerInput): Promise<boolean> {
this.enabled = false;
if (!this._connected) {
return Promise.resolve(this._profilerService.connectSession(input.id).then(() => {
this.enabled = true;
this.connected = true;
input.state.change({ isConnected: true, isRunning: false, isPaused: false, isStopped: true });
return true;
}));
} else {
return Promise.resolve(this._profilerService.disconnectSession(input.id).then(() => {
this.enabled = true;
this.connected = false;
input.state.change({ isConnected: false, isRunning: false, isPaused: false, isStopped: false });
return true;
}));
}
}
public set connected(value: boolean) {
this._connected = value;
this._setClass(value ? 'disconnect' : 'connect');
this._setLabel(value ? ProfilerConnect.DisconnectText : ProfilerConnect.ConnectText);
}
public get connected(): boolean {
return this._connected;
}
}
export class ProfilerStart extends Action {
public static ID = 'profiler.start';
public static LABEL = nls.localize('start', "Start");
constructor(
id: string, label: string,
@IProfilerService private _profilerService: IProfilerService
) {
super(id, label, 'sql start');
}
public run(input: ProfilerInput): Promise<boolean> {
input.data.clear();
return Promise.resolve(this._profilerService.startSession(input.id, input.sessionName));
}
}
export class ProfilerCreate extends Action {
public static ID = 'profiler.create';
public static LABEL = nls.localize('create', "New Session");
constructor(
id: string, label: string,
@ICommandService private _commandService: ICommandService,
@IProfilerService private _profilerService: IProfilerService,
@INotificationService private _notificationService: INotificationService
) {
super(id, label, 'add');
}
public run(input: ProfilerInput): Promise<boolean> {
return Promise.resolve(this._profilerService.launchCreateSessionDialog(input).then(() => {
return true;
}));
}
}
export class ProfilerPause extends Action {
private static readonly PauseText = nls.localize('profilerAction.pauseCapture', 'Pause');
private static readonly ResumeText = nls.localize('profilerAction.resumeCapture', 'Resume');
private static readonly PauseCssClass = 'sql pause';
private static readonly ResumeCssClass = 'sql continue';
public static ID = 'profiler.pause';
public static LABEL = ProfilerPause.PauseText;
private _paused: boolean = false;
constructor(
id: string, label: string,
@IProfilerService private _profilerService: IProfilerService
) {
super(id, label, ProfilerPause.PauseCssClass);
}
public run(input: ProfilerInput): Promise<boolean> {
return Promise.resolve(this._profilerService.pauseSession(input.id).then(() => {
this.paused = !this._paused;
input.state.change({ isPaused: this.paused, isStopped: false, isRunning: !this.paused });
return true;
}));
}
public set paused(value: boolean) {
this._paused = value;
this._setClass(value ? ProfilerPause.ResumeCssClass : ProfilerPause.PauseCssClass);
this._setLabel(value ? ProfilerPause.ResumeText : ProfilerPause.PauseText);
}
public get paused(): boolean {
return this._paused;
}
}
export class ProfilerStop extends Action {
public static ID = 'profiler.stop';
public static LABEL = nls.localize('profilerStop.stop', "Stop");
constructor(
id: string, label: string,
@IProfilerService private _profilerService: IProfilerService
) {
super(id, label, 'sql stop');
}
public run(input: ProfilerInput): Promise<boolean> {
return Promise.resolve(this._profilerService.stopSession(input.id));
}
}
export class ProfilerClear extends Action {
public static ID = 'profiler.clear';
public static LABEL = nls.localize('profiler.clear', "Clear Data");
constructor(id: string, label: string) {
super(id, label, 'clear-results');
}
run(input: ProfilerInput): Promise<void> {
input.data.clear();
return Promise.resolve(null);
}
}
export class ProfilerAutoScroll extends Action {
private static readonly AutoScrollOnText = nls.localize('profilerAction.autoscrollOn', 'Auto Scroll: On');
private static readonly AutoScrollOffText = nls.localize('profilerAction.autoscrollOff', 'Auto Scroll: Off');
private static readonly CheckedCssClass = 'sql checked';
public static ID = 'profiler.autoscroll';
public static LABEL = ProfilerAutoScroll.AutoScrollOnText;
constructor(id: string, label: string) {
super(id, label, ProfilerAutoScroll.CheckedCssClass);
}
run(input: ProfilerInput): Promise<boolean> {
this.checked = !this.checked;
this._setLabel(this.checked ? ProfilerAutoScroll.AutoScrollOnText : ProfilerAutoScroll.AutoScrollOffText);
this._setClass(this.checked ? ProfilerAutoScroll.CheckedCssClass : '');
input.state.change({ autoscroll: this.checked });
return Promise.resolve(true);
}
}
export class ProfilerCollapsablePanelAction extends Action {
public static ID = 'profiler.toggleCollapsePanel';
public static LABEL = nls.localize('profiler.toggleCollapsePanel', "Toggle Collapsed Panel");
private _collapsed: boolean;
constructor(id: string, label: string) {
super(id, label, 'minimize-panel-action');
}
public run(input: ProfilerInput): Promise<boolean> {
this.collapsed = !this._collapsed;
input.state.change({ isPanelCollapsed: this._collapsed });
return Promise.resolve(true);
}
set collapsed(val: boolean) {
this._collapsed = val === false ? false : true;
this._setClass(this._collapsed ? 'maximize-panel-action' : 'minimize-panel-action');
}
}
export class ProfilerEditColumns extends Action {
public static ID = 'profiler.';
public static LABEL = nls.localize('profiler.editColumns', "Edit Columns");
constructor(
id: string, label: string,
@IProfilerService private _profilerService: IProfilerService
) {
super(id, label);
}
public run(input: ProfilerInput): Promise<boolean> {
return Promise.resolve(this._profilerService.launchColumnEditor(input)).then(() => true);
}
}
export class ProfilerFindNext implements IEditorAction {
public readonly id = 'profiler.findNext';
public readonly label = nls.localize('profiler.findNext', "Find Next String");
public readonly alias = '';
constructor(private profiler: IProfilerController) { }
run(): Promise<void> {
this.profiler.findNext();
return Promise.resolve(null);
}
isSupported(): boolean {
return true;
}
}
export class ProfilerFindPrevious implements IEditorAction {
public readonly id = 'profiler.findPrevious';
public readonly label = nls.localize('profiler.findPrevious', "Find Previous String");
public readonly alias = '';
constructor(private profiler: IProfilerController) { }
run(): Promise<void> {
this.profiler.findPrevious();
return Promise.resolve(null);
}
isSupported(): boolean {
return true;
}
}
export class NewProfilerAction extends Task {
public static readonly ID = 'profiler.newProfiler';
public static readonly LABEL = nls.localize('profilerAction.newProfiler', 'Launch Profiler');
public static readonly ICON = 'profile';
private _connectionProfile: ConnectionProfile;
constructor() {
super({
id: NewProfilerAction.ID,
title: NewProfilerAction.LABEL,
iconPath: { dark: NewProfilerAction.ICON, light: NewProfilerAction.ICON },
iconClass: NewProfilerAction.ICON
});
}
public runTask(accessor: ServicesAccessor, profile: IConnectionProfile): Promise<void> {
let profilerInput = accessor.get<IInstantiationService>(IInstantiationService).createInstance(ProfilerInput, profile);
return accessor.get<IEditorService>(IEditorService).openEditor(profilerInput, { pinned: true }, ACTIVE_GROUP).then(() => {
let options: IConnectionCompletionOptions = {
params: undefined,
saveTheConnection: false,
showConnectionDialogOnError: true,
showDashboard: false,
showFirewallRuleOnError: true
};
accessor.get<IConnectionManagementService>(IConnectionManagementService).connect(this._connectionProfile, profilerInput.id, options);
return Promise.resolve(void 0);
});
}
}
export class ProfilerFilterSession extends Action {
public static ID = 'profiler.filter';
public static LABEL = nls.localize('profiler.filter', "Filter…");
constructor(
id: string, label: string,
@IProfilerService private _profilerService: IProfilerService
) {
super(id, label, 'filterLabel');
}
public run(input: ProfilerInput): Promise<boolean> {
this._profilerService.launchFilterSessionDialog(input);
return Promise.resolve(true);
}
}
export class ProfilerClearSessionFilter extends Action {
public static ID = 'profiler.clearFilter';
public static LABEL = nls.localize('profiler.clearFilter', "Clear Filter");
constructor(
id: string, label: string
) {
super(id, label, 'clear-filter');
}
public run(input: ProfilerInput): Promise<boolean> {
input.clearFilter();
return Promise.resolve(true);
}
}

View File

@@ -0,0 +1,410 @@
/*---------------------------------------------------------------------------------------------
* 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/profiler';
import { Modal } from 'sql/workbench/browser/modal/modal';
import { attachModalDialogStyler } from 'sql/platform/theme/common/styler';
import { ProfilerInput } from 'sql/workbench/parts/profiler/browser/profilerInput';
import * as TelemetryKeys from 'sql/platform/telemetry/telemetryKeys';
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import * as nls from 'vs/nls';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import * as DOM from 'vs/base/browser/dom';
import { IDataSource, ITree, IRenderer } from 'vs/base/parts/tree/browser/tree';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { Event, Emitter } from 'vs/base/common/event';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
class EventItem {
constructor(
private _name: string,
private _parent: SessionItem,
private _columns?: Array<ColumnItem>,
) {
if (!_columns) {
this._columns = new Array<ColumnItem>();
}
}
public hasChildren(): boolean {
return this._columns && this._columns.length > 0;
}
public getChildren(): Array<ColumnItem> {
return this._columns;
}
public get id(): string {
return this._name;
}
public addColumn(...columns: Array<ColumnItem>) {
this._columns = this._columns.concat(columns);
}
public get parent(): SessionItem {
return this._parent;
}
public get selected(): boolean {
return this._columns.every(i => i.selected);
}
public set selected(val: boolean) {
this._columns.forEach(i => i.selected = val);
}
public get indeterminate(): boolean {
return this._columns.some(i => i.selected) && !this.selected;
}
}
class ColumnItem {
public selected: boolean;
public readonly indeterminate = false;
constructor(
private _name: string,
private _parent: EventItem
) { }
public get id(): string {
return this._name;
}
public get parent(): EventItem {
return this._parent;
}
}
class ColumnSortedColumnItem {
constructor(
private _name: string,
private _parent: SessionItem
) { }
public get id(): string {
return this._name;
}
public get parent(): SessionItem {
return this._parent;
}
public get selected(): boolean {
return this._parent.getUnsortedChildren()
.every(e => e.getChildren().filter(c => c.id === this.id)
.every(c => c.selected));
}
public set selected(val: boolean) {
this._parent.getUnsortedChildren()
.forEach(e => e.getChildren()
.filter(c => c.id === this.id)
.forEach(c => c.selected = val));
}
public get indeterminate(): boolean {
return this._parent.getUnsortedChildren()
.some(e => e.getChildren()
.filter(c => c.id === this.id)
.some(c => c.selected))
&& !this.selected;
}
}
class SessionItem {
private _sortedColumnItems: Array<ColumnSortedColumnItem> = [];
constructor(
private _name: string,
private _sort: 'event' | 'column',
private _events?: Array<EventItem>
) {
if (!_events) {
this._events = new Array<EventItem>();
} else {
_events.forEach(e => {
e.getChildren().forEach(c => {
if (!this._sortedColumnItems.some(i => i.id === c.id)) {
this._sortedColumnItems.push(new ColumnSortedColumnItem(c.id, this));
}
});
});
}
}
public get id(): string {
return this._name;
}
public hasChildren(): boolean {
if (this._sort === 'event') {
return this._events && this._events.length > 0;
} else {
return this._events && this._events.some(i => i.hasChildren());
}
}
public getUnsortedChildren(): Array<EventItem> {
return this._events;
}
public getChildren(): Array<EventItem | ColumnSortedColumnItem> {
if (this._sort === 'event') {
return this._events;
} else {
return this._sortedColumnItems;
}
}
public addEvents(...events: Array<EventItem>) {
this._events = this._events.concat(events);
events.forEach(e => {
e.getChildren().forEach(c => {
if (!this._sortedColumnItems.some(i => i.id === c.id)) {
this._sortedColumnItems.push(new ColumnSortedColumnItem(c.id, this));
}
});
});
}
public changeSort(type: 'event' | 'column'): void {
this._sort = type;
}
}
class TreeRenderer implements IRenderer {
private _onSelectedChange = new Emitter<any>();
public onSelectedChange: Event<any> = this._onSelectedChange.event;
getHeight(tree: ITree, element: any): number {
return 22;
}
getTemplateId(tree: ITree, element: any): string {
if (element instanceof SessionItem) {
return 'session';
} else if (element instanceof EventItem) {
return 'event';
} else if (element instanceof ColumnItem) {
return 'column';
} else if (element instanceof ColumnSortedColumnItem) {
return 'columnSorted';
} else {
return undefined;
}
}
renderTemplate(tree: ITree, templateId: string, container: HTMLElement): RenderTemplate {
const data = Object.create(null);
const row = document.createElement('div');
row.className = 'tree-row';
DOM.append(container, row);
data.toDispose = [];
data.checkbox = document.createElement('input');
DOM.append(row, data.checkbox);
data.checkbox.type = 'checkbox';
data.toDispose.push(DOM.addStandardDisposableListener(data.checkbox, 'change', () => {
data.context.selected = !data.context.selected;
this._onSelectedChange.fire(data.context);
}));
data.label = document.createElement('div');
DOM.append(row, data.label);
return data;
}
renderElement(tree: ITree, element: any, templateId: string, templateData: RenderTemplate): void {
templateData.context = element;
templateData.label.innerText = element.id;
templateData.checkbox.checked = element.selected;
templateData.checkbox.indeterminate = element.indeterminate;
}
disposeTemplate(tree: ITree, templateId: string, templateData: RenderTemplate): void {
dispose(templateData.toDispose);
}
}
interface RenderTemplate {
label: HTMLElement;
toDispose: Array<IDisposable>;
checkbox: HTMLInputElement;
context?: any;
}
class TreeDataSource implements IDataSource {
getId(tree: ITree, element: any): string {
if (element instanceof EventItem) {
return element.parent.id + element.id;
} else if (element instanceof ColumnItem) {
return element.parent.parent.id + element.parent.id + element.id;
} else if (element instanceof SessionItem) {
return element.id;
} else if (element instanceof ColumnSortedColumnItem) {
return element.id;
} else {
return undefined;
}
}
hasChildren(tree: ITree, element: any): boolean {
if (element instanceof SessionItem) {
return element.hasChildren();
} else if (element instanceof EventItem) {
return element.hasChildren();
} else {
return undefined;
}
}
getChildren(tree: ITree, element: any): Promise<Array<any>> {
if (element instanceof EventItem) {
return Promise.resolve(element.getChildren());
} else if (element instanceof SessionItem) {
return Promise.resolve(element.getChildren());
} else {
return Promise.resolve(null);
}
}
getParent(tree: ITree, element: any): Promise<any> {
if (element instanceof ColumnItem) {
return Promise.resolve(element.parent);
} else if (element instanceof EventItem) {
return Promise.resolve(element.parent);
} else if (element instanceof ColumnSortedColumnItem) {
return Promise.resolve(element.parent);
} else {
return Promise.resolve(null);
}
}
shouldAutoexpand?(tree: ITree, element: any): boolean {
return false;
}
}
export class ProfilerColumnEditorDialog extends Modal {
private _selectBox: SelectBox;
private readonly _options = [
{ text: nls.localize('eventSort', "Sort by event") },
{ text: nls.localize('nameColumn', "Sort by column") }
];
private _tree: Tree;
private _element: SessionItem;
private _treeContainer: HTMLElement;
constructor(
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IThemeService themeService: IThemeService,
@ITelemetryService telemetryService: ITelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IContextViewService private _contextViewService: IContextViewService,
@IClipboardService clipboardService: IClipboardService
) {
super(nls.localize('profilerColumnDialog.profiler', 'Profiler'), TelemetryKeys.Profiler, telemetryService, layoutService, clipboardService, themeService, contextKeyService);
}
public render(): void {
super.render();
this._register(attachModalDialogStyler(this, this._themeService));
this.addFooterButton(nls.localize('profilerColumnDialog.ok', "OK"), () => this.onAccept(undefined));
this.addFooterButton(nls.localize('profilerColumnDialog.cancel', "Cancel"), () => this.onClose(undefined));
}
protected renderBody(container: HTMLElement): void {
const body = DOM.append(container, DOM.$(''));
this._selectBox = new SelectBox(this._options, 0, this._contextViewService);
this._selectBox.render(body);
this._register(this._selectBox.onDidSelect(e => {
this._element.changeSort(e.index === 0 ? 'event' : 'column');
this._tree.refresh(this._element, true);
}));
this._treeContainer = DOM.append(body, DOM.$('.profiler-column-tree'));
const renderer = new TreeRenderer();
this._tree = new Tree(this._treeContainer, { dataSource: new TreeDataSource(), renderer });
this._register(renderer.onSelectedChange(e => this._tree.refresh(e, true)));
this._register(attachListStyler(this._tree, this._themeService));
}
public open(input: ProfilerInput): void {
super.show();
this._updateList();
}
protected onAccept(e: StandardKeyboardEvent): void {
this._updateInput();
super.onAccept(e);
}
// currently not used, this dialog is a work in progress
// tracked in issue #1545 https://github.com/Microsoft/azuredatastudio/issues/1545
private _updateInput(): void {
/*
this._element.getUnsortedChildren().forEach(e => {
let origEvent = this._input.sessionTemplate.view.events.find(i => i.name === e.id);
if (e.indeterminate) {
e.getChildren().forEach(c => {
if (origEvent.columns.includes(c.id) && !c.selected) {
origEvent.columns = origEvent.columns.filter(i => i !== c.id);
} else if (!origEvent.columns.includes(c.id) && c.selected) {
origEvent.columns.push(c.id);
}
});
} else {
origEvent.columns = e.getChildren()
.filter(c => c.selected)
.map(c => c.id);
}
});
let newColumns = this._input.sessionTemplate.view.events.reduce<Array<string>>((p, e) => {
e.columns.forEach(c => {
if (!p.includes(c)) {
p.push(c);
}
});
return p;
}, []);
newColumns.unshift('EventClass');
this._input.setColumns(newColumns);
*/
}
// currently not used, this dialog is a work in progress
// tracked in issue #1545 https://github.com/Microsoft/azuredatastudio/issues/1545
private _updateList(): void {
/*
this._element = new SessionItem(this._input.sessionTemplate.name, this._selectedValue === 0 ? 'event' : 'column');
this._input.sessionTemplate.events.forEach(item => {
let event = new EventItem(item.name, this._element);
item.optionalColumns.forEach(col => {
let column = new ColumnItem(col, event);
column.selected = this._input.sessionTemplate.view.events.find(i => i.name === event.id).columns.includes(col);
event.addColumn(column);
});
this._element.addEvents(event);
});
this._tree.setInput(this._element);
this._tree.layout(DOM.getTotalHeight(this._treeContainer));
*/
}
protected layout(height?: number): void {
this._tree.layout(DOM.getContentHeight(this._treeContainer));
}
}

View File

@@ -0,0 +1,616 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ProfilerInput } from 'sql/workbench/parts/profiler/browser/profilerInput';
import { TabbedPanel } from 'sql/base/browser/ui/panel/panel';
import { Table } from 'sql/base/browser/ui/table/table';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { IProfilerService, IProfilerViewTemplate } from 'sql/workbench/services/profiler/common/interfaces';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { IProfilerStateChangedEvent } from 'sql/workbench/parts/profiler/common/profilerState';
import { ProfilerTableEditor, ProfilerTableViewState } from 'sql/workbench/parts/profiler/browser/profilerTableEditor';
import * as Actions from 'sql/workbench/parts/profiler/browser/profilerActions';
import { CONTEXT_PROFILER_EDITOR, PROFILER_TABLE_COMMAND_SEARCH } from 'sql/workbench/parts/profiler/common/interfaces';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { textFormatter, slickGridDataItemColumnValueExtractor } from 'sql/base/browser/ui/table/formatters';
import { ProfilerResourceEditor } from 'sql/workbench/parts/profiler/browser/profilerResourceEditor';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { ITextModel } from 'vs/editor/common/model';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import * as nls from 'vs/nls';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Command } from 'vs/editor/browser/editorExtensions';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { ContextKeyExpr, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { CommonFindController, FindStartFocusAction } from 'vs/editor/contrib/find/findController';
import * as types from 'vs/base/common/types';
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
import { DARK, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IView, SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview';
import * as DOM from 'vs/base/browser/dom';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorOptions } from 'vs/workbench/common/editor';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkbenchThemeService, VS_DARK_THEME, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import { clamp } from 'vs/base/common/numbers';
class BasicView implements IView {
public get element(): HTMLElement {
return this._element;
}
private _onDidChange = new Emitter<number>();
public readonly onDidChange: Event<number> = this._onDidChange.event;
private _collapsed = false;
private size: number;
private previousSize: number;
private _minimumSize: number;
public get minimumSize(): number {
return this._minimumSize;
}
private _maximumSize: number;
public get maximumSize(): number {
return this._maximumSize;
}
constructor(
private _defaultMinimumSize: number,
private _defaultMaximumSize: number,
private _layout: (size: number) => void,
private _element: HTMLElement,
private options: { headersize?: number } = {}
) {
this._minimumSize = _defaultMinimumSize;
this._maximumSize = _defaultMaximumSize;
}
public layout(size: number): void {
this.size = size;
this._layout(size);
}
public set collapsed(val: boolean) {
if (val !== this._collapsed && this.options.headersize) {
this._collapsed = val;
if (this.collapsed) {
this.previousSize = this.size;
this._minimumSize = this.options.headersize;
this._maximumSize = this.options.headersize;
this._onDidChange.fire(undefined);
} else {
this._maximumSize = this._defaultMaximumSize;
this._minimumSize = this._defaultMinimumSize;
this._onDidChange.fire(clamp(this.previousSize, this.minimumSize, this.maximumSize));
}
}
}
public get collapsed(): boolean {
return this._collapsed;
}
}
export interface IDetailData {
label: string;
value: string;
}
export class ProfilerEditor extends BaseEditor {
public static readonly ID: string = 'workbench.editor.profiler';
private _editor: ProfilerResourceEditor;
private _editorModel: ITextModel;
private _editorInput: UntitledEditorInput;
private _splitView: SplitView;
private _container: HTMLElement;
private _body: HTMLElement;
private _header: HTMLElement;
private _actionBar: Taskbar;
private _tabbedPanel: TabbedPanel;
private _profilerTableEditor: ProfilerTableEditor;
private _detailTable: Table<IDetailData>;
private _detailTableData: TableDataView<IDetailData>;
private _stateListener: IDisposable;
private _panelView: BasicView;
private _profilerEditorContextKey: IContextKey<boolean>;
private _viewTemplateSelector: SelectBox;
private _viewTemplates: Array<IProfilerViewTemplate>;
private _sessionSelector: SelectBox;
private _sessionsList: Array<string>;
// Actions
private _connectAction: Actions.ProfilerConnect;
private _startAction: Actions.ProfilerStart;
private _pauseAction: Actions.ProfilerPause;
private _stopAction: Actions.ProfilerStop;
private _autoscrollAction: Actions.ProfilerAutoScroll;
private _createAction: Actions.ProfilerCreate;
private _collapsedPanelAction: Actions.ProfilerCollapsablePanelAction;
private _filterAction: Actions.ProfilerFilterSession;
private _clearFilterAction: Actions.ProfilerClearSessionFilter;
private _savedTableViewStates = new Map<ProfilerInput, ProfilerTableViewState>();
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IWorkbenchThemeService themeService: IWorkbenchThemeService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IModelService private _modelService: IModelService,
@IProfilerService private _profilerService: IProfilerService,
@IContextKeyService private _contextKeyService: IContextKeyService,
@IContextViewService private _contextViewService: IContextViewService,
@IEditorService editorService: IEditorService,
@IStorageService storageService: IStorageService
) {
super(ProfilerEditor.ID, telemetryService, themeService, storageService);
this._profilerEditorContextKey = CONTEXT_PROFILER_EDITOR.bindTo(this._contextKeyService);
if (editorService) {
editorService.overrideOpenEditor((editor, options, group) => {
if (this.isVisible() && (editor !== this.input || group !== this.group)) {
this.saveEditorViewState();
}
return {};
});
}
}
protected createEditor(parent: HTMLElement): void {
this._container = document.createElement('div');
this._container.className = 'carbon-profiler';
parent.appendChild(this._container);
this._createHeader();
this._body = document.createElement('div');
this._body.className = 'profiler-body';
this._container.appendChild(this._body);
this._splitView = new SplitView(this._body);
let tableContainer = this._createProfilerTable();
let paneContainer = this._createProfilerPane();
this._splitView.addView(new BasicView(
300,
Number.POSITIVE_INFINITY,
size => this._profilerTableEditor.layout(new DOM.Dimension(parseFloat(DOM.getComputedStyle(this._body).width), size)),
tableContainer
), Sizing.Distribute);
this._panelView = new BasicView(
300,
Number.POSITIVE_INFINITY,
size => this._tabbedPanel.layout(new DOM.Dimension(DOM.getTotalWidth(this._body), size)),
paneContainer,
{ headersize: 35 }
);
this._splitView.addView(this._panelView, Sizing.Distribute);
}
private _createHeader(): void {
this._header = document.createElement('div');
this._header.className = 'profiler-header';
this._container.appendChild(this._header);
this._actionBar = new Taskbar(this._header);
this._startAction = this._instantiationService.createInstance(Actions.ProfilerStart, Actions.ProfilerStart.ID, Actions.ProfilerStart.LABEL);
this._startAction.enabled = false;
this._createAction = this._instantiationService.createInstance(Actions.ProfilerCreate, Actions.ProfilerCreate.ID, Actions.ProfilerCreate.LABEL);
this._createAction.enabled = true;
this._stopAction = this._instantiationService.createInstance(Actions.ProfilerStop, Actions.ProfilerStop.ID, Actions.ProfilerStop.LABEL);
this._stopAction.enabled = false;
this._pauseAction = this._instantiationService.createInstance(Actions.ProfilerPause, Actions.ProfilerPause.ID, Actions.ProfilerPause.LABEL);
this._pauseAction.enabled = false;
this._connectAction = this._instantiationService.createInstance(Actions.ProfilerConnect, Actions.ProfilerConnect.ID, Actions.ProfilerConnect.LABEL);
this._autoscrollAction = this._instantiationService.createInstance(Actions.ProfilerAutoScroll, Actions.ProfilerAutoScroll.ID, Actions.ProfilerAutoScroll.LABEL);
this._filterAction = this._instantiationService.createInstance(Actions.ProfilerFilterSession, Actions.ProfilerFilterSession.ID, Actions.ProfilerFilterSession.LABEL);
this._filterAction.enabled = true;
this._clearFilterAction = this._instantiationService.createInstance(Actions.ProfilerClearSessionFilter, Actions.ProfilerClearSessionFilter.ID, Actions.ProfilerClearSessionFilter.LABEL);
this._clearFilterAction.enabled = true;
this._viewTemplates = this._profilerService.getViewTemplates();
this._viewTemplateSelector = new SelectBox(this._viewTemplates.map(i => i.name), 'Standard View', this._contextViewService);
this._viewTemplateSelector.setAriaLabel(nls.localize('profiler.viewSelectAccessibleName', 'Select View'));
this._register(this._viewTemplateSelector.onDidSelect(e => {
if (this.input) {
this.input.viewTemplate = this._viewTemplates.find(i => i.name === e.selected);
}
}));
let viewTemplateContainer = document.createElement('div');
viewTemplateContainer.style.width = '150px';
viewTemplateContainer.style.paddingRight = '5px';
this._viewTemplateSelector.render(viewTemplateContainer);
this._sessionsList = [''];
this._sessionSelector = new SelectBox(this._sessionsList, '', this._contextViewService);
this._sessionSelector.setAriaLabel(nls.localize('profiler.sessionSelectAccessibleName', 'Select Session'));
this._register(this._sessionSelector.onDidSelect(e => {
if (this.input) {
this.input.sessionName = e.selected;
}
}));
let sessionsContainer = document.createElement('div');
sessionsContainer.style.minWidth = '150px';
sessionsContainer.style.maxWidth = '250px';
sessionsContainer.style.paddingRight = '5px';
this._sessionSelector.render(sessionsContainer);
this._register(attachSelectBoxStyler(this._viewTemplateSelector, this.themeService));
this._register(attachSelectBoxStyler(this._sessionSelector, this.themeService));
this._actionBar.setContent([
{ action: this._createAction },
{ element: Taskbar.createTaskbarSeparator() },
{ element: this._createTextElement(nls.localize('profiler.sessionSelectLabel', 'Select Session:')) },
{ element: sessionsContainer },
{ action: this._startAction },
{ action: this._stopAction },
{ action: this._pauseAction },
{ element: Taskbar.createTaskbarSeparator() },
{ action: this._filterAction },
{ action: this._clearFilterAction },
{ element: Taskbar.createTaskbarSeparator() },
{ element: this._createTextElement(nls.localize('profiler.viewSelectLabel', 'Select View:')) },
{ element: viewTemplateContainer },
{ action: this._autoscrollAction },
{ action: this._instantiationService.createInstance(Actions.ProfilerClear, Actions.ProfilerClear.ID, Actions.ProfilerClear.LABEL) }
]);
}
private _createTextElement(text: string): HTMLDivElement {
let textElement = document.createElement('div');
textElement.style.paddingRight = '10px';
textElement.innerText = text;
textElement.style.textAlign = 'center';
textElement.style.display = 'flex';
textElement.style.alignItems = 'center';
return textElement;
}
private _createProfilerTable(): HTMLElement {
let profilerTableContainer = document.createElement('div');
profilerTableContainer.className = 'profiler-table monaco-editor';
profilerTableContainer.style.width = '100%';
profilerTableContainer.style.height = '100%';
profilerTableContainer.style.overflow = 'hidden';
profilerTableContainer.style.position = 'relative';
let theme = this.themeService.getTheme();
if (theme.type === DARK) {
DOM.addClass(profilerTableContainer, VS_DARK_THEME);
} else if (theme.type === HIGH_CONTRAST) {
DOM.addClass(profilerTableContainer, VS_HC_THEME);
}
this.themeService.onThemeChange(e => {
DOM.removeClasses(profilerTableContainer, VS_DARK_THEME, VS_HC_THEME);
if (e.type === DARK) {
DOM.addClass(profilerTableContainer, VS_DARK_THEME);
} else if (e.type === HIGH_CONTRAST) {
DOM.addClass(profilerTableContainer, VS_HC_THEME);
}
});
this._profilerTableEditor = this._instantiationService.createInstance(ProfilerTableEditor);
this._profilerTableEditor.createEditor(profilerTableContainer);
this._profilerTableEditor.onSelectedRowsChanged((e, args) => {
let data = this.input.data.getItem(args.rows[0]);
if (data) {
this._modelService.updateModel(this._editorModel, data['TextData']);
this._detailTableData.clear();
this._detailTableData.push(Object.keys(data).filter(key => {
return data[key] !== ' ';
}).map(key => {
return {
label: key,
value: data[key]
};
}));
if (this.input && types.isUndefinedOrNull(this.input.state.isPanelCollapsed)) {
this.input.state.change({ isPanelCollapsed: false });
}
} else {
this._modelService.updateModel(this._editorModel, '');
this._detailTableData.clear();
}
});
return profilerTableContainer;
}
private _createProfilerPane(): HTMLElement {
let editorContainer = this._createProfilerEditor();
let tabbedPanelContainer = document.createElement('div');
tabbedPanelContainer.className = 'profiler-tabbedPane';
this._tabbedPanel = new TabbedPanel(tabbedPanelContainer);
this._tabbedPanel.pushTab({
identifier: 'editor',
title: nls.localize('text', "Text"),
view: {
layout: dim => this._editor.layout(dim),
render: parent => parent.appendChild(editorContainer)
}
});
let detailTableContainer = document.createElement('div');
detailTableContainer.className = 'profiler-detailTable';
detailTableContainer.style.width = '100%';
detailTableContainer.style.height = '100%';
this._detailTableData = new TableDataView<IDetailData>();
this._detailTable = new Table(detailTableContainer, {
dataProvider: this._detailTableData, columns: [
{
id: 'label',
name: nls.localize('label', "Label"),
field: 'label',
formatter: textFormatter
},
{
id: 'value',
name: nls.localize('profilerEditor.value', "Value"),
field: 'value',
formatter: textFormatter
}
]
}, {
forceFitColumns: true,
dataItemColumnValueExtractor: slickGridDataItemColumnValueExtractor
});
this._detailTableData.onRowCountChange(() => {
this._detailTable.updateRowCount();
});
this._tabbedPanel.pushTab({
identifier: 'detailTable',
title: nls.localize('details', "Details"),
view: {
layout: dim => this._detailTable.layout(dim),
render: parent => parent.appendChild(detailTableContainer)
}
});
this._collapsedPanelAction = this._instantiationService.createInstance(Actions.ProfilerCollapsablePanelAction, Actions.ProfilerCollapsablePanelAction.ID, Actions.ProfilerCollapsablePanelAction.LABEL);
this._tabbedPanel.pushAction(this._collapsedPanelAction, { icon: true, label: false });
this._register(attachTableStyler(this._detailTable, this.themeService));
return tabbedPanelContainer;
}
private _createProfilerEditor(): HTMLElement {
this._editor = this._instantiationService.createInstance(ProfilerResourceEditor);
let editorContainer = document.createElement('div');
editorContainer.className = 'profiler-editor';
this._editor.create(editorContainer);
this._editor.setVisible(true);
this._editorInput = this._instantiationService.createInstance(UntitledEditorInput, URI.from({ scheme: Schemas.untitled }), false, 'sql', '', '');
this._editor.setInput(this._editorInput, undefined);
this._editorInput.resolve().then(model => this._editorModel = model.textEditorModel);
return editorContainer;
}
public get input(): ProfilerInput {
return this._input as ProfilerInput;
}
public setInput(input: ProfilerInput, options?: EditorOptions): Promise<void> {
let savedViewState = this._savedTableViewStates.get(input);
this._profilerEditorContextKey.set(true);
if (input instanceof ProfilerInput && input.matches(this.input)) {
if (savedViewState) {
this._profilerTableEditor.restoreViewState(savedViewState);
}
return Promise.resolve(null);
}
return super.setInput(input, options, CancellationToken.None).then(() => {
this._profilerTableEditor.setInput(input);
if (input.viewTemplate) {
this._viewTemplateSelector.selectWithOptionName(input.viewTemplate.name);
} else {
input.viewTemplate = this._viewTemplates.find(i => i.name === 'Standard View');
}
this._actionBar.context = input;
this._tabbedPanel.actionBarContext = input;
if (this._stateListener) {
this._stateListener.dispose();
}
this._stateListener = input.state.addChangeListener(e => this._onStateChange(e));
this._onStateChange({
isConnected: true,
isRunning: true,
isPaused: true,
isStopped: true,
autoscroll: true,
isPanelCollapsed: true
});
this._profilerTableEditor.updateState();
this._profilerTableEditor.focus();
if (savedViewState) {
this._profilerTableEditor.restoreViewState(savedViewState);
}
});
}
public clearInput(): void {
this._profilerEditorContextKey.set(false);
}
public toggleSearch(): void {
if (this._editor.getControl().hasTextFocus()) {
let editor = this._editor.getControl() as ICodeEditor;
let controller = CommonFindController.get(editor);
if (controller) {
controller.start({
forceRevealReplace: false,
seedSearchStringFromGlobalClipboard: false,
seedSearchStringFromSelection: (controller.getState().searchString.length === 0),
shouldFocus: FindStartFocusAction.FocusFindInput,
shouldAnimate: true,
updateSearchScope: false
});
}
} else {
this._profilerTableEditor.toggleSearch();
}
}
private _onStateChange(e: IProfilerStateChangedEvent): void {
if (e.autoscroll) {
this._autoscrollAction.checked = this.input.state.autoscroll;
}
if (e.isPanelCollapsed) {
this._collapsedPanelAction.collapsed = this.input.state.isPanelCollapsed;
this._tabbedPanel.collapsed = this.input.state.isPanelCollapsed;
this._panelView.collapsed = this.input.state.isPanelCollapsed;
}
if (e.isConnected) {
this._connectAction.connected = this.input.state.isConnected;
if (this.input.state.isConnected) {
this._updateToolbar();
// Launch the create session dialog if openning a new window.
let uiState = this._profilerService.getSessionViewState(this.input.id);
let previousSessionName = uiState && uiState.previousSessionName;
if (!this.input.sessionName && !previousSessionName) {
this._profilerService.launchCreateSessionDialog(this.input);
}
this._updateSessionSelector(previousSessionName);
} else {
this._startAction.enabled = false;
this._stopAction.enabled = false;
this._pauseAction.enabled = false;
this._sessionSelector.setOptions([]);
this._sessionSelector.disable();
return;
}
}
if (e.isPaused) {
this._pauseAction.paused = this.input.state.isPaused;
this._updateToolbar();
}
if (e.isStopped || e.isRunning) {
if (this.input.state.isRunning) {
this._updateToolbar();
this._sessionSelector.setOptions([this.input.sessionName]);
this._sessionSelector.selectWithOptionName(this.input.sessionName);
this._sessionSelector.disable();
this._viewTemplateSelector.selectWithOptionName(this.input.viewTemplate.name);
}
if (this.input.state.isStopped) {
this._updateToolbar();
this._updateSessionSelector();
}
}
}
private _updateSessionSelector(previousSessionName: string = undefined) {
this._sessionSelector.enable();
this._profilerService.getXEventSessions(this.input.id).then((r) => {
if (!r) {
r = [];
}
this._sessionSelector.setOptions(r);
this._sessionsList = r;
if (this._sessionsList.length > 0) {
if (!this.input.sessionName) {
this.input.sessionName = previousSessionName;
}
if (this._sessionsList.indexOf(this.input.sessionName) === -1) {
this.input.sessionName = this._sessionsList[0];
}
this._sessionSelector.selectWithOptionName(this.input.sessionName);
}
});
}
private _updateToolbar(): void {
this._startAction.enabled = !this.input.state.isRunning && !this.input.state.isPaused && this.input.state.isConnected;
this._createAction.enabled = !this.input.state.isRunning && !this.input.state.isPaused && this.input.state.isConnected;
this._stopAction.enabled = !this.input.state.isStopped && (this.input.state.isRunning || this.input.state.isPaused) && this.input.state.isConnected;
this._pauseAction.enabled = !this.input.state.isStopped && (this.input.state.isRunning || this.input.state.isPaused && this.input.state.isConnected);
}
public layout(dimension: DOM.Dimension): void {
this._container.style.width = dimension.width + 'px';
this._container.style.height = dimension.height + 'px';
this._body.style.width = dimension.width + 'px';
this._body.style.height = (dimension.height - (28 + 4)) + 'px';
this._splitView.layout(dimension.height - (28 + 4));
}
private saveEditorViewState(): void {
if (this.input && this._profilerTableEditor) {
this._savedTableViewStates.set(this.input, this._profilerTableEditor.saveViewState());
}
}
public focus() {
this._profilerEditorContextKey.set(true);
super.focus();
let savedViewState = this._savedTableViewStates.get(this.input);
if (savedViewState) {
this._profilerTableEditor.restoreViewState(savedViewState);
}
}
}
abstract class SettingsCommand extends Command {
protected getProfilerEditor(accessor: ServicesAccessor): ProfilerEditor {
const activeEditor = accessor.get(IEditorService).activeControl;
if (activeEditor instanceof ProfilerEditor) {
return activeEditor;
}
return null;
}
}
class StartSearchProfilerTableCommand extends SettingsCommand {
public runCommand(accessor: ServicesAccessor, args: any): void {
const preferencesEditor = this.getProfilerEditor(accessor);
if (preferencesEditor) {
preferencesEditor.toggleSearch();
}
}
}
const command = new StartSearchProfilerTableCommand({
id: PROFILER_TABLE_COMMAND_SEARCH,
precondition: ContextKeyExpr.and(CONTEXT_PROFILER_EDITOR),
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.KEY_F,
weight: KeybindingWeight.EditorContrib
}
});
command.register();

View File

@@ -0,0 +1,307 @@
/*---------------------------------------------------------------------------------------------
* 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/profilerFilterDialog';
import { Button } from 'sql/base/browser/ui/button/button';
import { Modal } from 'sql/workbench/browser/modal/modal';
import * as TelemetryKeys from 'sql/platform/telemetry/telemetryKeys';
import { attachButtonStyler, attachModalDialogStyler, attachInputBoxStyler } from 'sql/platform/theme/common/styler';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { localize } from 'vs/nls';
import { ProfilerInput } from 'sql/workbench/parts/profiler/browser/profilerInput';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { generateUuid } from 'vs/base/common/uuid';
import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ProfilerFilter, ProfilerFilterClause, ProfilerFilterClauseOperator } from 'sql/workbench/services/profiler/common/interfaces';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
const ClearText: string = localize('profilerFilterDialog.clear', "Clear All");
const ApplyText: string = localize('profilerFilterDialog.apply', "Apply");
const OkText: string = localize('profilerFilterDialog.ok', "OK");
const CancelText: string = localize('profilerFilterDialog.cancel', "Cancel");
const DialogTitle: string = localize('profilerFilterDialog.title', "Filters");
const RemoveText: string = localize('profilerFilterDialog.remove', "Remove");
const AddText: string = localize('profilerFilterDialog.add', "Add");
const AddClausePromptText: string = localize('profilerFilterDialog.addClauseText', "Click here to add a clause");
const TitleIconClass: string = 'icon filterLabel';
const FieldText: string = localize('profilerFilterDialog.fieldColumn', "Field");
const OperatorText: string = localize('profilerFilterDialog.operatorColumn', "Operator");
const ValueText: string = localize('profilerFilterDialog.valueColumn', "Value");
const Equals: string = '=';
const NotEquals: string = '<>';
const LessThan: string = '<';
const LessThanOrEquals: string = '<=';
const GreaterThan: string = '>';
const GreaterThanOrEquals: string = '>=';
const IsNull: string = localize('profilerFilterDialog.isNullOperator', "Is Null");
const IsNotNull: string = localize('profilerFilterDialog.isNotNullOperator', "Is Not Null");
const Contains: string = localize('profilerFilterDialog.containsOperator', "Contains");
const NotContains: string = localize('profilerFilterDialog.notContainsOperator', "Not Contains");
const StartsWith: string = localize('profilerFilterDialog.startsWithOperator', "Starts With");
const NotStartsWith: string = localize('profilerFilterDialog.notStartsWithOperator', "Not Starts With");
const Operators = [Equals, NotEquals, LessThan, LessThanOrEquals, GreaterThan, GreaterThanOrEquals, GreaterThan, GreaterThanOrEquals, IsNull, IsNotNull, Contains, NotContains, StartsWith, NotStartsWith];
export class ProfilerFilterDialog extends Modal {
private _clauseBuilder: HTMLElement;
private _okButton: Button;
private _cancelButton: Button;
private _clearButton: Button;
private _applyButton: Button;
private _addClauseButton: Button;
private _input: ProfilerInput;
private _clauseRows: ClauseRowUI[] = [];
constructor(
@IThemeService themeService: IThemeService,
@IClipboardService clipboardService: IClipboardService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@ITelemetryService telemetryService: ITelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IContextViewService private contextViewService: IContextViewService
) {
super('', TelemetryKeys.ProfilerFilter, telemetryService, layoutService, clipboardService, themeService, contextKeyService, { isFlyout: false, hasTitleIcon: true });
}
public open(input: ProfilerInput) {
this._input = input;
this.render();
this.show();
this._okButton.focus();
}
public dispose(): void {
}
public render() {
super.render();
this.title = DialogTitle;
this.titleIconClassName = TitleIconClass;
this._register(attachModalDialogStyler(this, this._themeService));
this._addClauseButton = this.addFooterButton(AddText, () => this.addClauseRow(false), 'left');
this._clearButton = this.addFooterButton(ClearText, () => this.handleClearButtonClick(), 'left');
this._applyButton = this.addFooterButton(ApplyText, () => this.filterSession());
this._okButton = this.addFooterButton(OkText, () => this.handleOkButtonClick());
this._cancelButton = this.addFooterButton(CancelText, () => this.hide());
this._register(attachButtonStyler(this._okButton, this._themeService));
this._register(attachButtonStyler(this._cancelButton, this._themeService));
this._register(attachButtonStyler(this._clearButton, this._themeService));
this._register(attachButtonStyler(this._applyButton, this._themeService));
this._register(attachButtonStyler(this._addClauseButton, this._themeService));
}
protected renderBody(container: HTMLElement) {
const body = DOM.append(container, DOM.$('.profiler-filter-dialog'));
this._clauseBuilder = DOM.append(body, DOM.$('table.profiler-filter-clause-table'));
const headerRow = DOM.append(this._clauseBuilder, DOM.$('tr'));
DOM.append(headerRow, DOM.$('td')).innerText = FieldText;
DOM.append(headerRow, DOM.$('td')).innerText = OperatorText;
DOM.append(headerRow, DOM.$('td')).innerText = ValueText;
DOM.append(headerRow, DOM.$('td')).innerText = '';
this._input.filter.clauses.forEach(clause => {
this.addClauseRow(true, clause.field, this.convertToOperatorString(clause.operator), clause.value);
});
const prompt = DOM.append(body, DOM.$('.profiler-filter-add-clause-prompt', { tabIndex: '0' }));
prompt.innerText = AddClausePromptText;
DOM.addDisposableListener(prompt, DOM.EventType.CLICK, () => this.addClauseRow(false));
DOM.addStandardDisposableListener(prompt, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => {
if (e.equals(KeyCode.Space) || e.equals(KeyCode.Enter)) {
this.addClauseRow(false);
e.stopPropagation();
}
});
}
protected layout(height?: number): void {
// Nothing to re-layout
}
/* espace key */
protected onClose() {
this.hide();
}
/* enter key */
protected onAccept() {
this.handleOkButtonClick();
}
private handleOkButtonClick(): void {
this.filterSession();
this.hide();
}
private handleClearButtonClick() {
this._clauseRows.forEach(clause => {
clause.row.remove();
});
this._clauseRows = [];
}
private createSelectBox(container: HTMLElement, options: string[], selectedOption: string, ariaLabel: string): SelectBox {
const dropdown = new SelectBox(options, selectedOption, this.contextViewService, undefined, { ariaLabel: ariaLabel });
dropdown.render(container);
this._register(attachSelectBoxStyler(dropdown, this._themeService));
return dropdown;
}
private filterSession() {
this._input.filterSession(this.getFilter());
}
private getFilter(): ProfilerFilter {
const clauses: ProfilerFilterClause[] = [];
this._clauseRows.forEach(row => {
clauses.push({
field: row.field.value,
operator: this.convertToOperatorEnum(row.operator.value),
value: row.value.value
});
});
return {
clauses: clauses
};
}
private addClauseRow(setInitialValue: boolean, field?: string, operator?: string, value?: string): any {
const row = DOM.append(this._clauseBuilder, DOM.$('tr'));
const clauseId = generateUuid();
const columns = this._input.columns.map(column => column.name);
const fieldDropDown = this.createSelectBox(DOM.append(row, DOM.$('td')), columns, columns[0], FieldText);
const operatorDropDown = this.createSelectBox(DOM.append(row, DOM.$('td')), Operators, Operators[0], OperatorText);
const valueText = new InputBox(DOM.append(row, DOM.$('td')), undefined, {});
this._register(attachInputBoxStyler(valueText, this._themeService));
const removeCell = DOM.append(row, DOM.$('td'));
const removeClauseButton = DOM.append(removeCell, DOM.$('.profiler-filter-remove-condition.icon.remove', {
'tabIndex': '0',
'aria-label': RemoveText,
'title': RemoveText
}));
DOM.addStandardDisposableListener(removeClauseButton, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => {
if (e.equals(KeyCode.Space) || e.equals(KeyCode.Enter)) {
this.removeRow(clauseId);
e.stopPropagation();
}
});
DOM.addDisposableListener(removeClauseButton, DOM.EventType.CLICK, (e: MouseEvent) => {
this.removeRow(clauseId);
});
if (setInitialValue) {
fieldDropDown.selectWithOptionName(field);
operatorDropDown.selectWithOptionName(operator);
valueText.value = value;
}
this._clauseRows.push({
id: clauseId,
row,
field: fieldDropDown,
operator: operatorDropDown,
value: valueText
});
}
private removeRow(clauseId: string) {
const idx = this._clauseRows.findIndex((entry) => { return entry.id === clauseId; });
if (idx !== -1) {
this._clauseRows[idx].row.remove();
this._clauseRows.splice(idx, 1);
}
}
private convertToOperatorEnum(operator: string): ProfilerFilterClauseOperator {
switch (operator) {
case Equals:
return ProfilerFilterClauseOperator.Equals;
case NotEquals:
return ProfilerFilterClauseOperator.NotEquals;
case LessThan:
return ProfilerFilterClauseOperator.LessThan;
case LessThanOrEquals:
return ProfilerFilterClauseOperator.LessThanOrEquals;
case GreaterThan:
return ProfilerFilterClauseOperator.GreaterThan;
case GreaterThanOrEquals:
return ProfilerFilterClauseOperator.GreaterThanOrEquals;
case IsNull:
return ProfilerFilterClauseOperator.IsNull;
case IsNotNull:
return ProfilerFilterClauseOperator.IsNotNull;
case Contains:
return ProfilerFilterClauseOperator.Contains;
case NotContains:
return ProfilerFilterClauseOperator.NotContains;
case StartsWith:
return ProfilerFilterClauseOperator.StartsWith;
case NotStartsWith:
return ProfilerFilterClauseOperator.NotStartsWith;
default:
throw new Error(`Not a valid operator: ${operator}`);
}
}
private convertToOperatorString(operator: ProfilerFilterClauseOperator): string {
switch (operator) {
case ProfilerFilterClauseOperator.Equals:
return Equals;
case ProfilerFilterClauseOperator.NotEquals:
return NotEquals;
case ProfilerFilterClauseOperator.LessThan:
return LessThan;
case ProfilerFilterClauseOperator.LessThanOrEquals:
return LessThanOrEquals;
case ProfilerFilterClauseOperator.GreaterThan:
return GreaterThan;
case ProfilerFilterClauseOperator.GreaterThanOrEquals:
return GreaterThanOrEquals;
case ProfilerFilterClauseOperator.IsNull:
return IsNull;
case ProfilerFilterClauseOperator.IsNotNull:
return IsNotNull;
case ProfilerFilterClauseOperator.Contains:
return Contains;
case ProfilerFilterClauseOperator.NotContains:
return NotContains;
case ProfilerFilterClauseOperator.StartsWith:
return StartsWith;
case ProfilerFilterClauseOperator.NotStartsWith:
return NotStartsWith;
default:
throw new Error(`Not a valid operator: ${operator}`);
}
}
}
interface ClauseRowUI {
id: string;
row: HTMLElement;
field: SelectBox;
operator: SelectBox;
value: InputBox;
}

View File

@@ -0,0 +1,651 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { onUnexpectedError } from 'vs/base/common/errors';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import * as platform from 'vs/base/common/platform';
import * as strings from 'vs/base/common/strings';
import * as dom from 'vs/base/browser/dom';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput';
import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox';
import { Widget } from 'vs/base/browser/ui/widget';
import { Sash, IHorizontalSashLayoutProvider, ISashEvent, Orientation } from 'vs/base/browser/ui/sash/sash';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
import { FIND_IDS, CONTEXT_FIND_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel';
import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/findState';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import { IEditorAction } from 'vs/editor/common/editorCommon';
import { IDisposable } from 'vs/base/common/lifecycle';
const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find");
const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find");
const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous match");
const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next match");
const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close");
const NLS_MATCHES_COUNT_LIMIT_TITLE = nls.localize('title.matchesCountLimit', "Your search returned a large number of results, only the first 999 matches will be highlighted.");
const NLS_MATCHES_LOCATION = nls.localize('label.matchesLocation', "{0} of {1}");
const NLS_NO_RESULTS = nls.localize('label.noResults', "No Results");
const FIND_WIDGET_INITIAL_WIDTH = 411;
const PART_WIDTH = 275;
const FIND_INPUT_AREA_WIDTH = PART_WIDTH - 54;
let MAX_MATCHES_COUNT_WIDTH = 69;
export const PROFILER_MAX_MATCHES = 999;
export const ACTION_IDS = {
FIND_NEXT: 'findNext',
FIND_PREVIOUS: 'findPrev'
};
export interface ITableController {
focus(): void;
getConfiguration(): any;
layoutOverlayWidget(widget: IOverlayWidget): void;
addOverlayWidget(widget: IOverlayWidget): void;
getAction(id: string): IEditorAction;
onDidChangeConfiguration(fn: (e: IConfigurationChangedEvent) => void): IDisposable;
}
export interface IConfigurationChangedEvent {
layoutInfo?: boolean;
}
export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSashLayoutProvider {
private static ID = 'editor.contrib.findWidget';
private _tableController: ITableController;
private _state: FindReplaceState;
private _contextViewProvider: IContextViewProvider;
private _keybindingService: IKeybindingService;
private _domNode: HTMLElement;
private _findInput: FindInput;
private _matchesCount: HTMLElement;
private _prevBtn: SimpleButton;
private _nextBtn: SimpleButton;
private _closeBtn: SimpleButton;
private _isVisible: boolean;
private _focusTracker: dom.IFocusTracker;
private _findInputFocussed: IContextKey<boolean>;
private _resizeSash: Sash;
private searchTimeoutHandle: NodeJS.Timer;
constructor(
tableController: ITableController,
state: FindReplaceState,
contextViewProvider: IContextViewProvider,
keybindingService: IKeybindingService,
contextKeyService: IContextKeyService,
themeService: IThemeService
) {
super();
this._tableController = tableController;
this._state = state;
this._contextViewProvider = contextViewProvider;
this._keybindingService = keybindingService;
this._isVisible = false;
this._register(this._state.onFindReplaceStateChange((e) => this._onStateChanged(e)));
this._buildDomNode();
this._updateButtons();
let checkEditorWidth = () => {
let editorWidth = this._tableController.getConfiguration().layoutInfo.width;
let collapsedFindWidget = false;
let reducedFindWidget = false;
let narrowFindWidget = false;
let widgetWidth = dom.getTotalWidth(this._domNode);
if (widgetWidth > FIND_WIDGET_INITIAL_WIDTH) {
// as the widget is resized by users, we may need to change the max width of the widget as the editor width changes.
this._domNode.style.maxWidth = `${editorWidth - 28 - 15}px`;
return;
}
if (FIND_WIDGET_INITIAL_WIDTH + 28 >= editorWidth) {
reducedFindWidget = true;
}
if (FIND_WIDGET_INITIAL_WIDTH + 28 - MAX_MATCHES_COUNT_WIDTH >= editorWidth) {
narrowFindWidget = true;
}
if (FIND_WIDGET_INITIAL_WIDTH + 28 - MAX_MATCHES_COUNT_WIDTH >= editorWidth + 50) {
collapsedFindWidget = true;
}
dom.toggleClass(this._domNode, 'collapsed-find-widget', collapsedFindWidget);
dom.toggleClass(this._domNode, 'narrow-find-widget', narrowFindWidget);
dom.toggleClass(this._domNode, 'reduced-find-widget', reducedFindWidget);
if (!narrowFindWidget && !collapsedFindWidget) {
// the minimal left offset of findwidget is 15px.
this._domNode.style.maxWidth = `${editorWidth - 28 - 15}px`;
}
};
checkEditorWidth();
this._register(this._tableController.onDidChangeConfiguration((e: IConfigurationChangedEvent) => {
if (e.layoutInfo) {
checkEditorWidth();
}
}));
this._findInputFocussed = CONTEXT_FIND_INPUT_FOCUSED.bindTo(contextKeyService);
this._focusTracker = this._register(dom.trackFocus(this._findInput.inputBox.inputElement));
this._focusTracker.onDidFocus(() => {
this._findInputFocussed.set(true);
});
this._focusTracker.onDidBlur(() => {
this._findInputFocussed.set(false);
});
this._tableController.addOverlayWidget(this);
this._applyTheme(themeService.getTheme());
this._register(themeService.onThemeChange(this._applyTheme.bind(this)));
}
// ----- IOverlayWidget API
public getId(): string {
return FindWidget.ID;
}
public getDomNode(): HTMLElement {
return this._domNode;
}
public getPosition(): IOverlayWidgetPosition {
if (this._isVisible) {
return {
preference: OverlayWidgetPositionPreference.TOP_RIGHT_CORNER
};
}
return null;
}
// ----- React to state changes
private _onStateChanged(e: FindReplaceStateChangedEvent): void {
if (e.searchString) {
this._findInput.setValue(this._state.searchString);
this._updateButtons();
}
if (e.isRevealed) {
if (this._state.isRevealed) {
this._reveal(true);
} else {
this._hide(true);
}
}
if (e.isRegex) {
this._findInput.setRegex(this._state.isRegex);
}
if (e.wholeWord) {
this._findInput.setWholeWords(this._state.wholeWord);
}
if (e.matchCase) {
this._findInput.setCaseSensitive(this._state.matchCase);
}
if (e.searchString || e.matchesCount || e.matchesPosition) {
let showRedOutline = (this._state.searchString.length > 0 && this._state.matchesCount === 0);
dom.toggleClass(this._domNode, 'no-results', showRedOutline);
this._updateMatchesCount();
}
}
private _updateMatchesCount(): void {
this._matchesCount.style.minWidth = MAX_MATCHES_COUNT_WIDTH + 'px';
if (this._state.matchesCount >= PROFILER_MAX_MATCHES) {
this._matchesCount.title = NLS_MATCHES_COUNT_LIMIT_TITLE;
} else {
this._matchesCount.title = '';
}
// remove previous content
if (this._matchesCount.firstChild) {
this._matchesCount.removeChild(this._matchesCount.firstChild);
}
let label: string;
if (this._state.matchesCount > 0) {
let matchesCount: string = String(this._state.matchesCount);
if (this._state.matchesCount >= PROFILER_MAX_MATCHES) {
matchesCount = PROFILER_MAX_MATCHES + '+';
}
let matchesPosition: string = String(this._state.matchesPosition);
if (matchesPosition === '0') {
matchesPosition = '?';
}
label = strings.format(NLS_MATCHES_LOCATION, matchesPosition, matchesCount);
} else {
label = NLS_NO_RESULTS;
}
this._matchesCount.appendChild(document.createTextNode(label));
MAX_MATCHES_COUNT_WIDTH = Math.max(MAX_MATCHES_COUNT_WIDTH, this._matchesCount.clientWidth);
}
// ----- actions
private _updateButtons(): void {
this._findInput.setEnabled(this._isVisible);
this._closeBtn.setEnabled(this._isVisible);
let findInputIsNonEmpty = (this._state.searchString.length > 0);
this._prevBtn.setEnabled(this._isVisible && findInputIsNonEmpty);
this._nextBtn.setEnabled(this._isVisible && findInputIsNonEmpty);
}
private _reveal(animate: boolean): void {
if (!this._isVisible) {
this._isVisible = true;
this._updateButtons();
setTimeout(() => {
dom.addClass(this._domNode, 'visible');
this._domNode.setAttribute('aria-hidden', 'false');
if (!animate) {
dom.addClass(this._domNode, 'noanimation');
setTimeout(() => {
dom.removeClass(this._domNode, 'noanimation');
}, 200);
}
}, 0);
this._tableController.layoutOverlayWidget(this);
}
}
private _hide(focusTheEditor: boolean): void {
if (this._isVisible) {
this._isVisible = false;
this._updateButtons();
dom.removeClass(this._domNode, 'visible');
this._domNode.setAttribute('aria-hidden', 'true');
if (focusTheEditor) {
this._tableController.focus();
}
this._tableController.layoutOverlayWidget(this);
}
}
private _applyTheme(theme: ITheme) {
let inputStyles: IFindInputStyles = {
inputActiveOptionBorder: theme.getColor(colors.inputActiveOptionBorder),
inputBackground: theme.getColor(colors.inputBackground),
inputForeground: theme.getColor(colors.inputForeground),
inputBorder: theme.getColor(colors.inputBorder),
inputValidationInfoBackground: theme.getColor(colors.inputValidationInfoBackground),
inputValidationInfoBorder: theme.getColor(colors.inputValidationInfoBorder),
inputValidationWarningBackground: theme.getColor(colors.inputValidationWarningBackground),
inputValidationWarningBorder: theme.getColor(colors.inputValidationWarningBorder),
inputValidationErrorBackground: theme.getColor(colors.inputValidationErrorBackground),
inputValidationErrorBorder: theme.getColor(colors.inputValidationErrorBorder)
};
this._findInput.style(inputStyles);
}
// ----- Public
public focusFindInput(): void {
this._findInput.focus();
}
public highlightFindOptions(): void {
this._findInput.highlightFindOptions();
}
private _onFindInputMouseDown(e: IMouseEvent): void {
// on linux, middle key does pasting.
if (e.middleButton) {
e.stopPropagation();
}
}
private _onFindInputKeyDown(e: IKeyboardEvent): void {
if (e.equals(KeyCode.Enter)) {
this._tableController.getAction(ACTION_IDS.FIND_NEXT).run().then(null, onUnexpectedError);
e.preventDefault();
return;
}
if (e.equals(KeyMod.Shift | KeyCode.Enter)) {
this._tableController.getAction(ACTION_IDS.FIND_NEXT).run().then(null, onUnexpectedError);
e.preventDefault();
return;
}
if (e.equals(KeyCode.Tab)) {
this._findInput.focusOnCaseSensitive();
e.preventDefault();
return;
}
if (e.equals(KeyMod.CtrlCmd | KeyCode.DownArrow)) {
this._tableController.focus();
e.preventDefault();
return;
}
}
// ----- sash
public getHorizontalSashTop(sash: Sash): number {
return 0;
}
public getHorizontalSashLeft?(sash: Sash): number {
return 0;
}
public getHorizontalSashWidth?(sash: Sash): number {
return 500;
}
// ----- initialization
private _keybindingLabelFor(actionId: string): string {
let kb = this._keybindingService.lookupKeybinding(actionId);
if (!kb) {
return '';
}
return ` (${kb.getLabel()})`;
}
private _buildFindPart(): HTMLElement {
// Find input
this._findInput = this._register(new FindInput(null, this._contextViewProvider, true, {
width: FIND_INPUT_AREA_WIDTH,
label: NLS_FIND_INPUT_LABEL,
placeholder: NLS_FIND_INPUT_PLACEHOLDER,
appendCaseSensitiveLabel: this._keybindingLabelFor(FIND_IDS.ToggleCaseSensitiveCommand),
appendWholeWordsLabel: this._keybindingLabelFor(FIND_IDS.ToggleWholeWordCommand),
appendRegexLabel: this._keybindingLabelFor(FIND_IDS.ToggleRegexCommand),
validation: (value: string): InputBoxMessage => {
if (value.length === 0) {
return null;
}
if (!this._findInput.getRegex()) {
return null;
}
try {
/* tslint:disable:no-unused-expression */
new RegExp(value);
/* tslint:enable:no-unused-expression */
return null;
} catch (e) {
return { content: e.message };
}
}
}));
this._findInput.setRegex(!!this._state.isRegex);
this._findInput.setCaseSensitive(!!this._state.matchCase);
this._findInput.setWholeWords(!!this._state.wholeWord);
this._register(this._findInput.onKeyDown((e) => this._onFindInputKeyDown(e)));
this._register(this._findInput.onInput(() => {
let self = this;
if (self.searchTimeoutHandle) {
clearTimeout(self.searchTimeoutHandle);
}
this.searchTimeoutHandle = setTimeout(function () {
self._state.change({ searchString: self._findInput.getValue() }, true);
}, 300);
}));
this._register(this._findInput.onDidOptionChange(() => {
this._state.change({
isRegex: this._findInput.getRegex(),
wholeWord: this._findInput.getWholeWords(),
matchCase: this._findInput.getCaseSensitive()
}, true);
}));
if (platform.isLinux) {
this._register(this._findInput.onMouseDown((e) => this._onFindInputMouseDown(e)));
}
this._matchesCount = document.createElement('div');
this._matchesCount.className = 'matchesCount';
this._updateMatchesCount();
// Previous button
this._prevBtn = this._register(new SimpleButton({
label: NLS_PREVIOUS_MATCH_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.PreviousMatchFindAction),
className: 'previous',
onTrigger: () => {
this._tableController.getAction(ACTION_IDS.FIND_PREVIOUS).run().then(null, onUnexpectedError);
},
onKeyDown: (e) => { }
}));
// Next button
this._nextBtn = this._register(new SimpleButton({
label: NLS_NEXT_MATCH_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.NextMatchFindAction),
className: 'next',
onTrigger: () => {
this._tableController.getAction(ACTION_IDS.FIND_NEXT).run().then(null, onUnexpectedError);
},
onKeyDown: (e) => { }
}));
let findPart = document.createElement('div');
findPart.className = 'find-part';
findPart.appendChild(this._findInput.domNode);
findPart.appendChild(this._matchesCount);
findPart.appendChild(this._prevBtn.domNode);
findPart.appendChild(this._nextBtn.domNode);
// Close button
this._closeBtn = this._register(new SimpleButton({
label: NLS_CLOSE_BTN_LABEL + this._keybindingLabelFor(FIND_IDS.CloseFindWidgetCommand),
className: 'close-fw',
onTrigger: () => {
this._state.change({ isRevealed: false, searchScope: null }, false);
},
onKeyDown: () => { }
}));
findPart.appendChild(this._closeBtn.domNode);
return findPart;
}
private _buildDomNode(): void {
// Find part
let findPart = this._buildFindPart();
// Widget
this._domNode = document.createElement('div');
this._domNode.className = 'editor-widget find-widget';
this._domNode.setAttribute('aria-hidden', 'true');
this._domNode.appendChild(findPart);
this._buildSash();
}
private _buildSash() {
this._resizeSash = new Sash(this._domNode, this, { orientation: Orientation.VERTICAL });
let originalWidth = FIND_WIDGET_INITIAL_WIDTH;
this._register(this._resizeSash.onDidStart((e: ISashEvent) => {
originalWidth = dom.getTotalWidth(this._domNode);
}));
this._register(this._resizeSash.onDidChange((evt: ISashEvent) => {
let width = originalWidth + evt.startX - evt.currentX;
if (width < FIND_WIDGET_INITIAL_WIDTH) {
// narrow down the find widget should be handled by CSS.
return;
}
let maxWidth = parseFloat(dom.getComputedStyle(this._domNode).maxWidth) || 0;
if (width > maxWidth) {
return;
}
this._domNode.style.width = `${width}px`;
}));
}
}
interface ISimpleCheckboxOpts {
parent: HTMLElement;
title: string;
onChange: () => void;
}
class SimpleCheckbox extends Widget {
private static _COUNTER = 0;
private _opts: ISimpleCheckboxOpts;
private _domNode: HTMLElement;
private _checkbox: HTMLInputElement;
private _label: HTMLLabelElement;
constructor(opts: ISimpleCheckboxOpts) {
super();
this._opts = opts;
this._domNode = document.createElement('div');
this._domNode.className = 'monaco-checkbox';
this._domNode.title = this._opts.title;
this._domNode.tabIndex = 0;
this._checkbox = document.createElement('input');
this._checkbox.type = 'checkbox';
this._checkbox.className = 'checkbox';
this._checkbox.id = 'checkbox-' + SimpleCheckbox._COUNTER++;
this._checkbox.tabIndex = -1;
this._label = document.createElement('label');
this._label.className = 'label';
// Connect the label and the checkbox. Checkbox will get checked when the label recieves a click.
this._label.htmlFor = this._checkbox.id;
this._label.tabIndex = -1;
this._domNode.appendChild(this._checkbox);
this._domNode.appendChild(this._label);
this._opts.parent.appendChild(this._domNode);
this.onchange(this._checkbox, (e) => {
this._opts.onChange();
});
}
public get domNode(): HTMLElement {
return this._domNode;
}
public get checked(): boolean {
return this._checkbox.checked;
}
public set checked(newValue: boolean) {
this._checkbox.checked = newValue;
}
public focus(): void {
this._checkbox.focus();
}
private enable(): void {
this._checkbox.removeAttribute('disabled');
}
private disable(): void {
this._checkbox.disabled = true;
}
public setEnabled(enabled: boolean): void {
if (enabled) {
this.enable();
this.domNode.tabIndex = 0;
} else {
this.disable();
this.domNode.tabIndex = -1;
}
}
}
interface ISimpleButtonOpts {
label: string;
className: string;
onTrigger: () => void;
onKeyDown: (e: IKeyboardEvent) => void;
}
class SimpleButton extends Widget {
private _opts: ISimpleButtonOpts;
private _domNode: HTMLElement;
constructor(opts: ISimpleButtonOpts) {
super();
this._opts = opts;
this._domNode = document.createElement('div');
this._domNode.title = this._opts.label;
this._domNode.tabIndex = 0;
this._domNode.className = 'button ' + this._opts.className;
this._domNode.setAttribute('role', 'button');
this._domNode.setAttribute('aria-label', this._opts.label);
this.onclick(this._domNode, (e) => {
this._opts.onTrigger();
e.preventDefault();
});
this.onkeydown(this._domNode, (e) => {
if (e.equals(KeyCode.Space) || e.equals(KeyCode.Enter)) {
this._opts.onTrigger();
e.preventDefault();
return;
}
this._opts.onKeyDown(e);
});
}
public get domNode(): HTMLElement {
return this._domNode;
}
public isEnabled(): boolean {
return (this._domNode.tabIndex >= 0);
}
public focus(): void {
this._domNode.focus();
}
public setEnabled(enabled: boolean): void {
dom.toggleClass(this._domNode, 'disabled', !enabled);
this._domNode.setAttribute('aria-disabled', String(!enabled));
this._domNode.tabIndex = enabled ? 0 : -1;
}
public setExpanded(expanded: boolean): void {
this._domNode.setAttribute('aria-expanded', String(!!expanded));
}
public toggleClass(className: string, shouldHaveIt: boolean): void {
dom.toggleClass(this._domNode, className, shouldHaveIt);
}
}

View File

@@ -0,0 +1,316 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { IProfilerSession, IProfilerService, ProfilerSessionID, IProfilerViewTemplate, ProfilerFilter } from 'sql/workbench/services/profiler/common/interfaces';
import { ProfilerState } from 'sql/workbench/parts/profiler/common/profilerState';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import * as azdata from 'azdata';
import * as nls from 'vs/nls';
import { EditorInput, ConfirmResult } from 'vs/workbench/common/editor';
import { IEditorModel } from 'vs/platform/editor/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { Event, Emitter } from 'vs/base/common/event';
import { generateUuid } from 'vs/base/common/uuid';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import * as types from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import Severity from 'vs/base/common/severity';
import { FilterData } from 'sql/workbench/services/profiler/common/profilerFilter';
export class ProfilerInput extends EditorInput implements IProfilerSession {
public static ID: string = 'workbench.editorinputs.profilerinputs';
public static SCHEMA: string = 'profiler';
private _data: TableDataView<Slick.SlickData>;
private _id: ProfilerSessionID;
private _state: ProfilerState;
private _columns: string[] = [];
private _sessionName: string;
private _viewTemplate: IProfilerViewTemplate;
// mapping of event categories to what column they display under
// used for coallescing multiple events with different names to the same column
private _columnMapping: { [event: string]: string } = {};
private _onColumnsChanged = new Emitter<Slick.Column<Slick.SlickData>[]>();
public onColumnsChanged: Event<Slick.Column<Slick.SlickData>[]> = this._onColumnsChanged.event;
private _filter: ProfilerFilter = { clauses: [] };
constructor(
public connection: IConnectionProfile,
@IInstantiationService private _instantiationService: IInstantiationService,
@IProfilerService private _profilerService: IProfilerService,
@INotificationService private _notificationService: INotificationService,
@IDialogService private _dialogService: IDialogService
) {
super();
this._state = new ProfilerState();
// set inital state
this.state.change({
isConnected: false,
isStopped: true,
isPaused: false,
isRunning: false,
autoscroll: true
});
this._profilerService.registerSession(generateUuid(), connection, this).then((id) => {
this._id = id;
this.state.change({ isConnected: true });
});
let searchFn = (val: { [x: string]: string }, exp: string): Array<number> => {
let ret = new Array<number>();
for (let i = 0; i < this._columns.length; i++) {
let colVal = val[this._columns[i]];
if (colVal && colVal.toLocaleLowerCase().includes(exp.toLocaleLowerCase())) {
ret.push(i);
}
}
return ret;
};
let filterFn = (data: Array<Slick.SlickData>): Array<Slick.SlickData> => {
return FilterData(this._filter, data);
};
this._data = new TableDataView<Slick.SlickData>(undefined, searchFn, undefined, filterFn);
}
public get providerType(): string {
return this.connection ? this.connection.providerName : undefined;
}
public set viewTemplate(template: IProfilerViewTemplate) {
this._data.clear();
this._viewTemplate = template;
let newColumns = this._viewTemplate.columns.reduce<Array<string>>((p, e) => {
p.push(e.name);
return p;
}, []);
let newMapping: { [event: string]: string } = {};
this._viewTemplate.columns.forEach(c => {
c.eventsMapped.forEach(e => {
newMapping[e] = c.name;
});
});
this.setColumnMapping(newColumns, newMapping);
}
public get viewTemplate(): IProfilerViewTemplate {
return this._viewTemplate;
}
public set sessionName(name: string) {
if (!this.state.isRunning || !this.state.isPaused) {
this._sessionName = name;
}
}
public get sessionName(): string {
return this._sessionName;
}
public getTypeId(): string {
return ProfilerInput.ID;
}
public resolve(refresh?: boolean): Promise<IEditorModel> {
return undefined;
}
public getName(): string {
let name: string = nls.localize('profilerInput.profiler', 'Profiler');
if (!this.connection) {
return name;
}
name += ': ' + this.connection.serverName.substring(0, 20);
return name;
}
public getResource(): URI {
return URI.from({
scheme: ProfilerInput.SCHEMA,
path: 'profiler'
});
}
public get data(): TableDataView<Slick.SlickData> {
return this._data;
}
public get columns(): Slick.Column<Slick.SlickData>[] {
if (this._columns) {
return this._columns.map(i => {
return <Slick.Column<Slick.SlickData>>{
id: i,
field: i,
name: i,
sortable: true
};
});
} else {
return [];
}
}
public setColumns(columns: Array<string>) {
this._columns = columns;
this._onColumnsChanged.fire(this.columns);
}
public setColumnMapping(columns: Array<string>, mapping: { [event: string]: string }) {
this._columns = columns;
this._columnMapping = mapping;
this._onColumnsChanged.fire(this.columns);
}
public get connectionName(): string {
if (!types.isUndefinedOrNull(this.connection)) {
if (this.connection.databaseName) {
return `${this.connection.serverName} ${this.connection.databaseName}`;
} else {
return `${this.connection.serverName}`;
}
}
else {
return nls.localize('profilerInput.notConnected', "Not connected");
}
}
public get id(): ProfilerSessionID {
return this._id;
}
public get state(): ProfilerState {
return this._state;
}
public get filter(): ProfilerFilter {
return this._filter;
}
public onSessionStopped(notification: azdata.ProfilerSessionStoppedParams) {
this._notificationService.error(nls.localize("profiler.sessionStopped", "XEvent Profiler Session stopped unexpectedly on the server {0}.", this.connection.serverName));
this.state.change({
isStopped: true,
isPaused: false,
isRunning: false
});
}
public onProfilerSessionCreated(params: azdata.ProfilerSessionCreatedParams) {
if (types.isUndefinedOrNull(params.sessionName) || types.isUndefinedOrNull(params.templateName)) {
this._notificationService.error(nls.localize("profiler.sessionCreationError", "Error while starting new session"));
} else {
this._sessionName = params.sessionName;
let sessionTemplate = this._profilerService.getSessionTemplates().find((template) => {
return template.name === params.templateName;
});
if (!types.isUndefinedOrNull(sessionTemplate)) {
let newView = this._profilerService.getViewTemplates().find((view) => {
return view.name === sessionTemplate.defaultView;
});
if (!types.isUndefinedOrNull(newView)) {
this.viewTemplate = newView;
}
}
this.data.clear();
this.state.change({
isStopped: false,
isPaused: false,
isRunning: true
});
}
}
public onSessionStateChanged(state: ProfilerState) {
this.state.change(state);
}
public onMoreRows(eventMessage: azdata.ProfilerSessionEvents) {
if (eventMessage.eventsLost) {
this._notificationService.warn(nls.localize("profiler.eventsLost", "The XEvent Profiler session for {0} has lost events.", this.connection.serverName));
}
let newEvents = [];
for (let i: number = 0; i < eventMessage.events.length && i < 500; ++i) {
let e: azdata.ProfilerEvent = eventMessage.events[i];
let data = {};
data['EventClass'] = e.name;
data['StartTime'] = e.timestamp;
// Using ' ' instead of '' fixed the error where clicking through events
// with empty text fields causes future text panes to be highlighted.
// This is a temporary fix
data['TextData'] = ' ';
for (let key in e.values) {
let columnName = this._columnMapping[key];
if (columnName) {
let value = e.values[key];
data[columnName] = value;
}
}
newEvents.push(data);
}
if (newEvents.length > 0) {
this._data.push(newEvents);
}
}
filterSession(filter: ProfilerFilter) {
this._filter = filter;
if (this._filter.clauses.length !== 0) {
this.data.filter();
} else {
this.data.clearFilter();
}
}
clearFilter() {
this._filter = { clauses: [] };
this.data.clearFilter();
}
confirmSave(): Promise<ConfirmResult> {
if (this.state.isRunning || this.state.isPaused) {
return this._dialogService.show(Severity.Warning,
nls.localize('confirmStopProfilerSession', "Would you like to stop the running XEvent session?"),
[
nls.localize('profilerClosingActions.yes', 'Yes'),
nls.localize('profilerClosingActions.no', 'No'),
nls.localize('profilerClosingActions.cancel', 'Cancel')
]).then((selection: number) => {
if (selection === 0) {
this._profilerService.stopSession(this.id);
return ConfirmResult.DONT_SAVE;
} else if (selection === 1) {
return ConfirmResult.DONT_SAVE;
} else {
return ConfirmResult.CANCEL;
}
});
} else {
return Promise.resolve(ConfirmResult.DONT_SAVE);
}
}
isDirty(): boolean {
return this.state.isRunning || this.state.isPaused;
}
dispose() {
super.dispose();
this._profilerService.disconnectSession(this.id);
}
}

View File

@@ -0,0 +1,96 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import * as nls from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { EditorOptions } from 'vs/workbench/common/editor';
import { StandaloneCodeEditor } from 'vs/editor/standalone/browser/standaloneCodeEditor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
class ProfilerResourceCodeEditor extends StandaloneCodeEditor {
// protected _getContributions(): IEditorContributionCtor[] {
// let contributions = super._getContributions();
// let skipContributions = [FoldingController.prototype];
// contributions = contributions.filter(c => skipContributions.indexOf(c.prototype) === -1);
// return contributions;
// }
}
/**
* Extension of TextResourceEditor that is always readonly rather than only with non UntitledInputs
*/
export class ProfilerResourceEditor extends BaseTextEditor {
public static ID = 'profiler.editors.textEditor';
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IInstantiationService instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService,
@ITextResourceConfigurationService configurationService: ITextResourceConfigurationService,
@IThemeService themeService: IThemeService,
@ITextFileService textFileService: ITextFileService,
@IEditorService protected editorService: IEditorService,
@IEditorGroupsService editorGroupService: IEditorGroupsService,
@IWindowService windowService: IWindowService
) {
super(ProfilerResourceEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService, editorService, editorGroupService, windowService);
}
public createEditorControl(parent: HTMLElement, configuration: IEditorOptions): editorCommon.IEditor {
return this.instantiationService.createInstance(ProfilerResourceCodeEditor, parent, configuration);
}
protected getConfigurationOverrides(): IEditorOptions {
const options = super.getConfigurationOverrides();
options.readOnly = true;
if (this.input) {
options.inDiffEditor = true;
options.scrollBeyondLastLine = false;
options.folding = false;
options.renderWhitespace = 'none';
options.wordWrap = 'on';
options.renderIndentGuides = false;
options.rulers = [];
options.glyphMargin = true;
options.minimap = {
enabled: false
};
}
return options;
}
setInput(input: UntitledEditorInput, options: EditorOptions): Promise<void> {
return super.setInput(input, options, CancellationToken.None)
.then(() => this.input.resolve()
.then(editorModel => editorModel.load())
.then(editorModel => this.getControl().setModel((<ResourceEditorModel>editorModel).textEditorModel)));
}
protected getAriaLabel(): string {
return nls.localize('profilerTextEditorAriaLabel', 'Profiler editor for event text. Readonly');
}
public layout(dimension: DOM.Dimension) {
this.getControl().layout(dimension);
}
}

View File

@@ -0,0 +1,297 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IProfilerController } from 'sql/workbench/parts/profiler/common/interfaces';
import { ProfilerInput } from 'sql/workbench/parts/profiler/browser/profilerInput';
import { Table } from 'sql/base/browser/ui/table/table';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin';
import { IProfilerStateChangedEvent } from 'sql/workbench/parts/profiler/common/profilerState';
import { FindWidget, ITableController, IConfigurationChangedEvent, ACTION_IDS, PROFILER_MAX_MATCHES } from 'sql/workbench/parts/profiler/browser/profilerFindWidget';
import { ProfilerFindNext, ProfilerFindPrevious } from 'sql/workbench/parts/profiler/browser/profilerActions';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IEditorAction } from 'vs/editor/common/editorCommon';
import { IOverlayWidget } from 'vs/editor/browser/editorBrowser';
import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/findState';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { Event, Emitter } from 'vs/base/common/event';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Dimension } from 'vs/base/browser/dom';
import { textFormatter, slickGridDataItemColumnValueExtractor } from 'sql/base/browser/ui/table/formatters';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStatusbarService, StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar';
import { localize } from 'vs/nls';
export interface ProfilerTableViewState {
scrollTop: number;
scrollLeft: number;
}
export class ProfilerTableEditor extends BaseEditor implements IProfilerController, ITableController {
public static ID: string = 'workbench.editor.profiler.table';
protected _input: ProfilerInput;
private _profilerTable: Table<Slick.SlickData>;
private _columnListener: IDisposable;
private _stateListener: IDisposable;
private _findCountChangeListener: IDisposable;
private _findState: FindReplaceState;
private _finder: FindWidget;
private _overlay: HTMLElement;
private _currentDimensions: Dimension;
private _actionMap: { [x: string]: IEditorAction } = {};
private _statusbarItem: IDisposable;
private _showStatusBarItem: boolean;
private _onDidChangeConfiguration = new Emitter<IConfigurationChangedEvent>();
public onDidChangeConfiguration: Event<IConfigurationChangedEvent> = this._onDidChangeConfiguration.event;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IWorkbenchThemeService private _themeService: IWorkbenchThemeService,
@IContextViewService private _contextViewService: IContextViewService,
@IKeybindingService private _keybindingService: IKeybindingService,
@IContextKeyService private _contextKeyService: IContextKeyService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService,
@IStatusbarService private _statusbarService: IStatusbarService
) {
super(ProfilerTableEditor.ID, telemetryService, _themeService, storageService);
this._actionMap[ACTION_IDS.FIND_NEXT] = this._instantiationService.createInstance(ProfilerFindNext, this);
this._actionMap[ACTION_IDS.FIND_PREVIOUS] = this._instantiationService.createInstance(ProfilerFindPrevious, this);
this._showStatusBarItem = true;
}
public createEditor(parent: HTMLElement): void {
this._overlay = document.createElement('div');
this._overlay.className = 'overlayWidgets';
this._overlay.style.width = '100%';
this._overlay.style.zIndex = '4';
parent.appendChild(this._overlay);
this._profilerTable = new Table(parent, {
sorter: (args) => {
let input = this.input as ProfilerInput;
if (input && input.data) {
input.data.sort(args);
}
}
}, {
dataItemColumnValueExtractor: slickGridDataItemColumnValueExtractor
});
this._profilerTable.setSelectionModel(new RowSelectionModel());
attachTableStyler(this._profilerTable, this._themeService);
this._findState = new FindReplaceState();
this._findState.onFindReplaceStateChange(e => this._onFindStateChange(e));
this._finder = new FindWidget(
this,
this._findState,
this._contextViewService,
this._keybindingService,
this._contextKeyService,
this._themeService
);
}
public setInput(input: ProfilerInput): Promise<void> {
this._showStatusBarItem = true;
this._input = input;
this._updateRowCountStatus();
if (this._columnListener) {
this._columnListener.dispose();
}
this._columnListener = input.onColumnsChanged(e => {
this._profilerTable.columns = e.map(e => {
e.formatter = textFormatter;
return e;
});
this._profilerTable.autosizeColumns();
});
if (this._stateListener) {
this._stateListener.dispose();
}
this._stateListener = input.state.addChangeListener(e => this._onStateChange(e));
input.data.onRowCountChange(() => {
this._profilerTable.updateRowCount();
this._updateRowCountStatus();
});
input.data.onFilterStateChange(() => {
this._profilerTable.grid.invalidateAllRows();
this._profilerTable.updateRowCount();
this._updateRowCountStatus();
});
if (this._findCountChangeListener) {
this._findCountChangeListener.dispose();
}
this._findCountChangeListener = input.data.onFindCountChange(() => this._updateFinderMatchState());
this._profilerTable.setData(input.data);
this._profilerTable.columns = input.columns;
this._profilerTable.autosizeColumns();
this._input.data.currentFindPosition.then(val => {
this._profilerTable.setActiveCell(val.row, val.col);
this._updateFinderMatchState();
}, er => { });
this._input.onDispose(() => {
this._disposeStatusbarItem();
});
return Promise.resolve(null);
}
public toggleSearch(): void {
this._findState.change({
isRevealed: true
}, false);
this._finder.focusFindInput();
}
public findNext(): void {
this._input.data.findNext().then(p => {
this._profilerTable.setActiveCell(p.row, p.col);
this._updateFinderMatchState();
}, er => { });
}
public findPrevious(): void {
this._input.data.findPrevious().then(p => {
this._profilerTable.setActiveCell(p.row, p.col);
this._updateFinderMatchState();
}, er => { });
}
public getConfiguration() {
return {
layoutInfo: {
width: this._currentDimensions ? this._currentDimensions.width : 0
}
};
}
public layoutOverlayWidget(widget: IOverlayWidget): void {
// no op
}
public addOverlayWidget(widget: IOverlayWidget): void {
let domNode = widget.getDomNode();
domNode.style.right = '28px';
this._overlay.appendChild(widget.getDomNode());
this._findState.change({ isRevealed: false }, false);
}
public getAction(id: string): IEditorAction {
return this._actionMap[id];
}
public focus(): void {
this._profilerTable.focus();
}
public layout(dimension: Dimension): void {
this._currentDimensions = dimension;
this._profilerTable.layout(dimension);
this._profilerTable.autosizeColumns();
this._onDidChangeConfiguration.fire({ layoutInfo: true });
}
public onSelectedRowsChanged(fn: (e: Slick.EventData, args: Slick.OnSelectedRowsChangedEventArgs<Slick.SlickData>) => any): void {
if (this._profilerTable) {
this._profilerTable.onSelectedRowsChanged(fn);
}
}
private _onStateChange(e: IProfilerStateChangedEvent): void {
if (e.autoscroll) {
this._profilerTable.autoScroll = this._input.state.autoscroll;
}
}
public updateState(): void {
this._onStateChange({ autoscroll: true });
}
private _onFindStateChange(e: FindReplaceStateChangedEvent): void {
if (e.isRevealed) {
if (this._findState.isRevealed) {
this._finder.getDomNode().style.top = '0px';
this._updateFinderMatchState();
} else {
this._finder.getDomNode().style.top = '';
}
}
if (e.searchString) {
if (this._input && this._input.data) {
if (this._findState.searchString) {
this._input.data.find(this._findState.searchString, PROFILER_MAX_MATCHES).then(p => {
if (p) {
this._profilerTable.setActiveCell(p.row, p.col);
this._updateFinderMatchState();
this._finder.focusFindInput();
}
});
} else {
this._input.data.clearFind();
}
}
}
}
private _updateFinderMatchState(): void {
if (this._input && this._input.data) {
this._findState.changeMatchInfo(this._input.data.findPosition, this._input.data.findCount, undefined);
} else {
this._findState.changeMatchInfo(0, 0, undefined);
}
}
private _updateRowCountStatus(): void {
if (this._showStatusBarItem) {
let message = this._input.data.filterEnabled ?
localize('ProfilerTableEditor.eventCountFiltered', 'Events (Filtered): {0}/{1}', this._input.data.getLength(), this._input.data.getLengthNonFiltered())
: localize('ProfilerTableEditor.eventCount', 'Events: {0}', this._input.data.getLength());
this._disposeStatusbarItem();
this._statusbarItem = this._statusbarService.addEntry({ text: message }, StatusbarAlignment.RIGHT);
}
}
private _disposeStatusbarItem() {
if (this._statusbarItem) {
this._statusbarItem.dispose();
}
}
public saveViewState(): ProfilerTableViewState {
this._disposeStatusbarItem();
this._showStatusBarItem = false;
let viewElement = this._profilerTable.grid.getCanvasNode().parentElement;
return {
scrollTop: viewElement.scrollTop,
scrollLeft: viewElement.scrollLeft
};
}
public restoreViewState(state: ProfilerTableViewState): void {
this._showStatusBarItem = true;
this._updateRowCountStatus();
let viewElement = this._profilerTable.grid.getCanvasNode().parentElement;
viewElement.scrollTop = state.scrollTop;
viewElement.scrollLeft = state.scrollLeft;
}
}

View File

@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
export interface IProfilerController {
findNext(): void;
findPrevious(): void;
}
export const CONTEXT_PROFILER_EDITOR = new RawContextKey<boolean>('inProfilerTableEditor', false);
export const PROFILER_TABLE_COMMAND_SEARCH = 'profiler.table.action.search';

View File

@@ -0,0 +1,115 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EventEmitter } from 'sql/base/common/eventEmitter';
import { IDisposable } from 'vs/base/common/lifecycle';
export interface IProfilerStateChangedEvent {
isConnected?: boolean;
isRunning?: boolean;
isPaused?: boolean;
isStopped?: boolean;
autoscroll?: boolean;
isPanelCollapsed?: boolean;
}
export interface INewProfilerState {
isConnected?: boolean;
isRunning?: boolean;
isPaused?: boolean;
isStopped?: boolean;
autoscroll?: boolean;
isPanelCollapsed?: boolean;
}
export class ProfilerState implements IDisposable {
private static _CHANGED_EVENT = 'changed';
private _isConnected: boolean;
private _isRunning: boolean;
private _isPaused: boolean;
private _isStopped: boolean;
private _autoscroll: boolean;
private _isPanelCollapsed = true;
private _eventEmitter: EventEmitter;
public get isConnected(): boolean { return this._isConnected; }
public get isRunning(): boolean { return this._isRunning; }
public get isPaused(): boolean { return this._isPaused; }
public get isStopped(): boolean { return this._isStopped; }
public get autoscroll(): boolean { return this._autoscroll; }
public get isPanelCollapsed(): boolean { return this._isPanelCollapsed; }
constructor() {
this._eventEmitter = new EventEmitter();
}
public dispose(): void {
this._eventEmitter.dispose();
}
public addChangeListener(listener: (e: IProfilerStateChangedEvent) => void): IDisposable {
return this._eventEmitter.addListener(ProfilerState._CHANGED_EVENT, listener);
}
public change(newState: INewProfilerState): void {
let changeEvent: IProfilerStateChangedEvent = {
isConnected: false,
isRunning: false,
isPaused: false,
isStopped: false,
autoscroll: false,
isPanelCollapsed: false
};
let somethingChanged = false;
if (typeof newState.isConnected !== 'undefined') {
if (this._isConnected !== newState.isConnected) {
this._isConnected = newState.isConnected;
changeEvent.isConnected = true;
somethingChanged = true;
}
}
if (typeof newState.isRunning !== 'undefined') {
if (this._isRunning !== newState.isRunning) {
this._isRunning = newState.isRunning;
changeEvent.isRunning = true;
somethingChanged = true;
}
}
if (typeof newState.isPaused !== 'undefined') {
if (this._isPaused !== newState.isPaused) {
this._isPaused = newState.isPaused;
changeEvent.isPaused = true;
somethingChanged = true;
}
}
if (typeof newState.isStopped !== 'undefined') {
if (this._isStopped !== newState.isStopped) {
this._isStopped = newState.isStopped;
changeEvent.isStopped = true;
somethingChanged = true;
}
}
if (typeof newState.autoscroll !== 'undefined') {
if (this._autoscroll !== newState.autoscroll) {
this._autoscroll = newState.autoscroll;
changeEvent.autoscroll = true;
somethingChanged = true;
}
}
if (typeof newState.isPanelCollapsed !== 'undefined') {
if (this._isPanelCollapsed !== newState.isPanelCollapsed) {
this._isPanelCollapsed = newState.isPanelCollapsed;
changeEvent.isPanelCollapsed = true;
somethingChanged = true;
}
}
if (somethingChanged) {
this._eventEmitter.emit(ProfilerState._CHANGED_EVENT, changeEvent);
}
}
}

View File

@@ -1,4 +1,4 @@
/*---------------------------------------------------------------------------------------------
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.restore-dialog {
padding: 15px
}
.restore-panel .tabbedPanel {
border-top-color: transparent;
}
.modal .restore-dialog .dialog-label.header {
font-size: 15px;
}
.modal .restore-dialog .new-section {
padding-bottom: 50px;
}
.modal .restore-dialog .sub-section {
padding-left: 15px;
padding-right: 15px;
}
.modal .restore-dialog .dialog-input-section {
display: flex;
padding-left: 15px;
padding-right: 15px;
padding-top: 10px;
padding-bottom: 10px;
}
.modal .restore-dialog .dialog-input-section .dialog-label {
flex: 0 0 100px;
align-self: center;
}
.modal .restore-dialog .dialog-input-section .dialog-input {
flex: 1 1 auto;
}
.modal .restore-dialog .file-browser {
width: 30px;
}
.modal .restore-dialog .file-browser .monaco-button {
height: 100%;
}
.modal .restore-dialog .restore-plan-section {
width: 100%;
}
.modal .restore-dialog .restore-list {
width: 100%;
height: 500px;
margin-bottom: 15px;
overflow: hidden;
box-sizing: border-box;
}
.modal .restore-dialog .restore-list .backup-table-content .restore-data {
white-space: nowrap;
padding: 5px;
}
.modal .restore-dialog .more-options-button {
width: 100%;
display: flex;
flex-direction: row-reverse;
}
.modal .restore-dialog .more-options-button a.monaco-button.monaco-text-button {
margin-right: 15px;
}

View File

@@ -0,0 +1,854 @@
/*---------------------------------------------------------------------------------------------
* 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/restoreDialog';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Event, Emitter } from 'vs/base/common/event';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Widget } from 'vs/base/browser/ui/widget';
import { MessageType, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { localize } from 'vs/nls';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { mixin } from 'vs/base/common/objects';
import * as strings from 'vs/base/common/strings';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import * as DOM from 'vs/base/browser/dom';
import * as azdata from 'azdata';
import { Button } from 'sql/base/browser/ui/button/button';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { InputBox, OnLoseFocusParams } from 'sql/base/browser/ui/inputBox/inputBox';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin';
import { CheckboxSelectColumn } from 'sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin';
import { Table } from 'sql/base/browser/ui/table/table';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper';
import { Modal } from 'sql/workbench/browser/modal/modal';
import { attachButtonStyler, attachModalDialogStyler, attachTableStyler, attachInputBoxStyler, attachSelectBoxStyler, attachEditableDropdownStyler, attachCheckboxStyler } from 'sql/platform/theme/common/styler';
import * as TelemetryKeys from 'sql/platform/telemetry/telemetryKeys';
import * as BackupConstants from 'sql/workbench/parts/backup/common/constants';
import { RestoreViewModel, RestoreOptionParam, SouceDatabaseNamesParam } from 'sql/workbench/parts/restore/browser/restoreViewModel';
import * as FileValidationConstants from 'sql/workbench/services/fileBrowser/common/fileValidationServiceConstants';
import { Dropdown } from 'sql/base/browser/ui/editableDropdown/dropdown';
import { TabbedPanel, PanelTabIdentifier } from 'sql/base/browser/ui/panel/panel';
import { ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes';
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
import { IFileBrowserDialogController } from 'sql/workbench/services/fileBrowser/common/fileBrowserDialogController';
interface FileListElement {
logicalFileName: string;
fileType: string;
originalFileName: string;
restoreAs: string;
}
const LocalizedStrings = {
BACKFILEPATH: localize('backupFilePath', "Backup file path"),
TARGETDATABASE: localize('targetDatabase', 'Target database')
};
export class RestoreDialog extends Modal {
public viewModel: RestoreViewModel;
private _scriptButton: Button;
private _restoreButton: Button;
private _closeButton: Button;
private _optionsMap: { [name: string]: Widget } = {};
private _restoreLabel: string;
private _restoreTitle: string;
private _databaseTitle: string;
private _backupFileTitle: string;
private _ownerUri: string;
private _databaseDropdown: Dropdown;
private _isBackupFileCheckboxChanged: boolean;
// General options
private _filePathInputBox: InputBox;
private _browseFileButton: Button;
private _destinationRestoreToInputBox: InputBox;
private _restoreFromSelectBox: SelectBox;
private _sourceDatabaseSelectBox: SelectBox;
private _panel: TabbedPanel;
private _generalTabId: PanelTabIdentifier;
// File option
private readonly _relocateDatabaseFilesOption = 'relocateDbFiles';
private readonly _relocatedDataFileFolderOption = 'dataFileFolder';
private readonly _relocatedLogFileFolderOption = 'logFileFolder';
// other options
private readonly _withReplaceDatabaseOption = 'replaceDatabase';
private readonly _withKeepReplicationOption = 'keepReplication';
private readonly _withRestrictedUserOption = 'setRestrictedUser';
private readonly _recoveryStateOption = 'recoveryState';
private readonly _standbyFileOption = 'standbyFile';
private readonly _takeTaillogBackupOption = 'backupTailLog';
private readonly _tailLogWithNoRecoveryOption = 'tailLogWithNoRecovery';
private readonly _tailLogBackupFileOption = 'tailLogBackupFile';
private readonly _closeExistingConnectionsOption = 'closeExistingConnections';
private _restoreFromBackupFileElement: HTMLElement;
private _fileListTable: Table<FileListElement>;
private _fileListData: TableDataView<FileListElement>;
private _fileListTableContainer: HTMLElement;
private _restorePlanTable: Table<Slick.SlickData>;
private _restorePlanData: TableDataView<Slick.SlickData>;
private _restorePlanColumn;
private _restorePlanTableContainer: HTMLElement;
private _isRenderedRestorePlanTable: boolean;
private _onRestore = new Emitter<boolean>();
public onRestore: Event<boolean> = this._onRestore.event;
private _onValidate = new Emitter<boolean>();
public onValidate: Event<boolean> = this._onValidate.event;
private _onCancel = new Emitter<void>();
public onCancel: Event<void> = this._onCancel.event;
private _onCloseEvent = new Emitter<void>();
public onCloseEvent: Event<void> = this._onCloseEvent.event;
private _onDatabaseListFocused = new Emitter<void>();
public onDatabaseListFocused: Event<void> = this._onDatabaseListFocused.event;
constructor(
optionsMetadata: azdata.ServiceOption[],
@ILayoutService layoutService: ILayoutService,
@IThemeService themeService: IThemeService,
@IContextViewService private _contextViewService: IContextViewService,
@ITelemetryService telemetryService: ITelemetryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IFileBrowserDialogController private fileBrowserDialogService: IFileBrowserDialogController,
@IClipboardService clipboardService: IClipboardService,
) {
super(localize('RestoreDialogTitle', "Restore database"), TelemetryKeys.Restore, telemetryService, layoutService, clipboardService, themeService, contextKeyService, { hasErrors: true, isWide: true, hasSpinner: true });
this._restoreTitle = localize('restoreDialog.restoreTitle', "Restore database");
this._databaseTitle = localize('restoreDialog.database', "Database");
this._backupFileTitle = localize('restoreDialog.backupFile', "Backup file");
this._restoreLabel = localize('restoreDialog.restore', "Restore");
// view model
this.viewModel = new RestoreViewModel(optionsMetadata);
this.viewModel.onSetLastBackupTaken((value) => this.updateLastBackupTaken(value));
this.viewModel.onSetfilePath((value) => this.updateFilePath(value));
this.viewModel.onSetSourceDatabaseNames((databaseNamesParam) => this.updateSourceDatabaseName(databaseNamesParam));
this.viewModel.onSetTargetDatabaseName((value) => this.updateTargetDatabaseName(value));
this.viewModel.onSetLastBackupTaken((value) => this.updateLastBackupTaken(value));
this.viewModel.onSetRestoreOption((optionParams) => this.updateRestoreOption(optionParams));
this.viewModel.onUpdateBackupSetsToRestore((backupSets) => this.updateBackupSetsToRestore(backupSets));
this.viewModel.onUpdateRestoreDatabaseFiles((files) => this.updateRestoreDatabaseFiles(files));
this._isRenderedRestorePlanTable = false;
}
public render() {
super.render();
attachModalDialogStyler(this, this._themeService);
let cancelLabel = localize('restoreDialog.cancel', 'Cancel');
this._scriptButton = this.addFooterButton(localize('restoreDialog.script', 'Script'), () => this.restore(true));
this._restoreButton = this.addFooterButton(this._restoreLabel, () => this.restore(false));
this._closeButton = this.addFooterButton(cancelLabel, () => this.cancel());
this.registerListeners();
this._destinationRestoreToInputBox.disable();
}
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._restoreFromBackupFileElement = DOM.$('.backup-file-path');
DOM.hide(this._restoreFromBackupFileElement);
const errorMessage = localize('missingBackupFilePathError', "Backup file path is required.");
const validationOptions: IInputOptions = {
validationOptions: {
validation: (value: string) => !value ? ({ type: MessageType.ERROR, content: errorMessage }) : null
},
placeholder: localize('multipleBackupFilePath', "Please enter one or more file paths separated by commas"),
ariaLabel: LocalizedStrings.BACKFILEPATH
};
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._browseFileButton = new Button(DOM.append(filePathInputContainer, DOM.$('.file-browser')));
this._browseFileButton.label = '...';
const sourceDatabasesElement = DOM.$('.source-database-list');
this._sourceDatabaseSelectBox = this.createSelectBoxHelper(sourceDatabasesElement, localize('database', "Database"), [], '');
// Source section
const sourceElement = DOM.$('.source-section.new-section');
sourceElement.append(restoreFromElement);
sourceElement.append(this._restoreFromBackupFileElement);
sourceElement.append(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;
const dropdownContainer = DOM.append(destinationInputContainer, DOM.$('.dialog-input'));
// Get the bootstrap params and perform the bootstrap
dropdownContainer.style.width = '100%';
this._databaseDropdown = new Dropdown(dropdownContainer, this._contextViewService, this.layoutService,
{
strictSelection: false,
ariaLabel: LocalizedStrings.TARGETDATABASE,
actionLabel: localize('restoreDialog.toggleDatabaseNameDropdown', 'Select Database Toggle Dropdown')
}
);
this._databaseDropdown.onValueChange(s => {
this.databaseSelected(s);
});
this._databaseDropdown.onBlur(() => {
this.databaseSelected(this._databaseDropdown.value);
});
this._databaseDropdown.onFocus(() => {
this._onDatabaseListFocused.fire();
});
this._databaseDropdown.value = this.viewModel.targetDatabaseName;
attachEditableDropdownStyler(this._databaseDropdown, this._themeService);
this._destinationRestoreToInputBox = this.createInputBoxHelper(destinationElement, localize('restoreTo', "Restore to"));
// Restore plan section
const restorePlanElement = DOM.$('.restore-plan-section.new-section');
this.createLabelElement(restorePlanElement, localize('restorePlan', "Restore plan"), true);
this.createLabelElement(restorePlanElement, localize('backupSetsToRestore', "Backup sets to restore"));
// Backup sets table
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.setSelectionModel(new RowSelectionModel({ selectActiveRow: false }));
this._restorePlanTable.onSelectedRowsChanged((e, data) => this.backupFileCheckboxChanged(e, data));
// Content in general tab
const generalTab = DOM.$('.restore-dialog');
generalTab.append(sourceElement);
generalTab.append(destinationElement);
generalTab.append(restorePlanElement);
// Content in file tab
const fileContentElement = DOM.$('.restore-dialog');
const restoreAsSectionContainer = DOM.append(fileContentElement, DOM.$('.new-section'));
this.createLabelElement(restoreAsSectionContainer, localize('restoreDatabaseFileAs', "Restore database files as"), true);
this.createOptionControl(restoreAsSectionContainer, this._relocateDatabaseFilesOption);
const subSectionContainer = DOM.append(restoreAsSectionContainer, DOM.$('.sub-section'));
this.createOptionControl(subSectionContainer, this._relocatedDataFileFolderOption);
this.createOptionControl(subSectionContainer, this._relocatedLogFileFolderOption);
// Restore database file details section
const restoreFileSectionContainer = DOM.append(fileContentElement, DOM.$('.new-section'));
this.createLabelElement(restoreFileSectionContainer, localize('restoreDatabaseFileDetails', "Restore database file details"), true);
// file list table
this._fileListTableContainer = DOM.append(restoreFileSectionContainer, DOM.$('.dialog-input-section.restore-list'));
DOM.hide(this._fileListTableContainer);
const logicalFileName = localize('logicalFileName', "Logical file Name");
const fileType = localize('fileType', "File type");
const originalFileName = localize('originalFileName', "Original File Name");
const restoreAs = localize('restoreAs', "Restore as");
const columns = [{
id: 'logicalFileName',
name: logicalFileName,
field: 'logicalFileName'
}, {
id: 'fileType',
name: fileType,
field: 'fileType'
}, {
id: 'originalFileName',
name: originalFileName,
field: 'originalFileName'
}, {
id: 'restoreAs',
name: restoreAs,
field: 'restoreAs'
}];
this._fileListData = new TableDataView<FileListElement>();
this._fileListTable = new Table<FileListElement>(this._fileListTableContainer,
{ dataProvider: this._fileListData, columns }, { enableColumnReorder: false });
this._fileListTable.setSelectionModel(new RowSelectionModel());
// Content in options tab
const optionsContentElement = DOM.$('.restore-dialog');
// Restore options section
const restoreOptions = DOM.append(optionsContentElement, DOM.$('.new-section'));
this.createLabelElement(restoreOptions, localize('restoreOptions', "Restore options"), true);
this.createOptionControl(restoreOptions, this._withReplaceDatabaseOption);
this.createOptionControl(restoreOptions, this._withKeepReplicationOption);
this.createOptionControl(restoreOptions, this._withRestrictedUserOption);
this.createOptionControl(restoreOptions, this._recoveryStateOption);
this.createOptionControl(DOM.append(restoreOptions, DOM.$('.sub-section')), this._standbyFileOption);
// Tail-Log backup section
const tailLog = DOM.append(optionsContentElement, DOM.$('.new-section'));
this.createLabelElement(tailLog, localize('taillogBackup', "Tail-Log backup"), true);
this.createOptionControl(tailLog, this._takeTaillogBackupOption);
const tailLogOptions = DOM.append(tailLog, DOM.$('.sub-section'));
this.createOptionControl(tailLogOptions, this._tailLogWithNoRecoveryOption);
this.createOptionControl(tailLogOptions, this._tailLogBackupFileOption);
// Server connections section
const serverConnections = DOM.append(optionsContentElement, DOM.$('.new-section'));
this.createLabelElement(serverConnections, localize('serverConnection', "Server connections"), true);
this.createOptionControl(serverConnections, this._closeExistingConnectionsOption);
const restorePanel = DOM.$('.restore-panel');
container.appendChild(restorePanel);
this._panel = new TabbedPanel(restorePanel);
this._generalTabId = this._panel.pushTab({
identifier: 'general',
title: localize('generalTitle', 'General'),
view: {
render: c => {
DOM.append(c, generalTab);
},
layout: () => { }
}
});
const fileTab = this._panel.pushTab({
identifier: 'fileContent',
title: localize('filesTitle', 'Files'),
view: {
layout: () => { },
render: c => {
c.appendChild(fileContentElement);
}
}
});
this._panel.pushTab({
identifier: 'options',
title: localize('optionsTitle', 'Options'),
view: {
layout: () => { },
render: c => {
c.appendChild(optionsContentElement);
}
}
});
this._panel.onTabChange(c => {
if (c === fileTab && this._fileListTable) {
this._fileListTable.resizeCanvas();
this._fileListTable.autosizeColumns();
}
if (c !== this._generalTabId) {
this._restoreFromSelectBox.hideMessage();
}
});
this._restorePlanTable.grid.onKeyDown.subscribe((e: KeyboardEvent) => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
this._destinationRestoreToInputBox.isEnabled() ? this._destinationRestoreToInputBox.focus() : this._databaseDropdown.focus();
e.stopImmediatePropagation();
} else if (event.equals(KeyCode.Tab)) {
this.focusOnFirstEnabledFooterButton();
e.stopImmediatePropagation();
}
});
this._fileListTable.grid.onKeyDown.subscribe((e: KeyboardEvent) => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
if ((<InputBox>this._optionsMap[this._relocatedLogFileFolderOption]).isEnabled()) {
(<InputBox>this._optionsMap[this._relocatedLogFileFolderOption]).focus();
} else if ((<InputBox>this._optionsMap[this._relocatedDataFileFolderOption]).isEnabled()) {
(<InputBox>this._optionsMap[this._relocatedDataFileFolderOption]).focus();
} else {
(<Checkbox>this._optionsMap[this._relocateDatabaseFilesOption]).focus();
}
e.stopImmediatePropagation();
} else if (event.equals(KeyCode.Tab)) {
this.focusOnFirstEnabledFooterButton();
e.stopImmediatePropagation();
}
});
}
private focusOnFirstEnabledFooterButton() {
if (this._scriptButton.enabled) {
this._scriptButton.focus();
} else if (this._restoreButton.enabled) {
this._restoreButton.focus();
} else {
this._closeButton.focus();
}
}
private databaseSelected(dbName: string): void {
if (this.viewModel.targetDatabaseName !== dbName) {
this.viewModel.targetDatabaseName = dbName;
this.validateRestore(false);
}
}
public set databaseListOptions(vals: string[]) {
this._databaseDropdown.values = vals;
}
private createLabelElement(container: HTMLElement, content: string, isHeader?: boolean) {
let className = 'dialog-label';
if (isHeader) {
className += ' header';
}
DOM.append(container, DOM.$(`.${className}`)).innerText = content;
}
private createOptionControl(container: HTMLElement, optionName: string): void {
const option = this.viewModel.getOptionMetadata(optionName);
let propertyWidget: SelectBox | InputBox | Checkbox;
switch (option.valueType) {
case ServiceOptionType.boolean:
propertyWidget = this.createCheckBoxHelper(container, option.description,
DialogHelper.getBooleanValueFromStringOrBoolean(option.defaultValue), () => this.onBooleanOptionChecked(optionName));
break;
case ServiceOptionType.category:
propertyWidget = this.createSelectBoxHelper(container, option.description, option.categoryValues.map(c => c.displayName), DialogHelper.getCategoryDisplayName(option.categoryValues, option.defaultValue));
this._register(attachSelectBoxStyler(propertyWidget, this._themeService));
this._register(propertyWidget.onDidSelect(selectedDatabase => {
this.onCatagoryOptionChanged(optionName);
}));
break;
case ServiceOptionType.string:
propertyWidget = this.createInputBoxHelper(container, option.description);
this._register(attachInputBoxStyler(propertyWidget, this._themeService));
this._register(propertyWidget.onLoseFocus(params => {
this.onStringOptionChanged(optionName, params);
}));
}
this._optionsMap[optionName] = propertyWidget;
}
private onBooleanOptionChecked(optionName: string) {
this.viewModel.setOptionValue(optionName, (<Checkbox>this._optionsMap[optionName]).checked);
this.validateRestore(false);
}
private onCatagoryOptionChanged(optionName: string) {
this.viewModel.setOptionValue(optionName, (<SelectBox>this._optionsMap[optionName]).value);
this.validateRestore(false);
}
private onStringOptionChanged(optionName: string, params: OnLoseFocusParams) {
if (params.hasChanged && params.value) {
this.viewModel.setOptionValue(optionName, params.value);
this.validateRestore(false);
}
}
private createCheckBoxHelper(container: HTMLElement, label: string, isChecked: boolean, onCheck: (viaKeyboard: boolean) => void): Checkbox {
const checkbox = 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;
}
private createSelectBoxHelper(container: HTMLElement, label: string, options: string[], selectedOption: string): SelectBox {
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 });
selectBox.render(inputCellContainer);
return selectBox;
}
private createInputBoxHelper(container: HTMLElement, label: string, options?: IInputOptions): InputBox {
const ariaOptions = {
ariaLabel: label
};
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));
}
private clearRestorePlanDataTable(): void {
if (this._restorePlanData.getLength() > 0) {
this._restorePlanData.clear();
DOM.hide(this._restorePlanTableContainer);
}
}
private clearFileListTable(): void {
if (this._fileListData.getLength() > 0) {
this._fileListData.clear();
DOM.hide(this._fileListTableContainer);
}
}
private resetRestoreContent(): void {
this.clearRestorePlanDataTable();
this.clearFileListTable();
this._restoreButton.enabled = false;
this._scriptButton.enabled = false;
}
public onValidateResponseFail(errorMessage: string) {
this.resetRestoreContent();
if (this.isRestoreFromDatabaseSelected) {
this._sourceDatabaseSelectBox.showMessage({ type: MessageType.ERROR, content: errorMessage });
} else {
this._sourceDatabaseSelectBox.setOptions([]);
this._filePathInputBox.showMessage({ type: MessageType.ERROR, content: errorMessage });
}
}
public removeErrorMessage() {
this._filePathInputBox.hideMessage();
this._sourceDatabaseSelectBox.hideMessage();
this._destinationRestoreToInputBox.hideMessage();
}
public enableRestoreButton(enabled: boolean) {
this.spinner = false;
this._restoreButton.enabled = enabled;
this._scriptButton.enabled = enabled;
}
public showError(errorMessage: string): void {
this.setError(errorMessage);
}
private backupFileCheckboxChanged(e: Slick.EventData, data: Slick.OnSelectedRowsChangedEventArgs<Slick.SlickData>): void {
let selectedFiles = [];
data.grid.getSelectedRows().forEach(row => {
selectedFiles.push(data.grid.getDataItem(row)['Id']);
});
let isSame = false;
if (this.viewModel.selectedBackupSets && this.viewModel.selectedBackupSets.length === selectedFiles.length) {
isSame = this.viewModel.selectedBackupSets.some(item => selectedFiles.includes(item));
}
if (!isSame) {
this.viewModel.selectedBackupSets = selectedFiles;
this.validateRestore(false, true);
}
}
private registerListeners(): void {
// Theme styler
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._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._filePathInputBox.onLoseFocus(params => {
this.onFilePathLoseFocus(params);
}));
this._browseFileButton.onDidClick(() => {
this.onFileBrowserRequested();
});
this._register(this._sourceDatabaseSelectBox.onDidSelect(selectedDatabase => {
this.onSourceDatabaseChanged(selectedDatabase.selected);
}));
this._register(this._restoreFromSelectBox.onDidSelect(selectedRestoreFrom => {
this.onRestoreFromChanged(selectedRestoreFrom.selected);
}));
}
private onFileBrowserRequested(): void {
this.fileBrowserDialogService.showDialog(this._ownerUri,
this.viewModel.defaultBackupFolder,
BackupConstants.fileFiltersSet,
FileValidationConstants.restore,
true,
filepath => this.onFileBrowsed(filepath));
}
private onFileBrowsed(filepath: string) {
const oldFilePath = this._filePathInputBox.value;
if (strings.isFalsyOrWhitespace(this._filePathInputBox.value)) {
this._filePathInputBox.value = filepath;
} else {
this._filePathInputBox.value = this._filePathInputBox.value + ', ' + filepath;
}
if (oldFilePath !== this._filePathInputBox.value) {
this.onFilePathChanged(this._filePathInputBox.value);
}
}
private onFilePathLoseFocus(params: OnLoseFocusParams) {
if (params.value) {
if (params.hasChanged || (this.viewModel.filePath !== params.value)) {
this.onFilePathChanged(params.value);
}
}
}
private onFilePathChanged(filePath: string) {
this.viewModel.filePath = filePath;
this.viewModel.selectedBackupSets = null;
this.validateRestore(true);
}
private onSourceDatabaseChanged(selectedDatabase: string) {
this.viewModel.sourceDatabaseName = selectedDatabase;
this.viewModel.selectedBackupSets = null;
this.validateRestore(true);
}
private onRestoreFromChanged(selectedRestoreFrom: string) {
this.removeErrorMessage();
if (selectedRestoreFrom === this._backupFileTitle) {
this.viewModel.onRestoreFromChanged(true);
DOM.show(this._restoreFromBackupFileElement);
} else {
this.viewModel.onRestoreFromChanged(false);
DOM.hide(this._restoreFromBackupFileElement);
}
this.resetRestoreContent();
}
private get isRestoreFromDatabaseSelected(): boolean {
return this._restoreFromSelectBox.value === this._databaseTitle;
}
public validateRestore(overwriteTargetDatabase: boolean = false, isBackupFileCheckboxChanged: boolean = false): void {
this._isBackupFileCheckboxChanged = isBackupFileCheckboxChanged;
this.spinner = true;
this._restoreButton.enabled = false;
this._scriptButton.enabled = false;
this._onValidate.fire(overwriteTargetDatabase);
}
public restore(isScriptOnly: boolean): void {
if (this._restoreButton.enabled) {
this._onRestore.fire(isScriptOnly);
}
}
public hideError() {
this.setError('');
}
/* Overwrite esapce key behavior */
protected onClose() {
this.cancel();
}
/* Overwrite enter key behavior */
protected onAccept() {
this.restore(false);
}
public cancel() {
this._onCancel.fire();
this.close();
}
public close() {
this.resetDialog();
this.hide();
this._onCloseEvent.fire();
}
private resetDialog(): void {
this.hideError();
this._restoreFromSelectBox.selectWithOptionName(this._databaseTitle);
this.onRestoreFromChanged(this._databaseTitle);
this._sourceDatabaseSelectBox.select(0);
this._panel.showTab(this._generalTabId);
this._isBackupFileCheckboxChanged = false;
this.removeErrorMessage();
this.resetRestoreContent();
}
public open(serverName: string, ownerUri: string) {
this.title = this._restoreTitle + ' - ' + serverName;
this._ownerUri = ownerUri;
this.show();
this._restoreFromSelectBox.focus();
}
protected layout(height?: number): void {
// Nothing currently laid out statically in this class
}
public dispose(): void {
super.dispose();
for (let key in this._optionsMap) {
let widget: Widget = this._optionsMap[key];
widget.dispose();
delete this._optionsMap[key];
}
}
private updateLastBackupTaken(value: string) {
this._destinationRestoreToInputBox.value = value;
}
private updateFilePath(value: string) {
this._filePathInputBox.value = value;
if (!value) {
this._filePathInputBox.hideMessage();
}
}
private updateSourceDatabaseName(databaseNamesParam: SouceDatabaseNamesParam) {
// 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[] = [];
if (this.isRestoreFromDatabaseSelected && databaseNamesParam.databaseNames
&& databaseNamesParam.databaseNames.length > 0 && databaseNamesParam.databaseNames[0] !== '') {
dbNames = [''].concat(databaseNamesParam.databaseNames);
} else {
dbNames = databaseNamesParam.databaseNames;
}
this._sourceDatabaseSelectBox.setOptions(dbNames);
this._sourceDatabaseSelectBox.selectWithOptionName(databaseNamesParam.selectedDatabase);
}
private updateTargetDatabaseName(value: string) {
this._databaseDropdown.value = value;
}
private updateRestoreOption(optionParam: RestoreOptionParam) {
const widget = this._optionsMap[optionParam.optionName];
if (widget) {
if (widget instanceof Checkbox) {
(<Checkbox>widget).checked = optionParam.value;
this.enableDisableWiget(widget, optionParam.isReadOnly);
} else if (widget instanceof SelectBox) {
(<SelectBox>widget).selectWithOptionName(optionParam.value);
this.enableDisableWiget(widget, optionParam.isReadOnly);
} else if (widget instanceof InputBox) {
(<InputBox>widget).value = optionParam.value;
this.enableDisableWiget(widget, optionParam.isReadOnly);
}
}
}
private enableDisableWiget(widget: Checkbox | SelectBox | InputBox, isReadOnly: boolean) {
if (isReadOnly) {
widget.disable();
} else {
widget.enable();
}
}
private updateRestoreDatabaseFiles(dbFiles: azdata.RestoreDatabaseFileInfo[]) {
this.clearFileListTable();
if (dbFiles && dbFiles.length > 0) {
const data = [];
for (let i = 0; i < dbFiles.length; i++) {
data[i] = {
logicalFileName: dbFiles[i].logicalFileName,
fileType: dbFiles[i].fileType,
originalFileName: dbFiles[i].originalFileName,
restoreAs: dbFiles[i].restoreAsFileName
};
}
DOM.show(this._fileListTableContainer);
this._fileListData.push(data);
// Select the first row for the table by default
this._fileListTable.setSelectedRows([0]);
this._fileListTable.setActiveCell(0, 0);
}
}
private updateBackupSetsToRestore(backupSetsToRestore: azdata.DatabaseFileInfo[]) {
if (this._isBackupFileCheckboxChanged) {
const selectedRow = [];
for (let i = 0; i < backupSetsToRestore.length; i++) {
if (backupSetsToRestore[i].isSelected) {
selectedRow.push(i);
}
}
this._restorePlanTable.setSelectedRows(selectedRow);
} else {
this.clearRestorePlanDataTable();
if (backupSetsToRestore && backupSetsToRestore.length > 0) {
if (!this._restorePlanColumn) {
const firstRow = backupSetsToRestore[0];
this._restorePlanColumn = firstRow.properties.map(item => {
return {
id: item.propertyName,
name: item.propertyDisplayName,
field: item.propertyName
};
});
const checkboxSelectColumn = new CheckboxSelectColumn({ title: this._restoreLabel, toolTip: this._restoreLabel, width: 15 });
this._restorePlanColumn.unshift(checkboxSelectColumn.getColumnDefinition());
this._restorePlanTable.columns = this._restorePlanColumn;
this._restorePlanTable.registerPlugin(checkboxSelectColumn);
this._restorePlanTable.autosizeColumns();
}
const data = [];
const selectedRow = [];
for (let i = 0; i < backupSetsToRestore.length; i++) {
const backupFile = backupSetsToRestore[i];
const newData = {};
for (let j = 0; j < backupFile.properties.length; j++) {
newData[backupFile.properties[j].propertyName] = backupFile.properties[j].propertyValueDisplayName;
}
data.push(newData);
if (backupFile.isSelected) {
selectedRow.push(i);
}
}
DOM.show(this._restorePlanTableContainer);
this._restorePlanData.push(data);
this._restorePlanTable.setSelectedRows(selectedRow);
this._restorePlanTable.setActiveCell(selectedRow[0], 0);
if (!this._isRenderedRestorePlanTable) {
this._isRenderedRestorePlanTable = true;
this._restorePlanTable.resizeCanvas();
this._restorePlanTable.autosizeColumns();
}
}
}
}
}

View File

@@ -0,0 +1,298 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper';
import * as types from 'vs/base/common/types';
import { Event, Emitter } from 'vs/base/common/event';
import { ServiceOptionType } from 'sql/workbench/api/common/sqlExtHostTypes';
export interface RestoreOptionsElement {
optionMetadata: azdata.ServiceOption;
defaultValue: any;
currentValue: any;
}
/**
* Parameters for setting the widget in the restore dialog
*/
export interface RestoreOptionParam {
optionName: string;
value: any;
isReadOnly: boolean;
}
/**
* Parameters for setting the list of source database names and selected database name in the restore dialog
*/
export interface SouceDatabaseNamesParam {
databaseNames: string[];
selectedDatabase: string;
}
/**
* View model for restore dialog
*/
export class RestoreViewModel {
public filePath: string;
public sourceDatabaseName: string;
public targetDatabaseName: string;
public lastBackupTaken: string;
public databaseList: string[];
public readHeaderFromMedia: boolean;
public selectedBackupSets: string[];
public defaultBackupFolder: string;
private _onSetLastBackupTaken = new Emitter<string>();
public onSetLastBackupTaken: Event<string> = this._onSetLastBackupTaken.event;
private _onSetfilePath = new Emitter<string>();
public onSetfilePath: Event<string> = this._onSetfilePath.event;
private _onSetSourceDatabaseNames = new Emitter<SouceDatabaseNamesParam>();
public onSetSourceDatabaseNames: Event<SouceDatabaseNamesParam> = this._onSetSourceDatabaseNames.event;
private _onSetTargetDatabaseName = new Emitter<string>();
public onSetTargetDatabaseName: Event<string> = this._onSetTargetDatabaseName.event;
private _onSetRestoreOption = new Emitter<RestoreOptionParam>();
public onSetRestoreOption: Event<RestoreOptionParam> = this._onSetRestoreOption.event;
private _onUpdateBackupSetsToRestore = new Emitter<azdata.DatabaseFileInfo[]>();
public onUpdateBackupSetsToRestore: Event<azdata.DatabaseFileInfo[]> = this._onUpdateBackupSetsToRestore.event;
private _onUpdateRestoreDatabaseFiles = new Emitter<azdata.RestoreDatabaseFileInfo[]>();
public onUpdateRestoreDatabaseFiles: Event<azdata.RestoreDatabaseFileInfo[]> = this._onUpdateRestoreDatabaseFiles.event;
private _optionsMap: { [name: string]: RestoreOptionsElement } = {};
constructor(optionsMetadata: azdata.ServiceOption[]) {
optionsMetadata.forEach(optionMetadata => {
let defaultValue = this.getDisplayValue(optionMetadata, optionMetadata.defaultValue);
this._optionsMap[optionMetadata.name] = {
optionMetadata: optionMetadata,
defaultValue: defaultValue,
currentValue: defaultValue
};
});
}
/**
* Get option display value
*/
public getDisplayValue(optionMetadata: azdata.ServiceOption, optionValue: any): any {
let displayValue: any;
switch (optionMetadata.valueType) {
case ServiceOptionType.boolean:
displayValue = DialogHelper.getBooleanValueFromStringOrBoolean(optionValue);
break;
case ServiceOptionType.category:
let optionName = optionValue;
if (!optionName && optionMetadata.categoryValues[0]) {
optionName = optionMetadata.categoryValues[0].name;
}
displayValue = DialogHelper.getCategoryDisplayName(optionMetadata.categoryValues, optionName);
break;
case ServiceOptionType.string:
displayValue = optionValue ? optionValue : '';
}
return displayValue;
}
/**
* On restore from changed set readHeaderFromMedia and reset the source database names and selected database name based on isFromBackupFile value.
*/
public onRestoreFromChanged(isFromBackupFile: boolean) {
this.readHeaderFromMedia = isFromBackupFile;
if (isFromBackupFile) {
this.updateFilePath('');
this.updateSourceDatabaseNames([], undefined);
} else {
this.updateSourceDatabaseNames(this.databaseList, this.databaseList[0]);
}
}
/**
* Get option metadata from the option map
*/
public getOptionMetadata(optionName: string): azdata.ServiceOption {
return this._optionsMap[optionName] ? this._optionsMap[optionName].optionMetadata : undefined;
}
/**
* Set current value for restore option
*/
public setOptionValue(optionName: string, value: any): void {
if (this._optionsMap[optionName]) {
this._optionsMap[optionName].currentValue = value;
}
}
/**
* Get current value for restore option
*/
public getOptionValue(optionName: string): any {
if (this._optionsMap[optionName]) {
return this._optionsMap[optionName].currentValue;
}
return undefined;
}
/**
* Get restore advanced options. Only return the options that are different from the default options
*/
public getRestoreAdvancedOptions(options: { [name: string]: any }) {
for (let key in this._optionsMap) {
let optionElement = this._optionsMap[key];
switch (optionElement.optionMetadata.valueType) {
case ServiceOptionType.boolean:
if (optionElement.currentValue !== optionElement.defaultValue) {
options[key] = optionElement.currentValue;
}
break;
case ServiceOptionType.category:
if (optionElement.currentValue !== optionElement.defaultValue) {
options[key] = DialogHelper.getCategoryName(optionElement.optionMetadata.categoryValues, optionElement.currentValue);
}
break;
case ServiceOptionType.string:
if (optionElement.currentValue && optionElement.currentValue !== optionElement.defaultValue) {
options[key] = optionElement.currentValue;
}
}
}
}
/**
* On restore plan response will update all the information from restore plan response
*/
public onRestorePlanResponse(restorePlanResponse: azdata.RestorePlanResponse): void {
if (restorePlanResponse.planDetails && restorePlanResponse.planDetails['lastBackupTaken']) {
this.updateLastBackupTaken(restorePlanResponse.planDetails['lastBackupTaken'].currentValue);
}
if (restorePlanResponse.planDetails && restorePlanResponse.planDetails['targetDatabaseName']) {
this.updateTargetDatabaseName(restorePlanResponse.planDetails['targetDatabaseName'].currentValue);
}
this._onUpdateRestoreDatabaseFiles.fire(restorePlanResponse.dbFiles);
this.updateSourceDatabaseNames(restorePlanResponse.databaseNamesFromBackupSets, restorePlanResponse.planDetails['sourceDatabaseName'].currentValue);
this.updateOptionWithPlanDetail(restorePlanResponse.planDetails);
this.updateBackupSetsToRestore(restorePlanResponse.backupSetsToRestore);
}
/**
* Update options with plan details
*/
public updateOptionWithPlanDetail(planDetails: { [key: string]: azdata.RestorePlanDetailInfo }): void {
if (planDetails) {
for (let key in planDetails) {
let optionElement = this._optionsMap[key];
if (optionElement) {
let planDetailInfo = planDetails[key];
optionElement.defaultValue = this.getDisplayValue(optionElement.optionMetadata, planDetailInfo.defaultValue);
optionElement.currentValue = this.getDisplayValue(optionElement.optionMetadata, planDetailInfo.currentValue);
this._onSetRestoreOption.fire({ optionName: key, value: this._optionsMap[key].currentValue, isReadOnly: planDetailInfo.isReadOnly });
}
}
}
}
/**
* Update options with restore config info. The option values will be both default and current values.
*/
public updateOptionWithConfigInfo(configInfo: { [key: string]: any }): void {
if (configInfo) {
if (configInfo['sourceDatabaseNamesWithBackupSets']) {
let databaseList = configInfo['sourceDatabaseNamesWithBackupSets'];
if (types.isStringArray(databaseList)) {
this.databaseList = databaseList;
this.databaseList.unshift('');
this.readHeaderFromMedia = false;
this.updateSourceDatabaseNames(this.databaseList, this.sourceDatabaseName);
}
}
if (configInfo['defaultBackupFolder']) {
this.defaultBackupFolder = configInfo['defaultBackupFolder'];
}
for (let key in configInfo) {
let optionElement = this._optionsMap[key];
if (optionElement) {
let planDetailInfo = configInfo[key];
optionElement.defaultValue = this.getDisplayValue(optionElement.optionMetadata, planDetailInfo);
optionElement.currentValue = optionElement.defaultValue;
this._onSetRestoreOption.fire({ optionName: key, value: this._optionsMap[key].currentValue, isReadOnly: true });
}
}
}
}
/**
* Update backup sets to restore
*/
public updateBackupSetsToRestore(backupSetsToRestore: azdata.DatabaseFileInfo[]): void {
this.selectedBackupSets = null;
if (backupSetsToRestore) {
this.selectedBackupSets = [];
backupSetsToRestore.forEach(backupFile => {
if (backupFile.isSelected) {
this.selectedBackupSets.push(backupFile.id);
}
});
this._onUpdateBackupSetsToRestore.fire(backupSetsToRestore);
}
}
/**
* Reset restore options to the default value
*/
public resetRestoreOptions(databaseName: string): void {
this.sourceDatabaseName = databaseName ? databaseName : '';
this.updateTargetDatabaseName(databaseName);
this.updateSourceDatabaseNames([], this.sourceDatabaseName);
this.updateFilePath('');
this.updateLastBackupTaken('');
this.databaseList = [];
this.selectedBackupSets = null;
for (let key in this._optionsMap) {
this._optionsMap[key].defaultValue = this.getDisplayValue(this._optionsMap[key].optionMetadata, this._optionsMap[key].optionMetadata.defaultValue);
this._optionsMap[key].currentValue = this._optionsMap[key].defaultValue;
this._onSetRestoreOption.fire({ optionName: key, value: this._optionsMap[key].defaultValue, isReadOnly: false });
}
}
/**
* Update last backup taken
*/
public updateLastBackupTaken(value: string) {
this.lastBackupTaken = value;
this._onSetLastBackupTaken.fire(value);
}
/**
* Update file path
*/
public updateFilePath(value: string) {
this.filePath = value;
this._onSetfilePath.fire(value);
}
/**
* Update source database names and selected database
*/
public updateSourceDatabaseNames(options: string[], selectedDatabase: string) {
this.sourceDatabaseName = selectedDatabase;
this._onSetSourceDatabaseNames.fire({ databaseNames: options, selectedDatabase: selectedDatabase });
}
/**
* Update target database name
*/
public updateTargetDatabaseName(value: string) {
this.targetDatabaseName = value;
this._onSetTargetDatabaseName.fire(value);
}
}