organizationTeams.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import {useState} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import debounce from 'lodash/debounce';
  5. import partition from 'lodash/partition';
  6. import {openCreateTeamModal} from 'sentry/actionCreators/modal';
  7. import {Button} from 'sentry/components/button';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import Panel from 'sentry/components/panels/panel';
  10. import PanelBody from 'sentry/components/panels/panelBody';
  11. import PanelHeader from 'sentry/components/panels/panelHeader';
  12. import SearchBar from 'sentry/components/searchBar';
  13. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  14. import {TeamRoleColumnLabel} from 'sentry/components/teamRoleUtils';
  15. import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
  16. import {IconAdd} from 'sentry/icons';
  17. import {t} from 'sentry/locale';
  18. import {space} from 'sentry/styles/space';
  19. import {AccessRequest, Organization} from 'sentry/types';
  20. import {useTeams} from 'sentry/utils/useTeams';
  21. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  22. import {RoleOverwritePanelAlert} from 'sentry/views/settings/organizationTeams/roleOverwriteWarning';
  23. import AllTeamsList from './allTeamsList';
  24. import {GRID_TEMPLATE} from './allTeamsRow';
  25. import OrganizationAccessRequests from './organizationAccessRequests';
  26. type Props = {
  27. access: Set<string>;
  28. features: Set<string>;
  29. onRemoveAccessRequest: (id: string, isApproved: boolean) => void;
  30. organization: Organization;
  31. requestList: AccessRequest[];
  32. } & RouteComponentProps<{}, {}>;
  33. function OrganizationTeams({
  34. organization,
  35. access,
  36. features,
  37. requestList,
  38. onRemoveAccessRequest,
  39. }: Props) {
  40. const [teamQuery, setTeamQuery] = useState('');
  41. const {initiallyLoaded} = useTeams({provideUserTeams: true});
  42. const {teams, onSearch, loadMore, hasMore, fetching} = useTeams();
  43. if (!organization) {
  44. return null;
  45. }
  46. const canCreateTeams = access.has('project:admin');
  47. const action = (
  48. <Button
  49. priority="primary"
  50. size="sm"
  51. disabled={!canCreateTeams}
  52. title={
  53. !canCreateTeams ? t('You do not have permission to create teams') : undefined
  54. }
  55. onClick={() =>
  56. openCreateTeamModal({
  57. organization,
  58. })
  59. }
  60. icon={<IconAdd isCircled />}
  61. >
  62. {t('Create Team')}
  63. </Button>
  64. );
  65. const title = t('Teams');
  66. const debouncedSearch = debounce(onSearch, DEFAULT_DEBOUNCE_DURATION);
  67. function handleSearch(query: string) {
  68. setTeamQuery(query);
  69. debouncedSearch(query);
  70. }
  71. const {slug: orgSlug, orgRole, orgRoleList, teamRoleList} = organization;
  72. const filteredTeams = teams.filter(team =>
  73. `#${team.slug}`.toLowerCase().includes(teamQuery.toLowerCase())
  74. );
  75. const [userTeams, otherTeams] = partition(filteredTeams, team => team.isMember);
  76. return (
  77. <div data-test-id="team-list">
  78. <SentryDocumentTitle title={title} orgSlug={orgSlug} />
  79. <SettingsPageHeader title={title} action={action} />
  80. <OrganizationAccessRequests
  81. orgSlug={organization.slug}
  82. requestList={requestList}
  83. onRemoveAccessRequest={onRemoveAccessRequest}
  84. />
  85. <StyledSearchBar
  86. placeholder={t('Search teams')}
  87. onChange={handleSearch}
  88. query={teamQuery}
  89. />
  90. <Panel>
  91. <StyledPanelHeader>
  92. <div>{t('Your Teams')}</div>
  93. <div />
  94. <div>
  95. <TeamRoleColumnLabel />
  96. </div>
  97. <div />
  98. </StyledPanelHeader>
  99. <PanelBody>
  100. <RoleOverwritePanelAlert
  101. orgRole={orgRole}
  102. orgRoleList={orgRoleList}
  103. teamRoleList={teamRoleList}
  104. isSelf
  105. />
  106. {initiallyLoaded ? (
  107. <AllTeamsList
  108. organization={organization}
  109. teamList={userTeams.filter(team => team.slug.includes(teamQuery))}
  110. access={access}
  111. openMembership={false}
  112. />
  113. ) : (
  114. <LoadingIndicator />
  115. )}
  116. </PanelBody>
  117. </Panel>
  118. <Panel>
  119. <PanelHeader>{t('Other Teams')}</PanelHeader>
  120. <PanelBody>
  121. <AllTeamsList
  122. organization={organization}
  123. teamList={otherTeams}
  124. access={access}
  125. openMembership={
  126. !!(features.has('open-membership') || access.has('org:write'))
  127. }
  128. />
  129. </PanelBody>
  130. </Panel>
  131. {hasMore && (
  132. <LoadMoreWrapper>
  133. {fetching && <LoadingIndicator mini />}
  134. <Button onClick={() => loadMore(teamQuery)}>{t('Show more')}</Button>
  135. </LoadMoreWrapper>
  136. )}
  137. </div>
  138. );
  139. }
  140. const StyledSearchBar = styled(SearchBar)`
  141. margin-bottom: ${space(2)};
  142. `;
  143. const StyledPanelHeader = styled(PanelHeader)`
  144. ${GRID_TEMPLATE}
  145. `;
  146. const LoadMoreWrapper = styled('div')`
  147. display: grid;
  148. gap: ${space(2)};
  149. align-items: center;
  150. justify-content: end;
  151. grid-auto-flow: column;
  152. `;
  153. export default OrganizationTeams;