import { ArvadosTheme } from "common/custom-theme";
import { createTree } from 'models/tree';
import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
-import { MoreOptionsIcon } from 'components/icon/icon';
+import { CloseIcon, MaximizeIcon, MoreOptionsIcon } from 'components/icon/icon';
import { PaperProps } from '@material-ui/core/Paper';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
-type CssRules = 'searchBox' | "toolbar" | "toolbarUnderTitle" | "footer" | "root" | 'moreOptionsButton' | 'title';
+type CssRules = 'searchBox' | "toolbar" | "toolbarUnderTitle" | "footer" | "root" | 'moreOptionsButton' | 'title' | 'dataTable' | 'container';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
searchBox: {
paddingBottom: theme.spacing.unit * 2
},
toolbar: {
- paddingTop: theme.spacing.unit * 2
+ paddingTop: theme.spacing.unit,
+ paddingRight: theme.spacing.unit * 2,
},
toolbarUnderTitle: {
paddingTop: 0
overflow: 'auto'
},
root: {
- height: '100%'
+ height: '100%',
},
moreOptionsButton: {
padding: 0
paddingLeft: theme.spacing.unit * 3,
paddingTop: theme.spacing.unit * 3,
fontSize: '18px'
- }
+ },
+ dataTable: {
+ height: '100%',
+ overflow: 'auto',
+ },
+ container: {
+ height: '100%',
+ },
});
interface DataExplorerDataProps<T> {
extractKey?: (item: T) => React.Key;
}
-type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules>;
+type DataExplorerProps<T> = DataExplorerDataProps<T> &
+ DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
export const DataExplorer = withStyles(styles)(
class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
+
componentDidMount() {
if (this.props.onSetColumns) {
this.props.onSetColumns(this.props.columns);
}
}
+
render() {
const {
columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
- paperKey, fetchMode, currentItemUuid, title
+ paperKey, fetchMode, currentItemUuid, title,
+ doHidePanel, doMaximizePanel, panelName, panelMaximized
} = this.props;
- return <Paper className={classes.root} {...paperProps} key={paperKey}>
+
- {title && <div className={classes.title}>{title}</div>}
- {(!hideColumnSelector || !hideSearchInput || !!actions) && <Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
+ const dataCy = this.props["data-cy"];
+ return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={dataCy}>
- {(!hideColumnSelector || !hideSearchInput) && <Grid item xs><Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
+ <Grid container direction="column" wrap="nowrap" className={classes.container}>
+ {title && <Grid item xs className={classes.title}>{title}</Grid>}
++ {(!hideColumnSelector || !hideSearchInput || !!actions) && <Grid item xs><Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
<Grid container justify="space-between" wrap="nowrap" alignItems="center">
- <div className={classes.searchBox}>
+ {!hideSearchInput && <div className={classes.searchBox}>
{!hideSearchInput && <SearchInput
label={searchLabel}
value={searchValue}
+ selfClearProp={currentItemUuid}
onSearch={onSearch} />}
- </div>
+ </div>}
{actions}
{!hideColumnSelector && <ColumnSelector
columns={columns}
onColumnToggle={onColumnToggle} />}
</Grid>
- </Toolbar>}
- <DataTable
+ { doMaximizePanel && !panelMaximized &&
+ <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
+ <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
+ </Tooltip> }
+ { doHidePanel &&
+ <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
+ <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
+ </Tooltip> }
+ </Toolbar></Grid>}
+ <Grid item xs="auto" className={classes.dataTable}><DataTable
columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
items={items}
onRowClick={(_, item: T) => onRowClick(item)}
working={working}
defaultView={dataTableDefaultView}
currentItemUuid={currentItemUuid}
- currentRoute={paperKey} />
- <Toolbar className={classes.footer}>
+ currentRoute={paperKey} /></Grid>
+ <Grid item xs><Toolbar className={classes.footer}>
<Grid container justify="flex-end">
{fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
count={itemsAvailable}
onClick={this.loadMore}
>Load more</Button>}
</Grid>
- </Toolbar>
+ </Toolbar></Grid>
+ </Grid>
</Paper>;
}
import Star from '@material-ui/icons/Star';
import StarBorder from '@material-ui/icons/StarBorder';
import Warning from '@material-ui/icons/Warning';
+import Visibility from '@material-ui/icons/Visibility';
+import VisibilityOff from '@material-ui/icons/VisibilityOff';
import VpnKey from '@material-ui/icons/VpnKey';
import LinkOutlined from '@material-ui/icons/LinkOutlined';
+ import RemoveRedEye from '@material-ui/icons/RemoveRedEye';
+ import Computer from '@material-ui/icons/Computer';
// Import FontAwesome icons
import { library } from '@fortawesome/fontawesome-svg-core';
- import { faPencilAlt, faSlash } from '@fortawesome/free-solid-svg-icons';
+ import { faPencilAlt, faSlash, faUsers } from '@fortawesome/free-solid-svg-icons';
+import { CropFreeSharp } from '@material-ui/icons';
library.add(
faPencilAlt,
faSlash,
+ faUsers,
);
export const ReadOnlyIcon = (props: any) =>
</div>
</span>;
+ export const GroupsIcon = (props: any) =>
+ <span {...props}>
+ <span className="fas fa-users" />
+ </span>;
+
export const CollectionOldVersionIcon = (props: any) =>
<Tooltip title='Old version'>
<Badge badgeContent={<History fontSize='small' />}>
export const HelpIcon: IconType = (props) => <Help {...props} />;
export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
export const ImportContactsIcon: IconType = (props) => <ImportContacts {...props} />;
+export const InfoIcon: IconType = (props) => <Info {...props} />;
export const InputIcon: IconType = (props) => <InsertDriveFile {...props} />;
export const KeyIcon: IconType = (props) => <VpnKey {...props} />;
export const LogIcon: IconType = (props) => <SettingsEthernet {...props} />;
export const MailIcon: IconType = (props) => <Mail {...props} />;
+export const MaximizeIcon: IconType = (props) => <CropFreeSharp {...props} />;
export const MoreOptionsIcon: IconType = (props) => <MoreVert {...props} />;
export const MoveToIcon: IconType = (props) => <Input {...props} />;
export const NewProjectIcon: IconType = (props) => <CreateNewFolder {...props} />;
export const TrashIcon: IconType = (props) => <Delete {...props} />;
export const UserPanelIcon: IconType = (props) => <Person {...props} />;
export const UsedByIcon: IconType = (props) => <Folder {...props} />;
+export const VisibleIcon: IconType = (props) => <Visibility {...props} />;
+export const InvisibleIcon: IconType = (props) => <VisibilityOff {...props} />;
export const WorkflowIcon: IconType = (props) => <Code {...props} />;
export const WarningIcon: IconType = (props) => <Warning style={{ color: '#fbc02d', height: '30px', width: '30px' }} {...props} />;
export const Link: IconType = (props) => <LinkOutlined {...props} />;
export const FolderSharedIcon: IconType = (props) => <FolderShared {...props} />;
+ export const CanReadIcon: IconType = (props) => <RemoveRedEye {...props} />;
+ export const CanWriteIcon: IconType = (props) => <Edit {...props} />;
+ export const CanManageIcon: IconType = (props) => <Computer {...props} />;
import { DataExplorer } from "views-components/data-explorer/data-explorer";
import { DataColumns } from 'components/data-table/data-table';
- import { ResourceUuid, ResourceFirstName, ResourceLastName, ResourceEmail, ResourceUsername } from 'views-components/data-explorer/renderers';
+ import { ResourceLinkHeadUuid, ResourceLinkTailUsername, ResourceLinkHeadPermissionLevel, ResourceLinkTailPermissionLevel, ResourceLinkHead, ResourceLinkTail, ResourceLinkDelete, ResourceLinkTailIsActive, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
import { createTree } from 'models/tree';
import { noop } from 'lodash/fp';
import { RootState } from 'store/store';
- import { GROUP_DETAILS_PANEL_ID, openAddGroupMembersDialog } from 'store/group-details-panel/group-details-panel-actions';
+ import { GROUP_DETAILS_MEMBERS_PANEL_ID, GROUP_DETAILS_PERMISSIONS_PANEL_ID, openAddGroupMembersDialog, getCurrentGroupDetailsPanelUuid } from 'store/group-details-panel/group-details-panel-actions';
import { openContextMenu } from 'store/context-menu/context-menu-actions';
import { ResourcesState, getResource } from 'store/resources/resources';
- import { ContextMenuKind } from 'views-components/context-menu/context-menu';
- import { PermissionResource } from 'models/permission';
- import { Grid, Button } from '@material-ui/core';
-import { Grid, Button, Tabs, Tab, Paper } from '@material-ui/core';
++import { Grid, Button, Tabs, Tab, Paper, WithStyles, withStyles, StyleRulesCallback } from '@material-ui/core';
import { AddIcon } from 'components/icon/icon';
- export enum GroupDetailsPanelColumnNames {
- FIRST_NAME = "First name",
- LAST_NAME = "Last name",
- UUID = "UUID",
- EMAIL = "Email",
+ import { getUserUuid } from 'common/getuser';
+ import { GroupResource, isBuiltinGroup } from 'models/group';
++import { ArvadosTheme } from 'common/custom-theme';
+
++type CssRules = "root";
++
++const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
++ root: {
++ width: '100%',
++ }
++});
+
+ export enum GroupDetailsPanelMembersColumnNames {
+ FULL_NAME = "Name",
USERNAME = "Username",
+ ACTIVE = "User Active",
+ VISIBLE = "Visible to other members",
+ PERMISSION = "Permission",
+ REMOVE = "Remove",
+ }
+
+ export enum GroupDetailsPanelPermissionsColumnNames {
+ NAME = "Name",
+ PERMISSION = "Permission",
+ UUID = "UUID",
+ REMOVE = "Remove",
}
- export const groupDetailsPanelColumns: DataColumns<string> = [
+ export const groupDetailsMembersPanelColumns: DataColumns<string> = [
{
- name: GroupDetailsPanelColumnNames.FIRST_NAME,
+ name: GroupDetailsPanelMembersColumnNames.FULL_NAME,
selected: true,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceFirstName uuid={uuid} />
+ render: uuid => <ResourceLinkTail uuid={uuid} />
},
{
- name: GroupDetailsPanelColumnNames.LAST_NAME,
+ name: GroupDetailsPanelMembersColumnNames.USERNAME,
selected: true,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceLastName uuid={uuid} />
+ render: uuid => <ResourceLinkTailUsername uuid={uuid} />
},
{
- name: GroupDetailsPanelColumnNames.UUID,
+ name: GroupDetailsPanelMembersColumnNames.ACTIVE,
selected: true,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceUuid uuid={uuid} />
+ render: uuid => <ResourceLinkTailIsActive uuid={uuid} disabled={true} />
},
{
- name: GroupDetailsPanelColumnNames.EMAIL,
+ name: GroupDetailsPanelMembersColumnNames.VISIBLE,
selected: true,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceEmail uuid={uuid} />
+ render: uuid => <ResourceLinkTailIsVisible uuid={uuid} />
},
{
- name: GroupDetailsPanelColumnNames.USERNAME,
+ name: GroupDetailsPanelMembersColumnNames.PERMISSION,
selected: true,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceUsername uuid={uuid} />
+ render: uuid => <ResourceLinkTailPermissionLevel uuid={uuid} />
+ },
+ {
+ name: GroupDetailsPanelMembersColumnNames.REMOVE,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceLinkDelete uuid={uuid} />
+ },
+ ];
+
+ export const groupDetailsPermissionsPanelColumns: DataColumns<string> = [
+ {
+ name: GroupDetailsPanelPermissionsColumnNames.NAME,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceLinkHead uuid={uuid} />
+ },
+ {
+ name: GroupDetailsPanelPermissionsColumnNames.PERMISSION,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceLinkHeadPermissionLevel uuid={uuid} />
+ },
+ {
+ name: GroupDetailsPanelPermissionsColumnNames.UUID,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceLinkHeadUuid uuid={uuid} />
+ },
+ {
+ name: GroupDetailsPanelPermissionsColumnNames.REMOVE,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceLinkDelete uuid={uuid} />
},
];
const mapStateToProps = (state: RootState) => {
+ const groupUuid = getCurrentGroupDetailsPanelUuid(state.properties);
+ const group = getResource<GroupResource>(groupUuid || '')(state.resources);
+ const userUuid = getUserUuid(state);
+
return {
- resources: state.resources
+ resources: state.resources,
+ groupCanManage: userUuid && !isBuiltinGroup(group?.uuid || '')
+ ? group?.writableBy?.includes(userUuid)
+ : false,
};
};
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
onAddUser: () => void;
resources: ResourcesState;
+ groupCanManage: boolean;
}
--export const GroupDetailsPanel = connect(
++export const GroupDetailsPanel = withStyles(styles)(connect(
mapStateToProps, mapDispatchToProps
)(
-- class GroupDetailsPanel extends React.Component<GroupDetailsPanelProps> {
++ class GroupDetailsPanel extends React.Component<GroupDetailsPanelProps & WithStyles<CssRules>> {
+ state = {
+ value: 0,
+ };
+
+ componentDidMount() {
+ this.setState({ value: 0 });
+ }
render() {
+ const { value } = this.state;
return (
- <DataExplorer
- id={GROUP_DETAILS_PANEL_ID}
- onRowClick={noop}
- onRowDoubleClick={noop}
- onContextMenu={this.handleContextMenu}
- contextMenuColumn={true}
- hideColumnSelector
- hideSearchInput
- actions={
- <Grid container justify='flex-end'>
- <Button
- variant="contained"
- color="primary"
- onClick={this.props.onAddUser}>
- <AddIcon /> Add user
- </Button>
- </Grid>
- } />
- <Paper>
++ <Paper className={this.props.classes.root}>
+ <Tabs value={value} onChange={this.handleChange} variant="fullWidth">
+ <Tab data-cy="group-details-members-tab" label="MEMBERS" />
+ <Tab data-cy="group-details-permissions-tab" label="PERMISSIONS" />
+ </Tabs>
+ {value === 0 &&
+ <DataExplorer
+ id={GROUP_DETAILS_MEMBERS_PANEL_ID}
+ data-cy="group-members-data-explorer"
+ onRowClick={noop}
+ onRowDoubleClick={noop}
+ onContextMenu={noop}
+ contextMenuColumn={false}
+ hideColumnSelector
+ hideSearchInput
+ actions={
+ this.props.groupCanManage &&
+ <Grid container justify='flex-end'>
+ <Button
+ data-cy="group-member-add"
+ variant="contained"
+ color="primary"
+ onClick={this.props.onAddUser}>
+ <AddIcon /> Add user
+ </Button>
+ </Grid>
+ }
+ paperProps={{
+ elevation: 0,
+ }} />
+ }
+ {value === 1 &&
+ <DataExplorer
+ id={GROUP_DETAILS_PERMISSIONS_PANEL_ID}
+ data-cy="group-permissions-data-explorer"
+ onRowClick={noop}
+ onRowDoubleClick={noop}
+ onContextMenu={noop}
+ contextMenuColumn={false}
+ hideColumnSelector
+ hideSearchInput
+ paperProps={{
+ elevation: 0,
+ }} />
+ }
+ </Paper>
);
}
- handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
- const resource = getResource<PermissionResource>(resourceUuid)(this.props.resources);
- if (resource) {
- this.props.onContextMenu(event, {
- name: '',
- uuid: resource.uuid,
- ownerUuid: resource.ownerUuid,
- kind: resource.kind,
- menuKind: ContextMenuKind.GROUP_MEMBER
- });
- }
+ handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+ this.setState({ value });
}
-- });
-
++ }));
import React from 'react';
import { connect } from 'react-redux';
-import { Grid, Button, Typography } from "@material-ui/core";
+import { Grid, Button, Typography, StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core";
import { DataExplorer } from "views-components/data-explorer/data-explorer";
import { DataColumns } from 'components/data-table/data-table';
import { SortDirection } from 'components/data-table/data-column';
- import { ResourceOwner } from 'views-components/data-explorer/renderers';
+ import { ResourceUuid } from 'views-components/data-explorer/renderers';
import { AddIcon } from 'components/icon/icon';
import { ResourceName } from 'views-components/data-explorer/renderers';
import { createTree } from 'models/tree';
import { openContextMenu } from 'store/context-menu/context-menu-actions';
import { ResourceKind } from 'models/resource';
import { LinkClass, LinkResource } from 'models/link';
- import { navigateToGroupDetails } from 'store/navigation/navigation-action';
+import { ArvadosTheme } from 'common/custom-theme';
+
+type CssRules = "root";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ width: '100%',
+ }
+});
export enum GroupsPanelColumnNames {
GROUP = "Name",
- OWNER = "Owner",
+ UUID = "UUID",
MEMBERS = "Members",
}
render: uuid => <ResourceName uuid={uuid} />
},
{
- name: GroupsPanelColumnNames.OWNER,
+ name: GroupsPanelColumnNames.UUID,
selected: true,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceOwner uuid={uuid} />,
+ render: uuid => <ResourceUuid uuid={uuid} />,
},
{
name: GroupsPanelColumnNames.MEMBERS,
const mapDispatchToProps = {
onContextMenu: openContextMenu,
- onRowDoubleClick: (uuid: string) =>
- navigateToGroupDetails(uuid),
onNewGroup: openCreateGroupDialog,
};
export interface GroupsPanelProps {
onNewGroup: () => void;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
- onRowDoubleClick: (item: string) => void;
resources: ResourcesState;
}
-export const GroupsPanel = connect(
+export const GroupsPanel = withStyles(styles)(connect(
mapStateToProps, mapDispatchToProps
)(
- class GroupsPanel extends React.Component<GroupsPanelProps> {
+ class GroupsPanel extends React.Component<GroupsPanelProps & WithStyles<CssRules>> {
render() {
return (
- <DataExplorer
+ <div className={this.props.classes.root}><DataExplorer
id={GROUPS_PANEL_ID}
+ data-cy="groups-panel-data-explorer"
onRowClick={noop}
- onRowDoubleClick={this.props.onRowDoubleClick}
+ onRowDoubleClick={noop}
onContextMenu={this.handleContextMenu}
contextMenuColumn={true}
hideColumnSelector
actions={
<Grid container justify='flex-end'>
<Button
+ data-cy="groups-panel-new-group"
variant="contained"
color="primary"
onClick={this.props.onNewGroup}>
<AddIcon /> New group
</Button>
</Grid>
- } />
+ } /></div>
);
}
const resource = getResource<GroupResource>(resourceUuid)(this.props.resources);
if (resource) {
this.props.onContextMenu(event, {
- name: '',
+ name: resource.name,
uuid: resource.uuid,
+ description: resource.description,
ownerUuid: resource.ownerUuid,
kind: resource.kind,
menuKind: ContextMenuKind.GROUPS
});
}
}
- });
+ }));
const GroupMembersCount = connect(
const permissions = filterResources((resource: LinkResource) =>
resource.kind === ResourceKind.LINK &&
resource.linkClass === LinkClass.PERMISSION &&
- resource.tailUuid === props.uuid
+ resource.headUuid === props.uuid
)(state.resources);
return {
};
}
- )(Typography);
+ )((props: {children: number}) => (<Typography children={props.children} />));
import { UserManageDialog } from 'views-components/user-dialog/manage-dialog';
import { SetupShellAccountDialog } from 'views-components/dialog-forms/setup-shell-account-dialog';
import { GroupsPanel } from 'views/groups-panel/groups-panel';
- import { CreateGroupDialog } from 'views-components/dialog-forms/create-group-dialog';
import { RemoveGroupDialog } from 'views-components/groups-dialog/remove-dialog';
import { GroupAttributesDialog } from 'views-components/groups-dialog/attributes-dialog';
import { GroupDetailsPanel } from 'views/group-details-panel/group-details-panel';
import { RemoveGroupMemberDialog } from 'views-components/groups-dialog/member-remove-dialog';
import { GroupMemberAttributesDialog } from 'views-components/groups-dialog/member-attributes-dialog';
- import { AddGroupMembersDialog } from 'views-components/dialog-forms/add-group-member-dialog';
import { PartialCopyToCollectionDialog } from 'views-components/dialog-forms/partial-copy-to-collection-dialog';
import { PublicFavoritePanel } from 'views/public-favorites-panel/public-favorites-panel';
import { LinkAccountPanel } from 'views/link-account-panel/link-account-panel';
minWidth: 0,
paddingLeft: theme.spacing.unit * 3,
paddingRight: theme.spacing.unit * 3,
+ // Reserve vertical space for app bar + MainContentBar
+ minHeight: `calc(100vh - ${theme.spacing.unit * 16}px)`,
+ display: 'flex',
}
});
<Grid item>
<DetailsPanel />
</Grid>
- <AddGroupMembersDialog />
<AdvancedTabDialog />
<AttributesApiClientAuthorizationDialog />
<AttributesKeepServiceDialog />
<CopyCollectionDialog />
<CopyProcessDialog />
<CreateCollectionDialog />
- <CreateGroupDialog />
<CreateProjectDialog />
<CreateRepositoryDialog />
<CreateSshKeyDialog />