From 900c94fd8ae72168439697cde5c509574b596962 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 29 Sep 2021 12:00:15 -0400 Subject: [PATCH] Admin: Show licensed roles and unit membership in UI (#39773) (#39802) * Extend User type * Render licensed roles and org units * Combine admin icon with units * Extract search users to a new service * Fix wire provider * remove mock data * Fix icon margin * Fix common_test and remove RouteRegister * Remove old endpoints * Fix test * Add indexes to dashboards and orgs tables * Fix lint * Revert docs changes * undo docs formatting * Change order of input and filters * Abstract aria-label into a function * Add accessible info about user's membership * UI tweaks Co-authored-by: spinillos (cherry picked from commit 62dc10829a63623416b5dd4a960f0cf0e14b4f21) Co-authored-by: Alex Khomenko --- packages/grafana-ui/src/types/icon.ts | 1 + .../app/features/admin/UserListAdminPage.tsx | 191 ++++++++++++++++-- public/app/types/user.ts | 6 + 3 files changed, 179 insertions(+), 19 deletions(-) diff --git a/packages/grafana-ui/src/types/icon.ts b/packages/grafana-ui/src/types/icon.ts index 93f5e4b3c21..eb06b512fe9 100644 --- a/packages/grafana-ui/src/types/icon.ts +++ b/packages/grafana-ui/src/types/icon.ts @@ -28,6 +28,7 @@ export const getAvailableIcons = () => 'book-open', 'brackets-curly', 'bug', + 'building', 'calculator-alt', 'calendar-alt', 'camera', diff --git a/public/app/features/admin/UserListAdminPage.tsx b/public/app/features/admin/UserListAdminPage.tsx index d1da75059bb..45961d2d8a9 100644 --- a/public/app/features/admin/UserListAdminPage.tsx +++ b/public/app/features/admin/UserListAdminPage.tsx @@ -1,14 +1,23 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo, memo } from 'react'; import { css, cx } from '@emotion/css'; import { connect, ConnectedProps } from 'react-redux'; -import { Pagination, Tooltip, LinkButton, Icon, RadioButtonGroup, useStyles2, FilterInput } from '@grafana/ui'; +import { + Icon, + IconName, + LinkButton, + Pagination, + RadioButtonGroup, + Tooltip, + useStyles2, + FilterInput, +} from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data'; import Page from 'app/core/components/Page/Page'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { contextSrv } from 'app/core/core'; import { getNavModel } from '../../core/selectors/navModel'; -import { AccessControlAction, StoreState, UserDTO } from '../../types'; -import { fetchUsers, changeQuery, changePage, changeFilter } from './state/actions'; +import { AccessControlAction, StoreState, Unit, UserDTO } from '../../types'; +import { changeFilter, changePage, changeQuery, fetchUsers } from './state/actions'; import PageLoader from '../../core/components/PageLoader/PageLoader'; const mapDispatchToProps = { @@ -55,11 +64,19 @@ const UserListAdminPageUnConnected: React.FC = ({ fetchUsers(); }, [fetchUsers]); + const showLicensedRole = useMemo(() => users.some((user) => user.licensedRole), [users]); + return (
+ = ({ value={filter} className={styles.filter} /> -
{contextSrv.hasPermission(AccessControlAction.UsersCreate) && ( @@ -94,7 +105,31 @@ const UserListAdminPageUnConnected: React.FC = ({ Login Email Name - Server admin + Belongs to + {showLicensedRole && ( + + Licensed role{' '} + + Licensed role is based on a user's Org role (i.e. Viewer, Editor, Admin) and their + dashboard/folder permissions.{' '} + + Learn more + + + } + > + + + + )} Last active  @@ -104,7 +139,11 @@ const UserListAdminPageUnConnected: React.FC = ({ - {users.map(renderUser)} + + {users.map((user) => ( + + ))} +
{showPaging && } @@ -115,7 +154,17 @@ const UserListAdminPageUnConnected: React.FC = ({ ); }; -const renderUser = (user: UserDTO) => { +const getUsersAriaLabel = (name: string) => { + return `Edit user's ${name} details`; +}; + +type UserListItemProps = { + user: UserDTO; + showLicensedRole: boolean; +}; + +const UserListItem = memo(({ user, showLicensedRole }: UserListItemProps) => { + const styles = useStyles2(getStyles); const editUrl = `admin/users/edit/${user.id}`; return ( @@ -126,36 +175,61 @@ const renderUser = (user: UserDTO) => { - + {user.login} - + {user.email} - + {user.name} - + + org.name).join(',')}` + : undefined + } + > + {user.isAdmin && ( - + )} + {showLicensedRole && ( + + + {user.licensedRole === 'None' ? ( + + Not assigned{' '} + + + + + ) : ( + user.licensedRole + )} + + + )} {user.lastSeenAtAge && ( - {user.lastSeenAtAge} + {user.lastSeenAtAge === '10 years' ? Never : user.lastSeenAtAge} )} @@ -169,6 +243,53 @@ const renderUser = (user: UserDTO) => { ); +}); + +UserListItem.displayName = 'UserListItem'; + +type OrgUnitProps = { units?: Unit[]; icon: IconName }; + +const OrgUnits = ({ units, icon }: OrgUnitProps) => { + const styles = useStyles2(getStyles); + + if (!units?.length) { + return null; + } + + return units.length > 1 ? ( + + {units?.map((unit) => ( + + {unit.name} + + ))} + + } + > +
+ {units.length} +
+
+ ) : ( + + {units[0].name} + + ); }; const getStyles = (theme: GrafanaTheme2) => { @@ -177,8 +298,40 @@ const getStyles = (theme: GrafanaTheme2) => { margin-top: ${theme.spacing(3)}; `, filter: css` + margin: 0 ${theme.spacing(1)}; + `, + iconRow: css` + svg { + margin-left: ${theme.spacing(0.5)}; + } + `, + row: css` + display: flex; + align-items: center; + height: 100% !important; + + a { + padding: ${theme.spacing(0.5)} 0 !important; + } + `, + unitTooltip: css` + display: flex; + flex-direction: column; + `, + unitItem: css` + cursor: pointer; + padding: ${theme.spacing(0.5)} 0; margin-right: ${theme.spacing(1)}; `, + disabled: css` + color: ${theme.colors.text.disabled}; + `, + link: css` + color: ${theme.colors.text.link}; + :hover { + text-decoration: underline; + } + `, }; }; diff --git a/public/app/types/user.ts b/public/app/types/user.ts index c0a15c40618..13cffb0db4f 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -22,6 +22,8 @@ export interface User { orgId?: number; } +export type Unit = { name: string; url: string }; + export interface UserDTO { id: number; login: string; @@ -37,6 +39,10 @@ export interface UserDTO { avatarUrl?: string; orgId?: number; lastSeenAtAge?: string; + licensedRole?: string; + permissions?: string[]; + teams?: Unit[]; + orgs?: Unit[]; } export interface Invitee {