organizationTeams.tsx 4.6 KB

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