diff --git a/extensions/mssql/src/config.json b/extensions/mssql/src/config.json index 80bf8df933..07822768fa 100644 --- a/extensions/mssql/src/config.json +++ b/extensions/mssql/src/config.json @@ -16,4 +16,4 @@ }, "installDirectory": "../sqltoolsservice/{#platform#}/{#version#}", "executableFiles": ["MicrosoftSqlToolsServiceLayer.exe", "MicrosoftSqlToolsServiceLayer"] -} +} \ No newline at end of file diff --git a/src/sql/parts/dashboard/dashboard.module.ts b/src/sql/parts/dashboard/dashboard.module.ts index a02e67064b..d046b9c72a 100644 --- a/src/sql/parts/dashboard/dashboard.module.ts +++ b/src/sql/parts/dashboard/dashboard.module.ts @@ -51,7 +51,7 @@ import { JobHistoryComponent } from 'sql/parts/jobManagement/views/jobHistory.co let baseComponents = [DashboardHomeContainer, DashboardComponent, DashboardWidgetWrapper, DashboardWebviewContainer, DashboardWidgetContainer, DashboardGridContainer, DashboardErrorContainer, DashboardNavSection, WebviewContent, WidgetContent, ComponentHostDirective, BreadcrumbComponent, ControlHostContent, DashboardControlHostContainer, - JobsViewComponent, AgentViewComponent, JobHistoryComponent]; + JobsViewComponent, AgentViewComponent, JobHistoryComponent, JobStepsViewComponent]; /* Panel */ import { PanelModule } from 'sql/base/browser/ui/panel/panel.module'; @@ -68,6 +68,7 @@ import { ExplorerWidget } from 'sql/parts/dashboard/widgets/explorer/explorerWid import { TasksWidget } from 'sql/parts/dashboard/widgets/tasks/tasksWidget.component'; import { InsightsWidget } from 'sql/parts/dashboard/widgets/insights/insightsWidget.component'; import { WebviewWidget } from 'sql/parts/dashboard/widgets/webview/webviewWidget.component'; +import { JobStepsViewComponent } from '../jobManagement/views/jobStepsView.component'; let widgetComponents = [ PropertiesWidgetComponent, diff --git a/src/sql/parts/jobManagement/views/jobHistory.component.html b/src/sql/parts/jobManagement/views/jobHistory.component.html index 43da359029..96b92c9947 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.component.html +++ b/src/sql/parts/jobManagement/views/jobHistory.component.html @@ -106,7 +106,15 @@

Status:

-

+

{{_runStatus}}

+ + + + + Job ID: + + + {{agentJobHistoryInfo?.jobId}} @@ -127,10 +135,10 @@ - Log: + Server: - + {{agentJobHistoryInfo?.server}} @@ -138,7 +146,7 @@ SQL message ID: - + {{agentJobHistoryInfo?.sqlMessageId}} @@ -150,6 +158,7 @@ + diff --git a/src/sql/parts/jobManagement/views/jobHistory.component.ts b/src/sql/parts/jobManagement/views/jobHistory.component.ts index ebc5775a04..9331a399a0 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.component.ts +++ b/src/sql/parts/jobManagement/views/jobHistory.component.ts @@ -5,7 +5,8 @@ import 'vs/css!./jobHistory'; -import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Input, Injectable } from '@angular/core'; +import { OnInit, OnChanges, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Input, Injectable } from '@angular/core'; +import { AgentJobHistoryInfo, AgentJobInfo } from 'sqlops'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachListStyler } from 'vs/platform/theme/common/styler'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; @@ -18,8 +19,8 @@ import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboar import { AgentViewComponent } from 'sql/parts/jobManagement/agent/agentView.component'; import { JobHistoryController, JobHistoryDataSource, JobHistoryRenderer, JobHistoryFilter, JobHistoryModel, JobHistoryRow } from 'sql/parts/jobManagement/views/jobHistoryTree'; -import { AgentJobHistoryInfo, AgentJobInfo } from 'sqlops'; - +import { JobStepsViewComponent } from 'sql/parts/jobManagement/views/jobStepsView.component'; +import { JobStepsViewRow } from './jobStepsViewTree'; export const DASHBOARD_SELECTOR: string = 'jobhistory-component'; @@ -27,7 +28,7 @@ export const DASHBOARD_SELECTOR: string = 'jobhistory-component'; selector: DASHBOARD_SELECTOR, templateUrl: decodeURI(require.toUrl('./jobHistory.component.html')) }) -export class JobHistoryComponent extends Disposable implements OnInit, OnDestroy { +export class JobHistoryComponent extends Disposable implements OnInit { private _jobManagementService: IJobManagementService; private _tree: Tree; @@ -42,8 +43,11 @@ export class JobHistoryComponent extends Disposable implements OnInit, OnDestroy @Input() public jobId: string = undefined; @Input() public agentJobHistoryInfo: AgentJobHistoryInfo = undefined; - private prevJobId: string = undefined; - private isVisible: boolean = false; + private _prevJobId: string = undefined; + private _isVisible: boolean = false; + private _stepRows: JobStepsViewRow[] = []; + private _showSteps: boolean = false; + private _runStatus: string = undefined; constructor( @@ -65,23 +69,31 @@ export class JobHistoryComponent extends Disposable implements OnInit, OnDestroy 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); self.agentJobHistoryInfo = self._treeController.jobHistories.filter(history => history.instanceId === element.instanceID)[0]; - self.agentJobHistoryInfo.runDate = self.formatTime(self.agentJobHistoryInfo.runDate); - self._cd.detectChanges(); + if (self.agentJobHistoryInfo) { + self.agentJobHistoryInfo.runDate = self.formatTime(self.agentJobHistoryInfo.runDate); + self._stepRows = self.agentJobHistoryInfo.steps.map(step => { + let stepViewRow = new JobStepsViewRow(); + stepViewRow.message = step.message; + stepViewRow.runStatus = JobHistoryRow.convertToStatusString(self.agentJobHistoryInfo.runStatus); + self._runStatus = stepViewRow.runStatus; + stepViewRow.stepName = step.stepName; + stepViewRow.stepID = step.stepId.toString(); + return stepViewRow; + }); + this._showSteps = true; + self._cd.detectChanges(); + } } return true; }; @@ -95,14 +107,11 @@ export class JobHistoryComponent extends Disposable implements OnInit, OnDestroy this._tree.layout(1024); } - ngOnDestroy() { - } - ngAfterContentChecked() { - if (this.isVisible === false && this._tableContainer.nativeElement.offsetParent !== null) { - if (this.prevJobId !== this.jobId) { + if (this._isVisible === false && this._tableContainer.nativeElement.offsetParent !== null) { + if (this._prevJobId !== this.jobId) { this.loadHistory(); - this.prevJobId = this.jobId; + this._prevJobId = this.jobId; } } } @@ -139,7 +148,7 @@ export class JobHistoryComponent extends Disposable implements OnInit, OnDestroy } private goToJobs(): void { - this.isVisible = false; + this._isVisible = false; this._agentViewComponent.showHistory = false; } @@ -154,5 +163,9 @@ export class JobHistoryComponent extends Disposable implements OnInit, OnDestroy private formatTime(time: string): string { return time.replace('T', ' '); } + + public showSteps(): boolean { + return this._showSteps; + } } diff --git a/src/sql/parts/jobManagement/views/jobHistory.css b/src/sql/parts/jobManagement/views/jobHistory.css index 5ff2b29c6e..68cec7e72d 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.css +++ b/src/sql/parts/jobManagement/views/jobHistory.css @@ -153,6 +153,11 @@ input#accordion:checked ~ .accordion-content { padding: 10px; } +.accordion-content #col1, +.accordion-content #col3 { + font-weight: bold; +} + .accordion-content #col2 { padding-right: 300px; } @@ -171,6 +176,7 @@ table.step-list tr.step-row td { border-left: 3px solid #444444; padding-left: 10px; height: 100%; + width: 90%; } .history-details > .job-steps { @@ -203,15 +209,15 @@ table.step-list tr.step-row td { display: inline-block; } -.passed { +.job-passed { background: green; } -.failed { +.job-failed { background: red; } -.unknown { +.job-unknown { background: yellow; } @@ -219,3 +225,8 @@ table.step-list tr.step-row td { padding-left: 50px; width: 140px; } + +.steps-tree .monaco-tree .monaco-tree-row { + white-space: normal; + height: 40px !important; +} \ No newline at end of file diff --git a/src/sql/parts/jobManagement/views/jobHistoryTree.ts b/src/sql/parts/jobManagement/views/jobHistoryTree.ts index de2f3e484f..80e44d55a3 100644 --- a/src/sql/parts/jobManagement/views/jobHistoryTree.ts +++ b/src/sql/parts/jobManagement/views/jobHistoryTree.ts @@ -145,11 +145,11 @@ export class JobHistoryRenderer implements tree.IRenderer { templateData.label.innerText = element.runDate + '\t\t\t' + element.runStatus; let statusClass: string; if (element.runStatus === 'Succeeded') { - statusClass = ' passed'; + statusClass = ' job-passed'; } else if (element.runStatus === 'Failed') { - statusClass = ' failed'; + statusClass = ' job-failed'; } else { - statusClass = ' unknown'; + statusClass = ' job-unknown'; } this._statusIcon.className += statusClass; } diff --git a/src/sql/parts/jobManagement/views/jobStepsView.component.html b/src/sql/parts/jobManagement/views/jobStepsView.component.html new file mode 100644 index 0000000000..a6f68f325e --- /dev/null +++ b/src/sql/parts/jobManagement/views/jobStepsView.component.html @@ -0,0 +1,22 @@ + + +

Steps

+ + + + + + +
+ Step ID + + Step Name + + Message +
+
\ No newline at end of file diff --git a/src/sql/parts/jobManagement/views/jobStepsView.component.ts b/src/sql/parts/jobManagement/views/jobStepsView.component.ts new file mode 100644 index 0000000000..f20d3cbabc --- /dev/null +++ b/src/sql/parts/jobManagement/views/jobStepsView.component.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./jobStepsView'; + +import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnChanges, ViewChild, Input, Injectable } from '@angular/core'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +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 { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService'; +import { IJobManagementService } from '../common/interfaces'; +import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service'; +import { AgentJobHistoryInfo } from 'sqlops'; +import { JobStepsViewController, JobStepsViewDataSource, JobStepsViewFilter, + JobStepsViewRenderer, JobStepsViewRow, JobStepsViewModel} from 'sql/parts/jobManagement/views/jobStepsViewTree'; + +export const JOBSTEPSVIEW_SELECTOR: string = 'jobstepsview-component'; + +@Component({ + selector: JOBSTEPSVIEW_SELECTOR, + templateUrl: decodeURI(require.toUrl('./jobStepsView.component.html')) +}) +export class JobStepsViewComponent extends Disposable implements OnInit, OnChanges { + + private _jobManagementService: IJobManagementService; + private _tree: Tree; + private _treeController = new JobStepsViewController(); + private _treeDataSource = new JobStepsViewDataSource(); + private _treeRenderer = new JobStepsViewRenderer(); + private _treeFilter = new JobStepsViewFilter(); + private static _pageSize = 1024; + + @ViewChild('table') private _tableContainer: ElementRef; + + @Input() public stepRows: JobStepsViewRow[] = []; + + constructor( + @Inject(BOOTSTRAP_SERVICE_ID) private bootstrapService: IBootstrapService, + @Inject(forwardRef(() => ElementRef)) el: ElementRef, + @Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef, + @Inject(forwardRef(() => DashboardServiceInterface)) private _dashboardService: DashboardServiceInterface + ) { + super(); + this._jobManagementService = bootstrapService.jobManagementService; + } + + ngOnInit() { + let ownerUri: string = this._dashboardService.connectionManagementService.connectionInfo.ownerUri; + this._tree = new Tree(this._tableContainer.nativeElement, { + controller: this._treeController, + dataSource: this._treeDataSource, + filter: this._treeFilter, + renderer: this._treeRenderer + }); + } + + ngOnChanges() { + if (this.stepRows.length > 0) { + this._treeDataSource.data = this.stepRows; + if (!this._tree) { + this._tree = new Tree(this._tableContainer.nativeElement, { + controller: this._treeController, + dataSource: this._treeDataSource, + filter: this._treeFilter, + renderer: this._treeRenderer + }); + } + this._tree.layout(JobStepsViewComponent._pageSize); + this._tree.setInput(new JobStepsViewModel()); + } + } + + ngAfterContentChecked() { + } +} + diff --git a/src/sql/parts/jobManagement/views/jobStepsView.css b/src/sql/parts/jobManagement/views/jobStepsView.css new file mode 100644 index 0000000000..cf0bd6a7c6 --- /dev/null +++ b/src/sql/parts/jobManagement/views/jobStepsView.css @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.steps-tree .list-row .status-icon { + height: 10px; + width: 10px; + display: inline-block; + margin-top: 4px; +} + +.list-row .label { + padding-left: 10px; + display: flex; + text-align: center; +} + +.step-passed { + background: green; +} + +.step-failed { + background: red; +} + +.step-unknown { + background: yellow; +} + +.steps-tree .list-row { + display: inline-flex; + height: 20px +} + +.step-columns { + padding-left: 50px; +} + +.step-id-col, .tree-id-col { + padding-left: 10px; + white-space: normal; + text-align: center; + width: 60px; + +} + +.step-name-col, .tree-name-col { + padding-right: 10px; + white-space: normal; + text-align: center; + width: 350px; +} + +.step-message-col, .tree-message-col { + padding-right: 10px; + white-space: normal; + text-align: center; + width: 680px; + height: +} \ No newline at end of file diff --git a/src/sql/parts/jobManagement/views/jobStepsViewTree.ts b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts new file mode 100644 index 0000000000..070f76d53f --- /dev/null +++ b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts @@ -0,0 +1,186 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Router } from '@angular/router'; + +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; +import { MetadataType } from 'sql/parts/connection/common/connectionManagement'; +import { SingleConnectionManagementService } from 'sql/parts/dashboard/services/dashboardServiceInterface.service'; +import { + NewQueryAction, ScriptSelectAction, EditDataAction, ScriptCreateAction, ScriptExecuteAction, ScriptAlterAction, + BackupAction, ManageActionContext, BaseActionContext, ManageAction, RestoreAction +} from 'sql/workbench/common/actions'; +import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService'; +import { ConnectionManagementInfo } from 'sql/parts/connection/common/connectionManagementInfo'; +import * as Constants from 'sql/parts/connection/common/constants'; +import * as tree from 'vs/base/parts/tree/browser/tree'; +import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults'; +import { Promise, TPromise } from 'vs/base/common/winjs.base'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IAction } from 'vs/base/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { generateUuid } from 'vs/base/common/uuid'; +import * as DOM from 'vs/base/browser/dom'; +import { OEAction } from 'sql/parts/registeredServer/viewlet/objectExplorerActions'; +import { Builder, $, withElementById } from 'vs/base/browser/builder'; +import { AgentJobHistoryInfo } from 'sqlops'; +import { Agent } from 'vs/base/node/request'; + +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 { + private _jobHistories: AgentJobHistoryInfo[]; + + 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 set jobHistories(value: AgentJobHistoryInfo[]) { + this._jobHistories = value; + } + + public get jobHistories(): AgentJobHistoryInfo[] { + return this._jobHistories; + } + +} + +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 { + if (element instanceof JobStepsViewModel) { + return TPromise.as(this._data); + } else { + return TPromise.as(undefined); + } + } + + public getParent(tree: tree.ITree, element: JobStepsViewRow | JobStepsViewModel): Promise { + if (element instanceof JobStepsViewModel) { + return TPromise.as(undefined); + } else { + return TPromise.as(new JobStepsViewModel()); + } + } + + public set data(data: JobStepsViewRow[]) { + this._data = data; + } +} + +export interface IListTemplate { + statusIcon: HTMLElement; + label: HTMLElement; +} + +export class JobStepsViewRenderer implements tree.IRenderer { + private _statusIcon: HTMLElement; + + public getHeight(tree: tree.ITree, element: JobStepsViewRow): number { + return 22; + } + + 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'); + this._statusIcon = this.createStatusIcon(); + row.appendChild(this._statusIcon); + row.appendChild(label); + container.appendChild(row); + let statusIcon = this._statusIcon; + 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; + templateData.label.appendChild(stepIdCol); + templateData.label.appendChild(stepNameCol); + templateData.label.appendChild(stepMessageCol); + let statusClass: string; + if (element.runStatus === 'Succeeded') { + statusClass = ' step-passed'; + } else if (element.runStatus === 'Failed') { + statusClass = ' step-failed'; + } else { + statusClass = ' step-unknown'; + } + this._statusIcon.className += statusClass; + } + + public disposeTemplate(tree: tree.ITree, templateId: string, templateData: IListTemplate): void { + // no op + } + + private createStatusIcon(): HTMLElement { + let statusIcon: HTMLElement = DOM.$('div'); + statusIcon.className += ' status-icon'; + 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; + } +} diff --git a/src/sql/parts/jobManagement/views/jobsView.component.ts b/src/sql/parts/jobManagement/views/jobsView.component.ts index ac80d9bca5..3eac711029 100644 --- a/src/sql/parts/jobManagement/views/jobsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobsView.component.ts @@ -208,8 +208,8 @@ export class JobsViewComponent implements OnInit, OnDestroy { } private getJob(args: Slick.OnClickEventArgs): sqlops.AgentJobInfo { - let cell = args.cell; - let jobName = args.grid.getCellNode(1, cell).innerText.trim(); + let row = args.row; + let jobName = args.grid.getCellNode(row, 1).innerText.trim(); let job = this.jobs.filter(job => job.name === jobName)[0]; return job; } diff --git a/src/sql/sqlops.d.ts b/src/sql/sqlops.d.ts index aa32e247ae..27c45762c2 100644 --- a/src/sql/sqlops.d.ts +++ b/src/sql/sqlops.d.ts @@ -1060,6 +1060,13 @@ declare module 'sqlops' { jobId: string; } + export interface AgentJobStep { + stepId: number; + stepName: string; + message: string; + runDate: string; + } + export interface AgentJobHistoryInfo { instanceId: number; sqlMessageId: number; @@ -1077,6 +1084,7 @@ declare module 'sqlops' { operatorPaged: string; retriesAttempted: number; server: string; + steps: AgentJobStep[]; } export interface AgentServicesProvider extends DataProvider {