|
@@ -1,7 +1,8 @@
|
|
-import {useCallback, useEffect, useState} from 'react';
|
|
|
|
|
|
+import {Fragment, useCallback, useEffect, useState} from 'react';
|
|
import styled from '@emotion/styled';
|
|
import styled from '@emotion/styled';
|
|
import * as qs from 'query-string';
|
|
import * as qs from 'query-string';
|
|
|
|
|
|
|
|
+import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
|
|
import {openInviteMissingMembersModal} from 'sentry/actionCreators/modal';
|
|
import {openInviteMissingMembersModal} from 'sentry/actionCreators/modal';
|
|
import {promptsCheck, promptsUpdate} from 'sentry/actionCreators/prompts';
|
|
import {promptsCheck, promptsUpdate} from 'sentry/actionCreators/prompts';
|
|
import {Button} from 'sentry/components/button';
|
|
import {Button} from 'sentry/components/button';
|
|
@@ -26,32 +27,30 @@ const MAX_MEMBERS_TO_SHOW = 5;
|
|
|
|
|
|
type Props = {
|
|
type Props = {
|
|
allowedRoles: OrgRole[];
|
|
allowedRoles: OrgRole[];
|
|
- missingMembers: {integration: string; users: MissingMember[]};
|
|
|
|
onModalClose: () => void;
|
|
onModalClose: () => void;
|
|
- onSendInvite: (email: string) => void;
|
|
|
|
|
|
+ onSendInvite: () => void;
|
|
organization: Organization;
|
|
organization: Organization;
|
|
};
|
|
};
|
|
|
|
|
|
export function InviteBanner({
|
|
export function InviteBanner({
|
|
- missingMembers,
|
|
|
|
- onSendInvite,
|
|
|
|
organization,
|
|
organization,
|
|
allowedRoles,
|
|
allowedRoles,
|
|
|
|
+ onSendInvite,
|
|
onModalClose,
|
|
onModalClose,
|
|
}: Props) {
|
|
}: Props) {
|
|
- // NOTE: this is currently used for Github only
|
|
|
|
-
|
|
|
|
const isEligibleForBanner =
|
|
const isEligibleForBanner =
|
|
organization.features.includes('integrations-gh-invite') &&
|
|
organization.features.includes('integrations-gh-invite') &&
|
|
organization.access.includes('org:write') &&
|
|
organization.access.includes('org:write') &&
|
|
- organization.githubNudgeInvite &&
|
|
|
|
- missingMembers?.users?.length > 0;
|
|
|
|
|
|
+ organization.githubNudgeInvite;
|
|
const [sendingInvite, setSendingInvite] = useState<boolean>(false);
|
|
const [sendingInvite, setSendingInvite] = useState<boolean>(false);
|
|
const [showBanner, setShowBanner] = useState<boolean>(false);
|
|
const [showBanner, setShowBanner] = useState<boolean>(false);
|
|
|
|
+ const [missingMembers, setMissingMembers] = useState<MissingMember[]>(
|
|
|
|
+ [] as MissingMember[]
|
|
|
|
+ );
|
|
|
|
|
|
const api = useApi();
|
|
const api = useApi();
|
|
- const integrationName = missingMembers?.integration;
|
|
|
|
- const promptsFeature = `${integrationName}_missing_members`;
|
|
|
|
|
|
+ // NOTE: this is currently used for Github only
|
|
|
|
+ const promptsFeature = `github_missing_members`;
|
|
const location = useLocation();
|
|
const location = useLocation();
|
|
|
|
|
|
const snoozePrompt = useCallback(async () => {
|
|
const snoozePrompt = useCallback(async () => {
|
|
@@ -75,17 +74,37 @@ export function InviteBanner({
|
|
});
|
|
});
|
|
}, [allowedRoles, missingMembers, organization, onModalClose]);
|
|
}, [allowedRoles, missingMembers, organization, onModalClose]);
|
|
|
|
|
|
|
|
+ const fetchMissingMembers = useCallback(async () => {
|
|
|
|
+ try {
|
|
|
|
+ const data = await api.requestPromise(
|
|
|
|
+ `/organizations/${organization.slug}/missing-members/`,
|
|
|
|
+ {
|
|
|
|
+ method: 'GET',
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+ const githubMissingMembers = data?.filter(
|
|
|
|
+ integrationMissingMembers => integrationMissingMembers.integration === 'github'
|
|
|
|
+ )[0];
|
|
|
|
+ setMissingMembers(githubMissingMembers?.users);
|
|
|
|
+ } catch (err) {
|
|
|
|
+ if (err.status !== 403) {
|
|
|
|
+ addErrorMessage(t('Unable to fetching missing commit authors'));
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }, [api, organization]);
|
|
|
|
+
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
if (!isEligibleForBanner) {
|
|
if (!isEligibleForBanner) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
+ fetchMissingMembers();
|
|
promptsCheck(api, {
|
|
promptsCheck(api, {
|
|
organizationId: organization.id,
|
|
organizationId: organization.id,
|
|
feature: promptsFeature,
|
|
feature: promptsFeature,
|
|
}).then(prompt => {
|
|
}).then(prompt => {
|
|
setShowBanner(!promptIsDismissed(prompt));
|
|
setShowBanner(!promptIsDismissed(prompt));
|
|
});
|
|
});
|
|
- }, [api, organization, promptsFeature, isEligibleForBanner]);
|
|
|
|
|
|
+ }, [api, organization, promptsFeature, isEligibleForBanner, fetchMissingMembers]);
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
const {inviteMissingMembers} = qs.parse(location.search);
|
|
const {inviteMissingMembers} = qs.parse(location.search);
|
|
@@ -95,13 +114,13 @@ export function InviteBanner({
|
|
}
|
|
}
|
|
}, [openInviteModal, location, isEligibleForBanner]);
|
|
}, [openInviteModal, location, isEligibleForBanner]);
|
|
|
|
|
|
- if (isEligibleForBanner && showBanner) {
|
|
|
|
|
|
+ if (isEligibleForBanner && showBanner && missingMembers) {
|
|
trackAnalytics('github_invite_banner.viewed', {
|
|
trackAnalytics('github_invite_banner.viewed', {
|
|
organization,
|
|
organization,
|
|
- members_shown: missingMembers.users.slice(0, MAX_MEMBERS_TO_SHOW).length,
|
|
|
|
|
|
+ members_shown: missingMembers.slice(0, MAX_MEMBERS_TO_SHOW).length,
|
|
});
|
|
});
|
|
}
|
|
}
|
|
- if (!isEligibleForBanner || !showBanner) {
|
|
|
|
|
|
+ if (!isEligibleForBanner || !showBanner || missingMembers.length === 0) {
|
|
return null;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -124,55 +143,26 @@ export function InviteBanner({
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
setSendingInvite(true);
|
|
setSendingInvite(true);
|
|
- await onSendInvite(email);
|
|
|
|
|
|
+ try {
|
|
|
|
+ await api.requestPromise(
|
|
|
|
+ `/organizations/${organization.slug}/members/?referrer=github_nudge_invite`,
|
|
|
|
+ {
|
|
|
|
+ method: 'POST',
|
|
|
|
+ data: {email},
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+ addSuccessMessage(tct('Sent invite to [email]', {email}));
|
|
|
|
+ onSendInvite();
|
|
|
|
+ const updatedMissingMembers = missingMembers.filter(
|
|
|
|
+ member => member.email !== email
|
|
|
|
+ );
|
|
|
|
+ setMissingMembers(updatedMissingMembers);
|
|
|
|
+ } catch {
|
|
|
|
+ addErrorMessage(t('Error sending invite'));
|
|
|
|
+ }
|
|
setSendingInvite(false);
|
|
setSendingInvite(false);
|
|
};
|
|
};
|
|
|
|
|
|
- const users = missingMembers.users;
|
|
|
|
-
|
|
|
|
- const cards = users.slice(0, MAX_MEMBERS_TO_SHOW).map(member => {
|
|
|
|
- const username = member.externalId.split(':').pop();
|
|
|
|
- return (
|
|
|
|
- <MemberCard
|
|
|
|
- key={member.externalId}
|
|
|
|
- data-test-id={`member-card-${member.externalId}`}
|
|
|
|
- >
|
|
|
|
- <MemberCardContent>
|
|
|
|
- <MemberCardContentRow>
|
|
|
|
- <IconGithub size="sm" />
|
|
|
|
- {/* TODO(cathy): create mapping from integration to lambda external link function */}
|
|
|
|
- <StyledExternalLink href={`https://github.com/${username}`}>
|
|
|
|
- @{username}
|
|
|
|
- </StyledExternalLink>
|
|
|
|
- </MemberCardContentRow>
|
|
|
|
- <MemberCardContentRow>
|
|
|
|
- <IconCommit size="xs" />
|
|
|
|
- {tct('[commitCount] Recent Commits', {commitCount: member.commitCount})}
|
|
|
|
- </MemberCardContentRow>
|
|
|
|
- <MemberEmail>{member.email}</MemberEmail>
|
|
|
|
- </MemberCardContent>
|
|
|
|
- <Button
|
|
|
|
- size="sm"
|
|
|
|
- onClick={() => handleSendInvite(member.email)}
|
|
|
|
- data-test-id="invite-missing-member"
|
|
|
|
- icon={<IconMail />}
|
|
|
|
- analyticsEventName="Github Invite Banner: Invite"
|
|
|
|
- analyticsEventKey="github_invite_banner.invite"
|
|
|
|
- >
|
|
|
|
- {t('Invite')}
|
|
|
|
- </Button>
|
|
|
|
- </MemberCard>
|
|
|
|
- );
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- cards.push(
|
|
|
|
- <SeeMoreCard
|
|
|
|
- key="see-more"
|
|
|
|
- missingMembers={missingMembers}
|
|
|
|
- openInviteModal={openInviteModal}
|
|
|
|
- />
|
|
|
|
- );
|
|
|
|
-
|
|
|
|
return (
|
|
return (
|
|
<StyledCard>
|
|
<StyledCard>
|
|
<CardTitleContainer>
|
|
<CardTitleContainer>
|
|
@@ -180,7 +170,7 @@ export function InviteBanner({
|
|
<CardTitle>{t('Bring your full GitHub team on board in Sentry')}</CardTitle>
|
|
<CardTitle>{t('Bring your full GitHub team on board in Sentry')}</CardTitle>
|
|
<Subtitle>
|
|
<Subtitle>
|
|
{tct('[missingMemberCount] missing members', {
|
|
{tct('[missingMemberCount] missing members', {
|
|
- missingMemberCount: users.length,
|
|
|
|
|
|
+ missingMemberCount: missingMembers.length,
|
|
})}
|
|
})}
|
|
<QuestionTooltip
|
|
<QuestionTooltip
|
|
title={t(
|
|
title={t(
|
|
@@ -211,47 +201,96 @@ export function InviteBanner({
|
|
/>
|
|
/>
|
|
</ButtonBar>
|
|
</ButtonBar>
|
|
</CardTitleContainer>
|
|
</CardTitleContainer>
|
|
- <Carousel>{cards}</Carousel>
|
|
|
|
|
|
+ <Carousel>
|
|
|
|
+ <MemberCards
|
|
|
|
+ missingMembers={missingMembers}
|
|
|
|
+ handleSendInvite={handleSendInvite}
|
|
|
|
+ openInviteModal={openInviteModal}
|
|
|
|
+ />
|
|
|
|
+ </Carousel>
|
|
</StyledCard>
|
|
</StyledCard>
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
export default withOrganization(InviteBanner);
|
|
export default withOrganization(InviteBanner);
|
|
|
|
|
|
-type SeeMoreCardProps = {
|
|
|
|
- missingMembers: {integration: string; users: MissingMember[]};
|
|
|
|
|
|
+type MemberCardsProps = {
|
|
|
|
+ handleSendInvite: (email: string) => void;
|
|
|
|
+ missingMembers: MissingMember[];
|
|
openInviteModal: () => void;
|
|
openInviteModal: () => void;
|
|
};
|
|
};
|
|
|
|
|
|
-function SeeMoreCard({missingMembers, openInviteModal}: SeeMoreCardProps) {
|
|
|
|
- const {users} = missingMembers;
|
|
|
|
-
|
|
|
|
|
|
+function MemberCards({
|
|
|
|
+ missingMembers,
|
|
|
|
+ handleSendInvite,
|
|
|
|
+ openInviteModal,
|
|
|
|
+}: MemberCardsProps) {
|
|
return (
|
|
return (
|
|
- <MemberCard data-test-id="see-more-card">
|
|
|
|
- <MemberCardContent>
|
|
|
|
- <MemberCardContentRow>
|
|
|
|
- <SeeMore>
|
|
|
|
- {tct('See all [missingMembersCount] missing members', {
|
|
|
|
- missingMembersCount: users.length,
|
|
|
|
|
|
+ <Fragment>
|
|
|
|
+ {missingMembers.slice(0, MAX_MEMBERS_TO_SHOW).map(member => {
|
|
|
|
+ const username = member.externalId.split(':').pop();
|
|
|
|
+ return (
|
|
|
|
+ <MemberCard
|
|
|
|
+ key={member.externalId}
|
|
|
|
+ data-test-id={`member-card-${member.externalId}`}
|
|
|
|
+ >
|
|
|
|
+ <MemberCardContent>
|
|
|
|
+ <MemberCardContentRow>
|
|
|
|
+ <IconGithub size="sm" />
|
|
|
|
+ {/* TODO(cathy): create mapping from integration to lambda external link function */}
|
|
|
|
+ <StyledExternalLink href={`https://github.com/${username}`}>
|
|
|
|
+ @{username}
|
|
|
|
+ </StyledExternalLink>
|
|
|
|
+ </MemberCardContentRow>
|
|
|
|
+ <MemberCardContentRow>
|
|
|
|
+ <IconCommit size="xs" />
|
|
|
|
+ {tct('[commitCount] Recent Commits', {commitCount: member.commitCount})}
|
|
|
|
+ </MemberCardContentRow>
|
|
|
|
+ <MemberEmail>{member.email}</MemberEmail>
|
|
|
|
+ </MemberCardContent>
|
|
|
|
+ <Button
|
|
|
|
+ size="sm"
|
|
|
|
+ onClick={() => handleSendInvite(member.email)}
|
|
|
|
+ data-test-id="invite-missing-member"
|
|
|
|
+ icon={<IconMail />}
|
|
|
|
+ analyticsEventName="Github Invite Banner: Invite"
|
|
|
|
+ analyticsEventKey="github_invite_banner.invite"
|
|
|
|
+ >
|
|
|
|
+ {t('Invite')}
|
|
|
|
+ </Button>
|
|
|
|
+ </MemberCard>
|
|
|
|
+ );
|
|
|
|
+ })}
|
|
|
|
+
|
|
|
|
+ <MemberCard data-test-id="see-more-card" key="see-more">
|
|
|
|
+ <MemberCardContent>
|
|
|
|
+ <MemberCardContentRow>
|
|
|
|
+ <SeeMore>
|
|
|
|
+ {tct('See all [missingMembersCount] missing members', {
|
|
|
|
+ missingMembersCount: missingMembers.length,
|
|
|
|
+ })}
|
|
|
|
+ </SeeMore>
|
|
|
|
+ </MemberCardContentRow>
|
|
|
|
+ <Subtitle>
|
|
|
|
+ {tct('Accounting for [totalCommits] recent commits', {
|
|
|
|
+ totalCommits: missingMembers.reduce(
|
|
|
|
+ (acc, curr) => acc + curr.commitCount,
|
|
|
|
+ 0
|
|
|
|
+ ),
|
|
})}
|
|
})}
|
|
- </SeeMore>
|
|
|
|
- </MemberCardContentRow>
|
|
|
|
- <Subtitle>
|
|
|
|
- {tct('Accounting for [totalCommits] recent commits', {
|
|
|
|
- totalCommits: users.reduce((acc, curr) => acc + curr.commitCount, 0),
|
|
|
|
- })}
|
|
|
|
- </Subtitle>
|
|
|
|
- </MemberCardContent>
|
|
|
|
- <Button
|
|
|
|
- size="sm"
|
|
|
|
- priority="primary"
|
|
|
|
- onClick={openInviteModal}
|
|
|
|
- analyticsEventName="Github Invite Banner: View All"
|
|
|
|
- analyticsEventKey="github_invite_banner.view_all"
|
|
|
|
- >
|
|
|
|
- {t('View All')}
|
|
|
|
- </Button>
|
|
|
|
- </MemberCard>
|
|
|
|
|
|
+ </Subtitle>
|
|
|
|
+ </MemberCardContent>
|
|
|
|
+ <Button
|
|
|
|
+ size="sm"
|
|
|
|
+ priority="primary"
|
|
|
|
+ onClick={openInviteModal}
|
|
|
|
+ analyticsEventName="Github Invite Banner: View All"
|
|
|
|
+ analyticsEventKey="github_invite_banner.view_all"
|
|
|
|
+ >
|
|
|
|
+ {t('View All')}
|
|
|
|
+ </Button>
|
|
|
|
+ </MemberCard>
|
|
|
|
+ </Fragment>
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|