diff --git a/extensions/agent/src/data/jobData.ts b/extensions/agent/src/data/jobData.ts index c5b6bff3c0..ca16a07208 100644 --- a/extensions/agent/src/data/jobData.ts +++ b/extensions/agent/src/data/jobData.ts @@ -59,6 +59,9 @@ export class JobData implements IAgentDialogData { this.category = jobInfo.category; this.description = jobInfo.description; this.enabled = jobInfo.enabled; + this.jobSteps = jobInfo.JobSteps; + this.jobSchedules = jobInfo.JobSchedules; + this.alerts = jobInfo.Alerts; } } @@ -105,8 +108,6 @@ export class JobData implements IAgentDialogData { displayName: this.JobCompletionActionCondition_Always, name: sqlops.JobCompletionActionCondition.Always.toString() }]; - - this.jobSchedules = []; } public async save() { diff --git a/extensions/agent/src/dialogs/jobDialog.ts b/extensions/agent/src/dialogs/jobDialog.ts index 926642ca15..ab86a4f699 100644 --- a/extensions/agent/src/dialogs/jobDialog.ts +++ b/extensions/agent/src/dialogs/jobDialog.ts @@ -62,6 +62,8 @@ export class JobDialog extends AgentDialog { private readonly AlertsTopLabelString: string = localize('jobDialog.alertsList', 'Alerts list'); private readonly NewAlertButtonString: string = localize('jobDialog.newAlert', 'New Alert'); private readonly AlertNameLabelString: string = localize('jobDialog.alertNameLabel', 'Alert Name'); + private readonly AlertEnabledLabelString: string = localize('jobDialog.alertEnabledLabel', 'Enabled'); + private readonly AlertTypeLabelString: string = localize('jobDialog.alertTypeLabel', 'Type'); // UI Components private generalTab: sqlops.window.modelviewdialog.DialogTab; @@ -197,6 +199,8 @@ export class JobDialog extends AgentDialog { .withProperties({ value: 'Feature Preview' }).component(); + let steps = this.model.jobSteps ? this.model.jobSteps : []; + let data = this.convertStepsToData(steps); this.stepsTable = view.modelBuilder.table() .withProperties({ columns: [ @@ -206,8 +210,8 @@ export class JobDialog extends AgentDialog { this.StepsTable_SuccessColumnString, this.StepsTable_FailureColumnString ], - data: [], - height: 430 + data: data, + height: 750 }).component(); this.moveStepUpButton = view.modelBuilder.button() @@ -250,6 +254,31 @@ export class JobDialog extends AgentDialog { }).component(); this.stepsTable.enabled = false; + this.editStepButton.enabled = false; + this.deleteStepButton.enabled = false; + + this.stepsTable.onRowSelected(() => { + // only let edit or delete steps if there's + // one step selection + if (this.stepsTable.selectedRows.length === 1) { + let rowNumber = this.stepsTable.selectedRows[0]; + let stepData = steps[rowNumber]; + this.deleteStepButton.enabled = true; + this.editStepButton.enabled = true; + this.editStepButton.onDidClick((e) => { + // implement edit steps + + // let stepDialog = new JobStepDialog(this.model.ownerUri, this.nameTextBox.value, '' , 1, this.model); + // stepDialog.openNewStepDialog(); + }); + + this.deleteStepButton.onDidClick((e) => { + // implement delete steps + + + }); + } + }); let formModel = view.modelBuilder.formContainer() .withFormItems([{ @@ -271,12 +300,16 @@ export class JobDialog extends AgentDialog { .withProperties({ value: 'Feature Preview' }).component(); + let alerts = this.model.alerts ? this.model.alerts : []; + let data = this.convertAlertsToData(alerts); this.alertsTable = view.modelBuilder.table() .withProperties({ columns: [ - this.AlertNameLabelString + this.AlertNameLabelString, + this.AlertEnabledLabelString, + this.AlertTypeLabelString ], - data: [], + data: data, height: 430, width: 400 }).component(); @@ -312,10 +345,12 @@ export class JobDialog extends AgentDialog { this.schedulesTable = view.modelBuilder.table() .withProperties({ columns: [ - this.ScheduleNameLabelString + PickScheduleDialog.SchedulesIDText, + PickScheduleDialog.ScheduleNameLabelText, + PickScheduleDialog.ScheduleDescription ], data: [], - height: 430, + height: 750, width: 420 }).component(); @@ -350,13 +385,11 @@ export class JobDialog extends AgentDialog { } private populateScheduleTable() { - if (this.model.jobSchedules) { - let data: any[][] = []; - for (let i = 0; i < this.model.jobSchedules.length; ++i) { - let schedule = this.model.jobSchedules[i]; - data[i] = [ schedule.name ]; - } + let schedules = this.model.jobSchedules ? this.model.jobSchedules : []; + let data = this.convertSchedulesToData(schedules); + if (data.length > 0) { this.schedulesTable.data = data; + this.schedulesTable.height = 750; } } @@ -466,6 +499,45 @@ export class JobDialog extends AgentDialog { }); } + private convertStepsToData(jobSteps: sqlops.AgentJobStepInfo[]): any[][] { + let result = []; + jobSteps.forEach(jobStep => { + let cols = []; + cols.push(jobStep.id); + cols.push(jobStep.stepName); + cols.push(jobStep.subSystem); + cols.push(jobStep.successAction); + cols.push(jobStep.failureAction); + result.push(cols); + }); + return result; + } + + private convertSchedulesToData(jobSchedules: sqlops.AgentJobScheduleInfo[]): any[][] { + let result = []; + jobSchedules.forEach(schedule => { + let cols = []; + cols.push(schedule.id); + cols.push(schedule.name); + cols.push(schedule.description); + result.push(cols); + }); + return result; + } + + private convertAlertsToData(alerts: sqlops.AgentAlertInfo[]): any[][] { + let result = []; + alerts.forEach(alert => { + let cols = []; + console.log(alert); + cols.push(alert.name); + cols.push(alert.isEnabled); + cols.push(alert.alertType.toString()); + result.push(cols); + }); + return result; + } + protected updateModel() { this.model.name = this.nameTextBox.value; this.model.owner = this.ownerTextBox.value; diff --git a/extensions/agent/src/dialogs/pickScheduleDialog.ts b/extensions/agent/src/dialogs/pickScheduleDialog.ts index d2d0a6188c..76e17e94a3 100644 --- a/extensions/agent/src/dialogs/pickScheduleDialog.ts +++ b/extensions/agent/src/dialogs/pickScheduleDialog.ts @@ -18,8 +18,11 @@ export class PickScheduleDialog { private readonly DialogTitle: string = localize('pickSchedule.jobSchedules', 'Job Schedules'); private readonly OkButtonText: string = localize('pickSchedule.ok', 'OK'); private readonly CancelButtonText: string = localize('pickSchedule.cancel', 'Cancel'); - private readonly ScheduleNameLabelText: string = localize('pickSchedule.scheduleName', 'Schedule Name'); - private readonly SchedulesLabelText: string = localize('pickSchedule.schedules', 'Schedules'); + private readonly SchedulesLabelText: string = localize('pickSchedule.availableSchedules', 'Available Schedules:'); + public static readonly ScheduleNameLabelText: string = localize('pickSchedule.scheduleName', 'Name'); + public static readonly SchedulesIDText: string = localize('pickSchedule.scheduleID','ID'); + public static readonly ScheduleDescription: string = localize('pickSchedule.description','Description'); + // UI Components private dialog: sqlops.window.modelviewdialog.Dialog; @@ -50,11 +53,13 @@ export class PickScheduleDialog { this.schedulesTable = view.modelBuilder.table() .withProperties({ columns: [ - this.ScheduleNameLabelText + PickScheduleDialog.SchedulesIDText, + PickScheduleDialog.ScheduleNameLabelText, + PickScheduleDialog.ScheduleDescription ], data: [], - height: '80em', - width: '40em' + height: 750, + width: 430 }).component(); let formModel = view.modelBuilder.formContainer() @@ -69,7 +74,8 @@ export class PickScheduleDialog { let data: any[][] = []; for (let i = 0; i < this.model.schedules.length; ++i) { let schedule = this.model.schedules[i]; - data[i] = [ schedule.name ]; + console.log(schedule); + data[i] = [ schedule.id, schedule.name, schedule.description ]; } this.schedulesTable.data = data; } diff --git a/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts b/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts index 9a821b1ea6..a24cbaddab 100644 --- a/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/headerFilter.plugin.ts @@ -39,16 +39,26 @@ export class HeaderFilter { this.handler.subscribe(this.grid.onHeaderCellRendered, (e, args) => this.handleHeaderCellRendered(e , args)) .subscribe(this.grid.onBeforeHeaderCellDestroy, (e, args) => this.handleBeforeHeaderCellDestroy(e, args)) .subscribe(this.grid.onClick, (e) => this.handleBodyMouseDown) - .subscribe(this.grid.onColumnsResized, () => this.columnsResized()); - + .subscribe(this.grid.onColumnsResized, () => this.columnsResized()) + .subscribe(this.grid.onKeyDown, (e) => this.handleKeyDown); this.grid.setColumns(this.grid.getColumns()); $(document.body).bind('mousedown', this.handleBodyMouseDown); + $(document.body).bind('keydown', this.handleKeyDown); } public destroy() { this.handler.unsubscribeAll(); $(document.body).unbind('mousedown', this.handleBodyMouseDown); + $(document.body).unbind('keydown', this.handleKeyDown); + } + + private handleKeyDown = (e) => { + if (this.$menu && (e.key === 'Escape' || e.keyCode === 27)) { + this.hideMenu(); + e.preventDefault(); + e.stopPropagation(); + } } private handleBodyMouseDown = (e) => { diff --git a/src/sql/parts/jobManagement/common/jobManagementUtilities.ts b/src/sql/parts/jobManagement/common/jobManagementUtilities.ts index febf1b4837..9b75625d20 100644 --- a/src/sql/parts/jobManagement/common/jobManagementUtilities.ts +++ b/src/sql/parts/jobManagement/common/jobManagementUtilities.ts @@ -17,7 +17,9 @@ export class JobManagementUtilities { switch(status) { case(0): return nls.localize('agentUtilities.failed','Failed'); case(1): return nls.localize('agentUtilities.succeeded', 'Succeeded'); - case(3): return nls.localize('agentUtilities.canceled', 'Canceled'); + case(2): return nls.localize('agentUtilities.retry', 'Retry'); + case(3): return nls.localize('agentUtilities.canceled', 'Cancelled'); + case(4): return nls.localize('agentUtilities.inProgress', 'In Progress'); case(5): return nls.localize('agentUtilities.statusUnknown', 'Status Unknown'); default: return nls.localize('agentUtilities.statusUnknown', 'Status Unknown'); } diff --git a/src/sql/parts/jobManagement/views/jobHistory.component.ts b/src/sql/parts/jobManagement/views/jobHistory.component.ts index 76148857ff..7bb4953634 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.component.ts +++ b/src/sql/parts/jobManagement/views/jobHistory.component.ts @@ -185,8 +185,8 @@ export class JobHistoryComponent extends JobManagementView implements OnInit { stepViewRow.runStatus = jobStepStatus ? JobManagementUtilities.convertToStatusString(0) : JobManagementUtilities.convertToStatusString(step.runStatus); self._runStatus = JobManagementUtilities.convertToStatusString(self.agentJobHistoryInfo.runStatus); - stepViewRow.stepName = step.stepName; - stepViewRow.stepID = step.stepId.toString(); + stepViewRow.stepName = step.stepDetails.stepName; + stepViewRow.stepId = step.stepDetails.id.toString(); return stepViewRow; }); this._showSteps = self._stepRows.length > 0; diff --git a/src/sql/parts/jobManagement/views/jobStepsView.component.ts b/src/sql/parts/jobManagement/views/jobStepsView.component.ts index 01988379f3..495e62abe8 100644 --- a/src/sql/parts/jobManagement/views/jobStepsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobStepsView.component.ts @@ -66,7 +66,7 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit, }, { verticalScrollMode: ScrollbarVisibility.Visible }); this._register(attachListStyler(this._tree, this.themeService)); } - this._tree.layout(JobStepsViewComponent._pageSize); + this._tree.layout(500); this._tree.setInput(new JobStepsViewModel()); $('jobstepsview-component .steps-tree .monaco-tree').attr('tabIndex', '-1'); $('jobstepsview-component .steps-tree .monaco-tree-row').attr('tabIndex', '0'); @@ -74,8 +74,6 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit, } ngOnInit() { - - let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri; this._tree = new Tree(this._tableContainer.nativeElement, { controller: this._treeController, dataSource: this._treeDataSource, diff --git a/src/sql/parts/jobManagement/views/jobStepsViewTree.ts b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts index 9859f10611..49c021f4cd 100644 --- a/src/sql/parts/jobManagement/views/jobStepsViewTree.ts +++ b/src/sql/parts/jobManagement/views/jobStepsViewTree.ts @@ -14,7 +14,7 @@ import { AgentJobHistoryInfo } from 'sqlops'; import { JobManagementUtilities } from 'sql/parts/jobManagement/common/jobManagementUtilities'; export class JobStepsViewRow { - public stepID: string; + public stepId: string; public stepName: string; public message: string; public rowID: string = generateUuid(); @@ -121,7 +121,7 @@ export class JobStepsViewRenderer implements tree.IRenderer { 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; + stepIdCol.innerText = element.stepId; let stepNameCol: HTMLElement = DOM.$('div'); stepNameCol.className = 'tree-name-col'; stepNameCol.innerText = element.stepName; diff --git a/src/sql/parts/jobManagement/views/jobsView.component.ts b/src/sql/parts/jobManagement/views/jobsView.component.ts index 5a5738efb1..6625051cad 100644 --- a/src/sql/parts/jobManagement/views/jobsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobsView.component.ts @@ -37,6 +37,7 @@ import { IDashboardService } from 'sql/services/dashboard/common/dashboardServic import { escape } from 'sql/base/common/strings'; import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { tableBackground, cellBackground, tableHoverBackground, jobsHeadingBackground, cellBorderColor } from 'sql/common/theme/colors'; +import { JobStepsViewRow } from 'sql/parts/jobManagement/views/jobStepsViewTree'; export const JOBSVIEW_SELECTOR: string = 'jobsview-component'; export const ROW_HEIGHT: number = 45; @@ -171,7 +172,6 @@ export class JobsViewComponent extends JobManagementView implements OnInit { }); this.rowDetail = rowDetail; columns.unshift(this.rowDetail.getColumnDefinition()); - let filterPlugin = new HeaderFilter({}, this._themeService); this.filterPlugin = filterPlugin; $(this._gridEl.nativeElement).empty(); @@ -475,7 +475,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit { case ('Failed'): resultIndicatorClass = 'jobview-jobnameindicatorfailure'; break; - case ('Canceled'): + case ('Cancelled'): resultIndicatorClass = 'jobview-jobnameindicatorcancel'; break; case ('Status Unknown'): @@ -523,17 +523,15 @@ export class JobsViewComponent extends JobManagementView implements OnInit { this.rowDetail.applyTemplateNewLineHeight(item, true); } - private loadJobHistories(): void { + private async loadJobHistories() { if (this.jobs) { - let erroredJobs = 0; 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]; - this.curateJobHistory(failing, ownerUri); let passing = separatedJobs[1]; - this.curateJobHistory(passing, ownerUri); + Promise.all([this.curateJobHistory(failing, ownerUri), this.curateJobHistory(passing, ownerUri)]); } } @@ -578,11 +576,10 @@ export class JobsViewComponent extends JobManagementView implements OnInit { return job; } - private curateJobHistory(jobs: sqlops.AgentJobInfo[], ownerUri: string) { + private async curateJobHistory(jobs: sqlops.AgentJobInfo[], ownerUri: string) { const self = this; - for (let i = 0; i < jobs.length; i++) { - let job = jobs[i]; - this._jobManagementService.getJobHistory(ownerUri, job.jobId).then((result) => { + jobs.forEach(async (job) => { + await this._jobManagementService.getJobHistory(ownerUri, job.jobId).then((result) => { if (result && result.jobs) { self.jobHistories[job.jobId] = result.jobs; self._jobCacheObject.setJobHistory(job.jobId, result.jobs); @@ -600,7 +597,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit { } } }); - } + }); } private createJobChart(jobId: string, jobHistories: sqlops.AgentJobHistoryInfo[]): void { @@ -848,6 +845,40 @@ export class JobsViewComponent extends JobManagementView implements OnInit { return TPromise.as(actions); } + protected convertStepsToStepInfos(steps: sqlops.AgentJobStep[], job: sqlops.AgentJobInfo): sqlops.AgentJobStepInfo[] { + let result = []; + steps.forEach(step => { + let stepInfo: sqlops.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): any { let data = this._table.grid.getData(); if (!data || rowIndex >= data.getLength()) { @@ -855,10 +886,60 @@ export class JobsViewComponent extends JobManagementView implements OnInit { } let jobId = data.getItem(rowIndex).jobId; - let job = this.jobs.filter(job => { + 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: sqlops.AgentJobInfo[] = this.jobs.filter(job => { return job.jobId === jobId; }); - + let jobHistories = this.jobHistories[jobId]; + let steps: sqlops.AgentJobStep[] = undefined; + let schedules: sqlops.AgentJobScheduleInfo[] = undefined; + let alerts: sqlops.AgentAlertInfo[] = undefined; + if (jobHistories && jobHistories[jobHistories.length-1]) { + // add steps + steps = jobHistories[jobHistories.length-1].steps; + if (steps && steps.length > 0) { + if (!job[0].JobSteps) { + job[0].JobSteps = []; + } + if (job[0].JobSteps.length !== steps.length) { + job[0].JobSteps = []; + steps.forEach(step => { + job[0].JobSteps.push(step.stepDetails); + }); + } + } + // add schedules + schedules = jobHistories[jobHistories.length-1].schedules; + if (schedules && schedules.length > 0) { + if (!job[0].JobSchedules) { + job[0].JobSchedules = []; + } + if (job[0].JobSchedules.length !== schedules.length) { + job[0].JobSchedules = []; + schedules.forEach(schedule => { + job[0].JobSchedules.push(schedule); + }); + } + } + // add alerts + alerts = jobHistories[jobHistories.length-1].alerts; + if (!job[0].Alerts) { + job[0].Alerts = []; + } + if (job[0].Alerts.length !== alerts.length) { + job[0].Alerts = []; + alerts.forEach(alert => { + job[0].Alerts.push(alert); + }); + } + } return job && job.length > 0 ? job[0] : undefined; } diff --git a/src/sql/sqlops.d.ts b/src/sql/sqlops.d.ts index 2515752e79..401cc534e4 100644 --- a/src/sql/sqlops.d.ts +++ b/src/sql/sqlops.d.ts @@ -1313,6 +1313,7 @@ declare module 'sqlops' { jobCount: number; activeEndDate: string; scheduleUid: string; + description: string; } export interface AgentJobStep { @@ -1322,6 +1323,7 @@ declare module 'sqlops' { message: string; runDate: string; runStatus: number; + stepDetails: AgentJobStepInfo; } export interface AgentJobStepInfo { @@ -1369,6 +1371,8 @@ declare module 'sqlops' { retriesAttempted: string; server: string; steps: AgentJobStep[]; + schedules: AgentJobScheduleInfo[]; + alerts: AgentAlertInfo[]; } export interface AgentProxyInfo {