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

@@ -13,7 +13,7 @@ import { MainThreadModelViewDialogShape, SqlMainContext, ExtHostModelViewDialogS
import { Dialog, DialogTab, DialogButton, WizardPage, Wizard } from 'sql/platform/dialog/dialogTypes';
import { CustomDialogService } from 'sql/platform/dialog/customDialogService';
import { IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails, IModelViewWizardPageDetails, IModelViewWizardDetails } from 'sql/workbench/api/common/sqlExtHostTypes';
import { ModelViewInput, ModelViewInputModel, ModeViewSaveHandler } from 'sql/parts/modelComponents/modelEditor/modelViewInput';
import { ModelViewInput, ModelViewInputModel, ModeViewSaveHandler } from 'sql/workbench/electron-browser/modelComponents/modelViewInput';
import * as vscode from 'vscode';
import * as azdata from 'azdata';

View File

@@ -30,7 +30,7 @@ import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilit
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
import { notebookModeId } from 'sql/parts/common/customInputConverter';
import { notebookModeId } from 'sql/workbench/common/customInputConverter';
class MainThreadNotebookEditor extends Disposable {
private _contentChangedEmitter = new Emitter<NotebookContentChange>();

View File

@@ -0,0 +1,248 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EditorInput, IEditorInput } from 'vs/workbench/common/editor';
import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { URI } from 'vs/base/common/uri';
import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput';
import { QueryInput } from 'sql/parts/query/common/queryInput';
import { IQueryEditorOptions } from 'sql/workbench/services/queryEditor/common/queryEditorService';
import { QueryPlanInput } from 'sql/workbench/parts/queryPlan/common/queryPlanInput';
import { NotebookInput } from 'sql/workbench/parts/notebook/notebookInput';
import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
const fs = require('fs');
////// Exported public functions/vars
// prefix for untitled sql editors
export const untitledFilePrefix = 'SQLQuery';
// mode identifier for SQL mode
export const sqlModeId = 'sql';
export const notebookModeId = 'notebook';
/**
* Checks if the specified input is supported by one our custom input types, and if so convert it
* to that type.
* @param input The input to check for conversion
* @param options Editor options for controlling the conversion
* @param instantiationService The instantiation service to use to create the new input types
*/
export function convertEditorInput(input: EditorInput, options: IQueryEditorOptions, instantiationService: IInstantiationService): EditorInput {
let denyQueryEditor = options && options.denyQueryEditor;
if (input && !denyQueryEditor) {
//QueryInput
let uri: URI = getQueryEditorFileUri(input);
if (uri) {
const queryResultsInput: QueryResultsInput = instantiationService.createInstance(QueryResultsInput, uri.toString());
let queryInput: QueryInput = instantiationService.createInstance(QueryInput, '', input, queryResultsInput, undefined);
return queryInput;
}
//QueryPlanInput
uri = getQueryPlanEditorUri(input);
if (uri) {
let queryPlanXml: string = fs.readFileSync(uri.fsPath);
let queryPlanInput: QueryPlanInput = instantiationService.createInstance(QueryPlanInput, queryPlanXml, 'aaa', undefined);
return queryPlanInput;
}
//Notebook
uri = getNotebookEditorUri(input, instantiationService);
if (uri) {
let fileName: string = 'untitled';
if (input) {
fileName = input.getName();
}
let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, uri, input);
return notebookInput;
}
}
return input;
}
/**
* Gets the resource of the input if it's one of the ones we support.
* @param input The IEditorInput to get the resource of
*/
export function getSupportedInputResource(input: IEditorInput): URI {
if (input instanceof UntitledEditorInput) {
let untitledCast: UntitledEditorInput = <UntitledEditorInput>input;
if (untitledCast) {
return untitledCast.getResource();
}
}
if (input instanceof FileEditorInput) {
let fileCast: FileEditorInput = <FileEditorInput>input;
if (fileCast) {
return fileCast.getResource();
}
}
if (input instanceof ResourceEditorInput) {
let resourceCast: ResourceEditorInput = <ResourceEditorInput>input;
if (resourceCast) {
return resourceCast.getResource();
}
}
return undefined;
}
////// Non-Exported Private functions/vars
// file extensions for the inputs we support (should be all upper case for comparison)
const sqlFileTypes = ['SQL'];
const sqlPlanFileTypes = ['SQLPLAN'];
/**
* If input is a supported query editor file, return it's URI. Otherwise return undefined.
* @param input The EditorInput to retrieve the URI of
*/
function getQueryEditorFileUri(input: EditorInput): URI {
if (!input || !input.getName()) {
return undefined;
}
// If this editor is not already of type queryinput
if (!(input instanceof QueryInput)) {
// If this editor has a URI
let uri: URI = getSupportedInputResource(input);
if (uri) {
let isValidUri: boolean = !!uri && !!uri.toString;
if (isValidUri && (hasFileExtension(sqlFileTypes, input, true) || hasSqlFileMode(input))) {
return uri;
}
}
}
return undefined;
}
/**
* If input is a supported query plan editor file (.sqlplan), return it's URI. Otherwise return undefined.
* @param input The EditorInput to get the URI of
*/
function getQueryPlanEditorUri(input: EditorInput): URI {
if (!input || !input.getName()) {
return undefined;
}
// If this editor is not already of type queryinput
if (!(input instanceof QueryPlanInput)) {
let uri: URI = getSupportedInputResource(input);
if (uri) {
if (hasFileExtension(sqlPlanFileTypes, input, false)) {
return uri;
}
}
}
return undefined;
}
/**
* If input is a supported notebook editor file (.ipynb), return it's URI. Otherwise return undefined.
* @param input The EditorInput to get the URI of.
*/
function getNotebookEditorUri(input: EditorInput, instantiationService: IInstantiationService): URI {
if (!input || !input.getName()) {
return undefined;
}
// If this editor is not already of type notebook input
if (!(input instanceof NotebookInput)) {
let uri: URI = getSupportedInputResource(input);
if (uri) {
if (hasFileExtension(getNotebookFileExtensions(instantiationService), input, false) || hasNotebookFileMode(input)) {
return uri;
}
}
}
return undefined;
}
function getNotebookFileExtensions(instantiationService: IInstantiationService): string[] {
return withService<INotebookService, string[]>(instantiationService, INotebookService, notebookService => {
return notebookService.getSupportedFileExtensions();
});
}
/**
* Checks whether the given EditorInput is set to either undefined or notebook mode
* @param input The EditorInput to check the mode of
*/
function hasNotebookFileMode(input: EditorInput): boolean {
if (input instanceof UntitledEditorInput) {
let untitledCast: UntitledEditorInput = <UntitledEditorInput>input;
return (untitledCast && untitledCast.getModeId() === notebookModeId);
}
return false;
}
function withService<TService, TResult>(instantiationService: IInstantiationService, serviceId: ServiceIdentifier<TService>, action: (service: TService) => TResult, ): TResult {
return instantiationService.invokeFunction(accessor => {
let service = accessor.get(serviceId);
return action(service);
});
}
/**
* Checks whether the given EditorInput is set to either undefined or sql mode
* @param input The EditorInput to check the mode of
*/
function hasSqlFileMode(input: EditorInput): boolean {
if (input instanceof UntitledEditorInput) {
let untitledCast: UntitledEditorInput = <UntitledEditorInput>input;
return untitledCast && (untitledCast.getModeId() === undefined || untitledCast.getModeId() === sqlModeId);
}
return false;
}
/**
* Checks whether the name of the specified input has an extension that is
* @param extensions The extensions to check for
* @param input The input to check for the specified extensions
*/
function hasFileExtension(extensions: string[], input: EditorInput, checkUntitledFileType: boolean): boolean {
// Check the extension type
let lastPeriodIndex = input.getName().lastIndexOf('.');
if (lastPeriodIndex > -1) {
let extension: string = input.getName().substr(lastPeriodIndex + 1).toUpperCase();
return !!extensions.find(x => x === extension);
}
// Check for untitled file type
if (checkUntitledFileType && input.getName().includes(untitledFilePrefix)) {
return true;
}
// Return false if not a queryEditor file
return false;
}
// Returns file mode - notebookModeId or sqlModeId
export function getFileMode(instantiationService: IInstantiationService, resource: URI): string {
if (!resource) {
return sqlModeId;
}
return withService<INotebookService, string>(instantiationService, INotebookService, notebookService => {
for (const editor of notebookService.listNotebookEditors()) {
if (editor.notebookParams.notebookUri === resource) {
return notebookModeId;
}
}
return sqlModeId;
});
}

View File

@@ -13,7 +13,7 @@ import {
} from 'sql/platform/connection/common/connectionManagement';
import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService';
import { IScriptingService } from 'sql/platform/scripting/common/scriptingService';
import { EditDataInput } from 'sql/parts/editData/common/editDataInput';
import { EditDataInput } from 'sql/workbench/parts/editData/common/editDataInput';
import { IRestoreDialogController } from 'sql/platform/restore/common/restoreService';
import { IInsightsConfig } from 'sql/workbench/parts/dashboard/widgets/insights/interfaces';
import { IInsightsDialogService } from 'sql/workbench/services/insights/common/insightsDialogService';
@@ -21,7 +21,7 @@ import { ConnectionManagementInfo } from 'sql/platform/connection/common/connect
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
import { QueryInput } from 'sql/parts/query/common/queryInput';
import { DashboardInput } from 'sql/workbench/parts/dashboard/dashboardInput';
import { ProfilerInput } from 'sql/parts/profiler/editor/profilerInput';
import { ProfilerInput } from 'sql/workbench/parts/profiler/browser/profilerInput';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
import { IBackupUiService } from 'sql/workbench/services/backup/common/backupUiService';

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 'vs/css!./media/button';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
ViewChild, ElementRef, OnDestroy, AfterViewInit
} from '@angular/core';
import * as azdata from 'azdata';
import { ComponentWithIconBase } from 'sql/workbench/electron-browser/modelComponents/componentWithIconBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { attachButtonStyler } from 'sql/platform/theme/common/styler';
import { SIDE_BAR_BACKGROUND, SIDE_BAR_TITLE_FOREGROUND } from 'vs/workbench/common/theme';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { focusBorder, foreground } from 'vs/platform/theme/common/colorRegistry';
import { Button } from 'sql/base/browser/ui/button/button';
import { Color } from 'vs/base/common/color';
@Component({
selector: 'modelview-button',
template: `
<div>
<label for={{this.label}}>
<div #input style="width: 100%">
<input #fileInput *ngIf="this.isFile === true" id={{this.label}} type="file" accept=".sql" style="display: none">
</div>
</label>
</div>
`
})
export default class ButtonComponent extends ComponentWithIconBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _button: Button;
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
@ViewChild('fileInput', { read: ElementRef }) private _fileInputContainer: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
if (this._inputContainer) {
this._button = new Button(this._inputContainer.nativeElement);
this._register(this._button);
this._register(attachButtonStyler(this._button, this.themeService, {
buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND, buttonForeground: SIDE_BAR_TITLE_FOREGROUND
}));
this._register(this._button.onDidClick(e => {
if (this._fileInputContainer) {
const self = this;
this._fileInputContainer.nativeElement.onchange = () => {
let file = self._fileInputContainer.nativeElement.files[0];
let reader = new FileReader();
reader.onload = (e) => {
let text = (<FileReader>e.target).result;
self.fileContent = text.toString();
self.fireEvent({
eventType: ComponentEventType.onDidClick,
args: self.fileContent
});
};
reader.readAsText(file);
};
} else {
this.fireEvent({
eventType: ComponentEventType.onDidClick,
args: e
});
}
}));
}
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this._button.enabled = this.enabled;
this._button.label = this.label;
this._button.title = this.title;
if (this.width) {
this._button.setWidth(this.convertSize(this.width.toString()));
}
if (this.height) {
this._button.setWidth(this.convertSize(this.height.toString()));
}
this.updateIcon();
}
protected updateIcon() {
if (this.iconPath) {
if (!this._iconClass) {
super.updateIcon();
this._button.icon = this._iconClass + ' icon';
// Styling for icon button
this._register(attachButtonStyler(this._button, this.themeService, {
buttonBackground: Color.transparent.toString(),
buttonHoverBackground: Color.transparent.toString(),
buttonFocusOutline: focusBorder,
buttonForeground: foreground
}));
} else {
super.updateIcon();
}
}
}
// CSS-bound properties
private get label(): string {
return this.getPropertyOrDefault<azdata.ButtonProperties, string>((props) => props.label, '');
}
private set label(newValue: string) {
this.setPropertyFromUI<azdata.ButtonProperties, string>(this.setValueProperties, newValue);
}
private get isFile(): boolean {
return this.getPropertyOrDefault<azdata.ButtonProperties, boolean>((props) => props.isFile, false);
}
private set isFile(newValue: boolean) {
this.setPropertyFromUI<azdata.ButtonProperties, boolean>(this.setFileProperties, newValue);
}
private get fileContent(): string {
return this.getPropertyOrDefault<azdata.ButtonProperties, string>((props) => props.fileContent, '');
}
private set fileContent(newValue: string) {
this.setPropertyFromUI<azdata.ButtonProperties, string>(this.setFileContentProperties, newValue);
}
private setFileContentProperties(properties: azdata.ButtonProperties, fileContent: string): void {
properties.fileContent = fileContent;
}
private setValueProperties(properties: azdata.ButtonProperties, label: string): void {
properties.label = label;
}
private setFileProperties(properties: azdata.ButtonProperties, isFile: boolean): void {
properties.isFile = isFile;
}
private get title(): string {
return this.getPropertyOrDefault<azdata.ButtonProperties, string>((props) => props.title, '');
}
private set title(newValue: string) {
this.setPropertyFromUI<azdata.ButtonProperties, string>((properties, title) => { properties.title = title; }, newValue);
}
}

View File

@@ -0,0 +1,53 @@
<div *ngIf="label" [class]="getClass()" (click)="onCardClick()" (mouseover)="onCardHoverChanged($event)"
(mouseout)="onCardHoverChanged($event)" tabIndex="0">
<ng-container *ngIf="isVerticalButton || isDetailsCard">
<span *ngIf="hasStatus" class="card-status">
<div class="status-content" [style.backgroundColor]="statusColor"></div>
</span>
<span *ngIf="showRadioButton" class="selection-indicator-container">
<div *ngIf="showAsSelected" class="selection-indicator"></div>
</span>
<ng-container *ngIf="isVerticalButton">
<div class="card-vertical-button">
<div *ngIf="iconPath" class="iconContainer">
<div [class]="iconClass" [style.maxWidth]="iconWidth" [style.maxHeight]="iconHeight"></div>
</div>
<h4 class="card-label">{{label}}</h4>
<div *ngFor="let desc of descriptions">
<div class="list-item-description">{{desc}}</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="isDetailsCard">
<div class="card-content">
<h4 class="card-label">{{label}}</h4>
<p class="card-value">{{value}}</p>
<span *ngIf="actions">
<table class="model-table">
<tr *ngFor="let action of actions">
<td class="table-row">{{action.label}}</td>
<td *ngIf="action.actionTitle" class="table-row">
<a class="pointer prominent"
(click)="onDidActionClick(action)">{{action.actionTitle}}</a>
</td>
</tr>
</table>
</span>
</div>
</ng-container>
</ng-container>
<ng-container *ngIf="isListItemCard">
<div class="list-item-content">
<div>
<div [class]="iconClass">{{label}}</div>
<div *ngFor="let desc of descriptions">
<div class="list-item-description">{{desc}}</div>
</div>
<span *ngIf="showRadioButton" class="selection-indicator-container">
<div *ngIf="showAsSelected" class="selection-indicator"></div>
</span>
</div>
</div>
</ng-container>
</div>

View File

@@ -0,0 +1,187 @@
/*---------------------------------------------------------------------------------------------
* 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/card';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, OnDestroy
} from '@angular/core';
import * as azdata from 'azdata';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { ComponentWithIconBase } from 'sql/workbench/electron-browser/modelComponents/componentWithIconBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { StatusIndicator, CardProperties, ActionDescriptor } from 'sql/workbench/api/common/sqlExtHostTypes';
@Component({
templateUrl: decodeURI(require.toUrl('sql/workbench/electron-browser/modelComponents/card.component.html'))
})
export default class CardComponent extends ComponentWithIconBase implements IComponent, OnDestroy {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private backgroundColor: string;
constructor(@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService
) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
}
ngOnDestroy(): void {
this.baseDestroy();
}
private _defaultBorderColor = 'rgb(214, 214, 214)';
private _hasFocus: boolean;
public onCardClick() {
if (this.selectable) {
this.selected = !this.selected;
this._changeRef.detectChanges();
this.fireEvent({
eventType: ComponentEventType.onDidClick,
args: this.selected
});
}
}
public getBorderColor() {
if (this.selectable && this.selected || this._hasFocus) {
return 'Blue';
} else {
return this._defaultBorderColor;
}
}
public getClass(): string {
let cardClass = this.isListItemCard ? 'model-card-list-item' : 'model-card';
return (this.selectable && this.selected || this._hasFocus) ? `${cardClass} selected` :
`${cardClass} unselected`;
}
public onCardHoverChanged(event: any) {
if (this.selectable) {
this._hasFocus = event.type === 'mouseover';
this._changeRef.detectChanges();
}
}
/// IComponent implementation
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this.updateIcon();
}
public get iconClass(): string {
if (this.isListItemCard) {
return this._iconClass + ' icon' + ' list-item-icon';
}
else {
return this._iconClass + ' icon' + ' cardIcon';
}
}
private get selectable(): boolean {
return this.enabled && (this.cardType === 'VerticalButton' || this.cardType === 'ListItem');
}
// CSS-bound properties
public get label(): string {
return this.getPropertyOrDefault<CardProperties, string>((props) => props.label, '');
}
public get value(): string {
return this.getPropertyOrDefault<CardProperties, string>((props) => props.value, '');
}
public get cardType(): string {
return this.getPropertyOrDefault<CardProperties, string>((props) => props.cardType, 'Details');
}
public get selected(): boolean {
return this.getPropertyOrDefault<azdata.CardProperties, boolean>((props) => props.selected, false);
}
public set selected(newValue: boolean) {
this.setPropertyFromUI<azdata.CardProperties, boolean>((props, value) => props.selected = value, newValue);
}
public get isDetailsCard(): boolean {
return !this.cardType || this.cardType === 'Details';
}
public get isListItemCard(): boolean {
return !this.cardType || this.cardType === 'ListItem';
}
public get isVerticalButton(): boolean {
return this.cardType === 'VerticalButton';
}
public get showRadioButton(): boolean {
return this.selectable && (this.selected || this._hasFocus);
}
public get showAsSelected(): boolean {
return this.selectable && this.selected;
}
public get descriptions(): string[] {
return this.getPropertyOrDefault<CardProperties, string[]>((props) => props.descriptions, []);
}
public get actions(): ActionDescriptor[] {
return this.getPropertyOrDefault<CardProperties, ActionDescriptor[]>((props) => props.actions, []);
}
public hasStatus(): boolean {
let status = this.getPropertyOrDefault<CardProperties, StatusIndicator>((props) => props.status, StatusIndicator.None);
return status !== StatusIndicator.None;
}
public get statusColor(): string {
let status = this.getPropertyOrDefault<CardProperties, StatusIndicator>((props) => props.status, StatusIndicator.None);
switch (status) {
case StatusIndicator.Ok:
return 'green';
case StatusIndicator.Warning:
return 'orange';
case StatusIndicator.Error:
return 'red';
default:
return this.backgroundColor;
}
}
private updateTheme(theme: IColorTheme) {
this.backgroundColor = theme.getColor(colors.editorBackground, true).toString();
this._changeRef.detectChanges();
}
private onDidActionClick(action: ActionDescriptor): void {
this.fireEvent({
eventType: ComponentEventType.onDidClick,
args: action
});
}
}

View File

@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
ViewChild, ElementRef, OnDestroy, AfterViewInit
} from '@angular/core';
import * as azdata from 'azdata';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { Checkbox, ICheckboxOptions } from 'sql/base/browser/ui/checkbox/checkbox';
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
import { attachCheckboxStyler } from 'sql/platform/theme/common/styler';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
@Component({
selector: 'modelview-checkbox',
template: `
<div #input [style.width]="getWidth()"></div>
`
})
export default class CheckBoxComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _input: Checkbox;
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(forwardRef(() => ElementRef)) el: ElementRef, ) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
if (this._inputContainer) {
let inputOptions: ICheckboxOptions = {
label: ''
};
this._input = new Checkbox(this._inputContainer.nativeElement, inputOptions);
this._register(this._input);
this._register(this._input.onChange(e => {
this.checked = this._input.checked;
this.fireEvent({
eventType: ComponentEventType.onDidChange,
args: e
});
}));
this._register(attachCheckboxStyler(this._input, this.themeService));
}
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this._input.checked = this.checked;
this._input.label = this.label;
if (this.enabled) {
this._input.enable();
} else {
this._input.disable();
}
}
// CSS-bound properties
public get checked(): boolean {
return this.getPropertyOrDefault<azdata.CheckBoxProperties, boolean>((props) => props.checked, false);
}
public set checked(newValue: boolean) {
this.setPropertyFromUI<azdata.CheckBoxProperties, boolean>((properties, value) => { properties.checked = value; }, newValue);
}
private get label(): string {
return this.getPropertyOrDefault<azdata.CheckBoxProperties, string>((props) => props.label, '');
}
private set label(newValue: string) {
this.setPropertyFromUI<azdata.CheckBoxProperties, string>((properties, label) => { properties.label = label; }, newValue);
}
}

View File

@@ -0,0 +1,325 @@
/*---------------------------------------------------------------------------------------------
* 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/flexContainer';
import {
ChangeDetectorRef, ViewChildren, ElementRef, OnDestroy, OnInit, QueryList
} from '@angular/core';
import * as types from 'vs/base/common/types';
import { IComponent, IComponentDescriptor, IModelStore, IComponentEventArgs, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import * as azdata from 'azdata';
import { Emitter } from 'vs/base/common/event';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { ModelComponentWrapper } from 'sql/workbench/electron-browser/modelComponents/modelComponentWrapper.component';
import { URI } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
export type IUserFriendlyIcon = string | URI | { light: string | URI; dark: string | URI };
export class ItemDescriptor<T> {
constructor(public descriptor: IComponentDescriptor, public config: T) { }
}
export abstract class ComponentBase extends Disposable implements IComponent, OnDestroy, OnInit {
protected properties: { [key: string]: any; } = {};
private _valid: boolean = true;
protected _validations: (() => boolean | Thenable<boolean>)[] = [];
private _eventQueue: IComponentEventArgs[] = [];
private _CSSStyles: { [key: string]: string } = {};
constructor(
protected _changeRef: ChangeDetectorRef,
protected _el: ElementRef) {
super();
}
/// IComponent implementation
abstract descriptor: IComponentDescriptor;
abstract modelStore: IModelStore;
protected _onEventEmitter = new Emitter<IComponentEventArgs>();
public layout(): void {
if (!this._changeRef['destroyed']) {
this._changeRef.detectChanges();
}
}
protected baseInit(): void {
if (this.modelStore) {
this.modelStore.registerComponent(this);
this._validations.push(() => this.modelStore.validate(this));
}
}
abstract ngOnInit(): void;
protected baseDestroy(): void {
if (this.modelStore) {
this.modelStore.unregisterComponent(this);
}
this.dispose();
}
ngOnDestroy(): void {
this.dispose();
}
abstract setLayout(layout: any): void;
getHtml(): any {
return this._el.nativeElement;
}
public setDataProvider(handle: number, componentId: string, context: any): void {
}
public refreshDataProvider(item: any): void {
}
public updateStyles() {
const element = (<HTMLElement>this._el.nativeElement);
for (const style in this.CSSStyles) {
element.style[style] = this.CSSStyles[style];
}
}
public setProperties(properties: { [key: string]: any; }): void {
if (!properties) {
this.properties = {};
}
this.properties = properties;
if (this.CSSStyles !== this._CSSStyles) {
this.updateStyles();
}
this.layout();
this.validate();
}
// Helper Function to update single property
public updateProperty(key: string, value: any): void {
if (key) {
this.properties[key] = value;
if (this.CSSStyles !== this._CSSStyles) {
this.updateStyles();
}
this.layout();
this.validate();
}
}
protected getProperties<TPropertyBag>(): TPropertyBag {
return this.properties as TPropertyBag;
}
protected getPropertyOrDefault<TPropertyBag, TValue>(propertyGetter: (TPropertyBag) => TValue, defaultVal: TValue) {
let property = propertyGetter(this.getProperties<TPropertyBag>());
return types.isUndefinedOrNull(property) ? defaultVal : property;
}
protected setPropertyFromUI<TPropertyBag, TValue>(propertySetter: (TPropertyBag, TValue) => void, value: TValue) {
propertySetter(this.getProperties<TPropertyBag>(), value);
this.fireEvent({
eventType: ComponentEventType.PropertiesChanged,
args: this.getProperties()
});
this.validate();
}
public get enabled(): boolean {
let properties = this.getProperties();
let enabled = properties['enabled'];
if (enabled === undefined) {
enabled = true;
properties['enabled'] = enabled;
}
return <boolean>enabled;
}
public set enabled(value: boolean) {
let properties = this.getProperties();
properties['enabled'] = value;
this.setProperties(properties);
}
public get height(): number | string {
return this.getPropertyOrDefault<azdata.ComponentProperties, number | string>((props) => props.height, undefined);
}
public set height(newValue: number | string) {
this.setPropertyFromUI<azdata.ComponentProperties, number | string>((props, value) => props.height = value, newValue);
}
public get width(): number | string {
return this.getPropertyOrDefault<azdata.ComponentProperties, number | string>((props) => props.width, undefined);
}
public set width(newValue: number | string) {
this.setPropertyFromUI<azdata.ComponentProperties, number | string>((props, value) => props.width = value, newValue);
}
public get position(): string {
return this.getPropertyOrDefault<azdata.ComponentProperties, string>((props) => props.position, '');
}
public set position(newValue: string) {
this.setPropertyFromUI<azdata.ComponentProperties, string>((properties, position) => { properties.position = position; }, newValue);
}
public get CSSStyles(): { [key: string]: string } {
return this.getPropertyOrDefault<azdata.ComponentProperties, { [key: string]: string }>((props) => props.CSSStyles, {});
}
public set CSSStyles(newValue: { [key: string]: string }) {
this.setPropertyFromUI<azdata.ComponentProperties, { [key: string]: string }>((properties, CSSStyles) => { properties.CSSStyles = CSSStyles; }, newValue);
}
public convertSizeToNumber(size: number | string): number {
if (size && typeof (size) === 'string') {
if (size.toLowerCase().endsWith('px')) {
return +size.replace('px', '');
} else if (size.toLowerCase().endsWith('em')) {
return +size.replace('em', '') * 11;
}
} else if (!size) {
return 0;
}
return +size;
}
protected getWidth(): string {
return this.width ? this.convertSize(this.width) : '';
}
protected getHeight(): string {
return this.height ? this.convertSize(this.height) : '';
}
public convertSize(size: number | string, defaultValue?: string): string {
defaultValue = defaultValue || '';
if (types.isUndefinedOrNull(size)) {
return defaultValue;
}
let convertedSize: string = size ? size.toString() : defaultValue;
if (!convertedSize.toLowerCase().endsWith('px') && !convertedSize.toLowerCase().endsWith('%')) {
convertedSize = convertedSize + 'px';
}
return convertedSize;
}
public get valid(): boolean {
return this._valid;
}
public registerEventHandler(handler: (event: IComponentEventArgs) => void): IDisposable {
if (this._eventQueue) {
while (this._eventQueue.length > 0) {
let event = this._eventQueue.pop();
handler(event);
}
this._eventQueue = undefined;
}
return this._onEventEmitter.event(handler);
}
protected fireEvent(event: IComponentEventArgs) {
this._onEventEmitter.fire(event);
if (this._eventQueue) {
this._eventQueue.push(event);
}
}
public validate(): Thenable<boolean> {
let validations = this._validations.map(validation => Promise.resolve(validation()));
return Promise.all(validations).then(values => {
let isValid = values.every(value => value === true);
if (this._valid !== isValid) {
this._valid = isValid;
this.fireEvent({
eventType: ComponentEventType.validityChanged,
args: this._valid
});
}
return isValid;
});
}
}
export abstract class ContainerBase<T> extends ComponentBase {
protected items: ItemDescriptor<T>[];
@ViewChildren(ModelComponentWrapper) protected _componentWrappers: QueryList<ModelComponentWrapper>;
constructor(
_changeRef: ChangeDetectorRef,
_el: ElementRef
) {
super(_changeRef, _el);
this.items = [];
this._validations.push(() => this.items.every(item => {
return this.modelStore.getComponent(item.descriptor.id).valid;
}));
}
/// IComponent container-related implementation
public addToContainer(componentDescriptor: IComponentDescriptor, config: any, index?: number): void {
if (this.items.some(item => item.descriptor.id === componentDescriptor.id && item.descriptor.type === componentDescriptor.type)) {
return;
}
if (index !== undefined && index !== null && index >= 0 && index < this.items.length) {
this.items.splice(index, 0, new ItemDescriptor(componentDescriptor, config));
} else if (!index) {
this.items.push(new ItemDescriptor(componentDescriptor, config));
} else {
throw new Error(nls.localize('invalidIndex', 'The index is invalid.'));
}
this.modelStore.eventuallyRunOnComponent(componentDescriptor.id, component => component.registerEventHandler(event => {
if (event.eventType === ComponentEventType.validityChanged) {
this.validate();
}
}));
this._changeRef.detectChanges();
return;
}
public removeFromContainer(componentDescriptor: IComponentDescriptor): boolean {
let index = this.items.findIndex(item => item.descriptor.id === componentDescriptor.id && item.descriptor.type === componentDescriptor.type);
if (index >= 0) {
this.items.splice(index, 1);
this._changeRef.detectChanges();
return true;
}
return false;
}
public clearContainer(): void {
this.items = [];
this._changeRef.detectChanges();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this.items.forEach(item => {
let component = this.modelStore.getComponent(item.descriptor.id);
if (component) {
component.enabled = this.enabled;
}
});
}
public layout(): void {
if (this._componentWrappers) {
this._componentWrappers.forEach(wrapper => {
wrapper.layout();
});
}
super.layout();
}
abstract setLayout(layout: any): void;
}

View File

@@ -0,0 +1,106 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ChangeDetectorRef, ElementRef } from '@angular/core';
import { IComponentDescriptor } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import * as azdata from 'azdata';
import { URI } from 'vs/base/common/uri';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { createCSSRule, removeCSSRulesContainingSelector } from 'vs/base/browser/dom';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
export type IUserFriendlyIcon = string | URI | { light: string | URI; dark: string | URI };
export class ItemDescriptor<T> {
constructor(public descriptor: IComponentDescriptor, public config: T) { }
}
const ids = new IdGenerator('model-view-component-icon-');
export abstract class ComponentWithIconBase extends ComponentBase {
protected _iconClass: string;
protected _iconPath: IUserFriendlyIcon;
constructor(
changeRef: ChangeDetectorRef,
el: ElementRef, ) {
super(changeRef, el);
}
/// IComponent implementation
public get iconClass(): string {
return this._iconClass + ' icon';
}
protected updateIcon() {
if (this.iconPath && this.iconPath !== this._iconPath) {
this._iconPath = this.iconPath;
if (!this._iconClass) {
this._iconClass = ids.nextId();
}
removeCSSRulesContainingSelector(this._iconClass);
const icon = this.getLightIconPath(this.iconPath);
const iconDark = this.getDarkIconPath(this.iconPath) || icon;
createCSSRule(`.icon.${this._iconClass}`, `background-image: url("${icon}")`);
createCSSRule(`.vs-dark .icon.${this._iconClass}, .hc-black .icon.${this._iconClass}`, `background-image: url("${iconDark}")`);
this._changeRef.detectChanges();
}
}
private getLightIconPath(iconPath: IUserFriendlyIcon): string {
if (iconPath && iconPath['light']) {
return this.getIconPath(iconPath['light']);
} else {
return this.getIconPath(<string | URI>iconPath);
}
}
private getDarkIconPath(iconPath: IUserFriendlyIcon): string {
if (iconPath && iconPath['dark']) {
return this.getIconPath(iconPath['dark']);
}
return null;
}
private getIconPath(iconPath: string | URI): string {
if (typeof iconPath === 'string') {
return URI.file(iconPath).toString();
} else {
let uri = URI.revive(iconPath);
return uri.toString();
}
}
public getIconWidth(): string {
return this.convertSize(this.iconWidth, '40px');
}
public getIconHeight(): string {
return this.convertSize(this.iconHeight, '40px');
}
public get iconPath(): string | URI | { light: string | URI; dark: string | URI } {
return this.getPropertyOrDefault<azdata.ComponentWithIcon, IUserFriendlyIcon>((props) => props.iconPath, undefined);
}
public get iconHeight(): number | string {
return this.getPropertyOrDefault<azdata.ComponentWithIcon, number | string>((props) => props.iconHeight, '50px');
}
public get iconWidth(): number | string {
return this.getPropertyOrDefault<azdata.ComponentWithIcon, number | string>((props) => props.iconWidth, '50px');
}
ngOnDestroy(): void {
if (this._iconClass) {
removeCSSRulesContainingSelector(this._iconClass);
}
super.ngOnDestroy();
}
}

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 DivContainer from './divContainer.component';
import FlexContainer from './flexContainer.component';
import FormContainer from './formContainer.component';
import ToolbarContainer from './toolbarContainer.component';
import GroupContainer from './groupContainer.component';
import CardComponent from './card.component';
import InputBoxComponent from './inputbox.component';
import DropDownComponent from './dropdown.component';
import DeclarativeTableComponent from './declarativeTable.component';
import ListBoxComponent from './listbox.component';
import ButtonComponent from './button.component';
import CheckBoxComponent from './checkbox.component';
import TreeComponent from './tree.component';
import RadioButtonComponent from './radioButton.component';
import WebViewComponent from './webview.component';
import TableComponent from './table.component';
import TextComponent from './text.component';
import LoadingComponent from './loadingComponent.component';
import FileBrowserTreeComponent from './fileBrowserTree.component';
import EditorComponent from './editor.component';
import DiffEditorComponent from './diffeditor.component';
import DomComponent from './dom.component';
import { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry';
import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes';
import HyperlinkComponent from 'sql/workbench/electron-browser/modelComponents/hyperlink.component';
import SplitViewContainer from 'sql/workbench/electron-browser/modelComponents/splitviewContainer.component';
export const DIV_CONTAINER = 'div-container';
registerComponentType(DIV_CONTAINER, ModelComponentTypes.DivContainer, DivContainer);
export const FLEX_CONTAINER = 'flex-container';
registerComponentType(FLEX_CONTAINER, ModelComponentTypes.FlexContainer, FlexContainer);
export const SPLITVIEW_CONTAINER = 'splitView-container';
registerComponentType(SPLITVIEW_CONTAINER, ModelComponentTypes.SplitViewContainer, SplitViewContainer);
export const FORM_CONTAINER = 'form-container';
registerComponentType(FORM_CONTAINER, ModelComponentTypes.Form, FormContainer);
export const TOOLBAR_CONTAINER = 'toolbar-container';
registerComponentType(TOOLBAR_CONTAINER, ModelComponentTypes.Toolbar, ToolbarContainer);
export const GROUP_CONTAINER = 'group-container';
registerComponentType(GROUP_CONTAINER, ModelComponentTypes.Group, GroupContainer);
export const CARD_COMPONENT = 'card-component';
registerComponentType(CARD_COMPONENT, ModelComponentTypes.Card, CardComponent);
export const INPUTBOX_COMPONENT = 'inputbox-component';
registerComponentType(INPUTBOX_COMPONENT, ModelComponentTypes.InputBox, InputBoxComponent);
export const DROPDOWN_COMPONENT = 'dropdown-component';
registerComponentType(DROPDOWN_COMPONENT, ModelComponentTypes.DropDown, DropDownComponent);
export const DECLARATIVETABLE_COMPONENT = 'declarativeTable-component';
registerComponentType(DECLARATIVETABLE_COMPONENT, ModelComponentTypes.DeclarativeTable, DeclarativeTableComponent);
export const LISTBOX_COMPONENT = 'lisbox-component';
registerComponentType(LISTBOX_COMPONENT, ModelComponentTypes.ListBox, ListBoxComponent);
export const BUTTON_COMPONENT = 'button-component';
registerComponentType(BUTTON_COMPONENT, ModelComponentTypes.Button, ButtonComponent);
export const CHECKBOX_COMPONENT = 'checkbox-component';
registerComponentType(CHECKBOX_COMPONENT, ModelComponentTypes.CheckBox, CheckBoxComponent);
export const RADIOBUTTON_COMPONENT = 'radiobutton-component';
registerComponentType(RADIOBUTTON_COMPONENT, ModelComponentTypes.RadioButton, RadioButtonComponent);
export const WEBVIEW_COMPONENT = 'webview-component';
registerComponentType(WEBVIEW_COMPONENT, ModelComponentTypes.WebView, WebViewComponent);
export const TEXT_COMPONENT = 'text-component';
registerComponentType(TEXT_COMPONENT, ModelComponentTypes.Text, TextComponent);
export const TABLE_COMPONENT = 'table-component';
registerComponentType(TABLE_COMPONENT, ModelComponentTypes.Table, TableComponent);
export const LOADING_COMPONENT = 'loading-component';
registerComponentType(LOADING_COMPONENT, ModelComponentTypes.LoadingComponent, LoadingComponent);
export const TREE_COMPONENT = 'tree-component';
registerComponentType(TREE_COMPONENT, ModelComponentTypes.TreeComponent, TreeComponent);
export const FILEBROWSERTREE_COMPONENT = 'filebrowsertree-component';
registerComponentType(FILEBROWSERTREE_COMPONENT, ModelComponentTypes.FileBrowserTree, FileBrowserTreeComponent);
export const EDITOR_COMPONENT = 'editor-component';
registerComponentType(EDITOR_COMPONENT, ModelComponentTypes.Editor, EditorComponent);
export const DIFF_EDITOR_COMPONENT = 'diff-editor-component';
registerComponentType(DIFF_EDITOR_COMPONENT, ModelComponentTypes.DiffEditor, DiffEditorComponent);
export const DOM_COMPONENT = 'dom-component';
registerComponentType(DOM_COMPONENT, ModelComponentTypes.Dom, DomComponent);
export const HYPERLINK_COMPONENT = 'hyperlink-component';
registerComponentType(HYPERLINK_COMPONENT, ModelComponentTypes.Hyperlink, HyperlinkComponent);

View File

@@ -0,0 +1,208 @@
/*---------------------------------------------------------------------------------------------
* 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/declarativeTable';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ViewChild, ElementRef, OnDestroy, AfterViewInit
} from '@angular/core';
import * as azdata from 'azdata';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { ISelectData } from 'vs/base/browser/ui/selectBox/selectBox';
export enum DeclarativeDataType {
string = 'string',
category = 'category',
boolean = 'boolean',
editableCategory = 'editableCategory'
}
@Component({
selector: 'modelview-declarativeTable',
template: `
<table role=grid aria-labelledby="ID_REF" #container *ngIf="columns" class="declarative-table" [style.height]="getHeight()">
<thead>
<ng-container *ngFor="let column of columns;let h = index">
<th class="declarative-table-header" tabindex="-1" role="button" aria-sort="none">{{column.displayName}}</th>
</ng-container>
</thead>
<ng-container *ngIf="data">
<ng-container *ngFor="let row of data;let r = index">
<tr class="declarative-table-row" >
<ng-container *ngFor="let cellData of row;let c = index">
<td class="declarative-table-cell" tabindex="-1" role="button" [style.width]="getColumnWidth(c)">
<checkbox *ngIf="isCheckBox(c)" label="" (onChange)="onCheckBoxChanged($event,r,c)" [enabled]="isControlEnabled(c)" [checked]="isChecked(r,c)"></checkbox>
<select-box *ngIf="isSelectBox(c)" [options]="GetOptions(c)" (onDidSelect)="onSelectBoxChanged($event,r,c)" [selectedOption]="GetSelectedOptionDisplayName(r,c)"></select-box>
<editable-select-box *ngIf="isEditableSelectBox(c)" [options]="GetOptions(c)" (onDidSelect)="onSelectBoxChanged($event,r,c)" [selectedOption]="GetSelectedOptionDisplayName(r,c)"></editable-select-box>
<input-box *ngIf="isInputBox(c)" [value]="cellData" (onDidChange)="onInputBoxChanged($event,r,c)"></input-box>
<ng-container *ngIf="isLabel(c)" >{{cellData}}</ng-container>
</td>
</ng-container>
</tr>
</ng-container>
</ng-container>
</table>
`
})
export default class DeclarativeTableComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
@ViewChild('container', { read: ElementRef }) private _tableContainer: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
}
public validate(): Thenable<boolean> {
return super.validate().then(valid => {
return valid;
});
}
ngOnDestroy(): void {
this.baseDestroy();
}
private isCheckBox(cell: number): boolean {
let column: azdata.DeclarativeTableColumn = this.columns[cell];
return column.valueType === DeclarativeDataType.boolean;
}
private isControlEnabled(cell: number): boolean {
let column: azdata.DeclarativeTableColumn = this.columns[cell];
return !column.isReadOnly;
}
private isLabel(cell: number): boolean {
let column: azdata.DeclarativeTableColumn = this.columns[cell];
return column.isReadOnly && column.valueType === DeclarativeDataType.string;
}
private isChecked(row: number, cell: number): boolean {
let cellData = this.data[row][cell];
return cellData;
}
private onInputBoxChanged(e: string, row: number, cell: number): void {
this.onCellDataChanged(e, row, cell);
}
private onCheckBoxChanged(e: boolean, row: number, cell: number): void {
this.onCellDataChanged(e, row, cell);
}
private onSelectBoxChanged(e: ISelectData | string, row: number, cell: number): void {
let column: azdata.DeclarativeTableColumn = this.columns[cell];
if (column.categoryValues) {
if (typeof e === 'string') {
let category = column.categoryValues.find(c => c.displayName === e);
if (category) {
this.onCellDataChanged(category.name, row, cell);
} else {
this.onCellDataChanged(e, row, cell);
}
} else {
this.onCellDataChanged(column.categoryValues[e.index].name, row, cell);
}
}
}
private onCellDataChanged(newValue: any, row: number, cell: number): void {
this.data[row][cell] = newValue;
this.data = this.data;
let newCellData: azdata.TableCell = {
row: row,
column: cell,
value: newValue
};
this.fireEvent({
eventType: ComponentEventType.onDidChange,
args: newCellData
});
}
private isSelectBox(cell: number): boolean {
let column: azdata.DeclarativeTableColumn = this.columns[cell];
return column.valueType === DeclarativeDataType.category;
}
private isEditableSelectBox(cell: number): boolean {
let column: azdata.DeclarativeTableColumn = this.columns[cell];
return column.valueType === DeclarativeDataType.editableCategory;
}
private isInputBox(cell: number): boolean {
let column: azdata.DeclarativeTableColumn = this.columns[cell];
return column.valueType === DeclarativeDataType.string && !column.isReadOnly;
}
private getColumnWidth(cell: number): string {
let column: azdata.DeclarativeTableColumn = this.columns[cell];
return this.convertSize(column.width, '30px');
}
private GetOptions(cell: number): string[] {
let column: azdata.DeclarativeTableColumn = this.columns[cell];
return column.categoryValues ? column.categoryValues.map(x => x.displayName) : [];
}
private GetSelectedOptionDisplayName(row: number, cell: number): string {
let column: azdata.DeclarativeTableColumn = this.columns[cell];
let cellData = this.data[row][cell];
if (cellData && column.categoryValues) {
let category = column.categoryValues.find(v => v.name === cellData);
if (category) {
return category.displayName;
} else if (this.isEditableSelectBox(cell)) {
return cellData;
} else {
return undefined;
}
} else {
return '';
}
}
/// IComponent implementation
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
}
public get data(): any[][] {
return this.getPropertyOrDefault<azdata.DeclarativeTableProperties, any[]>((props) => props.data, []);
}
public set data(newValue: any[][]) {
this.setPropertyFromUI<azdata.DeclarativeTableProperties, any[][]>((props, value) => props.data = value, newValue);
}
public get columns(): azdata.DeclarativeTableColumn[] {
return this.getPropertyOrDefault<azdata.DeclarativeTableProperties, azdata.DeclarativeTableColumn[]>((props) => props.columns, []);
}
public set columns(newValue: azdata.DeclarativeTableColumn[]) {
this.setPropertyFromUI<azdata.DeclarativeTableProperties, azdata.DeclarativeTableColumn[]>((props, value) => props.columns = value, newValue);
}
}

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/editor';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, OnDestroy
} from '@angular/core';
import * as azdata from 'azdata';
import * as DOM from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { SimpleProgressService } from 'vs/editor/standalone/browser/simpleServices';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
@Component({
template: `
<div *ngIf="_title">
<div class="modelview-diff-editor-title" style="width: 100%; height:100%; padding-left:3px !important; border: 1px solid #BFBDBD;">
{{_title}}
</div>
</div>`,
selector: 'modelview-diff-editor-component'
})
export default class DiffEditorComponent extends ComponentBase implements IComponent, OnDestroy {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _editor: TextDiffEditor;
private _editorInput: DiffEditorInput;
private _editorModel: TextDiffEditorModel;
private _renderedContentLeft: string;
private _renderedContentRight: string;
private _languageMode: string;
private _isAutoResizable: boolean;
private _minimumHeight: number;
private _instancetiationService: IInstantiationService;
protected _title: string;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
@Inject(IModelService) private _modelService: IModelService,
@Inject(IModeService) private _modeService: IModeService
) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
this._createEditor();
this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, e => {
this.layout();
}));
}
private _createEditor(): void {
this._instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleProgressService()]));
this._editor = this._instantiationService.createInstance(TextDiffEditor);
this._editor.reverseColoring();
this._editor.create(this._el.nativeElement);
this._editor.setVisible(true);
let uri1 = this.createUri('source');
this.editorUriLeft = uri1.toString();
let uri2 = this.createUri('target');
this.editorUriRight = uri2.toString();
let cancellationTokenSource = new CancellationTokenSource();
let editorinput1 = this._instantiationService.createInstance(UntitledEditorInput, uri1, false, 'plaintext', '', '');
let editorinput2 = this._instantiationService.createInstance(UntitledEditorInput, uri2, false, 'plaintext', '', '');
this._editorInput = this._instantiationService.createInstance(DiffEditorInput, 'MyEditor', 'My description', editorinput1, editorinput2, true);
this._editor.setInput(this._editorInput, undefined, cancellationTokenSource.token);
this._editorInput.resolve().then(model => {
this._editorModel = model as TextDiffEditorModel;
this.updateModel();
this.layout();
this.validate();
});
this._register(this._editor);
this._register(this._editorInput);
this._register(this._editorModel);
}
private createUri(input: string): URI {
let uri = URI.from({ scheme: Schemas.untitled, path: `${this.descriptor.type}-${this.descriptor.id}-${input}` });
return uri;
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public layout(): void {
let width: number = this.convertSizeToNumber(this.width);
let height: number = this.convertSizeToNumber(this.height);
if (this._isAutoResizable) {
height = Math.max(this._editor.maximumHeight, this._minimumHeight ? this._minimumHeight : 0);
}
this._editor.layout(new DOM.Dimension(
width && width > 0 ? width : DOM.getContentWidth(this._el.nativeElement),
height && height > 0 ? height : DOM.getContentHeight(this._el.nativeElement)));
let element = <HTMLElement>this._el.nativeElement;
element.style.position = this.position;
super.layout();
}
/// Editor Functions
private updateModel() {
if (this._editorModel) {
this._renderedContentLeft = this.contentLeft;
this._renderedContentRight = this.contentRight;
this._modelService.updateModel(this._editorModel.originalModel.textEditorModel, this._renderedContentLeft);
this._modelService.updateModel(this._editorModel.modifiedModel.textEditorModel, this._renderedContentRight);
}
}
private updateLanguageMode() {
if (this._editorModel && this._editor) {
this._languageMode = this.languageMode;
let languageSelection = this._modeService.create(this._languageMode);
this._modelService.setMode(this._editorModel.originalModel.textEditorModel, languageSelection);
this._modelService.setMode(this._editorModel.modifiedModel.textEditorModel, languageSelection);
}
}
/// IComponent implementation
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
if (this.contentLeft !== this._renderedContentLeft || this.contentRight !== this._renderedContentRight) {
this.updateModel();
}
if (this.languageMode !== this._languageMode) {
this.updateLanguageMode();
}
this._isAutoResizable = this.isAutoResizable;
this._minimumHeight = this.minimumHeight;
this._title = this.title;
this.layout();
this.validate();
}
// CSS-bound properties
public get contentLeft(): string {
return this.getPropertyOrDefault<azdata.EditorProperties, string>((props) => props.contentLeft, undefined);
}
public set contentLeft(newValue: string) {
this.setPropertyFromUI<azdata.EditorProperties, string>((properties, contentLeft) => { properties.contentLeft = contentLeft; }, newValue);
}
public get contentRight(): string {
return this.getPropertyOrDefault<azdata.EditorProperties, string>((props) => props.contentRight, undefined);
}
public set contentRight(newValue: string) {
this.setPropertyFromUI<azdata.EditorProperties, string>((properties, contentRight) => { properties.contentRight = contentRight; }, newValue);
}
public get languageMode(): string {
return this.getPropertyOrDefault<azdata.EditorProperties, string>((props) => props.languageMode, undefined);
}
public set languageMode(newValue: string) {
this.setPropertyFromUI<azdata.EditorProperties, string>((properties, languageMode) => { properties.languageMode = languageMode; }, newValue);
}
public get isAutoResizable(): boolean {
return this.getPropertyOrDefault<azdata.EditorProperties, boolean>((props) => props.isAutoResizable, false);
}
public set isAutoResizable(newValue: boolean) {
this.setPropertyFromUI<azdata.EditorProperties, boolean>((properties, isAutoResizable) => { properties.isAutoResizable = isAutoResizable; }, newValue);
}
public get minimumHeight(): number {
return this.getPropertyOrDefault<azdata.EditorProperties, number>((props) => props.minimumHeight, this._editor.minimumHeight);
}
public set minimumHeight(newValue: number) {
this.setPropertyFromUI<azdata.EditorProperties, number>((properties, minimumHeight) => { properties.minimumHeight = minimumHeight; }, newValue);
}
public get editorUriLeft(): string {
return this.getPropertyOrDefault<azdata.EditorProperties, string>((props) => props.editorUriLeft, '');
}
public set editorUriLeft(newValue: string) {
this.setPropertyFromUI<azdata.EditorProperties, string>((properties, editorUriLeft) => { properties.editorUriLeft = editorUriLeft; }, newValue);
}
public get editorUriRight(): string {
return this.getPropertyOrDefault<azdata.EditorProperties, string>((props) => props.editorUriRight, '');
}
public set editorUriRight(newValue: string) {
this.setPropertyFromUI<azdata.EditorProperties, string>((properties, editorUriRight) => { properties.editorUriRight = editorUriRight; }, newValue);
}
public get title(): string {
return this.getPropertyOrDefault<azdata.EditorProperties, string>((props) => props.title, undefined);
}
public set title(newValue: string) {
this.setPropertyFromUI<azdata.EditorProperties, string>((properties, title) => { properties.title = title; }, newValue);
}
}

View File

@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* 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/divContainer';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
ViewChild, ElementRef, OnDestroy,
} from '@angular/core';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import * as azdata from 'azdata';
import { ContainerBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
class DivItem {
constructor(public descriptor: IComponentDescriptor, public config: azdata.DivItemLayout) { }
}
@Component({
template: `
<div #divContainer *ngIf="items" class="divContainer" [style.height]="height" [style.width]="width" (click)="onClick()" (keyup)="onKey($event)" [tabIndex]="tabIndex">
<div *ngFor="let item of items" [style.order]="getItemOrder(item)" [ngStyle]="getItemStyles(item)">
<model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore">
</model-component-wrapper>
</div>
</div>
`
})
export default class DivContainer extends ContainerBase<azdata.DivItemLayout> implements IComponent, OnDestroy {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
@ViewChild('divContainer', { read: ElementRef }) divContainer;
private _height: string;
private _width: string;
private _overflowY: string;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
super(changeRef, el);
this._overflowY = ''; // default
}
ngOnInit(): void {
this.baseInit();
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public setLayout(layout: azdata.DivLayout): void {
this._height = this.convertSize(layout.height);
this._width = this.convertSize(layout.width);
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
if (this.overflowY !== this._overflowY) {
this.updateOverflowY();
}
this.updateScroll();
}
private updateOverflowY() {
this._overflowY = this.overflowY;
if (this._overflowY) {
let element = <HTMLElement>this.divContainer.nativeElement;
element.style.overflowY = this._overflowY;
}
}
private updateScroll() {
let element = <HTMLElement>this.divContainer.nativeElement;
element.scrollTop = element.scrollTop - this.yOffsetChange;
element.dispatchEvent(new Event('scroll'));
}
private onClick() {
this.fireEvent({
eventType: ComponentEventType.onDidClick,
args: undefined
});
}
// CSS-bound properties
public get height(): string {
return this._height;
}
public get width(): string {
return this._width;
}
// CSS-bound properties
public get overflowY(): string {
return this.getPropertyOrDefault<azdata.DivContainerProperties, any>((props) => props.overflowY, '');
}
public set overflowY(newValue: string) {
this.setPropertyFromUI<azdata.DivContainerProperties, any>((properties, newValue) => { properties.overflowY = newValue; }, newValue);
}
public get yOffsetChange(): number {
return this.getPropertyOrDefault<azdata.DivContainerProperties, any>((props) => props.yOffsetChange, 0);
}
public set yOffsetChange(newValue: number) {
this.setPropertyFromUI<azdata.DivContainerProperties, any>((properties, newValue) => { properties.yOffsetChange = newValue; }, newValue);
}
public get clickable(): boolean {
return this.getPropertyOrDefault<azdata.DivContainerProperties, boolean>((props) => props.clickable, false);
}
public get tabIndex(): number {
return this.clickable ? 0 : -1;
}
private onKey(e: KeyboardEvent) {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
this.onClick();
e.stopPropagation();
}
}
private getItemOrder(item: DivItem): number {
return item.config ? item.config.order : 0;
}
private getItemStyles(item: DivItem): { [key: string]: string } {
return item.config && item.config.CSSStyles ? item.config.CSSStyles : {};
}
}

View File

@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* 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/dom';
import 'vs/css!./media/highlight';
import 'vs/css!./media/markdown';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, OnDestroy
} from '@angular/core';
import * as azdata from 'azdata';
import * as DOM from 'vs/base/browser/dom';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/electron-browser/modelComponents/interfaces';
@Component({
template: '',
selector: 'modelview-dom-component'
})
export default class DomComponent extends ComponentBase implements IComponent, OnDestroy {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _renderedHtml: string;
private _rootElement: HTMLElement;
private _bodyElement: HTMLElement;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
this.createDomElement();
this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, e => {
this.layout();
}));
}
ngOnDestroy(): void {
this.baseDestroy();
}
private createDomElement() {
this._rootElement = this._el.nativeElement;
this._bodyElement = DOM.$('.dom-body');
this._rootElement.append(this._bodyElement);
}
/// Dom Functions
private setHtml(): void {
if (this.html) {
this._renderedHtml = this.html;
this._bodyElement.innerHTML = this._renderedHtml;
}
}
/// IComponent implementation
public layout(): void {
super.layout();
const element = <HTMLElement>this._el.nativeElement;
element.style.width = this.getWidth();
element.style.height = this.getHeight();
}
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
if (this.html !== this._renderedHtml) {
this.setHtml();
}
}
// CSS-bound properties
public get html(): string {
return this.getPropertyOrDefault<azdata.DomProperties, string>((props) => props.html, '');
}
public set html(newValue: string) {
this.setPropertyFromUI<azdata.DomProperties, string>((properties, html) => { properties.html = html; }, newValue);
}
}

View File

@@ -0,0 +1,207 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
ViewChild, ElementRef, OnDestroy, AfterViewInit
} from '@angular/core';
import * as azdata from 'azdata';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { Dropdown, IDropdownOptions } from 'sql/base/browser/ui/editableDropdown/dropdown';
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
import { attachEditableDropdownStyler } from 'sql/platform/theme/common/styler';
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
@Component({
selector: 'modelview-dropdown',
template: `
<div [style.width]="getWidth()">
<div [style.display]="getEditableDisplay()" #editableDropDown style="width: 100%;"></div>
<div [style.display]="getNotEditableDisplay()" #dropDown style="width: 100%;"></div>
</div>
`
})
export default class DropDownComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _editableDropdown: Dropdown;
private _selectBox: SelectBox;
@ViewChild('editableDropDown', { read: ElementRef }) private _editableDropDownContainer: ElementRef;
@ViewChild('dropDown', { read: ElementRef }) private _dropDownContainer: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IContextViewService) private contextViewService: IContextViewService,
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(ILayoutService) private readonly layoutService: ILayoutService
) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
if (this._editableDropDownContainer) {
let dropdownOptions: IDropdownOptions = {
values: [],
strictSelection: false,
placeholder: '',
maxHeight: 125,
ariaLabel: '',
actionLabel: ''
};
this._editableDropdown = new Dropdown(this._editableDropDownContainer.nativeElement, this.contextViewService, this.layoutService,
dropdownOptions);
this._register(this._editableDropdown);
this._register(attachEditableDropdownStyler(this._editableDropdown, this.themeService));
this._register(this._editableDropdown.onValueChange(e => {
if (this.editable) {
this.setSelectedValue(this._editableDropdown.value);
this.fireEvent({
eventType: ComponentEventType.onDidChange,
args: e
});
}
}));
}
if (this._dropDownContainer) {
this._selectBox = new SelectBox(this.getValues(), this.getSelectedValue(), this.contextViewService, this._dropDownContainer.nativeElement);
this._selectBox.render(this._dropDownContainer.nativeElement);
this._register(this._selectBox);
this._register(attachSelectBoxStyler(this._selectBox, this.themeService));
this._register(this._selectBox.onDidSelect(e => {
if (!this.editable) {
this.setSelectedValue(this._selectBox.value);
this.fireEvent({
eventType: ComponentEventType.onDidChange,
args: e
});
}
}));
}
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
if (this.editable) {
this._editableDropdown.values = this.getValues();
if (this.value) {
this._editableDropdown.value = this.getSelectedValue();
}
this._editableDropdown.enabled = this.enabled;
this._editableDropdown.fireOnTextChange = this.fireOnTextChange;
} else {
this._selectBox.setOptions(this.getValues());
this._selectBox.selectWithOptionName(this.getSelectedValue());
if (this.enabled) {
this._selectBox.enable();
} else {
this._selectBox.disable();
}
}
}
private getValues(): string[] {
if (this.values && this.values.length > 0) {
if (!this.valuesHaveDisplayName()) {
return this.values as string[];
} else {
return (<azdata.CategoryValue[]>this.values).map(v => v.displayName);
}
}
return [];
}
private valuesHaveDisplayName(): boolean {
return typeof (this.values[0]) !== 'string';
}
private getSelectedValue(): string {
if (this.values && this.values.length > 0 && this.valuesHaveDisplayName()) {
let selectedValue = <azdata.CategoryValue>this.value || <azdata.CategoryValue>this.values[0];
let valueCategory = (<azdata.CategoryValue[]>this.values).find(v => v.name === selectedValue.name);
return valueCategory && valueCategory.displayName;
} else {
if (!this.value && this.values && this.values.length > 0) {
return <string>this.values[0];
}
return <string>this.value;
}
}
private setSelectedValue(newValue: string): void {
if (this.values && this.valuesHaveDisplayName()) {
let valueCategory = (<azdata.CategoryValue[]>this.values).find(v => v.displayName === newValue);
this.value = valueCategory;
} else {
this.value = newValue;
}
}
// CSS-bound properties
private get value(): string | azdata.CategoryValue {
return this.getPropertyOrDefault<azdata.DropDownProperties, string | azdata.CategoryValue>((props) => props.value, '');
}
private get editable(): boolean {
return this.getPropertyOrDefault<azdata.DropDownProperties, boolean>((props) => props.editable, false);
}
private get fireOnTextChange(): boolean {
return this.getPropertyOrDefault<azdata.DropDownProperties, boolean>((props) => props.fireOnTextChange, false);
}
public getEditableDisplay(): string {
return this.editable ? '' : 'none';
}
public getNotEditableDisplay(): string {
return !this.editable ? '' : 'none';
}
private set value(newValue: string | azdata.CategoryValue) {
this.setPropertyFromUI<azdata.DropDownProperties, string | azdata.CategoryValue>(this.setValueProperties, newValue);
}
private get values(): string[] | azdata.CategoryValue[] {
return this.getPropertyOrDefault<azdata.DropDownProperties, string[] | azdata.CategoryValue[]>((props) => props.values, []);
}
private set values(newValue: string[] | azdata.CategoryValue[]) {
this.setPropertyFromUI<azdata.DropDownProperties, string[] | azdata.CategoryValue[]>(this.setValuesProperties, newValue);
}
private setValueProperties(properties: azdata.DropDownProperties, value: string | azdata.CategoryValue): void {
properties.value = value;
}
private setValuesProperties(properties: azdata.DropDownProperties, values: string[] | azdata.CategoryValue[]): void {
properties.values = values;
}
}

View File

@@ -0,0 +1,201 @@
/*---------------------------------------------------------------------------------------------
* 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/editor';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, OnDestroy
} from '@angular/core';
import * as azdata from 'azdata';
import * as DOM from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITextModel } from 'vs/editor/common/model';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { QueryTextEditor } from 'sql/workbench/electron-browser/modelComponents/queryTextEditor';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { SimpleProgressService } from 'vs/editor/standalone/browser/simpleServices';
import { IProgressService } from 'vs/platform/progress/common/progress';
@Component({
template: '',
selector: 'modelview-editor-component'
})
export default class EditorComponent extends ComponentBase implements IComponent, OnDestroy {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _editor: QueryTextEditor;
private _editorInput: UntitledEditorInput;
private _editorModel: ITextModel;
private _renderedContent: string;
private _languageMode: string;
private _uri: string;
private _isAutoResizable: boolean;
private _minimumHeight: number;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
@Inject(IModelService) private _modelService: IModelService,
@Inject(IModeService) private _modeService: IModeService
) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
this._createEditor();
this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, e => {
this.layout();
}));
}
private _createEditor(): void {
let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleProgressService()]));
this._editor = instantiationService.createInstance(QueryTextEditor);
this._editor.create(this._el.nativeElement);
this._editor.setVisible(true);
let uri = this.createUri();
this._editorInput = instantiationService.createInstance(UntitledEditorInput, uri, false, 'plaintext', '', '');
this._editor.setInput(this._editorInput, undefined);
this._editorInput.resolve().then(model => {
this._editorModel = model.textEditorModel;
this.fireEvent({
eventType: ComponentEventType.onComponentCreated,
args: this._uri
});
});
this._register(this._editor);
this._register(this._editorInput);
this._register(this._editorModel.onDidChangeContent(e => {
this.content = this._editorModel.getValue();
if (this._isAutoResizable) {
if (this._minimumHeight) {
this._editor.setMinimumHeight(this._minimumHeight);
}
this._editor.setHeightToScrollHeight();
}
// Notify via an event so that extensions can detect and propagate changes
this.fireEvent({
eventType: ComponentEventType.onDidChange,
args: e
});
}));
}
private createUri(): URI {
let uri = URI.from({ scheme: Schemas.untitled, path: `${this.descriptor.type}-${this.descriptor.id}` });
// Use this to set the internal (immutable) and public (shared with extension) uri properties
this._uri = uri.toString();
this.editorUri = this._uri;
return uri;
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public layout(): void {
let width: number = this.convertSizeToNumber(this.width);
let height: number = this.convertSizeToNumber(this.height);
if (this._isAutoResizable) {
this._editor.setHeightToScrollHeight();
height = Math.max(this._editor.scrollHeight, this._minimumHeight ? this._minimumHeight : 0);
}
this._editor.layout(new DOM.Dimension(
width && width > 0 ? width : DOM.getContentWidth(this._el.nativeElement),
height && height > 0 ? height : DOM.getContentHeight(this._el.nativeElement)));
let element = <HTMLElement>this._el.nativeElement;
element.style.position = this.position;
}
/// Editor Functions
private updateModel() {
if (this._editorModel) {
this._renderedContent = this.content;
this._modelService.updateModel(this._editorModel, this._renderedContent);
}
}
private updateLanguageMode() {
if (this._editorModel && this._editor) {
this._languageMode = this.languageMode;
let languageSelection = this._modeService.create(this._languageMode);
this._modelService.setMode(this._editorModel, languageSelection);
}
}
/// IComponent implementation
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
if (this.content !== this._renderedContent) {
this.updateModel();
}
if (this.languageMode !== this._languageMode) {
this.updateLanguageMode();
}
// Intentionally always updating editorUri as it's wiped out by parent setProperties call.
this.editorUri = this._uri;
this._isAutoResizable = this.isAutoResizable;
this._minimumHeight = this.minimumHeight;
}
// CSS-bound properties
public get content(): string {
return this.getPropertyOrDefault<azdata.EditorProperties, string>((props) => props.content, undefined);
}
public set content(newValue: string) {
this.setPropertyFromUI<azdata.EditorProperties, string>((properties, content) => { properties.content = content; }, newValue);
}
public get languageMode(): string {
return this.getPropertyOrDefault<azdata.EditorProperties, string>((props) => props.languageMode, undefined);
}
public set languageMode(newValue: string) {
this.setPropertyFromUI<azdata.EditorProperties, string>((properties, languageMode) => { properties.languageMode = languageMode; }, newValue);
}
public get isAutoResizable(): boolean {
return this.getPropertyOrDefault<azdata.EditorProperties, boolean>((props) => props.isAutoResizable, false);
}
public set isAutoResizable(newValue: boolean) {
this.setPropertyFromUI<azdata.EditorProperties, boolean>((properties, isAutoResizable) => { properties.isAutoResizable = isAutoResizable; }, newValue);
}
public get minimumHeight(): number {
return this.getPropertyOrDefault<azdata.EditorProperties, number>((props) => props.minimumHeight, this._editor.minimumHeight);
}
public set minimumHeight(newValue: number) {
this.setPropertyFromUI<azdata.EditorProperties, number>((properties, minimumHeight) => { properties.minimumHeight = minimumHeight; }, newValue);
}
public get editorUri(): string {
return this.getPropertyOrDefault<azdata.EditorProperties, string>((props) => props.editorUri, '');
}
public set editorUri(newValue: string) {
this.setPropertyFromUI<azdata.EditorProperties, string>((properties, editorUri) => { properties.editorUri = editorUri; }, newValue);
}
}

View File

@@ -0,0 +1,133 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ViewChild, ElementRef, OnDestroy, AfterViewInit
} from '@angular/core';
import * as azdata from 'azdata';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { FileBrowserViewModel } from 'sql/workbench/services/fileBrowser/common/fileBrowserViewModel';
import { FileNode } from 'sql/workbench/services/fileBrowser/common/fileNode';
import { FileBrowserTreeView } from 'sql/workbench/services/fileBrowser/browser/fileBrowserTreeView';
@Component({
selector: 'modelview-fileBrowserTree',
template: `
<div #fileBrowserTree [style.width]="getWidth()" [style.height]="getHeight()"></div>
`
})
export default class FileBrowserTreeComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _treeView: FileBrowserTreeView;
private _viewModel: FileBrowserViewModel;
private _fileFilters: [{ label: string, filters: string[] }] = [
{ label: 'All Files', filters: ['*'] }
];
@ViewChild('fileBrowserTree', { read: ElementRef }) private _treeContainer: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
@Inject(forwardRef(() => ElementRef)) el: ElementRef) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
this._viewModel = this._instantiationService.createInstance(FileBrowserViewModel);
this._viewModel.onAddFileTree(args => this.handleOnAddFileTree(args.rootNode, args.selectedNode, args.expandedNodes));
this._viewModel.onPathValidate(args => this.handleOnValidate(args.succeeded, args.message));
}
public initialize() {
this._viewModel.initialize(this.ownerUri, '', this._fileFilters, 'Backup');
this._treeView = this._instantiationService.createInstance(FileBrowserTreeView);
this._treeView.setOnClickedCallback((arg) => {
this.onClicked(arg);
});
this._treeView.setOnDoubleClickedCallback((arg) => this.onDoubleClicked(arg));
this._register(this._treeView);
this._viewModel.openFileBrowser(0, false);
}
private onClicked(selectedNode: FileNode) {
this.fireEvent({
eventType: ComponentEventType.onDidChange,
args: { fullPath: selectedNode.fullPath, isFile: selectedNode.isFile }
});
}
private onDoubleClicked(selectedNode: FileNode) {
if (selectedNode.isFile === true) {
}
}
private handleOnAddFileTree(rootNode: FileNode, selectedNode: FileNode, expandedNodes: FileNode[]) {
this.updateFileTree(rootNode, selectedNode, expandedNodes);
}
private updateFileTree(rootNode: FileNode, selectedNode: FileNode, expandedNodes: FileNode[]): void {
this._treeView.renderBody(this._treeContainer.nativeElement, rootNode, selectedNode, expandedNodes);
this._treeView.setVisible(true);
this.layoutTree();
this._changeRef.detectChanges();
}
private handleOnValidate(succeeded: boolean, errorMessage: string) {
if (succeeded === false) {
if (errorMessage === '') {
errorMessage = 'The provided path is invalid.';
}
}
}
public validate(): Thenable<boolean> {
return super.validate().then(valid => {
// TODO: tree validation?
return valid;
});
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public setLayout(): void {
// TODO allow configuring the look and feel
this.layout();
}
private layoutTree(): void {
this._treeView.layout(700);
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this.validate();
if (this.ownerUri) {
this.initialize();
}
}
// CSS-bound properties
public get ownerUri(): string {
return this.getPropertyOrDefault<azdata.FileBrowserTreeProperties, string>((props) => props.ownerUri, '');
}
public set ownerUri(newValue: string) {
this.setPropertyFromUI<azdata.FileBrowserTreeProperties, string>((props, value) => props.ownerUri = value, newValue);
}
}

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/flexContainer';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
ElementRef, OnDestroy
} from '@angular/core';
import { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { FlexLayout, FlexItemLayout } from 'azdata';
import { ContainerBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
export class FlexItem {
constructor(public descriptor: IComponentDescriptor, public config: FlexItemLayout) { }
}
@Component({
template: `
<div *ngIf="items" class="flexContainer" [style.flexFlow]="flexFlow" [style.justifyContent]="justifyContent" [style.position]="position"
[style.alignItems]="alignItems" [style.alignContent]="alignContent" [style.height]="height" [style.width]="width">
<div *ngFor="let item of items" [style.flex]="getItemFlex(item)" [style.textAlign]="textAlign" [style.order]="getItemOrder(item)" [ngStyle]="getItemStyles(item)">
<model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore">
</model-component-wrapper>
</div>
</div>
`
})
export default class FlexContainer extends ContainerBase<FlexItemLayout> implements IComponent, OnDestroy {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _flexFlow: string;
private _justifyContent: string;
private _alignItems: string;
private _alignContent: string;
private _textAlign: string;
private _height: string;
private _width: string;
private _position: string;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
super(changeRef, el);
this._flexFlow = ''; // default
this._justifyContent = ''; // default
}
ngOnInit(): void {
this.baseInit();
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public setLayout(layout: FlexLayout): void {
this._flexFlow = layout.flexFlow ? layout.flexFlow : '';
this._justifyContent = layout.justifyContent ? layout.justifyContent : '';
this._alignItems = layout.alignItems ? layout.alignItems : '';
this._alignContent = layout.alignContent ? layout.alignContent : '';
this._textAlign = layout.textAlign ? layout.textAlign : '';
this._position = layout.position ? layout.position : '';
this._height = this.convertSize(layout.height);
this._width = this.convertSize(layout.width);
this.layout();
}
// CSS-bound properties
public get flexFlow(): string {
return this._flexFlow;
}
public get justifyContent(): string {
return this._justifyContent;
}
public get alignItems(): string {
return this._alignItems;
}
public get height(): string {
return this._height;
}
public get width(): string {
return this._width;
}
public get alignContent(): string {
return this._alignContent;
}
public get textAlign(): string {
return this._textAlign;
}
public get position(): string {
return this._position;
}
private getItemFlex(item: FlexItem): string {
return item.config ? item.config.flex : '1 1 auto';
}
private getItemOrder(item: FlexItem): number {
return item.config ? item.config.order : 0;
}
private getItemStyles(item: FlexItem): { [key: string]: string } {
return item.config && item.config.CSSStyles ? item.config.CSSStyles : {};
}
}

View File

@@ -0,0 +1,231 @@
/*---------------------------------------------------------------------------------------------
* 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/formLayout';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
ViewChild, ElementRef, OnDestroy, AfterViewInit
} from '@angular/core';
import { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { FormLayout, FormItemLayout } from 'azdata';
import { ContainerBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
export interface TitledFormItemLayout {
title: string;
actions?: string[];
isFormComponent: boolean;
horizontal: boolean;
componentWidth?: number | string;
componentHeight?: number | string;
titleFontSize?: number | string;
required?: boolean;
info?: string;
isInGroup?: boolean;
isGroupLabel?: boolean;
}
export interface FormLayout {
width: number;
}
class FormItem {
constructor(public descriptor: IComponentDescriptor, public config: TitledFormItemLayout) { }
}
@Component({
template: `
<div #container *ngIf="items" class="form-table" [style.padding]="getFormPadding()" [style.width]="getFormWidth()" [style.height]="getFormHeight()">
<ng-container *ngFor="let item of items">
<div class="form-row" *ngIf="isGroupLabel(item)" [style.font-size]="getItemTitleFontSize(item)">
<div class="form-item-row form-group-label">
<model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore">
</model-component-wrapper>
</div>
</div>
<div class="form-row" *ngIf="isFormComponent(item)" [style.height]="getRowHeight(item)">
<ng-container *ngIf="isHorizontal(item)">
<div class="form-cell" [style.font-size]="getItemTitleFontSize(item)" [ngClass]="{'form-group-item': isInGroup(item)}">
{{getItemTitle(item)}}<span class="form-required" *ngIf="isItemRequired(item)">*</span>
<span class="icon help form-info" *ngIf="itemHasInfo(item)" [title]="getItemInfo(item)"></span>
</div>
<div class="form-cell">
<div class="form-component-container">
<div [style.width]="getComponentWidth(item)" [ngClass]="{'form-input-flex': !getComponentWidth(item)}">
<model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore">
</model-component-wrapper>
</div>
<div *ngIf="itemHasActions(item)" class="form-component-actions">
<ng-container *ngFor="let actionItem of getActionComponents(item)">
<model-component-wrapper [descriptor]="actionItem.descriptor" [modelStore]="modelStore" >
</model-component-wrapper>
</ng-container>
</div>
</div>
</div>
</ng-container>
<div class="form-vertical-container" *ngIf="isVertical(item)" [style.height]="getRowHeight(item)" [ngClass]="{'form-group-item': isInGroup(item)}">
<div class="form-item-row" [style.font-size]="getItemTitleFontSize(item)">
{{getItemTitle(item)}}<span class="form-required" *ngIf="isItemRequired(item)">*</span>
<span class="icon help form-info" *ngIf="itemHasInfo(item)" [title]="getItemInfo(item)"></span>
</div>
<div class="form-item-row" [style.width]="getComponentWidth(item)" [style.height]="getRowHeight(item)">
<model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore" [style.width]="getComponentWidth(item)" [style.height]="getRowHeight(item)">
</model-component-wrapper>
</div>
<div *ngIf="itemHasActions(item)" class="form-item-row form-actions-table form-item-last-row">
<div *ngFor="let actionItem of getActionComponents(item)" class="form-actions-cell" >
<model-component-wrapper [descriptor]="actionItem.descriptor" [modelStore]="modelStore">
</model-component-wrapper>
</div>
</div>
</div>
</div>
</ng-container>
</div>
`
})
export default class FormContainer extends ContainerBase<FormItemLayout> implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _alignItems: string;
private _alignContent: string;
private _formLayout: FormLayout;
@ViewChild('container', { read: ElementRef }) private _container: ElementRef;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
ngOnDestroy(): void {
this.baseDestroy();
}
ngAfterViewInit(): void {
}
public layout(): void {
super.layout();
}
/// IComponent implementation
public get alignItems(): string {
return this._alignItems;
}
public get alignContent(): string {
return this._alignContent;
}
private getFormWidth(): string {
return this.convertSize(this._formLayout && this._formLayout.width, '');
}
private getFormPadding(): string {
return this._formLayout && this._formLayout.padding ? this._formLayout.padding : '10px 30px 0px 30px';
}
private getFormHeight(): string {
return this.convertSize(this._formLayout && this._formLayout.height, '');
}
private getComponentWidth(item: FormItem): string {
let itemConfig = item.config;
return (itemConfig && itemConfig.componentWidth) ? this.convertSize(itemConfig.componentWidth, '') : '';
}
private getRowHeight(item: FormItem): string {
let itemConfig = item.config;
return (itemConfig && itemConfig.componentHeight) ? this.convertSize(itemConfig.componentHeight, '') : '';
}
private isItemRequired(item: FormItem): boolean {
let itemConfig = item.config;
return itemConfig && itemConfig.required;
}
private getItemInfo(item: FormItem): string {
let itemConfig = item.config;
return itemConfig && itemConfig.info;
}
private itemHasInfo(item: FormItem): boolean {
let itemConfig = item.config;
return itemConfig && itemConfig.info !== undefined;
}
private getItemTitle(item: FormItem): string {
let itemConfig = item.config;
return itemConfig ? itemConfig.title : '';
}
private getItemTitleFontSize(item: FormItem): string {
let defaultFontSize = '14px';
if (this.isInGroup(item)) {
defaultFontSize = '12px';
}
let itemConfig = item.config;
return itemConfig && itemConfig.titleFontSize ? this.convertSize(itemConfig.titleFontSize, defaultFontSize) : defaultFontSize;
}
private getActionComponents(item: FormItem): FormItem[] {
let items = this.items;
let itemConfig = item.config;
if (itemConfig && itemConfig.actions) {
let resultItems = itemConfig.actions.map(x => {
let actionComponent = items.find(i => i.descriptor.id === x);
return <FormItem>actionComponent;
});
return resultItems.filter(r => r && r.descriptor);
}
return [];
}
private isGroupLabel(item: FormItem): boolean {
return item && item.config && item.config.isGroupLabel;
}
private isInGroup(item: FormItem): boolean {
return item && item.config && item.config.isInGroup;
}
private isFormComponent(item: FormItem): boolean {
return item && item.config && item.config.isFormComponent;
}
private itemHasActions(item: FormItem): boolean {
let itemConfig = item.config;
return itemConfig && itemConfig.actions !== undefined && itemConfig.actions.length > 0;
}
public setLayout(layout: FormLayout): void {
this._formLayout = layout;
this.layout();
}
private isHorizontal(item: FormItem): boolean {
return item && item.config && item.config.horizontal;
}
private isVertical(item: FormItem): boolean {
return item && item.config && !item.config.horizontal;
}
}

View File

@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* 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/groupLayout';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
ViewChild, ElementRef, OnDestroy, AfterViewInit
} from '@angular/core';
import { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { GroupLayout } from 'azdata';
import { ContainerBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
@Component({
selector: 'modelview-groupContainer',
template: `
<div *ngIf="hasHeader()" [class]="getHeaderClass()" (click)="changeState()">
{{_containerLayout.header}}
</div>
<div #container *ngIf="items" class="modelview-group-container" [style.width]="getContainerWidth()" [style.display]="getContainerDisplayStyle()">
<ng-container *ngFor="let item of items">
<div class="modelview-group-row" >
<div class="modelview-group-cell">
<model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore" >
</model-component-wrapper>
</div>
</div>
</ng-container>
</div>
`
})
export default class GroupContainer extends ContainerBase<GroupLayout> implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _containerLayout: GroupLayout;
private _collapsed: boolean;
@ViewChild('container', { read: ElementRef }) private _container: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef) {
super(changeRef, el);
this._collapsed = false;
}
ngOnInit(): void {
this.baseInit();
}
ngOnDestroy(): void {
this.baseDestroy();
}
ngAfterViewInit(): void {
}
/// IComponent implementation
public setLayout(layout: GroupLayout): void {
this._containerLayout = layout;
this._collapsed = !!layout.collapsed;
this.layout();
}
private hasHeader(): boolean {
return this._containerLayout && this._containerLayout && this._containerLayout.header !== undefined;
}
private isCollapsible(): boolean {
return this.hasHeader() && this._containerLayout.collapsible === true;
}
private getContainerWidth(): string {
if (this._containerLayout && this._containerLayout.width) {
let width: string = this._containerLayout.width.toString();
if (!width.endsWith('%') && !width.toLowerCase().endsWith('px')) {
width = width + 'px';
}
return width;
} else {
return '100%';
}
}
private getContainerDisplayStyle(): string {
return !this.isCollapsible() || !this._collapsed ? 'block' : 'none';
}
private getHeaderClass(): string {
if (this.isCollapsible()) {
let modifier = this._collapsed ? 'collapsed' : 'expanded';
return `modelview-group-header-collapsible ${modifier}`;
} else {
return 'modelview-group-header';
}
}
private changeState(): void {
if (this.isCollapsible()) {
this._collapsed = !this._collapsed;
this._changeRef.detectChanges();
}
}
}

View File

@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
OnDestroy, AfterViewInit, ElementRef
} from '@angular/core';
import * as azdata from 'azdata';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/electron-browser/modelComponents/interfaces';
@Component({
selector: 'modelview-hyperlink',
template: `<a [href]="getUrl()" target="blank">{{getLabel()}}</a>`
})
export default class HyperlinkComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
}
ngOnDestroy(): void {
this.baseDestroy();
}
public setLayout(layout: any): void {
this.layout();
}
public set label(newValue: string) {
this.setPropertyFromUI<azdata.HyperlinkComponentProperties, string>((properties, value) => { properties.label = value; }, newValue);
}
public get label(): string {
return this.getPropertyOrDefault<azdata.HyperlinkComponentProperties, string>((props) => props.label, '');
}
public getLabel(): string {
return this.label;
}
public set url(newValue: string) {
this.setPropertyFromUI<azdata.HyperlinkComponentProperties, string>((properties, value) => { properties.url = value; }, newValue);
}
public get url(): string {
return this.getPropertyOrDefault<azdata.HyperlinkComponentProperties, string>((props) => props.url, '');
}
public getUrl(): string {
return this.url;
}
}

View File

@@ -0,0 +1,292 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
ViewChild, ElementRef, OnDestroy, AfterViewInit
} from '@angular/core';
import * as azdata from 'azdata';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { attachInputBoxStyler } from 'sql/platform/theme/common/styler';
import { IInputOptions, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import * as nls from 'vs/nls';
import { inputBackground, inputBorder } from 'vs/platform/theme/common/colorRegistry';
import * as DomUtils from 'vs/base/browser/dom';
import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
@Component({
selector: 'modelview-inputBox',
template: `
<div [style.display]="getInputBoxDisplay()" #input style="width: 100%"></div>
<div [style.display]="getTextAreaDisplay()" #textarea style="width: 100%"></div>
`
})
export default class InputBoxComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _input: InputBox;
private _textAreaInput: InputBox;
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
@ViewChild('textarea', { read: ElementRef }) private _textareaContainer: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IContextViewService) private contextViewService: IContextViewService,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
let inputOptions: IInputOptions = {
placeholder: '',
ariaLabel: '',
validationOptions: {
validation: () => {
if (this.valid) {
return undefined;
} else {
return {
content: this.inputElement.inputElement.validationMessage || nls.localize('invalidValueError', 'Invalid value'),
type: MessageType.ERROR
};
}
}
},
useDefaultValidation: true
};
if (this._inputContainer) {
this._input = new InputBox(this._inputContainer.nativeElement, this.contextViewService, inputOptions);
this.registerInput(this._input, () => !this.multiline);
}
if (this._textareaContainer) {
let textAreaInputOptions = Object.assign({}, inputOptions, { flexibleHeight: true, type: 'textarea' });
this._textAreaInput = new InputBox(this._textareaContainer.nativeElement, this.contextViewService, textAreaInputOptions);
this.onkeydown(this._textAreaInput.inputElement, (e: StandardKeyboardEvent) => {
if (this.tryHandleKeyEvent(e)) {
e.stopPropagation();
}
// Else assume that keybinding service handles routing this to a command
});
this.registerInput(this._textAreaInput, () => this.multiline);
}
this.inputElement.hideErrors = true;
}
private onkeydown(domNode: HTMLElement, listener: (e: IKeyboardEvent) => void): void {
this._register(DomUtils.addDisposableListener(domNode, DomUtils.EventType.KEY_DOWN, (e: KeyboardEvent) => listener(new StandardKeyboardEvent(e))));
}
private tryHandleKeyEvent(e: StandardKeyboardEvent): boolean {
let handled: boolean = false;
if (this.multiline && e.keyCode === KeyCode.Enter) {
handled = true;
}
return handled;
}
private get inputElement(): InputBox {
return this.multiline ? this._textAreaInput : this._input;
}
private registerInput(input: InputBox, checkOption: () => boolean): void {
if (input) {
this._validations.push(() => !input.inputElement.validationMessage);
this._register(input);
this._register(attachInputBoxStyler(input, this.themeService, {
inputValidationInfoBackground: inputBackground,
inputValidationInfoBorder: inputBorder,
}));
this._register(input.onDidChange(async e => {
if (checkOption()) {
this.value = input.value;
await this.validate();
if (input.hideErrors) {
input.hideErrors = false;
}
this.fireEvent({
eventType: ComponentEventType.onDidChange,
args: e
});
}
}));
}
}
public getInputBoxDisplay(): string {
return !this.multiline ? '' : 'none';
}
public getTextAreaDisplay(): string {
return this.multiline ? '' : 'none';
}
public validate(): Thenable<boolean> {
return super.validate().then(valid => {
this.inputElement.validate();
return valid;
});
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public layout(): void {
super.layout();
this.layoutInputBox();
}
private layoutInputBox(): void {
if (this.width) {
this.inputElement.width = this.convertSizeToNumber(this.width);
}
if (this.height) {
this.inputElement.setHeight(this.convertSize(this.height));
}
}
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this.setInputProperties(this.inputElement);
this.validate();
}
private setInputProperties(input: InputBox): void {
if (!this.multiline) {
input.inputElement.type = this.inputType;
if (this.inputType === 'number') {
input.inputElement.step = 'any';
if (this.min) {
input.inputElement.min = this.min.toString();
}
if (this.max) {
input.inputElement.max = this.max.toString();
}
}
}
input.value = this.value;
input.setAriaLabel(this.ariaLabel);
input.setPlaceHolder(this.placeHolder);
input.setEnabled(this.enabled);
this.layoutInputBox();
if (this.multiline) {
if (this.rows) {
this.inputElement.rows = this.rows;
}
if (this.columns) {
this.inputElement.columns = this.columns;
}
}
input.inputElement.required = this.required;
}
// CSS-bound properties
public get value(): string {
return this.getPropertyOrDefault<azdata.InputBoxProperties, string>((props) => props.value, '');
}
public set value(newValue: string) {
this.setPropertyFromUI<azdata.InputBoxProperties, string>((props, value) => props.value = value, newValue);
}
public get ariaLabel(): string {
return this.getPropertyOrDefault<azdata.InputBoxProperties, string>((props) => props.ariaLabel, '');
}
public set ariaLabel(newValue: string) {
this.setPropertyFromUI<azdata.InputBoxProperties, string>((props, value) => props.ariaLabel = value, newValue);
}
public get placeHolder(): string {
return this.getPropertyOrDefault<azdata.InputBoxProperties, string>((props) => props.placeHolder, '');
}
public set placeHolder(newValue: string) {
this.setPropertyFromUI<azdata.InputBoxProperties, string>((props, value) => props.placeHolder = value, newValue);
}
public set columns(newValue: number) {
this.setPropertyFromUI<azdata.InputBoxProperties, number>((props, value) => props.columns = value, newValue);
}
public get rows(): number {
return this.getPropertyOrDefault<azdata.InputBoxProperties, number>((props) => props.rows, undefined);
}
public get columns(): number {
return this.getPropertyOrDefault<azdata.InputBoxProperties, number>((props) => props.columns, undefined);
}
public set rows(newValue: number) {
this.setPropertyFromUI<azdata.InputBoxProperties, number>((props, value) => props.rows = value, newValue);
}
public get min(): number {
return this.getPropertyOrDefault<azdata.InputBoxProperties, number>((props) => props.min, undefined);
}
public set min(newValue: number) {
this.setPropertyFromUI<azdata.InputBoxProperties, number>((props, value) => props.min = value, newValue);
}
public get max(): number {
return this.getPropertyOrDefault<azdata.InputBoxProperties, number>((props) => props.max, undefined);
}
public set max(newValue: number) {
this.setPropertyFromUI<azdata.InputBoxProperties, number>((props, value) => props.max = value, newValue);
}
public get inputType(): string {
return this.getPropertyOrDefault<azdata.InputBoxProperties, string>((props) => props.inputType, 'text');
}
public set inputType(newValue: string) {
this.setPropertyFromUI<azdata.InputBoxProperties, string>((props, value) => props.inputType = value, newValue);
}
public get multiline(): boolean {
return this.getPropertyOrDefault<azdata.InputBoxProperties, boolean>((props) => props.multiline, false);
}
public set multiline(newValue: boolean) {
this.setPropertyFromUI<azdata.InputBoxProperties, boolean>((props, value) => props.multiline = value, newValue);
}
public get required(): boolean {
return this.getPropertyOrDefault<azdata.InputBoxProperties, boolean>((props) => props.required, false);
}
public set required(newValue: boolean) {
this.setPropertyFromUI<azdata.InputBoxProperties, boolean>((props, value) => props.required = value, newValue);
}
}

View File

@@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { InjectionToken } from '@angular/core';
import { IDisposable } from 'vs/base/common/lifecycle';
/**
* An instance of a model-backed component. This will be a UI element
*
* @export
*/
export interface IComponent extends IDisposable {
descriptor: IComponentDescriptor;
modelStore: IModelStore;
layout();
registerEventHandler(handler: (event: IComponentEventArgs) => void): IDisposable;
clearContainer?: () => void;
addToContainer?: (componentDescriptor: IComponentDescriptor, config: any, index?: number) => void;
removeFromContainer?: (componentDescriptor: IComponentDescriptor) => void;
setLayout?: (layout: any) => void;
getHtml: () => any;
setProperties?: (properties: { [key: string]: any; }) => void;
enabled: boolean;
readonly valid?: boolean;
validate(): Thenable<boolean>;
setDataProvider(handle: number, componentId: string, context: any): void;
refreshDataProvider(item: any): void;
}
export const COMPONENT_CONFIG = new InjectionToken<IComponentConfig>('component_config');
export interface IComponentConfig {
descriptor: IComponentDescriptor;
modelStore: IModelStore;
}
/**
* Defines a component and can be used to map from the model-backed version of the
* world to the frontend UI;
*
* @export
*/
export interface IComponentDescriptor {
/**
* The type of this component. Used to map to the correct angular selector
* when loading the component
*/
type: string;
/**
* A unique ID for this component
*/
id: string;
}
export interface IComponentEventArgs {
eventType: ComponentEventType;
args: any;
componentId?: string;
}
export enum ComponentEventType {
PropertiesChanged,
onDidChange,
onDidClick,
validityChanged,
onMessage,
onSelectedRowChanged,
onComponentCreated
}
export interface IModelStore {
/**
* Creates and saves the reference of a component descriptor.
* This can be used during creation of a component later
*/
createComponentDescriptor(type: string, createComponentDescriptor): IComponentDescriptor;
/**
* gets the descriptor for a previously created component ID
*/
getComponentDescriptor(componentId: string): IComponentDescriptor;
registerComponent(component: IComponent): void;
unregisterComponent(component: IComponent): void;
getComponent(componentId: string): IComponent;
/**
* Runs on a component immediately if the component exists, or runs on
* registration of the component otherwise
*
* @param componentId unique identifier of the component
* @param action some action to perform
*/
eventuallyRunOnComponent<T>(componentId: string, action: (component: IComponent) => T): Promise<T>;
/**
* Register a callback that will validate components when given a component ID
*/
registerValidationCallback(callback: (componentId: string) => Thenable<boolean>): void;
/**
* Run all validations for the given component and return the new validation value
*/
validate(component: IComponent): Thenable<boolean>;
}

View File

@@ -0,0 +1,106 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
ViewChild, ElementRef, OnDestroy, AfterViewInit
} from '@angular/core';
import * as azdata from 'azdata';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { ListBox } from 'sql/base/browser/ui/listBox/listBox';
import { attachListBoxStyler } from 'sql/platform/theme/common/styler';
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';
@Component({
selector: 'modelview-listBox',
template: `
<div #input style="width: 100%"></div>
`
})
export default class ListBoxComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _input: ListBox;
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IContextViewService) private contextViewService: IContextViewService,
@Inject(IClipboardService) private clipboardService: IClipboardService,
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
if (this._inputContainer) {
this._input = new ListBox([], this.contextViewService, this.clipboardService);
this._input.render(this._inputContainer.nativeElement);
this._register(this._input);
this._register(attachListBoxStyler(this._input, this.themeService));
this._register(this._input.onDidSelect(e => {
this.selectedRow = e.index;
this.fireEvent({
eventType: ComponentEventType.onSelectedRowChanged,
args: e
});
}));
}
}
public validate(): Thenable<boolean> {
return super.validate().then(valid => {
return valid;
});
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this._input.setOptions(this.values.map(value => { return { text: value }; }), this.selectedRow);
this.validate();
}
// CSS-bound properties
private get values(): string[] {
return this.getPropertyOrDefault<azdata.ListBoxProperties, string[]>((props) => props.values, undefined);
}
private set values(newValue: string[]) {
this.setPropertyFromUI<azdata.ListBoxProperties, string[]>((props, value) => props.values = value, newValue);
}
private get selectedRow(): number {
return this.getPropertyOrDefault<azdata.ListBoxProperties, number>((props) => props.selectedRow, undefined);
}
private set selectedRow(newValue: number) {
this.setPropertyFromUI<azdata.ListBoxProperties, number>((props, value) => props.selectedRow = value, newValue);
}
}

View File

@@ -0,0 +1,82 @@
/*---------------------------------------------------------------------------------------------
* 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/loadingComponent';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, OnDestroy, AfterViewInit, ElementRef
} from '@angular/core';
import * as azdata from 'azdata';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import * as nls from 'vs/nls';
@Component({
selector: 'modelview-loadingComponent',
template: `
<div class="modelview-loadingComponent-container" *ngIf="loading">
<div class="modelview-loadingComponent-spinner" *ngIf="loading" [title]=_loadingTitle #spinnerElement></div>
</div>
<model-component-wrapper #childElement [descriptor]="_component" [modelStore]="modelStore" *ngIf="_component" [ngClass]="{'modelview-loadingComponent-content-loading': loading}">
</model-component-wrapper>
`
})
export default class LoadingComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
private readonly _loadingTitle = nls.localize('loadingMessage', 'Loading');
private _component: IComponentDescriptor;
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef) {
super(changeRef, el);
this._validations.push(() => {
if (!this._component) {
return true;
}
return this.modelStore.getComponent(this._component.id).validate();
});
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
this.setLayout();
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public setLayout(): void {
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
}
public get loading(): boolean {
return this.getPropertyOrDefault<azdata.LoadingComponentProperties, boolean>((props) => props.loading, false);
}
public set loading(newValue: boolean) {
this.setPropertyFromUI<azdata.LoadingComponentProperties, boolean>((properties, value) => { properties.loading = value; }, newValue);
this.layout();
}
public addToContainer(componentDescriptor: IComponentDescriptor): void {
this._component = componentDescriptor;
this.layout();
}
}

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* 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/loadingComponent';
import { Component, Input } from '@angular/core';
import * as nls from 'vs/nls';
@Component({
selector: 'loading-spinner',
template: `
<div class="modelview-loadingComponent-container" *ngIf="loading">
<div class="modelview-loadingComponent-spinner" *ngIf="loading" [title]=_loadingTitle #spinnerElement></div>
</div>
`
})
export default class LoadingSpinner {
private readonly _loadingTitle = nls.localize('loadingMessage', 'Loading');
@Input() loading: boolean;
}

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
modelview-button a.monaco-button.monaco-text-button.icon {
background-repeat: no-repeat;
background-position: 0% 50%;
background-size: contain;
}

View File

@@ -0,0 +1,200 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.model-card {
position: relative;
display: inline-block;
height: 90%;
width: auto;
margin: 15px;
border-width: 1px;
border-style: solid;
text-align: left;
vertical-align: top;
}
.model-card-list-item.selected,
.model-card.selected {
border-color: rgb(0, 120, 215);
box-shadow: rgba(0, 120, 215, 0.75) 0px 0px 6px;
}
.model-card-list-item.unselected,
.model-card.unselected {
border-color: rgb(214, 214, 214);
box-shadow: none;
}
.model-card .card-content {
position: relative;
display: inline-block;
height: auto;
width: auto;
padding: 10px 45px 20px 45px;
min-height: 30px;
min-width: 30px;
}
.model-card .card-vertical-button {
position: relative;
display: flex;
flex-direction: column;
text-align: center;
height: auto;
width: auto;
padding: 5px 5px 5px 5px;
min-height: 130px;
min-width: 130px;
}
.model-card .card-label {
font-size: 12px;
font-weight: bold;
}
.model-card .card-value {
font-size: 12px;
line-height: 18px;
}
.model-card .iconContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-grow: 1;
border-bottom-width: 1px;
border-bottom-style: solid;
padding: 10px 0px 10px 0px;
border-color: rgb(214, 214, 214);
}
.model-card .cardIcon {
display: inline-block;
flex-grow: 1;
width: 100%;
height: 100%;
max-width: 50px;
max-height: 50px;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
.model-card .card-status {
position: absolute;
top: 7px;
left: 5px;
overflow: hidden;
width: 22px;
height: 22px;
}
.model-card .status-content {
position: absolute;
top: 0px;
right: 0px;
min-width: 16px;
height: 16px;
border-radius: 8px;
text-align: center;
}
.model-card-list-item .selection-indicator-container,
.model-card .selection-indicator-container {
position: absolute;
top: 5px;
right: 5px;
overflow: hidden;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: white;
border-width: 1px;
border-color: rgb(0, 120, 215);
border-style: solid;
}
.model-card-list-item .selection-indicator-container,
.model-card .selection-indicator-container {
position: absolute;
overflow: hidden;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: white;
border-width: 1px;
border-color: rgb(0, 120, 215);
border-style: solid;
}
.model-card-list-item .selection-indicator-container {
top: 10px;
right: 10px;
}
.model-card .selection-indicator-container {
top: 5px;
right: 5px;
}
.model-card-list-item .selection-indicator,
.model-card .selection-indicator {
margin: 4px;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: rgb(0, 120, 215);
}
.model-card .model-table {
border-spacing: 5px;
}
.model-table .table-row {
width: auto;
clear: both;
}
.model-table .table-cell {
vertical-align: top;
padding: 7px;
}
.model-table a {
cursor: pointer;
text-decoration: underline
}
.model-card-list-item {
display: inline-block;
height: 100%;
width: 100%;
margin: 5px 0px 5px 0px;
border-width: 1px;
border-style: solid;
text-align: left;
vertical-align: top;
}
.model-card-list-item .list-item-content {
height: auto;
padding: 5px 26px 5px 5px;
min-height: 30px;
min-width: 300px;
}
.model-card-list-item .list-item-icon {
background-position: 2px 2px;
padding-left:22px;
font-size: 15px;
background-repeat: no-repeat;
background-size: 16px 16px;
}
.model-card-list-item .list-item-description {
padding-left:22px;
}

View File

@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.declarative-table {
padding: 5px 30px 0px 30px;
box-sizing: border-box;
border-collapse: collapse;
}
.declarative-table-header {
padding: 5px;
border: 1px solid gray;
background-color: #F5F5F5;
vertical-align: top;
}
.vs-dark .declarative-table-header {
padding: 5px;
border: 1px solid gray;
background-color: #333334;
}
.hc-black .declarative-table-header {
padding: 5px;
border: 1px solid gray;
background-color: #333334;
}
.declarative-table-cell {
padding: 5px;
border: 1px solid gray;
}
.declarative-table [role="gridcell"]:focus,
.declarative-table [role="gridcell"] *:focus,
.declarative-table [role="grid"] [tabindex="0"]:focus {
outline: #005a9c;
outline-style: dotted;
outline-width: 3px;
}

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.divContainer {
display: block;
height: 100%;
}

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
modelview-dom-component {
display: block;
-webkit-user-select: text;
}

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
modelview-editor-component {
height: 100%;
width : 100%;
display: block;
}
modelview-diff-editor-component {
height: 100%;
width : 100%;
display: block;
}
.vs-dark modelview-diff-editor-title {
background: #444444;
}
modelview-diff-editor-title {
background: #f4f4f4;
}

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.flexContainer {
display: flex;
}

View File

@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.form-table {
display: table;
padding: 5px 30px 0px 30px;
box-sizing: border-box;
}
.form-actions-table {
display: table;
}
.form-row {
display: table-row;
}
.form-item-row {
padding-bottom: 5px;
}
.form-vertical-container {
padding-bottom: 5px;
width: 100%;
}
.form-cell {
padding-bottom: 10px;
padding-right: 5px;
display: table-cell;
}
.form-component-container {
display: flex;
flex-direction: row;
}
.form-input-flex {
flex: 1;
}
.form-required {
color: red;
padding-left: 5px;
}
.form-info {
width: 15px;
height: 15px;
}
.form-component-actions {
padding-left: 5px;
}
.form-actions-cell {
padding-top: 5px;
padding-right: 5px;
display: table-cell;
}
.form-group-label {
padding-top: 3px;
padding-bottom: 0px;
font-weight: bold;
}

View File

@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.modelview-group-container {
display: table;
padding: 5px 10px 5px 10px;
box-sizing: border-box;
}
.modelview-group-row {
display: table-row;
}
.modelview-group-header-collapsible,
.modelview-group-header {
padding-bottom: 5px;
font-size: 14px;
}
.modelview-group-header-collapsible {
padding-left: 20px;
background-position: 2px 2px;
background-size: 16px 16px;
background-repeat: no-repeat;
cursor: pointer;
}
.vs .modelview-group-header-collapsible.expanded {
background-image: url("../../media/icons/expanded.svg");
}
.vs-dark .modelview-group-header-collapsible.expanded,
.hc-black .modelview-group-header-collapsible.expanded {
background-image: url("../../media/icons/expanded_inverse.svg");
}
.vs .modelview-group-header-collapsible.collapsed {
background-image: url("../../media/icons/collapsed.svg");
}
.vs-dark .modelview-group-header-collapsible.collapsed,
.hc-black .modelview-group-header-collapsible.collapsed {
background-image: url("../../media/icons/collapsed_inverse.svg");
}
.modelview-group-cell {
padding-bottom: 5px;
display: table-cell;
}

View File

@@ -0,0 +1,183 @@
/*
https://raw.githubusercontent.com/isagalaev/highlight.js/master/src/styles/vs2015.css
*/
/*
* Visual Studio 2015 dark style
* Author: Nicolas LLOBERA <nllobera@gmail.com>
*/
modelview-dom-component .hljs-keyword,
modelview-dom-component .hljs-literal,
modelview-dom-component .hljs-symbol,
modelview-dom-component .hljs-name {
color: #569CD6;
}
modelview-dom-component .hljs-link {
color: #569CD6;
text-decoration: underline;
}
modelview-dom-component .hljs-built_in,
modelview-dom-component .hljs-type {
color: #4EC9B0;
}
modelview-dom-component .hljs-number,
modelview-dom-component .hljs-class {
color: #B8D7A3;
}
modelview-dom-component .hljs-string,
modelview-dom-component .hljs-meta-string {
color: #D69D85;
}
modelview-dom-component .hljs-regexp,
modelview-dom-component .hljs-template-tag {
color: #9A5334;
}
modelview-dom-component .hljs-subst,
modelview-dom-component .hljs-function,
modelview-dom-component .hljs-title,
modelview-dom-component .hljs-params,
modelview-dom-component .hljs-formula {
color: #DCDCDC;
}
modelview-dom-component .hljs-comment,
modelview-dom-component .hljs-quote {
color: #57A64A;
font-style: italic;
}
modelview-dom-component .hljs-doctag {
color: #608B4E;
}
modelview-dom-component .hljs-meta,
modelview-dom-component .hljs-meta-keyword,
modelview-dom-component .hljs-tag {
color: #9B9B9B;
}
modelview-dom-component .hljs-variable,
modelview-dom-component .hljs-template-variable {
color: #BD63C5;
}
modelview-dom-component .hljs-attr,
modelview-dom-component .hljs-attribute,
modelview-dom-component .hljs-builtin-name {
color: #9CDCFE;
}
modelview-dom-component .hljs-section {
color: gold;
}
modelview-dom-component .hljs-emphasis {
font-style: italic;
}
modelview-dom-component .hljs-strong {
font-weight: bold;
}
/*.hljs-code {
font-family:'Monospace';
}*/
modelview-dom-component .hljs-bullet,
modelview-dom-component .hljs-selector-tag,
modelview-dom-component .hljs-selector-id,
modelview-dom-component .hljs-selector-class,
modelview-dom-component .hljs-selector-attr,
modelview-dom-component .hljs-selector-pseudo {
color: #D7BA7D;
}
modelview-dom-component .hljs-addition {
background-color: #144212;
display: inline-block;
width: 100%;
}
modelview-dom-component .hljs-deletion {
background-color: #600;
display: inline-block;
width: 100%;
}
/*
From https://raw.githubusercontent.com/isagalaev/highlight.js/master/src/styles/vs.css
*/
/*
Visual Studio-like style based on original C# coloring by Jason Diamond <jason@diamond.name>
*/
/*
.vscode-light .hljs-function,
.vscode-light .hljs-params {
color: inherit;
}
.vscode-light .hljs-comment,
.vscode-light .hljs-quote,
.vscode-light .hljs-variable {
color: #008000;
}
.vscode-light .hljs-keyword,
.vscode-light .hljs-selector-tag,
.vscode-light .hljs-built_in,
.vscode-light .hljs-name,
.vscode-light .hljs-tag {
color: #00f;
}
.vscode-light .hljs-string,
.vscode-light .hljs-title,
.vscode-light .hljs-section,
.vscode-light .hljs-attribute,
.vscode-light .hljs-literal,
.vscode-light .hljs-template-tag,
.vscode-light .hljs-template-variable,
.vscode-light .hljs-type,
.vscode-light .hljs-addition {
color: #a31515;
}
.vscode-light .hljs-deletion,
.vscode-light .hljs-selector-attr,
.vscode-light .hljs-selector-pseudo,
.vscode-light .hljs-meta {
color: #2b91af;
}
.vscode-light .hljs-doctag {
color: #808080;
}
.vscode-light .hljs-attr {
color: #f00;
}
.vscode-light .hljs-symbol,
.vscode-light .hljs-bullet,
.vscode-light .hljs-link {
color: #00b0e8;
}
.vscode-light .hljs-emphasis {
font-style: italic;
}
.vscode-light .hljs-strong {
font-weight: bold;
}
*/

View File

@@ -0,0 +1,31 @@
<?xml version='1.0' standalone='no' ?>
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
<style>
circle {
animation: ball 0.6s linear infinite;
}
circle:nth-child(2) { animation-delay: 0.075s; }
circle:nth-child(3) { animation-delay: 0.15s; }
circle:nth-child(4) { animation-delay: 0.225s; }
circle:nth-child(5) { animation-delay: 0.3s; }
circle:nth-child(6) { animation-delay: 0.375s; }
circle:nth-child(7) { animation-delay: 0.45s; }
circle:nth-child(8) { animation-delay: 0.525s; }
@keyframes ball {
from { opacity: 1; }
to { opacity: 0.3; }
}
</style>
<g>
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.modelview-loadingComponent-container {
display: flex;
flex-direction: row;
justify-content: center;
}
.vs .modelview-loadingComponent-spinner {
content: url("loading.svg");
}
.vs-dark .modelview-loadingComponent-spinner,
.hc-black .modelview-loadingComponent-spinner {
content: url("loading_inverse.svg");
}
.modelview-loadingComponent-spinner {
height: 20px;
padding-top: 5px;
padding-bottom: 5px;
}
.modelview-loadingComponent-content-loading {
display: none;
}

View File

@@ -0,0 +1,31 @@
<?xml version='1.0' standalone='no' ?>
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
<style>
circle {
animation: ball 0.6s linear infinite;
}
circle:nth-child(2) { animation-delay: 0.075s; }
circle:nth-child(3) { animation-delay: 0.15s; }
circle:nth-child(4) { animation-delay: 0.225s; }
circle:nth-child(5) { animation-delay: 0.3s; }
circle:nth-child(6) { animation-delay: 0.375s; }
circle:nth-child(7) { animation-delay: 0.45s; }
circle:nth-child(8) { animation-delay: 0.525s; }
@keyframes ball {
from { opacity: 1; }
to { opacity: 0.3; }
}
</style>
<g style="fill:white;">
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,238 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
modelview-dom-component {
font-family: "Segoe WPC", "Segoe UI", "SFUIText-Light", "HelveticaNeue-Light", sans-serif, "Droid Sans Fallback";
font-size: 14px;
padding: 0 26px;
line-height: 22px;
word-wrap: break-word;
}
modelview-dom-component #code-csp-warning {
position: fixed;
top: 0;
right: 0;
color: white;
margin: 16px;
text-align: center;
font-size: 12px;
font-family: sans-serif;
background-color:#444444;
cursor: pointer;
padding: 6px;
box-shadow: 1px 1px 1px rgba(0,0,0,.25);
}
modelview-dom-component #code-csp-warning:hover {
text-decoration: none;
background-color:#007acc;
box-shadow: 2px 2px 2px rgba(0,0,0,.25);
}
modelview-dom-component .scrollBeyondLastLine {
margin-bottom: calc(100vh - 22px);
}
modelview-dom-component .showEditorSelection .code-line {
position: relative;
}
modelview-dom-component .showEditorSelection .code-active-line:before,
modelview-dom-component .showEditorSelection .code-line:hover:before {
content: "";
display: block;
position: absolute;
top: 0;
left: -12px;
height: 100%;
}
modelview-dom-component .showEditorSelection li.code-active-line:before,
modelview-dom-component .showEditorSelection li.code-line:hover:before {
left: -30px;
}
modelview-dom-component .showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(0, 0, 0, 0.15);
}
modelview-dom-component .showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(0, 0, 0, 0.40);
}
modelview-dom-component .showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.vs-dark .monaco-workbench modelview-dom-component .showEditorSelection .code-active-line:before
.hc-black .monaco-workbench modelview-dom-component .showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(255, 255, 255, 0.4);
}
.vs-dark .monaco-workbench modelview-dom-component .showEditorSelection .code-line:hover:before
.hc-black .monaco-workbench modelview-dom-component .showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(255, 255, 255, 0.60);
}
.vs-dark .monaco-workbench modelview-dom-component .showEditorSelection .code-line .code-line:hover:before
.hc-black .monaco-workbench modelview-dom-component .showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.hc-black modelview-dom-component .showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(255, 160, 0, 0.7);
}
.hc-black modelview-dom-component .showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(255, 160, 0, 1);
}
modelview-dom-component .showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
modelview-dom-component img {
max-width: 100%;
max-height: 100%;
}
modelview-dom-component a, modelview-dom-component a:link{
text-decoration: none;
}
modelview-dom-component a:hover, modelview-dom-component a:link {
text-decoration: underline;
}
modelview-dom-component a:focus,
modelview-dom-component input:focus,
modelview-dom-component select:focus,
modelview-dom-component textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
modelview-dom-component hr {
border: 0;
height: 2px;
border-bottom: 2px solid;
}
modelview-dom-component h1 {
padding-bottom: 0.3em;
line-height: 1.2;
border-bottom-width: 1px;
border-bottom-style: solid;
}
modelview-dom-component h1,
modelview-dom-component h2,
modelview-dom-component h3 {
font-weight: normal;
}
modelview-dom-component h1 code,
modelview-dom-component h2 code,
modelview-dom-component h3 code,
modelview-dom-component h4 code,
modelview-dom-component h5 code,
modelview-dom-component h6 code {
font-size: inherit;
line-height: auto;
}
modelview-dom-component table {
border-collapse: collapse;
}
modelview-dom-component table > thead > tr > th {
text-align: left;
border-bottom: 1px solid;
}
modelview-dom-component table > thead > tr > th,
modelview-dom-component table > thead > tr > td,
modelview-dom-component table > tbody > tr > th,
modelview-dom-component table > tbody > tr > td {
padding: 5px 10px;
}
modelview-dom-component table > tbody > tr + tr > td {
border-top: 1px solid;
}
modelview-dom-component blockquote {
margin: 0 7px 0 5px;
padding: 0 16px 0 10px;
border-left-width: 5px;
border-left-style: solid;
}
modelview-dom-component code {
font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
font-size: 14px;
line-height: 19px;
}
modelview-dom-component .wordWrap pre {
white-space: pre-wrap;
}
modelview-dom-component .mac code {
font-size: 12px;
line-height: 18px;
}
modelview-dom-component pre:not(.hljs),
modelview-dom-component pre.hljs code > div {
padding: 16px;
border-radius: 3px;
overflow: auto;
}
/** Theming */
modelview-dom-component pre code {
color: var(--vscode-editor-foreground);
}
modelview-dom-component pre {
background-color: rgba(220, 220, 220, 0.4);
}
.vs-dark .monaco-workbench modelview-dom-component pre {
background-color: rgba(10, 10, 10, 0.4);
}
.hc-black .monaco-workbench modelview-dom-component pre {
background-color: rgb(0, 0, 0);
}
.hc-black .monaco-workbench modelview-dom-component h1 {
border-color: rgb(0, 0, 0);
}
modelview-dom-component table > thead > tr > th {
border-color: rgba(0, 0, 0, 0.69);
}
.vs-dark .monaco-workbench modelview-dom-component table > thead > tr > th {
border-color: rgba(255, 255, 255, 0.69);
}
modelview-dom-component h1,
modelview-dom-component hr,
modelview-dom-component table > tbody > tr + tr > td {
border-color: rgba(0, 0, 0, 0.18);
}
.vs-dark .monaco-workbench modelview-dom-component h1,
.vs-dark .monaco-workbench modelview-dom-component hr,
.vs-dark .monaco-workbench modelview-dom-component table > tbody > tr + tr > td {
border-color: rgba(255, 255, 255, 0.18);
}

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
modelview-content {
height: 100%;
width : 100%;
display: block;
}

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.model-view-container {
height: 100%;
width : 100%;
}

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.modelview-radiobutton-container {
align-items: flex-start;
}
.modelview-radiobutton-item {
align-self: flex-start ;
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.center-align
{
text-align: center;
}
.align-with-header
{
padding-left:3px !important;
}
.no-borders
{
border: none !important
}

View File

@@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.modelview-toolbar-container {
display: flex;
justify-content: flex-start;
line-height: 1.4em;
white-space: nowrap;
flex-wrap: wrap;
}
.modelview-toolbar-container.toolbar-horizontal {
flex-direction: row;
border-bottom-width: .5px;
border-bottom-style: solid;
box-sizing: border-box;
border-bottom-color: rgba(128, 128, 128, 0.35);
}
.modelview-toolbar-container.toolbar-vertical {
flex-direction: column;
}
.modelview-toolbar-container .modelview-toolbar-item {
flex: 0 0;
flex-direction: row;
display: flex;
padding-left: 10px;
}
.modelview-toolbar-container.toolbar-vertical .modelview-toolbar-item {
flex: 0 0;
flex-direction: row;
display: flex;
padding-left: 0px;
}
.modelview-toolbar-container .modelview-toolbar-title {
padding: 4px;
font-size: 13px;
cursor: pointer;
}
.modelview-toolbar-container .modelview-toolbar-component select,
.modelview-toolbar-container .modelview-toolbar-component .monaco-inputbox {
width: 200px;
height: 25px;
}
.modelview-toolbar-container .modelview-toolbar-component modelview-button .monaco-text-button.icon {
padding-left: 15px;
background-size: 11px;
margin-right: 0.3em;
}
/* Vertical button handling */
.modelview-toolbar-container.toolbar-vertical .modelview-toolbar-component modelview-button .monaco-text-button.icon {
padding: 20px 16px 20px 16px;
background-size: 20px;
margin-right: 0.3em;
background-position: 50% 50%;
}
.modelview-toolbar-container .modelview-toolbar-component modelview-button .monaco-text-button.active {
-ms-transform: scale(1.272019649, 1.272019649); /* 1.272019649 = √φ */
-webkit-transform: scale(1.272019649, 1.272019649);
-moz-transform: scale(1.272019649, 1.272019649);
-o-transform: scale(1.272019649, 1.272019649);
transform: scale(1.272019649, 1.272019649);
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.tree-component-node-tile {
display: flex;
}
.tree-component-node-tile .model-view-tree-node-item-icon {
width: 17px;
height: 17px;
flex-shrink: 0;
}
.tree-component-node-tile .model-view-tree-node-item-label {
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
modelview-webview-component {
height: 100%;
width : 100%;
display: block;
}

View File

@@ -0,0 +1,169 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Component, Input, Inject, forwardRef, ComponentFactoryResolver, ViewChild,
ElementRef, OnInit, ChangeDetectorRef, ReflectiveInjector, Injector, ComponentRef
} from '@angular/core';
import { ComponentHostDirective } from 'sql/workbench/parts/dashboard/common/componentHost.directive';
import { error } from 'sql/base/common/log';
import { AngularDisposable } from 'sql/base/node/lifecycle';
import { IComponent, IComponentConfig, IComponentDescriptor, IModelStore, COMPONENT_CONFIG } from './interfaces';
import { Extensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import * as themeColors from 'vs/workbench/common/theme';
import { Registry } from 'vs/platform/registry/common/platform';
import { memoize } from 'vs/base/common/decorators';
import { generateUuid } from 'vs/base/common/uuid';
import { IBootstrapParams } from 'sql/platform/bootstrap/node/bootstrapService';
import { Event } from 'vs/base/common/event';
import { LayoutRequestParams } from 'sql/platform/dialog/dialogContainer.component';
const componentRegistry = <IComponentRegistry>Registry.as(Extensions.ComponentContribution);
export interface ModelComponentParams extends IBootstrapParams {
onLayoutRequested: Event<LayoutRequestParams>;
modelViewId: string;
}
@Component({
selector: 'model-component-wrapper',
template: `
<ng-template component-host>
</ng-template>
`
})
export class ModelComponentWrapper extends AngularDisposable implements OnInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
@memoize
public get guid(): string {
return generateUuid();
}
private _componentInstance: IComponent;
private _modelViewId: string;
@ViewChild(ComponentHostDirective) componentHost: ComponentHostDirective;
constructor(
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
@Inject(forwardRef(() => ElementRef)) private _ref: ElementRef,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeref: ChangeDetectorRef,
@Inject(forwardRef(() => Injector)) private _injector: Injector,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IBootstrapParams) private _params: ModelComponentParams
) {
super();
if (_params && _params.onLayoutRequested) {
this._modelViewId = _params.modelViewId;
_params.onLayoutRequested(layoutParams => {
if (layoutParams && (layoutParams.alwaysRefresh || layoutParams.modelViewId === this._modelViewId)) {
this.layout();
}
});
}
}
ngOnInit() {
let self = this;
this._register(self.themeService.onDidColorThemeChange((event: IColorTheme) => {
self.updateTheme(event);
}));
}
ngAfterViewInit() {
this.updateTheme(this.themeService.getColorTheme());
if (this.componentHost) {
this.loadComponent();
}
this._changeref.detectChanges();
this.layout();
}
public layout(): void {
if (this.componentInstance && this.componentInstance.layout) {
this.componentInstance.layout();
}
}
public get id(): string {
return this._componentInstance.descriptor.id;
}
private get componentConfig(): IComponentConfig {
return {
descriptor: this.descriptor,
modelStore: this.modelStore
};
}
private get componentInstance(): IComponent {
if (!this._componentInstance) {
this.loadComponent();
}
return this._componentInstance;
}
private loadComponent(): void {
if (!this.descriptor || !this.descriptor.type) {
error('No descriptor or type defined for this component');
return;
}
let selector = componentRegistry.getCtorFromId(this.descriptor.type);
if (selector === undefined) {
error('No selector defined for type {0}', this.descriptor.type);
return;
}
let componentFactory = this._componentFactoryResolver.resolveComponentFactory(selector);
let viewContainerRef = this.componentHost.viewContainerRef;
viewContainerRef.clear();
let injector = ReflectiveInjector.resolveAndCreate([{ provide: COMPONENT_CONFIG, useValue: this.componentConfig }], this._injector);
let componentRef: ComponentRef<IComponent>;
try {
componentRef = viewContainerRef.createComponent(componentFactory, 0, injector);
this._componentInstance = componentRef.instance;
this._componentInstance.descriptor = this.descriptor;
this._componentInstance.modelStore = this.modelStore;
this._changeref.detectChanges();
} catch (e) {
error('Error rendering component: {0}', e);
return;
}
let el = <HTMLElement>componentRef.location.nativeElement;
// set widget styles to conform to its box
el.style.overflow = 'hidden';
el.style.position = 'relative';
}
private updateTheme(theme: IColorTheme): void {
// TODO handle theming appropriately
let el = <HTMLElement>this._ref.nativeElement;
let borderColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true);
let backgroundColor = theme.getColor(colors.editorBackground, true);
let foregroundColor = theme.getColor(themeColors.SIDE_BAR_FOREGROUND, true);
let border = theme.getColor(colors.contrastBorder, true);
if (backgroundColor) {
el.style.backgroundColor = backgroundColor.toString();
}
if (foregroundColor) {
el.style.color = foregroundColor.toString();
}
}
}

View File

@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IModelStore, IComponentDescriptor, IComponent } from './interfaces';
import { Deferred } from 'sql/base/common/promise';
import { entries } from 'sql/base/common/objects';
class ComponentDescriptor implements IComponentDescriptor {
constructor(public readonly id: string, public readonly type: string) {
}
}
export class ModelStore implements IModelStore {
private static baseId = 0;
private _descriptorMappings: { [x: string]: IComponentDescriptor } = {};
private _componentMappings: { [x: string]: IComponent } = {};
private _componentActions: { [x: string]: Deferred<IComponent> } = {};
private _validationCallbacks: ((componentId: string) => Thenable<boolean>)[] = [];
constructor() {
}
public createComponentDescriptor(type: string, id: string): IComponentDescriptor {
let descriptor = new ComponentDescriptor(id, type);
this._descriptorMappings[id] = descriptor;
return descriptor;
}
getComponentDescriptor(id: string): IComponentDescriptor {
return this._descriptorMappings[id];
}
registerComponent(component: IComponent): void {
let id = component.descriptor.id;
this._componentMappings[id] = component;
this.runPendingActions(id, component);
}
unregisterComponent(component: IComponent): void {
let id = component.descriptor.id;
this._componentMappings[id] = undefined;
this._componentActions[id] = undefined;
this._descriptorMappings[id] = undefined;
// TODO notify model for cleanup
}
getComponent(componentId: string): IComponent {
return this._componentMappings[componentId];
}
eventuallyRunOnComponent<T>(componentId: string, action: (component: IComponent) => T): Promise<T> {
let component = this.getComponent(componentId);
if (component) {
return Promise.resolve(action(component));
} else {
return this.addPendingAction(componentId, action);
}
}
registerValidationCallback(callback: (componentId: string) => Thenable<boolean>): void {
this._validationCallbacks.push(callback);
}
validate(component: IComponent): Thenable<boolean> {
let componentId = entries(this._componentMappings).find(([id, mappedComponent]) => component === mappedComponent)[0];
return Promise.all(this._validationCallbacks.map(callback => callback(componentId))).then(validations => validations.every(validation => validation === true));
}
private addPendingAction<T>(componentId: string, action: (component: IComponent) => T): Promise<T> {
// We create a promise and chain it onto a tracking promise whose resolve method
// will only be called once the component is created
let deferredPromise = this._componentActions[componentId];
if (!deferredPromise) {
deferredPromise = new Deferred();
this._componentActions[componentId] = deferredPromise;
}
let promise = deferredPromise.promise.then((component) => {
return action(component);
});
return promise;
}
private runPendingActions(componentId: string, component: IComponent) {
let promiseTracker = this._componentActions[componentId];
if (promiseTracker) {
promiseTracker.resolve(component);
}
}
}

View File

@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Component, forwardRef, Input, OnInit, Inject, ChangeDetectorRef } from '@angular/core';
import { Event, Emitter } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { addDisposableListener, EventType } from 'vs/base/browser/dom';
import { memoize } from 'vs/base/common/decorators';
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
import { IModelView } from 'sql/platform/model/common/modelViewService';
import { ViewBase } from 'sql/workbench/electron-browser/modelComponents/viewBase';
import { IModelViewService } from 'sql/platform/modelComponents/common/modelViewService';
import * as azdata from 'azdata';
@Component({
selector: 'modelview-content',
template: `
<div *ngIf="rootDescriptor" style="width: 100%; height: 100%;">
<model-component-wrapper style="display: block; height: 100%" [descriptor]="rootDescriptor" [modelStore]="modelStore">
</model-component-wrapper>
</div>
`
})
export class ModelViewContent extends ViewBase implements OnInit, IModelView {
@Input() private modelViewId: string;
private _onResize = new Emitter<void>();
public readonly onResize: Event<void> = this._onResize.event;
private _onMessage = new Emitter<string>();
public readonly onMessage: Event<string> = this._onMessage.event;
private _onMessageDisposable: IDisposable;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(IModelViewService) private modelViewService: IModelViewService
) {
super(changeRef);
}
ngOnInit() {
this.modelViewService.registerModelView(this);
this._register(addDisposableListener(window, EventType.RESIZE, e => {
this.layout();
}));
}
ngOnDestroy() {
this._onDestroy.fire();
super.ngOnDestroy();
}
public layout(): void {
this.changeRef.detectChanges();
}
public get id(): string {
return this.modelViewId;
}
@memoize
public get connection(): azdata.connection.Connection {
if (!this._commonService.connectionManagementService || !this._commonService.connectionManagementService.connectionInfo) {
return undefined;
}
let currentConnection = this._commonService.connectionManagementService.connectionInfo.connectionProfile;
let connection: azdata.connection.Connection = {
providerName: currentConnection.providerName,
connectionId: currentConnection.id,
options: currentConnection.options
};
return connection;
}
@memoize
public get serverInfo(): azdata.ServerInfo {
if (!this._commonService.connectionManagementService || !this._commonService.connectionManagementService.connectionInfo) {
return undefined;
}
return this._commonService.connectionManagementService.connectionInfo.serverInfo;
}
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ModelViewInput } from 'sql/workbench/electron-browser/modelComponents/modelViewInput';
import { ModelViewEditor } from 'sql/workbench/electron-browser/modelComponents/modelViewEditor';
// Model View editor registration
const viewModelEditorDescriptor = new EditorDescriptor(
ModelViewEditor,
ModelViewEditor.ID,
'ViewModel'
);
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
.registerEditor(viewModelEditorDescriptor, [new SyncDescriptor(ModelViewInput)]);

View File

@@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* 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/modelViewEditor';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorOptions } from 'vs/workbench/common/editor';
import * as DOM from 'vs/base/browser/dom';
import { ModelViewInput } from 'sql/workbench/electron-browser/modelComponents/modelViewInput';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService } from 'vs/platform/storage/common/storage';
export class ModelViewEditor extends BaseEditor {
public static ID: string = 'workbench.editor.modelViewEditor';
private _editorFrame: HTMLElement;
private _content: HTMLElement;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService
) {
super(ModelViewEditor.ID, telemetryService, themeService, storageService);
}
/**
* Called to create the editor in the parent element.
*/
public createEditor(parent: HTMLElement): void {
this._editorFrame = parent;
this._content = document.createElement('div');
parent.appendChild(this._content);
}
/**
* Sets focus on this editor. Specifically, it sets the focus on the hosted text editor.
*/
public focus(): void {
}
public clearInput() {
this.hideOrRemoveModelViewContainer();
super.clearInput();
}
private hideOrRemoveModelViewContainer() {
if (this.input instanceof ModelViewInput) {
if (this.input.container) {
if (this.input.options && this.input.options.retainContextWhenHidden) {
this.input.container.style.visibility = 'hidden';
} else {
this.input.removeModelViewContainer();
this.input.container.style.visibility = 'hidden';
}
}
}
}
async setInput(input: ModelViewInput, options?: EditorOptions): Promise<void> {
if (this.input && this.input.matches(input)) {
return Promise.resolve(undefined);
}
this.hideOrRemoveModelViewContainer();
input.appendModelViewContainer();
input.container.style.visibility = 'visible';
this._content.setAttribute('aria-flowto', input.container.id);
await super.setInput(input, options, CancellationToken.None);
this.doUpdateContainer();
}
private doUpdateContainer() {
let modelViewInput = this.input as ModelViewInput;
const modelViewContainer = modelViewInput && modelViewInput.container;
if (modelViewContainer) {
const frameRect = this._editorFrame.getBoundingClientRect();
const containerRect = modelViewContainer.parentElement.getBoundingClientRect();
modelViewContainer.style.position = 'absolute';
modelViewContainer.style.top = `${frameRect.top}px`;
modelViewContainer.style.left = `${frameRect.left - containerRect.left}px`;
modelViewContainer.style.width = `${frameRect.width}px`;
modelViewContainer.style.height = `${frameRect.height}px`;
if (modelViewInput.dialogPane) {
modelViewInput.dialogPane.layout(true);
}
}
}
/**
* 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 {
if (this.input instanceof ModelViewInput) {
if (this.input.container && this.input.dialogPane) {
this.doUpdateContainer();
// todo: layout this.input.dialogPane (Github issue: #1484)
}
}
}
}

View File

@@ -0,0 +1,160 @@
/*---------------------------------------------------------------------------------------------
* 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 { IEditorModel } from 'vs/platform/editor/common/editor';
import { EditorInput, EditorModel, ConfirmResult } from 'vs/workbench/common/editor';
import * as DOM from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { DialogPane } from 'sql/platform/dialog/dialogPane';
import { Emitter, Event } from 'vs/base/common/event';
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
export type ModeViewSaveHandler = (handle: number) => Thenable<boolean>;
export class ModelViewInputModel extends EditorModel {
private dirty: boolean;
private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
get onDidChangeDirty(): Event<void> { return this._onDidChangeDirty.event; }
constructor(public readonly modelViewId, private readonly handle: number, private saveHandler?: ModeViewSaveHandler) {
super();
this.dirty = false;
}
get isDirty(): boolean {
return this.dirty;
}
public setDirty(dirty: boolean): void {
if (this.dirty === dirty) {
return;
}
this.dirty = dirty;
this._onDidChangeDirty.fire();
}
save(): Promise<boolean> {
if (this.saveHandler) {
return Promise.resolve(this.saveHandler(this.handle));
}
return Promise.resolve(true);
}
}
export class ModelViewInput extends EditorInput {
public static ID: string = 'workbench.editorinputs.ModelViewEditorInput';
private _container: HTMLElement;
private _dialogPaneContainer: HTMLElement;
private _dialogPane: DialogPane;
constructor(private _title: string, private _model: ModelViewInputModel,
private _options: azdata.ModelViewEditorOptions,
@IInstantiationService private _instantiationService: IInstantiationService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService
) {
super();
this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire());
this._container = document.createElement('div');
this._container.id = `modelView-${_model.modelViewId}`;
this.layoutService.getContainer(Parts.EDITOR_PART).appendChild(this._container);
}
public get title(): string {
return this._title;
}
public get modelViewId(): string {
return this._model.modelViewId;
}
public getTypeId(): string {
return 'ModelViewEditorInput';
}
public resolve(refresh?: boolean): Promise<IEditorModel> {
return undefined;
}
public getName(): string {
return this._title;
}
public get container(): HTMLElement {
return this._container;
}
public appendModelViewContainer(): void {
if (!this._dialogPane) {
this.createDialogPane();
}
if (!this._container.contains(this._dialogPaneContainer)) {
this._container.appendChild(this._dialogPaneContainer);
}
}
public removeModelViewContainer(): void {
if (this._dialogPaneContainer) {
this._container.removeChild(this._dialogPaneContainer);
}
}
private createDialogPane(): void {
this._dialogPaneContainer = DOM.$('div.model-view-container');
this._dialogPane = new DialogPane(this.title, this.modelViewId, () => undefined, this._instantiationService, false);
this._dialogPane.createBody(this._dialogPaneContainer);
}
public get dialogPane(): DialogPane {
return this._dialogPane;
}
public get options(): azdata.ModelViewEditorOptions {
return this._options;
}
/**
* An editor that is dirty will be asked to be saved once it closes.
*/
isDirty(): boolean {
return this._model.isDirty;
}
/**
* Subclasses should bring up a proper dialog for the user if the editor is dirty and return the result.
*/
confirmSave(): Promise<ConfirmResult> {
// TODO #2530 support save on close / confirm save. This is significantly more work
// as we need to either integrate with textFileService (seems like this isn't viable)
// or register our own complimentary service that handles the lifecycle operations such
// as close all, auto save etc.
return Promise.resolve(ConfirmResult.DONT_SAVE);
}
/**
* Saves the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation.
*/
save(): Promise<boolean> {
return this._model.save();
}
public dispose(): void {
if (this._dialogPane) {
this._dialogPane.dispose();
}
if (this._container) {
this._container.remove();
this._container = undefined;
}
if (this._model) {
this._model.dispose();
}
super.dispose();
}
}

View File

@@ -0,0 +1,205 @@
/*---------------------------------------------------------------------------------------------
* 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, IEditorConfiguration } 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 { CancellationToken } from 'vs/base/common/cancellation';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Configuration } from 'vs/editor/browser/config/configuration';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
/**
* Extension of TextResourceEditor that is always readonly rather than only with non UntitledInputs
*/
export class QueryTextEditor extends BaseTextEditor {
public static ID = 'modelview.editors.textEditor';
private _dimension: DOM.Dimension;
private _config: editorCommon.IConfiguration;
private _minHeight: number = 0;
private _maxHeight: number = 4000;
private _selected: boolean;
private _hideLineNumbers: boolean;
private _editorWorkspaceConfig;
private _scrollbarHeight: number;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IInstantiationService instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService,
@ITextResourceConfigurationService configurationService: ITextResourceConfigurationService,
@IThemeService themeService: IThemeService,
@ITextFileService textFileService: ITextFileService,
@IEditorGroupsService editorGroupService: IEditorGroupsService,
@IEditorService protected editorService: IEditorService,
@IWindowService windowService: IWindowService,
@IConfigurationService private workspaceConfigurationService: IConfigurationService,
@IAccessibilityService private accessibilityService: IAccessibilityService
) {
super(
QueryTextEditor.ID, telemetryService, instantiationService, storageService,
configurationService, themeService, textFileService, editorService, editorGroupService, windowService);
}
public createEditorControl(parent: HTMLElement, configuration: IEditorOptions): editorCommon.IEditor {
return this.instantiationService.createInstance(StandaloneCodeEditor, parent, configuration);
}
protected getConfigurationOverrides(): IEditorOptions {
const options = super.getConfigurationOverrides();
if (this.input) {
options.inDiffEditor = true;
options.scrollBeyondLastLine = false;
options.folding = false;
options.renderIndentGuides = false;
options.rulers = [];
options.glyphMargin = true;
options.minimap = {
enabled: false
};
options.overviewRulerLanes = 0;
options.overviewRulerBorder = false;
options.hideCursorInOverviewRuler = true;
if (!this._selected) {
options.renderLineHighlight = 'none';
options.parameterHints = { enabled: false };
options.matchBrackets = false;
}
if (this._hideLineNumbers) {
options.lineNumbers = 'off';
}
}
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('queryTextEditorAriaLabel', 'modelview code editor for view model.');
}
public layout(dimension?: DOM.Dimension) {
if (dimension) {
this._dimension = dimension;
}
this.getControl().layout(dimension);
}
public setWidth(width: number) {
if (this._dimension) {
this._dimension.width = width;
this.layout();
}
}
public setHeight(height: number) {
if (this._dimension) {
this._dimension.height = height;
this.layout(this._dimension);
}
}
public get scrollHeight(): number {
let editorWidget = this.getControl() as ICodeEditor;
return editorWidget.getScrollHeight();
}
public setHeightToScrollHeight(configChanged?: boolean): void {
let editorWidget = this.getControl() as ICodeEditor;
if (!this._config) {
this._config = new Configuration(true, undefined, editorWidget.getDomNode(), this.accessibilityService);
this._scrollbarHeight = this._config.editor.viewInfo.scrollbar.horizontalScrollbarSize;
}
let editorWidgetModel = editorWidget.getModel();
if (!editorWidgetModel) {
// Not ready yet
return;
}
let lineCount = editorWidgetModel.getLineCount();
// Need to also keep track of lines that wrap; if we just keep into account line count, then the editor's height would not be
// tall enough and we would need to show a scrollbar. Unfortunately, it looks like there isn't any metadata saved in a ICodeEditor
// around max column length for an editor (which we could leverage to see if we need to loop through every line to determine
// number of lines that wrap). Finally, viewportColumn is calculated on editor resizing automatically; we can use it to ensure
// that the viewportColumn will always be greater than any character's column in an editor.
let numberWrappedLines = 0;
let shouldAddHorizontalScrollbarHeight = false;
if (!this._editorWorkspaceConfig || configChanged) {
this._editorWorkspaceConfig = this.workspaceConfigurationService.getValue('editor');
}
let wordWrapEnabled: boolean = this._editorWorkspaceConfig && this._editorWorkspaceConfig['wordWrap'] && this._editorWorkspaceConfig['wordWrap'] === 'on' ? true : false;
if (wordWrapEnabled) {
for (let line = 1; line <= lineCount; line++) {
// 4 columns is equivalent to the viewport column width and the edge of the editor
if (editorWidgetModel.getLineMaxColumn(line) >= this._config.editor.layoutInfo.viewportColumn + 4) {
numberWrappedLines += Math.ceil(editorWidgetModel.getLineMaxColumn(line) / this._config.editor.layoutInfo.viewportColumn);
}
}
} else {
for (let line = 1; line <= lineCount; line++) {
// The horizontal scrollbar always appears 1 column past the viewport column when word wrap is disabled
if (editorWidgetModel.getLineMaxColumn(line) >= this._config.editor.layoutInfo.viewportColumn + 1) {
shouldAddHorizontalScrollbarHeight = true;
break;
}
}
}
let editorHeightUsingLines = this._config.editor.lineHeight * (lineCount + numberWrappedLines);
let editorHeightUsingMinHeight = Math.max(Math.min(editorHeightUsingLines, this._maxHeight), this._minHeight);
editorHeightUsingMinHeight = shouldAddHorizontalScrollbarHeight ? editorHeightUsingMinHeight + this._scrollbarHeight : editorHeightUsingMinHeight;
this.setHeight(editorHeightUsingMinHeight);
}
public setMinimumHeight(height: number): void {
this._minHeight = height;
}
public setMaximumHeight(height: number): void {
this._maxHeight = height;
}
public toggleEditorSelected(selected: boolean): void {
this._selected = selected;
this.refreshEditorConfiguration();
}
public set hideLineNumbers(value: boolean) {
this._hideLineNumbers = value;
this.refreshEditorConfiguration();
}
private refreshEditorConfiguration(configuration = this.configurationService.getValue<IEditorConfiguration>(this.getResource())): void {
if (!this.getControl()) {
return;
}
const editorConfiguration = this.computeConfiguration(configuration);
let editorSettingsToApply = editorConfiguration;
this.getControl().updateOptions(editorSettingsToApply);
}
}

View File

@@ -0,0 +1,118 @@
/*---------------------------------------------------------------------------------------------
* 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/radioButton';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
ViewChild, ElementRef, OnDestroy, AfterViewInit
} from '@angular/core';
import * as azdata from 'azdata';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { RadioButton } from 'sql/base/browser/ui/radioButton/radioButton';
@Component({
selector: 'modelview-radioButton',
template: `
<div #input class="modelview-radiobutton-container">
</div>
`
})
export default class RadioButtonComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _input: RadioButton;
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
if (this._inputContainer) {
this._input = new RadioButton(this._inputContainer.nativeElement, {
label: this.label
});
this._register(this._input);
this._register(this._input.onClicked(e => {
this.checked = this._input.checked;
this.fireEvent({
eventType: ComponentEventType.onDidClick,
args: e
});
}));
}
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this._input.name = this.name;
this._input.value = this.value;
this._input.label = this.label;
this._input.enabled = this.enabled;
this._input.checked = this.checked;
}
// CSS-bound properties
public get checked(): boolean {
return this.getPropertyOrDefault<azdata.RadioButtonProperties, boolean>((props) => props.checked, false);
}
public set checked(newValue: boolean) {
this.setPropertyFromUI<azdata.RadioButtonProperties, boolean>((properties, value) => { properties.checked = value; }, newValue);
}
public set value(newValue: string) {
this.setPropertyFromUI<azdata.RadioButtonProperties, string>((properties, value) => { properties.value = value; }, newValue);
}
public get value(): string {
return this.getPropertyOrDefault<azdata.RadioButtonProperties, string>((props) => props.value, '');
}
public getLabel(): string {
return this.label;
}
public get label(): string {
return this.getPropertyOrDefault<azdata.RadioButtonProperties, string>((props) => props.label, '');
}
public set label(newValue: string) {
this.setPropertyFromUI<azdata.RadioButtonProperties, string>((properties, label) => { properties.label = label; }, newValue);
}
public get name(): string {
return this.getPropertyOrDefault<azdata.RadioButtonProperties, string>((props) => props.name, '');
}
public set name(newValue: string) {
this.setPropertyFromUI<azdata.RadioButtonProperties, string>((properties, label) => { properties.name = label; }, newValue);
}
}

View File

@@ -0,0 +1,174 @@
/*---------------------------------------------------------------------------------------------
* 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/flexContainer';
import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, OnDestroy } from '@angular/core';
import { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { FlexItemLayout, SplitViewLayout } from 'azdata';
import { FlexItem } from './flexContainer.component';
import { ContainerBase, ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { Event } from 'vs/base/common/event';
import { SplitView, Orientation, Sizing, IView } from 'vs/base/browser/ui/splitview/splitview';
class SplitPane implements IView {
orientation: Orientation;
element: HTMLElement;
minimumSize: number;
maximumSize: number;
onDidChange: Event<number> = Event.None;
size: number;
component: ComponentBase;
layout(size: number): void {
this.size = size;
try {
if (this.orientation === Orientation.VERTICAL) {
this.component.updateProperty('height', size);
}
else {
this.component.updateProperty('width', size);
}
} catch { }
}
}
@Component({
template: `
<div *ngIf="items" class="splitViewContainer" [style.flexFlow]="flexFlow" [style.justifyContent]="justifyContent" [style.position]="position"
[style.alignItems]="alignItems" [style.alignContent]="alignContent" [style.height]="height" [style.width]="width">
<div *ngFor="let item of items" [style.flex]="getItemFlex(item)" [style.textAlign]="textAlign" [style.order]="getItemOrder(item)" [ngStyle]="getItemStyles(item)">
<model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore">
</model-component-wrapper>
</div>
</div>
`
})
export default class SplitViewContainer extends ContainerBase<FlexItemLayout> implements IComponent, OnDestroy {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _flexFlow: string;
private _justifyContent: string;
private _alignItems: string;
private _alignContent: string;
private _textAlign: string;
private _height: string;
private _width: string;
private _position: string;
private _splitView: SplitView;
private _orientation: Orientation;
private _splitViewHeight: number;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
super(changeRef, el);
this._flexFlow = ''; // default
this._justifyContent = ''; // default
this._orientation = Orientation.VERTICAL; // default
}
ngOnInit(): void {
this.baseInit();
}
ngOnDestroy(): void {
this.baseDestroy();
}
ngAfterViewInit(): void {
this._splitView = this._register(new SplitView(this._el.nativeElement, { orientation: this._orientation }));
}
private GetCorrespondingView(component: IComponent, orientation: Orientation): IView {
let c = component as ComponentBase;
let basicView: SplitPane = new SplitPane();
basicView.orientation = orientation;
basicView.element = c.getHtml(),
basicView.component = c;
basicView.minimumSize = 50;
basicView.maximumSize = Number.MAX_VALUE;
return basicView;
}
/// IComponent implementation
public setLayout(layout: SplitViewLayout): void {
this._flexFlow = layout.flexFlow ? layout.flexFlow : '';
this._justifyContent = layout.justifyContent ? layout.justifyContent : '';
this._alignItems = layout.alignItems ? layout.alignItems : '';
this._alignContent = layout.alignContent ? layout.alignContent : '';
this._textAlign = layout.textAlign ? layout.textAlign : '';
this._position = layout.position ? layout.position : '';
this._height = this.convertSize(layout.height);
this._width = this.convertSize(layout.width);
this._orientation = layout.orientation.toLowerCase() === 'vertical' ? Orientation.VERTICAL : Orientation.HORIZONTAL;
this._splitViewHeight = this.convertSizeToNumber(layout.splitViewHeight);
if (this._componentWrappers) {
this._componentWrappers.forEach(item => {
let component = item.modelStore.getComponent(item.descriptor.id);
item.modelStore.validate(component).then(value => {
if (value === true) {
let view = this.GetCorrespondingView(component, this._orientation);
this._splitView.addView(view, Sizing.Distribute);
}
else {
console.log('Could not add views inside split view container');
}
});
});
}
this._splitView.layout(this._splitViewHeight);
}
// CSS-bound properties
public get flexFlow(): string {
return this._flexFlow;
}
public get justifyContent(): string {
return this._justifyContent;
}
public get alignItems(): string {
return this._alignItems;
}
public get height(): string {
return this._height;
}
public get width(): string {
return this._width;
}
public get alignContent(): string {
return this._alignContent;
}
public get textAlign(): string {
return this._textAlign;
}
public get position(): string {
return this._position;
}
public get orientation(): string {
return this._orientation.toString();
}
private getItemFlex(item: FlexItem): string {
return item.config ? item.config.flex : '1 1 auto';
}
private getItemOrder(item: FlexItem): number {
return item.config ? item.config.order : 0;
}
private getItemStyles(item: FlexItem): { [key: string]: string } {
return item.config && item.config.CSSStyles ? item.config.CSSStyles : {};
}
}

View File

@@ -0,0 +1,202 @@
/*---------------------------------------------------------------------------------------------
* 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/table';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ViewChild, ElementRef, OnDestroy, AfterViewInit
} from '@angular/core';
import * as azdata from 'azdata';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { Table } from 'sql/base/browser/ui/table/table';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { getContentHeight, getContentWidth, Dimension } from 'vs/base/browser/dom';
import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin';
@Component({
selector: 'modelview-table',
template: `
<div #table style="width: 100%;height:100%" [style.font-size]="fontSize"></div>
`
})
export default class TableComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _table: Table<Slick.SlickData>;
private _tableData: TableDataView<Slick.SlickData>;
private _tableColumns;
@ViewChild('table', { read: ElementRef }) private _inputContainer: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(forwardRef(() => ElementRef)) el: ElementRef) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
transformColumns(columns: string[] | azdata.TableColumn[]): Slick.Column<any>[] {
let tableColumns: any[] = <any[]>columns;
if (tableColumns) {
return (<any[]>columns).map(col => {
if (col.value) {
return <Slick.Column<any>>{
name: col.value,
id: col.value,
field: col.value,
width: col.width,
cssClass: col.cssClass,
headerCssClass: col.headerCssClass,
toolTip: col.toolTip
};
} else {
return <Slick.Column<any>>{
name: <string>col,
id: <string>col,
field: <string>col
};
}
});
} else {
return (<string[]>columns).map(col => {
return <Slick.Column<any>>{
name: col,
id: col,
field: col
};
});
}
}
public static transformData(rows: string[][], columns: any[]): { [key: string]: string }[] {
if (rows && columns) {
return rows.map(row => {
let object: { [key: string]: string } = {};
if (row.forEach) {
row.forEach((val, index) => {
let columnName: string = (columns[index].value) ? columns[index].value : <string>columns[index];
object[columnName] = val;
});
}
return object;
});
} else {
return [];
}
}
ngAfterViewInit(): void {
if (this._inputContainer) {
this._tableData = new TableDataView<Slick.SlickData>();
let options = <Slick.GridOptions<any>>{
syncColumnCellResize: true,
enableColumnReorder: false,
enableCellNavigation: true,
forceFitColumns: true
};
this._table = new Table<Slick.SlickData>(this._inputContainer.nativeElement, { dataProvider: this._tableData, columns: this._tableColumns }, options);
this._table.setData(this._tableData);
this._table.setSelectionModel(new RowSelectionModel({ selectActiveRow: true }));
this._register(this._table);
this._register(attachTableStyler(this._table, this.themeService));
this._register(this._table.onSelectedRowsChanged((e, data) => {
this.selectedRows = data.rows;
this.fireEvent({
eventType: ComponentEventType.onSelectedRowChanged,
args: e
});
}));
}
}
public validate(): Thenable<boolean> {
return super.validate().then(valid => {
// TODO: table validation?
return valid;
});
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public layout(): void {
this.layoutTable();
super.layout();
}
private layoutTable(): void {
let width: number = this.convertSizeToNumber(this.width);
let height: number = this.convertSizeToNumber(this.height);
this._table.layout(new Dimension(
width && width > 0 ? width : getContentWidth(this._inputContainer.nativeElement),
height && height > 0 ? height : getContentHeight(this._inputContainer.nativeElement)));
}
public setLayout(): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this._tableData.clear();
this._tableData.push(TableComponent.transformData(this.data, this.columns));
this._tableColumns = this.transformColumns(this.columns);
this._table.columns = this._tableColumns;
this._table.setData(this._tableData);
if (this.selectedRows) {
this._table.setSelectedRows(this.selectedRows);
}
this.layoutTable();
this.validate();
}
// CSS-bound properties
public get data(): any[][] {
return this.getPropertyOrDefault<azdata.TableComponentProperties, any[]>((props) => props.data, []);
}
public set data(newValue: any[][]) {
this.setPropertyFromUI<azdata.TableComponentProperties, any[][]>((props, value) => props.data = value, newValue);
}
public get columns(): string[] {
return this.getPropertyOrDefault<azdata.TableComponentProperties, string[]>((props) => props.columns, []);
}
public get fontSize(): number | string {
return this.getPropertyOrDefault<azdata.TableComponentProperties, number | string>((props) => props.fontSize, '');
}
public set columns(newValue: string[]) {
this.setPropertyFromUI<azdata.TableComponentProperties, string[]>((props, value) => props.columns = value, newValue);
}
public get selectedRows(): number[] {
return this.getPropertyOrDefault<azdata.TableComponentProperties, number[]>((props) => props.selectedRows, []);
}
public set selectedRows(newValue: number[]) {
this.setPropertyFromUI<azdata.TableComponentProperties, number[]>((props, value) => props.selectedRows = value, newValue);
}
}

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.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/radioButton';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
OnDestroy, AfterViewInit, ElementRef, SecurityContext
} from '@angular/core';
import * as azdata from 'azdata';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { SafeHtml, DomSanitizer } from '@angular/platform-browser';
@Component({
selector: 'modelview-text',
template: `
<p [style.width]="getWidth()" [innerHTML]="getValue()"></p>`
})
export default class TextComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(forwardRef(() => DomSanitizer)) private _domSanitizer: DomSanitizer) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public set value(newValue: string) {
this.setPropertyFromUI<azdata.TextComponentProperties, string>((properties, value) => { properties.value = value; }, newValue);
}
public get value(): string {
return this.getPropertyOrDefault<azdata.TextComponentProperties, string>((props) => props.value, '');
}
public getValue(): SafeHtml {
let links = this.getPropertyOrDefault<azdata.TextComponentProperties, azdata.LinkArea[]>((props) => props.links, []);
let text = this._domSanitizer.sanitize(SecurityContext.HTML, this.value);
if (links.length !== 0) {
for (let i: number = 0; i < links.length; i++) {
let link = links[i];
let linkTag = `<a href="${this._domSanitizer.sanitize(SecurityContext.URL, link.url)}" tabIndex="0" target="blank">${this._domSanitizer.sanitize(SecurityContext.HTML, link.text)}</a>`;
text = text.replace(`{${i}}`, linkTag);
}
}
return text;
}
}

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/toolbarLayout';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
ViewChild, ElementRef, OnDestroy, AfterViewInit
} from '@angular/core';
import { Orientation, ToolbarLayout } from 'sql/workbench/api/common/sqlExtHostTypes';
import { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { ContainerBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
export interface ToolbarItemConfig {
title?: string;
toolbarSeparatorAfter?: boolean;
}
export class ToolbarItem {
constructor(public descriptor: IComponentDescriptor, public config: ToolbarItemConfig) { }
}
@Component({
selector: 'modelview-toolbarContainer',
template: `
<div #container *ngIf="items" [class]="toolbarClass" >
<ng-container *ngFor="let item of items">
<div class="modelview-toolbar-item" [style.paddingTop]="paddingTop">
<div *ngIf="shouldShowTitle(item)" class="modelview-toolbar-title" >
{{getItemTitle(item)}}
</div>
<div class="modelview-toolbar-component">
<model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore" >
</model-component-wrapper>
</div>
<div *ngIf="shouldShowToolbarSeparator(item)" class="taskbarSeparator" >
</div>
</div>
</ng-container>
</div>
`
})
export default class ToolbarContainer extends ContainerBase<ToolbarItemConfig> implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
@ViewChild('container', { read: ElementRef }) private _container: ElementRef;
private _orientation: Orientation;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef) {
super(changeRef, el);
this._orientation = Orientation.Horizontal;
}
ngOnInit(): void {
this.baseInit();
}
ngOnDestroy(): void {
this.baseDestroy();
}
ngAfterViewInit(): void {
}
/// IComponent implementation
public setLayout(layout: ToolbarLayout): void {
this._orientation = layout.orientation ? layout.orientation : Orientation.Horizontal;
this.layout();
}
public getItemTitle(item: ToolbarItem): string {
let itemConfig = item.config;
return itemConfig ? itemConfig.title : '';
}
public shouldShowTitle(item: ToolbarItem): boolean {
return this.hasTitle(item) && this.isHorizontal();
}
public shouldShowToolbarSeparator(item: ToolbarItem): boolean {
if (!item || !item.config) {
return false;
}
return item.config.toolbarSeparatorAfter;
}
private hasTitle(item: ToolbarItem): boolean {
return item && item.config && item.config.title !== undefined;
}
public get paddingTop(): string {
return this.isHorizontal() ? '' : '';
}
public get toolbarClass(): string {
let classes = ['modelview-toolbar-container'];
if (this.isHorizontal()) {
classes.push('toolbar-horizontal');
} else {
classes.push('toolbar-vertical');
}
return classes.join(' ');
}
private isHorizontal(): boolean {
return this._orientation === Orientation.Horizontal;
}
}

View File

@@ -0,0 +1,163 @@
/*---------------------------------------------------------------------------------------------
* 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/treeComponent';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
ViewChild, ElementRef, OnDestroy, AfterViewInit
} from '@angular/core';
import * as azdata from 'azdata';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { CommonServiceInterface } from 'sql/platform/bootstrap/node/commonServiceInterface.service';
import { TreeComponentRenderer } from 'sql/workbench/electron-browser/modelComponents/treeComponentRenderer';
import { TreeComponentDataSource } from 'sql/workbench/electron-browser/modelComponents/treeDataSource';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { DefaultFilter, DefaultAccessibilityProvider, DefaultController } from 'vs/base/parts/tree/browser/treeDefaults';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITreeComponentItem } from 'sql/workbench/common/views';
import { TreeViewDataProvider } from 'sql/workbench/electron-browser/modelComponents/treeViewDataProvider';
import { getContentHeight, getContentWidth } from 'vs/base/browser/dom';
class Root implements ITreeComponentItem {
label = {
label: 'root'
};
handle = '0';
parentHandle = null;
collapsibleState = 2;
children = void 0;
options = undefined;
}
@Component({
selector: 'modelview-tree',
template: `
<div #input style="width: 100%;height:100%"></div>
`
})
export default class TreeComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _tree: Tree;
private _treeRenderer: TreeComponentRenderer;
private _dataProvider: TreeViewDataProvider;
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IContextViewService) private contextViewService: IContextViewService,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
@Inject(forwardRef(() => ElementRef)) el: ElementRef
) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
if (this._inputContainer) {
this.createTreeControl();
}
}
ngOnDestroy(): void {
this.baseDestroy();
}
public setDataProvider(handle: number, componentId: string, context: any): any {
this._dataProvider = new TreeViewDataProvider(handle, componentId, context);
this.createTreeControl();
}
public refreshDataProvider(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeComponentItem }): void {
if (this._dataProvider) {
this._dataProvider.getItemsToRefresh(itemsToRefreshByHandle);
}
if (this._tree) {
for (const item of Object.values(itemsToRefreshByHandle)) {
this._tree.refresh(<ITreeComponentItem>item);
}
}
}
private createTreeControl(): void {
if (!this._tree && this._dataProvider) {
const dataSource = this._instantiationService.createInstance(TreeComponentDataSource, this._dataProvider);
const renderer = this._instantiationService.createInstance(TreeComponentRenderer, this._dataProvider, this.themeService, { withCheckbox: this.withCheckbox });
this._treeRenderer = renderer;
const controller = new DefaultController();
const filter = new DefaultFilter();
const sorter = undefined;
const dnd = undefined;
const accessibilityProvider = new DefaultAccessibilityProvider();
this._tree = new Tree(this._inputContainer.nativeElement,
{ dataSource, renderer, controller, dnd, filter, sorter, accessibilityProvider },
{
indentPixels: 10,
twistiePixels: 20,
ariaLabel: 'Tree Node'
});
this._tree.setInput(new Root());
this._tree.domFocus();
this._register(this._tree);
this._register(attachListStyler(this._tree, this.themeService));
this._register(this._tree.onDidChangeSelection(e => {
this._dataProvider.onNodeSelected(e.selection);
}));
this._tree.refresh();
this.layout();
}
}
/// IComponent implementation
public layout(): void {
if (this._tree) {
this.layoutTree();
this._tree.refresh();
}
super.layout();
}
private layoutTree(): void {
let width: number = this.convertSizeToNumber(this.width);
let height: number = this.convertSizeToNumber(this.height);
this._tree.layout(
height && height > 0 ? height : getContentHeight(this._inputContainer.nativeElement),
width && width > 0 ? width : getContentWidth(this._inputContainer.nativeElement));
}
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this._treeRenderer.options.withCheckbox = this.withCheckbox;
}
public get withCheckbox(): boolean {
return this.getPropertyOrDefault<azdata.TreeProperties, boolean>((props) => props.withCheckbox, false);
}
public set withCheckbox(newValue: boolean) {
this.setPropertyFromUI<azdata.TreeProperties, boolean>((properties, value) => { properties.withCheckbox = value; }, newValue);
}
}

View File

@@ -0,0 +1,180 @@
/*---------------------------------------------------------------------------------------------
* 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 { ITree, IRenderer } from 'vs/base/parts/tree/browser/tree';
import { LIGHT } from 'vs/platform/theme/common/themeService';
import { Disposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { ITreeComponentItem } from 'sql/workbench/common/views';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { TreeViewDataProvider } from './treeViewDataProvider';
import { URI } from 'vs/base/common/uri';
export enum TreeCheckboxState {
Intermediate = 0,
Checked = 1,
Unchecked = 2
}
export class TreeDataTemplate extends Disposable {
root: HTMLElement;
label: HTMLSpanElement;
icon: HTMLElement;
private _checkbox: HTMLInputElement;
model: ITreeComponentItem;
private _onChange = new Emitter<boolean>();
public readonly onChange: Event<boolean> = this._onChange.event;
public set checkbox(input: HTMLInputElement) {
this._checkbox = input;
this.handleOnChange(this._checkbox, () => {
this._onChange.fire(this._checkbox.checked);
if (this.model && this.model.onCheckedChanged) {
this.model.onCheckedChanged(this._checkbox.checked);
}
});
}
public get checkboxState(): TreeCheckboxState {
if (this._checkbox.indeterminate) {
return TreeCheckboxState.Intermediate;
} else {
return this.checkbox.checked ? TreeCheckboxState.Checked : TreeCheckboxState.Unchecked;
}
}
public set checkboxState(value: TreeCheckboxState) {
if (this.checkboxState !== value) {
switch (value) {
case TreeCheckboxState.Checked:
this._checkbox.indeterminate = false;
this._checkbox.checked = true;
break;
case TreeCheckboxState.Unchecked:
this._checkbox.indeterminate = false;
this._checkbox.checked = false;
break;
case TreeCheckboxState.Intermediate:
this._checkbox.indeterminate = true;
break;
default:
break;
}
}
}
public set enableCheckbox(value: boolean) {
if (value === undefined) {
value = true;
}
this._checkbox.disabled = !value;
}
public get checkbox(): HTMLInputElement {
return this._checkbox;
}
protected handleOnChange(domNode: HTMLElement, listener: (e: Event<void>) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.CHANGE, listener));
}
}
/**
* Renders the tree items.
* Uses the dom template to render connection groups and connections.
*/
export class TreeComponentRenderer extends Disposable implements IRenderer {
public static DEFAULT_TEMPLATE = 'DEFAULT_TEMPLATE';
public static DEFAULT_HEIGHT = 20;
constructor(
private _dataProvider: TreeViewDataProvider,
private themeService: IWorkbenchThemeService,
public options?: { withCheckbox: boolean }
) {
super();
}
/**
* Returns the element's height in the tree, in pixels.
*/
public getHeight(tree: ITree, element: any): number {
return TreeComponentRenderer.DEFAULT_HEIGHT;
}
/**
* Returns a template ID for a given element.
*/
public getTemplateId(tree: ITree, element: any): string {
return TreeComponentRenderer.DEFAULT_TEMPLATE;
}
/**
* Render template in a dom element based on template id
*/
public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any {
if (templateId === TreeComponentRenderer.DEFAULT_TEMPLATE) {
const nodeTemplate: TreeDataTemplate = new TreeDataTemplate();
nodeTemplate.root = dom.append(container, dom.$('.tree-component-node-tile'));
nodeTemplate.icon = dom.append(nodeTemplate.root, dom.$('div.model-view-tree-node-item-icon'));
if (this.options && this.options.withCheckbox) {
let checkboxWrapper = dom.append(nodeTemplate.root, dom.$('div.checkboxWrapper'));
nodeTemplate.checkbox = dom.append(checkboxWrapper, dom.$<HTMLInputElement>('input.checkbox', { type: 'checkbox' }));
}
nodeTemplate.label = dom.append(nodeTemplate.root, dom.$('div.model-view-tree-node-item-label'));
return nodeTemplate;
}
}
/**
* Render a element, given an object bag returned by the template
*/
public renderElement(tree: ITree, element: ITreeComponentItem, templateId: string, templateData: TreeDataTemplate): void {
const icon = this.themeService.getTheme().type === LIGHT ? element.icon : element.iconDark;
const iconUri = icon ? URI.revive(icon) : null;
templateData.icon.style.backgroundImage = iconUri ? `url('${iconUri.toString(true)}')` : '';
templateData.icon.style.backgroundRepeat = 'no-repeat';
templateData.icon.style.backgroundPosition = 'center';
dom.toggleClass(templateData.icon, 'model-view-tree-node-item-icon', !!icon);
if (element) {
element.onCheckedChanged = (checked: boolean) => {
this._dataProvider.onNodeCheckedChanged(element.handle, checked);
};
templateData.model = element;
}
if (templateId === TreeComponentRenderer.DEFAULT_TEMPLATE) {
this.renderNode(element, templateData);
}
}
private renderNode(treeNode: ITreeComponentItem, templateData: TreeDataTemplate): void {
let label = treeNode.label;
templateData.label.textContent = label.label;
templateData.root.title = label.label;
templateData.checkboxState = this.getCheckboxState(treeNode);
templateData.enableCheckbox = treeNode.enabled;
}
private getCheckboxState(treeNode: ITreeComponentItem): TreeCheckboxState {
if (treeNode.checked === undefined) {
return TreeCheckboxState.Intermediate;
} else {
return treeNode.checked ? TreeCheckboxState.Checked : TreeCheckboxState.Unchecked;
}
}
public disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
this.dispose();
// no op
}
}

View File

@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITree, IDataSource } from 'vs/base/parts/tree/browser/tree';
import { IModelViewTreeViewDataProvider, ITreeComponentItem } from 'sql/workbench/common/views';
import { TreeItemCollapsibleState } from 'vs/workbench/common/views';
/**
* Implements the DataSource(that returns a parent/children of an element) for the recent connection tree
*/
export class TreeComponentDataSource implements IDataSource {
/**
*
*/
constructor(
private _dataProvider: IModelViewTreeViewDataProvider) {
}
/**
* Returns the unique identifier of the given element.
* No more than one element may use a given identifier.
*/
public getId(tree: ITree, node: ITreeComponentItem): string {
return node.handle;
}
/**
* Returns a boolean value indicating whether the element has children.
*/
public hasChildren(tree: ITree, node: ITreeComponentItem): boolean {
return this._dataProvider !== undefined && node.collapsibleState !== TreeItemCollapsibleState.None;
}
/**
* Returns the element's children as an array in a promise.
*/
public getChildren(tree: ITree, node: ITreeComponentItem): Promise<any> {
if (this._dataProvider) {
if (node && node.handle === '0') {
return this._dataProvider.getChildren(undefined);
} else {
return this._dataProvider.getChildren(node);
}
}
return Promise.resolve([]);
}
public getParent(tree: ITree, node: any): Promise<any> {
return Promise.resolve(null);
}
public shouldAutoexpand(tree: ITree, node: ITreeComponentItem): boolean {
return node.collapsibleState === TreeItemCollapsibleState.Expanded;
}
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExtHostModelViewTreeViewsShape, SqlExtHostContext } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { IExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
import { IModelViewTreeViewDataProvider, ITreeComponentItem } from 'sql/workbench/common/views';
import { INotificationService } from 'vs/platform/notification/common/notification';
import * as vsTreeView from 'vs/workbench/api/browser/mainThreadTreeViews';
export class TreeViewDataProvider extends vsTreeView.TreeViewDataProvider implements IModelViewTreeViewDataProvider {
constructor(handle: number, treeViewId: string,
context: IExtHostContext,
notificationService?: INotificationService
) {
super(`${handle}-${treeViewId}`, context.getProxy(SqlExtHostContext.ExtHostModelViewTreeViews), notificationService);
}
onNodeCheckedChanged(treeItemHandle?: string, checked?: boolean) {
(<ExtHostModelViewTreeViewsShape>this._proxy).$onNodeCheckedChanged(this.treeViewId, treeItemHandle, checked);
}
onNodeSelected(items: ITreeComponentItem[]) {
if (items) {
(<ExtHostModelViewTreeViewsShape>this._proxy).$onNodeSelected(this.treeViewId, items.map(i => i.handle));
}
}
refresh(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeComponentItem }) {
}
}

View File

@@ -0,0 +1,148 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ChangeDetectorRef } from '@angular/core';
import { Registry } from 'vs/platform/registry/common/platform';
import * as nls from 'vs/nls';
import * as azdata from 'azdata';
import { IModelStore, IComponentDescriptor, IComponent } from './interfaces';
import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes';
import { IModelView, IModelViewEventArgs } from 'sql/platform/model/common/modelViewService';
import { Extensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry';
import { AngularDisposable } from 'sql/base/node/lifecycle';
import { ModelStore } from 'sql/workbench/electron-browser/modelComponents/modelStore';
import { Event, Emitter } from 'vs/base/common/event';
const componentRegistry = <IComponentRegistry>Registry.as(Extensions.ComponentContribution);
/**
* Provides common logic required for any implementation that hooks to a model provided by
* the extension host
*/
export abstract class ViewBase extends AngularDisposable implements IModelView {
protected readonly modelStore: IModelStore;
protected rootDescriptor: IComponentDescriptor;
protected _onDestroy = new Emitter<void>();
public readonly onDestroy = this._onDestroy.event;
constructor(protected changeRef: ChangeDetectorRef) {
super();
this.modelStore = new ModelStore();
}
// Properties needed by the model view code
abstract id: string;
abstract connection: azdata.connection.Connection;
abstract serverInfo: azdata.ServerInfo;
private _onEventEmitter = new Emitter<IModelViewEventArgs>();
initializeModel(rootComponent: IComponentShape, validationCallback: (componentId: string) => Thenable<boolean>): void {
let descriptor = this.defineComponent(rootComponent);
this.rootDescriptor = descriptor;
this.modelStore.registerValidationCallback(validationCallback);
// Kick off the build by detecting changes to the model
this.changeRef.detectChanges();
}
private defineComponent(component: IComponentShape): IComponentDescriptor {
let existingDescriptor = this.modelStore.getComponentDescriptor(component.id);
if (existingDescriptor) {
return existingDescriptor;
}
let typeId = componentRegistry.getIdForTypeMapping(component.type);
if (!typeId) {
// failure case
throw new Error(nls.localize('componentTypeNotRegistered', "Could not find component for type {0}", ModelComponentTypes[component.type]));
}
let descriptor = this.modelStore.createComponentDescriptor(typeId, component.id);
this.setProperties(component.id, component.properties);
this.setLayout(component.id, component.layout);
this.registerEvent(component.id);
if (component.itemConfigs) {
for (let item of component.itemConfigs) {
this.addToContainer(component.id, item);
}
}
return descriptor;
}
private removeComponent(component: IComponentShape): void {
if (component.itemConfigs) {
for (let item of component.itemConfigs) {
this.removeFromContainer(component.id, item);
}
}
}
clearContainer(componentId: string): void {
this.queueAction(componentId, (component) => component.clearContainer());
}
addToContainer(containerId: string, itemConfig: IItemConfig, index?: number): void {
// Do not return the promise as this should be non-blocking
this.queueAction(containerId, (component) => {
let childDescriptor = this.defineComponent(itemConfig.componentShape);
component.addToContainer(childDescriptor, itemConfig.config, index);
});
}
removeFromContainer(containerId: string, itemConfig: IItemConfig): void {
let childDescriptor = this.modelStore.getComponentDescriptor(itemConfig.componentShape.id);
this.queueAction(containerId, (component) => {
component.removeFromContainer(childDescriptor);
this.removeComponent(itemConfig.componentShape);
});
}
setLayout(componentId: string, layout: any): void {
if (!layout) {
return;
}
this.queueAction(componentId, (component) => component.setLayout(layout));
}
setProperties(componentId: string, properties: { [key: string]: any; }): void {
if (!properties) {
return;
}
this.queueAction(componentId, (component) => component.setProperties(properties));
}
refreshDataProvider(componentId: string, item: any): void {
this.queueAction(componentId, (component) => component.refreshDataProvider(item));
}
private queueAction<T>(componentId: string, action: (component: IComponent) => T): void {
this.modelStore.eventuallyRunOnComponent(componentId, action).catch(err => {
// TODO add error handling
});
}
registerEvent(componentId: string) {
this.queueAction(componentId, (component) => {
this._register(component.registerEventHandler(e => {
let modelViewEvent: IModelViewEventArgs = Object.assign({
componentId: componentId,
isRootComponent: componentId === this.rootDescriptor.id
}, e);
this._onEventEmitter.fire(modelViewEvent);
}));
});
}
public get onEvent(): Event<IModelViewEventArgs> {
return this._onEventEmitter.event;
}
public validate(componentId: string): Thenable<boolean> {
return new Promise(resolve => this.modelStore.eventuallyRunOnComponent(componentId, component => resolve(component.validate())));
}
public setDataProvider(handle: number, componentId: string, context: any): any {
return this.queueAction(componentId, (component) => component.setDataProvider(handle, componentId, context));
}
}

View File

@@ -0,0 +1,203 @@
/*---------------------------------------------------------------------------------------------
* 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/webview';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef, OnDestroy
} from '@angular/core';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { addDisposableListener, EventType } from 'vs/base/browser/dom';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ComponentBase } from 'sql/workbench/electron-browser/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/workbench/electron-browser/modelComponents/interfaces';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
import { WebviewContentOptions } from 'vs/workbench/contrib/webview/common/webview';
function reviveWebviewOptions(options: vscode.WebviewOptions): vscode.WebviewOptions {
return {
...options,
localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(URI.revive) : undefined
};
}
@Component({
template: '',
selector: 'modelview-webview-component'
})
export default class WebViewComponent extends ComponentBase implements IComponent, OnDestroy {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private static readonly standardSupportedLinkSchemes = ['http', 'https', 'mailto'];
private _webview: WebviewElement;
private _renderedHtml: string;
private _extensionLocationUri: URI;
protected contextKey: IContextKey<boolean>;
protected findInputFocusContextKey: IContextKey<boolean>;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(IOpenerService) private readonly _openerService: IOpenerService,
@Inject(IWorkspaceContextService) private readonly _contextService: IWorkspaceContextService,
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
this._createWebview();
this._register(addDisposableListener(window, EventType.RESIZE, e => {
this.layout();
}));
}
private _createWebview(): void {
this._webview = this.instantiationService.createInstance(WebviewElement,
{
allowSvgs: true
},
{
allowScripts: true
});
this._webview.mountTo(this._el.nativeElement);
this._register(this._webview.onDidClickLink(link => this.onDidClickLink(link)));
this._register(this._webview.onMessage(e => {
this.fireEvent({
eventType: ComponentEventType.onMessage,
args: e
});
}));
this.setHtml();
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// Webview Functions
private setHtml(): void {
if (this._webview && this.html) {
this._renderedHtml = this.html;
this._webview.contents = this._renderedHtml;
this._webview.layout();
}
}
private sendMessage(): void {
if (this._webview && this.message) {
this._webview.sendMessage(this.message);
}
}
private onDidClickLink(link: URI): any {
if (!link) {
return;
}
if (WebViewComponent.standardSupportedLinkSchemes.indexOf(link.scheme) >= 0 || this.enableCommandUris && link.scheme === 'command') {
this._openerService.open(link);
}
}
private get enableCommandUris(): boolean {
if (this.options && this.options.enableCommandUris) {
return true;
}
return false;
}
/// IComponent implementation
public layout(): void {
let element = <HTMLElement>this._el.nativeElement;
element.style.position = this.position;
this._webview.layout();
}
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
if (this.options) {
this._webview.options = this.getExtendedOptions();
}
if (this.html !== this._renderedHtml) {
this.setHtml();
}
if (this.extensionLocation) {
this._extensionLocationUri = URI.revive(this.extensionLocation);
}
this.sendMessage();
}
// CSS-bound properties
public get message(): any {
return this.getPropertyOrDefault<azdata.WebViewProperties, any>((props) => props.message, undefined);
}
public set message(newValue: any) {
this.setPropertyFromUI<azdata.WebViewProperties, any>((properties, message) => { properties.message = message; }, newValue);
}
public get html(): string {
return this.getPropertyOrDefault<azdata.WebViewProperties, string>((props) => props.html, undefined);
}
public set html(newValue: string) {
this.setPropertyFromUI<azdata.WebViewProperties, string>((properties, html) => { properties.html = html; }, newValue);
}
public get options(): vscode.WebviewOptions {
return this.getPropertyOrDefault<azdata.WebViewProperties, vscode.WebviewOptions>((props) => props.options, undefined);
}
public get extensionLocation(): UriComponents {
return this.getPropertyOrDefault<azdata.WebViewProperties, UriComponents>((props) => props.extensionLocation, undefined);
}
private get extensionLocationUri(): URI {
if (!this._extensionLocationUri && this.extensionLocation) {
this._extensionLocationUri = URI.revive(this.extensionLocation);
}
return this._extensionLocationUri;
}
private getExtendedOptions(): WebviewContentOptions {
let options = this.options || { enableScripts: true };
options = reviveWebviewOptions(options);
return {
allowScripts: options.enableScripts,
localResourceRoots: options!.localResourceRoots || this.getDefaultLocalResourceRoots()
};
}
private getDefaultLocalResourceRoots(): URI[] {
const rootPaths = this._contextService.getWorkspace().folders.map(x => x.uri);
if (this.extensionLocationUri) {
rootPaths.push(this.extensionLocationUri);
}
return rootPaths;
}
}

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

Some files were not shown because too many files have changed in this diff Show More