organizations.tsx 6.0 KB

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