organizationTeams.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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 {RoleOverwritePanelAlert} from 'sentry/views/settings/organizationTeams/roleOverwriteWarning';
  21. import AllTeamsList from './allTeamsList';
  22. import OrganizationAccessRequests from './organizationAccessRequests';
  23. type Props = {
  24. access: Set<string>;
  25. features: Set<string>;
  26. onRemoveAccessRequest: (id: string, isApproved: boolean) => void;
  27. organization: Organization;
  28. requestList: AccessRequest[];
  29. } & RouteComponentProps<{orgId: string}, {}>;
  30. function OrganizationTeams({
  31. organization,
  32. access,
  33. features,
  34. routes,
  35. params,
  36. requestList,
  37. onRemoveAccessRequest,
  38. }: Props) {
  39. const [teamQuery, setTeamQuery] = useState('');
  40. const {initiallyLoaded} = useTeams({provideUserTeams: true});
  41. const {teams, onSearch, loadMore, hasMore, fetching} = useTeams();
  42. if (!organization) {
  43. return null;
  44. }
  45. const canCreateTeams = access.has('project:admin');
  46. const action = (
  47. <Button
  48. priority="primary"
  49. size="sm"
  50. disabled={!canCreateTeams}
  51. title={
  52. !canCreateTeams ? t('You do not have permission to create teams') : undefined
  53. }
  54. onClick={() =>
  55. openCreateTeamModal({
  56. organization,
  57. })
  58. }
  59. icon={<IconAdd size="xs" isCircled />}
  60. >
  61. {t('Create Team')}
  62. </Button>
  63. );
  64. const teamRoute = routes.find(({path}) => path === 'teams/');
  65. const urlPrefix = teamRoute
  66. ? recreateRoute(teamRoute, {routes, params, stepBack: -2})
  67. : '';
  68. const title = t('Teams');
  69. const debouncedSearch = debounce(onSearch, DEFAULT_DEBOUNCE_DURATION);
  70. function handleSearch(query: string) {
  71. setTeamQuery(query);
  72. debouncedSearch(query);
  73. }
  74. const {slug: orgSlug, orgRole, orgRoleList, teamRoleList} = organization;
  75. const filteredTeams = teams.filter(team =>
  76. `#${team.slug}`.toLowerCase().includes(teamQuery.toLowerCase())
  77. );
  78. const [userTeams, otherTeams] = partition(filteredTeams, team => team.isMember);
  79. return (
  80. <div data-test-id="team-list">
  81. <SentryDocumentTitle title={title} orgSlug={orgSlug} />
  82. <SettingsPageHeader title={title} action={action} />
  83. <OrganizationAccessRequests
  84. orgId={params.orgId}
  85. requestList={requestList}
  86. onRemoveAccessRequest={onRemoveAccessRequest}
  87. />
  88. <StyledSearchBar
  89. placeholder={t('Search teams')}
  90. onChange={handleSearch}
  91. query={teamQuery}
  92. />
  93. <Panel>
  94. <PanelHeader>{t('Your Teams')}</PanelHeader>
  95. <PanelBody>
  96. {features.has('team-roles') && (
  97. <RoleOverwritePanelAlert
  98. orgRole={orgRole}
  99. orgRoleList={orgRoleList}
  100. teamRoleList={teamRoleList}
  101. isSelf
  102. />
  103. )}
  104. {initiallyLoaded ? (
  105. <AllTeamsList
  106. urlPrefix={urlPrefix}
  107. organization={organization}
  108. teamList={userTeams.filter(team => team.slug.includes(teamQuery))}
  109. access={access}
  110. openMembership={false}
  111. />
  112. ) : (
  113. <LoadingIndicator />
  114. )}
  115. </PanelBody>
  116. </Panel>
  117. <Panel>
  118. <PanelHeader>{t('Other Teams')}</PanelHeader>
  119. <PanelBody>
  120. <AllTeamsList
  121. urlPrefix={urlPrefix}
  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 LoadMoreWrapper = styled('div')`
  144. display: grid;
  145. gap: ${space(2)};
  146. align-items: center;
  147. justify-content: end;
  148. grid-auto-flow: column;
  149. `;
  150. export default OrganizationTeams;