From: Lucas Di Pentima Date: Tue, 12 Apr 2022 19:29:17 +0000 (-0300) Subject: Merge branch '18881-process-runtime-status'. Closes #18881 X-Git-Tag: 2.4.1~5 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/08ac60f877d6495a748747a3a0d30ca9f0e289d5?hp=a9d377ba93f19f1b4ebb389d3baa23b26ef0d860 Merge branch '18881-process-runtime-status'. Closes #18881 Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima --- diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js index 74acd056..39a2af42 100644 --- a/cypress/integration/collection.spec.js +++ b/cypress/integration/collection.spec.js @@ -252,8 +252,8 @@ describe('Collection panel tests', function () { .and('not.contain', 'anotherKey: anotherValue'); // Check that the file listing show both read & write operations cy.get('[data-cy=collection-files-panel]').within(() => { - cy.wait(1000); - cy.root().should('contain', fileName); + cy.get('[data-cy=collection-files-right-panel]', { timeout: 5000 }) + .should('contain', fileName); if (isWritable) { cy.get('[data-cy=upload-button]') .should(`${isWritable ? '' : 'not.'}contain`, 'Upload data'); @@ -335,7 +335,7 @@ describe('Collection panel tests', function () { ]; eachPair(names, (from, to) => { cy.get('[data-cy=collection-files-panel]') - .contains(`${from}`).rightclick(); + .contains(`${from}`).rightclick({force: true}); cy.get('[data-cy=context-menu]') .contains('Rename') .click(); @@ -842,7 +842,7 @@ describe('Collection panel tests', function () { cy.get('[data-cy=form-submit-btn]').click(); - cy.get('.layout-pane-primary', { wait: 12000 }).contains('Projects').click(); + cy.get('.layout-pane-primary', { timeout: 12000 }).contains('Projects').click(); cy.get('main').contains(`Files extracted from: ${this.collection.name}`).should('exist'); }); @@ -976,7 +976,6 @@ describe('Collection panel tests', function () { cy.get('[data-cy=form-submit-btn]').should('not.exist'); cy.get('[data-cy=collection-files-right-panel]') .contains('5mb_b.bin').should('exist'); - }); }); }); diff --git a/cypress/integration/process.spec.js b/cypress/integration/process.spec.js index 75c318db..3234f7c4 100644 --- a/cypress/integration/process.spec.js +++ b/cypress/integration/process.spec.js @@ -191,4 +191,58 @@ describe('Process tests', function() { }); }); }); + + it('should show runtime status indicators', function() { + // Setup running container with runtime_status error & warning messages + createContainerRequest( + activeUser, + 'test_container_request', + 'arvados/jobs', + ['echo', 'hello world'], + false, 'Committed') + .as('containerRequest') + .then(function(containerRequest) { + expect(containerRequest.state).to.equal('Committed'); + expect(containerRequest.container_uuid).not.to.be.equal(''); + + cy.getContainer(activeUser.token, containerRequest.container_uuid) + .then(function(queuedContainer) { + expect(queuedContainer.state).to.be.equal('Queued'); + }); + cy.updateContainer(adminUser.token, containerRequest.container_uuid, { + state: 'Locked' + }).then(function(lockedContainer) { + expect(lockedContainer.state).to.be.equal('Locked'); + + cy.updateContainer(adminUser.token, lockedContainer.uuid, { + state: 'Running', + runtime_status: { + error: 'Something went wrong', + errorDetail: 'Process exited with status 1', + warning: 'Free disk space is low', + } + }) + .as('runningContainer') + .then(function(runningContainer) { + expect(runningContainer.state).to.be.equal('Running'); + expect(runningContainer.runtime_status).to.be.deep.equal({ + 'error': 'Something went wrong', + 'errorDetail': 'Process exited with status 1', + 'warning': 'Free disk space is low', + }); + }); + }) + }); + // Test that the UI shows the error and warning messages + cy.getAll('@containerRequest', '@runningContainer').then(function([containerRequest]) { + cy.loginAs(activeUser); + cy.goToPath(`/processes/${containerRequest.uuid}`); + cy.get('[data-cy=process-runtime-status-error]') + .should('contain', 'Something went wrong') + .and('contain', 'Process exited with status 1'); + cy.get('[data-cy=process-runtime-status-warning]') + .should('contain', 'Free disk space is low') + .and('contain', 'No additional warning details available'); + }); + }); }); \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 5a2428b2..a28308e3 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -135,11 +135,7 @@ Cypress.Commands.add( Cypress.Commands.add( "getCollection", (token, uuid) => { - return cy.doRequest('GET', `/arvados/v1/collections/${uuid}`, null, {}, token) - .its('body') - .then(function (theCollection) { - return theCollection; - }) + return cy.getResource(token, 'collections', uuid) } ) @@ -160,6 +156,20 @@ Cypress.Commands.add( } ) +Cypress.Commands.add( + "getContainer", (token, uuid) => { + return cy.getResource(token, 'containers', uuid) + } +) + +Cypress.Commands.add( + "updateContainer", (token, uuid, data) => { + return cy.updateResource(token, 'containers', uuid, { + container: JSON.stringify(data) + }) + } +) + Cypress.Commands.add( 'createContainerRequest', (token, data) => { return cy.createResource(token, 'container_requests', { @@ -212,13 +222,23 @@ Cypress.Commands.add( } ) +Cypress.Commands.add( + "getResource", (token, suffix, uuid) => { + return cy.doRequest('GET', `/arvados/v1/${suffix}/${uuid}`, null, {}, token) + .its('body') + .then(function (resource) { + return resource; + }) + } +) + Cypress.Commands.add( "createResource", (token, suffix, data) => { return cy.doRequest('POST', '/arvados/v1/' + suffix, data, null, token, true) - .its('body').as('resource') - .then(function () { - createdResources.push({suffix, uuid: this.resource.uuid}); - return this.resource; + .its('body') + .then(function (resource) { + createdResources.push({suffix, uuid: resource.uuid}); + return resource; }) } ) @@ -226,19 +246,19 @@ Cypress.Commands.add( Cypress.Commands.add( "deleteResource", (token, suffix, uuid, failOnStatusCode = true) => { return cy.doRequest('DELETE', '/arvados/v1/' + suffix + '/' + uuid, null, null, token, false, true, failOnStatusCode) - .its('body').as('resource') - .then(function () { - return this.resource; + .its('body') + .then(function (resource) { + return resource; }) } ) Cypress.Commands.add( "updateResource", (token, suffix, uuid, data) => { - return cy.doRequest('PUT', '/arvados/v1/' + suffix + '/' + uuid, data, null, token, true) - .its('body').as('resource') - .then(function () { - return this.resource; + return cy.doRequest('PATCH', '/arvados/v1/' + suffix + '/' + uuid, data, null, token, true) + .its('body') + .then(function (resource) { + return resource; }) } ) diff --git a/src/common/custom-theme.ts b/src/common/custom-theme.ts index 74dee7f6..b0703237 100644 --- a/src/common/custom-theme.ts +++ b/src/common/custom-theme.ts @@ -23,13 +23,18 @@ export interface ArvadosTheme extends Theme { interface Colors { green700: string; + yellow100: string; yellow700: string; + yellow900: string; + red100: string; red900: string; blue500: string; + grey500: string; purple: string; } const arvadosPurple = '#361336'; +const grey500 = grey["500"]; const grey600 = grey["600"]; const grey700 = grey["700"]; const grey900 = grey["900"]; @@ -41,9 +46,13 @@ export const themeOptions: ArvadosThemeOptions = { customs: { colors: { green700: green["700"], + yellow100: yellow["100"], yellow700: yellow["700"], + yellow900: yellow["900"], + red100: red["100"], red900: red['900'], blue500: blue['500'], + grey500: grey500, purple: arvadosPurple } }, diff --git a/src/models/container.ts b/src/models/container.ts index e931c4bf..127c2508 100644 --- a/src/models/container.ts +++ b/src/models/container.ts @@ -6,6 +6,7 @@ import { Resource, ResourceKind } from "./resource"; import { MountType } from 'models/mount-types'; import { RuntimeConstraints } from "models/runtime-constraints"; import { SchedulingParameters } from './scheduling-parameters'; +import { RuntimeStatus } from "./runtime-status"; export enum ContainerState { QUEUED = 'Queued', @@ -27,6 +28,7 @@ export interface ContainerResource extends Resource { outputPath: string; mounts: MountType[]; runtimeConstraints: RuntimeConstraints; + runtimeStatus: RuntimeStatus; schedulingParameters: SchedulingParameters; output: string | null; containerImage: string; diff --git a/src/models/runtime-status.ts b/src/models/runtime-status.ts new file mode 100644 index 00000000..c659930d --- /dev/null +++ b/src/models/runtime-status.ts @@ -0,0 +1,11 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +export interface RuntimeStatus { + error?: string; + warning?: string; + activity?: string; + errorDetail?: string; + warningDetail?: string; +} diff --git a/src/store/process-panel/process-panel-actions.ts b/src/store/process-panel/process-panel-actions.ts index 962f5dfc..e77c300d 100644 --- a/src/store/process-panel/process-panel-actions.ts +++ b/src/store/process-panel/process-panel-actions.ts @@ -56,6 +56,8 @@ export const initProcessPanelFilters = processPanelActions.SET_PROCESS_PANEL_FIL ProcessStatus.COMPLETED, ProcessStatus.FAILED, ProcessStatus.RUNNING, - ProcessStatus.LOCKED, + ProcessStatus.ONHOLD, + ProcessStatus.FAILING, + ProcessStatus.WARNING, ProcessStatus.CANCELLED ]); diff --git a/src/store/processes/process.ts b/src/store/processes/process.ts index 60505be0..b72a0c2b 100644 --- a/src/store/processes/process.ts +++ b/src/store/processes/process.ts @@ -19,10 +19,12 @@ export enum ProcessStatus { CANCELLED = 'Cancelled', COMPLETED = 'Completed', DRAFT = 'Draft', + FAILING = 'Failing', FAILED = 'Failed', - LOCKED = 'Locked', + ONHOLD = 'On hold', QUEUED = 'Queued', RUNNING = 'Running', + WARNING = 'Warning', UNKNOWN = 'Unknown', } @@ -71,44 +73,59 @@ export const getProcessRuntime = ({ container }: Process) => { } }; -export const getProcessStatusColor = (status: string, { customs, palette }: ArvadosTheme) => { +export const getProcessStatusColor = (status: string, { customs }: ArvadosTheme) => { switch (status) { case ProcessStatus.RUNNING: return customs.colors.blue500; case ProcessStatus.COMPLETED: return customs.colors.green700; + case ProcessStatus.WARNING: + return customs.colors.yellow700; + case ProcessStatus.FAILING: case ProcessStatus.CANCELLED: case ProcessStatus.FAILED: return customs.colors.red900; default: - return palette.grey["500"]; + return customs.colors.grey500; } }; export const getProcessStatus = ({ containerRequest, container }: Process): ProcessStatus => { switch (true) { + case containerRequest.state === ContainerRequestState.FINAL && + container?.state !== ContainerState.COMPLETE: + // Request was finalized before its container started (or the + // container was cancelled) + return ProcessStatus.CANCELLED; + case containerRequest.state === ContainerRequestState.UNCOMMITTED: return ProcessStatus.DRAFT; - case container && container.state === ContainerState.COMPLETE && container.exitCode === 0: - return ProcessStatus.COMPLETED; + case container?.state === ContainerState.COMPLETE: + if (container?.exitCode === 0) { + return ProcessStatus.COMPLETED; + } + return ProcessStatus.FAILED; - case containerRequest.priority === 0: - case container && container.state === ContainerState.CANCELLED: + case container?.state === ContainerState.CANCELLED: return ProcessStatus.CANCELLED; - case container && container.state === ContainerState.QUEUED: + case container?.state === ContainerState.QUEUED || + container?.state === ContainerState.LOCKED: + if (containerRequest.priority === 0) { + return ProcessStatus.ONHOLD; + } return ProcessStatus.QUEUED; - case container && container.state === ContainerState.LOCKED: - return ProcessStatus.LOCKED; - - case container && container.state === ContainerState.RUNNING: + case container?.state === ContainerState.RUNNING: + if (!!container?.runtimeStatus.error) { + return ProcessStatus.FAILING; + } + if (!!container?.runtimeStatus.warning) { + return ProcessStatus.WARNING; + } return ProcessStatus.RUNNING; - case container && container.state === ContainerState.COMPLETE && container.exitCode !== 0: - return ProcessStatus.FAILED; - default: return ProcessStatus.UNKNOWN; } diff --git a/src/store/project-panel/project-panel-middleware-service.ts b/src/store/project-panel/project-panel-middleware-service.ts index be569b49..ccfa4fff 100644 --- a/src/store/project-panel/project-panel-middleware-service.ts +++ b/src/store/project-panel/project-panel-middleware-service.ts @@ -17,7 +17,11 @@ import { OrderBuilder, OrderDirection } from "services/api/order-builder"; import { FilterBuilder, joinFilters } from "services/api/filter-builder"; import { GroupContentsResource, GroupContentsResourcePrefix } from "services/groups-service/groups-service"; import { updateFavorites } from "store/favorites/favorites-actions"; -import { IS_PROJECT_PANEL_TRASHED, projectPanelActions, getProjectPanelCurrentUuid } from 'store/project-panel/project-panel-action'; +import { + IS_PROJECT_PANEL_TRASHED, + projectPanelActions, + getProjectPanelCurrentUuid +} from 'store/project-panel/project-panel-action'; import { Dispatch, MiddlewareAPI } from "redux"; import { ProjectResource } from "models/project"; import { updateResources } from "store/resources/resources-actions"; @@ -29,7 +33,10 @@ import { ListResults } from 'services/common-service/common-service'; import { loadContainers } from 'store/processes/processes-actions'; import { ResourceKind } from 'models/resource'; import { getSortColumn } from "store/data-explorer/data-explorer-reducer"; -import { serializeResourceTypeFilters, ProcessStatusFilter } from 'store/resource-type-filters/resource-type-filters'; +import { + serializeResourceTypeFilters, + buildProcessStatusFilters +} from 'store/resource-type-filters/resource-type-filters'; import { updatePublicFavorites } from 'store/public-favorites/public-favorites-actions'; export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService { @@ -116,27 +123,10 @@ export const getFilters = (dataExplorer: DataExplorer) => { .getFilters(); // Filter by container status - const fb = new FilterBuilder(); - switch (activeStatusFilter) { - case ProcessStatusFilter.COMPLETED: { - fb.addEqual('container.state', 'Complete', GroupContentsResourcePrefix.PROCESS); - fb.addEqual('container.exit_code', '0', GroupContentsResourcePrefix.PROCESS); - break; - } - case ProcessStatusFilter.FAILED: { - fb.addEqual('container.state', 'Complete', GroupContentsResourcePrefix.PROCESS); - fb.addDistinct('container.exit_code', '0', GroupContentsResourcePrefix.PROCESS); - break; - } - case ProcessStatusFilter.CANCELLED: - case ProcessStatusFilter.LOCKED: - case ProcessStatusFilter.QUEUED: - case ProcessStatusFilter.RUNNING: { - fb.addEqual('container.state', activeStatusFilter, GroupContentsResourcePrefix.PROCESS); - break; - } - } - const statusFilters = fb.getFilters(); + const statusFilters = buildProcessStatusFilters( + new FilterBuilder(), + activeStatusFilter || '', + GroupContentsResourcePrefix.PROCESS).getFilters(); return joinFilters( statusFilters, diff --git a/src/store/resource-type-filters/resource-type-filters.test.ts b/src/store/resource-type-filters/resource-type-filters.test.ts index 71b00b2e..698515bd 100644 --- a/src/store/resource-type-filters/resource-type-filters.test.ts +++ b/src/store/resource-type-filters/resource-type-filters.test.ts @@ -2,10 +2,29 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { getInitialResourceTypeFilters, serializeResourceTypeFilters, ObjectTypeFilter, CollectionTypeFilter, ProcessTypeFilter, GroupTypeFilter } from './resource-type-filters'; +import { getInitialResourceTypeFilters, serializeResourceTypeFilters, ObjectTypeFilter, CollectionTypeFilter, ProcessTypeFilter, GroupTypeFilter, buildProcessStatusFilters, ProcessStatusFilter } from './resource-type-filters'; import { ResourceKind } from 'models/resource'; import { deselectNode } from 'models/tree'; import { pipe } from 'lodash/fp'; +import { FilterBuilder } from 'services/api/filter-builder'; + +describe("buildProcessStatusFilters", () => { + [ + [ProcessStatusFilter.ALL, ""], + [ProcessStatusFilter.ONHOLD, `["state","!=","Final"],["priority","=","0"],["container.state","in",["Queued","Locked"]]`], + [ProcessStatusFilter.COMPLETED, `["container.state","=","Complete"],["container.exit_code","=","0"]`], + [ProcessStatusFilter.FAILED, `["container.state","=","Complete"],["container.exit_code","!=","0"]`], + [ProcessStatusFilter.QUEUED, `["container.state","=","Queued"],["priority","!=","0"]`], + [ProcessStatusFilter.CANCELLED, `["container.state","=","Cancelled"]`], + [ProcessStatusFilter.RUNNING, `["container.state","=","Running"]`], + ].forEach(([status, expected]) => { + it(`can filter "${status}" processes`, () => { + const filters = buildProcessStatusFilters(new FilterBuilder(), status); + expect(filters.getFilters()) + .toEqual(expected); + }) + }); +}); describe("serializeResourceTypeFilters", () => { it("should serialize all filters", () => { diff --git a/src/store/resource-type-filters/resource-type-filters.ts b/src/store/resource-type-filters/resource-type-filters.ts index e42a16d8..a39807d5 100644 --- a/src/store/resource-type-filters/resource-type-filters.ts +++ b/src/store/resource-type-filters/resource-type-filters.ts @@ -11,6 +11,7 @@ import { getSelectedNodes } from 'models/tree'; import { CollectionType } from 'models/collection'; import { GroupContentsResourcePrefix } from 'services/groups-service/groups-service'; import { ContainerState } from 'models/container'; +import { ContainerRequestState } from 'models/container-request'; export enum ProcessStatusFilter { ALL = 'All', @@ -18,7 +19,7 @@ export enum ProcessStatusFilter { FAILED = 'Failed', COMPLETED = 'Completed', CANCELLED = 'Cancelled', - LOCKED = 'Locked', + ONHOLD = 'On hold', QUEUED = 'Queued' } @@ -95,12 +96,12 @@ export const getInitialProcessStatusFilters = pipe( (): DataTableFilters => createTree(), pipe( initFilter(ProcessStatusFilter.ALL, '', true), + initFilter(ProcessStatusFilter.ONHOLD, '', false), + initFilter(ProcessStatusFilter.QUEUED, '', false), initFilter(ProcessStatusFilter.RUNNING, '', false), - initFilter(ProcessStatusFilter.FAILED, '', false), initFilter(ProcessStatusFilter.COMPLETED, '', false), initFilter(ProcessStatusFilter.CANCELLED, '', false), - initFilter(ProcessStatusFilter.QUEUED, '', false), - initFilter(ProcessStatusFilter.LOCKED, '', false), + initFilter(ProcessStatusFilter.FAILED, '', false), ), ); @@ -272,27 +273,32 @@ export const serializeSimpleObjectTypeFilters = (filters: Tree { +export const buildProcessStatusFilters = ( fb: FilterBuilder, activeStatusFilter: string, resourcePrefix?: string ): FilterBuilder => { switch (activeStatusFilter) { + case ProcessStatusFilter.ONHOLD: { + fb.addDistinct('state', ContainerRequestState.FINAL, resourcePrefix); + fb.addEqual('priority', '0', resourcePrefix); + fb.addIn('container.state', [ContainerState.QUEUED, ContainerState.LOCKED], resourcePrefix); + break; + } case ProcessStatusFilter.COMPLETED: { - fb.addEqual('container.state', ContainerState.COMPLETE); - fb.addEqual('container.exit_code', '0'); + fb.addEqual('container.state', ContainerState.COMPLETE, resourcePrefix); + fb.addEqual('container.exit_code', '0', resourcePrefix); break; } case ProcessStatusFilter.FAILED: { - fb.addEqual('container.state', ContainerState.COMPLETE); - fb.addDistinct('container.exit_code', '0'); + fb.addEqual('container.state', ContainerState.COMPLETE, resourcePrefix); + fb.addDistinct('container.exit_code', '0', resourcePrefix); break; } case ProcessStatusFilter.QUEUED: { - fb.addEqual('container.state', ContainerState.QUEUED); - fb.addDistinct('container.priority', '0'); + fb.addEqual('container.state', ContainerState.QUEUED, resourcePrefix); + fb.addDistinct('priority', '0', resourcePrefix); break; } case ProcessStatusFilter.CANCELLED: - case ProcessStatusFilter.LOCKED: case ProcessStatusFilter.RUNNING: { - fb.addEqual('container.state', activeStatusFilter); + fb.addEqual('container.state', activeStatusFilter, resourcePrefix); break; } } diff --git a/src/views-components/data-explorer/renderers.test.tsx b/src/views-components/data-explorer/renderers.test.tsx index f0efdf74..229d9909 100644 --- a/src/views-components/data-explorer/renderers.test.tsx +++ b/src/views-components/data-explorer/renderers.test.tsx @@ -4,11 +4,14 @@ import React from 'react'; import { mount, configure } from 'enzyme'; -import { ResourceFileSize } from './renderers'; +import { ProcessStatus, ResourceFileSize } from './renderers'; import Adapter from "enzyme-adapter-react-16"; import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store' import { ResourceKind } from '../../models/resource'; +import { ContainerRequestState as CR } from '../../models/container-request'; +import { ContainerState as C } from '../../models/container'; +import { ProcessStatus as PS } from '../../store/processes/process'; const middlewares = []; const mockStore = configureMockStore(middlewares); @@ -18,6 +21,75 @@ configure({ adapter: new Adapter() }); describe('renderers', () => { let props = null; + describe('ProcessStatus', () => { + props = { + uuid: 'zzzzz-xvhdp-zzzzzzzzzzzzzzz', + theme: { + customs: { + colors: { + // Color values are arbitrary, but they should be + // representative of the colors used in the UI. + blue500: 'rgb(0, 0, 255)', + green700: 'rgb(0, 255, 0)', + yellow700: 'rgb(255, 255, 0)', + red900: 'rgb(255, 0, 0)', + grey500: 'rgb(128, 128, 128)', + } + }, + spacing: { + unit: 8, + }, + palette: { + common: { + white: 'rgb(255, 255, 255)', + }, + }, + }, + }; + + [ + // CR Status ; Priority ; C Status ; Exit Code ; C RuntimeStatus ; Expected label ; Expected Color + [CR.COMMITTED, 1, C.RUNNING, null, {}, PS.RUNNING, props.theme.customs.colors.blue500], + [CR.COMMITTED, 1, C.RUNNING, null, {error: 'whoops'}, PS.FAILING, props.theme.customs.colors.red900], + [CR.COMMITTED, 1, C.RUNNING, null, {warning: 'watch out!'}, PS.WARNING, props.theme.customs.colors.yellow700], + [CR.FINAL, 1, C.CANCELLED, null, {}, PS.CANCELLED, props.theme.customs.colors.red900], + [CR.FINAL, 1, C.COMPLETE, 137, {}, PS.FAILED, props.theme.customs.colors.red900], + [CR.FINAL, 1, C.COMPLETE, 0, {}, PS.COMPLETED, props.theme.customs.colors.green700], + [CR.COMMITTED, 0, C.LOCKED, null, {}, PS.ONHOLD, props.theme.customs.colors.grey500], + [CR.COMMITTED, 0, C.QUEUED, null, {}, PS.ONHOLD, props.theme.customs.colors.grey500], + [CR.COMMITTED, 1, C.LOCKED, null, {}, PS.QUEUED, props.theme.customs.colors.grey500], + [CR.COMMITTED, 1, C.QUEUED, null, {}, PS.QUEUED, props.theme.customs.colors.grey500], + ].forEach(([crState, crPrio, cState, exitCode, rs, eLabel, eColor]) => { + it(`should render the state label '${eLabel}' and color '${eColor}' for CR state=${crState}, priority=${crPrio}, C state=${cState}, exitCode=${exitCode} and RuntimeStatus=${JSON.stringify(rs)}`, () => { + const containerUuid = 'zzzzz-dz642-zzzzzzzzzzzzzzz'; + const store = mockStore({ resources: { + [props.uuid]: { + kind: ResourceKind.CONTAINER_REQUEST, + state: crState, + containerUuid: containerUuid, + priority: crPrio, + }, + [containerUuid]: { + kind: ResourceKind.CONTAINER, + state: cState, + runtimeStatus: rs, + exitCode: exitCode, + }, + }}); + + const wrapper = mount( + + ); + + expect(wrapper.text()).toEqual(eLabel); + expect(getComputedStyle(wrapper.getDOMNode()) + .getPropertyValue('color')).toEqual(props.theme.palette.common.white); + expect(getComputedStyle(wrapper.getDOMNode()) + .getPropertyValue('background-color')).toEqual(eColor); + }); + }) + }); + describe('ResourceFileSize', () => { beforeEach(() => { props = { diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx index e854da0e..cd9f972e 100644 --- a/src/views-components/data-explorer/renderers.tsx +++ b/src/views-components/data-explorer/renderers.tsx @@ -3,7 +3,15 @@ // SPDX-License-Identifier: AGPL-3.0 import React from 'react'; -import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core'; +import { + Grid, + Typography, + withStyles, + Tooltip, + IconButton, + Checkbox, + Chip +} from '@material-ui/core'; import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star'; import { Resource, ResourceKind, TrashableResource } from 'models/resource'; import { @@ -825,14 +833,21 @@ export const ProcessStatus = compose( return { process: getProcess(props.uuid)(state.resources) }; }), withStyles({}, { withTheme: true })) - ((props: { process?: Process, theme: ArvadosTheme }) => { - const status = props.process ? getProcessStatus(props.process) : "-"; - return - {status} - ; - }); + ((props: { process?: Process, theme: ArvadosTheme }) => + props.process + ? + : - + ); export const ProcessStartDate = connect( (state: RootState, props: { uuid: string }) => { diff --git a/src/views-components/process-runtime-status/process-runtime-status.tsx b/src/views-components/process-runtime-status/process-runtime-status.tsx new file mode 100644 index 00000000..26e0459d --- /dev/null +++ b/src/views-components/process-runtime-status/process-runtime-status.tsx @@ -0,0 +1,86 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import React from 'react'; +import { + ExpansionPanel, + ExpansionPanelDetails, + ExpansionPanelSummary, + StyleRulesCallback, + Typography, + withStyles, + WithStyles +} from "@material-ui/core"; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { RuntimeStatus } from "models/runtime-status"; +import { ArvadosTheme } from 'common/custom-theme'; +import classNames from 'classnames'; + +type CssRules = 'heading' | 'summary' | 'details' | 'error' | 'errorColor' | 'warning' | 'warningColor'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + heading: { + fontSize: '1rem', + }, + summary: { + paddingLeft: theme.spacing.unit * 1, + paddingRight: theme.spacing.unit * 1, + }, + details: { + paddingLeft: theme.spacing.unit * 1, + paddingRight: theme.spacing.unit * 1, + }, + errorColor: { + color: theme.customs.colors.red900, + }, + error: { + backgroundColor: theme.customs.colors.red100, + + }, + warning: { + backgroundColor: theme.customs.colors.yellow100, + }, + warningColor: { + color: theme.customs.colors.yellow900, + }, +}); +export interface ProcessRuntimeStatusDataProps { + runtimeStatus: RuntimeStatus | undefined; +} + +type ProcessRuntimeStatusProps = ProcessRuntimeStatusDataProps & WithStyles; + +export const ProcessRuntimeStatus = withStyles(styles)( + ({ runtimeStatus, classes }: ProcessRuntimeStatusProps) => { + return <> + { runtimeStatus?.error && +
+ }> + + {`Error: ${runtimeStatus.error }`} + + + + + {runtimeStatus?.errorDetail || 'No additional error details available'} + + +
+ } + { runtimeStatus?.warning && +
+ }> + + {`Warning: ${runtimeStatus.warning }`} + + + + + {runtimeStatus?.warningDetail || 'No additional warning details available'} + + +
+ } + +}); \ No newline at end of file diff --git a/src/views/process-panel/process-information-card.tsx b/src/views/process-panel/process-information-card.tsx index fc34a31c..4fcbf257 100644 --- a/src/views/process-panel/process-information-card.tsx +++ b/src/views/process-panel/process-information-card.tsx @@ -4,20 +4,29 @@ import React from 'react'; import { - StyleRulesCallback, WithStyles, withStyles, Card, - CardHeader, IconButton, CardContent, Grid, Chip, Typography, Tooltip + StyleRulesCallback, + WithStyles, + withStyles, + Card, + CardHeader, + IconButton, + CardContent, + Grid, + Typography, + Tooltip } from '@material-ui/core'; import { ArvadosTheme } from 'common/custom-theme'; import { CloseIcon, MoreOptionsIcon, ProcessIcon } from 'components/icon/icon'; import { DetailsAttribute } from 'components/details-attribute/details-attribute'; import { Process } from 'store/processes/process'; -import { getProcessStatus, getProcessStatusColor } from 'store/processes/process'; import { formatDate } from 'common/formatters'; import classNames from 'classnames'; import { ContainerState } from 'models/container'; import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view'; +import { ProcessRuntimeStatus } from 'views-components/process-runtime-status/process-runtime-status'; +import { ProcessStatus } from 'views-components/data-explorer/renderers'; -type CssRules = 'card' | 'iconHeader' | 'label' | 'value' | 'chip' | 'link' | 'content' | 'title' | 'avatar' | 'cancelButton' | 'header'; +type CssRules = 'card' | 'iconHeader' | 'label' | 'value' | 'link' | 'content' | 'title' | 'avatar' | 'cancelButton' | 'header'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ card: { @@ -37,7 +46,7 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ }, label: { display: 'flex', - justifyContent: 'flex-end', + justifyContent: 'flex-start', fontSize: '0.875rem', marginRight: theme.spacing.unit * 3, paddingRight: theme.spacing.unit @@ -53,16 +62,12 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ cursor: 'pointer' } }, - chip: { - height: theme.spacing.unit * 3, - width: theme.spacing.unit * 12, - color: theme.palette.common.white, - fontSize: '0.875rem', - borderRadius: theme.spacing.unit * 0.625, - }, content: { + paddingTop: '0px', + paddingLeft: theme.spacing.unit * 1, + paddingRight: theme.spacing.unit * 1, '&:last-child': { - paddingBottom: theme.spacing.unit * 2, + paddingBottom: theme.spacing.unit * 1, } }, title: { @@ -107,9 +112,7 @@ export const ProcessInformationCard = withStyles(styles, { withTheme: true })(
{process.container && process.container.state === ContainerState.RUNNING && cancelProcess(process.containerRequest.uuid)}>Cancel} - + }
} - title={ - - - {process.containerRequest.name} - - + title={ !!process.containerRequest.name && + + {process.containerRequest.name} + } subheader={ - - - {getDescription(process)} - - } /> + + {process.containerRequest.description} + + } + /> + + + {process.containerRequest.properties.workflowUuid && openWorkflow(process.containerRequest.properties.workflowUuid)}> @@ -164,6 +168,3 @@ export const ProcessInformationCard = withStyles(styles, { withTheme: true })( ; } ); - -const getDescription = (process: Process) => - process.containerRequest.description || '(no-description)';