|
@@ -1,6 +1,7 @@
|
|
|
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';
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
hasOrgRoleOverwrite,
|
|
|
RoleOverwritePanelAlert,
|
|
|
} from 'sentry/views/settings/organizationTeams/roleOverwriteWarning';
|
|
|
+import {getButtonHelpText} from 'sentry/views/settings/organizationTeams/utils';
|
|
|
|
|
|
type Props = {
|
|
|
/**
|
|
@@ -50,6 +52,11 @@ type Props = {
|
|
|
* 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
|
|
|
*/
|
|
@@ -78,6 +85,7 @@ type Props = {
|
|
|
|
|
|
function TeamSelect({
|
|
|
disabled,
|
|
|
+ isOrgOwner,
|
|
|
loadingTeams,
|
|
|
enforceIdpProvisioned,
|
|
|
menuHeader,
|
|
@@ -166,6 +174,7 @@ function TeamSelect({
|
|
|
confirmMessage={confirmMessage}
|
|
|
organization={organization}
|
|
|
team={team}
|
|
|
+ isOrgOwner={isOrgOwner ?? false}
|
|
|
selectedOrgRole={effectiveOrgRole}
|
|
|
selectedTeamRole={r.role}
|
|
|
onChangeTeamRole={onChangeTeamRole}
|
|
@@ -180,26 +189,31 @@ function TeamSelect({
|
|
|
// Only show options that aren't selected in the dropdown
|
|
|
const options = teams
|
|
|
.filter(team => !slugsToFilter.some(slug => slug === team.slug))
|
|
|
- .map((team, index) => ({
|
|
|
- index,
|
|
|
- value: team.slug,
|
|
|
- searchKey: team.slug,
|
|
|
- label: () => {
|
|
|
- if (enforceIdpProvisioned && team.flags['idp:provisioned']) {
|
|
|
- return (
|
|
|
- <Tooltip
|
|
|
- title={t(
|
|
|
- "Membership to this team is managed through your organization's identity provider."
|
|
|
- )}
|
|
|
- >
|
|
|
- <DropdownTeamBadgeDisabled avatarSize={18} team={team} />
|
|
|
- </Tooltip>
|
|
|
- );
|
|
|
- }
|
|
|
- return <DropdownTeamBadge avatarSize={18} team={team} />;
|
|
|
- },
|
|
|
- disabled: enforceIdpProvisioned && team.flags['idp:provisioned'],
|
|
|
- }));
|
|
|
+ .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 (
|
|
|
+ <Tooltip title={buttonHelpText}>
|
|
|
+ <DropdownTeamBadgeDisabled avatarSize={18} team={team} />
|
|
|
+ </Tooltip>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return <DropdownTeamBadge avatarSize={18} team={team} />;
|
|
|
+ },
|
|
|
+ disabled: disabled || isIdpProvisioned || (team.orgRole !== null && !isOrgOwner),
|
|
|
+ };
|
|
|
+ });
|
|
|
|
|
|
return (
|
|
|
<Panel>
|
|
@@ -273,6 +287,7 @@ const ProjectTeamRow = ({
|
|
|
|
|
|
type MemberTeamRowProps = {
|
|
|
enforceIdpProvisioned: boolean;
|
|
|
+ isOrgOwner: boolean;
|
|
|
onChangeTeamRole: Props['onChangeTeamRole'];
|
|
|
selectedOrgRole: Member['orgRole'];
|
|
|
selectedTeamRole: Member['teamRoles'][0]['role'];
|
|
@@ -285,6 +300,7 @@ const MemberTeamRow = ({
|
|
|
selectedTeamRole,
|
|
|
onRemoveTeam,
|
|
|
onChangeTeamRole,
|
|
|
+ isOrgOwner,
|
|
|
disabled,
|
|
|
confirmMessage,
|
|
|
enforceIdpProvisioned,
|
|
@@ -300,12 +316,22 @@ const MemberTeamRow = ({
|
|
|
? 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 (
|
|
|
<TeamPanelItem data-test-id="team-row-for-member">
|
|
|
<StyledLink to={`/settings/${organization.slug}/teams/${team.slug}/`}>
|
|
|
<TeamBadge team={team} />
|
|
|
</StyledLink>
|
|
|
|
|
|
+ <TeamOrgRole>{orgRoleFromTeam}</TeamOrgRole>
|
|
|
+
|
|
|
{organization.features.includes('team-roles') && onChangeTeamRole && (
|
|
|
<React.Fragment>
|
|
|
<StyledRoleSelectControl
|
|
@@ -323,19 +349,13 @@ const MemberTeamRow = ({
|
|
|
message={confirmMessage}
|
|
|
bypass={!confirmMessage}
|
|
|
onConfirm={() => onRemoveTeam(team.slug)}
|
|
|
- disabled={disabled || (enforceIdpProvisioned && team.flags['idp:provisioned'])}
|
|
|
+ disabled={isRemoveDisabled}
|
|
|
>
|
|
|
<Button
|
|
|
size="xs"
|
|
|
icon={<IconSubtract isCircled size="xs" />}
|
|
|
- disabled={disabled || (enforceIdpProvisioned && team.flags['idp:provisioned'])}
|
|
|
- title={
|
|
|
- enforceIdpProvisioned && team.flags['idp:provisioned']
|
|
|
- ? t(
|
|
|
- "Membership to this team is managed through your organization's identity provider."
|
|
|
- )
|
|
|
- : undefined
|
|
|
- }
|
|
|
+ disabled={isRemoveDisabled}
|
|
|
+ title={buttonHelpText}
|
|
|
>
|
|
|
{t('Remove')}
|
|
|
</Button>
|
|
@@ -364,7 +384,14 @@ const TeamPanelItem = styled(PanelItem)`
|
|
|
`;
|
|
|
|
|
|
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)`
|