From: Stephen Smith Date: Tue, 12 Apr 2022 21:16:15 +0000 (-0400) Subject: 16068: Merge branch 'main' of git.arvados.org:arvados-workbench2 into 16068 X-Git-Tag: 2.4.1~1^2~8^2~5 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/082c1782d96a4b1c897df64d9b325d102a5c1144?hp=08ac60f877d6495a748747a3a0d30ca9f0e289d5 16068: Merge branch 'main' of git.arvados.org:arvados-workbench2 into 16068 Arvados-DCO-1.1-Signed-off-by: Stephen Smith --- diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js index 39a2af42..568121f1 100644 --- a/cypress/integration/collection.spec.js +++ b/cypress/integration/collection.spec.js @@ -334,8 +334,8 @@ describe('Collection panel tests', function () { 'bar' // make sure we can go back to the original name as a last step ]; eachPair(names, (from, to) => { - cy.get('[data-cy=collection-files-panel]') - .contains(`${from}`).rightclick({force: true}); + cy.waitForDom().get('[data-cy=collection-files-panel]') + .contains(`${from}`).rightclick(); cy.get('[data-cy=context-menu]') .contains('Rename') .click(); diff --git a/cypress/integration/process.spec.js b/cypress/integration/process.spec.js index 3234f7c4..48b936cf 100644 --- a/cypress/integration/process.spec.js +++ b/cypress/integration/process.spec.js @@ -95,7 +95,7 @@ describe('Process tests', function() { .then(function(containerRequest) { cy.loginAs(activeUser); cy.goToPath(`/processes/${containerRequest.uuid}`); - cy.get('[data-cy=process-info]').should('contain', crName); + cy.get('[data-cy=process-details]').should('contain', crName); cy.get('[data-cy=process-logs]') .should('contain', 'No logs yet') .and('not.contain', 'hello world'); @@ -245,4 +245,4 @@ describe('Process tests', function() { .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 a28308e3..c2d78b54 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -401,3 +401,73 @@ function b64toBlob(b64Data, contentType = '', sliceSize = 512) { const blob = new Blob(byteArrays, { type: contentType }); return blob } + +// From https://github.com/cypress-io/cypress/issues/7306#issuecomment-1076451070= +// This command requires the async package (https://www.npmjs.com/package/async) +Cypress.Commands.add('waitForDom', () => { + cy.window().then(win => { + let timeElapsed = 0; + + cy.log("Waiting for DOM mutations to complete"); + + return new Cypress.Promise((resolve) => { + // set the required variables + let async = require("async"); + let observerConfig = { attributes: true, childList: true, subtree: true }; + let items = Array.apply(null, { length: 50 }).map(Number.call, Number); + win.mutationCount = 0; + win.previousMutationCount = null; + + // create an observer instance + let observer = new win.MutationObserver((mutations) => { + mutations.forEach((mutation) => { + // Only record "attributes" type mutations that are not a "class" mutation. + // If the mutation is not an "attributes" type, then we always record it. + if (mutation.type === 'attributes' && mutation.attributeName !== 'class') { + win.mutationCount += 1; + } else if (mutation.type !== 'attributes') { + win.mutationCount += 1; + } + }); + + // initialize the previousMutationCount + if (win.previousMutationCount == null) win.previousMutationCount = 0; + }); + + // watch the document body for the specified mutations + observer.observe(win.document.body, observerConfig); + + // check the DOM for mutations up to 50 times for a maximum time of 5 seconds + async.eachSeries(items, function iteratee(item, callback) { + // keep track of the elapsed time so we can log it at the end of the command + timeElapsed = timeElapsed + 100; + + // make each iteration of the loop 100ms apart + setTimeout(() => { + if (win.mutationCount === win.previousMutationCount) { + // pass an argument to the async callback to exit the loop + return callback('Resolved - DOM changes complete.'); + } else if (win.previousMutationCount != null) { + // only set the previous count if the observer has checked the DOM at least once + win.previousMutationCount = win.mutationCount; + return callback(); + } else if (win.mutationCount === 0 && win.previousMutationCount == null && item === 4) { + // this is an early exit in case nothing is changing in the DOM. That way we only + // wait 500ms instead of the full 5 seconds when no DOM changes are occurring. + return callback('Resolved - Exiting early since no DOM changes were detected.'); + } else { + // proceed to the next iteration + return callback(); + } + }, 100); + }, function done() { + // Log the total wait time so users can see it + cy.log(`DOM mutations ${timeElapsed >= 5000 ? "did not complete" : "completed"} in ${timeElapsed} ms`); + + // disconnect the observer and resolve the promise + observer.disconnect(); + resolve(); + }); + }); + }); + }); diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts new file mode 100644 index 00000000..d74d5b3d --- /dev/null +++ b/cypress/support/index.d.ts @@ -0,0 +1,24 @@ +/** + * This command tries to ensure that the elements in the DOM are actually visible + * and done (re)rendering. This is due to how React re-renders components. + * + * IMPORTANT NOTES: + * => You should only use this command in instances where a test is failing due + * to detached elements. Cypress will probably give you a warning along the lines + * of, "Element has an effective width/height of 0". This warning is not very useful + * in pointing out it is due to the element being detached from the DOM AFTER the + * cy.get command had already retrieved it. This command can save you from that + * by explicitly waiting for the DOM to stop changing. + * => This command can take anywhere from 100ms to 5 seconds to complete + * => This command will exit early (500ms) when no changes are occurring in the DOM. + * We wait a minimum of 500ms because sometimes it can take up to around that time + * for mutations to start occurring. + * + * GitHub Issues: + * * https://github.com/cypress-io/cypress/issues/695 (Closed - no activity) + * * https://github.com/cypress-io/cypress/issues/7306 (Open - re-get detached elements) + * + * @example Wait for the DOM to stop changing before retrieving an element + * cy.waitForDom().get('#an-elements-id') + */ + waitForDom(): Chainable diff --git a/src/views-components/details-panel/process-details.tsx b/src/views-components/details-panel/process-details.tsx index d9c991f5..bb0e8a40 100644 --- a/src/views-components/details-panel/process-details.tsx +++ b/src/views-components/details-panel/process-details.tsx @@ -15,6 +15,6 @@ export class ProcessDetails extends DetailsData { } getDetails() { - return ; + return ; } } diff --git a/src/views/process-panel/process-details-attributes.tsx b/src/views/process-panel/process-details-attributes.tsx index 4f26a71f..6ddeab7a 100644 --- a/src/views/process-panel/process-details-attributes.tsx +++ b/src/views/process-panel/process-details-attributes.tsx @@ -3,63 +3,122 @@ // SPDX-License-Identifier: AGPL-3.0 import React from "react"; -import { Grid } from "@material-ui/core"; +import { Grid, StyleRulesCallback, withStyles } from "@material-ui/core"; +import { Dispatch } from 'redux'; import { formatDate } from "common/formatters"; import { resourceLabel } from "common/labels"; import { DetailsAttribute } from "components/details-attribute/details-attribute"; -import { ProcessResource } from "models/process"; import { ResourceKind } from "models/resource"; import { ResourceOwnerWithName } from "views-components/data-explorer/renderers"; +import { getProcess } from "store/processes/process"; +import { RootState } from "store/store"; +import { connect } from "react-redux"; +import { ProcessResource } from "models/process"; +import { ContainerResource } from "models/container"; +import { openProcessInputDialog } from "store/processes/process-input-actions"; +import { navigateToOutput, openWorkflow } from "store/process-panel/process-panel-actions"; +import { ArvadosTheme } from "common/custom-theme"; + +type CssRules = 'link'; -type CssRules = 'label' | 'value'; +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + link: { + fontSize: '0.875rem', + color: theme.palette.primary.main, + '&:hover': { + cursor: 'pointer' + } + }, +}); -export const ProcessDetailsAttributes = (props: { item: ProcessResource, twoCol?: boolean, classes?: Record }) => { - const item = props.item; - const classes = props.classes || { label: '', value: '', button: '' }; - const mdSize = props.twoCol ? 6 : 12; - return - - - - - } /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ; +const mapStateToProps = (state: RootState, props: { request: ProcessResource }) => { + return { + container: getProcess(props.request.uuid)(state.resources)?.container, + }; }; + +interface ProcessDetailsAttributesActionProps { + openProcessInputDialog: (uuid: string) => void; + navigateToOutput: (uuid: string) => void; + openWorkflow: (uuid: string) => void; +} + +const mapDispatchToProps = (dispatch: Dispatch): ProcessDetailsAttributesActionProps => ({ + openProcessInputDialog: (uuid) => dispatch(openProcessInputDialog(uuid)), + navigateToOutput: (uuid) => dispatch(navigateToOutput(uuid)), + openWorkflow: (uuid) => dispatch(openWorkflow(uuid)), +}); + +export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })( + connect(mapStateToProps, mapDispatchToProps)( + (props: { request: ProcessResource, container?: ContainerResource, twoCol?: boolean, classes?: Record } & ProcessDetailsAttributesActionProps) => { + const containerRequest = props.request; + const container = props.container; + const classes = props.classes || { label: '', value: '', button: '', link: '' }; + const mdSize = props.twoCol ? 6 : 12; + return + + + + + } /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + props.navigateToOutput(containerRequest.outputUuid!)}> + + + props.openProcessInputDialog(containerRequest.uuid)}> + + + + {containerRequest.properties.workflowUuid && + + props.openWorkflow(containerRequest.properties.workflowUuid)}> + + + } + + + + + + + + + + ; + } + ) +); diff --git a/src/views/process-panel/process-details-card.tsx b/src/views/process-panel/process-details-card.tsx index d3349c3a..59d0b61b 100644 --- a/src/views/process-panel/process-details-card.tsx +++ b/src/views/process-panel/process-details-card.tsx @@ -12,14 +12,17 @@ import { IconButton, CardContent, Tooltip, + Typography, } from '@material-ui/core'; import { ArvadosTheme } from 'common/custom-theme'; -import { CloseIcon } from 'components/icon/icon'; +import { CloseIcon, MoreOptionsIcon, ProcessIcon } from 'components/icon/icon'; import { Process } from 'store/processes/process'; import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view'; import { ProcessDetailsAttributes } from './process-details-attributes'; +import { ProcessStatus } from 'views-components/data-explorer/renderers'; +import { ContainerState } from 'models/container'; -type CssRules = 'card' | 'content' | 'title' | 'header'; +type CssRules = 'card' | 'content' | 'title' | 'header' | 'cancelButton' | 'avatar' | 'iconHeader'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ card: { @@ -29,6 +32,14 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ paddingTop: theme.spacing.unit, paddingBottom: theme.spacing.unit, }, + iconHeader: { + fontSize: '1.875rem', + color: theme.customs.colors.green700, + }, + avatar: { + alignSelf: 'flex-start', + paddingTop: theme.spacing.unit * 0.5 + }, content: { '&:last-child': { paddingBottom: theme.spacing.unit * 2, @@ -38,31 +49,71 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ overflow: 'hidden', paddingTop: theme.spacing.unit * 0.5 }, + cancelButton: { + paddingRight: theme.spacing.unit * 2, + fontSize: '14px', + color: theme.customs.colors.red900, + "&:hover": { + cursor: 'pointer' + } + }, }); export interface ProcessDetailsCardDataProps { process: Process; + cancelProcess: (uuid: string) => void; + onContextMenu: (event: React.MouseEvent) => void; } type ProcessDetailsCardProps = ProcessDetailsCardDataProps & WithStyles & MPVPanelProps; export const ProcessDetailsCard = withStyles(styles)( - ({ classes, process, doHidePanel, panelName }: ProcessDetailsCardProps) => { + ({ cancelProcess, onContextMenu, classes, process, doHidePanel, panelName }: ProcessDetailsCardProps) => { return } + title={ + + + {process.containerRequest.name} + + + } + subheader={ + + + {getDescription(process)} + + } + action={ +
+ {process.container && process.container.state === ContainerState.RUNNING && + cancelProcess(process.containerRequest.uuid)}>Cancel} + + + onContextMenu(event)}> + + + + { doHidePanel && - } /> + } +
+ } /> - +
; } ); +const getDescription = (process: Process) => + process.containerRequest.description || '(no-description)'; diff --git a/src/views/process-panel/process-information-card.tsx b/src/views/process-panel/process-information-card.tsx deleted file mode 100644 index 4fcbf257..00000000 --- a/src/views/process-panel/process-information-card.tsx +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import React from 'react'; -import { - 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 { 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' | 'link' | 'content' | 'title' | 'avatar' | 'cancelButton' | 'header'; - -const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ - card: { - height: '100%' - }, - header: { - paddingTop: theme.spacing.unit, - paddingBottom: theme.spacing.unit, - }, - iconHeader: { - fontSize: '1.875rem', - color: theme.customs.colors.green700, - }, - avatar: { - alignSelf: 'flex-start', - paddingTop: theme.spacing.unit * 0.5 - }, - label: { - display: 'flex', - justifyContent: 'flex-start', - fontSize: '0.875rem', - marginRight: theme.spacing.unit * 3, - paddingRight: theme.spacing.unit - }, - value: { - textTransform: 'none', - fontSize: '0.875rem', - }, - link: { - fontSize: '0.875rem', - color: theme.palette.primary.main, - '&:hover': { - cursor: 'pointer' - } - }, - content: { - paddingTop: '0px', - paddingLeft: theme.spacing.unit * 1, - paddingRight: theme.spacing.unit * 1, - '&:last-child': { - paddingBottom: theme.spacing.unit * 1, - } - }, - title: { - overflow: 'hidden', - paddingTop: theme.spacing.unit * 0.5 - }, - cancelButton: { - paddingRight: theme.spacing.unit * 2, - fontSize: '14px', - color: theme.customs.colors.red900, - "&:hover": { - cursor: 'pointer' - } - } -}); - -export interface ProcessInformationCardDataProps { - process: Process; - onContextMenu: (event: React.MouseEvent) => void; - openProcessInputDialog: (uuid: string) => void; - navigateToOutput: (uuid: string) => void; - openWorkflow: (uuid: string) => void; - cancelProcess: (uuid: string) => void; -} - -type ProcessInformationCardProps = ProcessInformationCardDataProps & WithStyles & MPVPanelProps; - -export const ProcessInformationCard = withStyles(styles, { withTheme: true })( - ({ classes, process, onContextMenu, theme, openProcessInputDialog, navigateToOutput, openWorkflow, cancelProcess, doHidePanel, panelName }: ProcessInformationCardProps) => { - const { container } = process; - const startedAt = container ? formatDate(container.startedAt) : 'N/A'; - const finishedAt = container ? formatDate(container.finishedAt) : 'N/A'; - return - } - action={ -
- {process.container && process.container.state === ContainerState.RUNNING && - cancelProcess(process.containerRequest.uuid)}>Cancel} - - - onContextMenu(event)}> - - - - { doHidePanel && - - - } -
- } - title={ !!process.containerRequest.name && - - {process.containerRequest.name} - - } - subheader={ - - {process.containerRequest.description} - - } - /> - - - - - - - - - {process.containerRequest.properties.workflowUuid && - openWorkflow(process.containerRequest.properties.workflowUuid)}> - - } - - - navigateToOutput(process.containerRequest.outputUuid!)}> - - - openProcessInputDialog(process.containerRequest.uuid)}> - - - - - -
; - } -); diff --git a/src/views/process-panel/process-panel-root.tsx b/src/views/process-panel/process-panel-root.tsx index 156789a2..4f95d0d8 100644 --- a/src/views/process-panel/process-panel-root.tsx +++ b/src/views/process-panel/process-panel-root.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { Grid, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; -import { ProcessInformationCard } from './process-information-card'; import { DefaultView } from 'components/default-view/default-view'; import { ProcessIcon } from 'components/icon/icon'; import { Process } from 'store/processes/process'; @@ -35,9 +34,6 @@ export interface ProcessPanelRootDataProps { export interface ProcessPanelRootActionProps { onContextMenu: (event: React.MouseEvent, process: Process) => void; onToggle: (status: string) => void; - openProcessInputDialog: (uuid: string) => void; - navigateToOutput: (uuid: string) => void; - navigateToWorkflow: (uuid: string) => void; cancelProcess: (uuid: string) => void; onLogFilterChange: (filter: FilterOption) => void; navigateToLog: (uuid: string) => void; @@ -47,8 +43,7 @@ export interface ProcessPanelRootActionProps { export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps & WithStyles; const panelsData: MPVPanelState[] = [ - {name: "Info"}, - {name: "Details", visible: false}, + {name: "Details"}, {name: "Logs", visible: true}, {name: "Subprocesses"}, ]; @@ -57,19 +52,13 @@ export const ProcessPanelRoot = withStyles(styles)( ({ process, processLogsPanel, ...props }: ProcessPanelRootProps) => process ? - - + props.onContextMenu(event, process)} - openProcessInputDialog={props.openProcessInputDialog} - navigateToOutput={props.navigateToOutput} - openWorkflow={props.navigateToWorkflow} cancelProcess={props.cancelProcess} /> - - - onToggle: status => { dispatch(toggleProcessPanelFilter(status)); }, - openProcessInputDialog: (uuid) => dispatch(openProcessInputDialog(uuid)), - navigateToOutput: (uuid) => dispatch(navigateToOutput(uuid)), - navigateToWorkflow: (uuid) => dispatch(openWorkflow(uuid)), cancelProcess: (uuid) => dispatch(cancelRunningWorkflow(uuid)), onLogFilterChange: (filter) => dispatch(setProcessLogsPanelFilter(filter.value)), navigateToLog: (uuid) => dispatch(navigateToLogCollection(uuid)),