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