Browse Source

feat(ui): Move group details page to lightweight org tree (#15657)

Previously large enterprise customers had to wait for organization details to resolve before seeing an issue/group details page that may have been linked to them. This is a little unfortunate as the organization projects and teams from the heavyweight organization details are not even used directly in this view, but are responsible for upwards of 10 second cold start load times. Switching this page to the lightweight org tree should help a lot in cold start load times.
David Wang 5 years ago
parent
commit
76e95576cb

+ 9 - 9
src/sentry/static/sentry/app/components/organizations/multipleProjectSelector.jsx

@@ -157,15 +157,7 @@ export default class MultipleProjectSelector extends React.PureComponent {
     // `forceProject` can be undefined if it is loading the project
     // We are intentionally using an empty string as its "loading" state
 
-    return loadingProjects ? (
-      <StyledHeaderItem
-        data-test-id="global-header-project-selector"
-        icon={<StyledInlineSvg src="icon-project" />}
-        loading={loadingProjects}
-      >
-        {t('Loading\u2026')}
-      </StyledHeaderItem>
-    ) : shouldForceProject ? (
+    return shouldForceProject ? (
       <StyledHeaderItem
         data-test-id="global-header-project-selector"
         icon={<StyledInlineSvg src="icon-project" />}
@@ -181,6 +173,14 @@ export default class MultipleProjectSelector extends React.PureComponent {
       >
         {forceProject ? forceProject.slug : ''}
       </StyledHeaderItem>
+    ) : loadingProjects ? (
+      <StyledHeaderItem
+        data-test-id="global-header-project-selector"
+        icon={<StyledInlineSvg src="icon-project" />}
+        loading={loadingProjects}
+      >
+        {t('Loading\u2026')}
+      </StyledHeaderItem>
     ) : (
       <StyledProjectSelector
         {...this.props}

+ 74 - 74
src/sentry/static/sentry/app/routes.jsx

@@ -1027,6 +1027,80 @@ function routes() {
           }
           component={errorHandler(LazyLoad)}
         />
+        {/* Once org issues is complete, these routes can be nested under
+          /organizations/:orgId/issues */}
+        <Route
+          path="/organizations/:orgId/issues/:groupId/"
+          componentPromise={() =>
+            import(/* webpackChunkName: "OrganizationGroupDetails" */ 'app/views/organizationGroupDetails')
+          }
+          component={errorHandler(LazyLoad)}
+        >
+          {/* XXX: if we change the path for group details, we *must* update `OrganizationContext`.
+            There is behavior that depends on this path and unfortunately no great way to test for this contract */}
+          <IndexRoute
+            componentPromise={() =>
+              import(/* webpackChunkName: "OrganizationGroupEventDetails" */ 'app/views/organizationGroupDetails/groupEventDetails')
+            }
+            component={errorHandler(LazyLoad)}
+          />
+          <Route
+            path="/organizations/:orgId/issues/:groupId/activity/"
+            componentPromise={() =>
+              import(/* webpackChunkName: "GroupActivity" */ 'app/views/organizationGroupDetails/groupActivity')
+            }
+            component={errorHandler(LazyLoad)}
+          />
+          <Route
+            path="/organizations/:orgId/issues/:groupId/events/:eventId/"
+            componentPromise={() =>
+              import(/* webpackChunkName: "OrganizationGroupEventDetails" */ 'app/views/organizationGroupDetails/groupEventDetails')
+            }
+            component={errorHandler(LazyLoad)}
+          />
+          <Route
+            path="/organizations/:orgId/issues/:groupId/events/"
+            componentPromise={() =>
+              import(/* webpackChunkName: "OrganizationGroupEvents" */ 'app/views/organizationGroupDetails/groupEvents')
+            }
+            component={errorHandler(LazyLoad)}
+          />
+          <Route
+            path="/organizations/:orgId/issues/:groupId/tags/"
+            componentPromise={() =>
+              import(/* webpackChunkName: "OrganizationGroupTags" */ 'app/views/organizationGroupDetails/groupTags')
+            }
+            component={errorHandler(LazyLoad)}
+          />
+          <Route
+            path="/organizations/:orgId/issues/:groupId/tags/:tagKey/"
+            componentPromise={() =>
+              import(/* webpackChunkName: "OrganizationGroupTagsValues" */ 'app/views/organizationGroupDetails/groupTagValues')
+            }
+            component={errorHandler(LazyLoad)}
+          />
+          <Route
+            path="/organizations/:orgId/issues/:groupId/feedback/"
+            componentPromise={() =>
+              import(/* webpackChunkName: "OrganizationGroupUserFeedback" */ 'app/views/organizationGroupDetails/groupUserFeedback')
+            }
+            component={errorHandler(LazyLoad)}
+          />
+          <Route
+            path="/organizations/:orgId/issues/:groupId/similar/"
+            componentPromise={() =>
+              import(/* webpackChunkName: "GroupSimilarView" */ 'app/views/organizationGroupDetails/groupSimilar')
+            }
+            component={errorHandler(LazyLoad)}
+          />
+          <Route
+            path="/organizations/:orgId/issues/:groupId/merged/"
+            componentPromise={() =>
+              import(/* webpackChunkName: "GroupSimilarView" */ 'app/views/organizationGroupDetails/groupMerged')
+            }
+            component={errorHandler(LazyLoad)}
+          />
+        </Route>
       </Route>
       {/* The heavyweight organization detail views */}
       <Route path="/:orgId/" component={errorHandler(OrganizationDetails)}>
@@ -1198,80 +1272,6 @@ function routes() {
               component={errorHandler(IssueListOverview)}
             />
           </Route>
-          {/* Once org issues is complete, these routes can be nested under
-          /organizations/:orgId/issues */}
-          <Route
-            path="/organizations/:orgId/issues/:groupId/"
-            componentPromise={() =>
-              import(/* webpackChunkName: "OrganizationGroupDetails" */ 'app/views/organizationGroupDetails')
-            }
-            component={errorHandler(LazyLoad)}
-          >
-            {/* XXX: if we change the path for group details, we *must* update `OrganizationContext`.
-            There is behavior that depends on this path and unfortunately no great way to test for this contract */}
-            <IndexRoute
-              componentPromise={() =>
-                import(/* webpackChunkName: "OrganizationGroupEventDetails" */ 'app/views/organizationGroupDetails/groupEventDetails')
-              }
-              component={errorHandler(LazyLoad)}
-            />
-            <Route
-              path="/organizations/:orgId/issues/:groupId/activity/"
-              componentPromise={() =>
-                import(/* webpackChunkName: "GroupActivity" */ 'app/views/organizationGroupDetails/groupActivity')
-              }
-              component={errorHandler(LazyLoad)}
-            />
-            <Route
-              path="/organizations/:orgId/issues/:groupId/events/:eventId/"
-              componentPromise={() =>
-                import(/* webpackChunkName: "OrganizationGroupEventDetails" */ 'app/views/organizationGroupDetails/groupEventDetails')
-              }
-              component={errorHandler(LazyLoad)}
-            />
-            <Route
-              path="/organizations/:orgId/issues/:groupId/events/"
-              componentPromise={() =>
-                import(/* webpackChunkName: "OrganizationGroupEvents" */ 'app/views/organizationGroupDetails/groupEvents')
-              }
-              component={errorHandler(LazyLoad)}
-            />
-            <Route
-              path="/organizations/:orgId/issues/:groupId/tags/"
-              componentPromise={() =>
-                import(/* webpackChunkName: "OrganizationGroupTags" */ 'app/views/organizationGroupDetails/groupTags')
-              }
-              component={errorHandler(LazyLoad)}
-            />
-            <Route
-              path="/organizations/:orgId/issues/:groupId/tags/:tagKey/"
-              componentPromise={() =>
-                import(/* webpackChunkName: "OrganizationGroupTagsValues" */ 'app/views/organizationGroupDetails/groupTagValues')
-              }
-              component={errorHandler(LazyLoad)}
-            />
-            <Route
-              path="/organizations/:orgId/issues/:groupId/feedback/"
-              componentPromise={() =>
-                import(/* webpackChunkName: "OrganizationGroupUserFeedback" */ 'app/views/organizationGroupDetails/groupUserFeedback')
-              }
-              component={errorHandler(LazyLoad)}
-            />
-            <Route
-              path="/organizations/:orgId/issues/:groupId/similar/"
-              componentPromise={() =>
-                import(/* webpackChunkName: "GroupSimilarView" */ 'app/views/organizationGroupDetails/groupSimilar')
-              }
-              component={errorHandler(LazyLoad)}
-            />
-            <Route
-              path="/organizations/:orgId/issues/:groupId/merged/"
-              componentPromise={() =>
-                import(/* webpackChunkName: "GroupSimilarView" */ 'app/views/organizationGroupDetails/groupMerged')
-              }
-              component={errorHandler(LazyLoad)}
-            />
-          </Route>
           <Route
             path="/organizations/:orgId/releases/"
             componentPromise={() =>

+ 9 - 1
src/sentry/static/sentry/app/utils/projects.jsx

@@ -92,7 +92,8 @@ class Projects extends React.Component {
     this.setState({
       // placeholders for projects we need to fetch
       fetchedProjects: notInStore.map(slug => ({slug})),
-      initiallyLoaded: true,
+      // set initallyLoaded if any projects were fetched from store
+      initiallyLoaded: !!inStore.length,
       projectsFromStore,
     });
 
@@ -254,6 +255,13 @@ class Projects extends React.Component {
       //
       // fn(searchTerm, {append: bool})
       onSearch: this.handleSearch,
+
+      // Reflects whether or not the initial fetch for the requested projects
+      // was fulfilled
+      initiallyLoaded: this.state.initiallyLoaded,
+
+      // The error that occurred if fetching failed
+      fetchError: this.state.fetchError,
     });
   }
 }

+ 17 - 9
src/sentry/static/sentry/app/views/organizationGroupDetails/groupDetails.jsx

@@ -13,7 +13,7 @@ import GlobalSelectionHeader from 'app/components/organizations/globalSelectionH
 import GroupStore from 'app/stores/groupStore';
 import LoadingError from 'app/components/loadingError';
 import LoadingIndicator from 'app/components/loadingIndicator';
-import ProjectsStore from 'app/stores/projectsStore';
+import Projects from 'app/utils/projects';
 import SentryTypes from 'app/sentryTypes';
 import profiler from 'app/utils/profiler';
 import withApi from 'app/utils/withApi';
@@ -27,9 +27,6 @@ const GroupDetails = createReactClass({
   propTypes: {
     api: PropTypes.object,
 
-    // Provided in the project version of group details
-    project: SentryTypes.Project,
-
     organization: SentryTypes.Organization,
     environments: PropTypes.arrayOf(PropTypes.string),
     enableSnuba: PropTypes.bool,
@@ -130,8 +127,7 @@ const GroupDetails = createReactClass({
           );
           return;
         }
-
-        const project = this.props.project || ProjectsStore.getById(data.project.id);
+        const project = data.project;
 
         if (!project) {
           Sentry.withScope(() => {
@@ -226,9 +222,9 @@ const GroupDetails = createReactClass({
     }
   },
 
-  renderContent(shouldShowGlobalHeader) {
+  renderContent(shouldShowGlobalHeader, project) {
     const {environments} = this.props;
-    const {group, project} = this.state;
+    const {group} = this.state;
 
     const Content = (
       <DocumentTitle title={this.getTitle()}>
@@ -286,7 +282,19 @@ const GroupDetails = createReactClass({
             <LoadingIndicator />
           </PageContent>
         ) : (
-          this.renderContent(showGlobalHeader)
+          <Projects orgId={organization.slug} slugs={[project.slug]}>
+            {({projects, initiallyLoaded, fetchError}) =>
+              initiallyLoaded ? (
+                fetchError ? (
+                  <LoadingError message={t('Error loading the specified project')} />
+                ) : (
+                  this.renderContent(showGlobalHeader, projects[0])
+                )
+              ) : (
+                <LoadingIndicator />
+              )
+            }
+          </Projects>
         )}
       </React.Fragment>
     );

+ 6 - 4
src/sentry/static/sentry/app/views/organizationGroupDetails/groupEventDetails/groupEventDetails.tsx

@@ -105,10 +105,12 @@ class GroupEventDetails extends React.Component<Props, State> {
     // the timing for that is not guaranteed.
     //
     // TBD: if this behavior is actually desired
-    GlobalSelectionStore.loadInitialData(organization, this.props.location.query, {
-      onlyIfNeverLoaded: true,
-      forceUrlSync: true,
-    });
+    if (organization.projects) {
+      GlobalSelectionStore.loadInitialData(organization, this.props.location.query, {
+        onlyIfNeverLoaded: true,
+        forceUrlSync: true,
+      });
+    }
 
     api.clear();
   }