From: Peter Amstutz Date: Thu, 23 Feb 2023 23:03:40 +0000 (-0500) Subject: 19482: Fix context menu, breadcrumbs X-Git-Tag: 2.6.0~6^2~9 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/0c37a7e69b11fb23a4ffa2902a1343b3cd6769bc 19482: Fix context menu, breadcrumbs Also improve styling of Run button Also fixed #19932 (CLI, python, CURL examples show every example code twice) this was a bug in the code view panel. Arvados-DCO-1.1-Signed-off-by: Peter Amstutz --- diff --git a/src/store/advanced-tab/advanced-tab.tsx b/src/store/advanced-tab/advanced-tab.tsx index ac088f02..82b4dfb0 100644 --- a/src/store/advanced-tab/advanced-tab.tsx +++ b/src/store/advanced-tab/advanced-tab.tsx @@ -20,6 +20,7 @@ import { SshKeyResource } from 'models/ssh-key'; import { VirtualMachinesResource } from 'models/virtual-machines'; import { UserResource } from 'models/user'; import { LinkResource } from 'models/link'; +import { WorkflowResource } from 'models/workflow'; import { KeepServiceResource } from 'models/keep-services'; import { ApiClientAuthorization } from 'models/api-client-authorization'; import React from 'react'; @@ -101,9 +102,14 @@ enum LinkData { PROPERTIES = 'properties' } -type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ApiClientAuthorizationsData | UserData | LinkData; +enum WorkflowData { + WORKFLOW = 'workflow', + CREATED_AT = 'created_at' +} + +type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ApiClientAuthorizationsData | UserData | LinkData | WorkflowData; type AdvanceResourcePrefix = GroupContentsResourcePrefix | ResourcePrefix; -type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | ApiClientAuthorization | UserResource | LinkResource | undefined; +type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | ApiClientAuthorization | UserResource | LinkResource | WorkflowResource | undefined; export const openAdvancedTabDialog = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { @@ -267,6 +273,23 @@ export const openAdvancedTabDialog = (uuid: string) => }); dispatch(initAdvancedTabDialog(advanceDataLink)); break; + case ResourceKind.WORKFLOW: + const wfResources = getState().resources; + const dataWf = getResource(uuid)(wfResources); + const advanceDataWf = advancedTabData({ + uuid, + metadata: '', + user: '', + apiResponseKind: wfApiResponse, + data: dataWf, + resourceKind: WorkflowData.WORKFLOW, + resourcePrefix: GroupContentsResourcePrefix.WORKFLOW, + resourceKindProperty: WorkflowData.CREATED_AT, + property: dataWf!.createdAt + }); + dispatch(initAdvancedTabDialog(advanceDataWf)); + break; + default: dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR })); } @@ -600,3 +623,22 @@ const linkApiResponse = (apiResponse: LinkResource): JSX.Element => { return {'{'} {response} {'\n'} {'}'}; }; + + +const wfApiResponse = (apiResponse: WorkflowResource): JSX.Element => { + const { + uuid, name, + ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, description, definition + } = apiResponse; + const response = ` +"uuid": "${uuid}", +"name": "${name}", +"owner_uuid": "${ownerUuid}", +"created_at": "${stringify(createdAt)}", +"modified_at": ${stringify(modifiedAt)}, +"modified_by_client_uuid": ${stringify(modifiedByClientUuid)}, +"modified_by_user_uuid": ${stringify(modifiedByUserUuid)} +"description": ${stringify(description)}`; + + return {'{'} {response} {'\n'} {'}'}; +}; diff --git a/src/store/breadcrumbs/breadcrumbs-actions.ts b/src/store/breadcrumbs/breadcrumbs-actions.ts index 74cfde00..7d6f182d 100644 --- a/src/store/breadcrumbs/breadcrumbs-actions.ts +++ b/src/store/breadcrumbs/breadcrumbs-actions.ts @@ -22,20 +22,21 @@ import { ProcessResource } from 'models/process'; import { OrderBuilder } from 'services/api/order-builder'; import { Breadcrumb } from 'components/breadcrumbs/breadcrumbs'; import { ContainerRequestResource, containerRequestFieldsNoMounts } from 'models/container-request'; -import { CollectionIcon, IconType, ProcessIcon, ProjectIcon } from 'components/icon/icon'; +import { CollectionIcon, IconType, ProcessIcon, ProjectIcon, WorkflowIcon } from 'components/icon/icon'; import { CollectionResource } from 'models/collection'; import { getSidePanelIcon } from 'views-components/side-panel-tree/side-panel-tree'; +import { WorkflowResource } from 'models/workflow'; export const BREADCRUMBS = 'breadcrumbs'; -export const setBreadcrumbs = (breadcrumbs: any, currentItem?: CollectionResource | ContainerRequestResource | GroupResource) => { +export const setBreadcrumbs = (breadcrumbs: any, currentItem?: CollectionResource | ContainerRequestResource | GroupResource | WorkflowResource) => { if (currentItem) { breadcrumbs.push(resourceToBreadcrumb(currentItem)); } return propertiesActions.SET_PROPERTY({ key: BREADCRUMBS, value: breadcrumbs }); }; -const resourceToBreadcrumbIcon = (resource: CollectionResource | ContainerRequestResource | GroupResource): IconType | undefined => { +const resourceToBreadcrumbIcon = (resource: CollectionResource | ContainerRequestResource | GroupResource | WorkflowResource): IconType | undefined => { switch (resource.kind) { case ResourceKind.PROJECT: return ProjectIcon; @@ -43,12 +44,14 @@ const resourceToBreadcrumbIcon = (resource: CollectionResource | ContainerReques return ProcessIcon; case ResourceKind.COLLECTION: return CollectionIcon; + case ResourceKind.WORKFLOW: + return WorkflowIcon; default: return undefined; } } -const resourceToBreadcrumb = (resource: CollectionResource | ContainerRequestResource | GroupResource): Breadcrumb => ({ +const resourceToBreadcrumb = (resource: CollectionResource | ContainerRequestResource | GroupResource | WorkflowResource): Breadcrumb => ({ label: resource.name, uuid: resource.uuid, icon: resourceToBreadcrumbIcon(resource), @@ -90,6 +93,9 @@ export const setSidePanelBreadcrumbs = (uuid: string) => breadcrumbs.push(resourceToBreadcrumb(parentProcessItem)); } dispatch(setBreadcrumbs(breadcrumbs, processItem)); + } else if (uuidKind === ResourceKind.WORKFLOW) { + const workflowItem = await services.workflowService.get(currentUuid); + dispatch(setBreadcrumbs(breadcrumbs, workflowItem)); } dispatch(setBreadcrumbs(breadcrumbs)); }; @@ -172,10 +178,10 @@ const getCollectionParent = (collection: CollectionResource) => }); const [parentOutput, parentLog] = await Promise.all([parentOutputPromise, parentLogPromise]); return parentOutput.items.length > 0 ? - parentOutput.items[0] : - parentLog.items.length > 0 ? - parentLog.items[0] : - undefined; + parentOutput.items[0] : + parentLog.items.length > 0 ? + parentLog.items[0] : + undefined; } @@ -200,6 +206,15 @@ export const setProcessBreadcrumbs = (processUuid: string) => } }; +export const setWorkflowBreadcrumbs = (workflowUuid: string) => + (dispatch: Dispatch, getState: () => RootState) => { + const { resources } = getState(); + const workflow = getResource(workflowUuid)(resources); + if (workflow) { + dispatch(setProjectBreadcrumbs(workflow.ownerUuid)); + } + }; + export const setGroupsBreadcrumbs = () => setBreadcrumbs([{ label: SidePanelTreeCategory.GROUPS, @@ -234,7 +249,7 @@ export const setUserProfileBreadcrumbs = (userUuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { try { const user = getResource(userUuid)(getState().resources) - || await services.userService.get(userUuid, false); + || await services.userService.get(userUuid, false); const breadcrumbs: Breadcrumb[] = [ { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL }, { label: user ? user.username : userUuid, uuid: userUuid }, diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts index 7cb0987b..cd151113 100644 --- a/src/store/workbench/workbench-actions.ts +++ b/src/store/workbench/workbench-actions.ts @@ -32,6 +32,7 @@ import { setGroupDetailsBreadcrumbs, setGroupsBreadcrumbs, setProcessBreadcrumbs, + setWorkflowBreadcrumbs, setSharedWithMeBreadcrumbs, setSidePanelBreadcrumbs, setTrashBreadcrumbs, @@ -591,7 +592,13 @@ export const loadProcess = (uuid: string) => export const loadRegisteredWorkflow = (uuid: string) => handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const workflow = await services.workflowService.get(uuid); - dispatch(updateResources([workflow])); + if (workflow) { + dispatch(updateResources([workflow])); + await dispatch( + activateSidePanelTreeItem(workflow.ownerUuid) + ); + dispatch(setWorkflowBreadcrumbs(uuid)); + } }); export const updateProcess = diff --git a/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx b/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx index bc84ed2c..3505faed 100644 --- a/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx +++ b/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx @@ -120,6 +120,6 @@ const dialogContentExample = (example: JSX.Element | string, classes: any) => { className={classes.codeSnippet} lines={stringData ? [stringData] : []} > - {example as JSX.Element || null} + {React.isValidElement(example) ? (example as JSX.Element) : undefined} ; } diff --git a/src/views-components/context-menu/action-sets/workflow-action-set.ts b/src/views-components/context-menu/action-sets/workflow-action-set.ts index 2aa78904..cf28bcd3 100644 --- a/src/views-components/context-menu/action-sets/workflow-action-set.ts +++ b/src/views-components/context-menu/action-sets/workflow-action-set.ts @@ -4,10 +4,55 @@ import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set"; import { openRunProcess } from "store/workflow-panel/workflow-panel-actions"; +import { + RenameIcon, + ShareIcon, + MoveToIcon, + CopyIcon, + DetailsIcon, + AdvancedIcon, + OpenIcon, + Link, + RestoreVersionIcon, + FolderSharedIcon, + StartIcon +} from "components/icon/icon"; +import { copyToClipboardAction, openInNewTabAction } from "store/open-in-new-tab/open-in-new-tab.actions"; +import { toggleDetailsPanel } from 'store/details-panel/details-panel-action'; +import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab"; export const workflowActionSet: ContextMenuActionSet = [[ { - name: "Run", + icon: OpenIcon, + name: "Open in new tab", + execute: (dispatch, resource) => { + dispatch(openInNewTabAction(resource)); + } + }, + { + icon: Link, + name: "Copy to clipboard", + execute: (dispatch, resource) => { + dispatch(copyToClipboardAction(resource)); + } + }, + { + icon: DetailsIcon, + name: "View details", + execute: dispatch => { + dispatch(toggleDetailsPanel()); + } + }, + { + icon: AdvancedIcon, + name: "API Details", + execute: (dispatch, resource) => { + dispatch(openAdvancedTabDialog(resource.uuid)); + } + }, + { + icon: StartIcon, + name: "Run Workflow", execute: (dispatch, resource) => { dispatch(openRunProcess(resource.uuid, resource.ownerUuid, resource.name)); } diff --git a/src/views-components/details-panel/workflow-details.tsx b/src/views-components/details-panel/workflow-details.tsx index 98978dd2..cb5e6a66 100644 --- a/src/views-components/details-panel/workflow-details.tsx +++ b/src/views-components/details-panel/workflow-details.tsx @@ -3,8 +3,11 @@ // SPDX-License-Identifier: AGPL-3.0 import React from 'react'; -import { WorkflowIcon } from 'components/icon/icon'; -import { WorkflowResource } from 'models/workflow'; +import { WorkflowIcon, StartIcon } from 'components/icon/icon'; +import { + WorkflowResource, parseWorkflowDefinition, getWorkflowInputs, + getWorkflowOutputs, getWorkflow +} from 'models/workflow'; import { DetailsData } from "./details-data"; import { DetailsAttribute } from 'components/details-attribute/details-attribute'; import { ResourceWithName } from 'views-components/data-explorer/renderers'; @@ -15,6 +18,11 @@ import { openRunProcess } from "store/workflow-panel/workflow-panel-actions"; import { Dispatch } from 'redux'; import { connect } from 'react-redux'; import { ArvadosTheme } from 'common/custom-theme'; +import { ProcessIOParameter } from 'views/process-panel/process-io-card'; +import { formatInputData, formatOutputData } from 'store/process-panel/process-panel-actions'; +import { AuthState } from 'store/auth/auth-reducer'; +import { RootState } from 'store/store'; +import { getPropertyChip } from "views-components/resource-properties-form/property-chip"; export interface WorkflowDetailsCardDataProps { workflow?: WorkflowResource; @@ -29,29 +37,96 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ () => wf && dispatch(openRunProcess(wf.uuid, wf.ownerUuid, wf.name)), }); -type CssRules = 'runButton'; +type CssRules = 'runButton' | 'propertyTag'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ runButton: { + backgroundColor: theme.customs.colors.green700, + '&:hover': { + backgroundColor: theme.customs.colors.green800, + }, + marginRight: "5px", boxShadow: 'none', padding: '2px 10px 2px 5px', - fontSize: '0.75rem' + marginLeft: 'auto' + }, + propertyTag: { + marginRight: theme.spacing.unit / 2, + marginBottom: theme.spacing.unit / 2 }, }); -export const WorkflowDetailsAttributes = connect(null, mapDispatchToProps)( +interface AuthStateDataProps { + auth: AuthState; +}; + +export interface RegisteredWorkflowPanelDataProps { + item: WorkflowResource; + workflowCollection: string; + inputParams: ProcessIOParameter[]; + outputParams: ProcessIOParameter[]; + gitprops: { [key: string]: string; }; +}; + +export const getRegisteredWorkflowPanelData = (item: WorkflowResource, auth: AuthState): RegisteredWorkflowPanelDataProps => { + let inputParams: ProcessIOParameter[] = []; + let outputParams: ProcessIOParameter[] = []; + let workflowCollection = ""; + const gitprops: { [key: string]: string; } = {}; + + // parse definition + const wfdef = parseWorkflowDefinition(item); + + const inputs = getWorkflowInputs(wfdef); + if (inputs) { + inputs.forEach(elm => { + if (elm.default !== undefined && elm.default !== null) { + elm.value = elm.default; + } + }); + inputParams = formatInputData(inputs, auth); + } + + const outputs = getWorkflowOutputs(wfdef); + if (outputs) { + outputParams = formatOutputData(outputs, {}, undefined, auth); + } + + const wf = getWorkflow(wfdef); + if (wf) { + const REGEX = /keep:([0-9a-f]{32}\+\d+)\/.*/; + if (wf["steps"]) { + workflowCollection = wf["steps"][0].run.match(REGEX)[1]; + } + } + + for (const elm in wfdef) { + if (elm.startsWith("http://arvados.org/cwl#git")) { + gitprops[elm.substr(23)] = wfdef[elm] + } + } + + return { item, workflowCollection, inputParams, outputParams, gitprops }; +}; + +const mapStateToProps = (state: RootState): AuthStateDataProps => { + return { auth: state.auth }; +}; + +export const WorkflowDetailsAttributes = connect(mapStateToProps, mapDispatchToProps)( withStyles(styles)( - ({ workflow, onClick, classes }: WorkflowDetailsCardDataProps & WorkflowDetailsCardActionProps & WithStyles) => { + ({ workflow, onClick, auth, classes }: WorkflowDetailsCardDataProps & AuthStateDataProps & WorkflowDetailsCardActionProps & WithStyles) => { + if (!workflow) { + return + } + + const data = getRegisteredWorkflowPanelData(workflow, auth); return - {workflow && workflow.description !== "" && - - } } /> + + + {Object.keys(data.gitprops).map(k => + getPropertyChip(k, data.gitprops[k], undefined, classes.propertyTag))} + ; })); diff --git a/src/views/process-panel/process-details-card.tsx b/src/views/process-panel/process-details-card.tsx index 4fa4701a..2e5f40b5 100644 --- a/src/views/process-panel/process-details-card.tsx +++ b/src/views/process-panel/process-details-card.tsx @@ -114,7 +114,7 @@ export const ProcessDetailsCard = withStyles(styles)( className={classes.runButton} onClick={() => startProcess(process.containerRequest.uuid)}> - Run Process + Run Workflow } {process.container && process.container.state === ContainerState.RUNNING && cancelProcess(process.containerRequest.uuid)}>Cancel} @@ -126,10 +126,10 @@ export const ProcessDetailsCard = withStyles(styles)( - { doHidePanel && - - - } + {doHidePanel && + + + } } /> diff --git a/src/views/workflow-panel/registered-workflow-panel.tsx b/src/views/workflow-panel/registered-workflow-panel.tsx index 554ae426..4b658452 100644 --- a/src/views/workflow-panel/registered-workflow-panel.tsx +++ b/src/views/workflow-panel/registered-workflow-panel.tsx @@ -11,26 +11,22 @@ import { Tooltip, Typography, Card, - CardHeader + CardHeader, + IconButton, } from '@material-ui/core'; +import { Dispatch } from "redux"; import { connect, DispatchProp } from "react-redux"; import { RouteComponentProps } from 'react-router'; import { ArvadosTheme } from 'common/custom-theme'; import { RootState } from 'store/store'; -import { WorkflowIcon } from 'components/icon/icon'; -import { - WorkflowResource, parseWorkflowDefinition, getWorkflowInputs, - getWorkflowOutputs, getWorkflow -} from 'models/workflow'; +import { WorkflowIcon, MoreOptionsIcon } from 'components/icon/icon'; +import { WorkflowResource } from 'models/workflow'; import { ProcessOutputCollectionFiles } from 'views/process-panel/process-output-collection-files'; -import { WorkflowDetailsAttributes } from 'views-components/details-panel/workflow-details'; +import { WorkflowDetailsAttributes, RegisteredWorkflowPanelDataProps, getRegisteredWorkflowPanelData } from 'views-components/details-panel/workflow-details'; import { getResource } from 'store/resources/resources'; import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions'; import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view'; -import { ProcessIOCard, ProcessIOCardType, ProcessIOParameter } from 'views/process-panel/process-io-card'; -import { formatInputData, formatOutputData } from 'store/process-panel/process-panel-actions'; -import { DetailsAttribute } from 'components/details-attribute/details-attribute'; -import { getPropertyChip } from "views-components/resource-properties-form/property-chip"; +import { ProcessIOCard, ProcessIOCardType } from 'views/process-panel/process-io-card'; type CssRules = 'root' | 'button' @@ -48,8 +44,7 @@ type CssRules = 'root' | 'readOnlyIcon' | 'header' | 'title' - | 'avatar' - | 'propertyTag'; + | 'avatar'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ root: { @@ -118,68 +113,21 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ alignSelf: 'flex-start', paddingTop: theme.spacing.unit * 0.5 }, - propertyTag: { - marginRight: theme.spacing.unit / 2, - marginBottom: theme.spacing.unit / 2 - }, }); -interface RegisteredWorkflowPanelDataProps { - item: WorkflowResource; - workflowCollection: string; - inputParams: ProcessIOParameter[]; - outputParams: ProcessIOParameter[]; - gitprops: { [key: string]: string; }; -} - type RegisteredWorkflowPanelProps = RegisteredWorkflowPanelDataProps & DispatchProp & WithStyles export const RegisteredWorkflowPanel = withStyles(styles)(connect( (state: RootState, props: RouteComponentProps<{ id: string }>) => { const item = getResource(props.match.params.id)(state.resources); - let inputParams: ProcessIOParameter[] = []; - let outputParams: ProcessIOParameter[] = []; - let workflowCollection = ""; - const gitprops: { [key: string]: string; } = {}; if (item) { - // parse definition - const wfdef = parseWorkflowDefinition(item); - - const inputs = getWorkflowInputs(wfdef); - if (inputs) { - inputs.forEach(elm => { - if (elm.default !== undefined && elm.default !== null) { - elm.value = elm.default; - } - }); - inputParams = formatInputData(inputs, state.auth); - } - - const outputs = getWorkflowOutputs(wfdef); - if (outputs) { - outputParams = formatOutputData(outputs, {}, undefined, state.auth); - } - - const wf = getWorkflow(wfdef); - if (wf) { - const REGEX = /keep:([0-9a-f]{32}\+\d+)\/.*/; - if (wf["steps"]) { - workflowCollection = wf["steps"][0].run.match(REGEX)[1]; - } - } - - for (const elm in wfdef) { - if (elm.startsWith("http://arvados.org/cwl#git")) { - gitprops[elm.substr(23)] = wfdef[elm] - } - } - + return getRegisteredWorkflowPanelData(item, state.auth); } - return { item, inputParams, outputParams, workflowCollection, gitprops }; + return { item, inputParams: [], outputParams: [], workflowCollection: "", gitprops: {} }; })( class extends React.Component { render() { - const { classes, item, inputParams, outputParams, workflowCollection, gitprops, dispatch } = this.props; + const { classes, item, inputParams, outputParams, workflowCollection } = this.props; const panelsData: MPVPanelState[] = [ { name: "Details" }, { name: "Inputs" }, @@ -210,18 +158,21 @@ export const RegisteredWorkflowPanel = withStyles(styles)(connect( {item.description || '(no-description)'} } + action={ + + this.handleContextMenu(event)}> + + + } + /> - - - - {Object.keys(gitprops).map(k => - getPropertyChip(k, gitprops[k], undefined, classes.propertyTag))} -