From 6d89b9e2034e9c532af1be101b7e43537a7a4ff4 Mon Sep 17 00:00:00 2001 From: Aditya Bist Date: Tue, 12 Jun 2018 09:28:40 -0700 Subject: [PATCH] Agent: Previous Runs chart functionality (#1564) * added basic prev run chart functionality * removed dead code * resizing columns generates charts again --- .../widgets/tasks/tasksWidget.component.ts | 2 +- .../jobManagement/common/agentJobUtilities.ts | 6 ++ .../parts/jobManagement/common/media/jobs.css | 21 +++++ .../parts/jobManagement/views/jobHistory.css | 3 - .../jobManagement/views/jobsView.component.ts | 93 ++++++++++++++++--- 5 files changed, 110 insertions(+), 15 deletions(-) diff --git a/src/sql/parts/dashboard/widgets/tasks/tasksWidget.component.ts b/src/sql/parts/dashboard/widgets/tasks/tasksWidget.component.ts index fab70d7279..e9ed27c9e8 100644 --- a/src/sql/parts/dashboard/widgets/tasks/tasksWidget.component.ts +++ b/src/sql/parts/dashboard/widgets/tasks/tasksWidget.component.ts @@ -86,7 +86,7 @@ export class TasksWidget extends DashboardWidget implements IDashboardWidget, On }).filter(i => !!i); } - this._tasks = tasks.map(i => MenuRegistry.getCommand(i)); + this._tasks = tasks.map(i => MenuRegistry.getCommand(i)).filter(v => !!v); } ngOnInit() { diff --git a/src/sql/parts/jobManagement/common/agentJobUtilities.ts b/src/sql/parts/jobManagement/common/agentJobUtilities.ts index 37d8d28423..265d1dacea 100644 --- a/src/sql/parts/jobManagement/common/agentJobUtilities.ts +++ b/src/sql/parts/jobManagement/common/agentJobUtilities.ts @@ -93,6 +93,12 @@ export class AgentJobUtilities { } } + public static convertDurationToSeconds(duration: string): number { + let split = duration.split(':'); + let seconds = (+split[0]) * 60 * 60 + (+split[1]) * 60 + (+split[2]); + return seconds; + } + public static convertColFieldToName(colField: string) { switch(colField) { case('name'): diff --git a/src/sql/parts/jobManagement/common/media/jobs.css b/src/sql/parts/jobManagement/common/media/jobs.css index 0af48b64df..f751462eb1 100644 --- a/src/sql/parts/jobManagement/common/media/jobs.css +++ b/src/sql/parts/jobManagement/common/media/jobs.css @@ -205,4 +205,25 @@ agentview-component .jobview-grid .grid-canvas > .ui-widget-content.slick-row.od #jobsDiv jobsview-component .jobview-grid .slick-cell.l1.r1.error-row td.jobview-jobnameindicatorfailure { width: 0; background: none; +} + +table.jobprevruns div.bar1, table.jobprevruns div.bar2, table.jobprevruns div.bar3, +table.jobprevruns div.bar4, table.jobprevruns div.bar5 { + padding-top: 3px; + padding-left: 5px; + width: 10px; +} + +.jobview-grid .slick-cell.l10.r10 { + text-align: center; + display: inline-flex; +} + +table.jobprevruns { + margin: auto; + height: 100%; +} + +table.jobprevruns > tbody { + vertical-align: bottom; } \ No newline at end of file diff --git a/src/sql/parts/jobManagement/views/jobHistory.css b/src/sql/parts/jobManagement/views/jobHistory.css index 4042d1bcc9..1351b86377 100644 --- a/src/sql/parts/jobManagement/views/jobHistory.css +++ b/src/sql/parts/jobManagement/views/jobHistory.css @@ -245,6 +245,3 @@ jobhistory-component > .actionbar-container .monaco-action-bar > ul.actions-cont border-top: 3px solid #444444; } - - - diff --git a/src/sql/parts/jobManagement/views/jobsView.component.ts b/src/sql/parts/jobManagement/views/jobsView.component.ts index 9163981f0f..706e2af598 100644 --- a/src/sql/parts/jobManagement/views/jobsView.component.ts +++ b/src/sql/parts/jobManagement/views/jobsView.component.ts @@ -55,16 +55,18 @@ export class JobsViewComponent implements AfterContentChecked { private columns: Array> = [ { name: nls.localize('jobColumns.name','Name'), field: 'name', formatter: (row, cell, value, columnDef, dataContext) => this.renderName(row, cell, value, columnDef, dataContext), width: 200 , id: 'name' }, - { name: nls.localize('jobColumns.lastRun','Last Run'), field: 'lastRun', minWidth: 150, id: 'lastRun' }, - { name: nls.localize('jobColumns.nextRun','Next Run'), field: 'nextRun', minWidth: 150, id: 'nextRun' }, - { name: nls.localize('jobColumns.enabled','Enabled'), field: 'enabled', minWidth: 70, id: 'enabled' }, - { name: nls.localize('jobColumns.status','Status'), field: 'currentExecutionStatus', minWidth: 60, id: 'currentExecutionStatus' }, - { name: nls.localize('jobColumns.category','Category'), field: 'category', minWidth: 150, id: 'category' }, - { name: nls.localize('jobColumns.runnable','Runnable'), field: 'runnable', minWidth: 50, id: 'runnable' }, - { name: nls.localize('jobColumns.schedule','Schedule'), field: 'hasSchedule', minWidth: 50, id: 'hasSchedule' }, - { name: nls.localize('jobColumns.lastRunOutcome', 'Last Run Outcome'), field: 'lastRunOutcome', minWidth: 150, id: 'lastRunOutcome'}, + { name: nls.localize('jobColumns.lastRun','Last Run'), field: 'lastRun', width: 120, id: 'lastRun' }, + { name: nls.localize('jobColumns.nextRun','Next Run'), field: 'nextRun', width: 120, id: 'nextRun' }, + { name: nls.localize('jobColumns.enabled','Enabled'), field: 'enabled', width: 50, id: 'enabled' }, + { name: nls.localize('jobColumns.status','Status'), field: 'currentExecutionStatus', width: 60, id: 'currentExecutionStatus' }, + { name: nls.localize('jobColumns.category','Category'), field: 'category', width: 120, 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: 120, id: 'lastRunOutcome' }, + { name: nls.localize('jobColumns.previousRuns', 'Previous Runs'), formatter: this.renderChartsPostHistory, field: 'previousRuns', width: 80, id: 'previousRuns'} ]; + private options: Slick.GridOptions = { syncColumnCellResize: true, enableColumnReorder: false, @@ -349,6 +351,14 @@ export class JobsViewComponent implements AfterContentChecked { $('#jobsDiv .jobview-grid .slick-cell.l1.r1 .jobview-jobnametext').css('width', `${nameWidth - 10}px`); // adjust error message when resized $('#jobsDiv .jobview-grid .slick-cell.l1.r1.error-row .jobview-jobnametext').css('width', '100%'); + + // generate job charts again + self.jobs.forEach(job => { + let jobId = job.jobId; + let jobHistories = self._jobCacheObject.getJobHistory(job.jobId); + let previousRuns = jobHistories.slice(jobHistories.length-5, jobHistories.length); + self.createJobChart(job.jobId, previousRuns); + }); }); // cache the dataview for future use this._jobCacheObject.dataView = this.dataView; @@ -369,7 +379,8 @@ export class JobsViewComponent implements AfterContentChecked { 'category': errorClass, 'runnable': errorClass, 'hasSchedule': errorClass, - 'lastRunOutcome': errorClass + 'lastRunOutcome': errorClass, + 'previousRuns': errorClass }; return hash; } @@ -424,6 +435,17 @@ export class JobsViewComponent implements AfterContentChecked { ''; } + private renderChartsPostHistory(row, cell, value, columnDef, dataContext) { + return ` + + + + + + +
`; + } + private expandJobRowDetails(rowIdx: number, message?: string): void { let item = this.dataView.getItemByIdx(rowIdx); item.message = this._agentViewComponent.expanded.get(item.jobId); @@ -493,11 +515,14 @@ export class JobsViewComponent implements AfterContentChecked { if (result && result.jobs) { self.jobHistories[job.jobId] = result.jobs; self._jobCacheObject.setJobHistory(job.jobId, result.jobs); + let jobHistories = self._jobCacheObject.getJobHistory(job.jobId); + let previousRuns = jobHistories.slice(jobHistories.length-5, jobHistories.length); + self.createJobChart(job.jobId, previousRuns); if (self._agentViewComponent.expanded.has(job.jobId)) { - let jobHistory = self._jobCacheObject.getJobHistory(job.jobId)[result.jobs.length - 1]; + let lastJobHistory = jobHistories[result.jobs.length-1]; let item = self.dataView.getItemById(job.jobId + '.error'); let noStepsMessage = nls.localize('jobsView.noSteps', 'No Steps available for this job.'); - let errorMessage = jobHistory ? jobHistory.message : noStepsMessage; + 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); @@ -507,6 +532,52 @@ export class JobsViewComponent implements AfterContentChecked { } } + private createJobChart(jobId: string, jobHistories: sqlops.AgentJobHistoryInfo[]): void { + let chartHeights = this.getChartHeights(jobHistories); + for (let i = 0; i < jobHistories.length; i++) { + let runGraph = $(`table#${jobId}.jobprevruns > tbody > tr > td > div.bar${i+1}`); + if (jobHistories && jobHistories.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; + }); + } else { + runGraph.css('height', '5px'); + runGraph.css('background', 'red'); + runGraph.hover((e) => { + let currentTarget = e.currentTarget; + currentTarget.title = 'Job not run.'; + }); + } + } + } + + // chart height normalization logic + private getChartHeights(jobHistories: sqlops.AgentJobHistoryInfo[]): string[] { + if (!jobHistories || jobHistories.length === 0) { + return ['5px','5px','5px','5px','5px']; + } + let maxDuration: number = 0; + jobHistories.forEach(history => { + let historyDuration = AgentJobUtilities.convertDurationToSeconds(history.runDuration) ; + if (historyDuration > maxDuration) { + maxDuration = historyDuration; + } + }); + maxDuration = maxDuration === 0 ? 1 : maxDuration; + let maxBarHeight: number = 24; + let chartHeights = []; + for (let i = 0; i < jobHistories.length; i++) { + let duration = jobHistories[i].runDuration; + let chartHeight = (maxBarHeight * AgentJobUtilities.convertDurationToSeconds(duration))/maxDuration; + chartHeights.push(`${chartHeight}px`); + } + return chartHeights; + } + private expandJobs(start: boolean): void { let expandedJobs = this._agentViewComponent.expanded; let expansions = 0;