[arvados] updated: 2.7.0-6368-g773a6cd8e3
git repository hosting
git at public.arvados.org
Mon May 20 20:04:26 UTC 2024
Summary of changes:
services/workbench2/cypress/e2e/workflow.cy.js | 25 +
services/workbench2/package.json | 2 +-
.../conditional-tabs/conditional-tabs.test.tsx | 106 +++++
.../conditional-tabs/conditional-tabs.tsx | 50 ++
.../data-table-multiselect-popover.tsx | 4 +-
.../src/components/data-table/data-table.tsx | 2 +-
services/workbench2/src/components/icon/icon.tsx | 7 +
.../multiselect-toolbar/ms-menu-actions.ts | 28 ++
.../ms-toolbar-action-filters.ts | 1 +
.../store/workflow-panel/workflow-panel-actions.ts | 67 ++-
.../action-sets/workflow-action-set.ts | 10 +-
.../multiselect-toolbar/ms-process-action-set.ts | 6 +-
.../multiselect-toolbar/ms-workflow-action-set.ts | 12 +-
.../sharing-dialog/permission-select.tsx | 1 +
.../sharing-dialog/sharing-dialog-component.tsx | 25 +-
.../sharing-invitation-form-component.tsx | 36 +-
.../sharing-dialog/sharing-invitation-form.tsx | 1 -
.../sharing-management-form-component.tsx | 15 +-
.../sharing-public-access-form-component.tsx | 4 +-
.../sharing-dialog/visibility-level-select.tsx | 1 -
.../workflow-remove-dialog.tsx} | 8 +-
.../views/process-panel/process-io-card.test.tsx | 30 ++
.../src/views/process-panel/process-io-card.tsx | 528 ++++++++++-----------
.../workbench2/src/views/workbench/workbench.tsx | 2 +
24 files changed, 622 insertions(+), 349 deletions(-)
create mode 100644 services/workbench2/src/components/conditional-tabs/conditional-tabs.test.tsx
create mode 100644 services/workbench2/src/components/conditional-tabs/conditional-tabs.tsx
create mode 100644 services/workbench2/src/components/multiselect-toolbar/ms-menu-actions.ts
copy services/workbench2/src/views-components/{process-remove-dialog/process-remove-dialog.tsx => workflow-remove-dialog/workflow-remove-dialog.tsx} (67%)
via 773a6cd8e35e65dc152ef7e0e2dd01c040c6e0bb (commit)
via e03f38876cb20149327065bd108f2cd13d438b9d (commit)
via 2811de2338fb60ef6bc097b796d63eb6e86be0b7 (commit)
via ac7999550528b78affae37fb7f01e217ee16c966 (commit)
from 9f2e8304caea45f8e7a603a0e4019fd5845f4264 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
commit 773a6cd8e35e65dc152ef7e0e2dd01c040c6e0bb
Author: Lisa Knox <lisaknox83 at gmail.com>
Date: Thu May 16 09:41:37 2024 -0400
Merge branch '21535-multi-wf-delete'
closes #21535
Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox at curii.com>
diff --git a/services/workbench2/cypress/e2e/workflow.cy.js b/services/workbench2/cypress/e2e/workflow.cy.js
index b9cf86c556..b17ddf980a 100644
--- a/services/workbench2/cypress/e2e/workflow.cy.js
+++ b/services/workbench2/cypress/e2e/workflow.cy.js
@@ -242,10 +242,35 @@ describe('Registered workflow panel tests', function() {
cy.goToPath(`/projects/${activeUser.user.uuid}`);
cy.get('[data-cy=project-panel] table tbody').contains(workflowResource.name).rightclick();
cy.get('[data-cy=context-menu]').contains('Delete Workflow').click();
+ cy.get('[data-cy=confirmation-dialog-ok-btn]').should('exist').click();
cy.get('[data-cy=project-panel] table tbody').should('not.contain', workflowResource.name);
});
});
+ it('can delete multiple workflows', function() {
+ const wfNames = ["Test wf1", "Test wf2", "Test wf3"];
+
+ wfNames.forEach((wfName) => {
+ cy.createResource(activeUser.token, "workflows", {workflow: {name: wfName}})
+ });
+
+ cy.loginAs(activeUser);
+
+ wfNames.forEach((wfName) => {
+ cy.get('tr').contains('td', wfName).should('exist').parent('tr').find('input[type="checkbox"]').click();
+ });
+
+ cy.waitForDom().get('[data-cy=multiselect-button]', {timeout: 10000}).should('be.visible')
+ cy.get('[data-cy=multiselect-button]', {timeout: 10000}).should('have.length', '1').trigger('mouseover');
+ cy.get('body').contains('Delete Workflow', {timeout: 10000}).should('exist')
+ cy.get('[data-cy=multiselect-button]').eq(0).click();
+ cy.get('[data-cy=confirmation-dialog-ok-btn]').should('exist').click();
+
+ wfNames.forEach((wfName) => {
+ cy.get('tr').contains(wfName).should('not.exist');
+ });
+ });
+
it('cannot delete readonly workflow', function() {
cy.createProject({
owningUser: adminUser,
diff --git a/services/workbench2/src/components/data-table-multiselect-popover/data-table-multiselect-popover.tsx b/services/workbench2/src/components/data-table-multiselect-popover/data-table-multiselect-popover.tsx
index 0248c8267d..ced5c89983 100644
--- a/services/workbench2/src/components/data-table-multiselect-popover/data-table-multiselect-popover.tsx
+++ b/services/workbench2/src/components/data-table-multiselect-popover/data-table-multiselect-popover.tsx
@@ -84,7 +84,8 @@ export const DataTableMultiselectPopover = withStyles(styles)(
<>
<Tooltip
disableFocusListener
- title="Select Options"
+ title="Select options"
+ data-cy="data-table-multiselect-popover"
>
<ButtonBase
className={classnames(classes.root)}
@@ -118,6 +119,7 @@ export const DataTableMultiselectPopover = withStyles(styles)(
{options.length &&
options.map((option, i) => (
<div
+ data-cy={`multiselect-popover-${option.name}`}
key={i}
className={classes.option}
onClick={() => {
diff --git a/services/workbench2/src/components/data-table/data-table.tsx b/services/workbench2/src/components/data-table/data-table.tsx
index 7b78799457..89a2e2a8e6 100644
--- a/services/workbench2/src/components/data-table/data-table.tsx
+++ b/services/workbench2/src/components/data-table/data-table.tsx
@@ -354,7 +354,7 @@ export const DataTable = withStyles(styles)(
key={key || index}
className={classes.checkBoxCell}>
<div className={classes.checkBoxHead}>
- <Tooltip title={this.state.isSelected ? "Deselect All" : "Select All"}>
+ <Tooltip title={this.state.isSelected ? "Deselect all" : "Select all"}>
<input
type="checkbox"
className={classes.checkBox}
diff --git a/services/workbench2/src/components/icon/icon.tsx b/services/workbench2/src/components/icon/icon.tsx
index 08c2e8f454..476c0cbe57 100644
--- a/services/workbench2/src/components/icon/icon.tsx
+++ b/services/workbench2/src/components/icon/icon.tsx
@@ -186,6 +186,13 @@ export const VerticalLineDivider: IconType = (props: any) => (
</SvgIcon>
)
+//https://pictogrammers.com/library/mdi/icon/delete-forever/
+export const DeleteForever: IconType = (props: any) => (
+ <SvgIcon {...props}>
+ <path d="M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19M8.46,11.88L9.87,10.47L12,12.59L14.12,10.47L15.53,11.88L13.41,14L15.53,16.12L14.12,17.53L12,15.41L9.88,17.53L8.47,16.12L10.59,14L8.46,11.88M15.5,4L14.5,3H9.5L8.5,4H5V6H19V4H15.5Z" />
+ </SvgIcon>
+)
+
export type IconType = React.SFC<{ className?: string; style?: object }>;
export const AddIcon: IconType = props => <Add {...props} />;
diff --git a/services/workbench2/src/components/multiselect-toolbar/ms-menu-actions.ts b/services/workbench2/src/components/multiselect-toolbar/ms-menu-actions.ts
new file mode 100644
index 0000000000..18ca94662b
--- /dev/null
+++ b/services/workbench2/src/components/multiselect-toolbar/ms-menu-actions.ts
@@ -0,0 +1,28 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export enum MultiSelectMenuActionNames {
+ ADD_TO_FAVORITES = 'Add to Favorites',
+ MOVE_TO_TRASH = 'Move to trash',
+ ADD_TO_PUBLIC_FAVORITES = 'Add to public favorites',
+ API_DETAILS = 'API Details',
+ CANCEL = 'CANCEL',
+ COPY_AND_RERUN_PROCESS = 'Copy and re-run process',
+ COPY_TO_CLIPBOARD = 'Copy to clipboard',
+ DELETE_WORKFLOW = 'Delete Workflow',
+ EDIT_COLLECTION = 'Edit collection',
+ EDIT_PROJECT = 'Edit project',
+ EDIT_PROCESS = 'Edit process',
+ FREEZE_PROJECT = 'Freeze Project',
+ MAKE_A_COPY = 'Make a copy',
+ MOVE_TO = 'Move to',
+ NEW_PROJECT = 'New project',
+ OPEN_IN_NEW_TAB = 'Open in new tab',
+ OPEN_W_3RD_PARTY_CLIENT = 'Open with 3rd party client',
+ OUTPUTS = 'Outputs',
+ REMOVE = 'Remove',
+ RUN_WORKFLOW = 'Run Workflow',
+ SHARE = 'Share',
+ VIEW_DETAILS = 'View details',
+};
diff --git a/services/workbench2/src/components/multiselect-toolbar/ms-toolbar-action-filters.ts b/services/workbench2/src/components/multiselect-toolbar/ms-toolbar-action-filters.ts
index 2b30525e56..5a98d4df77 100644
--- a/services/workbench2/src/components/multiselect-toolbar/ms-toolbar-action-filters.ts
+++ b/services/workbench2/src/components/multiselect-toolbar/ms-toolbar-action-filters.ts
@@ -65,4 +65,5 @@ export const multiselectActionsFilters: TMultiselectActionsFilters = {
[WORKFLOW]: [msWorkflowActionSet, msWorkflowActionFilter],
[READONLY_WORKFLOW]: [msWorkflowActionSet, msReadOnlyWorkflowActionFilter],
+ [ResourceKind.WORKFLOW]: [msWorkflowActionSet, msWorkflowActionFilter],
};
diff --git a/services/workbench2/src/store/workflow-panel/workflow-panel-actions.ts b/services/workbench2/src/store/workflow-panel/workflow-panel-actions.ts
index 37b96bd9b0..ecb0c96b73 100644
--- a/services/workbench2/src/store/workflow-panel/workflow-panel-actions.ts
+++ b/services/workbench2/src/store/workflow-panel/workflow-panel-actions.ts
@@ -24,6 +24,12 @@ import { getResource } from 'store/resources/resources';
import { ProjectResource } from 'models/project';
import { UserResource } from 'models/user';
import { getWorkflowInputs, parseWorkflowDefinition } from 'models/workflow';
+import { ContextMenuResource } from 'store/context-menu/context-menu-actions';
+import { dialogActions } from 'store/dialog/dialog-actions';
+import { ResourceKind, Resource } from 'models/resource';
+import { selectedToArray } from "components/multiselect-toolbar/MultiselectToolbar";
+import { CommonResourceServiceError, getCommonResourceServiceError } from "services/common-service/common-resource-service";
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
export const WORKFLOW_PANEL_ID = "workflowPanel";
const UUID_PREFIX_PROPERTY_NAME = 'uuidPrefix';
@@ -120,10 +126,57 @@ export const getWorkflowDetails = (state: RootState) => {
return workflow || undefined;
};
-export const deleteWorkflow = (workflowUuid: string, ownerUuid: string) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch<any>(navigateTo(ownerUuid));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...', kind: SnackbarKind.INFO }));
- await services.workflowService.delete(workflowUuid);
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
- };
+export const openRemoveWorkflowDialog =
+(resource: ContextMenuResource, numOfWorkflows: Number) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const confirmationText =
+ numOfWorkflows === 1
+ ? "Are you sure you want to remove this workflow?"
+ : `Are you sure you want to remove these ${numOfWorkflows} workflows?`;
+ const titleText = numOfWorkflows === 1 ? "Remove workflow permanently" : "Remove workflows permanently";
+
+ dispatch(
+ dialogActions.OPEN_DIALOG({
+ id: REMOVE_WORKFLOW_DIALOG,
+ data: {
+ title: titleText,
+ text: confirmationText,
+ confirmButtonLabel: "Remove",
+ uuid: resource.uuid,
+ resource,
+ },
+ })
+ );
+};
+
+export const REMOVE_WORKFLOW_DIALOG = "removeWorkflowDialog";
+
+export const removeWorkflowPermanently = (uuid: string, ownerUuid?: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const resource = getState().dialog.removeWorkflowDialog.data.resource;
+ const checkedList = getState().multiselect.checkedList;
+
+ const uuidsToRemove: string[] = resource.fromContextMenu ? [resource.uuid] : selectedToArray(checkedList);
+
+ //if no items in checkedlist, default to normal context menu behavior
+ if (!uuidsToRemove.length) uuidsToRemove.push(uuid);
+ if(ownerUuid) dispatch<any>(navigateTo(ownerUuid));
+
+ const workflowsToRemove = uuidsToRemove
+ .map(uuid => getResource(uuid)(getState().resources) as Resource)
+ .filter(resource => resource.kind === ResourceKind.WORKFLOW);
+
+ for (const workflow of workflowsToRemove) {
+ try {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Removing ...", kind: SnackbarKind.INFO }));
+ await services.workflowService.delete(workflow.uuid);
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Removed.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+ } catch (e) {
+ const error = getCommonResourceServiceError(e);
+ if (error === CommonResourceServiceError.PERMISSION_ERROR_FORBIDDEN) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Access denied`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ } else {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Deletion failed`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ }
+ }
+ }
+};
\ No newline at end of file
diff --git a/services/workbench2/src/views-components/context-menu/action-sets/workflow-action-set.ts b/services/workbench2/src/views-components/context-menu/action-sets/workflow-action-set.ts
index f03340db4b..4e76f2aa4d 100644
--- a/services/workbench2/src/views-components/context-menu/action-sets/workflow-action-set.ts
+++ b/services/workbench2/src/views-components/context-menu/action-sets/workflow-action-set.ts
@@ -3,8 +3,8 @@
// SPDX-License-Identifier: AGPL-3.0
import { ContextMenuActionSet, ContextMenuActionNames } from "views-components/context-menu/context-menu-action-set";
-import { openRunProcess, deleteWorkflow } from "store/workflow-panel/workflow-panel-actions";
-import { DetailsIcon, AdvancedIcon, OpenIcon, Link, StartIcon, TrashIcon } from "components/icon/icon";
+import { openRunProcess, openRemoveWorkflowDialog } from "store/workflow-panel/workflow-panel-actions";
+import { DetailsIcon, AdvancedIcon, OpenIcon, Link, StartIcon, DeleteForever } 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";
@@ -53,10 +53,10 @@ export const workflowActionSet: ContextMenuActionSet = [
[
...readOnlyWorkflowActionSet[0],
{
- icon: TrashIcon,
- name: ContextMenuActionNames.DELETE_WORKFLOW,
+ icon: DeleteForever,
+ name: "Delete Workflow",
execute: (dispatch, resources) => {
- dispatch<any>(deleteWorkflow(resources[0].uuid, resources[0].ownerUuid));
+ dispatch<any>(openRemoveWorkflowDialog(resources[0], resources.length));
},
},
],
diff --git a/services/workbench2/src/views-components/multiselect-toolbar/ms-process-action-set.ts b/services/workbench2/src/views-components/multiselect-toolbar/ms-process-action-set.ts
index 73aebe27bc..0637fcc468 100644
--- a/services/workbench2/src/views-components/multiselect-toolbar/ms-process-action-set.ts
+++ b/services/workbench2/src/views-components/multiselect-toolbar/ms-process-action-set.ts
@@ -2,11 +2,11 @@
//
// SPDX-License-Identifier: AGPL-3.0
-import { RemoveIcon, ReRunProcessIcon, OutputIcon, RenameIcon, StopIcon } from "components/icon/icon";
+import { ContextMenuActionNames } from "views-components/context-menu/context-menu-action-set";
+import { DeleteForever, ReRunProcessIcon, OutputIcon, RenameIcon, StopIcon } from "components/icon/icon";
import { openCopyProcessDialog } from "store/processes/process-copy-actions";
import { openRemoveProcessDialog } from "store/processes/processes-actions";
import { MultiSelectMenuAction, MultiSelectMenuActionSet, msCommonActionSet } from "./ms-menu-actions";
-import { ContextMenuActionNames } from "views-components/context-menu/context-menu-action-set";
import { openProcessUpdateDialog } from "store/processes/process-update-actions";
import { msNavigateToOutput } from "store/multiselect/multiselect-actions";
import { cancelRunningWorkflow } from "store/processes/processes-actions";
@@ -25,7 +25,7 @@ const msCopyAndRerunProcess: MultiSelectMenuAction = {
const msRemoveProcess: MultiSelectMenuAction = {
name: ContextMenuActionNames.REMOVE,
- icon: RemoveIcon,
+ icon: DeleteForever,
hasAlts: false,
isForMulti: true,
execute: (dispatch, resources) => {
diff --git a/services/workbench2/src/views-components/multiselect-toolbar/ms-workflow-action-set.ts b/services/workbench2/src/views-components/multiselect-toolbar/ms-workflow-action-set.ts
index 9c5cdd79e0..124981aa17 100644
--- a/services/workbench2/src/views-components/multiselect-toolbar/ms-workflow-action-set.ts
+++ b/services/workbench2/src/views-components/multiselect-toolbar/ms-workflow-action-set.ts
@@ -2,8 +2,8 @@
//
// SPDX-License-Identifier: AGPL-3.0
-import { openRunProcess, deleteWorkflow } from 'store/workflow-panel/workflow-panel-actions';
-import { StartIcon, TrashIcon, Link } from 'components/icon/icon';
+import { openRunProcess, openRemoveWorkflowDialog } from 'store/workflow-panel/workflow-panel-actions';
+import { StartIcon, DeleteForever, Link } from 'components/icon/icon';
import { MultiSelectMenuAction, MultiSelectMenuActionSet, msCommonActionSet } from './ms-menu-actions';
import { ContextMenuActionNames } from 'views-components/context-menu/context-menu-action-set';
import { copyToClipboardAction } from 'store/open-in-new-tab/open-in-new-tab.actions';
@@ -24,11 +24,13 @@ const msRunWorkflow: MultiSelectMenuAction = {
const msDeleteWorkflow: MultiSelectMenuAction = {
name: DELETE_WORKFLOW,
- icon: TrashIcon,
+ icon: DeleteForever,
hasAlts: false,
- isForMulti: false,
+ isForMulti: true,
execute: (dispatch, resources) => {
- dispatch<any>(deleteWorkflow(resources[0].uuid, resources[0].ownerUuid));
+ for (const resource of [...resources]){
+ dispatch<any>(openRemoveWorkflowDialog(resource, resources.length));
+ }
},
};
diff --git a/services/workbench2/src/views-components/workflow-remove-dialog/workflow-remove-dialog.tsx b/services/workbench2/src/views-components/workflow-remove-dialog/workflow-remove-dialog.tsx
new file mode 100644
index 0000000000..d3f4874370
--- /dev/null
+++ b/services/workbench2/src/views-components/workflow-remove-dialog/workflow-remove-dialog.tsx
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch, compose } from 'redux';
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "components/confirmation-dialog/confirmation-dialog";
+import { withDialog, WithDialogProps } from "store/dialog/with-dialog";
+import { removeWorkflowPermanently, REMOVE_WORKFLOW_DIALOG } from 'store/workflow-panel/workflow-panel-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+ onConfirm: () => {
+ props.closeDialog();
+ dispatch<any>(removeWorkflowPermanently(props.data.uuid));
+ }
+});
+
+export const RemoveWorkflowDialog = compose(
+ withDialog(REMOVE_WORKFLOW_DIALOG),
+ connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
diff --git a/services/workbench2/src/views/workbench/workbench.tsx b/services/workbench2/src/views/workbench/workbench.tsx
index 3020e0d298..3da00e492a 100644
--- a/services/workbench2/src/views/workbench/workbench.tsx
+++ b/services/workbench2/src/views/workbench/workbench.tsx
@@ -39,6 +39,7 @@ import { PartialMoveToNewCollectionDialog } from "views-components/dialog-forms/
import { PartialMoveToExistingCollectionDialog } from "views-components/dialog-forms/partial-move-to-existing-collection-dialog";
import { PartialMoveToSeparateCollectionsDialog } from "views-components/dialog-forms/partial-move-to-separate-collections-dialog";
import { RemoveProcessDialog } from "views-components/process-remove-dialog/process-remove-dialog";
+import { RemoveWorkflowDialog } from "views-components/workflow-remove-dialog/workflow-remove-dialog";
import { MainContentBar } from "views-components/main-content-bar/main-content-bar";
import { Grid } from "@material-ui/core";
import { TrashPanel } from "views/trash-panel/trash-panel";
@@ -435,6 +436,7 @@ const { classes, sidePanelIsCollapsed, isNotLinking, isTransitioning, isUserActi
<RemoveKeepServiceDialog />
<RemoveLinkDialog />
<RemoveProcessDialog />
+ <RemoveWorkflowDialog />
<RemoveRepositoryDialog />
<RemoveSshKeyDialog />
<RemoveVirtualMachineDialog />
commit e03f38876cb20149327065bd108f2cd13d438b9d
Author: Stephen Smith <stephen at curii.com>
Date: Wed May 1 11:37:07 2024 -0400
Merge branch '21642-io-panel-collection-tab-bug' into main. Closes #21642
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/services/workbench2/package.json b/services/workbench2/package.json
index 103ec4bc57..bfdf5ed756 100644
--- a/services/workbench2/package.json
+++ b/services/workbench2/package.json
@@ -30,7 +30,7 @@
"babel-core": "6.26.3",
"babel-runtime": "6.26.0",
"bootstrap": "^5.3.2",
- "caniuse-lite": "1.0.30001606",
+ "caniuse-lite": "1.0.30001612",
"classnames": "2.2.6",
"cwlts": "1.15.29",
"date-fns": "^2.28.0",
diff --git a/services/workbench2/src/components/conditional-tabs/conditional-tabs.test.tsx b/services/workbench2/src/components/conditional-tabs/conditional-tabs.test.tsx
new file mode 100644
index 0000000000..db30135b2e
--- /dev/null
+++ b/services/workbench2/src/components/conditional-tabs/conditional-tabs.test.tsx
@@ -0,0 +1,106 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { mount, configure } from "enzyme";
+import { ConditionalTabs, TabData } from "./conditional-tabs";
+import Adapter from 'enzyme-adapter-react-16';
+import { Tab } from "@material-ui/core";
+
+configure({ adapter: new Adapter() });
+
+describe("<ConditionalTabs />", () => {
+ let tabs: TabData[] = [];
+
+ beforeEach(() => {
+ tabs = [{
+ show: true,
+ label: "Tab1",
+ content: <div id="content1">Content1</div>,
+ },{
+ show: false,
+ label: "Tab2",
+ content: <div id="content2">Content2</div>,
+ },{
+ show: true,
+ label: "Tab3",
+ content: <div id="content3">Content3</div>,
+ }];
+ });
+
+ it("renders only visible tabs", () => {
+ // given
+ const tabContainer = mount(<ConditionalTabs
+ tabs={tabs}
+ />);
+
+ // expect 2 visible tabs
+ expect(tabContainer.find(Tab)).toHaveLength(2);
+ expect(tabContainer.find(Tab).at(0).text()).toBe("Tab1");
+ expect(tabContainer.find(Tab).at(1).text()).toBe("Tab3");
+ // expect visible content 1 and tab 3 to be hidden but exist
+ // content 2 stays unrendered since the tab is hidden
+ expect(tabContainer.find('div#content1').text()).toBe("Content1");
+ expect(tabContainer.find('div#content1').prop('hidden')).toBeFalsy();
+ expect(tabContainer.find('div#content2').exists()).toBeFalsy();
+ expect(tabContainer.find('div#content3').prop('hidden')).toBeTruthy();
+
+ // Show second tab
+ tabs[1].show = true;
+ tabContainer.setProps({ tabs: tabs });
+ tabContainer.update();
+
+ // Expect 3 visible tabs
+ expect(tabContainer.find(Tab)).toHaveLength(3);
+ expect(tabContainer.find(Tab).at(0).text()).toBe("Tab1");
+ expect(tabContainer.find(Tab).at(1).text()).toBe("Tab2");
+ expect(tabContainer.find(Tab).at(2).text()).toBe("Tab3");
+ // Expect visible content 1 and hidden content 2/3
+ expect(tabContainer.find('div#content1').text()).toBe("Content1");
+ expect(tabContainer.find('div#content1').prop('hidden')).toBeFalsy();
+ expect(tabContainer.find('div#content2').prop('hidden')).toBeTruthy();
+ expect(tabContainer.find('div#content3').prop('hidden')).toBeTruthy();
+
+ // Click on Tab2 (position 1)
+ tabContainer.find(Tab).at(1).simulate('click');
+
+ // Expect 3 visible tabs
+ expect(tabContainer.find(Tab)).toHaveLength(3);
+ expect(tabContainer.find(Tab).at(0).text()).toBe("Tab1");
+ expect(tabContainer.find(Tab).at(1).text()).toBe("Tab2");
+ expect(tabContainer.find(Tab).at(2).text()).toBe("Tab3");
+ // Expect visible content2 and hidden content 1/3
+ expect(tabContainer.find('div#content2').text()).toBe("Content2");
+ expect(tabContainer.find('div#content1').prop('hidden')).toBeTruthy();
+ expect(tabContainer.find('div#content2').prop('hidden')).toBeFalsy();
+ expect(tabContainer.find('div#content3').prop('hidden')).toBeTruthy();
+ });
+
+ it("resets selected tab on tab visibility change", () => {
+ // given
+ const tabContainer = mount(<ConditionalTabs
+ tabs={tabs}
+ />);
+
+ // Expect second tab to be Tab3
+ expect(tabContainer.find(Tab).at(1).text()).toBe("Tab3");
+ // Click on Tab3 (position 2)
+ tabContainer.find(Tab).at(1).simulate('click');
+ expect(tabContainer.find('div#content3').text()).toBe("Content3");
+ expect(tabContainer.find('div#content1').prop('hidden')).toBeTruthy();
+ expect(tabContainer.find('div#content2').exists()).toBeFalsy();
+ expect(tabContainer.find('div#content3').prop('hidden')).toBeFalsy();
+
+ // when Tab2 becomes visible
+ tabs[1].show = true;
+ tabContainer.setProps({ tabs: tabs });
+ tabContainer.update(); // Needed or else tab1 content will still be hidden
+
+ // Selected tab resets to 1, tabs 2/3 are hidden
+ expect(tabContainer.find('div#content1').text()).toBe("Content1");
+ expect(tabContainer.find('div#content1').prop('hidden')).toBeFalsy();
+ expect(tabContainer.find('div#content2').prop('hidden')).toBeTruthy();
+ expect(tabContainer.find('div#content3').prop('hidden')).toBeTruthy();
+ });
+});
diff --git a/services/workbench2/src/components/conditional-tabs/conditional-tabs.tsx b/services/workbench2/src/components/conditional-tabs/conditional-tabs.tsx
new file mode 100644
index 0000000000..248c9c0551
--- /dev/null
+++ b/services/workbench2/src/components/conditional-tabs/conditional-tabs.tsx
@@ -0,0 +1,50 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { ReactElement, useEffect, useState } from "react";
+import { Tabs, Tab } from "@material-ui/core";
+import { TabsProps } from "@material-ui/core/Tabs";
+
+interface ComponentWithHidden {
+ hidden: boolean;
+};
+
+export type TabData = {
+ show: boolean;
+ label: string;
+ content: ReactElement<ComponentWithHidden>;
+};
+
+type ConditionalTabsProps = {
+ tabs: TabData[];
+};
+
+export const ConditionalTabs = (props: Omit<TabsProps, 'value' | 'onChange'> & ConditionalTabsProps) => {
+ const [tabState, setTabState] = useState(0);
+ const visibleTabs = props.tabs.filter(tab => tab.show);
+ const visibleTabNames = visibleTabs.map(tab => tab.label).join();
+
+ const handleTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+ setTabState(value);
+ };
+
+ // Reset tab to 0 when tab visibility changes
+ // (or if tab set change causes visible set to change)
+ useEffect(() => {
+ setTabState(0);
+ }, [visibleTabNames]);
+
+ return <>
+ <Tabs
+ {...props}
+ value={tabState}
+ onChange={handleTabChange} >
+ {visibleTabs.map(tab => <Tab key={tab.label} label={tab.label} />)}
+ </Tabs>
+
+ {visibleTabs.map((tab, i) => (
+ React.cloneElement(tab.content, {key: i, hidden: i !== tabState})
+ ))}
+ </>;
+};
diff --git a/services/workbench2/src/views/process-panel/process-io-card.test.tsx b/services/workbench2/src/views/process-panel/process-io-card.test.tsx
index c0feead398..38061e3f06 100644
--- a/services/workbench2/src/views/process-panel/process-io-card.test.tsx
+++ b/services/workbench2/src/views/process-panel/process-io-card.test.tsx
@@ -138,6 +138,36 @@ describe('renderers', () => {
expect(panel.find(TableBody).text()).toContain('someValue');
});
+ it('shows main process with output collection', () => {
+ // when
+ const outputCollection = '987654321';
+ const parameters = [{id: 'someId', label: 'someLabel', value: {display: 'someValue'}}];
+ let panel = mount(
+ <Provider store={store}>
+ <MuiThemeProvider theme={CustomTheme}>
+ <ProcessIOCard
+ label={ProcessIOCardType.OUTPUT}
+ process={false} // Treat as a main process, no requestingContainerUuid
+ outputUuid={outputCollection}
+ params={parameters}
+ raw={{}}
+ />
+ </MuiThemeProvider>
+ </Provider>
+ );
+
+ // then
+ expect(panel.find(CircularProgress).exists()).toBeFalsy();
+ expect(panel.find(Tab).length).toBe(3); // Empty raw is shown if parameters are present
+ expect(panel.find(TableBody).text()).toContain('someId');
+ expect(panel.find(TableBody).text()).toContain('someLabel');
+ expect(panel.find(TableBody).text()).toContain('someValue');
+
+ // Visit output tab
+ panel.find(Tab).at(2).simulate('click');
+ expect(panel.find(ProcessOutputCollectionFiles).prop('currentItemUuid')).toBe(outputCollection);
+ });
+
// Subprocess
it('shows subprocess loading', () => {
diff --git a/services/workbench2/src/views/process-panel/process-io-card.tsx b/services/workbench2/src/views/process-panel/process-io-card.tsx
index 9fce7e83d4..6d60b8cf22 100644
--- a/services/workbench2/src/views/process-panel/process-io-card.tsx
+++ b/services/workbench2/src/views/process-panel/process-io-card.tsx
@@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: AGPL-3.0
-import React, { ReactElement, memo, useState } from "react";
+import React, { ReactElement, memo } from "react";
import { Dispatch } from "redux";
import {
StyleRulesCallback,
@@ -14,8 +14,6 @@ import {
CardContent,
Tooltip,
Typography,
- Tabs,
- Tab,
Table,
TableHead,
TableBody,
@@ -70,6 +68,7 @@ import { KEEP_URL_REGEX } from "models/resource";
import { FixedSizeList } from 'react-window';
import AutoSizer from "react-virtualized-auto-sizer";
import { LinkProps } from "@material-ui/core/Link";
+import { ConditionalTabs } from "components/conditional-tabs/conditional-tabs";
type CssRules =
| "card"
@@ -268,272 +267,206 @@ export interface ProcessIOCardDataProps {
forceShowParams?: boolean;
}
-export interface ProcessIOCardActionProps {
- navigateTo: (uuid: string) => void;
-}
-
-const mapDispatchToProps = (dispatch: Dispatch): ProcessIOCardActionProps => ({
- navigateTo: uuid => dispatch<any>(navigateTo(uuid)),
-});
-
-type ProcessIOCardProps = ProcessIOCardDataProps & ProcessIOCardActionProps & WithStyles<CssRules> & MPVPanelProps;
+type ProcessIOCardProps = ProcessIOCardDataProps & WithStyles<CssRules> & MPVPanelProps;
export const ProcessIOCard = withStyles(styles)(
- connect(
- null,
- mapDispatchToProps
- )(
- ({
- classes,
- label,
- params,
- raw,
- mounts,
- outputUuid,
- doHidePanel,
- doMaximizePanel,
- doUnMaximizePanel,
- panelMaximized,
- panelName,
- process,
- navigateTo,
- forceShowParams,
- }: ProcessIOCardProps) => {
- const [mainProcTabState, setMainProcTabState] = useState(0);
- const [subProcTabState, setSubProcTabState] = useState(0);
- const handleMainProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
- setMainProcTabState(value);
- };
- const handleSubProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
- setSubProcTabState(value);
- };
-
- const PanelIcon = label === ProcessIOCardType.INPUT ? InputIcon : OutputIcon;
- const mainProcess = !(process && process!.containerRequest.requestingContainerUuid);
- const showParamTable = mainProcess || forceShowParams;
-
- const loading = raw === null || raw === undefined || params === null;
-
- const hasRaw = !!(raw && Object.keys(raw).length > 0);
- const hasParams = !!(params && params.length > 0);
- // isRawLoaded allows subprocess panel to display raw even if it's {}
- const isRawLoaded = !!(raw && Object.keys(raw).length >= 0);
+ ({
+ classes,
+ label,
+ params,
+ raw,
+ mounts,
+ outputUuid,
+ doHidePanel,
+ doMaximizePanel,
+ doUnMaximizePanel,
+ panelMaximized,
+ panelName,
+ process,
+ forceShowParams,
+ }: ProcessIOCardProps) => {
+ const PanelIcon = label === ProcessIOCardType.INPUT ? InputIcon : OutputIcon;
+ const mainProcess = !(process && process!.containerRequest.requestingContainerUuid);
+ const showParamTable = mainProcess || forceShowParams;
+
+ const loading = raw === null || raw === undefined || params === null;
+
+ const hasRaw = !!(raw && Object.keys(raw).length > 0);
+ const hasParams = !!(params && params.length > 0);
+ // isRawLoaded allows subprocess panel to display raw even if it's {}
+ const isRawLoaded = !!(raw && Object.keys(raw).length >= 0);
+
+ // Subprocess
+ const hasInputMounts = !!(label === ProcessIOCardType.INPUT && mounts && mounts.length);
+ const hasOutputCollecton = !!(label === ProcessIOCardType.OUTPUT && outputUuid);
+ // Subprocess should not show loading if hasOutputCollection or hasInputMounts
+ const subProcessLoading = loading && !hasOutputCollecton && !hasInputMounts;
- // Subprocess
- const hasInputMounts = !!(label === ProcessIOCardType.INPUT && mounts && mounts.length);
- const hasOutputCollecton = !!(label === ProcessIOCardType.OUTPUT && outputUuid);
- // Subprocess should not show loading if hasOutputCollection or hasInputMounts
- const subProcessLoading = loading && !hasOutputCollecton && !hasInputMounts;
-
- return (
- <Card
- className={classes.card}
- data-cy="process-io-card"
- >
- <CardHeader
- className={classes.header}
- classes={{
- content: classes.title,
- avatar: classes.avatar,
- }}
- avatar={<PanelIcon className={classes.iconHeader} />}
- title={
- <Typography
- noWrap
- variant="h6"
- color="inherit"
- >
- {label}
- </Typography>
- }
- action={
- <div>
- {doUnMaximizePanel && panelMaximized && (
- <Tooltip
- title={`Unmaximize ${panelName || "panel"}`}
- disableFocusListener
- >
- <IconButton onClick={doUnMaximizePanel}>
- <UnMaximizeIcon />
- </IconButton>
- </Tooltip>
- )}
- {doMaximizePanel && !panelMaximized && (
- <Tooltip
- title={`Maximize ${panelName || "panel"}`}
- disableFocusListener
- >
- <IconButton onClick={doMaximizePanel}>
- <MaximizeIcon />
- </IconButton>
- </Tooltip>
- )}
- {doHidePanel && (
- <Tooltip
- title={`Close ${panelName || "panel"}`}
- disableFocusListener
- >
- <IconButton
- disabled={panelMaximized}
- onClick={doHidePanel}
- >
- <CloseIcon />
- </IconButton>
- </Tooltip>
- )}
- </div>
- }
- />
- <CardContent className={classes.content}>
- {showParamTable ? (
- <>
- {/* raw is undefined until params are loaded */}
- {loading && (
- <Grid
- container
- item
- alignItems="center"
- justify="center"
+ return (
+ <Card
+ className={classes.card}
+ data-cy="process-io-card"
+ >
+ <CardHeader
+ className={classes.header}
+ classes={{
+ content: classes.title,
+ avatar: classes.avatar,
+ }}
+ avatar={<PanelIcon className={classes.iconHeader} />}
+ title={
+ <Typography
+ noWrap
+ variant="h6"
+ color="inherit"
+ >
+ {label}
+ </Typography>
+ }
+ action={
+ <div>
+ {doUnMaximizePanel && panelMaximized && (
+ <Tooltip
+ title={`Unmaximize ${panelName || "panel"}`}
+ disableFocusListener
+ >
+ <IconButton onClick={doUnMaximizePanel}>
+ <UnMaximizeIcon />
+ </IconButton>
+ </Tooltip>
+ )}
+ {doMaximizePanel && !panelMaximized && (
+ <Tooltip
+ title={`Maximize ${panelName || "panel"}`}
+ disableFocusListener
+ >
+ <IconButton onClick={doMaximizePanel}>
+ <MaximizeIcon />
+ </IconButton>
+ </Tooltip>
+ )}
+ {doHidePanel && (
+ <Tooltip
+ title={`Close ${panelName || "panel"}`}
+ disableFocusListener
+ >
+ <IconButton
+ disabled={panelMaximized}
+ onClick={doHidePanel}
>
- <CircularProgress />
- </Grid>
- )}
- {/* Once loaded, either raw or params may still be empty
- * Raw when all params are empty
- * Params when raw is provided by containerRequest properties but workflow mount is absent for preview
- */}
- {!loading && (hasRaw || hasParams) && (
- <>
- <Tabs
- value={mainProcTabState}
- onChange={handleMainProcTabChange}
- variant="fullWidth"
- className={classes.symmetricTabs}
- >
- {/* params will be empty on processes without workflow definitions in mounts, so we only show raw */}
- {hasParams && <Tab label="Parameters" />}
- {!forceShowParams && <Tab label="JSON" />}
- {hasOutputCollecton && <Tab label="Collection" />}
- </Tabs>
- {mainProcTabState === 0 && params && hasParams && (
- <div className={classes.tableWrapper}>
- <ProcessIOPreview
- data={params}
+ <CloseIcon />
+ </IconButton>
+ </Tooltip>
+ )}
+ </div>
+ }
+ />
+ <CardContent className={classes.content}>
+ {showParamTable ? (
+ <>
+ {/* raw is undefined until params are loaded */}
+ {loading && (
+ <Grid
+ container
+ item
+ alignItems="center"
+ justify="center"
+ >
+ <CircularProgress />
+ </Grid>
+ )}
+ {/* Once loaded, either raw or params may still be empty
+ * Raw when all params are empty
+ * Params when raw is provided by containerRequest properties but workflow mount is absent for preview
+ */}
+ {!loading && (hasRaw || hasParams) && (
+ <ConditionalTabs
+ variant="fullWidth"
+ className={classes.symmetricTabs}
+ tabs={[
+ {
+ // params will be empty on processes without workflow definitions in mounts, so we only show raw
+ show: hasParams,
+ label: "Parameters",
+ content: <ProcessIOPreview
+ data={params || []}
valueLabel={forceShowParams ? "Default value" : "Value"}
- />
- </div>
- )}
- {(mainProcTabState === 1 || !hasParams) && (
- <div className={classes.jsonWrapper}>
- <ProcessIORaw data={raw} />
- </div>
- )}
- {mainProcTabState === 2 && hasOutputCollecton && (
- <>
- {outputUuid && (
- <Typography className={classes.collectionLink}>
- Output Collection:{" "}
- <MuiLink
- className={classes.keepLink}
- onClick={() => {
- navigateTo(outputUuid || "");
- }}
- >
- {outputUuid}
- </MuiLink>
- </Typography>
- )}
- <ProcessOutputCollectionFiles
- isWritable={false}
- currentItemUuid={outputUuid}
- />
- </>
- )}
-
- </>
- )}
- {!loading && !hasRaw && !hasParams && (
- <Grid
- container
- item
- alignItems="center"
- justify="center"
- >
- <DefaultView messages={["No parameters found"]} />
- </Grid>
- )}
- </>
- ) : (
- // Subprocess
- <>
- {subProcessLoading ? (
- <Grid
- container
- item
- alignItems="center"
- justify="center"
- >
- <CircularProgress />
- </Grid>
- ) : !subProcessLoading && (hasInputMounts || hasOutputCollecton || isRawLoaded) ? (
- <>
- <Tabs
- value={subProcTabState}
- onChange={handleSubProcTabChange}
- variant="fullWidth"
- className={classes.symmetricTabs}
- >
- {hasInputMounts && <Tab label="Collections" />}
- {hasOutputCollecton && <Tab label="Collection" />}
- {isRawLoaded && <Tab label="JSON" />}
- </Tabs>
- {subProcTabState === 0 && hasInputMounts && <ProcessInputMounts mounts={mounts || []} />}
- {subProcTabState === 0 && hasOutputCollecton && (
- <div className={classes.tableWrapper}>
- <>
- {outputUuid && (
- <Typography className={classes.collectionLink}>
- Output Collection:{" "}
- <MuiLink
- className={classes.keepLink}
- onClick={() => {
- navigateTo(outputUuid || "");
- }}
- >
- {outputUuid}
- </MuiLink>
- </Typography>
- )}
- <ProcessOutputCollectionFiles
- isWritable={false}
- currentItemUuid={outputUuid}
- />
- </>
- </div>
- )}
- {isRawLoaded && (subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && (
- <div className={classes.jsonWrapper}>
- <ProcessIORaw data={raw} />
- </div>
- )}
- </>
- ) : (
- <Grid
- container
- item
- alignItems="center"
- justify="center"
- >
- <DefaultView messages={["No data to display"]} />
- </Grid>
- )}
- </>
- )}
- </CardContent>
- </Card>
- );
- }
- )
+ />,
+ },
+ {
+ show: !forceShowParams,
+ label: "JSON",
+ content: <ProcessIORaw data={raw} />,
+ },
+ {
+ show: hasOutputCollecton,
+ label: "Collection",
+ content: <ProcessOutputCollection outputUuid={outputUuid} />,
+ },
+ ]}
+ />
+ )}
+ {!loading && !hasRaw && !hasParams && (
+ <Grid
+ container
+ item
+ alignItems="center"
+ justify="center"
+ >
+ <DefaultView messages={["No parameters found"]} />
+ </Grid>
+ )}
+ </>
+ ) : (
+ // Subprocess
+ <>
+ {subProcessLoading ? (
+ <Grid
+ container
+ item
+ alignItems="center"
+ justify="center"
+ >
+ <CircularProgress />
+ </Grid>
+ ) : !subProcessLoading && (hasInputMounts || hasOutputCollecton || isRawLoaded) ? (
+ <ConditionalTabs
+ variant="fullWidth"
+ className={classes.symmetricTabs}
+ tabs={[
+ {
+ show: hasInputMounts,
+ label: "Collections",
+ content: <ProcessInputMounts mounts={mounts || []} />,
+ },
+ {
+ show: hasOutputCollecton,
+ label: "Collection",
+ content: <ProcessOutputCollection outputUuid={outputUuid} />,
+ },
+ {
+ show: isRawLoaded,
+ label: "JSON",
+ content: <ProcessIORaw data={raw} />,
+ },
+ ]}
+ />
+ ) : (
+ <Grid
+ container
+ item
+ alignItems="center"
+ justify="center"
+ >
+ <DefaultView messages={["No data to display"]} />
+ </Grid>
+ )}
+ </>
+ )}
+ </CardContent>
+ </Card>
+ );
+ }
);
export type ProcessIOValue = {
@@ -552,12 +485,13 @@ export type ProcessIOParameter = {
interface ProcessIOPreviewDataProps {
data: ProcessIOParameter[];
valueLabel: string;
+ hidden?: boolean;
}
type ProcessIOPreviewProps = ProcessIOPreviewDataProps & WithStyles<CssRules>;
const ProcessIOPreview = memo(
- withStyles(styles)(({ classes, data, valueLabel }: ProcessIOPreviewProps) => {
+ withStyles(styles)(({ data, valueLabel, hidden, classes }: ProcessIOPreviewProps) => {
const showLabel = data.some((param: ProcessIOParameter) => param.label);
const hasMoreValues = (index: number) => (
@@ -613,7 +547,7 @@ const ProcessIOPreview = memo(
</TableRow>;
};
- return (
+ return <div className={classes.tableWrapper} hidden={hidden}>
<Table
className={classes.paramTableRoot}
aria-label="Process IO Preview"
@@ -641,7 +575,7 @@ const ProcessIOPreview = memo(
</AutoSizer>
</TableBody>
</Table>
- );
+ </div>;
})
);
@@ -657,19 +591,23 @@ const ProcessValuePreview = withStyles(styles)(({ value, classes }: ProcessValue
interface ProcessIORawDataProps {
data: ProcessIOParameter[];
+ hidden?: boolean;
}
-const ProcessIORaw = withStyles(styles)(({ data }: ProcessIORawDataProps) => (
- <Paper elevation={0} style={{minWidth: "100%", height: "100%"}}>
- <DefaultVirtualCodeSnippet
- lines={JSON.stringify(data, null, 2).split('\n')}
- linked
- />
- </Paper>
+const ProcessIORaw = withStyles(styles)(({ data, hidden, classes }: ProcessIORawDataProps & WithStyles<CssRules>) => (
+ <div className={classes.jsonWrapper} hidden={hidden}>
+ <Paper elevation={0} style={{minWidth: "100%", height: "100%"}}>
+ <DefaultVirtualCodeSnippet
+ lines={JSON.stringify(data, null, 2).split('\n')}
+ linked
+ />
+ </Paper>
+ </div>
));
interface ProcessInputMountsDataProps {
mounts: InputCollectionMount[];
+ hidden?: boolean;
}
type ProcessInputMountsProps = ProcessInputMountsDataProps & WithStyles<CssRules>;
@@ -677,10 +615,11 @@ type ProcessInputMountsProps = ProcessInputMountsDataProps & WithStyles<CssRules
const ProcessInputMounts = withStyles(styles)(
connect((state: RootState) => ({
auth: state.auth,
- }))(({ mounts, classes, auth }: ProcessInputMountsProps & { auth: AuthState }) => (
+ }))(({ mounts, hidden, classes, auth }: ProcessInputMountsProps & { auth: AuthState }) => (
<Table
className={classes.mountsTableRoot}
aria-label="Process Input Mounts"
+ hidden={hidden}
>
<TableHead>
<TableRow>
@@ -709,6 +648,40 @@ const ProcessInputMounts = withStyles(styles)(
))
);
+export interface ProcessOutputCollectionActionProps {
+ navigateTo: (uuid: string) => void;
+}
+
+const mapNavigateToProps = (dispatch: Dispatch): ProcessOutputCollectionActionProps => ({
+ navigateTo: uuid => dispatch<any>(navigateTo(uuid)),
+});
+
+type ProcessOutputCollectionProps = {outputUuid: string | undefined, hidden?: boolean} & ProcessOutputCollectionActionProps & WithStyles<CssRules>;
+
+const ProcessOutputCollection = withStyles(styles)(connect(null, mapNavigateToProps)(({ outputUuid, hidden, navigateTo, classes }: ProcessOutputCollectionProps) => (
+ <div className={classes.tableWrapper} hidden={hidden}>
+ <>
+ {outputUuid && (
+ <Typography className={classes.collectionLink}>
+ Output Collection:{" "}
+ <MuiLink
+ className={classes.keepLink}
+ onClick={() => {
+ navigateTo(outputUuid || "");
+ }}
+ >
+ {outputUuid}
+ </MuiLink>
+ </Typography>
+ )}
+ <ProcessOutputCollectionFiles
+ isWritable={false}
+ currentItemUuid={outputUuid}
+ />
+ </>
+ </div>
+)));
+
type FileWithSecondaryFiles = {
secondaryFiles: File[];
};
commit 2811de2338fb60ef6bc097b796d63eb6e86be0b7
Author: Stephen Smith <stephen at curii.com>
Date: Fri Apr 12 09:22:25 2024 -0400
Merge branch '21688-io-panel-style-fixes' into main. Closes #21688
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/services/workbench2/src/views/process-panel/process-io-card.tsx b/services/workbench2/src/views/process-panel/process-io-card.tsx
index 5851b145d4..9fce7e83d4 100644
--- a/services/workbench2/src/views/process-panel/process-io-card.tsx
+++ b/services/workbench2/src/views/process-panel/process-io-card.tsx
@@ -123,7 +123,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
color: theme.customs.colors.greyD,
fontSize: "1.875rem",
},
- // Applies to table tab's content
+ // Applies to table tab and collection table content
tableWrapper: {
height: "auto",
maxHeight: `calc(100% - ${theme.spacing.unit * 6}px)`,
@@ -131,7 +131,8 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
// Use flexbox to keep scrolling at the virtual list level
display: "flex",
flexDirection: "column",
- alignItems: "start", // Prevents scroll bars at different levels in json tab
+ alignItems: "stretch", // Stretches output collection to full width
+
},
// Param table virtual list styles
@@ -486,9 +487,9 @@ export const ProcessIOCard = withStyles(styles)(
{hasOutputCollecton && <Tab label="Collection" />}
{isRawLoaded && <Tab label="JSON" />}
</Tabs>
- <div className={classes.tableWrapper}>
- {subProcTabState === 0 && hasInputMounts && <ProcessInputMounts mounts={mounts || []} />}
- {subProcTabState === 0 && hasOutputCollecton && (
+ {subProcTabState === 0 && hasInputMounts && <ProcessInputMounts mounts={mounts || []} />}
+ {subProcTabState === 0 && hasOutputCollecton && (
+ <div className={classes.tableWrapper}>
<>
{outputUuid && (
<Typography className={classes.collectionLink}>
@@ -508,13 +509,13 @@ export const ProcessIOCard = withStyles(styles)(
currentItemUuid={outputUuid}
/>
</>
- )}
- {isRawLoaded && (subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && (
- <div className={classes.jsonWrapper}>
- <ProcessIORaw data={raw} />
- </div>
- )}
- </div>
+ </div>
+ )}
+ {isRawLoaded && (subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && (
+ <div className={classes.jsonWrapper}>
+ <ProcessIORaw data={raw} />
+ </div>
+ )}
</>
) : (
<Grid
commit ac7999550528b78affae37fb7f01e217ee16c966
Author: Lisa Knox <lisaknox83 at gmail.com>
Date: Wed Apr 10 14:52:16 2024 -0400
Merge branch '21313-share-dialog-warning'
closes #21313
Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox at curii.com>
diff --git a/services/workbench2/src/views-components/sharing-dialog/permission-select.tsx b/services/workbench2/src/views-components/sharing-dialog/permission-select.tsx
index 3c4471f648..5201ba00d5 100644
--- a/services/workbench2/src/views-components/sharing-dialog/permission-select.tsx
+++ b/services/workbench2/src/views-components/sharing-dialog/permission-select.tsx
@@ -47,6 +47,7 @@ export const formatPermissionLevel = (value: PermissionLevel) => {
export const PermissionSelect = (props: SelectProps) =>
<Select
{...props}
+ disableUnderline
renderValue={renderPermissionItem}>
<MenuItem value={PermissionSelectValue.READ}>
{renderPermissionItem(PermissionSelectValue.READ)}
diff --git a/services/workbench2/src/views-components/sharing-dialog/sharing-dialog-component.tsx b/services/workbench2/src/views-components/sharing-dialog/sharing-dialog-component.tsx
index f83cec60f2..01082211d3 100644
--- a/services/workbench2/src/views-components/sharing-dialog/sharing-dialog-component.tsx
+++ b/services/workbench2/src/views-components/sharing-dialog/sharing-dialog-component.tsx
@@ -90,7 +90,7 @@ export default (props: SharingDialogComponentProps) => {
fullWidth
maxWidth='sm'
disableBackdropClick={saveEnabled}
- disableEscapeKeyDown={saveEnabled}>
+ >
<DialogTitle>
Sharing settings
</DialogTitle>
@@ -111,7 +111,7 @@ export default (props: SharingDialogComponentProps) => {
{tabNr === SharingDialogTab.PERMISSIONS &&
<Grid container direction='column' spacing={24}>
<Grid item>
- <SharingInvitationForm onSave={onSave} saveEnabled={saveEnabled} />
+ <SharingInvitationForm onSave={onSave} />
</Grid>
<Grid item>
<SharingManagementForm onSave={onSave} />
@@ -182,8 +182,25 @@ export default (props: SharingDialogComponentProps) => {
<Button onClick={() => {
onClose();
setWithExpiration(false);
- }}>
- Close
+ }}
+ disabled={saveEnabled}
+ color='primary'
+ size='small'
+ style={{ marginLeft: '10px' }}
+ >
+ Close
+ </Button>
+ <Button onClick={() => {
+ onSave();
+ }}
+ data-cy="add-invited-people"
+ disabled={!saveEnabled}
+ color='primary'
+ variant='contained'
+ size='small'
+ style={{ marginLeft: '10px' }}
+ >
+ Save
</Button>
</Grid>
</Grid>
diff --git a/services/workbench2/src/views-components/sharing-dialog/sharing-invitation-form-component.tsx b/services/workbench2/src/views-components/sharing-dialog/sharing-invitation-form-component.tsx
index 871ea503ec..19fcf58849 100644
--- a/services/workbench2/src/views-components/sharing-dialog/sharing-invitation-form-component.tsx
+++ b/services/workbench2/src/views-components/sharing-dialog/sharing-invitation-form-component.tsx
@@ -4,61 +4,33 @@
import React from 'react';
import { Field, WrappedFieldProps, FieldArray, WrappedFieldArrayProps } from 'redux-form';
-import { Grid, FormControl, InputLabel, Tooltip, IconButton, StyleRulesCallback } from '@material-ui/core';
+import { Grid, FormControl, InputLabel, StyleRulesCallback, Divider } from '@material-ui/core';
import { PermissionSelect, parsePermissionLevel, formatPermissionLevel } from './permission-select';
import { ParticipantSelect, Participant } from './participant-select';
-import { AddIcon } from 'components/icon/icon';
import { WithStyles } from '@material-ui/core/styles';
import withStyles from '@material-ui/core/styles/withStyles';
import { ArvadosTheme } from 'common/custom-theme';
-type SharingStyles = 'root' | 'addButtonRoot' | 'addButtonPrimary' | 'addButtonDisabled';
+type SharingStyles = 'root';
const styles: StyleRulesCallback<SharingStyles> = (theme: ArvadosTheme) => ({
root: {
padding: `${theme.spacing.unit}px 0`,
},
- addButtonRoot: {
- height: "36px",
- width: "36px",
- marginRight: "6px",
- marginLeft: "6px",
- marginTop: "12px",
- },
- addButtonPrimary: {
- color: theme.palette.primary.contrastText,
- background: theme.palette.primary.main,
- "&:hover": {
- background: theme.palette.primary.dark,
- }
- },
- addButtonDisabled: {
- background: 'none',
- }
});
-const SharingInvitationFormComponent = (props: { onSave: () => void, saveEnabled: boolean }) => <StyledSharingInvitationFormComponent onSave={props.onSave} saveEnabled={props.saveEnabled} />
+const SharingInvitationFormComponent = (props: { onSave: () => void }) => <StyledSharingInvitationFormComponent onSave={props.onSave} />
export default SharingInvitationFormComponent;
const StyledSharingInvitationFormComponent = withStyles(styles)(
- ({ onSave, saveEnabled, classes }: { onSave: () => void, saveEnabled: boolean } & WithStyles<SharingStyles>) =>
+ ({ classes }: { onSave: () => void } & WithStyles<SharingStyles>) =>
<Grid container spacing={8} wrap='nowrap' className={classes.root} >
<Grid data-cy="invite-people-field" item xs={8}>
<InvitedPeopleField />
</Grid>
<Grid data-cy="permission-select-field" item xs={4} container wrap='nowrap'>
<PermissionSelectField />
- <IconButton onClick={onSave} disabled={!saveEnabled} color="primary" classes={{
- root: classes.addButtonRoot,
- colorPrimary: classes.addButtonPrimary,
- disabled: classes.addButtonDisabled
- }}
- data-cy='add-invited-people'>
- <Tooltip title="Add authorization">
- <AddIcon />
- </Tooltip>
- </IconButton>
</Grid>
</Grid >);
diff --git a/services/workbench2/src/views-components/sharing-dialog/sharing-invitation-form.tsx b/services/workbench2/src/views-components/sharing-dialog/sharing-invitation-form.tsx
index 3315473225..46f94dd348 100644
--- a/services/workbench2/src/views-components/sharing-dialog/sharing-invitation-form.tsx
+++ b/services/workbench2/src/views-components/sharing-dialog/sharing-invitation-form.tsx
@@ -14,7 +14,6 @@ interface InvitationFormData {
interface SaveProps {
onSave: () => void;
- saveEnabled: boolean;
}
export const SharingInvitationForm =
diff --git a/services/workbench2/src/views-components/sharing-dialog/sharing-management-form-component.tsx b/services/workbench2/src/views-components/sharing-dialog/sharing-management-form-component.tsx
index b7ac8ced76..fa3cc46189 100644
--- a/services/workbench2/src/views-components/sharing-dialog/sharing-management-form-component.tsx
+++ b/services/workbench2/src/views-components/sharing-dialog/sharing-management-form-component.tsx
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0
import React from 'react';
-import { Grid, StyleRulesCallback, Divider, IconButton, Typography } from '@material-ui/core';
+import { Grid, StyleRulesCallback, Divider, IconButton, Typography, Tooltip } from '@material-ui/core';
import {
Field,
WrappedFieldProps,
@@ -52,9 +52,16 @@ const PermissionManagementRow = withStyles(permissionManagementRowStyles)(
({ field, index, fields, classes, onSave }: { field: string, index: number, fields: FieldArrayFieldsProps<{ email: string }>, onSave: () => void; } & WithStyles<'root'>) =>
<>
<Grid container alignItems='center' spacing={8} wrap='nowrap' className={classes.root}>
- <Grid item xs={8}>
+ <Grid item xs={7}>
<Typography noWrap variant='subtitle1'>{fields.get(index).email}</Typography>
</Grid>
+ <Grid item xs={1} container wrap='nowrap'>
+ <Tooltip title='Remove access'>
+ <IconButton onClick={() => { fields.remove(index); onSave(); }}>
+ <CloseIcon />
+ </IconButton>
+ </Tooltip>
+ </Grid>
<Grid item xs={4} container wrap='nowrap'>
<Field
name={`${field}.permissions` as string}
@@ -63,9 +70,7 @@ const PermissionManagementRow = withStyles(permissionManagementRowStyles)(
parse={parsePermissionLevel}
onChange={onSave}
/>
- <IconButton onClick={() => { fields.remove(index); onSave(); }}>
- <CloseIcon />
- </IconButton>
+
</Grid>
</Grid>
<Divider />
diff --git a/services/workbench2/src/views-components/sharing-dialog/sharing-public-access-form-component.tsx b/services/workbench2/src/views-components/sharing-dialog/sharing-public-access-form-component.tsx
index 5fc3f4e38e..161cff58c7 100644
--- a/services/workbench2/src/views-components/sharing-dialog/sharing-public-access-form-component.tsx
+++ b/services/workbench2/src/views-components/sharing-dialog/sharing-public-access-form-component.tsx
@@ -29,13 +29,13 @@ const SharingPublicAccessForm = withStyles(sharingPublicAccessStyles)(
({ classes, visibility, includePublic, onSave }: WithStyles<'root' | 'heading'> & AccessProps) =>
<>
<Typography className={classes.heading}>General access</Typography>
- <Grid container alignItems='center' spacing={8} className={classes.root}>
+ <Grid container alignItems='center' className={classes.root}>
<Grid item xs={8}>
<Typography variant='subtitle1'>
{renderVisibilityInfo(visibility)}
</Typography>
</Grid>
- <Grid item xs={4} container wrap='nowrap'>
+ <Grid item xs={4} wrap='nowrap'>
<Field<{ includePublic: boolean }> name='visibility' component={VisibilityLevelSelectComponent} includePublic={includePublic} onChange={onSave} />
</Grid>
</Grid>
diff --git a/services/workbench2/src/views-components/sharing-dialog/visibility-level-select.tsx b/services/workbench2/src/views-components/sharing-dialog/visibility-level-select.tsx
index 4f12e3eacd..b90bc79c9d 100644
--- a/services/workbench2/src/views-components/sharing-dialog/visibility-level-select.tsx
+++ b/services/workbench2/src/views-components/sharing-dialog/visibility-level-select.tsx
@@ -17,7 +17,6 @@ type VisibilityLevelSelectClasses = 'root';
const VisibilityLevelSelectStyles: StyleRulesCallback<VisibilityLevelSelectClasses> = theme => ({
root: {
- marginLeft: theme.spacing.unit,
}
});
export const VisibilityLevelSelect = withStyles(VisibilityLevelSelectStyles)(
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list