organizations.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import type {NavigateFunction} from 'react-router-dom';
  2. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  3. import {resetPageFilters} from 'sentry/actionCreators/pageFilters';
  4. import type {Client} from 'sentry/api';
  5. import {USING_CUSTOMER_DOMAIN} from 'sentry/constants';
  6. import ConfigStore from 'sentry/stores/configStore';
  7. import GuideStore from 'sentry/stores/guideStore';
  8. import LatestContextStore from 'sentry/stores/latestContextStore';
  9. import OrganizationsStore from 'sentry/stores/organizationsStore';
  10. import OrganizationStore from 'sentry/stores/organizationStore';
  11. import ProjectsStore from 'sentry/stores/projectsStore';
  12. import TeamStore from 'sentry/stores/teamStore';
  13. import type {Organization} from 'sentry/types/organization';
  14. import {browserHistory} from 'sentry/utils/browserHistory';
  15. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  16. type RedirectRemainingOrganizationParams = {
  17. /**
  18. * The organization slug
  19. */
  20. orgId: string;
  21. /**
  22. * navigate function from useNavigate
  23. */
  24. navigate?: NavigateFunction;
  25. /**
  26. * Should remove org?
  27. */
  28. removeOrg?: boolean;
  29. };
  30. /**
  31. * After removing an organization, this will redirect to a remaining active organization or
  32. * the screen to create a new organization.
  33. *
  34. * Can optionally remove organization from organizations store.
  35. */
  36. export function redirectToRemainingOrganization({
  37. navigate,
  38. orgId,
  39. removeOrg,
  40. }: RedirectRemainingOrganizationParams) {
  41. // Remove queued, should redirect
  42. const allOrgs = OrganizationsStore.getAll().filter(
  43. org => org.status.id === 'active' && org.slug !== orgId
  44. );
  45. if (!allOrgs.length) {
  46. if (navigate) {
  47. navigate('/organizations/new/');
  48. } else {
  49. browserHistory.push('/organizations/new/');
  50. }
  51. return;
  52. }
  53. // Let's be smart and select the best org to redirect to
  54. const firstRemainingOrg = allOrgs[0]!;
  55. const route = `/organizations/${firstRemainingOrg.slug}/issues/`;
  56. if (USING_CUSTOMER_DOMAIN) {
  57. const {organizationUrl} = firstRemainingOrg.links;
  58. window.location.assign(`${organizationUrl}${normalizeUrl(route)}`);
  59. return;
  60. }
  61. if (navigate) {
  62. navigate(route);
  63. } else {
  64. browserHistory.push(route);
  65. }
  66. // Remove org from SidebarDropdown
  67. if (removeOrg) {
  68. OrganizationsStore.remove(orgId);
  69. }
  70. }
  71. type RemoveParams = {
  72. /**
  73. * The organization slug
  74. */
  75. orgId: string;
  76. /**
  77. * An optional error message to be used in a toast, if remove fails
  78. */
  79. errorMessage?: string;
  80. /**
  81. * An optional success message to be used in a toast, if remove succeeds
  82. */
  83. successMessage?: string;
  84. };
  85. export function remove(api: Client, {successMessage, errorMessage, orgId}: RemoveParams) {
  86. const endpoint = `/organizations/${orgId}/`;
  87. return api
  88. .requestPromise(endpoint, {
  89. method: 'DELETE',
  90. })
  91. .then(() => {
  92. OrganizationsStore.onRemoveSuccess(orgId);
  93. if (successMessage) {
  94. addSuccessMessage(successMessage);
  95. }
  96. })
  97. .catch(() => {
  98. if (errorMessage) {
  99. addErrorMessage(errorMessage);
  100. }
  101. });
  102. }
  103. export function switchOrganization() {
  104. resetPageFilters();
  105. }
  106. export function removeAndRedirectToRemainingOrganization(
  107. api: Client,
  108. params: RedirectRemainingOrganizationParams & RemoveParams
  109. ) {
  110. remove(api, params).then(() => redirectToRemainingOrganization(params));
  111. }
  112. /**
  113. * Set active organization
  114. */
  115. export function setActiveOrganization(org: Organization) {
  116. GuideStore.setActiveOrganization(org);
  117. LatestContextStore.onSetActiveOrganization(org);
  118. }
  119. export function changeOrganizationSlug(
  120. prev: Organization,
  121. next: Partial<Organization> & Pick<Organization, 'slug'>
  122. ) {
  123. OrganizationsStore.onChangeSlug(prev, next);
  124. }
  125. /**
  126. * Updates an organization for the store
  127. *
  128. * Accepts a partial organization as it will merge will existing organization
  129. */
  130. export function updateOrganization(org: Partial<Organization>) {
  131. OrganizationsStore.onUpdate(org);
  132. OrganizationStore.onUpdate(org);
  133. }
  134. type FetchOrganizationByMemberParams = {
  135. addOrg?: boolean;
  136. fetchOrgDetails?: boolean;
  137. };
  138. export async function fetchOrganizationByMember(
  139. api: Client,
  140. memberId: string,
  141. {addOrg, fetchOrgDetails}: FetchOrganizationByMemberParams
  142. ) {
  143. const data = await api.requestPromise(`/organizations/?query=member_id:${memberId}`);
  144. if (!data.length) {
  145. return null;
  146. }
  147. const org = data[0];
  148. if (addOrg) {
  149. // add org to SwitchOrganization dropdown
  150. OrganizationsStore.addOrReplace(org);
  151. }
  152. if (fetchOrgDetails) {
  153. // load SidebarDropdown with org details including `access`
  154. await fetchOrganizationDetails(api, org.slug, {setActive: true, loadProjects: true});
  155. }
  156. return org;
  157. }
  158. type FetchOrganizationDetailsParams = {
  159. /**
  160. * Should load projects in ProjectsStore
  161. */
  162. loadProjects?: boolean;
  163. /**
  164. * Should load teams in TeamStore?
  165. */
  166. loadTeam?: boolean;
  167. /**
  168. * Should set as active organization?
  169. */
  170. setActive?: boolean;
  171. };
  172. export async function fetchOrganizationDetails(
  173. api: Client,
  174. orgId: string,
  175. {setActive, loadProjects, loadTeam}: FetchOrganizationDetailsParams
  176. ) {
  177. const data = await api.requestPromise(`/organizations/${orgId}/`, {
  178. query: {
  179. include_feature_flags: 1,
  180. },
  181. });
  182. if (setActive) {
  183. setActiveOrganization(data);
  184. }
  185. if (loadTeam) {
  186. TeamStore.loadInitialData(data.teams, false, null);
  187. }
  188. if (loadProjects) {
  189. ProjectsStore.loadInitialData(data.projects || []);
  190. }
  191. return data;
  192. }
  193. /**
  194. * Get all organizations for the current user.
  195. *
  196. * Will perform a fan-out across all multi-tenant regions,
  197. * and single-tenant regions the user has membership in.
  198. *
  199. * This function is challenging to type as the structure of the response
  200. * from /organizations can vary based on query parameters
  201. */
  202. export async function fetchOrganizations(api: Client, query?: Record<string, any>) {
  203. const regions = ConfigStore.get('memberRegions');
  204. const results = await Promise.all(
  205. regions.map(region =>
  206. api.requestPromise(`/organizations/`, {
  207. host: region.url,
  208. query,
  209. // Authentication errors can happen as we span regions.
  210. allowAuthError: true,
  211. })
  212. )
  213. );
  214. return results.reduce((acc, response) => {
  215. // Don't append error results to the org list.
  216. if (response[0]) {
  217. acc = acc.concat(response);
  218. }
  219. return acc;
  220. }, []);
  221. }