import React from 'react';
import styled from '@emotion/styled';
import debounce from 'lodash/debounce';
import startCase from 'lodash/startCase';
import {Button} from 'sentry/components/button';
import Confirm from 'sentry/components/confirm';
import DropdownAutoComplete from 'sentry/components/dropdownAutoComplete';
import {Item} from 'sentry/components/dropdownAutoComplete/types';
import DropdownButton from 'sentry/components/dropdownButton';
import EmptyMessage from 'sentry/components/emptyMessage';
import {TeamBadge} from 'sentry/components/idBadge/teamBadge';
import Link from 'sentry/components/links/link';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import {Panel, PanelBody, PanelHeader, PanelItem} from 'sentry/components/panels';
import RoleSelectControl from 'sentry/components/roleSelectControl';
import {Tooltip} from 'sentry/components/tooltip';
import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
import {IconSubtract} from 'sentry/icons';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {Member, Organization, Team} from 'sentry/types';
import {getEffectiveOrgRole} from 'sentry/utils/orgRole';
import useTeams from 'sentry/utils/useTeams';
import {
hasOrgRoleOverwrite,
RoleOverwritePanelAlert,
} from 'sentry/views/settings/organizationTeams/roleOverwriteWarning';
import {getButtonHelpText} from 'sentry/views/settings/organizationTeams/utils';
type Props = {
/**
* Should button be disabled
*/
disabled: boolean;
/**
* Used when showing Teams for a Member
* Prevent changes to a SCIM-provisioned member
*/
enforceIdpProvisioned: boolean;
/**
* callback when teams are added
*/
onAddTeam: (teamSlug: string) => void;
/**
* Callback when teams are removed
*/
onRemoveTeam: (teamSlug: string) => void;
organization: Organization;
/**
* Message to display when the last team is removed
* if empty no confirm will be displayed.
*/
confirmLastTeamRemoveMessage?: string;
/**
* Allow adding to teams with org role
* if the user is an org owner
*/
isOrgOwner?: boolean;
/**
* Used to determine whether we should show a loading state while waiting for teams
*/
loadingTeams?: boolean;
/**
* Optional menu header.
*/
menuHeader?: React.ReactElement;
/**
* Used when showing Teams for a Member
*/
onChangeTeamRole?: (teamSlug: string, teamRole: string) => void;
/**
* Used when showing Teams for a Member
*/
selectedOrgRole?: Member['orgRole'];
/**
* Used when showing Teams for a Member
*/
selectedTeamRoles?: Member['teamRoles'];
/**
* Used when showing Teams for a Project
*/
selectedTeams?: Team[];
};
function TeamSelect({
disabled,
isOrgOwner,
loadingTeams,
enforceIdpProvisioned,
menuHeader,
confirmLastTeamRemoveMessage,
selectedOrgRole,
selectedTeamRoles,
selectedTeams,
organization,
onAddTeam,
onRemoveTeam,
onChangeTeamRole,
}: Props) {
const {teams, onSearch, fetching} = useTeams();
const {orgRoleList, teamRoleList} = organization;
const slugsToFilter: string[] =
selectedTeams?.map(tm => tm.slug) || selectedTeamRoles?.map(tm => tm.teamSlug) || [];
// Determine if adding a team changes the minimum team-role
// Get org roles from team membership, if any
const orgRolesFromTeams = teams
.filter(team => slugsToFilter.includes(team.slug) && team.orgRole)
.map(team => team.orgRole as string);
if (selectedOrgRole) {
orgRolesFromTeams.push(selectedOrgRole);
}
// Sort them and to get the highest priority role
// Highest prio role may change minimum team role
const effectiveOrgRole = getEffectiveOrgRole(orgRolesFromTeams, orgRoleList)?.id;
const renderBody = () => {
const numTeams = selectedTeams?.length || selectedTeamRoles?.length;
if (numTeams === 0) {
return {t('No Teams assigned')};
}
const confirmMessage =
numTeams === 1 && confirmLastTeamRemoveMessage
? confirmLastTeamRemoveMessage
: null;
return (
{organization.features.includes('team-roles') && effectiveOrgRole && (
)}
{selectedTeams &&
selectedTeams.map(team => (
onRemoveTeam(slug)}
/>
))}
{effectiveOrgRole &&
selectedTeamRoles &&
/**
* "Map + Find" operation is O(n * n), leaving it as it us because it is unlikely to cause performance issues because a Member is unlikely to be in 1000+ teams
*/
selectedTeamRoles.map(r => {
const team = teams.find(tm => tm.slug === r.teamSlug);
if (!team) {
return (
{tct(`Cannot find #[slug]`, {slug: r.teamSlug})}
);
}
return (
onRemoveTeam(slug)}
/>
);
})}
);
};
// Only show options that aren't selected in the dropdown
const options = teams
.filter(team => !slugsToFilter.some(slug => slug === team.slug))
.map((team, index) => {
const isIdpProvisioned = enforceIdpProvisioned && team.flags['idp:provisioned'];
return {
index,
value: team.slug,
searchKey: team.slug,
label: () => {
// TODO(team-roles): team admins can also manage membership
const isPermissionGroup = team.orgRole !== null && !isOrgOwner;
const buttonHelpText = getButtonHelpText(isIdpProvisioned, isPermissionGroup);
if (isIdpProvisioned || isPermissionGroup) {
return (
);
}
return ;
},
disabled: disabled || isIdpProvisioned || (team.orgRole !== null && !isOrgOwner),
};
});
return (
{t('Team')}
) => void>(
e => onSearch(e.target.value),
DEFAULT_DEBOUNCE_DURATION
)}
onSelect={(option: Item) => onAddTeam(option.value)}
emptyMessage={t('No teams')}
menuHeader={menuHeader}
disabled={disabled}
alignMenu="right"
>
{({isOpen}) => (
{t('Add Team')}
)}
{loadingTeams ? : renderBody()}
);
}
type TeamRowProps = {
confirmMessage: string | null;
disabled: boolean;
onRemoveTeam: Props['onRemoveTeam'];
organization: Organization;
team: Team;
};
type ProjectTeamRowProps = {} & TeamRowProps;
const ProjectTeamRow = ({
organization,
team,
onRemoveTeam,
disabled,
confirmMessage,
}: ProjectTeamRowProps) => (
onRemoveTeam(team.slug)}
disabled={disabled}
>
} disabled={disabled}>
{t('Remove')}
);
type MemberTeamRowProps = {
enforceIdpProvisioned: boolean;
isOrgOwner: boolean;
onChangeTeamRole: Props['onChangeTeamRole'];
selectedOrgRole: Member['orgRole'];
selectedTeamRole: Member['teamRoles'][0]['role'];
} & TeamRowProps;
const MemberTeamRow = ({
organization,
team,
selectedOrgRole,
selectedTeamRole,
onRemoveTeam,
onChangeTeamRole,
isOrgOwner,
disabled,
confirmMessage,
enforceIdpProvisioned,
}: MemberTeamRowProps) => {
const {teamRoleList, orgRoleList} = organization;
const isRoleOverwritten = hasOrgRoleOverwrite({
orgRole: selectedOrgRole,
orgRoleList,
teamRoleList,
});
const teamRoleObj = isRoleOverwritten
? teamRoleList[1] // set as team admin
: teamRoleList.find(r => r.id === selectedTeamRole) || teamRoleList[0];
const orgRoleFromTeam = team.orgRole ? `${startCase(team.orgRole)} Team` : null;
const isIdpProvisioned = enforceIdpProvisioned && team.flags['idp:provisioned'];
const isPermissionGroup = team.orgRole !== null && !isOrgOwner;
const isRemoveDisabled = disabled || isIdpProvisioned || isPermissionGroup;
const buttonHelpText = getButtonHelpText(isIdpProvisioned, isPermissionGroup);
return (
{orgRoleFromTeam}
{organization.features.includes('team-roles') && onChangeTeamRole && (
onChangeTeamRole(team.slug, option.value)}
/>
)}
onRemoveTeam(team.slug)}
disabled={isRemoveDisabled}
>
}
disabled={isRemoveDisabled}
title={buttonHelpText}
>
{t('Remove')}
);
};
const DropdownTeamBadge = styled(TeamBadge)`
font-weight: normal;
font-size: ${p => p.theme.fontSizeMedium};
text-transform: none;
`;
const DropdownTeamBadgeDisabled = styled(TeamBadge)`
font-weight: normal;
font-size: ${p => p.theme.fontSizeMedium};
text-transform: none;
filter: grayscale(1);
`;
const TeamPanelItem = styled(PanelItem)`
padding: ${space(2)};
align-items: center;
justify-content: space-between;
`;
const StyledLink = styled(Link)`
flex-grow: 4;
`;
const TeamOrgRole = styled('div')`
min-width: 90px;
flex-grow: 1;
display: flex;
justify-content: center;
`;
const StyledRoleSelectControl = styled(RoleSelectControl)`
min-width: 200px;
margin-right: ${space(2)};
`;
export default TeamSelect;