mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
302 lines
8.9 KiB
TypeScript
302 lines
8.9 KiB
TypeScript
import {Fragment, PureComponent} from 'react';
|
|
import styled from '@emotion/styled';
|
|
|
|
import UserAvatar from 'sentry/components/avatar/userAvatar';
|
|
import {Button} from 'sentry/components/button';
|
|
import Confirm from 'sentry/components/confirm';
|
|
import HookOrDefault from 'sentry/components/hookOrDefault';
|
|
import Link from 'sentry/components/links/link';
|
|
import LoadingIndicator from 'sentry/components/loadingIndicator';
|
|
import PanelItem from 'sentry/components/panels/panelItem';
|
|
import {IconCheckmark, IconClose, IconFlag, IconMail, IconSubtract} from 'sentry/icons';
|
|
import {t, tct} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {AvatarUser, Member, Organization} from 'sentry/types';
|
|
import isMemberDisabledFromLimit from 'sentry/utils/isMemberDisabledFromLimit';
|
|
import {capitalize} from 'sentry/utils/string/capitalize';
|
|
|
|
type Props = {
|
|
canAddMembers: boolean;
|
|
canRemoveMembers: boolean;
|
|
currentUser: AvatarUser;
|
|
member: Member;
|
|
memberCanLeave: boolean;
|
|
onLeave: (member: Member) => void;
|
|
onRemove: (member: Member) => void;
|
|
onSendInvite: (member: Member) => void;
|
|
organization: Organization;
|
|
requireLink: boolean;
|
|
status: '' | 'loading' | 'success' | 'error' | null;
|
|
};
|
|
|
|
type State = {
|
|
busy: boolean;
|
|
};
|
|
|
|
const DisabledMemberTooltip = HookOrDefault({
|
|
hookName: 'component:disabled-member-tooltip',
|
|
defaultComponent: ({children}) => <Fragment>{children}</Fragment>,
|
|
});
|
|
|
|
export default class OrganizationMemberRow extends PureComponent<Props, State> {
|
|
state: State = {
|
|
busy: false,
|
|
};
|
|
|
|
handleRemove = () => {
|
|
const {onRemove} = this.props;
|
|
|
|
if (typeof onRemove !== 'function') {
|
|
return;
|
|
}
|
|
|
|
this.setState({busy: true});
|
|
onRemove(this.props.member);
|
|
};
|
|
|
|
handleLeave = () => {
|
|
const {onLeave} = this.props;
|
|
|
|
if (typeof onLeave !== 'function') {
|
|
return;
|
|
}
|
|
|
|
this.setState({busy: true});
|
|
onLeave(this.props.member);
|
|
};
|
|
|
|
handleSendInvite = () => {
|
|
const {onSendInvite, member} = this.props;
|
|
|
|
if (typeof onSendInvite !== 'function') {
|
|
return;
|
|
}
|
|
onSendInvite(member);
|
|
};
|
|
|
|
renderMemberRole() {
|
|
const {member} = this.props;
|
|
const {roleName, pending, expired} = member;
|
|
if (isMemberDisabledFromLimit(member)) {
|
|
return <DisabledMemberTooltip>{t('Deactivated')}</DisabledMemberTooltip>;
|
|
}
|
|
if (pending) {
|
|
return (
|
|
<InvitedRole>
|
|
<IconMail size="md" />
|
|
{expired ? t('Expired Invite') : tct('Invited [roleName]', {roleName})}
|
|
</InvitedRole>
|
|
);
|
|
}
|
|
return <Fragment>{capitalize(member.orgRole)}</Fragment>;
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
member,
|
|
organization,
|
|
status,
|
|
requireLink,
|
|
memberCanLeave,
|
|
currentUser,
|
|
canRemoveMembers,
|
|
canAddMembers,
|
|
} = this.props;
|
|
|
|
const {id, flags, email, name, pending, user} = member;
|
|
|
|
// if member is not the only owner, they can leave
|
|
const isIdpProvisioned = flags['idp:provisioned'];
|
|
const isPartnershipUser = flags['partnership:restricted'];
|
|
const needsSso = !flags['sso:linked'] && requireLink;
|
|
const isCurrentUser = currentUser.email === email;
|
|
const showRemoveButton = !isCurrentUser;
|
|
const showLeaveButton = isCurrentUser;
|
|
const canRemoveMember =
|
|
canRemoveMembers && !isCurrentUser && !isIdpProvisioned && !isPartnershipUser;
|
|
// member has a `user` property if they are registered with sentry
|
|
// i.e. has accepted an invite to join org
|
|
const has2fa = user?.has2fa;
|
|
const detailsUrl = `/settings/${organization.slug}/members/${id}/`;
|
|
const isInviteSuccessful = status === 'success';
|
|
const isInviting = status === 'loading';
|
|
const showResendButton = pending || needsSso;
|
|
|
|
return (
|
|
<StyledPanelItem data-test-id={email}>
|
|
<MemberHeading>
|
|
<UserAvatar
|
|
size={32}
|
|
user={user ?? {email, id: email, name: email, type: 'user'}}
|
|
/>
|
|
<MemberDescription to={detailsUrl}>
|
|
<h5 style={{margin: '0 0 3px'}}>
|
|
<UserName>{name}</UserName>
|
|
</h5>
|
|
<Email>{email}</Email>
|
|
</MemberDescription>
|
|
</MemberHeading>
|
|
|
|
<div data-test-id="member-role">{this.renderMemberRole()}</div>
|
|
|
|
<div data-test-id="member-status">
|
|
{showResendButton ? (
|
|
<Fragment>
|
|
{isInviting && (
|
|
<LoadingContainer>
|
|
<LoadingIndicator mini />
|
|
</LoadingContainer>
|
|
)}
|
|
{isInviteSuccessful && <span>{t('Sent!')}</span>}
|
|
{!isInviting && !isInviteSuccessful && (
|
|
<Button
|
|
disabled={!canAddMembers}
|
|
priority="primary"
|
|
size="sm"
|
|
onClick={this.handleSendInvite}
|
|
>
|
|
{pending ? t('Resend invite') : t('Resend SSO link')}
|
|
</Button>
|
|
)}
|
|
</Fragment>
|
|
) : (
|
|
<AuthStatus>
|
|
{has2fa ? (
|
|
<IconCheckmark isCircled color="success" />
|
|
) : (
|
|
<IconFlag color="error" />
|
|
)}
|
|
{has2fa ? t('2FA Enabled') : t('2FA Not Enabled')}
|
|
</AuthStatus>
|
|
)}
|
|
</div>
|
|
|
|
{showRemoveButton || showLeaveButton ? (
|
|
<RightColumn>
|
|
{showRemoveButton && canRemoveMember && (
|
|
<Confirm
|
|
message={tct('Are you sure you want to remove [name] from [orgName]?', {
|
|
name,
|
|
orgName: organization.slug,
|
|
})}
|
|
onConfirm={this.handleRemove}
|
|
>
|
|
<Button
|
|
data-test-id="remove"
|
|
icon={<IconSubtract isCircled />}
|
|
size="sm"
|
|
busy={this.state.busy}
|
|
>
|
|
{t('Remove')}
|
|
</Button>
|
|
</Confirm>
|
|
)}
|
|
|
|
{showRemoveButton && !canRemoveMember && (
|
|
<Button
|
|
disabled
|
|
size="sm"
|
|
title={
|
|
isIdpProvisioned
|
|
? t(
|
|
"This user is managed through your organization's identity provider."
|
|
)
|
|
: isPartnershipUser
|
|
? t('You cannot make changes to this partner-provisioned user.')
|
|
: t('You do not have access to remove members')
|
|
}
|
|
icon={<IconSubtract isCircled />}
|
|
>
|
|
{t('Remove')}
|
|
</Button>
|
|
)}
|
|
|
|
{showLeaveButton && memberCanLeave && (
|
|
<Confirm
|
|
message={tct('Are you sure you want to leave [orgName]?', {
|
|
orgName: organization.slug,
|
|
})}
|
|
onConfirm={this.handleLeave}
|
|
>
|
|
<Button priority="danger" size="sm" icon={<IconClose />}>
|
|
{t('Leave')}
|
|
</Button>
|
|
</Confirm>
|
|
)}
|
|
|
|
{showLeaveButton && !memberCanLeave && (
|
|
<Button
|
|
size="sm"
|
|
icon={<IconClose />}
|
|
disabled
|
|
title={
|
|
isIdpProvisioned
|
|
? t(
|
|
"Your account is managed through your organization's identity provider."
|
|
)
|
|
: isPartnershipUser
|
|
? t('You cannot make changes as a partner-provisioned user.')
|
|
: t(
|
|
'You cannot leave this organization as you are the only organization owner.'
|
|
)
|
|
}
|
|
>
|
|
{t('Leave')}
|
|
</Button>
|
|
)}
|
|
</RightColumn>
|
|
) : null}
|
|
</StyledPanelItem>
|
|
);
|
|
}
|
|
}
|
|
|
|
const StyledPanelItem = styled(PanelItem)`
|
|
display: grid;
|
|
grid-template-columns: minmax(150px, 4fr) minmax(90px, 2fr) minmax(120px, 2fr) minmax(
|
|
100px,
|
|
1fr
|
|
);
|
|
gap: ${space(2)};
|
|
align-items: center;
|
|
`;
|
|
// Force action button at the end to align to right
|
|
const RightColumn = styled('div')`
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
`;
|
|
|
|
const Section = styled('div')`
|
|
display: inline-grid;
|
|
grid-template-columns: max-content auto;
|
|
gap: ${space(1)};
|
|
align-items: center;
|
|
`;
|
|
|
|
const MemberHeading = styled(Section)``;
|
|
const MemberDescription = styled(Link)`
|
|
overflow: hidden;
|
|
`;
|
|
|
|
const UserName = styled('div')`
|
|
display: block;
|
|
overflow: hidden;
|
|
font-size: ${p => p.theme.fontSizeMedium};
|
|
text-overflow: ellipsis;
|
|
`;
|
|
|
|
const Email = styled('div')`
|
|
color: ${p => p.theme.subText};
|
|
font-size: ${p => p.theme.fontSizeSmall};
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
`;
|
|
|
|
const InvitedRole = styled(Section)``;
|
|
const LoadingContainer = styled('div')`
|
|
margin-top: 0;
|
|
margin-bottom: ${space(1.5)};
|
|
`;
|
|
|
|
const AuthStatus = styled(Section)``;
|