import {Fragment, useEffect, useMemo, useState} from 'react';
import styled from '@emotion/styled';
import * as Sentry from '@sentry/react';
import isEqual from 'lodash/isEqual';
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 FieldGroup from 'sentry/components/forms/fieldGroup';
import HookOrDefault from 'sentry/components/hookOrDefault';
import ExternalLink from 'sentry/components/links/externalLink';
import LoadingError from 'sentry/components/loadingError';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import Panel from 'sentry/components/panels/panel';
import PanelBody from 'sentry/components/panels/panelBody';
import PanelHeader from 'sentry/components/panels/panelHeader';
import PanelItem from 'sentry/components/panels/panelItem';
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
import {Tooltip} from 'sentry/components/tooltip';
import {IconRefresh} from 'sentry/icons';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Member} from 'sentry/types/organization';
import isMemberDisabledFromLimit from 'sentry/utils/isMemberDisabledFromLimit';
import {
type ApiQueryKey,
setApiQueryData,
useApiQuery,
useMutation,
useQueryClient,
} from 'sentry/utils/queryClient';
import type RequestError from 'sentry/utils/requestError/requestError';
import Teams from 'sentry/utils/teams';
import useApi from 'sentry/utils/useApi';
import {useNavigate} from 'sentry/utils/useNavigate';
import useOrganization from 'sentry/utils/useOrganization';
import {useParams} from 'sentry/utils/useParams';
import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
import TeamSelectForMember from 'sentry/views/settings/components/teamSelect/teamSelectForMember';
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'
);
const DisabledMemberTooltip = HookOrDefault({
hookName: 'component:disabled-member-tooltip',
defaultComponent: ({children}) => {children},
});
function MemberStatus({
member,
memberDeactivated,
}: {
member: Member;
memberDeactivated: boolean;
}) {
if (memberDeactivated) {
return (
{t('Deactivated')}
);
}
if (member.expired) {
return {t('Invitation Expired')};
}
if (member.pending) {
return {t('Invitation Pending')};
}
return t('Active');
}
const getMemberQueryKey = (orgSlug: string, memberId: string): ApiQueryKey => [
`/organizations/${orgSlug}/members/${memberId}/`,
];
function OrganizationMemberDetailContent({member}: {member: Member}) {
const api = useApi();
const queryClient = useQueryClient();
const organization = useOrganization();
const navigate = useNavigate();
const [orgRole, setOrgRole] = useState('');
const [teamRoles, setTeamRoles] = useState([]);
const hasTeamRoles = organization.features.includes('team-roles');
useEffect(() => {
if (member) {
setOrgRole(member.orgRole);
setTeamRoles(member.teamRoles);
}
}, [member]);
const {mutate: updatedMember, isPending: isSaving} = useMutation({
mutationFn: () => {
return updateMember(api, {
orgId: organization.slug,
memberId: member.id,
data: {orgRole, teamRoles} as any,
});
},
onMutate: () => {
addLoadingMessage(t('Saving\u2026'));
},
onSuccess: data => {
addSuccessMessage(t('Saved'));
setApiQueryData(
queryClient,
getMemberQueryKey(organization.slug, member.id),
data
);
},
onError: error => {
addErrorMessage(
(error?.responseJSON?.detail as string) ?? t('Failed to update member')
);
},
});
const {mutate: inviteMember, isPending: isInviting} = useMutation(
{
mutationFn: () => {
return resendMemberInvite(api, {
orgId: organization.slug,
memberId: member.id,
regenerate: true,
});
},
onSuccess: data => {
addSuccessMessage(t('Sent invite!'));
setApiQueryData(
queryClient,
getMemberQueryKey(organization.slug, member.id),
data
);
},
onError: () => {
addErrorMessage(t('Could not send invite'));
},
}
);
const {mutate: reset2fa, isPending: isResetting2fa} = useMutation({
mutationFn: () => {
const {user} = member;
const promises =
user?.authenticators?.map(auth => removeAuthenticator(api, user.id, auth.id)) ??
[];
return Promise.all(promises);
},
onSuccess: () => {
addSuccessMessage(t('All authenticators have been removed'));
navigate(`/settings/${organization.slug}/members/`);
},
onError: error => {
addErrorMessage(t('Error removing authenticators'));
Sentry.captureException(error);
},
});
const onAddTeam = (teamSlug: string) => {
const newTeamRoles = [...teamRoles];
const i = newTeamRoles.findIndex(r => r.teamSlug === teamSlug);
if (i !== -1) {
return;
}
newTeamRoles.push({teamSlug, role: null});
setTeamRoles(newTeamRoles);
};
const onRemoveTeam = (teamSlug: string) => {
const newTeamRoles = teamRoles.filter(r => r.teamSlug !== teamSlug);
setTeamRoles(newTeamRoles);
};
const onChangeTeamRole = (teamSlug: string, role: string) => {
if (!hasTeamRoles) {
return;
}
const newTeamRoles = [...teamRoles];
const i = newTeamRoles.findIndex(r => r.teamSlug === teamSlug);
if (i === -1) {
return;
}
newTeamRoles[i] = {...newTeamRoles[i], role};
setTeamRoles(newTeamRoles);
};
const showResetButton = useMemo(() => {
const {user} = member;
if (!user || !user.authenticators || organization.require2FA) {
return false;
}
const hasAuth = user.authenticators.length >= 1;
return hasAuth && user.canReset2fa;
}, [member, organization.require2FA]);
const getTooltip = (): string => {
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 '';
};
function hasFormChanged() {
if (!member) {
return false;
}
if (orgRole !== member.orgRole || !isEqual(teamRoles, member.teamRoles)) {
return true;
}
return false;
}
const memberDeactivated = isMemberDisabledFromLimit(member);
const canEdit = organization.access.includes('org:write') && !memberDeactivated;
const isPartnershipUser = member.flags['partnership:restricted'] === true;
const {email, expired, pending} = member;
const canResend = !expired;
const showAuth = !pending;
const showResendButton = (member.pending || member.expired) && canResend;
return (
{member.name}
{t('Member Settings')}
}
/>
{t('Basics')}
{showResendButton && (
}
title={t('Generate a new invite link and send a new email.')}
busy={isInviting}
onClick={() => inviteMember()}
>
{t('Resend Invite')}
)}