import {Fragment} from 'react'; import {browserHistory, RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import * as Sentry from '@sentry/react'; import {removeAuthenticator} from 'sentry/actionCreators/account'; import { addErrorMessage, addLoadingMessage, addSuccessMessage, } from 'sentry/actionCreators/indicator'; import {resendMemberInvite, updateMember} from 'sentry/actionCreators/members'; import Button from 'sentry/components/button'; import Confirm from 'sentry/components/confirm'; import DateTime from 'sentry/components/dateTime'; import NotFound from 'sentry/components/errors/notFound'; import Field from 'sentry/components/forms/field'; import HookOrDefault from 'sentry/components/hookOrDefault'; import ExternalLink from 'sentry/components/links/externalLink'; import {Panel, PanelBody, PanelHeader, PanelItem} from 'sentry/components/panels'; import TextCopyInput from 'sentry/components/textCopyInput'; import Tooltip from 'sentry/components/tooltip'; import {t, tct} from 'sentry/locale'; import space from 'sentry/styles/space'; import {Member, Organization, Team} from 'sentry/types'; import isMemberDisabledFromLimit from 'sentry/utils/isMemberDisabledFromLimit'; import recreateRoute from 'sentry/utils/recreateRoute'; import Teams from 'sentry/utils/teams'; import withOrganization from 'sentry/utils/withOrganization'; import AsyncView from 'sentry/views/asyncView'; import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader'; import TeamSelect from 'sentry/views/settings/components/teamSelect'; import OrganizationRoleSelect from './inviteMember/orgRoleSelect'; const MULTIPLE_ORGS = t('Cannot be reset since user is in more than one organization'); const NOT_ENROLLED = t('Not enrolled in two-factor authentication'); const NO_PERMISSION = t('You do not have permission to perform this action'); const TWO_FACTOR_REQUIRED = t( 'Cannot be reset since two-factor is required for this organization' ); type RouteParams = { memberId: string; orgId: string; }; type Props = { organization: Organization; } & RouteComponentProps; type State = { member: Member | null; roleList: Member['roles']; selectedRole: Member['role']; } & AsyncView['state']; const DisabledMemberTooltip = HookOrDefault({ hookName: 'component:disabled-member-tooltip', defaultComponent: ({children}) => {children}, }); class OrganizationMemberDetail extends AsyncView { getDefaultState(): State { return { ...super.getDefaultState(), roleList: [], selectedRole: '', member: null, }; } getEndpoints(): ReturnType { const {organization, params} = this.props; return [ ['member', `/organizations/${organization.slug}/members/${params.memberId}/`], ]; } redirectToMemberPage() { const {location, params, routes} = this.props; const members = recreateRoute('members/', { location, routes, params, stepBack: -2, }); browserHistory.push(members); } handleSave = async () => { const {organization, params} = this.props; addLoadingMessage(t('Saving...')); this.setState({busy: true}); try { await updateMember(this.api, { orgId: organization.slug, memberId: params.memberId, data: this.state.member, }); addSuccessMessage(t('Saved')); this.redirectToMemberPage(); } catch (resp) { const errorMessage = (resp && resp.responseJSON && resp.responseJSON.detail) || t('Could not save...'); addErrorMessage(errorMessage); } this.setState({busy: false}); }; handleInvite = async (regenerate: boolean) => { const {organization, params} = this.props; addLoadingMessage(t('Sending invite...')); this.setState({busy: true}); try { const data = await resendMemberInvite(this.api, { orgId: organization.slug, memberId: params.memberId, regenerate, }); addSuccessMessage(t('Sent invite!')); if (regenerate) { this.setState(state => ({member: {...state.member, ...data}})); } } catch (_err) { addErrorMessage(t('Could not send invite')); } this.setState({busy: false}); }; handleAddTeam = (team: Team) => { const {member} = this.state; if (!member!.teams.includes(team.slug)) { member!.teams.push(team.slug); } this.setState({member}); }; handleRemoveTeam = (removedTeam: string) => { const {member} = this.state; this.setState({ member: { ...member!, teams: member!.teams.filter(slug => slug !== removedTeam), }, }); }; handle2faReset = async () => { const {organization, router} = this.props; const {user} = this.state.member!; const requests = user.authenticators.map(auth => removeAuthenticator(this.api, user.id, auth.id) ); try { await Promise.all(requests); router.push(`/settings/${organization.slug}/members/`); addSuccessMessage(t('All authenticators have been removed')); } catch (err) { addErrorMessage(t('Error removing authenticators')); Sentry.captureException(err); } }; showResetButton = () => { const {organization} = this.props; const {member} = this.state; const {user} = member!; if (!user || !user.authenticators || organization.require2FA) { return false; } const hasAuth = user.authenticators.length >= 1; return hasAuth && user.canReset2fa; }; getTooltip = (): string => { const {organization} = this.props; const {member} = this.state; const {user} = member!; if (!user) { return ''; } if (!user.authenticators) { return NO_PERMISSION; } if (!user.authenticators.length) { return NOT_ENROLLED; } if (!user.canReset2fa) { return MULTIPLE_ORGS; } if (organization.require2FA) { return TWO_FACTOR_REQUIRED; } return ''; }; get memberDeactivated() { return isMemberDisabledFromLimit(this.state.member); } renderMemberStatus(member: Member) { if (this.memberDeactivated) { return ( {t('Deactivated')} ); } if (member.expired) { return {t('Invitation Expired')}; } if (member.pending) { return {t('Invitation Pending')}; } return t('Active'); } renderBody() { const {organization} = this.props; const {member} = this.state; if (!member) { return ; } const {access, features} = organization; const inviteLink = member.invite_link; const canEdit = access.includes('org:write') && !this.memberDeactivated; const hasTeamRoles = features.includes('team-roles'); const {email, expired, pending} = member; const canResend = !expired; const showAuth = !pending; return (
{member.name}
{t('Member Settings')}
} /> {t('Basics')}
{t('Email')}
{email}
{t('Status')}
{this.renderMemberStatus(member)}
{t('Added')}
{inviteLink && (
{t('Invite Link')} {inviteLink}

{t('This unique invite link may only be used by this member.')}

{canResend && ( )}
)}
{showAuth && ( {t('Authentication')} )} this.setState({member: {...member, role: slug}})} /> {({teams, initiallyLoaded}) => ( )} ); } } export default withOrganization(OrganizationMemberDetail); const ExtraHeaderText = styled('div')` color: ${p => p.theme.gray300}; font-weight: normal; font-size: ${p => p.theme.fontSizeLarge}; `; const Details = styled('div')` display: grid; grid-auto-flow: column; grid-template-columns: 2fr 1fr 1fr; gap: ${space(2)}; width: 100%; @media (max-width: ${p => p.theme.breakpoints.small}) { grid-auto-flow: row; grid-template-columns: auto; } `; const DetailLabel = styled('div')` font-weight: bold; margin-bottom: ${space(0.5)}; color: ${p => p.theme.textColor}; `; const OverflowWrapper = styled('div')` overflow: hidden; flex: 1; `; const InviteSection = styled('div')` border-top: 1px solid ${p => p.theme.border}; margin-top: ${space(2)}; padding-top: ${space(2)}; `; const InviteActions = styled('div')` display: grid; gap: ${space(1)}; grid-auto-flow: column; justify-content: flex-end; margin-top: ${space(2)}; `; const Footer = styled('div')` display: flex; justify-content: flex-end; `;