import { css, cx } from '@emotion/css'; import React, { PureComponent, ReactElement } from 'react'; import { GrafanaTheme, GrafanaTheme2 } from '@grafana/data'; import { Button, ConfirmButton, Field, HorizontalGroup, Icon, Modal, stylesFactory, Themeable, Tooltip, useStyles2, useTheme, withTheme, } from '@grafana/ui'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api'; import { OrgPicker, OrgSelectItem } from 'app/core/components/Select/OrgPicker'; import { contextSrv } from 'app/core/core'; import { AccessControlAction, Organization, OrgRole, Role, UserDTO, UserOrg } from 'app/types'; import { OrgRolePicker } from './OrgRolePicker'; interface Props { orgs: UserOrg[]; user?: UserDTO; isExternalUser?: boolean; onOrgRemove: (orgId: number) => void; onOrgRoleChange: (orgId: number, newRole: OrgRole) => void; onOrgAdd: (orgId: number, role: OrgRole) => void; } interface State { showAddOrgModal: boolean; } export class UserOrgs extends PureComponent { addToOrgButtonRef = React.createRef(); state = { showAddOrgModal: false, }; showOrgAddModal = () => { this.setState({ showAddOrgModal: true }); }; dismissOrgAddModal = () => { this.setState({ showAddOrgModal: false }, () => { this.addToOrgButtonRef.current?.focus(); }); }; render() { const { user, orgs, isExternalUser, onOrgRoleChange, onOrgRemove, onOrgAdd } = this.props; const { showAddOrgModal } = this.state; const addToOrgContainerClass = css` margin-top: 0.8rem; `; const canAddToOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersAdd); return ( <>

Organizations

{orgs.map((org, index) => ( ))}
{canAddToOrg && ( )}
); } } const getOrgRowStyles = stylesFactory((theme: GrafanaTheme) => { return { removeButton: css` margin-right: 0.6rem; text-decoration: underline; color: ${theme.palette.blue95}; `, label: css` font-weight: 500; `, disabledTooltip: css` display: flex; `, tooltipItem: css` margin-left: 5px; `, tooltipItemLink: css` color: ${theme.palette.blue95}; `, rolePickerWrapper: css` display: flex; `, rolePicker: css` flex: auto; margin-right: ${theme.spacing.sm}; `, }; }); interface OrgRowProps extends Themeable { user?: UserDTO; org: UserOrg; isExternalUser?: boolean; onOrgRemove: (orgId: number) => void; onOrgRoleChange: (orgId: number, newRole: OrgRole) => void; } class UnThemedOrgRow extends PureComponent { state = { currentRole: this.props.org.role, isChangingRole: false, roleOptions: [], builtInRoles: {}, }; componentDidMount() { if (contextSrv.licensedAccessControlEnabled()) { if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { fetchRoleOptions(this.props.org.orgId) .then((roles) => this.setState({ roleOptions: roles })) .catch((e) => console.error(e)); } if (contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)) { fetchRoleOptions(this.props.org.orgId) .then((roles) => this.setState({ builtInRoles: roles })) .catch((e) => console.error(e)); } } } onOrgRemove = async () => { const { org, user } = this.props; this.props.onOrgRemove(org.orgId); if (contextSrv.licensedAccessControlEnabled()) { if (contextSrv.hasPermission(AccessControlAction.OrgUsersRemove)) { user && (await updateUserRoles([], user.id, org.orgId)); } } }; onChangeRoleClick = () => { const { org } = this.props; this.setState({ isChangingRole: true, currentRole: org.role }); }; onOrgRoleChange = (newRole: OrgRole) => { this.setState({ currentRole: newRole }); }; onOrgRoleSave = () => { this.props.onOrgRoleChange(this.props.org.orgId, this.state.currentRole); }; onCancelClick = () => { this.setState({ isChangingRole: false }); }; onBuiltinRoleChange = (newRole: OrgRole) => { this.props.onOrgRoleChange(this.props.org.orgId, newRole); }; render() { const { user, org, isExternalUser, theme } = this.props; const { currentRole, isChangingRole } = this.state; const styles = getOrgRowStyles(theme); const labelClass = cx('width-16', styles.label); const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersWrite); const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove); const rolePickerDisabled = isExternalUser || !canChangeRole; const inputId = `${org.name}-input`; return ( {contextSrv.licensedAccessControlEnabled() ? (
{isExternalUser && }
) : ( <> {isChangingRole ? ( ) : ( {org.role} )}
{canChangeRole && ( )}
)}
{canRemoveFromOrg && ( Remove from organization )}
); } } const OrgRow = withTheme(UnThemedOrgRow); const getAddToOrgModalStyles = stylesFactory(() => ({ modal: css` width: 500px; `, buttonRow: css` text-align: center; `, modalContent: css` overflow: visible; `, })); interface AddToOrgModalProps { isOpen: boolean; user?: UserDTO; userOrgs: UserOrg[]; onOrgAdd(orgId: number, role: string): void; onDismiss?(): void; } interface AddToOrgModalState { selectedOrg: Organization | null; role: OrgRole; roleOptions: Role[]; pendingOrgId: number | null; pendingUserId: number | null; pendingRoles: Role[]; } export class AddToOrgModal extends PureComponent { state: AddToOrgModalState = { selectedOrg: null, role: OrgRole.Viewer, roleOptions: [], pendingOrgId: null, pendingUserId: null, pendingRoles: [], }; onOrgSelect = (org: OrgSelectItem) => { const userOrg = this.props.userOrgs.find((userOrg) => userOrg.orgId === org.value?.id); this.setState({ selectedOrg: org.value!, role: userOrg?.role || OrgRole.Viewer }); if (contextSrv.licensedAccessControlEnabled()) { if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { fetchRoleOptions(org.value?.id) .then((roles) => this.setState({ roleOptions: roles })) .catch((e) => console.error(e)); } } }; onOrgRoleChange = (newRole: OrgRole) => { this.setState({ role: newRole, }); }; onAddUserToOrg = async () => { const { selectedOrg, role } = this.state; this.props.onOrgAdd(selectedOrg!.id, role); // add the stored userRoles also if (contextSrv.licensedAccessControlEnabled()) { if (contextSrv.hasPermission(AccessControlAction.OrgUsersWrite)) { if (this.state.pendingUserId) { await updateUserRoles(this.state.pendingRoles, this.state.pendingUserId!, this.state.pendingOrgId!); // clear pending state this.state.pendingOrgId = null; this.state.pendingRoles = []; this.state.pendingUserId = null; } } } }; onCancel = () => { // clear selectedOrg when modal is canceled this.setState({ selectedOrg: null, pendingRoles: [], pendingOrgId: null, pendingUserId: null, }); if (this.props.onDismiss) { this.props.onDismiss(); } }; onRoleUpdate = async (roles: Role[], userId: number, orgId: number | undefined) => { // keep the new role assignments for user this.setState({ pendingRoles: roles, pendingOrgId: orgId!, pendingUserId: userId, }); }; render() { const { isOpen, user, userOrgs } = this.props; const { role, roleOptions, selectedOrg } = this.state; const styles = getAddToOrgModalStyles(); return ( {contextSrv.accessControlEnabled() ? ( ) : ( )} ); } } interface ChangeOrgButtonProps { isExternalUser?: boolean; onChangeRoleClick: () => void; onCancelClick: () => void; onOrgRoleSave: () => void; } const getChangeOrgButtonTheme = (theme: GrafanaTheme2) => ({ disabledTooltip: css` display: flex; `, tooltipItemLink: css` color: ${theme.v1.palette.blue95}; `, }); export function ChangeOrgButton({ onChangeRoleClick, isExternalUser, onOrgRoleSave, onCancelClick, }: ChangeOrgButtonProps): ReactElement { const styles = useStyles2(getChangeOrgButtonTheme); return (
Change role {isExternalUser && ( This user's role is not editable because it is synchronized from your auth provider. Refer to the  Grafana authentication docs  for details.
} > )} ); } const ExternalUserTooltip: React.FC = () => { const theme = useTheme(); const styles = getTooltipStyles(theme); return (
This user's built-in role is not editable because it is synchronized from your auth provider. Refer to the  Grafana authentication docs  for details.
} > ); }; const getTooltipStyles = stylesFactory((theme: GrafanaTheme) => ({ disabledTooltip: css` display: flex; `, tooltipItemLink: css` color: ${theme.palette.blue95}; `, }));