diff --git a/extensions/agent/src/dialogs/createStepDialog.ts b/extensions/agent/src/dialogs/createStepDialog.ts index 011e7a51be..c6a812e9c6 100644 --- a/extensions/agent/src/dialogs/createStepDialog.ts +++ b/extensions/agent/src/dialogs/createStepDialog.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import { CreateStepData } from '../data/createStepData'; import { AgentUtils } from '../agentUtils'; import { CreateJobData } from '../data/createJobData'; +const path = require('path'); export class CreateStepDialog { @@ -15,6 +16,7 @@ export class CreateStepDialog { // Top level // private static readonly DialogTitle: string = 'New Job Step'; + private static readonly FileBrowserDialogTitle: string = 'Locate Database Files - '; private static readonly OkButtonText: string = 'OK'; private static readonly CancelButtonText: string = 'Cancel'; private static readonly GeneralTabText: string = 'General'; @@ -35,36 +37,51 @@ export class CreateStepDialog { private static readonly QuitJobReportingFailure: string = 'Quit the job reporting failure'; // UI Components - // + + // Dialogs private dialog: sqlops.window.modelviewdialog.Dialog; + private fileBrowserDialog: sqlops.window.modelviewdialog.Dialog; + + // Dialog tabs private generalTab: sqlops.window.modelviewdialog.DialogTab; private advancedTab: sqlops.window.modelviewdialog.DialogTab; + + //Input boxes private nameTextBox: sqlops.InputBoxComponent; + private commandTextBox: sqlops.InputBoxComponent; + private selectedPathTextBox: sqlops.InputBoxComponent; + private retryAttemptsBox: sqlops.InputBoxComponent; + private retryIntervalBox: sqlops.InputBoxComponent; + private outputFileNameBox: sqlops.InputBoxComponent; + private fileBrowserNameBox: sqlops.InputBoxComponent; + + // Dropdowns private typeDropdown: sqlops.DropDownComponent; private runAsDropdown: sqlops.DropDownComponent; private databaseDropdown: sqlops.DropDownComponent; private successActionDropdown: sqlops.DropDownComponent; private failureActionDropdown: sqlops.DropDownComponent; - private commandTextBox: sqlops.InputBoxComponent; + private fileTypeDropdown: sqlops.DropDownComponent; + + // Buttons private openButton: sqlops.ButtonComponent; private parseButton: sqlops.ButtonComponent; private nextButton: sqlops.ButtonComponent; private previousButton: sqlops.ButtonComponent; - private retryAttemptsBox: sqlops.InputBoxComponent; - private retryIntervalBox: sqlops.InputBoxComponent; - private appendToExistingFileCheckbox: sqlops.CheckBoxComponent; - private logToTableCheckbox: sqlops.CheckBoxComponent; - private outputFileNameBox: sqlops.InputBoxComponent; private outputFileBrowserButton: sqlops.ButtonComponent; + // Checkbox + private appendToExistingFileCheckbox: sqlops.CheckBoxComponent; + private logToTableCheckbox: sqlops.CheckBoxComponent; + + private fileBrowserTree: sqlops.FileBrowserTreeComponent; + private jobModel: CreateJobData; private model: CreateStepData; private ownerUri: string; private jobName: string; private server: string; private stepId: number; - private jobModel: CreateJobData; - constructor( ownerUri: string, jobName: string, @@ -344,12 +361,63 @@ export class CreateStepDialog { return retryFlexContainer; } + private openFileBrowserDialog() { + let fileBrowserTitle = CreateStepDialog.FileBrowserDialogTitle + `${this.server}`; + this.fileBrowserDialog = sqlops.window.modelviewdialog.createDialog(fileBrowserTitle); + let fileBrowserTab = sqlops.window.modelviewdialog.createTab('File Browser'); + this.fileBrowserDialog.content = [fileBrowserTab]; + fileBrowserTab.registerContent(async (view) => { + this.fileBrowserTree = view.modelBuilder.fileBrowserTree() + .withProperties({ ownerUri: this.ownerUri }) + .component(); + this.selectedPathTextBox = view.modelBuilder.inputBox() + .withProperties({ inputType: 'text'}) + .component(); + this.fileBrowserTree.onDidChange((args) => { + this.selectedPathTextBox.value = args.fullPath; + this.fileBrowserNameBox.value = args.isFile ? path.win32.basename(args.fullPath) : ''; + }); + this.fileTypeDropdown = view.modelBuilder.dropDown() + .withProperties({ + value: 'All Files (*)', + values: ['All Files (*)'] + }) + .component(); + this.fileBrowserNameBox = view.modelBuilder.inputBox() + .withProperties({}) + .component(); + let fileBrowserContainer = view.modelBuilder.formContainer() + .withFormItems([{ + component: this.fileBrowserTree, + title: '' + }, { + component: this.selectedPathTextBox, + title: 'Selected path:' + }, { + component: this.fileTypeDropdown, + title: 'Files of type:' + }, { + component: this.fileBrowserNameBox, + title: 'File name:' + } + ]).component(); + view.initializeModel(fileBrowserContainer); + }); + this.fileBrowserDialog.okButton.onClick(() => { + this.outputFileNameBox.value = path.join(path.dirname(this.selectedPathTextBox.value), this.fileBrowserNameBox.value); + }); + this.fileBrowserDialog.okButton.label = CreateStepDialog.OkButtonText; + this.fileBrowserDialog.cancelButton.label = CreateStepDialog.CancelButtonText; + sqlops.window.modelviewdialog.openDialog(this.fileBrowserDialog); + } + private createTSQLOptions(view) { this.outputFileBrowserButton = view.modelBuilder.button() .withProperties({ width: '20px', label: '...' }).component(); + this.outputFileBrowserButton.onDidClick(() => this.openFileBrowserDialog()); this.outputFileNameBox = view.modelBuilder.inputBox() .withProperties({ - width: '100px', + width: '150px', inputType: 'text' }).component(); let outputViewButton = view.modelBuilder.button() @@ -407,6 +475,7 @@ export class CreateStepDialog { this.model.retryInterval = +this.retryIntervalBox.value; this.model.failureAction = this.failureActionDropdown.value as string; this.model.outputFileName = this.outputFileNameBox.value; + this.model.appendToLogFile = this.appendToExistingFileCheckbox.checked; await this.model.save(); } diff --git a/src/sql/parts/fileBrowser/fileBrowserTreeView.ts b/src/sql/parts/fileBrowser/fileBrowserTreeView.ts index c5e537aba4..619a63e0c8 100644 --- a/src/sql/parts/fileBrowser/fileBrowserTreeView.ts +++ b/src/sql/parts/fileBrowser/fileBrowserTreeView.ts @@ -23,7 +23,7 @@ import { ITree } from 'vs/base/parts/tree/browser/tree'; /** * Implements tree view for file browser */ -export class FileBrowserTreeView { +export class FileBrowserTreeView implements IDisposable { private _tree: ITree; private _toDispose: IDisposable[] = []; diff --git a/src/sql/parts/modelComponents/components.contribution.ts b/src/sql/parts/modelComponents/components.contribution.ts index 7bd5b191fa..fc91037971 100644 --- a/src/sql/parts/modelComponents/components.contribution.ts +++ b/src/sql/parts/modelComponents/components.contribution.ts @@ -19,6 +19,7 @@ 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 { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry'; import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes'; @@ -70,3 +71,6 @@ registerComponentType(TABLE_COMPONENT, ModelComponentTypes.Table, TableComponent export const LOADING_COMPONENT = 'loading-component'; registerComponentType(LOADING_COMPONENT, ModelComponentTypes.LoadingComponent, LoadingComponent); + +export const FILEBROWSERTREE_COMPONENT = 'filebrowsertree-component'; +registerComponentType(FILEBROWSERTREE_COMPONENT, ModelComponentTypes.FileBrowserTree, FileBrowserTreeComponent); diff --git a/src/sql/parts/modelComponents/fileBrowserTree.component.ts b/src/sql/parts/modelComponents/fileBrowserTree.component.ts new file mode 100644 index 0000000000..fb7dfb6a43 --- /dev/null +++ b/src/sql/parts/modelComponents/fileBrowserTree.component.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sqlops from 'sqlops'; + +import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; +import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { FileBrowserViewModel } from '../fileBrowser/fileBrowserViewModel'; +import { FileNode } from 'sql/parts/fileBrowser/common/fileNode'; +import { FileBrowserTreeView } from '../fileBrowser/fileBrowserTreeView'; + +@Component({ + selector: 'modelview-fileBrowserTree', + template: ` +
+ ` +}) +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) { + super(changeRef); + } + + 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._onEventEmitter.fire({ + 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 { + return super.validate().then(valid => { + // TODO: tree validation? + return valid; + }); + } + + ngOnDestroy(): void { + this.baseDestroy(); + } + + /// IComponent implementation + + public layout(): void { + this._changeRef.detectChanges(); + } + + 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((props) => props.ownerUri, ''); + } + + public set ownerUri(newValue: string) { + this.setPropertyFromUI((props, value) => props.ownerUri = value, newValue); + } +} diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index b9847ede0b..efc8902941 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -35,6 +35,7 @@ declare module 'sqlops' { groupContainer(): GroupBuilder; toolbarContainer(): ToolbarBuilder; loadingComponent(): LoadingComponentBuilder; + fileBrowserTree(): ComponentBuilder; } export interface ComponentBuilder { @@ -362,6 +363,10 @@ declare module 'sqlops' { selectedRows?: number[]; } + export interface FileBrowserTreeProperties extends ComponentProperties { + ownerUri: string; + } + export interface CheckBoxProperties { checked?: boolean; label?: string; @@ -471,6 +476,10 @@ declare module 'sqlops' { onRowSelected: vscode.Event; } + export interface FileBrowserTreeComponent extends Component, FileBrowserTreeProperties { + onDidChange: vscode.Event; + } + export interface WebViewComponent extends Component { html: string; message: any; diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 612ebead31..5b6395d439 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -144,7 +144,8 @@ export enum ModelComponentTypes { Form, Group, Toolbar, - LoadingComponent + LoadingComponent, + FileBrowserTree } export interface IComponentShape { diff --git a/src/sql/workbench/api/node/extHostModelView.ts b/src/sql/workbench/api/node/extHostModelView.ts index b379355608..a9e5510279 100644 --- a/src/sql/workbench/api/node/extHostModelView.ts +++ b/src/sql/workbench/api/node/extHostModelView.ts @@ -158,6 +158,13 @@ class ModelBuilderImpl implements sqlops.ModelBuilder { return builder; } + fileBrowserTree(): sqlops.ComponentBuilder { + let id = this.getNextComponentId(); + let builder: ComponentBuilderImpl = this.getComponentBuilder(new FileBrowserTreeComponentWrapper(this._proxy, this._handle, id), id); + this._componentBuilders.set(id, builder); + return builder; + } + getComponentBuilder(component: ComponentWrapper, id: string): ComponentBuilderImpl { let componentBuilder: ComponentBuilderImpl = new ComponentBuilderImpl(component); this._componentBuilders.set(id, componentBuilder); @@ -960,6 +967,28 @@ class LoadingComponentWrapper extends ComponentWrapper implements sqlops.Loading } } +class FileBrowserTreeComponentWrapper extends ComponentWrapper implements sqlops.FileBrowserTreeComponent { + + constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { + super(proxy, handle, ModelComponentTypes.FileBrowserTree, id); + this.properties = {}; + this._emitterMap.set(ComponentEventType.onDidChange, new Emitter()); + } + + public get ownerUri(): string { + return this.properties['ownerUri']; + } + + public set ownerUri(value: string) { + this.setProperty('ownerUri', value); + } + + public get onDidChange(): vscode.Event { + let emitter = this._emitterMap.get(ComponentEventType.onDidChange); + return emitter && emitter.event; + } +} + class ModelViewImpl implements sqlops.ModelView { public onClosedEmitter = new Emitter();