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