123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352 |
- import {Component, Fragment} from 'react';
- import {PlainRoute, RouteComponentProps} from 'react-router';
- import styled from '@emotion/styled';
- import * as Sentry from '@sentry/react';
- import {fetchOrganizationDetails} from 'sentry/actionCreators/organization';
- import {openSudo} from 'sentry/actionCreators/sudoModal';
- import {Client} from 'sentry/api';
- import {Alert} from 'sentry/components/alert';
- import HookOrDefault from 'sentry/components/hookOrDefault';
- import LoadingError from 'sentry/components/loadingError';
- import LoadingTriangle from 'sentry/components/loadingTriangle';
- import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
- import Sidebar from 'sentry/components/sidebar';
- import {ORGANIZATION_FETCH_ERROR_TYPES} from 'sentry/constants';
- import {t} from 'sentry/locale';
- import SentryTypes from 'sentry/sentryTypes';
- import ConfigStore from 'sentry/stores/configStore';
- import HookStore from 'sentry/stores/hookStore';
- import OrganizationStore from 'sentry/stores/organizationStore';
- import {space} from 'sentry/styles/space';
- import {Organization} from 'sentry/types';
- import {metric} from 'sentry/utils/analytics';
- import {callIfFunction} from 'sentry/utils/callIfFunction';
- import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
- import RequestError from 'sentry/utils/requestError/requestError';
- import withApi from 'sentry/utils/withApi';
- import withOrganizations from 'sentry/utils/withOrganizations';
- import {OrganizationContext} from './organizationContext';
- type Props = RouteComponentProps<{orgId: string}, {}> & {
- api: Client;
- includeSidebar: boolean;
- organizations: Organization[];
- organizationsLoading: boolean;
- routes: PlainRoute[];
- useLastOrganization: boolean;
- children?: React.ReactNode;
- };
- type State = {
- loading: boolean;
- organization: Organization | null;
- prevProps: {
- location: Props['location'];
- orgId: string;
- organizationsLoading: boolean;
- };
- dirty?: boolean;
- error?: RequestError | null;
- errorType?: string | null;
- hooks?: React.ReactNode[];
- };
- const OrganizationHeader = HookOrDefault({
- hookName: 'component:organization-header',
- });
- function getOrganizationSlug(props: Props) {
- return (
- props.params.orgId ||
- ((props.useLastOrganization &&
- (ConfigStore.get('lastOrganization') || props.organizations?.[0]?.slug)) as string)
- );
- }
- function isOrgChanging(props: Props) {
- const {organization} = OrganizationStore.get();
- if (!organization) {
- return false;
- }
- return organization.slug !== getOrganizationSlug(props);
- }
- function isOrgStorePopulatedCorrectly(props: Props) {
- const {organization, dirty} = OrganizationStore.get();
- return !dirty && organization && !isOrgChanging(props);
- }
- class OrganizationContextContainer extends Component<Props, State> {
- static getDerivedStateFromProps(props: Readonly<Props>, prevState: State): State {
- const {prevProps} = prevState;
- if (OrganizationContextContainer.shouldRemount(prevProps, props)) {
- return OrganizationContextContainer.getDefaultState(props);
- }
- const {organizationsLoading, location, params} = props;
- const {orgId} = params;
- return {
- ...prevState,
- prevProps: {
- orgId,
- organizationsLoading,
- location,
- },
- };
- }
- static shouldRemount(prevProps: State['prevProps'], props: Props): boolean {
- const hasOrgIdAndChanged =
- prevProps.orgId && props.params.orgId && prevProps.orgId !== props.params.orgId;
- const hasOrgId =
- props.params.orgId ||
- (props.useLastOrganization && ConfigStore.get('lastOrganization'));
- // protect against the case where we finish fetching org details
- // and then `OrganizationsStore` finishes loading:
- // only fetch in the case where we don't have an orgId
- //
- // Compare `getOrganizationSlug` because we may have a last used org from server
- // if there is no orgId in the URL
- const organizationLoadingChanged =
- prevProps.organizationsLoading !== props.organizationsLoading &&
- props.organizationsLoading === false;
- return (
- hasOrgIdAndChanged ||
- (!hasOrgId && organizationLoadingChanged) ||
- (props.location.state === 'refresh' && prevProps.location.state !== 'refresh')
- );
- }
- static getDefaultState(props: Props): State {
- const prevProps = {
- orgId: props.params.orgId,
- organizationsLoading: props.organizationsLoading,
- location: props.location,
- };
- if (isOrgStorePopulatedCorrectly(props)) {
- // retrieve initial state from store
- return {
- ...OrganizationStore.get(),
- prevProps,
- };
- }
- return {
- loading: true,
- error: null,
- errorType: null,
- organization: null,
- prevProps,
- };
- }
- static childContextTypes = {
- organization: SentryTypes.Organization,
- };
- constructor(props: Props) {
- super(props);
- this.state = OrganizationContextContainer.getDefaultState(props);
- }
- getChildContext() {
- return {
- organization: this.state.organization,
- };
- }
- componentDidMount() {
- this.fetchData(true);
- }
- componentDidUpdate(prevProps: Props) {
- const remountPrevProps: State['prevProps'] = {
- orgId: prevProps.params.orgId,
- organizationsLoading: prevProps.organizationsLoading,
- location: prevProps.location,
- };
- if (OrganizationContextContainer.shouldRemount(remountPrevProps, this.props)) {
- this.remountComponent();
- }
- }
- componentWillUnmount() {
- this.unlisteners.forEach(callIfFunction);
- }
- unlisteners = [
- OrganizationStore.listen(data => this.loadOrganization(data), undefined),
- ];
- remountComponent = () => {
- this.setState(
- OrganizationContextContainer.getDefaultState(this.props),
- this.fetchData
- );
- };
- isLoading() {
- // In the absence of an organization slug, the loading state should be
- // derived from this.props.organizationsLoading from OrganizationsStore
- if (!getOrganizationSlug(this.props)) {
- return this.props.organizationsLoading;
- }
- return this.state.loading;
- }
- fetchData(isInitialFetch = false) {
- const orgSlug = getOrganizationSlug(this.props);
- if (!orgSlug) {
- return;
- }
- // fetch from the store, then fetch from the API if necessary
- if (isOrgStorePopulatedCorrectly(this.props)) {
- return;
- }
- metric.mark({name: 'organization-details-fetch-start'});
- fetchOrganizationDetails(
- this.props.api,
- orgSlug,
- !isOrgChanging(this.props), // if true, will preserve a lightweight org that was fetched,
- isInitialFetch
- );
- }
- loadOrganization(orgData: State) {
- const {organization, error} = orgData;
- const hooks: React.ReactNode[] = [];
- if (organization && !error) {
- HookStore.get('organization:header').forEach(cb => {
- hooks.push(cb(organization));
- });
- // Configure scope to have organization tag
- Sentry.configureScope(scope => {
- // XXX(dcramer): this is duplicated in sdk.py on the backend
- scope.setTag('organization', organization.id);
- scope.setTag('organization.slug', organization.slug);
- scope.setContext('organization', {id: organization.id, slug: organization.slug});
- });
- } else if (error) {
- // If user is superuser, open sudo window
- const user = ConfigStore.get('user');
- if (!user || !user.isSuperuser || error.status !== 403) {
- // This `catch` can swallow up errors in development (and tests)
- // So let's log them. This may create some noise, especially the test case where
- // we specifically test this branch
- console.error(error); // eslint-disable-line no-console
- } else {
- openSudo({
- retryRequest: () => Promise.resolve(this.fetchData()),
- isSuperuser: true,
- needsReload: true,
- });
- }
- }
- this.setState({...orgData, hooks}, () => {
- // Take a measurement for when organization details are done loading and the new state is applied
- if (organization) {
- metric.measure({
- name: 'app.component.perf',
- start: 'organization-details-fetch-start',
- data: {
- name: 'org-details',
- route: getRouteStringFromRoutes(this.props.routes),
- organization_id: parseInt(organization.id, 10),
- },
- });
- }
- });
- }
- renderSidebar(): React.ReactNode {
- if (!this.props.includeSidebar) {
- return null;
- }
- return <Sidebar organization={this.state.organization ?? undefined} />;
- }
- renderError() {
- let errorComponent: React.ReactElement;
- switch (this.state.errorType) {
- case ORGANIZATION_FETCH_ERROR_TYPES.ORG_NO_ACCESS:
- // We can still render when an org can't be loaded due to 401. The
- // backend will handle redirects when this is a problem.
- return this.renderBody();
- case ORGANIZATION_FETCH_ERROR_TYPES.ORG_NOT_FOUND:
- errorComponent = (
- <Alert type="error" data-test-id="org-loading-error">
- {t('The organization you were looking for was not found.')}
- </Alert>
- );
- break;
- default:
- errorComponent = <LoadingError onRetry={this.remountComponent} />;
- }
- return <ErrorWrapper>{errorComponent}</ErrorWrapper>;
- }
- renderBody() {
- const {organization} = this.state;
- return (
- <SentryDocumentTitle noSuffix title={organization?.name ?? 'Sentry'}>
- <OrganizationContext.Provider value={organization}>
- <div className="app">
- {this.state.hooks}
- {organization && <OrganizationHeader organization={organization} />}
- {this.renderSidebar()}
- {this.props.children}
- </div>
- </OrganizationContext.Provider>
- </SentryDocumentTitle>
- );
- }
- render() {
- if (this.isLoading()) {
- return (
- <LoadingTriangle>{t('Loading data for your organization.')}</LoadingTriangle>
- );
- }
- if (this.state.error) {
- return (
- <Fragment>
- {this.renderSidebar()}
- {this.renderError()}
- </Fragment>
- );
- }
- return this.renderBody();
- }
- }
- export default withApi(
- withOrganizations(Sentry.withProfiler(OrganizationContextContainer))
- );
- const ErrorWrapper = styled('div')`
- padding: ${space(3)};
- `;
|