From: Lisa Knox Date: Fri, 19 Apr 2024 18:40:16 +0000 (-0400) Subject: Merge branch '21448-menu-reorder' into 21224-project-details X-Git-Url: https://git.arvados.org/arvados.git/commitdiff_plain/562687ce72e709d485aa47773117a51a764a6606?hp=52d652233f981839e4a8cfe25fadf985adda82ee Merge branch '21448-menu-reorder' into 21224-project-details refs #21448 Arvados-DCO-1.1-Signed-off-by: Lisa Knox --- diff --git a/services/workbench2/cypress/e2e/details-card.spec.js b/services/workbench2/cypress/e2e/details-card.spec.js new file mode 100644 index 0000000000..3fbfd97e22 --- /dev/null +++ b/services/workbench2/cypress/e2e/details-card.spec.js @@ -0,0 +1,261 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +describe('User Details Card tests', function () { + let activeUser; + let adminUser; + + before(function () { + // Only set up common users once. These aren't set up as aliases because + // aliases are cleaned up after every test. Also it doesn't make sense + // to set the same users on beforeEach() over and over again, so we + // separate a little from Cypress' 'Best Practices' here. + cy.getUser('admin', 'Admin', 'User', true, true) + .as('adminUser') + .then(function () { + adminUser = this.adminUser; + }); + cy.getUser('activeUser1', 'Active', 'User', false, true) + .as('activeUser') + .then(function () { + activeUser = this.activeUser; + }); + cy.on('uncaught:exception', (err, runnable) => { + console.error(err); + }); + }); + + beforeEach(function () { + cy.clearCookies(); + cy.clearLocalStorage(); + }); + + it('should display the user details card', () => { + cy.loginAs(adminUser); + + cy.get('[data-cy=user-details-card]').should('be.visible'); + cy.get('[data-cy=user-details-card]').contains(adminUser.user.full_name).should('be.visible'); + }); + + it('should contain a context menu with the correct options', () => { + cy.loginAs(adminUser); + + cy.get('[data-cy=kebab-icon]').should('be.visible').click(); + + //admin options + cy.get('[data-cy=context-menu]').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('API Details').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Account Settings').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Attributes').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Project').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Deactivate User').should('be.visible'); + + cy.loginAs(activeUser); + + cy.get('[data-cy=kebab-icon]').should('be.visible').click(); + + //active user options + cy.get('[data-cy=context-menu]').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('API Details').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Account Settings').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Attributes').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Project').should('be.visible'); + }); +}); + +describe('Project Details Card tests', function () { + let activeUser; + let adminUser; + + before(function () { + // Only set up common users once. These aren't set up as aliases because + // aliases are cleaned up after every test. Also it doesn't make sense + // to set the same users on beforeEach() over and over again, so we + // separate a little from Cypress' 'Best Practices' here. + cy.getUser('admin', 'Admin', 'User', true, true) + .as('adminUser') + .then(function () { + adminUser = this.adminUser; + }); + cy.getUser('activeUser1', 'Active', 'User', false, true) + .as('activeUser') + .then(function () { + activeUser = this.activeUser; + }); + cy.on('uncaught:exception', (err, runnable) => { + console.error(err); + }); + }); + + beforeEach(function () { + cy.clearCookies(); + cy.clearLocalStorage(); + }); + + it('should display the project details card', () => { + const projName = `Test project (${Math.floor(999999 * Math.random())})`; + cy.loginAs(adminUser); + + // Create project + cy.get('[data-cy=side-panel-button]').click(); + cy.get('[data-cy=side-panel-new-project]').click(); + cy.get('[data-cy=form-dialog]') + .should('contain', 'New Project') + .within(() => { + cy.get('[data-cy=name-field]').within(() => { + cy.get('input').type(projName); + }); + }); + cy.get('[data-cy=form-submit-btn]').click(); + cy.get('[data-cy=form-dialog]').should('not.exist'); + + cy.get('[data-cy=project-details-card]').should('be.visible'); + cy.get('[data-cy=project-details-card]').contains(projName).should('be.visible'); + }); + + it('should contain a context menu with the correct options', () => { + const adminProjName = `Test project (${Math.floor(999999 * Math.random())})`; + cy.loginAs(adminUser); + + cy.get('[data-cy=side-panel-button]').click(); + cy.get('[data-cy=side-panel-new-project]').click(); + cy.get('[data-cy=form-dialog]') + .should('contain', 'New Project') + .within(() => { + cy.get('[data-cy=name-field]').within(() => { + cy.get('input').type(projName); + }); + }); + cy.get('[data-cy=form-submit-btn]').click(); + + cy.waitForDom(); + cy.get('[data-cy=kebab-icon]').should('be.visible').click(); + + //admin options + cy.get('[data-cy=context-menu]').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('API Details').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Copy to clipboard').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Edit project').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Move to').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('New project').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Open in new tab').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Open with 3rd party client').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Share').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Add to favorites').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Freeze project').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Add to public favorites').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Move to trash').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('View details').should('be.visible'); + + //create project + const projName = `Test project (${Math.floor(999999 * Math.random())})`; + cy.loginAs(activeUser); + + cy.get('[data-cy=side-panel-button]').click(); + cy.get('[data-cy=side-panel-new-project]').click(); + cy.get('[data-cy=form-dialog]') + .should('contain', 'New Project') + .within(() => { + cy.get('[data-cy=name-field]').within(() => { + cy.get('input').type(projName); + }); + }); + cy.get('[data-cy=form-submit-btn]').click(); + + cy.waitForDom(); + cy.get('[data-cy=kebab-icon]').should('be.visible').click({ force: true }); + + //active user options + cy.get('[data-cy=context-menu]').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('API Details').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Copy to clipboard').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Edit project').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Move to').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('New project').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Open in new tab').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Open with 3rd party client').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Share').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Add to favorites').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Freeze project').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('Move to trash').should('be.visible'); + cy.get('[data-cy=context-menu]').contains('View details').should('be.visible'); + }); + + it('should toggle description display', () => { + const projName = `Test project (${Math.floor(999999 * Math.random())})`; + const projDescription = 'Lorem ipsum dolor sit amet, consectetur adipiscing vultures, whose wings are dull realities.'; + cy.loginAs(adminUser); + + // Create project + cy.get('[data-cy=side-panel-button]').click(); + cy.get('[data-cy=side-panel-new-project]').click(); + cy.get('[data-cy=form-dialog]') + .should('contain', 'New Project') + .within(() => { + cy.get('[data-cy=name-field]').within(() => { + cy.get('input').type(projName); + }); + }); + cy.get('[data-cy=form-submit-btn]').click(); + + //check for no description + cy.get('[data-cy=no-description').should('be.visible'); + + //add description + cy.get('[data-cy=side-panel-tree]').contains('Home Projects').click(); + cy.get('[data-cy=project-panel] tbody tr').contains(projName).rightclick({ force: true }); + cy.get('[data-cy=context-menu]').contains('Edit').click(); + cy.get('[data-cy=form-dialog]').within(() => { + cy.get('div[contenteditable=true]').click().type(projDescription); + cy.get('[data-cy=form-submit-btn]').click(); + }); + cy.get('[data-cy=project-panel] tbody tr').contains(projName).click({ force: true }); + cy.get('[data-cy=project-details-card]').contains(projName).should('be.visible'); + + //toggle description + cy.get('[data-cy=toggle-description').click(); + cy.waitForDom(); + cy.get('[data-cy=project-description]').should('be.visible'); + cy.get('[data-cy=project-details-card]').contains(projDescription).should('be.visible'); + cy.get('[data-cy=toggle-description').click(); + cy.waitForDom(); + cy.get('[data-cy=project-description]').should('be.hidden'); + }); + + it('should display key/value pairs', () => { + const projName = `Test project (${Math.floor(999999 * Math.random())})`; + cy.loginAs(adminUser); + + // Create project wih key/value pairs + cy.get('[data-cy=side-panel-button]').click(); + cy.get('[data-cy=side-panel-new-project]').click(); + cy.get('[data-cy=form-dialog]') + .should('contain', 'New Project') + .within(() => { + cy.get('[data-cy=name-field]').within(() => { + cy.get('input').type(projName); + }); + }); + + cy.get('[data-cy=key-input]').should('be.visible').click().type('Animal'); + cy.get('[data-cy=value-input]').should('be.visible').click().type('Dog'); + cy.get('[data-cy=property-add-btn]').should('be.visible').click(); + + cy.get('[data-cy=key-input]').should('be.visible').click().type('Importance'); + cy.get('[data-cy=value-input]').should('be.visible').click().type('Critical'); + cy.get('[data-cy=property-add-btn]').should('be.visible').click(); + + cy.get('[data-cy=form-submit-btn]').click(); + + //check for key/value pairs in project details card + cy.get('[data-cy=project-details-card]').contains('Animal').should('be.visible'); + cy.get('[data-cy=project-details-card]').contains('Importance').should('be.visible').click(); + cy.waitForDom(); + cy.window().then((win) => + win.navigator.clipboard.readText().then((text) => { + expect(text).to.match(new RegExp(`Importance: Critical`)); + }) + ); + }); +}); diff --git a/services/workbench2/src/components/data-explorer/data-explorer.tsx b/services/workbench2/src/components/data-explorer/data-explorer.tsx index ba710bc783..4737ed06dc 100644 --- a/services/workbench2/src/components/data-explorer/data-explorer.tsx +++ b/services/workbench2/src/components/data-explorer/data-explorer.tsx @@ -17,13 +17,19 @@ import { CloseIcon, IconType, MaximizeIcon, UnMaximizeIcon, MoreVerticalIcon } f import { PaperProps } from "@material-ui/core/Paper"; import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view"; -type CssRules = "titleWrapper" | "searchBox" | "headerMenu" | "toolbar" | "footer" | "root" | "moreOptionsButton" | "title" | 'subProcessTitle' | "dataTable" | "container"; +type CssRules = "titleWrapper" | "msToolbarStyles" | "subpanelToolbarStyles" | "searchBox" | "headerMenu" | "toolbar" | "footer" | "root" | "moreOptionsButton" | "title" | 'subProcessTitle' | "dataTable" | "container"; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ titleWrapper: { display: "flex", justifyContent: "space-between", }, + msToolbarStyles: { + paddingTop: "0.6rem", + }, + subpanelToolbarStyles: { + paddingTop: "1.2rem", + }, searchBox: { paddingBottom: 0, }, @@ -37,6 +43,8 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ }, root: { height: "100%", + flex: 1, + overflowY: "auto", }, moreOptionsButton: { padding: 0, @@ -92,7 +100,8 @@ interface DataExplorerDataProps { title?: React.ReactNode; progressBar?: React.ReactNode; paperKey?: string; - currentItemUuid: string; + currentRouteUuid: string; + selectedResourceUuid: string; elementPath?: string; isMSToolbarVisible: boolean; checkedList: TCheckedList; @@ -114,6 +123,7 @@ interface DataExplorerActionProps { extractKey?: (item: T) => React.Key; toggleMSToolbar: (isVisible: boolean) => void; setCheckedListOnStore: (checkedList: TCheckedList) => void; + setSelectedUuid: (uuid: string) => void; } type DataExplorerProps = DataExplorerDataProps & DataExplorerActionProps & WithStyles & MPVPanelProps; @@ -155,7 +165,7 @@ export const DataExplorer = withStyles(styles)( hideSearchInput, paperKey, fetchMode, - currentItemUuid, + selectedResourceUuid, currentRoute, title, progressBar, @@ -194,7 +204,7 @@ export const DataExplorer = withStyles(styles)( )} {!!progressBar && progressBar} - {this.multiSelectToolbarInTitle && } + {this.multiSelectToolbarInTitle && } {(!hideColumnSelector || !hideSearchInput || !!actions) && ( )} - {!this.multiSelectToolbarInTitle && } + {!this.multiSelectToolbarInTitle && } diff --git a/services/workbench2/src/components/data-table/data-table.tsx b/services/workbench2/src/components/data-table/data-table.tsx index 7b78799457..e7a358580c 100644 --- a/services/workbench2/src/components/data-table/data-table.tsx +++ b/services/workbench2/src/components/data-table/data-table.tsx @@ -29,6 +29,7 @@ import { SvgIconProps } from "@material-ui/core/SvgIcon"; import ArrowDownwardIcon from "@material-ui/icons/ArrowDownward"; import { createTree } from "models/tree"; import { DataTableMultiselectOption } from "../data-table-multiselect-popover/data-table-multiselect-popover"; +import { isExactlyOneSelected } from "store/multiselect/multiselect-actions"; import { PendingIcon } from "components/icon/icon"; export type DataColumns = Array>; @@ -50,11 +51,13 @@ export interface DataTableDataProps { working?: boolean; defaultViewIcon?: IconType; defaultViewMessages?: string[]; - currentItemUuid?: string; - currentRoute?: string; toggleMSToolbar: (isVisible: boolean) => void; setCheckedListOnStore: (checkedList: TCheckedList) => void; + currentRoute?: string; + currentRouteUuid: string; checkedList: TCheckedList; + selectedResourceUuid: string; + setSelectedUuid: (uuid: string | null) => void; isNotFound?: boolean; } @@ -159,8 +162,9 @@ export const DataTable = withStyles(styles)( } componentDidUpdate(prevProps: Readonly>, prevState: DataTableState) { - const { items, setCheckedListOnStore } = this.props; + const { items, currentRouteUuid, setCheckedListOnStore } = this.props; const { isSelected } = this.state; + const singleSelected = isExactlyOneSelected(this.props.checkedList); if (prevProps.items !== items) { if (isSelected === true) this.setState({ isSelected: false }); if (items.length) this.initializeCheckedList(items); @@ -169,6 +173,15 @@ export const DataTable = withStyles(styles)( if (prevProps.currentRoute !== this.props.currentRoute) { this.initializeCheckedList([]) } + if (singleSelected && singleSelected !== isExactlyOneSelected(prevProps.checkedList)) { + this.props.setSelectedUuid(singleSelected); + } + if (!singleSelected && !!currentRouteUuid && !this.isAnySelected()) { + this.props.setSelectedUuid(currentRouteUuid); + } + if (!singleSelected && this.isAnySelected()) { + this.props.setSelectedUuid(null); + } if(prevProps.working === true && this.props.working === false) { this.setState({ isLoaded: true }); } @@ -220,11 +233,12 @@ export const DataTable = withStyles(styles)( initializeCheckedList = (uuids: any[]): void => { const newCheckedList = { ...this.props.checkedList }; - uuids.forEach(uuid => { - if (!newCheckedList.hasOwnProperty(uuid)) { - newCheckedList[uuid] = false; + if(Object.keys(newCheckedList).length === 0){ + for(const uuid of uuids){ + newCheckedList[uuid] = false } - }); + } + for (const key in newCheckedList) { if (!uuids.includes(key)) { delete newCheckedList[key]; @@ -412,7 +426,7 @@ export const DataTable = withStyles(styles)( ); renderBodyRow = (item: any, index: number) => { - const { onRowClick, onRowDoubleClick, extractKey, classes, currentItemUuid, currentRoute } = this.props; + const { onRowClick, onRowDoubleClick, extractKey, classes, selectedResourceUuid, currentRoute } = this.props; return ( onRowClick && onRowClick(event, item)} onContextMenu={this.handleRowContextMenu(item)} onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item)} - selected={item === currentItemUuid}> + selected={item === selectedResourceUuid}> {this.mapVisibleColumns((column, index) => ( = (theme: ArvadosTheme) => ({ + root: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '24px', + height: '24px', + cursor: 'pointer', + }, + default: { + transition: 'all 0.1s ease', + transform: 'rotate(0deg)', + }, + expanded: { + transition: 'all 0.1s ease', + transform: 'rotate(90deg)', + }, +}); + +export interface ExpandChevronRightDataProps { + expanded: boolean; +} + +type ExpandChevronRightProps = ExpandChevronRightDataProps & WithStyles; + +export const ExpandChevronRight = withStyles(styles)( + class extends React.Component { + render() { + const { classes, expanded } = this.props; + return ( +
+ +
+ ); + } + } +); \ No newline at end of file diff --git a/services/workbench2/src/components/multiselect-toolbar/MultiselectToolbar.tsx b/services/workbench2/src/components/multiselect-toolbar/MultiselectToolbar.tsx index 194950b134..0d74b6b38b 100644 --- a/services/workbench2/src/components/multiselect-toolbar/MultiselectToolbar.tsx +++ b/services/workbench2/src/components/multiselect-toolbar/MultiselectToolbar.tsx @@ -32,8 +32,9 @@ import { CollectionResource } from "models/collection"; import { getProcess } from "store/processes/process"; import { Process } from "store/processes/process"; import { PublicFavoritesState } from "store/public-favorites/public-favorites-reducer"; -import { isExactlyOneSelected } from "store/multiselect/multiselect-actions"; +import { AuthState } from "store/auth/auth-reducer"; import { IntersectionObserverWrapper } from "./ms-toolbar-overflow-wrapper"; +import classNames from "classnames"; import { ContextMenuKind, sortMenuItems, menuDirection } from 'views-components/context-menu/menu-item-sort'; type CssRules = "root" | "button" | "iconContainer" | "icon" | "divider"; @@ -43,7 +44,7 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ display: "flex", flexDirection: "row", width: 0, - height: '2.7rem', + height: '2.5rem', padding: 0, margin: "1rem auto auto 0.3rem", overflow: 'hidden', @@ -68,11 +69,15 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ export type MultiselectToolbarProps = { checkedList: TCheckedList; - singleSelectedUuid: string | null + selectedResourceUuid: string | null; iconProps: IconProps user: User | null disabledButtons: Set - executeMulti: (action: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void; + auth: AuthState; + location: string; + isSubPanel?: boolean; + injectedStyles?: string; + executeMulti: (action: ContextMenuAction | MultiSelectMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void; }; type IconProps = { @@ -81,13 +86,25 @@ type IconProps = { publicFavorites: PublicFavoritesState; } +const disallowedPaths = [ + "/favorites", + "/public-favorites", + "/trash", + "/group", +] + +const isPathDisallowed = (location: string): boolean => { + return disallowedPaths.some(path => location.includes(path)) +} + export const MultiselectToolbar = connect( mapStateToProps, mapDispatchToProps )( withStyles(styles)((props: MultiselectToolbarProps & WithStyles) => { - const { classes, checkedList, singleSelectedUuid, iconProps, user, disabledButtons } = props; - const singleResourceKind = singleSelectedUuid ? [resourceToMsResourceKind(singleSelectedUuid, iconProps.resources, user)] : null + const { classes, checkedList, iconProps, user, disabledButtons, location, isSubPanel, injectedStyles } = props; + const selectedResourceUuid = isPathDisallowed(location) ? null : props.selectedResourceUuid; + const singleResourceKind = selectedResourceUuid && !isSubPanel ? [resourceToMsResourceKind(selectedResourceUuid, iconProps.resources, user)] : null const currentResourceKinds = singleResourceKind ? singleResourceKind : Array.from(selectedToKindSet(checkedList)); const currentPathIsTrash = window.location.pathname === "/trash"; @@ -95,7 +112,7 @@ export const MultiselectToolbar = connect( currentPathIsTrash && selectedToKindSet(checkedList).size ? [msToggleTrashAction] : selectActionsByKind(currentResourceKinds as string[], multiselectActionsFilters).filter((action) => - singleSelectedUuid === null ? action.isForMulti : true + selectedResourceUuid === null ? action.isForMulti : true ); const actions: ContextMenuAction[] | MultiSelectMenuAction[] = sortMenuItems( @@ -104,6 +121,8 @@ export const MultiselectToolbar = connect( menuDirection.HORIZONTAL ); + const targetResources = selectedResourceUuid ? {[selectedResourceUuid]: true} as TCheckedList : checkedList + return ( @@ -137,10 +156,10 @@ export const MultiselectToolbar = connect( props.executeMulti(action, checkedList, iconProps.resources)} + onClick={() => props.executeMulti(action, targetResources, iconProps.resources)} className={classes.icon} > - {currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altIcon && altIcon({}) : icon({})} + {currentPathIsTrash || (useAlts && useAlts(selectedResourceUuid, iconProps)) ? altIcon && altIcon({}) : icon({})} @@ -155,7 +174,8 @@ export const MultiselectToolbar = connect( props.executeMulti(action, checkedList, iconProps.resources)} + onClick={() => { + props.executeMulti(action, targetResources, iconProps.resources)}} className={classes.icon} > {action.icon({})} @@ -214,7 +234,7 @@ const resourceToMsResourceKind = (uuid: string, resources: ResourcesState, user: const { isAdmin } = user; const kind = extractUuidKind(uuid); - const isFrozen = resourceIsFrozen(resource, resources); + const isFrozen = resource?.kind && resource.kind === ResourceKind.PROJECT ? resourceIsFrozen(resource, resources) : false; const isEditable = (user.isAdmin || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen; switch (kind) { @@ -259,7 +279,7 @@ const resourceToMsResourceKind = (uuid: string, resources: ResourcesState, user: ? ContextMenuKind.RUNNING_PROCESS_RESOURCE : ContextMenuKind.PROCESS_RESOURCE; case ResourceKind.USER: - return ContextMenuKind.ROOT_PROJECT; + return isAdmin ? ContextMenuKind.ROOT_PROJECT_ADMIN : ContextMenuKind.ROOT_PROJECT; case ResourceKind.LINK: return ContextMenuKind.LINK; case ResourceKind.WORKFLOW: @@ -305,12 +325,14 @@ function selectActionsByKind(currentResourceKinds: Array, filterSet: TMu //--------------------------------------------------// -function mapStateToProps({auth, multiselect, resources, favorites, publicFavorites}: RootState) { +function mapStateToProps({auth, multiselect, resources, favorites, publicFavorites, selectedResourceUuid}: RootState) { return { checkedList: multiselect.checkedList as TCheckedList, - singleSelectedUuid: isExactlyOneSelected(multiselect.checkedList), user: auth && auth.user ? auth.user : null, disabledButtons: new Set(multiselect.disabledButtons), + auth, + selectedResourceUuid, + location: window.location.pathname, iconProps: { resources, favorites, @@ -323,6 +345,7 @@ function mapDispatchToProps(dispatch: Dispatch) { return { executeMulti: (selectedAction: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState): void => { const kindGroups = groupByKind(checkedList, resources); + const currentList = selectedToArray(checkedList) switch (selectedAction.name) { case ContextMenuActionNames.MOVE_TO: case ContextMenuActionNames.REMOVE: diff --git a/services/workbench2/src/components/multiselect-toolbar/ms-kind-action-differentiator.ts b/services/workbench2/src/components/multiselect-toolbar/ms-kind-action-differentiator.ts index 5a84d4c573..1e49ae664f 100644 --- a/services/workbench2/src/components/multiselect-toolbar/ms-kind-action-differentiator.ts +++ b/services/workbench2/src/components/multiselect-toolbar/ms-kind-action-differentiator.ts @@ -8,16 +8,18 @@ import { msCollectionActionSet } from "views-components/multiselect-toolbar/ms-c import { msProjectActionSet } from "views-components/multiselect-toolbar/ms-project-action-set"; import { msProcessActionSet } from "views-components/multiselect-toolbar/ms-process-action-set"; import { msWorkflowActionSet } from "views-components/multiselect-toolbar/ms-workflow-action-set"; +import { UserDetailsActionSet } from "views-components/multiselect-toolbar/ms-user-details-action-set"; export function findActionByName(name: string, actionSet: MultiSelectMenuActionSet) { return actionSet[0].find(action => action.name === name); } -const { COLLECTION, PROCESS, PROJECT, WORKFLOW } = ResourceKind; +const { COLLECTION, PROCESS, PROJECT, WORKFLOW, USER } = ResourceKind; export const kindToActionSet: Record = { [COLLECTION]: msCollectionActionSet, [PROCESS]: msProcessActionSet, [PROJECT]: msProjectActionSet, [WORKFLOW]: msWorkflowActionSet, + [USER]: UserDetailsActionSet, }; 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..9af91daa53 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 @@ -15,6 +15,7 @@ import { } from 'views-components/multiselect-toolbar/ms-project-action-set'; import { msProcessActionSet, msCommonProcessActionFilter, msAdminProcessActionFilter, msRunningProcessActionFilter } from 'views-components/multiselect-toolbar/ms-process-action-set'; import { msWorkflowActionSet, msWorkflowActionFilter, msReadOnlyWorkflowActionFilter } from 'views-components/multiselect-toolbar/ms-workflow-action-set'; +import { UserDetailsActionSet } from 'views-components/multiselect-toolbar/ms-user-details-action-set'; import { ResourceKind } from 'models/resource'; import { ContextMenuKind } from 'views-components/context-menu/menu-item-sort'; @@ -27,7 +28,9 @@ const { RUNNING_PROCESS_ADMIN, PROCESS_ADMIN, PROJECT, + ROOT_PROJECT, PROJECT_ADMIN, + ROOT_PROJECT_ADMIN, FROZEN_PROJECT, FROZEN_PROJECT_ADMIN, READONLY_PROJECT, @@ -65,4 +68,7 @@ export const multiselectActionsFilters: TMultiselectActionsFilters = { [WORKFLOW]: [msWorkflowActionSet, msWorkflowActionFilter], [READONLY_WORKFLOW]: [msWorkflowActionSet, msReadOnlyWorkflowActionFilter], + + [ROOT_PROJECT]: [UserDetailsActionSet, allActionNames(UserDetailsActionSet)], + [ROOT_PROJECT_ADMIN]: [UserDetailsActionSet, allActionNames(UserDetailsActionSet)], }; diff --git a/services/workbench2/src/components/multiselect-toolbar/ms-toolbar-overflow-wrapper.tsx b/services/workbench2/src/components/multiselect-toolbar/ms-toolbar-overflow-wrapper.tsx index e0f32f1fa6..5c1c433712 100644 --- a/services/workbench2/src/components/multiselect-toolbar/ms-toolbar-overflow-wrapper.tsx +++ b/services/workbench2/src/components/multiselect-toolbar/ms-toolbar-overflow-wrapper.tsx @@ -46,9 +46,8 @@ export const IntersectionObserverWrapper = withStyles(styles)((props: WrapperPro const navRef = useRef(null); const [visibilityMap, setVisibilityMap] = useState>({}); const [numHidden, setNumHidden] = useState(() => findNumHidden(visibilityMap)); - const prevNumHidden = useRef(numHidden); - + const handleIntersection = (entries) => { const updatedEntries: Record = {}; entries.forEach((entry) => { diff --git a/services/workbench2/src/index.tsx b/services/workbench2/src/index.tsx index 400b975d4d..784d1ffdb0 100644 --- a/services/workbench2/src/index.tsx +++ b/services/workbench2/src/index.tsx @@ -77,6 +77,7 @@ import { keepServiceActionSet } from "views-components/context-menu/action-sets/ import { loadVocabulary } from "store/vocabulary/vocabulary-actions"; import { virtualMachineActionSet } from "views-components/context-menu/action-sets/virtual-machine-action-set"; import { userActionSet } from "views-components/context-menu/action-sets/user-action-set"; +import { UserDetailsActionSet } from "views-components/context-menu/action-sets/user-details-action-set"; import { apiClientAuthorizationActionSet } from "views-components/context-menu/action-sets/api-client-authorization-action-set"; import { groupActionSet } from "views-components/context-menu/action-sets/group-action-set"; import { groupMemberActionSet } from "views-components/context-menu/action-sets/group-member-action-set"; @@ -125,6 +126,7 @@ addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet); addMenuActionSet(ContextMenuKind.VIRTUAL_MACHINE, virtualMachineActionSet); addMenuActionSet(ContextMenuKind.KEEP_SERVICE, keepServiceActionSet); addMenuActionSet(ContextMenuKind.USER, userActionSet); +addMenuActionSet(ContextMenuKind.USER_DETAILS, UserDetailsActionSet); addMenuActionSet(ContextMenuKind.LINK, linkActionSet); addMenuActionSet(ContextMenuKind.API_CLIENT_AUTHORIZATION, apiClientAuthorizationActionSet); addMenuActionSet(ContextMenuKind.GROUPS, groupActionSet); diff --git a/services/workbench2/src/models/details.ts b/services/workbench2/src/models/details.ts index b6eabd7014..5bde295e9a 100644 --- a/services/workbench2/src/models/details.ts +++ b/services/workbench2/src/models/details.ts @@ -8,5 +8,6 @@ import { ProcessResource } from "./process"; import { EmptyResource } from "./empty"; import { CollectionFile, CollectionDirectory } from 'models/collection-file'; import { WorkflowResource } from 'models/workflow'; +import { UserResource } from "./user"; -export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource | CollectionFile | CollectionDirectory | WorkflowResource; +export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource | CollectionFile | CollectionDirectory | WorkflowResource | UserResource & {name?: string}; diff --git a/services/workbench2/src/store/details-panel/details-panel-action.ts b/services/workbench2/src/store/details-panel/details-panel-action.ts index e14c70ace7..ca2db7217f 100644 --- a/services/workbench2/src/store/details-panel/details-panel-action.ts +++ b/services/workbench2/src/store/details-panel/details-panel-action.ts @@ -15,6 +15,7 @@ import { CollectionResource } from 'models/collection'; import { extractUuidKind, ResourceKind } from 'models/resource'; export const SLIDE_TIMEOUT = 500; +export const CLOSE_DRAWER = 'CLOSE_DRAWER' export const detailsPanelActions = unionize({ TOGGLE_DETAILS_PANEL: ofType<{}>(), @@ -66,20 +67,26 @@ export const refreshCollectionVersionsList = (uuid: string) => ); }; -export const toggleDetailsPanel = () => (dispatch: Dispatch, getState: () => RootState) => { - // because of material-ui issue resizing details panel breaks tabs. - // triggering window resize event fixes that. - setTimeout(() => { - window.dispatchEvent(new Event('resize')); - }, SLIDE_TIMEOUT); - startDetailsPanelTransition(dispatch) - dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL()); - if (getState().detailsPanel.isOpened) { - dispatch(loadDetailsPanel(getState().detailsPanel.resourceUuid)); +export const toggleDetailsPanel = (uuid: string = '') => (dispatch: Dispatch, getState: () => RootState) => { + const { detailsPanel }= getState() + const isTargetUuidNew = uuid !== detailsPanel.resourceUuid + if(isTargetUuidNew && uuid !== CLOSE_DRAWER && detailsPanel.isOpened){ + dispatch(loadDetailsPanel(uuid)); + } else { + // because of material-ui issue resizing details panel breaks tabs. + // triggering window resize event fixes that. + setTimeout(() => { + window.dispatchEvent(new Event('resize')); + }, SLIDE_TIMEOUT); + startDetailsPanelTransition(dispatch) + dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL()); + if (getState().detailsPanel.isOpened) { + dispatch(loadDetailsPanel(isTargetUuidNew ? uuid : detailsPanel.resourceUuid)); + } } -}; - -const startDetailsPanelTransition = (dispatch) => { + }; + + const startDetailsPanelTransition = (dispatch) => { dispatch(detailsPanelActions.START_TRANSITION()) setTimeout(() => { dispatch(detailsPanelActions.END_TRANSITION()) diff --git a/services/workbench2/src/store/multiselect/multiselect-reducer.tsx b/services/workbench2/src/store/multiselect/multiselect-reducer.tsx index b488932f0b..b73a4be4bb 100644 --- a/services/workbench2/src/store/multiselect/multiselect-reducer.tsx +++ b/services/workbench2/src/store/multiselect/multiselect-reducer.tsx @@ -5,17 +5,15 @@ import { multiselectActionConstants } from "./multiselect-actions"; import { TCheckedList } from "components/data-table/data-table"; -type MultiselectToolbarState = { +export type MultiselectToolbarState = { isVisible: boolean; checkedList: TCheckedList; - selectedUuid: string; disabledButtons: string[]; }; const multiselectToolbarInitialState = { isVisible: false, checkedList: {}, - selectedUuid: '', disabledButtons: [] }; @@ -33,7 +31,7 @@ const toggleOneCheck = (inputList: TCheckedList, uuid: string)=>{ return { ...inputList, [uuid]: (checkedlist[uuid] && checkedlist[uuid] === true) && isOnlyOneSelected ? false : true }; } -const { TOGGLE_VISIBLITY, SET_CHECKEDLIST, SELECT_ONE, DESELECT_ONE, DESELECT_ALL_OTHERS, TOGGLE_ONE, SET_SELECTED_UUID, ADD_DISABLED, REMOVE_DISABLED } = multiselectActionConstants; +const { TOGGLE_VISIBLITY, SET_CHECKEDLIST, SELECT_ONE, DESELECT_ONE, DESELECT_ALL_OTHERS, TOGGLE_ONE, ADD_DISABLED, REMOVE_DISABLED } = multiselectActionConstants; export const multiselectReducer = (state: MultiselectToolbarState = multiselectToolbarInitialState, action) => { switch (action.type) { @@ -49,8 +47,6 @@ export const multiselectReducer = (state: MultiselectToolbarState = multiselectT return { ...state, checkedList: uncheckAllOthers(state.checkedList, action.payload) }; case TOGGLE_ONE: return { ...state, checkedList: toggleOneCheck(state.checkedList, action.payload) }; - case SET_SELECTED_UUID: - return {...state, selectedUuid: action.payload || ''} case ADD_DISABLED: return { ...state, disabledButtons: [...state.disabledButtons, action.payload]} case REMOVE_DISABLED: diff --git a/services/workbench2/src/store/project-panel/project-panel-middleware-service.ts b/services/workbench2/src/store/project-panel/project-panel-middleware-service.ts index e8d03dfcd7..61c89cd6a7 100644 --- a/services/workbench2/src/store/project-panel/project-panel-middleware-service.ts +++ b/services/workbench2/src/store/project-panel/project-panel-middleware-service.ts @@ -58,7 +58,7 @@ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService api.dispatch(dataExplorerActions.SET_IS_NOT_FOUND({ id: this.id, isNotFound: false })); if (!background) { api.dispatch(progressIndicatorActions.START_WORKING(this.getId())); } const response = await this.services.groupsService.contents(projectUuid, getParams(dataExplorer, !!isProjectTrashed)); - const resourceUuids = response.items.map(item => item.uuid); + const resourceUuids = [...response.items.map(item => item.uuid), projectUuid]; api.dispatch(updateFavorites(resourceUuids)); api.dispatch(updatePublicFavorites(resourceUuids)); api.dispatch(updateResources(response.items)); diff --git a/services/workbench2/src/store/projects/project-lock-actions.ts b/services/workbench2/src/store/projects/project-lock-actions.ts index cd72e35196..84cea43809 100644 --- a/services/workbench2/src/store/projects/project-lock-actions.ts +++ b/services/workbench2/src/store/projects/project-lock-actions.ts @@ -9,21 +9,29 @@ import { loadResource } from "store/resources/resources-actions"; import { RootState } from "store/store"; import { ContextMenuActionNames } from "views-components/context-menu/context-menu-action-set"; import { addDisabledButton, removeDisabledButton } from "store/multiselect/multiselect-actions"; +import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions"; export const freezeProject = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { dispatch(addDisabledButton(ContextMenuActionNames.FREEZE_PROJECT)) const userUUID = getState().auth.user!.uuid; - - const updatedProject = await services.projectService.update(uuid, { - frozenByUuid: userUUID, - }); - + let updatedProject; + + try { + updatedProject = await services.projectService.update(uuid, { + frozenByUuid: userUUID, + }); + } catch (e) { + console.error(e); + dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not freeze project', hideDuration: 4000, kind: SnackbarKind.ERROR })); + } + dispatch(projectPanelActions.REQUEST_ITEMS()); dispatch(loadResource(uuid, false)); dispatch(removeDisabledButton(ContextMenuActionNames.FREEZE_PROJECT)) return updatedProject; }; + export const unfreezeProject = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { dispatch(addDisabledButton(ContextMenuActionNames.FREEZE_PROJECT)) const updatedProject = await services.projectService.update(uuid, { diff --git a/services/workbench2/src/store/selected-resource/selected-resource-actions.ts b/services/workbench2/src/store/selected-resource/selected-resource-actions.ts new file mode 100644 index 0000000000..36f92224b3 --- /dev/null +++ b/services/workbench2/src/store/selected-resource/selected-resource-actions.ts @@ -0,0 +1,17 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +export const selectedResourceActions = { + SET_SELECTED_RESOURCE: 'SET_SELECTED_RESOURCE', +} + +type SelectedResourceAction = { + type: string; + payload: string | null; +}; + +export const setSelectedResourceUuid = (resourceUuid: string | null): SelectedResourceAction => ({ + type: selectedResourceActions.SET_SELECTED_RESOURCE, + payload: resourceUuid +}); diff --git a/services/workbench2/src/store/selected-resource/selected-resource-reducer.ts b/services/workbench2/src/store/selected-resource/selected-resource-reducer.ts new file mode 100644 index 0000000000..502fa264c1 --- /dev/null +++ b/services/workbench2/src/store/selected-resource/selected-resource-reducer.ts @@ -0,0 +1,14 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { selectedResourceActions } from "./selected-resource-actions"; + +type SelectedResourceState = string | null; + +export const selectedResourceReducer = (state: SelectedResourceState = null, action: any) => { + if (action.type === selectedResourceActions.SET_SELECTED_RESOURCE) { + return action.payload; + } + return state; +}; \ No newline at end of file diff --git a/services/workbench2/src/store/store.ts b/services/workbench2/src/store/store.ts index ee861f18be..4033166bb9 100644 --- a/services/workbench2/src/store/store.ts +++ b/services/workbench2/src/store/store.ts @@ -81,6 +81,7 @@ import { sidePanelReducer } from "./side-panel/side-panel-reducer"; import { bannerReducer } from "./banner/banner-reducer"; import { multiselectReducer } from "./multiselect/multiselect-reducer"; import { composeWithDevTools } from "redux-devtools-extension"; +import { selectedResourceReducer } from "./selected-resource/selected-resource-reducer"; declare global { interface Window { @@ -186,6 +187,7 @@ const createRootReducer = (services: ServiceRepository) => properties: propertiesReducer, resources: resourcesReducer, router: routerReducer, + selectedResourceUuid: selectedResourceReducer, snackbar: snackbarReducer, treePicker: treePickerReducer, treePickerSearch: treePickerSearchReducer, diff --git a/services/workbench2/src/store/users/users-actions.ts b/services/workbench2/src/store/users/users-actions.ts index 4c789dbeed..60b81b7ba4 100644 --- a/services/workbench2/src/store/users/users-actions.ts +++ b/services/workbench2/src/store/users/users-actions.ts @@ -136,16 +136,6 @@ export const openUserPanel = () => } }; -export const toggleIsAdmin = (uuid: string) => - async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const { resources } = getState(); - const data = getResource(uuid)(resources); - const isAdmin = data!.isAdmin; - const newActivity = await services.userService.update(uuid, { isAdmin: !isAdmin }); - dispatch(loadUsersPanel()); - return newActivity; - }; - export const loadUsersPanel = () => (dispatch: Dispatch) => { dispatch(userBindedActions.RESET_EXPLORER_SEARCH_VALUE()); @@ -156,6 +146,7 @@ export enum UserAccountStatus { ACTIVE = 'Active', INACTIVE = 'Inactive', SETUP = 'Setup', + OTHER = 'Other', } export const getUserAccountStatus = (state: RootState, uuid: string) => { diff --git a/services/workbench2/src/views-components/context-menu/action-sets/user-details-action-set.ts b/services/workbench2/src/views-components/context-menu/action-sets/user-details-action-set.ts new file mode 100644 index 0000000000..eb9d888fe1 --- /dev/null +++ b/services/workbench2/src/views-components/context-menu/action-sets/user-details-action-set.ts @@ -0,0 +1,37 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { ContextMenuActionSet } from 'views-components/context-menu/context-menu-action-set'; +import { AdvancedIcon, AttributesIcon, UserPanelIcon } from 'components/icon/icon'; +import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab'; +import { openUserAttributes } from 'store/users/users-actions'; +import { navigateToUserProfile } from 'store/navigation/navigation-action'; +import { needsUserProfileLink } from 'store/context-menu/context-menu-filters'; + +export const UserDetailsActionSet: ContextMenuActionSet = [ + [ + { + name: 'Attributes', + icon: AttributesIcon, + execute: (dispatch, resources) => { + dispatch(openUserAttributes(resources[0].uuid)); + }, + }, + { + name: 'API Details', + icon: AdvancedIcon, + execute: (dispatch, resources) => { + dispatch(openAdvancedTabDialog(resources[0].uuid)); + }, + }, + { + name: 'User Account', + icon: UserPanelIcon, + execute: (dispatch, resources) => { + dispatch(navigateToUserProfile(resources[0].uuid)); + }, + filters: [needsUserProfileLink], + }, + ], +]; diff --git a/services/workbench2/src/views-components/data-explorer/data-explorer.tsx b/services/workbench2/src/views-components/data-explorer/data-explorer.tsx index 643949a20e..034399b124 100644 --- a/services/workbench2/src/views-components/data-explorer/data-explorer.tsx +++ b/services/workbench2/src/views-components/data-explorer/data-explorer.tsx @@ -12,6 +12,7 @@ import { DataColumn } from "components/data-table/data-column"; import { DataColumns, TCheckedList } from "components/data-table/data-table"; import { DataTableFilters } from "components/data-table-filters/data-table-filters-tree"; import { toggleMSToolbar, setCheckedListOnStore } from "store/multiselect/multiselect-actions"; +import { setSelectedResourceUuid } from "store/selected-resource/selected-resource-actions"; interface Props { id: string; @@ -26,16 +27,12 @@ const mapStateToProps = ({ progressIndicator, dataExplorer, router, multiselect, const working = !!progressIndicator.some(p => p.id === id && p.working); const dataExplorerState = getDataExplorer(dataExplorer, id); const currentRoute = router.location ? router.location.pathname : ""; - const isDetailsResourceChecked = multiselect.checkedList[detailsPanel.resourceUuid] - const isOnlyOneSelected = Object.values(multiselect.checkedList).filter(x => x === true).length === 1; - const currentItemUuid = - currentRoute === '/workflows' ? properties.workflowPanelDetailsUuid : isDetailsResourceChecked && isOnlyOneSelected ? detailsPanel.resourceUuid : multiselect.selectedUuid; const isMSToolbarVisible = multiselect.isVisible; return { ...dataExplorerState, currentRoute: currentRoute, paperKey: currentRoute, - currentItemUuid, + currentRouteUuid: properties.currentRouteUuid, isMSToolbarVisible, checkedList: multiselect.checkedList, working, @@ -84,6 +81,10 @@ const mapDispatchToProps = () => { dispatch(setCheckedListOnStore(checkedList)); }, + setSelectedUuid: (uuid: string | null) => { + dispatch(setSelectedResourceUuid(uuid)); + }, + onRowClick, onRowDoubleClick, diff --git a/services/workbench2/src/views-components/data-explorer/renderers.tsx b/services/workbench2/src/views-components/data-explorer/renderers.tsx index 91b06c2b2f..bb49b6cf42 100644 --- a/services/workbench2/src/views-components/data-explorer/renderers.tsx +++ b/services/workbench2/src/views-components/data-explorer/renderers.tsx @@ -37,7 +37,6 @@ import { ResourceStatus as WorkflowStatus } from "views/workflow-panel/workflow- import { getUuidPrefix, openRunProcess } from "store/workflow-panel/workflow-panel-actions"; import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions"; import { getUserFullname, getUserDisplayName, User, UserResource } from "models/user"; -import { toggleIsAdmin } from "store/users/users-actions"; import { LinkClass, LinkResource } from "models/link"; import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from "store/navigation/navigation-action"; import { withResourceData } from "views-components/data-explorer/with-resources"; @@ -54,6 +53,18 @@ import { VirtualMachinesResource } from "models/virtual-machines"; import { CopyToClipboardSnackbar } from "components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar"; import { ProjectResource } from "models/project"; import { ProcessResource } from "models/process"; +import { ServiceRepository } from "services/services"; +import { loadUsersPanel } from "store/users/users-actions"; + +export const toggleIsAdmin = (uuid: string) => + async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const { resources } = getState(); + const data = getResource(uuid)(resources); + const isAdmin = data!.isAdmin; + const newActivity = await services.userService.update(uuid, { isAdmin: !isAdmin }); + dispatch(loadUsersPanel()); + return newActivity; + }; import { InlinePulser } from "components/loading/inline-pulser"; const renderName = (dispatch: Dispatch, item: GroupContentsResource) => { @@ -90,7 +101,7 @@ const renderName = (dispatch: Dispatch, item: GroupContentsResource) => { ); }; -const FrozenProject = (props: { item: ProjectResource }) => { +export const FrozenProject = (props: { item: ProjectResource }) => { const [fullUsername, setFullusername] = React.useState(null); const getFullName = React.useCallback(() => { if (props.item.frozenByUuid) { diff --git a/services/workbench2/src/views-components/details-panel/details-panel.tsx b/services/workbench2/src/views-components/details-panel/details-panel.tsx index 2653a21033..eb6e65d645 100644 --- a/services/workbench2/src/views-components/details-panel/details-panel.tsx +++ b/services/workbench2/src/views-components/details-panel/details-panel.tsx @@ -15,6 +15,7 @@ import { EmptyResource } from 'models/empty'; import { Dispatch } from "redux"; import { ResourceKind } from "models/resource"; import { ProjectDetails } from "./project-details"; +import { RootProjectDetails } from './root-project-details'; import { CollectionDetails } from "./collection-details"; import { ProcessDetails } from "./process-details"; import { EmptyDetails } from "./empty-details"; @@ -28,6 +29,7 @@ import { toggleDetailsPanel, SLIDE_TIMEOUT, openDetailsPanel } from 'store/detai import { FileDetails } from 'views-components/details-panel/file-details'; import { getNode } from 'models/tree'; import { resourceIsFrozen } from 'common/frozen-resources'; +import { CLOSE_DRAWER } from 'store/details-panel/details-panel-action'; type CssRules = 'root' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'tabContainer'; @@ -64,7 +66,7 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ const EMPTY_RESOURCE: EmptyResource = { kind: undefined, name: 'Projects' }; -const getItem = (res: DetailsResource): DetailsData => { +const getItem = (res: DetailsResource, pathName: string): DetailsData => { if ('kind' in res) { switch (res.kind) { case ResourceKind.PROJECT: @@ -75,19 +77,21 @@ const getItem = (res: DetailsResource): DetailsData => { return new ProcessDetails(res); case ResourceKind.WORKFLOW: return new WorkflowDetails(res); + case ResourceKind.USER: + if(pathName.includes('projects')) { + return new RootProjectDetails(res); + } + return new EmptyDetails(EMPTY_RESOURCE); default: - return new EmptyDetails(res); + return new EmptyDetails(res as EmptyResource); } } else { return new FileDetails(res); } }; -const mapStateToProps = ({ auth, detailsPanel, resources, collectionPanelFiles, multiselect, router }: RootState) => { - const isDetailsResourceChecked = multiselect.checkedList[detailsPanel.resourceUuid] - const currentRoute = router.location ? router.location.pathname : ""; - const currentItemUuid = isDetailsResourceChecked || currentRoute.includes('collections') ? detailsPanel.resourceUuid : multiselect.selectedUuid ? multiselect.selectedUuid : currentRoute.split('/')[2]; - const resource = getResource(currentItemUuid)(resources) as DetailsResource | undefined; +const mapStateToProps = ({ auth, detailsPanel, resources, collectionPanelFiles, selectedResourceUuid, properties, router }: RootState) => { + const resource = getResource(selectedResourceUuid ?? properties.currentRouteUuid)(resources) as DetailsResource | undefined; const file = resource ? undefined : getNode(detailsPanel.resourceUuid)(collectionPanelFiles); @@ -103,12 +107,13 @@ const mapStateToProps = ({ auth, detailsPanel, resources, collectionPanelFiles, isOpened: detailsPanel.isOpened, tabNr: detailsPanel.tabNr, res: resource || (file && file.value) || EMPTY_RESOURCE, + pathname: router.location ? router.location.pathname : "", }; }; const mapDispatchToProps = (dispatch: Dispatch) => ({ - onCloseDrawer: () => { - dispatch(toggleDetailsPanel()); + onCloseDrawer: (currentItemId) => { + dispatch(toggleDetailsPanel(currentItemId)); }, setActiveTab: (tabNr: number) => { dispatch(openDetailsPanel(undefined, tabNr)); @@ -116,13 +121,14 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ }); export interface DetailsPanelDataProps { - onCloseDrawer: () => void; + onCloseDrawer: (currentItemId) => void; setActiveTab: (tabNr: number) => void; authConfig: Config; isOpened: boolean; tabNr: number; res: DetailsResource; isFrozen: boolean; + pathname: string; } type DetailsPanelProps = DetailsPanelDataProps & WithStyles; @@ -162,8 +168,7 @@ export const DetailsPanel = withStyles(styles)( } renderContent() { - const { classes, onCloseDrawer, res, tabNr, authConfig } = this.props; - + const { classes, onCloseDrawer, res, tabNr, authConfig, pathname } = this.props; let shouldShowInlinePreview = false; if (!('kind' in res)) { shouldShowInlinePreview = isInlineFileUrlSafe( @@ -173,7 +178,7 @@ export const DetailsPanel = withStyles(styles)( ) || authConfig.clusterConfig.Collections.TrustAllContent; } - const item = getItem(res); + const item = getItem(res, pathname); return - + onCloseDrawer(CLOSE_DRAWER)}> diff --git a/services/workbench2/src/views-components/details-panel/root-project-details.tsx b/services/workbench2/src/views-components/details-panel/root-project-details.tsx new file mode 100644 index 0000000000..58c046b68f --- /dev/null +++ b/services/workbench2/src/views-components/details-panel/root-project-details.tsx @@ -0,0 +1,74 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import React from 'react'; +import { connect } from 'react-redux'; +import { ProjectsIcon } from 'components/icon/icon'; +import { formatDate } from 'common/formatters'; +import { DetailsData } from "./details-data"; +import { DetailsAttribute } from "components/details-attribute/details-attribute"; +import { withStyles, StyleRulesCallback, WithStyles } from '@material-ui/core'; +import { ArvadosTheme } from 'common/custom-theme'; +import { Dispatch } from 'redux'; +import { openProjectUpdateDialog, ProjectUpdateFormDialogData } from 'store/projects/project-update-actions'; +import { RootState } from 'store/store'; +import { ResourcesState } from 'store/resources/resources'; +import { UserResource } from 'models/user'; +import { UserResourceFullName } from 'views-components/data-explorer/renderers'; + +export class RootProjectDetails extends DetailsData { + getIcon(className?: string) { + return ; + } + + getDetails() { + return ; + } +} + +type CssRules = 'tag' | 'editIcon' | 'editButton'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + tag: { + marginRight: theme.spacing.unit / 2, + marginBottom: theme.spacing.unit / 2, + }, + editIcon: { + paddingRight: theme.spacing.unit / 2, + fontSize: '1.125rem', + }, + editButton: { + boxShadow: 'none', + padding: '2px 10px 2px 5px', + fontSize: '0.75rem' + }, +}); + +interface RootProjectDetailsComponentDataProps { + rootProject: any; +} + +const mapStateToProps = (state: RootState): { resources: ResourcesState } => { + return { + resources: state.resources + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + onClick: (prj: ProjectUpdateFormDialogData) => + () => dispatch(openProjectUpdateDialog(prj)), +}); + +type RootProjectDetailsComponentProps = RootProjectDetailsComponentDataProps & WithStyles; + +const RootProjectDetailsComponent = connect(mapStateToProps, mapDispatchToProps)( + withStyles(styles)( + ({ rootProject}: RootProjectDetailsComponentProps & { resources: ResourcesState }) =>
+ + + + + +
+ )); diff --git a/services/workbench2/src/views-components/main-content-bar/main-content-bar.tsx b/services/workbench2/src/views-components/main-content-bar/main-content-bar.tsx index 3f4de301f2..5999b4855a 100644 --- a/services/workbench2/src/views-components/main-content-bar/main-content-bar.tsx +++ b/services/workbench2/src/views-components/main-content-bar/main-content-bar.tsx @@ -34,6 +34,7 @@ interface MainContentBarProps { onRefreshPage: () => void; onDetailsPanelToggle: () => void; buttonVisible: boolean; + projectUuid: string; } const isButtonVisible = ({ router }: RootState) => { @@ -53,13 +54,18 @@ const isButtonVisible = ({ router }: RootState) => { Routes.matchFavoritesRoute(pathname); }; -const mapStateToProps = (state: RootState) => ({ - buttonVisible: isButtonVisible(state), - projectUuid: state.detailsPanel.resourceUuid, -}); +const mapStateToProps = (state: RootState) => { + const currentRoute = state.router.location?.pathname.split('/') || []; + const projectUuid = currentRoute[currentRoute.length - 1]; + + return { + buttonVisible: isButtonVisible(state), + projectUuid, + } +}; const mapDispatchToProps = () => (dispatch: Dispatch) => ({ - onDetailsPanelToggle: () => dispatch(toggleDetailsPanel()), + onDetailsPanelToggle: (uuid: string) => dispatch(toggleDetailsPanel(uuid)), onRefreshButtonClick: (id) => { dispatch(loadSidePanelTreeProjects(id)); } @@ -77,11 +83,11 @@ export const MainContentBar = connect(mapStateToProps, mapDispatchToProps)(withS }} />
- {props.buttonVisible && + {props.buttonVisible && + onClick={()=>props.onDetailsPanelToggle(props.projectUuid)}> } diff --git a/services/workbench2/src/views-components/multiselect-toolbar/ms-menu-actions.ts b/services/workbench2/src/views-components/multiselect-toolbar/ms-menu-actions.ts index 12840cdea2..d129a8c662 100644 --- a/services/workbench2/src/views-components/multiselect-toolbar/ms-menu-actions.ts +++ b/services/workbench2/src/views-components/multiselect-toolbar/ms-menu-actions.ts @@ -67,8 +67,8 @@ const msViewDetailsAction: MultiSelectMenuAction = { icon: DetailsIcon, hasAlts: false, isForMulti: false, - execute: (dispatch) => { - dispatch(toggleDetailsPanel()); + execute: (dispatch, resources) => { + dispatch(toggleDetailsPanel(resources[0].uuid)); }, }; diff --git a/services/workbench2/src/views-components/multiselect-toolbar/ms-user-details-action-set.ts b/services/workbench2/src/views-components/multiselect-toolbar/ms-user-details-action-set.ts new file mode 100644 index 0000000000..33698259c1 --- /dev/null +++ b/services/workbench2/src/views-components/multiselect-toolbar/ms-user-details-action-set.ts @@ -0,0 +1,41 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { AdvancedIcon, AttributesIcon, UserPanelIcon } from 'components/icon/icon'; +import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab'; +import { openUserAttributes } from 'store/users/users-actions'; +import { navigateToUserProfile } from 'store/navigation/navigation-action'; +import { MultiSelectMenuActionSet, MultiSelectMenuActionNames } from './ms-menu-actions'; + +export const UserDetailsActionSet: MultiSelectMenuActionSet= [ + [ + { + name: MultiSelectMenuActionNames.ATTRIBUTES, + icon: AttributesIcon, + hasAlts: false, + isForMulti: false, + execute: (dispatch, resources) => { + dispatch(openUserAttributes(resources[0].uuid)); + }, + }, + { + name: MultiSelectMenuActionNames.API_DETAILS, + icon: AdvancedIcon, + hasAlts: false, + isForMulti: false, + execute: (dispatch, resources) => { + dispatch(openAdvancedTabDialog(resources[0].uuid)); + }, + }, + { + name: MultiSelectMenuActionNames.USER_ACCOUNT, + icon: UserPanelIcon, + hasAlts: false, + isForMulti: false, + execute: (dispatch, resources) => { + dispatch(navigateToUserProfile(resources[0].uuid)); + }, + }, + ], +]; diff --git a/services/workbench2/src/views-components/project-details-card/project-details-card.tsx b/services/workbench2/src/views-components/project-details-card/project-details-card.tsx new file mode 100644 index 0000000000..49884e45de --- /dev/null +++ b/services/workbench2/src/views-components/project-details-card/project-details-card.tsx @@ -0,0 +1,380 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import React from 'react'; +import { StyleRulesCallback, Card, CardHeader, WithStyles, withStyles, Typography, CardContent, Tooltip, Collapse, Grid } from '@material-ui/core'; +import { ArvadosTheme } from 'common/custom-theme'; +import { RootState } from 'store/store'; +import { connect } from 'react-redux'; +import { getResource } from 'store/resources/resources'; +import { getPropertyChip } from '../resource-properties-form/property-chip'; +import { ProjectResource } from 'models/project'; +import { ResourceKind } from 'models/resource'; +import { UserResource } from 'models/user'; +import { UserResourceAccountStatus } from 'views-components/data-explorer/renderers'; +import { FavoriteStar, PublicFavoriteStar } from 'views-components/favorite-star/favorite-star'; +import { FreezeIcon } from 'components/icon/icon'; +import { Resource } from 'models/resource'; +import { Dispatch } from 'redux'; +import { loadDetailsPanel } from 'store/details-panel/details-panel-action'; +import { ExpandChevronRight } from 'components/expand-chevron-right/expand-chevron-right'; +import { MultiselectToolbar } from 'components/multiselect-toolbar/MultiselectToolbar'; +import { setSelectedResourceUuid } from 'store/selected-resource/selected-resource-actions'; +import { deselectAllOthers } from 'store/multiselect/multiselect-actions'; + +type CssRules = + | 'root' + | 'cardHeaderContainer' + | 'cardHeader' + | 'projectToolbar' + | 'descriptionToggle' + | 'showMore' + | 'noDescription' + | 'userNameContainer' + | 'cardContent' + | 'nameSection' + | 'namePlate' + | 'faveIcon' + | 'frozenIcon' + | 'accountStatusSection' + | 'chipSection' + | 'tag' + | 'description'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + root: { + width: '100%', + marginBottom: '1rem', + flex: '0 0 auto', + padding: 0, + minHeight: '3rem', + }, + showMore: { + cursor: 'pointer', + }, + noDescription: { + color: theme.palette.grey['600'], + fontStyle: 'italic', + padding: '0 0 0.5rem 1rem', + marginTop: '-0.5rem', + }, + userNameContainer: { + display: 'flex', + alignItems: 'center', + minHeight: '2.7rem', + }, + cardHeaderContainer: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + }, + cardHeader: { + minWidth: '30rem', + padding: '0.2rem 0.4rem 0.2rem 1rem', + }, + projectToolbar: { + //shows only the first 3 buttons + width: '12rem !important', + }, + descriptionToggle: { + display: 'flex', + flexDirection: 'row', + cursor: 'pointer', + paddingBottom: '0.5rem', + }, + cardContent: { + display: 'flex', + flexDirection: 'column', + paddingTop: 0, + paddingLeft: '0.1rem', + }, + nameSection: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + namePlate: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + margin: 0, + minHeight: '2.7rem', + }, + faveIcon: { + fontSize: '0.8rem', + margin: 'auto 0 1rem 0.3rem', + color: theme.palette.text.primary, + }, + frozenIcon: { + fontSize: '0.5rem', + marginLeft: '0.3rem', + height: '1rem', + color: theme.palette.text.primary, + }, + accountStatusSection: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + paddingLeft: '1rem', + }, + chipSection: { + marginBottom: '-2rem', + }, + tag: { + marginRight: '0.75rem', + marginBottom: '0.5rem', + }, + description: { + maxWidth: '95%', + marginTop: 0, + }, +}); + +const mapStateToProps = ({ auth, selectedResourceUuid, resources, properties }: RootState) => { + const currentResource = getResource(properties.currentRouteUuid)(resources); + const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(resources); + const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName; + const isSelected = selectedResourceUuid === properties.currentRouteUuid; + + return { + isAdmin: auth.user?.isAdmin, + currentResource, + frozenByFullName, + isSelected, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + handleCardClick: (uuid: string) => { + dispatch(loadDetailsPanel(uuid)); + dispatch(setSelectedResourceUuid(uuid)); + dispatch(deselectAllOthers(uuid)); + }, + +}); + +type DetailsCardProps = WithStyles & { + currentResource: ProjectResource | UserResource; + frozenByFullName?: string; + isAdmin: boolean; + isSelected: boolean; + handleCardClick: (resource: any) => void; +}; + +type UserCardProps = WithStyles & { + currentResource: UserResource; + isAdmin: boolean; + isSelected: boolean; + handleCardClick: (resource: any) => void; +}; + +type ProjectCardProps = WithStyles & { + currentResource: ProjectResource; + frozenByFullName: string | undefined; + isAdmin: boolean; + isSelected: boolean; + handleCardClick: (resource: any) => void; +}; + +export const ProjectDetailsCard = connect( + mapStateToProps, + mapDispatchToProps +)( + withStyles(styles)((props: DetailsCardProps) => { + const { classes, currentResource, frozenByFullName, handleCardClick, isAdmin, isSelected } = props; + if (!currentResource) { + return null; + } + switch (currentResource.kind as string) { + case ResourceKind.USER: + return ( + + ); + case ResourceKind.PROJECT: + return ( + + ); + default: + return null; + } + }) +); + +const UserCard: React.FC = ({ classes, currentResource, handleCardClick, isSelected }) => { + const { fullName, uuid } = currentResource as UserResource & { fullName: string }; + + return ( + handleCardClick(uuid)} + data-cy='user-details-card' + > + + + + {fullName} + +
+ {!currentResource.isActive && ( + + + + )} +
+ + } + /> + {isSelected && } +
+
+ ); +}; + +const ProjectCard: React.FC = ({ classes, currentResource, frozenByFullName, handleCardClick, isSelected }) => { + const { name, description, uuid } = currentResource as ProjectResource; + const [showDescription, setShowDescription] = React.useState(false); + const [showProperties, setShowProperties] = React.useState(false); + + const toggleDescription = () => { + setShowDescription(!showDescription); + }; + + const toggleProperties = () => { + setShowProperties(!showProperties); + }; + + return ( + handleCardClick(uuid)} + data-cy='project-details-card' + > + + +
+ + {name} + + + + {!!frozenByFullName && ( + Project was frozen by {frozenByFullName}} + > + + + )} +
+ + } + /> + {isSelected && } +
+
ev.stopPropagation()}> + {description ? ( +
+ +
+ + + +
+
+ ) : ( + + no description available + + )} + {typeof currentResource.properties === 'object' && Object.keys(currentResource.properties).length > 0 ? ( +
+ +
+ +
+ + + {Object.keys(currentResource.properties).map((k) => + Array.isArray(currentResource.properties[k]) + ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag)) + : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag) + )} + + +
+
+
+
+ ) : null} +
+
+ ); +}; diff --git a/services/workbench2/src/views-components/resource-properties-form/property-chip.tsx b/services/workbench2/src/views-components/resource-properties-form/property-chip.tsx index 24b5c0a96d..cfdae3fe83 100644 --- a/services/workbench2/src/views-components/resource-properties-form/property-chip.tsx +++ b/services/workbench2/src/views-components/resource-properties-form/property-chip.tsx @@ -43,10 +43,12 @@ export const PropertyChipComponent = connect(mapStateToProps, mapDispatchToProps ({ propKey, propValue, vocabulary, className, onCopy, onDelete }: PropertyChipComponentProps) => { const label = `${getTagKeyLabel(propKey, vocabulary)}: ${getTagValueLabel(propKey, propValue, vocabulary)}`; return ( - onCopy("Copied to clipboard")}> - - + ev.stopPropagation()}> + onCopy("Copied to clipboard")}> + + + ); } ); diff --git a/services/workbench2/src/views-components/resource-properties-form/resource-properties-form.tsx b/services/workbench2/src/views-components/resource-properties-form/resource-properties-form.tsx index 0147312912..df23aeeb89 100644 --- a/services/workbench2/src/views-components/resource-properties-form/resource-properties-form.tsx +++ b/services/workbench2/src/views-components/resource-properties-form/resource-properties-form.tsx @@ -42,10 +42,12 @@ export const ResourcePropertiesForm = connect(mapStateToProps)(({ handleSubmit, const propertyValue = applySelector(formValueSelector(props.form)); return
- + - + diff --git a/services/workbench2/src/views-components/side-panel-tree/side-panel-tree.tsx b/services/workbench2/src/views-components/side-panel-tree/side-panel-tree.tsx index f338687de9..cc1b57da48 100644 --- a/services/workbench2/src/views-components/side-panel-tree/side-panel-tree.tsx +++ b/services/workbench2/src/views-components/side-panel-tree/side-panel-tree.tsx @@ -16,6 +16,7 @@ import { noop } from 'lodash'; import { ResourceKind } from "models/resource"; import { IllegalNamingWarning } from "components/warning/warning"; import { GroupClass } from "models/group"; +import { setSelectedResourceUuid } from "store/selected-resource/selected-resource-actions"; export interface SidePanelTreeProps { onItemActivation: (id: string) => void; @@ -32,6 +33,8 @@ const mapDispatchToProps = (dispatch: Dispatch, props: SidePanelTreeProps): Side }, toggleItemActive: (_, { id }) => { dispatch(activateSidePanelTreeItem(id)); + const isSidePanelCat = Object.values(SidePanelTreeCategory).includes(id as SidePanelTreeCategory); + dispatch(setSelectedResourceUuid(isSidePanelCat ? null : id)); props.onItemActivation(id); }, toggleItemOpen: (_, { id }) => { diff --git a/services/workbench2/src/views/main-panel/main-panel-root.tsx b/services/workbench2/src/views/main-panel/main-panel-root.tsx index cdfd0c300f..9654f4eb27 100644 --- a/services/workbench2/src/views/main-panel/main-panel-root.tsx +++ b/services/workbench2/src/views/main-panel/main-panel-root.tsx @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 -import React from 'react'; +import React, { useEffect } from 'react'; import { StyleRulesCallback, WithStyles, withStyles, Grid, LinearProgress } from '@material-ui/core'; import { User } from "models/user"; import { ArvadosTheme } from 'common/custom-theme'; @@ -11,6 +11,7 @@ import { LoginPanel } from 'views/login-panel/login-panel'; import { InactivePanel } from 'views/inactive-panel/inactive-panel'; import { WorkbenchLoadingScreen } from 'views/workbench/workbench-loading-screen'; import { MainAppBar } from 'views-components/main-app-bar/main-app-bar'; +import { Routes } from 'routes/routes'; type CssRules = 'root'; @@ -35,10 +36,12 @@ export interface MainPanelRootDataProps { sidePanelIsCollapsed: boolean; isTransitioning: boolean; currentSideWidth: number; + currentRoute: string; } interface MainPanelRootDispatchProps { - toggleSidePanel: () => void + toggleSidePanel: () => void, + setCurrentRouteUuid: (uuid: string | null) => void; } type MainPanelRootProps = MainPanelRootDataProps & MainPanelRootDispatchProps & WithStyles; @@ -46,7 +49,19 @@ type MainPanelRootProps = MainPanelRootDataProps & MainPanelRootDispatchProps & export const MainPanelRoot = withStyles(styles)( ({ classes, loading, working, user, buildInfo, uuidPrefix, isNotLinking, isLinkingPath, siteBanner, sessionIdleTimeout, - sidePanelIsCollapsed, isTransitioning, currentSideWidth}: MainPanelRootProps) =>{ + sidePanelIsCollapsed, isTransitioning, currentSideWidth, currentRoute, setCurrentRouteUuid}: MainPanelRootProps) =>{ + + useEffect(() => { + const splitRoute = currentRoute.split('/'); + const uuid = splitRoute[splitRoute.length - 1]; + if(Object.values(Routes).includes(`/${uuid}`) === false) { + setCurrentRouteUuid(uuid); + } else { + setCurrentRouteUuid(null); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentRoute]); + return loading ? : <> diff --git a/services/workbench2/src/views/main-panel/main-panel.tsx b/services/workbench2/src/views/main-panel/main-panel.tsx index 264390a8b3..556ce0d75c 100644 --- a/services/workbench2/src/views/main-panel/main-panel.tsx +++ b/services/workbench2/src/views/main-panel/main-panel.tsx @@ -11,6 +11,7 @@ import { isWorkbenchLoading } from 'store/workbench/workbench-actions'; import { LinkAccountPanelStatus } from 'store/link-account-panel/link-account-panel-reducer'; import { matchLinkAccountRoute } from 'routes/routes'; import { toggleSidePanel } from "store/side-panel/side-panel-action"; +import { propertiesActions } from 'store/properties/properties-actions'; const mapStateToProps = (state: RootState): MainPanelRootDataProps => { return { @@ -25,7 +26,8 @@ const mapStateToProps = (state: RootState): MainPanelRootDataProps => { sessionIdleTimeout: parse(state.auth.config.clusterConfig.Workbench.IdleTimeout, 's') || 0, sidePanelIsCollapsed: state.sidePanel.collapsedState, isTransitioning: state.detailsPanel.isTransitioning, - currentSideWidth: state.sidePanel.currentSideWidth + currentSideWidth: state.sidePanel.currentSideWidth, + currentRoute: state.router.location ? state.router.location.pathname : '', }; }; @@ -33,7 +35,9 @@ const mapDispatchToProps = (dispatch) => { return { toggleSidePanel: (collapsedState)=>{ return dispatch(toggleSidePanel(collapsedState)) - } + }, + setCurrentRouteUuid: (uuid: string) => { + return dispatch(propertiesActions.SET_PROPERTY({key: 'currentRouteUuid', value: uuid}))} } }; diff --git a/services/workbench2/src/views/project-panel/project-panel.tsx b/services/workbench2/src/views/project-panel/project-panel.tsx index 2ddfca8178..0425335e42 100644 --- a/services/workbench2/src/views/project-panel/project-panel.tsx +++ b/services/workbench2/src/views/project-panel/project-panel.tsx @@ -52,12 +52,15 @@ import { CollectionResource } from 'models/collection'; import { resourceIsFrozen } from 'common/frozen-resources'; import { ProjectResource } from 'models/project'; import { deselectAllOthers, toggleOne } from 'store/multiselect/multiselect-actions'; +import { ProjectDetailsCard } from 'views-components/project-details-card/project-details-card'; type CssRules = 'root' | 'button' ; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ root: { width: '100%', + display: 'flex', + flexDirection: 'column', }, button: { marginLeft: theme.spacing.unit, @@ -266,6 +269,7 @@ export const ProjectPanel = withStyles(styles)( render() { const { classes } = this.props; return
+
}