Browse Source

feat(discover): Add new route for homepage (#39545)

This changes the landing page from re-using the `/results/` route and
adds a new `/homepage/` route which is dedicated to querying the
homepage endpoint and rendering it properly
Nar Saynorath 2 years ago
parent
commit
e451caae02

+ 7 - 1
static/app/routes.tsx

@@ -1149,9 +1149,15 @@ function buildRoutes() {
       component={make(() => import('sentry/views/eventsV2'))}
     >
       <Feature features={['discover-query-builder-as-landing-page']}>
-        <IndexRedirect to="results/" />
+        <IndexRedirect to="homepage/" />
       </Feature>
       <IndexRedirect to="queries/" />
+      <Feature features={['discover-query-builder-as-landing-page']}>
+        <Route
+          path="homepage/"
+          component={make(() => import('sentry/views/eventsV2/homepage'))}
+        />
+      </Feature>
       <Route
         path="queries/"
         component={make(() => import('sentry/views/eventsV2/landing'))}

+ 6 - 2
static/app/utils/discover/eventView.tsx

@@ -1172,9 +1172,13 @@ class EventView {
     return eventQuery;
   }
 
-  getResultsViewUrlTarget(slug: string): {pathname: string; query: Query} {
+  getResultsViewUrlTarget(
+    slug: string,
+    isHomepage: boolean = false
+  ): {pathname: string; query: Query} {
+    const target = isHomepage ? 'homepage' : 'results';
     return {
-      pathname: `/organizations/${slug}/discover/results/`,
+      pathname: `/organizations/${slug}/discover/${target}/`,
       query: this.generateQueryStringObject(),
     };
   }

+ 1 - 1
static/app/utils/discover/urls.spec.jsx

@@ -15,6 +15,6 @@ describe('getDiscoverLandingUrl', function () {
     const org = TestStubs.Organization({
       features: ['discover-query', 'discover-query-builder-as-landing-page'],
     });
-    expect(getDiscoverLandingUrl(org)).toBe('/organizations/org-slug/discover/results/');
+    expect(getDiscoverLandingUrl(org)).toBe('/organizations/org-slug/discover/homepage/');
   });
 });

+ 7 - 5
static/app/utils/discover/urls.tsx

@@ -33,10 +33,12 @@ export function eventDetailsRouteWithEventView({
   orgSlug,
   eventSlug,
   eventView,
+  isHomepage,
 }: {
   eventSlug: string;
   eventView: EventView;
   orgSlug: string;
+  isHomepage?: boolean;
 }) {
   const pathname = eventDetailsRoute({
     orgSlug,
@@ -45,7 +47,7 @@ export function eventDetailsRouteWithEventView({
 
   return {
     pathname,
-    query: eventView.generateQueryStringObject(),
+    query: {...eventView.generateQueryStringObject(), homepage: isHomepage},
   };
 }
 
@@ -54,10 +56,10 @@ export function eventDetailsRouteWithEventView({
  * feature flags.
  */
 export function getDiscoverLandingUrl(organization: OrganizationSummary): string {
-  if (
-    organization.features.includes('discover-query') &&
-    !organization.features.includes('discover-query-builder-as-landing-page')
-  ) {
+  if (organization.features.includes('discover-query')) {
+    if (organization.features.includes('discover-query-builder-as-landing-page')) {
+      return `/organizations/${organization.slug}/discover/homepage/`;
+    }
     return getDiscoverQueriesUrl(organization);
   }
   return `/organizations/${organization.slug}/discover/results/`;

+ 11 - 3
static/app/views/eventsV2/breadcrumb.tsx

@@ -1,4 +1,5 @@
 import {Location} from 'history';
+import omit from 'lodash/omit';
 
 import Breadcrumbs, {Crumb} from 'sentry/components/breadcrumbs';
 import {t} from 'sentry/locale';
@@ -12,9 +13,16 @@ type Props = {
   location: Location;
   organization: Organization;
   event?: Event;
+  isHomepage?: boolean;
 };
 
-function DiscoverBreadcrumb({eventView, event, organization, location}: Props) {
+function DiscoverBreadcrumb({
+  eventView,
+  event,
+  organization,
+  location,
+  isHomepage,
+}: Props) {
   const crumbs: Crumb[] = [];
   const discoverTarget = organization.features.includes('discover-query')
     ? {
@@ -22,7 +30,7 @@ function DiscoverBreadcrumb({eventView, event, organization, location}: Props) {
           ? getDiscoverQueriesUrl(organization)
           : getDiscoverLandingUrl(organization),
         query: {
-          ...location.query,
+          ...omit(location.query, 'homepage'),
           ...eventView.generateBlankQueryStringObject(),
           ...eventView.getPageFiltersQuery(),
         },
@@ -36,7 +44,7 @@ function DiscoverBreadcrumb({eventView, event, organization, location}: Props) {
 
   if (eventView && eventView.isValid()) {
     crumbs.push({
-      to: eventView.getResultsViewUrlTarget(organization.slug),
+      to: eventView.getResultsViewUrlTarget(organization.slug, isHomepage),
       label: eventView.name || '',
     });
   }

+ 28 - 1
static/app/views/eventsV2/eventDetails.spec.jsx

@@ -1,6 +1,6 @@
 import {mountWithTheme} from 'sentry-test/enzyme';
 import {initializeOrg} from 'sentry-test/initializeOrg';
-import {act} from 'sentry-test/reactTestingLibrary';
+import {act, render, screen} from 'sentry-test/reactTestingLibrary';
 
 import ProjectsStore from 'sentry/stores/projectsStore';
 import EventView from 'sentry/utils/discover/eventView';
@@ -300,4 +300,31 @@ describe('EventsV2 > EventDetails', function () {
       'Dumpster release:82ebf297206a title:"Oh no something bad"'
     );
   });
+
+  it('links back to the homepage if the query param contains homepage flag', async () => {
+    const {organization, router, routerContext} = initializeOrg({
+      organization: TestStubs.Organization({
+        features: ['discover-query-builder-as-landing-page'],
+      }),
+      router: {
+        location: {
+          pathname: '/organizations/org-slug/discover/project-slug:deadbeef',
+          query: {...allEventsView.generateQueryStringObject(), homepage: true},
+        },
+      },
+    });
+
+    render(
+      <EventDetails
+        organization={organization}
+        params={{eventSlug: 'project-slug:deadbeef'}}
+        location={router.location}
+      />,
+      {context: routerContext, organization}
+    );
+
+    expect((await screen.findByText('All Events')).pathname).toEqual(
+      '/organizations/org-slug/discover/homepage/'
+    );
+  });
 });

+ 3 - 1
static/app/views/eventsV2/eventDetails/content.tsx

@@ -54,6 +54,7 @@ type Props = Pick<
   eventSlug: string;
   eventView: EventView;
   organization: Organization;
+  isHomepage?: boolean;
 };
 
 type State = {
@@ -124,7 +125,7 @@ class EventDetailsContent extends AsyncComponent<Props, State> {
   }
 
   renderContent(event: Event) {
-    const {organization, location, eventView, route, router} = this.props;
+    const {organization, location, eventView, route, router, isHomepage} = this.props;
     const {isSidebarVisible} = this.state;
 
     // metrics
@@ -162,6 +163,7 @@ class EventDetailsContent extends AsyncComponent<Props, State> {
               event={event}
               organization={organization}
               location={location}
+              isHomepage={isHomepage}
             />
             <EventHeader event={event} />
           </Layout.HeaderContent>

+ 6 - 1
static/app/views/eventsV2/eventDetails/index.tsx

@@ -1,5 +1,6 @@
 import {RouteComponentProps} from 'react-router';
 import styled from '@emotion/styled';
+import omit from 'lodash/omit';
 
 import NoProjectMessage from 'sentry/components/noProjectMessage';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
@@ -18,7 +19,10 @@ type Props = RouteComponentProps<{eventSlug: string}, {}> & {
 function EventDetails({organization, location, params, router, route}: Props) {
   const eventSlug = typeof params.eventSlug === 'string' ? params.eventSlug.trim() : '';
 
-  const eventView = EventView.fromLocation(location);
+  const isHomepage = location.query.homepage;
+  const eventView = EventView.fromLocation(
+    isHomepage ? {...location, query: omit(location.query, 'id')} : location
+  );
   const eventName = eventView.name;
 
   const documentTitle =
@@ -44,6 +48,7 @@ function EventDetails({organization, location, params, router, route}: Props) {
             eventSlug={eventSlug}
             router={router}
             route={route}
+            isHomepage={isHomepage}
           />
         </NoProjectMessage>
       </StyledPageContent>

+ 3 - 2
static/app/views/eventsV2/eventInputName.tsx

@@ -13,6 +13,7 @@ import {handleUpdateQueryName} from './savedQuery/utils';
 type Props = {
   eventView: EventView;
   organization: Organization;
+  isHomepage?: boolean;
   savedQuery?: SavedQuery;
 };
 
@@ -22,7 +23,7 @@ const NAME_DEFAULT = t('Untitled query');
  * Allows user to edit the name of the query.
  * By pressing Enter or clicking outside the component, the changes will be saved, if valid.
  */
-function EventInputName({organization, eventView, savedQuery}: Props) {
+function EventInputName({organization, eventView, savedQuery, isHomepage}: Props) {
   const api = useApi();
 
   function handleChange(nextQueryName: string) {
@@ -59,7 +60,7 @@ function EventInputName({organization, eventView, savedQuery}: Props) {
       <EditableText
         value={value}
         onChange={handleChange}
-        isDisabled={!eventView.id}
+        isDisabled={!eventView.id || isHomepage}
         errorMessage={t('Please set a name for this query')}
       />
     </StyledTitle>

+ 149 - 0
static/app/views/eventsV2/homepage.spec.tsx

@@ -0,0 +1,149 @@
+import {browserHistory} from 'react-router';
+
+import {initializeOrg} from 'sentry-test/initializeOrg';
+import {mountGlobalModal} from 'sentry-test/modal';
+import {act, render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
+
+import Homepage from './homepage';
+
+describe('Discover > Homepage', () => {
+  const features = [
+    'global-views',
+    'discover-query',
+    'discover-query-builder-as-landing-page',
+  ];
+  let initialData, organization, mockHomepage;
+
+  beforeEach(() => {
+    organization = TestStubs.Organization({
+      features,
+    });
+    initialData = initializeOrg({
+      ...initializeOrg(),
+      organization,
+      router: {
+        location: TestStubs.location(),
+      },
+    });
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/eventsv2/',
+      body: [],
+    });
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/events-meta/',
+      body: {
+        count: 2,
+      },
+    });
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/events-stats/',
+      body: {data: [[123, []]]},
+    });
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/tags/',
+      body: [],
+    });
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/releases/stats/',
+      body: [],
+    });
+    mockHomepage = MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/discover/homepage/',
+      method: 'GET',
+      statusCode: 200,
+      body: {
+        id: '2',
+        name: 'homepage query',
+        projects: [],
+        version: 2,
+        expired: false,
+        dateCreated: '2021-04-08T17:53:25.195782Z',
+        dateUpdated: '2021-04-09T12:13:18.567264Z',
+        createdBy: {
+          id: '2',
+        },
+        environment: ['alpha'],
+        fields: ['environment'],
+        widths: ['-1'],
+        range: '24h',
+        orderby: '-environment',
+        display: 'previous',
+        query: 'event.type:error',
+      },
+    });
+  });
+
+  it('fetches from the homepage URL and renders fields, page filters, and chart information', async () => {
+    render(
+      <Homepage
+        organization={organization}
+        location={initialData.router.location}
+        router={initialData.router}
+        setSavedQuery={jest.fn()}
+        loading={false}
+      />,
+      {context: initialData.routerContext, organization: initialData.organization}
+    );
+
+    expect(mockHomepage).toHaveBeenCalled();
+    await screen.findByText('environment');
+
+    // Only the environment field
+    expect(screen.getAllByTestId('grid-head-cell').length).toEqual(1);
+    screen.getByText('Previous Period');
+    screen.getByText('alpha');
+    screen.getByText('event.type:error');
+  });
+
+  it('applies URL changes with the homepage pathname', async () => {
+    render(
+      <Homepage
+        organization={organization}
+        location={initialData.router.location}
+        router={initialData.router}
+        setSavedQuery={jest.fn()}
+        loading={false}
+      />,
+      {context: initialData.routerContext, organization: initialData.organization}
+    );
+    userEvent.click(screen.getByText('Columns'));
+    await act(async () => {
+      await mountGlobalModal();
+    });
+
+    userEvent.click(screen.getByTestId('label'));
+    userEvent.click(screen.getByText('event.type'));
+    userEvent.click(screen.getByText('Apply'));
+
+    expect(browserHistory.push).toHaveBeenCalledWith(
+      expect.objectContaining({
+        pathname: '/organizations/org-slug/discover/homepage/',
+        query: expect.objectContaining({
+          field: ['event.type'],
+        }),
+      })
+    );
+  });
+
+  it('does not show an editable header or author information', () => {
+    render(
+      <Homepage
+        organization={organization}
+        location={initialData.router.location}
+        router={initialData.router}
+        setSavedQuery={jest.fn()}
+        loading={false}
+      />,
+      {context: initialData.routerContext, organization: initialData.organization}
+    );
+
+    userEvent.click(screen.getByTestId('editable-text-label'));
+
+    // Check that clicking the label didn't render a textbox for editing
+    expect(
+      within(screen.getByTestId('editable-text-label')).queryByRole('textbox')
+    ).not.toBeInTheDocument();
+    expect(screen.queryByText(/Created by:/)).not.toBeInTheDocument();
+    expect(screen.queryByText(/Last edited:/)).not.toBeInTheDocument();
+  });
+});

Some files were not shown because too many files changed in this diff