organizationContextContainer.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. import {Component, Fragment} from 'react';
  2. import {PlainRoute, RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import * as Sentry from '@sentry/react';
  5. import {openSudo} from 'sentry/actionCreators/modal';
  6. import {fetchOrganizationDetails} from 'sentry/actionCreators/organization';
  7. import ProjectActions from 'sentry/actions/projectActions';
  8. import {Client} from 'sentry/api';
  9. import Alert from 'sentry/components/alert';
  10. import LoadingError from 'sentry/components/loadingError';
  11. import LoadingTriangle from 'sentry/components/loadingTriangle';
  12. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  13. import {SentryReplayInit} from 'sentry/components/sentryReplayInit';
  14. import Sidebar from 'sentry/components/sidebar';
  15. import {ORGANIZATION_FETCH_ERROR_TYPES} from 'sentry/constants';
  16. import {t} from 'sentry/locale';
  17. import SentryTypes from 'sentry/sentryTypes';
  18. import ConfigStore from 'sentry/stores/configStore';
  19. import HookStore from 'sentry/stores/hookStore';
  20. import OrganizationStore from 'sentry/stores/organizationStore';
  21. import space from 'sentry/styles/space';
  22. import {Organization} from 'sentry/types';
  23. import {metric} from 'sentry/utils/analytics';
  24. import {callIfFunction} from 'sentry/utils/callIfFunction';
  25. import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
  26. import RequestError from 'sentry/utils/requestError/requestError';
  27. import withApi from 'sentry/utils/withApi';
  28. import withOrganizations from 'sentry/utils/withOrganizations';
  29. import {OrganizationContext} from './organizationContext';
  30. type Props = RouteComponentProps<{orgId: string}, {}> & {
  31. api: Client;
  32. includeSidebar: boolean;
  33. organizations: Organization[];
  34. organizationsLoading: boolean;
  35. routes: PlainRoute[];
  36. useLastOrganization: boolean;
  37. };
  38. type State = {
  39. loading: boolean;
  40. organization: Organization | null;
  41. prevProps: {
  42. location: RouteComponentProps<{orgId: string}, {}>['location'];
  43. orgId: string;
  44. organizationsLoading: boolean;
  45. };
  46. dirty?: boolean;
  47. error?: RequestError | null;
  48. errorType?: string | null;
  49. hooks?: React.ReactNode[];
  50. };
  51. class OrganizationContextContainer extends Component<Props, State> {
  52. static getDerivedStateFromProps(props: Readonly<Props>, prevState: State): State {
  53. const {prevProps} = prevState;
  54. if (OrganizationContextContainer.shouldRemount(prevProps, props)) {
  55. return OrganizationContextContainer.getDefaultState(props);
  56. }
  57. const {organizationsLoading, location, params} = props;
  58. const {orgId} = params;
  59. return {
  60. ...prevState,
  61. prevProps: {
  62. orgId,
  63. organizationsLoading,
  64. location,
  65. },
  66. };
  67. }
  68. static shouldRemount(prevProps: State['prevProps'], props: Props): boolean {
  69. const hasOrgIdAndChanged =
  70. prevProps.orgId && props.params.orgId && prevProps.orgId !== props.params.orgId;
  71. const hasOrgId =
  72. props.params.orgId ||
  73. (props.useLastOrganization && ConfigStore.get('lastOrganization'));
  74. // protect against the case where we finish fetching org details
  75. // and then `OrganizationsStore` finishes loading:
  76. // only fetch in the case where we don't have an orgId
  77. //
  78. // Compare `getOrganizationSlug` because we may have a last used org from server
  79. // if there is no orgId in the URL
  80. const organizationLoadingChanged =
  81. prevProps.organizationsLoading !== props.organizationsLoading &&
  82. props.organizationsLoading === false;
  83. return (
  84. hasOrgIdAndChanged ||
  85. (!hasOrgId && organizationLoadingChanged) ||
  86. (props.location.state === 'refresh' && prevProps.location.state !== 'refresh')
  87. );
  88. }
  89. static getDefaultState(props: Props): State {
  90. const prevProps = {
  91. orgId: props.params.orgId,
  92. organizationsLoading: props.organizationsLoading,
  93. location: props.location,
  94. };
  95. if (OrganizationContextContainer.isOrgStorePopulatedCorrectly(props)) {
  96. // retrieve initial state from store
  97. return {
  98. ...OrganizationStore.get(),
  99. prevProps,
  100. };
  101. }
  102. return {
  103. loading: true,
  104. error: null,
  105. errorType: null,
  106. organization: null,
  107. prevProps,
  108. };
  109. }
  110. static getOrganizationSlug(props: Props) {
  111. return (
  112. props.params.orgId ||
  113. ((props.useLastOrganization &&
  114. (ConfigStore.get('lastOrganization') ||
  115. props.organizations?.[0]?.slug)) as string)
  116. );
  117. }
  118. static isOrgChanging(props: Props): boolean {
  119. const {organization} = OrganizationStore.get();
  120. if (!organization) {
  121. return false;
  122. }
  123. return organization.slug !== OrganizationContextContainer.getOrganizationSlug(props);
  124. }
  125. static isOrgStorePopulatedCorrectly(props: Props) {
  126. const {organization, dirty} = OrganizationStore.get();
  127. return !dirty && organization && !OrganizationContextContainer.isOrgChanging(props);
  128. }
  129. static childContextTypes = {
  130. organization: SentryTypes.Organization,
  131. };
  132. constructor(props: Props) {
  133. super(props);
  134. this.state = OrganizationContextContainer.getDefaultState(props);
  135. }
  136. getChildContext() {
  137. return {
  138. organization: this.state.organization,
  139. };
  140. }
  141. componentDidMount() {
  142. this.fetchData(true);
  143. }
  144. componentDidUpdate(prevProps: Props) {
  145. const remountPrevProps: State['prevProps'] = {
  146. orgId: prevProps.params.orgId,
  147. organizationsLoading: prevProps.organizationsLoading,
  148. location: prevProps.location,
  149. };
  150. if (OrganizationContextContainer.shouldRemount(remountPrevProps, this.props)) {
  151. this.remountComponent();
  152. }
  153. }
  154. componentWillUnmount() {
  155. this.unlisteners.forEach(callIfFunction);
  156. }
  157. unlisteners = [
  158. ProjectActions.createSuccess.listen(() => this.onProjectCreation(), undefined),
  159. OrganizationStore.listen(data => this.loadOrganization(data), undefined),
  160. ];
  161. remountComponent = () => {
  162. this.setState(
  163. OrganizationContextContainer.getDefaultState(this.props),
  164. this.fetchData
  165. );
  166. };
  167. onProjectCreation() {
  168. // If a new project was created, we need to re-fetch the
  169. // org details endpoint, which will propagate re-rendering
  170. // for the entire component tree
  171. fetchOrganizationDetails(
  172. this.props.api,
  173. OrganizationContextContainer.getOrganizationSlug(this.props),
  174. true,
  175. false
  176. );
  177. }
  178. isLoading() {
  179. // In the absence of an organization slug, the loading state should be
  180. // derived from this.props.organizationsLoading from OrganizationsStore
  181. if (!OrganizationContextContainer.getOrganizationSlug(this.props)) {
  182. return this.props.organizationsLoading;
  183. }
  184. return this.state.loading;
  185. }
  186. fetchData(isInitialFetch = false) {
  187. if (!OrganizationContextContainer.getOrganizationSlug(this.props)) {
  188. return;
  189. }
  190. // fetch from the store, then fetch from the API if necessary
  191. if (OrganizationContextContainer.isOrgStorePopulatedCorrectly(this.props)) {
  192. return;
  193. }
  194. metric.mark({name: 'organization-details-fetch-start'});
  195. fetchOrganizationDetails(
  196. this.props.api,
  197. OrganizationContextContainer.getOrganizationSlug(this.props),
  198. !OrganizationContextContainer.isOrgChanging(this.props), // if true, will preserve a lightweight org that was fetched,
  199. isInitialFetch
  200. );
  201. }
  202. loadOrganization(orgData: State) {
  203. const {organization, error} = orgData;
  204. const hooks: React.ReactNode[] = [];
  205. if (organization && !error) {
  206. HookStore.get('organization:header').forEach(cb => {
  207. hooks.push(cb(organization));
  208. });
  209. // Configure scope to have organization tag
  210. Sentry.configureScope(scope => {
  211. // XXX(dcramer): this is duplicated in sdk.py on the backend
  212. scope.setTag('organization', organization.id);
  213. scope.setTag('organization.slug', organization.slug);
  214. scope.setContext('organization', {id: organization.id, slug: organization.slug});
  215. });
  216. } else if (error) {
  217. // If user is superuser, open sudo window
  218. const user = ConfigStore.get('user');
  219. if (!user || !user.isSuperuser || error.status !== 403) {
  220. // This `catch` can swallow up errors in development (and tests)
  221. // So let's log them. This may create some noise, especially the test case where
  222. // we specifically test this branch
  223. console.error(error); // eslint-disable-line no-console
  224. } else {
  225. openSudo({
  226. retryRequest: () => Promise.resolve(this.fetchData()),
  227. isSuperuser: true,
  228. needsReload: true,
  229. });
  230. }
  231. }
  232. this.setState({...orgData, hooks}, () => {
  233. // Take a measurement for when organization details are done loading and the new state is applied
  234. if (organization) {
  235. metric.measure({
  236. name: 'app.component.perf',
  237. start: 'organization-details-fetch-start',
  238. data: {
  239. name: 'org-details',
  240. route: getRouteStringFromRoutes(this.props.routes),
  241. organization_id: parseInt(organization.id, 10),
  242. },
  243. });
  244. }
  245. });
  246. }
  247. getOrganizationDetailsEndpoint() {
  248. return `/organizations/${OrganizationContextContainer.getOrganizationSlug(
  249. this.props
  250. )}/`;
  251. }
  252. getTitle() {
  253. return this.state.organization?.name ?? 'Sentry';
  254. }
  255. renderSidebar(): React.ReactNode {
  256. if (!this.props.includeSidebar) {
  257. return null;
  258. }
  259. const {children: _, ...props} = this.props;
  260. return <Sidebar {...props} organization={this.state.organization as Organization} />;
  261. }
  262. renderError() {
  263. let errorComponent: React.ReactElement;
  264. switch (this.state.errorType) {
  265. case ORGANIZATION_FETCH_ERROR_TYPES.ORG_NO_ACCESS:
  266. // We can still render when an org can't be loaded due to 401. The
  267. // backend will handle redirects when this is a problem.
  268. return this.renderBody();
  269. case ORGANIZATION_FETCH_ERROR_TYPES.ORG_NOT_FOUND:
  270. errorComponent = (
  271. <Alert type="error">
  272. {t('The organization you were looking for was not found.')}
  273. </Alert>
  274. );
  275. break;
  276. default:
  277. errorComponent = <LoadingError onRetry={this.remountComponent} />;
  278. }
  279. return <ErrorWrapper>{errorComponent}</ErrorWrapper>;
  280. }
  281. renderBody() {
  282. return (
  283. <SentryDocumentTitle noSuffix title={this.getTitle()}>
  284. <OrganizationContext.Provider value={this.state.organization}>
  285. <div className="app">
  286. <SentryReplayInit organization={this.state.organization} />
  287. {this.state.hooks}
  288. {this.renderSidebar()}
  289. {this.props.children}
  290. </div>
  291. </OrganizationContext.Provider>
  292. </SentryDocumentTitle>
  293. );
  294. }
  295. render() {
  296. if (this.isLoading()) {
  297. return (
  298. <LoadingTriangle>{t('Loading data for your organization.')}</LoadingTriangle>
  299. );
  300. }
  301. if (this.state.error) {
  302. return (
  303. <Fragment>
  304. {this.renderSidebar()}
  305. {this.renderError()}
  306. </Fragment>
  307. );
  308. }
  309. return this.renderBody();
  310. }
  311. }
  312. export default withApi(
  313. withOrganizations(Sentry.withProfiler(OrganizationContextContainer))
  314. );
  315. export {OrganizationContextContainer as OrganizationLegacyContext, OrganizationContext};
  316. const ErrorWrapper = styled('div')`
  317. padding: ${space(3)};
  318. `;