From 1c752ed0d47e265c33ead57c75f0d847362a7311 Mon Sep 17 00:00:00 2001 From: Kathy Tran Date: Fri, 2 Feb 2024 14:37:30 -0500 Subject: [PATCH] Add drop down to display execution metrics by status (#1930) https://ucsc-cgl.atlassian.net/browse/SEAB-6199 * Display metrics by status https://ucsc-cgl.atlassian.net/browse/SEAB-6205 * Hide ALL platform if there's only one platform --- cypress/e2e/group3/metrics.ts | 15 + cypress/fixtures/sampleMetrics.json | 358 +++++++++++++----- package.json | 2 +- .../entry/execution-status.pipe.spec.ts | 25 ++ src/app/shared/entry/execution-status.pipe.ts | 32 ++ src/app/shared/pipe/pipe.module.ts | 2 + .../executions/executions-tab.component.html | 28 +- .../executions/executions-tab.component.ts | 61 ++- 8 files changed, 404 insertions(+), 119 deletions(-) create mode 100644 src/app/shared/entry/execution-status.pipe.spec.ts create mode 100644 src/app/shared/entry/execution-status.pipe.ts diff --git a/cypress/e2e/group3/metrics.ts b/cypress/e2e/group3/metrics.ts index 62732a07bc..a5858448e2 100644 --- a/cypress/e2e/group3/metrics.ts +++ b/cypress/e2e/group3/metrics.ts @@ -50,10 +50,25 @@ describe('Dockstore Metrics', () => { cy.get('[data-cy=metrics-partner-dropdown]').click(); //change partner to AGC cy.get('[data-cy=metrics-partner-options]').contains('AGC').click(); + // Successful status is selected by default + cy.get('[data-cy=metrics-execution-status-dropdown]').should('contain', 'Successful'); cy.get('[data-cy=execution-metrics-table]').should('be.visible'); + cy.get('[data-cy=execution-metrics-table]').contains('td', 'Not Available').should('not.exist'); cy.get('[data-cy=metrics-partner-dropdown]').should('contain', 'AGC'); cy.get('[data-cy=execution-metrics-total-executions-div]').should('contain', 4); cy.get('[data-cy=validations-table]').should('not.exist'); + // Change execution status to All Statuses + cy.get('[data-cy=metrics-execution-status-dropdown]').click(); + cy.get('[data-cy=metrics-execution-status-options]').contains('All Statuses').click(); + cy.get('[data-cy=metrics-execution-status-dropdown]').should('contain', 'All Statuses'); + cy.get('[data-cy=execution-metrics-table]').should('be.visible'); + cy.get('[data-cy=execution-metrics-table]').contains('td', 'Not Available').should('not.exist'); + // Change execution status to FAILED_RUNTIME_INVALID + cy.get('[data-cy=metrics-execution-status-dropdown]').click(); + cy.get('[data-cy=metrics-execution-status-options]').contains('Failed Runtime Invalid').click(); + cy.get('[data-cy=metrics-execution-status-dropdown]').should('contain', 'Failed Runtime Invalid'); + cy.get('[data-cy=execution-metrics-table]').should('exist'); + cy.get('[data-cy=execution-metrics-table]').contains('td', 'Not Available').should('be.visible'); // There were no other metrics for the FAILED_RUNTIME_INVALID executions }); it('Should not see metrics checked on versions table if no metrics', () => { diff --git a/cypress/fixtures/sampleMetrics.json b/cypress/fixtures/sampleMetrics.json index c8bbb3e586..78e10cc24b 100644 --- a/cypress/fixtures/sampleMetrics.json +++ b/cypress/fixtures/sampleMetrics.json @@ -1,11 +1,7 @@ { "GALAXY": { - "cpu": null, "executionStatusCount": null, - "executionTime": null, "id": 124, - "memory": null, - "cost": null, "validationStatus": { "id": 134, "validatorTools": { @@ -65,151 +61,305 @@ } }, "TERRA": { - "cpu": null, "executionStatusCount": { "count": { - "SUCCESSFUL": 2, - "FAILED_RUNTIME_INVALID": 1 + "ALL": { + "executionStatusCount": 3, + "cpu": null, + "executionTime": null, + "memory": null, + "cost": null + }, + "SUCCESSFUL": { + "executionStatusCount": 2, + "cpu": null, + "executionTime": null, + "memory": null, + "cost": null + }, + "FAILED_RUNTIME_INVALID": { + "executionStatusCount": 1, + "cpu": null, + "executionTime": null, + "memory": null, + "cost": null + } }, "id": 136, "numberOfFailedExecutions": 1, "numberOfSuccessfulExecutions": 2, "numberOfAbortedExecutions": 0 }, - "executionTime": null, "id": 126, - "memory": null, - "validationStatus": null, - "cost": null + "validationStatus": null + }, "AGC": { - "cpu": { - "average": 1.75, - "id": 103, - "maximum": 2, - "minimum": 1, - "numberOfDataPointsForAverage": 4, - "unit": null - }, "executionStatusCount": { "count": { - "SUCCESSFUL": 2, - "FAILED_RUNTIME_INVALID": 2 + "ALL": { + "executionStatusCount": 4, + "cpu": { + "average": 2, + "id": 103, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 2, + "unit": null + }, + "executionTime": { + "average": 20, + "maximum": 20, + "minimum": 20, + "numberOfDataPointsForAverage": 2, + "id": 104, + "unit": "s" + }, + "memory": { + "average": 2, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 2, + "id": 105, + "unit": "GB" + }, + "cost": { + "average": 2.00, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 2, + "id": 200, + "unit": "USD" + } + }, + "SUCCESSFUL": { + "executionStatusCount": 2, + "cpu": { + "average": 2, + "id": 103, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 2, + "unit": null + }, + "executionTime": { + "average": 20, + "maximum": 20, + "minimum": 20, + "numberOfDataPointsForAverage": 20, + "id": 104, + "unit": "s" + }, + "memory": { + "average": 2, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 2, + "id": 105, + "unit": "GB" + }, + "cost": { + "average": 2.00, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 2, + "id": 200, + "unit": "USD" + } + }, + "FAILED_RUNTIME_INVALID": { + "executionStatusCount": 2, + "cpu": null, + "executionTime": null, + "memory": null, + "cost": null + } }, "id": 133, "numberOfFailedExecutions": 2, "numberOfSuccessfulExecutions": 2, "numberOfAbortedExecutions": 0 }, - "executionTime": { - "average": 52.75, - "maximum": 90, - "minimum": 1, - "numberOfDataPointsForAverage": 4, - "id": 104, - "unit": "s" - }, "id": 123, - "memory": { - "average": 3.25, - "maximum": 4, - "minimum": 2, - "numberOfDataPointsForAverage": 4, - "id": 105, - "unit": "GB" - }, - "cost": { - "average": 3.25, - "maximum": 4, - "minimum": 2, - "numberOfDataPointsForAverage": 4, - "id": 200, - "unit": "USD" - }, "validationStatus": null }, "OTHER": { - "cpu": { - "average": 1.5, - "id": 106, - "maximum": 2, - "minimum": 1, - "numberOfDataPointsForAverage": 2, - "unit": null - }, "executionStatusCount": { "count": { - "FAILED_RUNTIME_INVALID": 2 + "ALL": { + "executionStatusCount": 2, + "cpu": { + "average": 1.5, + "id": 106, + "maximum": 2, + "minimum": 1, + "numberOfDataPointsForAverage": 2, + "unit": null + }, + "executionTime": { + "average": 45.5, + "maximum": 90, + "minimum": 1, + "numberOfDataPointsForAverage": 2, + "id": 107, + "unit": "s" + }, + "memory": { + "average": 3.5, + "maximum": 4, + "minimum": 3, + "numberOfDataPointsForAverage": 2, + "id": 108, + "unit": "GB" + }, + "cost": null + }, + "FAILED_RUNTIME_INVALID": { + "executionStatusCount": 2, + "cpu": { + "average": 1.5, + "id": 106, + "maximum": 2, + "minimum": 1, + "numberOfDataPointsForAverage": 2, + "unit": null + }, + "executionTime": { + "average": 45.5, + "maximum": 90, + "minimum": 1, + "numberOfDataPointsForAverage": 2, + "id": 107, + "unit": "s" + }, + "memory": { + "average": 3.5, + "maximum": 4, + "minimum": 3, + "numberOfDataPointsForAverage": 2, + "id": 108, + "unit": "GB" + }, + "cost": null + } }, "id": 135, "numberOfFailedExecutions": 2, "numberOfSuccessfulExecutions": 0, "numberOfAbortedExecutions": 0 }, - "executionTime": { - "average": 45.5, - "maximum": 90, - "minimum": 1, - "numberOfDataPointsForAverage": 2, - "id": 107, - "unit": "s" - }, "id": 125, - "memory": { - "average": 3.5, - "maximum": 4, - "minimum": 3, - "numberOfDataPointsForAverage": 2, - "id": 108, - "unit": "GB" - }, "cost": null, "validationStatus": null }, "ALL": { - "cpu": { - "average": 1.6666666666666665, - "id": 109, - "maximum": 2, - "minimum": 1, - "numberOfDataPointsForAverage": 6, - "unit": null - }, "executionStatusCount": { "count": { - "SUCCESSFUL": 4, - "FAILED_RUNTIME_INVALID": 5 + "ALL": { + "executionStatusCount": 9, + "cpu": { + "average": 2, + "id": 109, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 6, + "unit": null + }, + "executionTime": { + "average": 20, + "maximum": 20, + "minimum": 20, + "numberOfDataPointsForAverage": 6, + "id": 110, + "unit": "s" + }, + "memory": { + "average": 2, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 6, + "id": 111, + "unit": "GB" + }, + "cost": { + "average": 2, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 4, + "id": 201, + "unit": "USD" + } + }, + "SUCCESSFUL": { + "executionStatusCount": 4, + "cpu": { + "average": 2, + "id": 103, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 2, + "unit": null + }, + "executionTime": { + "average": 20, + "maximum": 20, + "minimum": 20, + "numberOfDataPointsForAverage": 20, + "id": 104, + "unit": "s" + }, + "memory": { + "average": 2, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 2, + "id": 105, + "unit": "GB" + }, + "cost": { + "average": 2.00, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 2, + "id": 200, + "unit": "USD" + } + }, + "FAILED_RUNTIME_INVALID": { + "executionStatusCount": 5, + "cpu": { + "average": 2, + "id": 106, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 2, + "unit": null + }, + "executionTime": { + "average": 20, + "maximum": 20, + "minimum": 20, + "numberOfDataPointsForAverage": 2, + "id": 107, + "unit": "s" + }, + "memory": { + "average": 2, + "maximum": 2, + "minimum": 2, + "numberOfDataPointsForAverage": 2, + "id": 108, + "unit": "GB" + }, + "cost": null + } }, "id": 137, "numberOfFailedExecutions": 5, "numberOfSuccessfulExecutions": 4, "numberOfAbortedExecutions": 0 }, - "executionTime": { - "average": 50.33333333333333, - "maximum": 90, - "minimum": 1, - "numberOfDataPointsForAverage": 6, - "id": 110, - "unit": "s" - }, "id": 127, - "memory": { - "average": 3.333333333333333, - "maximum": 4, - "minimum": 2, - "numberOfDataPointsForAverage": 6, - "id": 111, - "unit": "GB" - }, - "cost": { - "average": 3.25, - "maximum": 4, - "minimum": 2, - "numberOfDataPointsForAverage": 4, - "id": 201, - "unit": "USD" - }, "validationStatus": { "id": 138, "validatorTools": { diff --git a/package.json b/package.json index 39d570a6c6..d6d730b94e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "2.10.0", "license": "Apache License 2.0", "config": { - "webservice_version": "1.15.0-rc.0", + "webservice_version": "1.15.0-rc.2", "use_circle": false, "circle_ci_source": "https://app.circleci.com/pipelines/github/dockstore/dockstore/10707/workflows/cbcf24c8-061a-4003-9d46-9de3b953b25f/jobs/39503/artifacts", "circle_build_id": "39503" diff --git a/src/app/shared/entry/execution-status.pipe.spec.ts b/src/app/shared/entry/execution-status.pipe.spec.ts new file mode 100644 index 0000000000..c7ce6be60d --- /dev/null +++ b/src/app/shared/entry/execution-status.pipe.spec.ts @@ -0,0 +1,25 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ExecutionStatusPipe } from './execution-status.pipe'; +import { RunExecution } from '../openapi'; +import ExecutionStatusEnum = RunExecution.ExecutionStatusEnum; + +describe('Pipe: ExecutionStatus', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ExecutionStatusPipe], + }); + }); + + it('create an instance', () => { + const pipe = new ExecutionStatusPipe(); + expect(pipe).toBeTruthy(); + expect(pipe.transform(ExecutionStatusEnum.ALL)).toBe('All Statuses'); + expect(pipe.transform(ExecutionStatusEnum.SUCCESSFUL)).toBe('Successful'); + expect(pipe.transform(ExecutionStatusEnum.ABORTED)).toBe('Aborted'); + expect(pipe.transform(ExecutionStatusEnum.FAILED)).toBe('Failed'); + expect(pipe.transform(ExecutionStatusEnum.FAILEDRUNTIMEINVALID)).toBe('Failed Runtime Invalid'); + expect(pipe.transform(ExecutionStatusEnum.FAILEDSEMANTICINVALID)).toBe('Failed Semantic Invalid'); + }); +}); diff --git a/src/app/shared/entry/execution-status.pipe.ts b/src/app/shared/entry/execution-status.pipe.ts new file mode 100644 index 0000000000..33407f44f2 --- /dev/null +++ b/src/app/shared/entry/execution-status.pipe.ts @@ -0,0 +1,32 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { RunExecution } from '../../shared/openapi'; +import ExecutionStatusEnum = RunExecution.ExecutionStatusEnum; + +@Pipe({ + name: 'executionStatus', +}) +export class ExecutionStatusPipe implements PipeTransform { + /** + * Transforms ExecutionStatusEnum into their display name + * @param executionStatus + * @returns {string} + */ + transform(executionStatus: string): string { + switch (executionStatus) { + case ExecutionStatusEnum.ALL: + return 'All Statuses'; + case ExecutionStatusEnum.SUCCESSFUL: + return 'Successful'; + case ExecutionStatusEnum.ABORTED: + return 'Aborted'; + case ExecutionStatusEnum.FAILED: + return 'Failed'; + case ExecutionStatusEnum.FAILEDRUNTIMEINVALID: + return 'Failed Runtime Invalid'; + case ExecutionStatusEnum.FAILEDSEMANTICINVALID: + return 'Failed Semantic Invalid'; + default: + return executionStatus; + } + } +} diff --git a/src/app/shared/pipe/pipe.module.ts b/src/app/shared/pipe/pipe.module.ts index 503d65dedc..1e971fd872 100644 --- a/src/app/shared/pipe/pipe.module.ts +++ b/src/app/shared/pipe/pipe.module.ts @@ -18,6 +18,7 @@ import { SearchAuthorsHtmlPipe } from 'app/search/search-authors-html.pipe'; import { PlatformPartnerPipe } from '../entry/platform-partner.pipe'; import { JoinWithEllipsesPipe } from 'app/search/join-with-ellipses.pipe'; import { SecondsToHoursMinutesSecondsPipe } from 'app/workflow/executions/seconds-to-hours-minutes-seconds.pipe'; +import { ExecutionStatusPipe } from '../entry/execution-status.pipe'; const DECLARATIONS: any[] = [ FilePathPipe, @@ -37,6 +38,7 @@ const DECLARATIONS: any[] = [ PlatformPartnerPipe, JoinWithEllipsesPipe, SecondsToHoursMinutesSecondsPipe, + ExecutionStatusPipe, ]; @NgModule({ imports: [CommonModule], diff --git a/src/app/workflow/executions/executions-tab.component.html b/src/app/workflow/executions/executions-tab.component.html index 5e5fb39af9..32c6b3f8c5 100644 --- a/src/app/workflow/executions/executions-tab.component.html +++ b/src/app/workflow/executions/executions-tab.component.html @@ -38,9 +38,20 @@
Failed executions: {{ failedExecutions | number }}
Aborted executions: {{ abortedExecutions | number }}
+ + Select an Execution Status + + + {{ executionStatus | executionStatus }} + + + - + + + + + +
Minimum + Minimum + - Average + Average + - Maximum + Maximum +
Not Available
diff --git a/src/app/workflow/executions/executions-tab.component.ts b/src/app/workflow/executions/executions-tab.component.ts index 1bf4827179..3ac088ec05 100644 --- a/src/app/workflow/executions/executions-tab.component.ts +++ b/src/app/workflow/executions/executions-tab.component.ts @@ -25,11 +25,13 @@ import { BioWorkflow, Notebook, Service, + MetricsByStatus, + RunExecution, } from '../../shared/openapi'; import { SessionQuery } from '../../shared/session/session.query'; import { takeUntil } from 'rxjs/operators'; -import { CheckerWorkflowQuery } from '../../shared/state/checker-workflow.query'; import PartnerEnum = CloudInstance.PartnerEnum; +import ExecutionStatusEnum = RunExecution.ExecutionStatusEnum; import { MatSelectChange } from '@angular/material/select'; import { AlertService } from '../../shared/alert/state/alert.service'; @@ -60,6 +62,9 @@ export class ExecutionsTabComponent extends EntryTab implements OnChanges { abortedExecutions: number; successfulExecutionRate: number | null; executionMetricsExist: boolean; + currentExecutionStatus: string; + executionStatusToMetrics: Map; + executionStatuses: string[]; // Fields for validator tool metrics validationsColumns: string[] = [ 'validatorToolVersionName', @@ -104,7 +109,10 @@ export class ExecutionsTabComponent extends EntryTab implements OnChanges { this.partners = Array.from(this.metrics.keys()); this.metricsExist = this.partners.length > 0; if (this.metricsExist) { - // Display metrics for ALL platforms + // Remove the ALL platform if there's only one execution + if (this.partners.length === 2 && this.partners.filter((partner) => partner === PartnerEnum.ALL).length === 1) { + this.partners = this.partners.filter((partner) => partner !== PartnerEnum.ALL); + } const platform = this.partners.filter((partner) => partner === PartnerEnum.ALL).length === 1 ? this.partners.filter((partner) => partner === PartnerEnum.ALL)[0] @@ -140,6 +148,7 @@ export class ExecutionsTabComponent extends EntryTab implements OnChanges { this.failedExecutions = null; this.abortedExecutions = null; this.successfulExecutionRate = null; + this.currentExecutionStatus = null; this.currentPartner = null; this.currentValidatorTool = null; this.validatorTools = []; @@ -147,15 +156,32 @@ export class ExecutionsTabComponent extends EntryTab implements OnChanges { private loadExecutionMetricsData(partner: PartnerEnum) { const metrics = this.metrics.get(partner); - this.executionMetricsTable = this.createExecutionsTable(metrics); - this.executionMetricsExist = - metrics?.cpu !== null || - metrics?.memory !== null || - metrics?.executionTime !== null || - metrics?.executionStatusCount !== null || - metrics?.cost !== null; + this.executionMetricsExist = metrics?.executionStatusCount !== null; if (metrics?.executionStatusCount) { + this.executionStatusToMetrics = new Map(Object.entries(metrics.executionStatusCount.count)); + + // Set the default execution status + this.executionStatuses = Array.from(this.executionStatusToMetrics.keys()); + if (this.executionStatuses) { + // Remove the ALL status if there's only one execution + if ( + this.executionStatuses.length === 2 && + this.executionStatuses.filter((status) => status === ExecutionStatusEnum.ALL).length === 1 + ) { + this.executionStatuses = this.executionStatuses.filter((status) => status !== ExecutionStatusEnum.ALL); + } + // Pick the default execution status to display. + let defaultExecutionStatus = this.executionStatuses[0]; + if (this.executionStatuses.includes(ExecutionStatusEnum.SUCCESSFUL)) { + defaultExecutionStatus = this.executionStatuses.find((status) => status === ExecutionStatusEnum.SUCCESSFUL); + } else if (this.executionStatuses.includes(ExecutionStatusEnum.ALL)) { + defaultExecutionStatus = this.executionStatuses.find((status) => status === ExecutionStatusEnum.ALL); + } + + this.onSelectedExecutionStatusChange(defaultExecutionStatus); + } + this.successfulExecutions = metrics.executionStatusCount.numberOfSuccessfulExecutions; this.failedExecutions = metrics.executionStatusCount.numberOfFailedExecutions; this.abortedExecutions = metrics.executionStatusCount.numberOfAbortedExecutions; @@ -171,7 +197,7 @@ export class ExecutionsTabComponent extends EntryTab implements OnChanges { * @param metrics * @returns */ - private createExecutionsTable(metrics: Metrics | null): ExecutionMetricsTableObject[] { + private createExecutionsTable(metrics: MetricsByStatus | null): ExecutionMetricsTableObject[] { let executionsTable: ExecutionMetricsTableObject[] = []; // Only add the rows if there are data for that type if (metrics) { @@ -191,6 +217,21 @@ export class ExecutionsTabComponent extends EntryTab implements OnChanges { return executionsTable; } + /** + * Called when the selected execution status is changed + * @param {string} executionStatus - New execution status + * @return {void} + */ + onSelectedExecutionStatusChange(executionStatus: string): void { + if (executionStatus) { + this.currentExecutionStatus = executionStatus; + this.executionMetricsTable = this.createExecutionsTable(this.executionStatusToMetrics.get(executionStatus)); + } else { + this.currentExecutionStatus = null; + this.executionMetricsTable = []; + } + } + private loadValidationsData(partner: PartnerEnum) { this.validatorToolMetricsExist = this.metrics.get(partner).validationStatus != null; if (this.validatorToolMetricsExist) {