organizationContextContainer.tsx 10 KB

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