mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
300 lines
7.4 KiB
TypeScript
300 lines
7.4 KiB
TypeScript
import {Component} from 'react';
|
|
import styled from '@emotion/styled';
|
|
|
|
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
|
|
import {fetchOrganizationDetails} from 'sentry/actionCreators/organizations';
|
|
import {joinTeam, leaveTeam} from 'sentry/actionCreators/teams';
|
|
import type {Client} from 'sentry/api';
|
|
import {Button} from 'sentry/components/button';
|
|
import IdBadge from 'sentry/components/idBadge';
|
|
import Link from 'sentry/components/links/link';
|
|
import PanelItem from 'sentry/components/panels/panelItem';
|
|
import {t, tct, tn} from 'sentry/locale';
|
|
import TeamStore from 'sentry/stores/teamStore';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {Organization, Team} from 'sentry/types';
|
|
import withApi from 'sentry/utils/withApi';
|
|
import {getButtonHelpText} from 'sentry/views/settings/organizationTeams/utils';
|
|
|
|
type Props = {
|
|
api: Client;
|
|
openMembership: boolean;
|
|
organization: Organization;
|
|
team: Team;
|
|
};
|
|
|
|
type State = {
|
|
error: boolean;
|
|
loading: boolean;
|
|
};
|
|
|
|
class AllTeamsRow extends Component<Props, State> {
|
|
state: State = {
|
|
loading: false,
|
|
error: false,
|
|
};
|
|
|
|
reloadProjects() {
|
|
const {api, organization} = this.props;
|
|
// After a change in teams has happened, refresh the project store
|
|
fetchOrganizationDetails(api, organization.slug, {
|
|
loadProjects: true,
|
|
});
|
|
}
|
|
|
|
handleRequestAccess = () => {
|
|
const {team} = this.props;
|
|
|
|
try {
|
|
this.joinTeam({
|
|
successMessage: tct('You have requested access to [team]', {
|
|
team: `#${team.slug}`,
|
|
}),
|
|
|
|
errorMessage: tct('Unable to request access to [team]', {
|
|
team: `#${team.slug}`,
|
|
}),
|
|
});
|
|
|
|
// Update team so that `isPending` is true
|
|
TeamStore.onUpdateSuccess(team.slug, {
|
|
...team,
|
|
isPending: true,
|
|
});
|
|
} catch (_err) {
|
|
// No need to do anything
|
|
}
|
|
};
|
|
|
|
handleJoinTeam = async () => {
|
|
const {team} = this.props;
|
|
|
|
await this.joinTeam({
|
|
successMessage: tct('You have joined [team]', {
|
|
team: `#${team.slug}`,
|
|
}),
|
|
errorMessage: tct('Unable to join [team]', {
|
|
team: `#${team.slug}`,
|
|
}),
|
|
});
|
|
|
|
this.reloadProjects();
|
|
};
|
|
|
|
joinTeam = ({
|
|
successMessage,
|
|
errorMessage,
|
|
}: {
|
|
errorMessage: React.ReactNode;
|
|
successMessage: React.ReactNode;
|
|
}) => {
|
|
const {api, organization, team} = this.props;
|
|
|
|
this.setState({
|
|
loading: true,
|
|
});
|
|
|
|
return new Promise<void>((resolve, reject) =>
|
|
joinTeam(
|
|
api,
|
|
{
|
|
orgId: organization.slug,
|
|
teamId: team.slug,
|
|
},
|
|
{
|
|
success: () => {
|
|
this.setState({
|
|
loading: false,
|
|
error: false,
|
|
});
|
|
addSuccessMessage(successMessage);
|
|
resolve();
|
|
},
|
|
error: () => {
|
|
this.setState({
|
|
loading: false,
|
|
error: true,
|
|
});
|
|
addErrorMessage(errorMessage);
|
|
reject(new Error('Unable to join team'));
|
|
},
|
|
}
|
|
)
|
|
);
|
|
};
|
|
|
|
handleLeaveTeam = () => {
|
|
const {api, organization, team} = this.props;
|
|
|
|
this.setState({
|
|
loading: true,
|
|
});
|
|
|
|
leaveTeam(
|
|
api,
|
|
{
|
|
orgId: organization.slug,
|
|
teamId: team.slug,
|
|
},
|
|
{
|
|
success: () => {
|
|
this.setState({
|
|
loading: false,
|
|
error: false,
|
|
});
|
|
addSuccessMessage(
|
|
tct('You have left [team]', {
|
|
team: `#${team.slug}`,
|
|
})
|
|
);
|
|
|
|
// Reload ProjectsStore
|
|
this.reloadProjects();
|
|
},
|
|
error: () => {
|
|
this.setState({
|
|
loading: false,
|
|
error: true,
|
|
});
|
|
addErrorMessage(
|
|
tct('Unable to leave [team]', {
|
|
team: `#${team.slug}`,
|
|
})
|
|
);
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
getTeamRoleName = () => {
|
|
const {organization, team} = this.props;
|
|
const {teamRoleList} = organization;
|
|
const roleName = teamRoleList.find(r => r.id === team.teamRole)?.name;
|
|
|
|
return roleName;
|
|
};
|
|
|
|
render() {
|
|
const {team, openMembership, organization} = this.props;
|
|
const {access} = organization;
|
|
const urlPrefix = `/settings/${organization.slug}/teams/`;
|
|
const canEditTeam = access.includes('org:write') || access.includes('team:admin');
|
|
|
|
// TODO(team-roles): team admins can also manage membership
|
|
// org:admin is a unique scope that only org owners have
|
|
const isOrgOwner = access.includes('org:admin');
|
|
const isPermissionGroup = !canEditTeam || !isOrgOwner;
|
|
const isIdpProvisioned = team.flags['idp:provisioned'];
|
|
|
|
const buttonHelpText = getButtonHelpText(isIdpProvisioned, isPermissionGroup);
|
|
|
|
const display = (
|
|
<IdBadge
|
|
team={team}
|
|
avatarSize={36}
|
|
description={tn('%s Member', '%s Members', team.memberCount)}
|
|
/>
|
|
);
|
|
|
|
// You can only view team details if you have access to team -- this should account
|
|
// for your role + org open membership
|
|
const canViewTeam = team.hasAccess;
|
|
|
|
const teamRoleName = this.getTeamRoleName();
|
|
const isDisabled = isIdpProvisioned || isPermissionGroup;
|
|
|
|
return (
|
|
<TeamPanelItem>
|
|
<div>
|
|
{canViewTeam ? (
|
|
<TeamLink data-test-id="team-link" to={`${urlPrefix}${team.slug}/`}>
|
|
{display}
|
|
</TeamLink>
|
|
) : (
|
|
display
|
|
)}
|
|
</div>
|
|
<DisplayRole isHidden={teamRoleName === null}>{teamRoleName}</DisplayRole>
|
|
<div>
|
|
{this.state.loading ? (
|
|
<Button size="sm" disabled>
|
|
...
|
|
</Button>
|
|
) : team.isMember ? (
|
|
<Button
|
|
size="sm"
|
|
onClick={this.handleLeaveTeam}
|
|
disabled={isDisabled}
|
|
title={buttonHelpText}
|
|
>
|
|
{t('Leave Team')}
|
|
</Button>
|
|
) : team.isPending ? (
|
|
<Button
|
|
size="sm"
|
|
disabled
|
|
title={t(
|
|
'Your request to join this team is being reviewed by organization owners'
|
|
)}
|
|
>
|
|
{t('Request Pending')}
|
|
</Button>
|
|
) : openMembership ? (
|
|
<Button
|
|
size="sm"
|
|
onClick={this.handleJoinTeam}
|
|
disabled={isDisabled}
|
|
title={buttonHelpText}
|
|
>
|
|
{t('Join Team')}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
size="sm"
|
|
onClick={this.handleRequestAccess}
|
|
disabled={isDisabled}
|
|
title={buttonHelpText}
|
|
>
|
|
{t('Request Access')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</TeamPanelItem>
|
|
);
|
|
}
|
|
}
|
|
|
|
const TeamLink = styled(Link)`
|
|
display: inline-block;
|
|
|
|
&:focus-visible {
|
|
margin: -${space(1)};
|
|
padding: ${space(1)};
|
|
background: #f2eff5;
|
|
border-radius: 3px;
|
|
outline: none;
|
|
}
|
|
`;
|
|
|
|
export {AllTeamsRow};
|
|
export default withApi(AllTeamsRow);
|
|
|
|
export const GRID_TEMPLATE = `
|
|
display: grid;
|
|
grid-template-columns: minmax(150px, 4fr) minmax(0px, 100px) 125px minmax(150px, 1fr);
|
|
gap: ${space(1)};
|
|
`;
|
|
|
|
const TeamPanelItem = styled(PanelItem)`
|
|
${GRID_TEMPLATE}
|
|
align-items: center;
|
|
|
|
> div:last-child {
|
|
margin-left: auto;
|
|
}
|
|
`;
|
|
|
|
const DisplayRole = styled('div')<{isHidden: boolean}>`
|
|
display: ${props => (props.isHidden ? 'none' : 'block')};
|
|
`;
|